2026 03 08_sector_d_canvas_surface_plan - mark-ik/graphshell GitHub Wiki

Sector D โ€” Canvas Surface Registry Development Plan

Doc role: Implementation plan for the canvas surface registry sector Status: Complete / implemented Date: 2026-03-08 Parent: 2026-03-08_registry_development_plan.md Registries covered: CanvasRegistry, LayoutRegistry, PhysicsProfileRegistry, LayoutDomainRegistry, PresentationDomainRegistry Specs: canvas_registry_spec.md, layout_registry_spec.md, physics_profile_registry_spec.md, layout_domain_registry_spec.md, presentation_domain_registry_spec.md Also depends on: ../2026-03-08_graph_app_decomposition_plan.md


Purpose

The canvas surface is how the spatial browser looks and moves. Every visual property of the graph โ€” node layout algorithm, force simulation parameters, label rendering, animation curves, zoom behaviour โ€” currently lives as hardcoded constants scattered across render/mod.rs and graph_app.rs. None of these five registries have Rust implementations.

These registries are tightly coupled. The layout-first principle from layout_domain_registry_spec.md requires that layout resolves before presentation, and CanvasRegistry is the graph-domain surface authority that coordinates topology, layout, and interaction policy. All five must be developed together in this sector.

CanvasRegistry
 โ”œโ”€โ”€ topology policy   โ”€โ”€โ–บ node culling, edge routing, selection geometry
 โ”œโ”€โ”€ layout policy     โ”€โ”€โ–บ LayoutRegistry (named algorithms)  โ”€โ”€โ–บ LayoutDomainRegistry
 โ””โ”€โ”€ interaction policy โ”€โ”€โ–บ PhysicsProfileRegistry (force presets)

LayoutDomainRegistry  โ”€โ”€โ–บ coordinates layout-first across graph + workbench + viewer surfaces
PresentationDomainRegistry โ”€โ”€โ–บ appearance + motion semantics after layout resolves
                             โ”€โ”€โ–บ ThemeRegistry (Sector G)

Current State

Implementation update (2026-03-10):

  • PhysicsProfileRegistry is implemented as a runtime-owned active-profile authority in shell/desktop/runtime/registries/physics_profile.rs and wired through RegistryRuntime.
  • CanvasRegistry is implemented as a runtime-owned active-profile authority in shell/desktop/runtime/registries/canvas.rs and now drives live keyboard-pan and lasso policy.
  • LayoutRegistry is implemented as a runtime-owned active-algorithm authority in shell/desktop/runtime/registries/layout.rs, with extracted algorithms/adapters in app/graph_layout.rs.
  • LayoutDomainRegistry exists as a domain coordinator and is now owned by RegistryRuntime for active viewer-surface resolution.
  • PresentationDomainRegistry now resolves concrete presentation tokens used by render/mod.rs and tile_compositor.rs instead of leaving those colors hardcoded.

Implementation note:

  • egui_graphs remains the widget/render substrate for graph panes, but algorithm ownership is no longer embedded in render/mod.rs. The render path now resolves and applies layouts through the runtime layout registry and the extracted app/graph_layout.rs adapter layer.

The graph_app_decomposition_plan.md (dated 2026-03-08) is the parallel structural work; these registry implementations are the policy surface that decomposed app code will call into.


Phase D1 โ€” PhysicsProfileRegistry: Named force presets

Start here. Physics profiles are the simplest atomic registry and unlock immediate user-visible behaviour changes (graph "feel" modes).

The physics_profile_registry_spec.md documents the Fruchterman-Reingold 1991 algorithm and three canonical preset families: Liquid, Gas, Solid.

D1.1 โ€” Define PhysicsProfile and PhysicsProfileRegistry

