Full Code of pascalorg/editor for AI

main 0bd0cab533a6 cached
476 files
23.0 MB
469.2k tokens
1081 symbols
2 requests
Download .txt
Showing preview only (1,861K chars total). Download the full file or copy to clipboard to get everything.
Repository: pascalorg/editor
Branch: main
Commit: 0bd0cab533a6
Files: 476
Total size: 23.0 MB

Directory structure:
gitextract_f568hpnc/

├── .claude/
│   └── settings.json
├── .cursor/
│   └── rules/
│       ├── creating-rules.mdc
│       ├── events.mdc
│       ├── layers.mdc
│       ├── node-schemas.mdc
│       ├── renderers.mdc
│       ├── scene-registry.mdc
│       ├── selection-managers.mdc
│       ├── spatial-queries.mdc
│       ├── systems.mdc
│       ├── tools.mdc
│       └── viewer-isolation.mdc
├── .github/
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.yml
│   │   ├── config.yml
│   │   └── feature_request.yml
│   ├── pull_request_template.md
│   └── workflows/
│       └── release.yml
├── .gitignore
├── .npmrc
├── .vscode/
│   └── settings.json
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── SETUP.md
├── apps/
│   └── editor/
│       ├── .gitignore
│       ├── README.md
│       ├── app/
│       │   ├── api/
│       │   │   └── health/
│       │   │       └── route.ts
│       │   ├── globals.css
│       │   ├── layout.tsx
│       │   ├── page.tsx
│       │   ├── privacy/
│       │   │   └── page.tsx
│       │   └── terms/
│       │       └── page.tsx
│       ├── env.mjs
│       ├── lib/
│       │   └── utils.ts
│       ├── next.config.ts
│       ├── package.json
│       ├── postcss.config.mjs
│       ├── public/
│       │   ├── demos/
│       │   │   └── demo_1.json
│       │   └── items/
│       │       ├── ac-block/
│       │       │   └── model.glb
│       │       ├── air-conditioner/
│       │       │   └── model.glb
│       │       ├── air-conditioner-block/
│       │       │   └── model.glb
│       │       ├── air-conditioning/
│       │       │   └── model.glb
│       │       ├── alarm-keypad/
│       │       │   └── model.glb
│       │       ├── ball/
│       │       │   └── model.glb
│       │       ├── barbell/
│       │       │   └── model.glb
│       │       ├── barbell-stand/
│       │       │   └── model.glb
│       │       ├── basket-hoop/
│       │       │   └── model.glb
│       │       ├── bathroom-sink/
│       │       │   └── model.glb
│       │       ├── bathtub/
│       │       │   └── model.glb
│       │       ├── bean-bag/
│       │       │   └── model.glb
│       │       ├── bedside-table/
│       │       │   └── model.glb
│       │       ├── books/
│       │       │   └── model.glb
│       │       ├── bookshelf/
│       │       │   └── model.glb
│       │       ├── bunkbed/
│       │       │   └── model.glb
│       │       ├── bush/
│       │       │   └── model.glb
│       │       ├── cactus/
│       │       │   └── model.glb
│       │       ├── car-toy/
│       │       │   └── model.glb
│       │       ├── ceiling-fan/
│       │       │   └── model.glb
│       │       ├── ceiling-lamp/
│       │       │   └── model.glb
│       │       ├── ceiling-light/
│       │       │   └── model.glb
│       │       ├── circular-ceiling-light/
│       │       │   └── model.glb
│       │       ├── closet/
│       │       │   └── model.glb
│       │       ├── coat-rack/
│       │       │   └── model.glb
│       │       ├── coffee-machine/
│       │       │   └── model.glb
│       │       ├── coffee-table/
│       │       │   └── model.glb
│       │       ├── column/
│       │       │   └── model.glb
│       │       ├── computer/
│       │       │   └── model.glb
│       │       ├── couch-medium/
│       │       │   └── model.glb
│       │       ├── couch-small/
│       │       │   └── model.glb
│       │       ├── cutting-board/
│       │       │   └── model.glb
│       │       ├── desk/
│       │       │   └── model.glb
│       │       ├── dining-chair/
│       │       │   └── model.glb
│       │       ├── dining-table/
│       │       │   └── model.glb
│       │       ├── door/
│       │       │   └── model.glb
│       │       ├── door-bar/
│       │       │   └── model.glb
│       │       ├── door-with-bar/
│       │       │   └── model.glb
│       │       ├── doorway-front/
│       │       │   └── model.glb
│       │       ├── double-bed/
│       │       │   └── model.glb
│       │       ├── dresser/
│       │       │   └── model.glb
│       │       ├── drying-rack/
│       │       │   └── model.glb
│       │       ├── easel/
│       │       │   └── model.glb
│       │       ├── electric-panel/
│       │       │   └── model.glb
│       │       ├── ev-wall-charger/
│       │       │   └── model.glb
│       │       ├── exercise-bike/
│       │       │   └── model.glb
│       │       ├── exit-sign/
│       │       │   └── model.glb
│       │       ├── fence/
│       │       │   └── model.glb
│       │       ├── fir-tree/
│       │       │   └── model.glb
│       │       ├── fire-alarm/
│       │       │   └── model.glb
│       │       ├── fire-detector/
│       │       │   └── model.glb
│       │       ├── fire-extinguisher/
│       │       │   └── model.glb
│       │       ├── flat-screen-tv/
│       │       │   └── model.glb
│       │       ├── floor-lamp/
│       │       │   └── model.glb
│       │       ├── freezer/
│       │       │   └── model.glb
│       │       ├── fridge/
│       │       │   └── model.glb
│       │       ├── fruits/
│       │       │   └── model.glb
│       │       ├── frying-pan/
│       │       │   └── model.glb
│       │       ├── glass-door/
│       │       │   └── model.glb
│       │       ├── guitar/
│       │       │   └── model.glb
│       │       ├── hedge/
│       │       │   └── model.glb
│       │       ├── high-fence/
│       │       │   └── model.glb
│       │       ├── hood/
│       │       │   └── model.glb
│       │       ├── hydrant/
│       │       │   └── model.glb
│       │       ├── indoor-plant/
│       │       │   └── model.glb
│       │       ├── iron/
│       │       │   └── model.glb
│       │       ├── ironing-board/
│       │       │   └── model.glb
│       │       ├── kettle/
│       │       │   └── model.glb
│       │       ├── kitchen/
│       │       │   └── model.glb
│       │       ├── kitchen-cabinet/
│       │       │   └── model.glb
│       │       ├── kitchen-counter/
│       │       │   └── model.glb
│       │       ├── kitchen-fridge/
│       │       │   └── model.glb
│       │       ├── kitchen-shelf/
│       │       │   └── model.glb
│       │       ├── kitchen-utensils/
│       │       │   └── model.glb
│       │       ├── laundry-bag/
│       │       │   └── model.glb
│       │       ├── livingroom-chair/
│       │       │   └── model.glb
│       │       ├── lounge-chair/
│       │       │   └── model.glb
│       │       ├── low-fence/
│       │       │   └── model.glb
│       │       ├── medium-fence/
│       │       │   └── model.glb
│       │       ├── microwave/
│       │       │   └── model.glb
│       │       ├── office-chair/
│       │       │   └── model.glb
│       │       ├── office-table/
│       │       │   └── model.glb
│       │       ├── outdoor-playhouse/
│       │       │   └── model.glb
│       │       ├── palm/
│       │       │   └── model.glb
│       │       ├── parking-spot/
│       │       │   └── model.glb
│       │       ├── patio-umbrella/
│       │       │   └── model.glb
│       │       ├── piano/
│       │       │   ├── Fireplace_13.glb
│       │       │   └── model.glb
│       │       ├── picture/
│       │       │   └── model.glb
│       │       ├── pillar/
│       │       │   └── model.glb
│       │       ├── pool-table/
│       │       │   └── model.glb
│       │       ├── recessed-light/
│       │       │   └── model.glb
│       │       ├── rectangular-carpet/
│       │       │   └── model.glb
│       │       ├── rectangular-ceiling-light/
│       │       │   └── model.glb
│       │       ├── rectangular-mirror/
│       │       │   └── model.glb
│       │       ├── round-carpet/
│       │       │   └── model.glb
│       │       ├── round-mirror/
│       │       │   └── model.glb
│       │       ├── scooter/
│       │       │   └── model.glb
│       │       ├── sewing-machine/
│       │       │   └── model.glb
│       │       ├── shelf/
│       │       │   └── model.glb
│       │       ├── shower/
│       │       │   └── model.glb
│       │       ├── shower-angle/
│       │       │   └── model.glb
│       │       ├── shower-rug/
│       │       │   └── model.glb
│       │       ├── shower-square/
│       │       │   └── model.glb
│       │       ├── single-bed/
│       │       │   └── model.glb
│       │       ├── sink-cabinet/
│       │       │   └── model.glb
│       │       ├── skate/
│       │       │   └── model.glb
│       │       ├── small-indoor-plant/
│       │       │   └── model.glb
│       │       ├── small-kitchen-cabinet/
│       │       │   └── model.glb
│       │       ├── smoke-detector/
│       │       │   └── model.glb
│       │       ├── sofa/
│       │       │   └── model.glb
│       │       ├── sprinkler/
│       │       │   └── model.glb
│       │       ├── stairs/
│       │       │   └── model.glb
│       │       ├── stereo-speaker/
│       │       │   └── model.glb
│       │       ├── stool/
│       │       │   └── model.glb
│       │       ├── stove/
│       │       │   └── model.glb
│       │       ├── sunbed/
│       │       │   └── model.glb
│       │       ├── suspended-fireplace/
│       │       │   └── model.glb
│       │       ├── table/
│       │       │   └── model.glb
│       │       ├── table-lamp/
│       │       │   └── model.glb
│       │       ├── television/
│       │       │   └── model.glb
│       │       ├── tesla/
│       │       │   └── model.glb
│       │       ├── thermostat/
│       │       │   └── model.glb
│       │       ├── threadmill/
│       │       │   └── model.glb
│       │       ├── toaster/
│       │       │   └── model.glb
│       │       ├── toilet/
│       │       │   └── model.glb
│       │       ├── toilet-brush/
│       │       │   └── model.glb
│       │       ├── toilet-paper/
│       │       │   └── model.glb
│       │       ├── toy/
│       │       │   └── model.glb
│       │       ├── trash-bin/
│       │       │   └── model.glb
│       │       ├── tree/
│       │       │   └── model.glb
│       │       ├── tub/
│       │       │   └── model.glb
│       │       ├── tv-stand/
│       │       │   └── model.glb
│       │       ├── wall-art-06/
│       │       │   └── model.glb
│       │       ├── wall-sink/
│       │       │   └── model.glb
│       │       ├── washing-machine/
│       │       │   └── model.glb
│       │       ├── window-double/
│       │       │   └── model.glb
│       │       ├── window-large/
│       │       │   └── model.glb
│       │       ├── window-rectangle/
│       │       │   └── model.glb
│       │       ├── window-round/
│       │       │   └── model.glb
│       │       ├── window-simple/
│       │       │   └── model.glb
│       │       ├── window-small/
│       │       │   └── model.glb
│       │       ├── window-small-2/
│       │       │   └── model.glb
│       │       ├── window-square/
│       │       │   └── model.glb
│       │       ├── window1-black-open-1731/
│       │       │   └── model.glb
│       │       └── wine-bottle/
│       │           └── model.glb
│       └── tsconfig.json
├── biome.jsonc
├── package.json
├── packages/
│   ├── core/
│   │   ├── README.md
│   │   ├── package.json
│   │   ├── src/
│   │   │   ├── events/
│   │   │   │   └── bus.ts
│   │   │   ├── hooks/
│   │   │   │   ├── scene-registry/
│   │   │   │   │   └── scene-registry.ts
│   │   │   │   └── spatial-grid/
│   │   │   │       ├── spatial-grid-manager.ts
│   │   │   │       ├── spatial-grid-sync.ts
│   │   │   │       ├── spatial-grid.ts
│   │   │   │       ├── use-spatial-query.ts
│   │   │   │       └── wall-spatial-grid.ts
│   │   │   ├── index.ts
│   │   │   ├── lib/
│   │   │   │   ├── asset-storage.ts
│   │   │   │   └── space-detection.ts
│   │   │   ├── schema/
│   │   │   │   ├── base.ts
│   │   │   │   ├── camera.ts
│   │   │   │   ├── collections.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── material.ts
│   │   │   │   ├── nodes/
│   │   │   │   │   ├── building.ts
│   │   │   │   │   ├── ceiling.ts
│   │   │   │   │   ├── door.ts
│   │   │   │   │   ├── guide.ts
│   │   │   │   │   ├── item.ts
│   │   │   │   │   ├── level.ts
│   │   │   │   │   ├── roof-segment.ts
│   │   │   │   │   ├── roof.ts
│   │   │   │   │   ├── scan.ts
│   │   │   │   │   ├── site.ts
│   │   │   │   │   ├── slab.ts
│   │   │   │   │   ├── stair-segment.ts
│   │   │   │   │   ├── stair.ts
│   │   │   │   │   ├── wall.ts
│   │   │   │   │   ├── window.ts
│   │   │   │   │   └── zone.ts
│   │   │   │   └── types.ts
│   │   │   ├── store/
│   │   │   │   ├── actions/
│   │   │   │   │   └── node-actions.ts
│   │   │   │   ├── use-interactive.ts
│   │   │   │   └── use-scene.ts
│   │   │   ├── systems/
│   │   │   │   ├── ceiling/
│   │   │   │   │   └── ceiling-system.tsx
│   │   │   │   ├── door/
│   │   │   │   │   └── door-system.tsx
│   │   │   │   ├── item/
│   │   │   │   │   └── item-system.tsx
│   │   │   │   ├── roof/
│   │   │   │   │   └── roof-system.tsx
│   │   │   │   ├── slab/
│   │   │   │   │   └── slab-system.tsx
│   │   │   │   ├── stair/
│   │   │   │   │   └── stair-system.tsx
│   │   │   │   ├── wall/
│   │   │   │   │   ├── wall-footprint.ts
│   │   │   │   │   ├── wall-mitering.ts
│   │   │   │   │   └── wall-system.tsx
│   │   │   │   └── window/
│   │   │   │       └── window-system.tsx
│   │   │   └── utils/
│   │   │       ├── clone-scene-graph.ts
│   │   │       └── types.ts
│   │   └── tsconfig.json
│   ├── editor/
│   │   ├── package.json
│   │   ├── src/
│   │   │   ├── components/
│   │   │   │   ├── editor/
│   │   │   │   │   ├── custom-camera-controls.tsx
│   │   │   │   │   ├── editor-layout-v2.tsx
│   │   │   │   │   ├── export-manager.tsx
│   │   │   │   │   ├── first-person-controls.tsx
│   │   │   │   │   ├── floating-action-menu.tsx
│   │   │   │   │   ├── floorplan-panel.tsx
│   │   │   │   │   ├── grid.tsx
│   │   │   │   │   ├── index.tsx
│   │   │   │   │   ├── node-action-menu.tsx
│   │   │   │   │   ├── preset-thumbnail-generator.tsx
│   │   │   │   │   ├── selection-manager.tsx
│   │   │   │   │   ├── site-edge-labels.tsx
│   │   │   │   │   ├── thumbnail-generator.tsx
│   │   │   │   │   └── wall-measurement-label.tsx
│   │   │   │   ├── feedback-dialog.tsx
│   │   │   │   ├── pascal-radio.tsx
│   │   │   │   ├── preview-button.tsx
│   │   │   │   ├── systems/
│   │   │   │   │   ├── ceiling/
│   │   │   │   │   │   └── ceiling-system.tsx
│   │   │   │   │   ├── roof/
│   │   │   │   │   │   └── roof-edit-system.tsx
│   │   │   │   │   ├── stair/
│   │   │   │   │   │   └── stair-edit-system.tsx
│   │   │   │   │   └── zone/
│   │   │   │   │       ├── zone-label-editor-system.tsx
│   │   │   │   │       └── zone-system.tsx
│   │   │   │   ├── tools/
│   │   │   │   │   ├── ceiling/
│   │   │   │   │   │   ├── ceiling-boundary-editor.tsx
│   │   │   │   │   │   ├── ceiling-hole-editor.tsx
│   │   │   │   │   │   └── ceiling-tool.tsx
│   │   │   │   │   ├── door/
│   │   │   │   │   │   ├── door-math.ts
│   │   │   │   │   │   ├── door-tool.tsx
│   │   │   │   │   │   └── move-door-tool.tsx
│   │   │   │   │   ├── item/
│   │   │   │   │   │   ├── item-tool.tsx
│   │   │   │   │   │   ├── move-tool.tsx
│   │   │   │   │   │   ├── placement-math.ts
│   │   │   │   │   │   ├── placement-strategies.ts
│   │   │   │   │   │   ├── placement-types.ts
│   │   │   │   │   │   ├── use-draft-node.ts
│   │   │   │   │   │   └── use-placement-coordinator.tsx
│   │   │   │   │   ├── roof/
│   │   │   │   │   │   ├── move-roof-tool.tsx
│   │   │   │   │   │   └── roof-tool.tsx
│   │   │   │   │   ├── select/
│   │   │   │   │   │   └── box-select-tool.tsx
│   │   │   │   │   ├── shared/
│   │   │   │   │   │   ├── cursor-sphere.tsx
│   │   │   │   │   │   └── polygon-editor.tsx
│   │   │   │   │   ├── site/
│   │   │   │   │   │   └── site-boundary-editor.tsx
│   │   │   │   │   ├── slab/
│   │   │   │   │   │   ├── slab-boundary-editor.tsx
│   │   │   │   │   │   ├── slab-hole-editor.tsx
│   │   │   │   │   │   └── slab-tool.tsx
│   │   │   │   │   ├── stair/
│   │   │   │   │   │   └── stair-tool.tsx
│   │   │   │   │   ├── tool-manager.tsx
│   │   │   │   │   ├── wall/
│   │   │   │   │   │   ├── wall-drafting.ts
│   │   │   │   │   │   └── wall-tool.tsx
│   │   │   │   │   ├── window/
│   │   │   │   │   │   ├── move-window-tool.tsx
│   │   │   │   │   │   ├── window-math.ts
│   │   │   │   │   │   └── window-tool.tsx
│   │   │   │   │   └── zone/
│   │   │   │   │       ├── zone-boundary-editor.tsx
│   │   │   │   │       └── zone-tool.tsx
│   │   │   │   ├── ui/
│   │   │   │   │   ├── action-menu/
│   │   │   │   │   │   ├── action-button.tsx
│   │   │   │   │   │   ├── camera-actions.tsx
│   │   │   │   │   │   ├── control-modes.tsx
│   │   │   │   │   │   ├── furnish-tools.tsx
│   │   │   │   │   │   ├── index.tsx
│   │   │   │   │   │   ├── structure-tools.tsx
│   │   │   │   │   │   └── view-toggles.tsx
│   │   │   │   │   ├── command-palette/
│   │   │   │   │   │   ├── editor-commands.tsx
│   │   │   │   │   │   └── index.tsx
│   │   │   │   │   ├── controls/
│   │   │   │   │   │   ├── action-button.tsx
│   │   │   │   │   │   ├── material-picker.tsx
│   │   │   │   │   │   ├── metric-control.tsx
│   │   │   │   │   │   ├── panel-section.tsx
│   │   │   │   │   │   ├── segmented-control.tsx
│   │   │   │   │   │   ├── slider-control.tsx
│   │   │   │   │   │   └── toggle-control.tsx
│   │   │   │   │   ├── floating-level-selector.tsx
│   │   │   │   │   ├── helpers/
│   │   │   │   │   │   ├── ceiling-helper.tsx
│   │   │   │   │   │   ├── helper-manager.tsx
│   │   │   │   │   │   ├── item-helper.tsx
│   │   │   │   │   │   ├── roof-helper.tsx
│   │   │   │   │   │   ├── slab-helper.tsx
│   │   │   │   │   │   └── wall-helper.tsx
│   │   │   │   │   ├── item-catalog/
│   │   │   │   │   │   ├── catalog-items.tsx
│   │   │   │   │   │   └── item-catalog.tsx
│   │   │   │   │   ├── panels/
│   │   │   │   │   │   ├── ceiling-panel.tsx
│   │   │   │   │   │   ├── collections/
│   │   │   │   │   │   │   └── collections-popover.tsx
│   │   │   │   │   │   ├── door-panel.tsx
│   │   │   │   │   │   ├── item-panel.tsx
│   │   │   │   │   │   ├── panel-manager.tsx
│   │   │   │   │   │   ├── panel-wrapper.tsx
│   │   │   │   │   │   ├── presets/
│   │   │   │   │   │   │   └── presets-popover.tsx
│   │   │   │   │   │   ├── reference-panel.tsx
│   │   │   │   │   │   ├── roof-panel.tsx
│   │   │   │   │   │   ├── roof-segment-panel.tsx
│   │   │   │   │   │   ├── slab-panel.tsx
│   │   │   │   │   │   ├── stair-panel.tsx
│   │   │   │   │   │   ├── stair-segment-panel.tsx
│   │   │   │   │   │   ├── wall-panel.tsx
│   │   │   │   │   │   └── window-panel.tsx
│   │   │   │   │   ├── primitives/
│   │   │   │   │   │   ├── button.tsx
│   │   │   │   │   │   ├── card.tsx
│   │   │   │   │   │   ├── color-dot.tsx
│   │   │   │   │   │   ├── context-menu.tsx
│   │   │   │   │   │   ├── dialog.tsx
│   │   │   │   │   │   ├── dropdown-menu.tsx
│   │   │   │   │   │   ├── error-boundary.tsx
│   │   │   │   │   │   ├── input.tsx
│   │   │   │   │   │   ├── number-input.tsx
│   │   │   │   │   │   ├── opacity-control.tsx
│   │   │   │   │   │   ├── popover.tsx
│   │   │   │   │   │   ├── separator.tsx
│   │   │   │   │   │   ├── sheet.tsx
│   │   │   │   │   │   ├── shortcut-token.tsx
│   │   │   │   │   │   ├── sidebar.tsx
│   │   │   │   │   │   ├── skeleton.tsx
│   │   │   │   │   │   ├── slider.tsx
│   │   │   │   │   │   ├── switch.tsx
│   │   │   │   │   │   └── tooltip.tsx
│   │   │   │   │   ├── scene-loader.tsx
│   │   │   │   │   ├── sidebar/
│   │   │   │   │   │   ├── app-sidebar.tsx
│   │   │   │   │   │   ├── icon-rail.tsx
│   │   │   │   │   │   ├── panels/
│   │   │   │   │   │   │   ├── settings-panel/
│   │   │   │   │   │   │   │   ├── audio-settings-dialog.tsx
│   │   │   │   │   │   │   │   ├── index.tsx
│   │   │   │   │   │   │   │   └── keyboard-shortcuts-dialog.tsx
│   │   │   │   │   │   │   ├── site-panel/
│   │   │   │   │   │   │   │   ├── building-tree-node.tsx
│   │   │   │   │   │   │   │   ├── ceiling-tree-node.tsx
│   │   │   │   │   │   │   │   ├── door-tree-node.tsx
│   │   │   │   │   │   │   │   ├── index.tsx
│   │   │   │   │   │   │   │   ├── inline-rename-input.tsx
│   │   │   │   │   │   │   │   ├── item-tree-node.tsx
│   │   │   │   │   │   │   │   ├── level-tree-node.tsx
│   │   │   │   │   │   │   │   ├── roof-tree-node.tsx
│   │   │   │   │   │   │   │   ├── slab-tree-node.tsx
│   │   │   │   │   │   │   │   ├── stair-tree-node.tsx
│   │   │   │   │   │   │   │   ├── tree-node-actions.tsx
│   │   │   │   │   │   │   │   ├── tree-node-drag.tsx
│   │   │   │   │   │   │   │   ├── tree-node.tsx
│   │   │   │   │   │   │   │   ├── wall-tree-node.tsx
│   │   │   │   │   │   │   │   ├── window-tree-node.tsx
│   │   │   │   │   │   │   │   └── zone-tree-node.tsx
│   │   │   │   │   │   │   └── zone-panel/
│   │   │   │   │   │   │       └── index.tsx
│   │   │   │   │   │   └── tab-bar.tsx
│   │   │   │   │   ├── slider-demo.tsx
│   │   │   │   │   ├── slider.tsx
│   │   │   │   │   └── viewer-toolbar.tsx
│   │   │   │   ├── viewer-overlay.tsx
│   │   │   │   └── viewer-zone-system.tsx
│   │   │   ├── contexts/
│   │   │   │   └── presets-context.tsx
│   │   │   ├── hooks/
│   │   │   │   ├── use-auto-save.ts
│   │   │   │   ├── use-contextual-tools.ts
│   │   │   │   ├── use-grid-events.ts
│   │   │   │   ├── use-keyboard.ts
│   │   │   │   ├── use-mobile.ts
│   │   │   │   └── use-reduced-motion.ts
│   │   │   ├── index.tsx
│   │   │   ├── lib/
│   │   │   │   ├── constants.ts
│   │   │   │   ├── level-selection.ts
│   │   │   │   ├── scene.ts
│   │   │   │   ├── sfx/
│   │   │   │   │   └── index.ts
│   │   │   │   ├── sfx-bus.ts
│   │   │   │   ├── sfx-player.ts
│   │   │   │   └── utils.ts
│   │   │   ├── store/
│   │   │   │   ├── use-audio.tsx
│   │   │   │   ├── use-command-registry.ts
│   │   │   │   ├── use-editor.tsx
│   │   │   │   ├── use-palette-view-registry.ts
│   │   │   │   └── use-upload.ts
│   │   │   └── three-types.ts
│   │   └── tsconfig.json
│   ├── eslint-config/
│   │   ├── README.md
│   │   ├── base.js
│   │   ├── next.js
│   │   ├── package.json
│   │   └── react-internal.js
│   ├── typescript-config/
│   │   ├── base.json
│   │   ├── nextjs.json
│   │   ├── package.json
│   │   └── react-library.json
│   ├── ui/
│   │   ├── eslint.config.mjs
│   │   ├── package.json
│   │   ├── src/
│   │   │   ├── button.tsx
│   │   │   ├── card.tsx
│   │   │   └── code.tsx
│   │   └── tsconfig.json
│   └── viewer/
│       ├── README.md
│       ├── package.json
│       ├── src/
│       │   ├── components/
│       │   │   ├── error-boundary.tsx
│       │   │   ├── renderers/
│       │   │   │   ├── building/
│       │   │   │   │   └── building-renderer.tsx
│       │   │   │   ├── ceiling/
│       │   │   │   │   └── ceiling-renderer.tsx
│       │   │   │   ├── door/
│       │   │   │   │   └── door-renderer.tsx
│       │   │   │   ├── guide/
│       │   │   │   │   └── guide-renderer.tsx
│       │   │   │   ├── item/
│       │   │   │   │   └── item-renderer.tsx
│       │   │   │   ├── level/
│       │   │   │   │   └── level-renderer.tsx
│       │   │   │   ├── node-renderer.tsx
│       │   │   │   ├── roof/
│       │   │   │   │   ├── roof-materials.ts
│       │   │   │   │   └── roof-renderer.tsx
│       │   │   │   ├── roof-segment/
│       │   │   │   │   └── roof-segment-renderer.tsx
│       │   │   │   ├── scan/
│       │   │   │   │   └── scan-renderer.tsx
│       │   │   │   ├── scene-renderer.tsx
│       │   │   │   ├── site/
│       │   │   │   │   └── site-renderer.tsx
│       │   │   │   ├── slab/
│       │   │   │   │   └── slab-renderer.tsx
│       │   │   │   ├── stair/
│       │   │   │   │   └── stair-renderer.tsx
│       │   │   │   ├── stair-segment/
│       │   │   │   │   └── stair-segment-renderer.tsx
│       │   │   │   ├── wall/
│       │   │   │   │   └── wall-renderer.tsx
│       │   │   │   ├── window/
│       │   │   │   │   └── window-renderer.tsx
│       │   │   │   └── zone/
│       │   │   │       └── zone-renderer.tsx
│       │   │   └── viewer/
│       │   │       ├── ground-occluder.tsx
│       │   │       ├── index.tsx
│       │   │       ├── lights.tsx
│       │   │       ├── perf-monitor.tsx
│       │   │       ├── post-processing.tsx
│       │   │       ├── selection-manager.tsx
│       │   │       └── viewer-camera.tsx
│       │   ├── hooks/
│       │   │   ├── use-asset-url.ts
│       │   │   ├── use-gltf-ktx2.tsx
│       │   │   └── use-node-events.ts
│       │   ├── index.ts
│       │   ├── lib/
│       │   │   ├── asset-url.ts
│       │   │   ├── layers.ts
│       │   │   └── materials.ts
│       │   ├── r3f.d.ts
│       │   ├── store/
│       │   │   ├── use-item-light-pool.ts
│       │   │   ├── use-viewer.d.ts
│       │   │   └── use-viewer.ts
│       │   └── systems/
│       │       ├── export/
│       │       │   └── export-system.tsx
│       │       ├── guide/
│       │       │   └── guide-system.tsx
│       │       ├── interactive/
│       │       │   └── interactive-system.tsx
│       │       ├── item-light/
│       │       │   └── item-light-system.tsx
│       │       ├── level/
│       │       │   ├── level-system.d.ts
│       │       │   ├── level-system.tsx
│       │       │   └── level-utils.ts
│       │       ├── scan/
│       │       │   └── scan-system.tsx
│       │       ├── wall/
│       │       │   └── wall-cutout.tsx
│       │       └── zone/
│       │           └── zone-system.tsx
│       └── tsconfig.json
├── tooling/
│   ├── release/
│   │   ├── android-playstore-release.sh
│   │   ├── ios-appstore-release.sh
│   │   └── release.sh
│   └── typescript/
│       ├── base.json
│       ├── expo.json
│       ├── nextjs.json
│       ├── package.json
│       └── react-library.json
└── turbo.json

================================================
FILE CONTENTS
================================================

================================================
FILE: .claude/settings.json
================================================
{
  "permissions": {
    "allow": [
      "Bash(npx turbo:*)"
    ]
  }
}


================================================
FILE: .cursor/rules/creating-rules.mdc
================================================
---
description: How to create and maintain project rules
globs: .cursor/rules/**
alwaysApply: false
---

# Creating Rules

Rules live in two places and are kept in sync via symlinks:

- `.cursor/rules/<rule-name>.mdc` — source of truth (Cursor format)
- `.claude/rules/<rule-name>.md` — symlink pointing to the cursor file

## Workflow

**1. Write the rule in `.cursor/rules/`**

```
.cursor/rules/my-rule.mdc
```

**2. Create a symlink in `.claude/rules/`**

```bash
ln -s ../../.cursor/rules/my-rule.mdc .claude/rules/my-rule.md
```

The `../../` prefix is required because the symlink lives two levels deep.

**3. Verify**

```bash
ls -la .claude/rules/my-rule.md
# → .claude/rules/my-rule.md -> ../../.cursor/rules/my-rule.mdc
```

## Rule File Format

```markdown
---
description: One-line summary of what this rule covers
globs:
alwaysApply: false
---

# Rule Title

Short intro paragraph.

## Section

Concrete guidance with examples.
```

- Set `alwaysApply: true` only for rules that apply to every file in the project.
- Use `globs` to scope a rule to specific paths (e.g. `packages/viewer/**`).

## Good Practices

- Keep rules under 500 lines. Split large rules into smaller focused files.
- Include concrete examples or reference real files with `@filename`.
- Add a rule when the same mistake has been made more than once — not preemptively.
- Prefer showing a correct example over listing prohibitions.

## Existing Rules

| Rule | Covers |
|---|---|
| `creating-rules` | This file — how to add rules |
| `renderers` | Node renderer pattern in `packages/viewer` |
| `systems` | Core and viewer systems architecture |
| `tools` | Editor tools structure in `apps/editor` |
| `viewer-isolation` | Keeping `@pascal-app/viewer` editor-agnostic |
| `scene-registry` | Global node ID → Object3D map and `useRegistry` |
| `selection-managers` | Two-layer selection (viewer + editor), events, outliner |
| `events` | Typed event bus — emitting and listening to node and grid events |
| `node-schemas` | Zod schema pattern for node types, createNode, updateNode |
| `spatial-queries` | Placement validation (canPlaceOnFloor/Wall/Ceiling) for tools |
| `layers` | Three.js layer constants, ownership, and rendering separation |


================================================
FILE: .cursor/rules/events.mdc
================================================
---
description: Typed event bus — emitting and listening to node and grid events
globs: packages/core/src/events/**,packages/viewer/**,apps/editor/**
alwaysApply: false
---

# Events

The event bus (`emitter`) is a global `mitt` instance typed with `EditorEvents`. It decouples renderers (which emit) from selection managers and tools (which listen).

**Source**: @packages/core/src/events/bus.ts

## Event Key Format

```
<nodeType>:<suffix>
```

Example keys: `wall:click`, `item:enter`, `door:double-click`, `grid:pointerdown`

### Node Types
`wall` `item` `site` `building` `level` `zone` `slab` `ceiling` `roof` `window` `door`

### Suffixes
```ts
'click' | 'move' | 'enter' | 'leave' | 'pointerdown' | 'pointerup' | 'context-menu' | 'double-click'
```

The `grid:*` events fire when the user interacts with empty space (no node hit). They are **not** emitted by a mesh — `useGridEvents(gridY)` (@apps/editor/hooks/use-grid-events.ts) manually raycasts against a ground plane and calls `emitter.emit('grid:click', …)`. Mount it in any tool or editor component that needs empty-space interactions.

## NodeEvent Shape

```ts
interface NodeEvent<T extends AnyNode = AnyNode> {
  node: T                                  // typed node that triggered the event
  position: [number, number, number]       // world-space hit position
  localPosition: [number, number, number]  // object-local hit position
  normal?: [number, number, number]        // face normal, if available
  stopPropagation: () => void
  nativeEvent: ThreeEvent<PointerEvent>
}
```

Grid events only carry `position` and `nativeEvent` (no `node`).

## Emitting

Renderers emit via `useNodeEvents` — never call `emitter.emit` directly in a renderer:

```tsx
// packages/viewer/src/hooks/use-node-events.ts
const events = useNodeEvents(node, 'wall')
return <mesh ref={ref} {...events} />
```

`useNodeEvents` converts R3F `ThreeEvent` into a `NodeEvent` and emits `wall:click`, `wall:enter`, etc. It suppresses events while the camera is dragging.

## Listening

Listen in a `useEffect`. Always clean up with `emitter.off` using the **same function reference**:

```ts
// Single event
useEffect(() => {
  const handler = (e: WallEvent) => { /* … */ }
  emitter.on('wall:click', handler)
  return () => emitter.off('wall:click', handler)
}, [])

// Multiple node types, same handler
useEffect(() => {
  const types = ['wall', 'slab', 'door'] as const
  const handler = (e: NodeEvent) => { /* … */ }
  types.forEach(t => emitter.on(`${t}:click`, handler as any))
  return () => types.forEach(t => emitter.off(`${t}:click`, handler as any))
}, [])
```

See @apps/editor/components/editor/selection-manager.tsx for a full multi-type listener example.

## Rules

- **Renderers only emit, never listen.** Listening belongs in selection managers, tools, or systems.
- **Always clean up.** Forgetting `emitter.off` causes duplicate handlers and memory leaks.
- **Use the same function reference** for `on` and `off`. Anonymous functions inside `useEffect` are fine as long as the ref is captured in the same scope.
- **Don't use emitter for state.** It's for one-shot interaction events. Persistent state goes in `useScene`, `useViewer`, or `useEditor`.
- **`stopPropagation`** prevents the event from being handled by overlapping listeners (e.g. a door on a wall). Call it when a handler should be the final consumer.


================================================
FILE: .cursor/rules/layers.mdc
================================================
---
description: Three.js layer conventions — which layer each object type lives on and why
globs: packages/viewer/**,apps/editor/**
alwaysApply: false
---

# Three.js Layers

Three.js `Layers` control which objects each camera and render pass sees. We use them to separate scene geometry, editor helpers, and zone overlays into distinct rendering buckets without duplicating scene structure.

## Layer Map

| Constant | Value | Package | Purpose |
|---|---|---|---|
| `SCENE_LAYER` | `0` | `@pascal-app/viewer` | Default Three.js layer — all regular scene geometry |
| `EDITOR_LAYER` | `1` | `apps/editor` | Editor-only helpers: grid, tool previews, cursor meshes, snap guides |
| `ZONE_LAYER` | `2` | `@pascal-app/viewer` | Zone floor fills and wall borders — composited in a separate post-processing pass |

Import the constants from their owning packages:

```ts
// In viewer code
import { SCENE_LAYER, ZONE_LAYER } from '@pascal-app/viewer'

// In editor code
import { EDITOR_LAYER } from '@/lib/constants'
```

## Why Separate Zones onto Layer 2

Zones use semi-transparent, `depthTest: false` materials that must be composited *on top of* the scene without being fed into SSGI or TRAA. The post-processing pipeline in `post-processing.tsx` renders a dedicated `zonePass` with a `Layers` mask that enables only `ZONE_LAYER` (and disables `SCENE_LAYER`), then blends its output into the final composite manually:

```ts
const zoneLayers = useMemo(() => {
  const l = new Layers()
  l.enable(ZONE_LAYER)
  l.disable(SCENE_LAYER)
  return l
}, [])

zonePass.setLayers(zoneLayers)
```

This keeps zones out of the SSGI depth/normal buffers (which would produce incorrect AO on transparent surfaces) while still letting them appear correctly over the scene.

## Why Separate Editor Helpers onto Layer 1

The editor camera enables `EDITOR_LAYER` so tools and helpers are visible during editing. The thumbnail generator disables `EDITOR_LAYER` so exports show clean geometry without snap lines or cursor spheres.

## Rules

- **Never hardcode layer numbers.** Always use the named constants.
- **`SCENE_LAYER` and `ZONE_LAYER` belong in `@pascal-app/viewer`** — they are renderer concerns, not editor concerns.
- **`EDITOR_LAYER` belongs in `apps/editor`** — the viewer must never import it; editor behaviour is injected via props/children.
- **Zone meshes must set `layers={ZONE_LAYER}`** so they are picked up by `zonePass` and excluded from `scenePass` depth buffers.
- **Editor helper meshes must set `layers={EDITOR_LAYER}`** so they are invisible to the thumbnail camera and the viewer's render passes.
- **Do not add new layers without updating this rule** and the post-processing pipeline accordingly.


================================================
FILE: .cursor/rules/node-schemas.mdc
================================================
---
description: Node type definitions, Zod schema pattern, and how to create nodes in the scene
globs: packages/core/src/schema/**
alwaysApply: false
---

# Node Schemas

All node types are defined as Zod schemas in `packages/core/src/schema/nodes/`. Each schema extends `BaseNode` and exports both the schema and its inferred TypeScript type.

**Sources**: @packages/core/src/schema/base.ts, @packages/core/src/schema/nodes/

## BaseNode

Every node shares these fields:

```ts
{
  object: 'node'            // always literal 'node'
  id: string                // typed ID e.g. "wall_abc123"
  type: string              // node type discriminator e.g. "wall"
  name?: string             // optional display name
  parentId: string | null   // parent node ID; null = root
  visible: boolean          // defaults to true
  metadata: Record<string, unknown>  // arbitrary JSON, defaults to {}
}
```

## Defining a New Node Type

```ts
// packages/core/src/schema/nodes/my-node.ts
import { z } from 'zod'
import { BaseNode, objectId, nodeType } from '../base'

export const MyNode = BaseNode.extend({
  id: objectId('my-node'),      // generates IDs like "my-node_abc123"
  type: nodeType('my-node'),    // sets literal type discriminator
  // add node-specific fields:
  width: z.number().default(1),
  label: z.string().optional(),
}).describe('My node — one-line description of what it represents')

export type MyNode = z.infer<typeof MyNode>
export type MyNodeId = MyNode['id']
```

Then add `MyNode` to the `AnyNode` union in `packages/core/src/schema/types.ts`.

## Creating Nodes in Tools

Always use `.parse()` to validate and generate a proper typed ID. Never construct a plain object manually.

```ts
import { WallNode } from '@pascal-app/core'
import { useScene } from '@pascal-app/core'

// 1. Parse validates and fills defaults (including auto-generated id)
const wall = WallNode.parse({ name: 'Wall 1', start: [0, 0], end: [5, 0] })

// 2. createNode(node, parentId?) inserts it into the scene
const { createNode } = useScene.getState()
createNode(wall, levelId)
```

For batch creation:

```ts
const { createNodes } = useScene.getState()
createNodes([
  { node: WallNode.parse({ start: [0, 0], end: [5, 0] }), parentId: levelId },
  { node: WallNode.parse({ start: [5, 0], end: [5, 4] }), parentId: levelId },
])
```

## Updating Nodes

```ts
const { updateNode } = useScene.getState()
updateNode(wall.id, { height: 2.8 })   // partial update, merges with existing
```

## Real Examples

- **Simple geometry node**: @packages/core/src/schema/nodes/wall.ts — `start`, `end`, `thickness`, `height`
- **Polygon node**: @packages/core/src/schema/nodes/slab.ts — `polygon: [number, number][]`, `holes`
- **Positioned node**: @packages/core/src/schema/nodes/item.ts — `position`, `rotation`, `scale`, `asset`

## Rules

- **Always use `.parse()`** — it generates the correct ID prefix and fills defaults. `WallNode.parse({...})` not `{ type: 'wall', id: '...' }`.
- **Never hardcode IDs.** Let `objectId('type')` generate them.
- **Add new node types to `AnyNode`** in `types.ts` or they won't be accepted by the store.
- **Keep schemas in `packages/core`**, not in the viewer or editor — the schema is shared by all packages.


================================================
FILE: .cursor/rules/renderers.mdc
================================================
---
description: Node renderer pattern in packages/viewer
globs: packages/viewer/**
alwaysApply: false
---

# Renderers

Renderers live in `packages/viewer/src/components/renderers/`. Each renderer is responsible for one node type's Three.js geometry and materials — nothing else.

## Dispatch Chain

```
<SceneRenderer>          — iterates rootNodeIds from useScene
  └─ <NodeRenderer>      — switches on node.type, renders the matching component
       └─ <WallRenderer> — (or SlabRenderer, DoorRenderer, …)
```

See @packages/viewer/src/components/renderers/scene-renderer.tsx and @packages/viewer/src/components/renderers/node-renderer.tsx.

## Renderer Responsibilities

A renderer **should**:
- Read its node from `useScene` via the node's ID
- Register its mesh(es) with `useRegistry()` so other systems can look them up
- Subscribe to pointer events via `useNodeEvents()`
- Render geometry and apply materials based on node properties

A renderer **must not**:
- Run geometry generation logic (that belongs in a System)
- Import anything from `apps/editor`
- Manage selection state directly (use `useViewer` for read, emit events for write)
- Perform expensive per-frame calculations in the component body

## Example — Minimal Renderer

```tsx
// packages/viewer/src/components/renderers/my-node/index.tsx
import { useRegistry } from '@pascal-app/core'
import { useNodeEvents } from '../../hooks/use-node-events'
import { useScene } from '@pascal-app/core'

export function MyNodeRenderer({ node }: { node: MyNode }) {
  const ref = useRef<Mesh>(null!)
  useRegistry(node.id, 'my-node', ref)   // 3 args: id, type, ref — no return value
  const events = useNodeEvents(node, 'my-node')

  return (
    <mesh ref={ref} {...events}>
      <boxGeometry args={[node.width, node.height, node.depth]} />
      <meshStandardMaterial color={node.color} />
    </mesh>
  )
}
```

## Adding a New Node Type

1. Create `packages/viewer/src/components/renderers/<type>/index.tsx`
2. Add a case to `NodeRenderer` in `node-renderer.tsx`
3. Add the corresponding system in `packages/core/src/systems/` if the node needs derived geometry
4. Export from `packages/viewer/src/index.ts` if needed externally

## Performance Notes

- Use `useMemo` for geometry that depends on node properties — avoid recreating on every render.
- For complex cutout or boolean geometry, delegate to a System (e.g. `WallCutout`).
- Register one mesh per node ID; if a renderer spawns multiple meshes, use a group ref or pick the primary one for registry.


================================================
FILE: .cursor/rules/scene-registry.mdc
================================================
---
description: Scene registry pattern — mapping node IDs to live THREE.Object3D instances
globs: packages/core/src/hooks/scene-registry/**,packages/viewer/**
alwaysApply: false
---

# Scene Registry

The scene registry is a global, mutable map that links node IDs to their live `THREE.Object3D` instances. It avoids tree traversal and lets systems and selection managers do O(1) lookups.

**Source**: @packages/core/src/hooks/scene-registry/scene-registry.ts

## Structure

```ts
export const sceneRegistry = {
  nodes: new Map<string, THREE.Object3D>(),   // id → Object3D
  byType: {
    wall: new Set<string>(),
    slab: new Set<string>(),
    item: new Set<string>(),
    // … one Set per node type
  },
}
```

`nodes` is the primary lookup. `byType` lets systems iterate all objects of one type without scanning the whole map.

## Registering in a Renderer

Every renderer must call `useRegistry` with a `ref` to its root mesh or group. Registration is synchronous (`useLayoutEffect`) so it's available before the first paint.

```tsx
import { useRegistry } from '@pascal-app/core'

export function WallRenderer({ node }: { node: WallNode }) {
  const ref = useRef<Mesh>(null!)
  useRegistry(node.id, 'wall', ref)   // ← required in every renderer

  return <mesh ref={ref} … />
}
```

The hook handles both registration on mount and cleanup on unmount automatically.

## Looking Up Objects

Anywhere outside the renderer — in systems, selection managers, export logic:

```ts
// Single lookup
const obj = sceneRegistry.nodes.get(nodeId)
if (obj) { /* use obj */ }

// Iterate all walls
for (const id of sceneRegistry.byType.wall) {
  const obj = sceneRegistry.nodes.get(id)
}
```

## Rules

- **One registration per node ID.** If a renderer spawns multiple meshes, register the outermost group (the one that represents the node).
- **Never hold a stale reference.** Always read from `sceneRegistry.nodes.get(id)` at the time you need it — don't cache the result across frames.
- **Don't mutate the registry manually.** Only `useRegistry` should add/remove entries. Systems and selection managers are read-only consumers.
- **Core systems must not use the registry.** They work with plain node data. Only viewer systems and selection managers may do Three.js object lookups.

## Outliner Sync

The `outliner` in `useViewer` holds live `Object3D[]` arrays used by the post-processing outline pass. Selection managers sync them imperatively for performance (array mutation rather than new allocations):

```ts
outliner.selectedObjects.length = 0
for (const id of selection.selectedIds) {
  const obj = sceneRegistry.nodes.get(id)
  if (obj) outliner.selectedObjects.push(obj)
}
```

See @packages/viewer/src/components/viewer/selection-manager.tsx for the full sync pattern.


================================================
FILE: .cursor/rules/selection-managers.mdc
================================================
---
description: Selection managers — two-layer architecture for viewer and editor selection
globs: packages/viewer/src/components/viewer/selection-manager.tsx,apps/editor/components/editor/selection-manager.tsx
alwaysApply: false
---

# Selection Managers

There are two selection managers. They are separate components, not the same component configured differently.

| Component | Location | Knows about |
|---|---|---|
| `SelectionManager` | `packages/viewer/src/components/viewer/selection-manager.tsx` | Viewer state only |
| `SelectionManager` (editor) | `apps/editor/components/editor/selection-manager.tsx` | Phase, mode, tool state |

The viewer's manager is the default. The editor mounts its own manager as a child of `<Viewer>`, overriding the default behaviour via the viewer-isolation pattern.

---

## How Selection Works

**Event flow:**

```
useNodeEvents(node, type) on a renderer mesh
  → emitter.emit('wall:click', NodeEvent)
  → SelectionManager listens via emitter.on(…)
  → calls useViewer.setSelection(…)
  → outliner sync re-runs → Three.js outline updates
```

`useNodeEvents` returns R3F pointer handlers. Spread them onto the mesh:

```tsx
const events = useNodeEvents(node, 'wall')
return <mesh ref={ref} {...events} />
```

Events are suppressed during camera drag (`useViewer.getState().cameraDragging`).

---

## Viewer Selection Manager

Hierarchical path: **Building → Level → Zone → Elements**

At each level, only the next tier is selectable. Clicking outside deselects. The path is stored in `useViewer`:

```ts
type SelectionPath = {
  buildingId: string | null
  levelId: string | null
  zoneId: string | null
  selectedIds: string[]   // walls, items, slabs, etc.
}
```

`setSelection` has a hierarchy guard: setting `levelId` without `buildingId` resets children. Use `resetSelection()` to clear everything.

Multi-select: `Ctrl/Meta + click` toggles an ID in `selectedIds`. Regular click replaces it.

---

## Editor Selection Manager

Extends selection with phase awareness from `useEditor`. The viewer's `SelectionManager` is **not** mounted in the editor; this one takes its place (injected as a child of `<Viewer>`).

```
phase: 'site'      → selectable: buildings
phase: 'structure' → selectable: walls, zones, slabs, ceilings, roofs, doors, windows
  structureLayer: 'zones'    → only zones
  structureLayer: 'elements' → all structure types
phase: 'furnish'   → selectable: furniture items only
```

Clicking a node of a different phase auto-switches the phase. Double-click drills into a context level.

---

## Rules

- **Never add selection logic to renderers.** Renderers spread `useNodeEvents` events and stop there. All selection decisions live in the selection manager.
- **Never add editor phase logic to the viewer's SelectionManager.** Phase, mode, and tool awareness belong exclusively in the editor's selection manager.
- **`useViewer` is the single source of truth for selection state.** Both managers read and write through `setSelection` / `resetSelection`. Nothing else should mutate `selection` directly.
- **Outliner arrays are mutated in-place** (not replaced) for performance. Don't assign new arrays to `outliner.selectedObjects` or `outliner.hoveredObjects`.
- **Hover is a separate scalar** (`hoveredId: string | null`), not part of `selectedIds`. Update it via `setHoveredId`.

---

## Adding Selectability to a New Node Type

1. Add the type to `SelectableNodeType` in the viewer store / selection manager.
2. Make sure its renderer calls `useNodeEvents(node, type)` and spreads the handlers.
3. Add a case to whichever selection strategy needs it (viewer hierarchy level or editor phase).
4. Ensure `useRegistry` is called in the renderer so the outliner can highlight it.


================================================
FILE: .cursor/rules/spatial-queries.mdc
================================================
---
description: Placement validation for tools — canPlaceOnFloor, canPlaceOnWall, canPlaceOnCeiling
globs: apps/editor/components/tools/**
alwaysApply: false
---

# Spatial Queries

`useSpatialQuery()` validates whether an item can be placed at a given position without overlapping existing items. Every placement tool must call it before committing a node to the scene.

**Source**: @packages/core/src/hooks/spatial-grid/use-spatial-query.ts

## Hook

```ts
const { canPlaceOnFloor, canPlaceOnWall, canPlaceOnCeiling } = useSpatialQuery()
```

All three methods return `{ valid: boolean; conflictIds: string[] }`.
`canPlaceOnWall` additionally returns `adjustedY: number` (snapped height).

---

## canPlaceOnFloor

```ts
canPlaceOnFloor(
  levelId: string,
  position: [number, number, number],
  dimensions: [number, number, number],   // scaled width/height/depth
  rotation: [number, number, number],
  ignoreIds?: string[],                   // pass [draftItem.id] to exclude self
): { valid: boolean; conflictIds: string[] }
```

**Usage in a tool:**
```ts
const pos: [number, number, number] = [x, 0, z]
const { valid } = canPlaceOnFloor(levelId, pos, getScaledDimensions(item), item.rotation, [item.id])
if (valid) createNode(item, levelId)
```

---

## canPlaceOnWall

```ts
canPlaceOnWall(
  levelId: string,
  wallId: string,
  localX: number,          // distance along wall from start
  localY: number,          // height from floor
  dimensions: [number, number, number],
  attachType: 'wall' | 'wall-side',  // 'wall' needs clearance both sides; 'wall-side' only one
  side?: 'front' | 'back',
  ignoreIds?: string[],
): { valid: boolean; conflictIds: string[]; adjustedY: number }
```

`adjustedY` contains the snapped Y so items sit flush on the slab — always use it instead of the raw `localY`:

```ts
const { valid, adjustedY } = canPlaceOnWall(levelId, wallId, x, y, dims, 'wall', undefined, [item.id])
if (valid) updateNode(item.id, { wallT: x, wallY: adjustedY })
```

---

## canPlaceOnCeiling

```ts
canPlaceOnCeiling(
  ceilingId: string,
  position: [number, number, number],
  dimensions: [number, number, number],
  rotation: [number, number, number],
  ignoreIds?: string[],
): { valid: boolean; conflictIds: string[] }
```

---

## Slab Elevation

When items rest on a slab (not flat ground), use these to get the correct Y:

```ts
import { spatialGridManager } from '@pascal-app/core'

// Y at a single point
const y = spatialGridManager.getSlabElevationAt(levelId, x, z)

// Y considering the item's full footprint (highest slab point under item)
const y = spatialGridManager.getSlabElevationForItem(levelId, position, dimensions, rotation)
```

---

## Rules

- **Always pass `[item.id]` in `ignoreIds`** when validating a draft item that already exists in the scene — otherwise it collides with itself.
- **Use `adjustedY` from `canPlaceOnWall`** — don't use the raw cursor Y for wall-mounted items.
- **Use `getScaledDimensions(item)`** (@packages/core/src/schema/nodes/item.ts) to account for item scale, not the raw `asset.dimensions`.
- Validate on every pointer move for live feedback (highlight ghost red/green). Only `createNode` / `updateNode` on pointer up or click.

See @apps/editor/components/tools/item/use-placement-coordinator.tsx for a full implementation.


================================================
FILE: .cursor/rules/systems.mdc
================================================
---
description: Core and viewer systems architecture
globs: packages/core/src/systems/**,packages/viewer/src/systems/**
alwaysApply: false
---

# Systems

Systems own business logic, geometry generation, and constraints. They run in the Three.js frame loop and are never rendered directly.

## Two Kinds of Systems

### Core Systems — `packages/core/src/systems/`

Pure logic: no rendering, no Three.js objects. They read nodes from `useScene`, compute derived values (geometry, constraints), and write results back.

| System | Responsibility |
|---|---|
| `WallSystem` | Wall mitering, corner joints |
| `SlabSystem` | Polygon-based floor/roof generation |
| `CeilingSystem` | Polygon-based ceiling generation |
| `RoofSystem` | Pitched roof shape |
| `DoorSystem` | Placement constraints on walls |
| `WindowSystem` | Placement constraints on walls |
| `ItemSystem` | Item transforms, collision |

### Viewer Systems — `packages/viewer/src/systems/`

Access Three.js objects (via `useRegistry`) and manage rendering side-effects.

| System | Responsibility |
|---|---|
| `LevelSystem` | Stacked / exploded / solo / manual level positions |
| `WallCutout` | Cuts door/window holes in wall geometry |
| `ZoneSystem` | Zone display and label placement |
| `InteractiveSystem` | Item toggles and sliders in the scene |
| `GuideSystem` | Temporary helper geometry |
| `ScanSystem` | Point cloud rendering |

## Pattern

Systems are React components that render nothing (`return null`) and use `useFrame` for per-frame logic.

```tsx
// packages/core/src/systems/my-system.tsx
import { useFrame } from '@react-three/fiber'
import { useScene } from '../store/use-scene'

export function MySystem() {
  const nodes = useScene(s => s.nodes)

  useFrame(() => {
    // compute and write back derived state
  })

  return null
}
```

Core and viewer systems are mounted inside `<Viewer>` alongside renderers. See @packages/viewer/src/components/viewer/index.tsx for the mount order.

**Systems are a customization point.** Any consumer of `<Viewer>` — the editor app, an embed, a read-only preview — can inject its own systems as children. This is how editor-specific behaviour (space detection, tool feedback) is added without touching the viewer package.

## Rules

- **Core systems must not import Three.js** — they work with plain data.
- **Viewer systems must not contain business logic** — delegate to core if the rule is domain-level.
- **Never duplicate logic** between a system and a renderer — if the renderer needs it, the system should compute and store it, and the renderer reads the result.
- Systems should be **idempotent**: given the same nodes, they produce the same output.
- Mark nodes as `dirty` in the scene store to signal that a system should re-run. Avoid running expensive logic every frame without a dirty check.

## Adding a New System

1. Decide the scope:
   - **Domain logic** → `packages/core/src/systems/`
   - **Viewer rendering side-effect** → `packages/viewer/src/systems/` — mount in `packages/viewer/src/components/viewer/index.tsx`
   - **Editor-specific or integration-specific** → keep it in the consuming app (e.g. `apps/editor/components/systems/`) and inject it as a child of `<Viewer>`

2. Create `<name>-system.tsx` in the appropriate directory.

3. Mount it in the right place:
   - Viewer-internal systems go in `packages/viewer/src/components/viewer/index.tsx`
   - App-specific systems are injected as children from outside:
     ```tsx
     // apps/editor — editor injects its own systems without modifying the viewer
     <Viewer>
       <MyEditorSystem />
       <ToolManager />
     </Viewer>
     ```

4. **Mount order matters.** Most viewer systems run *after* renderers in the JSX tree — they consume `sceneRegistry` data that renderers populate on mount. Only place a system before renderers if it explicitly does not read the registry.


================================================
FILE: .cursor/rules/tools.mdc
================================================
---
description: Editor tools structure in apps/editor
globs: apps/editor/components/tools/**
alwaysApply: false
---

# Tools

Tools are React components that capture user input (pointer, keyboard) and translate it into `useScene` mutations. They live exclusively in `apps/editor/components/tools/`.

## Lifecycle

`ToolManager` reads `useEditor` (phase + mode + tool) and mounts the active tool component. When the tool changes, the old component unmounts, cleaning up any transient state.

See @apps/editor/components/tools/tool-manager.tsx.

## Tool Categories by Phase

**Site**
- `site-boundary-editor` — draw/edit property boundary polygon

**Structure**
- `wall-tool` — draw walls segment by segment
- `slab-tool` + `slab-boundary-editor` + `slab-hole-editor`
- `ceiling-tool` + `ceiling-boundary-editor` + `ceiling-hole-editor`
- `roof-tool`
- `door-tool` + `door-move-tool`
- `window-tool` + `window-move-tool`
- `item-tool` + `item-move-tool`
- `zone-tool` + `zone-boundary-editor`

**Furnish**
- `item-tool` — place furniture

**Shared utilities**
- `polygon-editor` — reusable boundary/hole editing logic
- `cursor-sphere` — 3D cursor visualisation

## Pattern

```tsx
// apps/editor/components/tools/my-tool/index.tsx
import { useScene } from '@pascal-app/core'
import { useEditor } from '../../store/use-editor'

export function MyTool() {
  const createNode = useScene(s => s.createNode)
  const setTool = useEditor(s => s.setTool)

  // Pointer handlers mutate the scene store directly.
  // No local geometry — use a renderer for any preview mesh.

  return (
    <mesh onPointerDown={handleDown} onPointerMove={handleMove}>
      {/* ghost / preview geometry only */}
    </mesh>
  )
}
```

## Rules

- **Tools only mutate `useScene`** — they do not call Three.js APIs directly.
- **No business logic in tools** — delegate geometry/constraint rules to core systems.
- **Preview geometry is local** — transient meshes shown while a tool is active live in the tool component, not in the scene store.
- **Clean up on unmount** — remove any pending/incomplete nodes when the tool unmounts.
- **Tools must not import from `@pascal-app/viewer`** — use the scene store and core hooks only.
- Each tool should handle a single, well-scoped interaction. Split complex tools (e.g. "draw + move") into separate components selected by `useEditor`.

## Adding a New Tool

1. Create `apps/editor/components/tools/<name>/index.tsx`.
2. Register the tool in `ToolManager` under the correct phase and mode.
3. Add the tool identifier to the `useEditor` tool union type.
4. If the tool requires new node types, add schema + renderer + system first.


================================================
FILE: .cursor/rules/viewer-isolation.mdc
================================================
---
description: Viewer must be editor-agnostic — controlled from outside via props and children
globs: packages/viewer/**
alwaysApply: false
---

# Viewer Isolation

`@pascal-app/viewer` is a standalone 3D canvas library. It must never know about editor-specific features, UI state, or tools. This keeps it usable in the read-only `/viewer/[id]` route and in any future embedding context.

## The Rule

> The viewer is controlled from outside. It exposes control points (props, callbacks, children). It never reaches into `apps/editor`.

## Forbidden in `packages/viewer`

```ts
// ❌ Never import from the editor app
import { useEditor } from '@/store/use-editor'
import { ToolManager } from '@/components/tools/tool-manager'

// ❌ Never reference editor-specific concepts
if (isEditorMode) { … }
```

## Correct Pattern — Pass Control from Outside

The editor mounts the viewer and passes what it needs:

```tsx
// apps/editor/components/editor-canvas.tsx  ✅
import { Viewer } from '@pascal-app/viewer'
import { ToolManager } from '../tools/tool-manager'
import { useEditor } from '../../store/use-editor'

export function EditorCanvas() {
  const { selection } = useViewer()

  return (
    <Viewer
      theme="light"
      onSelect={(id) => useViewer.getState().setSelection(id)}
      onExport={handleExport}
    >
      {/* Editor injects tools as children — viewer renders them inside the canvas */}
      <ToolManager />
    </Viewer>
  )
}
```

The viewer accepts `children` and renders them inside the R3F canvas. This is the extension point for tools, overlays, and editor-specific systems.

## Viewer's Own State (`useViewer`)

The viewer store contains **only presentation state**:

- `selection` — which nodes are highlighted
- `cameraMode` — perspective / orthographic
- `levelMode` — stacked / exploded / solo / manual
- `wallMode` — up / cutaway / down
- `theme` — light / dark
- Display toggles: `showScans`, `showGuides`, `showGrid`

If a piece of state is only meaningful inside the editor (e.g. active tool, phase, edit mode) — it belongs in `useEditor`, not `useViewer`.

## Nested Viewer for Editor-Specific Features

When an editor feature needs to live "inside" the canvas but must not pollute the viewer package, inject it as a child:

```tsx
// ✅ Editor-specific overlay injected as child
<Viewer>
  <SelectionBoxOverlay />   {/* editor only */}
  <SnapIndicator />         {/* editor only */}
  <ToolManager />           {/* editor only */}
</Viewer>
```

This pattern lets the viewer stay ignorant of these components while they still have access to the R3F context.

## Checklist Before Adding Code to `packages/viewer`

- [ ] Does this feature make sense in the read-only viewer route?
- [ ] Does it reference `useEditor`, tool state, or phase/mode?
- [ ] Could it be passed in as a prop or child instead?

If any answer is "editor-specific", keep it in `apps/editor` and inject it via children or props.


================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.yml
================================================
name: 🐛 Bug Report
description: Report something that isn't working correctly
labels: ["bug", "needs-triage"]
body:
  - type: markdown
    attributes:
      value: |
        Thanks for taking the time to report a bug! Please fill out the sections below so we can reproduce and fix the issue.

  - type: textarea
    id: description
    attributes:
      label: What happened?
      description: A clear description of the bug.
      placeholder: Describe what went wrong...
    validations:
      required: true

  - type: textarea
    id: steps
    attributes:
      label: Steps to reproduce
      description: How can we reproduce the issue?
      placeholder: |
        1. Go to '...'
        2. Click on '...'
        3. See error
    validations:
      required: true

  - type: textarea
    id: expected
    attributes:
      label: Expected behavior
      description: What did you expect to happen?
    validations:
      required: true

  - type: input
    id: browser
    attributes:
      label: Browser & OS
      description: e.g. Chrome 120, macOS 14
      placeholder: Chrome 120, macOS 14
    validations:
      required: false

  - type: textarea
    id: screenshots
    attributes:
      label: Screenshots or screen recordings
      description: |
        A short screen recording is the fastest way to show us the bug.
        Drag and drop a video file or paste a link — even a quick clip helps a lot.
    validations:
      required: false

  - type: textarea
    id: additional
    attributes:
      label: Additional context
      description: Any other information that might be helpful (console errors, related issues, etc.)
    validations:
      required: false


================================================
FILE: .github/ISSUE_TEMPLATE/config.yml
================================================
blank_issues_enabled: false
contact_links:
  - name: 💬 Questions & Help
    url: https://github.com/pascalorg/editor/discussions/categories/q-a
    about: Ask questions and get help from the community
  - name: 💡 Ideas & Discussion
    url: https://github.com/pascalorg/editor/discussions/categories/ideas
    about: Share ideas and discuss features before opening a formal request


================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.yml
================================================
name: ✨ Feature Request
description: Suggest a new feature or improvement
labels: ["enhancement", "needs-triage"]
body:
  - type: markdown
    attributes:
      value: |
        We love hearing ideas from the community! Please describe what you'd like to see in Pascal Editor.

  - type: textarea
    id: problem
    attributes:
      label: Problem or motivation
      description: What problem does this solve? Why do you want this?
      placeholder: I'm always frustrated when...
    validations:
      required: true

  - type: textarea
    id: solution
    attributes:
      label: Proposed solution
      description: How do you think this should work?
    validations:
      required: false

  - type: textarea
    id: alternatives
    attributes:
      label: Alternatives considered
      description: Any workarounds or alternative approaches you've thought about?
    validations:
      required: false

  - type: textarea
    id: additional
    attributes:
      label: Additional context
      description: Mockups, screenshots, references to other tools, etc.
    validations:
      required: false


================================================
FILE: .github/pull_request_template.md
================================================
## What does this PR do?

<!-- A brief description of the change. Link to a related issue if applicable: Fixes #123 -->

## How to test

<!-- Steps for reviewers to verify this change works -->

1. 
2. 
3. 

## Screenshots / screen recording

<!--
🎥 Screen recordings are strongly encouraged for any visual or interactive change.
Even a quick 15-second clip helps reviewers understand the change far better than screenshots alone.

Drag and drop a video or paste a link here.
-->

## Checklist

- [ ] I've tested this locally with `bun dev`
- [ ] My code follows the existing code style (run `bun check` to verify)
- [ ] I've updated relevant documentation (if applicable)
- [ ] This PR targets the `main` branch


================================================
FILE: .github/workflows/release.yml
================================================
name: Release

on:
  workflow_dispatch:
    inputs:
      package:
        description: "Package to release"
        required: true
        type: choice
        options:
          - core
          - viewer
          - both
      bump:
        description: "Version bump"
        required: true
        type: choice
        options:
          - patch
          - minor
          - major
      dry-run:
        description: "Dry run (no publish)"
        required: false
        type: boolean
        default: false

jobs:
  release:
    runs-on: ubuntu-latest
    environment: npm
    permissions:
      contents: write
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - uses: oven-sh/setup-bun@v2

      - uses: actions/setup-node@v4
        with:
          node-version: 22
          registry-url: "https://registry.npmjs.org"

      - name: Install dependencies
        run: bun install --frozen-lockfile

      - name: Configure git
        run: |
          git config user.name "github-actions[bot]"
          git config user.email "github-actions[bot]@users.noreply.github.com"

      - name: Bump & publish core
        if: inputs.package == 'core' || inputs.package == 'both'
        working-directory: packages/core
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
        run: |
          BUMP=${{ inputs.bump }}
          VERSION=$(jq -r '.version' package.json)
          IFS='.' read -r MAJ MIN PAT <<< "$VERSION"
          if [ "$BUMP" = "major" ]; then MAJ=$((MAJ+1)); MIN=0; PAT=0; fi
          if [ "$BUMP" = "minor" ]; then MIN=$((MIN+1)); PAT=0; fi
          if [ "$BUMP" = "patch" ]; then PAT=$((PAT+1)); fi
          VERSION="$MAJ.$MIN.$PAT"
          jq --arg v "$VERSION" '.version = $v' package.json > tmp.json && mv tmp.json package.json
          echo "CORE_VERSION=$VERSION" >> $GITHUB_ENV

          bun run build

          if [ "${{ inputs.dry-run }}" = "true" ]; then
            echo "🏜️ Dry run — would publish @pascal-app/core@$VERSION"
            npm publish --dry-run --access public
          else
            npm publish --access public
            echo "📦 Published @pascal-app/core@$VERSION"
          fi

      - name: Bump & publish viewer
        if: inputs.package == 'viewer' || inputs.package == 'both'
        working-directory: packages/viewer
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
        run: |
          BUMP=${{ inputs.bump }}
          VERSION=$(jq -r '.version' package.json)
          IFS='.' read -r MAJ MIN PAT <<< "$VERSION"
          if [ "$BUMP" = "major" ]; then MAJ=$((MAJ+1)); MIN=0; PAT=0; fi
          if [ "$BUMP" = "minor" ]; then MIN=$((MIN+1)); PAT=0; fi
          if [ "$BUMP" = "patch" ]; then PAT=$((PAT+1)); fi
          VERSION="$MAJ.$MIN.$PAT"
          jq --arg v "$VERSION" '.version = $v' package.json > tmp.json && mv tmp.json package.json
          echo "VIEWER_VERSION=$VERSION" >> $GITHUB_ENV

          bun run build

          if [ "${{ inputs.dry-run }}" = "true" ]; then
            echo "🏜️ Dry run — would publish @pascal-app/viewer@$VERSION"
            npm publish --dry-run --access public
          else
            npm publish --access public
            echo "📦 Published @pascal-app/viewer@$VERSION"
          fi

      - name: Commit version bumps & tag
        if: inputs.dry-run == false
        run: |
          git add -A
          PKGS=""
          TAGS=""

          if [ -n "$CORE_VERSION" ]; then
            PKGS="$PKGS @pascal-app/core@$CORE_VERSION"
            TAGS="$TAGS @pascal-app/core@$CORE_VERSION"
          fi
          if [ -n "$VIEWER_VERSION" ]; then
            PKGS="$PKGS @pascal-app/viewer@$VIEWER_VERSION"
            TAGS="$TAGS @pascal-app/viewer@$VIEWER_VERSION"
          fi

          git commit -m "release:${PKGS}"

          for TAG in $TAGS; do
            git tag "$TAG"
          done

          git push --follow-tags


================================================
FILE: .gitignore
================================================
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# Dependencies
node_modules
package-lock.json
.pnp
.pnp.js

# Local env files
.env
.env.local
.env.development.local
.env.test.local
.env.production.local

# Testing
coverage

# Turbo
.turbo

# Supabase local state
supabase/.branches/
supabase/.temp/

# Vercel
.vercel

# Build Outputs
.next/
out/
build
dist
*.tsbuildinfo


# Debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Misc
.DS_Store
*.pem
/.playwright-mcp
og-test
.env*.local


================================================
FILE: .npmrc
================================================


================================================
FILE: .vscode/settings.json
================================================
{
  "typescript.tsdk": "node_modules/typescript/lib",
  "biome.configPath": "./biome.jsonc",
  "editor.defaultFormatter": "biomejs.biome",
  "editor.formatOnSave": false,
  "editor.formatOnPaste": true,
  "emmet.showExpandedAbbreviation": "never",
  "editor.codeActionsOnSave": {
    "source.fixAll.biome": "always",
    "quickfix.biome": "always",
    "source.organizeImports.biome": "always",
    "source.organizeImports": "never"
  },
  "[typescript]": {
    "editor.defaultFormatter": "biomejs.biome"
  },
  "[json]": {
    "editor.defaultFormatter": "biomejs.biome"
  },
  "[javascript]": {
    "editor.defaultFormatter": "biomejs.biome"
  },
  "[jsonc]": {
    "editor.defaultFormatter": "biomejs.biome"
  },
  "[typescriptreact]": {
    "editor.defaultFormatter": "biomejs.biome",
    "editor.formatOnPaste": false
  },
  "[javascriptreact]": {
    "editor.defaultFormatter": "biomejs.biome"
  },
  "[css]": {
    "editor.defaultFormatter": "biomejs.biome"
  },
  "css.lint.unknownAtRules": "ignore",
  "[graphql]": {
    "editor.defaultFormatter": "biomejs.biome"
  },
  "evenBetterToml.formatter.allowedBlankLines": 4
}


================================================
FILE: CONTRIBUTING.md
================================================
# Contributing to Pascal Editor

Thanks for your interest in contributing! We welcome all kinds of contributions — bug fixes, new features, documentation, and ideas.

## Getting started

### Prerequisites

- [Bun](https://bun.sh/) 1.3+ (or Node.js 18+)

### Setup

```bash
git clone https://github.com/pascalorg/editor.git
cd editor
bun install
bun dev
```

The editor will be running at **http://localhost:3000**. That's it!

### Optional

Copy `.env.example` to `.env` and add a Google Maps API key if you want address search functionality. The editor works fully without it.

## Making changes

### Code style

We use [Biome](https://biomejs.dev/) for linting and formatting. Before submitting a PR:

```bash
bun check        # Check for issues
bun check:fix    # Auto-fix issues
```

### Project structure

| Package | What it does |
|---------|-------------|
| `packages/core` | Scene schema, state management, systems — no UI |
| `packages/viewer` | 3D rendering with React Three Fiber |
| `apps/editor` | The full editor app (Next.js) |

A key rule: **`packages/viewer` must never import from `apps/editor`**. The viewer is a standalone component; editor-specific behavior is injected via props/children.

## Submitting a PR

1. **Fork the repo** and create a branch from `main`
2. **Make your changes** and test locally with `bun dev`
3. **Run `bun check`** to make sure linting passes
4. **Open a PR** with a clear description of what changed and why
5. **Link related issues** if applicable (e.g., "Fixes #42")

### PR tips

- Keep PRs focused — one feature or fix per PR
- Include screenshots or recordings for visual changes
- If you're unsure about an approach, open an issue or discussion first

## Reporting bugs

Use the [Bug Report](https://github.com/pascalorg/editor/issues/new?template=bug_report.yml) template. Include steps to reproduce — this helps us fix things faster.

## Suggesting features

Use the [Feature Request](https://github.com/pascalorg/editor/issues/new?template=feature_request.yml) template, or start a [Discussion](https://github.com/pascalorg/editor/discussions) if you want to brainstorm first.

## Questions?

Head to [Discussions](https://github.com/pascalorg/editor/discussions) — we're happy to help!


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2026 Pascal Group Inc.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================
# Pascal Editor

A 3D building editor built with React Three Fiber and WebGPU.

[![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
[![npm @pascal-app/core](https://img.shields.io/npm/v/@pascal-app/core?label=%40pascal-app%2Fcore)](https://www.npmjs.com/package/@pascal-app/core)
[![npm @pascal-app/viewer](https://img.shields.io/npm/v/@pascal-app/viewer?label=%40pascal-app%2Fviewer)](https://www.npmjs.com/package/@pascal-app/viewer)
[![Discord](https://img.shields.io/badge/Discord-Join%20Server-5865F2?logo=discord&logoColor=white)](https://discord.gg/SaBRA9t2)
[![X (Twitter)](https://img.shields.io/badge/follow-%40pascal__app-black?logo=x&logoColor=white)](https://x.com/pascal_app)

https://github.com/user-attachments/assets/8b50e7cf-cebe-4579-9cf3-8786b35f7b6b



## Repository Architecture

This is a Turborepo monorepo with three main packages:

```
editor-v2/
├── apps/
│   └── editor/          # Next.js application
├── packages/
│   ├── core/            # Schema definitions, state management, systems
│   └── viewer/          # 3D rendering components
```

### Separation of Concerns

| Package | Responsibility |
|---------|---------------|
| **@pascal-app/core** | Node schemas, scene state (Zustand), systems (geometry generation), spatial queries, event bus |
| **@pascal-app/viewer** | 3D rendering via React Three Fiber, default camera/controls, post-processing |
| **apps/editor** | UI components, tools, custom behaviors, editor-specific systems |

The **viewer** renders the scene with sensible defaults. The **editor** extends it with interactive tools, selection management, and editing capabilities.

### Stores

Each package has its own Zustand store for managing state:

| Store | Package | Responsibility |
|-------|---------|----------------|
| `useScene` | `@pascal-app/core` | Scene data: nodes, root IDs, dirty nodes, CRUD operations. Persisted to IndexedDB with undo/redo via Zundo. |
| `useViewer` | `@pascal-app/viewer` | Viewer state: current selection (building/level/zone IDs), level display mode (stacked/exploded/solo), camera mode. |
| `useEditor` | `apps/editor` | Editor state: active tool, structure layer visibility, panel states, editor-specific preferences. |

**Access patterns:**

```typescript
// Subscribe to state changes (React component)
const nodes = useScene((state) => state.nodes)
const levelId = useViewer((state) => state.selection.levelId)
const activeTool = useEditor((state) => state.tool)

// Access state outside React (callbacks, systems)
const node = useScene.getState().nodes[id]
useViewer.getState().setSelection({ levelId: 'level_123' })
```

---

## Core Concepts

### Nodes

Nodes are the data primitives that describe the 3D scene. All nodes extend `BaseNode`:

```typescript
BaseNode {
  id: string              // Auto-generated with type prefix (e.g., "wall_abc123")
  type: string            // Discriminator for type-safe handling
  parentId: string | null // Parent node reference
  visible: boolean
  camera?: Camera         // Optional saved camera position
  metadata?: JSON         // Arbitrary metadata (e.g., { isTransient: true })
}
```

**Node Hierarchy:**

```
Site
└── Building
    └── Level
        ├── Wall → Item (doors, windows)
        ├── Slab
        ├── Ceiling → Item (lights)
        ├── Roof
        ├── Zone
        ├── Scan (3D reference)
        └── Guide (2D reference)
```

Nodes are stored in a **flat dictionary** (`Record<id, Node>`), not a nested tree. Parent-child relationships are defined via `parentId` and `children` arrays.

---

### Scene State (Zustand Store)

The scene is managed by a Zustand store in `@pascal-app/core`:

```typescript
useScene.getState() = {
  nodes: Record<id, AnyNode>,  // All nodes
  rootNodeIds: string[],       // Top-level nodes (sites)
  dirtyNodes: Set<string>,     // Nodes pending system updates

  createNode(node, parentId),
  updateNode(id, updates),
  deleteNode(id),
}
```

**Middleware:**
- **Persist** - Saves to IndexedDB (excludes transient nodes)
- **Temporal** (Zundo) - Undo/redo with 50-step history

---

### Scene Registry

The registry maps node IDs to their Three.js objects for fast lookup:

```typescript
sceneRegistry = {
  nodes: Map<id, Object3D>,    // ID → 3D object
  byType: {
    wall: Set<id>,
    item: Set<id>,
    zone: Set<id>,
    // ...
  }
}
```

Renderers register their refs using the `useRegistry` hook:

```tsx
const ref = useRef<Mesh>(null!)
useRegistry(node.id, 'wall', ref)
```

This allows systems to access 3D objects directly without traversing the scene graph.

---

### Node Renderers

Renderers are React components that create Three.js objects for each node type:

```
SceneRenderer
└── NodeRenderer (dispatches by type)
    ├── BuildingRenderer
    ├── LevelRenderer
    ├── WallRenderer
    ├── SlabRenderer
    ├── ZoneRenderer
    ├── ItemRenderer
    └── ...
```

**Pattern:**
1. Renderer creates a placeholder mesh/group
2. Registers it with `useRegistry`
3. Systems update geometry based on node data

Example (simplified):
```tsx
const WallRenderer = ({ node }) => {
  const ref = useRef<Mesh>(null!)
  useRegistry(node.id, 'wall', ref)

  return (
    <mesh ref={ref}>
      <boxGeometry args={[0, 0, 0]} />  {/* Replaced by WallSystem */}
      <meshStandardMaterial />
      {node.children.map(id => <NodeRenderer key={id} nodeId={id} />)}
    </mesh>
  )
}
```

---

### Systems

Systems are React components that run in the render loop (`useFrame`) to update geometry and transforms. They process **dirty nodes** marked by the store.

**Core Systems (in `@pascal-app/core`):**

| System | Responsibility |
|--------|---------------|
| `WallSystem` | Generates wall geometry with mitering and CSG cutouts for doors/windows |
| `SlabSystem` | Generates floor geometry from polygons |
| `CeilingSystem` | Generates ceiling geometry |
| `RoofSystem` | Generates roof geometry |
| `ItemSystem` | Positions items on walls, ceilings, or floors (slab elevation) |

**Viewer Systems (in `@pascal-app/viewer`):**

| System | Responsibility |
|--------|---------------|
| `LevelSystem` | Handles level visibility and vertical positioning (stacked/exploded/solo modes) |
| `ScanSystem` | Controls 3D scan visibility |
| `GuideSystem` | Controls guide image visibility |

**Processing Pattern:**
```typescript
useFrame(() => {
  for (const id of dirtyNodes) {
    const obj = sceneRegistry.nodes.get(id)
    const node = useScene.getState().nodes[id]

    // Update geometry, transforms, etc.
    updateGeometry(obj, node)

    dirtyNodes.delete(id)
  }
})
```

---

### Dirty Nodes

When a node changes, it's marked as **dirty** in `useScene.getState().dirtyNodes`. Systems check this set each frame and only recompute geometry for dirty nodes.

```typescript
// Automatic: createNode, updateNode, deleteNode mark nodes dirty
useScene.getState().updateNode(wallId, { thickness: 0.2 })
// → wallId added to dirtyNodes
// → WallSystem regenerates geometry next frame
// → wallId removed from dirtyNodes
```

**Manual marking:**
```typescript
useScene.getState().dirtyNodes.add(wallId)
```

---

### Event Bus

Inter-component communication uses a typed event emitter (mitt):

```typescript
// Node events
emitter.on('wall:click', (event) => { ... })
emitter.on('item:enter', (event) => { ... })
emitter.on('zone:context-menu', (event) => { ... })

// Grid events (background)
emitter.on('grid:click', (event) => { ... })

// Event payload
NodeEvent {
  node: AnyNode
  position: [x, y, z]
  localPosition: [x, y, z]
  normal?: [x, y, z]
  stopPropagation: () => void
}
```

---

### Spatial Grid Manager

Handles collision detection and placement validation:

```typescript
spatialGridManager.canPlaceOnFloor(levelId, position, dimensions, rotation)
spatialGridManager.canPlaceOnWall(wallId, t, height, dimensions)
spatialGridManager.getSlabElevationAt(levelId, x, z)
```

Used by item placement tools to validate positions and calculate slab elevations.

---

## Editor Architecture

The editor extends the viewer with:

### Tools

Tools are activated via the toolbar and handle user input for specific operations:

- **SelectTool** - Selection and manipulation
- **WallTool** - Draw walls
- **ZoneTool** - Create zones
- **ItemTool** - Place furniture/fixtures
- **SlabTool** - Create floor slabs

### Selection Manager

The editor uses a custom selection manager with hierarchical navigation:

```
Site → Building → Level → Zone → Items
```

Each depth level has its own selection strategy for hover/click behavior.

### Editor-Specific Systems

- `ZoneSystem` - Controls zone visibility based on level mode
- Custom camera controls with node focusing

---

## Data Flow

```
User Action (click, drag)
       ↓
Tool Handler
       ↓
useScene.createNode() / updateNode()
       ↓
Node added/updated in store
Node marked dirty
       ↓
React re-renders NodeRenderer
useRegistry() registers 3D object
       ↓
System detects dirty node (useFrame)
Updates geometry via sceneRegistry
Clears dirty flag
```

---

## Technology Stack

- **React 19** + **Next.js 16**
- **Three.js** (WebGPU renderer)
- **React Three Fiber** + **Drei**
- **Zustand** (state management)
- **Zod** (schema validation)
- **Zundo** (undo/redo)
- **three-bvh-csg** (Boolean geometry operations)
- **Turborepo** (monorepo management)
- **Bun** (package manager)

---

## Getting Started

### Development

Run the development server from the **root directory** to enable hot reload for all packages:

```bash
# Install dependencies
bun install

# Run development server (builds packages + starts editor with watch mode)
bun dev

# This will:
# 1. Build @pascal-app/core and @pascal-app/viewer
# 2. Start watching both packages for changes
# 3. Start the Next.js editor dev server
# Open http://localhost:3000
```

**Important:** Always run `bun dev` from the root directory to ensure the package watchers are running. This enables hot reload when you edit files in `packages/core/src/` or `packages/viewer/src/`.

### Building for Production

```bash
# Build all packages
turbo build

# Build specific package
turbo build --filter=@pascal-app/core
```

### Publishing Packages

```bash
# Build packages
turbo build --filter=@pascal-app/core --filter=@pascal-app/viewer

# Publish to npm
npm publish --workspace=@pascal-app/core --access public
npm publish --workspace=@pascal-app/viewer --access public
```

---

## Key Files

| Path | Description |
|------|-------------|
| `packages/core/src/schema/` | Node type definitions (Zod schemas) |
| `packages/core/src/store/use-scene.ts` | Scene state store |
| `packages/core/src/hooks/scene-registry/` | 3D object registry |
| `packages/core/src/systems/` | Geometry generation systems |
| `packages/viewer/src/components/renderers/` | Node renderers |
| `packages/viewer/src/components/viewer/` | Main Viewer component |
| `apps/editor/components/tools/` | Editor tools |
| `apps/editor/store/` | Editor-specific state |

---

## Contributors

<a href="https://github.com/Aymericr"><img src="https://avatars.githubusercontent.com/u/4444492?v=4" width="60" height="60" alt="Aymeric Rabot" style="border-radius:50%"></a>
<a href="https://github.com/wass08"><img src="https://avatars.githubusercontent.com/u/6551176?v=4" width="60" height="60" alt="Wassim Samad" style="border-radius:50%"></a>

---

<a href="https://trendshift.io/repositories/23831" target="_blank"><img src="https://trendshift.io/api/badge/repositories/23831" alt="pascalorg/editor | Trendshift" width="250" height="55"/></a>


================================================
FILE: SETUP.md
================================================
# Pascal Editor — Setup

## Prerequisites

- [Bun](https://bun.sh/) 1.3+ (or Node.js 18+)

## Quick Start

```bash
bun install
bun dev
```

The editor will be running at **http://localhost:3000**.

## Environment Variables (optional)

Copy `.env.example` to `.env` if you need:

```bash
cp .env.example .env
```

| Variable | Required | Description |
|----------|----------|-------------|
| `NEXT_PUBLIC_GOOGLE_MAPS_API_KEY` | No | Enables address search in the editor |
| `PORT` | No | Dev server port (default: 3000) |

The editor works fully without any environment variables.

## Monorepo Structure

```
├── apps/
│   └── editor/          # Next.js editor application
├── packages/
│   ├── core/            # @pascal-app/core — Scene schema, state, systems
│   ├── viewer/          # @pascal-app/viewer — 3D rendering
│   └── ui/              # Shared UI components
└── tooling/             # Build & release tooling
```

## Scripts

| Command | Description |
|---------|-------------|
| `bun dev` | Start the development server |
| `bun build` | Build all packages |
| `bun check` | Lint and format check (Biome) |
| `bun check:fix` | Auto-fix lint and format issues |
| `bun check-types` | TypeScript type checking |

## Contributing

See [CONTRIBUTING.md](./CONTRIBUTING.md) for guidelines on submitting PRs and reporting issues.


================================================
FILE: apps/editor/.gitignore
================================================
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# env files (can opt-in for commiting if needed)
.env*

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts
.env*.local


================================================
FILE: apps/editor/README.md
================================================
# Pascal Editor

A 3D building editor built with React Three Fiber and WebGPU.

## Repository Architecture

This is a Turborepo monorepo with three main packages:

```
editor-v2/
├── apps/
│   └── editor/          # Next.js application (this package)
├── packages/
│   ├── core/            # Schema definitions, state management, systems
│   └── viewer/          # 3D rendering components
```

### Separation of Concerns

| Package | Responsibility |
|---------|---------------|
| **@pascal-app/core** | Node schemas, scene state (Zustand), systems (geometry generation), spatial queries, event bus |
| **@pascal-app/viewer** | 3D rendering via React Three Fiber, default camera/controls, post-processing |
| **apps/editor** | UI components, tools, custom behaviors, editor-specific systems |

The **viewer** renders the scene with sensible defaults. The **editor** extends it with interactive tools, selection management, and editing capabilities.

### Stores

Each package has its own Zustand store for managing state:

| Store | Package | Responsibility |
|-------|---------|----------------|
| `useScene` | `@pascal-app/core` | Scene data: nodes, root IDs, dirty nodes, CRUD operations. Persisted to IndexedDB with undo/redo via Zundo. |
| `useViewer` | `@pascal-app/viewer` | Viewer state: current selection (building/level/zone IDs), level display mode (stacked/exploded/solo), camera mode. |
| `useEditor` | `apps/editor` | Editor state: active tool, structure layer visibility, panel states, editor-specific preferences. |

**Access patterns:**

```typescript
// Subscribe to state changes (React component)
const nodes = useScene((state) => state.nodes)
const levelId = useViewer((state) => state.selection.levelId)
const activeTool = useEditor((state) => state.tool)

// Access state outside React (callbacks, systems)
const node = useScene.getState().nodes[id]
useViewer.getState().setSelection({ levelId: 'level_123' })
```

---

## Core Concepts

### Nodes

Nodes are the data primitives that describe the 3D scene. All nodes extend `BaseNode`:

```typescript
BaseNode {
  id: string              // Auto-generated with type prefix (e.g., "wall_abc123")
  type: string            // Discriminator for type-safe handling
  parentId: string | null // Parent node reference
  visible: boolean
  camera?: Camera         // Optional saved camera position
  metadata?: JSON         // Arbitrary metadata (e.g., { isTransient: true })
}
```

**Node Hierarchy:**

```
Site
└── Building
    └── Level
        ├── Wall → Item (doors, windows)
        ├── Slab
        ├── Ceiling → Item (lights)
        ├── Roof
        ├── Zone
        ├── Scan (3D reference)
        └── Guide (2D reference)
```

Nodes are stored in a **flat dictionary** (`Record<id, Node>`), not a nested tree. Parent-child relationships are defined via `parentId` and `children` arrays.

---

### Scene State (Zustand Store)

The scene is managed by a Zustand store in `@pascal-app/core`:

```typescript
useScene.getState() = {
  nodes: Record<id, AnyNode>,  // All nodes
  rootNodeIds: string[],       // Top-level nodes (sites)
  dirtyNodes: Set<string>,     // Nodes pending system updates

  createNode(node, parentId),
  updateNode(id, updates),
  deleteNode(id),
}
```

**Middleware:**
- **Persist** - Saves to IndexedDB (excludes transient nodes)
- **Temporal** (Zundo) - Undo/redo with 50-step history

---

### Scene Registry

The registry maps node IDs to their Three.js objects for fast lookup:

```typescript
sceneRegistry = {
  nodes: Map<id, Object3D>,    // ID → 3D object
  byType: {
    wall: Set<id>,
    item: Set<id>,
    zone: Set<id>,
    // ...
  }
}
```

Renderers register their refs using the `useRegistry` hook:

```tsx
const ref = useRef<Mesh>(null!)
useRegistry(node.id, 'wall', ref)
```

This allows systems to access 3D objects directly without traversing the scene graph.

---

### Node Renderers

Renderers are React components that create Three.js objects for each node type:

```
SceneRenderer
└── NodeRenderer (dispatches by type)
    ├── BuildingRenderer
    ├── LevelRenderer
    ├── WallRenderer
    ├── SlabRenderer
    ├── ZoneRenderer
    ├── ItemRenderer
    └── ...
```

**Pattern:**
1. Renderer creates a placeholder mesh/group
2. Registers it with `useRegistry`
3. Systems update geometry based on node data

Example (simplified):
```tsx
const WallRenderer = ({ node }) => {
  const ref = useRef<Mesh>(null!)
  useRegistry(node.id, 'wall', ref)

  return (
    <mesh ref={ref}>
      <boxGeometry args={[0, 0, 0]} />  {/* Replaced by WallSystem */}
      <meshStandardMaterial />
      {node.children.map(id => <NodeRenderer key={id} nodeId={id} />)}
    </mesh>
  )
}
```

---

### Systems

Systems are React components that run in the render loop (`useFrame`) to update geometry and transforms. They process **dirty nodes** marked by the store.

**Core Systems (in `@pascal-app/core`):**

| System | Responsibility |
|--------|---------------|
| `WallSystem` | Generates wall geometry with mitering and CSG cutouts for doors/windows |
| `SlabSystem` | Generates floor geometry from polygons |
| `CeilingSystem` | Generates ceiling geometry |
| `RoofSystem` | Generates roof geometry |
| `ItemSystem` | Positions items on walls, ceilings, or floors (slab elevation) |

**Viewer Systems (in `@pascal-app/viewer`):**

| System | Responsibility |
|--------|---------------|
| `LevelSystem` | Handles level visibility and vertical positioning (stacked/exploded/solo modes) |
| `ScanSystem` | Controls 3D scan visibility |
| `GuideSystem` | Controls guide image visibility |

**Processing Pattern:**
```typescript
useFrame(() => {
  for (const id of dirtyNodes) {
    const obj = sceneRegistry.nodes.get(id)
    const node = useScene.getState().nodes[id]

    // Update geometry, transforms, etc.
    updateGeometry(obj, node)

    dirtyNodes.delete(id)
  }
})
```

---

### Dirty Nodes

When a node changes, it's marked as **dirty** in `useScene.getState().dirtyNodes`. Systems check this set each frame and only recompute geometry for dirty nodes.

```typescript
// Automatic: createNode, updateNode, deleteNode mark nodes dirty
useScene.getState().updateNode(wallId, { thickness: 0.2 })
// → wallId added to dirtyNodes
// → WallSystem regenerates geometry next frame
// → wallId removed from dirtyNodes
```

**Manual marking:**
```typescript
useScene.getState().dirtyNodes.add(wallId)
```

---

### Event Bus

Inter-component communication uses a typed event emitter (mitt):

```typescript
// Node events
emitter.on('wall:click', (event) => { ... })
emitter.on('item:enter', (event) => { ... })
emitter.on('zone:context-menu', (event) => { ... })

// Grid events (background)
emitter.on('grid:click', (event) => { ... })

// Event payload
NodeEvent {
  node: AnyNode
  position: [x, y, z]
  localPosition: [x, y, z]
  normal?: [x, y, z]
  stopPropagation: () => void
}
```

---

### Spatial Grid Manager

Handles collision detection and placement validation:

```typescript
spatialGridManager.canPlaceOnFloor(levelId, position, dimensions, rotation)
spatialGridManager.canPlaceOnWall(wallId, t, height, dimensions)
spatialGridManager.getSlabElevationAt(levelId, x, z)
```

Used by item placement tools to validate positions and calculate slab elevations.

---

## Editor Architecture

The editor extends the viewer with:

### Tools

Tools are activated via the toolbar and handle user input for specific operations:

- **SelectTool** - Selection and manipulation
- **WallTool** - Draw walls
- **ZoneTool** - Create zones
- **ItemTool** - Place furniture/fixtures
- **SlabTool** - Create floor slabs

### Selection Manager

The editor uses a custom selection manager with hierarchical navigation:

```
Site → Building → Level → Zone → Items
```

Each depth level has its own selection strategy for hover/click behavior.

### Editor-Specific Systems

- `ZoneSystem` - Controls zone visibility based on level mode
- Custom camera controls with node focusing

---

## Data Flow

```
User Action (click, drag)
       ↓
Tool Handler
       ↓
useScene.createNode() / updateNode()
       ↓
Node added/updated in store
Node marked dirty
       ↓
React re-renders NodeRenderer
useRegistry() registers 3D object
       ↓
System detects dirty node (useFrame)
Updates geometry via sceneRegistry
Clears dirty flag
```

---

## Technology Stack

- **React 19** + **Next.js 15**
- **Three.js** (WebGPU renderer)
- **React Three Fiber** + **Drei**
- **Zustand** (state management)
- **Zod** (schema validation)
- **Zundo** (undo/redo)
- **three-bvh-csg** (Boolean geometry operations)

---

## Getting Started

```bash
# Install dependencies
pnpm install

# Run development server
pnpm dev

# Open http://localhost:3000
```

---

## Key Files

| Path | Description |
|------|-------------|
| `packages/core/src/schema/` | Node type definitions (Zod schemas) |
| `packages/core/src/hooks/use-scene.ts` | Scene state store |
| `packages/core/src/hooks/scene-registry/` | 3D object registry |
| `packages/core/src/systems/` | Geometry generation systems |
| `packages/viewer/src/components/renderers/` | Node renderers |
| `packages/viewer/src/components/viewer/` | Main Viewer component |
| `apps/editor/components/tools/` | Editor tools |
| `apps/editor/store/` | Editor-specific state |


================================================
FILE: apps/editor/app/api/health/route.ts
================================================
export function GET() {
  return Response.json({ status: 'ok', app: 'editor', timestamp: new Date().toISOString() })
}


================================================
FILE: apps/editor/app/globals.css
================================================
@import "tailwindcss";
@import "tw-animate-css";
@source "../../../packages/editor/src";

@custom-variant dark (&:is(.dark *));

@theme {
  --font-sans:
    var(--font-barlow), var(--font-geist-sans), ui-sans-serif, system-ui,
    sans-serif;
  --font-mono:
    var(--font-geist-mono), ui-monospace, SFMono-Regular, Menlo, Monaco,
    Consolas, monospace;
  --font-pixel:
    var(--font-geist-pixel-square), var(--font-geist-mono), ui-monospace,
    SFMono-Regular, Menlo, Monaco, Consolas, monospace;
  --font-barlow:
    var(--font-barlow), var(--font-geist-sans), ui-sans-serif, system-ui,
    sans-serif;
}

@theme inline {
  --color-background: var(--background);
  --color-foreground: var(--foreground);
  --font-sans: var(--font-barlow), sans-serif;
  --font-mono: var(--font-geist-mono), monospace;
  --font-pixel:
    var(--font-geist-pixel-square), var(--font-geist-mono), monospace;
  --font-barlow: var(--font-barlow), sans-serif;
  --color-sidebar-border: var(--sidebar-border);
  --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
  --color-sidebar-accent: var(--sidebar-accent);
  --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
  --color-sidebar-primary: var(--sidebar-primary);
  --color-sidebar-foreground: var(--sidebar-foreground);
  --color-sidebar: var(--sidebar);
  --color-chart-5: var(--chart-5);
  --color-chart-4: var(--chart-4);
  --color-chart-3: var(--chart-3);
  --color-chart-2: var(--chart-2);
  --color-chart-1: var(--chart-1);
  --color-ring: var(--ring);
  --color-input: var(--input);
  --color-border: var(--border);
  --color-destructive: var(--destructive);
  --color-accent-foreground: var(--accent-foreground);
  --color-accent: var(--accent);
  --color-muted-foreground: var(--muted-foreground);
  --color-muted: var(--muted);
  --color-secondary-foreground: var(--secondary-foreground);
  --color-secondary: var(--secondary);
  --color-primary-foreground: var(--primary-foreground);
  --color-primary: var(--primary);
  --color-popover-foreground: var(--popover-foreground);
  --color-popover: var(--popover);
  --color-card-foreground: var(--card-foreground);
  --color-card: var(--card);
  --radius-sm: calc(var(--radius) - 4px);
  --radius-md: calc(var(--radius) - 2px);
  --radius-lg: var(--radius);
  --radius-xl: calc(var(--radius) + 4px);
}

:root {
  --radius: 0.625rem;
  --background: oklch(0.998 0 0);
  --foreground: oklch(0.145 0 0);
  --card: oklch(0.998 0 0);
  --card-foreground: oklch(0.145 0 0);
  --popover: oklch(0.998 0 0);
  --popover-foreground: oklch(0.145 0 0);
  --primary: oklch(0.205 0 0);
  --primary-foreground: oklch(0.985 0 0);
  --secondary: oklch(0.97 0 0);
  --secondary-foreground: oklch(0.205 0 0);
  --muted: oklch(0.97 0 0);
  --muted-foreground: oklch(0.556 0 0);
  --accent: oklch(0.97 0 0);
  --accent-foreground: oklch(0.205 0 0);
  --destructive: oklch(0.577 0.245 27.325);
  --border: oklch(0.922 0 0);
  --input: oklch(0.922 0 0);
  --ring: oklch(0.708 0 0);
  --chart-1: oklch(0.646 0.222 41.116);
  --chart-2: oklch(0.6 0.118 184.704);
  --chart-3: oklch(0.398 0.07 227.392);
  --chart-4: oklch(0.828 0.189 84.429);
  --chart-5: oklch(0.769 0.188 70.08);
  --sidebar: oklch(0.985 0 0);
  --sidebar-foreground: oklch(0.145 0 0);
  --sidebar-primary: oklch(0.205 0 0);
  --sidebar-primary-foreground: oklch(0.985 0 0);
  --sidebar-accent: oklch(0.97 0 0);
  --sidebar-accent-foreground: oklch(0.205 0 0);
  --sidebar-border: oklch(0.922 0 0);
  --sidebar-ring: oklch(0.708 0 0);
}

.dark {
  --background: oklch(0.205 0 0); /* ~171717 */
  --foreground: oklch(0.985 0 0);
  --card: oklch(0.205 0 0);
  --card-foreground: oklch(0.985 0 0);
  --popover: oklch(0.205 0 0);
  --popover-foreground: oklch(0.985 0 0);
  --primary: oklch(0.922 0 0);
  --primary-foreground: oklch(0.205 0 0);
  --secondary: oklch(0.269 0 0);
  --secondary-foreground: oklch(0.985 0 0);
  --muted: oklch(0.269 0 0);
  --muted-foreground: oklch(0.708 0 0);
  --accent: oklch(
    0.235 0 0
  ); /* slightly lighter than background (0.205) but darker than previous (0.269) */
  --accent-foreground: oklch(0.985 0 0);
  --destructive: oklch(0.704 0.191 22.216);
  --border: oklch(1 0 0 / 10%);
  --input: oklch(1 0 0 / 15%);
  --ring: oklch(0.556 0 0);
  --chart-1: oklch(0.488 0.243 264.376);
  --chart-2: oklch(0.696 0.17 162.48);
  --chart-3: oklch(0.769 0.188 70.08);
  --chart-4: oklch(0.627 0.265 303.9);
  --chart-5: oklch(0.645 0.246 16.439);
  --sidebar: oklch(0.205 0 0);
  --sidebar-foreground: oklch(0.985 0 0);
  --sidebar-primary: oklch(0.488 0.243 264.376);
  --sidebar-primary-foreground: oklch(0.985 0 0);
  --sidebar-accent: oklch(0.235 0 0); /* matching accent */
  --sidebar-accent-foreground: oklch(0.985 0 0);
  --sidebar-border: oklch(1 0 0 / 10%);
  --sidebar-ring: oklch(0.556 0 0);
}

@layer base {
  * {
    @apply border-border outline-ring/50;
  }
  body {
    @apply bg-background text-foreground;
  }
  button,
  [role="button"],
  a {
    cursor: pointer;
  }
}

/* Apple-style smooth corners (squircle) — progressive enhancement */
.rounded-smooth {
  border-radius: var(--radius-lg);
  corner-shape: squircle;
}
.rounded-smooth-xl {
  border-radius: var(--radius-xl);
  corner-shape: squircle;
}

.no-scrollbar::-webkit-scrollbar {
  display: none;
}

.no-scrollbar {
  -ms-overflow-style: none; /* IE and Edge */
  scrollbar-width: none; /* Firefox */
}

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

/* Loaders */
.pascal-loader-1 {
  width: 45px;
  aspect-ratio: 1;
  --c: no-repeat linear-gradient(currentColor 0 0);
  background: var(--c), var(--c), var(--c);
  animation:
    pascal-l1-1 1s infinite,
    pascal-l1-2 1s infinite;
}
@keyframes pascal-l1-1 {
  0%,
  100% {
    background-size: 20% 100%;
  }
  33%,
  66% {
    background-size: 20% 20%;
  }
}
@keyframes pascal-l1-2 {
  0%,
  33% {
    background-position:
      0 0,
      50% 50%,
      100% 100%;
  }
  66%,
  100% {
    background-position:
      100% 0,
      50% 50%,
      0 100%;
  }
}

.pascal-loader-2 {
  width: 45px;
  aspect-ratio: 0.75;
  --c: no-repeat linear-gradient(currentColor 0 0);
  background:
    var(--c) 0% 50%,
    var(--c) 50% 50%,
    var(--c) 100% 50%;
  background-size: 20% 50%;
  animation: pascal-l2 1s infinite linear;
}
@keyframes pascal-l2 {
  20% {
    background-position:
      0% 0%,
      50% 50%,
      100% 50%;
  }
  40% {
    background-position:
      0% 100%,
      50% 0%,
      100% 50%;
  }
  60% {
    background-position:
      0% 50%,
      50% 100%,
      100% 0%;
  }
  80% {
    background-position:
      0% 50%,
      50% 50%,
      100% 100%;
  }
}

.pascal-loader-3 {
  width: 45px;
  aspect-ratio: 0.75;
  --c: no-repeat linear-gradient(currentColor 0 0);
  background:
    var(--c) 0% 100%,
    var(--c) 50% 100%,
    var(--c) 100% 100%;
  background-size: 20% 65%;
  animation: pascal-l3 1s infinite linear;
}
@keyframes pascal-l3 {
  16.67% {
    background-position:
      0% 0%,
      50% 100%,
      100% 100%;
  }
  33.33% {
    background-position:
      0% 0%,
      50% 0%,
      100% 100%;
  }
  50% {
    background-position:
      0% 0%,
      50% 0%,
      100% 0%;
  }
  66.67% {
    background-position:
      0% 100%,
      50% 0%,
      100% 0%;
  }
  83.33% {
    background-position:
      0% 100%,
      50% 100%,
      100% 0%;
  }
}

.pascal-loader-4 {
  width: 45px;
  aspect-ratio: 1;
  --c: no-repeat linear-gradient(currentColor 0 0);
  background: var(--c), var(--c), var(--c);
  animation:
    pascal-l4-1 1s infinite,
    pascal-l4-2 1s infinite;
}
@keyframes pascal-l4-1 {
  0%,
  100% {
    background-size: 20% 100%;
  }
  33%,
  66% {
    background-size: 20% 40%;
  }
}
@keyframes pascal-l4-2 {
  0%,
  33% {
    background-position:
      0 0,
      50% 100%,
      100% 100%;
  }
  66%,
  100% {
    background-position:
      100% 0,
      0 100%,
      50% 100%;
  }
}

.pascal-loader-5 {
  width: 45px;
  aspect-ratio: 1;
  --c: no-repeat linear-gradient(currentColor 0 0);
  background: var(--c), var(--c), var(--c);
  animation:
    pascal-l5-1 1s infinite,
    pascal-l5-2 1s infinite;
}
@keyframes pascal-l5-1 {
  0%,
  100% {
    background-size: 20% 100%;
  }
  33%,
  66% {
    background-size: 20% 40%;
  }
}
@keyframes pascal-l5-2 {
  0%,
  33% {
    background-position:
      0 0,
      50% 100%,
      100% 0;
  }
  66%,
  100% {
    background-position:
      0 100%,
      50% 0,
      100% 100%;
  }
}


================================================
FILE: apps/editor/app/layout.tsx
================================================
import { Agentation } from 'agentation'
import { GeistPixelSquare } from 'geist/font/pixel'
import { Barlow } from 'next/font/google'
import localFont from 'next/font/local'
import Script from 'next/script'
import './globals.css'

const geistSans = localFont({
  src: './fonts/GeistVF.woff',
  variable: '--font-geist-sans',
})
const geistMono = localFont({
  src: './fonts/GeistMonoVF.woff',
  variable: '--font-geist-mono',
})

const barlow = Barlow({
  subsets: ['latin'],
  weight: ['400', '500', '600', '700'],
  variable: '--font-barlow',
  display: 'swap',
})

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode
}>) {
  return (
    <html
      className={`${geistSans.variable} ${geistMono.variable} ${GeistPixelSquare.variable} ${barlow.variable}`}
      lang="en"
    >
      <head>
        {process.env.NODE_ENV === 'development' && (
          <Script
            crossOrigin="anonymous"
            src="//unpkg.com/react-scan/dist/auto.global.js"
            strategy="beforeInteractive"
          />
        )}
      </head>
      <body className="font-sans">
        {children}
        {process.env.NODE_ENV === 'development' && <Agentation />}
      </body>
    </html>
  )
}


================================================
FILE: apps/editor/app/page.tsx
================================================
'use client'

import {
  Editor,
  type SidebarTab,
  ViewerToolbarLeft,
  ViewerToolbarRight,
} from '@pascal-app/editor'

const SIDEBAR_TABS: (SidebarTab & { component: React.ComponentType })[] = [
  {
    id: 'site',
    label: 'Scene',
    component: () => null, // Built-in SitePanel handles this
  },
]

export default function Home() {
  return (
    <div className="h-screen w-screen">
      <Editor
        layoutVersion="v2"
        projectId="local-editor"
        sidebarTabs={SIDEBAR_TABS}
        viewerToolbarLeft={<ViewerToolbarLeft />}
        viewerToolbarRight={<ViewerToolbarRight />}
      />
    </div>
  )
}


================================================
FILE: apps/editor/app/privacy/page.tsx
================================================
import type { Metadata } from 'next'
import Link from 'next/link'

export const metadata: Metadata = {
  title: 'Privacy Policy',
  description: 'Privacy Policy for Pascal Editor and the Pascal platform.',
}

export default function PrivacyPage() {
  return (
    <div className="min-h-screen bg-background">
      <header className="sticky top-0 z-10 border-border border-b bg-background/95 backdrop-blur">
        <div className="container mx-auto px-6 py-4">
          <nav className="flex items-center gap-4 text-sm">
            <Link
              className="text-muted-foreground transition-colors hover:text-foreground"
              href="/"
            >
              Home
            </Link>
            <span className="text-muted-foreground">/</span>
            <Link
              className="text-muted-foreground transition-colors hover:text-foreground"
              href="/terms"
            >
              Terms of Service
            </Link>
            <span className="text-muted-foreground">|</span>
            <span className="font-medium text-foreground">Privacy Policy</span>
          </nav>
        </div>
      </header>

      <main className="container mx-auto max-w-3xl px-6 py-12">
        <article className="prose prose-neutral dark:prose-invert max-w-none">
          <h1 className="mb-2 font-bold text-3xl">Privacy Policy</h1>
          <p className="mb-8 text-muted-foreground text-sm">Effective Date: February 20, 2026</p>

          <section className="mb-8 space-y-4">
            <h2 className="font-semibold text-xl">1. Introduction</h2>
            <p className="text-foreground/90 leading-relaxed">
              Pascal Group Inc. (&quot;we,&quot; &quot;us,&quot; or &quot;our&quot;) operates the
              Pascal Editor and Platform at pascal.app. This Privacy Policy explains how we collect,
              use, and protect your information when you use our services.
            </p>
          </section>

          <section className="mb-8 space-y-4">
            <h2 className="font-semibold text-xl">2. Information We Collect</h2>

            <h3 className="mt-4 font-medium text-lg">Account Information</h3>
            <p className="text-foreground/90 leading-relaxed">
              When you create an account, we collect:
            </p>
            <ul className="list-disc space-y-2 pl-6 text-foreground/90">
              <li>Email address</li>
              <li>Name</li>
              <li>Profile picture/avatar</li>
              <li>OAuth provider data (from Google when you sign in with Google)</li>
            </ul>

            <h3 className="mt-4 font-medium text-lg">Project Data</h3>
            <p className="text-foreground/90 leading-relaxed">
              When you use the Platform, we store your projects, including 3D building designs,
              floor plans, and associated metadata.
            </p>

            <h3 className="mt-4 font-medium text-lg">Usage Analytics</h3>
            <p className="text-foreground/90 leading-relaxed">
              We use Vercel Analytics and Speed Insights to collect anonymized usage data, including
              page views, performance metrics, and general usage patterns. This helps us improve the
              Platform.
            </p>
          </section>

          <section className="mb-8 space-y-4">
            <h2 className="font-semibold text-xl">3. How We Use Your Information</h2>
            <p className="text-foreground/90 leading-relaxed">We use your information to:</p>
            <ul className="list-disc space-y-2 pl-6 text-foreground/90">
              <li>Provide and maintain your account</li>
              <li>Store and sync your projects across devices</li>
              <li>Improve our services based on usage patterns</li>
              <li>
                Send optional email notifications about new features and updates (you can opt out in
                settings)
              </li>
              <li>Respond to support requests</li>
              <li>Ensure platform security and prevent abuse</li>
            </ul>
          </section>

          <section className="mb-8 space-y-4">
            <h2 className="font-semibold text-xl">4. Data Storage</h2>
            <p className="text-foreground/90 leading-relaxed">
              Your data is stored using Supabase (PostgreSQL database) on secure cloud
              infrastructure. We implement appropriate technical and organizational measures to
              protect your data.
            </p>
          </section>

          <section className="mb-8 space-y-4">
            <h2 className="font-semibold text-xl">5. Third-Party Services</h2>
            <p className="text-foreground/90 leading-relaxed">
              We use the following third-party services to operate the Platform:
            </p>
            <ul className="list-disc space-y-2 pl-6 text-foreground/90">
              <li>
                <strong>Google</strong> - OAuth authentication for sign-in
              </li>
              <li>
                <strong>Vercel</strong> - Application hosting, analytics, and performance monitoring
              </li>
              <li>
                <strong>Supabase</strong> - Database hosting and authentication infrastructure
              </li>
            </ul>
            <p className="mt-4 text-foreground/90 leading-relaxed">
              Each of these services has their own privacy policies governing their handling of your
              data.
            </p>
          </section>

          <section className="mb-8 space-y-4">
            <h2 className="font-semibold text-xl">6. Cookies</h2>
            <p className="text-foreground/90 leading-relaxed">
              We use minimal cookies necessary for the Platform to function:
            </p>
            <ul className="list-disc space-y-2 pl-6 text-foreground/90">
              <li>
                <strong>Session cookies</strong> - Essential for authentication and keeping you
                signed in
              </li>
              <li>
                <strong>Analytics cookies</strong> - Used by Vercel Analytics to collect anonymized
                usage data
              </li>
            </ul>
          </section>

          <section className="mb-8 space-y-4">
            <h2 className="font-semibold text-xl">7. Your Rights</h2>
            <p className="text-foreground/90 leading-relaxed">You have the right to:</p>
            <ul className="list-disc space-y-2 pl-6 text-foreground/90">
              <li>Access the personal data we hold about you</li>
              <li>Request correction of inaccurate data</li>
              <li>Request deletion of your data</li>
              <li>Export your project data</li>
              <li>Opt out of marketing communications</li>
            </ul>
            <p className="mt-4 text-foreground/90 leading-relaxed">
              To exercise any of these rights, please contact us at{' '}
              <a
                className="text-foreground underline hover:text-foreground/80"
                href="mailto:support@pascal.app"
              >
                support@pascal.app
              </a>
              .
            </p>
          </section>

          <section className="mb-8 space-y-4">
            <h2 className="font-semibold text-xl">8. Data Retention</h2>
            <p className="text-foreground/90 leading-relaxed">
              We retain your data for as long as your account is active. If you delete your account,
              we will delete your personal data and project data within 30 days, except where we are
              required by law to retain certain information.
            </p>
          </section>

          <section className="mb-8 space-y-4">
            <h2 className="font-semibold text-xl">9. Children&apos;s Privacy</h2>
            <p className="text-foreground/90 leading-relaxed">
              The Platform is not intended for children under 13. We do not knowingly collect
              personal information from children under 13. If you believe we have collected such
              information, please contact us immediately.
            </p>
          </section>

          <section className="mb-8 space-y-4">
            <h2 className="font-semibold text-xl">10. Changes to This Policy</h2>
            <p className="text-foreground/90 leading-relaxed">
              We may update this Privacy Policy from time to time. We will notify you of material
              changes by posting the updated policy on the Platform. Your continued use of the
              Platform after changes are posted constitutes your acceptance of the revised policy.
            </p>
          </section>

          <section className="space-y-4">
            <h2 className="font-semibold text-xl">11. Contact Us</h2>
            <p className="text-foreground/90 leading-relaxed">
              If you have questions about this Privacy Policy or how we handle your data, please
              contact us at{' '}
              <a
                className="text-foreground underline hover:text-foreground/80"
                href="mailto:support@pascal.app"
              >
                support@pascal.app
              </a>
              .
            </p>
          </section>
        </article>
      </main>
    </div>
  )
}


================================================
FILE: apps/editor/app/terms/page.tsx
================================================
import type { Metadata } from 'next'
import Link from 'next/link'

export const metadata: Metadata = {
  title: 'Terms of Service',
  description: 'Terms of Service for Pascal Editor and the Pascal platform.',
}

export default function TermsPage() {
  return (
    <div className="min-h-screen bg-background">
      <header className="sticky top-0 z-10 border-border border-b bg-background/95 backdrop-blur">
        <div className="container mx-auto px-6 py-4">
          <nav className="flex items-center gap-4 text-sm">
            <Link
              className="text-muted-foreground transition-colors hover:text-foreground"
              href="/"
            >
              Home
            </Link>
            <span className="text-muted-foreground">/</span>
            <span className="font-medium text-foreground">Terms of Service</span>
            <span className="text-muted-foreground">|</span>
            <Link
              className="text-muted-foreground transition-colors hover:text-foreground"
              href="/privacy"
            >
              Privacy Policy
            </Link>
          </nav>
        </div>
      </header>

      <main className="container mx-auto max-w-3xl px-6 py-12">
        <article className="prose prose-neutral dark:prose-invert max-w-none">
          <h1 className="mb-2 font-bold text-3xl">Terms of Service</h1>
          <p className="mb-8 text-muted-foreground text-sm">Effective Date: February 20, 2026</p>

          <section className="mb-8 space-y-4">
            <h2 className="font-semibold text-xl">1. Introduction</h2>
            <p className="text-foreground/90 leading-relaxed">
              Welcome to Pascal Editor (&quot;Editor&quot;) and the Pascal platform at pascal.app
              (&quot;Platform&quot;), operated by Pascal Group Inc. (&quot;we,&quot; &quot;us,&quot;
              or &quot;our&quot;). By accessing or using our services, you agree to these Terms of
              Service.
            </p>
          </section>

          <section className="mb-8 space-y-4">
            <h2 className="font-semibold text-xl">2. The Editor and Platform</h2>
            <p className="text-foreground/90 leading-relaxed">
              The Pascal Editor is open-source software released under the MIT License. You may use,
              copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Editor
              software in accordance with the MIT License terms.
            </p>
            <p className="text-foreground/90 leading-relaxed">
              The Pascal platform (pascal.app) and its associated services, including user accounts,
              cloud storage, and project hosting, are proprietary services owned and operated by
              Pascal Group Inc. These Terms govern your use of the Platform.
            </p>
          </section>

          <section className="mb-8 space-y-4">
            <h2 className="font-semibold text-xl">3. Accounts and Authentication</h2>
            <p className="text-foreground/90 leading-relaxed">
              To use certain features of the Platform, you must create an account. We use Google
              OAuth and magic link email authentication through Supabase. You are responsible for
              maintaining the security of your account credentials and for all activities that occur
              under your account.
            </p>
          </section>

          <section className="mb-8 space-y-4">
            <h2 className="font-semibold text-xl">4. Acceptable Use</h2>
            <p className="text-foreground/90 leading-relaxed">You agree not to:</p>
            <ul className="list-disc space-y-2 pl-6 text-foreground/90">
              <li>
                Use the Platform for any unlawful purpose or in violation of any applicable laws
              </li>
              <li>
                Upload, share, or distribute content that infringes intellectual property rights
              </li>
              <li>Attempt to gain unauthorized access to the Platform or its systems</li>
              <li>Interfere with or disrupt the Platform&apos;s infrastructure</li>
              <li>Upload malicious code, viruses, or harmful content</li>
              <li>Harass, abuse, or harm other users</li>
              <li>Use the Platform to send spam or unsolicited communications</li>
            </ul>
          </section>

          <section className="mb-8 space-y-4">
            <h2 className="font-semibold text-xl">5. Your Content and Intellectual Property</h2>
            <p className="text-foreground/90 leading-relaxed">
              You retain full ownership of all content, projects, and data you create or upload to
              the Platform (&quot;Your Content&quot;). By using the Platform, you grant us a limited
              license to store, display, and transmit Your Content solely to provide our services to
              you.
            </p>
            <p className="text-foreground/90 leading-relaxed">
              We do not claim any ownership rights over Your Content. You may export or delete Your
              Content at any time.
            </p>
          </section>

          <section className="mb-8 space-y-4">
            <h2 className="font-semibold text-xl">6. Platform Ownership</h2>
            <p className="text-foreground/90 leading-relaxed">
              The Platform, including its design, features, and proprietary code, is owned by Pascal
              Group Inc. and protected by intellectual property laws. While the Editor source code
              is open-source under the MIT License, the Platform services, branding, and
              infrastructure remain our proprietary property.
            </p>
          </section>

          <section className="mb-8 space-y-4">
            <h2 className="font-semibold text-xl">7. Account Termination</h2>
            <p className="text-foreground/90 leading-relaxed">
              We reserve the right to suspend or terminate your account if you violate these Terms
              or engage in conduct that we determine is harmful to the Platform or other users. You
              may also delete your account at any time by contacting us at{' '}
              <a
                className="text-foreground underline hover:text-foreground/80"
                href="mailto:support@pascal.app"
              >
                support@pascal.app
              </a>
              .
            </p>
          </section>

          <section className="mb-8 space-y-4">
            <h2 className="font-semibold text-xl">8. Disclaimer of Warranties</h2>
            <p className="text-foreground/90 leading-relaxed">
              THE PLATFORM IS PROVIDED &quot;AS IS&quot; AND &quot;AS AVAILABLE&quot; WITHOUT
              WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO
              IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND
              NON-INFRINGEMENT.
            </p>
            <p className="text-foreground/90 leading-relaxed">
              We do not warrant that the Platform will be uninterrupted, error-free, or free of
              harmful components.
            </p>
          </section>

          <section className="mb-8 space-y-4">
            <h2 className="font-semibold text-xl">9. Limitation of Liability</h2>
            <p className="text-foreground/90 leading-relaxed">
              TO THE MAXIMUM EXTENT PERMITTED BY LAW, PASCAL GROUP INC. SHALL NOT BE LIABLE FOR ANY
              INDIRECT, INCIDENTAL, SPECIAL, CONSEQUENTIAL, OR PUNITIVE DAMAGES, INCLUDING LOSS OF
              DATA, PROFITS, OR GOODWILL, ARISING FROM YOUR USE OF THE PLATFORM.
            </p>
          </section>

          <section className="mb-8 space-y-4">
            <h2 className="font-semibold text-xl">10. Changes to Terms</h2>
            <p className="text-foreground/90 leading-relaxed">
              We may update these Terms from time to time. We will notify you of material changes by
              posting the updated Terms on the Platform. Your continued use of the Platform after
              changes are posted constitutes your acceptance of the revised Terms.
            </p>
          </section>

          <section className="space-y-4">
            <h2 className="font-semibold text-xl">11. Contact Us</h2>
            <p className="text-foreground/90 leading-relaxed">
              If you have questions about these Terms, please contact us at{' '}
              <a
                className="text-foreground underline hover:text-foreground/80"
                href="mailto:support@pascal.app"
              >
                support@pascal.app
              </a>
              .
            </p>
          </section>
        </article>
      </main>
    </div>
  )
}


================================================
FILE: apps/editor/env.mjs
================================================
/**
 * Environment variable validation for the editor app.
 *
 * This file validates that required environment variables are set at runtime.
 * Variables are defined in the root .env file.
 *
 * @see https://env.t3.gg/docs/nextjs
 */
import { createEnv } from '@t3-oss/env-nextjs'
import { z } from 'zod'

export const env = createEnv({
  /**
   * Server-side environment variables (not exposed to client)
   */
  server: {
    // Database
    POSTGRES_URL: z.string().min(1),
    SUPABASE_SERVICE_ROLE_KEY: z.string().min(1),

    // Auth
    BETTER_AUTH_SECRET: z.string().min(1),
    GOOGLE_CLIENT_ID: z.string().optional(),
    GOOGLE_CLIENT_SECRET: z.string().optional(),

    // Email
    RESEND_API_KEY: z.string().optional(),
  },

  /**
   * Client-side environment variables (exposed to browser via NEXT_PUBLIC_)
   */
  client: {
    NEXT_PUBLIC_SUPABASE_URL: z.string().min(1),
    NEXT_PUBLIC_SUPABASE_ANON_KEY: z.string().optional(),
  },

  /**
   * Runtime values - pulls from process.env
   */
  runtimeEnv: {
    // Server
    POSTGRES_URL: process.env.POSTGRES_URL,
    SUPABASE_SERVICE_ROLE_KEY: process.env.SUPABASE_SERVICE_ROLE_KEY,
    BETTER_AUTH_SECRET: process.env.BETTER_AUTH_SECRET,
    GOOGLE_CLIENT_ID: process.env.GOOGLE_CLIENT_ID,
    GOOGLE_CLIENT_SECRET: process.env.GOOGLE_CLIENT_SECRET,
    RESEND_API_KEY: process.env.RESEND_API_KEY,
    // Client
    NEXT_PUBLIC_SUPABASE_URL: process.env.NEXT_PUBLIC_SUPABASE_URL,
    NEXT_PUBLIC_SUPABASE_ANON_KEY: process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY,
  },

  /**
   * Skip validation during build (env vars come from Vercel at runtime)
   */
  skipValidation: !!process.env.SKIP_ENV_VALIDATION,
})


================================================
FILE: apps/editor/lib/utils.ts
================================================
import { type ClassValue, clsx } from 'clsx'
import { twMerge } from 'tailwind-merge'

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs))
}

export const isDevelopment =
  process.env.NODE_ENV === 'development' || process.env.NEXT_PUBLIC_VERCEL_ENV === 'development'

export const isProduction =
  process.env.NODE_ENV === 'production' || process.env.NEXT_PUBLIC_VERCEL_ENV === 'production'

export const isPreview = process.env.NEXT_PUBLIC_VERCEL_ENV === 'preview'

/**
 * Base URL for the application
 * Uses NEXT_PUBLIC_* variables which are available at build time
 */
export const BASE_URL = (() => {
  // Development: localhost
  if (isDevelopment) {
    return process.env.NEXT_PUBLIC_APP_URL || `http://localhost:${process.env.PORT || 3000}`
  }

  // Preview deployments: use Vercel branch URL
  if (isPreview && process.env.NEXT_PUBLIC_VERCEL_URL) {
    return `https://${process.env.NEXT_PUBLIC_VERCEL_URL}`
  }

  // Production: use custom domain or Vercel production URL
  if (isProduction) {
    return (
      process.env.NEXT_PUBLIC_APP_URL ||
      (process.env.NEXT_PUBLIC_VERCEL_PROJECT_PRODUCTION_URL
        ? `https://${process.env.NEXT_PUBLIC_VERCEL_PROJECT_PRODUCTION_URL}`
        : 'https://editor.pascal.app')
    )
  }

  // Fallback (should never reach here in normal operation)
  return process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'
})()


================================================
FILE: apps/editor/next.config.ts
================================================
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  typescript: {
    ignoreBuildErrors: true,
  },
  transpilePackages: ['three', '@pascal-app/viewer', '@pascal-app/core', '@pascal-app/editor'],
  turbopack: {
    resolveAlias: {
      react: './node_modules/react',
      three: './node_modules/three',
      '@react-three/fiber': './node_modules/@react-three/fiber',
      '@react-three/drei': './node_modules/@react-three/drei',
    },
  },
  experimental: {
    serverActions: {
      bodySizeLimit: '100mb',
    },
  },
  images: {
    unoptimized: process.env.NEXT_PUBLIC_ASSETS_CDN_URL?.startsWith('http://localhost') ?? false,
    remotePatterns: [
      {
        protocol: 'https',
        hostname: '**',
      },
      {
        protocol: 'http',
        hostname: '**',
      },
    ],
  },
}

export default nextConfig


================================================
FILE: apps/editor/package.json
================================================
{
  "name": "editor",
  "version": "0.1.0",
  "type": "module",
  "private": true,
  "scripts": {
    "dev": "dotenv -e ./.env.local --override -- next dev --port 3002",
    "build": "dotenv -e ./.env.local --override -- next build",
    "start": "next start",
    "lint": "biome lint",
    "check-types": "next typegen && tsc --noEmit"
  },
  "dependencies": {
    "@number-flow/react": "^0.5.14",
    "@pascal-app/core": "*",
    "@pascal-app/editor": "*",
    "@pascal-app/viewer": "*",
    "@react-three/drei": "^10.7.7",
    "@react-three/fiber": "^9.5.0",
    "@tailwindcss/postcss": "^4.2.1",
    "clsx": "^2.1.1",
    "geist": "^1.7.0",
    "next": "16.2.1",
    "postcss": "^8.5.6",
    "react": "^19.2.4",
    "react-dom": "^19.2.4",
    "tailwind-merge": "^3.5.0",
    "tailwindcss": "^4.2.1",
    "three": "^0.183.1"
  },
  "devDependencies": {
    "@pascal/typescript-config": "*",
    "@types/howler": "^2.2.12",
    "@types/node": "^22.19.12",
    "@types/react": "19.2.2",
    "@types/react-dom": "19.2.2",
    "agentation": "^2.3.2",
    "react-grab": "^0.1.25",
    "react-scan": "^0.5.3",
    "tw-animate-css": "^1.4.0",
    "typescript": "5.9.3"
  }
}


================================================
FILE: apps/editor/postcss.config.mjs
================================================
export default {
  plugins: {
    '@tailwindcss/postcss': {},
  },
}


================================================
FILE: apps/editor/public/demos/demo_1.json
================================================
{
  "nodes": {
    "building_bv4ilcjivnxn8wkd": {
      "object": "node",
      "id": "building_bv4ilcjivnxn8wkd",
      "type": "building",
      "parentId": null,
      "visible": true,
      "metadata": {},
      "children": ["level_pojp0mw3qssu110w", "level_bbyvfs9qwzh4arjf"],
      "position": [0, 0, 0],
      "rotation": [0, 0, 0]
    },
    "level_pojp0mw3qssu110w": {
      "object": "node",
      "id": "level_pojp0mw3qssu110w",
      "type": "level",
      "parentId": null,
      "visible": true,
      "metadata": {},
      "children": [
        "slab_gr6zxi4915gqwjbn",
        "zone_iozx54yy1hmmoads",
        "item_ketiyaswru3k7vub",
        "item_8zi2layxh9s8erc1",
        "item_t29aoosaypbiwkq2",
        "item_ycf4n2wudq423w5g",
        "wall_0j28n7nskm2sst7m",
        "wall_3wwt9bjqrdc5w09s",
        "wall_785y11hb3nztn1ua",
        "wall_g4h1v4vm9ou0wryc",
        "item_4vch778zg55nfmgb",
        "item_vqk00ajqdznqsygs",
        "wall_ejrf1znv4twbeszy",
        "wall_9l64ckn6p3yzmfxf",
        "item_e3bxmnrhz9eclzgw",
        "item_vf8hqhemi33y4n0w",
        "item_k2zexryozzwgokox",
        "item_6p3y4rva0zv24s1t",
        "item_r7idtbazaf6482kq",
        "item_0x8hjyork26n4g2m",
        "item_tmprvzxa85izusug",
        "item_g6219gs7mrpg7bzd",
        "item_dts7f41ictd7hah4",
        "item_in5trmidft4sglhx",
        "zone_c7k2ssvmbv5d1xlm",
        "item_0e9paq67kdbm5ux0",
        "item_iyu7knyxqe82c3yg",
        "item_nwe34qk8vzs1vag7",
        "item_oay55zmjjo76s1fs",
        "item_g060bhthvcra992w",
        "item_38kt7s45alt2vrjg",
        "item_n10cp9ke7n9hxl96",
        "item_nt5fxip4a03cmaoi",
        "item_1quoil3ytsuuarni",
        "item_e1w89kkg2pql1v45",
        "item_3akotmiffzdr8ule",
        "item_1b3tinfswueb6gr8",
        "item_y164oe3lxfx9qefg",
        "item_dsalxofuqf96h8t4",
        "roof_ui8zhim41alg6lq4",
        "guide_acs9nzz19rm4vl2c"
      ],
      "level": 0,
      "camera": {
        "position": [32.19770918094574, 13.355189178183336, 32.63027548275616],
        "target": [
          4.8501257048479305, -6.656412040461838e-16, 7.3634421472590255
        ],
        "mode": "perspective"
      }
    },
    "slab_gr6zxi4915gqwjbn": {
      "object": "node",
      "id": "slab_gr6zxi4915gqwjbn",
      "type": "slab",
      "name": "Plancher",
      "parentId": "level_pojp0mw3qssu110w",
      "visible": true,
      "metadata": {},
      "polygon": [[13, 6], [9, 6], [9, 0], [13, 0]],
      "elevation": 0.05
    },
    "level_bbyvfs9qwzh4arjf": {
      "object": "node",
      "id": "level_bbyvfs9qwzh4arjf",
      "type": "level",
      "parentId": "building_bv4ilcjivnxn8wkd",
      "visible": true,
      "metadata": {},
      "children": ["roof_jxd8tc6rcuaujl25"],
      "level": 1,
      "camera": {
        "position": [11.709072311989358, 25.635955613557638, 50.59653427090403],
        "target": [6.9995635873796855, 2.4999999999999996, 0.6911898255966076],
        "mode": "perspective"
      }
    },
    "roof_jxd8tc6rcuaujl25": {
      "object": "node",
      "id": "roof_jxd8tc6rcuaujl25",
      "type": "roof",
      "name": "Roof 1",
      "parentId": "level_bbyvfs9qwzh4arjf",
      "visible": true,
      "metadata": {},
      "position": [10.25, 0, 3.5],
      "rotation": 0,
      "length": 5.5,
      "height": 1.6,
      "leftWidth": 4.7,
      "rightWidth": 2.7
    },
    "zone_iozx54yy1hmmoads": {
      "object": "node",
      "id": "zone_iozx54yy1hmmoads",
      "type": "zone",
      "name": "Wawa Zone",
      "parentId": "level_pojp0mw3qssu110w",
      "visible": true,
      "metadata": {},
      "polygon": [[9, 6], [9, 0], [13, 0], [13, 6]],
      "color": "#3b82f6",
      "camera": {
        "position": [18.715003971902778, 18.8254836683251, 12.086656976691291],
        "target": [
          6.9995635873796855, 1.6971845387795204e-17, 0.6911898255966076
        ],
        "mode": "perspective"
      }
    },
    "item_137wje66gax2c6bc": {
      "object": "node",
      "id": "item_137wje66gax2c6bc",
      "type": "item",
      "name": "window-large",
      "parentId": "wall_q455ycyoqnjxjcdr",
      "visible": true,
      "metadata": {},
      "position": [2, 0.5, 0],
      "rotation": [0, 0, 0],
      "side": "front",
      "asset": {
        "id": "window-large",
        "category": "window",
        "name": "window-large",
        "thumbnail": "/items/window-large/thumbnail.webp",
        "src": "/items/window-large/model.glb",
        "dimensions": [2, 2, 0.4],
        "attachTo": "wall",
        "offset": [0, 1, 0],
        "rotation": [0, 0, 0],
        "scale": [1, 1, 1]
      }
    },
    "item_ketiyaswru3k7vub": {
      "object": "node",
      "id": "item_ketiyaswru3k7vub",
      "type": "item",
      "name": "lounge-chair",
      "parentId": "level_pojp0mw3qssu110w",
      "visible": true,
      "metadata": {},
      "position": [9.5, 0, 6.75],
      "rotation": [0, 0, 0],
      "asset": {
        "id": "lounge-chair",
        "category": "furniture",
        "name": "lounge-chair",
        "thumbnail": "/items/lounge-chair/thumbnail.webp",
        "src": "/items/lounge-chair/model.glb",
        "dimensions": [1, 1.1, 1.5],
        "offset": [0, 0, 0.09],
        "rotation": [0, 0, 0],
        "scale": [1, 1, 1]
      }
    },
    "item_8zi2layxh9s8erc1": {
      "object": "node",
      "id": "item_8zi2layxh9s8erc1",
      "type": "item",
      "name": "Lounge Chair 2",
      "parentId": "level_pojp0mw3qssu110w",
      "visible": true,
      "metadata": {},
      "position": [12, 0, 6.75],
      "rotation": [0, 0, 0],
      "asset": {
        "id": "lounge-chair",
        "category": "furniture",
        "name": "lounge-chair",
        "thumbnail": "/items/lounge-chair/thumbnail.webp",
        "src": "/items/lounge-chair/model.glb",
        "dimensions": [1, 1.1, 1.5],
        "offset": [0, 0, 0.09],
        "rotation": [0, 0, 0],
        "scale": [1, 1, 1]
      }
    },
    "item_t29aoosaypbiwkq2": {
      "object": "node",
      "id": "item_t29aoosaypbiwkq2",
      "type": "item",
      "name": "trash-bin",
      "parentId": "level_pojp0mw3qssu110w",
      "visible": true,
      "metadata": {},
      "position": [8.25, 0, 6.75],
      "rotation": [0, 0, 0],
      "asset": {
        "id": "trash-bin",
        "category": "furniture",
        "name": "trash-bin",
        "thumbnail": "/items/trash-bin/thumbnail.webp",
        "src": "/items/trash-bin/model.glb",
        "dimensions": [0.5, 0.6, 0.5],
        "offset": [0, 0, 0],
        "rotation": [0, 0, 0],
        "scale": [1, 1, 1]
      }
    },
    "item_ycf4n2wudq423w5g": {
      "object": "node",
      "id": "item_ycf4n2wudq423w5g",
      "type": "item",
      "name": "toy",
      "parentId": "level_pojp0mw3qssu110w",
      "visible": true,
      "metadata": {},
      "position": [7.25, 0, 6.75],
      "rotation": [0, 0, 0],
      "asset": {
        "id": "toy",
        "category": "furniture",
        "name": "toy",
        "thumbnail": "/items/toy/thumbnail.webp",
        "src": "/items/toy/model.glb",
        "dimensions": [0.5, 0.5, 0.5],
        "offset": [0, 0, 0],
        "rotation": [0, 0, 0],
        "scale": [1, 1, 1]
      }
    },
    "wall_0j28n7nskm2sst7m": {
      "object": "node",
      "id": "wall_0j28n7nskm2sst7m",
      "type": "wall",
      "parentId": "level_pojp0mw3qssu110w",
      "visible": true,
      "metadata": {},
      "children": ["item_53okxjgbe9htx0yb", "item_u94w6z7xl4a8icgn"],
      "start": [9, 0],
      "end": [13, 0]
    },
    "wall_3wwt9bjqrdc5w09s": {
      "object": "node",
      "id": "wall_3wwt9bjqrdc5w09s",
      "type": "wall",
      "parentId": "level_pojp0mw3qssu110w",
      "visible": true,
      "metadata": {},
      "children": ["item_4g68nbqcpnzrqpsa", "item_b5xi5dwyufh17763"],
      "start": [13, 0],
      "end": [13, 6]
    },
    "item_b5xi5dwyufh17763": {
      "object": "node",
      "id": "item_b5xi5dwyufh17763",
      "type": "item",
      "name": "Window-simple",
      "parentId": "wall_3wwt9bjqrdc5w09s",
      "visible": true,
      "metadata": {},
      "position": [1.5, 0, 0],
      "rotation": [0, 0, 0],
      "side": "front",
      "asset": {
        "id": "window-simple",
        "category": "window",
        "name": "Window-simple",
        "thumbnail": "/items/window-simple/thumbnail.webp",
        "src": "/items/window-simple/model.glb",
        "dimensions": [1.5, 2, 0.5],
        "attachTo": "wall",
        "offset": [1.06, -0.35, 0.05],
        "rotation": [0, 3.14, 0],
        "scale": [1, 1, 1]
      }
    },
    "item_53okxjgbe9htx0yb": {
      "object": "node",
      "id": "item_53okxjgbe9htx0yb",
      "type": "item",
      "name": "Window-double",
      "parentId": "wall_0j28n7nskm2sst7m",
      "visible": true,
      "metadata": {},
      "position": [2.5, 0.5, 0],
      "rotation": [0, 0, 0],
      "side": "front",
      "asset": {
        "id": "window-double",
        "category": "window",
        "name": "Window-double",
        "thumbnail": "/items/window-double/thumbnail.webp",
        "src": "/items/window-double/model.glb",
        "dimensions": [1.5, 2, 0.5],
        "attachTo": "wall",
        "offset": [0, -0.38, 0.02],
        "rotation": [0, 3.14, 0],
        "scale": [1, 1, 1]
      }
    },
    "item_4g68nbqcpnzrqpsa": {
      "object": "node",
      "id": "item_4g68nbqcpnzrqpsa",
      "type": "item",
      "name": "Window-rectangle",
      "parentId": "wall_3wwt9bjqrdc5w09s",
      "visible": true,
      "metadata": {},
      "position": [4, 0.5, 0],
      "rotation": [0, 0, 0],
      "side": "front",
      "asset": {
        "id": "window-rectangle",
        "category": "window",
        "name": "Window-rectangle",
        "thumbnail": "/items/window-rectangle/thumbnail.webp",
        "src": "/items/window-rectangle/model.glb",
        "dimensions": [3, 2, 0.5],
        "attachTo": "wall",
        "offset": [-1.65, -0.34, 0.1],
        "rotation": [0, 3.14, 0],
        "scale": [0.95, 1, 1]
      }
    },
    "item_u94w6z7xl4a8icgn": {
      "object": "node",
      "id": "item_u94w6z7xl4a8icgn",
      "type": "item",
      "name": "Window-square",
      "parentId": "wall_0j28n7nskm2sst7m",
      "visible": true,
      "metadata": {},
      "position": [1, 1, 0],
      "rotation": [0, 0, 0],
      "side": "front",
      "asset": {
        "id": "window-square",
        "category": "window",
        "name": "Window-square",
        "thumbnail": "/items/window-square/thumbnail.webp",
        "src": "/items/window-square/model.glb",
        "dimensions": [1, 1.5, 0.3],
        "attachTo": "wall",
        "offset": [0, 0.72, 0],
        "rotation": [0, 3.141592653589793, 0],
        "scale": [0.5, 0.5, 0.5]
      }
    },
    "wall_785y11hb3nztn1ua": {
      "object": "node",
      "id": "wall_785y11hb3nztn1ua",
      "type": "wall",
      "parentId": "level_pojp0mw3qssu110w",
      "visible": true,
      "metadata": {},
      "children": ["item_d7yqucsv4neczzyc"],
      "start": [8.5, 0],
      "end": [6, 0]
    },
    "item_d7yqucsv4neczzyc": {
      "object": "node",
      "id": "item_d7yqucsv4neczzyc",
      "type": "item",
      "name": "door-bar",
      "parentId": "wall_785y11hb3nztn1ua",
      "visible": true,
      "metadata": {},
      "position": [1, 0, 0],
      "rotation": [0, 3.141592653589793, 0],
      "side": "back",
      "asset": {
        "id": "door-bar",
        "category": "door",
        "name": "door-bar",
        "thumbnail": "/items/door-bar/thumbnail.webp",
        "src": "/items/door-bar/model.glb",
        "dimensions": [1.5, 2.5, 0.5],
        "attachTo": "wall",
        "offset": [-0.48, 0, 0],
        "rotation": [0, 0, 0],
        "scale": [1, 1, 1]
      }
    },
    "wall_g4h1v4vm9ou0wryc": {
      "object": "node",
      "id": "wall_g4h1v4vm9ou0wryc",
      "type": "wall",
      "parentId": "level_pojp0mw3qssu110w",
      "visible": true,
      "metadata": {},
      "children": ["item_mslnrvkv4302epxo", "item_xurqne401l2bdtd1"],
      "start": [6, 0],
      "end": [0, 0]
    },
    "item_mslnrvkv4302epxo": {
      "object": "node",
      "id": "item_mslnrvkv4302epxo",
      "type": "item",
      "name": "glass-door",
      "parentId": "wall_g4h1v4vm9ou0wryc",
      "visible": true,
      "metadata": {},
      "position": [2, 0, 0],
      "rotation": [0, 3.141592653589793, 0],
      "side": "back",
      "asset": {
        "id": "glass-door",
        "category": "door",
        "name": "glass-door",
        "thumbnail": "/items/glass-door/thumbnail.webp",
        "src": "/items/glass-door/model.glb",
        "dimensions": [1.5, 2.5, 0.4],
        "attachTo": "wall",
        "offset": [-0.52, 0, 0],
        "rotation": [0, 0, 0],
        "scale": [0.9, 0.9, 0.9]
      }
    },
    "item_xurqne401l2bdtd1": {
      "object": "node",
      "id": "item_xurqne401l2bdtd1",
      "type": "item",
      "name": "door",
      "parentId": "wall_g4h1v4vm9ou0wryc",
      "visible": true,
      "metadata": {},
      "position": [4.5, 0, 0],
      "rotation": [0, 3.141592653589793, 0],
      "side": "back",
      "asset": {
        "id": "door",
        "category": "door",
        "name": "door",
        "thumbnail": "/items/door/thumbnail.webp",
        "src": "/items/door/model.glb",
        "dimensions": [1.5, 2.5, 0.4],
        "attachTo": "wall",
        "offset": [-0.43, 0, 0],
        "rotation": [0, 0, 0],
        "scale": [0.9, 0.9, 0.9]
      }
    },
    "item_6e09t7muyapqji6s": {
      "object": "node",
      "id": "item_6e09t7muyapqji6s",
      "type": "item",
      "name": "Window-double",
      "parentId": "wall_lyuq9em1b88jx85g",
      "visible": true,
      "metadata": {},
      "position": [4.5, 0.5, 0],
      "rotation": [0, 0, 0],
      "side": "front",
      "asset": {
        "id": "window-double",
        "category": "window",
        "name": "Window-double",
        "thumbnail": "/items/window-double/thumbnail.webp",
        "src": "/items/window-double/model.glb",
        "dimensions": [1.5, 2, 0.5],
        "attachTo": "wall",
        "offset": [0, -0.38, 0.02],
        "rotation": [0, 3.14, 0],
        "scale": [1, 1, 1]
      }
    },
    "item_wcp57nwt4r0zl73b": {
      "object": "node",
      "id": "item_wcp57nwt4r0zl73b",
      "type": "item",
      "name": "Window-rectangle",
      "parentId": "wall_lyuq9em1b88jx85g",
      "visible": true,
      "metadata": {},
      "position": [7.5, 0.5, 0],
      "rotation": [0, 0, 0],
      "side": "front",
      "asset": {
        "id": "window-rectangle",
        "category": "window",
        "name": "Window-rectangle",
        "thumbnail": "/items/window-rectangle/thumbnail.webp",
        "src": "/items/window-rectangle/model.glb",
        "dimensions": [3, 2, 0.5],
        "attachTo": "wall",
        "offset": [-1.65, -0.34, 0.1],
        "rotation": [0, 3.14, 0],
        "scale": [0.95, 1, 1]
      }
    },
    "item_qpmack787iazwo77": {
      "object": "node",
      "id": "item_qpmack787iazwo77",
      "type": "item",
      "name": "Window-double",
      "parentId": "wall_lyuq9em1b88jx85g",
      "visible": true,
      "metadata": {},
      "position": [10.5, 0.5, 0],
      "rotation": [0, 0, 0],
      "side": "front",
      "asset": {
        "id": "window-double",
        "category": "window",
        "name": "Window-double",
        "thumbnail": "/items/window-double/thumbnail.webp",
        "src": "/items/window-double/model.glb",
        "dimensions": [1.5, 2, 0.5],
        "attachTo": "wall",
        "offset": [0, -0.38, 0.02],
        "rotation": [0, 3.14, 0],
        "scale": [1, 1, 1]
      }
    },
    "item_n78zaonsptayyn9k": {
      "object": "node",
      "id": "item_n78zaonsptayyn9k",
      "type": "item",
      "name": "Window-double",
      "parentId": "wall_lyuq9em1b88jx85g",
      "visible": true,
      "metadata": {},
      "position": [14, 0.5, 0],
      "rotation": [0, 0, 0],
      "side": "front",
      "asset": {
        "id": "window-double",
        "category": "window",
        "name": "Window-double",
        "thumbnail": "/items/window-double/thumbnail.webp",
        "src": "/items/window-double/model.glb",
        "dimensions": [1, 1, 0.5],
        "attachTo": "wall",
        "offset": [0, -0.18, 0.02],
        "rotation": [0, 3.14, 0],
        "scale": [0.5, 0.5, 0.5]
      }
    },
    "item_ee7tg7583z4d2w6m": {
      "object": "node",
      "id": "item_ee7tg7583z4d2w6m",
      "type": "item",
      "name": "Window-double",
      "parentId": "wall_lyuq9em1b88jx85g",
      "visible": true,
      "metadata": {},
      "position": [15.5, 1, 0],
      "rotation": [0, 0, 0],
      "side": "front",
      "asset": {
        "id": "window-double",
        "category": "window",
        "name": "Window-double",
        "thumbnail": "/items/window-double/thumbnail.webp",
        "src": "/items/window-double/model.glb",
        "dimensions": [1, 1, 0.5],
        "attachTo": "wall",
        "offset": [0, -0.18, 0.02],
        "rotation": [0, 3.14, 0],
        "scale": [0.5, 0.5, 0.5]
      }
    },
    "item_1kave256antvprwp": {
      "object": "node",
      "id": "item_1kave256antvprwp",
      "type": "item",
      "name": "Window-double",
      "parentId": "wall_lyuq9em1b88jx85g",
      "visible": true,
      "metadata": {},
      "position": [17, 1, 0],
      "rotation": [0, 0, 0],
      "side": "front",
      "asset": {
        "id": "window-double",
        "category": "window",
        "name": "Window-double",
        "thumbnail": "/items/window-double/thumbnail.webp",
        "src": "/items/window-double/model.glb",
        "dimensions": [1, 1, 0.5],
        "attachTo": "wall",
        "offset": [0, -0.18, 0.02],
        "rotation": [0, 3.14, 0],
        "scale": [0.5, 0.5, 0.5]
      }
    },
    "item_4vch778zg55nfmgb": {
      "object": "node",
      "id": "item_4vch778zg55nfmgb",
      "type": "item",
      "name": "lounge-chair",
      "parentId": "level_pojp0mw3qssu110w",
      "visible": true,
      "metadata": {},
      "position": [2.5, 0, 20.75],
      "rotation": [0, -1.5707963267948966, 0],
      "asset": {
        "id": "lounge-chair",
        "category": "furniture",
        "name": "lounge-chair",
        "thumbnail": "/items/lounge-chair/thumbnail.webp",
        "src": "/items/lounge-chair/model.glb",
        "dimensions": [1, 1.1, 1.5],
        "offset": [0, 0, 0.09],
        "rotation": [0, 0, 0],
        "scale": [1, 1, 1]
      }
    },
    "item_vqk00ajqdznqsygs": {
      "object": "node",
      "id": "item_vqk00ajqdznqsygs",
      "type": "item",
      "name": "tv-stand",
      "parentId": "level_pojp0mw3qssu110w",
      "visible": true,
      "metadata": {},
      "position": [3, 0, 19.25],
      "rotation": [0, -1.5707963267948966, 0],
      "asset": {
        "id": "tv-stand",
        "category": "furniture",
        "name": "tv-stand",
        "thumbnail": "/items/tv-stand/thumbnail.webp",
        "src": "/items/tv-stand/model.glb",
        "dimensions": [2, 0.4, 0.5],
        "offset": [0, 0.21, 0],
        "rotation": [0, 0, 0],
        "scale": [1, 1, 1]
      }
    },
    "wall_ejrf1znv4twbeszy": {
      "object": "node",
      "id": "wall_ejrf1znv4twbeszy",
      "type": "wall",
      "parentId": "level_pojp0mw3qssu110w",
      "visible": true,
      "metadata": {},
      "children": [
        "item_s9fs055u0ilri7pi",
        "item_0173tdywhm704hah",
        "item_7ykyzy2yre3761dq",
        "item_4dehdkm4vx6r4ghv"
      ],
      "start": [1, 13],
      "end": [1, 0.5]
    },
    "item_s9fs055u0ilri7pi": {
      "object": "node",
      "id": "item_s9fs055u0ilri7pi",
      "type": "item",
      "name": "Window-double",
      "parentId": "wall_ejrf1znv4twbeszy",
      "visible": true,
      "metadata": {},
      "position": [10.5, 0.5, 0],
      "rotation": [0, 3.141592653589793, 0],
      "side": "back",
      "asset": {
        "id": "window-double",
        "category": "window",
        "name": "Window-double",
        "thumbnail": "/items/window-double/thumbnail.webp",
        "src": "/items/window-double/model.glb",
        "dimensions": [1.5, 1.5, 0.5],
        "attachTo": "wall",
        "offset": [0, -0.18, 0.02],
        "rotation": [0, 3.14, 0],
        "scale": [0.75, 0.75, 0.75]
      }
    },
    "item_4dehdkm4vx6r4ghv": {
      "object": "node",
      "id": "item_4dehdkm4vx6r4ghv",
      "type": "item",
      "name": "Window-double",
      "parentId": "wall_ejrf1znv4twbeszy",
      "visible": true,
      "metadata": {},
      "position": [8.5, 0.5, 0],
      "rotation": [0, 0, 0],
      "side": "front",
      "asset": {
        "id": "window-double",
        "category": "window",
        "name": "Window-double",
        "thumbnail": "/items/window-double/thumbnail.webp",
        "src": "/items/window-double/model.glb",
        "dimensions": [1.5, 1.5, 0.5],
        "attachTo": "wall",
        "offset": [0, -0.18, 0.02],
        "rotation": [0, 3.14, 0],
        "scale": [0.75, 0.75, 0.75]
      }
    },
    "item_0173tdywhm704hah": {
      "object": "node",
      "id": "item_0173tdywhm704hah",
      "type": "item",
      "name": "door",
      "parentId": "wall_ejrf1znv4twbeszy",
      "visible": true,
      "metadata": {},
      "position": [6.5, 0, 0],
      "rotation": [0, 0, 0],
      "side": "front",
      "asset": {
        "id": "door",
        "category": "door",
        "name": "door",
        "thumbnail": "/items/door/thumbnail.webp",
        "src": "/items/door/model.glb",
        "dimensions": [1.5, 2, 0.4],
        "attachTo": "wall",
        "offset": [-0.43, 0, 0],
        "rotation": [0, 0, 0],
        "scale": [0.8, 0.8, 0.8]
      }
    },
    "item_7ykyzy2yre3761dq": {
      "object": "node",
      "id": "item_7ykyzy2yre3761dq",
      "type": "item",
      "name": "air-conditioning",
      "parentId": "wall_ejrf1znv4twbeszy",
      "visible": true,
      "metadata": {},
      "position": [4, 1.5, 0],
      "rotation": [0, 0, 0],
      "side": "front",
      "asset": {
        "id": "air-conditioning",
        "category": "appliance",
        "name": "air-conditioning",
        "thumbnail": "/items/air-conditioning/thumbnail.webp",
        "src": "/items/air-conditioning/model.glb",
        "dimensions": [2, 1, 0.9],
        "attachTo": "wall-side",
        "offset": [0, 0.37, 0.21],
        "rotation": [0, 0, 0],
        "scale": [1, 1, 1]
      }
    },
    "wall_9l64ckn6p3yzmfxf": {
      "object": "node",
      "id": "wall_9l64ckn6p3yzmfxf",
      "type": "wall",
      "parentId": "level_pojp0mw3qssu110w",
      "visible": true,
      "metadata": {},
      "children": [
        "item_7x3elrsxyvoubfbi",
        "item_ii6ymw4nrg64bw8h",
        "item_bt8uiq7mb9w3m0up"
      ],
      "start": [-4.5, 4],
      "end": [-4.5, -2.5]
    },
    "item_7x3elrsxyvoubfbi": {
      "object": "node",
      "id": "item_7x3elrsxyvoubfbi",
      "type": "item",
      "name": "Window-simple",
      "parentId": "wall_9l64ckn6p3yzmfxf",
      "visible": true,
      "metadata": {},
      "position": [4.5, 0, 0],
      "rotation": [0, 3.141592653589793, 0],
      "side": "back",
      "asset": {
        "id": "window-simple",
        "category": "window",
        "name": "Window-simple",
        "thumbnail": "/items/window-simple/thumbnail.webp",
        "src": "/items/window-simple/model.glb",
        "dimensions": [1.5, 2, 0.5],
        "attachTo": "wall",
        "offset": [1.06, 0, 0.05],
        "rotation": [0, 3.14, 0],
        "scale": [1, 1, 1]
      }
    },
    "item_ii6ymw4nrg64bw8h": {
      "object": "node",
      "id": "item_ii6ymw4nrg64bw8h",
      "type": "item",
      "name": "Window-square",
      "parentId": "wall_9l64ckn6p3yzmfxf",
      "visible": true,
      "metadata": {},
      "position": [1, 0.5, 0],
      "rotation": [0, 3.141592653589793, 0],
      "side": "back",
      "asset": {
        "id": "window-square",
        "category": "window",
        "name": "Window-square",
        "thumbnail": "/items/window-square/thumbnail.webp",
        "src": "/items/window-square/model.glb",
        "dimensions": [1, 1.5, 0.3],
        "attachTo": "wall",
        "offset": [0, 0.72, 0],
        "rotation": [0, 3.141592653589793, 0],
        "scale": [0.5, 0.5, 0.5]
      }
    },
    "item_bt8uiq7mb9w3m0up": {
      "object": "node",
      "id": "item_bt8uiq7mb9w3m0up",
      "type": "item",
      "name": "Window-square",
      "parentId": "wall_9l64ckn6p3yzmfxf",
      "visible": true,
      "metadata": {},
      "position": [2.5, 0.5, 0],
      "rotation": [0, 3.141592653589793, 0],
      "side": "back",
      "asset": {
        "id": "window-square",
        "category": "window",
        "name": "Window-square",
        "thumbnail": "/items/window-square/thumbnail.webp",
        "src": "/items/window-square/model.glb",
        "dimensions": [1, 1.5, 0.3],
        "attachTo": "wall",
        "offset": [0, 0.72, 0],
        "rotation": [0, 3.141592653589793, 0],
        "scale": [0.5, 0.5, 0.5]
      }
    },
    "item_e3bxmnrhz9eclzgw": {
      "object": "node",
      "id": "item_e3bxmnrhz9eclzgw",
      "type": "item",
      "name": "low-fence",
      "parentId": "level_pojp0mw3qssu110w",
      "visible": true,
      "metadata": {},
      "position": [12, 0, 12.25],
      "rotation": [0, 0, 0],
      "asset": {
        "id": "low-fence",
        "category": "outdoor",
        "name": "low-fence",
        "thumbnail": "/items/low-fence/thumbnail.webp",
        "src": "/items/low-fence/model.glb",
        "dimensions": [2, 0.8, 0.5],
        "offset": [0, 0.01, 0],
        "rotation": [0, 0, 0],
        "scale": [1, 1, 1]
      }
    },
    "item_vf8hqhemi33y4n0w": {
      "object": "node",
      "id": "item_vf8hqhemi33y4n0w",
      "type": "item",
      "name": "low-fence",
      "parentId": "level_pojp0mw3qssu110w",
      "visible": true,
      "metadata": {},
      "position": [10, 0, 12.25],
      "rotation": [0, 0, 0],
      "asset": {
        "id": "low-fence",
        "category": "outdoor",
        "name": "low-fence",
        "thumbnail": "/items/low-fence/thumbnail.webp",
        "src": "/items/low-fence/model.glb",
        "dimensions": [2, 0.8, 0.5],
        "offset": [0, 0.01, 0],
        "rotation": [0, 0, 0],
        "scale": [1, 1, 1]
      }
    },
    "item_k2zexryozzwgokox": {
      "object": "node",
      "id": "item_k2zexryozzwgokox",
      "type": "item",
      "name": "low-fence",
      "parentId": "level_pojp0mw3qssu110w",
      "visible": true,
      "metadata": {},
      "position": [8, 0, 12.25],
      "rotation": [0, 0, 0],
      "asset": {
        "id": "low-fence",
        "category": "outdoor",
        "name": "low-fence",
        "thumbnail": "/items/low-fence/thumbnail.webp",
        "src": "/items/low-fence/model.glb",
        "dimensions": [2, 0.8, 0.5],
        "offset": [0, 0.01, 0],
        "rotation": [0, 0, 0],
        "scale": [1, 1, 1]
      }
    },
    "item_6p3y4rva0zv24s1t": {
      "object": "node",
      "id": "item_6p3y4rva0zv24s1t",
      "type": "item",
      "name": "parking-spot",
      "parentId": "level_pojp0mw3qssu110w",
      "visible": true,
      "metadata": {},
      "position": [7.5, 0, 14.25],
      "rotation": [0, 0, 0],
      "asset": {
        "id": "parking-spot",
        "category": "outdoor",
        "name": "parking-spot",
        "thumbnail": "/items/parking-spot/thumbnail.webp",
        "src": "/items/parking-spot/model.glb",
        "dimensions": [5, 1, 2.5],
        "offset": [0, 0, 0.01],
        "rotation": [0, 0, 0],
        "scale": [0.9, 1, 0.78]
      }
    },
    "item_r7idtbazaf6482kq": {
      "object": "node",
      "id": "item_r7idtbazaf6482kq",
      "type": "item",
      "name": "fir-tree",
      "parentId": "level_pojp0mw3qssu110w",
      "visible": true,
      "metadata": {},
      "position": [5.75, 0, 11.75],
      "rotation": [0, 0, 0],
      "asset": {
        "id": "fir-tree",
        "category": "outdoor",
        "name": "fir-tree",
        "thumbnail": "/items/fir-tree/thumbnail.webp",
        "src": "/items/fir-tree/model.glb",
        "dimensions": [1.5, 3.2, 1.5],
        "offset": [-0.09, 0.05, 0.03],
        "rotation": [0, 0, 0],
        "scale": [1, 1, 1]
      }
    },
    "item_0x8hjyork26n4g2m": {
      "object": "node",
      "id": "item_0x8hjyork26n4g2m",
      "type": "item",
      "name": "tree",
      "parentId": "level_pojp0mw3qssu110w",
      "visible": true,
      "metadata": {},
      "position": [12, 0, 16],
      "rotation": [0, 0, 0],
      "asset": {
        "id": "tree",
        "category": "outdoor",
        "name": "tree",
        "thumbnail": "/items/tree/thumbnail.webp",
        "src": "/items/tree/model.glb",
        "dimensions": [4, 5, 4],
        "offset": [0.09, 0.17, 0.06],
        "rotation": [0, 0, 0],
        "scale": [0.65, 0.65, 0.65]
      }
    },
    "item_tmprvzxa85izusug": {
      "object": "node",
      "id": "item_tmprvzxa85izusug",
      "type": "item",
      "name": "pillar",
      "parentId": "level_pojp0mw3qssu110w",
      "visible": true,
      "metadata": {},
      "position": [3.75, 0, 13.75],
      "rotation": [0, 0, 0],
      "asset": {
        "id": "pillar",
        "category": "outdoor",
        "name": "pillar",
        "thumbnail": "/items/pillar/thumbnail.webp",
        "src": "/items/pillar/model.glb",
        "dimensions": [0.5, 1.3, 0.5],
        "offset": [0, 0, 0],
        "rotation": [0, 0, 0],
        "scale": [1, 1, 1]
      }
    },
    "item_g6219gs7mrpg7bzd": {
      "object": "node",
      "id": "item_g6219gs7mrpg7bzd",
      "type": "item",
      "name": "pillar",
      "parentId": "level_pojp0mw3qssu110w",
      "visible": true,
      "metadata": {},
      "position": [6.75, 0, 12.25],
      "rotation": [0, 0, 0],
      "asset": {
        "id": "pillar",
        "category": "outdoor",
        "name": "pillar",
        "thumbnail": "/items/pillar/thumbnail.webp",
        "src": "/items/pillar/model.glb",
        "dimensions": [0.5, 1.3, 0.5],
        "offset": [0, 0, 0],
        "rotation": [0, 0, 0],
        "scale": [1, 1, 1]
      }
    },
    "item_dts7f41ictd7hah4": {
      "object": "node",
      "id": "item_dts7f41ictd7hah4",
      "type": "item",
      "name": "low-fence",
      "parentId": "level_pojp0mw3qssu110w",
      "visible": true,
      "metadata": {},
      "position": [13, 0, 11.25],
      "rotation": [0, 1.5707963267948966, 0],
      "asset": {
        "id": "low-fence",
        "category": "outdoor",
        "name": "low-fence",
        "thumbnail": "/items/low-fence/thumbnail.webp",
        "src": "/items/low-fence/model.glb",
        "dimensions": [2, 0.8, 0.5],
        "offset": [0, 0.01, 0],
        "rotation": [0, 0, 0],
        "scale": [1, 1, 1]
      }
    },
    "item_in5trmidft4sglhx": {
      "object": "node",
      "id": "item_in5trmidft4sglhx",
      "type": "item",
      "name": "low-fence",
      "parentId": "level_pojp0mw3qssu110w",
      "visible": true,
      "metadata": {},
      "position": [13, 0, 9.25],
      "rotation": [0, 1.5707963267948966, 0],
      "asset": {
        "id": "low-fence",
        "category": "outdoor",
        "name": "low-fence",
        "thumbnail": "/items/low-fence/thumbnail.webp",
        "src": "/items/low-fence/model.glb",
        "dimensions": [2, 0.8, 0.5],
        "offset": [0, 0.01, 0],
        "rotation": [0, 0, 0],
        "scale": [1, 1, 1]
      }
    },
    "zone_c7k2ssvmbv5d1xlm": {
      "object": "node",
      "id": "zone_c7k2ssvmbv5d1xlm",
      "type": "zone",
      "name": "Relax Zone",
      "parentId": "level_pojp0mw3qssu110w",
      "visible": true,
      "metadata": {},
      "polygon": [
        [3, 15.5],
        [3, 0],
        [9, 0],
        [9, 6],
        [13, 6],
        [16, 6],
        [16, 15.5]
      ],
      "color": "#22c55e",
      "camera": {
        "position": [
          -6.654332076100337, 17.996846152830106, 22.501743052051737
        ],
        "target": [
          5.317880378107912, -2.779128022959309e-17, 10.080522989431769
        ],
        "mode": "perspective"
      }
    },
    "item_0e9paq67kdbm5ux0": {
      "object": "node",
      "id": "item_0e9paq67kdbm5ux0",
      "type": "item",
      "name": "high-fence",
      "parentId": "level_pojp0mw3qssu110w",
      "visible": true,
      "metadata": {},
      "position": [17, 0, 6.25],
      "rotation": [0, 0, 0],
      "asset": {
        "id": "high-fence",
        "category": "outdoor",
        "name": "high-fence",
        "thumbnail": "/items/high-fence/thumbnail.webp",
        "src": "/items/high-fence/model.glb",
        "dimensions": [4, 4.1, 0.5],
        "offset": [0, 0.01, 0],
        "rotation": [0, 0, 0],
        "scale": [1, 1, 1]
      }
    },
    "item_iyu7knyxqe82c3yg": {
      "object": "node",
      "id": "item_iyu7knyxqe82c3yg",
      "type": "item",
      "name": "high-fence",
      "parentId": "level_pojp0mw3qssu110w",
      "visible": true,
      "metadata": {},
      "position": [21, 0, 6.25],
      "rotation": [0, 0, 0],
      "asset": {
        "id": "high-fence",
        "category": "outdoor",
        "name": "high-fence",
        "thumbnail": "/items/high-fence/thumbnail.webp",
        "src": "/items/high-fence/model.glb",
        "dimensions": [4, 4.1, 0.5],
        "offset": [0, 0.01, 0],
        "rotation": [0, 0, 0],
        "scale": [1, 1, 1]
      }
    },
    "item_nwe34qk8vzs1vag7": {
      "object": "node",
      "id": "item_nwe34qk8vzs1vag7",
      "type": "item",
      "name": "tree",
      "parentId": "level_pojp0mw3qssu110w",
      "visible": true,
      "metadata": {},
      "position": [16.5, 0, 9],
      "rotation": [0, 0, 0],
      "asset": {
        "id": "tree",
        "category": "outdoor",
        "name": "tree",
        "thumbnail": "/items/tree/thumbnail.webp",
        "src": "/items/tree/model.glb",
        "dimensions": [4, 5, 4],
        "offset": [0.09, 0.17, 0.06],
        "rotation": [0, 0, 0],
        "scale": [0.65, 0.65, 0.65]
      }
    },
    "item_oay55zmjjo76s1fs": {
      "object": "node",
      "id": "item_oay55zmjjo76s1fs",
      "type": "item",
      "name": "High Fence",
      "parentId": "level_pojp0mw3qssu110w",
      "visible": true,
      "metadata": {},
      "position": [23, 0, 4.25],
      "rotation": [0, 1.5707963267948966, 0],
      "asset": {
        "id": "high-fence",
        "category": "outdoor",
        "name": "High Fence",
        "thumbnail": "/items/high-fence/thumbnail.webp",
        "src": "/items/high-fence/model.glb",
        "dimensions": [4, 4.1, 0.5],
        "offset": [0, 0.01, 0],
        "rotation": [0, 0, 0],
        "scale": [1, 1, 1]
      }
    },
    "item_g060bhthvcra992w": {
      "object": "node",
      "id": "item_g060bhthvcra992w",
      "type": "item",
      "name": "Medium Fence",
      "parentId": "level_pojp0mw3qssu110w",
      "visible": true,
      "metadata": {},
      "position": [23, 0, -1.25],
      "rotation": [0, 0, 0],
      "asset": {
        "id": "medium-fence",
        "category": "outdoor",
        "name": "Medium Fence",
        "thumbnail": "/items/medium-fence/thumbnail.webp",
        "src": "/items/medium-fence/model.glb",
        "dimensions": [2, 2, 0.5],
        "offset": [0, 0.01, 0],
        "rotation": [0, 0, 0],
        "scale": [0.49, 0.49, 0.49]
      }
    },
    "item_38kt7s45alt2vrjg": {
      "object": "node",
      "id": "item_38kt7s45alt2vrjg",
      "type": "item",
      "name": "Medium Fence",
      "parentId": "level_pojp0mw3qssu110w",
      "visible": true,
      "metadata": {},
      "position": [22, 0, -2.25],
      "rotation": [0, 1.5707963267948966, 0],
      "asset": {
        "id": "medium-fence",
        "category": "outdoor",
        "name": "Medium Fence",
        "thumbnail": "/items/medium-fence/thumbnail.webp",
        "src": "/items/medium-fence/model.glb",
        "dimensions": [2, 2, 0.5],
        "offset": [0, 0.01, 0],
        "rotation": [0, 0, 0],
        "scale": [0.49, 0.49, 0.49]
      }
    },
    "item_n10cp9ke7n9hxl96": {
      "object": "node",
      "id": "item_n10cp9ke7n9hxl96",
      "type": "item",
      "name": "Low Fence",
      "parentId": "level_pojp0mw3qssu110w",
      "visible": true,
      "metadata": {},
      "position": [23, 0, -4.75],
      "rotation": [0, 0, 0],
      "asset": {
        "id": "low-fence",
        "category": "outdoor",
        "name": "Low Fence",
        "thumbnail": "/items/low-fence/thumbnail.webp",
        "src": "/items/low-fence/model.glb",
        "dimensions": [2, 0.8, 0.5],
        "offset": [0, 0.01, 0],
        "rotation": [0, 0, 0],
        "scale": [1, 1, 1]
      }
    },
    "item_nt5fxip4a03cmaoi": {
      "object": "node",
      "id": "item_nt5fxip4a03cmaoi",
      "type": "item",
      "name": "Low Fence",
      "parentId": "level_pojp0mw3qssu110w",
      "visible": true,
      "metadata": {},
      "position": [22, 0, -5.75],
      "rotation": [0, 1.5707963267948966, 0],
      "asset": {
        "id": "low-fence",
        "category": "outdoor",
        "name": "Low Fence",
        "thumbnail": "/items/low-fence/thumbnail.webp",
        "src": "/items/low-fence/model.glb",
        "dimensions": [2, 0.8, 0.5],
        "offset": [0, 0.01, 0],
        "rotation": [0, 0, 0],
        "scale": [1, 1, 1]
      }
    },
    "item_1quoil3ytsuuarni": {
      "object": "node",
      "id": "item_1quoil3ytsuuarni",
      "type": "item",
      "name": "High Fence",
      "parentId": "level_pojp0mw3qssu110w",
      "visible": true,
      "metadata": {},
      "position": [15, 0, 4.25],
      "rotation": [0, 1.5707963267948966, 0],
      "asset": {
        "id": "high-fence",
        "category": "outdoor",
        "name": "High Fence",
        "thumbnail": "/items/high-fence/thumbnail.webp",
        "src": "/items/high-fence/model.glb",
        "dimensions": [4, 4.1, 0.5],
        "offset": [0, 0.01, 0],
        "rotation": [0, 0, 0],
        "scale": [1, 1, 1]
      }
    },
    "item_e1w89kkg2pql1v45": {
      "object": "node",
      "id": "item_e1w89kkg2pql1v45",
      "type": "item",
      "name": "Medium Fence",
      "parentId": "level_pojp0mw3qssu110w",
      "visible": true,
      "metadata": {},
      "position": [17, 0, 0.25],
      "rotation": [0, 1.5707963267948966, 0],
      "asset": {
        "id": "medium-fence",
        "category": "outdoor",
        "name": "Medium Fence",
        "thumbnail": "/items/medium-fence/thumbnail.webp",
        "src": "/items/medium-fence/model.glb",
        "dimensions": [2, 2, 0.5],
        "offset": [0, 0.01, 0],
        "rotation": [0, 0, 0],
        "scale": [0.49, 0.49, 0.49]
      }
    },
    "item_3akotmiffzdr8ule": {
      "object": "node",
      "id": "item_3akotmiffzdr8ule",
      "type": "item",
      "name": "Medium Fence",
      "parentId": "level_pojp0mw3qssu110w",
      "visible": true,
      "metadata": {},
      "position": [17, 0, -1.75],
      "rotation": [0, 1.5707963267948966, 0],
      "asset": {
        "id": "medium-fence",
        "category": "outdoor",
        "name": "Medium Fence",
        "thumbnail": "/items/medium-fence/thumbnail.webp",
        "src": "/items/medium-fence/model.glb",
        "dimensions": [2, 2, 0.5],
        "offset": [0, 0.01, 0],
        "rotation": [0, 0, 0],
        "scale": [0.49, 0.49, 0.49]
      }
    },
    "item_1b3tinfswueb6gr8": {
      "object": "node",
      "id": "item_1b3tinfswueb6gr8",
      "type": "item",
      "name": "Medium Fence",
      "parentId": "level_pojp0mw3qssu110w",
      "visible": true,
      "metadata": {},
      "position": [17, 0, -4.75],
      "rotation": [0, 4.71238898038469, 0],
      "asset": {
        "id": "medium-fence",
        "category": "outdoor",
        "name": "Medium Fence",
        "thumbnail": "/items/medium-fence/thumbnail.webp",
        "src": "/items/medium-fence/model.glb",
        "dimensions": [2, 2, 0.5],
        "offset": [0, 0.01, 0],
        "rotation": [0, 0, 0],
        "scale": [0.49, 0.49, 0.49]
      }
    },
    "item_y164oe3lxfx9qefg": {
      "object": "node",
      "id": "item_y164oe3lxfx9qefg",
      "type": "item",
      "name": "Medium Fence",
      "parentId": "level_pojp0mw3qssu110w",
      "visible": true,
      "metadata": {},
      "position": [17, 0, -6.75],
      "rotation": [0, 4.71238898038469, 0],
      "asset": {
        "id": "medium-fence",
        "category": "outdoor",
        "name": "Medium Fence",
        "thumbnail": "/items/medium-fence/thumbnail.webp",
        "src": "/items/medium-fence/model.glb",
        "dimensions": [2, 2, 0.5],
        "offset": [0, 0.01, 0],
        "rotation": [0, 0, 0],
        "scale": [0.49, 0.49, 0.49]
      }
    },
    "roof_ui8zhim41alg6lq4": {
      "object": "node",
      "id": "roof_ui8zhim41alg6lq4",
      "type": "roof",
      "name": "Roof 2",
      "parentId": "level_pojp0mw3qssu110w",
      "visible": true,
      "metadata": {},
      "position": [1, 0, -5.5],
      "rotation": 0,
      "length": 0.5,
      "height": 1.5,
      "leftWidth": 12.1,
      "rightWidth": 1
    },
    "guide_acs9nzz19rm4vl2c": {
      "object": "node",
      "id": "guide_acs9nzz19rm4vl2c",
      "type": "guide",
      "name": "FaceIt0703_FaceItProductPage.png",
      "parentId": "level_pojp0mw3qssu110w",
      "visible": true,
      "metadata": {},
      "url": "asset://1e66ba17-99d2-4c5c-ad2b-00dff438b6a7",
      "position": [0, 0, 0],
      "rotation": [0, 0, 0],
      "scale": 1,
      "opacity": 51
    }
  },
  "rootNodeIds": ["building_bv4ilcjivnxn8wkd"]
}


================================================
FILE: apps/editor/public/items/kitchen-cabinet/model.glb
================================================
[File too large to display: 10.7 MB]

================================================
FILE: apps/editor/public/items/small-kitchen-cabinet/model.glb
================================================
[File too large to display: 10.7 MB]

================================================
FILE: apps/editor/tsconfig.json
================================================
{
  "extends": "@pascal/typescript-config/nextjs.json",
  "compilerOptions": {
    "plugins": [
      {
        "name": "next"
      }
    ],
    "paths": {
      "@/*": ["./*"]
    }
  },
  "include": [
    "**/*.ts",
    "**/*.tsx",
    "next-env.d.ts",
    "next.config.js",
    ".next/types/**/*.ts"
  ],
  "exclude": ["node_modules"],
  "references": [
    { "path": "../../packages/core" },
    { "path": "../../packages/viewer" }
  ]
}


================================================
FILE: biome.jsonc
================================================
{
  "$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
  "vcs": {
    "enabled": true,
    "clientKind": "git",
    "useIgnoreFile": true
  },
  "assist": {
    "actions": {
      "source": {
        "organizeImports": "on"
      }
    }
  },
  "formatter": {
    "indentStyle": "space",
    "indentWidth": 2,
    "lineWidth": 100
  },
  "javascript": {
    "formatter": {
      "semicolons": "asNeeded",
      "trailingCommas": "all",
      "quoteStyle": "single",
      "jsxQuoteStyle": "double"
    }
  },
  "linter": {
    "rules": {
      "style": {
        "useDefaultSwitchClause": "off",
        "useConsistentTypeDefinitions": "off",
        "useAtIndex": "off",
        "useConsistentArrayType": "off",
        "noNestedTernary": "off",
        "useBlockStatements": "off",
        "noNonNullAssertion": "off",
        "useConst": "off",
        "noMagicNumbers": "off"
      },
      "performance": {
        "noNamespaceImport": "off",
        "noImgElement": "off",
        "noBarrelFile": "off",
        "noDelete": "off"
      },
      "suspicious": {
        "noEmptyBlockStatements": "off",
        "noImplicitAnyLet": "off",
        "noGlobalIsNan": "off",
        "noExplicitAny": "off",
        "useAwait": "off",
        "noConsole": "off",
        "noArrayIndexKey": "off",
        "noEvolvingTypes": "off",
        "noDocumentCookie": "off"
      },
      "complexity": {
        "noExcessiveCognitiveComplexity": "off",
        "noForEach": "off",
        "noBannedTypes": "off",
        "noUselessFragments": "off",
        "useLiteralKeys": "off"
      },
      "correctness": {
        "noUnusedVariables": "off",
        "noUnusedFunctionParameters": "off",
        "noUnusedImports": {
          "level": "warn",
          "fix": "safe"
        },
        "useExhaustiveDependencies": "info",
        "noPrecisionLoss": "off"
      },
      "a11y": {
        "noSvgWithoutTitle": "off",
        "useSemanticElements": "off",
        "noLabelWithoutControl": "off",
        "useKeyWithClickEvents": "off",
        "noStaticElementInteractions": "off",
        "noNoninteractiveElementInteractions": "off",
        "useButtonType": "off"
      },
      "nursery": {
        "noShadow": "off"
      },
      "security": {
        "noDangerouslySetInnerHtml": "info"
      }
    }
  },
  "files": {
    "ignoreUnknown": true,
    "includes": [
      "packages/**/*.ts",
      "packages/**/*.tsx",
      "packages/**/*.js",
      "packages/**/*.jsx",
      "packages/**/*.json",
      "packages/**/*.css",
      "packages/**/*.md",
      "packages/**/*.mdx",
      "apps/**/*.ts",
      "apps/**/*.tsx",
      "apps/**/*.js",
      "apps/**/*.jsx",
      "!**/node_modules",
      "!**/.next",
      "!**/dist",
      "!**/build",
      "!**/public",
      "!**/components/ui"
    ]
  },
  "overrides": [
    {
      "includes": ["packages/editor/components/debug/react-scan.tsx"],
      "assist": {
        "actions": {
          "source": {
            "organizeImports": "off"
          }
        }
      }
    }
  ]
}


================================================
FILE: package.json
================================================
{
  "name": "editor",
  "private": true,
  "scripts": {
    "build": "turbo run build",
    "dev": "set -a && . ./.env 2>/dev/null; set +a; turbo run dev --env-mode=loose",
    "lint": "biome lint",
    "lint:fix": "biome lint --write",
    "format": "biome format --write",
    "format:check": "biome format",
    "check": "biome check",
    "check:fix": "biome check --write",
    "check-types": "turbo run check-types",
    "kill": "lsof -ti:3002 | xargs kill -9 2>/dev/null || echo 'No processes found on port 3002'",
    "release": "gh workflow run release.yml -f package=both -f bump=patch",
    "release:viewer": "gh workflow run release.yml -f package=viewer -f bump=patch",
    "release:core": "gh workflow run release.yml -f package=core -f bump=patch",
    "release:minor": "gh workflow run release.yml -f package=both -f bump=minor",
    "release:major": "gh workflow run release.yml -f package=both -f bump=major"
  },
  "devDependencies": {
    "@biomejs/biome": "^2.4.6",
    "dotenv-cli": "^11.0.0",
    "turbo": "^2.8.15",
    "typescript": "5.9.3",
    "ultracite": "^7.2.5"
  },
  "engines": {
    "node": ">=18"
  },
  "packageManager": "bun@1.3.0",
  "workspaces": [
    "apps/*",
    "packages/*",
    "tooling/*"
  ]
}


================================================
FILE: packages/core/README.md
================================================
# @pascal-app/core

Core library for Pascal 3D building editor.

## Installation

```bash
npm install @pascal-app/core
```

## Peer Dependencies

```bash
npm install react three @react-three/fiber @react-three/drei
```

## What's Included

- **Node Schemas** - Zod schemas for all building primitives (walls, slabs, items, etc.)
- **Scene State** - Zustand store with IndexedDB persistence and undo/redo
- **Systems** - Geometry generation for walls, floors, ceilings, roofs
- **Scene Registry** - Fast lookup from node IDs to Three.js objects
- **Spatial Grid** - Collision detection and placement validation
- **Event Bus** - Typed event emitter for inter-component communication
- **Asset Storage** - IndexedDB-based file storage for user-uploaded assets

## Usage

```typescript
import { useScene, WallNode, ItemNode } from '@pascal-app/core'

// Create a wall
const wall = WallNode.parse({
  points: [[0, 0], [5, 0]],
  height: 3,
  thickness: 0.2,
})

useScene.getState().createNode(wall, parentLevelId)

// Subscribe to scene changes
function MyComponent() {
  const nodes = useScene((state) => state.nodes)
  const walls = Object.values(nodes).filter(n => n.type === 'wall')

  return <div>Total walls: {walls.length}</div>
}
```

## Node Types

- `SiteNode` - Root container
- `BuildingNode` - Building within a site
- `LevelNode` - Floor level
- `WallNode` - Vertical wall with optional openings
- `SlabNode` - Floor slab
- `CeilingNode` - Ceiling surface
- `RoofNode` - Roof geometry
- `ZoneNode` - Spatial zone/room
- `ItemNode` - Furniture, fixtures, appliances
- `ScanNode` - 3D scan reference
- `GuideNode` - 2D guide image reference

## Systems

Systems process dirty nodes each frame to update geometry:

- `WallSystem` - Wall geometry with mitering and CSG cutouts
- `SlabSystem` - Floor polygon generation
- `CeilingSystem` - Ceiling geometry
- `RoofSystem` - Roof generation
- `ItemSystem` - Item positioning on walls/ceilings/floors

## License

MIT


================================================
FILE: packages/core/package.json
================================================
{
  "name": "@pascal-app/core",
  "version": "0.3.3",
  "description": "Core library for Pascal 3D building editor",
  "type": "module",
  "main": "./dist/index.js",
  "types": "./dist/index.d.ts",
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.js",
      "default": "./dist/index.js"
    }
  },
  "files": [
    "dist",
    "README.md"
  ],
  "scripts": {
    "build": "tsc --build",
    "dev": "tsc --build --watch",
    "prepublishOnly": "npm run build"
  },
  "peerDependencies": {
    "@react-three/drei": "^10",
    "@react-three/fiber": "^9",
    "react": "^18 || ^19",
    "three": "^0.182"
  },
  "dependencies": {
    "dedent": "^1.7.1",
    "idb-keyval": "^6.2.2",
    "mitt": "^3.0.1",
    "nanoid": "^5.1.6",
    "three-bvh-csg": "^0.0.18",
    "three-mesh-bvh": "^0.9.8",
    "zod": "^4.3.5",
    "zundo": "^2.3.0",
    "zustand": "^5"
  },
  "devDependencies": {
    "@pascal/typescript-config": "*",
    "@types/react": "^19.2.2",
    "typescript": "5.9.3",
    "@types/three": "^0.183.0"
  },
  "keywords": [
    "3d",
    "building",
    "editor",
    "architecture",
    "webgpu",
    "three.js"
  ],
  "repository": {
    "type": "git",
    "url": "https://github.com/pascalorg/editor.git",
    "directory": "packages/core"
  },
  "license": "MIT",
  "homepage": "https://github.com/pascalorg/editor/tree/main/packages/core#readme",
  "bugs": "https://github.com/pascalorg/editor/issues"
}


================================================
FILE: packages/core/src/events/bus.ts
================================================
import type { ThreeEvent } from '@react-three/fiber'
import mitt from 'mitt'
import type {
  BuildingNode,
  CeilingNode,
  DoorNode,
  ItemNode,
  LevelNode,
  RoofNode,
  RoofSegmentNode,
  SiteNode,
  SlabNode,
  StairNode,
  StairSegmentNode,
  WallNode,
  WindowNode,
  ZoneNode,
} from '../schema'
import type { AnyNode } from '../schema/types'

// Base event interfaces
export interface GridEvent {
  position: [number, number, number]
  nativeEvent: ThreeEvent<PointerEvent>
}

export interface NodeEvent<T extends AnyNode = AnyNode> {
  node: T
  position: [number, number, number]
  localPosition: [number, number, number]
  normal?: [number, number, number]
  stopPropagation: () => void
  nativeEvent: ThreeEvent<PointerEvent>
}

export type WallEvent = NodeEvent<WallNode>
export type ItemEvent = NodeEvent<ItemNode>
export type SiteEvent = NodeEvent<SiteNode>
export type BuildingEvent = NodeEvent<BuildingNode>
export type LevelEvent = NodeEvent<LevelNode>
export type ZoneEvent = NodeEvent<ZoneNode>
export type SlabEvent = NodeEvent<SlabNode>
export type CeilingEvent = NodeEvent<CeilingNode>
export type RoofEvent = NodeEvent<RoofNode>
export type RoofSegmentEvent = NodeEvent<RoofSegmentNode>
export type StairEvent = NodeEvent<StairNode>
export type StairSegmentEvent = NodeEvent<StairSegmentNode>
export type WindowEvent = NodeEvent<WindowNode>
export type DoorEvent = NodeEvent<DoorNode>

// Event suffixes - exported for use in hooks
export const eventSuffixes = [
  'click',
  'move',
  'enter',
  'leave',
  'pointerdown',
  'pointerup',
  'context-menu',
  'double-click',
] as const

export type EventSuffix = (typeof eventSuffixes)[number]

type NodeEvents<T extends string, E> = {
  [K in `${T}:${EventSuffix}`]: E
}

type GridEvents = {
  [K in `grid:${EventSuffix}`]: GridEvent
}

export interface CameraControlEvent {
  nodeId: AnyNode['id']
}

export interface ThumbnailGenerateEvent {
  projectId: string
}

type CameraControlEvents = {
  'camera-controls:view': CameraControlEvent
  'camera-controls:focus': CameraControlEvent
  'camera-controls:capture': CameraControlEvent
  'camera-controls:top-view': undefined
  'camera-controls:orbit-cw': undefined
  'camera-controls:orbit-ccw': undefined
  'camera-controls:generate-thumbnail': ThumbnailGenerateEvent
}

type ToolEvents = {
  'tool:cancel': undefined
}

type PresetEvents = {
  'preset:generate-thumbnail': { presetId: string; nodeId: string }
  'preset:thumbnail-updated': { presetId: string; thumbnailUrl: string }
}

type EditorEvents = GridEvents &
  NodeEvents<'wall', WallEvent> &
  NodeEvents<'item', ItemEvent> &
  NodeEvents<'site', SiteEvent> &
  NodeEvents<'building', BuildingEvent> &
  NodeEvents<'level', LevelEvent> &
  NodeEvents<'zone', ZoneEvent> &
  NodeEvents<'slab', SlabEvent> &
  NodeEvents<'ceiling', CeilingEvent> &
  NodeEvents<'roof', RoofEvent> &
  NodeEvents<'roof-segment', RoofSegmentEvent> &
  NodeEvents<'stair', StairEvent> &
  NodeEvents<'stair-segment', StairSegmentEvent> &
  NodeEvents<'window', WindowEvent> &
  NodeEvents<'door', DoorEvent> &
  CameraControlEvents &
  ToolEvents &
  PresetEvents

export const emitter = mitt<EditorEvents>()


================================================
FILE: packages/core/src/hooks/scene-registry/scene-registry.ts
================================================
import { useLayoutEffect } from 'react'
import type * as THREE from 'three'

export const sceneRegistry = {
  // Master lookup: ID -> Object3D
  nodes: new Map<string, THREE.Object3D>(),

  // Categorized lookups: Type -> Set of IDs
  // Using a Set is faster for adding/deleting than an Array
  byType: {
    site: new Set<string>(),
    building: new Set<string>(),
    ceiling: new Set<string>(),
    level: new Set<string>(),
    wall: new Set<string>(),
    item: new Set<string>(),
    slab: new Set<string>(),
    zone: new Set<string>(),
    roof: new Set<string>(),
    'roof-segment': new Set<string>(),
    stair: new Set<string>(),
    'stair-segment': new Set<string>(),
    scan: new Set<string>(),
    guide: new Set<string>(),
    window: new Set<string>(),
    door: new Set<string>(),
  },

  /** Remove all entries. Call when unloading a scene to prevent stale 3D refs. */
  clear() {
    this.nodes.clear()
    for (const set of Object.values(this.byType)) {
      set.clear()
    }
  },
}

export function useRegistry(
  id: string,
  type: keyof typeof sceneRegistry.byType,
  ref: React.RefObject<THREE.Object3D>,
) {
  useLayoutEffect(() => {
    const obj = ref.current
    if (!obj) return

    // 1. Add to master map
    sceneRegistry.nodes.set(id, obj)

    // 2. Add to type-specific set
    sceneRegistry.byType[type].add(id)

    // 4. Cleanup when component unmounts
    return () => {
      sceneRegistry.nodes.delete(id)
      sceneRegistry.byType[type].delete(id)
    }
  }, [id, type, ref])
}


================================================
FILE: packages/core/src/hooks/spatial-grid/spatial-grid-manager.ts
================================================
import type { AnyNode, CeilingNode, ItemNode, SlabNode, WallNode } from '../../schema'
import { getScaledDimensions } from '../../schema'
import { SpatialGrid } from './spatial-grid'
import { WallSpatialGrid } from './wall-spatial-grid'

// ============================================================================
// GEOMETRY HELPERS
// ============================================================================

/**
 * Point-in-polygon test using ray casting algorithm.
 */
export function pointInPolygon(px: number, pz: number, polygon: Array<[number, number]>): boolean {
  let inside = false
  const n = polygon.length
  for (let i = 0, j = n - 1; i < n; j = i++) {
    const xi = polygon[i]![0],
      zi = polygon[i]![1]
    const xj = polygon[j]![0],
      zj = polygon[j]![1]

    if (zi > pz !== zj > pz && px < ((xj - xi) * (pz - zi)) / (zj - zi) + xi) {
      inside = !inside
    }
  }
  return inside
}

/**
 * Compute the 4 XZ footprint corners of an item given its position, dimensions, and Y rotation.
 */
function getItemFootprint(
  position: [number, number, number],
  dimensions: [number, number, number],
  rotation: [number, number, number],
  inset = 0,
): Array<[number, number]> {
  const [x, , z] = position
  const [w, , d] = dimensions
  const yRot = rotation[1]
  const halfW = Math.max(0, w / 2 - inset)
  const halfD = Math.max(0, d / 2 - inset)
  const cos = Math.cos(yRot)
  const sin = Math.sin(yRot)

  return [
    [x + (-halfW * cos + halfD * sin), z + (-halfW * sin - halfD * cos)],
    [x + (halfW * cos + halfD * sin), z + (halfW * sin - halfD * cos)],
    [x + (halfW * cos - halfD * sin), z + (halfW * sin + halfD * cos)],
    [x + (-halfW * cos - halfD * sin), z + (-halfW * sin + halfD * cos)],
  ]
}

/**
 * Test if two line segments (a1->a2) and (b1->b2) intersect.
 */
function segmentsIntersect(
  ax1: number,
  az1: number,
  ax2: number,
  az2: number,
  bx1: number,
  bz1: number,
  bx2: number,
  bz2: number,
): boolean {
  const cross = (ox: number, oz: number, ax: number, az: number, bx: number, bz: number) =>
    (ax - ox) * (bz - oz) - (az - oz) * (bx - ox)

  const d1 = cross(bx1, bz1, bx2, bz2, ax1, az1)
  const d2 = cross(bx1, bz1, bx2, bz2, ax2, az2)
  const d3 = cross(ax1, az1, ax2, az2, bx1, bz1)
  const d4 = cross(ax1, az1, ax2, az2, bx2, bz2)

  if (((d1 > 0 && d2 < 0) || (d1 < 0 && d2 > 0)) && ((d3 > 0 && d4 < 0) || (d3 < 0 && d4 > 0))) {
    return true
  }

  // Collinear touching cases
  const onSeg = (px: number, pz: number, qx: number, qz: number, rx: number, rz: number) =>
    Math.min(px, qx) <= rx &&
    rx <= Math.max(px, qx) &&
    Math.min(pz, qz) <= rz &&
    rz <= Math.max(pz, qz)

  if (d1 === 0 && onSeg(bx1, bz1, bx2, bz2, ax1, az1)) return true
  if (d2 === 0 && onSeg(bx1, bz1, bx2, bz2, ax2, az2)) return true
  if (d3 === 0 && onSeg(ax1, az1, ax2, az2, bx1, bz1)) return true
  if (d4 === 0 && onSeg(ax1, az1, ax2, az2, bx2, bz2)) return true

  return false
}

/**
 * Test if a line segment intersects any edge of a polygon.
 */
function segmentIntersectsPolygon(
  sx1: number,
  sz1: number,
  sx2: number,
  sz2: number,
  polygon: Array<[number, number]>,
): boolean {
  const n = polygon.length
  for (let i = 0; i < n; i++) {
    const j = (i + 1) % n
    if (
      segmentsIntersect(
        sx1,
        sz1,
        sx2,
        sz2,
        polygon[i]![0],
        polygon[i]![1],
        polygon[j]![0],
        polygon[j]![1],
      )
    ) {
      return true
    }
  }
  return false
}

/**
 * Test if an item's footprint overlaps with a polygon.
 * Checks: any item corner inside polygon, or any polygon vertex inside item AABB, or edges intersect.
 */
export function itemOverlapsPolygon(
  position: [number, number, number],
  dimensions: [number, number, number],
  rotation: [number, number, number],
  polygon: Array<[number, number]>,
  inset = 0,
): boolean {
  const corners = getItemFootprint(position, dimensions, rotation, inset)

  // Check if any item corner is inside the polygon
  for (const [cx, cz] of corners) {
    if (pointInPolygon(cx, cz, polygon)) return true
  }

  // Check if any polygon vertex is inside the item footprint
  // (handles case where slab is fully inside a large item)
  for (const [px, pz] of polygon) {
    if (pointInPolygon(px, pz, corners)) return true
  }

  // Check if any item edge intersects any polygon edge
  for (let i = 0; i < 4; i++) {
    const j = (i + 1) % 4
    if (
      segmentIntersectsPolygon(
        corners[i]![0],
        corners[i]![1],
        corners[j]![0],
        corners[j]![1],
        polygon,
      )
    )
      return true
  }

  return false
}

/**
 * Check if wall segment (a) is substantially on polygon edge segment (b).
 * Returns true only if BOTH endpoints of the wall are on or very close to the edge.
 * This prevents walls that just touch one point from being detected.
 */
function segmentsCollinearAndOverlap(
  ax1: number,
  az1: number,
  ax2: number,
  az2: number,
  bx1: number,
  bz1: number,
  bx2: number,
  bz2: number,
): boolean {
  const EPSILON = 1e-6

  // Cross product to check collinearity
  const cross1 = (ax2 - ax1) * (bz1 - az1) - (az2 - az1) * (bx1 - ax1)
  const cross2 = (ax2 - ax1) * (bz2 - az1) - (az2 - az1) * (bx2 - ax1)

  if (Math.abs(cross1) > EPSILON || Math.abs(cross2) > EPSILON) {
    return false // Not collinear
  }

  // Check if a point is on segment b
  const onSegment = (px: number, pz: number, qx: number, qz: number, rx: number, rz: number) =>
    Math.min(px, qx) - EPSILON <= rx &&
    rx <= Math.max(px, qx) + EPSILON &&
    Math.min(pz, qz) - EPSILON <= rz &&
    rz <= Math.max(pz, qz) + EPSILON

  // BOTH endpoints of wall (a) must be on edge (b) for substantial overlap
  const a1OnB = onSegment(bx1, bz1, bx2, bz2, ax1, az1)
  const a2OnB = onSegment(bx1, bz1, bx2, bz2, ax2, az2)

  return a1OnB && a2OnB
}

/**
 * Test if a wall segment overlaps with a polygon.
 * A wall is considered to overlap if:
 * - Its midpoint is inside the polygon (wall crosses through)
 * - At least one endpoint is inside (wall partially or fully in slab)
 * - It's collinear with and overlaps a polygon edge (wall on slab boundary)
 *
 * Note: A wall with just one endpoint touching the edge but the rest outside
 * is NOT considered overlapping (adjacent only).
 */
export function wallOverlapsPolygon(
  start: [number, number],
  end: [number, number],
  polygon: Array<[number, number]>,
): boolean {
  const dx = end[0] - start[0]
  const dz = end[1] - start[1]
  const len = Math.sqrt(dx * dx + dz * dz)

  // Nudge endpoint test points a tiny step inward along the wall direction before
  // testing containment. pointInPolygon (ray casting) produces false positives for
  // points exactly on polygon vertices or edges — specifically the minimum-z corner
  // of an axis-aligned polygon returns "inside" because the ray hits the opposite
  // vertical edge exactly at its base. Nudging by 1e-6 m avoids this: a wall that
  // merely starts at a slab corner and extends outward will have its nudged point
  // clearly outside, while a wall that genuinely starts inside stays inside.
  if (len > 1e-10) {
    const step = Math.min(1e-6, len * 0.01)
    const nx = (dx / len) * step
    const nz = (dz / len) * step
    if (pointInPolygon(start[0] + nx, start[1] + nz, polygon)) return true
    if (pointInPolygon(end[0] - nx, end[1] - nz, polygon)) return true

    // Also nudge perpendicular to the wall (into the slab interior) for walls that
    // lie exactly on the slab boundary. The along-wall nudge keeps points on the
    // boundary where pointInPolygon is unreliable; a perpendicular inward nudge
    // moves the point clearly inside (or outside) the polygon.
    // Sample the wall at 1/4, 1/2, 3/4 positions with a perpendicular nudge.
    const PERP_STEP = 1e-4
    const pnx = (-nz / step) * PERP_STEP // perpendicular left
    const pnz = (nx / step) * PERP_STEP
    for (const t of [0.25, 0.5, 0.75]) {
      const bx = start[0] + dx * t
      const bz = start[1] + dz * t
      if (pointInPolygon(bx + pnx, bz + pnz, polygon)) return true
      if (pointInPolygon(bx - pnx, bz - pnz, polygon)) return true
    }
  }

  // Check if midpoint is inside (catches walls crossing through)
  const midX = (start[0] + end[0]) / 2
  const midZ = (start[1] + end[1]) / 2
  if (pointInPolygon(midX, midZ, polygon)) return true

  // Check if the wall is collinear with and overlaps any polygon edge
  const n = polygon.length
  for (let i = 0; i < n; i++) {
    const j = (i + 1) % n
    const [p1x, p1z] = polygon[i]!
    const [p2x, p2z] = polygon[j]!

    if (segmentsCollinearAndOverlap(start[0], start[1], end[0], end[1], p1x, p1z, p2x, p2z)) {
      return true
    }
  }

  return false
}

export class SpatialGridManager {
  private readonly floorGrids = new Map<string, SpatialGrid>() // levelId -> grid
  private readonly wallGrids = new Map<string, WallSpatialGrid>() // levelId -> wall grid
  private readonly walls = new Map<string, WallNode>() // wallId -> wall data (for length calculations)
  private readonly slabsByLevel = new Map<string, Map<string, SlabNode>>() // levelId -> (slabId -> slab)
  private readonly ceilingGrids = new Map<string, SpatialGrid>() // ceilingId -> grid
  private readonly ceilings = new Map<string, CeilingNode>() // ceilingId -> ceiling data
  private readonly itemCeilingMap = new Map<string, string>() // itemId -> ceilingId (reverse lookup)

  private readonly cellSize: number

  constructor(cellSize = 0.5) {
    this.cellSize = cellSize
  }

  private getFloorGrid(levelId: string): SpatialGrid {
    if (!this.floorGrids.has(levelId)) {
      this.floorGrids.set(levelId, new SpatialGrid({ cellSize: this.cellSize }))
    }
    return this.floorGrids.get(levelId)!
  }

  private getWallGrid(levelId: string): WallSpatialGrid {
    if (!this.wallGrids.has(levelId)) {
      this.wallGrids.set(levelId, new WallSpatialGrid())
    }
    return this.wallGrids.get(levelId)!
  }

  private getWallLength(wallId: string): number {
    const wall = this.walls.get(wallId)
    if (!wall) return 0
    const dx = wall.end[0] - wall.start[0]
    const dy = wall.end[1] - wall.start[1]
    return Math.sqrt(dx * dx + dy * dy)
  }

  private getWallHeight(wallId: string): number {
    const wall = this.walls.get(wallId)
    return wall?.height ?? 2.5 // Default wall height
  }

  private getCeilingGrid(ceilingId: string): SpatialGrid {
    if (!this.ceilingGrids.has(ceilingId)) {
      this.ceilingGrids.set(ceilingId, new SpatialGrid({ cellSize: this.cellSize }))
    }
    return this.ceilingGrids.get(ceilingId)!
  }

  private getSlabMap(levelId: string): Map<string, SlabNode> {
    if (!this.slabsByLevel.has(levelId)) {
      this.slabsByLevel.set(levelId, new Map())
    }
    return this.slabsByLevel.get(levelId)!
  }

  // Called when nodes change
  handleNodeCreated(node: AnyNode, levelId: string) {
    if (node.type === 'slab') {
      this.getSlabMap(levelId).set(node.id, node as SlabNode)
    } else if (node.type === 'ceiling') {
      this.ceilings.set(node.id, node as CeilingNode)
    } else if (node.type === 'wall') {
      const wall = node as WallNode
      this.walls.set(wall.id, wall)
    } else if (node.type === 'item') {
      const item = node as ItemNode
      if (item.asset.attachTo === 'wall' || item.asset.attachTo === 'wall-side') {
        // Wall-attached item - use parentId as the wall ID
        const wallId = item.parentId
        if (wallId && this.walls.has(wallId)) {
          const wallLength = this.getWallLength(wallId)
          if (wallLength > 0) {
            const [width, height] = getScaledDimensions(item)
            const halfW = width / wallLength / 2
            // Calculate t from local X position (position[0] is distance along wall)
            const t = item.position[0] / wallLength
            // position[1] is the bottom of the item
            this.getWallGrid(levelId).insert({
              itemId: item.id,
              wallId,
              tStart: t - halfW,
              tEnd: t + halfW,
              yStart: item.position[1],
              yEnd: item.position[1] + height,
              attachType: item.asset.attachTo as 'wall' | 'wall-side',
              side: item.side,
            })
          }
        }
      } else if (item.asset.attachTo === 'ceiling') {
        // Ceiling item - use parentId as the ceiling ID
        const ceilingId = item.parentId
        if (ceilingId && this.ceilings.has(ceilingId)) {
          this.getCeilingGrid(ceilingId).insert(
            item.id,
            item.position,
            getScaledDimensions(item),
            item.rotation,
          )
          this.itemCeilingMap.set(item.id, ceilingId)
        }
      } else if (!item.asset.attachTo) {
        // Floor item
        this.getFloorGrid(levelId).insert(
          item.id,
          item.position,
          getScaledDimensions(item),
          item.rotation,
        )
      }
    }
  }

  handleNodeUpdated(node: AnyNode, levelId: string) {
    if (node.type === 'slab') {
      this.getSlabMap(levelId).set(node.id, node as SlabNode)
    } else if (node.type === 'ceiling') {
      this.ceilings.set(node.id, node as CeilingNode)
    } else if (node.type === 'wall') {
      const wall = node as WallNode
      this.walls.set(wall.id, wall)
    } else if (node.type === 'item') {
      const item = node as ItemNode
      if (item.asset.attachTo === 'wall' || item.asset.attachTo === 'wall-side') {
        // Remove old placement and re-insert
        this.getWallGrid(levelId).removeByItemId(item.id)
        const wallId = item.parentId
        if (wallId && this.walls.has(wallId)) {
          const wallLength = this.getWallLength(wallId)
          if (wallLength > 0) {
            const [width, height] = getScaledDimensions(item)
            const halfW = width / wallLength / 2
            // Calculate t from local X position (position[0] is distance along wall)
            const t = item.position[0] / wallLength
            // position[1] is the bottom of the item
            this.getWallGrid(levelId).insert({
              itemId: item.id,
              wallId,
              tStart: t - halfW,
              tEnd: t + halfW,
              yStart: item.position[1],
              yEnd: item.position[1] + height,
              attachType: item.asset.attachTo as 'wall' | 'wall-side',
              side: item.side,
            })
          }
        }
      } else if (item.asset.attachTo === 'ceiling') {
        // Remove from old ceiling grid
        const oldCeilingId = this.itemCeilingMap.get(item.id)
        if (oldCeilingId) {
          this.getCeilingGrid(oldCeilingId).remove(item.id)
          this.itemCeilingMap.delete(item.id)
        }
        // Insert into new ceiling grid
        const ceilingId = item.parentId
        if (ceilingId && this.ceilings.has(ceilingId)) {
          this.getCeilingGrid(ceilingId).insert(
            item.id,
            item.position,
            getScaledDimensions(item),
            item.rotation,
          )
          this.itemCeilingMap.set(item.id, ceilingId)
        }
      } else if (!item.asset.attachTo) {
        this.getFloorGrid(levelId).update(
          item.id,
          item.position,
          getScaledDimensions(item),
          item.rotation,
        )
      }
    }
  }

  handleNodeDeleted(nodeId: string, nodeType: string, levelId: string) {
    if (nodeType === 'slab') {
      this.getSlabMap(levelId).delete(nodeId)
    } else if (nodeType === 'ceiling') {
      this.ceilings.delete(nodeId)
      this.ceilingGrids.delete(nodeId)
    } else if (nodeType === 'wall') {
      this.walls.delete(nodeId)
      // Remove all items attached to this wall from the spatial grid
      const removedItemIds = this.getWallGrid(levelId).removeWall(nodeId)
      return removedItemIds // Caller can use this to delete the items from scene
    } else if (nodeType === 'item') {
      this.getFloorGrid(levelId).remove(nodeId)
      this.getWallGrid(levelId).removeByItemId(nodeId)
      // Also clean up ceiling grid
      const oldCeilingId = this.itemCeilingMap.get(nodeId)
      if (oldCeilingId) {
        this.getCeilingGrid(oldCeilingId).remove(nodeId)
        this.itemCeilingMap.delete(nodeId)
      }
    }
    return []
  }

  // Query methods
  canPlaceOnFloor(
    levelId: string,
    position: [number, number, number],
    dimensions: [number, number, number],
    rotation: [number, number, number],
    ignoreIds?: string[],
  ) {
    const grid = this.getFloorGrid(levelId)
    return grid.canPlace(position, dimensions, rotation, ignoreIds)
  }

  /**
   * Check if an item can be placed on a wall
   * @param levelId - the level containing the wall
   * @param wallId - the wall to check
   * @param localX - X position in wall-local space (distance from wall start)
   * @param localY - Y position (height from floor)
   * @param dimensions - item dimensions [width, height, depth]
   * @param attachType - 'wall' (needs both sides) or 'wall-side' (needs one side)
   * @param side - which side for 'wall-side' items
   * @param ignoreIds - item IDs to ignore in collision check
   */
  canPlaceOnWall(
    levelId: string,
    wallId: string,
    localX: number,
    localY: number,
    dimensions: [number, number, number],
    attachType: 'wall' | 'wall-side' = 'wall',
    side?: 'front' | 'back',
    ignoreIds?: string[],
  ) {
    const wallLength = this.getWallLength(wallId)
    if (wallLength === 0) {
      return { valid: false, conflictIds: [] }
    }
    const wallHeight = this.getWallHeight(wallId)
    // Convert local X position to parametric t (0-1)
    const tCenter = localX / wallLength
    const [itemWidth, itemHeight] = dimensions
    return this.getWallGrid(levelId).canPlaceOnWall(
      wallId,
      wallLength,
      wallHeight,
      tCenter,
      itemWidth,
      localY,
      itemHeight,
      attachType,
      side,
      ignoreIds,
    )
  }

  getWallForItem(levelId: string, itemId: string): string | undefined {
    return this.getWallGrid(levelId).getWallForItem(itemId)
  }

  /**
   * Get the total slab elevation at a given (x, z) position on a level.
   * Returns the highest slab elevation if the point is inside any slab polygon (but not in any holes), otherwise 0.
   */
  getSlabElevationAt(levelId: string, x: number, z: number): number {
    const slabMap = this.slabsByLevel.get(levelId)
    if (!slabMap) return 0

    let maxElevation = 0
    for (const slab of slabMap.values()) {
      if (slab.polygon.length >= 3 && pointInPolygon(x, z, slab.polygon)) {
        // Check if point is in any hole
        let inHole = false
        const holes = slab.holes || []
        for (const hole of holes) {
          if (hole.length >= 3 && pointInPolygon(x, z, hole)) {
            inHole = true
            break
          }
        }

        if (!inHole) {
          const elevation = slab.elevation ?? 0.05
          if (elevation > maxElevation) {
            maxElevation = elevation
          }
        }
      }
    }
    return maxElevation
  }

  /**
   * Get the slab elevation for an item using its full footprint (bounding box).
   * Checks if any part of the item's rotated footprint overlaps with any slab polygon (excluding holes).
   * Returns the highest overlapping slab elevation, or 0 if none.
   */
  getSlabElevationForItem(
    levelId: string,
    position: [number, number, number],
    dimensions: [number, number, number],
    rotation: [number, number, number],
  ): number {
    const slabMap = this.slabsByLevel.get(levelId)
    if (!slabMap) return 0

    let maxElevation = Number.NEGATIVE_INFINITY
    for (const slab of slabMap.values()) {
      if (
        slab.polygon.length >= 3 &&
        itemOverlapsPolygon(position, dimensions, rotation, slab.polygon, 0.01)
      ) {
        // Check if item is entirely within a hole (if so, ignore this slab)
        // We consider it entirely in a hole if the item center is in the hole
        let inHole = false
        const [cx, , cz] = position
        const holes = slab.holes || []
        for (const hole of holes) {
          if (hole.length >= 3 && pointInPolygon(cx, cz, hole)) {
            inHole = true
            break
          }
        }

        if (!inHole) {
          const elevation = slab.elevation ?? 0.05
          if (elevation > maxElevation) {
            maxElevation = elevation
          }
        }
      }
    }
    return maxElevation === Number.NEGATIVE_INFINITY ? 0 : maxElevation
  }

  /**
   * Get the slab elevation for a wall by checking if it overlaps with any slab polygon (excluding holes).
   * Uses wallOverlapsPolygon which handles edge cases (points on boundary, collinear segments).
   * Returns the highest slab elevation found, or 0 if none.
   */
  getSlabElevationForWall(levelId: string, start: [number, number], end: [number, number]): number {
    const slabMap = this.slabsByLevel.get(levelId)
    if (!slabMap) return 0

    let maxElevation = Number.NEGATIVE_INFINITY
    for (const slab of slabMap.values()) {
      if (slab.polygon.length < 3) continue
      if (!wallOverlapsPolygon(start, end, slab.polygon)) continue

      const holes = slab.holes || []
      if (holes.length === 0) {
        // No holes: wall is on this slab
        const elevation = slab.elevation ?? 0.05
        if (elevation > maxElevation) maxElevation = elevation
        continue
      }

      // Sample multiple points along the wall to check whether any portion lies on
      // solid slab (not inside any hole). Checking only the midpoint fails when the
      // midpoint falls in a staircase hole but the wall's endpoints are on solid slab.
      const dx = end[0] - start[0]
      const dz = end[1] - start[1]
      let hasValidPoint = false
      for (const t of [0, 0.25, 0.5, 0.75, 1]) {
        const px = start[0] + dx * t
        const pz = start[1] + dz * t
        let inHole = false
        for (const hole of holes) {
          if (hole.length >= 3 && pointInPolygon(px, pz, hole)) {
            inHole = true
            break
          }
        }
        if (!inHole) {
          hasValidPoint = true
          break
        }
      }

      if (hasValidPoint) {
        const elevation = slab.elevation ?? 0.05
        if (elevation > maxElevation) maxElevation = elevation
      }
    }
    return maxElevation === Number.NEGATIVE_INFINITY ? 0 : maxElevation
  }

  /**
   * Check if an item can be placed on a ceiling.
   * Validates that the footprint is within the ceiling polygon (but not in any holes) and doesn't overlap other ceiling items.
   */
  canPlaceOnCeiling(
    ceilingId: string,
    position: [number, number, number],
    dimensions: [number, number, number],
    rotation: [number, number, number],
    ignoreIds?: string[],
  ): { valid: boolean; conflictIds: string[] } {
    const ceiling = this.ceilings.get(ceilingId)
    if (!ceiling || ceiling.polygon.length < 3) {
      return { valid: false, conflictIds: [] }
    }

    // Check that the item footprint is entirely within the ceiling polygon
    const corners = getItemFootprint(position, dimensions, rotation)
    for (const [cx, cz] of corners) {
      if (!pointInPolygon(cx, cz, ceiling.polygon)) {
        return { valid: false, conflictIds: [] }
      }
    }

    // Check if item center is in any hole (if so, it cannot be placed)
    const [centerX, , centerZ] = position
    const holes = ceiling.holes || []
    for (const hole of holes) {
      if (hole.length >= 3 && pointInPolygon(centerX, centerZ, hole)) {
        return { valid: false, conflictIds: [] }
      }
    }

    // Check for overlaps with other ceiling items
    return this.getCeilingGrid(ceilingId).canPlace(position, dimensions, rotation, ignoreIds)
  }

  clearLevel(levelId: string) {
    this.floorGrids.delete(levelId)
    this.wallGrids.delete(levelId)
    this.slabsByLevel.delete(levelId)
  }

  clear() {
    this.floorGrids.clear()
    this.wallGrids.clear()
    this.walls.clear()
    this.slabsByLevel.clear()
    this.ceilingGrids.clear()
    this.ceilings.clear()
    this.itemCeilingMap.clear()
  }
}

// Singleton instance
export const spatialGridManager = new SpatialGridManager()


================================================
FILE: packages/core/src/hooks/spatial-grid/spatial-grid-sync.ts
================================================
import {
  type AnyNode,
  type AnyNodeId,
  getScaledDimensions,
  type ItemNode,
  type SlabNode,
  type WallNode,
} from '../../schema'
import useScene from '../../store/use-scene'
import {
  itemOverlapsPolygon,
  spatialGridManager,
  wallOverlapsPolygon,
} from './spatial-grid-manager'

export function resolveLevelId(node: AnyNode, nodes: Record<string, AnyNode>): string {
  // If the node itself is a level
  if (node.type === 'level') return node.id

  // Walk up parent chain to find level
  // This assumes you track parentId or can derive it
  let current: AnyNode | undefined = node

  while (current) {
    if (current.type === 'level') return current.id
    // Find parent (you might need to add parentId to your schema or derive it)
    if (current.parentId) {
      current = nodes[current.parentId]
    } else {
      current = undefined
    }
  }

  return 'default' // fallback for orphaned items
}

// Call this once at app initialization
export function initSpatialGridSync() {
  const store = useScene
  // 1. Initial sync - process all existing nodes
  const state = store.getState()
  for (const node of Object.values(state.nodes)) {
    const levelId = resolveLevelId(node, state.nodes)
    spatialGridManager.handleNodeCreated(node, levelId)
  }

  // 2. Then subscribe to future changes
  const markDirty = (id: AnyNodeId) => store.getState().markDirty(id)

  // Subscribe to all changes
  store.subscribe((state, prevState) => {
    // Detect added nodes
    for (const [id, node] of Object.entries(state.nodes)) {
      if (!prevState.nodes[id as AnyNode['id']]) {
        const levelId = resolveLevelId(node, state.nodes)
        spatialGridManager.handleNodeCreated(node, levelId)

        // When a slab is added, mark overlapping items/walls dirty
        if (node.type === 'slab') {
          markNodesOverlappingSlab(node as SlabNode, state.nodes, markDirty)
        }
      }
    }

    // Detect removed nodes
    for (const [id, node] of Object.entries(prevState.nodes)) {
      if (!state.nodes[id as AnyNode['id']]) {
        const levelId = resolveLevelId(node, prevState.nodes)
        spatialGridManager.handleNodeDeleted(id, node.type, levelId)

        // When a slab is removed, mark items/walls that were on it dirty (using current state)
        if (node.type === 'slab') {
          markNodesOverlappingSlab(node as SlabNode, state.nodes, markDirty)
        }
      }
    }

    // Detect updated nodes (items with position/rotation/parentId/side changes, slabs with polygon/elevation changes)
    for (const [id, node] of Object.entries(state.nodes)) {
      const prev = prevState.nodes[id as AnyNode['id']]
      if (!prev) continue

      if (node.type === 'item' && prev.type === 'item') {
        if (
          !(
            arraysEqual(node.position, prev.position) &&
            arraysEqual(node.rotation, prev.rotation) &&
            arraysEqual(node.scale, prev.scale)
          ) ||
          node.parentId !== prev.parentId ||
          node.side !== prev.side
        ) {
          const levelId = resolveLevelId(node, state.nodes)
          spatialGridManager.handleNodeUpdated(node, levelId)
          // Scale changes affect footprint size — mark dirty so slab elevation recalculates
          if (!arraysEqual(node.scale, prev.scale)) {
            markDirty(node.id)
          }
        }
      } else if (node.type === 'slab' && prev.type === 'slab') {
        if (
          node.polygon !== prev.polygon ||
          node.elevation !== prev.elevation ||
          node.holes !== prev.holes
        ) {
          const levelId = resolveLevelId(node, state.nodes)
          spatialGridManager.handleNodeUpdated(node, levelId)

          // Mark nodes overlapping old polygon and new polygon as dirty
          markNodesOverlappingSlab(prev as SlabNode, state.nodes, markDirty)
          markNodesOverlappingSlab(node as SlabNode, state.nodes, markDirty)
        }
      }
    }
  })
}

function arraysEqual(a: number[], b: number[]): boolean {
  return a.length === b.length && a.every((v, i) => v === b[i])
}

/**
 * Mark all floor items and walls that overlap a slab polygon as dirty.
 */
function markNodesOverlappingSlab(
  slab: SlabNode,
  nodes: Record<string, AnyNode>,
  markDirty: (id: AnyNodeId) => void,
) {
  if (slab.polygon.length < 3) return
  const slabLevelId = resolveLevelId(slab, nodes)

  for (const node of Object.values(nodes)) {
    if (node.type === 'item') {
      const item = node as ItemNode
      // Only floor items are affected by slabs
      if (item.asset.attachTo) continue
      if (resolveLevelId(node, nodes) !== slabLevelId) continue
      if (
        itemOverlapsPolygon(
          item.position,
          getScaledDimensions(item),
          item.rotation,
          slab.polygon,
          0.01,
        )
      ) {
        markDirty(node.id)
      }
    } else if (node.type === 'wall') {
      const wall = node as WallNode
      if (resolveLevelId(node, nodes) !== slabLevelId) continue
      if (wallOverlapsPolygon(wall.start, wall.end, slab.polygon)) {
        markDirty(node.id)
      }
    }
  }
}


================================================
FILE: packages/core/src/hooks/spatial-grid/spatial-grid.ts
================================================
type CellKey = `${number},${number}`

interface GridCell {
  itemIds: Set<string>
}

interface SpatialGridConfig {
  cellSize: number // e.g., 0.5 meters = Sims-style half-tile
}

export class SpatialGrid {
  private readonly cells = new Map<CellKey, GridCell>()
  private readonly itemCells = new Map<string, Set<CellKey>>() // reverse lookup

  private readonly config: SpatialGridConfig

  constructor(config: SpatialGridConfig) {
    this.config = config
  }

  private posToCell(x: number, z: number): [number, number] {
    return [Math.floor(x / this.config.cellSize), Math.floor(z / this.config.cellSize)]
  }

  private cellKey(cx: number, cz: number): CellKey {
    return `${cx},${cz}`
  }

  // Get all cells an item occupies based on its AABB
  private getItemCells(
    position: [number, number, number],
    dimensions: [number, number, number],
    rotation: [number, number, number],
  ): CellKey[] {
    // Simplified: axis-aligned bounding box
    // For full rotation support, compute rotated corners
    const [x, , z] = position
    const [w, , d] = dimensions
    const yRot = rotation[1] // Y-axis rotation

    // Compute rotated footprint (simplified for 90° increments)
    const cos = Math.abs(Math.cos(yRot))
    const sin = Math.abs(Math.sin(yRot))
    const rotatedW = w * cos + d * sin
    const rotatedD = w * sin + d * cos

    const minX = x - rotatedW / 2
    const maxX = x + rotatedW / 2
    const minZ = z - rotatedD / 2
    const maxZ = z + rotatedD / 2

    const [minCx, minCz] = this.posToCell(minX, minZ)
    // Use exclusive upper bound: subtract epsilon so exact boundaries don't overlap
    // This allows adjacent items (touching but not overlapping) to not conflict
    const epsilon = 1e-6
    const [maxCx, maxCz] = this.posToCell(maxX - epsilon, maxZ - epsilon)

    const keys: CellKey[] = []
    for (let cx = minCx; cx <= maxCx; cx++) {
      for (let cz = minCz; cz <= maxCz; cz++) {
        keys.push(this.cellKey(cx, cz))
      }
    }
    return keys
  }

  // Register an item
  insert(
    itemId: string,
    position: [number, number, number],
    dimensions: [number, number, number],
    rotation: [number, number, number],
  ) {
    const cellKeys = this.getItemCells(position, dimensions, rotation)

    this.itemCells.set(itemId, new Set(cellKeys))

    for (const key of cellKeys) {
      if (!this.cells.has(key)) {
        this.cells.set(key, { itemIds: new Set() })
      }
      this.cells.get(key)?.itemIds.add(itemId)
    }
  }

  // Remove an item
  remove(itemId: string) {
    const cellKeys = this.itemCells.get(itemId)
    if (!cellKeys) return

    for (const key of cellKeys) {
      const cell = this.cells.get(key)
      if (cell) {
        cell.itemIds.delete(itemId)
        if (cell.itemIds.size === 0) {
          this.cells.delete(key)
        }
      }
    }
    this.itemCells.delete(itemId)
  }

  // Update = remove + insert
  update(
    itemId: string,
    position: [number, number, number],
    dimensions: [number, number, number],
    rotation: [number, number, number],
  ) {
    this.remove(itemId)
    this.insert(itemId, position, dimensions, rotation)
  }

  // Query: is this placement valid?
  canPlace(
    position: [number, number, number],
    dimensions: [number, number, number],
    rotation: [number, number, number],
    ignoreIds: string[] = [],
  ): { valid: boolean; conflictIds: string[] } {
    const cellKeys = this.getItemCells(position, dimensions, rotation)
    const ignoreSet = new Set(ignoreIds)
    const conflicts = new Set<string>()

    for (const key of cellKeys) {
      const cell = this.cells.get(key)
      if (cell) {
        for (const id of cell.itemIds) {
          if (!ignoreSet.has(id)) {
            conflicts.add(id)
          }
        }
      }
    }

    return {
      valid: conflicts.size === 0,
      conflictIds: [...conflicts],
    }
  }

  // Query: get all items near a point (for snapping, selection, etc.)
  queryRadius(x: number, z: number, radius: number): string[] {
    const cellRadius = Math.ceil(radius / this.config.cellSize)
    const [cx, cz] = this.posToCell(x, z)
    const found = new Set<string>()

    for (let dx = -cellRadius; dx <= cellRadius; dx++) {
      for (let dz = -cellRadius; dz <= cellRadius; dz++) {
        const cell = this.cells.get(this.cellKey(cx + dx, cz + dz))
        if (cell) {
          for (const id of cell.itemIds) {
            found.add(id)
          }
        }
      }
    }
    return [...found]
  }

  getItemCount(): number {
    return this.itemCells.size
  }
}


================================================
FILE: packages/core/src/hooks/spatial-grid/use-spatial-query.ts
================================================
import { useCallback } from 'react'
import type { CeilingNode, LevelNode, WallNode } from '../../schema'
import { spatialGridManager } from './spatial-grid-manager'

export function useSpatialQuery() {
  const canPlaceOnFloor = useCallback(
    (
      levelId: LevelNode['id'],
      position: [number, number, number],
      dimensions: [number, number, number],
      rotation: [number, number, number],
      ignoreIds?: string[],
    ) => {
      return spatialGridManager.canPlaceOnFloor(levelId, position, dimensions, rotation, ignoreIds)
    },
    [],
  )

  const canPlaceOnWall = useCallback(
    (
      levelId: LevelNode['id'],
      wallId: WallNode['id'],
      localX: number,
      localY: number,
      dimensions: [number, number, number],
      attachType: 'wall' | 'wall-side' = 'wall',
      side?: 'front' | 'back',
      ignoreIds?: string[],
    ) => {
      return spatialGridManager.canPlaceOnWall(
        levelId,
        wallId,
        localX,
        localY,
        dimensions,
        attachType,
        side,
        ignoreIds,
      )
    },
    [],
  )

  const canPlaceOnCeiling = useCallback(
    (
      ceilingId: CeilingNode['id'],
      position: [number, number, number],
      dimensions: [number, number, number],
      rotation: [number, number, number],
      ignoreIds?: string[],
    ) => {
      return spatialGridManager.canPlaceOnCeiling(
        ceilingId,
        position,
        dimensions,
        rotation,
        ignoreIds,
      )
    },
    [],
  )

  return { canPlaceOnFloor, canPlaceOnWall, canPlaceOnCeiling }
}


================================================
FILE: packages/core/src/hooks/spatial-grid/wall-spatial-grid.ts
================================================
type WallSide = '
Download .txt
gitextract_f568hpnc/

├── .claude/
│   └── settings.json
├── .cursor/
│   └── rules/
│       ├── creating-rules.mdc
│       ├── events.mdc
│       ├── layers.mdc
│       ├── node-schemas.mdc
│       ├── renderers.mdc
│       ├── scene-registry.mdc
│       ├── selection-managers.mdc
│       ├── spatial-queries.mdc
│       ├── systems.mdc
│       ├── tools.mdc
│       └── viewer-isolation.mdc
├── .github/
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.yml
│   │   ├── config.yml
│   │   └── feature_request.yml
│   ├── pull_request_template.md
│   └── workflows/
│       └── release.yml
├── .gitignore
├── .npmrc
├── .vscode/
│   └── settings.json
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── SETUP.md
├── apps/
│   └── editor/
│       ├── .gitignore
│       ├── README.md
│       ├── app/
│       │   ├── api/
│       │   │   └── health/
│       │   │       └── route.ts
│       │   ├── globals.css
│       │   ├── layout.tsx
│       │   ├── page.tsx
│       │   ├── privacy/
│       │   │   └── page.tsx
│       │   └── terms/
│       │       └── page.tsx
│       ├── env.mjs
│       ├── lib/
│       │   └── utils.ts
│       ├── next.config.ts
│       ├── package.json
│       ├── postcss.config.mjs
│       ├── public/
│       │   ├── demos/
│       │   │   └── demo_1.json
│       │   └── items/
│       │       ├── ac-block/
│       │       │   └── model.glb
│       │       ├── air-conditioner/
│       │       │   └── model.glb
│       │       ├── air-conditioner-block/
│       │       │   └── model.glb
│       │       ├── air-conditioning/
│       │       │   └── model.glb
│       │       ├── alarm-keypad/
│       │       │   └── model.glb
│       │       ├── ball/
│       │       │   └── model.glb
│       │       ├── barbell/
│       │       │   └── model.glb
│       │       ├── barbell-stand/
│       │       │   └── model.glb
│       │       ├── basket-hoop/
│       │       │   └── model.glb
│       │       ├── bathroom-sink/
│       │       │   └── model.glb
│       │       ├── bathtub/
│       │       │   └── model.glb
│       │       ├── bean-bag/
│       │       │   └── model.glb
│       │       ├── bedside-table/
│       │       │   └── model.glb
│       │       ├── books/
│       │       │   └── model.glb
│       │       ├── bookshelf/
│       │       │   └── model.glb
│       │       ├── bunkbed/
│       │       │   └── model.glb
│       │       ├── bush/
│       │       │   └── model.glb
│       │       ├── cactus/
│       │       │   └── model.glb
│       │       ├── car-toy/
│       │       │   └── model.glb
│       │       ├── ceiling-fan/
│       │       │   └── model.glb
│       │       ├── ceiling-lamp/
│       │       │   └── model.glb
│       │       ├── ceiling-light/
│       │       │   └── model.glb
│       │       ├── circular-ceiling-light/
│       │       │   └── model.glb
│       │       ├── closet/
│       │       │   └── model.glb
│       │       ├── coat-rack/
│       │       │   └── model.glb
│       │       ├── coffee-machine/
│       │       │   └── model.glb
│       │       ├── coffee-table/
│       │       │   └── model.glb
│       │       ├── column/
│       │       │   └── model.glb
│       │       ├── computer/
│       │       │   └── model.glb
│       │       ├── couch-medium/
│       │       │   └── model.glb
│       │       ├── couch-small/
│       │       │   └── model.glb
│       │       ├── cutting-board/
│       │       │   └── model.glb
│       │       ├── desk/
│       │       │   └── model.glb
│       │       ├── dining-chair/
│       │       │   └── model.glb
│       │       ├── dining-table/
│       │       │   └── model.glb
│       │       ├── door/
│       │       │   └── model.glb
│       │       ├── door-bar/
│       │       │   └── model.glb
│       │       ├── door-with-bar/
│       │       │   └── model.glb
│       │       ├── doorway-front/
│       │       │   └── model.glb
│       │       ├── double-bed/
│       │       │   └── model.glb
│       │       ├── dresser/
│       │       │   └── model.glb
│       │       ├── drying-rack/
│       │       │   └── model.glb
│       │       ├── easel/
│       │       │   └── model.glb
│       │       ├── electric-panel/
│       │       │   └── model.glb
│       │       ├── ev-wall-charger/
│       │       │   └── model.glb
│       │       ├── exercise-bike/
│       │       │   └── model.glb
│       │       ├── exit-sign/
│       │       │   └── model.glb
│       │       ├── fence/
│       │       │   └── model.glb
│       │       ├── fir-tree/
│       │       │   └── model.glb
│       │       ├── fire-alarm/
│       │       │   └── model.glb
│       │       ├── fire-detector/
│       │       │   └── model.glb
│       │       ├── fire-extinguisher/
│       │       │   └── model.glb
│       │       ├── flat-screen-tv/
│       │       │   └── model.glb
│       │       ├── floor-lamp/
│       │       │   └── model.glb
│       │       ├── freezer/
│       │       │   └── model.glb
│       │       ├── fridge/
│       │       │   └── model.glb
│       │       ├── fruits/
│       │       │   └── model.glb
│       │       ├── frying-pan/
│       │       │   └── model.glb
│       │       ├── glass-door/
│       │       │   └── model.glb
│       │       ├── guitar/
│       │       │   └── model.glb
│       │       ├── hedge/
│       │       │   └── model.glb
│       │       ├── high-fence/
│       │       │   └── model.glb
│       │       ├── hood/
│       │       │   └── model.glb
│       │       ├── hydrant/
│       │       │   └── model.glb
│       │       ├── indoor-plant/
│       │       │   └── model.glb
│       │       ├── iron/
│       │       │   └── model.glb
│       │       ├── ironing-board/
│       │       │   └── model.glb
│       │       ├── kettle/
│       │       │   └── model.glb
│       │       ├── kitchen/
│       │       │   └── model.glb
│       │       ├── kitchen-cabinet/
│       │       │   └── model.glb
│       │       ├── kitchen-counter/
│       │       │   └── model.glb
│       │       ├── kitchen-fridge/
│       │       │   └── model.glb
│       │       ├── kitchen-shelf/
│       │       │   └── model.glb
│       │       ├── kitchen-utensils/
│       │       │   └── model.glb
│       │       ├── laundry-bag/
│       │       │   └── model.glb
│       │       ├── livingroom-chair/
│       │       │   └── model.glb
│       │       ├── lounge-chair/
│       │       │   └── model.glb
│       │       ├── low-fence/
│       │       │   └── model.glb
│       │       ├── medium-fence/
│       │       │   └── model.glb
│       │       ├── microwave/
│       │       │   └── model.glb
│       │       ├── office-chair/
│       │       │   └── model.glb
│       │       ├── office-table/
│       │       │   └── model.glb
│       │       ├── outdoor-playhouse/
│       │       │   └── model.glb
│       │       ├── palm/
│       │       │   └── model.glb
│       │       ├── parking-spot/
│       │       │   └── model.glb
│       │       ├── patio-umbrella/
│       │       │   └── model.glb
│       │       ├── piano/
│       │       │   ├── Fireplace_13.glb
│       │       │   └── model.glb
│       │       ├── picture/
│       │       │   └── model.glb
│       │       ├── pillar/
│       │       │   └── model.glb
│       │       ├── pool-table/
│       │       │   └── model.glb
│       │       ├── recessed-light/
│       │       │   └── model.glb
│       │       ├── rectangular-carpet/
│       │       │   └── model.glb
│       │       ├── rectangular-ceiling-light/
│       │       │   └── model.glb
│       │       ├── rectangular-mirror/
│       │       │   └── model.glb
│       │       ├── round-carpet/
│       │       │   └── model.glb
│       │       ├── round-mirror/
│       │       │   └── model.glb
│       │       ├── scooter/
│       │       │   └── model.glb
│       │       ├── sewing-machine/
│       │       │   └── model.glb
│       │       ├── shelf/
│       │       │   └── model.glb
│       │       ├── shower/
│       │       │   └── model.glb
│       │       ├── shower-angle/
│       │       │   └── model.glb
│       │       ├── shower-rug/
│       │       │   └── model.glb
│       │       ├── shower-square/
│       │       │   └── model.glb
│       │       ├── single-bed/
│       │       │   └── model.glb
│       │       ├── sink-cabinet/
│       │       │   └── model.glb
│       │       ├── skate/
│       │       │   └── model.glb
│       │       ├── small-indoor-plant/
│       │       │   └── model.glb
│       │       ├── small-kitchen-cabinet/
│       │       │   └── model.glb
│       │       ├── smoke-detector/
│       │       │   └── model.glb
│       │       ├── sofa/
│       │       │   └── model.glb
│       │       ├── sprinkler/
│       │       │   └── model.glb
│       │       ├── stairs/
│       │       │   └── model.glb
│       │       ├── stereo-speaker/
│       │       │   └── model.glb
│       │       ├── stool/
│       │       │   └── model.glb
│       │       ├── stove/
│       │       │   └── model.glb
│       │       ├── sunbed/
│       │       │   └── model.glb
│       │       ├── suspended-fireplace/
│       │       │   └── model.glb
│       │       ├── table/
│       │       │   └── model.glb
│       │       ├── table-lamp/
│       │       │   └── model.glb
│       │       ├── television/
│       │       │   └── model.glb
│       │       ├── tesla/
│       │       │   └── model.glb
│       │       ├── thermostat/
│       │       │   └── model.glb
│       │       ├── threadmill/
│       │       │   └── model.glb
│       │       ├── toaster/
│       │       │   └── model.glb
│       │       ├── toilet/
│       │       │   └── model.glb
│       │       ├── toilet-brush/
│       │       │   └── model.glb
│       │       ├── toilet-paper/
│       │       │   └── model.glb
│       │       ├── toy/
│       │       │   └── model.glb
│       │       ├── trash-bin/
│       │       │   └── model.glb
│       │       ├── tree/
│       │       │   └── model.glb
│       │       ├── tub/
│       │       │   └── model.glb
│       │       ├── tv-stand/
│       │       │   └── model.glb
│       │       ├── wall-art-06/
│       │       │   └── model.glb
│       │       ├── wall-sink/
│       │       │   └── model.glb
│       │       ├── washing-machine/
│       │       │   └── model.glb
│       │       ├── window-double/
│       │       │   └── model.glb
│       │       ├── window-large/
│       │       │   └── model.glb
│       │       ├── window-rectangle/
│       │       │   └── model.glb
│       │       ├── window-round/
│       │       │   └── model.glb
│       │       ├── window-simple/
│       │       │   └── model.glb
│       │       ├── window-small/
│       │       │   └── model.glb
│       │       ├── window-small-2/
│       │       │   └── model.glb
│       │       ├── window-square/
│       │       │   └── model.glb
│       │       ├── window1-black-open-1731/
│       │       │   └── model.glb
│       │       └── wine-bottle/
│       │           └── model.glb
│       └── tsconfig.json
├── biome.jsonc
├── package.json
├── packages/
│   ├── core/
│   │   ├── README.md
│   │   ├── package.json
│   │   ├── src/
│   │   │   ├── events/
│   │   │   │   └── bus.ts
│   │   │   ├── hooks/
│   │   │   │   ├── scene-registry/
│   │   │   │   │   └── scene-registry.ts
│   │   │   │   └── spatial-grid/
│   │   │   │       ├── spatial-grid-manager.ts
│   │   │   │       ├── spatial-grid-sync.ts
│   │   │   │       ├── spatial-grid.ts
│   │   │   │       ├── use-spatial-query.ts
│   │   │   │       └── wall-spatial-grid.ts
│   │   │   ├── index.ts
│   │   │   ├── lib/
│   │   │   │   ├── asset-storage.ts
│   │   │   │   └── space-detection.ts
│   │   │   ├── schema/
│   │   │   │   ├── base.ts
│   │   │   │   ├── camera.ts
│   │   │   │   ├── collections.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── material.ts
│   │   │   │   ├── nodes/
│   │   │   │   │   ├── building.ts
│   │   │   │   │   ├── ceiling.ts
│   │   │   │   │   ├── door.ts
│   │   │   │   │   ├── guide.ts
│   │   │   │   │   ├── item.ts
│   │   │   │   │   ├── level.ts
│   │   │   │   │   ├── roof-segment.ts
│   │   │   │   │   ├── roof.ts
│   │   │   │   │   ├── scan.ts
│   │   │   │   │   ├── site.ts
│   │   │   │   │   ├── slab.ts
│   │   │   │   │   ├── stair-segment.ts
│   │   │   │   │   ├── stair.ts
│   │   │   │   │   ├── wall.ts
│   │   │   │   │   ├── window.ts
│   │   │   │   │   └── zone.ts
│   │   │   │   └── types.ts
│   │   │   ├── store/
│   │   │   │   ├── actions/
│   │   │   │   │   └── node-actions.ts
│   │   │   │   ├── use-interactive.ts
│   │   │   │   └── use-scene.ts
│   │   │   ├── systems/
│   │   │   │   ├── ceiling/
│   │   │   │   │   └── ceiling-system.tsx
│   │   │   │   ├── door/
│   │   │   │   │   └── door-system.tsx
│   │   │   │   ├── item/
│   │   │   │   │   └── item-system.tsx
│   │   │   │   ├── roof/
│   │   │   │   │   └── roof-system.tsx
│   │   │   │   ├── slab/
│   │   │   │   │   └── slab-system.tsx
│   │   │   │   ├── stair/
│   │   │   │   │   └── stair-system.tsx
│   │   │   │   ├── wall/
│   │   │   │   │   ├── wall-footprint.ts
│   │   │   │   │   ├── wall-mitering.ts
│   │   │   │   │   └── wall-system.tsx
│   │   │   │   └── window/
│   │   │   │       └── window-system.tsx
│   │   │   └── utils/
│   │   │       ├── clone-scene-graph.ts
│   │   │       └── types.ts
│   │   └── tsconfig.json
│   ├── editor/
│   │   ├── package.json
│   │   ├── src/
│   │   │   ├── components/
│   │   │   │   ├── editor/
│   │   │   │   │   ├── custom-camera-controls.tsx
│   │   │   │   │   ├── editor-layout-v2.tsx
│   │   │   │   │   ├── export-manager.tsx
│   │   │   │   │   ├── first-person-controls.tsx
│   │   │   │   │   ├── floating-action-menu.tsx
│   │   │   │   │   ├── floorplan-panel.tsx
│   │   │   │   │   ├── grid.tsx
│   │   │   │   │   ├── index.tsx
│   │   │   │   │   ├── node-action-menu.tsx
│   │   │   │   │   ├── preset-thumbnail-generator.tsx
│   │   │   │   │   ├── selection-manager.tsx
│   │   │   │   │   ├── site-edge-labels.tsx
│   │   │   │   │   ├── thumbnail-generator.tsx
│   │   │   │   │   └── wall-measurement-label.tsx
│   │   │   │   ├── feedback-dialog.tsx
│   │   │   │   ├── pascal-radio.tsx
│   │   │   │   ├── preview-button.tsx
│   │   │   │   ├── systems/
│   │   │   │   │   ├── ceiling/
│   │   │   │   │   │   └── ceiling-system.tsx
│   │   │   │   │   ├── roof/
│   │   │   │   │   │   └── roof-edit-system.tsx
│   │   │   │   │   ├── stair/
│   │   │   │   │   │   └── stair-edit-system.tsx
│   │   │   │   │   └── zone/
│   │   │   │   │       ├── zone-label-editor-system.tsx
│   │   │   │   │       └── zone-system.tsx
│   │   │   │   ├── tools/
│   │   │   │   │   ├── ceiling/
│   │   │   │   │   │   ├── ceiling-boundary-editor.tsx
│   │   │   │   │   │   ├── ceiling-hole-editor.tsx
│   │   │   │   │   │   └── ceiling-tool.tsx
│   │   │   │   │   ├── door/
│   │   │   │   │   │   ├── door-math.ts
│   │   │   │   │   │   ├── door-tool.tsx
│   │   │   │   │   │   └── move-door-tool.tsx
│   │   │   │   │   ├── item/
│   │   │   │   │   │   ├── item-tool.tsx
│   │   │   │   │   │   ├── move-tool.tsx
│   │   │   │   │   │   ├── placement-math.ts
│   │   │   │   │   │   ├── placement-strategies.ts
│   │   │   │   │   │   ├── placement-types.ts
│   │   │   │   │   │   ├── use-draft-node.ts
│   │   │   │   │   │   └── use-placement-coordinator.tsx
│   │   │   │   │   ├── roof/
│   │   │   │   │   │   ├── move-roof-tool.tsx
│   │   │   │   │   │   └── roof-tool.tsx
│   │   │   │   │   ├── select/
│   │   │   │   │   │   └── box-select-tool.tsx
│   │   │   │   │   ├── shared/
│   │   │   │   │   │   ├── cursor-sphere.tsx
│   │   │   │   │   │   └── polygon-editor.tsx
│   │   │   │   │   ├── site/
│   │   │   │   │   │   └── site-boundary-editor.tsx
│   │   │   │   │   ├── slab/
│   │   │   │   │   │   ├── slab-boundary-editor.tsx
│   │   │   │   │   │   ├── slab-hole-editor.tsx
│   │   │   │   │   │   └── slab-tool.tsx
│   │   │   │   │   ├── stair/
│   │   │   │   │   │   └── stair-tool.tsx
│   │   │   │   │   ├── tool-manager.tsx
│   │   │   │   │   ├── wall/
│   │   │   │   │   │   ├── wall-drafting.ts
│   │   │   │   │   │   └── wall-tool.tsx
│   │   │   │   │   ├── window/
│   │   │   │   │   │   ├── move-window-tool.tsx
│   │   │   │   │   │   ├── window-math.ts
│   │   │   │   │   │   └── window-tool.tsx
│   │   │   │   │   └── zone/
│   │   │   │   │       ├── zone-boundary-editor.tsx
│   │   │   │   │       └── zone-tool.tsx
│   │   │   │   ├── ui/
│   │   │   │   │   ├── action-menu/
│   │   │   │   │   │   ├── action-button.tsx
│   │   │   │   │   │   ├── camera-actions.tsx
│   │   │   │   │   │   ├── control-modes.tsx
│   │   │   │   │   │   ├── furnish-tools.tsx
│   │   │   │   │   │   ├── index.tsx
│   │   │   │   │   │   ├── structure-tools.tsx
│   │   │   │   │   │   └── view-toggles.tsx
│   │   │   │   │   ├── command-palette/
│   │   │   │   │   │   ├── editor-commands.tsx
│   │   │   │   │   │   └── index.tsx
│   │   │   │   │   ├── controls/
│   │   │   │   │   │   ├── action-button.tsx
│   │   │   │   │   │   ├── material-picker.tsx
│   │   │   │   │   │   ├── metric-control.tsx
│   │   │   │   │   │   ├── panel-section.tsx
│   │   │   │   │   │   ├── segmented-control.tsx
│   │   │   │   │   │   ├── slider-control.tsx
│   │   │   │   │   │   └── toggle-control.tsx
│   │   │   │   │   ├── floating-level-selector.tsx
│   │   │   │   │   ├── helpers/
│   │   │   │   │   │   ├── ceiling-helper.tsx
│   │   │   │   │   │   ├── helper-manager.tsx
│   │   │   │   │   │   ├── item-helper.tsx
│   │   │   │   │   │   ├── roof-helper.tsx
│   │   │   │   │   │   ├── slab-helper.tsx
│   │   │   │   │   │   └── wall-helper.tsx
│   │   │   │   │   ├── item-catalog/
│   │   │   │   │   │   ├── catalog-items.tsx
│   │   │   │   │   │   └── item-catalog.tsx
│   │   │   │   │   ├── panels/
│   │   │   │   │   │   ├── ceiling-panel.tsx
│   │   │   │   │   │   ├── collections/
│   │   │   │   │   │   │   └── collections-popover.tsx
│   │   │   │   │   │   ├── door-panel.tsx
│   │   │   │   │   │   ├── item-panel.tsx
│   │   │   │   │   │   ├── panel-manager.tsx
│   │   │   │   │   │   ├── panel-wrapper.tsx
│   │   │   │   │   │   ├── presets/
│   │   │   │   │   │   │   └── presets-popover.tsx
│   │   │   │   │   │   ├── reference-panel.tsx
│   │   │   │   │   │   ├── roof-panel.tsx
│   │   │   │   │   │   ├── roof-segment-panel.tsx
│   │   │   │   │   │   ├── slab-panel.tsx
│   │   │   │   │   │   ├── stair-panel.tsx
│   │   │   │   │   │   ├── stair-segment-panel.tsx
│   │   │   │   │   │   ├── wall-panel.tsx
│   │   │   │   │   │   └── window-panel.tsx
│   │   │   │   │   ├── primitives/
│   │   │   │   │   │   ├── button.tsx
│   │   │   │   │   │   ├── card.tsx
│   │   │   │   │   │   ├── color-dot.tsx
│   │   │   │   │   │   ├── context-menu.tsx
│   │   │   │   │   │   ├── dialog.tsx
│   │   │   │   │   │   ├── dropdown-menu.tsx
│   │   │   │   │   │   ├── error-boundary.tsx
│   │   │   │   │   │   ├── input.tsx
│   │   │   │   │   │   ├── number-input.tsx
│   │   │   │   │   │   ├── opacity-control.tsx
│   │   │   │   │   │   ├── popover.tsx
│   │   │   │   │   │   ├── separator.tsx
│   │   │   │   │   │   ├── sheet.tsx
│   │   │   │   │   │   ├── shortcut-token.tsx
│   │   │   │   │   │   ├── sidebar.tsx
│   │   │   │   │   │   ├── skeleton.tsx
│   │   │   │   │   │   ├── slider.tsx
│   │   │   │   │   │   ├── switch.tsx
│   │   │   │   │   │   └── tooltip.tsx
│   │   │   │   │   ├── scene-loader.tsx
│   │   │   │   │   ├── sidebar/
│   │   │   │   │   │   ├── app-sidebar.tsx
│   │   │   │   │   │   ├── icon-rail.tsx
│   │   │   │   │   │   ├── panels/
│   │   │   │   │   │   │   ├── settings-panel/
│   │   │   │   │   │   │   │   ├── audio-settings-dialog.tsx
│   │   │   │   │   │   │   │   ├── index.tsx
│   │   │   │   │   │   │   │   └── keyboard-shortcuts-dialog.tsx
│   │   │   │   │   │   │   ├── site-panel/
│   │   │   │   │   │   │   │   ├── building-tree-node.tsx
│   │   │   │   │   │   │   │   ├── ceiling-tree-node.tsx
│   │   │   │   │   │   │   │   ├── door-tree-node.tsx
│   │   │   │   │   │   │   │   ├── index.tsx
│   │   │   │   │   │   │   │   ├── inline-rename-input.tsx
│   │   │   │   │   │   │   │   ├── item-tree-node.tsx
│   │   │   │   │   │   │   │   ├── level-tree-node.tsx
│   │   │   │   │   │   │   │   ├── roof-tree-node.tsx
│   │   │   │   │   │   │   │   ├── slab-tree-node.tsx
│   │   │   │   │   │   │   │   ├── stair-tree-node.tsx
│   │   │   │   │   │   │   │   ├── tree-node-actions.tsx
│   │   │   │   │   │   │   │   ├── tree-node-drag.tsx
│   │   │   │   │   │   │   │   ├── tree-node.tsx
│   │   │   │   │   │   │   │   ├── wall-tree-node.tsx
│   │   │   │   │   │   │   │   ├── window-tree-node.tsx
│   │   │   │   │   │   │   │   └── zone-tree-node.tsx
│   │   │   │   │   │   │   └── zone-panel/
│   │   │   │   │   │   │       └── index.tsx
│   │   │   │   │   │   └── tab-bar.tsx
│   │   │   │   │   ├── slider-demo.tsx
│   │   │   │   │   ├── slider.tsx
│   │   │   │   │   └── viewer-toolbar.tsx
│   │   │   │   ├── viewer-overlay.tsx
│   │   │   │   └── viewer-zone-system.tsx
│   │   │   ├── contexts/
│   │   │   │   └── presets-context.tsx
│   │   │   ├── hooks/
│   │   │   │   ├── use-auto-save.ts
│   │   │   │   ├── use-contextual-tools.ts
│   │   │   │   ├── use-grid-events.ts
│   │   │   │   ├── use-keyboard.ts
│   │   │   │   ├── use-mobile.ts
│   │   │   │   └── use-reduced-motion.ts
│   │   │   ├── index.tsx
│   │   │   ├── lib/
│   │   │   │   ├── constants.ts
│   │   │   │   ├── level-selection.ts
│   │   │   │   ├── scene.ts
│   │   │   │   ├── sfx/
│   │   │   │   │   └── index.ts
│   │   │   │   ├── sfx-bus.ts
│   │   │   │   ├── sfx-player.ts
│   │   │   │   └── utils.ts
│   │   │   ├── store/
│   │   │   │   ├── use-audio.tsx
│   │   │   │   ├── use-command-registry.ts
│   │   │   │   ├── use-editor.tsx
│   │   │   │   ├── use-palette-view-registry.ts
│   │   │   │   └── use-upload.ts
│   │   │   └── three-types.ts
│   │   └── tsconfig.json
│   ├── eslint-config/
│   │   ├── README.md
│   │   ├── base.js
│   │   ├── next.js
│   │   ├── package.json
│   │   └── react-internal.js
│   ├── typescript-config/
│   │   ├── base.json
│   │   ├── nextjs.json
│   │   ├── package.json
│   │   └── react-library.json
│   ├── ui/
│   │   ├── eslint.config.mjs
│   │   ├── package.json
│   │   ├── src/
│   │   │   ├── button.tsx
│   │   │   ├── card.tsx
│   │   │   └── code.tsx
│   │   └── tsconfig.json
│   └── viewer/
│       ├── README.md
│       ├── package.json
│       ├── src/
│       │   ├── components/
│       │   │   ├── error-boundary.tsx
│       │   │   ├── renderers/
│       │   │   │   ├── building/
│       │   │   │   │   └── building-renderer.tsx
│       │   │   │   ├── ceiling/
│       │   │   │   │   └── ceiling-renderer.tsx
│       │   │   │   ├── door/
│       │   │   │   │   └── door-renderer.tsx
│       │   │   │   ├── guide/
│       │   │   │   │   └── guide-renderer.tsx
│       │   │   │   ├── item/
│       │   │   │   │   └── item-renderer.tsx
│       │   │   │   ├── level/
│       │   │   │   │   └── level-renderer.tsx
│       │   │   │   ├── node-renderer.tsx
│       │   │   │   ├── roof/
│       │   │   │   │   ├── roof-materials.ts
│       │   │   │   │   └── roof-renderer.tsx
│       │   │   │   ├── roof-segment/
│       │   │   │   │   └── roof-segment-renderer.tsx
│       │   │   │   ├── scan/
│       │   │   │   │   └── scan-renderer.tsx
│       │   │   │   ├── scene-renderer.tsx
│       │   │   │   ├── site/
│       │   │   │   │   └── site-renderer.tsx
│       │   │   │   ├── slab/
│       │   │   │   │   └── slab-renderer.tsx
│       │   │   │   ├── stair/
│       │   │   │   │   └── stair-renderer.tsx
│       │   │   │   ├── stair-segment/
│       │   │   │   │   └── stair-segment-renderer.tsx
│       │   │   │   ├── wall/
│       │   │   │   │   └── wall-renderer.tsx
│       │   │   │   ├── window/
│       │   │   │   │   └── window-renderer.tsx
│       │   │   │   └── zone/
│       │   │   │       └── zone-renderer.tsx
│       │   │   └── viewer/
│       │   │       ├── ground-occluder.tsx
│       │   │       ├── index.tsx
│       │   │       ├── lights.tsx
│       │   │       ├── perf-monitor.tsx
│       │   │       ├── post-processing.tsx
│       │   │       ├── selection-manager.tsx
│       │   │       └── viewer-camera.tsx
│       │   ├── hooks/
│       │   │   ├── use-asset-url.ts
│       │   │   ├── use-gltf-ktx2.tsx
│       │   │   └── use-node-events.ts
│       │   ├── index.ts
│       │   ├── lib/
│       │   │   ├── asset-url.ts
│       │   │   ├── layers.ts
│       │   │   └── materials.ts
│       │   ├── r3f.d.ts
│       │   ├── store/
│       │   │   ├── use-item-light-pool.ts
│       │   │   ├── use-viewer.d.ts
│       │   │   └── use-viewer.ts
│       │   └── systems/
│       │       ├── export/
│       │       │   └── export-system.tsx
│       │       ├── guide/
│       │       │   └── guide-system.tsx
│       │       ├── interactive/
│       │       │   └── interactive-system.tsx
│       │       ├── item-light/
│       │       │   └── item-light-system.tsx
│       │       ├── level/
│       │       │   ├── level-system.d.ts
│       │       │   ├── level-system.tsx
│       │       │   └── level-utils.ts
│       │       ├── scan/
│       │       │   └── scan-system.tsx
│       │       ├── wall/
│       │       │   └── wall-cutout.tsx
│       │       └── zone/
│       │           └── zone-system.tsx
│       └── tsconfig.json
├── tooling/
│   ├── release/
│   │   ├── android-playstore-release.sh
│   │   ├── ios-appstore-release.sh
│   │   └── release.sh
│   └── typescript/
│       ├── base.json
│       ├── expo.json
│       ├── nextjs.json
│       ├── package.json
│       └── react-library.json
└── turbo.json
Download .txt
SYMBOL INDEX (1081 symbols across 219 files)

FILE: apps/editor/app/api/health/route.ts
  function GET (line 1) | function GET() {

FILE: apps/editor/app/layout.tsx
  function RootLayout (line 24) | function RootLayout({

FILE: apps/editor/app/page.tsx
  constant SIDEBAR_TABS (line 10) | const SIDEBAR_TABS: (SidebarTab & { component: React.ComponentType })[] = [
  function Home (line 18) | function Home() {

FILE: apps/editor/app/privacy/page.tsx
  function PrivacyPage (line 9) | function PrivacyPage() {

FILE: apps/editor/app/terms/page.tsx
  function TermsPage (line 9) | function TermsPage() {

FILE: apps/editor/lib/utils.ts
  function cn (line 4) | function cn(...inputs: ClassValue[]) {
  constant BASE_URL (line 20) | const BASE_URL = (() => {

FILE: packages/core/src/events/bus.ts
  type GridEvent (line 22) | interface GridEvent {
  type NodeEvent (line 27) | interface NodeEvent<T extends AnyNode = AnyNode> {
  type WallEvent (line 36) | type WallEvent = NodeEvent<WallNode>
  type ItemEvent (line 37) | type ItemEvent = NodeEvent<ItemNode>
  type SiteEvent (line 38) | type SiteEvent = NodeEvent<SiteNode>
  type BuildingEvent (line 39) | type BuildingEvent = NodeEvent<BuildingNode>
  type LevelEvent (line 40) | type LevelEvent = NodeEvent<LevelNode>
  type ZoneEvent (line 41) | type ZoneEvent = NodeEvent<ZoneNode>
  type SlabEvent (line 42) | type SlabEvent = NodeEvent<SlabNode>
  type CeilingEvent (line 43) | type CeilingEvent = NodeEvent<CeilingNode>
  type RoofEvent (line 44) | type RoofEvent = NodeEvent<RoofNode>
  type RoofSegmentEvent (line 45) | type RoofSegmentEvent = NodeEvent<RoofSegmentNode>
  type StairEvent (line 46) | type StairEvent = NodeEvent<StairNode>
  type StairSegmentEvent (line 47) | type StairSegmentEvent = NodeEvent<StairSegmentNode>
  type WindowEvent (line 48) | type WindowEvent = NodeEvent<WindowNode>
  type DoorEvent (line 49) | type DoorEvent = NodeEvent<DoorNode>
  type EventSuffix (line 63) | type EventSuffix = (typeof eventSuffixes)[number]
  type NodeEvents (line 65) | type NodeEvents<T extends string, E> = {
  type GridEvents (line 69) | type GridEvents = {
  type CameraControlEvent (line 73) | interface CameraControlEvent {
  type ThumbnailGenerateEvent (line 77) | interface ThumbnailGenerateEvent {
  type CameraControlEvents (line 81) | type CameraControlEvents = {
  type ToolEvents (line 91) | type ToolEvents = {
  type PresetEvents (line 95) | type PresetEvents = {
  type EditorEvents (line 100) | type EditorEvents = GridEvents &

FILE: packages/core/src/hooks/scene-registry/scene-registry.ts
  method clear (line 30) | clear() {
  function useRegistry (line 38) | function useRegistry(

FILE: packages/core/src/hooks/spatial-grid/spatial-grid-manager.ts
  function pointInPolygon (line 13) | function pointInPolygon(px: number, pz: number, polygon: Array<[number, ...
  function getItemFootprint (line 32) | function getItemFootprint(
  function segmentsIntersect (line 57) | function segmentsIntersect(
  function segmentIntersectsPolygon (line 97) | function segmentIntersectsPolygon(
  function itemOverlapsPolygon (line 129) | function itemOverlapsPolygon(
  function segmentsCollinearAndOverlap (line 172) | function segmentsCollinearAndOverlap(
  function wallOverlapsPolygon (line 216) | function wallOverlapsPolygon(
  class SpatialGridManager (line 275) | class SpatialGridManager {
    method constructor (line 286) | constructor(cellSize = 0.5) {
    method getFloorGrid (line 290) | private getFloorGrid(levelId: string): SpatialGrid {
    method getWallGrid (line 297) | private getWallGrid(levelId: string): WallSpatialGrid {
    method getWallLength (line 304) | private getWallLength(wallId: string): number {
    method getWallHeight (line 312) | private getWallHeight(wallId: string): number {
    method getCeilingGrid (line 317) | private getCeilingGrid(ceilingId: string): SpatialGrid {
    method getSlabMap (line 324) | private getSlabMap(levelId: string): Map<string, SlabNode> {
    method handleNodeCreated (line 332) | handleNodeCreated(node: AnyNode, levelId: string) {
    method handleNodeUpdated (line 389) | handleNodeUpdated(node: AnyNode, levelId: string) {
    method handleNodeDeleted (line 452) | handleNodeDeleted(nodeId: string, nodeType: string, levelId: string) {
    method canPlaceOnFloor (line 477) | canPlaceOnFloor(
    method canPlaceOnWall (line 499) | canPlaceOnWall(
    method getWallForItem (line 531) | getWallForItem(levelId: string, itemId: string): string | undefined {
    method getSlabElevationAt (line 539) | getSlabElevationAt(levelId: string, x: number, z: number): number {
    method getSlabElevationForItem (line 572) | getSlabElevationForItem(
    method getSlabElevationForWall (line 615) | getSlabElevationForWall(levelId: string, start: [number, number], end:...
    method canPlaceOnCeiling (line 666) | canPlaceOnCeiling(
    method clearLevel (line 699) | clearLevel(levelId: string) {
    method clear (line 705) | clear() {

FILE: packages/core/src/hooks/spatial-grid/spatial-grid-sync.ts
  function resolveLevelId (line 16) | function resolveLevelId(node: AnyNode, nodes: Record<string, AnyNode>): ...
  function initSpatialGridSync (line 38) | function initSpatialGridSync() {
  function arraysEqual (line 118) | function arraysEqual(a: number[], b: number[]): boolean {
  function markNodesOverlappingSlab (line 125) | function markNodesOverlappingSlab(

FILE: packages/core/src/hooks/spatial-grid/spatial-grid.ts
  type CellKey (line 1) | type CellKey = `${number},${number}`
  type GridCell (line 3) | interface GridCell {
  type SpatialGridConfig (line 7) | interface SpatialGridConfig {
  class SpatialGrid (line 11) | class SpatialGrid {
    method constructor (line 17) | constructor(config: SpatialGridConfig) {
    method posToCell (line 21) | private posToCell(x: number, z: number): [number, number] {
    method cellKey (line 25) | private cellKey(cx: number, cz: number): CellKey {
    method getItemCells (line 30) | private getItemCells(
    method insert (line 68) | insert(
    method remove (line 87) | remove(itemId: string) {
    method update (line 104) | update(
    method canPlace (line 115) | canPlace(
    method queryRadius (line 143) | queryRadius(x: number, z: number, radius: number): string[] {
    method getItemCount (line 161) | getItemCount(): number {

FILE: packages/core/src/hooks/spatial-grid/use-spatial-query.ts
  function useSpatialQuery (line 5) | function useSpatialQuery() {

FILE: packages/core/src/hooks/spatial-grid/wall-spatial-grid.ts
  type WallSide (line 1) | type WallSide = 'front' | 'back'
  type AttachType (line 2) | type AttachType = 'wall' | 'wall-side'
  constant EPSILON (line 5) | const EPSILON = 0.001
  constant AUTO_SNAP_MARGIN (line 8) | const AUTO_SNAP_MARGIN = 0.05
  type WallItemPlacement (line 10) | interface WallItemPlacement {
  function autoAdjustYPosition (line 25) | function autoAdjustYPosition(
  class WallSpatialGrid (line 51) | class WallSpatialGrid {
    method canPlaceOnWall (line 69) | canPlaceOnWall(
    method checkSideConflict (line 123) | private checkSideConflict(
    method insert (line 149) | insert(placement: WallItemPlacement) {
    method remove (line 159) | remove(wallId: string, itemId: string) {
    method removeByItemId (line 168) | removeByItemId(itemId: string) {
    method removeWall (line 176) | removeWall(wallId: string): string[] {
    method getWallForItem (line 189) | getWallForItem(itemId: string): string | undefined {
    method clear (line 193) | clear() {

FILE: packages/core/src/lib/asset-storage.ts
  constant ASSET_PREFIX (line 3) | const ASSET_PREFIX = 'asset_data:'
  function saveAsset (line 11) | async function saveAsset(file: File): Promise<string> {
  function loadAssetUrl (line 21) | async function loadAssetUrl(url: string): Promise<string | null> {

FILE: packages/core/src/lib/space-detection.ts
  type Space (line 7) | type Space = {
  function initSpaceDetectionSync (line 23) | function initSpaceDetectionSync(
  function runSpaceDetection (line 130) | function runSpaceDetection(
  type Grid (line 176) | type Grid = {
  type WallSideUpdate (line 187) | type WallSideUpdate = {
  function detectSpacesForLevel (line 201) | function detectSpacesForLevel(
  function buildGrid (line 238) | function buildGrid(walls: WallNode[], resolution: number): Grid {
  function markWallCells (line 285) | function markWallCells(grid: Grid, wall: WallNode): void {
  function floodFillFromEdges (line 333) | function floodFillFromEdges(grid: Grid): void {
  function findInteriorSpaces (line 385) | function findInteriorSpaces(grid: Grid, levelId: string): Space[] {
  function extractPolygonFromCells (line 459) | function extractPolygonFromCells(cells: Set<string>, grid: Grid): Array<...
  function assignWallSides (line 492) | function assignWallSides(walls: WallNode[], grid: Grid): WallSideUpdate[] {
  function classifySide (line 543) | function classifySide(cell: string | undefined): 'interior' | 'exterior'...
  function getCellKey (line 557) | function getCellKey(grid: Grid, x: number, z: number): string | null {
  function getCellKeyFromIndex (line 571) | function getCellKeyFromIndex(x: number, z: number, width: number): string {
  function parseCellKey (line 578) | function parseCellKey(key: string): [number, number] {
  function wallTouchesOthers (line 591) | function wallTouchesOthers(wall: WallNode, otherWalls: WallNode[]): bool...
  function distanceToSegment (line 614) | function distanceToSegment(

FILE: packages/core/src/schema/base.ts
  type BaseNode (line 32) | type BaseNode = z.infer<typeof BaseNode>

FILE: packages/core/src/schema/camera.ts
  type Camera (line 13) | type Camera = z.infer<typeof CameraSchema>

FILE: packages/core/src/schema/collections.ts
  type CollectionId (line 4) | type CollectionId = `collection_${string}`
  type Collection (line 6) | type Collection = {

FILE: packages/core/src/schema/material.ts
  type MaterialPreset (line 15) | type MaterialPreset = z.infer<typeof MaterialPreset>
  type MaterialProperties (line 25) | type MaterialProperties = z.infer<typeof MaterialProperties>
  type MaterialSchema (line 38) | type MaterialSchema = z.infer<typeof MaterialSchema>
  constant DEFAULT_MATERIALS (line 40) | const DEFAULT_MATERIALS: Record<MaterialPreset, MaterialProperties> = {
  function resolveMaterial (line 123) | function resolveMaterial(material?: MaterialSchema): MaterialProperties {

FILE: packages/core/src/schema/nodes/building.ts
  type BuildingNode (line 21) | type BuildingNode = z.infer<typeof BuildingNode>

FILE: packages/core/src/schema/nodes/ceiling.ts
  type CeilingNode (line 23) | type CeilingNode = z.infer<typeof CeilingNode>

FILE: packages/core/src/schema/nodes/door.ts
  type DoorSegment (line 19) | type DoorSegment = z.infer<typeof DoorSegment>
  type DoorNode (line 85) | type DoorNode = z.infer<typeof DoorNode>

FILE: packages/core/src/schema/nodes/guide.ts
  type GuideNode (line 14) | type GuideNode = z.infer<typeof GuideNode>

FILE: packages/core/src/schema/nodes/item.ts
  type ToggleControl (line 68) | type ToggleControl = z.infer<typeof toggleControlSchema>
  type SliderControl (line 69) | type SliderControl = z.infer<typeof sliderControlSchema>
  type TemperatureControl (line 70) | type TemperatureControl = z.infer<typeof temperatureControlSchema>
  type Control (line 71) | type Control = z.infer<typeof controlSchema>
  type AnimationEffect (line 72) | type AnimationEffect = z.infer<typeof animationEffectSchema>
  type LightEffect (line 73) | type LightEffect = z.infer<typeof lightEffectSchema>
  type Effect (line 74) | type Effect = z.infer<typeof effectSchema>
  type Interactive (line 75) | type Interactive = z.infer<typeof interactiveSchema>
  type AssetInput (line 98) | type AssetInput = z.input<typeof assetSchema>
  type Asset (line 99) | type Asset = z.infer<typeof assetSchema>
  type ItemNode (line 132) | type ItemNode = z.infer<typeof ItemNode>
  function getScaledDimensions (line 138) | function getScaledDimensions(item: ItemNode): [number, number, number] {

FILE: packages/core/src/schema/nodes/level.ts
  type LevelNode (line 40) | type LevelNode = z.infer<typeof LevelNode>

FILE: packages/core/src/schema/nodes/roof-segment.ts
  type RoofType (line 8) | type RoofType = z.infer<typeof RoofType>
  type RoofSegmentNode (line 45) | type RoofSegmentNode = z.infer<typeof RoofSegmentNode>

FILE: packages/core/src/schema/nodes/roof.ts
  type RoofNode (line 27) | type RoofNode = z.infer<typeof RoofNode>

FILE: packages/core/src/schema/nodes/scan.ts
  type ScanNode (line 14) | type ScanNode = z.infer<typeof ScanNode>

FILE: packages/core/src/schema/nodes/site.ts
  type SiteNode (line 47) | type SiteNode = z.infer<typeof SiteNode>

FILE: packages/core/src/schema/nodes/slab.ts
  type SlabNode (line 21) | type SlabNode = z.infer<typeof SlabNode>

FILE: packages/core/src/schema/nodes/stair-segment.ts
  type StairSegmentType (line 8) | type StairSegmentType = z.infer<typeof StairSegmentType>
  type AttachmentSide (line 12) | type AttachmentSide = z.infer<typeof AttachmentSide>
  type StairSegmentNode (line 53) | type StairSegmentNode = z.infer<typeof StairSegmentNode>

FILE: packages/core/src/schema/nodes/stair.ts
  type StairNode (line 27) | type StairNode = z.infer<typeof StairNode>

FILE: packages/core/src/schema/nodes/wall.ts
  type WallNode (line 35) | type WallNode = z.infer<typeof WallNode>

FILE: packages/core/src/schema/nodes/window.ts
  type WindowNode (line 48) | type WindowNode = z.infer<typeof WindowNode>

FILE: packages/core/src/schema/nodes/zone.ts
  type ZoneNode (line 27) | type ZoneNode = z.infer<typeof ZoneNode>

FILE: packages/core/src/schema/types.ts
  type AnyNode (line 38) | type AnyNode = z.infer<typeof AnyNode>
  type AnyNodeType (line 39) | type AnyNodeType = AnyNode['type']
  type AnyNodeId (line 40) | type AnyNodeId = AnyNode['id']

FILE: packages/core/src/store/actions/node-actions.ts
  type AnyContainerNode (line 5) | type AnyContainerNode = AnyNode & { children: string[] }

FILE: packages/core/src/store/use-interactive.ts
  type ControlValue (line 8) | type ControlValue = boolean | number
  type ItemInteractiveState (line 10) | type ItemInteractiveState = {
  type InteractiveStore (line 15) | type InteractiveStore = {

FILE: packages/core/src/store/use-scene.ts
  function migrateNodes (line 14) | function migrateNodes(nodes: Record<string, any>): Record<string, AnyNod...
  type SceneState (line 57) | type SceneState = {
  type UseSceneStore (line 102) | type UseSceneStore = UseBoundStore<StoreApi<SceneState>> & {
  function clearTemporalTracking (line 331) | function clearTemporalTracking() {
  function clearSceneHistory (line 337) | function clearSceneHistory() {

FILE: packages/core/src/systems/ceiling/ceiling-system.tsx
  function updateCeilingGeometry (line 39) | function updateCeilingGeometry(node: CeilingNode, mesh: THREE.Mesh) {
  function generateCeilingGeometry (line 58) | function generateCeilingGeometry(ceilingNode: CeilingNode): THREE.Buffer...

FILE: packages/core/src/systems/door/door-system.tsx
  function addBox (line 58) | function addBox(
  function updateDoorMesh (line 73) | function updateDoorMesh(node: DoorNode, mesh: THREE.Mesh) {

FILE: packages/core/src/systems/roof/roof-system.tsx
  constant MAX_ROOFS_PER_FRAME (line 24) | const MAX_ROOFS_PER_FRAME = 1
  constant MAX_SEGMENTS_PER_FRAME (line 25) | const MAX_SEGMENTS_PER_FRAME = 3
  function updateRoofSegmentGeometry (line 122) | function updateRoofSegmentGeometry(node: RoofSegmentNode, mesh: THREE.Me...
  function updateMergedRoofGeometry (line 134) | function updateMergedRoofGeometry(
  constant ROOF_MATERIAL_SLOT_COUNT (line 271) | const ROOF_MATERIAL_SLOT_COUNT = 4
  function mapRoofGroupMaterialIndex (line 273) | function mapRoofGroupMaterialIndex(
  function normalizeRoofMaterialIndex (line 284) | function normalizeRoofMaterialIndex(materialIndex: number | undefined): ...
  constant SHINGLE_SURFACE_EPSILON (line 291) | const SHINGLE_SURFACE_EPSILON = 0.02
  constant RAKE_FACE_NORMAL_EPSILON (line 292) | const RAKE_FACE_NORMAL_EPSILON = 0.3
  constant RAKE_FACE_ALIGNMENT_EPSILON (line 293) | const RAKE_FACE_ALIGNMENT_EPSILON = 0.35
  function getRoofSegmentBrushes (line 299) | function getRoofSegmentBrushes(
  function generateRoofSegmentGeometry (line 577) | function generateRoofSegmentGeometry(node: RoofSegmentNode): THREE.Buffe...
  type Insets (line 635) | type Insets = {
  function remapRoofShellFaces (line 643) | function remapRoofShellFaces(geometry: THREE.BufferGeometry, node: RoofS...
  function isRakeFace (line 718) | function isRakeFace(
  function getRakeAxis (line 748) | function getRakeAxis(node: RoofSegmentNode): 'x' | 'z' | null {
  function getModuleFaces (line 758) | function getModuleFaces(
  function createGeometryFromFaces (line 916) | function createGeometryFromFaces(

FILE: packages/core/src/systems/slab/slab-system.tsx
  function updateSlabGeometry (line 40) | function updateSlabGeometry(node: SlabNode, mesh: THREE.Mesh) {
  constant SLAB_OUTSET (line 48) | const SLAB_OUTSET = 0.05
  function outsetPolygon (line 54) | function outsetPolygon(polygon: Array<[number, number]>, amount: number)...
  function generateSlabGeometry (line 104) | function generateSlabGeometry(slabNode: SlabNode): THREE.BufferGeometry {

FILE: packages/core/src/systems/stair/stair-system.tsx
  constant MAX_STAIRS_PER_FRAME (line 9) | const MAX_STAIRS_PER_FRAME = 2
  constant MAX_SEGMENTS_PER_FRAME (line 10) | const MAX_SEGMENTS_PER_FRAME = 4
  function generateStairSegmentGeometry (line 114) | function generateStairSegmentGeometry(
  function updateStairSegmentGeometry (line 189) | function updateStairSegmentGeometry(node: StairSegmentNode, mesh: THREE....
  function syncSegmentMeshTransforms (line 207) | function syncSegmentMeshTransforms(stairNode: StairNode, nodes: Record<s...
  function updateMergedStairGeometry (line 237) | function updateMergedStairGeometry(
  type SegmentTransform (line 294) | interface SegmentTransform {
  function computeSegmentTransforms (line 303) | function computeSegmentTransforms(segments: StairSegmentNode[]): Segment...
  function computeAbsoluteHeight (line 354) | function computeAbsoluteHeight(node: StairSegmentNode): number {

FILE: packages/core/src/systems/wall/wall-footprint.ts
  constant DEFAULT_WALL_THICKNESS (line 4) | const DEFAULT_WALL_THICKNESS = 0.1
  constant DEFAULT_WALL_HEIGHT (line 5) | const DEFAULT_WALL_HEIGHT = 2.5
  function getWallThickness (line 7) | function getWallThickness(wallNode: WallNode): number {
  function getWallPlanFootprint (line 11) | function getWallPlanFootprint(wallNode: WallNode, miterData: WallMiterDa...

FILE: packages/core/src/systems/wall/wall-mitering.ts
  type Point2D (line 7) | interface Point2D {
  type LineEquation (line 12) | interface LineEquation {
  type WallIntersections (line 19) | type WallIntersections = Map<string, { left?: Point2D; right?: Point2D }>
  type JunctionData (line 22) | type JunctionData = Map<string, WallIntersections>
  constant TOLERANCE (line 28) | const TOLERANCE = 0.001
  function pointToKey (line 30) | function pointToKey(p: Point2D, tolerance = TOLERANCE): string {
  function createLineFromPointAndVector (line 35) | function createLineFromPointAndVector(p: Point2D, v: Point2D): LineEquat...
  function pointOnWallSegment (line 45) | function pointOnWallSegment(point: Point2D, wall: WallNode, tolerance = ...
  type Junction (line 79) | interface Junction {
  function findJunctions (line 84) | function findJunctions(walls: WallNode[]): Map<string, Junction> {
  type ProcessedWall (line 134) | interface ProcessedWall {
  function calculateJunctionIntersections (line 142) | function calculateJunctionIntersections(
  type WallMiterData (line 244) | interface WallMiterData {
  function calculateLevelMiters (line 254) | function calculateLevelMiters(walls: WallNode[]): WallMiterData {
  function getAdjacentWallIds (line 270) | function getAdjacentWallIds(allWalls: WallNode[], dirtyWallIds: Set<stri...

FILE: packages/core/src/systems/wall/wall-system.tsx
  function getLevelWalls (line 84) | function getLevelWalls(levelId: string): WallNode[] {
  function updateWallGeometry (line 104) | function updateWallGeometry(wallId: string, miterData: WallMiterData) {
  function generateExtrudedWall (line 143) | function generateExtrudedWall(
  function collectCutoutBrushes (line 248) | function collectCutoutBrushes(

FILE: packages/core/src/systems/window/window-system.tsx
  function addBox (line 58) | function addBox(
  function updateWindowMesh (line 73) | function updateWindowMesh(node: WindowNode, mesh: THREE.Mesh) {

FILE: packages/core/src/utils/clone-scene-graph.ts
  type SceneGraph (line 5) | type SceneGraph = {
  function extractIdPrefix (line 14) | function extractIdPrefix(id: string): string {
  function cloneSceneGraph (line 28) | function cloneSceneGraph(sceneGraph: SceneGraph): SceneGraph {
  function cloneLevelSubtree (line 135) | function cloneLevelSubtree(
  function forkSceneGraph (line 219) | function forkSceneGraph(sceneGraph: SceneGraph): SceneGraph {

FILE: packages/editor/src/components/editor/custom-camera-controls.tsx
  constant DEFAULT_MAX_POLAR_ANGLE (line 19) | const DEFAULT_MAX_POLAR_ANGLE = Math.PI / 2 - 0.1
  constant DEBUG_MAX_POLAR_ANGLE (line 20) | const DEBUG_MAX_POLAR_ANGLE = Math.PI - 0.05

FILE: packages/editor/src/components/editor/editor-layout-v2.tsx
  constant SIDEBAR_MIN_WIDTH (line 8) | const SIDEBAR_MIN_WIDTH = 300
  constant SIDEBAR_MAX_WIDTH (line 9) | const SIDEBAR_MAX_WIDTH = 800
  constant SIDEBAR_COLLAPSE_THRESHOLD (line 10) | const SIDEBAR_COLLAPSE_THRESHOLD = 220
  function LeftColumn (line 14) | function LeftColumn({
  function RightColumn (line 126) | function RightColumn({
  type EditorLayoutV2Props (line 170) | interface EditorLayoutV2Props {
  function EditorLayoutV2 (line 180) | function EditorLayoutV2({

FILE: packages/editor/src/components/editor/export-manager.tsx
  function ExportManager (line 10) | function ExportManager() {
  function downloadBlob (line 71) | function downloadBlob(blob: Blob, filename: string) {

FILE: packages/editor/src/components/editor/first-person-controls.tsx
  constant EYE_HEIGHT (line 9) | const EYE_HEIGHT = 1.65
  constant MOVE_SPEED (line 11) | const MOVE_SPEED = 5
  constant SPRINT_MULTIPLIER (line 13) | const SPRINT_MULTIPLIER = 2
  constant VERTICAL_SPEED (line 15) | const VERTICAL_SPEED = 3
  constant MOUSE_SENSITIVITY (line 17) | const MOUSE_SENSITIVITY = 0.002
  constant MIN_Y (line 19) | const MIN_Y = EYE_HEIGHT
  function ControlHint (line 231) | function ControlHint({ label, keys }: { label: string; keys: string[] }) {

FILE: packages/editor/src/components/editor/floating-action-menu.tsx
  constant ALLOWED_TYPES (line 23) | const ALLOWED_TYPES = ['item', 'door', 'window', 'roof', 'roof-segment',...
  constant DELETE_ONLY_TYPES (line 24) | const DELETE_ONLY_TYPES = ['wall', 'slab']
  function FloatingActionMenu (line 26) | function FloatingActionMenu() {

FILE: packages/editor/src/components/editor/floorplan-panel.tsx
  constant FALLBACK_VIEW_SIZE (line 55) | const FALLBACK_VIEW_SIZE = 12
  constant FLOORPLAN_PADDING (line 56) | const FLOORPLAN_PADDING = 2
  constant MIN_VIEWPORT_WIDTH_RATIO (line 57) | const MIN_VIEWPORT_WIDTH_RATIO = 0.08
  constant MAX_VIEWPORT_WIDTH_RATIO (line 58) | const MAX_VIEWPORT_WIDTH_RATIO = 40
  constant PANEL_MIN_WIDTH (line 59) | const PANEL_MIN_WIDTH = 420
  constant PANEL_MIN_HEIGHT (line 60) | const PANEL_MIN_HEIGHT = 320
  constant PANEL_DEFAULT_WIDTH (line 61) | const PANEL_DEFAULT_WIDTH = 560
  constant PANEL_DEFAULT_HEIGHT (line 62) | const PANEL_DEFAULT_HEIGHT = 360
  constant PANEL_MARGIN (line 63) | const PANEL_MARGIN = 16
  constant PANEL_DEFAULT_BOTTOM_OFFSET (line 64) | const PANEL_DEFAULT_BOTTOM_OFFSET = 96
  constant MIN_GRID_SCREEN_SPACING (line 65) | const MIN_GRID_SCREEN_SPACING = 12
  constant GRID_COORDINATE_PRECISION (line 66) | const GRID_COORDINATE_PRECISION = 6
  constant MAJOR_GRID_STEP (line 67) | const MAJOR_GRID_STEP = WALL_GRID_STEP * 2
  constant FLOORPLAN_WALL_THICKNESS_SCALE (line 68) | const FLOORPLAN_WALL_THICKNESS_SCALE = 1.18
  constant FLOORPLAN_MIN_VISIBLE_WALL_THICKNESS (line 69) | const FLOORPLAN_MIN_VISIBLE_WALL_THICKNESS = 0.13
  constant FLOORPLAN_MAX_EXTRA_THICKNESS (line 70) | const FLOORPLAN_MAX_EXTRA_THICKNESS = 0.035
  constant FLOORPLAN_PANEL_LAYOUT_STORAGE_KEY (line 71) | const FLOORPLAN_PANEL_LAYOUT_STORAGE_KEY = 'pascal-editor-floorplan-pane...
  constant EMPTY_WALL_MITER_DATA (line 72) | const EMPTY_WALL_MITER_DATA = calculateLevelMiters([])
  constant EDITOR_CURSOR (line 73) | const EDITOR_CURSOR = "url('/cursor.svg') 4 2, default"
  constant FLOORPLAN_CURSOR_INDICATOR_OFFSET_X (line 74) | const FLOORPLAN_CURSOR_INDICATOR_OFFSET_X = 20
  constant FLOORPLAN_CURSOR_INDICATOR_OFFSET_Y (line 75) | const FLOORPLAN_CURSOR_INDICATOR_OFFSET_Y = 14
  constant FLOORPLAN_CURSOR_MARKER_CORE_RADIUS (line 76) | const FLOORPLAN_CURSOR_MARKER_CORE_RADIUS = 0.06
  constant FLOORPLAN_CURSOR_MARKER_GLOW_RADIUS (line 77) | const FLOORPLAN_CURSOR_MARKER_GLOW_RADIUS = 0.2
  constant FLOORPLAN_HOVER_TRANSITION (line 78) | const FLOORPLAN_HOVER_TRANSITION = 'opacity 180ms cubic-bezier(0.2, 0, 0...
  constant FLOORPLAN_WALL_HIT_STROKE_WIDTH (line 79) | const FLOORPLAN_WALL_HIT_STROKE_WIDTH = 18
  constant FLOORPLAN_WALL_HOVER_GLOW_STROKE_WIDTH (line 80) | const FLOORPLAN_WALL_HOVER_GLOW_STROKE_WIDTH = 18
  constant FLOORPLAN_WALL_HOVER_RING_STROKE_WIDTH (line 81) | const FLOORPLAN_WALL_HOVER_RING_STROKE_WIDTH = 8
  constant FLOORPLAN_OPENING_HIT_STROKE_WIDTH (line 82) | const FLOORPLAN_OPENING_HIT_STROKE_WIDTH = 16
  constant FLOORPLAN_OPENING_STROKE_WIDTH (line 83) | const FLOORPLAN_OPENING_STROKE_WIDTH = 0.05
  constant FLOORPLAN_OPENING_DETAIL_STROKE_WIDTH (line 84) | const FLOORPLAN_OPENING_DETAIL_STROKE_WIDTH = 0.02
  constant FLOORPLAN_OPENING_DASHED_STROKE_WIDTH (line 85) | const FLOORPLAN_OPENING_DASHED_STROKE_WIDTH = 0.02
  constant FLOORPLAN_ENDPOINT_HIT_STROKE_WIDTH (line 86) | const FLOORPLAN_ENDPOINT_HIT_STROKE_WIDTH = 18
  constant FLOORPLAN_ENDPOINT_HOVER_GLOW_STROKE_WIDTH (line 87) | const FLOORPLAN_ENDPOINT_HOVER_GLOW_STROKE_WIDTH = 16
  constant FLOORPLAN_ENDPOINT_HOVER_RING_STROKE_WIDTH (line 88) | const FLOORPLAN_ENDPOINT_HOVER_RING_STROKE_WIDTH = 7
  constant FLOORPLAN_MARQUEE_DRAG_THRESHOLD_PX (line 89) | const FLOORPLAN_MARQUEE_DRAG_THRESHOLD_PX = 4
  constant FLOORPLAN_MEASUREMENT_OFFSET (line 90) | const FLOORPLAN_MEASUREMENT_OFFSET = 0.46
  constant FLOORPLAN_MEASUREMENT_EXTENSION_OVERSHOOT (line 91) | const FLOORPLAN_MEASUREMENT_EXTENSION_OVERSHOOT = 0.08
  constant FLOORPLAN_MEASUREMENT_LINE_WIDTH (line 92) | const FLOORPLAN_MEASUREMENT_LINE_WIDTH = 1.2
  constant FLOORPLAN_MEASUREMENT_LINE_OUTLINE_WIDTH (line 93) | const FLOORPLAN_MEASUREMENT_LINE_OUTLINE_WIDTH = 2.8
  constant FLOORPLAN_MEASUREMENT_LINE_OPACITY (line 94) | const FLOORPLAN_MEASUREMENT_LINE_OPACITY = 0.72
  constant FLOORPLAN_MEASUREMENT_LINE_OUTLINE_OPACITY (line 95) | const FLOORPLAN_MEASUREMENT_LINE_OUTLINE_OPACITY = 0.9
  constant FLOORPLAN_MEASUREMENT_LABEL_FONT_SIZE (line 96) | const FLOORPLAN_MEASUREMENT_LABEL_FONT_SIZE = 0.15
  constant FLOORPLAN_MEASUREMENT_LABEL_OPACITY (line 97) | const FLOORPLAN_MEASUREMENT_LABEL_OPACITY = 0.82
  constant FLOORPLAN_MEASUREMENT_LABEL_STROKE_WIDTH (line 98) | const FLOORPLAN_MEASUREMENT_LABEL_STROKE_WIDTH = 0.05
  constant FLOORPLAN_MEASUREMENT_LABEL_GAP (line 99) | const FLOORPLAN_MEASUREMENT_LABEL_GAP = 0.56
  constant FLOORPLAN_MEASUREMENT_LABEL_LINE_PADDING (line 100) | const FLOORPLAN_MEASUREMENT_LABEL_LINE_PADDING = 0.14
  constant FLOORPLAN_ACTION_MENU_HORIZONTAL_PADDING (line 101) | const FLOORPLAN_ACTION_MENU_HORIZONTAL_PADDING = 60
  constant FLOORPLAN_ACTION_MENU_MIN_ANCHOR_Y (line 102) | const FLOORPLAN_ACTION_MENU_MIN_ANCHOR_Y = 56
  constant FLOORPLAN_ACTION_MENU_OFFSET_Y (line 103) | const FLOORPLAN_ACTION_MENU_OFFSET_Y = 10
  constant FLOORPLAN_DEFAULT_WINDOW_LOCAL_Y (line 104) | const FLOORPLAN_DEFAULT_WINDOW_LOCAL_Y = 1.5
  constant FLOORPLAN_GUIDE_BASE_WIDTH (line 107) | const FLOORPLAN_GUIDE_BASE_WIDTH = 10
  constant FLOORPLAN_GUIDE_MIN_SCALE (line 108) | const FLOORPLAN_GUIDE_MIN_SCALE = 0.01
  constant FLOORPLAN_GUIDE_HANDLE_SIZE (line 109) | const FLOORPLAN_GUIDE_HANDLE_SIZE = 0.22
  constant FLOORPLAN_GUIDE_HANDLE_HIT_RADIUS (line 110) | const FLOORPLAN_GUIDE_HANDLE_HIT_RADIUS = 0.3
  constant FLOORPLAN_GUIDE_SELECTION_STROKE_WIDTH (line 111) | const FLOORPLAN_GUIDE_SELECTION_STROKE_WIDTH = 0.05
  constant FLOORPLAN_GUIDE_HANDLE_HINT_OFFSET (line 112) | const FLOORPLAN_GUIDE_HANDLE_HINT_OFFSET = 72
  constant FLOORPLAN_GUIDE_HANDLE_HINT_PADDING_X (line 113) | const FLOORPLAN_GUIDE_HANDLE_HINT_PADDING_X = 92
  constant FLOORPLAN_GUIDE_HANDLE_HINT_PADDING_Y (line 114) | const FLOORPLAN_GUIDE_HANDLE_HINT_PADDING_Y = 48
  constant FLOORPLAN_GUIDE_ROTATION_SNAP_DEGREES (line 115) | const FLOORPLAN_GUIDE_ROTATION_SNAP_DEGREES = 45
  constant FLOORPLAN_GUIDE_ROTATION_FINE_SNAP_DEGREES (line 116) | const FLOORPLAN_GUIDE_ROTATION_FINE_SNAP_DEGREES = 1
  constant FLOORPLAN_SITE_COLOR (line 117) | const FLOORPLAN_SITE_COLOR = '#10b981'
  type FloorplanViewport (line 119) | type FloorplanViewport = {
  type SvgPoint (line 125) | type SvgPoint = {
  type PanState (line 130) | type PanState = {
  type GestureLikeEvent (line 136) | type GestureLikeEvent = Event & {
  type PanelRect (line 142) | type PanelRect = {
  type ResizeDirection (line 149) | type ResizeDirection = 'n' | 's' | 'e' | 'w' | 'ne' | 'nw' | 'se' | 'sw'
  type PanelInteractionState (line 151) | type PanelInteractionState = {
  type ViewportBounds (line 160) | type ViewportBounds = {
  type OpeningNode (line 165) | type OpeningNode = WindowNode | DoorNode
  type WallEndpoint (line 167) | type WallEndpoint = 'start' | 'end'
  type FloorplanCursorIndicator (line 169) | type FloorplanCursorIndicator =
  type PersistedPanelLayout (line 179) | type PersistedPanelLayout = {
  type FloorplanSelectionBounds (line 184) | type FloorplanSelectionBounds = {
  type FloorplanMarqueeState (line 191) | type FloorplanMarqueeState = {
  type WallEndpointDragState (line 199) | type WallEndpointDragState = {
  constant GUIDE_CORNERS (line 207) | const GUIDE_CORNERS = ['nw', 'ne', 'se', 'sw'] as const
  type GuideCorner (line 209) | type GuideCorner = (typeof GUIDE_CORNERS)[number]
  type GuideInteractionMode (line 211) | type GuideInteractionMode = 'resize' | 'rotate' | 'translate'
  type GuideTransformDraft (line 213) | type GuideTransformDraft = {
  type GuideHandleHintAnchor (line 220) | type GuideHandleHintAnchor = {
  type GuideInteractionState (line 227) | type GuideInteractionState = {
  type WallEndpointDraft (line 241) | type WallEndpointDraft = {
  type SlabBoundaryDraft (line 248) | type SlabBoundaryDraft = {
  type SlabVertexDragState (line 253) | type SlabVertexDragState = {
  type SiteBoundaryDraft (line 259) | type SiteBoundaryDraft = {
  type SiteVertexDragState (line 264) | type SiteVertexDragState = {
  type ZoneBoundaryDraft (line 270) | type ZoneBoundaryDraft = {
  type ZoneVertexDragState (line 275) | type ZoneVertexDragState = {
  type WallPolygonEntry (line 281) | type WallPolygonEntry = {
  type OpeningPolygonEntry (line 287) | type OpeningPolygonEntry = {
  type SlabPolygonEntry (line 293) | type SlabPolygonEntry = {
  type SitePolygonEntry (line 300) | type SitePolygonEntry = {
  type ZonePolygonEntry (line 306) | type ZonePolygonEntry = {
  type FloorplanPalette (line 312) | type FloorplanPalette = {
  function clamp (line 380) | function clamp(value: number, min: number, max: number) {
  function getSelectionModifierKeys (line 384) | function getSelectionModifierKeys(event?: { metaKey?: boolean; ctrlKey?:...
  function toPoint2D (line 391) | function toPoint2D(point: WallPlanPoint): Point2D {
  function toWallPlanPoint (line 395) | function toWallPlanPoint(point: Point2D): WallPlanPoint {
  function toSvgX (line 399) | function toSvgX(value: number): number {
  function toSvgY (line 403) | function toSvgY(value: number): number {
  function toSvgPoint (line 407) | function toSvgPoint(point: Point2D): SvgPoint {
  function toSvgPlanPoint (line 414) | function toSvgPlanPoint(point: WallPlanPoint): SvgPoint {
  function toPlanPointFromSvgPoint (line 421) | function toPlanPointFromSvgPoint(svgPoint: SvgPoint): WallPlanPoint {
  function rotateVector (line 425) | function rotateVector([x, y]: WallPlanPoint, angle: number): WallPlanPoi...
  function addVectorToSvgPoint (line 431) | function addVectorToSvgPoint(point: SvgPoint, [dx, dy]: WallPlanPoint): ...
  function subtractSvgPoints (line 438) | function subtractSvgPoints(point: SvgPoint, origin: SvgPoint): WallPlanP...
  function midpointBetweenSvgPoints (line 442) | function midpointBetweenSvgPoints(start: SvgPoint, end: SvgPoint): SvgPo...
  function getGuideWidth (line 449) | function getGuideWidth(scale: number) {
  function getGuideHeight (line 453) | function getGuideHeight(width: number, aspectRatio: number) {
  function getGuideCenterSvgPoint (line 457) | function getGuideCenterSvgPoint(guide: GuideNode): SvgPoint {
  function getGuideCornerLocalOffset (line 464) | function getGuideCornerLocalOffset(
  function getGuideCornerSvgPoint (line 473) | function getGuideCornerSvgPoint(
  function snapAngleToIncrement (line 486) | function snapAngleToIncrement(angle: number, incrementDegrees: number) {
  function toPositiveAngleDegrees (line 491) | function toPositiveAngleDegrees(angle: number) {
  function getResizeCursorForAngle (line 496) | function getResizeCursorForAngle(angle: number) {
  function getGuideResizeCursor (line 514) | function getGuideResizeCursor(corner: GuideCorner, rotationSvg: number) {
  function buildCursorUrl (line 519) | function buildCursorUrl(svgMarkup: string, hotspotX: number, hotspotY: n...
  function getGuideRotateCursor (line 523) | function getGuideRotateCursor(isDarkMode: boolean) {
  function buildGuideTranslateDraft (line 538) | function buildGuideTranslateDraft(
  function normalizeAngle (line 555) | function normalizeAngle(angle: number) {
  function areGuideTransformDraftsEqual (line 569) | function areGuideTransformDraftsEqual(
  function doesGuideMatchDraft (line 591) | function doesGuideMatchDraft(guide: GuideNode, draft: GuideTransformDraf...
  function buildGuideResizeDraft (line 600) | function buildGuideResizeDraft(
  function buildGuideRotationDraft (line 631) | function buildGuideRotationDraft(
  function toSvgSelectionBounds (line 664) | function toSvgSelectionBounds(bounds: FloorplanSelectionBounds) {
  function getFloorplanSelectionBounds (line 673) | function getFloorplanSelectionBounds(
  function isPointInsideSelectionBounds (line 685) | function isPointInsideSelectionBounds(point: Point2D, bounds: FloorplanS...
  function isPointInsidePolygon (line 694) | function isPointInsidePolygon(point: Point2D, polygon: Point2D[]) {
  function getLineOrientation (line 722) | function getLineOrientation(start: Point2D, end: Point2D, point: Point2D) {
  function isPointOnSegment (line 726) | function isPointOnSegment(point: Point2D, start: Point2D, end: Point2D) {
  function doSegmentsIntersect (line 738) | function doSegmentsIntersect(
  function doesPolygonIntersectSelectionBounds (line 765) | function doesPolygonIntersectSelectionBounds(polygon: Point2D[], bounds:...
  function getDistanceToWallSegment (line 810) | function getDistanceToWallSegment(point: Point2D, start: WallPlanPoint, ...
  function getViewportBounds (line 830) | function getViewportBounds(): ViewportBounds {
  function getPanelSizeLimits (line 844) | function getPanelSizeLimits(bounds: ViewportBounds) {
  function constrainPanelRect (line 856) | function constrainPanelRect(rect: PanelRect, bounds: ViewportBounds): Pa...
  function getPanelPositionRatios (line 870) | function getPanelPositionRatios(rect: PanelRect, bounds: ViewportBounds) {
  function adaptPanelRectToBounds (line 880) | function adaptPanelRectToBounds(
  function isFiniteNumber (line 904) | function isFiniteNumber(value: unknown): value is number {
  function isValidPanelRect (line 908) | function isValidPanelRect(value: unknown): value is PanelRect {
  function isValidViewportBounds (line 919) | function isValidViewportBounds(value: unknown): value is ViewportBounds {
  function readPersistedPanelLayout (line 928) | function readPersistedPanelLayout(currentBounds: ViewportBounds): PanelR...
  function writePersistedPanelLayout (line 950) | function writePersistedPanelLayout(layout: PersistedPanelLayout) {
  function getInitialPanelRect (line 958) | function getInitialPanelRect(bounds: ViewportBounds): PanelRect {
  function movePanelRect (line 970) | function movePanelRect(
  function resizePanelRect (line 986) | function resizePanelRect(
  function formatPolygonPoints (line 1036) | function formatPolygonPoints(points: Point2D[]): string {
  function formatPolygonPath (line 1045) | function formatPolygonPath(points: Point2D[], holes: Point2D[][] = []): ...
  function toFloorplanPolygon (line 1067) | function toFloorplanPolygon(points: Array<[number, number]>): Point2D[] {
  function isPointInsidePolygonWithHoles (line 1071) | function isPointInsidePolygonWithHoles(
  function isPointNearPlanPoint (line 1081) | function isPointNearPlanPoint(a: WallPlanPoint, b: WallPlanPoint, thresh...
  function calculatePolygonSnapPoint (line 1085) | function calculatePolygonSnapPoint(
  function snapPolygonDraftPoint (line 1112) | function snapPolygonDraftPoint({
  function pointMatchesWallPlanPoint (line 1130) | function pointMatchesWallPlanPoint(
  function getWallHoverSidePaths (line 1142) | function getWallHoverSidePaths(polygon: Point2D[], wall: WallNode): [str...
  function buildDraftWall (line 1170) | function buildDraftWall(levelId: string, start: WallPlanPoint, end: Wall...
  function pointsEqual (line 1187) | function pointsEqual(a: WallPlanPoint, b: WallPlanPoint): boolean {
  function polygonsEqual (line 1191) | function polygonsEqual(a: WallPlanPoint[], b: Array<[number, number]>): ...
  function buildWallEndpointDraft (line 1205) | function buildWallEndpointDraft(
  function buildWallWithUpdatedEndpoints (line 1219) | function buildWallWithUpdatedEndpoints(
  function getFloorplanWallThickness (line 1231) | function getFloorplanWallThickness(wall: WallNode): number {
  function getFloorplanWall (line 1241) | function getFloorplanWall(wall: WallNode): WallNode {
  type WallMeasurementOverlay (line 1249) | type WallMeasurementOverlay = {
  function formatMeasurement (line 1262) | function formatMeasurement(value: number, unit: 'metric' | 'imperial') {
  function getPolygonAreaAndCentroid (line 1273) | function getPolygonAreaAndCentroid(polygon: Point2D[]) {
  function getSlabArea (line 1299) | function getSlabArea(polygon: Point2D[], holes: Point2D[][]) {
  function formatArea (line 1308) | function formatArea(areaSqM: number, unit: 'metric' | 'imperial') {
  function FloorplanMeasurementLine (line 1330) | function FloorplanMeasurementLine({
  function getWallMeasurementOverlay (line 1376) | function getWallMeasurementOverlay(
  function getOpeningFootprint (line 1482) | function getOpeningFootprint(wall: WallNode, node: WindowNode | DoorNode...
  function getOpeningCenterLine (line 1518) | function getOpeningCenterLine(polygon: Point2D[]) {
  function normalizeGridCoordinate (line 1537) | function normalizeGridCoordinate(value: number): number {
  function isGridAligned (line 1541) | function isGridAligned(value: number, step: number): boolean {
  function getVisibleGridSteps (line 1551) | function getVisibleGridSteps(
  function buildGridPath (line 1571) | function buildGridPath(
  function findClosestWallPoint (line 1618) | function findClosestWallPoint(
  type GuideImageDimensions (line 1656) | type GuideImageDimensions = {
  function useResolvedAssetUrl (line 1661) | function useResolvedAssetUrl(url: string) {
  function useGuideImageDimensions (line 1687) | function useGuideImageDimensions(url: string | null) {
  function FloorplanGuideImage (line 1731) | function FloorplanGuideImage({
  function FloorplanGuideSelectionOverlay (line 1896) | function FloorplanGuideSelectionOverlay({
  function FloorplanGuideHandleHint (line 1998) | function FloorplanGuideHandleHint({
  function FloorplanPanel (line 2998) | function FloorplanPanel() {

FILE: packages/editor/src/components/editor/index.tsx
  constant CAMERA_CONTROLS_HINT_DISMISSED_STORAGE_KEY (line 56) | const CAMERA_CONTROLS_HINT_DISMISSED_STORAGE_KEY = 'editor-camera-contro...
  function initializeEditorRuntime (line 58) | function initializeEditorRuntime() {
  type EditorProps (line 66) | interface EditorProps {
  function EditorSceneCrashFallback (line 110) | function EditorSceneCrashFallback() {
  function SidebarSlot (line 140) | function SidebarSlot({ children }: { children: ReactNode }) {
  function ViewerOverlays (line 234) | function ViewerOverlays({ left, children }: { left: number; children: Re...
  function SelectionPersistenceManager (line 256) | function SelectionPersistenceManager({ enabled }: { enabled: boolean }) {
  type ShortcutKey (line 270) | type ShortcutKey = {
  type CameraControlHint (line 274) | type CameraControlHint = {
  constant EDITOR_CAMERA_CONTROL_HINTS (line 280) | const EDITOR_CAMERA_CONTROL_HINTS: CameraControlHint[] = [
  constant PREVIEW_CAMERA_CONTROL_HINTS (line 289) | const PREVIEW_CAMERA_CONTROL_HINTS: CameraControlHint[] = [
  constant CAMERA_SHORTCUT_KEY_META (line 295) | const CAMERA_SHORTCUT_KEY_META: Record<string, { icon?: string; label: s...
  function readCameraControlsHintDismissed (line 318) | function readCameraControlsHintDismissed(): boolean {
  function writeCameraControlsHintDismissed (line 330) | function writeCameraControlsHintDismissed(dismissed: boolean) {
  function InlineShortcutKey (line 345) | function InlineShortcutKey({ shortcutKey }: { shortcutKey: ShortcutKey }) {
  function ShortcutSequence (line 369) | function ShortcutSequence({ keys }: { keys: ShortcutKey[] }) {
  function CameraControlHintItem (line 382) | function CameraControlHintItem({ hint }: { hint: CameraControlHint }) {
  function ViewerCanvasControlsHint (line 401) | function ViewerCanvasControlsHint({
  function Editor (line 447) | function Editor({

FILE: packages/editor/src/components/editor/node-action-menu.tsx
  type NodeActionMenuProps (line 6) | type NodeActionMenuProps = {
  function NodeActionMenu (line 16) | function NodeActionMenu({

FILE: packages/editor/src/components/editor/preset-thumbnail-generator.tsx
  constant THUMBNAIL_SIZE (line 9) | const THUMBNAIL_SIZE = 1080
  constant CAMERA_FOV (line 10) | const CAMERA_FOV = 45

FILE: packages/editor/src/components/editor/selection-manager.tsx
  type SelectableNodeType (line 26) | type SelectableNodeType =
  type ModifierKeys (line 40) | type ModifierKeys = {
  type SelectionStrategy (line 45) | interface SelectionStrategy {
  type SelectionTarget (line 52) | type SelectionTarget = {
  constant SELECTION_STRATEGIES (line 89) | const SELECTION_STRATEGIES: Record<string, SelectionStrategy> = {

FILE: packages/editor/src/components/editor/site-edge-labels.tsx
  function formatMeasurement (line 11) | function formatMeasurement(value: number, unit: 'metric' | 'imperial') {
  function SiteEdgeLabels (line 22) | function SiteEdgeLabels() {

FILE: packages/editor/src/components/editor/thumbnail-generator.tsx
  constant THUMBNAIL_WIDTH (line 10) | const THUMBNAIL_WIDTH = 1920
  constant THUMBNAIL_HEIGHT (line 11) | const THUMBNAIL_HEIGHT = 1080
  constant AUTO_SAVE_DELAY (line 12) | const AUTO_SAVE_DELAY = 10_000
  type ThumbnailGeneratorProps (line 14) | interface ThumbnailGeneratorProps {

FILE: packages/editor/src/components/editor/wall-measurement-label.tsx
  constant GUIDE_Y_OFFSET (line 21) | const GUIDE_Y_OFFSET = 0.08
  constant LABEL_LIFT (line 22) | const LABEL_LIFT = 0.08
  constant BAR_THICKNESS (line 23) | const BAR_THICKNESS = 0.012
  constant LINE_OPACITY (line 24) | const LINE_OPACITY = 0.95
  constant BAR_AXIS (line 26) | const BAR_AXIS = new THREE.Vector3(0, 1, 0)
  type Vec3 (line 28) | type Vec3 = [number, number, number]
  type MeasurementGuide (line 30) | type MeasurementGuide = {
  function formatMeasurement (line 40) | function formatMeasurement(value: number, unit: 'metric' | 'imperial') {
  function WallMeasurementLabel (line 51) | function WallMeasurementLabel() {
  function getLevelWalls (line 80) | function getLevelWalls(
  function getWallMiddlePoints (line 96) | function getWallMiddlePoints(
  function worldPointToWallLocal (line 125) | function worldPointToWallLocal(wall: WallNode, point: Point2D): Vec3 {
  function buildMeasurementGuide (line 135) | function buildMeasurementGuide(
  function MeasurementBar (line 175) | function MeasurementBar({ start, end, color }: { start: Vec3; end: Vec3;...
  function WallMeasurementAnnotation (line 212) | function WallMeasurementAnnotation({ wall }: { wall: WallNode }) {

FILE: packages/editor/src/components/feedback-dialog.tsx
  constant MAX_IMAGES (line 15) | const MAX_IMAGES = 5
  constant MAX_IMAGE_SIZE (line 16) | const MAX_IMAGE_SIZE = 5 * 1024 * 1024
  type ImagePreview (line 18) | type ImagePreview = { file: File; url: string }
  function FeedbackDialog (line 20) | function FeedbackDialog({

FILE: packages/editor/src/components/pascal-radio.tsx
  constant PLAYLIST (line 11) | const PLAYLIST = [
  function shuffleArray (line 55) | function shuffleArray<T>(array: T[]): T[] {
  function PascalRadio (line 64) | function PascalRadio() {

FILE: packages/editor/src/components/preview-button.tsx
  function PreviewButton (line 6) | function PreviewButton() {

FILE: packages/editor/src/components/systems/zone/zone-label-editor-system.tsx
  function ZoneLabelEditor (line 13) | function ZoneLabelEditor({ zoneId }: { zoneId: ZoneNode['id'] }) {
  function ZoneLabelEditorSystem (line 174) | function ZoneLabelEditorSystem() {

FILE: packages/editor/src/components/tools/ceiling/ceiling-boundary-editor.tsx
  type CeilingBoundaryEditorProps (line 6) | interface CeilingBoundaryEditorProps {

FILE: packages/editor/src/components/tools/ceiling/ceiling-hole-editor.tsx
  type CeilingHoleEditorProps (line 6) | interface CeilingHoleEditorProps {

FILE: packages/editor/src/components/tools/ceiling/ceiling-tool.tsx
  constant CEILING_HEIGHT (line 11) | const CEILING_HEIGHT = 2.52
  constant GRID_OFFSET (line 12) | const GRID_OFFSET = 0.02

FILE: packages/editor/src/components/tools/door/door-math.ts
  function wallLocalToWorld (line 14) | function wallLocalToWorld(
  function clampToWall (line 36) | function clampToWall(
  function hasWallChildOverlap (line 55) | function hasWallChildOverlap(

FILE: packages/editor/src/components/tools/item/move-tool.tsx
  function getInitialState (line 12) | function getInitialState(node: {
  function MoveItemContent (line 26) | function MoveItemContent({ movingNode }: { movingNode: ItemNode }) {

FILE: packages/editor/src/components/tools/item/placement-math.ts
  function snapToGrid (line 8) | function snapToGrid(position: number, dimension: number): number {
  function snapToHalf (line 18) | function snapToHalf(value: number): number {
  function calculateCursorRotation (line 25) | function calculateCursorRotation(
  function calculateItemRotation (line 46) | function calculateItemRotation(normal: [number, number, number] | undefi...
  function getSideFromNormal (line 57) | function getSideFromNormal(normal: [number, number, number] | undefined)...
  function isValidWallSideFace (line 73) | function isValidWallSideFace(normal: [number, number, number] | undefine...
  function stripTransient (line 81) | function stripTransient(meta: any): any {

FILE: packages/editor/src/components/tools/item/placement-strategies.ts
  constant DEFAULT_DIMENSIONS (line 32) | const DEFAULT_DIMENSIONS: [number, number, number] = [1, 1, 1]
  method move (line 43) | move(ctx: PlacementContext, event: GridEvent): PlacementResult | null {
  method click (line 67) | click(
  method enter (line 108) | enter(
  method move (line 171) | move(
  method click (line 224) | click(
  method leave (line 262) | leave(ctx: PlacementContext): TransitionResult | null {
  method enter (line 288) | enter(
  method move (line 325) | move(ctx: PlacementContext, event: CeilingEvent): PlacementResult | null {
  method click (line 349) | click(
  method leave (line 387) | leave(ctx: PlacementContext): TransitionResult | null {
  method enter (line 413) | enter(ctx: PlacementContext, event: ItemEvent): TransitionResult | null {
  method move (line 455) | move(ctx: PlacementContext, event: ItemEvent): PlacementResult | null {
  method click (line 489) | click(ctx: PlacementContext, _event: ItemEvent): CommitResult | null {
  function checkCanPlace (line 513) | function checkCanPlace(ctx: PlacementContext, validators: SpatialValidat...

FILE: packages/editor/src/components/tools/item/placement-types.ts
  type SurfaceType (line 15) | type SurfaceType = 'floor' | 'wall' | 'ceiling' | 'item-surface'
  type PlacementState (line 21) | interface PlacementState {
  type PlacementContext (line 35) | interface PlacementContext {
  type PlacementResult (line 50) | interface PlacementResult {
  type TransitionResult (line 62) | interface TransitionResult {
  type CommitResult (line 74) | interface CommitResult {
  type SpatialValidators (line 87) | interface SpatialValidators {
  type LevelResolver (line 117) | type LevelResolver = (node: AnyNode, nodes: Record<string, AnyNode>) => ...

FILE: packages/editor/src/components/tools/item/use-draft-node.ts
  type OriginalState (line 13) | interface OriginalState {
  type DraftNodeHandle (line 21) | interface DraftNodeHandle {
  function useDraftNode (line 49) | function useDraftNode(): DraftNodeHandle {

FILE: packages/editor/src/components/tools/item/use-placement-coordinator.tsx
  constant DEFAULT_DIMENSIONS (line 45) | const DEFAULT_DIMENSIONS: [number, number, number] = [1, 1, 1]
  type PlacementCoordinatorConfig (line 69) | interface PlacementCoordinatorConfig {
  function usePlacementCoordinator (line 80) | function usePlacementCoordinator(config: PlacementCoordinatorConfig): Re...

FILE: packages/editor/src/components/tools/roof/roof-tool.tsx
  constant DEFAULT_WALL_HEIGHT (line 22) | const DEFAULT_WALL_HEIGHT = 0.5
  constant DEFAULT_ROOF_HEIGHT (line 23) | const DEFAULT_ROOF_HEIGHT = 2.5
  constant GRID_OFFSET (line 24) | const GRID_OFFSET = 0.02
  type PreviewState (line 121) | type PreviewState = {

FILE: packages/editor/src/components/tools/select/box-select-tool.tsx
  type Bounds (line 44) | type Bounds = { minX: number; maxX: number; minZ: number; maxZ: number }
  function pointInBounds (line 46) | function pointInBounds(x: number, z: number, b: Bounds): boolean {
  function segmentsIntersect (line 50) | function segmentsIntersect(
  function cross (line 77) | function cross(ax: number, az: number, bx: number, bz: number, cx: numbe...
  function onSeg (line 81) | function onSeg(ax: number, az: number, bx: number, bz: number, cx: numbe...
  function segmentIntersectsBounds (line 90) | function segmentIntersectsBounds(
  function polygonIntersectsBounds (line 111) | function polygonIntersectsBounds(polygon: [number, number][], b: Bounds)...
  function pointInPolygon (line 139) | function pointInPolygon(x: number, z: number, polygon: [number, number][...
  function getNodeWorldXZ (line 155) | function getNodeWorldXZ(nodeId: string): [number, number] | null {
  function collectNodeIdsInBounds (line 162) | function collectNodeIdsInBounds(bounds: Bounds): string[] {
  function updateRectVisuals (line 248) | function updateRectVisuals(
  function createOutlineSegments (line 296) | function createOutlineSegments(): LineSegments {
  constant DRAG_THRESHOLD_PX (line 321) | const DRAG_THRESHOLD_PX = 4
  constant BOX_SELECT_TOOLTIP (line 335) | const BOX_SELECT_TOOLTIP = (

FILE: packages/editor/src/components/tools/shared/cursor-sphere.tsx
  type CursorSphereProps (line 10) | interface CursorSphereProps extends Omit<ThreeElements['group'], 'ref'> {

FILE: packages/editor/src/components/tools/shared/polygon-editor.tsx
  constant Y_OFFSET (line 8) | const Y_OFFSET = 0.02
  type DragState (line 10) | type DragState = {
  type PolygonEditorProps (line 17) | interface PolygonEditorProps {
  constant MIN_HANDLE_HEIGHT (line 32) | const MIN_HANDLE_HEIGHT = 0.15

FILE: packages/editor/src/components/tools/slab/slab-boundary-editor.tsx
  type SlabBoundaryEditorProps (line 6) | interface SlabBoundaryEditorProps {

FILE: packages/editor/src/components/tools/slab/slab-hole-editor.tsx
  type SlabHoleEditorProps (line 6) | interface SlabHoleEditorProps {

FILE: packages/editor/src/components/tools/slab/slab-tool.tsx
  constant Y_OFFSET (line 10) | const Y_OFFSET = 0.02

FILE: packages/editor/src/components/tools/stair/stair-tool.tsx
  constant GRID_OFFSET (line 16) | const GRID_OFFSET = 0.02
  constant DEFAULT_WIDTH (line 19) | const DEFAULT_WIDTH = 1.0
  constant DEFAULT_LENGTH (line 20) | const DEFAULT_LENGTH = 3.0
  constant DEFAULT_HEIGHT (line 21) | const DEFAULT_HEIGHT = 2.5
  constant DEFAULT_STEP_COUNT (line 22) | const DEFAULT_STEP_COUNT = 10
  function createStairPreviewGeometry (line 28) | function createStairPreviewGeometry(): THREE.BufferGeometry {
  function commitStairPlacement (line 62) | function commitStairPlacement(

FILE: packages/editor/src/components/tools/wall/wall-drafting.ts
  type WallPlanPoint (line 4) | type WallPlanPoint = [number, number]
  constant WALL_GRID_STEP (line 5) | const WALL_GRID_STEP = 0.5
  constant WALL_JOIN_SNAP_RADIUS (line 6) | const WALL_JOIN_SNAP_RADIUS = 0.35
  constant WALL_MIN_LENGTH (line 7) | const WALL_MIN_LENGTH = 0.5
  function distanceSquared (line 8) | function distanceSquared(a: WallPlanPoint, b: WallPlanPoint): number {
  function snapScalarToGrid (line 13) | function snapScalarToGrid(value: number, step = WALL_GRID_STEP): number {
  function snapPointToGrid (line 16) | function snapPointToGrid(point: WallPlanPoint, step = WALL_GRID_STEP): W...
  function snapPointTo45Degrees (line 19) | function snapPointTo45Degrees(start: WallPlanPoint, cursor: WallPlanPoin...
  function projectPointOntoWall (line 30) | function projectPointOntoWall(point: WallPlanPoint, wall: WallNode): Wal...
  function findWallSnapTarget (line 45) | function findWallSnapTarget(
  function snapWallDraftPoint (line 80) | function snapWallDraftPoint(args: {
  function isWallLongEnough (line 95) | function isWallLongEnough(start: WallPlanPoint, end: WallPlanPoint): boo...
  function createWallOnCurrentLevel (line 98) | function createWallOnCurrentLevel(

FILE: packages/editor/src/components/tools/wall/wall-tool.tsx
  constant WALL_HEIGHT (line 16) | const WALL_HEIGHT = 2.5

FILE: packages/editor/src/components/tools/window/window-math.ts
  function wallLocalToWorld (line 18) | function wallLocalToWorld(
  function clampToWall (line 39) | function clampToWall(
  function hasWallChildOverlap (line 62) | function hasWallChildOverlap(

FILE: packages/editor/src/components/tools/zone/zone-boundary-editor.tsx
  type ZoneBoundaryEditorProps (line 5) | interface ZoneBoundaryEditorProps {

FILE: packages/editor/src/components/tools/zone/zone-tool.tsx
  constant Y_OFFSET (line 10) | const Y_OFFSET = 0.02
  type PreviewState (line 73) | type PreviewState = {

FILE: packages/editor/src/components/ui/action-menu/action-button.tsx
  type ActionButtonProps (line 10) | interface ActionButtonProps extends React.ComponentProps<typeof Button> {

FILE: packages/editor/src/components/ui/action-menu/camera-actions.tsx
  function CameraActions (line 9) | function CameraActions() {

FILE: packages/editor/src/components/ui/action-menu/control-modes.tsx
  type ControlId (line 12) | type ControlId = 'select' | 'box-select' | 'site-edit' | 'build' | 'delete'
  type ControlConfig (line 14) | type ControlConfig = {
  function ControlModes (line 67) | function ControlModes() {

FILE: packages/editor/src/components/ui/action-menu/furnish-tools.tsx
  type FurnishToolConfig (line 8) | type FurnishToolConfig = {
  function FurnishTools (line 49) | function FurnishTools() {

FILE: packages/editor/src/components/ui/action-menu/index.tsx
  function ActionMenu (line 15) | function ActionMenu({ className }: { className?: string }) {

FILE: packages/editor/src/components/ui/action-menu/structure-tools.tsx
  type ToolConfig (line 14) | type ToolConfig = {
  function StructureTools (line 34) | function StructureTools() {

FILE: packages/editor/src/components/ui/action-menu/view-toggles.tsx
  function useLevelGuides (line 21) | function useLevelGuides(): GuideNode[] {
  function useLevelScans (line 37) | function useLevelScans(): ScanNode[] {
  function GuidesControl (line 53) | function GuidesControl() {
  function ScansControl (line 172) | function ScansControl() {
  function ViewToggles (line 287) | function ViewToggles() {

FILE: packages/editor/src/components/ui/command-palette/editor-commands.tsx
  function EditorCommands (line 43) | function EditorCommands() {

FILE: packages/editor/src/components/ui/command-palette/index.tsx
  type CommandPaletteStore (line 18) | interface CommandPaletteStore {
  function resolve (line 57) | function resolve(value: string | (() => string)): string {
  function Shortcut (line 61) | function Shortcut({ keys }: { keys: string[] }) {
  function Item (line 76) | function Item({
  function OptionItem (line 123) | function OptionItem({
  constant PAGE_LABEL (line 154) | const PAGE_LABEL: Record<string, string> = {
  function CommandPalette (line 166) | function CommandPalette() {

FILE: packages/editor/src/components/ui/controls/action-button.tsx
  type ActionButtonProps (line 5) | interface ActionButtonProps extends React.ButtonHTMLAttributes<HTMLButto...
  function ActionButton (line 10) | function ActionButton({ icon, label, className, ...props }: ActionButton...
  function ActionGroup (line 25) | function ActionGroup({

FILE: packages/editor/src/components/ui/controls/material-picker.tsx
  constant PRESET_COLORS (line 6) | const PRESET_COLORS: Record<MaterialPreset, string> = {
  constant PRESET_LABELS (line 19) | const PRESET_LABELS: Record<MaterialPreset, string> = {
  type MaterialPickerProps (line 32) | type MaterialPickerProps = {
  function MaterialPicker (line 37) | function MaterialPicker({ value, onChange }: MaterialPickerProps) {

FILE: packages/editor/src/components/ui/controls/metric-control.tsx
  type MetricControlProps (line 8) | interface MetricControlProps {
  function MetricControl (line 20) | function MetricControl({

FILE: packages/editor/src/components/ui/controls/panel-section.tsx
  type PanelSectionProps (line 8) | interface PanelSectionProps {
  function PanelSection (line 15) | function PanelSection({

FILE: packages/editor/src/components/ui/controls/segmented-control.tsx
  type SegmentedControlProps (line 5) | interface SegmentedControlProps<T extends string> {
  function SegmentedControl (line 12) | function SegmentedControl<T extends string>({

FILE: packages/editor/src/components/ui/controls/slider-control.tsx
  type SliderControlProps (line 7) | interface SliderControlProps {
  function SliderControl (line 19) | function SliderControl({

FILE: packages/editor/src/components/ui/controls/toggle-control.tsx
  type ToggleControlProps (line 6) | interface ToggleControlProps {
  function ToggleControl (line 13) | function ToggleControl({ label, checked, onChange, className }: ToggleCo...

FILE: packages/editor/src/components/ui/floating-level-selector.tsx
  function getLevelDisplayLabel (line 8) | function getLevelDisplayLabel(level: LevelNode) {
  function FloatingLevelSelector (line 12) | function FloatingLevelSelector() {

FILE: packages/editor/src/components/ui/helpers/ceiling-helper.tsx
  function CeilingHelper (line 3) | function CeilingHelper() {

FILE: packages/editor/src/components/ui/helpers/helper-manager.tsx
  function HelperManager (line 10) | function HelperManager() {

FILE: packages/editor/src/components/ui/helpers/item-helper.tsx
  type ItemHelperProps (line 3) | interface ItemHelperProps {
  function ItemHelper (line 7) | function ItemHelper({ showEsc }: ItemHelperProps) {

FILE: packages/editor/src/components/ui/helpers/roof-helper.tsx
  function RoofHelper (line 3) | function RoofHelper() {

FILE: packages/editor/src/components/ui/helpers/slab-helper.tsx
  function SlabHelper (line 3) | function SlabHelper() {

FILE: packages/editor/src/components/ui/helpers/wall-helper.tsx
  function WallHelper (line 3) | function WallHelper() {

FILE: packages/editor/src/components/ui/item-catalog/catalog-items.tsx
  constant CATALOG_ITEMS (line 2) | const CATALOG_ITEMS: AssetInput[] = [

FILE: packages/editor/src/components/ui/item-catalog/item-catalog.tsx
  constant PLACEMENT_TAGS (line 16) | const PLACEMENT_TAGS = new Set(['floor', 'wall', 'ceiling', 'countertop'])
  function ItemCatalog (line 18) | function ItemCatalog({ category }: { category: CatalogCategory }) {

FILE: packages/editor/src/components/ui/panels/ceiling-panel.tsx
  function CeilingPanel (line 14) | function CeilingPanel() {

FILE: packages/editor/src/components/ui/panels/collections/collections-popover.tsx
  type CollectionsPopoverProps (line 31) | interface CollectionsPopoverProps {
  function CollectionsPopover (line 37) | function CollectionsPopover({ nodeId, collectionIds, children }: Collect...

FILE: packages/editor/src/components/ui/panels/door-panel.tsx
  function DoorPanel (line 20) | function DoorPanel() {

FILE: packages/editor/src/components/ui/panels/item-panel.tsx
  function ItemPanel (line 16) | function ItemPanel() {

FILE: packages/editor/src/components/ui/panels/panel-manager.tsx
  function PanelManager (line 18) | function PanelManager() {

FILE: packages/editor/src/components/ui/panels/panel-wrapper.tsx
  type PanelWrapperProps (line 7) | interface PanelWrapperProps {
  function PanelWrapper (line 18) | function PanelWrapper({

FILE: packages/editor/src/components/ui/panels/presets/presets-popover.tsx
  type PresetType (line 28) | type PresetType = 'door' | 'window'
  type PresetData (line 30) | interface PresetData {
  type PresetsPopoverProps (line 41) | interface PresetsPopoverProps {
  function PresetsPopover (line 55) | function PresetsPopover({
  function TabButton (line 288) | function TabButton({
  function EmptyState (line 317) | function EmptyState({ tab, isAuthenticated }: { tab: PresetsTab; isAuthe...
  type PresetRowProps (line 332) | interface PresetRowProps {
  function PresetRow (line 352) | function PresetRow({

FILE: packages/editor/src/components/ui/panels/reference-panel.tsx
  type ReferenceNode (line 13) | type ReferenceNode = ScanNode | GuideNode
  function ReferencePanel (line 15) | function ReferencePanel() {

FILE: packages/editor/src/components/ui/panels/roof-panel.tsx
  function RoofPanel (line 25) | function RoofPanel() {

FILE: packages/editor/src/components/ui/panels/roof-segment-panel.tsx
  constant ROOF_TYPE_OPTIONS (line 25) | const ROOF_TYPE_OPTIONS: { label: string; value: RoofType }[] = [
  constant ROOF_TYPE_OPTIONS_2 (line 32) | const ROOF_TYPE_OPTIONS_2: { label: string; value: RoofType }[] = [
  function RoofSegmentPanel (line 38) | function RoofSegmentPanel() {

FILE: packages/editor/src/components/ui/panels/slab-panel.tsx
  function SlabPanel (line 14) | function SlabPanel() {

FILE: packages/editor/src/components/ui/panels/stair-panel.tsx
  function StairPanel (line 25) | function StairPanel() {

FILE: packages/editor/src/components/ui/panels/stair-segment-panel.tsx
  constant SEGMENT_TYPE_OPTIONS (line 26) | const SEGMENT_TYPE_OPTIONS: { label: string; value: StairSegmentType }[]...
  constant ATTACHMENT_SIDE_OPTIONS (line 31) | const ATTACHMENT_SIDE_OPTIONS: { label: string; value: AttachmentSide }[...
  function StairSegmentPanel (line 37) | function StairSegmentPanel() {

FILE: packages/editor/src/components/ui/panels/wall-panel.tsx
  function WallPanel (line 11) | function WallPanel() {

FILE: packages/editor/src/components/ui/panels/window-panel.tsx
  function WindowPanel (line 19) | function WindowPanel() {

FILE: packages/editor/src/components/ui/primitives/button.tsx
  function Button (line 37) | function Button({

FILE: packages/editor/src/components/ui/primitives/card.tsx
  function Card (line 5) | function Card({ className, ...props }: React.ComponentProps<'div'>) {
  function CardHeader (line 18) | function CardHeader({ className, ...props }: React.ComponentProps<'div'>) {
  function CardTitle (line 31) | function CardTitle({ className, ...props }: React.ComponentProps<'div'>) {
  function CardDescription (line 41) | function CardDescription({ className, ...props }: React.ComponentProps<'...
  function CardAction (line 51) | function CardAction({ className, ...props }: React.ComponentProps<'div'>) {
  function CardContent (line 61) | function CardContent({ className, ...props }: React.ComponentProps<'div'...
  function CardFooter (line 65) | function CardFooter({ className, ...props }: React.ComponentProps<'div'>) {

FILE: packages/editor/src/components/ui/primitives/color-dot.tsx
  constant PALETTE_COLORS (line 7) | const PALETTE_COLORS = [
  type ColorDotProps (line 22) | interface ColorDotProps {
  function ColorDot (line 27) | function ColorDot({ color, onChange }: ColorDotProps) {

FILE: packages/editor/src/components/ui/primitives/context-menu.tsx
  function ContextMenu (line 9) | function ContextMenu({ ...props }: React.ComponentProps<typeof ContextMe...
  function ContextMenuTrigger (line 13) | function ContextMenuTrigger({
  function ContextMenuGroup (line 19) | function ContextMenuGroup({ ...props }: React.ComponentProps<typeof Cont...
  function ContextMenuPortal (line 23) | function ContextMenuPortal({ ...props }: React.ComponentProps<typeof Con...
  function ContextMenuSub (line 27) | function ContextMenuSub({ ...props }: React.ComponentProps<typeof Contex...
  function ContextMenuRadioGroup (line 31) | function ContextMenuRadioGroup({
  function ContextMenuSubTrigger (line 37) | function ContextMenuSubTrigger({
  function ContextMenuSubContent (line 61) | function ContextMenuSubContent({
  function ContextMenuContent (line 77) | function ContextMenuContent({
  function ContextMenuItem (line 95) | function ContextMenuItem({
  function ContextMenuCheckboxItem (line 118) | function ContextMenuCheckboxItem({
  function ContextMenuRadioItem (line 144) | function ContextMenuRadioItem({
  function ContextMenuLabel (line 168) | function ContextMenuLabel({
  function ContextMenuSeparator (line 188) | function ContextMenuSeparator({
  function ContextMenuShortcut (line 201) | function ContextMenuShortcut({ className, ...props }: React.ComponentPro...

FILE: packages/editor/src/components/ui/primitives/dialog.tsx
  function Dialog (line 9) | function Dialog({ ...props }: React.ComponentProps<typeof DialogPrimitiv...
  function DialogTrigger (line 13) | function DialogTrigger({ ...props }: React.ComponentProps<typeof DialogP...
  function DialogPortal (line 17) | function DialogPortal({ ...props }: React.ComponentProps<typeof DialogPr...
  function DialogClose (line 21) | function DialogClose({ ...props }: React.ComponentProps<typeof DialogPri...
  function DialogOverlay (line 25) | function DialogOverlay({
  function DialogContent (line 41) | function DialogContent({
  function DialogHeader (line 75) | function DialogHeader({ className, ...props }: React.ComponentProps<'div...
  function DialogFooter (line 85) | function DialogFooter({ className, ...props }: React.ComponentProps<'div...
  function DialogTitle (line 95) | function DialogTitle({ className, ...props }: React.ComponentProps<typeo...
  function DialogDescription (line 105) | function DialogDescription({

FILE: packages/editor/src/components/ui/primitives/dropdown-menu.tsx
  function DropdownMenu (line 9) | function DropdownMenu({ ...props }: React.ComponentProps<typeof Dropdown...
  function DropdownMenuPortal (line 13) | function DropdownMenuPortal({
  function DropdownMenuTrigger (line 19) | function DropdownMenuTrigger({
  function DropdownMenuContent (line 25) | function DropdownMenuContent({
  function DropdownMenuGroup (line 45) | function DropdownMenuGroup({ ...props }: React.ComponentProps<typeof Dro...
  function DropdownMenuItem (line 49) | function DropdownMenuItem({
  function DropdownMenuCheckboxItem (line 72) | function DropdownMenuCheckboxItem({
  function DropdownMenuRadioGroup (line 98) | function DropdownMenuRadioGroup({
  function DropdownMenuRadioItem (line 104) | function DropdownMenuRadioItem({
  function DropdownMenuLabel (line 128) | function DropdownMenuLabel({
  function DropdownMenuSeparator (line 145) | function DropdownMenuSeparator({
  function DropdownMenuShortcut (line 158) | function DropdownMenuShortcut({ className, ...props }: React.ComponentPr...
  function DropdownMenuSub (line 168) | function DropdownMenuSub({ ...props }: React.ComponentProps<typeof Dropd...
  function DropdownMenuSubTrigger (line 172) | function DropdownMenuSubTrigger({
  function DropdownMenuSubContent (line 196) | function DropdownMenuSubContent({

FILE: packages/editor/src/components/ui/primitives/error-boundary.tsx
  type Props (line 5) | interface Props {
  type State (line 10) | interface State {
  class ErrorBoundary (line 15) | class ErrorBoundary extends Component<Props, State> {
    method getDerivedStateFromError (line 21) | public static getDerivedStateFromError(error: Error): State {
    method componentDidCatch (line 25) | public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
    method render (line 29) | public render() {

FILE: packages/editor/src/components/ui/primitives/input.tsx
  function Input (line 5) | function Input({ className, type, ...props }: React.ComponentProps<'inpu...

FILE: packages/editor/src/components/ui/primitives/number-input.tsx
  type NumberInputProps (line 7) | interface NumberInputProps {
  function NumberInput (line 18) | function NumberInput({

FILE: packages/editor/src/components/ui/primitives/opacity-control.tsx
  type OpacityControlProps (line 10) | interface OpacityControlProps {
  function OpacityControl (line 18) | function OpacityControl({

FILE: packages/editor/src/components/ui/primitives/popover.tsx
  function Popover (line 8) | function Popover({ ...props }: React.ComponentProps<typeof PopoverPrimit...
  function PopoverTrigger (line 12) | function PopoverTrigger({ ...props }: React.ComponentProps<typeof Popove...
  function PopoverContent (line 16) | function PopoverContent({
  function PopoverAnchor (line 38) | function PopoverAnchor({ ...props }: React.ComponentProps<typeof Popover...

FILE: packages/editor/src/components/ui/primitives/separator.tsx
  function Separator (line 8) | function Separator({

FILE: packages/editor/src/components/ui/primitives/sheet.tsx
  function Sheet (line 9) | function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive....
  function SheetTrigger (line 13) | function SheetTrigger({ ...props }: React.ComponentProps<typeof SheetPri...
  function SheetClose (line 17) | function SheetClose({ ...props }: React.ComponentProps<typeof SheetPrimi...
  function SheetPortal (line 21) | function SheetPortal({ ...props }: React.ComponentProps<typeof SheetPrim...
  function SheetOverlay (line 25) | function SheetOverlay({
  function SheetContent (line 41) | function SheetContent({
  function SheetHeader (line 78) | function SheetHeader({ className, ...props }: React.ComponentProps<'div'...
  function SheetFooter (line 88) | function SheetFooter({ className, ...props }: React.ComponentProps<'div'...
  function SheetTitle (line 98) | function SheetTitle({ className, ...props }: React.ComponentProps<typeof...
  function SheetDescription (line 108) | function SheetDescription({

FILE: packages/editor/src/components/ui/primitives/shortcut-token.tsx
  constant MOUSE_SHORTCUTS (line 6) | const MOUSE_SHORTCUTS = {
  type ShortcutTokenProps (line 25) | type ShortcutTokenProps = React.ComponentProps<'kbd'> & {
  function ShortcutToken (line 30) | function ShortcutToken({ className, displayValue, value, ...props }: Sho...

FILE: packages/editor/src/components/ui/primitives/sidebar.tsx
  constant SIDEBAR_COOKIE_NAME (line 29) | const SIDEBAR_COOKIE_NAME = 'sidebar_state'
  constant SIDEBAR_COOKIE_MAX_AGE (line 30) | const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7
  constant SIDEBAR_WIDTH (line 31) | const SIDEBAR_WIDTH = '18rem'
  constant SIDEBAR_WIDTH_MOBILE (line 32) | const SIDEBAR_WIDTH_MOBILE = '18rem'
  constant SIDEBAR_WIDTH_ICON (line 33) | const SIDEBAR_WIDTH_ICON = '3rem'
  constant SIDEBAR_KEYBOARD_SHORTCUT (line 34) | const SIDEBAR_KEYBOARD_SHORTCUT = 'b'
  constant SIDEBAR_COLLAPSE_THRESHOLD (line 36) | const SIDEBAR_COLLAPSE_THRESHOLD = 220
  constant SIDEBAR_MAX_WIDTH (line 37) | const SIDEBAR_MAX_WIDTH = 800
  type SidebarStore (line 39) | type SidebarStore = {
  type SidebarContextProps (line 71) | type SidebarContextProps = {
  function useSidebar (line 83) | function useSidebar() {
  function SidebarProvider (line 92) | function SidebarProvider({
  function SidebarResizer (line 191) | function SidebarResizer({ side }: { side: 'left' | 'right' }) {
  function Sidebar (line 237) | function Sidebar({
  function SidebarTrigger (line 342) | function SidebarTrigger({ className, onClick, ...props }: React.Componen...
  function SidebarRail (line 364) | function SidebarRail({ className, ...props }: React.ComponentProps<'butt...
  function SidebarInset (line 389) | function SidebarInset({ className, ...props }: React.ComponentProps<'mai...
  function SidebarInput (line 403) | function SidebarInput({ className, ...props }: React.ComponentProps<type...
  function SidebarHeader (line 414) | function SidebarHeader({ className, ...props }: React.ComponentProps<'di...
  function SidebarFooter (line 425) | function SidebarFooter({ className, ...props }: React.ComponentProps<'di...
  function SidebarSeparator (line 436) | function SidebarSeparator({ className, ...props }: React.ComponentProps<...
  function SidebarContent (line 447) | function SidebarContent({ className, ...props }: React.ComponentProps<'d...
  function SidebarGroup (line 461) | function SidebarGroup({ className, ...props }: React.ComponentProps<'div...
  function SidebarGroupLabel (line 472) | function SidebarGroupLabel({
  function SidebarGroupAction (line 509) | function SidebarGroupAction({
  function SidebarGroupContent (line 545) | function SidebarGroupContent({ className, ...props }: React.ComponentPro...
  function SidebarMenu (line 556) | function SidebarMenu({ className, ...props }: React.ComponentProps<'ul'>) {
  function SidebarMenuItem (line 567) | function SidebarMenuItem({ className, ...props }: React.ComponentProps<'...
  function SidebarMenuButton (line 600) | function SidebarMenuButton({
  function SidebarMenuAction (line 662) | function SidebarMenuAction({
  function SidebarMenuBadge (line 707) | function SidebarMenuBadge({ className, ...props }: React.ComponentProps<...
  function SidebarMenuSkeleton (line 726) | function SidebarMenuSkeleton({
  function SidebarMenuSub (line 757) | function SidebarMenuSub({ className, ...props }: React.ComponentProps<'u...
  function SidebarMenuSubItem (line 772) | function SidebarMenuSubItem({ className, ...props }: React.ComponentProp...
  function SidebarMenuSubButton (line 783) | function SidebarMenuSubButton({

FILE: packages/editor/src/components/ui/primitives/skeleton.tsx
  function Skeleton (line 3) | function Skeleton({ className, ...props }: React.ComponentProps<'div'>) {

FILE: packages/editor/src/components/ui/primitives/slider.tsx
  function Slider (line 8) | function Slider({

FILE: packages/editor/src/components/ui/primitives/tooltip.tsx
  function TooltipProvider (line 8) | function TooltipProvider({
  function Tooltip (line 21) | function Tooltip({ ...props }: React.ComponentProps<typeof TooltipPrimit...
  function TooltipTrigger (line 29) | function TooltipTrigger({ ...props }: React.ComponentProps<typeof Toolti...
  function TooltipContent (line 33) | function TooltipContent({

FILE: packages/editor/src/components/ui/scene-loader.tsx
  constant LOADERS (line 6) | const LOADERS = [
  type SceneLoaderProps (line 14) | interface SceneLoaderProps {
  function SceneLoader (line 19) | function SceneLoader({ className, fullScreen = false }: SceneLoaderProps) {

FILE: packages/editor/src/components/ui/sidebar/app-sidebar.tsx
  type AppSidebarProps (line 17) | interface AppSidebarProps {
  function AppSidebar (line 24) | function AppSidebar({

FILE: packages/editor/src/components/ui/sidebar/icon-rail.tsx
  type PanelId (line 14) | type PanelId = 'site' | 'settings'
  type IconRailProps (line 16) | interface IconRailProps {
  function IconRail (line 28) | function IconRail({ activePanel, onPanelChange, appMenuButton, className...

FILE: packages/editor/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx
  function AudioSettingsDialog (line 14) | function AudioSettingsDialog() {

FILE: packages/editor/src/components/ui/sidebar/panels/settings-panel/index.tsx
  type SceneNode (line 25) | type SceneNode = Record<string, unknown> & {
  type SceneGraphNode (line 33) | type SceneGraphNode = {
  type SceneGraphValue (line 43) | type SceneGraphValue = {
  type ProjectVisibility (line 158) | interface ProjectVisibility {
  type SettingsPanelProps (line 164) | interface SettingsPanelProps {
  function SettingsPanel (line 173) | function SettingsPanel({

FILE: packages/editor/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx
  type Shortcut (line 14) | type Shortcut = {
  type ShortcutCategory (line 20) | type ShortcutCategory = {
  constant KEY_DISPLAY_MAP (line 25) | const KEY_DISPLAY_MAP: Record<string, string> = {
  constant SHORTCUT_CATEGORIES (line 33) | const SHORTCUT_CATEGORIES: ShortcutCategory[] = [
  function getDisplayKey (line 117) | function getDisplayKey(key: string, isMac: boolean): string {
  function ShortcutKeys (line 123) | function ShortcutKeys({ keys }: { keys: string[] }) {
  function KeyboardShortcutsDialog (line 142) | function KeyboardShortcutsDialog() {

FILE: packages/editor/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx
  type BuildingTreeNodeProps (line 13) | interface BuildingTreeNodeProps {
  function BuildingTreeNode (line 19) | function BuildingTreeNode({ node, depth, isLast }: BuildingTreeNodeProps) {

FILE: packages/editor/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx
  type CeilingTreeNodeProps (line 10) | interface CeilingTreeNodeProps {
  function CeilingTreeNode (line 16) | function CeilingTreeNode({ node, depth, isLast }: CeilingTreeNodeProps) {
  function calculatePolygonArea (line 113) | function calculatePolygonArea(polygon: Array<[number, number]>): number {

FILE: packages/editor/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx
  type DoorTreeNodeProps (line 12) | interface DoorTreeNodeProps {
  function DoorTreeNode (line 18) | function DoorTreeNode({ node, depth, isLast }: DoorTreeNodeProps) {

FILE: packages/editor/src/components/ui/sidebar/panels/site-panel/index.tsx
  function calculatePerimeter (line 45) | function calculatePerimeter(points: Array<[number, number]>): number {
  function calculatePolygonArea (line 56) | function calculatePolygonArea(polygon: Array<[number, number]>): number {
  function useSiteNode (line 70) | function useSiteNode(): SiteNode | null {
  function PropertyLineSection (line 82) | function PropertyLineSection() {
  function CameraPopover (line 226) | function CameraPopover({
  function ReferenceItem (line 307) | function ReferenceItem({
  constant MAX_FILE_SIZE (line 379) | const MAX_FILE_SIZE = 200 * 1024 * 1024 // 200MB
  type LevelReferencesProps (line 381) | interface LevelReferencesProps {
  function LevelReferences (line 389) | function LevelReferences({
  function LevelItem (line 553) | function LevelItem({
  function LevelsSection (line 783) | function LevelsSection({
  function LayerToggle (line 862) | function LayerToggle() {
  function ZoneItem (line 992) | function ZoneItem({ zone, isLast }: { zone: ZoneNode; isLast?: boolean }) {
  function MultiSelectionBadge (line 1155) | function MultiSelectionBadge() {
  function ContentSection (line 1177) | function ContentSection() {
  function BuildingItem (line 1257) | function BuildingItem({
  type SitePanelProps (line 1427) | interface SitePanelProps {
  function SitePanel (line 1433) | function SitePanel({ projectId, onUploadAsset, onDeleteAsset }: SitePane...

FILE: packages/editor/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx
  type InlineRenameInputProps (line 6) | interface InlineRenameInputProps {
  function InlineRenameInput (line 15) | function InlineRenameInput({

FILE: packages/editor/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx
  constant CATEGORY_ICONS (line 10) | const CATEGORY_ICONS: Record<string, string> = {
  type ItemTreeNodeProps (line 20) | interface ItemTreeNodeProps {
  function ItemTreeNode (line 26) | function ItemTreeNode({ node, depth, isLast }: ItemTreeNodeProps) {

FILE: packages/editor/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx
  type LevelTreeNodeProps (line 9) | interface LevelTreeNodeProps {
  function LevelTreeNode (line 15) | function LevelTreeNode({ node, depth, isLast }: LevelTreeNodeProps) {

FILE: packages/editor/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx
  type RoofTreeNodeProps (line 12) | interface RoofTreeNodeProps {
  function RoofTreeNode (line 18) | function RoofTreeNode({ node, depth, isLast }: RoofTreeNodeProps) {
  function RoofSegmentTreeNode (line 140) | function RoofSegmentTreeNode({

FILE: packages/editor/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx
  type SlabTreeNodeProps (line 10) | interface SlabTreeNodeProps {
  function SlabTreeNode (line 16) | function SlabTreeNode({ node, depth, isLast }: SlabTreeNodeProps) {
  function calculatePolygonArea (line 83) | function calculatePolygonArea(polygon: Array<[number, number]>): number {

FILE: packages/editor/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx
  type StairTreeNodeProps (line 12) | interface StairTreeNodeProps {
  function StairTreeNode (line 18) | function StairTreeNode({ node, depth, isLast }: StairTreeNodeProps) {
  function StairSegmentTreeNode (line 140) | function StairSegmentTreeNode({

FILE: packages/editor/src/components/ui/sidebar/panels/site-panel/tree-node-actions.tsx
  type TreeNodeActionsProps (line 11) | interface TreeNodeActionsProps {
  function TreeNodeActions (line 15) | function TreeNodeActions({ node }: TreeNodeActionsProps) {

FILE: packages/editor/src/components/ui/sidebar/panels/site-panel/tree-node-drag.tsx
  constant REPARENT_TARGETS (line 21) | const REPARENT_TARGETS: Record<string, string[]> = {
  constant REMOVE_WHEN_EMPTY (line 26) | const REMOVE_WHEN_EMPTY = new Set(['roof'])
  function canDrag (line 28) | function canDrag(node: AnyNode): boolean {
  function canDrop (line 32) | function canDrop(draggedType: string, targetType: string): boolean {
  type Transform (line 40) | type Transform = {
  function getTransform (line 45) | function getTransform(node: AnyNode): Transform {
  function computeReparentTransform (line 58) | function computeReparentTransform(
  type DragState (line 87) | type DragState = {
  type DropTarget (line 96) | type DropTarget = {
  type TreeNodeDragContextValue (line 101) | type TreeNodeDragContextValue = {
  constant DRAG_THRESHOLD (line 128) | const DRAG_THRESHOLD = 4
  function TreeNodeDragProvider (line 130) | function TreeNodeDragProvider({ children }: { children: ReactNode }) {
  function FloatingPreview (line 312) | function FloatingPreview({ drag }: { drag: NonNullable<DragState> }) {
  function DropIndicatorLine (line 332) | function DropIndicatorLine() {

FILE: packages/editor/src/components/ui/sidebar/panels/site-panel/tree-node.tsx
  function handleTreeSelection (line 6) | function handleTreeSelection(
  function focusTreeNode (line 52) | function focusTreeNode(nodeId: AnyNodeId) {
  type TreeNodeProps (line 69) | interface TreeNodeProps {
  function TreeNode (line 75) | function TreeNode({ nodeId, depth = 0, isLast }: TreeNodeProps) {
  type TreeNodeWrapperProps (line 108) | interface TreeNodeWrapperProps {

FILE: packages/editor/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx
  type WallTreeNodeProps (line 10) | interface WallTreeNodeProps {
  function WallTreeNode (line 16) | function WallTreeNode({ node, depth, isLast }: WallTreeNodeProps) {

FILE: packages/editor/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx
  type WindowTreeNodeProps (line 12) | interface WindowTreeNodeProps {
  function WindowTreeNode (line 18) | function WindowTreeNode({ node, depth, isLast }: WindowTreeNodeProps) {

FILE: packages/editor/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx
  type ZoneTreeNodeProps (line 9) | interface ZoneTreeNodeProps {
  function ZoneTreeNode (line 15) | function ZoneTreeNode({ node, depth, isLast }: ZoneTreeNodeProps) {
  function calculatePolygonArea (line 74) | function calculatePolygonArea(polygon: Array<[number, number]>): number {

FILE: packages/editor/src/components/ui/sidebar/panels/zone-panel/index.tsx
  function ZoneItem (line 14) | function ZoneItem({ zone }: { zone: ZoneNode }) {
  function ZonePanel (line 125) | function ZonePanel() {

FILE: packages/editor/src/components/ui/sidebar/tab-bar.tsx
  type SidebarTab (line 5) | type SidebarTab = {
  type TabBarProps (line 10) | interface TabBarProps {
  function TabBar (line 16) | function TabBar({ tabs, activeTab, onTabChange }: TabBarProps) {

FILE: packages/editor/src/components/ui/slider-demo.tsx
  function SliderDemo (line 6) | function SliderDemo() {

FILE: packages/editor/src/components/ui/slider.tsx
  type SliderProps (line 54) | type SliderProps = React.ComponentProps<typeof SliderPrimitive.Root> &
  function Slider (line 57) | function Slider({ variant, className, ...props }: SliderProps) {

FILE: packages/editor/src/components/ui/viewer-toolbar.tsx
  constant TOOLBAR_CONTAINER (line 16) | const TOOLBAR_CONTAINER =
  constant TOOLBAR_BTN (line 20) | const TOOLBAR_BTN =
  constant VIEW_MODES (line 25) | const VIEW_MODES: { id: ViewMode; label: string; icon: React.ReactNode }...
  function ViewModeControl (line 43) | function ViewModeControl() {
  function CollapseSidebarButton (line 74) | function CollapseSidebarButton() {
  function WalkthroughButton (line 98) | function WalkthroughButton() {
  function UnitToggle (line 125) | function UnitToggle() {
  function ThemeToggle (line 147) | function ThemeToggle() {
  function LevelModeToggle (line 177) | function LevelModeToggle() {
  function WallModeToggle (line 231) | function WallModeToggle() {
  function CameraModeToggle (line 268) | function CameraModeToggle() {
  function PreviewButton (line 299) | function PreviewButton() {
  function ViewerToolbarLeft (line 319) | function ViewerToolbarLeft() {
  function ViewerToolbarRight (line 328) | function ViewerToolbarRight() {

FILE: packages/editor/src/components/viewer-overlay.tsx
  type ProjectOwner (line 22) | type ProjectOwner = {
  type ViewerOverlayProps (line 74) | interface ViewerOverlayProps {

FILE: packages/editor/src/contexts/presets-context.tsx
  type PresetsTab (line 8) | type PresetsTab = 'community' | 'mine'
  type PresetsAdapter (line 10) | interface PresetsAdapter {
  function PresetsProvider (line 105) | function PresetsProvider({
  function usePresetsAdapter (line 119) | function usePresetsAdapter(): PresetsAdapter {

FILE: packages/editor/src/hooks/use-auto-save.ts
  constant AUTOSAVE_DEBOUNCE_MS (line 7) | const AUTOSAVE_DEBOUNCE_MS = 1000
  type SaveStatus (line 9) | type SaveStatus = 'idle' | 'pending' | 'saving' | 'saved' | 'paused' | '...
  type UseAutoSaveOptions (line 11) | interface UseAutoSaveOptions {
  function useAutoSave (line 24) | function useAutoSave({

FILE: packages/editor/src/hooks/use-contextual-tools.ts
  function useContextualTools (line 6) | function useContextualTools() {

FILE: packages/editor/src/hooks/use-grid-events.ts
  function useGridEvents (line 11) | function useGridEvents(gridY: number) {

FILE: packages/editor/src/hooks/use-mobile.ts
  constant MOBILE_BREAKPOINT (line 3) | const MOBILE_BREAKPOINT = 768
  function useIsMobile (line 5) | function useIsMobile() {

FILE: packages/editor/src/hooks/use-reduced-motion.ts
  function useReducedMotion (line 7) | function useReducedMotion(): boolean {

FILE: packages/editor/src/lib/constants.ts
  constant EDITOR_LAYER (line 3) | const EDITOR_LAYER = 1

FILE: packages/editor/src/lib/level-selection.ts
  function getAdjacentLevelIdForDeletion (line 5) | function getAdjacentLevelIdForDeletion(levelId: AnyNodeId): LevelNode['i...
  function deleteLevelWithFallbackSelection (line 22) | function deleteLevelWithFallbackSelection(levelId: AnyNodeId) {

FILE: packages/editor/src/lib/scene.ts
  type SceneGraph (line 11) | type SceneGraph = {
  type PersistedSelectionPath (line 16) | type PersistedSelectionPath = {
  function toViewerSelection (line 27) | function toViewerSelection(s: PersistedSelectionPath) {
  constant EMPTY_PERSISTED_SELECTION (line 31) | const EMPTY_PERSISTED_SELECTION: PersistedSelectionPath = {
  constant SELECTION_STORAGE_KEY (line 38) | const SELECTION_STORAGE_KEY = 'pascal-editor-selection'
  function getSelectionStorageKey (line 40) | function getSelectionStorageKey(): string {
  function getSelectionStorageReadKeys (line 45) | function getSelectionStorageReadKeys(): string[] {
  function getDefaultLevelIdForBuilding (line 50) | function getDefaultLevelIdForBuilding(
  function normalizePersistedSelectionPath (line 81) | function normalizePersistedSelectionPath(
  function hasPersistedSelectionValue (line 94) | function hasPersistedSelectionValue(selection: PersistedSelectionPath): ...
  function readPersistedSelection (line 103) | function readPersistedSelection(): PersistedSelectionPath | null {
  function writePersistedSelection (line 126) | function writePersistedSelection(selection: {
  function getEditorUiStateForRestoredSelection (line 148) | function getEditorUiStateForRestoredSelection(
  function getValidatedSelectionForScene (line 198) | function getValidatedSelectionForScene(
  function getRestoredSelectionForScene (line 248) | function getRestoredSelectionForScene(
  function syncEditorSelectionFromCurrentScene (line 259) | function syncEditorSelectionFromCurrentScene() {
  function resetEditorInteractionState (line 329) | function resetEditorInteractionState() {
  function hasUsableSceneGraph (line 353) | function hasUsableSceneGraph(sceneGraph?: SceneGraph | null): sceneGraph...
  function applySceneGraphToEditor (line 361) | function applySceneGraphToEditor(sceneGraph?: SceneGraph | null) {
  constant LOCAL_STORAGE_KEY (line 372) | const LOCAL_STORAGE_KEY = 'pascal-editor-scene'
  function saveSceneToLocalStorage (line 374) | function saveSceneToLocalStorage(scene: SceneGraph): void {
  function loadSceneFromLocalStorage (line 382) | function loadSceneFromLocalStorage(): SceneGraph | null {

FILE: packages/editor/src/lib/sfx-bus.ts
  type SFXEvents (line 7) | type SFXEvents = {
  function initSFXBus (line 27) | function initSFXBus() {
  function triggerSFX (line 43) | function triggerSFX(event: keyof SFXEvents) {

FILE: packages/editor/src/lib/sfx-player.ts
  constant SFX (line 5) | const SFX = {
  type SFXName (line 15) | type SFXName = keyof typeof SFX
  function playSFX (line 33) | function playSFX(name: SFXName) {
  function updateSFXVolumes (line 53) | function updateSFXVolumes() {

FILE: packages/editor/src/lib/utils.ts
  function cn (line 4) | function cn(...inputs: ClassValue[]) {
  constant BASE_URL (line 20) | const BASE_URL = (() => {

FILE: packages/editor/src/store/use-audio.tsx
  type AudioState (line 6) | interface AudioState {

FILE: packages/editor/src/store/use-command-registry.ts
  type CommandAction (line 4) | type CommandAction = {
  type CommandRegistryStore (line 21) | interface CommandRegistryStore {

FILE: packages/editor/src/store/use-editor.tsx
  constant DEFAULT_ACTIVE_SIDEBAR_PANEL (line 21) | const DEFAULT_ACTIVE_SIDEBAR_PANEL = 'site'
  constant DEFAULT_FLOORPLAN_PANE_RATIO (line 22) | const DEFAULT_FLOORPLAN_PANE_RATIO = 0.5
  constant MIN_FLOORPLAN_PANE_RATIO (line 23) | const MIN_FLOORPLAN_PANE_RATIO = 0.15
  constant MAX_FLOORPLAN_PANE_RATIO (line 24) | const MAX_FLOORPLAN_PANE_RATIO = 0.85
  type ViewMode (line 26) | type ViewMode = '3d' | '2d' | 'split'
  type SplitOrientation (line 27) | type SplitOrientation = 'horizontal' | 'vertical'
  type Phase (line 29) | type Phase = 'site' | 'structure' | 'furnish'
  type Mode (line 31) | type Mode = 'select' | 'edit' | 'delete' | 'build'
  type StructureTool (line 34) | type StructureTool =
  type FurnishTool (line 49) | type FurnishTool = 'item'
  type SiteTool (line 52) | type SiteTool = 'property-line'
  type CatalogCategory (line 55) | type CatalogCategory =
  type StructureLayer (line 64) | type StructureLayer = 'zones' | 'elements'
  type FloorplanSelectionTool (line 66) | type FloorplanSelectionTool = 'click' | 'marquee'
  type Tool (line 69) | type Tool = SiteTool | StructureTool | FurnishTool
  type EditorState (line 71) | type EditorState = {
  type PersistedEditorUiState (line 124) | type PersistedEditorUiState = Pick<
  type PersistedEditorLayoutState (line 129) | type PersistedEditorLayoutState = Pick<
  type PersistedEditorState (line 133) | type PersistedEditorState = PersistedEditorUiState & PersistedEditorLayo...
  constant DEFAULT_PERSISTED_EDITOR_UI_STATE (line 135) | const DEFAULT_PERSISTED_EDITOR_UI_STATE: PersistedEditorUiState = {
  constant DEFAULT_PERSISTED_EDITOR_LAYOUT_STATE (line 145) | const DEFAULT_PERSISTED_EDITOR_LAYOUT_STATE: PersistedEditorLayoutState = {
  function normalizeModeForPhase (line 152) | function normalizeModeForPhase(phase: Phase, mode: Mode | undefined): Mo...
  function normalizeFloorplanPaneRatio (line 160) | function normalizeFloorplanPaneRatio(value: unknown): number {
  function normalizePersistedEditorUiState (line 168) | function normalizePersistedEditorUiState(
  function normalizePersistedEditorLayoutState (line 243) | function normalizePersistedEditorLayoutState(
  function hasCustomPersistedEditorUiState (line 257) | function hasCustomPersistedEditorUiState(
  function selectDefaultBuildingAndLevel (line 277) | function selectDefaultBuildingAndLevel() {

FILE: packages/editor/src/store/use-palette-view-registry.ts
  type PaletteViewProps (line 4) | type PaletteViewProps = {
  type PaletteView (line 9) | type PaletteView = {
  type PaletteViewRegistryStore (line 25) | interface PaletteViewRegistryStore {

FILE: packages/editor/src/store/use-upload.ts
  type UploadStatus (line 3) | type UploadStatus = 'preparing' | 'uploading' | 'confirming' | 'done' | ...
  type UploadEntry (line 5) | interface UploadEntry {
  type UploadHandler (line 14) | type UploadHandler = (
  type UploadState (line 21) | interface UploadState {

FILE: packages/ui/src/button.tsx
  type ButtonProps (line 5) | interface ButtonProps {

FILE: packages/ui/src/card.tsx
  function Card (line 3) | function Card({

FILE: packages/ui/src/code.tsx
  function Code (line 3) | function Code({

FILE: packages/viewer/src/components/error-boundary.tsx
  class ErrorBoundary (line 4) | class ErrorBoundary extends Component<
    method getDerivedStateFromError (line 9) | static getDerivedStateFromError() {
    method componentDidCatch (line 12) | componentDidCatch(_e: Error, _i: ErrorInfo) {}
    method render (line 13) | render() {

FILE: packages/viewer/src/components/renderers/ceiling/ceiling-renderer.tsx
  function createCeilingMaterials (line 17) | function createCeilingMaterials(color: string = '#999999') {

FILE: packages/viewer/src/components/renderers/site/site-renderer.tsx
  constant Y_OFFSET (line 7) | const Y_OFFSET = 0.01

FILE: packages/viewer/src/components/renderers/zone/zone-renderer.tsx
  constant Y_OFFSET (line 10) | const Y_OFFSET = 0.01
  constant WALL_HEIGHT (line 11) | const WALL_HEIGHT = 2.3

FILE: packages/viewer/src/components/viewer/index.tsx
  function AnimatedBackground (line 32) | function AnimatedBackground({ isDark }: { isDark: boolean }) {
  type ThreeElements (line 60) | interface ThreeElements extends ThreeToJSXElements<typeof THREE> {}
  function GPUDeviceWatcher (line 72) | function GPUDeviceWatcher() {
  type ViewerProps (line 92) | interface ViewerProps {

FILE: packages/viewer/src/components/viewer/lights.tsx
  function Lights (line 7) | function Lights() {

FILE: packages/viewer/src/components/viewer/perf-monitor.tsx
  constant SAMPLE_INTERVAL (line 6) | const SAMPLE_INTERVAL = 0.5 // seconds between display updates

FILE: packages/viewer/src/components/viewer/post-processing.tsx
  constant SSGI_PARAMS (line 29) | const SSGI_PARAMS = {
  constant MAX_PIPELINE_RETRIES (line 44) | const MAX_PIPELINE_RETRIES = 3
  constant RETRY_DELAY_MS (line 45) | const RETRY_DELAY_MS = 500
  constant DARK_BG (line 47) | const DARK_BG = '#1f2433'
  constant LIGHT_BG (line 48) | const LIGHT_BG = '#ffffff'
  function generateSelectedOutlinePass (line 200) | function generateSelectedOutlinePass() {
  function generateHoverOutlinePass (line 222) | function generateHoverOutlinePass() {

FILE: packages/viewer/src/components/viewer/selection-manager.tsx
  constant EDGE_TOLERANCE (line 25) | const EDGE_TOLERANCE = 0.5
  type SelectableNodeType (line 27) | type SelectableNodeType =
  type SelectionStrategy (line 78) | interface SelectionStrategy {

FILE: packages/viewer/src/hooks/use-asset-url.ts
  function useAssetUrl (line 8) | function useAssetUrl(url: string): string | null {

FILE: packages/viewer/src/hooks/use-node-events.ts
  type NodeConfig (line 36) | type NodeConfig = {
  type NodeType (line 53) | type NodeType = keyof NodeConfig
  function useNodeEvents (line 55) | function useNodeEvents<T extends NodeType>(node: NodeConfig[T]['node'], ...

FILE: packages/viewer/src/lib/asset-url.ts
  constant ASSETS_CDN_URL (line 3) | const ASSETS_CDN_URL = process.env.NEXT_PUBLIC_ASSETS_CDN_URL || 'https:...
  function resolveAssetUrl (line 12) | async function resolveAssetUrl(url: string | undefined | null): Promise<...
  function resolveCdnUrl (line 34) | function resolveCdnUrl(url: string | undefined | null): string | null {

FILE: packages/viewer/src/lib/layers.ts
  constant SCENE_LAYER (line 2) | const SCENE_LAYER = 0
  constant ZONE_LAYER (line 5) | const ZONE_LAYER = 2

FILE: packages/viewer/src/lib/materials.ts
  function getCacheKey (line 12) | function getCacheKey(props: MaterialProperties): string {
  function createMaterial (line 16) | function createMaterial(material?: MaterialSchema): THREE.MeshStandardMa...
  function createDefaultMaterial (line 37) | function createDefaultMaterial(
  constant DEFAULT_WALL_MATERIAL (line 49) | const DEFAULT_WALL_MATERIAL = createDefaultMaterial('#ffffff', 0.9)
  constant DEFAULT_SLAB_MATERIAL (line 50) | const DEFAULT_SLAB_MATERIAL = createDefaultMaterial('#e5e5e5', 0.8)
  constant DEFAULT_DOOR_MATERIAL (line 51) | const DEFAULT_DOOR_MATERIAL = createDefaultMaterial('#8b4513', 0.7)
  constant DEFAULT_WINDOW_MATERIAL (line 52) | const DEFAULT_WINDOW_MATERIAL = new THREE.MeshStandardMaterial({
  constant DEFAULT_CEILING_MATERIAL (line 60) | const DEFAULT_CEILING_MATERIAL = createDefaultMaterial('#f5f5dc', 0.95)
  constant DEFAULT_ROOF_MATERIAL (line 61) | const DEFAULT_ROOF_MATERIAL = createDefaultMaterial('#808080', 0.85)
  constant DEFAULT_STAIR_MATERIAL (line 62) | const DEFAULT_STAIR_MATERIAL = createDefaultMaterial('#ffffff', 0.9)
  function disposeMaterial (line 64) | function disposeMaterial(material: THREE.Material): void {
  function clearMaterialCache (line 68) | function clearMaterialCache(): void {

FILE: packages/viewer/src/r3f.d.ts
  type ThreeJSXElements (line 18) | interface ThreeJSXElements {
  type IntrinsicElements (line 82) | interface IntrinsicElements extends ThreeJSXElements {}
  type IntrinsicElements (line 88) | interface IntrinsicElements extends ThreeJSXElements {}
  type IntrinsicElements (line 94) | interface IntrinsicElements extends ThreeJSXElements {}

FILE: packages/viewer/src/store/use-item-light-pool.ts
  type LightRegistration (line 4) | type LightRegistration = {
  type ItemLightPoolStore (line 14) | type ItemLightPoolStore = {

FILE: packages/viewer/src/store/use-viewer.d.ts
  type SelectionPath (line 3) | type SelectionPath = {
  type Outliner (line 9) | type Outliner = {
  type ViewerState (line 13) | type ViewerState = {

FILE: packages/viewer/src/store/use-viewer.ts
  type SelectionPath (line 9) | type SelectionPath = {
  type Outliner (line 16) | type Outliner = {
  type ViewerState (line 21) | type ViewerState = {

FILE: packages/viewer/src/systems/export/export-system.tsx
  constant EDITOR_LAYER (line 12) | const EDITOR_LAYER = 1 // same constant used across the editor
  function downloadBlob (line 14) | function downloadBlob(blob: Blob, filename: string) {

FILE: packages/viewer/src/systems/item-light/item-light-system.tsx
  constant POOL_SIZE (line 9) | const POOL_SIZE = 12
  constant REASSIGN_INTERVAL (line 11) | const REASSIGN_INTERVAL = 0.2
  constant HYSTERESIS (line 15) | const HYSTERESIS = 0.15
  constant CAM_MOVE_DIST (line 18) | const CAM_MOVE_DIST = 0.5 // units
  constant CAM_ROT_DOT (line 19) | const CAM_ROT_DOT = 0.995 // cos(~5.7°)
  type SlotRuntime (line 21) | type SlotRuntime = {
  type SceneNodes (line 35) | type SceneNodes = ReturnType<typeof useScene.getState>['nodes']
  type InteractiveState (line 36) | type InteractiveState = ReturnType<typeof useInteractive.getState>
  function scoreRegistration (line 38) | function scoreRegistration(
  function ItemLightSystem (line 89) | function ItemLightSystem() {

FILE: packages/viewer/src/systems/level/level-system.tsx
  constant EXPLODED_GAP (line 7) | const EXPLODED_GAP = 5
  type LevelEntry (line 17) | type LevelEntry = {

FILE: packages/viewer/src/systems/level/level-utils.ts
  constant DEFAULT_LEVEL_HEIGHT (line 9) | const DEFAULT_LEVEL_HEIGHT = 2.5
  function getLevelHeight (line 17) | function getLevelHeight(
  function snapLevelsToTruePositions (line 64) | function snapLevelsToTruePositions(): () => void {

FILE: packages/viewer/src/systems/wall/wall-cutout.tsx
  type WallMaterials (line 30) | interface WallMaterials {
  function getMaterialHash (line 38) | function getMaterialHash(wallNode: WallNode): string {
  function getPresetColor (line 62) | function getPresetColor(preset: string): string {
  function getMaterialsForWall (line 66) | function getMaterialsForWall(wallNode: WallNode): WallMaterials {
  function getWallHideState (line 106) | function getWallHideState(

FILE: packages/viewer/src/systems/zone/zone-system.tsx
  constant TRANSITION_DURATION (line 8) | const TRANSITION_DURATION = 400 // ms
  constant EXIT_DEBOUNCE_MS (line 9) | const EXIT_DEBOUNCE_MS = 50 // ignore rapid exit→re-enter within this wi...
Condensed preview — 476 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (1,868K chars).
[
  {
    "path": ".claude/settings.json",
    "chars": 74,
    "preview": "{\n  \"permissions\": {\n    \"allow\": [\n      \"Bash(npx turbo:*)\"\n    ]\n  }\n}\n"
  },
  {
    "path": ".cursor/rules/creating-rules.mdc",
    "chars": 2233,
    "preview": "---\ndescription: How to create and maintain project rules\nglobs: .cursor/rules/**\nalwaysApply: false\n---\n\n# Creating Rul"
  },
  {
    "path": ".cursor/rules/events.mdc",
    "chars": 3381,
    "preview": "---\ndescription: Typed event bus — emitting and listening to node and grid events\nglobs: packages/core/src/events/**,pac"
  },
  {
    "path": ".cursor/rules/layers.mdc",
    "chars": 2709,
    "preview": "---\ndescription: Three.js layer conventions — which layer each object type lives on and why\nglobs: packages/viewer/**,ap"
  },
  {
    "path": ".cursor/rules/node-schemas.mdc",
    "chars": 3235,
    "preview": "---\ndescription: Node type definitions, Zod schema pattern, and how to create nodes in the scene\nglobs: packages/core/sr"
  },
  {
    "path": ".cursor/rules/renderers.mdc",
    "chars": 2526,
    "preview": "---\ndescription: Node renderer pattern in packages/viewer\nglobs: packages/viewer/**\nalwaysApply: false\n---\n\n# Renderers\n"
  },
  {
    "path": ".cursor/rules/scene-registry.mdc",
    "chars": 2781,
    "preview": "---\ndescription: Scene registry pattern — mapping node IDs to live THREE.Object3D instances\nglobs: packages/core/src/hoo"
  },
  {
    "path": ".cursor/rules/selection-managers.mdc",
    "chars": 3749,
    "preview": "---\ndescription: Selection managers — two-layer architecture for viewer and editor selection\nglobs: packages/viewer/src/"
  },
  {
    "path": ".cursor/rules/spatial-queries.mdc",
    "chars": 3309,
    "preview": "---\ndescription: Placement validation for tools — canPlaceOnFloor, canPlaceOnWall, canPlaceOnCeiling\nglobs: apps/editor/"
  },
  {
    "path": ".cursor/rules/systems.mdc",
    "chars": 3888,
    "preview": "---\ndescription: Core and viewer systems architecture\nglobs: packages/core/src/systems/**,packages/viewer/src/systems/**"
  },
  {
    "path": ".cursor/rules/tools.mdc",
    "chars": 2648,
    "preview": "---\ndescription: Editor tools structure in apps/editor\nglobs: apps/editor/components/tools/**\nalwaysApply: false\n---\n\n# "
  },
  {
    "path": ".cursor/rules/viewer-isolation.mdc",
    "chars": 2937,
    "preview": "---\ndescription: Viewer must be editor-agnostic — controlled from outside via props and children\nglobs: packages/viewer/"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.yml",
    "chars": 1691,
    "preview": "name: 🐛 Bug Report\ndescription: Report something that isn't working correctly\nlabels: [\"bug\", \"needs-triage\"]\nbody:\n  - "
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "chars": 382,
    "preview": "blank_issues_enabled: false\ncontact_links:\n  - name: 💬 Questions & Help\n    url: https://github.com/pascalorg/editor/dis"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.yml",
    "chars": 1114,
    "preview": "name: ✨ Feature Request\ndescription: Suggest a new feature or improvement\nlabels: [\"enhancement\", \"needs-triage\"]\nbody:\n"
  },
  {
    "path": ".github/pull_request_template.md",
    "chars": 713,
    "preview": "## What does this PR do?\n\n<!-- A brief description of the change. Link to a related issue if applicable: Fixes #123 -->\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "chars": 3939,
    "preview": "name: Release\n\non:\n  workflow_dispatch:\n    inputs:\n      package:\n        description: \"Package to release\"\n        req"
  },
  {
    "path": ".gitignore",
    "chars": 528,
    "preview": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# Dependencies\nnode_modules\npacka"
  },
  {
    "path": ".npmrc",
    "chars": 0,
    "preview": ""
  },
  {
    "path": ".vscode/settings.json",
    "chars": 1129,
    "preview": "{\n  \"typescript.tsdk\": \"node_modules/typescript/lib\",\n  \"biome.configPath\": \"./biome.jsonc\",\n  \"editor.defaultFormatter\""
  },
  {
    "path": "CONTRIBUTING.md",
    "chars": 2248,
    "preview": "# Contributing to Pascal Editor\n\nThanks for your interest in contributing! We welcome all kinds of contributions — bug f"
  },
  {
    "path": "LICENSE",
    "chars": 1074,
    "preview": "MIT License\n\nCopyright (c) 2026 Pascal Group Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining"
  },
  {
    "path": "README.md",
    "chars": 11537,
    "preview": "# Pascal Editor\n\nA 3D building editor built with React Three Fiber and WebGPU.\n\n[![MIT License](https://img.shields.io/b"
  },
  {
    "path": "SETUP.md",
    "chars": 1337,
    "preview": "# Pascal Editor — Setup\n\n## Prerequisites\n\n- [Bun](https://bun.sh/) 1.3+ (or Node.js 18+)\n\n## Quick Start\n\n```bash\nbun i"
  },
  {
    "path": "apps/editor/.gitignore",
    "chars": 428,
    "preview": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pn"
  },
  {
    "path": "apps/editor/README.md",
    "chars": 9291,
    "preview": "# Pascal Editor\n\nA 3D building editor built with React Three Fiber and WebGPU.\n\n## Repository Architecture\n\nThis is a Tu"
  },
  {
    "path": "apps/editor/app/api/health/route.ts",
    "chars": 119,
    "preview": "export function GET() {\n  return Response.json({ status: 'ok', app: 'editor', timestamp: new Date().toISOString() })\n}\n"
  },
  {
    "path": "apps/editor/app/globals.css",
    "chars": 8631,
    "preview": "@import \"tailwindcss\";\n@import \"tw-animate-css\";\n@source \"../../../packages/editor/src\";\n\n@custom-variant dark (&:is(.da"
  },
  {
    "path": "apps/editor/app/layout.tsx",
    "chars": 1233,
    "preview": "import { Agentation } from 'agentation'\nimport { GeistPixelSquare } from 'geist/font/pixel'\nimport { Barlow } from 'next"
  },
  {
    "path": "apps/editor/app/page.tsx",
    "chars": 631,
    "preview": "'use client'\n\nimport {\n  Editor,\n  type SidebarTab,\n  ViewerToolbarLeft,\n  ViewerToolbarRight,\n} from '@pascal-app/edito"
  },
  {
    "path": "apps/editor/app/privacy/page.tsx",
    "chars": 9354,
    "preview": "import type { Metadata } from 'next'\nimport Link from 'next/link'\n\nexport const metadata: Metadata = {\n  title: 'Privacy"
  },
  {
    "path": "apps/editor/app/terms/page.tsx",
    "chars": 8850,
    "preview": "import type { Metadata } from 'next'\nimport Link from 'next/link'\n\nexport const metadata: Metadata = {\n  title: 'Terms o"
  },
  {
    "path": "apps/editor/env.mjs",
    "chars": 1678,
    "preview": "/**\n * Environment variable validation for the editor app.\n *\n * This file validates that required environment variables"
  },
  {
    "path": "apps/editor/lib/utils.ts",
    "chars": 1407,
    "preview": "import { type ClassValue, clsx } from 'clsx'\nimport { twMerge } from 'tailwind-merge'\n\nexport function cn(...inputs: Cla"
  },
  {
    "path": "apps/editor/next.config.ts",
    "chars": 857,
    "preview": "import type { NextConfig } from 'next'\n\nconst nextConfig: NextConfig = {\n  typescript: {\n    ignoreBuildErrors: true,\n  "
  },
  {
    "path": "apps/editor/package.json",
    "chars": 1172,
    "preview": "{\n  \"name\": \"editor\",\n  \"version\": \"0.1.0\",\n  \"type\": \"module\",\n  \"private\": true,\n  \"scripts\": {\n    \"dev\": \"dotenv -e "
  },
  {
    "path": "apps/editor/postcss.config.mjs",
    "chars": 69,
    "preview": "export default {\n  plugins: {\n    '@tailwindcss/postcss': {},\n  },\n}\n"
  },
  {
    "path": "apps/editor/public/demos/demo_1.json",
    "chars": 41487,
    "preview": "{\n  \"nodes\": {\n    \"building_bv4ilcjivnxn8wkd\": {\n      \"object\": \"node\",\n      \"id\": \"building_bv4ilcjivnxn8wkd\",\n     "
  },
  {
    "path": "apps/editor/tsconfig.json",
    "chars": 443,
    "preview": "{\n  \"extends\": \"@pascal/typescript-config/nextjs.json\",\n  \"compilerOptions\": {\n    \"plugins\": [\n      {\n        \"name\": "
  },
  {
    "path": "biome.jsonc",
    "chars": 3065,
    "preview": "{\n  \"$schema\": \"./node_modules/@biomejs/biome/configuration_schema.json\",\n  \"vcs\": {\n    \"enabled\": true,\n    \"clientKin"
  },
  {
    "path": "package.json",
    "chars": 1242,
    "preview": "{\n  \"name\": \"editor\",\n  \"private\": true,\n  \"scripts\": {\n    \"build\": \"turbo run build\",\n    \"dev\": \"set -a && . ./.env 2"
  },
  {
    "path": "packages/core/README.md",
    "chars": 1971,
    "preview": "# @pascal-app/core\n\nCore library for Pascal 3D building editor.\n\n## Installation\n\n```bash\nnpm install @pascal-app/core\n`"
  },
  {
    "path": "packages/core/package.json",
    "chars": 1455,
    "preview": "{\n  \"name\": \"@pascal-app/core\",\n  \"version\": \"0.3.3\",\n  \"description\": \"Core library for Pascal 3D building editor\",\n  \""
  },
  {
    "path": "packages/core/src/events/bus.ts",
    "chars": 3175,
    "preview": "import type { ThreeEvent } from '@react-three/fiber'\nimport mitt from 'mitt'\nimport type {\n  BuildingNode,\n  CeilingNode"
  },
  {
    "path": "packages/core/src/hooks/scene-registry/scene-registry.ts",
    "chars": 1530,
    "preview": "import { useLayoutEffect } from 'react'\nimport type * as THREE from 'three'\n\nexport const sceneRegistry = {\n  // Master "
  },
  {
    "path": "packages/core/src/hooks/spatial-grid/spatial-grid-manager.ts",
    "chars": 24280,
    "preview": "import type { AnyNode, CeilingNode, ItemNode, SlabNode, WallNode } from '../../schema'\nimport { getScaledDimensions } fr"
  },
  {
    "path": "packages/core/src/hooks/spatial-grid/spatial-grid-sync.ts",
    "chars": 5126,
    "preview": "import {\n  type AnyNode,\n  type AnyNodeId,\n  getScaledDimensions,\n  type ItemNode,\n  type SlabNode,\n  type WallNode,\n} f"
  },
  {
    "path": "packages/core/src/hooks/spatial-grid/spatial-grid.ts",
    "chars": 4572,
    "preview": "type CellKey = `${number},${number}`\n\ninterface GridCell {\n  itemIds: Set<string>\n}\n\ninterface SpatialGridConfig {\n  cel"
  },
  {
    "path": "packages/core/src/hooks/spatial-grid/use-spatial-query.ts",
    "chars": 1582,
    "preview": "import { useCallback } from 'react'\nimport type { CeilingNode, LevelNode, WallNode } from '../../schema'\nimport { spatia"
  },
  {
    "path": "packages/core/src/hooks/spatial-grid/wall-spatial-grid.ts",
    "chars": 6520,
    "preview": "type WallSide = 'front' | 'back'\ntype AttachType = 'wall' | 'wall-side'\n\n// Small tolerance for floating point compariso"
  },
  {
    "path": "packages/core/src/index.ts",
    "chars": 2064,
    "preview": "// Store\n\nexport type {\n  BuildingEvent,\n  CameraControlEvent,\n  CeilingEvent,\n  DoorEvent,\n  EventSuffix,\n  GridEvent,\n"
  },
  {
    "path": "packages/core/src/lib/asset-storage.ts",
    "chars": 1425,
    "preview": "import { get, set } from 'idb-keyval'\n\nexport const ASSET_PREFIX = 'asset_data:'\n\n// Cache for active object URLs to pre"
  },
  {
    "path": "packages/core/src/lib/space-detection.ts",
    "chars": 17883,
    "preview": "import type { WallNode } from '../schema'\n\n// =========================================================================="
  },
  {
    "path": "packages/core/src/schema/base.ts",
    "chars": 1318,
    "preview": "import { customAlphabet } from 'nanoid'\nimport { z } from 'zod'\nimport { CameraSchema } from './camera'\n\nconst customId "
  },
  {
    "path": "packages/core/src/schema/camera.ts",
    "chars": 411,
    "preview": "import { z } from 'zod'\n\nconst Vector3Schema = z.tuple([z.number(), z.number(), z.number()])\n\nexport const CameraSchema "
  },
  {
    "path": "packages/core/src/schema/collections.ts",
    "chars": 342,
    "preview": "import { generateId } from './base'\nimport type { AnyNodeId } from './types'\n\nexport type CollectionId = `collection_${s"
  },
  {
    "path": "packages/core/src/schema/index.ts",
    "chars": 1438,
    "preview": "// Base\nexport { BaseNode, generateId, nodeType, objectId } from './base'\n// Camera\nexport { CameraSchema } from './came"
  },
  {
    "path": "packages/core/src/schema/material.ts",
    "chars": 2871,
    "preview": "import { z } from 'zod'\n\nexport const MaterialPreset = z.enum([\n  'white',\n  'brick',\n  'concrete',\n  'wood',\n  'glass',"
  },
  {
    "path": "packages/core/src/schema/nodes/building.ts",
    "chars": 772,
    "preview": "import dedent from 'dedent'\nimport { z } from 'zod'\nimport { BaseNode, nodeType, objectId } from '../base'\nimport { Leve"
  },
  {
    "path": "packages/core/src/schema/nodes/ceiling.ts",
    "chars": 838,
    "preview": "import dedent from 'dedent'\nimport { z } from 'zod'\nimport { BaseNode, nodeType, objectId } from '../base'\nimport { Mate"
  },
  {
    "path": "packages/core/src/schema/nodes/door.ts",
    "chars": 2788,
    "preview": "import dedent from 'dedent'\nimport { z } from 'zod'\nimport { BaseNode, nodeType, objectId } from '../base'\nimport { Mate"
  },
  {
    "path": "packages/core/src/schema/nodes/guide.ts",
    "chars": 487,
    "preview": "import { z } from 'zod'\nimport { BaseNode, nodeType, objectId } from '../base'\n\nexport const GuideNode = BaseNode.extend"
  },
  {
    "path": "packages/core/src/schema/nodes/item.ts",
    "chars": 5031,
    "preview": "import dedent from 'dedent'\nimport { z } from 'zod'\nimport { BaseNode, nodeType, objectId } from '../base'\nimport type {"
  },
  {
    "path": "packages/core/src/schema/nodes/level.ts",
    "chars": 1058,
    "preview": "import dedent from 'dedent'\nimport { z } from 'zod'\nimport { BaseNode, nodeType, objectId } from '../base'\nimport { Ceil"
  },
  {
    "path": "packages/core/src/schema/nodes/roof-segment.ts",
    "chars": 1692,
    "preview": "import dedent from 'dedent'\nimport { z } from 'zod'\nimport { BaseNode, nodeType, objectId } from '../base'\nimport { Mate"
  },
  {
    "path": "packages/core/src/schema/nodes/roof.ts",
    "chars": 960,
    "preview": "import dedent from 'dedent'\nimport { z } from 'zod'\nimport { BaseNode, nodeType, objectId } from '../base'\nimport { Mate"
  },
  {
    "path": "packages/core/src/schema/nodes/scan.ts",
    "chars": 483,
    "preview": "import { z } from 'zod'\nimport { BaseNode, nodeType, objectId } from '../base'\n\nexport const ScanNode = BaseNode.extend("
  },
  {
    "path": "packages/core/src/schema/nodes/site.ts",
    "chars": 1187,
    "preview": "// lib/scenegraph/schema/nodes/site.ts\n\nimport dedent from 'dedent'\nimport { z } from 'zod'\nimport { BaseNode, nodeType,"
  },
  {
    "path": "packages/core/src/schema/nodes/slab.ts",
    "chars": 713,
    "preview": "import dedent from 'dedent'\nimport { z } from 'zod'\nimport { BaseNode, nodeType, objectId } from '../base'\nimport { Mate"
  },
  {
    "path": "packages/core/src/schema/nodes/stair-segment.ts",
    "chars": 2177,
    "preview": "import dedent from 'dedent'\nimport { z } from 'zod'\nimport { BaseNode, nodeType, objectId } from '../base'\nimport { Mate"
  },
  {
    "path": "packages/core/src/schema/nodes/stair.ts",
    "chars": 1010,
    "preview": "import dedent from 'dedent'\nimport { z } from 'zod'\nimport { BaseNode, nodeType, objectId } from '../base'\nimport { Mate"
  },
  {
    "path": "packages/core/src/schema/nodes/wall.ts",
    "chars": 1377,
    "preview": "import dedent from 'dedent'\nimport { z } from 'zod'\nimport { BaseNode, nodeType, objectId } from '../base'\nimport { Mate"
  },
  {
    "path": "packages/core/src/schema/nodes/window.ts",
    "chars": 1653,
    "preview": "import dedent from 'dedent'\nimport { z } from 'zod'\nimport { BaseNode, nodeType, objectId } from '../base'\nimport { Mate"
  },
  {
    "path": "packages/core/src/schema/nodes/zone.ts",
    "chars": 837,
    "preview": "import dedent from 'dedent'\nimport { z } from 'zod'\nimport { BaseNode, nodeType, objectId } from '../base'\n\nexport const"
  },
  {
    "path": "packages/core/src/schema/types.ts",
    "chars": 1120,
    "preview": "import z from 'zod'\nimport { BuildingNode } from './nodes/building'\nimport { CeilingNode } from './nodes/ceiling'\nimport"
  },
  {
    "path": "packages/core/src/store/actions/node-actions.ts",
    "chars": 6804,
    "preview": "import type { AnyNode, AnyNodeId } from '../../schema'\nimport type { CollectionId } from '../../schema/collections'\nimpo"
  },
  {
    "path": "packages/core/src/store/use-interactive.ts",
    "chars": 2165,
    "preview": "'use client'\n\nimport { create } from 'zustand'\nimport type { Interactive } from '../schema/nodes/item'\nimport type { Any"
  },
  {
    "path": "packages/core/src/store/use-scene.ts",
    "chars": 13384,
    "preview": "'use client'\n\nimport type { TemporalState } from 'zundo'\nimport { temporal } from 'zundo'\nimport { create, type StoreApi"
  },
  {
    "path": "packages/core/src/systems/ceiling/ceiling-system.tsx",
    "chars": 2997,
    "preview": "import { useFrame } from '@react-three/fiber'\nimport * as THREE from 'three'\nimport { sceneRegistry } from '../../hooks/"
  },
  {
    "path": "packages/core/src/systems/door/door-system.tsx",
    "chars": 9504,
    "preview": "import { useFrame } from '@react-three/fiber'\nimport * as THREE from 'three'\nimport { DoubleSide, MeshStandardNodeMateri"
  },
  {
    "path": "packages/core/src/systems/item/item-system.tsx",
    "chars": 2345,
    "preview": "import { useFrame } from '@react-three/fiber'\nimport type * as THREE from 'three'\nimport { sceneRegistry } from '../../h"
  },
  {
    "path": "packages/core/src/systems/roof/roof-system.tsx",
    "chars": 30457,
    "preview": "import { useFrame } from '@react-three/fiber'\nimport * as THREE from 'three'\nimport { mergeVertices } from 'three/exampl"
  },
  {
    "path": "packages/core/src/systems/slab/slab-system.tsx",
    "chars": 4541,
    "preview": "import { useFrame } from '@react-three/fiber'\nimport * as THREE from 'three'\nimport { sceneRegistry } from '../../hooks/"
  },
  {
    "path": "packages/core/src/systems/stair/stair-system.tsx",
    "chars": 12390,
    "preview": "import { useFrame } from '@react-three/fiber'\nimport * as THREE from 'three'\nimport { mergeGeometries } from 'three/exam"
  },
  {
    "path": "packages/core/src/systems/wall/wall-footprint.ts",
    "chars": 1912,
    "preview": "import type { WallNode } from '../../schema'\nimport { type Point2D, pointToKey, type WallMiterData } from './wall-miteri"
  },
  {
    "path": "packages/core/src/systems/wall/wall-mitering.ts",
    "chars": 10825,
    "preview": "import type { WallNode } from '../../schema'\n\n// ======================================================================="
  },
  {
    "path": "packages/core/src/systems/wall/wall-system.tsx",
    "chars": 10261,
    "preview": "import { useFrame } from '@react-three/fiber'\nimport * as THREE from 'three'\nimport { Brush, Evaluator, SUBTRACTION } fr"
  },
  {
    "path": "packages/core/src/systems/window/window-system.tsx",
    "chars": 6615,
    "preview": "import { useFrame } from '@react-three/fiber'\nimport * as THREE from 'three'\nimport { DoubleSide, MeshStandardNodeMateri"
  },
  {
    "path": "packages/core/src/utils/clone-scene-graph.ts",
    "chars": 10031,
    "preview": "import type { AnyNode, AnyNodeId } from '../schema'\nimport { generateId } from '../schema/base'\nimport type { Collection"
  },
  {
    "path": "packages/core/src/utils/types.ts",
    "chars": 289,
    "preview": "/**\n * Type guard to check if a value is a plain object (and not null or an array).\n * Useful for narrowing down Zod's g"
  },
  {
    "path": "packages/core/tsconfig.json",
    "chars": 265,
    "preview": "{\n  \"extends\": \"@pascal/typescript-config/react-library.json\",\n  \"compilerOptions\": {\n    \"outDir\": \"dist\",\n    \"rootDir"
  },
  {
    "path": "packages/editor/package.json",
    "chars": 1736,
    "preview": "{\n  \"name\": \"@pascal-app/editor\",\n  \"version\": \"0.1.0\",\n  \"description\": \"Pascal building editor component\",\n  \"type\": \""
  },
  {
    "path": "packages/editor/src/components/editor/custom-camera-controls.tsx",
    "chars": 12463,
    "preview": "'use client'\r\n\r\nimport { type CameraControlEvent, emitter, sceneRegistry, useScene } from '@pascal-app/core'\r\nimport { u"
  },
  {
    "path": "packages/editor/src/components/editor/editor-layout-v2.tsx",
    "chars": 6408,
    "preview": "'use client'\n\nimport { type ReactNode, useCallback, useEffect, useRef } from 'react'\nimport useEditor from '../../store/"
  },
  {
    "path": "packages/editor/src/components/editor/export-manager.tsx",
    "chars": 2331,
    "preview": "'use client'\n\nimport { useViewer } from '@pascal-app/viewer'\nimport { useThree } from '@react-three/fiber'\nimport { useE"
  },
  {
    "path": "packages/editor/src/components/editor/first-person-controls.tsx",
    "chars": 8353,
    "preview": "'use client'\n\nimport { useFrame, useThree } from '@react-three/fiber'\nimport { useCallback, useEffect, useRef } from 're"
  },
  {
    "path": "packages/editor/src/components/editor/floating-action-menu.tsx",
    "chars": 6458,
    "preview": "'use client'\n\nimport {\n  type AnyNode,\n  type AnyNodeId,\n  DoorNode,\n  ItemNode,\n  RoofNode,\n  RoofSegmentNode,\n  sceneR"
  },
  {
    "path": "packages/editor/src/components/editor/floorplan-panel.tsx",
    "chars": 216102,
    "preview": "'use client'\n\nimport { Icon } from '@iconify/react'\nimport {\n  type AnyNodeId,\n  type BuildingNode,\n  calculateLevelMite"
  },
  {
    "path": "packages/editor/src/components/editor/grid.tsx",
    "chars": 4797,
    "preview": "'use client'\n\nimport { emitter, type GridEvent, sceneRegistry } from '@pascal-app/core'\nimport { useViewer } from '@pasc"
  },
  {
    "path": "packages/editor/src/components/editor/index.tsx",
    "chars": 26688,
    "preview": "'use client'\n\nimport { Icon } from '@iconify/react'\nimport { initSpaceDetectionSync, initSpatialGridSync, useScene } fro"
  },
  {
    "path": "packages/editor/src/components/editor/node-action-menu.tsx",
    "chars": 2053,
    "preview": "'use client'\n\nimport { Copy, Move, Trash2 } from 'lucide-react'\nimport type { MouseEventHandler, PointerEventHandler } f"
  },
  {
    "path": "packages/editor/src/components/editor/preset-thumbnail-generator.tsx",
    "chars": 4560,
    "preview": "'use client'\n\nimport { emitter, sceneRegistry } from '@pascal-app/core'\nimport { useThree } from '@react-three/fiber'\nim"
  },
  {
    "path": "packages/editor/src/components/editor/selection-manager.tsx",
    "chars": 19086,
    "preview": "import {\n  type AnyNode,\n  type AnyNodeId,\n  type BuildingNode,\n  emitter,\n  type ItemNode,\n  type NodeEvent,\n  resolveL"
  },
  {
    "path": "packages/editor/src/components/editor/site-edge-labels.tsx",
    "chars": 3026,
    "preview": "'use client'\n\nimport type { SiteNode } from '@pascal-app/core'\nimport { sceneRegistry, useScene } from '@pascal-app/core"
  },
  {
    "path": "packages/editor/src/components/editor/thumbnail-generator.tsx",
    "chars": 5249,
    "preview": "'use client'\n\nimport { emitter, sceneRegistry, useScene } from '@pascal-app/core'\nimport { snapLevelsToTruePositions } f"
  },
  {
    "path": "packages/editor/src/components/editor/wall-measurement-label.tsx",
    "chars": 8112,
    "preview": "'use client'\n\nimport {\n  type AnyNodeId,\n  calculateLevelMiters,\n  DEFAULT_WALL_HEIGHT,\n  getWallPlanFootprint,\n  type P"
  },
  {
    "path": "packages/editor/src/components/feedback-dialog.tsx",
    "chars": 8570,
    "preview": "'use client'\n\nimport { useScene } from '@pascal-app/core'\nimport { ImageIcon, MessageSquare, X } from 'lucide-react'\nimp"
  },
  {
    "path": "packages/editor/src/components/pascal-radio.tsx",
    "chars": 9437,
    "preview": "'use client'\n\nimport { Howl } from 'howler'\nimport { Disc3, Settings2, SkipBack, SkipForward, Volume2, VolumeX } from 'l"
  },
  {
    "path": "packages/editor/src/components/preview-button.tsx",
    "chars": 558,
    "preview": "'use client'\n\nimport { Eye } from 'lucide-react'\nimport useEditor from '../store/use-editor'\n\nexport function PreviewBut"
  },
  {
    "path": "packages/editor/src/components/systems/ceiling/ceiling-system.tsx",
    "chars": 2648,
    "preview": "import { type AnyNodeId, sceneRegistry, useScene } from '@pascal-app/core'\nimport { useViewer } from '@pascal-app/viewer"
  },
  {
    "path": "packages/editor/src/components/systems/roof/roof-edit-system.tsx",
    "chars": 2505,
    "preview": "import { type AnyNodeId, type RoofNode, sceneRegistry, useScene } from '@pascal-app/core'\nimport { useViewer } from '@pa"
  },
  {
    "path": "packages/editor/src/components/systems/stair/stair-edit-system.tsx",
    "chars": 2539,
    "preview": "import { type AnyNodeId, type StairNode, sceneRegistry, useScene } from '@pascal-app/core'\nimport { useViewer } from '@p"
  },
  {
    "path": "packages/editor/src/components/systems/zone/zone-label-editor-system.tsx",
    "chars": 5581,
    "preview": "'use client'\n\nimport { useScene, type ZoneNode } from '@pascal-app/core'\nimport { useViewer } from '@pascal-app/viewer'\n"
  },
  {
    "path": "packages/editor/src/components/systems/zone/zone-system.tsx",
    "chars": 1485,
    "preview": "import { sceneRegistry, useScene, type ZoneNode } from '@pascal-app/core'\nimport { useViewer } from '@pascal-app/viewer'"
  },
  {
    "path": "packages/editor/src/components/tools/ceiling/ceiling-boundary-editor.tsx",
    "chars": 1474,
    "preview": "import { type CeilingNode, resolveLevelId, useScene } from '@pascal-app/core'\nimport { useViewer } from '@pascal-app/vie"
  },
  {
    "path": "packages/editor/src/components/tools/ceiling/ceiling-hole-editor.tsx",
    "chars": 1646,
    "preview": "import { type CeilingNode, resolveLevelId, useScene } from '@pascal-app/core'\nimport { useViewer } from '@pascal-app/vie"
  },
  {
    "path": "packages/editor/src/components/tools/ceiling/ceiling-tool.tsx",
    "chars": 14541,
    "preview": "import { CeilingNode, emitter, type GridEvent, type LevelNode, useScene } from '@pascal-app/core'\nimport { useViewer } f"
  },
  {
    "path": "packages/editor/src/components/tools/door/door-math.ts",
    "chars": 3337,
    "preview": "import {\n  type AnyNodeId,\n  type DoorNode,\n  getScaledDimensions,\n  type ItemNode,\n  useScene,\n  type WallNode,\n  type "
  },
  {
    "path": "packages/editor/src/components/tools/door/door-tool.tsx",
    "chars": 8633,
    "preview": "import {\n  type AnyNodeId,\n  DoorNode,\n  emitter,\n  sceneRegistry,\n  spatialGridManager,\n  useScene,\n  type WallEvent,\n}"
  },
  {
    "path": "packages/editor/src/components/tools/door/move-door-tool.tsx",
    "chars": 11201,
    "preview": "import {\n  type AnyNodeId,\n  DoorNode,\n  emitter,\n  sceneRegistry,\n  spatialGridManager,\n  useScene,\n  type WallEvent,\n}"
  },
  {
    "path": "packages/editor/src/components/tools/item/item-tool.tsx",
    "chars": 740,
    "preview": "import { sfxEmitter } from '../../../lib/sfx-bus'\nimport useEditor from '../../../store/use-editor'\nimport { useDraftNod"
  },
  {
    "path": "packages/editor/src/components/tools/item/move-tool.tsx",
    "chars": 3260,
    "preview": "import type { DoorNode, ItemNode, RoofNode, RoofSegmentNode, WindowNode } from '@pascal-app/core'\nimport { Vector3 } fro"
  },
  {
    "path": "packages/editor/src/components/tools/item/placement-math.ts",
    "chars": 2807,
    "preview": "import { isObject } from '@pascal-app/core'\n\n/**\n * Snaps a position to 0.5 grid, with an offset to align item edges to "
  },
  {
    "path": "packages/editor/src/components/tools/item/placement-strategies.ts",
    "chars": 17112,
    "preview": "import type {\n  AnyNode,\n  AnyNodeId,\n  CeilingEvent,\n  CeilingNode,\n  GridEvent,\n  ItemEvent,\n  ItemNode,\n  WallEvent,\n"
  },
  {
    "path": "packages/editor/src/components/tools/item/placement-types.ts",
    "chars": 3311,
    "preview": "import type {\n  AnyNode,\n  AssetInput,\n  CeilingNode,\n  ItemNode,\n  LevelNode,\n  WallNode,\n} from '@pascal-app/core'\nimp"
  },
  {
    "path": "packages/editor/src/components/tools/item/use-draft-node.ts",
    "chars": 7511,
    "preview": "import {\n  type AnyNodeId,\n  type AssetInput,\n  ItemNode,\n  sceneRegistry,\n  useScene,\n} from '@pascal-app/core'\nimport "
  },
  {
    "path": "packages/editor/src/components/tools/item/use-placement-coordinator.tsx",
    "chars": 27025,
    "preview": "import type { AssetInput } from '@pascal-app/core'\nimport {\n  type AnyNodeId,\n  type CeilingEvent,\n  emitter,\n  type Gri"
  },
  {
    "path": "packages/editor/src/components/tools/roof/move-roof-tool.tsx",
    "chars": 9306,
    "preview": "import {\n  type AnyNodeId,\n  emitter,\n  type GridEvent,\n  type RoofNode,\n  type RoofSegmentNode,\n  sceneRegistry,\n  useS"
  },
  {
    "path": "packages/editor/src/components/tools/roof/roof-tool.tsx",
    "chars": 9461,
    "preview": "import {\n  type AnyNode,\n  type AnyNodeId,\n  emitter,\n  type GridEvent,\n  type LevelNode,\n  RoofNode,\n  RoofSegmentNode,"
  },
  {
    "path": "packages/editor/src/components/tools/select/box-select-tool.tsx",
    "chars": 17307,
    "preview": "import { Icon } from '@iconify/react'\nimport {\n  type AnyNodeId,\n  type CeilingNode,\n  emitter,\n  type GridEvent,\n  type"
  },
  {
    "path": "packages/editor/src/components/tools/shared/cursor-sphere.tsx",
    "chars": 3906,
    "preview": "import { Html } from '@react-three/drei'\nimport type { ThreeElements } from '@react-three/fiber'\nimport { forwardRef } f"
  },
  {
    "path": "packages/editor/src/components/tools/shared/polygon-editor.tsx",
    "chars": 12000,
    "preview": "import { emitter, type GridEvent, sceneRegistry } from '@pascal-app/core'\nimport { createPortal } from '@react-three/fib"
  },
  {
    "path": "packages/editor/src/components/tools/site/site-boundary-editor.tsx",
    "chars": 1212,
    "preview": "import { type SiteNode, useScene } from '@pascal-app/core'\nimport { useCallback } from 'react'\nimport { PolygonEditor } "
  },
  {
    "path": "packages/editor/src/components/tools/slab/slab-boundary-editor.tsx",
    "chars": 1400,
    "preview": "import { resolveLevelId, type SlabNode, useScene } from '@pascal-app/core'\nimport { useViewer } from '@pascal-app/viewer"
  },
  {
    "path": "packages/editor/src/components/tools/slab/slab-hole-editor.tsx",
    "chars": 1578,
    "preview": "import { resolveLevelId, type SlabNode, useScene } from '@pascal-app/core'\nimport { useViewer } from '@pascal-app/viewer"
  },
  {
    "path": "packages/editor/src/components/tools/slab/slab-tool.tsx",
    "chars": 10079,
    "preview": "import { emitter, type GridEvent, type LevelNode, SlabNode, useScene } from '@pascal-app/core'\nimport { useViewer } from"
  },
  {
    "path": "packages/editor/src/components/tools/stair/stair-tool.tsx",
    "chars": 5419,
    "preview": "import {\n  type AnyNode,\n  emitter,\n  type GridEvent,\n  type LevelNode,\n  StairNode,\n  StairSegmentNode,\n  useScene,\n} f"
  },
  {
    "path": "packages/editor/src/components/tools/tool-manager.tsx",
    "chars": 4787,
    "preview": "import { type AnyNodeId, type CeilingNode, type SlabNode, useScene } from '@pascal-app/core'\nimport { useViewer } from '"
  },
  {
    "path": "packages/editor/src/components/tools/wall/wall-drafting.ts",
    "chars": 3924,
    "preview": "import { useScene, type WallNode, WallNode as WallSchema } from '@pascal-app/core'\nimport { useViewer } from '@pascal-ap"
  },
  {
    "path": "packages/editor/src/components/tools/wall/wall-tool.tsx",
    "chars": 7048,
    "preview": "import { emitter, type GridEvent, type LevelNode, useScene, type WallNode } from '@pascal-app/core'\nimport { useViewer }"
  },
  {
    "path": "packages/editor/src/components/tools/window/move-window-tool.tsx",
    "chars": 13324,
    "preview": "import {\n  type AnyNodeId,\n  emitter,\n  sceneRegistry,\n  spatialGridManager,\n  useScene,\n  type WallEvent,\n  WindowNode,"
  },
  {
    "path": "packages/editor/src/components/tools/window/window-math.ts",
    "chars": 4033,
    "preview": "import {\n  type AnyNodeId,\n  type DoorNode,\n  getScaledDimensions,\n  type ItemNode,\n  useScene,\n  type WallNode,\n  type "
  },
  {
    "path": "packages/editor/src/components/tools/window/window-tool.tsx",
    "chars": 9231,
    "preview": "import {\n  type AnyNodeId,\n  emitter,\n  sceneRegistry,\n  spatialGridManager,\n  useScene,\n  type WallEvent,\n  WindowNode,"
  },
  {
    "path": "packages/editor/src/components/tools/zone/zone-boundary-editor.tsx",
    "chars": 1160,
    "preview": "import { resolveLevelId, useScene, type ZoneNode } from '@pascal-app/core'\nimport { useCallback } from 'react'\nimport { "
  },
  {
    "path": "packages/editor/src/components/tools/zone/zone-tool.tsx",
    "chars": 11215,
    "preview": "import { emitter, type GridEvent, type LevelNode, useScene, ZoneNode } from '@pascal-app/core'\nimport { useViewer } from"
  },
  {
    "path": "packages/editor/src/components/ui/action-menu/action-button.tsx",
    "chars": 1841,
    "preview": "import * as React from 'react'\nimport { Button } from './../../../components/ui/primitives/button'\nimport {\n  Tooltip,\n "
  },
  {
    "path": "packages/editor/src/components/ui/action-menu/camera-actions.tsx",
    "chars": 2555,
    "preview": "'use client'\r\n\r\nimport { Icon } from '@iconify/react'\r\nimport { emitter } from '@pascal-app/core'\r\nimport Image from 'ne"
  },
  {
    "path": "packages/editor/src/components/ui/action-menu/control-modes.tsx",
    "chars": 6274,
    "preview": "'use client'\n\nimport { Icon } from '@iconify/react'\nimport { type LevelNode, useScene } from '@pascal-app/core'\nimport {"
  },
  {
    "path": "packages/editor/src/components/ui/action-menu/furnish-tools.tsx",
    "chars": 2953,
    "preview": "'use client'\n\nimport NextImage from 'next/image'\nimport { cn } from './../../../lib/utils'\nimport useEditor, { type Cata"
  },
  {
    "path": "packages/editor/src/components/ui/action-menu/index.tsx",
    "chars": 5049,
    "preview": "'use client'\n\nimport { AnimatePresence, motion } from 'motion/react'\nimport { TooltipProvider } from './../../../compone"
  },
  {
    "path": "packages/editor/src/components/ui/action-menu/structure-tools.tsx",
    "chars": 3455,
    "preview": "'use client'\n\nimport NextImage from 'next/image'\nimport { useContextualTools } from '../../../hooks/use-contextual-tools"
  },
  {
    "path": "packages/editor/src/components/ui/action-menu/view-toggles.tsx",
    "chars": 10600,
    "preview": "'use client'\n\nimport {\n  type AnyNodeId,\n  type GuideNode,\n  type LevelNode,\n  type ScanNode,\n  useScene,\n} from '@pasca"
  },
  {
    "path": "packages/editor/src/components/ui/command-palette/editor-commands.tsx",
    "chars": 13123,
    "preview": "'use client'\n\nimport type { AnyNodeId } from '@pascal-app/core'\nimport { LevelNode, useScene } from '@pascal-app/core'\ni"
  },
  {
    "path": "packages/editor/src/components/ui/command-palette/index.tsx",
    "chars": 26164,
    "preview": "'use client'\n\nimport type { AnyNodeId, LevelNode } from '@pascal-app/core'\nimport { useScene } from '@pascal-app/core'\ni"
  },
  {
    "path": "packages/editor/src/components/ui/controls/action-button.tsx",
    "chars": 825,
    "preview": "'use client'\n\nimport { cn } from '../../../lib/utils'\n\ninterface ActionButtonProps extends React.ButtonHTMLAttributes<HT"
  },
  {
    "path": "packages/editor/src/components/ui/controls/material-picker.tsx",
    "chars": 6389,
    "preview": "'use client'\n\nimport { DEFAULT_MATERIALS, type MaterialPreset, type MaterialSchema } from '@pascal-app/core'\nimport { us"
  },
  {
    "path": "packages/editor/src/components/ui/controls/metric-control.tsx",
    "chars": 8468,
    "preview": "'use client'\n\nimport { useScene } from '@pascal-app/core'\nimport { useViewer } from '@pascal-app/viewer'\nimport { useCal"
  },
  {
    "path": "packages/editor/src/components/ui/controls/panel-section.tsx",
    "chars": 1997,
    "preview": "'use client'\n\nimport { ChevronDown } from 'lucide-react'\nimport { AnimatePresence, motion } from 'motion/react'\nimport {"
  },
  {
    "path": "packages/editor/src/components/ui/controls/segmented-control.tsx",
    "chars": 1272,
    "preview": "'use client'\n\nimport { cn } from '../../../lib/utils'\n\ninterface SegmentedControlProps<T extends string> {\n  value: T\n  "
  },
  {
    "path": "packages/editor/src/components/ui/controls/slider-control.tsx",
    "chars": 9120,
    "preview": "'use client'\n\nimport { useScene } from '@pascal-app/core'\nimport { useCallback, useEffect, useRef, useState } from 'reac"
  },
  {
    "path": "packages/editor/src/components/ui/controls/toggle-control.tsx",
    "chars": 1164,
    "preview": "'use client'\n\nimport { Check } from 'lucide-react'\nimport { cn } from '../../../lib/utils'\n\ninterface ToggleControlProps"
  },
  {
    "path": "packages/editor/src/components/ui/floating-level-selector.tsx",
    "chars": 2897,
    "preview": "'use client'\n\nimport { type BuildingNode, type LevelNode, useScene } from '@pascal-app/core'\nimport { useViewer } from '"
  },
  {
    "path": "packages/editor/src/components/ui/helpers/ceiling-helper.tsx",
    "chars": 849,
    "preview": "import { ShortcutToken } from '../primitives/shortcut-token'\n\nexport function CeilingHelper() {\n  return (\n    <div clas"
  },
  {
    "path": "packages/editor/src/components/ui/helpers/helper-manager.tsx",
    "chars": 817,
    "preview": "'use client'\n\nimport useEditor from '../../../store/use-editor'\nimport { CeilingHelper } from './ceiling-helper'\nimport "
  },
  {
    "path": "packages/editor/src/components/ui/helpers/item-helper.tsx",
    "chars": 1529,
    "preview": "import { ShortcutToken } from '../primitives/shortcut-token'\n\ninterface ItemHelperProps {\n  showEsc?: boolean\n}\n\nexport "
  },
  {
    "path": "packages/editor/src/components/ui/helpers/roof-helper.tsx",
    "chars": 662,
    "preview": "import { ShortcutToken } from '../primitives/shortcut-token'\n\nexport function RoofHelper() {\n  return (\n    <div classNa"
  },
  {
    "path": "packages/editor/src/components/ui/helpers/slab-helper.tsx",
    "chars": 846,
    "preview": "import { ShortcutToken } from '../primitives/shortcut-token'\n\nexport function SlabHelper() {\n  return (\n    <div classNa"
  },
  {
    "path": "packages/editor/src/components/ui/helpers/wall-helper.tsx",
    "chars": 857,
    "preview": "import { ShortcutToken } from '../primitives/shortcut-token'\n\nexport function WallHelper() {\n  return (\n    <div classNa"
  },
  {
    "path": "packages/editor/src/components/ui/item-catalog/catalog-items.tsx",
    "chars": 37129,
    "preview": "import { type AssetInput, ItemNode } from '@pascal-app/core'\nexport const CATALOG_ITEMS: AssetInput[] = [\n  {\n    id: 't"
  },
  {
    "path": "packages/editor/src/components/ui/item-catalog/item-catalog.tsx",
    "chars": 8718,
    "preview": "'use client'\n\nimport type { AssetInput } from '@pascal-app/core'\nimport { resolveCdnUrl } from '@pascal-app/viewer'\nimpo"
  },
  {
    "path": "packages/editor/src/components/ui/panels/ceiling-panel.tsx",
    "chars": 8021,
    "preview": "'use client'\n\nimport { type AnyNode, type CeilingNode, type MaterialSchema, useScene } from '@pascal-app/core'\nimport { "
  },
  {
    "path": "packages/editor/src/components/ui/panels/collections/collections-popover.tsx",
    "chars": 15022,
    "preview": "'use client'\n\nimport type { AnyNodeId, Collection, CollectionId } from '@pascal-app/core'\nimport { useScene } from '@pas"
  },
  {
    "path": "packages/editor/src/components/ui/panels/door-panel.tsx",
    "chars": 20685,
    "preview": "'use client'\n\nimport { type AnyNode, type AnyNodeId, type MaterialSchema, DoorNode, emitter, useScene } from '@pascal-ap"
  },
  {
    "path": "packages/editor/src/components/ui/panels/item-panel.tsx",
    "chars": 10359,
    "preview": "'use client'\n\nimport { type AnyNode, getScaledDimensions, ItemNode, useScene } from '@pascal-app/core'\nimport { useViewe"
  },
  {
    "path": "packages/editor/src/components/ui/panels/panel-manager.tsx",
    "chars": 1825,
    "preview": "'use client'\n\nimport { type AnyNodeId, useScene } from '@pascal-app/core'\nimport { useViewer } from '@pascal-app/viewer'"
  },
  {
    "path": "packages/editor/src/components/ui/panels/panel-wrapper.tsx",
    "chars": 2557,
    "preview": "'use client'\n\nimport { ChevronLeft, RotateCcw, X } from 'lucide-react'\nimport Image from 'next/image'\nimport { cn } from"
  },
  {
    "path": "packages/editor/src/components/ui/panels/presets/presets-popover.tsx",
    "chars": 16984,
    "preview": "'use client'\n\nimport { emitter } from '@pascal-app/core'\nimport {\n  BookMarked,\n  Check,\n  Globe,\n  GlobeLock,\n  MoreHor"
  },
  {
    "path": "packages/editor/src/components/ui/panels/reference-panel.tsx",
    "chars": 5209,
    "preview": "'use client'\n\nimport { type AnyNode, type GuideNode, type ScanNode, useScene } from '@pascal-app/core'\nimport { Box, Ima"
  },
  {
    "path": "packages/editor/src/components/ui/panels/roof-panel.tsx",
    "chars": 8441,
    "preview": "'use client'\n\nimport {\n  type AnyNode,\n  type AnyNodeId,\n  type MaterialSchema,\n  type RoofNode,\n  RoofNode as RoofNodeS"
  },
  {
    "path": "packages/editor/src/components/ui/panels/roof-segment-panel.tsx",
    "chars": 9764,
    "preview": "'use client'\n\nimport {\n  type AnyNode,\n  type AnyNodeId,\n  type MaterialSchema,\n  type RoofSegmentNode,\n  RoofSegmentNod"
  },
  {
    "path": "packages/editor/src/components/ui/panels/slab-panel.tsx",
    "chars": 8126,
    "preview": "'use client'\n\nimport { type AnyNode, type MaterialSchema, type SlabNode, useScene } from '@pascal-app/core'\nimport { use"
  },
  {
    "path": "packages/editor/src/components/ui/panels/stair-panel.tsx",
    "chars": 9773,
    "preview": "'use client'\n\nimport {\n  type AnyNode,\n  type AnyNodeId,\n  type MaterialSchema,\n  type StairNode,\n  StairNode as StairNo"
  },
  {
    "path": "packages/editor/src/components/ui/panels/stair-segment-panel.tsx",
    "chars": 10491,
    "preview": "'use client'\n\nimport {\n  type AnyNode,\n  type AnyNodeId,\n  type AttachmentSide,\n  type MaterialSchema,\n  type StairSegme"
  },
  {
    "path": "packages/editor/src/components/ui/panels/wall-panel.tsx",
    "chars": 3348,
    "preview": "'use client'\n\nimport { type AnyNode, type AnyNodeId, type MaterialSchema, useScene, type WallNode } from '@pascal-app/co"
  },
  {
    "path": "packages/editor/src/components/ui/panels/window-panel.tsx",
    "chars": 14731,
    "preview": "'use client'\n\nimport { type AnyNode, type AnyNodeId, emitter, type MaterialSchema, useScene, WindowNode } from '@pascal-"
  },
  {
    "path": "packages/editor/src/components/ui/primitives/button.tsx",
    "chars": 2334,
    "preview": "import { Slot } from '@radix-ui/react-slot'\nimport { cva, type VariantProps } from 'class-variance-authority'\nimport typ"
  },
  {
    "path": "packages/editor/src/components/ui/primitives/card.tsx",
    "chars": 1930,
    "preview": "import type * as React from 'react'\n\nimport { cn } from '../../../lib/utils'\n\nfunction Card({ className, ...props }: Rea"
  },
  {
    "path": "packages/editor/src/components/ui/primitives/color-dot.tsx",
    "chars": 1846,
    "preview": "'use client'\n\nimport { useState } from 'react'\nimport { cn } from '../../../lib/utils'\nimport { Popover, PopoverContent,"
  },
  {
    "path": "packages/editor/src/components/ui/primitives/context-menu.tsx",
    "chars": 8261,
    "preview": "'use client'\n\nimport * as ContextMenuPrimitive from '@radix-ui/react-context-menu'\nimport { CheckIcon, ChevronRightIcon,"
  },
  {
    "path": "packages/editor/src/components/ui/primitives/dialog.tsx",
    "chars": 3960,
    "preview": "'use client'\n\nimport * as DialogPrimitive from '@radix-ui/react-dialog'\nimport { XIcon } from 'lucide-react'\nimport type"
  },
  {
    "path": "packages/editor/src/components/ui/primitives/dropdown-menu.tsx",
    "chars": 8346,
    "preview": "'use client'\n\nimport * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'\nimport { CheckIcon, ChevronRightIco"
  },
  {
    "path": "packages/editor/src/components/ui/primitives/error-boundary.tsx",
    "chars": 1371,
    "preview": "'use client'\n\nimport React, { Component, type ErrorInfo, type ReactNode } from 'react'\n\ninterface Props {\n  children?: R"
  },
  {
    "path": "packages/editor/src/components/ui/primitives/input.tsx",
    "chars": 975,
    "preview": "import type * as React from 'react'\n\nimport { cn } from '../../../lib/utils'\n\nfunction Input({ className, type, ...props"
  },
  {
    "path": "packages/editor/src/components/ui/primitives/number-input.tsx",
    "chars": 6634,
    "preview": "'use client'\n\nimport NumberFlow from '@number-flow/react'\nimport { useScene } from '@pascal-app/core'\nimport { useCallba"
  },
  {
    "path": "packages/editor/src/components/ui/primitives/opacity-control.tsx",
    "chars": 2551,
    "preview": "'use client'\n\nimport { Eye, EyeOff } from 'lucide-react'\nimport { useState } from 'react'\nimport { Button } from '../../"
  },
  {
    "path": "packages/editor/src/components/ui/primitives/popover.tsx",
    "chars": 1642,
    "preview": "'use client'\n\nimport * as PopoverPrimitive from '@radix-ui/react-popover'\nimport type * as React from 'react'\n\nimport { "
  },
  {
    "path": "packages/editor/src/components/ui/primitives/separator.tsx",
    "chars": 712,
    "preview": "'use client'\n\nimport * as SeparatorPrimitive from '@radix-ui/react-separator'\nimport type * as React from 'react'\n\nimpor"
  },
  {
    "path": "packages/editor/src/components/ui/primitives/sheet.tsx",
    "chars": 4094,
    "preview": "'use client'\n\nimport * as SheetPrimitive from '@radix-ui/react-dialog'\nimport { XIcon } from 'lucide-react'\nimport type "
  },
  {
    "path": "packages/editor/src/components/ui/primitives/shortcut-token.tsx",
    "chars": 1633,
    "preview": "import { Icon } from '@iconify/react'\nimport type * as React from 'react'\n\nimport { cn } from '../../../lib/utils'\n\ncons"
  },
  {
    "path": "packages/editor/src/components/ui/primitives/sidebar.tsx",
    "chars": 25939,
    "preview": "'use client'\n\nimport { Slot } from '@radix-ui/react-slot'\nimport { cva, type VariantProps } from 'class-variance-authori"
  }
]

// ... and 276 more files (download for full content)

About this extraction

This page contains the full source code of the pascalorg/editor GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 476 files (23.0 MB), approximately 469.2k tokens, and a symbol index with 1081 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!