makepad/libs/flowgraph/tests/wire_route.rs
Admin f03c22e47d libs: the AI stack, flowgraph, git, audio_route, win_resource and the other libraries; Flow, the asset library and VJ move to Stage
Library changes outside the platform's own dependencies (those came with the platform commits above). libs/asset and libs/flow move to the Stage repository with the apps that use them.

Covers work ebf442a94..2b1a41df4 for libs:

- wm: the Android super-app compiles tiles on the phone against the host engine
- cargo-makepad: the super-app APK is packed by `android dyn-pack`, in Rust
- cef: a page's audio can be captured instead of played
- flow: the flow-ui app and the flowgraph canvas return to the workspace, with staged progress in libs/flow
- svg: a stroke that bends tighter than its half width collapses its inner edge to the corner
- the accent a name is written with stops deciding whether search finds it
- ai stems: LaneDemixer, VocalsModel and the platform Stage origin/main needs
- The regex engine accepts \b and \B, reports the earliest match so a scan can resume after it, and can decide word boundaries on ASCII so a code sear
- workspace: Scope's settings carry the code map's tab width and the directories each prepared project hides from its map
- cef, video, platform: what Stage's browser needs from the shared layers
- platform, wm: work builds again on iOS, tvOS and wasm, and the window manager is warning-free on Windows
- ci: tools/ci, a Splash runtime that watches a branch and runs every ci.splash it finds
- stitch: the crate says what the rest of the repository says about its license
- ci: a run is one target dir and one cargo batch per target, and the wall draws
- apps: what the CI box found
- Remove Flow, asset and VJ applications moved into Stage
- tests: the workspace suite, run as a whole for the first time, passes outside the example UI tests
- libs: no warnings in the workspace check on any row
- tests: the last three failures of the CI box's night
- gif: the crate's doc examples compile
- cef: a hosted browser survives its host's exit
- tests: the three binaries over ten seconds in release come under it
- git: the memory ledger can take what it is asked for
- script: re-entrant dispatch, thread index validation, Any-based handle downcasts, UTF-8 previews; regex: never_loop fix
- fix(text): enable ttf-parser gvar-alloc for many-tuple variable fonts
- platform, widgets, wm: a touch that is taken away is cancelled, never released
- platform, wm: hosted apps ask the WM for what they cannot do themselves
- wm on Android: apps build on the phone
- task, cargo-makepad, widgets, audio: the task manager chooses its columns, graphs every process' network and disk traffic and installs itself as a D
- flowgraph: ordered ports take many wires, sockets can override their icon, and the canvas embeds as a viewport
- cef: captured frames and audio carry callback clocks and a navigation epoch
- git: timings read a portable clock
- ai: the hub's rig-fixture tests find the asset library without the asset client
- ai llm: a Metal main buffer is filled in 32 MB chunks
- diskmap: a scan lists each macOS directory in one getattrlistbulk call and classifies files without allocating or locking
- platform, widgets, audio_route, ai: window crossfades and whole-frame presents, caption controls that click, themed menus, a stereo-pair audio tap,
- builder: the Builder TUI matches the new terminal design, Windows executables carry app icons, and every app has one
- sheets, score, git, home: saves cannot cut a file short or overwrite another one, and checkouts cannot write outside the repository
- git: worktree status matches git on real repositories and can be cancelled
- cuda: builds link only the CUDA toolkit they are given, never a system install
- audio_route: one API on macOS, Windows and Linux, monitoring by default and processing only when asked, plus shaders that discard a value compile to
- platform, widgets, ai: a restyle recompiles nothing it already has, shows only complete frames, and a window can own its caption
- audio_route example: print the samples that reached the processor each second
- widgets, builder: the big widget families are features, all on by default, and the Builder takes none of them
- ai hub: a local chat with no model says so once, plainly
- ai: Claude Desktop drives any app with the F10 panel
- wm: children open no console windows on Windows
- ai: Connect hands the .mcpb to Claude Desktop itself
- civil-time: one local wall clock for every app, and it knows the zone on Windows
- ai-speech: makepad-ai-sfx (and with it ai-h3) is built only for IndexTTS
- script + wasm_bridge: no build script that reruns on every file
- build: the AI stack, csg, Scope and Source Library join the one workspace
- ai chat: the chat's backends are the CLI, MCP and cloud providers; the local model is the `localai` feature
- widgets: every widget family is a crate of its own under widgets/families, and makepad-widgets links, re-exports and registers the ones its features
- apps, examples, libs: each crate builds only the widget families it uses, and the design overlay is each app's own default feature
- livepipe: takes makepad-widgets without its default families, now that the AI stack is in the one workspace
- ai chat: a build without local AI starts on the first provider that answers here, not on "No model"
- thiserror: RUSTC_BOOTSTRAP no longer turns on the unstable generic member access API
- builder: makepad-builder.exe declares itself like a well-formed Windows program and stops a stalled build through a job object instead of taskkill
- builder + ai-cuda: the CUDA kernel progress shows in the Builder's Compile row, not scribbled over its screen
- repo: no third-party comic archive, generated models or sample asset in the tree
- builder: the Windows Builder ships as source, runs in its own console, and apps compile with Rust's GNU toolchain or Microsoft's
- win_resource: PNG decode and encode on makepad-fast-inflate instead of zune-png
- builder: one folder layout everywhere, makepad-builder.zip with only the files a Windows build reads, a Rust download over parallel connections, and

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-26 20:10:57 +02:00

