@pascal-app/editor 1.0.3 → 1.0.5
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/README.md +106 -0
- package/package.json +42 -38
- package/src/components/editor/__bench__/room-label-placement.bench.ts +108 -0
- package/src/components/editor/action-menu-button.tsx +83 -0
- package/src/components/editor/bake-thumbnail.tsx +307 -5
- package/src/components/editor/capture-level-picker.tsx +72 -0
- package/src/components/editor/construction-reveal-audio.test.tsx +150 -0
- package/src/components/editor/construction-reveal-audio.tsx +40 -0
- package/src/components/editor/custom-camera-controls.tsx +275 -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 +37 -26
- package/src/components/editor/editor-layout-v2.tsx +132 -59
- package/src/components/editor/export-manager.tsx +47 -26
- package/src/components/editor/first-person/build-collider-world.test.ts +320 -0
- package/src/components/editor/first-person/build-collider-world.ts +28 -7
- package/src/components/editor/first-person/ground-holes.test.ts +128 -0
- package/src/components/editor/first-person/ground-holes.ts +130 -0
- package/src/components/editor/first-person/mechanism-targets.test.ts +162 -0
- package/src/components/editor/first-person/mechanism-targets.ts +88 -0
- package/src/components/editor/first-person-controls.tsx +220 -9
- package/src/components/editor/floating-action-menu.tsx +63 -207
- package/src/components/editor/floor-aware-camera-controls.test.ts +466 -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-mode-coordinator.tsx +2 -2
- package/src/components/editor/floorplan-panel.tsx +414 -363
- package/src/components/editor/follow-pascal.test.tsx +912 -0
- package/src/components/editor/follow-pascal.tsx +354 -0
- package/src/components/editor/footprint-height-handle.tsx +204 -0
- package/src/components/editor/group-actions.ts +56 -2
- package/src/components/editor/group-move-3d.ts +2 -0
- package/src/components/editor/group-rotate-handle.tsx +2 -0
- package/src/components/editor/group-transform-shared.test.ts +460 -0
- 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 +409 -197
- 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/riser-diagram-panel.tsx +14 -7
- 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 +233 -0
- package/src/components/editor/room-labels.test.ts +850 -0
- package/src/components/editor/room-labels.tsx +1890 -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 +474 -419
- package/src/components/editor/slab-hole-highlights.tsx +5 -2
- package/src/components/editor/snapshot-capture-overlay.tsx +7 -2
- package/src/components/editor/snapshot-capture.test.ts +119 -60
- package/src/components/editor/snapshot-capture.ts +36 -5
- package/src/components/editor/thumbnail-generator.test.ts +654 -0
- package/src/components/editor/thumbnail-generator.tsx +1103 -223
- package/src/components/editor/use-floorplan-background-placement.ts +34 -12
- 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 +663 -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.test.tsx +54 -0
- package/src/components/editor/views/view-bar.tsx +343 -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 +8 -4
- package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +8 -4
- package/src/components/editor-2d/floorplan-group-move.tsx +3 -0
- package/src/components/editor-2d/floorplan-open-wall-ends-layer.test.tsx +86 -0
- package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +290 -0
- package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +2 -2
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +11 -1
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +116 -106
- 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 +319 -233
- package/src/components/editor-2d/renderers/floorplan-reveal.test.tsx +254 -0
- package/src/components/editor-2d/renderers/floorplan-reveal.ts +141 -0
- package/src/components/editor-2d/svg-paths.ts +12 -15
- package/src/components/point-ask/bubble.tsx +538 -0
- package/src/components/point-ask/hover-chip.tsx +91 -0
- package/src/components/point-ask/mode-pill.tsx +92 -0
- package/src/components/point-ask/optional-motion.ts +22 -0
- package/src/components/point-ask/outline-corners.tsx +93 -0
- package/src/components/point-ask/pins.tsx +711 -0
- package/src/components/point-ask/point-ask-layer.tsx +83 -0
- package/src/components/point-ask/point-ask-styles.ts +105 -0
- package/src/components/point-ask/region-marquee-styles.ts +15 -0
- package/src/components/point-ask/region-marquee.tsx +64 -0
- package/src/components/procedural-items/inspector.tsx +40 -15
- package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +12 -2
- 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/roof/roof-edit-system.tsx +1 -0
- 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/distribution/connection-compatibility.test.ts +34 -0
- package/src/components/tools/distribution/connection-compatibility.ts +43 -0
- package/src/components/tools/distribution/distribution-public.ts +9 -0
- package/src/components/tools/distribution/distribution-run-contract.test.ts +26 -0
- package/src/components/tools/distribution/distribution-run-contract.ts +94 -0
- package/src/components/tools/distribution/distribution-run-tool.test.ts +241 -0
- package/src/components/tools/distribution/distribution-run-tool.test.tsx +125 -0
- package/src/components/tools/distribution/distribution-run-tool.tsx +1323 -0
- package/src/components/tools/distribution/draw-alignment.ts +35 -0
- package/src/components/tools/distribution/ports.test.ts +56 -0
- package/src/components/tools/distribution/ports.ts +378 -0
- package/src/components/tools/distribution/run-cursor.test.ts +135 -0
- package/src/components/tools/distribution/run-cursor.ts +36 -0
- package/src/components/tools/distribution/run-direction-feedback.test.ts +60 -0
- package/src/components/tools/distribution/run-direction-feedback.tsx +251 -0
- package/src/components/tools/distribution/run-port-snap.test.ts +79 -0
- package/src/components/tools/distribution/run-port-snap.ts +25 -0
- package/src/components/tools/distribution/run-screen-direction.test.ts +23 -0
- package/src/components/tools/distribution/run-screen-direction.ts +30 -0
- package/src/components/tools/elevator/elevator-tool.tsx +3 -6
- package/src/components/tools/elevator/move-elevator-tool.tsx +2 -3
- package/src/components/tools/item/item-surface-strategy-resolver.test.ts +67 -68
- package/src/components/tools/item/placement-history.test.tsx +512 -0
- package/src/components/tools/item/placement-strategies.test.ts +46 -1
- package/src/components/tools/item/placement-strategies.ts +11 -10
- package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +56 -56
- 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 +148 -95
- package/src/components/tools/paint-region/use-paint-tint.ts +37 -0
- package/src/components/tools/paint-region/wall-paint-region-tool.tsx +466 -0
- package/src/components/tools/registry/item-surface-move.test.ts +62 -63
- package/src/components/tools/registry/item-surface-move.ts +2 -0
- package/src/components/tools/registry/item-surface-resolver.test.ts +26 -26
- package/src/components/tools/registry/move-registry-node-tool.test.ts +268 -1
- package/src/components/tools/registry/move-registry-node-tool.tsx +66 -30
- 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/tool-manager.tsx +59 -24
- package/src/components/tools/wall/wall-drafting.test.ts +175 -47
- package/src/components/tools/zone/zone-tool.tsx +4 -12
- package/src/components/ui/action-menu/action-button.tsx +9 -1
- package/src/components/ui/action-menu/camera-actions.tsx +9 -4
- package/src/components/ui/action-menu/control-modes.tsx +15 -42
- package/src/components/ui/action-menu/follow-pascal-pill.tsx +32 -0
- package/src/components/ui/action-menu/follow-pascal-toggle.tsx +36 -0
- package/src/components/ui/action-menu/index.tsx +56 -33
- package/src/components/ui/action-menu/measurement-control.tsx +14 -19
- package/src/components/ui/action-menu/placement.ts +20 -0
- package/src/components/ui/action-menu/point-ask-button.tsx +52 -0
- package/src/components/ui/action-menu/view-toggles.tsx +47 -42
- package/src/components/ui/build-panel/build-panel.tsx +242 -0
- package/src/components/ui/command-palette/editor-commands.tsx +7 -42
- package/src/components/ui/command-palette/index.tsx +50 -39
- package/src/components/ui/command-palette/ranked-groups.test.ts +31 -0
- package/src/components/ui/controls/action-button.tsx +5 -2
- 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/metric-control.tsx +1 -1
- package/src/components/ui/controls/scene-material-list.tsx +4 -32
- package/src/components/ui/controls/segmented-control.tsx +21 -8
- package/src/components/ui/controls/select-control.tsx +65 -0
- package/src/components/ui/controls/slider-control.tsx +1 -1
- package/src/components/ui/controls/toggle-control.tsx +3 -3
- package/src/components/ui/controls/tool-options-panel.tsx +1 -1
- package/src/components/ui/floating-level-selector.tsx +159 -59
- package/src/components/ui/helpers/building-helper.tsx +4 -1
- package/src/components/ui/helpers/contextual-helper-panel.tsx +181 -72
- package/src/components/ui/helpers/helper-manager.test.tsx +121 -0
- package/src/components/ui/helpers/helper-manager.tsx +331 -14
- package/src/components/ui/helpers/item-helper.tsx +17 -1
- 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 +28 -22
- 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 +4 -4
- package/src/components/ui/panels/mobile-selection-bar.tsx +8 -5
- 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 +161 -5
- 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 +130 -62
- package/src/components/ui/panels/parametric-field-control.tsx +3 -3
- package/src/components/ui/panels/parametric-inspector.tsx +7 -7
- 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 +790 -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/badge.tsx +8 -0
- package/src/components/ui/primitives/color-dot.tsx +36 -24
- package/src/components/ui/primitives/sidebar.tsx +3 -1
- package/src/components/ui/right-stack.test.ts +29 -0
- package/src/components/ui/right-stack.tsx +164 -0
- package/src/components/ui/sidebar/app-sidebar.tsx +1 -1
- package/src/components/ui/sidebar/icon-rail.tsx +8 -3
- package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +1 -1
- package/src/components/ui/sidebar/panels/items-panel/index.tsx +6 -6
- package/src/components/ui/sidebar/panels/settings-panel/animation-speed-setting.tsx +40 -0
- package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +7 -8
- package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.test.tsx +170 -0
- package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.tsx +92 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +138 -27
- package/src/components/ui/sidebar/panels/settings-panel/interface-theme.test.tsx +20 -0
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +5 -5
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +4 -10
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +3 -11
- 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 +181 -223
- 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 +13 -37
- 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 +2 -4
- package/src/components/ui/sidebar/tab-bar.tsx +14 -4
- 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-controls-bar.tsx +18 -6
- package/src/components/viewer/viewer-hover-label.tsx +34 -0
- package/src/components/viewer/viewer-rooms.tsx +191 -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 +215 -43
- package/src/hooks/use-auto-save.ts +60 -9
- package/src/hooks/use-ceiling-events.test.ts +123 -118
- package/src/hooks/use-grid-events.test.ts +105 -105
- package/src/hooks/use-keyboard.test.ts +243 -19
- package/src/hooks/use-keyboard.ts +118 -83
- 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 +71 -0
- package/src/hooks/use-placement-notice.ts +73 -0
- package/src/hooks/use-selected-room.ts +143 -0
- package/src/index.tsx +312 -58
- package/src/lib/__fixtures__/owner-victor-seattle-parking.json +1 -0
- package/src/lib/__fixtures__/owner-villa-aurelia-basement.json +1 -0
- package/src/lib/add-storey.test.ts +177 -0
- package/src/lib/add-storey.ts +59 -0
- package/src/lib/below-grade.test.tsx +171 -0
- package/src/lib/below-grade.ts +133 -0
- package/src/lib/camera-facing.test.ts +24 -0
- package/src/lib/camera-facing.ts +24 -0
- package/src/lib/ceiling-edit-session.test.ts +321 -0
- package/src/lib/ceiling-edit-session.ts +206 -0
- package/src/lib/ceiling-region-session.test.ts +179 -0
- package/src/lib/continuation.test.ts +19 -1
- package/src/lib/continuation.ts +25 -4
- package/src/lib/direct-manipulation.ts +2 -0
- package/src/lib/door-interaction.ts +14 -0
- package/src/lib/download-project-file.ts +17 -0
- package/src/lib/editor-level-display.test.ts +96 -0
- package/src/lib/editor-level-display.ts +55 -0
- package/src/lib/editor-views.ts +178 -0
- package/src/lib/export-geometry-wait.test.ts +90 -0
- package/src/lib/export-texture-utils.ts +2 -4
- 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 +284 -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 +363 -27
- package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +177 -8
- package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +60 -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/pdfkit-standard-fonts.d.ts +5 -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 +124 -0
- package/src/lib/floorplan/sheet-export.ts +318 -0
- package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +768 -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 +230 -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 +157 -0
- package/src/lib/floorplan/site-plan/geometry.test.ts +269 -0
- package/src/lib/floorplan/site-plan/geometry.ts +142 -0
- package/src/lib/floorplan/site-plan/index.ts +55 -0
- package/src/lib/floorplan/site-plan/site-annotations.test.ts +483 -0
- package/src/lib/floorplan/site-plan/site-annotations.ts +1084 -0
- package/src/lib/floorplan/site-plan/site-parts.ts +287 -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 +70 -0
- package/src/lib/floorplan/types.ts +1 -27
- package/src/lib/follow-pascal-exterior.test-fixture.ts +17 -0
- package/src/lib/follow-pascal-exterior.test.ts +171 -0
- package/src/lib/follow-pascal-exterior.ts +39 -0
- package/src/lib/follow-pascal-presentation.test.ts +203 -0
- package/src/lib/follow-pascal-presentation.ts +129 -0
- package/src/lib/follow-pascal-sight.ts +84 -0
- package/src/lib/follow-pascal-targets.test.ts +406 -0
- package/src/lib/follow-pascal-targets.ts +198 -0
- package/src/lib/follow-pascal.test.ts +350 -0
- package/src/lib/follow-pascal.ts +284 -0
- package/src/lib/fresh-planar-placement.test.ts +148 -0
- package/src/lib/fresh-planar-placement.ts +114 -37
- package/src/lib/geometry-script/author.ts +157 -0
- package/src/lib/geometry-script/client.ts +159 -0
- package/src/lib/geometry-script/finish-scripts.ts +127 -0
- package/src/lib/geometry-script/protocol.ts +13 -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 +636 -126
- package/src/lib/glb-export.ts +241 -120
- package/src/lib/history.test.ts +18 -7
- 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 +22 -0
- package/src/lib/keyboard-pan.test.ts +129 -7
- package/src/lib/keyboard-pan.ts +161 -12
- package/src/lib/level-selection.test.ts +57 -0
- package/src/lib/level-selection.ts +16 -1
- 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 +187 -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 +392 -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/measurement-kind.test.ts +6 -8
- package/src/lib/measurements.test.ts +23 -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 +201 -0
- package/src/lib/orbit-floor-clearance.ts +154 -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 +122 -0
- package/src/lib/paint-pass-through.ts +78 -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 +267 -25
- package/src/lib/panel-rows.ts +21 -0
- package/src/lib/parametric-node-update.ts +16 -7
- 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 +65 -0
- package/src/lib/plugin-panels.test.ts +25 -0
- package/src/lib/plugin-panels.ts +94 -0
- package/src/lib/point-ask/__fixtures__/consumer-browser.tsx +76 -0
- package/src/lib/point-ask/__fixtures__/house.ts +81 -0
- package/src/lib/point-ask/ask-created.test.ts +195 -0
- package/src/lib/point-ask/ask-flow.test.ts +380 -0
- package/src/lib/point-ask/build-context.test.ts +173 -0
- package/src/lib/point-ask/build-context.ts +94 -0
- package/src/lib/point-ask/capture-crop.ts +129 -0
- package/src/lib/point-ask/choreography.ts +69 -0
- package/src/lib/point-ask/compare.test.ts +145 -0
- package/src/lib/point-ask/compare.ts +73 -0
- package/src/lib/point-ask/describe-target.test.ts +165 -0
- package/src/lib/point-ask/describe-target.ts +234 -0
- package/src/lib/point-ask/dictation.ts +79 -0
- package/src/lib/point-ask/edge-marker.test.ts +61 -0
- package/src/lib/point-ask/edge-marker.ts +27 -0
- package/src/lib/point-ask/frame-loop.ts +25 -0
- package/src/lib/point-ask/history-marks.test.ts +94 -0
- package/src/lib/point-ask/history-marks.ts +39 -0
- package/src/lib/point-ask/host-api.ts +32 -0
- package/src/lib/point-ask/outline-box.test.ts +30 -0
- package/src/lib/point-ask/outline-box.ts +11 -0
- package/src/lib/point-ask/overlay-metrics.ts +6 -0
- package/src/lib/point-ask/pick.test.ts +220 -0
- package/src/lib/point-ask/pick.ts +121 -0
- package/src/lib/point-ask/pin-machine.test.ts +494 -0
- package/src/lib/point-ask/pin-machine.ts +217 -0
- package/src/lib/point-ask/placement.test.ts +203 -0
- package/src/lib/point-ask/placement.ts +141 -0
- package/src/lib/point-ask/pointer-tracker.ts +34 -0
- package/src/lib/point-ask/projector.ts +83 -0
- package/src/lib/point-ask/region.test.ts +157 -0
- package/src/lib/point-ask/region.ts +136 -0
- package/src/lib/point-ask/retry.test.ts +217 -0
- package/src/lib/point-ask/retry.ts +37 -0
- package/src/lib/point-ask/scene-commit.test.ts +312 -0
- package/src/lib/point-ask/scene-commit.ts +32 -0
- package/src/lib/point-ask/session.ts +1391 -0
- package/src/lib/point-ask/spring-keyframes.test.ts +45 -0
- package/src/lib/point-ask/spring-keyframes.ts +38 -0
- package/src/lib/point-ask/spring.test.ts +63 -0
- package/src/lib/point-ask/spring.ts +107 -0
- package/src/lib/point-ask/suggestions.test.ts +80 -0
- package/src/lib/point-ask/suggestions.ts +125 -0
- package/src/lib/point-ask/target-scope.test.ts +622 -0
- package/src/lib/point-ask/target-scope.ts +338 -0
- package/src/lib/point-ask/types.ts +48 -0
- package/src/lib/portable-export.test.ts +34 -1
- package/src/lib/portable-export.ts +119 -53
- package/src/lib/print-feature-thickness.ts +10 -1
- package/src/lib/print-roof-solids.test.ts +39 -1
- package/src/lib/print-roof-solids.ts +43 -10
- package/src/lib/print-shell-compiler-manifold-core.ts +1 -1
- 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 +265 -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 +1017 -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 +610 -1
- package/src/lib/scene-clipboard.ts +269 -58
- 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 +54 -75
- package/src/lib/session-writes.test.ts +120 -0
- package/src/lib/session-writes.ts +205 -0
- package/src/lib/sfx-bus.ts +13 -0
- package/src/lib/sfx-player.test.ts +70 -9
- package/src/lib/sfx-player.ts +6 -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/stair-levels.test.ts +25 -0
- package/src/lib/stair-levels.ts +22 -3
- package/src/lib/terrace-draft.test.ts +145 -0
- package/src/lib/terrace-draft.ts +214 -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 +580 -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-placement-pointer.test.ts +42 -0
- package/src/lib/wall-placement-pointer.ts +57 -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-changes.ts +35 -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-audio.test.ts +146 -0
- package/src/store/use-audio.tsx +127 -3
- package/src/store/use-construction-reveal.test.ts +173 -0
- package/src/store/use-construction-reveal.ts +145 -0
- package/src/store/use-corner-snap-hint.test.ts +24 -0
- package/src/store/use-corner-snap-hint.ts +47 -0
- 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-follow-pascal.test.ts +62 -0
- package/src/store/use-follow-pascal.ts +53 -0
- package/src/store/use-point-ask.ts +247 -0
- package/src/store/use-quick-measurement-hud.ts +3 -3
- package/src/store/use-stair-build-preview.ts +33 -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 +201 -0
- package/src/store/view-layout.ts +199 -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/render-current-markup.tsx +16 -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/tools/stair/stair-click-guard.test.ts +0 -58
- package/src/components/tools/stair/stair-click-guard.ts +0 -72
- package/src/components/tools/stair/stair-defaults.ts +0 -18
- package/src/components/tools/stair/stair-tool.tsx +0 -634
- package/src/components/ui/sidebar/panels/plugins-panel.tsx +0 -218
- package/src/components/viewer/viewer-units-panel.tsx +0 -116
- package/src/lib/floorplan/stairs.ts +0 -489
- package/src/lib/level-duplication.test.ts +0 -144
- package/src/lib/level-duplication.ts +0 -195
package/src/lib/units.ts
CHANGED
|
@@ -4,7 +4,8 @@ import {
|
|
|
4
4
|
type BuildingNode,
|
|
5
5
|
type LevelNode,
|
|
6
6
|
pointInPolygon2D,
|
|
7
|
-
|
|
7
|
+
runAsSingleSceneHistoryStep,
|
|
8
|
+
sceneRegistry,
|
|
8
9
|
snapWorldXZToBuildingLocal,
|
|
9
10
|
UnitNode,
|
|
10
11
|
useScene,
|
|
@@ -12,14 +13,13 @@ import {
|
|
|
12
13
|
} from '@pascal-app/core'
|
|
13
14
|
import { useViewer } from '@pascal-app/viewer'
|
|
14
15
|
import { useEffect } from 'react'
|
|
15
|
-
import useEditor
|
|
16
|
+
import useEditor from '../store/use-editor'
|
|
16
17
|
import { getActiveBuildingPose } from './world-grid-snap'
|
|
17
18
|
|
|
18
19
|
type Nodes = Readonly<Record<AnyNodeId, AnyNode>>
|
|
19
20
|
|
|
20
21
|
// Unit focus is a session gesture: the layer the user was on when focus began
|
|
21
22
|
// comes back when focus ends, unless they switched layers by hand meanwhile.
|
|
22
|
-
let restoreLayer: StructureLayer | null = null
|
|
23
23
|
let entering = false
|
|
24
24
|
let leaving = false
|
|
25
25
|
|
|
@@ -49,11 +49,7 @@ export function enterUnitFocus(unitId: UnitNode['id']): void {
|
|
|
49
49
|
const unit = nodes[unitId]
|
|
50
50
|
if (unit?.type !== 'unit') return
|
|
51
51
|
const editor = useEditor.getState()
|
|
52
|
-
if (
|
|
53
|
-
if (editor.phase !== 'structure') editor.setPhase('structure')
|
|
54
|
-
if (useEditor.getState().structureLayer !== 'zones') {
|
|
55
|
-
useEditor.getState().setStructureLayer('zones')
|
|
56
|
-
}
|
|
52
|
+
if (editor.phase === 'site') editor.setPhase('building')
|
|
57
53
|
const viewer = useViewer.getState()
|
|
58
54
|
if (viewer.focusedUnitId !== unitId) viewer.setFocusedUnit(unitId)
|
|
59
55
|
|
|
@@ -72,19 +68,7 @@ export function enterUnitFocus(unitId: UnitNode['id']): void {
|
|
|
72
68
|
}
|
|
73
69
|
}
|
|
74
70
|
|
|
75
|
-
function finishLeave(unitId: UnitNode['id']
|
|
76
|
-
const layer = restoreLayer
|
|
77
|
-
restoreLayer = null
|
|
78
|
-
const editor = useEditor.getState()
|
|
79
|
-
if (
|
|
80
|
-
!keepLayer &&
|
|
81
|
-
layer &&
|
|
82
|
-
layer !== 'zones' &&
|
|
83
|
-
editor.phase === 'structure' &&
|
|
84
|
-
editor.structureLayer === 'zones'
|
|
85
|
-
) {
|
|
86
|
-
editor.setStructureLayer(layer)
|
|
87
|
-
}
|
|
71
|
+
function finishLeave(unitId: UnitNode['id']) {
|
|
88
72
|
const { selection, setSelection } = useViewer.getState()
|
|
89
73
|
if (selection.selectedIds.length === 1 && selection.selectedIds[0] === unitId) {
|
|
90
74
|
setSelection({ selectedIds: [] })
|
|
@@ -92,7 +76,7 @@ function finishLeave(unitId: UnitNode['id'], keepLayer: boolean) {
|
|
|
92
76
|
}
|
|
93
77
|
|
|
94
78
|
/** Ends unit focus. Returns false when no unit was focused. */
|
|
95
|
-
export function leaveUnitFocus(
|
|
79
|
+
export function leaveUnitFocus(): boolean {
|
|
96
80
|
const viewer = useViewer.getState()
|
|
97
81
|
const unitId = viewer.focusedUnitId
|
|
98
82
|
if (!unitId) return false
|
|
@@ -102,7 +86,7 @@ export function leaveUnitFocus(options?: { keepLayer?: boolean }): boolean {
|
|
|
102
86
|
} finally {
|
|
103
87
|
leaving = false
|
|
104
88
|
}
|
|
105
|
-
finishLeave(unitId
|
|
89
|
+
finishLeave(unitId)
|
|
106
90
|
return true
|
|
107
91
|
}
|
|
108
92
|
|
|
@@ -124,28 +108,75 @@ export function assignZoneToUnit(zoneId: ZoneNode['id'], unitId: UnitNode['id']
|
|
|
124
108
|
if (updates.length > 0) updateNodes(updates)
|
|
125
109
|
}
|
|
126
110
|
|
|
111
|
+
/** The units of the building a zone's level belongs to, and the one listing the zone. */
|
|
112
|
+
export function zoneUnits(
|
|
113
|
+
nodes: Nodes,
|
|
114
|
+
zoneId: string,
|
|
115
|
+
): { buildingId: BuildingNode['id'] | null; units: UnitNode[]; current: UnitNode | null } {
|
|
116
|
+
const zone = nodes[zoneId as AnyNodeId]
|
|
117
|
+
const level = zone?.parentId ? nodes[zone.parentId as AnyNodeId] : undefined
|
|
118
|
+
const building = level?.parentId ? nodes[level.parentId as AnyNodeId] : undefined
|
|
119
|
+
if (building?.type !== 'building') return { buildingId: null, units: [], current: null }
|
|
120
|
+
const units = building.children.flatMap((id) => {
|
|
121
|
+
const child = nodes[id as AnyNodeId]
|
|
122
|
+
return child?.type === 'unit' ? [child] : []
|
|
123
|
+
})
|
|
124
|
+
return {
|
|
125
|
+
buildingId: building.id,
|
|
126
|
+
units,
|
|
127
|
+
current: units.find((unit) => unit.members.includes(zoneId as ZoneNode['id'])) ?? null,
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Puts a zone in a new unit of its building (named "Unit N"), in one undo step, without focusing it. */
|
|
132
|
+
export function addZoneToNewUnit(zoneId: ZoneNode['id']): UnitNode['id'] | null {
|
|
133
|
+
const { buildingId, units } = zoneUnits(useScene.getState().nodes, zoneId)
|
|
134
|
+
if (!buildingId) return null
|
|
135
|
+
const unit = UnitNode.parse({ name: `Unit ${units.length + 1}` })
|
|
136
|
+
runAsSingleSceneHistoryStep(useScene, () => {
|
|
137
|
+
useScene.getState().createNode(unit, buildingId)
|
|
138
|
+
assignZoneToUnit(zoneId, unit.id)
|
|
139
|
+
})
|
|
140
|
+
return unit.id
|
|
141
|
+
}
|
|
142
|
+
|
|
127
143
|
export function toggleZoneMembership(unitId: UnitNode['id'], zoneId: ZoneNode['id']): void {
|
|
128
144
|
const unit = useScene.getState().nodes[unitId]
|
|
129
145
|
if (unit?.type !== 'unit') return
|
|
130
146
|
assignZoneToUnit(zoneId, unit.members.includes(zoneId) ? null : unitId)
|
|
131
147
|
}
|
|
132
148
|
|
|
149
|
+
/** How far (m) below a mezzanine's walking surface a hit still counts as on it (its plate and underside). */
|
|
150
|
+
const MEZZANINE_HIT_TOLERANCE = 0.05
|
|
151
|
+
|
|
133
152
|
/**
|
|
134
153
|
* The zone under a point on the current level, in building-local XZ. A 3D
|
|
135
154
|
* click lands on whatever surface is closest (a floor slab sits above the
|
|
136
155
|
* zone fill), so painting resolves the zone from the hit point instead of the
|
|
137
|
-
* hit mesh. Nested zones resolve to the smallest one
|
|
156
|
+
* hit mesh. Nested zones resolve to the smallest one, except that a mezzanine
|
|
157
|
+
* the hit may be on always wins over its host. With the hit's height (`y`,
|
|
158
|
+
* level-local) a mezzanine counts only when the hit is on or above its plate —
|
|
159
|
+
* the host floor under it is the host's; without one (the plan, seen from
|
|
160
|
+
* above) the mezzanine on top wins.
|
|
138
161
|
*/
|
|
139
|
-
export function zoneAtLevelPoint(x: number, z: number): ZoneNode | null {
|
|
162
|
+
export function zoneAtLevelPoint(x: number, z: number, y?: number): ZoneNode | null {
|
|
140
163
|
const levelId = useViewer.getState().selection.levelId
|
|
141
164
|
if (!levelId) return null
|
|
142
165
|
const nodes = useScene.getState().nodes
|
|
143
|
-
const resolve = (id: AnyNodeId) => nodes[id]
|
|
144
166
|
let best: ZoneNode | null = null
|
|
145
167
|
let bestArea = Number.POSITIVE_INFINITY
|
|
168
|
+
let bestOnTop = false
|
|
146
169
|
for (const node of Object.values(nodes)) {
|
|
147
170
|
if (node.type !== 'zone' || node.parentId !== levelId) continue
|
|
148
|
-
|
|
171
|
+
// An eligible mezzanine is the surface on top: it beats its host whatever
|
|
172
|
+
// their areas (a mezzanine may cover its whole host).
|
|
173
|
+
const onTop = node.floor?.support === 'open'
|
|
174
|
+
if (y !== undefined && onTop) {
|
|
175
|
+
const underside = (node.floor!.elevation ?? 0) - (node.floor!.thickness ?? 0.2)
|
|
176
|
+
if (y < underside - MEZZANINE_HIT_TOLERANCE) continue
|
|
177
|
+
}
|
|
178
|
+
if (bestOnTop && !onTop) continue
|
|
179
|
+
const polygon = node.polygon
|
|
149
180
|
if (!pointInPolygon2D([x, z], polygon)) continue
|
|
150
181
|
let area = 0
|
|
151
182
|
for (let i = 0; i < polygon.length; i++) {
|
|
@@ -154,20 +185,30 @@ export function zoneAtLevelPoint(x: number, z: number): ZoneNode | null {
|
|
|
154
185
|
area += a[0] * b[1] - b[0] * a[1]
|
|
155
186
|
}
|
|
156
187
|
area = Math.abs(area) / 2
|
|
157
|
-
if (area < bestArea) {
|
|
188
|
+
if (area < bestArea || (onTop && !bestOnTop)) {
|
|
158
189
|
best = node
|
|
159
190
|
bestArea = area
|
|
191
|
+
bestOnTop = onTop
|
|
160
192
|
}
|
|
161
193
|
}
|
|
162
194
|
return best
|
|
163
195
|
}
|
|
164
196
|
|
|
165
|
-
|
|
197
|
+
/** `worldY` (optional): the hit's height, so a click under a mezzanine picks the host. */
|
|
198
|
+
export function zoneAtWorldPoint(worldX: number, worldZ: number, worldY?: number): ZoneNode | null {
|
|
166
199
|
const pose = getActiveBuildingPose()
|
|
167
200
|
const [x, z] = pose
|
|
168
201
|
? snapWorldXZToBuildingLocal(worldX, worldZ, pose.position, pose.rotationY, 0).local
|
|
169
202
|
: [worldX, worldZ]
|
|
170
|
-
return zoneAtLevelPoint(x, z)
|
|
203
|
+
return zoneAtLevelPoint(x, z, worldY === undefined ? undefined : levelLocalY(worldY))
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function levelLocalY(worldY: number): number {
|
|
207
|
+
const levelId = useViewer.getState().selection.levelId
|
|
208
|
+
const level = levelId ? sceneRegistry.nodes.get(levelId) : undefined
|
|
209
|
+
if (!level) return worldY
|
|
210
|
+
level.updateWorldMatrix(true, false)
|
|
211
|
+
return worldY - level.matrixWorld.elements[13]!
|
|
171
212
|
}
|
|
172
213
|
|
|
173
214
|
export const ZONE_PAINT_DELAY_MS = 250
|
|
@@ -211,32 +252,29 @@ export function createUnitInBuilding(buildingId: BuildingNode['id']): UnitNode['
|
|
|
211
252
|
/**
|
|
212
253
|
* Keeps unit focus coherent with the rest of the editor: a focus set from
|
|
213
254
|
* anywhere (the unit inspector calls the viewer store directly) gets the full
|
|
214
|
-
* enter treatment, and focus ends when the user
|
|
215
|
-
*
|
|
255
|
+
* enter treatment, and focus ends when the user goes to site, selects
|
|
256
|
+
* anything other than the focused unit, or the unit is deleted.
|
|
216
257
|
*/
|
|
217
258
|
export function useUnitFocusRules(): void {
|
|
218
259
|
useEffect(() => {
|
|
219
260
|
const unsubscribeViewer = useViewer.subscribe((state, prev) => {
|
|
220
261
|
if (state.focusedUnitId !== prev.focusedUnitId) {
|
|
221
262
|
if (state.focusedUnitId) enterUnitFocus(state.focusedUnitId)
|
|
222
|
-
else if (prev.focusedUnitId && !leaving) finishLeave(prev.focusedUnitId
|
|
263
|
+
else if (prev.focusedUnitId && !leaving) finishLeave(prev.focusedUnitId)
|
|
223
264
|
return
|
|
224
265
|
}
|
|
225
266
|
const unitId = state.focusedUnitId
|
|
226
267
|
if (!unitId || state.selection === prev.selection) return
|
|
227
268
|
// Selecting a zone keeps focus: renaming or recolouring a zone row is
|
|
228
|
-
// part of arranging the unit
|
|
269
|
+
// part of arranging the unit.
|
|
229
270
|
const { selectedIds } = state.selection
|
|
230
271
|
const selectsOther =
|
|
231
272
|
selectedIds.length > 0 && !(selectedIds.length === 1 && selectedIds[0] === unitId)
|
|
232
273
|
if (selectsOther) leaveUnitFocus()
|
|
233
274
|
})
|
|
234
275
|
const unsubscribeEditor = useEditor.subscribe((state, prev) => {
|
|
235
|
-
if (state.
|
|
236
|
-
if (
|
|
237
|
-
if (state.phase !== 'structure' || state.structureLayer !== 'zones') {
|
|
238
|
-
leaveUnitFocus({ keepLayer: true })
|
|
239
|
-
}
|
|
276
|
+
if (state.phase === prev.phase || state.phase === 'building') return
|
|
277
|
+
if (useViewer.getState().focusedUnitId) leaveUnitFocus()
|
|
240
278
|
})
|
|
241
279
|
const unsubscribeScene = useScene.subscribe((state, prev) => {
|
|
242
280
|
if (state.nodes === prev.nodes) return
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import * as THREE from 'three'
|
|
3
|
+
import { candidateEdges, hideUndrawnMeshes, materialShows } from './vector-edges'
|
|
4
|
+
|
|
5
|
+
/** A unit box's feature edges: 12, six floats each. */
|
|
6
|
+
const BOX_EDGE_FLOATS = 12 * 6
|
|
7
|
+
|
|
8
|
+
function sceneWithProxy() {
|
|
9
|
+
const scene = new THREE.Scene()
|
|
10
|
+
const house = new THREE.Mesh(new THREE.BoxGeometry(1, 1, 1), new THREE.MeshBasicMaterial())
|
|
11
|
+
// a utility-pole pick proxy: a tall box that draws nothing
|
|
12
|
+
const proxy = new THREE.Mesh(
|
|
13
|
+
new THREE.BoxGeometry(0.5, 10.7, 0.5),
|
|
14
|
+
new THREE.MeshBasicMaterial({ colorWrite: false, depthWrite: false }),
|
|
15
|
+
)
|
|
16
|
+
proxy.position.set(3, 0, 0)
|
|
17
|
+
scene.add(house, proxy)
|
|
18
|
+
scene.updateMatrixWorld(true)
|
|
19
|
+
return { scene, house, proxy }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
describe('vector edges skip what draws nothing', () => {
|
|
23
|
+
test('a pick proxy (colorWrite false) has no lines', () => {
|
|
24
|
+
const { scene } = sceneWithProxy()
|
|
25
|
+
const camera = new THREE.OrthographicCamera(-10, 10, 10, -10, 0.1, 100)
|
|
26
|
+
const world = candidateEdges(scene, camera, 20, 400_000)
|
|
27
|
+
expect(world).toHaveLength(BOX_EDGE_FLOATS)
|
|
28
|
+
// every edge is the 1 m box's, none the 10.7 m proxy's
|
|
29
|
+
for (let i = 1; i < world.length; i += 3)
|
|
30
|
+
expect(Math.abs(world[i] as number)).toBeLessThanOrEqual(0.5)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
test('hidden materials, see-through hit boxes and colour-masked colliders all count as undrawn', () => {
|
|
34
|
+
expect(materialShows(new THREE.MeshBasicMaterial())).toBe(true)
|
|
35
|
+
expect(materialShows(new THREE.MeshBasicMaterial({ colorWrite: false }))).toBe(false)
|
|
36
|
+
expect(materialShows(new THREE.MeshBasicMaterial({ visible: false }))).toBe(false)
|
|
37
|
+
expect(materialShows(new THREE.MeshBasicMaterial({ transparent: true, opacity: 0 }))).toBe(
|
|
38
|
+
false,
|
|
39
|
+
)
|
|
40
|
+
expect(
|
|
41
|
+
materialShows([
|
|
42
|
+
new THREE.MeshBasicMaterial({ colorWrite: false }),
|
|
43
|
+
new THREE.MeshBasicMaterial(),
|
|
44
|
+
]),
|
|
45
|
+
).toBe(true)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
test('the depth and normal passes see no pick proxy, and it comes back after', () => {
|
|
49
|
+
const { scene, house, proxy } = sceneWithProxy()
|
|
50
|
+
const show = hideUndrawnMeshes(scene)
|
|
51
|
+
// under the passes' override material the proxy would write depth and
|
|
52
|
+
// hide the real edges behind it
|
|
53
|
+
expect(proxy.visible).toBe(false)
|
|
54
|
+
expect(house.visible).toBe(true)
|
|
55
|
+
show()
|
|
56
|
+
expect(proxy.visible).toBe(true)
|
|
57
|
+
})
|
|
58
|
+
})
|
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VECTOR EDGES FROM THE VIEWER — the elevation's lines taken from the same
|
|
3
|
+
* meshes the picture is rendered from, with hidden-line removal.
|
|
4
|
+
*
|
|
5
|
+
* A drawing built from a second model of the house (a plugin's section solids)
|
|
6
|
+
* never quite matches the render: its roof sat on the plate plane while the
|
|
7
|
+
* 3D roof stacks a deck and shingles on it, its door was the opening while
|
|
8
|
+
* the render showed the leaf. This is how Revit and Chief Architect avoid
|
|
9
|
+
* that — ONE model, the view's lines derived from it:
|
|
10
|
+
*
|
|
11
|
+
* 1. every visible mesh's FEATURE EDGES (creases over `thresholdDeg`,
|
|
12
|
+
* boundaries — three's EdgesGeometry, cached per geometry), carried
|
|
13
|
+
* to world space, instance by instance for InstancedMeshes;
|
|
14
|
+
* 2. a DEPTH pass of the scene through the capture camera — view-space
|
|
15
|
+
* depth as a float colour, read back once;
|
|
16
|
+
* 3. each edge projected through that camera and SAMPLED along its length
|
|
17
|
+
* against the depth buffer: a point deeper than what the buffer saw is
|
|
18
|
+
* behind something. The visible runs come back as world segments.
|
|
19
|
+
*
|
|
20
|
+
* Orthographic only (an elevation, a section): depth is linear across a face
|
|
21
|
+
* so the interpolated edge depth is exact. Not a boolean operation — an
|
|
22
|
+
* edge test, cheap enough to run at capture time.
|
|
23
|
+
*/
|
|
24
|
+
import * as THREE from 'three'
|
|
25
|
+
import { normalView, positionView, vec4 } from 'three/tsl'
|
|
26
|
+
import { MeshBasicNodeMaterial, RenderTarget } from 'three/webgpu'
|
|
27
|
+
|
|
28
|
+
export type VisibleEdges = {
|
|
29
|
+
/** World-space segments, six floats each: ax ay az bx by bz. */
|
|
30
|
+
segments: Float32Array
|
|
31
|
+
count: number
|
|
32
|
+
/** How many candidate edges were tested, and the depth pass' size. */
|
|
33
|
+
tested: number
|
|
34
|
+
width: number
|
|
35
|
+
height: number
|
|
36
|
+
ms: number
|
|
37
|
+
/** Whether the normal pass came back (without it every in-view edge is kept). */
|
|
38
|
+
normals: boolean
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const edgeCache = new WeakMap<THREE.BufferGeometry, Map<number, Float32Array | null>>()
|
|
42
|
+
|
|
43
|
+
/** The feature edges of a geometry, local space, as pairs of points (cached per threshold; null when the geometry is too heavy). */
|
|
44
|
+
function featureEdgesOf(
|
|
45
|
+
geometry: THREE.BufferGeometry,
|
|
46
|
+
thresholdDeg: number,
|
|
47
|
+
maxTriangles: number,
|
|
48
|
+
): Float32Array | null {
|
|
49
|
+
const position = geometry.getAttribute('position')
|
|
50
|
+
if (!position) return null
|
|
51
|
+
const triangles = geometry.index ? geometry.index.count / 3 : position.count / 3
|
|
52
|
+
if (triangles > maxTriangles) return null
|
|
53
|
+
let byThreshold = edgeCache.get(geometry)
|
|
54
|
+
if (!byThreshold) {
|
|
55
|
+
byThreshold = new Map()
|
|
56
|
+
edgeCache.set(geometry, byThreshold)
|
|
57
|
+
}
|
|
58
|
+
const cached = byThreshold.get(thresholdDeg)
|
|
59
|
+
if (cached !== undefined) return cached
|
|
60
|
+
let edges: Float32Array | null = null
|
|
61
|
+
try {
|
|
62
|
+
const eg = new THREE.EdgesGeometry(geometry, thresholdDeg)
|
|
63
|
+
const attr = eg.getAttribute('position')
|
|
64
|
+
edges = attr ? new Float32Array(attr.array as ArrayLike<number>) : null
|
|
65
|
+
eg.dispose()
|
|
66
|
+
} catch {
|
|
67
|
+
edges = null
|
|
68
|
+
}
|
|
69
|
+
byThreshold.set(thresholdDeg, edges)
|
|
70
|
+
return edges
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function isShown(object: THREE.Object3D): boolean {
|
|
74
|
+
let o: THREE.Object3D | null = object
|
|
75
|
+
while (o) {
|
|
76
|
+
if (!o.visible) return false
|
|
77
|
+
o = o.parent
|
|
78
|
+
}
|
|
79
|
+
return true
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Whether a material draws anything. `colorWrite: false` is how a pick-only
|
|
84
|
+
* collider hides on the GPU (a plugin's utility-pole proxy, 0.5 × 10.7 m, once
|
|
85
|
+
* drew a tall rectangle through the elevations, 2026-09-23 — the rule
|
|
86
|
+
* glb-export's `isRenderableMesh` already follows).
|
|
87
|
+
*/
|
|
88
|
+
export function materialShows(material: THREE.Material | THREE.Material[]): boolean {
|
|
89
|
+
const list = Array.isArray(material) ? material : [material]
|
|
90
|
+
return list.some(
|
|
91
|
+
(m) =>
|
|
92
|
+
m && m.visible !== false && m.colorWrite !== false && !(m.transparent && m.opacity <= 0.02),
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* The shown meshes whose own materials draw nothing — pick proxies, hit
|
|
98
|
+
* boxes — hidden for the length of the buffer passes: under the passes'
|
|
99
|
+
* override material they would write depth and hide the real edges behind
|
|
100
|
+
* them. The returned function shows them again.
|
|
101
|
+
*/
|
|
102
|
+
export function hideUndrawnMeshes(scene: THREE.Object3D): () => void {
|
|
103
|
+
const hidden: THREE.Object3D[] = []
|
|
104
|
+
scene.traverse((object) => {
|
|
105
|
+
const mesh = object as THREE.Mesh
|
|
106
|
+
if (!mesh.isMesh || !mesh.visible || materialShows(mesh.material)) return
|
|
107
|
+
mesh.visible = false
|
|
108
|
+
hidden.push(mesh)
|
|
109
|
+
})
|
|
110
|
+
return () => {
|
|
111
|
+
for (const mesh of hidden) mesh.visible = true
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* One pass of the scene through `material` into a float target, read back:
|
|
117
|
+
* the depth pass writes view-space depth (metres in front of the camera) in
|
|
118
|
+
* red; the normal pass the view-space normal in RGB.
|
|
119
|
+
*/
|
|
120
|
+
async function bufferPass(
|
|
121
|
+
renderer: THREE.WebGLRenderer,
|
|
122
|
+
scene: THREE.Scene,
|
|
123
|
+
camera: THREE.Camera,
|
|
124
|
+
width: number,
|
|
125
|
+
height: number,
|
|
126
|
+
colorNode: unknown,
|
|
127
|
+
): Promise<{ data: Float32Array; topDown: boolean } | null> {
|
|
128
|
+
const material = new MeshBasicNodeMaterial()
|
|
129
|
+
material.colorNode = colorNode as never
|
|
130
|
+
material.side = THREE.DoubleSide
|
|
131
|
+
material.transparent = false
|
|
132
|
+
material.depthTest = true
|
|
133
|
+
material.depthWrite = true
|
|
134
|
+
const target = new RenderTarget(width, height, {
|
|
135
|
+
type: THREE.FloatType,
|
|
136
|
+
format: THREE.RGBAFormat,
|
|
137
|
+
depthBuffer: true,
|
|
138
|
+
minFilter: THREE.NearestFilter,
|
|
139
|
+
magFilter: THREE.NearestFilter,
|
|
140
|
+
generateMipmaps: false,
|
|
141
|
+
})
|
|
142
|
+
const gl = renderer as unknown as {
|
|
143
|
+
getRenderTarget: () => unknown
|
|
144
|
+
setRenderTarget: (t: unknown) => void
|
|
145
|
+
getClearColor: (c: THREE.Color) => THREE.Color
|
|
146
|
+
getClearAlpha: () => number
|
|
147
|
+
setClearColor: (c: THREE.Color | number, a?: number) => void
|
|
148
|
+
renderAsync: (s: THREE.Scene, c: THREE.Camera) => Promise<void>
|
|
149
|
+
readRenderTargetPixelsAsync: (
|
|
150
|
+
t: unknown,
|
|
151
|
+
x: number,
|
|
152
|
+
y: number,
|
|
153
|
+
w: number,
|
|
154
|
+
h: number,
|
|
155
|
+
) => Promise<ArrayLike<number>>
|
|
156
|
+
backend?: { device?: unknown }
|
|
157
|
+
}
|
|
158
|
+
const previousTarget = gl.getRenderTarget()
|
|
159
|
+
const previousOverride = scene.overrideMaterial
|
|
160
|
+
const previousClear = gl.getClearColor(new THREE.Color())
|
|
161
|
+
const previousAlpha = gl.getClearAlpha()
|
|
162
|
+
try {
|
|
163
|
+
scene.overrideMaterial = material
|
|
164
|
+
gl.setClearColor(new THREE.Color(0, 0, 0), 0)
|
|
165
|
+
gl.setRenderTarget(target)
|
|
166
|
+
await gl.renderAsync(scene, camera)
|
|
167
|
+
gl.setRenderTarget(null)
|
|
168
|
+
const raw = await gl.readRenderTargetPixelsAsync(target, 0, 0, width, height)
|
|
169
|
+
const data = raw instanceof Float32Array ? raw : new Float32Array(raw)
|
|
170
|
+
if (data.length < width * height * 4) return null
|
|
171
|
+
// WebGPU reads rows top-down; the WebGL fallback bottom-up
|
|
172
|
+
return { data, topDown: Boolean(gl.backend?.device) }
|
|
173
|
+
} catch (error) {
|
|
174
|
+
console.warn('[vector-edges] buffer pass failed', error)
|
|
175
|
+
return null
|
|
176
|
+
} finally {
|
|
177
|
+
scene.overrideMaterial = previousOverride
|
|
178
|
+
gl.setClearColor(previousClear, previousAlpha)
|
|
179
|
+
gl.setRenderTarget(previousTarget)
|
|
180
|
+
target.dispose()
|
|
181
|
+
material.dispose()
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Every drawn mesh's feature edges in world space, six floats per edge —
|
|
187
|
+
* instance by instance for an InstancedMesh. What draws nothing (a hidden
|
|
188
|
+
* mesh, a pick proxy, a layer the camera does not see) has no lines.
|
|
189
|
+
*/
|
|
190
|
+
export function candidateEdges(
|
|
191
|
+
scene: THREE.Object3D,
|
|
192
|
+
camera: THREE.Camera,
|
|
193
|
+
thresholdDeg: number,
|
|
194
|
+
maxTriangles: number,
|
|
195
|
+
): number[] {
|
|
196
|
+
const world: number[] = []
|
|
197
|
+
const a = new THREE.Vector3()
|
|
198
|
+
const b = new THREE.Vector3()
|
|
199
|
+
const instance = new THREE.Matrix4()
|
|
200
|
+
const composed = new THREE.Matrix4()
|
|
201
|
+
scene.traverse((object) => {
|
|
202
|
+
const mesh = object as THREE.Mesh
|
|
203
|
+
if (!mesh.isMesh || !mesh.geometry) return
|
|
204
|
+
if ((mesh as unknown as { isSkinnedMesh?: boolean }).isSkinnedMesh) return
|
|
205
|
+
if (!camera.layers.test(mesh.layers)) return
|
|
206
|
+
if (!isShown(mesh) || !materialShows(mesh.material)) return
|
|
207
|
+
const edges = featureEdgesOf(mesh.geometry, thresholdDeg, maxTriangles)
|
|
208
|
+
if (!edges || edges.length < 6) return
|
|
209
|
+
const instanced = mesh as unknown as THREE.InstancedMesh
|
|
210
|
+
const matrices: THREE.Matrix4[] = []
|
|
211
|
+
if (instanced.isInstancedMesh) {
|
|
212
|
+
for (let i = 0; i < instanced.count; i++) {
|
|
213
|
+
instanced.getMatrixAt(i, instance)
|
|
214
|
+
matrices.push(composed.copy(mesh.matrixWorld).multiply(instance).clone())
|
|
215
|
+
}
|
|
216
|
+
} else {
|
|
217
|
+
matrices.push(mesh.matrixWorld)
|
|
218
|
+
}
|
|
219
|
+
for (const m of matrices) {
|
|
220
|
+
for (let i = 0; i + 5 < edges.length; i += 6) {
|
|
221
|
+
a.set(edges[i] as number, edges[i + 1] as number, edges[i + 2] as number).applyMatrix4(m)
|
|
222
|
+
b.set(edges[i + 3] as number, edges[i + 4] as number, edges[i + 5] as number).applyMatrix4(
|
|
223
|
+
m,
|
|
224
|
+
)
|
|
225
|
+
world.push(a.x, a.y, a.z, b.x, b.y, b.z)
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
})
|
|
229
|
+
return world
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* The visible feature edges of `scene` through `camera` (orthographic), as
|
|
234
|
+
* world segments. `width` sets the depth pass (a multiple of 16 keeps the
|
|
235
|
+
* WebGPU readback rows unpadded); `height` follows the camera's aspect.
|
|
236
|
+
*/
|
|
237
|
+
export async function extractVisibleEdges(
|
|
238
|
+
renderer: THREE.WebGLRenderer,
|
|
239
|
+
scene: THREE.Scene,
|
|
240
|
+
camera: THREE.OrthographicCamera,
|
|
241
|
+
options: { width?: number; thresholdDeg?: number; maxTriangles?: number; bias?: number } = {},
|
|
242
|
+
): Promise<VisibleEdges | null> {
|
|
243
|
+
const started = performance.now()
|
|
244
|
+
const thresholdDeg = options.thresholdDeg ?? 20
|
|
245
|
+
const maxTriangles = options.maxTriangles ?? 400_000
|
|
246
|
+
const aspect = (camera.right - camera.left) / Math.max(1e-6, camera.top - camera.bottom)
|
|
247
|
+
const width = Math.max(16, Math.round((options.width ?? 2048) / 16) * 16)
|
|
248
|
+
const height = Math.max(16, Math.round(width / aspect / 16) * 16)
|
|
249
|
+
camera.updateMatrixWorld(true)
|
|
250
|
+
scene.updateMatrixWorld(true)
|
|
251
|
+
|
|
252
|
+
const showUndrawn = hideUndrawnMeshes(scene)
|
|
253
|
+
let pass: Awaited<ReturnType<typeof bufferPass>>
|
|
254
|
+
let normalPass: Awaited<ReturnType<typeof bufferPass>> = null
|
|
255
|
+
try {
|
|
256
|
+
pass = await bufferPass(
|
|
257
|
+
renderer,
|
|
258
|
+
scene,
|
|
259
|
+
camera,
|
|
260
|
+
width,
|
|
261
|
+
height,
|
|
262
|
+
vec4(positionView.z.negate(), 0, 0, 1),
|
|
263
|
+
)
|
|
264
|
+
if (pass)
|
|
265
|
+
normalPass = await bufferPass(renderer, scene, camera, width, height, vec4(normalView, 1))
|
|
266
|
+
} finally {
|
|
267
|
+
showUndrawn()
|
|
268
|
+
}
|
|
269
|
+
if (!pass) return null
|
|
270
|
+
const { data: depth, topDown } = pass
|
|
271
|
+
const normals = normalPass?.data ?? null
|
|
272
|
+
|
|
273
|
+
const world = candidateEdges(scene, camera, thresholdDeg, maxTriangles)
|
|
274
|
+
|
|
275
|
+
// ── the visibility test ──────────────────────────────────────────────
|
|
276
|
+
const view = camera.matrixWorldInverse
|
|
277
|
+
const proj = camera.projectionMatrix
|
|
278
|
+
const bias = options.bias ?? 0.008
|
|
279
|
+
const out: number[] = []
|
|
280
|
+
const va = new THREE.Vector3()
|
|
281
|
+
const vb = new THREE.Vector3()
|
|
282
|
+
const pa = new THREE.Vector3()
|
|
283
|
+
const pb = new THREE.Vector3()
|
|
284
|
+
const indexAt = (px: number, py: number): number => {
|
|
285
|
+
const x = Math.floor(px)
|
|
286
|
+
const y = Math.floor(py)
|
|
287
|
+
if (x < 0 || y < 0 || x >= width || y >= height) return -1
|
|
288
|
+
const row = topDown ? y : height - 1 - y
|
|
289
|
+
return (row * width + x) * 4
|
|
290
|
+
}
|
|
291
|
+
const sampleAt = (px: number, py: number): number => {
|
|
292
|
+
const k = indexAt(px, py)
|
|
293
|
+
return k < 0 ? Number.POSITIVE_INFINITY : (depth[k] as number)
|
|
294
|
+
}
|
|
295
|
+
// A line is only a line where the render CHANGES across it: a step in
|
|
296
|
+
// depth (a silhouette, a reveal, the far side of a box) or a crease in
|
|
297
|
+
// the normal (wall meets roof, the corner of a post). A flat face's own
|
|
298
|
+
// triangulation — the CSG walls come with T-junctions, so the edge
|
|
299
|
+
// finder cannot pair those triangles — shows neither and is dropped.
|
|
300
|
+
const CREASE = Math.cos((15 * Math.PI) / 180)
|
|
301
|
+
const ACROSS = 2
|
|
302
|
+
const featureAt = (px: number, py: number, nx: number, ny: number): boolean => {
|
|
303
|
+
const c = sampleAt(px, py)
|
|
304
|
+
const l = sampleAt(px + nx * ACROSS, py + ny * ACROSS)
|
|
305
|
+
const r = sampleAt(px - nx * ACROSS, py - ny * ACROSS)
|
|
306
|
+
// the sky on either side, or off the frame: a silhouette
|
|
307
|
+
if (c === 0 || l === 0 || r === 0) return true
|
|
308
|
+
if (!Number.isFinite(l) || !Number.isFinite(r) || !Number.isFinite(c)) return true
|
|
309
|
+
// a step, not a slope: the second difference is zero on any plane
|
|
310
|
+
if (Math.abs(l + r - 2 * c) > 0.012) return true
|
|
311
|
+
if (!normals) return true
|
|
312
|
+
const kl = indexAt(px + nx * ACROSS, py + ny * ACROSS)
|
|
313
|
+
const kr = indexAt(px - nx * ACROSS, py - ny * ACROSS)
|
|
314
|
+
if (kl < 0 || kr < 0) return true
|
|
315
|
+
// unsigned: a CSG face's triangles may wind either way, and a flipped
|
|
316
|
+
// normal on a flat wall is not a corner
|
|
317
|
+
const dot =
|
|
318
|
+
(normals[kl] as number) * (normals[kr] as number) +
|
|
319
|
+
(normals[kl + 1] as number) * (normals[kr + 1] as number) +
|
|
320
|
+
(normals[kl + 2] as number) * (normals[kr + 2] as number)
|
|
321
|
+
return Math.abs(dot) < CREASE
|
|
322
|
+
}
|
|
323
|
+
const tested = world.length / 6
|
|
324
|
+
for (let i = 0; i + 5 < world.length; i += 6) {
|
|
325
|
+
va.set(world[i] as number, world[i + 1] as number, world[i + 2] as number).applyMatrix4(view)
|
|
326
|
+
vb.set(world[i + 3] as number, world[i + 4] as number, world[i + 5] as number).applyMatrix4(
|
|
327
|
+
view,
|
|
328
|
+
)
|
|
329
|
+
const da = -va.z
|
|
330
|
+
const db = -vb.z
|
|
331
|
+
if (da <= camera.near && db <= camera.near) continue
|
|
332
|
+
pa.copy(va).applyMatrix4(proj)
|
|
333
|
+
pb.copy(vb).applyMatrix4(proj)
|
|
334
|
+
const ax = ((pa.x + 1) / 2) * width
|
|
335
|
+
const ay = ((1 - pa.y) / 2) * height
|
|
336
|
+
const bx = ((pb.x + 1) / 2) * width
|
|
337
|
+
const by = ((1 - pb.y) / 2) * height
|
|
338
|
+
// wholly off the frame: nothing to test
|
|
339
|
+
if (
|
|
340
|
+
(ax < 0 && bx < 0) ||
|
|
341
|
+
(ax >= width && bx >= width) ||
|
|
342
|
+
(ay < 0 && by < 0) ||
|
|
343
|
+
(ay >= height && by >= height)
|
|
344
|
+
) {
|
|
345
|
+
continue
|
|
346
|
+
}
|
|
347
|
+
const lengthPx = Math.hypot(bx - ax, by - ay)
|
|
348
|
+
const n = Math.min(4096, Math.max(2, Math.ceil(lengthPx)))
|
|
349
|
+
const slope = Math.abs(db - da) / n
|
|
350
|
+
const tolerance = bias + slope * 1.5
|
|
351
|
+
// the screen-space perpendicular, to look across the edge
|
|
352
|
+
const nx = lengthPx > 1e-6 ? -(by - ay) / lengthPx : 1
|
|
353
|
+
const ny = lengthPx > 1e-6 ? (bx - ax) / lengthPx : 0
|
|
354
|
+
let runStart = -1
|
|
355
|
+
let gap = 0
|
|
356
|
+
const emit = (t0: number, t1: number) => {
|
|
357
|
+
if (t1 - t0 <= 0) return
|
|
358
|
+
out.push(
|
|
359
|
+
(world[i] as number) + ((world[i + 3] as number) - (world[i] as number)) * t0,
|
|
360
|
+
(world[i + 1] as number) + ((world[i + 4] as number) - (world[i + 1] as number)) * t0,
|
|
361
|
+
(world[i + 2] as number) + ((world[i + 5] as number) - (world[i + 2] as number)) * t0,
|
|
362
|
+
(world[i] as number) + ((world[i + 3] as number) - (world[i] as number)) * t1,
|
|
363
|
+
(world[i + 1] as number) + ((world[i + 4] as number) - (world[i + 1] as number)) * t1,
|
|
364
|
+
(world[i + 2] as number) + ((world[i + 5] as number) - (world[i + 2] as number)) * t1,
|
|
365
|
+
)
|
|
366
|
+
}
|
|
367
|
+
for (let s = 0; s <= n; s++) {
|
|
368
|
+
const t = s / n
|
|
369
|
+
const px = ax + (bx - ax) * t
|
|
370
|
+
const py = ay + (by - ay) * t
|
|
371
|
+
const d = da + (db - da) * t
|
|
372
|
+
const seen = sampleAt(px, py)
|
|
373
|
+
// the background (nothing drawn) reads 0; off the frame reads Infinity — both leave the edge in view
|
|
374
|
+
const inView = seen === 0 || seen === Number.POSITIVE_INFINITY || d <= seen + tolerance
|
|
375
|
+
const visible = inView && featureAt(px, py, nx, ny)
|
|
376
|
+
if (visible) {
|
|
377
|
+
if (runStart < 0) runStart = t
|
|
378
|
+
gap = 0
|
|
379
|
+
} else if (runStart >= 0) {
|
|
380
|
+
// a single hidden sample inside a run is rasterisation noise, not an occluder
|
|
381
|
+
gap++
|
|
382
|
+
if (gap > 1) {
|
|
383
|
+
emit(runStart, (s - gap) / n)
|
|
384
|
+
runStart = -1
|
|
385
|
+
gap = 0
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
if (runStart >= 0) emit(runStart, 1)
|
|
390
|
+
}
|
|
391
|
+
return {
|
|
392
|
+
segments: new Float32Array(out),
|
|
393
|
+
count: out.length / 6,
|
|
394
|
+
tested,
|
|
395
|
+
width,
|
|
396
|
+
height,
|
|
397
|
+
ms: Math.round(performance.now() - started),
|
|
398
|
+
normals: normals !== null,
|
|
399
|
+
}
|
|
400
|
+
}
|