Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions docs/OVERVIEW.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,10 @@ Overview:
3. Frontend calls parse_repo (Tauri IPC) -> cc-tauri first strips any prior parse state so re-parsing is idempotent -> parses each file with tree-sitter (parallel via rayon) in a single tree walk that attributes each raw reference to its innermost enclosing block (top-level imports/refs attributed to the File) and populates each block's children hierarchy -> only top-level blocks are appended to File children, with progress reported as ONE batched ParseEvent::Progress per ~100 files or ~50ms (cumulative counts + last file + that batch's per-file errors) instead of two events per file -> resolves imports first (yielding file-to-file Import edges plus an import map), then resolves references into edges via SymbolTable using a precision ladder (same-file > imported-file > global-unique > ambiguous, dropping references matching more than 5 global symbols — abandoned before the candidates are even cloned) so each edge carries a Resolution confidence -> keeps the full graph (nodes + edges, with adjacency rebuilt) in server-side GraphState and returns an edge-less ParseResult (nodes, root, edge_count, node_edge_kinds connectivity map) serialized straight out of the stored graph. Nodes go over the wire SLIM: every field except `signature`, which only the details panel and hover tooltip read and which they fetch per node via get_node_details.
4. Frontend graphStore converts the ParseResult into a CodeGraph (node tree + nodeEdgeKinds Map, no edges, no signatures) and computes visibility/expansion state. The hideUnconnectedNodes filter (visibilityFilter) runs synchronously from nodeEdgeKinds. handleParseEvent applies one store update per progress batch, so ingestion no longer broadcasts once per file.
5. Canvas derives the effective layout inputs from the zoom-level viewMode (default "module"): module view forces edge kinds to {Import} and treats files as collapsed (saved state preserved but ignored); symbol view uses the user's edge kinds + expansion; an active focus frame restricts visibility/expansion to the fetched focus ids (a node frame's neighborhood or an edge frame's edge detail). Canvas passes the (edge-less) graph + effective state to PixiRenderer.
6. The layout pipeline has two phases behind one coalescing queue in PixiRenderer, and the store's relayoutPolicy decides which one a given state change needs (full / edges / visibility / nothing -- at most ONE per user action):
6. The layout pipeline has two phases behind one coalescing queue owned by LayoutOrchestrator (canvas/layout/layoutOrchestrator.ts; PixiRenderer only supplies its effects), and the store's relayoutPolicy decides which one a given state change needs (full / edges / visibility / nothing -- at most ONE per user action):
- Positions phase (full): elkLayout builds the ELK node tree, collects the render set (renderIds), fetches per-view direct + aggregated edges via get_subgraph(render_ids, edge_kinds) computed server-side (direct edges carry a Resolution; ambiguous edges may be hidden client-side), then ELK places the nodes and routes the edges and the renderer rebuilds the canvas. Views over 1500 rendered nodes OR over 3000 view edges skip ELK edge routing and use straight-line fallback edges. Triggered by a new graph, expand/collapse, showing nodes, hide-unconnected, view-mode switches, focus changes, and the sidebar's explicit "Apply Layout Changes" button (graphStore.layoutVersion).
- Edges phase (cheap): edge-kind and hide-ambiguous toggles re-run only get_subgraph for the SAME render set and rebuild the edges on the cached node positions -- reusing each surviving edge's routed polyline and straight-lining edges that appear anew -- with no ELK run and no camera move (graphStore.edgeVersion). Hiding nodes is cheaper still: the canvas just flips the existing node displays.
While a pass is in flight, further requests collapse into ONE pending rerun with the latest inputs (elkjs cannot be aborted), and stale results are discarded by _layoutRequestId. At draw time the client re-routing pass (obstacle avoidance) is separately budgeted: node boxes are indexed once per redraw in an R-tree and each edge queries only the obstacles near it, candidate crossing-scoring is dropped above 250 rendered edges, and re-routing is skipped entirely above 500 rendered edges or 2000 visible nodes -- so edge redraw time stays bounded instead of growing without limit. Applying a layout performs exactly ONE full edge rebuild.
While a pass is in flight, further requests collapse into ONE pending rerun with the latest inputs (elkjs cannot be aborted), stale results are discarded by the orchestrator's request-id guard, the cheap visibility redraw is gated until the pass lands or fails, and a visibility change that arrived mid-pass is re-applied on top of the fresh displays so a slow layout cannot resurrect hidden nodes. An edge's endpoint ANCHORS (which side of which node box, at what offset) are decided exactly once, at layout time, from the pristine route -- read off the first/last segment's direction rather than guessed from tolerances -- and are then the durable contract every later stage consumes. At draw time all geometry belongs to one explicit route pipeline (layout/edgeRoutePipeline.ts) whose composition function IS the stage order: anchor endpoints -> spread endpoint lanes -> detour around obstacles, each a pure stage returning new records, with a stage that moves an endpoint also emitting its updated anchor. The detour stage is separately budgeted: node boxes are indexed lazily once per redraw in an R-tree and each edge queries only the obstacles near it, candidate crossing-scoring is dropped above 250 rendered edges, and re-routing is skipped entirely above 500 rendered edges or 2000 visible nodes -- so edge redraw time stays bounded instead of growing without limit. edgeDrawing.ts is left with layer management and stroking, and hit-testing measures the polyline that was actually drawn. Applying a layout performs exactly ONE full edge rebuild.
7. Both phases derive per-kind edge counts for the view from that same SubGraph payload; PixiRenderer publishes them to edgeLegendStore once the pass is known to be current, and the bottom-left EdgeLegend overlay renders one row per edge kind (colour, name, count) which doubles as the edge-kind toggle UI.
8. User interactions (hover, select, expand, drag, zoom) update stores and trigger re-renders. Selection is a node SET (`selectedNodeIds`, with `selectedNodeId` as the derived last-selected primary) and doubles as the pinned edge highlight: hovering previews a node's connections, clicking pins that same dim+highlight treatment so it survives unhover, and ctrl/cmd-clicking a second node switches the highlight to the induced subgraph (only edges with both endpoints selected). The pin is re-applied after every base-layer rebuild and invalidated when its nodes leave the graph. Hovering an EDGE additionally emphasises its two endpoint nodes' borders. Edge tooltips read kind + count from the layout edges (aggregated edges carry a collapsed count). Aggregated edges (count > 1) additionally render a world-space "×N" chip at the arc-length midpoint of their routed polyline, at the "detail" LOD only.
9. Selecting a node also drives the right-side details panel: it fetches get_neighborhood(selectedNodeId, 1, ALL kinds) (debounced, with a monotonic stale-request guard), splits those edges into incoming/outgoing around the selected node, groups them per kind, and renders clickable endpoint rows with per-row Focus buttons.
Expand All @@ -37,8 +37,8 @@ Overview:

