@pascal-app/editor 1.0.1 → 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 +16 -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 +221 -128
- 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 +37 -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 +536 -487
- package/src/components/editor/footprint-height-handle.tsx +204 -0
- package/src/components/editor/grid.tsx +31 -7
- package/src/components/editor/group-actions.ts +59 -11
- package/src/components/editor/group-floating-action-menu.tsx +19 -9
- package/src/components/editor/group-move-3d.ts +4 -6
- package/src/components/editor/group-rotate-handle.tsx +81 -35
- package/src/components/editor/group-selection-box-3d.tsx +25 -17
- package/src/components/editor/group-transform-shared.test.ts +126 -1
- package/src/components/editor/group-transform-shared.ts +79 -73
- package/src/components/editor/handles/handle-arrow.tsx +72 -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 +272 -131
- package/src/components/editor/index.tsx +316 -176
- package/src/components/editor/join-walls-pill.tsx +44 -0
- package/src/components/editor/node-action-menu.tsx +70 -70
- package/src/components/editor/node-arrow-handles.tsx +167 -14
- 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 +132 -0
- 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 +114 -163
- package/src/components/editor/wall-opening-highlights.tsx +2 -3
- 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-group-move.tsx +17 -24
- 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-registered-tool-layer.tsx +33 -12
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +11 -1
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +242 -126
- 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-measurement.tsx +166 -0
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +4 -1
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +87 -8
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +355 -245
- 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 +31 -66
- package/src/components/tools/__fixtures__/hosting.ts +10 -0
- package/src/components/tools/item/item-surface-placement.test.ts +3 -9
- package/src/components/tools/item/item-surface-strategy-resolver.test.ts +3 -9
- package/src/components/tools/item/placement-history.test.tsx +474 -0
- package/src/components/tools/item/placement-strategies.ts +63 -47
- package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +3 -3
- package/src/components/tools/item/use-draft-node.test.tsx +432 -1
- package/src/components/tools/item/use-draft-node.ts +207 -97
- package/src/components/tools/item/use-placement-coordinator.tsx +251 -111
- 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/item-surface-move.test.ts +10 -2
- package/src/components/tools/registry/item-surface-move.ts +46 -24
- package/src/components/tools/registry/item-surface-resolver.test.ts +2 -2
- package/src/components/tools/registry/move-registry-node-tool.tsx +26 -10
- 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/affordance-dispatch.test.ts +24 -0
- package/src/components/tools/shared/affordance-dispatch.ts +13 -0
- package/src/components/tools/shared/cursor-sphere.tsx +36 -12
- package/src/components/tools/shared/draft-measurement-label.tsx +38 -0
- package/src/components/tools/shared/pointer-support-cap.test.ts +27 -0
- package/src/components/tools/shared/pointer-support-cap.ts +17 -5
- package/src/components/tools/shared/surface-rejection.test.tsx +5 -4
- package/src/components/tools/shared/surface-rejection.tsx +14 -0
- 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 +82 -23
- 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 +101 -91
- package/src/components/ui/controls/material-picker.tsx +8 -67
- 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/controls/slider-control.tsx +76 -31
- 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 +348 -19
- package/src/components/ui/helpers/item-helper.tsx +8 -0
- package/src/components/ui/helpers/registered-tool-helper.tsx +10 -33
- package/src/components/ui/icon-ref.tsx +45 -1
- package/src/components/ui/item-catalog/item-catalog.tsx +5 -30
- 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-height-mode.tsx +27 -21
- 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 +7 -19
- 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/shortcut-token.tsx +4 -0
- 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/audio-settings-dialog.tsx +2 -2
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +615 -372
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +7 -7
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +1 -1
- 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-visibility.test.ts +35 -0
- package/src/components/viewer/floorplan-preview-visibility.ts +19 -0
- package/src/components/viewer/floorplan-preview.tsx +6 -18
- 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 +9 -9
- package/src/hooks/use-auto-save.integration.test.tsx +311 -0
- package/src/hooks/use-auto-save.ts +55 -14
- package/src/hooks/use-ceiling-events.test.ts +1 -0
- package/src/hooks/use-ceiling-events.ts +2 -0
- package/src/hooks/use-grid-events.test.ts +2 -0
- package/src/hooks/use-grid-events.ts +16 -11
- package/src/hooks/use-keyboard.test.ts +182 -0
- package/src/hooks/use-keyboard.ts +122 -91
- 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 +306 -39
- package/src/lib/catalog-panel-model.ts +39 -0
- package/src/lib/catalog-selection.test.ts +27 -0
- package/src/lib/catalog-selection.ts +8 -0
- 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/contextual-help.test.ts +27 -1
- package/src/lib/contextual-help.ts +16 -9
- 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 +15 -9
- package/src/lib/floorplan/floorplan-export.tsx +32 -8
- package/src/lib/floorplan/floorplan-extension.ts +48 -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 +191 -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 +544 -16
- package/src/lib/glb-export.ts +256 -125
- package/src/lib/grid-frame.test.ts +19 -0
- package/src/lib/grid-frame.ts +18 -0
- 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 +27 -1
- package/src/lib/keyboard-pan.test.ts +184 -0
- package/src/lib/keyboard-pan.ts +238 -0
- 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 +74 -0
- package/src/lib/material-paint-panel-model.ts +80 -0
- 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/overlay-transform.test.ts +28 -0
- package/src/lib/overlay-transform.ts +12 -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 +96 -0
- package/src/lib/panel-tool-options.test.ts +34 -0
- package/src/lib/panel-tool-options.ts +46 -0
- package/src/lib/parametric-node-update.ts +37 -0
- 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 +87 -0
- package/src/lib/scene.ts +39 -45
- 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 +17 -2
- package/src/lib/snapping-mode.ts +19 -7
- package/src/lib/spatial-drag-plane.test.ts +53 -0
- package/src/lib/spatial-drag-plane.ts +19 -0
- package/src/lib/spatial-pointer-input.test.ts +72 -0
- package/src/lib/spatial-pointer-input.ts +62 -0
- package/src/lib/spatial-snap.test.ts +47 -0
- package/src/lib/terrace-draft.test.ts +145 -0
- package/src/lib/terrace-draft.ts +213 -0
- package/src/lib/terrain-panel-model.ts +117 -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/world-grid-snap.test.ts +48 -0
- package/src/lib/world-grid-snap.ts +9 -5
- 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/rotation-snapping.test.ts +53 -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 +442 -323
- package/src/store/use-floorplan-draft-preview.ts +23 -1
- package/src/store/use-floorplan-marquee.ts +12 -2
- package/src/store/use-interaction-scope.ts +155 -6
- 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
package/src/lib/paint-scope.ts
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
import {
|
|
2
2
|
type AnyNode,
|
|
3
3
|
type AnyNodeId,
|
|
4
|
+
floorStepRole,
|
|
4
5
|
generateSceneMaterialId,
|
|
6
|
+
getWallLevelZones,
|
|
7
|
+
getWallLocalFaceZ,
|
|
8
|
+
getWallZoneSpans,
|
|
5
9
|
type ItemNode,
|
|
6
10
|
type MaterialSchema,
|
|
7
11
|
nodeRegistry,
|
|
12
|
+
parseFloorStepRole,
|
|
13
|
+
parseRoomFinishRole,
|
|
8
14
|
pointInPolygon2D,
|
|
9
15
|
resolveLevelId,
|
|
10
16
|
type SceneMaterial,
|
|
@@ -15,6 +21,7 @@ import {
|
|
|
15
21
|
toSceneMaterialRef,
|
|
16
22
|
useScene,
|
|
17
23
|
type WallNode,
|
|
24
|
+
wallRoomFinishRole,
|
|
18
25
|
} from '@pascal-app/core'
|
|
19
26
|
|
|
20
27
|
/**
|
|
@@ -36,6 +43,8 @@ export type PaintHoverInfo = {
|
|
|
36
43
|
slotLabel: string
|
|
37
44
|
/** Kind noun for the `'object'` label (e.g. "Whole shelf"). */
|
|
38
45
|
nodeNoun: string
|
|
46
|
+
/** Labels a surface names itself, per scope, over the generic ones. */
|
|
47
|
+
labels?: Partial<Record<PaintScope, string>>
|
|
39
48
|
}
|
|
40
49
|
|
|
41
50
|
function nodeHasAsset(node: AnyNode): boolean {
|
|
@@ -46,13 +55,55 @@ function nodeOffersRoomScope(node: AnyNode): boolean {
|
|
|
46
55
|
return nodeRegistry.get(node.type)?.capabilities?.paint?.roomScope === true
|
|
47
56
|
}
|
|
48
57
|
|
|
58
|
+
const isFloorPlate = (node: AnyNode): boolean => node.type === 'slab' && node.boundary === 'auto'
|
|
59
|
+
|
|
60
|
+
const FOOTPRINT_FACE_LABELS: Record<string, string> = {
|
|
61
|
+
edge: 'Floor edge · all around this floor',
|
|
62
|
+
riser: 'Riser · all around this floor',
|
|
63
|
+
underside: 'Underside · all of this floor',
|
|
64
|
+
foundation: 'Foundation · all around this floor',
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The scopes a generated floor plate offers for the surface under the cursor.
|
|
69
|
+
* A plate is not an object anyone paints whole: a room's floor is "this
|
|
70
|
+
* surface" (the painted part under the cursor, else the room floor) or the
|
|
71
|
+
* whole room; a step is its doorway or every step of the room; the footprint's
|
|
72
|
+
* faces are one finish all around the floor, so they have nothing to cycle.
|
|
73
|
+
*/
|
|
74
|
+
export function platePaintScopes(
|
|
75
|
+
node: AnyNode,
|
|
76
|
+
role: string,
|
|
77
|
+
): { scopes: PaintScope[]; labels: Partial<Record<PaintScope, string>> } | null {
|
|
78
|
+
if (node.type !== 'slab' || node.boundary !== 'auto') return null
|
|
79
|
+
if (parseFloorStepRole(role))
|
|
80
|
+
return {
|
|
81
|
+
scopes: ['single', 'room'],
|
|
82
|
+
labels: { single: 'This step', room: 'All steps in this room' },
|
|
83
|
+
}
|
|
84
|
+
if (parseRoomFinishRole(role))
|
|
85
|
+
return { scopes: ['single', 'room'], labels: { single: 'This surface', room: 'Whole room' } }
|
|
86
|
+
const fixed = node.plateRole === 'base' ? FOOTPRINT_FACE_LABELS[role] : undefined
|
|
87
|
+
return { scopes: ['single'], labels: fixed ? { single: fixed } : {} }
|
|
88
|
+
}
|
|
89
|
+
|
|
49
90
|
/**
|
|
50
91
|
* The scopes a hovered node offers, derived from the node itself: every node
|
|
51
92
|
* paints `single`; > 1 slot adds `object`; an `asset` adds `matching`; a
|
|
52
93
|
* `roomScope`-declaring kind adds `room`. `slotRoles` is the node's full slot set
|
|
53
|
-
* (declared or mesh-derived), passed in by the caller.
|
|
94
|
+
* (declared or mesh-derived), passed in by the caller. A floor plate's set
|
|
95
|
+
* depends on the surface under the cursor (`role`).
|
|
54
96
|
*/
|
|
55
|
-
export function availablePaintScopes(args: {
|
|
97
|
+
export function availablePaintScopes(args: {
|
|
98
|
+
node: AnyNode
|
|
99
|
+
slotRoles: string[]
|
|
100
|
+
role?: string
|
|
101
|
+
}): PaintScope[] {
|
|
102
|
+
// A wall paints the face under the cursor, or its room's walls: "whole wall"
|
|
103
|
+
// (both faces of one wall, across two rooms) is nobody's surface.
|
|
104
|
+
if (args.node.type === 'wall') return ['single', 'room']
|
|
105
|
+
const plate = args.role === undefined ? null : platePaintScopes(args.node, args.role)
|
|
106
|
+
if (plate) return plate.scopes
|
|
56
107
|
const scopes: PaintScope[] = ['single']
|
|
57
108
|
if (args.slotRoles.length > 1) scopes.push('object')
|
|
58
109
|
if (nodeHasAsset(args.node)) scopes.push('matching')
|
|
@@ -66,7 +117,24 @@ export function cyclePaintScope(scope: PaintScope, scopes: PaintScope[]): PaintS
|
|
|
66
117
|
return list[(index + 1) % list.length] ?? 'single'
|
|
67
118
|
}
|
|
68
119
|
|
|
120
|
+
/** The chip's hover info for `role` on `node`: its scopes and what each is called. */
|
|
121
|
+
export function paintHoverInfo(node: AnyNode, role: string, slotRoles: string[]): PaintHoverInfo {
|
|
122
|
+
return {
|
|
123
|
+
scopes: availablePaintScopes({ node, slotRoles, role }),
|
|
124
|
+
slotLabel: paintSurfaceLabel(node, role),
|
|
125
|
+
nodeNoun: node.type,
|
|
126
|
+
labels: platePaintScopes(node, role)?.labels,
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** The scope a click applies: the chosen one when the hovered surface offers it. */
|
|
131
|
+
export function effectivePaintScope(scope: PaintScope, scopes: PaintScope[]): PaintScope {
|
|
132
|
+
return scopes.includes(scope) ? scope : 'single'
|
|
133
|
+
}
|
|
134
|
+
|
|
69
135
|
export function paintScopeLabel(scope: PaintScope, info: PaintHoverInfo): string {
|
|
136
|
+
const own = info.labels?.[scope]
|
|
137
|
+
if (own) return own
|
|
70
138
|
switch (scope) {
|
|
71
139
|
case 'object':
|
|
72
140
|
return `Whole ${info.nodeNoun}`
|
|
@@ -91,13 +159,21 @@ export function nodeSlotRoles(node: AnyNode, meshSlotRoles: (node: AnyNode) => s
|
|
|
91
159
|
return meshSlotRoles(node)
|
|
92
160
|
}
|
|
93
161
|
|
|
162
|
+
/**
|
|
163
|
+
* What the paint HUD calls the surface under the cursor: a wall face is the
|
|
164
|
+
* "Face" (the narrow scope), a trim its own name, anything else its slot.
|
|
165
|
+
*/
|
|
166
|
+
export function paintSurfaceLabel(node: AnyNode, role: string): string {
|
|
167
|
+
if (node.type === 'wall' && !WALL_TRIM_ROLE.test(role)) return 'Face'
|
|
168
|
+
return slotDisplayLabel(node, role)
|
|
169
|
+
}
|
|
170
|
+
|
|
94
171
|
/** Display label for the hovered slot — declared label wins, else derived from the id. */
|
|
95
172
|
export function slotDisplayLabel(node: AnyNode, role: string): string {
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
return declared?.label ?? slotLabelFromId(role)
|
|
173
|
+
const capabilities = nodeRegistry.get(node.type)?.capabilities
|
|
174
|
+
const declared = capabilities?.slots?.(node)?.find((slot) => slot.slotId === role)
|
|
175
|
+
if (declared) return declared.label
|
|
176
|
+
return capabilities?.paint?.roleLabel?.(node, role) ?? slotLabelFromId(role)
|
|
101
177
|
}
|
|
102
178
|
|
|
103
179
|
// ── Fan-out resolution ──────────────────────────────────────────────────────
|
|
@@ -141,17 +217,87 @@ function distanceToPolyline(
|
|
|
141
217
|
return distance
|
|
142
218
|
}
|
|
143
219
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
220
|
+
const WALL_TRIM_ROLE = /^([ab])(Skirting|Crown|ChairRail)$/
|
|
221
|
+
const WALL_ROOM_FACE_ROLE = /^room:(.+)\/([ab])$/
|
|
222
|
+
|
|
223
|
+
/** The same surface on a boundary face: face slots and trims follow the physical face. */
|
|
224
|
+
function wallRoleForRoomFace(role: string, face: 'front' | 'back'): string | null {
|
|
225
|
+
const physical = face === 'front' ? 'a' : 'b'
|
|
226
|
+
if (role === 'a' || role === 'b') return physical
|
|
227
|
+
const trim = WALL_TRIM_ROLE.exec(role)
|
|
228
|
+
return trim ? `${physical}${trim[2]}` : null
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** The face a wall paint role sits on, when the role names one. */
|
|
232
|
+
function wallRoleFace(role: string): 'a' | 'b' | null {
|
|
233
|
+
if (role === 'a' || role === 'b') return role
|
|
234
|
+
return (WALL_TRIM_ROLE.exec(role)?.[1] ?? WALL_ROOM_FACE_ROLE.exec(role)?.[2] ?? null) as
|
|
235
|
+
| 'a'
|
|
236
|
+
| 'b'
|
|
237
|
+
| null
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** The room (zone) a wall face borders at the hit point, from the zones' boundary spans. */
|
|
241
|
+
function zoneAtWallHit(
|
|
242
|
+
wall: WallNode,
|
|
243
|
+
face: 'a' | 'b',
|
|
244
|
+
point: readonly [number, number],
|
|
245
|
+
nodes: Record<string, AnyNode>,
|
|
246
|
+
): string | null {
|
|
247
|
+
const dx = wall.end[0] - wall.start[0]
|
|
248
|
+
const dz = wall.end[1] - wall.start[1]
|
|
249
|
+
const lengthSquared = dx * dx + dz * dz
|
|
250
|
+
if (lengthSquared < 1e-12) return null
|
|
251
|
+
const t = ((point[0] - wall.start[0]) * dx + (point[1] - wall.start[1]) * dz) / lengthSquared
|
|
252
|
+
const spans = getWallZoneSpans(wall, getWallLevelZones(wall, nodes))
|
|
253
|
+
const span = spans.find(
|
|
254
|
+
(candidate) =>
|
|
255
|
+
candidate.face === face &&
|
|
256
|
+
t >= candidate.t0 - 1e-6 &&
|
|
257
|
+
(t < candidate.t1 || (candidate.t1 >= 1 && t <= 1 + 1e-6)),
|
|
258
|
+
)
|
|
259
|
+
return span?.zoneId ?? null
|
|
260
|
+
}
|
|
148
261
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
262
|
+
/**
|
|
263
|
+
* Room scope on a wall face inside a room paints the room's wall finish: one
|
|
264
|
+
* `room:<zoneId>` commit on the zone. Every wall bordering the room is listed so
|
|
265
|
+
* each previews the change; trims fan out to the face each wall turns to the room.
|
|
266
|
+
*/
|
|
267
|
+
function wallRoomTargets(
|
|
268
|
+
wall: WallNode,
|
|
269
|
+
role: string,
|
|
270
|
+
zoneId: string,
|
|
271
|
+
nodes: Record<string, AnyNode>,
|
|
272
|
+
): Array<{ nodeId: AnyNodeId; role: string }> {
|
|
273
|
+
const zone = nodes[zoneId]
|
|
274
|
+
if (zone?.type !== 'zone') return []
|
|
275
|
+
const walls = [
|
|
276
|
+
wall,
|
|
277
|
+
...zone.boundaryWallIds
|
|
278
|
+
.map((id) => nodes[id])
|
|
279
|
+
.filter((node): node is WallNode => node?.type === 'wall' && node.id !== wall.id),
|
|
280
|
+
]
|
|
281
|
+
const trim = WALL_TRIM_ROLE.exec(role)
|
|
282
|
+
if (trim) {
|
|
283
|
+
const targets = new Map<string, { nodeId: AnyNodeId; role: string }>()
|
|
284
|
+
for (const target of walls) {
|
|
285
|
+
for (const span of getWallZoneSpans(target, [zone])) {
|
|
286
|
+
const targetRole = `${span.face}${trim[2]}`
|
|
287
|
+
targets.set(`${target.id}:${targetRole}`, {
|
|
288
|
+
nodeId: target.id as AnyNodeId,
|
|
289
|
+
role: targetRole,
|
|
290
|
+
})
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return [...targets.values()]
|
|
294
|
+
}
|
|
295
|
+
const roomRole = wallRoomFinishRole(zoneId)
|
|
296
|
+
const targets = walls.map((target) => ({ nodeId: target.id as AnyNodeId, role: roomRole }))
|
|
297
|
+
// A room bounded by one wall still commits through the fan-out path, which
|
|
298
|
+
// routes the room role to the zone; the zone target itself previews nothing.
|
|
299
|
+
if (targets.length === 1) targets.push({ nodeId: zone.id as AnyNodeId, role: roomRole })
|
|
300
|
+
return targets
|
|
155
301
|
}
|
|
156
302
|
|
|
157
303
|
function resolveWallPaintSpace(args: {
|
|
@@ -162,7 +308,13 @@ function resolveWallPaintSpace(args: {
|
|
|
162
308
|
}): Space | null {
|
|
163
309
|
const { wall, wallHit, nodes, spaces } = args
|
|
164
310
|
const levelId = wall.parentId ?? resolveLevelId(wall, nodes)
|
|
165
|
-
const tolerance =
|
|
311
|
+
const tolerance =
|
|
312
|
+
getWallLocalFaceZ(
|
|
313
|
+
{ ...wall, thickness: wall.thickness ?? 0.2 },
|
|
314
|
+
wallHit.face === 'front' ? 'a' : 'b',
|
|
315
|
+
) *
|
|
316
|
+
(wallHit.face === 'front' ? 1 : -1) +
|
|
317
|
+
0.08
|
|
166
318
|
let best: { space: Space; distance: number } | null = null
|
|
167
319
|
|
|
168
320
|
for (const space of Object.values(spaces)) {
|
|
@@ -216,7 +368,13 @@ function connectedExteriorBoundaries(args: {
|
|
|
216
368
|
if (!boundary) return []
|
|
217
369
|
return [{ ...boundary, face: oppositeWallFace(boundary.face) }]
|
|
218
370
|
})
|
|
219
|
-
const tolerance =
|
|
371
|
+
const tolerance =
|
|
372
|
+
getWallLocalFaceZ(
|
|
373
|
+
{ ...wall, thickness: wall.thickness ?? 0.2 },
|
|
374
|
+
wallHit.face === 'front' ? 'a' : 'b',
|
|
375
|
+
) *
|
|
376
|
+
(wallHit.face === 'front' ? 1 : -1) +
|
|
377
|
+
0.08
|
|
220
378
|
const seed = exterior
|
|
221
379
|
.filter((boundary) => boundary.wallId === wall.id && boundary.face === wallHit.face)
|
|
222
380
|
.map((boundary) => ({ boundary, distance: distanceToPolyline(wallHit.point, boundary.points) }))
|
|
@@ -277,7 +435,7 @@ function wallTargetsForBoundaries(args: {
|
|
|
277
435
|
) {
|
|
278
436
|
continue
|
|
279
437
|
}
|
|
280
|
-
const targetRole = wallRoleForRoomFace(role,
|
|
438
|
+
const targetRole = wallRoleForRoomFace(role, boundary.face)
|
|
281
439
|
if (!targetRole) continue
|
|
282
440
|
const key = `${targetWall.id}:${targetRole}`
|
|
283
441
|
targets.set(key, { nodeId: targetWall.id as AnyNodeId, role: targetRole })
|
|
@@ -298,6 +456,20 @@ function polygonCentroid(
|
|
|
298
456
|
return [x / points.length, z / points.length]
|
|
299
457
|
}
|
|
300
458
|
|
|
459
|
+
/**
|
|
460
|
+
* The role one paint click writes under `scope`: a floor step paints its own
|
|
461
|
+
* doorway, and in the room scope every step of its room (`step:<zoneId>`); a
|
|
462
|
+
* room's floor in the room scope is the whole room (`room:<zoneId>/*`).
|
|
463
|
+
* Hover outline, preview and commit all start from this role.
|
|
464
|
+
*/
|
|
465
|
+
export function paintScopeRole(node: AnyNode, role: string, scope: PaintScope): string {
|
|
466
|
+
if (node.type !== 'slab' || scope !== 'room') return role
|
|
467
|
+
const step = parseFloorStepRole(role)
|
|
468
|
+
if (step) return floorStepRole(step.zoneId)
|
|
469
|
+
const floor = isFloorPlate(node) ? parseRoomFinishRole(role) : null
|
|
470
|
+
return floor ? `room:${floor.zoneId}/*` : role
|
|
471
|
+
}
|
|
472
|
+
|
|
301
473
|
/**
|
|
302
474
|
* Expand one paint hit (`node` + resolved `role`) into the full list of
|
|
303
475
|
* (node, role) targets the current `scope` should paint. Returns just the
|
|
@@ -318,8 +490,21 @@ export function resolvePaintScopeTargets(args: {
|
|
|
318
490
|
slotRolesOf: (node: AnyNode) => string[]
|
|
319
491
|
wallHit?: WallPaintHit
|
|
320
492
|
}): Array<{ nodeId: AnyNodeId; role: string }> {
|
|
321
|
-
const { node, role,
|
|
493
|
+
const { node, role, nodes, spaces, slotRolesOf, wallHit } = args
|
|
494
|
+
const plateScopes = platePaintScopes(node, role)?.scopes
|
|
495
|
+
const scope = plateScopes ? effectivePaintScope(args.scope, plateScopes) : args.scope
|
|
322
496
|
const single = [{ nodeId: node.id as AnyNodeId, role }]
|
|
497
|
+
if (node.type === 'slab' && parseFloorStepRole(role)) {
|
|
498
|
+
// A room's steps are drawn by whichever plates carry them.
|
|
499
|
+
const stepRole = paintScopeRole(node, role, scope)
|
|
500
|
+
const targets = Object.values(nodes)
|
|
501
|
+
.filter(
|
|
502
|
+
(other) =>
|
|
503
|
+
other.type === 'slab' && other.parentId === node.parentId && other.boundary === 'auto',
|
|
504
|
+
)
|
|
505
|
+
.map((other) => ({ nodeId: other.id, role: stepRole }))
|
|
506
|
+
return targets.length ? targets : [{ nodeId: node.id as AnyNodeId, role: stepRole }]
|
|
507
|
+
}
|
|
323
508
|
if (scope === 'single') return single
|
|
324
509
|
|
|
325
510
|
// Whole object: paint every slot of the clicked node. Generic across any
|
|
@@ -344,6 +529,13 @@ export function resolvePaintScopeTargets(args: {
|
|
|
344
529
|
if (!wallHit) return single
|
|
345
530
|
const levelId = wall.parentId ?? resolveLevelId(wall, nodes)
|
|
346
531
|
if (!levelId) return single
|
|
532
|
+
const face = wallRoleFace(role) ?? (wallHit.face === 'front' ? 'a' : 'b')
|
|
533
|
+
const zoneId =
|
|
534
|
+
WALL_ROOM_FACE_ROLE.exec(role)?.[1] ?? zoneAtWallHit(wall, face, wallHit.point, nodes)
|
|
535
|
+
if (zoneId) {
|
|
536
|
+
const targets = wallRoomTargets(wall, role, zoneId, nodes)
|
|
537
|
+
if (targets.length > 0) return targets
|
|
538
|
+
}
|
|
347
539
|
const space = resolveWallPaintSpace({ wall, wallHit, nodes, spaces })
|
|
348
540
|
const boundaries = space
|
|
349
541
|
? space.boundaryFaces
|
|
@@ -353,6 +545,24 @@ export function resolvePaintScopeTargets(args: {
|
|
|
353
545
|
}
|
|
354
546
|
|
|
355
547
|
if (node.type === 'slab' && scope === 'room') {
|
|
548
|
+
// A room's floor in the room scope: every finish source of that room (its
|
|
549
|
+
// floor, painted parts, steps and edge) on every plate of the level, as one
|
|
550
|
+
// `room:<zoneId>/*` role the zone commits.
|
|
551
|
+
const floor = parseRoomFinishRole(role)
|
|
552
|
+
if (floor) {
|
|
553
|
+
const roomWide = `room:${floor.zoneId}/*`
|
|
554
|
+
const plates = Object.values(nodes).filter(
|
|
555
|
+
(other) =>
|
|
556
|
+
other.type === 'slab' && other.parentId === node.parentId && other.boundary === 'auto',
|
|
557
|
+
)
|
|
558
|
+
return (plates.length ? plates : [node]).map((plate) => ({
|
|
559
|
+
nodeId: plate.id as AnyNodeId,
|
|
560
|
+
role: roomWide,
|
|
561
|
+
}))
|
|
562
|
+
}
|
|
563
|
+
// A floor plate's own faces have no room scope; below is the hand-drawn
|
|
564
|
+
// slab's spread over the slabs of its room.
|
|
565
|
+
if (isFloorPlate(node)) return single
|
|
356
566
|
const centroid = polygonCentroid((node as SlabNode).polygon)
|
|
357
567
|
if (!centroid) return single
|
|
358
568
|
// Space polygons are per-level footprints, and stacked storeys share a
|
|
@@ -386,7 +596,7 @@ function materialsEqual(a: unknown, b: unknown): boolean {
|
|
|
386
596
|
if (Object.is(a, b)) return true
|
|
387
597
|
if (typeof a !== typeof b || a === null || b === null) return false
|
|
388
598
|
if (Array.isArray(a) || Array.isArray(b)) {
|
|
389
|
-
if (!Array.isArray(a)
|
|
599
|
+
if (!(Array.isArray(a) && Array.isArray(b)) || a.length !== b.length) return false
|
|
390
600
|
return a.every((value, index) => materialsEqual(value, b[index]))
|
|
391
601
|
}
|
|
392
602
|
if (typeof a === 'object') {
|
|
@@ -401,18 +611,49 @@ function materialsEqual(a: unknown, b: unknown): boolean {
|
|
|
401
611
|
return false
|
|
402
612
|
}
|
|
403
613
|
|
|
614
|
+
/**
|
|
615
|
+
* Split a fan-out into slot writes and roles a kind routes elsewhere — a wall's
|
|
616
|
+
* `room:<zoneId>` paints the zone, through the wall's own commit, once per role.
|
|
617
|
+
* Targets without a paint capability (the zone listed for a one-wall room) only
|
|
618
|
+
* ride along for the preview.
|
|
619
|
+
*/
|
|
620
|
+
function partitionFanout(targets: ReadonlyArray<{ nodeId: AnyNodeId; role: string }>) {
|
|
621
|
+
const nodes = useScene.getState().nodes
|
|
622
|
+
const routed = new Map<string, { nodeId: AnyNodeId; role: string }>()
|
|
623
|
+
const slots: Array<{ nodeId: AnyNodeId; role: string }> = []
|
|
624
|
+
for (const target of targets) {
|
|
625
|
+
const node = nodes[target.nodeId]
|
|
626
|
+
if (!node) continue
|
|
627
|
+
const capabilities = nodeRegistry.get(node.type)?.capabilities
|
|
628
|
+
const declared = capabilities?.slots?.(node)
|
|
629
|
+
const isSlot = declared?.length
|
|
630
|
+
? declared.some((slot) => slot.slotId === target.role)
|
|
631
|
+
: !target.role.includes(':')
|
|
632
|
+
const key = target.role.includes(':') ? target.role : `${target.nodeId}:${target.role}`
|
|
633
|
+
if (isSlot) slots.push(target)
|
|
634
|
+
else if (capabilities?.paint?.commit && !routed.has(key)) routed.set(key, target)
|
|
635
|
+
}
|
|
636
|
+
return { routed: [...routed.values()], slots }
|
|
637
|
+
}
|
|
638
|
+
|
|
404
639
|
/**
|
|
405
640
|
* Apply one paint to many slot-model targets in a single undo step. Resolves
|
|
406
641
|
* the slot ref ONCE — a one-off colour creates a single shared scene material
|
|
407
642
|
* for the whole fan-out, not one per node — then writes every `node.slots[role]`
|
|
408
|
-
* (or deletes it, for the eraser) in one `useScene.setState`.
|
|
409
|
-
*
|
|
643
|
+
* (or deletes it, for the eraser) in one `useScene.setState`. Routed roles
|
|
644
|
+
* (see `partitionFanout`) commit through their kind instead.
|
|
410
645
|
*/
|
|
411
646
|
export function commitPaintScopeFanout(
|
|
412
|
-
|
|
647
|
+
fanout: ReadonlyArray<{ nodeId: AnyNodeId; role: string }>,
|
|
413
648
|
material: MaterialSchema | undefined,
|
|
414
649
|
materialPreset: string | undefined,
|
|
415
650
|
): void {
|
|
651
|
+
const { routed, slots: targets } = partitionFanout(fanout)
|
|
652
|
+
for (const { nodeId, role } of routed) {
|
|
653
|
+
const node = useScene.getState().nodes[nodeId]
|
|
654
|
+
const paint = node ? nodeRegistry.get(node.type)?.capabilities?.paint : undefined
|
|
655
|
+
if (node && paint?.commit) paint.commit({ node, role, material, materialPreset })
|
|
656
|
+
}
|
|
416
657
|
if (targets.length === 0) return
|
|
417
658
|
const state = useScene.getState()
|
|
418
659
|
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type { AnyNode, AnyNodeDefinition, AnyNodeId } from '@pascal-app/core'
|
|
2
|
+
|
|
3
|
+
export type PanelIcon = { color?: string; src?: string }
|
|
4
|
+
|
|
5
|
+
export type PanelAction = {
|
|
6
|
+
icon?: PanelIcon
|
|
7
|
+
disabled?: boolean
|
|
8
|
+
id: string
|
|
9
|
+
label: string
|
|
10
|
+
onSelect?: () => void
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type PanelRow = { section?: string } & (
|
|
14
|
+
| {
|
|
15
|
+
id: string
|
|
16
|
+
kind: 'action'
|
|
17
|
+
icon?: PanelIcon
|
|
18
|
+
label: string
|
|
19
|
+
onSelect?: () => void
|
|
20
|
+
disabled?: boolean
|
|
21
|
+
}
|
|
22
|
+
| {
|
|
23
|
+
actions: PanelAction[]
|
|
24
|
+
id: string
|
|
25
|
+
kind: 'actions'
|
|
26
|
+
}
|
|
27
|
+
| {
|
|
28
|
+
id: string
|
|
29
|
+
kind: 'choice'
|
|
30
|
+
label: string
|
|
31
|
+
onSelect?: () => void
|
|
32
|
+
value: string
|
|
33
|
+
}
|
|
34
|
+
| {
|
|
35
|
+
getValue: () => string
|
|
36
|
+
id: string
|
|
37
|
+
kind: 'subscribed-choice'
|
|
38
|
+
label: string
|
|
39
|
+
onSelect?: () => void
|
|
40
|
+
subscribe: (listener: () => void) => () => void
|
|
41
|
+
}
|
|
42
|
+
| {
|
|
43
|
+
id: string
|
|
44
|
+
kind: 'cycle'
|
|
45
|
+
label: string
|
|
46
|
+
next: () => void
|
|
47
|
+
previous: () => void
|
|
48
|
+
value: string
|
|
49
|
+
}
|
|
50
|
+
| {
|
|
51
|
+
id: string
|
|
52
|
+
kind: 'stepper'
|
|
53
|
+
label: string
|
|
54
|
+
max: number
|
|
55
|
+
min: number
|
|
56
|
+
onChange: (value: number) => void
|
|
57
|
+
step: number
|
|
58
|
+
unit?: string
|
|
59
|
+
value: number
|
|
60
|
+
}
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
export const PANEL_MODEL_EXTENSION = 'pascal:editor/panel-model'
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* A control the multi-selection inspector renders for every selected node of
|
|
67
|
+
* a kind at once (derived settings the raw parametrics can't express). `value`
|
|
68
|
+
* null means the selection is mixed: no option shows as active.
|
|
69
|
+
*/
|
|
70
|
+
export type MultiPanelControl = {
|
|
71
|
+
id: string
|
|
72
|
+
kind: 'segmented'
|
|
73
|
+
/** The parametrics group title the control sits in. */
|
|
74
|
+
section: string
|
|
75
|
+
label: string
|
|
76
|
+
options: ReadonlyArray<{ value: string; label: string }>
|
|
77
|
+
value: string | null
|
|
78
|
+
/** Applies to every selected node in one undo step. */
|
|
79
|
+
onChange: (value: string) => void
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export type NodePanelModel<N extends AnyNode = AnyNode> = {
|
|
83
|
+
rows: (context: {
|
|
84
|
+
node: N
|
|
85
|
+
nodes: Record<AnyNodeId, AnyNode>
|
|
86
|
+
update: (patch: Partial<N>) => void
|
|
87
|
+
}) => PanelRow[]
|
|
88
|
+
multiControls?: (context: {
|
|
89
|
+
selection: N[]
|
|
90
|
+
nodes: Record<AnyNodeId, AnyNode>
|
|
91
|
+
}) => MultiPanelControl[]
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function getNodePanelModel(definition: AnyNodeDefinition | undefined) {
|
|
95
|
+
return definition?.extensions?.[PANEL_MODEL_EXTENSION] as NodePanelModel | undefined
|
|
96
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { expect, test } from 'bun:test'
|
|
2
|
+
import { createToolHintsStore } from './panel-tool-options'
|
|
3
|
+
|
|
4
|
+
test('tool hints share subscriptions, react to visibility and chip state, and unsubscribe', () => {
|
|
5
|
+
let visible = false
|
|
6
|
+
let value = 'auto'
|
|
7
|
+
let subscriptions = 0
|
|
8
|
+
let cleanups = 0
|
|
9
|
+
const subscribe = (_listener: () => void) => {
|
|
10
|
+
subscriptions++
|
|
11
|
+
return () => {
|
|
12
|
+
cleanups++
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
const store = createToolHintsStore([
|
|
16
|
+
{
|
|
17
|
+
key: 'P',
|
|
18
|
+
label: 'Placement',
|
|
19
|
+
visible: { subscribe, value: () => visible },
|
|
20
|
+
chip: { subscribe, value: () => value, cycle: () => {}, labels: { auto: 'Auto' } },
|
|
21
|
+
},
|
|
22
|
+
])
|
|
23
|
+
const cleanup = store.subscribe(() => {})
|
|
24
|
+
expect(subscriptions).toBe(1)
|
|
25
|
+
const hidden = store.getSnapshot()
|
|
26
|
+
visible = true
|
|
27
|
+
expect(store.getSnapshot()).not.toBe(hidden)
|
|
28
|
+
const shown = store.getSnapshot()
|
|
29
|
+
value = 'roof'
|
|
30
|
+
expect(store.getSnapshot()).not.toBe(shown)
|
|
31
|
+
expect(store.getSnapshot()).toBe(store.getSnapshot())
|
|
32
|
+
cleanup()
|
|
33
|
+
expect(cleanups).toBe(1)
|
|
34
|
+
})
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { nodeRegistry, type ToolHint, useRegistryVersion } from '@pascal-app/core'
|
|
4
|
+
import { useMemo, useSyncExternalStore } from 'react'
|
|
5
|
+
import useEditor from '../store/use-editor'
|
|
6
|
+
|
|
7
|
+
export type PanelToolOption = {
|
|
8
|
+
id: string
|
|
9
|
+
label: string
|
|
10
|
+
value: string
|
|
11
|
+
choices: readonly { value: string; label: string; description?: string }[]
|
|
12
|
+
set: (value: string) => void
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function createToolHintsStore(hints: readonly ToolHint[]) {
|
|
16
|
+
return {
|
|
17
|
+
subscribe(listener: () => void) {
|
|
18
|
+
const subscriptions = hints.flatMap((hint) => [hint.chip?.subscribe, hint.visible?.subscribe])
|
|
19
|
+
const cleanups = [...new Set(subscriptions)].flatMap((subscribe) =>
|
|
20
|
+
subscribe ? [subscribe(listener)] : [],
|
|
21
|
+
)
|
|
22
|
+
return () => {
|
|
23
|
+
for (const cleanup of cleanups) cleanup()
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
getSnapshot: () =>
|
|
27
|
+
JSON.stringify(hints.map((hint) => [hint.visible?.value() ?? true, hint.chip?.value()])),
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function useVisibleToolHints(hints: readonly ToolHint[] = []) {
|
|
32
|
+
const draftVertexCount = useEditor((state) => state.draftVertexCount)
|
|
33
|
+
const store = useMemo(() => createToolHintsStore(hints), [hints])
|
|
34
|
+
useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot)
|
|
35
|
+
return hints.filter(
|
|
36
|
+
(hint) =>
|
|
37
|
+
!(hint.key === 'Shift' && hint.label === 'Cycle snapping mode') &&
|
|
38
|
+
(hint.visible?.value() ?? true) &&
|
|
39
|
+
(hint.minDraftVertices == null || draftVertexCount >= hint.minDraftVertices),
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function usePanelToolHints(kind: string | null | undefined) {
|
|
44
|
+
useRegistryVersion()
|
|
45
|
+
return useVisibleToolHints(kind ? nodeRegistry.get(kind)?.toolHints : undefined)
|
|
46
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type AnyNode,
|
|
3
|
+
type AnyNodeId,
|
|
4
|
+
nodeRegistry,
|
|
5
|
+
type ParametricDescriptor,
|
|
6
|
+
useScene,
|
|
7
|
+
} from '@pascal-app/core'
|
|
8
|
+
|
|
9
|
+
export function commitParametricNodeFields(
|
|
10
|
+
nodeId: AnyNodeId,
|
|
11
|
+
requestedPatch: Partial<AnyNode>,
|
|
12
|
+
): void {
|
|
13
|
+
const scene = useScene.getState()
|
|
14
|
+
const node = scene.nodes[nodeId]
|
|
15
|
+
if (!node) return
|
|
16
|
+
|
|
17
|
+
const parametrics = nodeRegistry.get(node.type)?.parametrics as
|
|
18
|
+
| ParametricDescriptor<AnyNode>
|
|
19
|
+
| undefined
|
|
20
|
+
let patch = requestedPatch as Record<string, unknown>
|
|
21
|
+
if (parametrics?.derive) {
|
|
22
|
+
const next = { ...node, ...patch } as AnyNode
|
|
23
|
+
patch = {
|
|
24
|
+
...patch,
|
|
25
|
+
...parametrics.derive(next, patch as Partial<AnyNode>, node),
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const updates: { id: AnyNodeId; data: Partial<AnyNode> }[] = [
|
|
30
|
+
{ id: nodeId, data: patch as Partial<AnyNode> },
|
|
31
|
+
]
|
|
32
|
+
if (parametrics?.reconcile) {
|
|
33
|
+
const next = { ...node, ...patch } as AnyNode
|
|
34
|
+
updates.push(...parametrics.reconcile(node, next, { ...scene.nodes, [nodeId]: next }))
|
|
35
|
+
}
|
|
36
|
+
scene.updateNodes(updates)
|
|
37
|
+
}
|