pub struct PhysicsProfile {
    pub id: PhysicsProfileId,
    pub display_name: String,
    pub repulsion_strength: f32,     // kยฒ / distance coefficient
    pub attraction_strength: f32,    // spring constant for edges
    pub gravity_center: f32,         // pull toward canvas centre
    pub damping: f32,                // velocity damping per tick
    pub max_displacement: f32,       // clamp per step
    pub cooling_factor: f32,         // temperature decay (0 < f < 1)
    pub iterations_per_frame: u8,
}

pub const PHYSICS_PROFILE_LIQUID: PhysicsProfileId = PhysicsProfileId("physics:liquid");
pub const PHYSICS_PROFILE_GAS: PhysicsProfileId = PhysicsProfileId("physics:gas");
pub const PHYSICS_PROFILE_SOLID: PhysicsProfileId = PhysicsProfileId("physics:solid");

pub struct PhysicsProfileRegistry {
    profiles: HashMap<PhysicsProfileId, PhysicsProfile>,
    active: PhysicsProfileId,
}

Built-in presets tuned for the existing graph feel (Gas โ‰ˆ current default).

Done gates:

  • PhysicsProfileRegistry struct in shell/desktop/runtime/registries/physics_profile.rs.
  • LIQUID, GAS, SOLID presets registered with calibrated values matching current graph behaviour.
  • set_active_profile() + active_profile() API.
  • Added to RegistryRuntime.
  • DIAG_PHYSICS_PROFILE channel (Info) emits on profile switch.
  • Unit test: each preset resolves to distinct parameter values.

D1.2 โ€” Replace hardcoded force constants in render/mod.rs

All FORCE_* constants in render/mod.rs are replaced by calls to the active physics profile:

let profile = registries.physics_profile.active_profile();
let repulsion = profile.repulsion_strength;
// ...

This is the key decomposition step that removes hardcoded physics from the render path.

Done gates:

  • All FORCE_* constants removed from render/mod.rs.
  • Physics simulation reads from PhysicsProfileRegistry::active_profile().
  • Visual regression check: default Gas profile produces identical graph layout to before.

D1.3 โ€” Profile switching via action

Register graph:set_physics_profile { profile_id } in ActionRegistry (Sector B). Switching profile emits GraphIntent::SetPhysicsProfile { profile_id } through the reducer; the reducer updates the active profile and triggers a physics reheat.

Done gates:

  • GraphIntent::SetPhysicsProfile variant defined and handled.
  • Physics reheats (temperature reset) on profile switch.
  • Profile switch persists to workspace state.

Phase D2 โ€” LayoutRegistry: Named layout algorithms

Unlocks: Layout algorithm selection; graph layout experiments.

The layout_registry_spec.md's algorithm-contract policy: every layout algorithm must define its input graph constraints, output coordinate contract, and determinism guarantee.

D2.1 โ€” Define LayoutAlgorithm trait and LayoutRegistry

pub trait LayoutAlgorithm: Send + Sync {
    fn id(&self) -> LayoutAlgorithmId;
    fn display_name(&self) -> &str;
    fn is_deterministic(&self) -> bool;

    /// Execute one step of the layout. Returns true if stable (converged).
    fn step(
        &self,
        nodes: &mut [(NodeKey, Vec2)],
        edges: &[(NodeKey, NodeKey)],
        profile: &PhysicsProfile,
        canvas_size: Vec2,
    ) -> bool;
}

pub struct LayoutRegistry {
    algorithms: HashMap<LayoutAlgorithmId, Box<dyn LayoutAlgorithm>>,
    active: LayoutAlgorithmId,
}

Built-in algorithms:

  • layout:fruchterman_reingold โ€” current default; extracted from render/mod.rs.
  • layout:force_atlas_2 โ€” prospective (stub for now, returns unimplemented!).
  • layout:hierarchical โ€” prospective (stub).

The no-hidden-mutation policy: layout steps only mutate the node coordinate buffer; they must not touch GraphBrowserApp fields directly.

Done gates:

  • LayoutAlgorithm trait defined.
  • FruchtermanReingold-equivalent adapter extracted from the render path.
  • LayoutRegistry struct with active algorithm selection.
  • LayoutRegistry added to RegistryRuntime.
  • Unit tests cover canonical resolution plus real grid/tree graph rewrites through the registry.

D2.2 โ€” Extract Fruchterman-Reingold from render/mod.rs

Move the existing force-directed algorithm implementation into a dedicated app/graph_layout.rs module that implements LayoutAlgorithm. This is the structural companion to the graph_app_decomposition_plan.md.

Done gates:

  • FruchtermanReingoldLayout-equivalent adapter exists in app/graph_layout.rs.
  • render/mod.rs resolves and applies layouts through the runtime layout registry.
  • Layout selection/application logic no longer lives inline in render/mod.rs.

Phase D3 โ€” CanvasRegistry: Graph-domain surface authority

Unlocks: Per-canvas topology / layout / interaction policy; CanvasStylePolicy, CanvasNavigationPolicy, CanvasTopologyPolicy canonical extension points (CLAUDE.md).

D3.1 โ€” Define CanvasProfile and CanvasRegistry

The canvas_registry_spec.md separates three concerns: topology policy, layout policy, and interaction/rendering policy. These must not be conflated.

pub struct CanvasProfile {
    pub id: CanvasProfileId,
    pub topology: CanvasTopologyPolicy,
    pub layout: CanvasLayoutPolicy,
    pub interaction: CanvasInteractionPolicy,
}

pub struct CanvasTopologyPolicy {
    pub edge_routing: EdgeRouting,        // Straight | Curved | Orthogonal
    pub culling_enabled: bool,
    pub lod_levels: Vec<LodLevel>,        // label/edge detail at zoom thresholds
    pub selection_geometry: SelectionGeometry,  // Single | Lasso | Box
}

pub struct CanvasLayoutPolicy {
    pub algorithm_id: LayoutAlgorithmId,
    pub physics_profile_id: PhysicsProfileId,
    pub initial_placement: InitialPlacement, // Random | Radial | Grid
}

pub struct CanvasInteractionPolicy {
    pub zoom_range: (f32, f32),
    pub pan_enabled: bool,
    pub node_drag_enabled: bool,
    pub edge_create_gesture: EdgeCreateGesture,
}

pub struct CanvasRegistry {
    profiles: HashMap<CanvasProfileId, CanvasProfile>,
    active: CanvasProfileId,
}

CanvasStylePolicy, CanvasNavigationPolicy, CanvasTopologyPolicy are the canonical extension points per CLAUDE.md โ€” they are the fields on CanvasProfile.

Done gates:

  • CanvasRegistry struct in shell/desktop/runtime/registries/canvas.rs.
  • CANVAS_PROFILE_DEFAULT registered with values matching current hardcoded behaviour.
  • Added to RegistryRuntime.
  • resolve_topology(), resolve_layout(), resolve_interaction_policy() APIs exposed.
  • DIAG_CANVAS_PROFILE channel (Info severity).

D3.2 โ€” Wire canvas profile into render path

render/mod.rs and render/panels.rs call into CanvasRegistry for topology, layout algorithm, and interaction policy instead of hardcoded values.