Features Index:
canvas-rendering:
description: Interactive Pixi.js canvas with node rendering, edge drawing, minimap, drag, and LOD-based visibility. Applying a layout performs exactly one full edge rebuild, and the edge re-routing pass is bounded by an R-tree obstacle index plus a routing budget (full > obstacles-only > none) chosen from the rendered-edge and visible-node counts. Edges are hit-tested by distance to their routed polyline, which drives edge hover, double-click-to-drill-in on aggregated edges, and a border emphasis on the hovered edge's two endpoint nodes (so it is visible where an edge lands without tracing it by eye). Node borders come from one emphasis table (selected > hovered-edge endpoint > plain). Aggregated (collapsed-container) edges carry "xN" count chips drawn at the arc-length midpoint of their routed polyline, shown at the "detail" LOD only and dimmed in step with the edge they label.
entry_points: [packages/app/src/canvas/renderers/PixiRenderer.ts, packages/app/src/canvas/Canvas.tsx, packages/app/src/canvas/renderers/edgeDrawing.ts, packages/app/src/canvas/renderers/edgeRoutingBudget.ts, packages/app/src/canvas/layout/obstacleIndex.ts, packages/app/src/canvas/renderers/edgeLabels.ts, packages/app/src/canvas/renderers/nodeEmphasis.ts]
description: Interactive Pixi.js canvas with node rendering, edge drawing, minimap, drag, and LOD-based visibility. Edge GEOMETRY has one owner -- an explicit route pipeline (anchor endpoints -> spread endpoint lanes -> detour around obstacles, each a pure stage) that consumes the anchors decided at layout time and keeps anchors and geometry in agreement; edgeDrawing.ts is left with layer management and stroking. Applying a layout performs exactly one full edge rebuild, and the detour stage is bounded by a lazily built R-tree obstacle index plus a routing budget (full > obstacles-only > none) chosen from the rendered-edge and visible-node counts. Edges are hit-tested by distance to the polyline that was ACTUALLY DRAWN, which drives edge hover, double-click-to-drill-in on aggregated edges, and a border emphasis on the hovered edge's two endpoint nodes (so it is visible where an edge lands without tracing it by eye). Node borders come from one emphasis table (selected > hovered-edge endpoint > plain). Aggregated (collapsed-container) edges carry "xN" count chips drawn at the arc-length midpoint of their routed polyline, shown at the "detail" LOD only and dimmed in step with the edge they label.
entry_points: [packages/app/src/canvas/renderers/PixiRenderer.ts, packages/app/src/canvas/Canvas.tsx, packages/app/src/canvas/renderers/edgeDrawing.ts, packages/app/src/canvas/layout/edgeRoutePipeline.ts, packages/app/src/canvas/layout/routingConstants.ts, packages/app/src/canvas/layout/edgeRoutingBudget.ts, packages/app/src/canvas/layout/obstacleIndex.ts, packages/app/src/canvas/renderers/edgeLabels.ts, packages/app/src/canvas/renderers/nodeEmphasis.ts]
depends_on: [graph-layout, palette]
doc: docs/features/canvas-rendering.md

