# Hierarchical Selection Implementation in Pascal Editor Selection Manager

> Discover the hierarchical selection implementation in Pascal Editor. Learn how the strategy pattern manages multi-level phase-aware selections for building, level, zone, and element layers.

- Repository: [Pascal/editor](https://github.com/pascalorg/editor)
- Tags: internals
- Published: 2026-03-25

---

**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`](https://github.com/pascalorg/editor/blob/main/packages/viewer/src/components/viewer/selection-manager.tsx). The state shape enforces a tree structure where each level depends on its parent:

```ts
{
  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:

1. **No building selected** → Strategy selects buildings only
2. **Building selected, no level** → Strategy selects levels
3. **Level selected, no zone** → Strategy selects zones
4. **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 level
- **`handleDeselect`** – Behavior when clicking empty space (typically clearing `selectedIds` or 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`](https://github.com/pascalorg/editor/blob/main/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:

1. Detects phase mismatches between the current mode and clicked element
2. Updates the editor store's `phase` field
3. Adjusts the `structureLayer` state (`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:

```tsx
import { SelectionManager } from '@pascal-app/viewer';

export default function ProjectViewer() {
  return (
    <>
      <ViewerCanvas />
      <SelectionManager />
    </>
  );
}

```

For the full editing environment with phase support:

```tsx
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 `useViewer` enforces a building → level → zone → contents selection tree that prevents invalid cross-level selections.
- **Strategy pattern** isolation in `SELECTION_STRATEGIES` allows 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 `modifierKeysRef` centralizes additive selection logic separately from the state mutation strategies.
- **Outliner synchronization** uses direct array mutation in `OutlinerSync` components 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`](https://github.com/pascalorg/editor/blob/main/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`](https://github.com/pascalorg/editor/blob/main/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`](https://github.com/pascalorg/editor/blob/main/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`](https://github.com/pascalorg/editor/blob/main/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.