@pascal-app/editor 1.0.3 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +11 -9
- package/src/components/editor/action-menu-button.tsx +83 -0
- package/src/components/editor/bake-thumbnail.tsx +315 -4
- package/src/components/editor/capture-level-picker.tsx +72 -0
- package/src/components/editor/custom-camera-controls.tsx +202 -57
- package/src/components/editor/delete-confirmation-dialog.tsx +297 -7
- package/src/components/editor/duplicate-node.ts +244 -0
- package/src/components/editor/editor-layout-mobile.tsx +36 -25
- package/src/components/editor/editor-layout-v2.tsx +131 -58
- package/src/components/editor/export-manager.tsx +34 -8
- package/src/components/editor/first-person/build-collider-world.test.ts +51 -0
- package/src/components/editor/first-person/build-collider-world.ts +6 -2
- package/src/components/editor/first-person/mechanism-targets.test.ts +75 -0
- package/src/components/editor/first-person/mechanism-targets.ts +36 -0
- package/src/components/editor/first-person-controls.tsx +205 -2
- package/src/components/editor/floating-action-menu.tsx +63 -207
- package/src/components/editor/floor-aware-camera-controls.test.ts +442 -0
- package/src/components/editor/floor-aware-camera-controls.ts +324 -0
- package/src/components/editor/floor-openings-overlay.tsx +243 -0
- package/src/components/editor/floor-region-controls.tsx +846 -0
- package/src/components/editor/floor-region-webgpu-materials.test.ts +12 -0
- package/src/components/editor/floor-surface.test.ts +165 -0
- package/src/components/editor/floor-surface.ts +182 -0
- package/src/components/editor/floorplan-background-selection.test.ts +11 -2
- package/src/components/editor/floorplan-background-selection.ts +1 -3
- package/src/components/editor/floorplan-panel.tsx +344 -342
- package/src/components/editor/footprint-height-handle.tsx +204 -0
- package/src/components/editor/group-actions.ts +54 -2
- package/src/components/editor/handles/handle-hit-priority.test.tsx +63 -0
- package/src/components/editor/handles/handle-hit-priority.tsx +37 -0
- package/src/components/editor/handles/swallow-next-click.test.ts +28 -0
- package/src/components/editor/handles/use-handle-drag.ts +269 -197
- package/src/components/editor/index.tsx +294 -169
- package/src/components/editor/join-walls-pill.tsx +44 -0
- package/src/components/editor/node-action-menu.tsx +68 -70
- package/src/components/editor/node-arrow-handles.tsx +25 -6
- package/src/components/editor/open-wall-ends-3d-layer.tsx +273 -0
- package/src/components/editor/open-wall-ends-hint.tsx +89 -0
- package/src/components/editor/quick-measurement-hud.tsx +3 -2
- package/src/components/editor/registry-action-contributions.test.tsx +63 -0
- package/src/components/editor/registry-action-contributions.tsx +80 -1
- package/src/components/editor/room-controls.tsx +847 -0
- package/src/components/editor/room-floor-highlight.tsx +128 -0
- package/src/components/editor/room-handles-2d.tsx +354 -0
- package/src/components/editor/room-handles.tsx +675 -0
- package/src/components/editor/room-highlight.tsx +225 -0
- package/src/components/editor/room-labels.test.ts +357 -0
- package/src/components/editor/room-labels.tsx +990 -0
- package/src/components/editor/room-transform-ghost.tsx +256 -0
- package/src/components/editor/room-webgpu-materials.test.ts +27 -0
- package/src/components/editor/selection-manager.tsx +451 -317
- package/src/components/editor/slab-hole-highlights.tsx +3 -0
- package/src/components/editor/snapshot-capture-overlay.tsx +4 -0
- package/src/components/editor/snapshot-capture.test.ts +52 -4
- package/src/components/editor/snapshot-capture.ts +30 -5
- package/src/components/editor/thumbnail-generator.test.ts +308 -0
- package/src/components/editor/thumbnail-generator.tsx +1035 -229
- package/src/components/editor/use-floorplan-background-placement.ts +31 -11
- package/src/components/editor/vector-edge-extractor.tsx +121 -0
- package/src/components/editor/views/host-view.tsx +61 -0
- package/src/components/editor/views/plugins-view.tsx +670 -0
- package/src/components/editor/views/scene-region.tsx +59 -0
- package/src/components/editor/views/use-editor-views.ts +57 -0
- package/src/components/editor/views/view-bar.tsx +220 -0
- package/src/components/editor/wall-justification.test.ts +140 -0
- package/src/components/editor/wall-move-side-handles.tsx +43 -142
- package/src/components/editor/wall-push-arrow.tsx +92 -0
- package/src/components/editor/wall-region-handles.tsx +509 -0
- package/src/components/editor/wall-snap-beacon-layer.tsx +3 -0
- package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +8 -4
- package/src/components/editor-2d/floorplan-open-wall-ends-layer.test.tsx +86 -0
- package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +290 -0
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +11 -1
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +113 -102
- package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +156 -0
- package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +278 -0
- package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +77 -0
- package/src/components/editor-2d/renderers/{floorplan-draft-wall-measurement.tsx → floorplan-draft-measurement.tsx} +8 -8
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +4 -1
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +38 -8
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +291 -227
- package/src/components/editor-2d/svg-paths.ts +12 -15
- package/src/components/procedural-items/inspector.tsx +40 -15
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +97 -14
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +18 -18
- package/src/components/systems/zone/zone-system.test.ts +47 -0
- package/src/components/systems/zone/zone-system.tsx +29 -68
- package/src/components/tools/item/placement-history.test.tsx +474 -0
- package/src/components/tools/item/placement-strategies.ts +8 -4
- package/src/components/tools/item/use-draft-node.test.tsx +432 -1
- package/src/components/tools/item/use-draft-node.ts +166 -95
- package/src/components/tools/item/use-placement-coordinator.tsx +93 -46
- package/src/components/tools/paint-region/use-paint-tint.ts +37 -0
- package/src/components/tools/paint-region/wall-paint-region-tool.tsx +466 -0
- package/src/components/tools/registry/move-registry-node-tool.tsx +3 -1
- package/src/components/tools/select/box-select-tool.tsx +2 -12
- package/src/components/tools/select/plane-box-select-tool.test.tsx +79 -0
- package/src/components/tools/select/plane-box-select-tool.tsx +35 -22
- package/src/components/tools/select/select-candidates.test.ts +37 -4
- package/src/components/tools/select/select-candidates.ts +15 -11
- package/src/components/tools/shared/cursor-sphere.tsx +36 -12
- package/src/components/tools/site/site-boundary-editor.tsx +1 -2
- package/src/components/tools/stair/stair-defaults.ts +0 -3
- package/src/components/tools/stair/stair-tool.tsx +90 -42
- package/src/components/tools/tool-manager.tsx +46 -20
- package/src/components/tools/wall/wall-drafting.test.ts +19 -4
- package/src/components/tools/zone/zone-tool.tsx +4 -12
- package/src/components/ui/action-menu/action-button.tsx +3 -0
- package/src/components/ui/action-menu/camera-actions.tsx +2 -1
- package/src/components/ui/action-menu/control-modes.tsx +2 -35
- package/src/components/ui/action-menu/measurement-control.tsx +3 -10
- package/src/components/ui/build-panel/build-panel.tsx +235 -0
- package/src/components/ui/command-palette/editor-commands.tsx +4 -30
- package/src/components/ui/command-palette/index.tsx +5 -24
- package/src/components/ui/controls/finish-swatch.tsx +110 -0
- package/src/components/ui/controls/material-paint-panel.tsx +97 -26
- package/src/components/ui/controls/material-picker.tsx +4 -1
- package/src/components/ui/controls/scene-material-list.tsx +4 -32
- package/src/components/ui/controls/select-control.tsx +65 -0
- package/src/components/ui/floating-level-selector.tsx +9 -14
- package/src/components/ui/helpers/building-helper.tsx +4 -1
- package/src/components/ui/helpers/contextual-helper-panel.tsx +154 -70
- package/src/components/ui/helpers/helper-manager.test.tsx +69 -0
- package/src/components/ui/helpers/helper-manager.tsx +325 -14
- package/src/components/ui/helpers/item-helper.tsx +8 -0
- package/src/components/ui/helpers/registered-tool-helper.tsx +8 -1
- package/src/components/ui/icon-ref.tsx +45 -1
- package/src/components/ui/level-duplicate-dialog.tsx +1 -1
- package/src/components/ui/node-icon-image.tsx +16 -0
- package/src/components/ui/panels/collections/collections-popover.tsx +20 -14
- package/src/components/ui/panels/floor-foundation-panel.tsx +551 -0
- package/src/components/ui/panels/floor-opening-panel.tsx +143 -0
- package/src/components/ui/panels/mobile-panel-sheet.tsx +2 -2
- package/src/components/ui/panels/mobile-selection-bar.tsx +2 -2
- package/src/components/ui/panels/multi-field-value.test.ts +26 -3
- package/src/components/ui/panels/multi-field-value.ts +6 -3
- package/src/components/ui/panels/multi-parametric-inspector.tsx +52 -2
- package/src/components/ui/panels/multi-selection-panel.tsx +43 -4
- package/src/components/ui/panels/node-display.ts +8 -3
- package/src/components/ui/panels/panel-manager.tsx +103 -68
- package/src/components/ui/panels/panel-wrapper.tsx +78 -36
- package/src/components/ui/panels/parametric-field-control.tsx +1 -1
- package/src/components/ui/panels/parametric-inspector.tsx +4 -4
- package/src/components/ui/panels/reference-panel.tsx +2 -3
- package/src/components/ui/panels/room-construction-rows.tsx +774 -0
- package/src/components/ui/panels/room-name-options.test.ts +55 -0
- package/src/components/ui/panels/room-name-options.ts +47 -0
- package/src/components/ui/panels/room-openings-field.tsx +110 -0
- package/src/components/ui/panels/room-panel.tsx +725 -0
- package/src/components/ui/panels/shape-choice.tsx +58 -0
- package/src/components/ui/panels/wall-region-list.tsx +109 -0
- package/src/components/ui/primitives/color-dot.tsx +36 -24
- package/src/components/ui/primitives/sidebar.tsx +2 -0
- package/src/components/ui/right-stack.test.ts +29 -0
- package/src/components/ui/right-stack.tsx +145 -0
- package/src/components/ui/sidebar/icon-rail.tsx +5 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +31 -4
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +5 -5
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -4
- package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +197 -0
- package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +24 -11
- package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +176 -211
- package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +30 -8
- package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +1 -3
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +4 -1
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +1 -3
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +1 -4
- package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +1 -3
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +21 -13
- package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +0 -27
- package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -3
- package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +9 -4
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +0 -2
- package/src/components/ui/sidebar/tab-bar.tsx +10 -0
- package/src/components/ui/sidebar/use-plugin-panels.tsx +15 -5
- package/src/components/viewer/floorplan-compass-button.tsx +9 -1
- package/src/components/viewer/floorplan-preview.tsx +5 -6
- package/src/components/viewer/viewer-hover-label.tsx +34 -0
- package/src/components/viewer/viewer-rooms.tsx +175 -0
- package/src/components/viewer/viewer-scene-header.tsx +181 -145
- package/src/components/viewer/viewer-selection-manager.tsx +124 -0
- package/src/components/viewer/viewer-stage.tsx +2 -2
- package/src/components/viewer-zone-system.tsx +3 -8
- package/src/hooks/use-auto-save.integration.test.tsx +123 -9
- package/src/hooks/use-auto-save.ts +34 -8
- package/src/hooks/use-keyboard.test.ts +178 -3
- package/src/hooks/use-keyboard.ts +89 -71
- package/src/hooks/use-open-wall-ends.test.ts +99 -0
- package/src/hooks/use-open-wall-ends.ts +238 -0
- package/src/hooks/use-placement-notice.test.ts +48 -0
- package/src/hooks/use-placement-notice.ts +42 -0
- package/src/hooks/use-selected-room.ts +143 -0
- package/src/index.tsx +246 -39
- package/src/lib/ceiling-edit-session.test.ts +321 -0
- package/src/lib/ceiling-edit-session.ts +206 -0
- package/src/lib/ceiling-region-session.test.ts +179 -0
- package/src/lib/continuation.test.ts +19 -1
- package/src/lib/continuation.ts +25 -4
- package/src/lib/door-interaction.ts +14 -0
- package/src/lib/editor-level-display.test.ts +96 -0
- package/src/lib/editor-level-display.ts +48 -0
- package/src/lib/editor-views.ts +144 -0
- package/src/lib/export-geometry-wait.test.ts +90 -0
- package/src/lib/floor-edit-session.ts +153 -0
- package/src/lib/floor-footprints.test.ts +563 -0
- package/src/lib/floor-footprints.ts +395 -0
- package/src/lib/floor-foundation-support.test.ts +283 -0
- package/src/lib/floor-opening-draft.ts +218 -0
- package/src/lib/floor-openings.test.ts +216 -0
- package/src/lib/floor-region-geometry.ts +191 -0
- package/src/lib/floor-region-session.test.ts +485 -0
- package/src/lib/floor-region-session.ts +589 -0
- package/src/lib/floor-region-snap.ts +101 -0
- package/src/lib/floor-step-hover.test.ts +32 -0
- package/src/lib/floorplan/annotation-visibility.ts +17 -0
- package/src/lib/floorplan/dimension-drive.test.ts +360 -0
- package/src/lib/floorplan/dimension-drive.ts +479 -0
- package/src/lib/floorplan/floorplan-export.test.ts +12 -8
- package/src/lib/floorplan/floorplan-export.tsx +32 -8
- package/src/lib/floorplan/floorplan-extension.ts +32 -0
- package/src/lib/floorplan/floorplan-mode.test.ts +4 -0
- package/src/lib/floorplan/floorplan-mode.ts +2 -0
- package/src/lib/floorplan/floorplan-pdfkit-document.ts +339 -18
- package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +138 -8
- package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +56 -25
- package/src/lib/floorplan/floorplan-plugin-install.test.tsx +138 -0
- package/src/lib/floorplan/floorplan-readonly.ts +6 -0
- package/src/lib/floorplan/geometry.ts +13 -6
- package/src/lib/floorplan/index.ts +0 -9
- package/src/lib/floorplan/items.ts +5 -2
- package/src/lib/floorplan/marks.test.ts +213 -0
- package/src/lib/floorplan/marks.ts +238 -0
- package/src/lib/floorplan/open-wall-ends.test.ts +268 -0
- package/src/lib/floorplan/open-wall-ends.ts +162 -0
- package/src/lib/floorplan/pdf-fonts/GeistMono-Bold.ttf +0 -0
- package/src/lib/floorplan/pdf-fonts/GeistMono-Regular.ttf +0 -0
- package/src/lib/floorplan/pdf-fonts/LICENSE-GeistMono.txt +92 -0
- package/src/lib/floorplan/pdf-fonts/LICENSE-LiberationSans.txt +102 -0
- package/src/lib/floorplan/pdf-fonts/LiberationSans-Bold.ttf +0 -0
- package/src/lib/floorplan/pdf-fonts/LiberationSans-Regular.ttf +0 -0
- package/src/lib/floorplan/schedules.test.ts +189 -0
- package/src/lib/floorplan/schedules.ts +339 -0
- package/src/lib/floorplan/sheet-export.test.ts +100 -0
- package/src/lib/floorplan/sheet-export.ts +318 -0
- package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +779 -0
- package/src/lib/floorplan/site-plan/contours.test.ts +43 -0
- package/src/lib/floorplan/site-plan/contributors.ts +97 -0
- package/src/lib/floorplan/site-plan/coverage.ts +232 -0
- package/src/lib/floorplan/site-plan/drawing-type-switch.test.ts +15 -0
- package/src/lib/floorplan/site-plan/drawing-type-switch.tsx +86 -0
- package/src/lib/floorplan/site-plan/front-edge.test.ts +103 -0
- package/src/lib/floorplan/site-plan/front-edge.ts +156 -0
- package/src/lib/floorplan/site-plan/geometry.test.ts +275 -0
- package/src/lib/floorplan/site-plan/geometry.ts +557 -0
- package/src/lib/floorplan/site-plan/index.ts +71 -0
- package/src/lib/floorplan/site-plan/site-annotations.test.ts +366 -0
- package/src/lib/floorplan/site-plan/site-annotations.ts +1083 -0
- package/src/lib/floorplan/site-plan/site-parts.ts +379 -0
- package/src/lib/floorplan/site-plan/site-plan-layer.tsx +138 -0
- package/src/lib/floorplan/site-plan/street-name.test.ts +69 -0
- package/src/lib/floorplan/site-plan/street-name.ts +302 -0
- package/src/lib/floorplan/site-plan/yards.test.ts +69 -0
- package/src/lib/floorplan/types.ts +1 -27
- package/src/lib/fresh-planar-placement.test.ts +36 -0
- package/src/lib/fresh-planar-placement.ts +59 -36
- package/src/lib/geometry-script/author.ts +152 -0
- package/src/lib/geometry-script/client.ts +152 -0
- package/src/lib/geometry-script/protocol.ts +12 -0
- package/src/lib/gesture-lifecycle.test.ts +595 -0
- package/src/lib/gesture-lifecycle.ts +290 -0
- package/src/lib/glb-export-zone-holes.test.ts +41 -0
- package/src/lib/glb-export.test.ts +485 -0
- package/src/lib/glb-export.ts +237 -119
- package/src/lib/history.test.ts +6 -0
- package/src/lib/history.ts +7 -0
- package/src/lib/hud-title.ts +109 -0
- package/src/lib/hydration-gated-storage.ts +49 -0
- package/src/lib/ifc-export.test.ts +174 -0
- package/src/lib/ifc-export.ts +201 -0
- package/src/lib/ifc-parts.ts +15 -0
- package/src/lib/inspector-expanded.test.ts +44 -0
- package/src/lib/inspector-expanded.ts +36 -0
- package/src/lib/interaction/scope.ts +18 -0
- package/src/lib/keyboard-pan.test.ts +129 -7
- package/src/lib/keyboard-pan.ts +161 -12
- package/src/lib/live-gesture-round-trip.test.ts +360 -0
- package/src/lib/lot/__fixtures__/map-dossier-miami-shores.json +1 -0
- package/src/lib/lot/__fixtures__/map-dossier-stpete.json +1 -0
- package/src/lib/lot/clean-ring.test.ts +186 -0
- package/src/lib/lot/clean-ring.ts +217 -0
- package/src/lib/lot/dossier.test.ts +202 -0
- package/src/lib/lot/dossier.ts +498 -0
- package/src/lib/lot/drop-in.test.ts +326 -0
- package/src/lib/lot/drop-in.ts +407 -0
- package/src/lib/lot/index.ts +43 -0
- package/src/lib/lot/lot-patch.test.ts +340 -0
- package/src/lib/lot/lot-patch.ts +394 -0
- package/src/lib/lot/parcel-provider.ts +43 -0
- package/src/lib/lot/terrain.test.ts +161 -0
- package/src/lib/lot/terrain.ts +266 -0
- package/src/lib/material-catalog-model.ts +3 -1
- package/src/lib/material-paint-panel-model.ts +9 -5
- package/src/lib/material-paint.ts +4 -0
- package/src/lib/mezzanine-actions.test.ts +342 -0
- package/src/lib/mezzanine-draft.test.ts +501 -0
- package/src/lib/mezzanine-draft.ts +278 -0
- package/src/lib/mezzanine-messages.ts +26 -0
- package/src/lib/mezzanine-selection.test.ts +207 -0
- package/src/lib/mezzanine-stairs.ts +46 -0
- package/src/lib/model-export.ts +3 -1
- package/src/lib/node-action-movement.ts +17 -6
- package/src/lib/node-display-label.ts +37 -0
- package/src/lib/orbit-floor-clearance.test.ts +165 -0
- package/src/lib/orbit-floor-clearance.ts +106 -0
- package/src/lib/paint-commit-route.test.ts +37 -0
- package/src/lib/paint-commit-route.ts +17 -0
- package/src/lib/paint-eyedropper.test.tsx +165 -0
- package/src/lib/paint-eyedropper.ts +99 -0
- package/src/lib/paint-pass-through.test.ts +111 -0
- package/src/lib/paint-pass-through.ts +76 -0
- package/src/lib/paint-region-hover.ts +25 -0
- package/src/lib/paint-region-mode.ts +154 -0
- package/src/lib/paint-regions.test.ts +166 -0
- package/src/lib/paint-regions.ts +192 -0
- package/src/lib/paint-scope.test.ts +317 -35
- package/src/lib/paint-scope.ts +266 -25
- package/src/lib/panel-rows.ts +21 -0
- package/src/lib/parametric-node-update.ts +1 -1
- package/src/lib/plate-paint-affected.test.ts +300 -0
- package/src/lib/plate-paint-affected.ts +177 -0
- package/src/lib/plugin-chat.ts +50 -0
- package/src/lib/plugin-panels.test.ts +25 -0
- package/src/lib/plugin-panels.ts +94 -0
- package/src/lib/print-shell-compiler.ts +1 -0
- package/src/lib/registered-draft-snap.ts +12 -0
- package/src/lib/roof-duplication.ts +10 -2
- package/src/lib/room-assembly-overlay.test.ts +275 -0
- package/src/lib/room-assembly-overlay.ts +266 -0
- package/src/lib/room-built-on.test.ts +379 -0
- package/src/lib/room-built-on.ts +138 -0
- package/src/lib/room-construction-commands.ts +712 -0
- package/src/lib/room-construction.test.ts +746 -0
- package/src/lib/room-construction.ts +385 -0
- package/src/lib/room-controls-phase6.test.ts +630 -0
- package/src/lib/room-divide-session.ts +523 -0
- package/src/lib/room-handle-drag.ts +1068 -0
- package/src/lib/room-name-catalog.test.ts +90 -0
- package/src/lib/room-name-catalog.ts +168 -0
- package/src/lib/room-push-dimensions.test.ts +311 -0
- package/src/lib/room-push-dimensions.ts +251 -0
- package/src/lib/room-selection-commands.ts +62 -0
- package/src/lib/room-selection.test.ts +973 -0
- package/src/lib/room-selection.ts +417 -0
- package/src/lib/room-structure-commands.ts +94 -0
- package/src/lib/room-structure.test.ts +921 -0
- package/src/lib/room-transform-session.ts +570 -0
- package/src/lib/room-zone-routing.test.ts +294 -0
- package/src/lib/room-zone-routing.ts +197 -0
- package/src/lib/scene-clipboard.test.ts +364 -0
- package/src/lib/scene-clipboard.ts +229 -48
- package/src/lib/scene-material-refs.test.ts +79 -0
- package/src/lib/scene-material-refs.ts +79 -0
- package/src/lib/scene.test.ts +24 -36
- package/src/lib/scene.ts +33 -57
- package/src/lib/selection-routing.test.ts +129 -45
- package/src/lib/selection-routing.ts +53 -74
- package/src/lib/session-writes.test.ts +120 -0
- package/src/lib/session-writes.ts +205 -0
- package/src/lib/sidebar-panel.ts +62 -0
- package/src/lib/site-boundary.test.ts +4 -6
- package/src/lib/site-boundary.ts +1 -1
- package/src/lib/snapping-mode.test.ts +4 -0
- package/src/lib/snapping-mode.ts +8 -1
- package/src/lib/terrace-draft.test.ts +145 -0
- package/src/lib/terrace-draft.ts +213 -0
- package/src/lib/tool-cursor-icon.test.ts +16 -0
- package/src/lib/tool-cursor-icon.ts +11 -0
- package/src/lib/typing-target.ts +9 -0
- package/src/lib/units.ts +77 -39
- package/src/lib/vector-edges.test.ts +58 -0
- package/src/lib/vector-edges.ts +400 -0
- package/src/lib/viewer-selection.test.ts +577 -0
- package/src/lib/viewer-selection.ts +569 -0
- package/src/lib/wall-draw-variant.test.ts +71 -0
- package/src/lib/wall-draw-variant.ts +91 -0
- package/src/lib/wall-paint-region-session.ts +246 -0
- package/src/lib/wall-paint-region.test.ts +373 -0
- package/src/lib/wall-polygon-draft.test.ts +262 -0
- package/src/lib/wall-polygon-draft.ts +286 -0
- package/src/lib/wall-push.test.ts +453 -0
- package/src/lib/wall-region-face.ts +352 -0
- package/src/lib/wall-region-handles.test.ts +304 -0
- package/src/lib/wall-region-handles.ts +368 -0
- package/src/lib/wall-region-list.test.ts +119 -0
- package/src/lib/wall-region-list.ts +136 -0
- package/src/lib/wall-region-snap.ts +110 -0
- package/src/lib/window-interaction.ts +14 -0
- package/src/lib/zone-content.ts +4 -1
- package/src/lib/zone-name.test.ts +17 -0
- package/src/lib/zone-name.ts +17 -0
- package/src/store/opens-in-select-mode.test.ts +223 -0
- package/src/store/paint-mode.test.ts +188 -0
- package/src/store/terrain-sculpt-mode.test.ts +19 -21
- package/src/store/tool-mode.test.ts +137 -6
- package/src/store/use-delete-confirmation.ts +28 -1
- package/src/store/use-drawing-view.test.ts +24 -3
- package/src/store/use-drawing-view.ts +29 -1
- package/src/store/use-editor.tsx +432 -321
- package/src/store/use-floorplan-draft-preview.ts +13 -0
- package/src/store/use-floorplan-marquee.ts +12 -2
- package/src/store/use-quick-measurement-hud.ts +3 -3
- package/src/store/use-stair-build-preview.ts +13 -5
- package/src/store/use-wall-move-ghosts.ts +4 -2
- package/src/store/use-wall-region-handles-selection.test.ts +222 -0
- package/src/store/use-wall-region-handles-selection.ts +139 -0
- package/src/store/view-layout.test.ts +175 -0
- package/src/store/view-layout.ts +196 -0
- package/src/test-utils/immediate-animation-frames.ts +16 -0
- package/src/test-utils/live-gesture-round-trip.ts +224 -0
- package/src/test-utils/selection-harness.ts +82 -0
- package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +0 -499
- package/src/components/systems/zone/zone-label-editor-system.tsx +0 -333
- package/src/components/tools/fence/fence-drafting.test.ts +0 -100
- package/src/components/tools/fence/fence-drafting.ts +0 -266
- package/src/components/ui/sidebar/panels/plugins-panel.tsx +0 -218
- package/src/components/viewer/viewer-units-panel.tsx +0 -116
- package/src/lib/floorplan/stairs.ts +0 -489
- package/src/lib/level-duplication.test.ts +0 -144
- package/src/lib/level-duplication.ts +0 -195
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type AnyNode,
|
|
3
|
+
type AnyNodeId,
|
|
4
|
+
ceilingRegionsOwner,
|
|
5
|
+
generateId,
|
|
6
|
+
generateSceneMaterialId,
|
|
7
|
+
type MaterialSchema,
|
|
8
|
+
type SceneMaterial,
|
|
9
|
+
type SceneMaterialId,
|
|
10
|
+
toSceneMaterialRef,
|
|
11
|
+
useScene,
|
|
12
|
+
WALL_FACE_REGION_LIMIT,
|
|
13
|
+
type WallFace,
|
|
14
|
+
type WallFaceRegion,
|
|
15
|
+
} from '@pascal-app/core'
|
|
16
|
+
|
|
17
|
+
// Data writes for the paint tool's region sub-modes: a wall face region
|
|
18
|
+
// (`wall.faceRegions`), a room floor region (`zone.floor.regions`) or a ceiling
|
|
19
|
+
// region (`zone.ceiling.regions` for a room's ceiling, `ceiling.regions` for a
|
|
20
|
+
// manual one). Every
|
|
21
|
+
// write is one scene update, so one undo step; a one-off colour mints one
|
|
22
|
+
// shared scene material, the same way the paint fan-out does.
|
|
23
|
+
|
|
24
|
+
export const WALL_REGION_CAP_MESSAGE = `Up to ${WALL_FACE_REGION_LIMIT} regions per wall face`
|
|
25
|
+
|
|
26
|
+
type Resolved = { ref: string; newSceneMaterial: SceneMaterial | null }
|
|
27
|
+
|
|
28
|
+
function sameMaterial(a: unknown, b: unknown) {
|
|
29
|
+
return JSON.stringify(a) === JSON.stringify(b)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The finish ref for the paint material, minting a scene material when it is a one-off colour. */
|
|
33
|
+
export function resolvePaintRegionFinish(
|
|
34
|
+
materials: Record<SceneMaterialId, SceneMaterial>,
|
|
35
|
+
material: MaterialSchema | undefined,
|
|
36
|
+
materialPreset: string | undefined,
|
|
37
|
+
): Resolved | null {
|
|
38
|
+
if (materialPreset) return { ref: materialPreset, newSceneMaterial: null }
|
|
39
|
+
if (!material) return null
|
|
40
|
+
const existing = Object.values(materials).find((scene) => sameMaterial(scene.material, material))
|
|
41
|
+
if (existing) return { ref: toSceneMaterialRef(existing.id), newSceneMaterial: null }
|
|
42
|
+
const id = generateSceneMaterialId()
|
|
43
|
+
return {
|
|
44
|
+
ref: toSceneMaterialRef(id),
|
|
45
|
+
newSceneMaterial: { id, name: `Material ${Object.keys(materials).length + 1}`, material },
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export type WallRegionBounds = Pick<WallFaceRegion, 'u0' | 'u1' | 'v0' | 'v1'>
|
|
50
|
+
export type RegionWriteResult = { ok: true; id: string } | { ok: false; message: string }
|
|
51
|
+
|
|
52
|
+
/** Whether a face can take another region. */
|
|
53
|
+
export function wallFaceHasRoom(
|
|
54
|
+
wall: { faceRegions?: WallFaceRegion[] } | undefined,
|
|
55
|
+
face: WallFace,
|
|
56
|
+
): boolean {
|
|
57
|
+
return (
|
|
58
|
+
(wall?.faceRegions ?? []).filter((region) => region.face === face).length <
|
|
59
|
+
WALL_FACE_REGION_LIMIT
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Adds a painted region to a wall face (later regions win). */
|
|
64
|
+
export function addWallRegion(
|
|
65
|
+
wallId: string,
|
|
66
|
+
face: WallFace,
|
|
67
|
+
bounds: WallRegionBounds,
|
|
68
|
+
paint: { material?: MaterialSchema; materialPreset?: string },
|
|
69
|
+
): RegionWriteResult {
|
|
70
|
+
const state = useScene.getState()
|
|
71
|
+
const wall = state.nodes[wallId as AnyNodeId]
|
|
72
|
+
if (wall?.type !== 'wall') return { ok: false, message: 'Select a wall' }
|
|
73
|
+
if (!wallFaceHasRoom(wall, face)) return { ok: false, message: WALL_REGION_CAP_MESSAGE }
|
|
74
|
+
const resolved = resolvePaintRegionFinish(state.materials, paint.material, paint.materialPreset)
|
|
75
|
+
if (!resolved) return { ok: false, message: 'Pick a material first' }
|
|
76
|
+
const id = generateId('region')
|
|
77
|
+
const region: WallFaceRegion = {
|
|
78
|
+
id,
|
|
79
|
+
face,
|
|
80
|
+
...Object.fromEntries(Object.entries(bounds).filter(([, value]) => value !== undefined)),
|
|
81
|
+
finish: resolved.ref,
|
|
82
|
+
}
|
|
83
|
+
writeNode(
|
|
84
|
+
wall.id,
|
|
85
|
+
{ faceRegions: [...(wall.faceRegions ?? []), region] },
|
|
86
|
+
resolved.newSceneMaterial,
|
|
87
|
+
)
|
|
88
|
+
return { ok: true, id }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Replaces a region's bounds (absent = to the edge). */
|
|
92
|
+
export function updateWallRegion(wallId: string, regionId: string, bounds: WallRegionBounds) {
|
|
93
|
+
const wall = useScene.getState().nodes[wallId as AnyNodeId]
|
|
94
|
+
if (wall?.type !== 'wall') return false
|
|
95
|
+
const regions = wall.faceRegions ?? []
|
|
96
|
+
if (!regions.some((region) => region.id === regionId)) return false
|
|
97
|
+
writeNode(wall.id, {
|
|
98
|
+
faceRegions: regions.map((region) => {
|
|
99
|
+
if (region.id !== regionId) return region
|
|
100
|
+
const { u0: _u0, u1: _u1, v0: _v0, v1: _v1, ...rest } = region
|
|
101
|
+
return {
|
|
102
|
+
...rest,
|
|
103
|
+
...Object.fromEntries(Object.entries(bounds).filter(([, value]) => value !== undefined)),
|
|
104
|
+
} as WallFaceRegion
|
|
105
|
+
}),
|
|
106
|
+
})
|
|
107
|
+
return true
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function removeWallRegion(wallId: string, regionId: string) {
|
|
111
|
+
const wall = useScene.getState().nodes[wallId as AnyNodeId]
|
|
112
|
+
if (wall?.type !== 'wall') return false
|
|
113
|
+
const regions = wall.faceRegions ?? []
|
|
114
|
+
if (!regions.some((region) => region.id === regionId)) return false
|
|
115
|
+
const next = regions.filter((region) => region.id !== regionId)
|
|
116
|
+
writeNode(wall.id, { faceRegions: next.length ? next : undefined })
|
|
117
|
+
return true
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Adds a painted region to a room floor (a polygon in level XZ, later wins). */
|
|
121
|
+
export function addFloorRegion(
|
|
122
|
+
zoneId: string,
|
|
123
|
+
polygon: [number, number][],
|
|
124
|
+
paint: { material?: MaterialSchema; materialPreset?: string },
|
|
125
|
+
): RegionWriteResult {
|
|
126
|
+
const state = useScene.getState()
|
|
127
|
+
const zone = state.nodes[zoneId as AnyNodeId]
|
|
128
|
+
if (zone?.type !== 'zone') return { ok: false, message: 'Select a room' }
|
|
129
|
+
if (polygon.length < 3) return { ok: false, message: 'Draw at least three points' }
|
|
130
|
+
const resolved = resolvePaintRegionFinish(state.materials, paint.material, paint.materialPreset)
|
|
131
|
+
if (!resolved) return { ok: false, message: 'Pick a material first' }
|
|
132
|
+
const id = generateId('region')
|
|
133
|
+
const floor = { ...zone.floor }
|
|
134
|
+
floor.regions = [...(floor.regions ?? []), { id, polygon, finish: resolved.ref }]
|
|
135
|
+
writeNode(zone.id, { floor }, resolved.newSceneMaterial)
|
|
136
|
+
return { ok: true, id }
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Adds a painted region to a ceiling's underside (a polygon in level XZ, later
|
|
141
|
+
* wins). An automatic ceiling is rebuilt from its room, so its room keeps it.
|
|
142
|
+
*/
|
|
143
|
+
export function addCeilingRegion(
|
|
144
|
+
ceilingId: string,
|
|
145
|
+
polygon: [number, number][],
|
|
146
|
+
paint: { material?: MaterialSchema; materialPreset?: string },
|
|
147
|
+
): RegionWriteResult {
|
|
148
|
+
const state = useScene.getState()
|
|
149
|
+
const ceiling = state.nodes[ceilingId as AnyNodeId]
|
|
150
|
+
if (ceiling?.type !== 'ceiling') return { ok: false, message: 'Select a ceiling' }
|
|
151
|
+
if (polygon.length < 3) return { ok: false, message: 'Draw at least three points' }
|
|
152
|
+
const resolved = resolvePaintRegionFinish(state.materials, paint.material, paint.materialPreset)
|
|
153
|
+
if (!resolved) return { ok: false, message: 'Pick a material first' }
|
|
154
|
+
const owner = ceilingRegionsOwner(ceiling)
|
|
155
|
+
const node = state.nodes[owner.id as AnyNodeId]
|
|
156
|
+
const id = generateId('region')
|
|
157
|
+
const region = { id, polygon, finish: resolved.ref }
|
|
158
|
+
if (node?.type === 'zone')
|
|
159
|
+
writeNode(
|
|
160
|
+
node.id,
|
|
161
|
+
{ ceiling: { ...node.ceiling, regions: [...(node.ceiling?.regions ?? []), region] } },
|
|
162
|
+
resolved.newSceneMaterial,
|
|
163
|
+
)
|
|
164
|
+
else if (node?.type === 'ceiling')
|
|
165
|
+
writeNode(node.id, { regions: [...(node.regions ?? []), region] }, resolved.newSceneMaterial)
|
|
166
|
+
else return { ok: false, message: 'Select a room' }
|
|
167
|
+
useScene.getState().markDirty(ceiling.id as AnyNodeId)
|
|
168
|
+
return { ok: true, id }
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function writeNode(id: string, data: Partial<AnyNode>, newSceneMaterial?: SceneMaterial | null) {
|
|
172
|
+
useScene.setState((current) => {
|
|
173
|
+
if (current.readOnly) return current
|
|
174
|
+
const node = current.nodes[id as AnyNodeId]
|
|
175
|
+
if (!node) return current
|
|
176
|
+
const next = { ...node, ...data } as Record<string, unknown>
|
|
177
|
+
for (const [key, value] of Object.entries(data)) if (value === undefined) delete next[key]
|
|
178
|
+
const nodes = { ...current.nodes, [id]: next as AnyNode }
|
|
179
|
+
return {
|
|
180
|
+
nodes,
|
|
181
|
+
...(newSceneMaterial
|
|
182
|
+
? {
|
|
183
|
+
materials: {
|
|
184
|
+
...current.materials,
|
|
185
|
+
[newSceneMaterial.id as SceneMaterialId]: newSceneMaterial,
|
|
186
|
+
},
|
|
187
|
+
}
|
|
188
|
+
: {}),
|
|
189
|
+
}
|
|
190
|
+
})
|
|
191
|
+
useScene.getState().markDirty(id as AnyNodeId)
|
|
192
|
+
}
|
|
@@ -1,18 +1,30 @@
|
|
|
1
1
|
import { describe, expect, it } from 'bun:test'
|
|
2
2
|
import {
|
|
3
3
|
type AnyNode,
|
|
4
|
+
type AnyNodeDefinition,
|
|
5
|
+
type AnyNodeId,
|
|
4
6
|
detectSpacesForLevel,
|
|
5
7
|
type ItemNode,
|
|
8
|
+
nodeRegistry,
|
|
9
|
+
registerNode,
|
|
6
10
|
type SlabNode,
|
|
7
11
|
type Space,
|
|
12
|
+
useScene,
|
|
8
13
|
type WallNode,
|
|
14
|
+
type ZoneNode,
|
|
9
15
|
} from '@pascal-app/core'
|
|
16
|
+
import { z } from 'zod'
|
|
10
17
|
import {
|
|
11
18
|
availablePaintScopes,
|
|
19
|
+
commitPaintScopeFanout,
|
|
12
20
|
cyclePaintScope,
|
|
21
|
+
effectivePaintScope,
|
|
13
22
|
type PaintHoverInfo,
|
|
14
23
|
type PaintScope,
|
|
24
|
+
paintHoverInfo,
|
|
15
25
|
paintScopeLabel,
|
|
26
|
+
paintScopeRole,
|
|
27
|
+
paintSurfaceLabel,
|
|
16
28
|
resolvePaintScopeTargets,
|
|
17
29
|
type WallPaintHit,
|
|
18
30
|
} from './paint-scope'
|
|
@@ -188,51 +200,43 @@ describe('resolvePaintScopeTargets', () => {
|
|
|
188
200
|
|
|
189
201
|
const leftRoom = resolve({
|
|
190
202
|
node: shared,
|
|
191
|
-
role: '
|
|
203
|
+
role: 'a',
|
|
192
204
|
scope: 'room',
|
|
193
205
|
nodes: walls,
|
|
194
206
|
spaces,
|
|
195
207
|
wallHit: { face: 'front', point: [3.9, 2] },
|
|
196
208
|
})
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
'shared:interior',
|
|
201
|
-
'top-left:interior',
|
|
202
|
-
])
|
|
209
|
+
// Without zones the detected space drives the spread; each wall paints the
|
|
210
|
+
// physical face it turns to the room.
|
|
211
|
+
expect(keys(leftRoom).sort()).toEqual(['bottom-left:a', 'left:a', 'shared:a', 'top-left:a'])
|
|
203
212
|
|
|
204
213
|
const rightRoom = resolve({
|
|
205
214
|
node: shared,
|
|
206
|
-
role: '
|
|
215
|
+
role: 'b',
|
|
207
216
|
scope: 'room',
|
|
208
217
|
nodes: walls,
|
|
209
218
|
spaces,
|
|
210
219
|
wallHit: { face: 'back', point: [4.1, 2] },
|
|
211
220
|
})
|
|
212
|
-
expect(keys(rightRoom).sort()).toEqual([
|
|
213
|
-
'bottom-right:interior',
|
|
214
|
-
'right:interior',
|
|
215
|
-
'shared:exterior',
|
|
216
|
-
'top-right:interior',
|
|
217
|
-
])
|
|
221
|
+
expect(keys(rightRoom).sort()).toEqual(['bottom-right:a', 'right:a', 'shared:b', 'top-right:a'])
|
|
218
222
|
})
|
|
219
223
|
|
|
220
|
-
it('wall room
|
|
224
|
+
it('wall room keeps the trim while mapping each boundary face side', () => {
|
|
221
225
|
const { walls, spaces } = adjacentRooms()
|
|
222
226
|
const shared = walls.find((candidate) => String(candidate.id) === 'shared')!
|
|
223
227
|
const result = resolve({
|
|
224
228
|
node: shared,
|
|
225
|
-
role: '
|
|
229
|
+
role: 'bSkirting',
|
|
226
230
|
scope: 'room',
|
|
227
231
|
nodes: walls,
|
|
228
232
|
spaces,
|
|
229
233
|
wallHit: { face: 'back', point: [4.1, 2] },
|
|
230
234
|
})
|
|
231
235
|
expect(keys(result).sort()).toEqual([
|
|
232
|
-
'bottom-right:
|
|
233
|
-
'right:
|
|
234
|
-
'shared:
|
|
235
|
-
'top-right:
|
|
236
|
+
'bottom-right:aSkirting',
|
|
237
|
+
'right:aSkirting',
|
|
238
|
+
'shared:bSkirting',
|
|
239
|
+
'top-right:aSkirting',
|
|
236
240
|
])
|
|
237
241
|
})
|
|
238
242
|
|
|
@@ -245,15 +249,16 @@ describe('resolvePaintScopeTargets', () => {
|
|
|
245
249
|
const shared = walls.find((candidate) => String(candidate.id) === 'shared')!
|
|
246
250
|
const result = resolve({
|
|
247
251
|
node: shared,
|
|
248
|
-
role: '
|
|
252
|
+
role: 'b',
|
|
249
253
|
scope: 'room',
|
|
250
254
|
nodes: walls,
|
|
251
255
|
spaces,
|
|
252
256
|
wallHit: { face: 'back', point: [4.1, 2] },
|
|
253
257
|
})
|
|
254
258
|
|
|
255
|
-
|
|
256
|
-
expect(keys(result)).
|
|
259
|
+
// Reversed, top-right turns its b face to the right room.
|
|
260
|
+
expect(keys(result)).toContain('top-right:b')
|
|
261
|
+
expect(keys(result)).not.toContain('top-right:a')
|
|
257
262
|
})
|
|
258
263
|
|
|
259
264
|
it('wall room excludes duplicate geometry and spaces from another level', () => {
|
|
@@ -267,7 +272,7 @@ describe('resolvePaintScopeTargets', () => {
|
|
|
267
272
|
const shared = levelA.walls.find((candidate) => String(candidate.id) === 'shared')!
|
|
268
273
|
const result = resolve({
|
|
269
274
|
node: shared,
|
|
270
|
-
role: '
|
|
275
|
+
role: 'a',
|
|
271
276
|
scope: 'room',
|
|
272
277
|
nodes: [...levelA.walls, ...levelBWalls],
|
|
273
278
|
spaces: [...levelA.spaces, ...otherSpaces],
|
|
@@ -291,7 +296,7 @@ describe('resolvePaintScopeTargets', () => {
|
|
|
291
296
|
|
|
292
297
|
const leftBay = resolve({
|
|
293
298
|
node: long,
|
|
294
|
-
role: '
|
|
299
|
+
role: 'b',
|
|
295
300
|
scope: 'room',
|
|
296
301
|
nodes: walls,
|
|
297
302
|
spaces,
|
|
@@ -299,7 +304,7 @@ describe('resolvePaintScopeTargets', () => {
|
|
|
299
304
|
})
|
|
300
305
|
const rightBay = resolve({
|
|
301
306
|
node: long,
|
|
302
|
-
role: '
|
|
307
|
+
role: 'b',
|
|
303
308
|
scope: 'room',
|
|
304
309
|
nodes: walls,
|
|
305
310
|
spaces,
|
|
@@ -314,9 +319,9 @@ describe('resolvePaintScopeTargets', () => {
|
|
|
314
319
|
|
|
315
320
|
it('wall room with no enclosing space falls back to single', () => {
|
|
316
321
|
const w1 = wall('w1', [0, 0], [4, 0])
|
|
317
|
-
expect(
|
|
318
|
-
|
|
319
|
-
|
|
322
|
+
expect(keys(resolve({ node: w1, role: 'a', scope: 'room', nodes: [w1], spaces: [] }))).toEqual([
|
|
323
|
+
'w1:a',
|
|
324
|
+
])
|
|
320
325
|
})
|
|
321
326
|
|
|
322
327
|
it('wall room paints the connected exterior envelope from an exterior face', () => {
|
|
@@ -331,14 +336,14 @@ describe('resolvePaintScopeTargets', () => {
|
|
|
331
336
|
keys(
|
|
332
337
|
resolve({
|
|
333
338
|
node: walls[0]!,
|
|
334
|
-
role: '
|
|
339
|
+
role: 'b',
|
|
335
340
|
scope: 'room',
|
|
336
341
|
nodes: walls,
|
|
337
342
|
spaces,
|
|
338
343
|
wallHit: { face: 'back', point: [2, -0.1] },
|
|
339
344
|
}),
|
|
340
345
|
).sort(),
|
|
341
|
-
).toEqual(['bottom:
|
|
346
|
+
).toEqual(['bottom:b', 'left:b', 'right:b', 'top:b'])
|
|
342
347
|
})
|
|
343
348
|
|
|
344
349
|
it('wall room does not cross to a disconnected exterior envelope', () => {
|
|
@@ -358,7 +363,7 @@ describe('resolvePaintScopeTargets', () => {
|
|
|
358
363
|
const spaces = detectSpacesForLevel('l1', walls).spaces
|
|
359
364
|
const result = resolve({
|
|
360
365
|
node: first[0]!,
|
|
361
|
-
role: '
|
|
366
|
+
role: 'b',
|
|
362
367
|
scope: 'room',
|
|
363
368
|
nodes: walls,
|
|
364
369
|
spaces,
|
|
@@ -374,7 +379,7 @@ describe('resolvePaintScopeTargets', () => {
|
|
|
374
379
|
const bottomLeft = walls.find((candidate) => String(candidate.id) === 'bottom-left')!
|
|
375
380
|
const result = resolve({
|
|
376
381
|
node: bottomLeft,
|
|
377
|
-
role: '
|
|
382
|
+
role: 'b',
|
|
378
383
|
scope: 'room',
|
|
379
384
|
nodes: walls,
|
|
380
385
|
spaces,
|
|
@@ -398,14 +403,14 @@ describe('resolvePaintScopeTargets', () => {
|
|
|
398
403
|
const spaces = detectSpacesForLevel('l1', walls).spaces
|
|
399
404
|
const result = resolve({
|
|
400
405
|
node: long,
|
|
401
|
-
role: '
|
|
406
|
+
role: 'b',
|
|
402
407
|
scope: 'room',
|
|
403
408
|
nodes: walls,
|
|
404
409
|
spaces,
|
|
405
410
|
wallHit: { face: 'back', point: [2, -0.1] },
|
|
406
411
|
})
|
|
407
412
|
|
|
408
|
-
expect(keys(result).filter((key) => key === 'long:
|
|
413
|
+
expect(keys(result).filter((key) => key === 'long:b')).toHaveLength(1)
|
|
409
414
|
expect(keys(result).some((key) => key.startsWith('divider:'))).toBe(false)
|
|
410
415
|
expect(result).toHaveLength(5)
|
|
411
416
|
})
|
|
@@ -508,3 +513,280 @@ describe('resolvePaintScopeTargets', () => {
|
|
|
508
513
|
expect(keys(result)).toEqual(['ground:surface'])
|
|
509
514
|
})
|
|
510
515
|
})
|
|
516
|
+
|
|
517
|
+
describe('wall room scope inside a zone', () => {
|
|
518
|
+
function zonedRooms() {
|
|
519
|
+
const { walls, spaces } = adjacentRooms()
|
|
520
|
+
const level = { id: 'l1', type: 'level', children: [] as string[] } as unknown as AnyNode
|
|
521
|
+
// The fixture walls carry plain ids, so the zones skip schema parsing.
|
|
522
|
+
const zone = (id: string, polygon: Array<[number, number]>, boundary: string[]) =>
|
|
523
|
+
({
|
|
524
|
+
id,
|
|
525
|
+
type: 'zone',
|
|
526
|
+
name: id,
|
|
527
|
+
parentId: 'l1',
|
|
528
|
+
polygon,
|
|
529
|
+
holes: [],
|
|
530
|
+
boundaryWallIds: boundary,
|
|
531
|
+
}) as unknown as ZoneNode
|
|
532
|
+
const zones = [
|
|
533
|
+
zone(
|
|
534
|
+
'zone_left',
|
|
535
|
+
[
|
|
536
|
+
[0, 0],
|
|
537
|
+
[4, 0],
|
|
538
|
+
[4, 4],
|
|
539
|
+
[0, 4],
|
|
540
|
+
],
|
|
541
|
+
['bottom-left', 'shared', 'top-left', 'left'],
|
|
542
|
+
),
|
|
543
|
+
zone(
|
|
544
|
+
'zone_right',
|
|
545
|
+
[
|
|
546
|
+
[4, 0],
|
|
547
|
+
[8, 0],
|
|
548
|
+
[8, 4],
|
|
549
|
+
[4, 4],
|
|
550
|
+
],
|
|
551
|
+
['bottom-right', 'right', 'top-right', 'shared'],
|
|
552
|
+
),
|
|
553
|
+
]
|
|
554
|
+
;(level as unknown as { children: string[] }).children = [
|
|
555
|
+
...walls.map((w) => String(w.id)),
|
|
556
|
+
...zones.map((z) => z.id),
|
|
557
|
+
]
|
|
558
|
+
const shared = walls.find((candidate) => String(candidate.id) === 'shared')!
|
|
559
|
+
return { nodes: [level, ...walls, ...zones] as AnyNode[], spaces, shared }
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
it('paints the room the hit face borders, as one room commit listed on its walls', () => {
|
|
563
|
+
const { nodes, spaces, shared } = zonedRooms()
|
|
564
|
+
const left = resolve({
|
|
565
|
+
node: shared,
|
|
566
|
+
role: 'a',
|
|
567
|
+
scope: 'room',
|
|
568
|
+
nodes,
|
|
569
|
+
spaces,
|
|
570
|
+
wallHit: { face: 'front', point: [3.9, 2] },
|
|
571
|
+
})
|
|
572
|
+
expect(keys(left).sort()).toEqual([
|
|
573
|
+
'bottom-left:room:zone_left',
|
|
574
|
+
'left:room:zone_left',
|
|
575
|
+
'shared:room:zone_left',
|
|
576
|
+
'top-left:room:zone_left',
|
|
577
|
+
])
|
|
578
|
+
// A hit on a face already showing its room finish names the room directly.
|
|
579
|
+
const right = resolve({
|
|
580
|
+
node: shared,
|
|
581
|
+
role: 'room:zone_right/b',
|
|
582
|
+
scope: 'room',
|
|
583
|
+
nodes,
|
|
584
|
+
spaces,
|
|
585
|
+
wallHit: { face: 'front', point: [4.1, 2] },
|
|
586
|
+
})
|
|
587
|
+
expect(new Set(right.map((target) => target.role))).toEqual(new Set(['room:zone_right']))
|
|
588
|
+
expect(right).toHaveLength(4)
|
|
589
|
+
})
|
|
590
|
+
|
|
591
|
+
it('fans a trim to the face each wall turns to the room, and keeps regions single', () => {
|
|
592
|
+
const { nodes, spaces, shared } = zonedRooms()
|
|
593
|
+
const trims = resolve({
|
|
594
|
+
node: shared,
|
|
595
|
+
role: 'bSkirting',
|
|
596
|
+
scope: 'room',
|
|
597
|
+
nodes,
|
|
598
|
+
spaces,
|
|
599
|
+
wallHit: { face: 'back', point: [4.1, 2] },
|
|
600
|
+
})
|
|
601
|
+
expect(keys(trims).sort()).toEqual([
|
|
602
|
+
'bottom-right:aSkirting',
|
|
603
|
+
'right:aSkirting',
|
|
604
|
+
'shared:bSkirting',
|
|
605
|
+
'top-right:aSkirting',
|
|
606
|
+
])
|
|
607
|
+
expect(
|
|
608
|
+
keys(
|
|
609
|
+
resolve({
|
|
610
|
+
node: shared,
|
|
611
|
+
role: 'region:wainscot',
|
|
612
|
+
scope: 'room',
|
|
613
|
+
nodes,
|
|
614
|
+
spaces,
|
|
615
|
+
wallHit: { face: 'front', point: [3.9, 2] },
|
|
616
|
+
}),
|
|
617
|
+
),
|
|
618
|
+
).toEqual([
|
|
619
|
+
'shared:room:zone_left',
|
|
620
|
+
'bottom-left:room:zone_left',
|
|
621
|
+
'top-left:room:zone_left',
|
|
622
|
+
'left:room:zone_left',
|
|
623
|
+
])
|
|
624
|
+
})
|
|
625
|
+
})
|
|
626
|
+
|
|
627
|
+
describe('wall and floor scopes', () => {
|
|
628
|
+
it('a wall offers its face or its room, never the whole wall', () => {
|
|
629
|
+
const wall = { id: 'wall_x', type: 'wall', parentId: 'level_x' } as unknown as AnyNode
|
|
630
|
+
expect(availablePaintScopes({ node: wall, slotRoles: ['a', 'b', 'aSkirting'] })).toEqual([
|
|
631
|
+
'single',
|
|
632
|
+
'room',
|
|
633
|
+
])
|
|
634
|
+
expect(paintScopeLabel('single', { scopes: [], slotLabel: 'Face', nodeNoun: 'wall' })).toBe(
|
|
635
|
+
'Face',
|
|
636
|
+
)
|
|
637
|
+
expect(paintSurfaceLabel(wall, 'room:zone_left/a')).toBe('Face')
|
|
638
|
+
expect(paintSurfaceLabel(wall, 'region:wainscot')).toBe('Face')
|
|
639
|
+
expect(paintSurfaceLabel(wall, 'a')).toBe('Face')
|
|
640
|
+
})
|
|
641
|
+
|
|
642
|
+
it('the room scope on a room floor is one room-wide role on every plate of the level', () => {
|
|
643
|
+
const plate = (id: string) =>
|
|
644
|
+
({
|
|
645
|
+
id,
|
|
646
|
+
type: 'slab',
|
|
647
|
+
parentId: 'level_f',
|
|
648
|
+
boundary: 'auto',
|
|
649
|
+
zoneIds: [],
|
|
650
|
+
}) as unknown as AnyNode
|
|
651
|
+
const nodes = { plate_a: plate('plate_a'), plate_b: plate('plate_b') } as Record<
|
|
652
|
+
string,
|
|
653
|
+
AnyNode
|
|
654
|
+
>
|
|
655
|
+
const targets = resolvePaintScopeTargets({
|
|
656
|
+
node: nodes.plate_a!,
|
|
657
|
+
role: 'room:zone_k/rug',
|
|
658
|
+
scope: 'room',
|
|
659
|
+
nodes,
|
|
660
|
+
spaces: {},
|
|
661
|
+
slotRolesOf: () => [],
|
|
662
|
+
})
|
|
663
|
+
expect(targets.map((target) => `${target.nodeId}:${target.role}`)).toEqual([
|
|
664
|
+
'plate_a:room:zone_k/*',
|
|
665
|
+
'plate_b:room:zone_k/*',
|
|
666
|
+
])
|
|
667
|
+
})
|
|
668
|
+
})
|
|
669
|
+
|
|
670
|
+
describe('commitPaintScopeFanout routing', () => {
|
|
671
|
+
it('commits a routed role once through its kind and writes slot roles as slots', () => {
|
|
672
|
+
const kind = 'paint-route-test'
|
|
673
|
+
const commits: string[] = []
|
|
674
|
+
if (!nodeRegistry.has(kind))
|
|
675
|
+
registerNode({
|
|
676
|
+
kind,
|
|
677
|
+
schemaVersion: 1,
|
|
678
|
+
schema: z.object({ type: z.literal(kind) }) as never,
|
|
679
|
+
category: 'structure',
|
|
680
|
+
defaults: () => ({}),
|
|
681
|
+
capabilities: {
|
|
682
|
+
slots: () => [{ slotId: 'a', label: 'A', default: 'library:x' }],
|
|
683
|
+
paint: {
|
|
684
|
+
resolveRole: () => null,
|
|
685
|
+
buildPatch: () => ({}),
|
|
686
|
+
applyPreview: () => null,
|
|
687
|
+
commit: ({ role }: { role: string }) => {
|
|
688
|
+
commits.push(role)
|
|
689
|
+
},
|
|
690
|
+
},
|
|
691
|
+
},
|
|
692
|
+
floorplanScope: 'level',
|
|
693
|
+
renderer: { kind: 'parametric', module: async () => ({ default: () => null }) },
|
|
694
|
+
} as unknown as AnyNodeDefinition)
|
|
695
|
+
const node = (id: string) => ({ id, type: kind, slots: {} }) as unknown as AnyNode
|
|
696
|
+
useScene.setState({
|
|
697
|
+
nodes: {
|
|
698
|
+
n1: node('n1'),
|
|
699
|
+
n2: node('n2'),
|
|
700
|
+
zone_x: { id: 'zone_x', type: 'zone' } as unknown as AnyNode,
|
|
701
|
+
},
|
|
702
|
+
materials: {},
|
|
703
|
+
dirtyNodes: new Set(),
|
|
704
|
+
} as never)
|
|
705
|
+
const at = (id: string, role: string) => ({ nodeId: id as AnyNodeId, role })
|
|
706
|
+
commitPaintScopeFanout(
|
|
707
|
+
[at('n1', 'room:zone_x'), at('n2', 'room:zone_x'), at('zone_x', 'room:zone_x')],
|
|
708
|
+
undefined,
|
|
709
|
+
'library:red',
|
|
710
|
+
)
|
|
711
|
+
expect(commits).toEqual(['room:zone_x'])
|
|
712
|
+
expect(
|
|
713
|
+
(useScene.getState().nodes.zone_x as unknown as { slots?: unknown }).slots,
|
|
714
|
+
).toBeUndefined()
|
|
715
|
+
commitPaintScopeFanout([at('n1', 'a'), at('n2', 'a')], undefined, 'library:red')
|
|
716
|
+
const slots = (id: string) =>
|
|
717
|
+
(useScene.getState().nodes[id as AnyNodeId] as never as { slots: object }).slots
|
|
718
|
+
expect(slots('n1')).toEqual({ a: 'library:red' })
|
|
719
|
+
expect(slots('n2')).toEqual({ a: 'library:red' })
|
|
720
|
+
expect(commits).toEqual(['room:zone_x'])
|
|
721
|
+
})
|
|
722
|
+
})
|
|
723
|
+
|
|
724
|
+
describe('generated floor plates: scopes per surface', () => {
|
|
725
|
+
const plate = (plateRole: 'base' | 'platform' = 'base') =>
|
|
726
|
+
({
|
|
727
|
+
id: `plate_${plateRole}`,
|
|
728
|
+
type: 'slab',
|
|
729
|
+
parentId: 'level_p',
|
|
730
|
+
boundary: 'auto',
|
|
731
|
+
plateRole,
|
|
732
|
+
zoneIds: [],
|
|
733
|
+
}) as unknown as AnyNode
|
|
734
|
+
const manual = { id: 'slab_hand', type: 'slab', parentId: 'level_p' } as unknown as AnyNode
|
|
735
|
+
const chip = (node: AnyNode, role: string) => {
|
|
736
|
+
const info = paintHoverInfo(node, role, ['foundation', 'surface', 'side', 'edge'])
|
|
737
|
+
return info.scopes.map((scope) => paintScopeLabel(scope, info))
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
it('a floor is this surface or the whole room', () => {
|
|
741
|
+
expect(chip(plate(), 'room:zone_k')).toEqual(['This surface', 'Whole room'])
|
|
742
|
+
expect(chip(plate(), 'room:zone_k/rug')).toEqual(['This surface', 'Whole room'])
|
|
743
|
+
expect(chip(plate('platform'), 'room:zone_k')).toEqual(['This surface', 'Whole room'])
|
|
744
|
+
})
|
|
745
|
+
|
|
746
|
+
it('a step is this step or every step of its room', () => {
|
|
747
|
+
expect(chip(plate(), 'step:zone_k/door_1')).toEqual(['This step', 'All steps in this room'])
|
|
748
|
+
})
|
|
749
|
+
|
|
750
|
+
it("a footprint's faces have one fixed label and nothing to cycle", () => {
|
|
751
|
+
expect(chip(plate(), 'edge')).toEqual(['Floor edge · all around this floor'])
|
|
752
|
+
expect(chip(plate(), 'foundation')).toEqual(['Foundation · all around this floor'])
|
|
753
|
+
expect(chip(plate(), 'riser')).toEqual(['Riser · all around this floor'])
|
|
754
|
+
expect(chip(plate(), 'underside')).toEqual(['Underside · all of this floor'])
|
|
755
|
+
})
|
|
756
|
+
|
|
757
|
+
it('no plate offers the whole slab; a hand-drawn slab still does', () => {
|
|
758
|
+
for (const role of ['room:zone_k', 'step:zone_k/door_1', 'edge', 'foundation', 'riser'])
|
|
759
|
+
expect(paintHoverInfo(plate(), role, ['foundation', 'surface', 'side']).scopes).not.toContain(
|
|
760
|
+
'object',
|
|
761
|
+
)
|
|
762
|
+
// (The room scope comes from the registry, not wired in this unit context.)
|
|
763
|
+
expect(chip(manual, 'surface')).toContain('Whole slab')
|
|
764
|
+
})
|
|
765
|
+
|
|
766
|
+
it('a scope the surface does not offer paints just that surface', () => {
|
|
767
|
+
const node = plate()
|
|
768
|
+
const nodes = { [node.id]: node } as Record<string, AnyNode>
|
|
769
|
+
const targets = (role: string, scope: PaintScope) =>
|
|
770
|
+
resolvePaintScopeTargets({
|
|
771
|
+
node,
|
|
772
|
+
role,
|
|
773
|
+
scope,
|
|
774
|
+
nodes,
|
|
775
|
+
spaces: {},
|
|
776
|
+
slotRolesOf: () => ['foundation', 'surface', 'side', 'edge', 'riser', 'underside'],
|
|
777
|
+
}).map((target) => `${target.nodeId}:${target.role}`)
|
|
778
|
+
expect(targets('room:zone_k', 'object')).toEqual(['plate_base:room:zone_k'])
|
|
779
|
+
expect(targets('edge', 'object')).toEqual(['plate_base:edge'])
|
|
780
|
+
expect(targets('edge', 'room')).toEqual(['plate_base:edge'])
|
|
781
|
+
expect(targets('foundation', 'room')).toEqual(['plate_base:foundation'])
|
|
782
|
+
expect(effectivePaintScope('object', ['single', 'room'])).toBe('single')
|
|
783
|
+
expect(effectivePaintScope('room', ['single', 'room'])).toBe('room')
|
|
784
|
+
})
|
|
785
|
+
|
|
786
|
+
it('the whole room is one room-wide role; its steps are one room step role', () => {
|
|
787
|
+
expect(paintScopeRole(plate(), 'room:zone_k/rug', 'room')).toBe('room:zone_k/*')
|
|
788
|
+
expect(paintScopeRole(plate(), 'room:zone_k', 'single')).toBe('room:zone_k')
|
|
789
|
+
expect(paintScopeRole(plate(), 'step:zone_k/door_1', 'room')).toBe('step:zone_k')
|
|
790
|
+
expect(paintScopeRole(manual, 'surface', 'room')).toBe('surface')
|
|
791
|
+
})
|
|
792
|
+
})
|