@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
|
@@ -66,12 +66,7 @@ export function buildSvgArcPath(
|
|
|
66
66
|
endAngle: number,
|
|
67
67
|
) {
|
|
68
68
|
const start = getArcPlanPoint(center, radius, startAngle)
|
|
69
|
-
|
|
70
|
-
const delta = endAngle - startAngle
|
|
71
|
-
const largeArcFlag = Math.abs(delta) > Math.PI ? 1 : 0
|
|
72
|
-
const sweepFlag = delta >= 0 ? 1 : 0
|
|
73
|
-
|
|
74
|
-
return `M ${toSvgX(start.x)} ${toSvgY(start.y)} A ${radius} ${radius} 0 ${largeArcFlag} ${sweepFlag} ${toSvgX(end.x)} ${toSvgY(end.y)}`
|
|
69
|
+
return `M ${toSvgX(start.x)} ${toSvgY(start.y)} ${svgArcCommands(center, radius, startAngle, endAngle)}`
|
|
75
70
|
}
|
|
76
71
|
|
|
77
72
|
export function buildSvgAnnularSectorPath(
|
|
@@ -82,23 +77,25 @@ export function buildSvgAnnularSectorPath(
|
|
|
82
77
|
endAngle: number,
|
|
83
78
|
) {
|
|
84
79
|
const outerStart = getArcPlanPoint(center, outerRadius, startAngle)
|
|
85
|
-
const outerEnd = getArcPlanPoint(center, outerRadius, endAngle)
|
|
86
80
|
const innerEnd = getArcPlanPoint(center, innerRadius, endAngle)
|
|
87
|
-
const innerStart = getArcPlanPoint(center, innerRadius, startAngle)
|
|
88
|
-
const delta = endAngle - startAngle
|
|
89
|
-
const largeArcFlag = Math.abs(delta) > Math.PI ? 1 : 0
|
|
90
|
-
const sweepFlag = delta >= 0 ? 1 : 0
|
|
91
|
-
const reverseSweepFlag = sweepFlag ? 0 : 1
|
|
92
|
-
|
|
93
81
|
return [
|
|
94
82
|
`M ${toSvgX(outerStart.x)} ${toSvgY(outerStart.y)}`,
|
|
95
|
-
|
|
83
|
+
svgArcCommands(center, outerRadius, startAngle, endAngle),
|
|
96
84
|
`L ${toSvgX(innerEnd.x)} ${toSvgY(innerEnd.y)}`,
|
|
97
|
-
|
|
85
|
+
svgArcCommands(center, innerRadius, endAngle, startAngle),
|
|
98
86
|
'Z',
|
|
99
87
|
].join(' ')
|
|
100
88
|
}
|
|
101
89
|
|
|
90
|
+
function svgArcCommands(center: Point2D, radius: number, startAngle: number, endAngle: number) {
|
|
91
|
+
const delta = endAngle - startAngle
|
|
92
|
+
const count = Math.max(1, Math.ceil(Math.abs(delta) / Math.PI))
|
|
93
|
+
return Array.from({ length: count }, (_, index) => {
|
|
94
|
+
const end = getArcPlanPoint(center, radius, startAngle + (delta * (index + 1)) / count)
|
|
95
|
+
return `A ${radius} ${radius} 0 0 ${delta >= 0 ? 1 : 0} ${toSvgX(end.x)} ${toSvgY(end.y)}`
|
|
96
|
+
}).join(' ')
|
|
97
|
+
}
|
|
98
|
+
|
|
102
99
|
export function formatSvgPolygonPoints(points: Point2D[]) {
|
|
103
100
|
return points.map((point) => `${toSvgX(point.x)},${toSvgY(point.y)}`).join(' ')
|
|
104
101
|
}
|
|
@@ -9,7 +9,9 @@ import { useViewer } from '@pascal-app/viewer'
|
|
|
9
9
|
import { useEffect, useState } from 'react'
|
|
10
10
|
import { useHandleGroup } from '../../store/use-handle-group'
|
|
11
11
|
import { PanelSection } from '../ui/controls/panel-section'
|
|
12
|
+
import { SegmentedControl } from '../ui/controls/segmented-control'
|
|
12
13
|
import { SliderControl } from '../ui/controls/slider-control'
|
|
14
|
+
import { ToggleControl } from '../ui/controls/toggle-control'
|
|
13
15
|
import { PanelWrapper } from '../ui/panels/panel-wrapper'
|
|
14
16
|
export function ProceduralInspector({
|
|
15
17
|
nodeId,
|
|
@@ -75,21 +77,44 @@ export function ProceduralInspector({
|
|
|
75
77
|
)}
|
|
76
78
|
</div>
|
|
77
79
|
<div className="space-y-3">
|
|
78
|
-
{fields.map((p) =>
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
80
|
+
{fields.map((p) => {
|
|
81
|
+
const value = node.parameters[p.id] ?? p.default
|
|
82
|
+
if (p.unit === 'bool')
|
|
83
|
+
return (
|
|
84
|
+
<ToggleControl
|
|
85
|
+
key={p.id}
|
|
86
|
+
label={p.label}
|
|
87
|
+
checked={value === 1}
|
|
88
|
+
onChange={(checked) => change(p.id, checked ? 1 : 0, true)}
|
|
89
|
+
/>
|
|
90
|
+
)
|
|
91
|
+
if (p.unit === 'choice' && p.options && p.options.length <= 4)
|
|
92
|
+
return (
|
|
93
|
+
<div key={p.id} className="space-y-1.5">
|
|
94
|
+
<div className="text-muted-foreground text-xs">{p.label}</div>
|
|
95
|
+
<SegmentedControl
|
|
96
|
+
value={String(value)}
|
|
97
|
+
options={p.options.map((label, index) => ({ label, value: String(index) }))}
|
|
98
|
+
onChange={(index) => change(p.id, Number(index), true)}
|
|
99
|
+
/>
|
|
100
|
+
</div>
|
|
101
|
+
)
|
|
102
|
+
return (
|
|
103
|
+
<SliderControl
|
|
104
|
+
key={p.id}
|
|
105
|
+
label={p.unit === 'choice' ? `${p.label}: ${p.options?.[value] ?? value}` : p.label}
|
|
106
|
+
value={value}
|
|
107
|
+
min={p.min}
|
|
108
|
+
max={p.max}
|
|
109
|
+
step={p.step}
|
|
110
|
+
precision={p.unit === 'choice' ? 0 : 3}
|
|
111
|
+
unit={p.unit === 'm' ? 'm' : ''}
|
|
112
|
+
onChange={(v) => change(p.id, v, false)}
|
|
113
|
+
onCommit={(v) => change(p.id, v, true)}
|
|
114
|
+
restoreOnCommit={false}
|
|
115
|
+
/>
|
|
116
|
+
)
|
|
117
|
+
})}
|
|
93
118
|
</div>
|
|
94
119
|
{partId && (
|
|
95
120
|
<button
|
|
@@ -12,6 +12,7 @@ import { useViewer } from '@pascal-app/viewer'
|
|
|
12
12
|
import { _roots, act, createRoot, events, extend, type RootState } from '@react-three/fiber'
|
|
13
13
|
import { createElement } from 'react'
|
|
14
14
|
import { Group, InstancedMesh, OrthographicCamera, Vector3, type WebGLRenderer } from 'three'
|
|
15
|
+
import { endCeilingEdit, startCeilingEdit } from '../../../lib/ceiling-edit-session'
|
|
15
16
|
import { sfxEmitter } from '../../../lib/sfx-bus'
|
|
16
17
|
import useEditor from '../../../store/use-editor'
|
|
17
18
|
import useInteractionScope from '../../../store/use-interaction-scope'
|
|
@@ -33,7 +34,13 @@ type BracketHarness = {
|
|
|
33
34
|
sounds: string[]
|
|
34
35
|
}
|
|
35
36
|
|
|
36
|
-
async function withMountedBrackets(
|
|
37
|
+
async function withMountedBrackets(
|
|
38
|
+
run: (harness: BracketHarness) => Promise<void>,
|
|
39
|
+
{
|
|
40
|
+
ceilingData = {},
|
|
41
|
+
session = true,
|
|
42
|
+
}: { ceilingData?: Partial<CeilingNode>; session?: boolean } = {},
|
|
43
|
+
) {
|
|
37
44
|
const previousScene = useScene.getState()
|
|
38
45
|
const previousViewer = useViewer.getState()
|
|
39
46
|
const previousEditor = useEditor.getState()
|
|
@@ -54,6 +61,7 @@ async function withMountedBrackets(run: (harness: BracketHarness) => Promise<voi
|
|
|
54
61
|
const building = BuildingNode.parse({})
|
|
55
62
|
const level = LevelNode.parse({ parentId: building.id, height: 3 })
|
|
56
63
|
const ceiling = CeilingNode.parse({
|
|
64
|
+
...ceilingData,
|
|
57
65
|
parentId: level.id,
|
|
58
66
|
height: 3,
|
|
59
67
|
polygon: [
|
|
@@ -81,8 +89,9 @@ async function withMountedBrackets(run: (harness: BracketHarness) => Promise<voi
|
|
|
81
89
|
hoveredId: null,
|
|
82
90
|
selection: { buildingId: building.id, levelId: level.id, zoneId: null, selectedIds: [] },
|
|
83
91
|
})
|
|
84
|
-
useEditor.setState({ phase: '
|
|
92
|
+
useEditor.setState({ phase: 'building', mode: 'select' })
|
|
85
93
|
useInteractionScope.setState({ scope: { kind: 'idle' } })
|
|
94
|
+
if (session) startCeilingEdit(ceiling.id)
|
|
86
95
|
const camera = Object.assign(new OrthographicCamera(-1, 5, 5, -1, 0.1, 100), { manual: true })
|
|
87
96
|
camera.position.set(0, 10, 0)
|
|
88
97
|
camera.up.set(0, 0, -1)
|
|
@@ -145,7 +154,10 @@ async function withMountedBrackets(run: (harness: BracketHarness) => Promise<voi
|
|
|
145
154
|
sounds,
|
|
146
155
|
})
|
|
147
156
|
} finally {
|
|
148
|
-
await act(async () =>
|
|
157
|
+
await act(async () => {
|
|
158
|
+
endCeilingEdit()
|
|
159
|
+
root.render(null)
|
|
160
|
+
})
|
|
149
161
|
clearBoxSelectHandled()
|
|
150
162
|
emitter.off('ceiling:click', onClick)
|
|
151
163
|
sfxEmitter.off('*', onSound)
|
|
@@ -186,6 +198,7 @@ test('mounted brackets preserve hover, clicks, drags, capture visibility, overri
|
|
|
186
198
|
await dispatch('onPointerDown', 4, 0)
|
|
187
199
|
|
|
188
200
|
const oldNormal = meshes().find((mesh) => mesh.renderOrder === 1000)
|
|
201
|
+
// Ceilings outside the session get no brackets.
|
|
189
202
|
await act(async () => {
|
|
190
203
|
const additions = Array.from({ length: 8 }, (_, index) =>
|
|
191
204
|
CeilingNode.parse({
|
|
@@ -206,12 +219,43 @@ test('mounted brackets preserve hover, clicks, drags, capture visibility, overri
|
|
|
206
219
|
},
|
|
207
220
|
})
|
|
208
221
|
})
|
|
222
|
+
expect(meshes().find((mesh) => mesh.renderOrder === 1000)).toBe(oldNormal)
|
|
223
|
+
expect(meshes().reduce((sum, mesh) => sum + mesh.count, 0)).toBe(12)
|
|
224
|
+
// Twelve corners overflow the initial capacity: the batch meshes are replaced
|
|
225
|
+
// while the pressed corner keeps its index.
|
|
226
|
+
await act(async () =>
|
|
227
|
+
useScene.setState({
|
|
228
|
+
nodes: {
|
|
229
|
+
...useScene.getState().nodes,
|
|
230
|
+
[ceiling.id]: CeilingNode.parse({
|
|
231
|
+
...ceiling,
|
|
232
|
+
polygon: [
|
|
233
|
+
[0, 0],
|
|
234
|
+
[4, 0],
|
|
235
|
+
[4, 1],
|
|
236
|
+
[4, 2],
|
|
237
|
+
[4, 3],
|
|
238
|
+
[4, 4],
|
|
239
|
+
[3, 4],
|
|
240
|
+
[2, 4],
|
|
241
|
+
[1, 4],
|
|
242
|
+
[0, 4],
|
|
243
|
+
[0, 3],
|
|
244
|
+
[0, 2],
|
|
245
|
+
],
|
|
246
|
+
}),
|
|
247
|
+
},
|
|
248
|
+
}),
|
|
249
|
+
)
|
|
209
250
|
expect(meshes()).toHaveLength(2)
|
|
210
251
|
expect(meshes().find((mesh) => mesh.renderOrder === 1000)).not.toBe(oldNormal)
|
|
211
252
|
expect(useViewer.getState().hoveredId).toBe(ceiling.id)
|
|
212
253
|
await dispatch('onClick', 4, 0)
|
|
213
254
|
expect(clicks).toHaveLength(2)
|
|
214
255
|
expect(clicks[1].position).toEqual([4, 3, 0])
|
|
256
|
+
await act(async () =>
|
|
257
|
+
useScene.setState({ nodes: { ...useScene.getState().nodes, [ceiling.id]: ceiling } }),
|
|
258
|
+
)
|
|
215
259
|
await dispatch('onPointerLeave', 4, 0)
|
|
216
260
|
expect(useViewer.getState().hoveredId).toBeNull()
|
|
217
261
|
|
|
@@ -277,7 +321,7 @@ test('mounted brackets preserve hover, clicks, drags, capture visibility, overri
|
|
|
277
321
|
)
|
|
278
322
|
})
|
|
279
323
|
|
|
280
|
-
test('
|
|
324
|
+
test('an adjacent ceiling outside the session has no brackets at a shared corner', async () => {
|
|
281
325
|
await withMountedBrackets(async ({ ceiling, dispatch, dispatchWindow, meshes, clicks }) => {
|
|
282
326
|
const adjacent = CeilingNode.parse({
|
|
283
327
|
...ceiling,
|
|
@@ -294,25 +338,64 @@ test('coincident ceiling corners keep the same hover, click, and drag owner acro
|
|
|
294
338
|
nodes: { ...useScene.getState().nodes, [adjacent.id]: adjacent },
|
|
295
339
|
}),
|
|
296
340
|
)
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
await dispatch('onPointerMove', 0, 0)
|
|
301
|
-
expect(useViewer.getState().hoveredId).toBe(owner.id)
|
|
302
|
-
expect(meshes().find((mesh) => mesh.renderOrder === 1001)!.count).toBe(7)
|
|
303
|
-
}
|
|
341
|
+
expect(meshes().reduce((sum, mesh) => sum + mesh.count, 0)).toBe(12)
|
|
342
|
+
await dispatch('onPointerMove', 0, 0)
|
|
343
|
+
expect(useViewer.getState().hoveredId).toBe(ceiling.id)
|
|
304
344
|
await dispatch('onPointerDown', 0, 0)
|
|
305
345
|
await dispatch('onClick', 0, 0)
|
|
306
|
-
expect(clicks[0].node.id).toBe(
|
|
346
|
+
expect(clicks[0].node.id).toBe(ceiling.id)
|
|
307
347
|
await dispatchWindow('pointermove', 0.5, 0.5)
|
|
308
|
-
expect(useLiveNodeOverrides.getState().overrides.has(
|
|
309
|
-
expect(useLiveNodeOverrides.getState().overrides.has(
|
|
348
|
+
expect(useLiveNodeOverrides.getState().overrides.has(ceiling.id)).toBe(true)
|
|
349
|
+
expect(useLiveNodeOverrides.getState().overrides.has(adjacent.id)).toBe(false)
|
|
310
350
|
await dispatchWindow('pointercancel', 0.5, 0.5)
|
|
311
351
|
await dispatch('onPointerLeave', 0, 0)
|
|
312
352
|
expect(useViewer.getState().hoveredId).toBeNull()
|
|
313
353
|
})
|
|
314
354
|
})
|
|
315
355
|
|
|
356
|
+
test('brackets and their hit boxes exist only inside the Edit ceiling session', async () => {
|
|
357
|
+
await withMountedBrackets(
|
|
358
|
+
async ({ ceiling, dispatch, meshes, levelObject }) => {
|
|
359
|
+
// A canvas selection of the ceiling alone shows nothing.
|
|
360
|
+
await act(async () => useViewer.getState().setSelection({ selectedIds: [ceiling.id] }))
|
|
361
|
+
expect(meshes()).toHaveLength(0)
|
|
362
|
+
expect(levelObject.children).toHaveLength(0)
|
|
363
|
+
await dispatch('onPointerMove', 0, 0)
|
|
364
|
+
expect(useViewer.getState().hoveredId).toBeNull()
|
|
365
|
+
|
|
366
|
+
await act(async () => {
|
|
367
|
+
startCeilingEdit(ceiling.id)
|
|
368
|
+
})
|
|
369
|
+
expect(meshes().reduce((sum, mesh) => sum + mesh.count, 0)).toBe(12)
|
|
370
|
+
|
|
371
|
+
await act(async () => useViewer.getState().setSelection({ selectedIds: [] }))
|
|
372
|
+
expect(meshes()).toHaveLength(0)
|
|
373
|
+
expect(levelObject.children).toHaveLength(0)
|
|
374
|
+
},
|
|
375
|
+
{ session: false },
|
|
376
|
+
)
|
|
377
|
+
})
|
|
378
|
+
|
|
379
|
+
test('a corner drag detaches an auto ceiling; the session stays open', async () => {
|
|
380
|
+
await withMountedBrackets(
|
|
381
|
+
async ({ ceiling, dispatch, dispatchWindow, meshes }) => {
|
|
382
|
+
expect(ceiling.boundary).toBe('auto')
|
|
383
|
+
await dispatch('onPointerDown', 0, 0)
|
|
384
|
+
await dispatchWindow('pointermove', 0.5, 0.5)
|
|
385
|
+
const preview = useLiveNodeOverrides.getState().overrides.get(ceiling.id)
|
|
386
|
+
?.polygon as CeilingNode['polygon']
|
|
387
|
+
await dispatchWindow('pointerup', 0.5, 0.5)
|
|
388
|
+
const committed = useScene.getState().nodes[ceiling.id] as CeilingNode
|
|
389
|
+
expect(committed.polygon).toEqual(preview)
|
|
390
|
+
expect(committed.boundary).toBeUndefined()
|
|
391
|
+
expect(committed.zoneId).toBeUndefined()
|
|
392
|
+
expect(useViewer.getState().selection.selectedIds).toEqual([ceiling.id])
|
|
393
|
+
expect(meshes().reduce((sum, mesh) => sum + mesh.count, 0)).toBe(12)
|
|
394
|
+
},
|
|
395
|
+
{ ceilingData: { boundary: 'auto', zoneId: 'zone_auto_ceiling' } },
|
|
396
|
+
)
|
|
397
|
+
})
|
|
398
|
+
|
|
316
399
|
test('same-id registry replacement rebinds the portal and drag plane without rewriting instance data', async () => {
|
|
317
400
|
await withMountedBrackets(
|
|
318
401
|
async ({ ceiling, level, levelObject, state, dispatch, dispatchWindow, meshes, clicks }) => {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
|
+
type AnyNodeId,
|
|
4
5
|
type CeilingNode,
|
|
5
6
|
emitter,
|
|
6
7
|
resolveCeilingHeight,
|
|
@@ -24,6 +25,7 @@ import {
|
|
|
24
25
|
} from 'react'
|
|
25
26
|
import { Group, type Object3D, Plane, Raycaster, Vector2, Vector3 } from 'three'
|
|
26
27
|
import { useShallow } from 'zustand/react/shallow'
|
|
28
|
+
import { useCeilingEditCeilingId } from '../../../lib/ceiling-edit-session'
|
|
27
29
|
import {
|
|
28
30
|
clearCeilingSnapFeedback,
|
|
29
31
|
resolveCeilingPlanPointSnap,
|
|
@@ -94,9 +96,7 @@ function clearCornerDragPreview(drag: CornerDragState) {
|
|
|
94
96
|
}
|
|
95
97
|
|
|
96
98
|
export const CeilingSelectionAffordanceSystem = () => {
|
|
97
|
-
const phase = useEditor((state) => state.phase)
|
|
98
99
|
const mode = useEditor((state) => state.mode)
|
|
99
|
-
const structureLayer = useEditor((state) => state.structureLayer)
|
|
100
100
|
// ANY active interaction (moving/placing a node, reshaping a boundary or
|
|
101
101
|
// curve, dragging a handle) unmounts the brackets: their ceiling-height hit
|
|
102
102
|
// boxes would otherwise catch drag-time hover, set `hoveredId` to the
|
|
@@ -105,26 +105,24 @@ export const CeilingSelectionAffordanceSystem = () => {
|
|
|
105
105
|
// scope, so it can't unmount itself.
|
|
106
106
|
const scopeIdle = useInteractionScope((state) => state.scope.kind === 'idle')
|
|
107
107
|
const currentLevelId = useViewer((state) => state.selection.levelId)
|
|
108
|
+
// Brackets and their hit boxes exist only for the ceiling in an explicit
|
|
109
|
+
// Edit ceiling session (`lib/ceiling-edit-session.ts`).
|
|
110
|
+
const editCeilingId = useCeilingEditCeilingId()
|
|
108
111
|
|
|
109
112
|
const ceilings = useScene(
|
|
110
|
-
useShallow((state) =>
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
),
|
|
113
|
+
useShallow((state) => {
|
|
114
|
+
const node = editCeilingId ? state.nodes[editCeilingId as AnyNodeId] : undefined
|
|
115
|
+
return node?.type === 'ceiling' &&
|
|
116
|
+
node.visible !== false &&
|
|
117
|
+
currentLevelId !== null &&
|
|
118
|
+
resolveLevelId(node, state.nodes) === currentLevelId
|
|
119
|
+
? [node]
|
|
120
|
+
: []
|
|
121
|
+
}),
|
|
120
122
|
)
|
|
121
123
|
|
|
122
124
|
const shouldRender =
|
|
123
|
-
|
|
124
|
-
mode === 'select' &&
|
|
125
|
-
structureLayer === 'elements' &&
|
|
126
|
-
scopeIdle &&
|
|
127
|
-
currentLevelId !== null
|
|
125
|
+
mode === 'select' && scopeIdle && currentLevelId !== null && ceilings.length > 0
|
|
128
126
|
|
|
129
127
|
if (!shouldRender) return null
|
|
130
128
|
|
|
@@ -528,7 +526,9 @@ const CeilingSelectionAffordance = memo(function CeilingSelectionAffordance({
|
|
|
528
526
|
suppressNextClick()
|
|
529
527
|
|
|
530
528
|
if (drag.previewPolygon) {
|
|
531
|
-
|
|
529
|
+
// A polygon edit converts a derived (`boundary: 'auto'`) ceiling into
|
|
530
|
+
// an authored one, like the boundary editor's commit.
|
|
531
|
+
useScene.getState().detachDerivedNode(drag.ceilingId, { polygon: drag.previewPolygon })
|
|
532
532
|
useViewer.getState().setSelection({ selectedIds: [drag.ceilingId] })
|
|
533
533
|
}
|
|
534
534
|
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { expect, test } from 'bun:test'
|
|
2
|
+
import { useScene, ZoneNode } from '@pascal-app/core'
|
|
3
|
+
import { useViewer } from '@pascal-app/viewer'
|
|
4
|
+
import { act, create } from '@react-three/test-renderer'
|
|
5
|
+
import { createElement } from 'react'
|
|
6
|
+
import useEditor from '../../../store/use-editor'
|
|
7
|
+
import { ZoneSystem } from './zone-system'
|
|
8
|
+
|
|
9
|
+
test('zone volumes mount only while a unit is painted, never for a selected zone', async () => {
|
|
10
|
+
const previousScene = useScene.getState()
|
|
11
|
+
const previousViewer = useViewer.getState()
|
|
12
|
+
const previousEditor = useEditor.getState()
|
|
13
|
+
const zone = ZoneNode.parse({
|
|
14
|
+
name: 'Courtyard',
|
|
15
|
+
polygon: [
|
|
16
|
+
[0, 0],
|
|
17
|
+
[4, 0],
|
|
18
|
+
[4, 4],
|
|
19
|
+
[0, 4],
|
|
20
|
+
],
|
|
21
|
+
})
|
|
22
|
+
useScene.setState({ nodes: { [zone.id]: zone } })
|
|
23
|
+
useViewer.setState({
|
|
24
|
+
focusedUnitId: null,
|
|
25
|
+
showZones: false,
|
|
26
|
+
selection: { buildingId: null, levelId: null, zoneId: null, selectedIds: [zone.id] },
|
|
27
|
+
})
|
|
28
|
+
useEditor.setState({ isCaptureMode: false })
|
|
29
|
+
const renderer = await create(createElement(ZoneSystem))
|
|
30
|
+
try {
|
|
31
|
+
// A selected drawn zone looks like a room: outline, no tinted volume.
|
|
32
|
+
expect(useViewer.getState().showZones).toBe(false)
|
|
33
|
+
await act(async () => useViewer.setState({ focusedUnitId: 'unit_paint' as never }))
|
|
34
|
+
expect(useViewer.getState().showZones).toBe(true)
|
|
35
|
+
await act(async () => useEditor.getState().setCaptureMode(true))
|
|
36
|
+
expect(useViewer.getState().showZones).toBe(false)
|
|
37
|
+
await act(async () => useEditor.getState().setCaptureMode(false))
|
|
38
|
+
expect(useViewer.getState().showZones).toBe(true)
|
|
39
|
+
await act(async () => useViewer.setState({ focusedUnitId: null }))
|
|
40
|
+
expect(useViewer.getState().showZones).toBe(false)
|
|
41
|
+
} finally {
|
|
42
|
+
await renderer.unmount()
|
|
43
|
+
useScene.setState(previousScene)
|
|
44
|
+
useViewer.setState(previousViewer)
|
|
45
|
+
useEditor.setState(previousEditor)
|
|
46
|
+
}
|
|
47
|
+
})
|
|
@@ -9,40 +9,32 @@ import useEditor from '../../../store/use-editor'
|
|
|
9
9
|
import useInteractionScope from '../../../store/use-interaction-scope'
|
|
10
10
|
|
|
11
11
|
// Disable raycasting on zone geometry so clicks pass through to items underneath.
|
|
12
|
-
// Zone selection in the editor is handled exclusively via the HTML label overlay.
|
|
13
12
|
const noopRaycast = () => {}
|
|
14
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Zone volumes are a working view for one job only: painting a unit's
|
|
16
|
+
* membership, where every zone on the level has to be in view. Otherwise every
|
|
17
|
+
* zone, drawn or detected, shows as a room: a light label pill (`RoomLabels3D`)
|
|
18
|
+
* and the room outline when selected. Mounting is the viewer's `showZones`
|
|
19
|
+
* flag: unmounting skips the meshes and drei labels entirely, and the cleanup
|
|
20
|
+
* restores the default so preview / first-person surfaces keep their labels.
|
|
21
|
+
*/
|
|
15
22
|
export const ZoneSystem = () => {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
// unmount cleanup restores the default so preview / first-person surfaces
|
|
20
|
-
// (which swap this system for ViewerZoneSystem) keep their labels.
|
|
21
|
-
const structureLayerState = useEditor((s) => s.structureLayer)
|
|
22
|
-
const isCaptureModeState = useEditor((s) => s.isCaptureMode)
|
|
23
|
+
const focusedUnitId = useViewer((s) => s.focusedUnitId)
|
|
24
|
+
const isCaptureMode = useEditor((s) => s.isCaptureMode)
|
|
25
|
+
const active = !!focusedUnitId && !isCaptureMode
|
|
23
26
|
useEffect(() => {
|
|
24
|
-
useViewer.getState().setShowZones(
|
|
27
|
+
useViewer.getState().setShowZones(active)
|
|
25
28
|
return () => useViewer.getState().setShowZones(true)
|
|
26
|
-
}, [
|
|
29
|
+
}, [active])
|
|
27
30
|
|
|
28
31
|
useFrame((_, delta) => {
|
|
29
32
|
if (!useViewer.getState().showZones) return
|
|
30
33
|
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
const selectedLevelId = useViewer.getState().selection.levelId
|
|
34
|
-
const selectedZoneId = useViewer.getState().selection.zoneId
|
|
35
|
-
const hoveredId = useViewer.getState().hoveredId
|
|
36
|
-
// Snapshot capture is a clean, camera-only surface — never show zone
|
|
37
|
-
// geometry or the HTML zone tags in the framed shot.
|
|
38
|
-
const isCaptureMode = useEditor.getState().isCaptureMode
|
|
39
|
-
|
|
40
|
-
// During any active interaction zone labels step back entirely — they are
|
|
41
|
-
// not a primary editing concern and would distract / invite misclicks.
|
|
34
|
+
const { levelId: selectedLevelId } = useViewer.getState().selection
|
|
35
|
+
// During any active interaction zone labels step back entirely.
|
|
42
36
|
const zoneLabelsHidden =
|
|
43
37
|
resolveOverlayPolicy(useInteractionScope.getState().scope).zoneLabels === 'hidden'
|
|
44
|
-
|
|
45
|
-
const zoneGeometryVisible = structureLayer === 'zones'
|
|
46
38
|
const zones = sceneRegistry.byType.zone || new Set()
|
|
47
39
|
const nodes = useScene.getState().nodes
|
|
48
40
|
const lerpSpeed = 10 * delta
|
|
@@ -52,59 +44,32 @@ export const ZoneSystem = () => {
|
|
|
52
44
|
if (!obj) return
|
|
53
45
|
|
|
54
46
|
const zone = nodes[zoneId as ZoneNode['id']] as ZoneNode | undefined
|
|
55
|
-
|
|
56
47
|
const isOnSelectedLevel = zone?.parentId === selectedLevelId
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
if (!obj.visible) obj.visible = true
|
|
63
|
-
const meshVisible = !isCaptureMode && (zoneGeometryVisible || isSelected || isDeleteHovered)
|
|
64
|
-
const targetOpacity = isCaptureMode
|
|
65
|
-
? 0
|
|
66
|
-
: isSelected || isDeleteHovered
|
|
67
|
-
? 1
|
|
68
|
-
: zoneGeometryVisible
|
|
69
|
-
? 1
|
|
70
|
-
: 0
|
|
71
|
-
|
|
72
|
-
// Raycast is re-disabled per frame (not once per group): the meshes
|
|
73
|
-
// remount whenever the zones layer toggles, so a one-shot flag on the
|
|
74
|
-
// persistent group would leave fresh meshes clickable.
|
|
75
|
-
const walls = (obj as Group).getObjectByName('walls') as Mesh | undefined
|
|
76
|
-
if (walls) {
|
|
77
|
-
walls.visible = meshVisible
|
|
78
|
-
walls.raycast = noopRaycast
|
|
79
|
-
const material = walls.material as MeshBasicNodeMaterial
|
|
80
|
-
if (material?.userData?.uOpacity) {
|
|
81
|
-
material.userData.uOpacity.value = MathUtils.lerp(
|
|
82
|
-
material.userData.uOpacity.value,
|
|
83
|
-
targetOpacity,
|
|
84
|
-
lerpSpeed,
|
|
85
|
-
)
|
|
86
|
-
}
|
|
87
|
-
}
|
|
48
|
+
// A zone the author hid (sidebar eye) takes its group with it; otherwise
|
|
49
|
+
// this per-frame write would undo the renderer's `visible` prop.
|
|
50
|
+
const nodeVisible = zone?.visible !== false
|
|
51
|
+
if (obj.visible !== nodeVisible) obj.visible = nodeVisible
|
|
52
|
+
const shown = nodeVisible && isOnSelectedLevel
|
|
88
53
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
const
|
|
54
|
+
// Raycast is re-disabled per frame: the meshes remount whenever zones
|
|
55
|
+
// mount again, so a one-shot flag on the persistent group would leave
|
|
56
|
+
// fresh meshes clickable.
|
|
57
|
+
for (const name of ['walls', 'floor']) {
|
|
58
|
+
const mesh = (obj as Group).getObjectByName(name) as Mesh | undefined
|
|
59
|
+
if (!mesh) continue
|
|
60
|
+
mesh.visible = shown
|
|
61
|
+
mesh.raycast = noopRaycast
|
|
62
|
+
const material = mesh.material as MeshBasicNodeMaterial
|
|
94
63
|
if (material?.userData?.uOpacity) {
|
|
95
64
|
material.userData.uOpacity.value = MathUtils.lerp(
|
|
96
65
|
material.userData.uOpacity.value,
|
|
97
|
-
|
|
66
|
+
shown ? 1 : 0,
|
|
98
67
|
lerpSpeed,
|
|
99
68
|
)
|
|
100
69
|
}
|
|
101
70
|
}
|
|
102
71
|
|
|
103
|
-
|
|
104
|
-
// during snapshot capture.
|
|
105
|
-
const showLabel =
|
|
106
|
-
!isCaptureMode && !zoneLabelsHidden && !!selectedLevelId && isOnSelectedLevel
|
|
107
|
-
const labelOpacity = showLabel ? '1' : '0'
|
|
72
|
+
const labelOpacity = shown && !zoneLabelsHidden ? '1' : '0'
|
|
108
73
|
const labelEl = document.getElementById(`${zoneId}-label`)
|
|
109
74
|
if (labelEl && labelEl.style.opacity !== labelOpacity) {
|
|
110
75
|
labelEl.style.opacity = labelOpacity
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type AnyNodeDefinition, registerNode } from '@pascal-app/core'
|
|
2
|
+
|
|
3
|
+
export function registerHostingTestNode(definition: AnyNodeDefinition) {
|
|
4
|
+
registerNode({
|
|
5
|
+
...definition,
|
|
6
|
+
geometry: () => {
|
|
7
|
+
throw new Error('Unit-test host geometry must not be rendered')
|
|
8
|
+
},
|
|
9
|
+
})
|
|
10
|
+
}
|
|
@@ -1,15 +1,9 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
|
|
2
|
-
import {
|
|
3
|
-
type ItemEvent,
|
|
4
|
-
ItemNode,
|
|
5
|
-
nodeRegistry,
|
|
6
|
-
registerNode,
|
|
7
|
-
sceneRegistry,
|
|
8
|
-
useScene,
|
|
9
|
-
} from '@pascal-app/core'
|
|
2
|
+
import { type ItemEvent, ItemNode, nodeRegistry, sceneRegistry, useScene } from '@pascal-app/core'
|
|
10
3
|
import { Group, Vector3 } from 'three'
|
|
11
4
|
import useEditor from '../../../store/use-editor'
|
|
12
5
|
import useInteractionScope from '../../../store/use-interaction-scope'
|
|
6
|
+
import { registerHostingTestNode } from '../__fixtures__/hosting'
|
|
13
7
|
import { itemSurfaceStrategy } from './placement-strategies'
|
|
14
8
|
import type { PlacementContext } from './placement-types'
|
|
15
9
|
|
|
@@ -28,7 +22,7 @@ const scopeState = useInteractionScope.getState()
|
|
|
28
22
|
beforeEach(() => {
|
|
29
23
|
restoreRegistry = nodeRegistry._snapshot()
|
|
30
24
|
nodeRegistry._reset()
|
|
31
|
-
|
|
25
|
+
registerHostingTestNode({
|
|
32
26
|
kind: 'item',
|
|
33
27
|
schemaVersion: 1,
|
|
34
28
|
schema: ItemNode,
|
|
@@ -1,16 +1,10 @@
|
|
|
1
1
|
import { afterEach, beforeEach, expect, spyOn, test } from 'bun:test'
|
|
2
2
|
import * as core from '@pascal-app/core'
|
|
3
|
-
import {
|
|
4
|
-
type ItemEvent,
|
|
5
|
-
ItemNode,
|
|
6
|
-
nodeRegistry,
|
|
7
|
-
registerNode,
|
|
8
|
-
sceneRegistry,
|
|
9
|
-
useScene,
|
|
10
|
-
} from '@pascal-app/core'
|
|
3
|
+
import { type ItemEvent, ItemNode, nodeRegistry, sceneRegistry, useScene } from '@pascal-app/core'
|
|
11
4
|
import { Group, Vector3 } from 'three'
|
|
12
5
|
import useEditor from '../../../store/use-editor'
|
|
13
6
|
import useInteractionScope from '../../../store/use-interaction-scope'
|
|
7
|
+
import { registerHostingTestNode } from '../__fixtures__/hosting'
|
|
14
8
|
import { itemSurfaceStrategy } from './placement-strategies'
|
|
15
9
|
import type { PlacementContext } from './placement-types'
|
|
16
10
|
|
|
@@ -37,7 +31,7 @@ let resolver: ReturnType<typeof spyOn<typeof core, 'resolveSurfacePlacement'>>
|
|
|
37
31
|
beforeEach(() => {
|
|
38
32
|
restoreRegistry = nodeRegistry._snapshot()
|
|
39
33
|
nodeRegistry._reset()
|
|
40
|
-
|
|
34
|
+
registerHostingTestNode({
|
|
41
35
|
kind: 'item',
|
|
42
36
|
schemaVersion: 1,
|
|
43
37
|
schema: ItemNode,
|