@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,479 @@
|
|
|
1
|
+
import type { AnyNode, AnyNodeId, DoorNode, WallNode, WindowNode } from '@pascal-app/core'
|
|
2
|
+
import { getLinkedWallUpdates } from '@pascal-app/core'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Driving dimensions — WS3.
|
|
6
|
+
*
|
|
7
|
+
* A dimension label in the floor plan is editable. Committing a new value
|
|
8
|
+
* does NOT re-letter the annotation: it MOVES the geometry so the dimension
|
|
9
|
+
* becomes true, the way a CAD "driving dimension" works.
|
|
10
|
+
*
|
|
11
|
+
* Everything in this module is pure: it reads a snapshot of the scene node
|
|
12
|
+
* map and returns node patches. The React layer
|
|
13
|
+
* (`floorplan-dimension-renderer.tsx` + `floorplan-registry-layer.tsx`)
|
|
14
|
+
* applies them through `sceneApi.update`.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
export type DimensionPlanPoint = [number, number]
|
|
18
|
+
|
|
19
|
+
// ── Value parsing ────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
const INCHES_PER_METER = 1 / 0.0254
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Parse a typed dimension value into METRES.
|
|
25
|
+
*
|
|
26
|
+
* Accepted (case-insensitive, whitespace tolerant):
|
|
27
|
+
* - `12'-6"`, `12' 6"`, `12'6"`, `12'`, `6"`, `12'-6 1/2"`, `6 1/2"`
|
|
28
|
+
* - `3810mm`, `381cm`, `3.81m`, `3810 mm`
|
|
29
|
+
* - bare number — `12.5` — interpreted in the caller's unit system:
|
|
30
|
+
* imperial → feet, metric → metres.
|
|
31
|
+
*
|
|
32
|
+
* Returns null for anything unparseable or non-positive.
|
|
33
|
+
*/
|
|
34
|
+
export function parseDimensionInput(
|
|
35
|
+
raw: string,
|
|
36
|
+
unit: 'metric' | 'imperial' = 'metric',
|
|
37
|
+
): number | null {
|
|
38
|
+
const text = raw.trim().toLowerCase().replace(/\s+/g, ' ')
|
|
39
|
+
if (!text) return null
|
|
40
|
+
|
|
41
|
+
// Explicit metric suffix.
|
|
42
|
+
const metric = /^([+-]?\d+(?:\.\d+)?)\s*(mm|cm|m)$/.exec(text)
|
|
43
|
+
if (metric) {
|
|
44
|
+
const value = Number.parseFloat(metric[1]!)
|
|
45
|
+
if (!Number.isFinite(value)) return null
|
|
46
|
+
const scale = metric[2] === 'mm' ? 0.001 : metric[2] === 'cm' ? 0.01 : 1
|
|
47
|
+
return positive(value * scale)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Feet and/or inches. Either part may be absent; inches may carry a fraction.
|
|
51
|
+
const feetInches =
|
|
52
|
+
/^([+-]?)(?:(\d+(?:\.\d+)?)\s*(?:'|ft|feet)\s*)?(?:-?\s*(?:(\d+(?:\.\d+)?)\s*)?(?:(\d+)\/(\d+)\s*)?(?:"|in|inch|inches))?$/.exec(
|
|
53
|
+
text,
|
|
54
|
+
)
|
|
55
|
+
if (feetInches && (feetInches[2] || feetInches[3] || feetInches[4])) {
|
|
56
|
+
const sign = feetInches[1] === '-' ? -1 : 1
|
|
57
|
+
const feet = feetInches[2] ? Number.parseFloat(feetInches[2]) : 0
|
|
58
|
+
const inches = feetInches[3] ? Number.parseFloat(feetInches[3]) : 0
|
|
59
|
+
const numerator = feetInches[4] ? Number.parseFloat(feetInches[4]) : 0
|
|
60
|
+
const denominator = feetInches[5] ? Number.parseFloat(feetInches[5]) : 1
|
|
61
|
+
if (denominator === 0) return null
|
|
62
|
+
const totalInches = feet * 12 + inches + numerator / denominator
|
|
63
|
+
return positive((sign * totalInches) / INCHES_PER_METER)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// `12'-6` / `12-6` shorthand without the inch mark, feet-first.
|
|
67
|
+
const shorthand =
|
|
68
|
+
/^([+-]?\d+(?:\.\d+)?)\s*(?:'|ft)\s*-?\s*(\d+(?:\.\d+)?)(?:\s+(\d+)\/(\d+))?$/.exec(text)
|
|
69
|
+
if (shorthand) {
|
|
70
|
+
const feet = Number.parseFloat(shorthand[1]!)
|
|
71
|
+
const inches = Number.parseFloat(shorthand[2]!)
|
|
72
|
+
const numerator = shorthand[3] ? Number.parseFloat(shorthand[3]) : 0
|
|
73
|
+
const denominator = shorthand[4] ? Number.parseFloat(shorthand[4]) : 1
|
|
74
|
+
if (denominator === 0) return null
|
|
75
|
+
const sign = feet < 0 ? -1 : 1
|
|
76
|
+
return positive(
|
|
77
|
+
((Math.abs(feet) * 12 + inches + numerator / denominator) * sign) / INCHES_PER_METER,
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Bare number — unit system decides.
|
|
82
|
+
const bare = /^([+-]?\d+(?:\.\d+)?)$/.exec(text)
|
|
83
|
+
if (bare) {
|
|
84
|
+
const value = Number.parseFloat(bare[1]!)
|
|
85
|
+
if (!Number.isFinite(value)) return null
|
|
86
|
+
return positive(unit === 'imperial' ? (value * 12) / INCHES_PER_METER : value)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return null
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function positive(value: number): number | null {
|
|
93
|
+
return Number.isFinite(value) && value > 1e-6 ? value : null
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ── Drive resolution ─────────────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
export type DimensionDriveTarget =
|
|
99
|
+
| {
|
|
100
|
+
kind: 'wall-endpoint'
|
|
101
|
+
wallId: AnyNodeId
|
|
102
|
+
endpoint: 'start' | 'end'
|
|
103
|
+
/** Unit vector along which the moving endpoint travels when the value grows. */
|
|
104
|
+
direction: DimensionPlanPoint
|
|
105
|
+
}
|
|
106
|
+
| {
|
|
107
|
+
kind: 'opening'
|
|
108
|
+
openingId: AnyNodeId
|
|
109
|
+
wallId: AnyNodeId
|
|
110
|
+
/** +1 when growing the dimension increases `position[0]`, -1 otherwise. */
|
|
111
|
+
sign: 1 | -1
|
|
112
|
+
}
|
|
113
|
+
| {
|
|
114
|
+
/**
|
|
115
|
+
* The dimension measures the opening itself (its own width tag), so
|
|
116
|
+
* the value resizes the opening rather than sliding it.
|
|
117
|
+
*/
|
|
118
|
+
kind: 'opening-width'
|
|
119
|
+
openingId: AnyNodeId
|
|
120
|
+
wallId: AnyNodeId
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export type DimensionDriveResolution =
|
|
124
|
+
| { drivable: true; target: DimensionDriveTarget }
|
|
125
|
+
| { drivable: false; reason: string }
|
|
126
|
+
|
|
127
|
+
/** How close a dimension witness point must sit to a feature to bind to it. */
|
|
128
|
+
const FEATURE_MATCH_TOLERANCE_M = 0.28
|
|
129
|
+
const PARALLEL_TOLERANCE = 0.02
|
|
130
|
+
|
|
131
|
+
export type DimensionDriveQuery = {
|
|
132
|
+
nodes: Readonly<Record<string, AnyNode>>
|
|
133
|
+
/** Node the dimension geometry was emitted by (the registry entry). */
|
|
134
|
+
ownerNodeId: AnyNodeId
|
|
135
|
+
/** Witness points of the dimension segment, in level-plan metres (x, z). */
|
|
136
|
+
start: DimensionPlanPoint
|
|
137
|
+
end: DimensionPlanPoint
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Work out what a dimension's value drives.
|
|
142
|
+
*
|
|
143
|
+
* Resolution is deliberately conservative: when nothing binds we say so and
|
|
144
|
+
* the caller falls back to `textOverride` (with an "override" badge) rather
|
|
145
|
+
* than moving something the author did not mean.
|
|
146
|
+
*/
|
|
147
|
+
export function resolveDimensionDrive(query: DimensionDriveQuery): DimensionDriveResolution {
|
|
148
|
+
const { nodes, ownerNodeId } = query
|
|
149
|
+
const owner = nodes[ownerNodeId]
|
|
150
|
+
if (!owner) return { drivable: false, reason: 'unknown node' }
|
|
151
|
+
if (isNodeLocked(owner)) return { drivable: false, reason: 'node is locked' }
|
|
152
|
+
|
|
153
|
+
const dimDirection = unit(subtract(query.end, query.start))
|
|
154
|
+
if (!dimDirection) return { drivable: false, reason: 'degenerate dimension' }
|
|
155
|
+
|
|
156
|
+
if (owner.type === 'door' || owner.type === 'window') {
|
|
157
|
+
return resolveOpeningDrive(query, owner as DoorNode | WindowNode, dimDirection)
|
|
158
|
+
}
|
|
159
|
+
if (owner.type === 'wall') {
|
|
160
|
+
return resolveWallDrive(query, owner as WallNode, dimDirection)
|
|
161
|
+
}
|
|
162
|
+
if (owner.type === 'construction-dimension') {
|
|
163
|
+
return resolveConstructionDimensionDrive(query, owner, dimDirection)
|
|
164
|
+
}
|
|
165
|
+
return { drivable: false, reason: `${owner.type} dimensions are not drivable` }
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function resolveWallDrive(
|
|
169
|
+
query: DimensionDriveQuery,
|
|
170
|
+
wall: WallNode,
|
|
171
|
+
dimDirection: DimensionPlanPoint,
|
|
172
|
+
): DimensionDriveResolution {
|
|
173
|
+
const wallDirection = unit(subtract(wall.end, wall.start))
|
|
174
|
+
if (!wallDirection) return { drivable: false, reason: 'degenerate wall' }
|
|
175
|
+
if (Math.abs(cross(wallDirection, dimDirection)) > PARALLEL_TOLERANCE) {
|
|
176
|
+
return { drivable: false, reason: 'dimension is not along the wall' }
|
|
177
|
+
}
|
|
178
|
+
if ((wall as { curvature?: number }).curvature) {
|
|
179
|
+
return { drivable: false, reason: 'curved walls are not drivable yet' }
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Prefer moving an opening: it is the cheaper, more local edit and it is
|
|
183
|
+
// what the "openings" dimension tier actually measures to.
|
|
184
|
+
const openings = wallOpenings(query.nodes, wall)
|
|
185
|
+
const endOpening = nearestOpening(openings, wall, wallDirection, query.end)
|
|
186
|
+
if (endOpening) {
|
|
187
|
+
return {
|
|
188
|
+
drivable: true,
|
|
189
|
+
target: {
|
|
190
|
+
kind: 'opening',
|
|
191
|
+
openingId: endOpening.id as AnyNodeId,
|
|
192
|
+
wallId: wall.id as AnyNodeId,
|
|
193
|
+
sign: dot(wallDirection, dimDirection) >= 0 ? 1 : -1,
|
|
194
|
+
},
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Otherwise the wall's own length: the endpoint the dimension runs TOWARD
|
|
199
|
+
// moves, the one it starts from is the datum.
|
|
200
|
+
const endpoint = nearestWallEndpoint(wall, query.end)
|
|
201
|
+
if (!endpoint) return { drivable: false, reason: 'dimension does not end on this wall' }
|
|
202
|
+
const fixed = endpoint === 'end' ? wall.start : wall.end
|
|
203
|
+
const moving = endpoint === 'end' ? wall.end : wall.start
|
|
204
|
+
const direction = unit(subtract(moving as DimensionPlanPoint, fixed as DimensionPlanPoint))
|
|
205
|
+
if (!direction) return { drivable: false, reason: 'degenerate wall' }
|
|
206
|
+
return {
|
|
207
|
+
drivable: true,
|
|
208
|
+
target: { kind: 'wall-endpoint', wallId: wall.id as AnyNodeId, endpoint, direction },
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function resolveOpeningDrive(
|
|
213
|
+
query: DimensionDriveQuery,
|
|
214
|
+
opening: DoorNode | WindowNode,
|
|
215
|
+
dimDirection: DimensionPlanPoint,
|
|
216
|
+
): DimensionDriveResolution {
|
|
217
|
+
const wall = opening.wallId ? query.nodes[opening.wallId] : undefined
|
|
218
|
+
if (wall?.type !== 'wall') return { drivable: false, reason: 'opening has no host wall' }
|
|
219
|
+
if (isNodeLocked(wall)) return { drivable: false, reason: 'host wall is locked' }
|
|
220
|
+
const wallDirection = unit(subtract((wall as WallNode).end, (wall as WallNode).start))
|
|
221
|
+
if (!wallDirection) return { drivable: false, reason: 'degenerate wall' }
|
|
222
|
+
if (Math.abs(cross(wallDirection, dimDirection)) > PARALLEL_TOLERANCE) {
|
|
223
|
+
return { drivable: false, reason: 'dimension is not along the host wall' }
|
|
224
|
+
}
|
|
225
|
+
if (measuresOpeningWidth(query, opening, wall as WallNode, wallDirection)) {
|
|
226
|
+
return {
|
|
227
|
+
drivable: true,
|
|
228
|
+
target: {
|
|
229
|
+
kind: 'opening-width',
|
|
230
|
+
openingId: opening.id as AnyNodeId,
|
|
231
|
+
wallId: wall.id as AnyNodeId,
|
|
232
|
+
},
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return {
|
|
236
|
+
drivable: true,
|
|
237
|
+
target: {
|
|
238
|
+
kind: 'opening',
|
|
239
|
+
openingId: opening.id as AnyNodeId,
|
|
240
|
+
wallId: wall.id as AnyNodeId,
|
|
241
|
+
sign: dot(wallDirection, dimDirection) >= 0 ? 1 : -1,
|
|
242
|
+
},
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* True when the dimension spans the opening edge to edge — the opening's own
|
|
248
|
+
* width tag. Typing into that resizes the opening; typing into a dimension
|
|
249
|
+
* that merely ENDS at the opening slides it.
|
|
250
|
+
*/
|
|
251
|
+
function measuresOpeningWidth(
|
|
252
|
+
query: DimensionDriveQuery,
|
|
253
|
+
opening: DoorNode | WindowNode,
|
|
254
|
+
wall: WallNode,
|
|
255
|
+
wallDirection: DimensionPlanPoint,
|
|
256
|
+
): boolean {
|
|
257
|
+
const origin = wall.start as DimensionPlanPoint
|
|
258
|
+
const startAlong = dot(subtract(query.start, origin), wallDirection)
|
|
259
|
+
const endAlong = dot(subtract(query.end, origin), wallDirection)
|
|
260
|
+
const span = Math.abs(endAlong - startAlong)
|
|
261
|
+
const centre = (startAlong + endAlong) / 2
|
|
262
|
+
return Math.abs(span - opening.width) <= 0.02 && Math.abs(centre - opening.position[0]) <= 0.02
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function resolveConstructionDimensionDrive(
|
|
266
|
+
query: DimensionDriveQuery,
|
|
267
|
+
node: AnyNode,
|
|
268
|
+
dimDirection: DimensionPlanPoint,
|
|
269
|
+
): DimensionDriveResolution {
|
|
270
|
+
const anchors = (node as { anchors?: unknown[] }).anchors ?? []
|
|
271
|
+
// The LAST anchor is the one the dimension runs toward; it is the driven end.
|
|
272
|
+
for (let index = anchors.length - 1; index >= 1; index--) {
|
|
273
|
+
const anchor = anchors[index]
|
|
274
|
+
if (!anchor || Array.isArray(anchor)) continue
|
|
275
|
+
const reference = (anchor as { reference?: { nodeId?: string } }).reference
|
|
276
|
+
const referenced = reference?.nodeId ? query.nodes[reference.nodeId] : undefined
|
|
277
|
+
if (!referenced) continue
|
|
278
|
+
if (isNodeLocked(referenced)) return { drivable: false, reason: 'anchored node is locked' }
|
|
279
|
+
if (referenced.type === 'door' || referenced.type === 'window') {
|
|
280
|
+
return resolveOpeningDrive(query, referenced as DoorNode | WindowNode, dimDirection)
|
|
281
|
+
}
|
|
282
|
+
if (referenced.type === 'wall') {
|
|
283
|
+
return resolveWallDrive(query, referenced as WallNode, dimDirection)
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return { drivable: false, reason: 'dimension anchors are free points' }
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function wallOpenings(
|
|
290
|
+
nodes: Readonly<Record<string, AnyNode>>,
|
|
291
|
+
wall: WallNode,
|
|
292
|
+
): Array<DoorNode | WindowNode> {
|
|
293
|
+
const openings: Array<DoorNode | WindowNode> = []
|
|
294
|
+
for (const id of wall.children ?? []) {
|
|
295
|
+
const child = nodes[id]
|
|
296
|
+
if (child && (child.type === 'door' || child.type === 'window')) {
|
|
297
|
+
openings.push(child as DoorNode | WindowNode)
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return openings
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function nearestOpening(
|
|
304
|
+
openings: ReadonlyArray<DoorNode | WindowNode>,
|
|
305
|
+
wall: WallNode,
|
|
306
|
+
wallDirection: DimensionPlanPoint,
|
|
307
|
+
point: DimensionPlanPoint,
|
|
308
|
+
): DoorNode | WindowNode | null {
|
|
309
|
+
let best: DoorNode | WindowNode | null = null
|
|
310
|
+
let bestDistance = FEATURE_MATCH_TOLERANCE_M
|
|
311
|
+
const projected = dot(subtract(point, wall.start as DimensionPlanPoint), wallDirection)
|
|
312
|
+
for (const opening of openings) {
|
|
313
|
+
const center = opening.position[0]
|
|
314
|
+
const half = opening.width / 2
|
|
315
|
+
for (const candidate of [center, center - half, center + half]) {
|
|
316
|
+
const distance = Math.abs(projected - candidate)
|
|
317
|
+
if (distance < bestDistance) {
|
|
318
|
+
bestDistance = distance
|
|
319
|
+
best = opening
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
return best
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function nearestWallEndpoint(wall: WallNode, point: DimensionPlanPoint): 'start' | 'end' | null {
|
|
327
|
+
const toStart = length(subtract(point, wall.start as DimensionPlanPoint))
|
|
328
|
+
const toEnd = length(subtract(point, wall.end as DimensionPlanPoint))
|
|
329
|
+
const nearest = toEnd <= toStart ? 'end' : 'start'
|
|
330
|
+
const distance = Math.min(toStart, toEnd)
|
|
331
|
+
// Face-datum dimensions sit half a wall thickness in from the centreline
|
|
332
|
+
// endpoint, so the tolerance has to clear the thickest plausible wall.
|
|
333
|
+
return distance <= FEATURE_MATCH_TOLERANCE_M + (wall.thickness ?? 0.1) ? nearest : null
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// ── Applying a drive ─────────────────────────────────────────────────
|
|
337
|
+
|
|
338
|
+
export type DimensionDriveUpdate = { id: AnyNodeId; data: Record<string, unknown> }
|
|
339
|
+
|
|
340
|
+
export type DimensionDrivePlan = {
|
|
341
|
+
updates: DimensionDriveUpdate[]
|
|
342
|
+
/** Signed change in metres that was applied. */
|
|
343
|
+
delta: number
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Turn a resolved target plus a length delta into node patches.
|
|
348
|
+
*
|
|
349
|
+
* `currentLength` is the dimension's own measured length (whatever datum it
|
|
350
|
+
* uses) and `nextLength` the typed value — the drive is applied as the
|
|
351
|
+
* DELTA between them, so face / centreline datum offsets cancel out and the
|
|
352
|
+
* dimension reads exactly the typed value afterwards.
|
|
353
|
+
*/
|
|
354
|
+
export function planDimensionDrive(args: {
|
|
355
|
+
nodes: Readonly<Record<string, AnyNode>>
|
|
356
|
+
target: DimensionDriveTarget
|
|
357
|
+
currentLength: number
|
|
358
|
+
nextLength: number
|
|
359
|
+
}): DimensionDrivePlan | null {
|
|
360
|
+
const delta = args.nextLength - args.currentLength
|
|
361
|
+
if (!Number.isFinite(delta) || Math.abs(delta) < 1e-9) return { updates: [], delta: 0 }
|
|
362
|
+
|
|
363
|
+
if (args.target.kind === 'opening-width') {
|
|
364
|
+
const opening = args.nodes[args.target.openingId]
|
|
365
|
+
if (!opening || (opening.type !== 'door' && opening.type !== 'window')) return null
|
|
366
|
+
const wall = args.nodes[args.target.wallId] as WallNode | undefined
|
|
367
|
+
const wallLength = wall
|
|
368
|
+
? length(subtract(wall.end as DimensionPlanPoint, wall.start as DimensionPlanPoint))
|
|
369
|
+
: Number.POSITIVE_INFINITY
|
|
370
|
+
if (args.nextLength < 0.05 || args.nextLength > wallLength) return null
|
|
371
|
+
return {
|
|
372
|
+
updates: [{ id: args.target.openingId, data: { width: args.nextLength } }],
|
|
373
|
+
delta,
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (args.target.kind === 'opening') {
|
|
378
|
+
const opening = args.nodes[args.target.openingId]
|
|
379
|
+
if (!opening || (opening.type !== 'door' && opening.type !== 'window')) return null
|
|
380
|
+
const wall = args.nodes[args.target.wallId] as WallNode | undefined
|
|
381
|
+
const node = opening as DoorNode | WindowNode
|
|
382
|
+
const next = node.position[0] + delta * args.target.sign
|
|
383
|
+
const limit = wall
|
|
384
|
+
? length(subtract(wall.end as DimensionPlanPoint, wall.start as DimensionPlanPoint))
|
|
385
|
+
: null
|
|
386
|
+
const clamped =
|
|
387
|
+
limit === null ? next : Math.min(Math.max(next, node.width / 2), limit - node.width / 2)
|
|
388
|
+
return {
|
|
389
|
+
updates: [
|
|
390
|
+
{
|
|
391
|
+
id: node.id as AnyNodeId,
|
|
392
|
+
data: { position: [clamped, node.position[1], node.position[2]] },
|
|
393
|
+
},
|
|
394
|
+
],
|
|
395
|
+
delta,
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const wall = args.nodes[args.target.wallId] as WallNode | undefined
|
|
400
|
+
if (!wall) return null
|
|
401
|
+
const originalStart = [...wall.start] as DimensionPlanPoint
|
|
402
|
+
const originalEnd = [...wall.end] as DimensionPlanPoint
|
|
403
|
+
const moving = args.target.endpoint === 'end' ? originalEnd : originalStart
|
|
404
|
+
const next: DimensionPlanPoint = [
|
|
405
|
+
moving[0] + args.target.direction[0] * delta,
|
|
406
|
+
moving[1] + args.target.direction[1] * delta,
|
|
407
|
+
]
|
|
408
|
+
const nextStart = args.target.endpoint === 'start' ? next : originalStart
|
|
409
|
+
const nextEnd = args.target.endpoint === 'end' ? next : originalEnd
|
|
410
|
+
if (length(subtract(nextEnd, nextStart)) < 0.01) return null
|
|
411
|
+
|
|
412
|
+
const updates: DimensionDriveUpdate[] = [
|
|
413
|
+
{ id: wall.id as AnyNodeId, data: { start: nextStart, end: nextEnd } },
|
|
414
|
+
]
|
|
415
|
+
|
|
416
|
+
// Keep connected junctions consistent: any wall in the same level that
|
|
417
|
+
// shares the moved corner follows it.
|
|
418
|
+
const linked = Object.values(args.nodes).filter(
|
|
419
|
+
(node): node is WallNode =>
|
|
420
|
+
node?.type === 'wall' &&
|
|
421
|
+
node.id !== wall.id &&
|
|
422
|
+
(node.parentId ?? null) === (wall.parentId ?? null) &&
|
|
423
|
+
!isNodeLocked(node) &&
|
|
424
|
+
(samePlanPoint(node.start as DimensionPlanPoint, moving) ||
|
|
425
|
+
samePlanPoint(node.end as DimensionPlanPoint, moving)),
|
|
426
|
+
)
|
|
427
|
+
for (const update of getLinkedWallUpdates(
|
|
428
|
+
linked.map((node) => ({ wall: node })),
|
|
429
|
+
originalStart,
|
|
430
|
+
originalEnd,
|
|
431
|
+
nextStart,
|
|
432
|
+
nextEnd,
|
|
433
|
+
)) {
|
|
434
|
+
updates.push({ id: update.id as AnyNodeId, data: { start: update.start, end: update.end } })
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
return { updates, delta }
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Pascal has no first-class node lock — `guideUi.locked` is UI state for
|
|
442
|
+
* reference guides only. `metadata.locked` is the convention used here, so a
|
|
443
|
+
* future lock feature can set it and driving will refuse without a change.
|
|
444
|
+
*/
|
|
445
|
+
function isNodeLocked(node: AnyNode): boolean {
|
|
446
|
+
const metadata = (node as { metadata?: Record<string, unknown> | null }).metadata
|
|
447
|
+
return (
|
|
448
|
+
!!metadata && typeof metadata === 'object' && (metadata as { locked?: unknown }).locked === true
|
|
449
|
+
)
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// ── Vector helpers ───────────────────────────────────────────────────
|
|
453
|
+
|
|
454
|
+
const SAME_POINT_EPSILON = 1e-6
|
|
455
|
+
|
|
456
|
+
function samePlanPoint(a: DimensionPlanPoint, b: DimensionPlanPoint): boolean {
|
|
457
|
+
return Math.abs(a[0] - b[0]) <= SAME_POINT_EPSILON && Math.abs(a[1] - b[1]) <= SAME_POINT_EPSILON
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function subtract(a: readonly number[], b: readonly number[]): DimensionPlanPoint {
|
|
461
|
+
return [a[0]! - b[0]!, a[1]! - b[1]!]
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function dot(a: DimensionPlanPoint, b: DimensionPlanPoint): number {
|
|
465
|
+
return a[0] * b[0] + a[1] * b[1]
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function cross(a: DimensionPlanPoint, b: DimensionPlanPoint): number {
|
|
469
|
+
return a[0] * b[1] - a[1] * b[0]
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function length(a: DimensionPlanPoint): number {
|
|
473
|
+
return Math.hypot(a[0], a[1])
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function unit(a: DimensionPlanPoint): DimensionPlanPoint | null {
|
|
477
|
+
const magnitude = length(a)
|
|
478
|
+
return magnitude <= 1e-9 ? null : [a[0] / magnitude, a[1] / magnitude]
|
|
479
|
+
}
|
|
@@ -37,7 +37,7 @@ import {
|
|
|
37
37
|
rotateFloorplanExportBounds,
|
|
38
38
|
} from './floorplan-export'
|
|
39
39
|
import { floorplanGeometryMetadata } from './floorplan-extension'
|
|
40
|
-
import { FloorplanPdfDocument } from './floorplan-pdfkit-document'
|
|
40
|
+
import { FloorplanPdfDocument, loadFloorplanPdfFonts } from './floorplan-pdfkit-document'
|
|
41
41
|
import { renderFloorplanGeometryToPdfKit } from './floorplan-pdfkit-renderer'
|
|
42
42
|
|
|
43
43
|
type GroupGeometry = Extract<FloorplanGeometry, { kind: 'group' }>
|
|
@@ -306,6 +306,8 @@ describe('floor plan export policy', () => {
|
|
|
306
306
|
openingMarks: true,
|
|
307
307
|
structuralGrids: false,
|
|
308
308
|
roomLabels: false,
|
|
309
|
+
roomDetails: true,
|
|
310
|
+
roofPlan: true,
|
|
309
311
|
stairAnnotations: true,
|
|
310
312
|
}
|
|
311
313
|
|
|
@@ -328,6 +330,8 @@ describe('floor plan export policy', () => {
|
|
|
328
330
|
openingMarks: false,
|
|
329
331
|
structuralGrids: false,
|
|
330
332
|
roomLabels: true,
|
|
333
|
+
roomDetails: false,
|
|
334
|
+
roofPlan: false,
|
|
331
335
|
stairAnnotations: false,
|
|
332
336
|
})
|
|
333
337
|
})
|
|
@@ -730,7 +734,7 @@ describe('collectFloorplanGeometry', () => {
|
|
|
730
734
|
rawPdf.on('data', (chunk: Buffer) => chunks.push(chunk))
|
|
731
735
|
const completedPdf = Promise.withResolvers<string>()
|
|
732
736
|
rawPdf.on('end', () => completedPdf.resolve(Buffer.concat(chunks).toString('latin1')))
|
|
733
|
-
const pdf = new FloorplanPdfDocument(rawPdf, [200, 200])
|
|
737
|
+
const pdf = new FloorplanPdfDocument(rawPdf, [200, 200], await loadFloorplanPdfFonts())
|
|
734
738
|
pdf.addPage()
|
|
735
739
|
for (const { model } of full) {
|
|
736
740
|
if (!model) continue
|
|
@@ -837,9 +841,9 @@ describe('resolveExportLevels', () => {
|
|
|
837
841
|
selectLevel(ground.id)
|
|
838
842
|
|
|
839
843
|
expect(resolveExportLevels(nodes)).toEqual([
|
|
840
|
-
{ id: ground.id, label: '
|
|
841
|
-
{ id: upper.id, label: '
|
|
842
|
-
{ id: attic.id, label: '
|
|
844
|
+
{ id: ground.id, label: 'Ground floor' },
|
|
845
|
+
{ id: upper.id, label: 'Floor 1' },
|
|
846
|
+
{ id: attic.id, label: 'Floor 3' },
|
|
843
847
|
])
|
|
844
848
|
})
|
|
845
849
|
|
|
@@ -847,9 +851,9 @@ describe('resolveExportLevels', () => {
|
|
|
847
851
|
selectLevel(roof.id)
|
|
848
852
|
|
|
849
853
|
expect(resolveExportLevels(nodes)).toEqual([
|
|
850
|
-
{ id: ground.id, label: '
|
|
851
|
-
{ id: upper.id, label: '
|
|
852
|
-
{ id: attic.id, label: '
|
|
854
|
+
{ id: ground.id, label: 'Ground floor' },
|
|
855
|
+
{ id: upper.id, label: 'Floor 1' },
|
|
856
|
+
{ id: attic.id, label: 'Floor 3' },
|
|
853
857
|
])
|
|
854
858
|
})
|
|
855
859
|
})
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
type FloorplanGeometry,
|
|
8
8
|
type FloorplanPalette,
|
|
9
9
|
type FloorplanPoint,
|
|
10
|
+
getLevelDisplayName,
|
|
10
11
|
isNodeKindEnabled,
|
|
11
12
|
type LiveNodeOverrides,
|
|
12
13
|
type NodeCategory,
|
|
@@ -169,8 +170,23 @@ export type FloorplanPageLayout = {
|
|
|
169
170
|
planBox: { x: number; y: number; width: number; height: number }
|
|
170
171
|
}
|
|
171
172
|
|
|
173
|
+
/**
|
|
174
|
+
* Install list the PDF export gates plugin kinds with. Mirrors the GLB exporter:
|
|
175
|
+
* a scene without explicit install state is legacy, so every loaded plugin kind
|
|
176
|
+
* draws (`undefined`); the store's `installedPlugins` then only holds host
|
|
177
|
+
* defaults and would hide plugin nodes the project has always had.
|
|
178
|
+
*/
|
|
179
|
+
export function floorplanExportInstalledPlugins(scene: {
|
|
180
|
+
installedPlugins: readonly string[]
|
|
181
|
+
hasExplicitPluginInstallState: boolean
|
|
182
|
+
}): readonly string[] | undefined {
|
|
183
|
+
return scene.hasExplicitPluginInstallState ? scene.installedPlugins : undefined
|
|
184
|
+
}
|
|
185
|
+
|
|
172
186
|
export async function exportFloorplanPdf(scope: FloorplanExportScope): Promise<void> {
|
|
173
|
-
const
|
|
187
|
+
const sceneState = useScene.getState()
|
|
188
|
+
const { nodes } = sceneState
|
|
189
|
+
const installedPlugins = floorplanExportInstalledPlugins(sceneState)
|
|
174
190
|
const viewer = useViewer.getState()
|
|
175
191
|
const unit = viewer.unit
|
|
176
192
|
const metricNotation = viewer.metricNotation
|
|
@@ -306,6 +322,7 @@ export function collectFloorplanSchedules(
|
|
|
306
322
|
levelId: AnyNodeId,
|
|
307
323
|
unit: 'metric' | 'imperial',
|
|
308
324
|
scope: FloorplanExportScope = 'full',
|
|
325
|
+
{ drafting = false }: { drafting?: boolean } = {},
|
|
309
326
|
): FloorplanSchedule[] {
|
|
310
327
|
const siblingsByType = new Map<string, AnyNode[]>()
|
|
311
328
|
const visit = (id: AnyNodeId) => {
|
|
@@ -329,7 +346,7 @@ export function collectFloorplanSchedules(
|
|
|
329
346
|
// or other site-category contributors whose plan geometry was excluded.
|
|
330
347
|
if (!isFloorplanNodeInExportScope(definition, scope)) continue
|
|
331
348
|
const siblings = siblingsByType.get(kind) ?? []
|
|
332
|
-
const schedule = scheduleContribution({ siblings, nodes, levelId, unit })
|
|
349
|
+
const schedule = scheduleContribution({ siblings, nodes, levelId, unit, drafting })
|
|
333
350
|
if (schedule && schedule.rows.length > 0) schedules.push(schedule)
|
|
334
351
|
}
|
|
335
352
|
return schedules
|
|
@@ -769,7 +786,8 @@ export function collectFloorplanGeometry(
|
|
|
769
786
|
annotationVisibility: FloorplanAnnotationVisibility,
|
|
770
787
|
drawingType: ConstructionDrawingType,
|
|
771
788
|
wallDimensionReference: FloorplanWallDimensionReference,
|
|
772
|
-
|
|
789
|
+
/** `undefined` = legacy scene without install state: every loaded kind draws. */
|
|
790
|
+
installedPlugins: readonly string[] | undefined,
|
|
773
791
|
): ExportGeometry[] {
|
|
774
792
|
const noLiveOverrides = new Map<string, LiveNodeOverrides>()
|
|
775
793
|
const levelNodeIdsByType = new Map<string, AnyNodeId[]>()
|
|
@@ -778,13 +796,17 @@ export function collectFloorplanGeometry(
|
|
|
778
796
|
const visit = (id: AnyNodeId) => {
|
|
779
797
|
const node = nodes[id]
|
|
780
798
|
if (!node) return
|
|
799
|
+
// Same install gate as the live layer: an uninstalled plugin's kinds draw
|
|
800
|
+
// nothing, while their hosted children keep their own gate.
|
|
801
|
+
const enabled = isNodeKindEnabled(node.type, installedPlugins)
|
|
781
802
|
const def = nodeRegistry.get(node.type)
|
|
782
|
-
if (def?.computeFloorplanLevelData) {
|
|
803
|
+
if (enabled && def?.computeFloorplanLevelData) {
|
|
783
804
|
const ids = levelNodeIdsByType.get(node.type)
|
|
784
805
|
if (ids) ids.push(id)
|
|
785
806
|
else levelNodeIdsByType.set(node.type, [id])
|
|
786
807
|
}
|
|
787
808
|
if (
|
|
809
|
+
enabled &&
|
|
788
810
|
def?.floorplan &&
|
|
789
811
|
isFloorplanNodeVisible(node) &&
|
|
790
812
|
isFloorplanNodeInExportScope(def, scope)
|
|
@@ -802,7 +824,11 @@ export function collectFloorplanGeometry(
|
|
|
802
824
|
if (activeLevelNode) {
|
|
803
825
|
for (const linked of collectFloorplanLinkedLevelNodes(nodes, levelId, collectedIds)) {
|
|
804
826
|
const definition = nodeRegistry.get(linked.node.type)
|
|
805
|
-
if (
|
|
827
|
+
if (
|
|
828
|
+
isNodeKindEnabled(linked.node.type, installedPlugins) &&
|
|
829
|
+
isFloorplanNodeVisible(linked.node) &&
|
|
830
|
+
isFloorplanNodeInExportScope(definition, scope)
|
|
831
|
+
) {
|
|
806
832
|
const drawingNode = resolveNodeForDrawingType(linked.node, nodes, drawingType)
|
|
807
833
|
if (drawingNode) {
|
|
808
834
|
entries.push({ id: linked.id, node: drawingNode, parentOverride: activeLevelNode })
|
|
@@ -1060,9 +1086,7 @@ function levelIndexOf(node: AnyNode): number {
|
|
|
1060
1086
|
}
|
|
1061
1087
|
|
|
1062
1088
|
function levelLabelOf(node: AnyNode): string {
|
|
1063
|
-
|
|
1064
|
-
if (name) return name
|
|
1065
|
-
return `Level ${levelIndexOf(node)}`
|
|
1089
|
+
return getLevelDisplayName({ name: node.name, level: levelIndexOf(node) })
|
|
1066
1090
|
}
|
|
1067
1091
|
|
|
1068
1092
|
function nextFrames(count: number): Promise<void> {
|