2026 03 08_sector_d_canvas_surface_plan - mark-ik/graphshell GitHub Wiki
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
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)
Implementation update (2026-03-10):
-
PhysicsProfileRegistryis implemented as a runtime-owned active-profile authority inshell/desktop/runtime/registries/physics_profile.rsand wired throughRegistryRuntime. -
CanvasRegistryis implemented as a runtime-owned active-profile authority inshell/desktop/runtime/registries/canvas.rsand now drives live keyboard-pan and lasso policy. -
LayoutRegistryis implemented as a runtime-owned active-algorithm authority inshell/desktop/runtime/registries/layout.rs, with extracted algorithms/adapters inapp/graph_layout.rs. -
LayoutDomainRegistryexists as a domain coordinator and is now owned byRegistryRuntimefor active viewer-surface resolution. -
PresentationDomainRegistrynow resolves concrete presentation tokens used byrender/mod.rsandtile_compositor.rsinstead of leaving those colors hardcoded.
Implementation note:
-
egui_graphsremains the widget/render substrate for graph panes, but algorithm ownership is no longer embedded inrender/mod.rs. The render path now resolves and applies layouts through the runtime layout registry and the extractedapp/graph_layout.rsadapter 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.
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.
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:
-
PhysicsProfileRegistrystruct inshell/desktop/runtime/registries/physics_profile.rs. -
LIQUID,GAS,SOLIDpresets registered with calibrated values matching current graph behaviour. -
set_active_profile()+active_profile()API. - Added to
RegistryRuntime. -
DIAG_PHYSICS_PROFILEchannel (Info) emits on profile switch. - Unit test: each preset resolves to distinct parameter values.
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 fromrender/mod.rs. - Physics simulation reads from
PhysicsProfileRegistry::active_profile(). - Visual regression check: default Gas profile produces identical graph layout to before.
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::SetPhysicsProfilevariant defined and handled. - Physics reheats (temperature reset) on profile switch.
- Profile switch persists to workspace state.
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.
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 fromrender/mod.rs. -
layout:force_atlas_2โ prospective (stub for now, returnsunimplemented!). -
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:
-
LayoutAlgorithmtrait defined. -
FruchtermanReingold-equivalent adapter extracted from the render path. -
LayoutRegistrystruct with active algorithm selection. -
LayoutRegistryadded toRegistryRuntime. - Unit tests cover canonical resolution plus real grid/tree graph rewrites through the registry.
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 inapp/graph_layout.rs. -
render/mod.rsresolves and applies layouts through the runtime layout registry. - Layout selection/application logic no longer lives inline in
render/mod.rs.
Unlocks: Per-canvas topology / layout / interaction policy; CanvasStylePolicy,
CanvasNavigationPolicy, CanvasTopologyPolicy canonical extension points (CLAUDE.md).
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:
-
CanvasRegistrystruct inshell/desktop/runtime/registries/canvas.rs. -
CANVAS_PROFILE_DEFAULTregistered with values matching current hardcoded behaviour. - Added to
RegistryRuntime. -
resolve_topology(),resolve_layout(),resolve_interaction_policy()APIs exposed. -
DIAG_CANVAS_PROFILEchannel (Info severity).
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.
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:
-
LodLevelthreshold table governs label visibility at each zoom level. - Label rendering uses
CanvasRegistry::active_profile().topology.lod_levels.
Unlocks: Cross-surface layout coordination; themed presentation; full layout-first policy.
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:
-
LayoutDomainRegistrystruct inregistries/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.
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:
-
PresentationDomainRegistrystruct defined. -
PRESENTATION_PROFILE_DEFAULTregistered. - 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
ThemeRegistrycompletion.
- No hardcoded force constants in
render/mod.rs; all resolved fromPhysicsProfileRegistry. - No hardcoded canvas constants in
render/panels.rs; policy-owned canvas behavior resolves throughCanvasRegistry. - Layout algorithm authority is extracted behind
LayoutAlgorithmandLayoutRegistry. -
CanvasTopologyPolicy,CanvasNavigationPolicy,CanvasStylePolicyare the documented extension points per CLAUDE.md. - LOD-based label visibility works at zoom-in and zoom-out thresholds.
-
LayoutDomainRegistrysequences layout-first across canvas, workbench, and viewer. -
PresentationDomainRegistryapplies 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_profileaction. - All five registries are in
RegistryRuntimeand 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.
- canvas_registry_spec.md
- layout_registry_spec.md
- physics_profile_registry_spec.md
- layout_domain_registry_spec.md
- presentation_domain_registry_spec.md
- ../2026-03-08_graph_app_decomposition_plan.md
- 2026-03-08_sector_g_mod_agent_plan.md โ ThemeRegistry dependency
- 2026-03-08_registry_development_plan.md โ master index