@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
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { expect, test } from 'bun:test'
|
|
2
|
+
import {
|
|
3
|
+
DIVE_DEPTH,
|
|
4
|
+
DIVE_MARGIN,
|
|
5
|
+
diveFloorY,
|
|
6
|
+
floorLimitAt,
|
|
7
|
+
glideFloorLimit,
|
|
8
|
+
LOOK_UP_PITCH_GAIN,
|
|
9
|
+
LOOK_UP_PUSH_HALF_ANGLE,
|
|
10
|
+
MAX_ORBIT_POLAR_ANGLE,
|
|
11
|
+
ORBIT_FLOOR_CLEARANCE,
|
|
12
|
+
ORBIT_TARGET_HEIGHT,
|
|
13
|
+
OVERVIEW_GROUND_CLEARANCE,
|
|
14
|
+
orbitAngleBelowFloor,
|
|
15
|
+
orbitCameraAboveFloor,
|
|
16
|
+
orbitFloorMinY,
|
|
17
|
+
overviewEyeY,
|
|
18
|
+
} from './orbit-floor-clearance'
|
|
19
|
+
|
|
20
|
+
type Vec3 = { x: number; y: number; z: number }
|
|
21
|
+
|
|
22
|
+
const FLOOR_Y = 3
|
|
23
|
+
const MIN_Y = FLOOR_Y + ORBIT_FLOOR_CLEARANCE
|
|
24
|
+
const target: Vec3 = { x: 1, y: FLOOR_Y, z: -2 }
|
|
25
|
+
|
|
26
|
+
// camera-controls' orbit: polar 0 looks straight down, PI/2 is level with the target.
|
|
27
|
+
function orbit(distance: number, polar: number, azimuth = 0.4): Vec3 {
|
|
28
|
+
return {
|
|
29
|
+
x: target.x + distance * Math.sin(polar) * Math.sin(azimuth),
|
|
30
|
+
y: target.y + distance * Math.cos(polar),
|
|
31
|
+
z: target.z + distance * Math.sin(polar) * Math.cos(azimuth),
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function present(position: Vec3) {
|
|
36
|
+
const out = { x: Number.NaN, y: Number.NaN, z: Number.NaN }
|
|
37
|
+
const polar = orbitCameraAboveFloor(out, position, target, MIN_Y)
|
|
38
|
+
return { grounded: out.y === MIN_Y && position.y < MIN_Y, out, polar }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const horizontalDistance = (p: Vec3) => Math.hypot(p.x - target.x, p.z - target.z)
|
|
42
|
+
const DEGREE = Math.PI / 180
|
|
43
|
+
|
|
44
|
+
test('an orbit above the clearance is drawn exactly where it is', () => {
|
|
45
|
+
const position = orbit(10, Math.PI / 3)
|
|
46
|
+
const { grounded, out, polar } = present(position)
|
|
47
|
+
expect(grounded).toBe(false)
|
|
48
|
+
expect(out).toEqual(position)
|
|
49
|
+
expect(polar).toBeCloseTo(Math.PI / 3, 9)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
test('tilting past the horizon keeps the camera on the floor clearance', () => {
|
|
53
|
+
for (const distance of [2, 8, 40]) {
|
|
54
|
+
for (const polar of [Math.PI / 2, 0.6 * Math.PI, 0.75 * Math.PI, Math.PI - 0.05]) {
|
|
55
|
+
const position = orbit(distance, polar)
|
|
56
|
+
const { grounded, out } = present(position)
|
|
57
|
+
expect(grounded).toBe(true)
|
|
58
|
+
expect(out.y).toBeCloseTo(MIN_Y, 9)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
test('past the clearance the view pitches up faster than the drag and stops at straight up', () => {
|
|
64
|
+
for (const distance of [2.2, 8, 40]) {
|
|
65
|
+
const contactPolar = Math.acos(ORBIT_FLOOR_CLEARANCE / distance)
|
|
66
|
+
for (let past = 0; past <= 60 * DEGREE; past += DEGREE) {
|
|
67
|
+
const { polar } = present(orbit(distance, contactPolar + past))
|
|
68
|
+
expect(polar).toBeCloseTo(
|
|
69
|
+
Math.min(contactPolar + past * LOOK_UP_PITCH_GAIN, MAX_ORBIT_POLAR_ANGLE),
|
|
70
|
+
9,
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
// From level to (almost) straight up within ~40° of drag.
|
|
74
|
+
const straightUp = (MAX_ORBIT_POLAR_ANGLE - contactPolar) / LOOK_UP_PITCH_GAIN
|
|
75
|
+
expect(straightUp).toBeLessThan(95 * DEGREE)
|
|
76
|
+
// And stops there: more drag changes nothing.
|
|
77
|
+
const atTop = present(orbit(distance, contactPolar + straightUp))
|
|
78
|
+
const beyond = present(orbit(distance, contactPolar + straightUp + 5 * DEGREE))
|
|
79
|
+
expect(beyond.polar).toBeCloseTo(MAX_ORBIT_POLAR_ANGLE, 9)
|
|
80
|
+
expect(horizontalDistance(beyond.out)).toBeCloseTo(horizontalDistance(atTop.out), 9)
|
|
81
|
+
}
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
test('past the clearance the camera closes on its target gently, at the half-angle rate', () => {
|
|
85
|
+
for (const distance of [2.2, 8, 40]) {
|
|
86
|
+
const contactPolar = Math.acos(ORBIT_FLOOR_CLEARANCE / distance)
|
|
87
|
+
const contactReach = horizontalDistance(present(orbit(distance, contactPolar)).out)
|
|
88
|
+
let previous = contactReach
|
|
89
|
+
const straightUp = (MAX_ORBIT_POLAR_ANGLE - contactPolar) / LOOK_UP_PITCH_GAIN
|
|
90
|
+
for (let polar = contactPolar + 0.01; polar <= contactPolar + straightUp; polar += 0.01) {
|
|
91
|
+
const position = orbit(distance, polar)
|
|
92
|
+
expect(orbitAngleBelowFloor(position, target, MIN_Y)).toBeCloseTo(polar - contactPolar, 9)
|
|
93
|
+
const reach = horizontalDistance(present(position).out)
|
|
94
|
+
expect(reach).toBeCloseTo(
|
|
95
|
+
contactReach * 0.5 ** ((polar - contactPolar) / LOOK_UP_PUSH_HALF_ANGLE),
|
|
96
|
+
9,
|
|
97
|
+
)
|
|
98
|
+
expect(reach).toBeLessThan(previous)
|
|
99
|
+
previous = reach
|
|
100
|
+
}
|
|
101
|
+
// By the time the view looks straight up it has closed at most ~25% of the way.
|
|
102
|
+
const there = horizontalDistance(present(orbit(distance, contactPolar + straightUp)).out)
|
|
103
|
+
expect(1 - there / contactReach).toBeLessThan(0.75)
|
|
104
|
+
expect(1 - there / contactReach).toBeGreaterThan(0.35)
|
|
105
|
+
}
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
test('crossing the clearance is continuous, so orbiting back up has no jump', () => {
|
|
109
|
+
const distance = 6
|
|
110
|
+
// The polar angle at which the orbit itself touches the clearance.
|
|
111
|
+
const contact = Math.acos(ORBIT_FLOOR_CLEARANCE / distance)
|
|
112
|
+
const before = present(orbit(distance, contact - 1e-6)).out
|
|
113
|
+
const after = present(orbit(distance, contact + 1e-6)).out
|
|
114
|
+
expect(Math.hypot(after.x - before.x, after.y - before.y, after.z - before.z)).toBeLessThan(1e-4)
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
test('zooming in while grounded still moves the camera toward the target', () => {
|
|
118
|
+
const polar = 0.8 * Math.PI
|
|
119
|
+
let previous = Number.POSITIVE_INFINITY
|
|
120
|
+
for (let distance = 40; distance >= 2; distance -= 2) {
|
|
121
|
+
const reach = horizontalDistance(present(orbit(distance, polar)).out)
|
|
122
|
+
expect(reach).toBeLessThan(previous)
|
|
123
|
+
previous = reach
|
|
124
|
+
}
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
test('with the pivot at standing height, a close orbit looks around before the floor stops it', () => {
|
|
128
|
+
const pivot = { x: 0, y: FLOOR_Y + ORBIT_TARGET_HEIGHT, z: 0 }
|
|
129
|
+
const distance = 2
|
|
130
|
+
const at = (polar: number) => ({
|
|
131
|
+
x: distance * Math.sin(polar),
|
|
132
|
+
y: pivot.y + distance * Math.cos(polar),
|
|
133
|
+
z: 0,
|
|
134
|
+
})
|
|
135
|
+
// Level with the pivot and a little past it: a free orbit at about eye
|
|
136
|
+
// height, already looking up, nothing pushed.
|
|
137
|
+
for (const polar of [Math.PI / 2, 100 * DEGREE, 105 * DEGREE]) {
|
|
138
|
+
const out = { x: 0, y: 0, z: 0 }
|
|
139
|
+
expect(orbitCameraAboveFloor(out, at(polar), pivot, MIN_Y)).toBeCloseTo(polar, 9)
|
|
140
|
+
expect(out).toEqual(at(polar))
|
|
141
|
+
}
|
|
142
|
+
// The clearance takes over only where the orbit would reach it.
|
|
143
|
+
const contact = Math.acos((MIN_Y - pivot.y) / distance)
|
|
144
|
+
expect(contact).toBeGreaterThan(105 * DEGREE)
|
|
145
|
+
const out = { x: 0, y: 0, z: 0 }
|
|
146
|
+
orbitCameraAboveFloor(out, at(contact + 5 * DEGREE), pivot, MIN_Y)
|
|
147
|
+
expect(out.y).toBeCloseTo(MIN_Y, 9)
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
test('the floor limit sits the clearance above the surface under the camera, or the level floor', () => {
|
|
151
|
+
// A raised platform 0.45 m above a level at 2.71.
|
|
152
|
+
expect(floorLimitAt(3.16, 2.71)).toBeCloseTo(3.16 + ORBIT_FLOOR_CLEARANCE, 9)
|
|
153
|
+
// A sunken floor below the level's own floor.
|
|
154
|
+
expect(floorLimitAt(2.21, 2.71)).toBeCloseTo(2.21 + ORBIT_FLOOR_CLEARANCE, 9)
|
|
155
|
+
// Nothing under the camera (outside, no terrain): the level floor, never 0.
|
|
156
|
+
expect(floorLimitAt(null, 2.71)).toBeCloseTo(2.71 + ORBIT_FLOOR_CLEARANCE, 9)
|
|
157
|
+
expect(floorLimitAt(null, 0)).toBe(ORBIT_FLOOR_CLEARANCE)
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
test('a change in floor height is glided over, never popped', () => {
|
|
161
|
+
expect(glideFloorLimit(null, 1, 1 / 60)).toBe(1)
|
|
162
|
+
let limit = 0.3
|
|
163
|
+
let previous = limit
|
|
164
|
+
for (let frame = 0; frame < 120; frame++) {
|
|
165
|
+
limit = glideFloorLimit(limit, 0.75, 1 / 60)
|
|
166
|
+
expect(limit).toBeGreaterThan(previous)
|
|
167
|
+
expect(limit - previous).toBeLessThan(0.07)
|
|
168
|
+
previous = limit
|
|
169
|
+
}
|
|
170
|
+
expect(limit).toBeCloseTo(0.75, 6)
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
test('a pivot sent four storeys down keeps the eye where it was, over the ground; a diver stays under', () => {
|
|
174
|
+
// Eye 12 m up over a pivot at 1.1; the pivot goes to -12: the move alone would put the eye at -0.1.
|
|
175
|
+
expect(overviewEyeY(12 - 13.1, 12, -0.05, false)).toBe(12)
|
|
176
|
+
// An eye that was already low is brought up to the clearance.
|
|
177
|
+
expect(overviewEyeY(-9, 0.5, -0.05, false)).toBe(-0.05 + OVERVIEW_GROUND_CLEARANCE)
|
|
178
|
+
// The move leaves it high enough: it goes with the pivot, as always.
|
|
179
|
+
expect(overviewEyeY(20, 25, -0.05, false)).toBe(20)
|
|
180
|
+
// The person is under the ground: the move is theirs.
|
|
181
|
+
expect(overviewEyeY(-8, 12, -0.05, true)).toBe(-8)
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
test('the dive floor sits 3 m under the lowest ground, or 1 m under the deepest drawn thing when that is deeper', () => {
|
|
185
|
+
// A house at grade: nothing drawn is deeper than the ground's 3 m.
|
|
186
|
+
expect(diveFloorY(-0.05, -0.36)).toBeCloseTo(-0.05 - DIVE_DEPTH, 9)
|
|
187
|
+
expect(diveFloorY(-0.05, null)).toBeCloseTo(-0.05 - DIVE_DEPTH, 9)
|
|
188
|
+
// The villa's basement footing at -3.83 goes a metre deeper.
|
|
189
|
+
expect(diveFloorY(-0.05, -3.83)).toBeCloseTo(-3.83 - DIVE_MARGIN, 9)
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
test('the eye may sit the clearance over the dive floor from any storey, over the level floor otherwise', () => {
|
|
193
|
+
const dive = diveFloorY(-0.05, null)
|
|
194
|
+
// The ground floor, and a storey four metres up: both dive to the same floor.
|
|
195
|
+
expect(orbitFloorMinY(0, dive)).toBeCloseTo(dive + ORBIT_FLOOR_CLEARANCE, 9)
|
|
196
|
+
expect(orbitFloorMinY(4, dive)).toBeCloseTo(dive + ORBIT_FLOOR_CLEARANCE, 9)
|
|
197
|
+
// No dive (Solid): the level's floor, as before.
|
|
198
|
+
expect(orbitFloorMinY(4, null)).toBeCloseTo(4 + ORBIT_FLOOR_CLEARANCE, 9)
|
|
199
|
+
// A basement deeper than the dive floor keeps its own floor: the dive never lifts the limit.
|
|
200
|
+
expect(orbitFloorMinY(-12, dive)).toBeCloseTo(-12 + ORBIT_FLOOR_CLEARANCE, 9)
|
|
201
|
+
})
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
type Vec3 = { x: number; y: number; z: number }
|
|
2
|
+
|
|
3
|
+
/** The lowest an orbiting camera sits above the active level's floor. */
|
|
4
|
+
export const ORBIT_FLOOR_CLEARANCE = 0.45
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* How high above the active level's floor the orbit's pivot rests when the
|
|
8
|
+
* editor aims it at the ground (level switch, panning, framing, focusing a
|
|
9
|
+
* floor or room): about standing eye height, so zoomed all the way in the
|
|
10
|
+
* camera looks around the room the way a walkthrough does.
|
|
11
|
+
*/
|
|
12
|
+
export const ORBIT_TARGET_HEIGHT = 1.1
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The dive under the ground: with the ground on Auto the orbit's eye may leave the ground for the
|
|
16
|
+
* earth under any building, down to a floor this far under the lowest ground, or this far under
|
|
17
|
+
* the deepest thing drawn (a basement's footing), whichever is deeper.
|
|
18
|
+
*/
|
|
19
|
+
export const DIVE_DEPTH = 3
|
|
20
|
+
export const DIVE_MARGIN = 1
|
|
21
|
+
|
|
22
|
+
const DEGREE = Math.PI / 180
|
|
23
|
+
|
|
24
|
+
/** The floor of the dive, world Y: the eye's clearance over it is {@link ORBIT_FLOOR_CLEARANCE}. */
|
|
25
|
+
export function diveFloorY(lowestGroundY: number, deepestDrawnY: number | null): number {
|
|
26
|
+
const underGround = lowestGroundY - DIVE_DEPTH
|
|
27
|
+
return deepestDrawnY === null ? underGround : Math.min(underGround, deepestDrawnY - DIVE_MARGIN)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The lowest the orbit's eye may sit: the clearance over the selected level's floor, or over the
|
|
32
|
+
* dive's floor when the person may dive and it lies deeper (`diveFloor` null: no dive). Selecting a
|
|
33
|
+
* level never lifts the limit above the dive floor, so a dive works from any storey.
|
|
34
|
+
*/
|
|
35
|
+
export function orbitFloorMinY(levelFloorY: number, diveFloor: number | null): number {
|
|
36
|
+
return Math.min(levelFloorY, diveFloor ?? levelFloorY) + ORBIT_FLOOR_CLEARANCE
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The steepest an orbit camera looks: straight down is 0, straight up π. */
|
|
40
|
+
export const MAX_ORBIT_POLAR_ANGLE = Math.PI - 0.05
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Past the point where the camera reached the floor clearance, the view
|
|
44
|
+
* pitches up this many times faster than the orbit drag: about 80° more drag
|
|
45
|
+
* takes it from level to straight up.
|
|
46
|
+
*/
|
|
47
|
+
export const LOOK_UP_PITCH_GAIN = 1.1
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The look-up push: every this many degrees of drag past the clearance, the
|
|
51
|
+
* camera's horizontal distance to its target halves. Over the ~80° it takes
|
|
52
|
+
* to look straight up, the camera closes about two thirds of the way in.
|
|
53
|
+
*/
|
|
54
|
+
export const LOOK_UP_PUSH_HALF_ANGLE = 55 * DEGREE
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* How far the orbit has tilted past the point where its camera reaches
|
|
58
|
+
* `minY` (radians, negative while it is still above). Zero exactly when the
|
|
59
|
+
* orbit camera sits at `minY`.
|
|
60
|
+
*/
|
|
61
|
+
export function orbitAngleBelowFloor(position: Vec3, target: Vec3, minY: number): number {
|
|
62
|
+
const horizontal = Math.hypot(position.x - target.x, position.z - target.z)
|
|
63
|
+
const height = position.y - target.y
|
|
64
|
+
const distance = Math.hypot(horizontal, height)
|
|
65
|
+
if (distance < 1e-9) return 0
|
|
66
|
+
const polar = Math.atan2(horizontal, height)
|
|
67
|
+
const contactPolar = Math.acos(Math.max(-1, Math.min(1, (minY - target.y) / distance)))
|
|
68
|
+
return polar - contactPolar
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Where to draw an orbit camera so it never drops below `minY`. Above it the
|
|
73
|
+
* orbit pose stands. An orbit that would take it lower keeps the camera on
|
|
74
|
+
* `minY`, pitches the view up `LOOK_UP_PITCH_GAIN` times faster than the
|
|
75
|
+
* drag (stopping at straight up), and pushes the camera gently toward its
|
|
76
|
+
* target at the `LOOK_UP_PUSH_HALF_ANGLE` rate. A pure function of the orbit
|
|
77
|
+
* state, so orbiting back up returns the exact orbit pose.
|
|
78
|
+
*
|
|
79
|
+
* Writes the drawn position into `out` and returns the drawn polar angle.
|
|
80
|
+
*/
|
|
81
|
+
export function orbitCameraAboveFloor(
|
|
82
|
+
out: Vec3,
|
|
83
|
+
position: Vec3,
|
|
84
|
+
target: Vec3,
|
|
85
|
+
minY: number,
|
|
86
|
+
): number {
|
|
87
|
+
const dx = position.x - target.x
|
|
88
|
+
const dz = position.z - target.z
|
|
89
|
+
const horizontal = Math.hypot(dx, dz)
|
|
90
|
+
const height = position.y - target.y
|
|
91
|
+
if (!(position.y < minY)) {
|
|
92
|
+
out.x = position.x
|
|
93
|
+
out.y = position.y
|
|
94
|
+
out.z = position.z
|
|
95
|
+
return Math.atan2(horizontal, height)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const rise = minY - target.y
|
|
99
|
+
const distance = Math.hypot(horizontal, height)
|
|
100
|
+
const contactRadius = Math.sqrt(Math.max(0, distance * distance - rise * rise))
|
|
101
|
+
const below = Math.max(0, orbitAngleBelowFloor(position, target, minY))
|
|
102
|
+
const contactPolar = Math.atan2(horizontal, height) - below
|
|
103
|
+
// Nothing moves once the view looks straight up.
|
|
104
|
+
const pastContact = Math.min(below, (MAX_ORBIT_POLAR_ANGLE - contactPolar) / LOOK_UP_PITCH_GAIN)
|
|
105
|
+
const reach = contactRadius * 0.5 ** (pastContact / LOOK_UP_PUSH_HALF_ANGLE)
|
|
106
|
+
const scale = horizontal > 1e-9 ? reach / horizontal : 0
|
|
107
|
+
out.x = target.x + dx * scale
|
|
108
|
+
out.y = minY
|
|
109
|
+
out.z = target.z + dz * scale
|
|
110
|
+
return contactPolar + pastContact * LOOK_UP_PITCH_GAIN
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// How quickly the floor limit follows a change in the floor under the camera
|
|
114
|
+
// (stepping over a raised platform's edge): about this many seconds per e-fold.
|
|
115
|
+
const FLOOR_GLIDE_SECONDS = 0.12
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* The lowest the camera may sit: the clearance above the floor surface under
|
|
119
|
+
* it, or above the level's own floor when there is none (outside, no terrain).
|
|
120
|
+
*/
|
|
121
|
+
export function floorLimitAt(surfaceTop: number | null, levelFloorY: number): number {
|
|
122
|
+
return (surfaceTop ?? levelFloorY) + ORBIT_FLOOR_CLEARANCE
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Eases a floor limit toward `target` over `delta` seconds, so it never pops. */
|
|
126
|
+
export function glideFloorLimit(current: number | null, target: number, delta: number): number {
|
|
127
|
+
if (current === null || !(delta > 0)) return current ?? target
|
|
128
|
+
return target + (current - target) * Math.exp(-delta / FLOOR_GLIDE_SECONDS)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* How far over the ground the overview's eye stays when the pivot goes down to a deep level: the
|
|
133
|
+
* ground is a medium, the eye above it sees the level through the hole and the dive is the
|
|
134
|
+
* person's own act, so picking a level four storeys under grade does not sink the eye with it.
|
|
135
|
+
*/
|
|
136
|
+
export const OVERVIEW_GROUND_CLEARANCE = 2
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* The height an overview eye is kept at when its pivot moves to another level: `movedEyeY` as the
|
|
140
|
+
* move leaves it (the eye following the pivot down), unless that would sink the eye under the
|
|
141
|
+
* ground (the eye was above it, `wasUnder` false): then the eye stays where it was, over the ground
|
|
142
|
+
* (at least {@link OVERVIEW_GROUND_CLEARANCE} over it), and looks down at the new pivot. An eye
|
|
143
|
+
* already under the ground (the person dived) goes where the move puts it.
|
|
144
|
+
*/
|
|
145
|
+
export function overviewEyeY(
|
|
146
|
+
movedEyeY: number,
|
|
147
|
+
previousEyeY: number,
|
|
148
|
+
groundY: number,
|
|
149
|
+
wasUnder: boolean,
|
|
150
|
+
): number {
|
|
151
|
+
const lowest = groundY + OVERVIEW_GROUND_CLEARANCE
|
|
152
|
+
if (wasUnder || movedEyeY >= lowest) return movedEyeY
|
|
153
|
+
return Math.max(previousEyeY, lowest)
|
|
154
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { expect, test } from 'bun:test'
|
|
2
|
+
import { paintCommitRoute } from './paint-commit-route'
|
|
3
|
+
|
|
4
|
+
test('only the hovered surface itself keeps the kind commit', () => {
|
|
5
|
+
expect(paintCommitRoute([], 'wall_a', 'a')).toBe('own')
|
|
6
|
+
expect(paintCommitRoute([{ nodeId: 'wall_a', role: 'a' }], 'wall_a', 'a')).toBe('own')
|
|
7
|
+
// A one-wall room: one target, but on the room role.
|
|
8
|
+
expect(paintCommitRoute([{ nodeId: 'wall_a', role: 'room:zone_1' }], 'wall_a', 'a')).toBe(
|
|
9
|
+
'fanout',
|
|
10
|
+
)
|
|
11
|
+
expect(paintCommitRoute([{ nodeId: 'wall_b', role: 'a' }], 'wall_a', 'a')).toBe('fanout')
|
|
12
|
+
expect(
|
|
13
|
+
paintCommitRoute(
|
|
14
|
+
[
|
|
15
|
+
{ nodeId: 'wall_a', role: 'a' },
|
|
16
|
+
{ nodeId: 'wall_b', role: 'a' },
|
|
17
|
+
],
|
|
18
|
+
'wall_a',
|
|
19
|
+
'a',
|
|
20
|
+
),
|
|
21
|
+
).toBe('fanout')
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
for (const role of ['step:zone_upper', 'edge:zone_upper'])
|
|
25
|
+
test(`${role} retains the room side role through paint routing`, () => {
|
|
26
|
+
expect(paintCommitRoute([{ nodeId: 'slab_platform', role }], 'slab_platform', role)).toBe('own')
|
|
27
|
+
expect(
|
|
28
|
+
paintCommitRoute(
|
|
29
|
+
[
|
|
30
|
+
{ nodeId: 'slab_platform', role },
|
|
31
|
+
{ nodeId: 'slab_component', role },
|
|
32
|
+
],
|
|
33
|
+
'slab_platform',
|
|
34
|
+
role,
|
|
35
|
+
),
|
|
36
|
+
).toBe('fanout')
|
|
37
|
+
})
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How a paint click commits its resolved spread. Only a spread that is exactly
|
|
3
|
+
* the hovered surface keeps the kind's own commit; anything else — several
|
|
4
|
+
* targets, or one target on another role such as a whole room's
|
|
5
|
+
* `room:<zoneId>` role — goes through the fan-out, which routes each role to
|
|
6
|
+
* the kind that owns it. A one-wall room therefore needs no second target to
|
|
7
|
+
* reach its room role.
|
|
8
|
+
*/
|
|
9
|
+
export function paintCommitRoute(
|
|
10
|
+
targets: ReadonlyArray<{ nodeId: string; role: string }>,
|
|
11
|
+
nodeId: string,
|
|
12
|
+
role: string,
|
|
13
|
+
): 'own' | 'fanout' {
|
|
14
|
+
if (targets.length === 0) return 'own'
|
|
15
|
+
const [only] = targets
|
|
16
|
+
return targets.length === 1 && only!.nodeId === nodeId && only!.role === role ? 'own' : 'fanout'
|
|
17
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { expect, test } from 'bun:test'
|
|
2
|
+
import {
|
|
3
|
+
type AnyNode,
|
|
4
|
+
nodeRegistry,
|
|
5
|
+
registerNode,
|
|
6
|
+
SlabNode,
|
|
7
|
+
useScene,
|
|
8
|
+
WallNode,
|
|
9
|
+
} from '@pascal-app/core'
|
|
10
|
+
import { useNodeEvents, useViewer } from '@pascal-app/viewer'
|
|
11
|
+
import { _roots, act, events } from '@react-three/fiber'
|
|
12
|
+
import type { OrthographicCamera } from 'three'
|
|
13
|
+
import { z } from 'zod'
|
|
14
|
+
import { SelectionManager, useEditor } from '../index'
|
|
15
|
+
import { withSelectionHarness } from '../test-utils/selection-harness'
|
|
16
|
+
import { resetPaintMode, usePaintRegionMode } from './paint-region-mode'
|
|
17
|
+
|
|
18
|
+
const picked = { materialPreset: 'library:wall-red', sourceTarget: 'wall' } as const
|
|
19
|
+
|
|
20
|
+
function Surface({ node }: { node: AnyNode }) {
|
|
21
|
+
const wall = node.type === 'wall'
|
|
22
|
+
return (
|
|
23
|
+
<mesh
|
|
24
|
+
{...useNodeEvents(node, node.type)}
|
|
25
|
+
name={node.id}
|
|
26
|
+
position={wall ? [0, 1.5, 0] : [0, 0, 0]}
|
|
27
|
+
rotation={wall ? [0, 0, 0] : [-Math.PI / 2, 0, 0]}
|
|
28
|
+
>
|
|
29
|
+
<planeGeometry args={wall ? [20, 3] : [20, 20]} />
|
|
30
|
+
<meshBasicMaterial />
|
|
31
|
+
</mesh>
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
for (const activation of ['icon', 'Alt'] as const) {
|
|
36
|
+
test(`${activation} eyedrop consumes the front wall click; only the next click paints the slab behind`, async () => {
|
|
37
|
+
const restoreRegistry = nodeRegistry._snapshot()
|
|
38
|
+
const previousPaint = usePaintRegionMode.getState()
|
|
39
|
+
const previousDocument = globalThis.document
|
|
40
|
+
const previousCancel = globalThis.cancelAnimationFrame
|
|
41
|
+
globalThis.cancelAnimationFrame = () => {}
|
|
42
|
+
globalThis.document = { body: { style: { cursor: '' } } } as unknown as Document
|
|
43
|
+
nodeRegistry._reset()
|
|
44
|
+
const writes: string[] = []
|
|
45
|
+
try {
|
|
46
|
+
for (const kind of ['wall', 'slab']) {
|
|
47
|
+
registerNode({
|
|
48
|
+
kind,
|
|
49
|
+
schemaVersion: 1,
|
|
50
|
+
schema: z.object({ type: z.literal(kind) }) as never,
|
|
51
|
+
category: 'structure',
|
|
52
|
+
defaults: () => ({}),
|
|
53
|
+
capabilities: {
|
|
54
|
+
slots: () => [{ slotId: 'default', label: 'Surface' }],
|
|
55
|
+
paint: {
|
|
56
|
+
resolveRole: () => 'default',
|
|
57
|
+
getEffectiveMaterial: () => ({
|
|
58
|
+
material: undefined,
|
|
59
|
+
materialPreset: picked.materialPreset,
|
|
60
|
+
}),
|
|
61
|
+
buildPatch: ({ materialPreset }) => ({ materialPreset }) as Partial<AnyNode>,
|
|
62
|
+
commit: ({ node, materialPreset }) => {
|
|
63
|
+
writes.push(node.id)
|
|
64
|
+
useScene.getState().updateNode(node.id, { materialPreset } as Partial<AnyNode>)
|
|
65
|
+
},
|
|
66
|
+
applyPreview: () => () => {},
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
await withSelectionHarness(async ({ canvas, render }) => {
|
|
72
|
+
const wall = WallNode.parse({ id: 'wall_front', start: [-10, 0], end: [10, 0] })
|
|
73
|
+
const slab = SlabNode.parse({
|
|
74
|
+
id: 'slab_behind',
|
|
75
|
+
polygon: [
|
|
76
|
+
[-10, -10],
|
|
77
|
+
[10, -10],
|
|
78
|
+
[10, 10],
|
|
79
|
+
[-10, 10],
|
|
80
|
+
],
|
|
81
|
+
})
|
|
82
|
+
useScene.setState({ nodes: { [wall.id]: wall, [slab.id]: slab }, readOnly: false })
|
|
83
|
+
useViewer.getState().resetSelection()
|
|
84
|
+
useViewer.setState({ cameraDragging: false, inputDragging: false })
|
|
85
|
+
resetPaintMode()
|
|
86
|
+
useEditor.getState().armMaterialPaint(undefined, activation === 'icon' ? 'pick' : 'surface')
|
|
87
|
+
await render(
|
|
88
|
+
<>
|
|
89
|
+
<SelectionManager />
|
|
90
|
+
<Surface node={wall} />
|
|
91
|
+
<Surface node={slab} />
|
|
92
|
+
</>,
|
|
93
|
+
)
|
|
94
|
+
if (activation === 'Alt') {
|
|
95
|
+
await act(async () => {
|
|
96
|
+
window.dispatchEvent(Object.assign(new Event('keydown'), { key: 'Alt', repeat: false }))
|
|
97
|
+
})
|
|
98
|
+
}
|
|
99
|
+
expect(usePaintRegionMode.getState().mode).toBe('pick')
|
|
100
|
+
const store = _roots.get(canvas)!.store
|
|
101
|
+
const state = store.getState()
|
|
102
|
+
const camera = state.camera as OrthographicCamera
|
|
103
|
+
Object.assign(camera, { left: -5, right: 5, top: 5, bottom: -5 })
|
|
104
|
+
camera.position.set(0, 5, 10)
|
|
105
|
+
camera.up.set(0, 1, 0)
|
|
106
|
+
camera.lookAt(0, 1, 0)
|
|
107
|
+
camera.updateProjectionMatrix()
|
|
108
|
+
camera.updateMatrixWorld(true)
|
|
109
|
+
state.setEvents(events(store))
|
|
110
|
+
state.scene.updateMatrixWorld(true)
|
|
111
|
+
const send = async (type: 'onPointerMove' | 'onPointerDown' | 'onPointerUp') => {
|
|
112
|
+
await act(async () => {
|
|
113
|
+
store.getState().events.handlers
|
|
123
|
+
})
|
|
124
|
+
}
|
|
125
|
+
// Hover first: sampling synchronously replays this event when it returns to paint.
|
|
126
|
+
await send('onPointerMove')
|
|
127
|
+
expect(useViewer.getState().hoveredId).toBe(wall.id)
|
|
128
|
+
const hits = state.raycaster.intersectObjects(state.internal.interaction, true)
|
|
129
|
+
expect([...new Set(hits.map((hit) => hit.object.name))]).toEqual([wall.id, slab.id])
|
|
130
|
+
const before = useScene.getState().nodes
|
|
131
|
+
await send('onPointerDown')
|
|
132
|
+
await send('onPointerUp')
|
|
133
|
+
expect(useEditor.getState().activePaintMaterial).toEqual(picked)
|
|
134
|
+
expect(usePaintRegionMode.getState().mode).toBe('surface')
|
|
135
|
+
expect(writes).toEqual([])
|
|
136
|
+
expect(useScene.getState().nodes).toBe(before)
|
|
137
|
+
if (activation === 'Alt') {
|
|
138
|
+
await act(async () =>
|
|
139
|
+
window.dispatchEvent(Object.assign(new Event('keyup'), { key: 'Alt' })),
|
|
140
|
+
)
|
|
141
|
+
}
|
|
142
|
+
// Expose the slab at the same cursor, then prove the next gesture paints once.
|
|
143
|
+
await render(
|
|
144
|
+
<>
|
|
145
|
+
<SelectionManager />
|
|
146
|
+
<Surface node={slab} />
|
|
147
|
+
</>,
|
|
148
|
+
)
|
|
149
|
+
state.scene.updateMatrixWorld(true)
|
|
150
|
+
await send('onPointerMove')
|
|
151
|
+
await send('onPointerDown')
|
|
152
|
+
await send('onPointerUp')
|
|
153
|
+
expect(writes).toEqual([slab.id])
|
|
154
|
+
expect((useScene.getState().nodes[slab.id] as SlabNode).materialPreset).toBe(
|
|
155
|
+
picked.materialPreset,
|
|
156
|
+
)
|
|
157
|
+
})
|
|
158
|
+
} finally {
|
|
159
|
+
restoreRegistry()
|
|
160
|
+
usePaintRegionMode.setState(previousPaint)
|
|
161
|
+
globalThis.document = previousDocument
|
|
162
|
+
globalThis.cancelAnimationFrame = previousCancel
|
|
163
|
+
}
|
|
164
|
+
})
|
|
165
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type AnyNode,
|
|
3
|
+
type AnyNodeId,
|
|
4
|
+
type MaterialSchema,
|
|
5
|
+
nodeRegistry,
|
|
6
|
+
type SceneMaterial,
|
|
7
|
+
type SceneMaterialId,
|
|
8
|
+
slotDefaultPaintMaterial,
|
|
9
|
+
} from '@pascal-app/core'
|
|
10
|
+
import type { Material, Object3D } from 'three'
|
|
11
|
+
import {
|
|
12
|
+
type ActivePaintMaterial,
|
|
13
|
+
hasActivePaintMaterial,
|
|
14
|
+
type PaintableMaterialTarget,
|
|
15
|
+
resolveActivePaintMaterialFromSelection,
|
|
16
|
+
} from './material-paint'
|
|
17
|
+
|
|
18
|
+
type RenderedMaterial = Material & {
|
|
19
|
+
color?: { getHexString: () => string }
|
|
20
|
+
roughness?: number
|
|
21
|
+
metalness?: number
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** The drawn material of a mesh hit, for a slot that carries no finish of its own (an item's GLB). */
|
|
25
|
+
function drawnMaterial(hitObject: Object3D | undefined, materialIndex: number | null) {
|
|
26
|
+
const material = (hitObject as { material?: Material | Material[] } | undefined)?.material
|
|
27
|
+
const drawn = (Array.isArray(material) ? material[materialIndex ?? 0] : material) as
|
|
28
|
+
| RenderedMaterial
|
|
29
|
+
| undefined
|
|
30
|
+
if (!drawn?.color) return null
|
|
31
|
+
return {
|
|
32
|
+
preset: 'custom',
|
|
33
|
+
properties: {
|
|
34
|
+
color: `#${drawn.color.getHexString()}`,
|
|
35
|
+
roughness: drawn.roughness ?? 0.5,
|
|
36
|
+
metalness: drawn.metalness ?? 0,
|
|
37
|
+
opacity: drawn.opacity ?? 1,
|
|
38
|
+
transparent: drawn.transparent ?? false,
|
|
39
|
+
side: 'front',
|
|
40
|
+
},
|
|
41
|
+
} satisfies MaterialSchema
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The material the eyedropper takes from `role` of `node`: what the surface
|
|
46
|
+
* shows, read through the paint path — the kind's `getEffectiveMaterial` (the
|
|
47
|
+
* finish a room, region or override gives it, with derived roles falling to
|
|
48
|
+
* what they draw), the legacy per-kind arms, then the slot's declared default
|
|
49
|
+
* (`node.slots[slot]` → `default`, the renderer's own chain). A slot with none
|
|
50
|
+
* of those (an item's GLB material) is read off the drawn mesh.
|
|
51
|
+
*/
|
|
52
|
+
export function eyedropperMaterial(args: {
|
|
53
|
+
node: AnyNode
|
|
54
|
+
role: string
|
|
55
|
+
nodes: Record<string, AnyNode>
|
|
56
|
+
materials?: Record<SceneMaterialId, SceneMaterial>
|
|
57
|
+
hitObject?: Object3D
|
|
58
|
+
materialIndex?: number | null
|
|
59
|
+
}): ActivePaintMaterial | null {
|
|
60
|
+
const { node, role, nodes } = args
|
|
61
|
+
const definition = nodeRegistry.get(node.type)
|
|
62
|
+
const paint = definition?.capabilities?.paint
|
|
63
|
+
const sourceTarget = (paint?.materialTarget ?? node.type) as PaintableMaterialTarget
|
|
64
|
+
const valid = (value: { material?: MaterialSchema; materialPreset?: string } | null) =>
|
|
65
|
+
value && hasActivePaintMaterial({ ...value, sourceTarget })
|
|
66
|
+
? { material: value.material, materialPreset: value.materialPreset, sourceTarget }
|
|
67
|
+
: null
|
|
68
|
+
|
|
69
|
+
const effective =
|
|
70
|
+
valid(
|
|
71
|
+
paint?.getEffectiveMaterial?.({
|
|
72
|
+
node,
|
|
73
|
+
role,
|
|
74
|
+
nodes: nodes as Record<AnyNodeId, AnyNode>,
|
|
75
|
+
rendered: true,
|
|
76
|
+
materials: args.materials,
|
|
77
|
+
}) ?? null,
|
|
78
|
+
) ??
|
|
79
|
+
resolveActivePaintMaterialFromSelection({
|
|
80
|
+
nodes,
|
|
81
|
+
selectedId: node.id,
|
|
82
|
+
materials: args.materials,
|
|
83
|
+
selectedMaterialTarget: { nodeId: node.id, role },
|
|
84
|
+
})
|
|
85
|
+
if (effective) return effective
|
|
86
|
+
|
|
87
|
+
const declared = definition?.capabilities?.slots?.(node).find((slot) => slot.slotId === role)
|
|
88
|
+
const fallback = valid(slotDefaultPaintMaterial(declared?.default))
|
|
89
|
+
if (fallback) return fallback
|
|
90
|
+
|
|
91
|
+
const drawn = drawnMaterial(args.hitObject, args.materialIndex ?? null)
|
|
92
|
+
return drawn ? { material: drawn, sourceTarget } : null
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** A stable key for a picked material, so an unchanged pick is not re-announced. */
|
|
96
|
+
export function paintMaterialKey(material: ActivePaintMaterial | null): string {
|
|
97
|
+
if (!material) return ''
|
|
98
|
+
return material.materialPreset ?? JSON.stringify(material.material ?? null)
|
|
99
|
+
}
|