Expand Down Expand Up @@ -70,8 +70,8 @@ Features Index:
doc: docs/features/palette.md

graph-layout:
description: ELK-based hierarchical graph layout running in a web worker (elk-api + elk-worker.min.js?worker) so layout does not block the UI thread, split into a POSITIONS phase (ELK) and an EDGES phase (get_subgraph fetch + edge rebuild against cached positions) behind a run-latest coalescing queue. A dependency-free trigger policy (relayoutPolicy) classifies every state change as full / edges / visibility / nothing, so one user action costs at most one layout pass. Fetches per-view direct + aggregated edges from the backend (get_subgraph) rather than filtering client-side, feeds them to ELK for routing, and falls back to straight-line edges (also used as the layout guard for views over 1500 rendered nodes or 3000 view edges -- routing cost scales with edges as much as with nodes).
entry_points: [packages/app/src/canvas/layout/elkLayout.ts, packages/app/src/canvas/layout/edgePhase.ts, packages/app/src/canvas/layout/layoutScheduler.ts, packages/app/src/stores/relayoutPolicy.ts, packages/app/src/canvas/renderers/edgeRoutingBudget.ts]
description: ELK-based hierarchical graph layout running in a web worker (elk-api + elk-worker.min.js?worker) so layout does not block the UI thread, split into a POSITIONS phase (ELK) and an EDGES phase (get_subgraph fetch + edge rebuild against cached positions) behind a run-latest coalescing queue. LayoutOrchestrator owns the whole request lifecycle -- the queue, the stale-result guard, the pending gate on the cheap visibility redraw, and the latest-vs-applied visible-set reconciliation -- with every side effect injected, so the renderer keeps only the drawing. A dependency-free trigger policy (relayoutPolicy) classifies every state change as full / edges / visibility / nothing, so one user action costs at most one layout pass. Fetches per-view direct + aggregated edges from the backend (get_subgraph) rather than filtering client-side, feeds them to ELK for routing, and falls back to straight-line edges (also used as the layout guard for views over 1500 rendered nodes or 3000 view edges -- routing cost scales with edges as much as with nodes). Extraction is composition over pure, ELK-free functions (elkExtract.ts) and is where each edge's endpoint ANCHORS are decided exactly once, exactly -- from the first/last segment's direction against the node box -- becoming the durable contract the edges phase and the draw-time route pipeline consume instead of re-deriving. anchorEdgePolyline reports whether it anchored the layout's route or discarded it for a fresh one rather than escalating silently.
entry_points: [packages/app/src/canvas/layout/elkLayout.ts, packages/app/src/canvas/layout/elkExtract.ts, packages/app/src/canvas/layout/edgePhase.ts, packages/app/src/canvas/layout/layoutOrchestrator.ts, packages/app/src/canvas/layout/layoutScheduler.ts, packages/app/src/stores/relayoutPolicy.ts, packages/app/src/canvas/layout/edgeRoutingBudget.ts]
depends_on: [graph-model]
doc: docs/features/graph-layout.md

Expand Down
12 changes: 10 additions & 2 deletions docs/features/benchmarking.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,8 +162,16 @@ Three scenarios per size:
The script degrades gracefully: `obstacleIndex.ts` and `edgeRoutingBudget.ts` are
loaded through a `try`/`catch` dynamic import, and their absence turns
`shipped_redraw` into "route everything, crossing-aware", which is the older
shipped behaviour. Runtime imports use explicit `.ts` specifiers so the module
chain loads under plain `node` (tsconfig sets `allowImportingTsExtensions`).
shipped behaviour. `edgeRoutingBudget.ts` is probed at BOTH
`src/canvas/layout/` (where it lives) and `src/canvas/renderers/` (where it lived
before the routing pipeline was consolidated), so a run on an older branch still
finds it and stays comparable. Runtime imports use explicit `.ts` specifiers so
the module chain loads under plain `node` (tsconfig sets
`allowImportingTsExtensions`).

`OBSTACLE_QUERY_MARGIN` is deliberately a LITERAL in the bench rather than an
import from `layout/routingConstants.ts`: the file has to stay byte-comparable
across branches where that module does not exist.

### 5. Runner -- `benchmarks/run_all.sh`

Expand Down
Loading