@pascal-app/editor 1.0.3 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +11 -9
- package/src/components/editor/action-menu-button.tsx +83 -0
- package/src/components/editor/bake-thumbnail.tsx +315 -4
- package/src/components/editor/capture-level-picker.tsx +72 -0
- package/src/components/editor/custom-camera-controls.tsx +202 -57
- package/src/components/editor/delete-confirmation-dialog.tsx +297 -7
- package/src/components/editor/duplicate-node.ts +244 -0
- package/src/components/editor/editor-layout-mobile.tsx +36 -25
- package/src/components/editor/editor-layout-v2.tsx +131 -58
- package/src/components/editor/export-manager.tsx +34 -8
- package/src/components/editor/first-person/build-collider-world.test.ts +51 -0
- package/src/components/editor/first-person/build-collider-world.ts +6 -2
- package/src/components/editor/first-person/mechanism-targets.test.ts +75 -0
- package/src/components/editor/first-person/mechanism-targets.ts +36 -0
- package/src/components/editor/first-person-controls.tsx +205 -2
- package/src/components/editor/floating-action-menu.tsx +63 -207
- package/src/components/editor/floor-aware-camera-controls.test.ts +442 -0
- package/src/components/editor/floor-aware-camera-controls.ts +324 -0
- package/src/components/editor/floor-openings-overlay.tsx +243 -0
- package/src/components/editor/floor-region-controls.tsx +846 -0
- package/src/components/editor/floor-region-webgpu-materials.test.ts +12 -0
- package/src/components/editor/floor-surface.test.ts +165 -0
- package/src/components/editor/floor-surface.ts +182 -0
- package/src/components/editor/floorplan-background-selection.test.ts +11 -2
- package/src/components/editor/floorplan-background-selection.ts +1 -3
- package/src/components/editor/floorplan-panel.tsx +344 -342
- package/src/components/editor/footprint-height-handle.tsx +204 -0
- package/src/components/editor/group-actions.ts +54 -2
- package/src/components/editor/handles/handle-hit-priority.test.tsx +63 -0
- package/src/components/editor/handles/handle-hit-priority.tsx +37 -0
- package/src/components/editor/handles/swallow-next-click.test.ts +28 -0
- package/src/components/editor/handles/use-handle-drag.ts +269 -197
- package/src/components/editor/index.tsx +294 -169
- package/src/components/editor/join-walls-pill.tsx +44 -0
- package/src/components/editor/node-action-menu.tsx +68 -70
- package/src/components/editor/node-arrow-handles.tsx +25 -6
- package/src/components/editor/open-wall-ends-3d-layer.tsx +273 -0
- package/src/components/editor/open-wall-ends-hint.tsx +89 -0
- package/src/components/editor/quick-measurement-hud.tsx +3 -2
- package/src/components/editor/registry-action-contributions.test.tsx +63 -0
- package/src/components/editor/registry-action-contributions.tsx +80 -1
- package/src/components/editor/room-controls.tsx +847 -0
- package/src/components/editor/room-floor-highlight.tsx +128 -0
- package/src/components/editor/room-handles-2d.tsx +354 -0
- package/src/components/editor/room-handles.tsx +675 -0
- package/src/components/editor/room-highlight.tsx +225 -0
- package/src/components/editor/room-labels.test.ts +357 -0
- package/src/components/editor/room-labels.tsx +990 -0
- package/src/components/editor/room-transform-ghost.tsx +256 -0
- package/src/components/editor/room-webgpu-materials.test.ts +27 -0
- package/src/components/editor/selection-manager.tsx +451 -317
- package/src/components/editor/slab-hole-highlights.tsx +3 -0
- package/src/components/editor/snapshot-capture-overlay.tsx +4 -0
- package/src/components/editor/snapshot-capture.test.ts +52 -4
- package/src/components/editor/snapshot-capture.ts +30 -5
- package/src/components/editor/thumbnail-generator.test.ts +308 -0
- package/src/components/editor/thumbnail-generator.tsx +1035 -229
- package/src/components/editor/use-floorplan-background-placement.ts +31 -11
- package/src/components/editor/vector-edge-extractor.tsx +121 -0
- package/src/components/editor/views/host-view.tsx +61 -0
- package/src/components/editor/views/plugins-view.tsx +670 -0
- package/src/components/editor/views/scene-region.tsx +59 -0
- package/src/components/editor/views/use-editor-views.ts +57 -0
- package/src/components/editor/views/view-bar.tsx +220 -0
- package/src/components/editor/wall-justification.test.ts +140 -0
- package/src/components/editor/wall-move-side-handles.tsx +43 -142
- package/src/components/editor/wall-push-arrow.tsx +92 -0
- package/src/components/editor/wall-region-handles.tsx +509 -0
- package/src/components/editor/wall-snap-beacon-layer.tsx +3 -0
- package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +8 -4
- package/src/components/editor-2d/floorplan-open-wall-ends-layer.test.tsx +86 -0
- package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +290 -0
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +11 -1
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +113 -102
- package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +156 -0
- package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +278 -0
- package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +77 -0
- package/src/components/editor-2d/renderers/{floorplan-draft-wall-measurement.tsx → floorplan-draft-measurement.tsx} +8 -8
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +4 -1
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +38 -8
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +291 -227
- package/src/components/editor-2d/svg-paths.ts +12 -15
- package/src/components/procedural-items/inspector.tsx +40 -15
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +97 -14
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +18 -18
- package/src/components/systems/zone/zone-system.test.ts +47 -0
- package/src/components/systems/zone/zone-system.tsx +29 -68
- package/src/components/tools/item/placement-history.test.tsx +474 -0
- package/src/components/tools/item/placement-strategies.ts +8 -4
- package/src/components/tools/item/use-draft-node.test.tsx +432 -1
- package/src/components/tools/item/use-draft-node.ts +166 -95
- package/src/components/tools/item/use-placement-coordinator.tsx +93 -46
- package/src/components/tools/paint-region/use-paint-tint.ts +37 -0
- package/src/components/tools/paint-region/wall-paint-region-tool.tsx +466 -0
- package/src/components/tools/registry/move-registry-node-tool.tsx +3 -1
- package/src/components/tools/select/box-select-tool.tsx +2 -12
- package/src/components/tools/select/plane-box-select-tool.test.tsx +79 -0
- package/src/components/tools/select/plane-box-select-tool.tsx +35 -22
- package/src/components/tools/select/select-candidates.test.ts +37 -4
- package/src/components/tools/select/select-candidates.ts +15 -11
- package/src/components/tools/shared/cursor-sphere.tsx +36 -12
- package/src/components/tools/site/site-boundary-editor.tsx +1 -2
- package/src/components/tools/stair/stair-defaults.ts +0 -3
- package/src/components/tools/stair/stair-tool.tsx +90 -42
- package/src/components/tools/tool-manager.tsx +46 -20
- package/src/components/tools/wall/wall-drafting.test.ts +19 -4
- package/src/components/tools/zone/zone-tool.tsx +4 -12
- package/src/components/ui/action-menu/action-button.tsx +3 -0
- package/src/components/ui/action-menu/camera-actions.tsx +2 -1
- package/src/components/ui/action-menu/control-modes.tsx +2 -35
- package/src/components/ui/action-menu/measurement-control.tsx +3 -10
- package/src/components/ui/build-panel/build-panel.tsx +235 -0
- package/src/components/ui/command-palette/editor-commands.tsx +4 -30
- package/src/components/ui/command-palette/index.tsx +5 -24
- package/src/components/ui/controls/finish-swatch.tsx +110 -0
- package/src/components/ui/controls/material-paint-panel.tsx +97 -26
- package/src/components/ui/controls/material-picker.tsx +4 -1
- package/src/components/ui/controls/scene-material-list.tsx +4 -32
- package/src/components/ui/controls/select-control.tsx +65 -0
- package/src/components/ui/floating-level-selector.tsx +9 -14
- package/src/components/ui/helpers/building-helper.tsx +4 -1
- package/src/components/ui/helpers/contextual-helper-panel.tsx +154 -70
- package/src/components/ui/helpers/helper-manager.test.tsx +69 -0
- package/src/components/ui/helpers/helper-manager.tsx +325 -14
- package/src/components/ui/helpers/item-helper.tsx +8 -0
- package/src/components/ui/helpers/registered-tool-helper.tsx +8 -1
- package/src/components/ui/icon-ref.tsx +45 -1
- package/src/components/ui/level-duplicate-dialog.tsx +1 -1
- package/src/components/ui/node-icon-image.tsx +16 -0
- package/src/components/ui/panels/collections/collections-popover.tsx +20 -14
- package/src/components/ui/panels/floor-foundation-panel.tsx +551 -0
- package/src/components/ui/panels/floor-opening-panel.tsx +143 -0
- package/src/components/ui/panels/mobile-panel-sheet.tsx +2 -2
- package/src/components/ui/panels/mobile-selection-bar.tsx +2 -2
- package/src/components/ui/panels/multi-field-value.test.ts +26 -3
- package/src/components/ui/panels/multi-field-value.ts +6 -3
- package/src/components/ui/panels/multi-parametric-inspector.tsx +52 -2
- package/src/components/ui/panels/multi-selection-panel.tsx +43 -4
- package/src/components/ui/panels/node-display.ts +8 -3
- package/src/components/ui/panels/panel-manager.tsx +103 -68
- package/src/components/ui/panels/panel-wrapper.tsx +78 -36
- package/src/components/ui/panels/parametric-field-control.tsx +1 -1
- package/src/components/ui/panels/parametric-inspector.tsx +4 -4
- package/src/components/ui/panels/reference-panel.tsx +2 -3
- package/src/components/ui/panels/room-construction-rows.tsx +774 -0
- package/src/components/ui/panels/room-name-options.test.ts +55 -0
- package/src/components/ui/panels/room-name-options.ts +47 -0
- package/src/components/ui/panels/room-openings-field.tsx +110 -0
- package/src/components/ui/panels/room-panel.tsx +725 -0
- package/src/components/ui/panels/shape-choice.tsx +58 -0
- package/src/components/ui/panels/wall-region-list.tsx +109 -0
- package/src/components/ui/primitives/color-dot.tsx +36 -24
- package/src/components/ui/primitives/sidebar.tsx +2 -0
- package/src/components/ui/right-stack.test.ts +29 -0
- package/src/components/ui/right-stack.tsx +145 -0
- package/src/components/ui/sidebar/icon-rail.tsx +5 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +31 -4
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +5 -5
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -4
- package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +197 -0
- package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +24 -11
- package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +176 -211
- package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +30 -8
- package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +1 -3
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +4 -1
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +1 -3
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +1 -4
- package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +1 -3
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +21 -13
- package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +0 -27
- package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -3
- package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +9 -4
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +0 -2
- package/src/components/ui/sidebar/tab-bar.tsx +10 -0
- package/src/components/ui/sidebar/use-plugin-panels.tsx +15 -5
- package/src/components/viewer/floorplan-compass-button.tsx +9 -1
- package/src/components/viewer/floorplan-preview.tsx +5 -6
- package/src/components/viewer/viewer-hover-label.tsx +34 -0
- package/src/components/viewer/viewer-rooms.tsx +175 -0
- package/src/components/viewer/viewer-scene-header.tsx +181 -145
- package/src/components/viewer/viewer-selection-manager.tsx +124 -0
- package/src/components/viewer/viewer-stage.tsx +2 -2
- package/src/components/viewer-zone-system.tsx +3 -8
- package/src/hooks/use-auto-save.integration.test.tsx +123 -9
- package/src/hooks/use-auto-save.ts +34 -8
- package/src/hooks/use-keyboard.test.ts +178 -3
- package/src/hooks/use-keyboard.ts +89 -71
- package/src/hooks/use-open-wall-ends.test.ts +99 -0
- package/src/hooks/use-open-wall-ends.ts +238 -0
- package/src/hooks/use-placement-notice.test.ts +48 -0
- package/src/hooks/use-placement-notice.ts +42 -0
- package/src/hooks/use-selected-room.ts +143 -0
- package/src/index.tsx +246 -39
- package/src/lib/ceiling-edit-session.test.ts +321 -0
- package/src/lib/ceiling-edit-session.ts +206 -0
- package/src/lib/ceiling-region-session.test.ts +179 -0
- package/src/lib/continuation.test.ts +19 -1
- package/src/lib/continuation.ts +25 -4
- package/src/lib/door-interaction.ts +14 -0
- package/src/lib/editor-level-display.test.ts +96 -0
- package/src/lib/editor-level-display.ts +48 -0
- package/src/lib/editor-views.ts +144 -0
- package/src/lib/export-geometry-wait.test.ts +90 -0
- package/src/lib/floor-edit-session.ts +153 -0
- package/src/lib/floor-footprints.test.ts +563 -0
- package/src/lib/floor-footprints.ts +395 -0
- package/src/lib/floor-foundation-support.test.ts +283 -0
- package/src/lib/floor-opening-draft.ts +218 -0
- package/src/lib/floor-openings.test.ts +216 -0
- package/src/lib/floor-region-geometry.ts +191 -0
- package/src/lib/floor-region-session.test.ts +485 -0
- package/src/lib/floor-region-session.ts +589 -0
- package/src/lib/floor-region-snap.ts +101 -0
- package/src/lib/floor-step-hover.test.ts +32 -0
- package/src/lib/floorplan/annotation-visibility.ts +17 -0
- package/src/lib/floorplan/dimension-drive.test.ts +360 -0
- package/src/lib/floorplan/dimension-drive.ts +479 -0
- package/src/lib/floorplan/floorplan-export.test.ts +12 -8
- package/src/lib/floorplan/floorplan-export.tsx +32 -8
- package/src/lib/floorplan/floorplan-extension.ts +32 -0
- package/src/lib/floorplan/floorplan-mode.test.ts +4 -0
- package/src/lib/floorplan/floorplan-mode.ts +2 -0
- package/src/lib/floorplan/floorplan-pdfkit-document.ts +339 -18
- package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +138 -8
- package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +56 -25
- package/src/lib/floorplan/floorplan-plugin-install.test.tsx +138 -0
- package/src/lib/floorplan/floorplan-readonly.ts +6 -0
- package/src/lib/floorplan/geometry.ts +13 -6
- package/src/lib/floorplan/index.ts +0 -9
- package/src/lib/floorplan/items.ts +5 -2
- package/src/lib/floorplan/marks.test.ts +213 -0
- package/src/lib/floorplan/marks.ts +238 -0
- package/src/lib/floorplan/open-wall-ends.test.ts +268 -0
- package/src/lib/floorplan/open-wall-ends.ts +162 -0
- package/src/lib/floorplan/pdf-fonts/GeistMono-Bold.ttf +0 -0
- package/src/lib/floorplan/pdf-fonts/GeistMono-Regular.ttf +0 -0
- package/src/lib/floorplan/pdf-fonts/LICENSE-GeistMono.txt +92 -0
- package/src/lib/floorplan/pdf-fonts/LICENSE-LiberationSans.txt +102 -0
- package/src/lib/floorplan/pdf-fonts/LiberationSans-Bold.ttf +0 -0
- package/src/lib/floorplan/pdf-fonts/LiberationSans-Regular.ttf +0 -0
- package/src/lib/floorplan/schedules.test.ts +189 -0
- package/src/lib/floorplan/schedules.ts +339 -0
- package/src/lib/floorplan/sheet-export.test.ts +100 -0
- package/src/lib/floorplan/sheet-export.ts +318 -0
- package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +779 -0
- package/src/lib/floorplan/site-plan/contours.test.ts +43 -0
- package/src/lib/floorplan/site-plan/contributors.ts +97 -0
- package/src/lib/floorplan/site-plan/coverage.ts +232 -0
- package/src/lib/floorplan/site-plan/drawing-type-switch.test.ts +15 -0
- package/src/lib/floorplan/site-plan/drawing-type-switch.tsx +86 -0
- package/src/lib/floorplan/site-plan/front-edge.test.ts +103 -0
- package/src/lib/floorplan/site-plan/front-edge.ts +156 -0
- package/src/lib/floorplan/site-plan/geometry.test.ts +275 -0
- package/src/lib/floorplan/site-plan/geometry.ts +557 -0
- package/src/lib/floorplan/site-plan/index.ts +71 -0
- package/src/lib/floorplan/site-plan/site-annotations.test.ts +366 -0
- package/src/lib/floorplan/site-plan/site-annotations.ts +1083 -0
- package/src/lib/floorplan/site-plan/site-parts.ts +379 -0
- package/src/lib/floorplan/site-plan/site-plan-layer.tsx +138 -0
- package/src/lib/floorplan/site-plan/street-name.test.ts +69 -0
- package/src/lib/floorplan/site-plan/street-name.ts +302 -0
- package/src/lib/floorplan/site-plan/yards.test.ts +69 -0
- package/src/lib/floorplan/types.ts +1 -27
- package/src/lib/fresh-planar-placement.test.ts +36 -0
- package/src/lib/fresh-planar-placement.ts +59 -36
- package/src/lib/geometry-script/author.ts +152 -0
- package/src/lib/geometry-script/client.ts +152 -0
- package/src/lib/geometry-script/protocol.ts +12 -0
- package/src/lib/gesture-lifecycle.test.ts +595 -0
- package/src/lib/gesture-lifecycle.ts +290 -0
- package/src/lib/glb-export-zone-holes.test.ts +41 -0
- package/src/lib/glb-export.test.ts +485 -0
- package/src/lib/glb-export.ts +237 -119
- package/src/lib/history.test.ts +6 -0
- package/src/lib/history.ts +7 -0
- package/src/lib/hud-title.ts +109 -0
- package/src/lib/hydration-gated-storage.ts +49 -0
- package/src/lib/ifc-export.test.ts +174 -0
- package/src/lib/ifc-export.ts +201 -0
- package/src/lib/ifc-parts.ts +15 -0
- package/src/lib/inspector-expanded.test.ts +44 -0
- package/src/lib/inspector-expanded.ts +36 -0
- package/src/lib/interaction/scope.ts +18 -0
- package/src/lib/keyboard-pan.test.ts +129 -7
- package/src/lib/keyboard-pan.ts +161 -12
- package/src/lib/live-gesture-round-trip.test.ts +360 -0
- package/src/lib/lot/__fixtures__/map-dossier-miami-shores.json +1 -0
- package/src/lib/lot/__fixtures__/map-dossier-stpete.json +1 -0
- package/src/lib/lot/clean-ring.test.ts +186 -0
- package/src/lib/lot/clean-ring.ts +217 -0
- package/src/lib/lot/dossier.test.ts +202 -0
- package/src/lib/lot/dossier.ts +498 -0
- package/src/lib/lot/drop-in.test.ts +326 -0
- package/src/lib/lot/drop-in.ts +407 -0
- package/src/lib/lot/index.ts +43 -0
- package/src/lib/lot/lot-patch.test.ts +340 -0
- package/src/lib/lot/lot-patch.ts +394 -0
- package/src/lib/lot/parcel-provider.ts +43 -0
- package/src/lib/lot/terrain.test.ts +161 -0
- package/src/lib/lot/terrain.ts +266 -0
- package/src/lib/material-catalog-model.ts +3 -1
- package/src/lib/material-paint-panel-model.ts +9 -5
- package/src/lib/material-paint.ts +4 -0
- package/src/lib/mezzanine-actions.test.ts +342 -0
- package/src/lib/mezzanine-draft.test.ts +501 -0
- package/src/lib/mezzanine-draft.ts +278 -0
- package/src/lib/mezzanine-messages.ts +26 -0
- package/src/lib/mezzanine-selection.test.ts +207 -0
- package/src/lib/mezzanine-stairs.ts +46 -0
- package/src/lib/model-export.ts +3 -1
- package/src/lib/node-action-movement.ts +17 -6
- package/src/lib/node-display-label.ts +37 -0
- package/src/lib/orbit-floor-clearance.test.ts +165 -0
- package/src/lib/orbit-floor-clearance.ts +106 -0
- package/src/lib/paint-commit-route.test.ts +37 -0
- package/src/lib/paint-commit-route.ts +17 -0
- package/src/lib/paint-eyedropper.test.tsx +165 -0
- package/src/lib/paint-eyedropper.ts +99 -0
- package/src/lib/paint-pass-through.test.ts +111 -0
- package/src/lib/paint-pass-through.ts +76 -0
- package/src/lib/paint-region-hover.ts +25 -0
- package/src/lib/paint-region-mode.ts +154 -0
- package/src/lib/paint-regions.test.ts +166 -0
- package/src/lib/paint-regions.ts +192 -0
- package/src/lib/paint-scope.test.ts +317 -35
- package/src/lib/paint-scope.ts +266 -25
- package/src/lib/panel-rows.ts +21 -0
- package/src/lib/parametric-node-update.ts +1 -1
- package/src/lib/plate-paint-affected.test.ts +300 -0
- package/src/lib/plate-paint-affected.ts +177 -0
- package/src/lib/plugin-chat.ts +50 -0
- package/src/lib/plugin-panels.test.ts +25 -0
- package/src/lib/plugin-panels.ts +94 -0
- package/src/lib/print-shell-compiler.ts +1 -0
- package/src/lib/registered-draft-snap.ts +12 -0
- package/src/lib/roof-duplication.ts +10 -2
- package/src/lib/room-assembly-overlay.test.ts +275 -0
- package/src/lib/room-assembly-overlay.ts +266 -0
- package/src/lib/room-built-on.test.ts +379 -0
- package/src/lib/room-built-on.ts +138 -0
- package/src/lib/room-construction-commands.ts +712 -0
- package/src/lib/room-construction.test.ts +746 -0
- package/src/lib/room-construction.ts +385 -0
- package/src/lib/room-controls-phase6.test.ts +630 -0
- package/src/lib/room-divide-session.ts +523 -0
- package/src/lib/room-handle-drag.ts +1068 -0
- package/src/lib/room-name-catalog.test.ts +90 -0
- package/src/lib/room-name-catalog.ts +168 -0
- package/src/lib/room-push-dimensions.test.ts +311 -0
- package/src/lib/room-push-dimensions.ts +251 -0
- package/src/lib/room-selection-commands.ts +62 -0
- package/src/lib/room-selection.test.ts +973 -0
- package/src/lib/room-selection.ts +417 -0
- package/src/lib/room-structure-commands.ts +94 -0
- package/src/lib/room-structure.test.ts +921 -0
- package/src/lib/room-transform-session.ts +570 -0
- package/src/lib/room-zone-routing.test.ts +294 -0
- package/src/lib/room-zone-routing.ts +197 -0
- package/src/lib/scene-clipboard.test.ts +364 -0
- package/src/lib/scene-clipboard.ts +229 -48
- package/src/lib/scene-material-refs.test.ts +79 -0
- package/src/lib/scene-material-refs.ts +79 -0
- package/src/lib/scene.test.ts +24 -36
- package/src/lib/scene.ts +33 -57
- package/src/lib/selection-routing.test.ts +129 -45
- package/src/lib/selection-routing.ts +53 -74
- package/src/lib/session-writes.test.ts +120 -0
- package/src/lib/session-writes.ts +205 -0
- package/src/lib/sidebar-panel.ts +62 -0
- package/src/lib/site-boundary.test.ts +4 -6
- package/src/lib/site-boundary.ts +1 -1
- package/src/lib/snapping-mode.test.ts +4 -0
- package/src/lib/snapping-mode.ts +8 -1
- package/src/lib/terrace-draft.test.ts +145 -0
- package/src/lib/terrace-draft.ts +213 -0
- package/src/lib/tool-cursor-icon.test.ts +16 -0
- package/src/lib/tool-cursor-icon.ts +11 -0
- package/src/lib/typing-target.ts +9 -0
- package/src/lib/units.ts +77 -39
- package/src/lib/vector-edges.test.ts +58 -0
- package/src/lib/vector-edges.ts +400 -0
- package/src/lib/viewer-selection.test.ts +577 -0
- package/src/lib/viewer-selection.ts +569 -0
- package/src/lib/wall-draw-variant.test.ts +71 -0
- package/src/lib/wall-draw-variant.ts +91 -0
- package/src/lib/wall-paint-region-session.ts +246 -0
- package/src/lib/wall-paint-region.test.ts +373 -0
- package/src/lib/wall-polygon-draft.test.ts +262 -0
- package/src/lib/wall-polygon-draft.ts +286 -0
- package/src/lib/wall-push.test.ts +453 -0
- package/src/lib/wall-region-face.ts +352 -0
- package/src/lib/wall-region-handles.test.ts +304 -0
- package/src/lib/wall-region-handles.ts +368 -0
- package/src/lib/wall-region-list.test.ts +119 -0
- package/src/lib/wall-region-list.ts +136 -0
- package/src/lib/wall-region-snap.ts +110 -0
- package/src/lib/window-interaction.ts +14 -0
- package/src/lib/zone-content.ts +4 -1
- package/src/lib/zone-name.test.ts +17 -0
- package/src/lib/zone-name.ts +17 -0
- package/src/store/opens-in-select-mode.test.ts +223 -0
- package/src/store/paint-mode.test.ts +188 -0
- package/src/store/terrain-sculpt-mode.test.ts +19 -21
- package/src/store/tool-mode.test.ts +137 -6
- package/src/store/use-delete-confirmation.ts +28 -1
- package/src/store/use-drawing-view.test.ts +24 -3
- package/src/store/use-drawing-view.ts +29 -1
- package/src/store/use-editor.tsx +432 -321
- package/src/store/use-floorplan-draft-preview.ts +13 -0
- package/src/store/use-floorplan-marquee.ts +12 -2
- package/src/store/use-quick-measurement-hud.ts +3 -3
- package/src/store/use-stair-build-preview.ts +13 -5
- package/src/store/use-wall-move-ghosts.ts +4 -2
- package/src/store/use-wall-region-handles-selection.test.ts +222 -0
- package/src/store/use-wall-region-handles-selection.ts +139 -0
- package/src/store/view-layout.test.ts +175 -0
- package/src/store/view-layout.ts +196 -0
- package/src/test-utils/immediate-animation-frames.ts +16 -0
- package/src/test-utils/live-gesture-round-trip.ts +224 -0
- package/src/test-utils/selection-harness.ts +82 -0
- package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +0 -499
- package/src/components/systems/zone/zone-label-editor-system.tsx +0 -333
- package/src/components/tools/fence/fence-drafting.test.ts +0 -100
- package/src/components/tools/fence/fence-drafting.ts +0 -266
- package/src/components/ui/sidebar/panels/plugins-panel.tsx +0 -218
- package/src/components/viewer/viewer-units-panel.tsx +0 -116
- package/src/lib/floorplan/stairs.ts +0 -489
- package/src/lib/level-duplication.test.ts +0 -144
- package/src/lib/level-duplication.ts +0 -195
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { Link2 } from 'lucide-react'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The "Join walls" action on an open wall end — shared by the floor plan and
|
|
7
|
+
* the 3D view so both offer the same pill. A refusal ("A door or window is in
|
|
8
|
+
* the way") sits in front of the button.
|
|
9
|
+
*/
|
|
10
|
+
export function JoinWallsPill({
|
|
11
|
+
onHoverEnd,
|
|
12
|
+
onHoverStart,
|
|
13
|
+
onJoin,
|
|
14
|
+
refusal,
|
|
15
|
+
}: {
|
|
16
|
+
onHoverEnd: () => void
|
|
17
|
+
onHoverStart: () => void
|
|
18
|
+
onJoin: () => void
|
|
19
|
+
refusal: string | null
|
|
20
|
+
}) {
|
|
21
|
+
return (
|
|
22
|
+
<div
|
|
23
|
+
className="pointer-events-auto flex items-center gap-1 whitespace-nowrap rounded-full border border-border bg-background/95 p-1 text-xs shadow-xl backdrop-blur-md"
|
|
24
|
+
data-open-wall-end=""
|
|
25
|
+
onPointerDown={(event) => event.stopPropagation()}
|
|
26
|
+
onPointerEnter={onHoverStart}
|
|
27
|
+
onPointerLeave={onHoverEnd}
|
|
28
|
+
onPointerUp={(event) => event.stopPropagation()}
|
|
29
|
+
>
|
|
30
|
+
{refusal ? <span className="px-2 text-muted-foreground">{refusal}</span> : null}
|
|
31
|
+
<button
|
|
32
|
+
className="flex items-center gap-1.5 rounded-full px-3 py-1.5 font-medium text-foreground transition-colors hover:bg-accent"
|
|
33
|
+
onClick={(event) => {
|
|
34
|
+
event.stopPropagation()
|
|
35
|
+
onJoin()
|
|
36
|
+
}}
|
|
37
|
+
type="button"
|
|
38
|
+
>
|
|
39
|
+
<Link2 className="h-3.5 w-3.5" />
|
|
40
|
+
Join walls
|
|
41
|
+
</button>
|
|
42
|
+
</div>
|
|
43
|
+
)
|
|
44
|
+
}
|
|
@@ -1,8 +1,20 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import { Icon } from '@iconify/react'
|
|
4
|
-
import {
|
|
5
|
-
|
|
4
|
+
import {
|
|
5
|
+
Copy,
|
|
6
|
+
Group,
|
|
7
|
+
Move,
|
|
8
|
+
PencilRuler,
|
|
9
|
+
RotateCcw,
|
|
10
|
+
RotateCw,
|
|
11
|
+
Search,
|
|
12
|
+
Spline,
|
|
13
|
+
Trash2,
|
|
14
|
+
Ungroup,
|
|
15
|
+
} from 'lucide-react'
|
|
16
|
+
import type { MouseEventHandler, PointerEventHandler, ReactNode } from 'react'
|
|
17
|
+
import { ActionMenuButton } from './action-menu-button'
|
|
6
18
|
import { RegistryActionContributions } from './registry-action-contributions'
|
|
7
19
|
|
|
8
20
|
type NodeActionMenuProps = {
|
|
@@ -13,6 +25,9 @@ type NodeActionMenuProps = {
|
|
|
13
25
|
onMove?: MouseEventHandler<HTMLButtonElement>
|
|
14
26
|
onEditMesh?: MouseEventHandler<HTMLButtonElement>
|
|
15
27
|
onCurve?: MouseEventHandler<HTMLButtonElement>
|
|
28
|
+
/** Quarter turns (a room); sit in the Curve slot (a room has no curve). */
|
|
29
|
+
onRotateLeft?: MouseEventHandler<HTMLButtonElement>
|
|
30
|
+
onRotateRight?: MouseEventHandler<HTMLButtonElement>
|
|
16
31
|
/** Session group (Ctrl/Cmd+G) — multi-selection floating pill. */
|
|
17
32
|
onGroup?: MouseEventHandler<HTMLButtonElement>
|
|
18
33
|
/** Dissolve session group (Ctrl/Cmd+Shift+G). */
|
|
@@ -21,6 +36,16 @@ type NodeActionMenuProps = {
|
|
|
21
36
|
onPointerUp?: PointerEventHandler<HTMLDivElement>
|
|
22
37
|
onPointerEnter?: PointerEventHandler<HTMLDivElement>
|
|
23
38
|
onPointerLeave?: PointerEventHandler<HTMLDivElement>
|
|
39
|
+
/**
|
|
40
|
+
* Buttons a caller contributes next to the registry contributions — for a
|
|
41
|
+
* selection that is not a scene-node selection (the room), whose actions the
|
|
42
|
+
* registry cannot find.
|
|
43
|
+
*/
|
|
44
|
+
children?: ReactNode
|
|
45
|
+
/** Tooltip / accessible name of the delete button. */
|
|
46
|
+
deleteLabel?: string
|
|
47
|
+
/** Shows the delete button greyed out, its tooltip saying why. */
|
|
48
|
+
deleteDisabledReason?: string
|
|
24
49
|
}
|
|
25
50
|
|
|
26
51
|
export function NodeActionMenu({
|
|
@@ -31,12 +56,17 @@ export function NodeActionMenu({
|
|
|
31
56
|
onMove,
|
|
32
57
|
onEditMesh,
|
|
33
58
|
onCurve,
|
|
59
|
+
onRotateLeft,
|
|
60
|
+
onRotateRight,
|
|
34
61
|
onGroup,
|
|
35
62
|
onUngroup,
|
|
36
63
|
onPointerDown,
|
|
37
64
|
onPointerUp,
|
|
38
65
|
onPointerEnter,
|
|
39
66
|
onPointerLeave,
|
|
67
|
+
children,
|
|
68
|
+
deleteLabel = 'Delete',
|
|
69
|
+
deleteDisabledReason,
|
|
40
70
|
}: NodeActionMenuProps) {
|
|
41
71
|
return (
|
|
42
72
|
<div
|
|
@@ -47,104 +77,72 @@ export function NodeActionMenu({
|
|
|
47
77
|
onPointerUp={onPointerUp}
|
|
48
78
|
>
|
|
49
79
|
{onFind && (
|
|
50
|
-
<
|
|
51
|
-
aria-label="Find in catalog"
|
|
52
|
-
className="tooltip-trigger rounded-md p-1.5 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
|
|
53
|
-
onClick={onFind}
|
|
54
|
-
title="Find in catalog"
|
|
55
|
-
type="button"
|
|
56
|
-
>
|
|
80
|
+
<ActionMenuButton label="Find in catalog" onClick={onFind}>
|
|
57
81
|
<Search className="h-4 w-4" />
|
|
58
|
-
</
|
|
82
|
+
</ActionMenuButton>
|
|
59
83
|
)}
|
|
60
84
|
{onMove && (
|
|
61
|
-
<
|
|
62
|
-
aria-label="Move"
|
|
63
|
-
className="tooltip-trigger rounded-md p-1.5 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
|
|
64
|
-
onClick={onMove}
|
|
65
|
-
title="Move"
|
|
66
|
-
type="button"
|
|
67
|
-
>
|
|
85
|
+
<ActionMenuButton label="Move" onClick={onMove}>
|
|
68
86
|
<Move className="h-4 w-4" />
|
|
69
|
-
</
|
|
87
|
+
</ActionMenuButton>
|
|
70
88
|
)}
|
|
71
89
|
{onEditMesh && (
|
|
72
|
-
<
|
|
73
|
-
aria-label="Edit mesh"
|
|
74
|
-
className="tooltip-trigger rounded-md p-1.5 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
|
|
75
|
-
onClick={onEditMesh}
|
|
76
|
-
title="Edit mesh"
|
|
77
|
-
type="button"
|
|
78
|
-
>
|
|
90
|
+
<ActionMenuButton label="Edit mesh" onClick={onEditMesh}>
|
|
79
91
|
<PencilRuler className="h-4 w-4" />
|
|
80
|
-
</
|
|
92
|
+
</ActionMenuButton>
|
|
81
93
|
)}
|
|
82
94
|
{onGroup && (
|
|
83
|
-
<
|
|
84
|
-
aria-label="Group selection"
|
|
85
|
-
className="tooltip-trigger rounded-md p-1.5 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
|
|
86
|
-
onClick={onGroup}
|
|
87
|
-
title="Group (Ctrl/Cmd+G)"
|
|
88
|
-
type="button"
|
|
89
|
-
>
|
|
95
|
+
<ActionMenuButton keys={['Cmd/Ctrl', 'G']} label="Group selection" onClick={onGroup}>
|
|
90
96
|
<Group className="h-4 w-4" />
|
|
91
|
-
</
|
|
97
|
+
</ActionMenuButton>
|
|
92
98
|
)}
|
|
93
99
|
{onUngroup && (
|
|
94
|
-
<
|
|
95
|
-
|
|
96
|
-
|
|
100
|
+
<ActionMenuButton
|
|
101
|
+
keys={['Cmd/Ctrl', 'Shift', 'G']}
|
|
102
|
+
label="Ungroup selection"
|
|
97
103
|
onClick={onUngroup}
|
|
98
|
-
title="Ungroup (Ctrl/Cmd+Shift+G)"
|
|
99
|
-
type="button"
|
|
100
104
|
>
|
|
101
105
|
<Ungroup className="h-4 w-4" />
|
|
102
|
-
</
|
|
106
|
+
</ActionMenuButton>
|
|
103
107
|
)}
|
|
104
108
|
{onCurve && (
|
|
105
|
-
<
|
|
106
|
-
aria-label="Curve"
|
|
107
|
-
className="tooltip-trigger rounded-md p-1.5 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
|
|
108
|
-
onClick={onCurve}
|
|
109
|
-
title="Curve"
|
|
110
|
-
type="button"
|
|
111
|
-
>
|
|
109
|
+
<ActionMenuButton label="Curve" onClick={onCurve}>
|
|
112
110
|
<Spline className="h-4 w-4" />
|
|
113
|
-
</
|
|
111
|
+
</ActionMenuButton>
|
|
112
|
+
)}
|
|
113
|
+
{onRotateLeft && (
|
|
114
|
+
<ActionMenuButton label="Rotate left" onClick={onRotateLeft}>
|
|
115
|
+
<RotateCcw className="h-4 w-4" />
|
|
116
|
+
</ActionMenuButton>
|
|
117
|
+
)}
|
|
118
|
+
{onRotateRight && (
|
|
119
|
+
<ActionMenuButton label="Rotate right" onClick={onRotateRight}>
|
|
120
|
+
<RotateCw className="h-4 w-4" />
|
|
121
|
+
</ActionMenuButton>
|
|
114
122
|
)}
|
|
115
123
|
<RegistryActionContributions />
|
|
124
|
+
{children}
|
|
116
125
|
{onDuplicate && (
|
|
117
|
-
<
|
|
118
|
-
aria-label="Duplicate"
|
|
119
|
-
className="tooltip-trigger rounded-md p-1.5 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
|
|
120
|
-
onClick={onDuplicate}
|
|
121
|
-
title="Duplicate"
|
|
122
|
-
type="button"
|
|
123
|
-
>
|
|
126
|
+
<ActionMenuButton label="Duplicate" onClick={onDuplicate}>
|
|
124
127
|
<Copy className="h-4 w-4" />
|
|
125
|
-
</
|
|
128
|
+
</ActionMenuButton>
|
|
126
129
|
)}
|
|
127
130
|
{onAddHole && (
|
|
128
|
-
<
|
|
129
|
-
aria-label="Cut Out"
|
|
130
|
-
className="tooltip-trigger rounded-md p-1.5 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
|
|
131
|
-
onClick={onAddHole}
|
|
132
|
-
title="Cut Out"
|
|
133
|
-
type="button"
|
|
134
|
-
>
|
|
131
|
+
<ActionMenuButton label="Cut out" onClick={onAddHole}>
|
|
135
132
|
<Icon height={16} icon="carbon:cut-out" width={16} />
|
|
136
|
-
</
|
|
133
|
+
</ActionMenuButton>
|
|
137
134
|
)}
|
|
138
135
|
{onDelete && (
|
|
139
|
-
<
|
|
140
|
-
|
|
141
|
-
|
|
136
|
+
<ActionMenuButton
|
|
137
|
+
destructive
|
|
138
|
+
disabled={!!deleteDisabledReason}
|
|
139
|
+
disabledReason={deleteDisabledReason}
|
|
140
|
+
keys={['Delete / Backspace']}
|
|
141
|
+
label={deleteLabel}
|
|
142
142
|
onClick={onDelete}
|
|
143
|
-
title="Delete"
|
|
144
|
-
type="button"
|
|
145
143
|
>
|
|
146
144
|
<Trash2 className="h-4 w-4" />
|
|
147
|
-
</
|
|
145
|
+
</ActionMenuButton>
|
|
148
146
|
)}
|
|
149
147
|
</div>
|
|
150
148
|
)
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
type ArcResizeHandle,
|
|
7
7
|
type CornerRadiusHandle,
|
|
8
8
|
type Cursor,
|
|
9
|
+
createAngleAccumulator,
|
|
9
10
|
createSceneApi,
|
|
10
11
|
DEFAULT_ANGLE_STEP,
|
|
11
12
|
type HandleDescriptor,
|
|
@@ -42,6 +43,7 @@ import {
|
|
|
42
43
|
import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js'
|
|
43
44
|
|
|
44
45
|
import { MeshBasicNodeMaterial } from 'three/webgpu'
|
|
46
|
+
import { useCeilingEditCeilingId } from '../../lib/ceiling-edit-session'
|
|
45
47
|
import { EDITOR_LAYER } from '../../lib/constants'
|
|
46
48
|
import { RESIZE_HANDLE_DRAG_LABEL, ROTATE_HANDLE_DRAG_LABEL } from '../../lib/contextual-help'
|
|
47
49
|
import { createEditorApi } from '../../lib/editor-api'
|
|
@@ -241,7 +243,12 @@ export function NodeArrowHandles() {
|
|
|
241
243
|
const activeGroup = useHandleGroup((s) =>
|
|
242
244
|
s.active && s.active.nodeId === node?.id ? s.active.group : null,
|
|
243
245
|
)
|
|
244
|
-
|
|
246
|
+
// A ceiling's handles belong to its Edit ceiling session; a plain selection shows none.
|
|
247
|
+
const editCeilingId = useCeilingEditCeilingId()
|
|
248
|
+
const def =
|
|
249
|
+
node && (node.type !== 'ceiling' || node.id === editCeilingId)
|
|
250
|
+
? nodeRegistry.get(node.type)
|
|
251
|
+
: null
|
|
245
252
|
const descriptorSceneApi = useMemo(() => createSceneApi(useScene), [])
|
|
246
253
|
const descriptors = useMemo(() => {
|
|
247
254
|
if (!(node && def?.handles)) return null
|
|
@@ -816,8 +823,16 @@ function LinearArrow({
|
|
|
816
823
|
}) => {
|
|
817
824
|
dragRideObject.matrixWorld.decompose(_resizePositionW, _resizeQuaternion, _resizeScale)
|
|
818
825
|
_resizeOriginW.set(...position).applyMatrix4(dragRideObject.matrixWorld)
|
|
819
|
-
|
|
820
|
-
|
|
826
|
+
const dragAxis =
|
|
827
|
+
descriptor.kind === 'linear-resize'
|
|
828
|
+
? descriptor.dragAxis?.(initialNode as never, sceneApi)
|
|
829
|
+
: undefined
|
|
830
|
+
if (dragAxis) _resizeAxisW.set(...dragAxis).normalize()
|
|
831
|
+
else axisVector(descriptor.axis, _resizeAxisW)
|
|
832
|
+
const localToWorldScale = dragAxis
|
|
833
|
+
? _resizeAxisW.clone().multiply(_resizeScale).length()
|
|
834
|
+
: axisScale(descriptor.axis, _resizeScale)
|
|
835
|
+
_resizeAxisW.applyQuaternion(_resizeQuaternion).normalize()
|
|
821
836
|
if (Math.abs(localToWorldScale) < 1e-6 || _resizeAxisW.lengthSq() === 0) return null
|
|
822
837
|
|
|
823
838
|
const initialPointer =
|
|
@@ -1350,6 +1365,7 @@ function ArcArrow({
|
|
|
1350
1365
|
return null
|
|
1351
1366
|
}
|
|
1352
1367
|
const initialAngle = angleOf(hitWorld)
|
|
1368
|
+
const accumulateAngle = createAngleAccumulator(initialAngle)
|
|
1353
1369
|
|
|
1354
1370
|
// A distinct label selects rotation snapping instead of resize measurements.
|
|
1355
1371
|
if (isRotateShape) {
|
|
@@ -1369,9 +1385,12 @@ function ArcArrow({
|
|
|
1369
1385
|
const hit = new Vector3()
|
|
1370
1386
|
if (!intersectMovePlane(moveEvent.clientX, moveEvent.clientY, plane, hit)) return null
|
|
1371
1387
|
const currentAngle = angleOf(hit)
|
|
1372
|
-
let delta =
|
|
1373
|
-
|
|
1374
|
-
|
|
1388
|
+
let delta = isRotateShape
|
|
1389
|
+
? Math.atan2(
|
|
1390
|
+
Math.sin(currentAngle - initialAngle),
|
|
1391
|
+
Math.cos(currentAngle - initialAngle),
|
|
1392
|
+
)
|
|
1393
|
+
: accumulateAngle(currentAngle)
|
|
1375
1394
|
|
|
1376
1395
|
if (
|
|
1377
1396
|
!descriptor.continuous &&
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { type OpenWallEnd, sceneRegistry } from '@pascal-app/core'
|
|
4
|
+
import { useViewer } from '@pascal-app/viewer'
|
|
5
|
+
import { Html } from '@react-three/drei'
|
|
6
|
+
import { useFrame } from '@react-three/fiber'
|
|
7
|
+
import { memo, useEffect, useMemo, useRef } from 'react'
|
|
8
|
+
import { BoxGeometry, CircleGeometry, type Group, RingGeometry } from 'three'
|
|
9
|
+
import { MeshBasicNodeMaterial } from 'three/webgpu'
|
|
10
|
+
import {
|
|
11
|
+
useOpenWallEndFocus,
|
|
12
|
+
useOpenWallEnds,
|
|
13
|
+
useOpenWallEndsSuppressed,
|
|
14
|
+
} from '../../hooks/use-open-wall-ends'
|
|
15
|
+
import { EDITOR_LAYER } from '../../lib/constants'
|
|
16
|
+
import { openWallEndKey, openWallEndLabel } from '../../lib/floorplan/open-wall-ends'
|
|
17
|
+
import { MEASUREMENT_DANGLING_COLOR } from '../../lib/measurements'
|
|
18
|
+
import { cn } from '../../lib/utils'
|
|
19
|
+
import useEditor from '../../store/use-editor'
|
|
20
|
+
import { isViewVisible, VIEW_3D, visibleScene } from '../../store/view-layout'
|
|
21
|
+
import { JoinWallsPill } from './join-walls-pill'
|
|
22
|
+
import { OpenWallEndsHint } from './open-wall-ends-hint'
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The 3D twin of `FloorplanOpenWallEndsLayer`: at each wall end that isn't
|
|
26
|
+
* joined, a red disc on the floor with a dashed ribbon to the wall it nearly
|
|
27
|
+
* meets, and a screen-space dot carrying the same label ("4 cm gap") and, on
|
|
28
|
+
* hover or click, the same "Join walls" pill. The floor geometry ignores depth
|
|
29
|
+
* so it reads through cutaway walls like the snap beacon; the dot is DOM, so
|
|
30
|
+
* it stays clickable where a wall stands between it and the camera.
|
|
31
|
+
*
|
|
32
|
+
* Shares the analysis with the floor plan (`useOpenWallEnds`). Mounted inside
|
|
33
|
+
* ToolManager's building-local group — wall points are building-local plan
|
|
34
|
+
* coordinates — and lifted to the active level's floor each frame.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
const FLOOR_LIFT = 0.02
|
|
38
|
+
const DISC_RADIUS = 0.07
|
|
39
|
+
const DISC_OUTLINE = 0.095
|
|
40
|
+
const DASH_LEN = 0.06
|
|
41
|
+
const DASH_GAP = 0.04
|
|
42
|
+
const LINE_WIDTH = 0.025
|
|
43
|
+
const MAX_DASHES = 40
|
|
44
|
+
const NO_RAYCAST = () => null
|
|
45
|
+
|
|
46
|
+
function createMarkerResources() {
|
|
47
|
+
const materialOptions = {
|
|
48
|
+
depthTest: false,
|
|
49
|
+
depthWrite: false,
|
|
50
|
+
toneMapped: false,
|
|
51
|
+
transparent: true,
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
redMaterial: new MeshBasicNodeMaterial({
|
|
55
|
+
...materialOptions,
|
|
56
|
+
color: MEASUREMENT_DANGLING_COLOR,
|
|
57
|
+
}),
|
|
58
|
+
outlineMaterial: new MeshBasicNodeMaterial({ ...materialOptions, color: 0xff_ff_ff }),
|
|
59
|
+
discGeometry: new CircleGeometry(1, 24),
|
|
60
|
+
ringGeometry: new RingGeometry(0.7, 1, 24),
|
|
61
|
+
dashGeometry: new BoxGeometry(1, 1, 1),
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
type MarkerResources = ReturnType<typeof createMarkerResources>
|
|
66
|
+
|
|
67
|
+
type Vec3 = [number, number, number]
|
|
68
|
+
|
|
69
|
+
export const OpenWallEnds3DLayer = memo(function OpenWallEnds3DLayer() {
|
|
70
|
+
const visible = useEditor((state) => isViewVisible(state, VIEW_3D))
|
|
71
|
+
const suppressed = useOpenWallEndsSuppressed()
|
|
72
|
+
return visible && !suppressed ? <ActiveOpenWallEnds3DLayer /> : null
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
const ActiveOpenWallEnds3DLayer = memo(function ActiveOpenWallEnds3DLayer() {
|
|
76
|
+
const levelId = useViewer((state) => state.selection.levelId)
|
|
77
|
+
const unit = useViewer((state) => state.unit)
|
|
78
|
+
const { drafting, ends } = useOpenWallEnds()
|
|
79
|
+
// Split view shows the hint once, over the floor plan.
|
|
80
|
+
const hintHere = useEditor((state) => visibleScene(state) === '3d')
|
|
81
|
+
const focus = useOpenWallEndFocus(ends, drafting)
|
|
82
|
+
const groupRef = useRef<Group>(null)
|
|
83
|
+
const hasEnds = ends.length > 0
|
|
84
|
+
const resources = useMemo(() => (hasEnds ? createMarkerResources() : null), [hasEnds])
|
|
85
|
+
// Resources passed as mesh props are shared and aren't disposed by R3F.
|
|
86
|
+
useEffect(
|
|
87
|
+
() => () => {
|
|
88
|
+
if (resources) for (const resource of Object.values(resources)) resource.dispose()
|
|
89
|
+
},
|
|
90
|
+
[resources],
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
useFrame(() => {
|
|
94
|
+
const group = groupRef.current
|
|
95
|
+
if (!group) return
|
|
96
|
+
const levelMesh = levelId ? sceneRegistry.nodes.get(levelId) : null
|
|
97
|
+
group.position.y = levelMesh ? levelMesh.position.y : 0
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
if (!resources) return null
|
|
101
|
+
return (
|
|
102
|
+
<group ref={groupRef}>
|
|
103
|
+
{ends.map((end) => {
|
|
104
|
+
const key = openWallEndKey(end)
|
|
105
|
+
const active = focus.activeEnd !== null && key === focus.activeKey
|
|
106
|
+
return (
|
|
107
|
+
<OpenWallEndMarker3D
|
|
108
|
+
active={active}
|
|
109
|
+
end={end}
|
|
110
|
+
interactive={!!end.candidate && !drafting}
|
|
111
|
+
key={key}
|
|
112
|
+
label={openWallEndLabel(end, unit)}
|
|
113
|
+
onHoverEnd={focus.releaseHover}
|
|
114
|
+
onHoverStart={() => focus.keepHover(key)}
|
|
115
|
+
onJoin={() => focus.join(end)}
|
|
116
|
+
onPin={() => focus.togglePin(key)}
|
|
117
|
+
refusal={active ? focus.refusal : null}
|
|
118
|
+
resources={resources}
|
|
119
|
+
/>
|
|
120
|
+
)
|
|
121
|
+
})}
|
|
122
|
+
{hintHere ? (
|
|
123
|
+
// Html only to reach the DOM from inside the canvas; the hint portals
|
|
124
|
+
// itself to the body and centres on the 3D view.
|
|
125
|
+
<Html style={{ pointerEvents: 'none' }}>
|
|
126
|
+
<OpenWallEndsHint ends={ends} onShow={(end) => focus.pin(openWallEndKey(end))} />
|
|
127
|
+
</Html>
|
|
128
|
+
) : null}
|
|
129
|
+
</group>
|
|
130
|
+
)
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
function OpenWallEndMarker3D({
|
|
134
|
+
active,
|
|
135
|
+
end,
|
|
136
|
+
interactive,
|
|
137
|
+
label,
|
|
138
|
+
onHoverEnd,
|
|
139
|
+
onHoverStart,
|
|
140
|
+
onJoin,
|
|
141
|
+
onPin,
|
|
142
|
+
refusal,
|
|
143
|
+
resources,
|
|
144
|
+
}: {
|
|
145
|
+
active: boolean
|
|
146
|
+
end: OpenWallEnd
|
|
147
|
+
interactive: boolean
|
|
148
|
+
label: string | null
|
|
149
|
+
onHoverEnd: () => void
|
|
150
|
+
onHoverStart: () => void
|
|
151
|
+
onJoin: () => void
|
|
152
|
+
onPin: () => void
|
|
153
|
+
refusal: string | null
|
|
154
|
+
resources: MarkerResources
|
|
155
|
+
}) {
|
|
156
|
+
const { dashGeometry, discGeometry, ringGeometry, redMaterial, outlineMaterial } = resources
|
|
157
|
+
const [x, z] = end.point
|
|
158
|
+
const target = end.candidate?.point
|
|
159
|
+
const { dashes, angleY, dashLength } = useMemo(() => {
|
|
160
|
+
if (!target) return { dashes: [] as Vec3[], angleY: 0, dashLength: 0 }
|
|
161
|
+
const dx = target[0] - x
|
|
162
|
+
const dz = target[1] - z
|
|
163
|
+
const length = Math.hypot(dx, dz)
|
|
164
|
+
const angle = -Math.atan2(dz, dx)
|
|
165
|
+
if (length < 1e-4) return { dashes: [] as Vec3[], angleY: angle, dashLength: 0 }
|
|
166
|
+
const period = Math.max(DASH_LEN + DASH_GAP, length / MAX_DASHES)
|
|
167
|
+
const centres: Vec3[] = []
|
|
168
|
+
for (let d = Math.min(period, length) / 2; d < length; d += period) {
|
|
169
|
+
centres.push([x + (dx / length) * d, FLOOR_LIFT, z + (dz / length) * d])
|
|
170
|
+
}
|
|
171
|
+
return { dashes: centres, angleY: angle, dashLength: Math.min(DASH_LEN, length) }
|
|
172
|
+
}, [target, x, z])
|
|
173
|
+
const stop = (event: { stopPropagation: () => void }) => event.stopPropagation()
|
|
174
|
+
|
|
175
|
+
return (
|
|
176
|
+
<>
|
|
177
|
+
{dashes.map((centre, index) => (
|
|
178
|
+
<mesh
|
|
179
|
+
geometry={dashGeometry}
|
|
180
|
+
key={index}
|
|
181
|
+
layers={EDITOR_LAYER}
|
|
182
|
+
material={redMaterial}
|
|
183
|
+
position={centre}
|
|
184
|
+
raycast={NO_RAYCAST}
|
|
185
|
+
renderOrder={1005}
|
|
186
|
+
rotation={[0, angleY, 0]}
|
|
187
|
+
scale={[dashLength, 0.002, LINE_WIDTH]}
|
|
188
|
+
/>
|
|
189
|
+
))}
|
|
190
|
+
{target ? (
|
|
191
|
+
<mesh
|
|
192
|
+
geometry={ringGeometry}
|
|
193
|
+
layers={EDITOR_LAYER}
|
|
194
|
+
material={redMaterial}
|
|
195
|
+
position={[target[0], FLOOR_LIFT + 0.001, target[1]]}
|
|
196
|
+
raycast={NO_RAYCAST}
|
|
197
|
+
renderOrder={1006}
|
|
198
|
+
rotation={[-Math.PI / 2, 0, 0]}
|
|
199
|
+
scale={[DISC_RADIUS * 0.8, DISC_RADIUS * 0.8, 1]}
|
|
200
|
+
/>
|
|
201
|
+
) : null}
|
|
202
|
+
<mesh
|
|
203
|
+
geometry={discGeometry}
|
|
204
|
+
layers={EDITOR_LAYER}
|
|
205
|
+
material={outlineMaterial}
|
|
206
|
+
position={[x, FLOOR_LIFT + 0.002, z]}
|
|
207
|
+
raycast={NO_RAYCAST}
|
|
208
|
+
renderOrder={1006}
|
|
209
|
+
rotation={[-Math.PI / 2, 0, 0]}
|
|
210
|
+
scale={[DISC_OUTLINE, DISC_OUTLINE, 1]}
|
|
211
|
+
/>
|
|
212
|
+
<mesh
|
|
213
|
+
geometry={discGeometry}
|
|
214
|
+
layers={EDITOR_LAYER}
|
|
215
|
+
material={redMaterial}
|
|
216
|
+
position={[x, FLOOR_LIFT + 0.003, z]}
|
|
217
|
+
raycast={NO_RAYCAST}
|
|
218
|
+
renderOrder={1007}
|
|
219
|
+
rotation={[-Math.PI / 2, 0, 0]}
|
|
220
|
+
scale={[DISC_RADIUS, DISC_RADIUS, 1]}
|
|
221
|
+
/>
|
|
222
|
+
<Html
|
|
223
|
+
center
|
|
224
|
+
position={[x, FLOOR_LIFT, z]}
|
|
225
|
+
style={{ pointerEvents: 'none', userSelect: 'none' }}
|
|
226
|
+
zIndexRange={[25, 0]}
|
|
227
|
+
>
|
|
228
|
+
<div className="relative flex items-center justify-center">
|
|
229
|
+
<div className="pointer-events-none absolute bottom-full left-1/2 mb-1.5 flex -translate-x-1/2 flex-col items-center gap-1.5">
|
|
230
|
+
{active ? (
|
|
231
|
+
<JoinWallsPill
|
|
232
|
+
onHoverEnd={onHoverEnd}
|
|
233
|
+
onHoverStart={onHoverStart}
|
|
234
|
+
onJoin={onJoin}
|
|
235
|
+
refusal={refusal}
|
|
236
|
+
/>
|
|
237
|
+
) : null}
|
|
238
|
+
{label ? (
|
|
239
|
+
<div
|
|
240
|
+
className="whitespace-nowrap rounded-[3px] px-[5px] py-[2px] font-medium font-sans text-[11px] text-white"
|
|
241
|
+
style={{ backgroundColor: MEASUREMENT_DANGLING_COLOR }}
|
|
242
|
+
>
|
|
243
|
+
{label}
|
|
244
|
+
</div>
|
|
245
|
+
) : null}
|
|
246
|
+
</div>
|
|
247
|
+
<button
|
|
248
|
+
aria-label={label ?? 'Wall end not joined'}
|
|
249
|
+
className={cn(
|
|
250
|
+
'h-3.5 w-3.5 rounded-full border-2 border-white shadow-sm transition-transform',
|
|
251
|
+
interactive
|
|
252
|
+
? 'pointer-events-auto cursor-pointer hover:scale-125'
|
|
253
|
+
: 'pointer-events-none',
|
|
254
|
+
active && 'scale-125',
|
|
255
|
+
)}
|
|
256
|
+
data-open-wall-end=""
|
|
257
|
+
onClick={(event) => {
|
|
258
|
+
event.stopPropagation()
|
|
259
|
+
onPin()
|
|
260
|
+
}}
|
|
261
|
+
onPointerDown={stop}
|
|
262
|
+
onPointerEnter={interactive ? onHoverStart : undefined}
|
|
263
|
+
onPointerLeave={interactive ? onHoverEnd : undefined}
|
|
264
|
+
onPointerUp={stop}
|
|
265
|
+
style={{ backgroundColor: MEASUREMENT_DANGLING_COLOR }}
|
|
266
|
+
tabIndex={interactive ? 0 : -1}
|
|
267
|
+
type="button"
|
|
268
|
+
/>
|
|
269
|
+
</div>
|
|
270
|
+
</Html>
|
|
271
|
+
</>
|
|
272
|
+
)
|
|
273
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { type AnyNodeId, emitter, type OpenWallEnd } from '@pascal-app/core'
|
|
4
|
+
import { useEffect, useState } from 'react'
|
|
5
|
+
import { createPortal } from 'react-dom'
|
|
6
|
+
import { openWallEndsSummary } from '../../lib/floorplan/open-wall-ends'
|
|
7
|
+
import { MEASUREMENT_DANGLING_COLOR } from '../../lib/measurements'
|
|
8
|
+
|
|
9
|
+
// The viewer area holds the floor plan and the 3D view side by side, so its
|
|
10
|
+
// top centre clears both views' corner controls in 2D, 3D and split alike.
|
|
11
|
+
const viewerArea = () => document.querySelector('[data-pascal-viewer-3d]')?.parentElement ?? null
|
|
12
|
+
|
|
13
|
+
// The views slide in and out without resizing, so the anchor's box is read
|
|
14
|
+
// per frame while the hint is up, like the Join pill's anchor.
|
|
15
|
+
function useViewerAreaTopCenter(): { left: number; top: number } | null {
|
|
16
|
+
const [anchor, setAnchor] = useState<{ left: number; top: number } | null>(null)
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
let raf = 0
|
|
19
|
+
const tick = () => {
|
|
20
|
+
raf = requestAnimationFrame(tick)
|
|
21
|
+
const rect = viewerArea()?.getBoundingClientRect()
|
|
22
|
+
const next =
|
|
23
|
+
rect && rect.width > 0 ? { left: rect.left + rect.width / 2, top: rect.top } : null
|
|
24
|
+
setAnchor((current) =>
|
|
25
|
+
current && next && current.left === next.left && current.top === next.top ? current : next,
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
tick()
|
|
29
|
+
return () => cancelAnimationFrame(raf)
|
|
30
|
+
}, [])
|
|
31
|
+
return anchor
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* "2 wall ends aren't joined — rooms can't close", centred at the top of the
|
|
36
|
+
* viewer area. Mounted by one view only (the floor plan when it shows, else
|
|
37
|
+
* the 3D view), so split view shows it once.
|
|
38
|
+
* Show walks the joinable ends one by one: it focuses the 3D camera on the
|
|
39
|
+
* wall and asks the view to pin that end's Join pill.
|
|
40
|
+
*/
|
|
41
|
+
export function OpenWallEndsHint({
|
|
42
|
+
ends,
|
|
43
|
+
onShow,
|
|
44
|
+
}: {
|
|
45
|
+
ends: readonly OpenWallEnd[]
|
|
46
|
+
onShow: (end: OpenWallEnd) => void
|
|
47
|
+
}) {
|
|
48
|
+
const anchor = useViewerAreaTopCenter()
|
|
49
|
+
const [cursor, setCursor] = useState(0)
|
|
50
|
+
const joinable = ends.filter((end) => end.candidate)
|
|
51
|
+
const summary = openWallEndsSummary(joinable.length)
|
|
52
|
+
if (!(anchor && summary)) return null
|
|
53
|
+
|
|
54
|
+
const showNext = () => {
|
|
55
|
+
const target = joinable[cursor % joinable.length]
|
|
56
|
+
if (!target) return
|
|
57
|
+
setCursor((value) => value + 1)
|
|
58
|
+
onShow(target)
|
|
59
|
+
emitter.emit('camera-controls:focus', { nodeId: target.wallId as AnyNodeId })
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return createPortal(
|
|
63
|
+
<div
|
|
64
|
+
className="pointer-events-auto fixed z-30 flex -translate-x-1/2 items-center gap-2 whitespace-nowrap rounded-full border border-border bg-background/95 py-1 pr-1 pl-3 text-xs shadow-lg backdrop-blur-md"
|
|
65
|
+
data-open-wall-end=""
|
|
66
|
+
onPointerDown={(event) => event.stopPropagation()}
|
|
67
|
+
onPointerUp={(event) => event.stopPropagation()}
|
|
68
|
+
style={{ left: anchor.left, top: anchor.top + 16 }}
|
|
69
|
+
>
|
|
70
|
+
<span
|
|
71
|
+
aria-hidden
|
|
72
|
+
className="h-2 w-2 shrink-0 rounded-full"
|
|
73
|
+
style={{ backgroundColor: MEASUREMENT_DANGLING_COLOR }}
|
|
74
|
+
/>
|
|
75
|
+
<span className="text-foreground">{summary}</span>
|
|
76
|
+
<button
|
|
77
|
+
className="rounded-full px-3 py-1 font-medium text-foreground transition-colors hover:bg-accent"
|
|
78
|
+
onClick={(event) => {
|
|
79
|
+
event.stopPropagation()
|
|
80
|
+
showNext()
|
|
81
|
+
}}
|
|
82
|
+
type="button"
|
|
83
|
+
>
|
|
84
|
+
Show
|
|
85
|
+
</button>
|
|
86
|
+
</div>,
|
|
87
|
+
document.body,
|
|
88
|
+
)
|
|
89
|
+
}
|