Done gates:

  • Culling and LOD toggles from CanvasTopologyPolicy (PLANNING_REGISTER ยง3 quick-win #10).
  • Edge routing mode read from CanvasTopologyPolicy.
  • Zoom range enforced from CanvasInteractionPolicy.
  • Render-time navigation/culling/lasso/layout policy now resolves through runtime canvas/layout authorities instead of inline selection constants.

D3.3 โ€” Zoom-adaptive label LOD

PLANNING_REGISTER ยง3 quick-win #8: "Add zoom-adaptive label LOD." Wire label detail levels from CanvasTopologyPolicy::lod_levels into the label rendering path.

Done gates:

  • LodLevel threshold table governs label visibility at each zoom level.
  • Label rendering uses CanvasRegistry::active_profile().topology.lod_levels.

Phase D4 โ€” LayoutDomainRegistry and PresentationDomainRegistry

Unlocks: Cross-surface layout coordination; themed presentation; full layout-first policy.

D4.1 โ€” LayoutDomainRegistry: coordinate layout-first across surfaces

The layout_domain_registry_spec.md's layout-first policy: layout resolves before presentation across graph, workbench, and viewer surfaces. The domain registry is the coordinator.

pub struct LayoutDomainRegistry {
    canvas_registry: Arc<CanvasRegistry>,
    workbench_surface: Arc<WorkbenchSurfaceRegistry>,  // Sector E
    viewer_surface: Arc<ViewerSurfaceRegistry>,         // Sector A
}

impl LayoutDomainRegistry {
    /// Resolve the complete layout intent for a frame tick.
    pub fn resolve_layout_frame(&self, context: &LayoutContext) -> LayoutFrameResult
}

The surface-sovereignty policy: each surface owns its own layout state; the domain registry coordinates sequencing, not ownership.

Done gates:

  • LayoutDomainRegistry struct in registries/domain/layout/mod.rs.
  • Layout-first sequencing is enforced through runtime order: canvas/layout application โ†’ workbench surface resolution โ†’ viewer surface resolution.
  • Added to RegistryRuntime.
  • No layout step for surface X modifies surface Y's layout state.

Implementation note (2026-03-10):

  • The original resolve_layout_frame() sketch was refined during implementation into runtime-owned resolution/apply ordering instead of one monolithic API.

D4.2 โ€” PresentationDomainRegistry: post-layout appearance

The presentation_domain_registry_spec.md's post-layout policy: presentation (colour, motion, typography, animation) applies only after layout resolves.

pub struct PresentationProfile {
    pub theme_id: ThemeId,              // resolved from ThemeRegistry (Sector G)
    pub motion: MotionPolicy,           // Reduced | Standard | Expressive
    pub node_label_font: FontSpec,
    pub edge_label_font: FontSpec,
    pub selection_highlight: Color,
    pub focus_ring: FocusRingSpec,
}

pub struct PresentationDomainRegistry {
    profiles: HashMap<PresentationProfileId, PresentationProfile>,
    active: PresentationProfileId,
}

ThemeRegistry (Sector G) provides the token set; PresentationDomainRegistry resolves the per-canvas profile that selects from those tokens.

Done gates:

  • PresentationDomainRegistry struct defined.
  • PRESENTATION_PROFILE_DEFAULT registered.
  • Added to RegistryRuntime.
  • resolve_presentation_profile() called from graph/workbench render paths instead of hardcoded colours.
  • Deferred: full theme token resolution remains owned by Sector G ThemeRegistry completion.

Acceptance Criteria (Sector D complete)

  • No hardcoded force constants in render/mod.rs; all resolved from PhysicsProfileRegistry.
  • No hardcoded canvas constants in render/panels.rs; policy-owned canvas behavior resolves through CanvasRegistry.
  • Layout algorithm authority is extracted behind LayoutAlgorithm and LayoutRegistry.
  • CanvasTopologyPolicy, CanvasNavigationPolicy, CanvasStylePolicy are the documented extension points per CLAUDE.md.
  • LOD-based label visibility works at zoom-in and zoom-out thresholds.
  • LayoutDomainRegistry sequences layout-first across canvas, workbench, and viewer.
  • PresentationDomainRegistry applies presentation after layout; no hardcoded colours remain in the graph/workbench presentation paths touched in D4.
  • Physics profile can be switched at runtime via graph:set_physics_profile action.
  • All five registries are in RegistryRuntime and covered by diagnostics channels.

Sector D completion note (2026-03-10):

  • Sector D is complete in code. The key implementation learning is that layout authority is enforced through extracted algorithm adapters plus runtime resolution/apply ordering, not through one giant resolve_layout_frame() API.

Related Documents

โš ๏ธ **GitHub.com Fallback** โš ๏ธ