TRAVELS — the guided constellation tour — design
Class:
design· Function axis:f0the guided-tour primitive over aGraph3D·f1TravelRoute+ the route KIND constructors ·f2theSpline(Catmull–Rom) + theTravelanimator (timeline · ghost · camera fly) ·f3the controls +state_jsonobservable surface ·f4the freeze / host-drives-data contract. Strict: this file states only the shape we run. Rejected alternatives are at the end.
f0 — what this kills
Rickard's idea: a glowing, ghostlike spline drawn in 3-D space that takes a guided TOUR through the constellation graph — a camera + highlight fly along a path visiting a selectable set of nodes ("latest changes"; "reported bugs — start at the top, travel down into the bug"; "spotlight this one node"). Built as a reusable primitive in facett-graph3d so dwarves' navigator page + korp can drive it with real warehouse data later.
It is default OFF: a Graph3D with no route set renders byte-for-byte as before (travel == None), so every existing render + snapshot golden is unchanged. The tour is purely additive.
f1 — TravelRoute + the route KINDS (the "list of interesting travels")
A route is pure data the HOST builds from nornir's warehouse metadata:
TravelRoute { name: String, kind: RouteKind, stops: Vec<NodeId> } // NodeId = graph node index
Built-in constructors (the "list of interesting travels") — each takes a node set the host supplies; facett fetches nothing:
TravelRoute::spotlight(node)— a single-node tour: fly to it and pulse (Spotlight).TravelRoute::latest_changes(nodes_by_recency)— visit recently-changed units, newest → oldest, in the host's order (LatestChanges).TravelRoute::bugs_top_down(bug_anchors)— "start at the top, travel down into the bug": visit reported-bug anchors in the host's top→down order (BugsTopDown).TravelRoute::new(name, stops)— a host-authored custom route (Custom).
RouteKind is a small serde enum so a robot / host asserts which tour is running from state_json (travel_kind).
f2 — the Spline + the Travel animator
Spline — a uniform Catmull–Rom curve through the stops' node centres. Catmull–Rom passes through every control point, so the path visits each stop exactly: point(seg, 0.0) == pts[seg] and point(seg, 1.0) == pts[seg+1] (the geometry proof the tests assert). Endpoints are clamped (the ends repeat their neighbour) so the curve stays put at the first/last stop. sample(t) walks the whole spline at t ∈ [0,1].
Travel — the animator. It holds the route, the stops' 3-D centres (the spline control points, sliced from the graph layout by Graph3D::set_travel), and a deterministic elapsed timeline (tour seconds). Timeline shape for m stops:
dwell(0) · travel(0→1) · dwell(1) · travel(1→2) · … · dwell(m-1)
total = m·dwell_secs + (m-1)·seg_secs (a lone stop is just its dwell)
- Ghost marker —
marker_pos()is the eased spline point: during a stop's dwell it sits exactly on the node (pausing + pulsing); during a segment it glides with asmoothstepease-in-out (rest-to-rest).is_dwelling()tells the two apart. - Camera fly —
camera_yaw() = atan2(marker.x, marker.z)brings the ghost to the FRONT of the orbit (max depth after the Y-rotation). Whilecamera_followis on the host'sGraph3D::viewflies the yaw to this target each frame (through the FC-2Msg::SetYawpath) and suspends the auto-spin — the "camera + highlight fly along the path". - Stop-visited event —
advance(dt)/seek(t)return the node ids of stops newly reached, monotonic-forward in route order (edge-triggered vialast_fired), so a largedtstill reports every crossed stop in order. - Determinism —
elapsedmoves ONLY throughadvance(relative) /seek(absolute) — neverInstant::now(). A fixed clock ⇒ a byte-stable ghost (the crate-wide LAW).
Ghost paint — Travel::paint(painter, project, phase) is a pure overlay: the ghostly translucent spline trail (breathing on decor::pulse), a soft glow at each stop node, and the pulsing emissive ghost marker at the head — all projected through the SAME camera closure the cloud paints with (L5, no re-derived projection). Reuses neon::paint_node_orb + facett_core::render::decor. A cyan-white spectre (rgb(185,240,255)), emissive + transparent, consistent with the Code Vault aesthetic.
f3 — controls + the state_json surface
Controls are Graph3D::Msg variants (the single FC-2 mutation path; no-ops when no route is set): TravelPlay · TravelPause · TravelToggle · TravelNextStop · TravelPrevStop · TravelSpeed(f32) · TravelTick(f64) (advance ONLY the tour, live). Msg::Tick / Msg::SetClock also drive the tour off the graph's master clock.
When (and only when) a route is set, Graph3D::state_json merges the tour surface:
travel_active · travel_playing · travel_name · travel_kind · travel_stops ·
travel_stop_index · travel_progress · travel_dwelling · travel_node · travel_speed ·
travel_elapsed · travel_camera_follow
No route ⇒ none of these keys appear, so existing state_json goldens are byte-stable.
f4 — the freeze / host-drives-data contract
facett renders the tour; it does not fetch data. The HOST supplies the ordered node sets (from nornir's warehouse — recency / bug anchors / a single node) and builds a TravelRoute; Graph3D::set_travel(route) slices the stops' laid-out centres for the spline. The layout is frozen (Barnes–Hut freeze cache); after a relayout / absorb the host calls Travel::refresh_centers(&graph_centers) to rebuild the spline without disturbing the timeline.
Follow-up (NOT this pass): dwarves' navigator page wires real warehouse data (recency / reported bugs) into TravelRoute::latest_changes / bugs_top_down and drives Graph3D's travel controls — a dwarves-side consumption of this primitive. korp drives the same seam with lakehouse data.
Tests (L4, RED-when-broken + testmatrix)
spline_passes_through_every_control_point_in_order— geometry proof (no GPU): the spline hits each stop at the segment boundaries, in order.travel_marker_sits_exactly_on_each_stop_at_its_dwell— the dwell "pause on the node".advancing_the_tour_visits_every_stop_in_order_and_progress_grows—tickfires the stop-visited event for every stop in order and climbs progress 0→1 (RED: a frozen tour fires nothing, progress stays 0).paused_tour_does_not_move/next_and_prev_stop_jump_between_stops/camera_yaw_faces_the_current_marker— controls + camera fly.- Integration (
tests/travels.rs, headless Elm bridge): default-OFF byte-stability, the ghost overlay tessellates strictly MORE geometry than the plain cloud, and drivingTickthrough the bridge advancestravel_progress+ flies the yaw. - wgpu snapshot (
tests/travels_snapshot.rs): the ghost paints non-blank (a toured frame emits more light than the no-tour frame at the same yaw) and the tour moves (a later clock changes the frame).
Rejected
- A standalone
TravelFacet that renders its own scene. Rejected: it would duplicate the graph's projection + camera and drift out of alignment with the constellation the host already paints. The tour is an OVERLAY on the existingGraph3D, reusing its camera/projection (L5). - A plain poly-line / cubic Bézier through the stops. A Bézier's control points are off-curve, so the path would miss the node centres. Catmull–Rom interpolates the knots, which is exactly "visit each node".
- Driving the tour off
clock_salone. A live tour must advance without pinning the graph's animation clock (that would freeze the auto-spin), so the tour keeps its ownelapsedand the live path uses a dedicatedTravelTick.