226 lines
7.9 KiB
Rust

use makepad_flowgraph::wire_route::{
obstacles_in_corridor, route_wire, route_wire_sticky, Obstacle, Point, PortSide, RouteKind,
RouteStyle, WireRoute,
};
const CLEARANCE: f64 = 12.0;
fn screenshot_cards(prompt_y: f64) -> [Obstacle; 5] {
[
Obstacle::from_xywh(-1359.0, prompt_y, 290.0, 150.0),
Obstacle::from_xywh(-955.0, -422.0, 290.0, 200.0),
Obstacle::from_xywh(-552.0, -554.0, 290.0, 200.0),
Obstacle::from_xywh(-567.0, -885.0, 290.0, 200.0),
Obstacle::from_xywh(-1210.0, -966.0, 290.0, 150.0),
]
}
fn assert_port_directions(route: &WireRoute, source: PortSide, target: PortSide) {
let RouteKind::Orthogonal { points, .. } = &route.kind else {
panic!("expected orthogonal geometry");
};
let sign = |side| if side == PortSide::Right { 1.0 } else { -1.0 };
assert_eq!(points[1].y, route.from.y, "{:?}", route.kind);
assert_eq!((points[1].x - route.from.x).signum(), sign(source));
let last = points[points.len() - 2];
assert_eq!(last.y, route.to.y, "{:?}", route.kind);
assert_eq!((last.x - route.to.x).signum(), sign(target));
}
fn assert_short(route: &WireRoute) {
let manhattan = (route.to.x - route.from.x).abs() + (route.to.y - route.from.y).abs();
assert!(
route.length() <= manhattan + 2.0 * CLEARANCE,
"length={} manhattan={manhattan} {:?}",
route.length(),
route.kind
);
}
fn assert_outside_cards(route: &WireRoute, cards: &[Obstacle]) {
for step in 0..=(route.length().ceil() as usize * 4) {
let point = route.point_at_distance(step as f64 * 0.25);
for card in cards {
assert!(
point.x <= card.min.x
|| point.x >= card.max.x
|| point.y <= card.min.y
|| point.y >= card.max.y,
"point={point:?} card={card:?} {:?}",
route.kind
);
}
}
}
fn screenshot_fixture(mirror_y: bool, target_side: PortSide) {
// Test both card-edge ports and the actual canvas anchor offsets:
// output tip = 13 + 4.5; input notch = 13 - 3; first port row = 26.
for (source_offset, target_offset) in [(0.0, 0.0), (17.5, 10.0)] {
let mut cards = screenshot_cards(30.0);
let from = Point::new(cards[0].max.x + source_offset, cards[0].min.y + 26.0);
let target_x = if target_side == PortSide::Left {
cards[1].min.x - target_offset
} else {
cards[1].max.x + target_offset
};
let to = Point::new(target_x, cards[1].min.y + 26.0);
let reflect = |p: Point| Point::new(p.x, if mirror_y { -p.y } else { p.y });
let from = reflect(from);
let to = reflect(to);
if mirror_y {
for card in &mut cards {
let (min, max) = (reflect(card.max), reflect(card.min));
card.min.y = min.y;
card.max.y = max.y;
}
}
let obstacles: Vec<_> = cards.iter().map(|card| card.inflate(CLEARANCE)).collect();
// Auto-flip/preview use every card; per-frame drawing uses the corridor.
let style = RouteStyle::default();
let local = obstacles_in_corridor(
from,
to,
&obstacles,
style.port_stub + style.corner_radius * 2.0,
);
for obstacles in [&obstacles, &local] {
let route = route_wire(
from,
PortSide::Right,
to,
target_side,
obstacles,
style,
0.0,
);
assert_short(&route);
assert_port_directions(&route, PortSide::Right, target_side);
assert_outside_cards(&route, &cards);
for point in route.slice(0.0, route.length()) {
assert!(
if mirror_y {
point.y <= cards[1].max.y
} else {
point.y >= cards[1].min.y
},
"{:?}",
route.kind
);
let max_x = if target_side == PortSide::Left {
cards[1].max.x
} else {
to.x + CLEARANCE
};
assert!(point.x <= max_x, "{:?}", route.kind);
}
}
}
}
#[test]
fn screenshot_prompt_below_left_of_expand_does_not_wrap() {
screenshot_fixture(false, PortSide::Left);
}
#[test]
fn screenshot_mirrored_prompt_above_left_of_expand_does_not_wrap() {
screenshot_fixture(true, PortSide::Left);
}
#[test]
fn screenshot_flipped_expand_uses_only_its_near_corner() {
screenshot_fixture(false, PortSide::Right);
}
#[test]
fn screenshot_expand_to_add_style_stays_between_its_ports() {
let cards = screenshot_cards(30.0);
let obstacles: Vec<_> = cards.iter().map(|card| card.inflate(CLEARANCE)).collect();
let from = Point::new(cards[1].max.x + 17.5, cards[1].min.y + 26.0);
let to = Point::new(cards[2].min.x - 10.0, cards[2].min.y + 26.0);
let route = route_wire(
from,
PortSide::Right,
to,
PortSide::Left,
&obstacles,
RouteStyle::default(),
0.0,
);
assert_short(&route);
assert_port_directions(&route, PortSide::Right, PortSide::Left);
assert_outside_cards(&route, &cards);
assert!(
route.slice(0.0, route.length()).iter().all(|point| {
point.x >= from.x && point.x <= to.x && point.y >= to.y && point.y <= from.y
}),
"{:?}",
route.kind
);
}
#[test]
fn nearly_level_ports_keep_short_fillets_and_card_clearance() {
for dy in [-31.0, -16.0, -4.0, -0.5, 0.5, 4.0, 16.0, 31.0] {
for offset in [-4.0, 0.0, 4.0] {
let cards = [
Obstacle::from_xywh(0.0, 0.0, 290.0, 200.0),
Obstacle::from_xywh(404.0, dy, 290.0, 200.0),
];
let obstacles: Vec<_> = cards.iter().map(|card| card.inflate(CLEARANCE)).collect();
let route = route_wire(
Point::new(290.0, 26.0),
PortSide::Right,
Point::new(404.0, 26.0 + dy),
PortSide::Left,
&obstacles,
RouteStyle::default(),
offset,
);
assert_short(&route);
assert_port_directions(&route, PortSide::Right, PortSide::Left);
assert_outside_cards(&route, &cards);
let RouteKind::Orthogonal { points, .. } = &route.kind else {
unreachable!();
};
assert_eq!(points.len(), 4, "{:?}", route.kind);
}
}
}
#[test]
fn screenshot_drag_from_saved_position_never_takes_an_outside_row() {
for (source_offset, target_offset) in [(0.0, 0.0), (17.5, 10.0)] {
let mut previous = None;
for y in -426..=30 {
let cards = screenshot_cards(y as f64);
let obstacles: Vec<_> = cards.iter().map(|card| card.inflate(CLEARANCE)).collect();
let from = Point::new(cards[0].max.x + source_offset, cards[0].min.y + 26.0);
let to = Point::new(cards[1].min.x - target_offset, cards[1].min.y + 26.0);
let route = route_wire_sticky(
from,
PortSide::Right,
to,
PortSide::Left,
&obstacles,
RouteStyle::default(),
0.0,
previous.as_ref(),
);
assert_short(&route);
assert_port_directions(&route, PortSide::Right, PortSide::Left);
assert_outside_cards(&route, &cards);
assert!(
route.slice(0.0, route.length()).iter().all(|point| {
point.y >= from.y.min(to.y)
&& point.y <= from.y.max(to.y)
&& point.x <= cards[1].max.x
}),
"y={y} {:?}",
route.kind
);
previous = Some(route);
}
}
}