Hierarchical Selection Implementation in Pascal Editor Selection Manager
The Pascal Editor implements a multi-level, phase-aware hierarchical selection system using a strategy pattern that delegates click handling to phase-specific logic while maintaining state across building, level, zone, and element layers.
The pascalorg/editor repository contains a sophisticated selection architecture that handles complex spatial hierarchies in both read-only viewers and full editing environments. This hierarchical selection implementation in the Pascal Editor selection manager leverages Zustand stores and strategic delegation to manage transitions from macro-level building selection down to individual furnishing items.
Core Selection State Architecture
The selection state follows a strict four-level hierarchy stored in the useViewer Zustand store located at packages/viewer/src/components/viewer/selection-manager.tsx. The state shape enforces a tree structure where each level depends on its parent:
{
buildingId: string | null, // Top-level container
levelId: string | null, // Vertical slice of building
zoneId: string | null, // Spatial polygon within level
selectedIds: string[], // Deep selection inside current zone
hoveredId: string | null,
}
The SelectionManager component subscribes to this store and registers global event listeners for every node type via emitter.on('wall:click', …) and similar handlers. According to the source code in lines 65‑78 and 100‑118, these listeners route all user interactions through a centralized strategy selector.
Strategy Pattern Implementation
The getStrategy() function (lines 63‑122) implements a cascading guard that determines which selection strategy to apply based on the current state:
- No building selected → Strategy selects buildings only
- Building selected, no level → Strategy selects levels
- Level selected, no zone → Strategy selects zones
- Zone selected → Strategy selects contents (walls, doors, items)
Each strategy defines four key properties:
types– Node types the strategy reacts to (e.g.,['wall', 'door'])handleClick– State mutation logic for the current hierarchy levelhandleDeselect– Behavior when clicking empty space (typically clearingselectedIdsor ascending the hierarchy)isValid– Guard ensuring the clicked node belongs to the current building/level/zone context
Spatial Validation with Zone Guards
When operating within a selected zone, the system validates clicks using geometric containment checks. The isNodeInZone function (lines 85‑120) ensures selection integrity by verifying that a node:
- Belongs to the current level via
isNodeOnLevel - Lies inside the zone polygon using
pointInPolygonWithTolerance
The tolerance logic expands zone boundaries by EDGE_TOLERANCE = 0.5 m to ensure that edge clicks register as interior selections. This geometric buffering is implemented in lines 40‑62 of the viewer's selection manager, accommodating real-world imprecision in CAD boundary interactions.
Phase-Aware Selection in the Editor
The full editor extends the viewer's hierarchical system with phase control (site | structure | furnish). Located at packages/editor/src/components/editor/selection-manager.tsx, the editor uses a constant map SELECTION_STRATEGIES (lines 85‑156) that defines behavior for each phase:
- Site Phase – Restricts selection to building footprints only
- Structure Phase – Enables selection of walls, slabs, ceilings, zones, and architectural items
- Furnish Phase – Limits selection to interior items excluding doors and windows
Each strategy reuses the computeNextIds helper (lines 65‑83) to implement additive selection when ⌘/Ctrl modifiers are active, toggling individual IDs in the selectedIds array without clearing existing selections.
Modifier Key Tracking
The editor tracks modifier states using a modifierKeysRef accessed via useRef. Lines 40‑66 attach keydown/keyup listeners that update this ref in real-time, enabling the conditional logic in click handlers to distinguish between replacing the current selection and adding to it.
Auto-Switching Between Phases
When a user clicks an element belonging to a different phase (e.g., selecting a zone while in structure mode), the manager automatically switches the editor phase. Lines 71‑98 contain the auto-switch logic that:
- Detects phase mismatches between the current mode and clicked element
- Updates the editor store's
phasefield - Adjusts the
structureLayerstate (elements↔zones) when necessary
Double-click behavior (lines 90‑145) accelerates navigation by descending the hierarchy—double-clicking a building enters structure phase, while double-clicking a zone prepares for content selection.
Synchronizing UI Components
Both viewer and editor maintain tiny React components that synchronize the imperative Three.js scene graph with the declarative outliner UI.
OutlinerSync (viewer, lines 77‑99) subscribes to selection changes and mutates outliner.selectedObjects and outliner.hoveredObjects arrays in place to preserve reference stability.
EditorOutlinerSync (editor, lines 16‑63) extends this pattern with phase awareness, filtering which IDs to highlight based on the current editing mode. Both components avoid React reconciliation costs by directly mutating the arrays consumed by the UI panel.
Implementation Example
To implement the selection system in a read-only viewer:
import { SelectionManager } from '@pascal-app/viewer';
export default function ProjectViewer() {
return (
<>
<ViewerCanvas />
<SelectionManager />
</>
);
}
For the full editing environment with phase support:
import { SelectionManager } from '@pascal-app/editor';
export default function ProjectEditor() {
return (
<>
<EditorCanvas />
<SelectionManager />
</>
);
}
Both components are pure React side-effect containers that attach global listeners without rendering DOM elements themselves.
Summary
- Hierarchical state in
useViewerenforces a building → level → zone → contents selection tree that prevents invalid cross-level selections. - Strategy pattern isolation in
SELECTION_STRATEGIESallows extending the system by adding new phase entries without modifying core click logic. - Spatial guards (
isNodeInZone,pointInPolygonWithTolerance) maintain geometric integrity with configurable edge tolerance. - Modifier key handling via
modifierKeysRefcentralizes additive selection logic separately from the state mutation strategies. - Outliner synchronization uses direct array mutation in
OutlinerSynccomponents to keep the Three.js scene and React UI perfectly aligned without expensive re-renders.
Frequently Asked Questions
How does the hierarchical selection state prevent invalid selections?
The state machine requires each level to be populated before descending to the next. The isValid guard in each strategy checks that the clicked node belongs to the currently selected building, level, and zone using helper functions like isNodeOnLevel and isNodeInZone. If a user clicks a wall belonging to a different level while in zone-selection mode, the guard returns false and the selection is ignored or triggers a phase switch.
What is the purpose of the EDGE_TOLERANCE constant in zone selection?
EDGE_TOLERANCE = 0.5 m (defined in viewer/selection-manager.tsx lines 40‑62) expands zone polygons geometrically when testing point containment. This ensures that clicks near boundary lines count as interior selections, accounting for precision limitations in CAD data and user input devices. Without this buffer, valid clicks on thin wall segments might fall outside the mathematical polygon boundary.
How does the editor handle clicks on elements from different phases?
When a click event fires, the handler in editor/selection-manager.tsx (lines 71‑98) compares the clicked element's phase against the current editor phase. If they mismatch (e.g., clicking a building while in furnish mode), the system automatically updates the phase field in the editor store and adjusts the structureLayer if necessary. This auto-switching allows seamless navigation between site planning and detailed furnishing without manual mode toggling.
Where is the selection state stored in the Pascal Editor architecture?
The primary selection state lives in the useViewer Zustand store at packages/viewer/src/store/use-viewer.ts, accessible to both viewer and editor packages. The editor augments this with additional state in useEditor (located at packages/editor/src/store/use-editor.ts) to track phase, structureLayer, and tool-specific contexts. Both stores are subscribed to by their respective SelectionManager components to coordinate UI updates.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →