@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,338 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type AnyNode,
|
|
3
|
+
containsPoint,
|
|
4
|
+
detectSpacesForLevel,
|
|
5
|
+
getBlockFaceFrame,
|
|
6
|
+
getFloorPlacedFootprints,
|
|
7
|
+
getWallBodyCenterOffset,
|
|
8
|
+
getWallCurveFrameAt,
|
|
9
|
+
getWallCurveLength,
|
|
10
|
+
measurementCentroid,
|
|
11
|
+
nodeRegistry,
|
|
12
|
+
polygonInteriorPoint,
|
|
13
|
+
type SeparatorNode,
|
|
14
|
+
type WallNode,
|
|
15
|
+
} from '@pascal-app/core'
|
|
16
|
+
import { Euler, Matrix4, Quaternion, Vector3 } from 'three'
|
|
17
|
+
|
|
18
|
+
type Nodes = Readonly<Record<string, AnyNode>>
|
|
19
|
+
type Point = [number, number]
|
|
20
|
+
type Frame = { nodeId: string; levelId: string; matrix: Matrix4 }
|
|
21
|
+
type Shape = { polygon: Point[]; holes: Point[][] }
|
|
22
|
+
|
|
23
|
+
function fields(node: AnyNode): Record<string, unknown> {
|
|
24
|
+
return node as unknown as Record<string, unknown>
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function vector(value: unknown): [number, number, number] | null {
|
|
28
|
+
return Array.isArray(value) && value.length === 3 && value.every(Number.isFinite)
|
|
29
|
+
? (value as [number, number, number])
|
|
30
|
+
: null
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function ring(value: unknown): Point[] {
|
|
34
|
+
return Array.isArray(value)
|
|
35
|
+
? value.filter(
|
|
36
|
+
(point): point is Point =>
|
|
37
|
+
Array.isArray(point) && point.length === 2 && point.every(Number.isFinite),
|
|
38
|
+
)
|
|
39
|
+
: []
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function centroid(polygon: Point[]): Point | null {
|
|
43
|
+
const centre = measurementCentroid(polygon.map(([x, z]) => [x, 0, z]))
|
|
44
|
+
return centre ? [centre[0], centre[2]] : null
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function covers(shape: Shape, point: Point): boolean {
|
|
48
|
+
return containsPoint([{ outer: shape.polygon, holes: shape.holes }], point)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function snapshotScope(nodes: Nodes, targetIds: readonly string[], roomContentsOnly = false) {
|
|
52
|
+
const children = new Map<string, Set<string>>()
|
|
53
|
+
const listedParents = new Map<string, string>()
|
|
54
|
+
const addChild = (parent: string, child: string) => {
|
|
55
|
+
const ids = children.get(parent) ?? new Set<string>()
|
|
56
|
+
ids.add(child)
|
|
57
|
+
children.set(parent, ids)
|
|
58
|
+
}
|
|
59
|
+
for (const node of Object.values(nodes)) {
|
|
60
|
+
if (node.parentId) addChild(node.parentId, node.id)
|
|
61
|
+
for (const child of 'children' in node ? (node.children ?? []) : []) {
|
|
62
|
+
addChild(node.id, child)
|
|
63
|
+
if (!listedParents.has(child)) listedParents.set(child, node.id)
|
|
64
|
+
}
|
|
65
|
+
const wallId = fields(node).wallId
|
|
66
|
+
if (typeof wallId === 'string') addChild(wallId, node.id)
|
|
67
|
+
}
|
|
68
|
+
const parentId = (node: AnyNode): string | undefined => {
|
|
69
|
+
const wallId = fields(node).wallId
|
|
70
|
+
if (typeof wallId === 'string' && nodes[wallId]?.type === 'wall') return wallId
|
|
71
|
+
return node.parentId ?? listedParents.get(node.id)
|
|
72
|
+
}
|
|
73
|
+
const frames = new Map<string, Frame | null>()
|
|
74
|
+
const resolving = new Set<string>()
|
|
75
|
+
const attachmentFrame = (node: AnyNode, host: Frame): Frame => {
|
|
76
|
+
const hostNode = nodes[host.nodeId]
|
|
77
|
+
if (node.type !== 'item' || hostNode?.type !== 'block' || !node.blockFaceId) return host
|
|
78
|
+
const face = getBlockFaceFrame(hostNode.topology, node.blockFaceId)
|
|
79
|
+
if (!face) return host
|
|
80
|
+
// Stored item coordinates are in the face basis, as in BlockFaceHostFrame.
|
|
81
|
+
const local = new Matrix4()
|
|
82
|
+
.makeBasis(
|
|
83
|
+
new Vector3(...face.xAxis),
|
|
84
|
+
new Vector3(...face.yAxis),
|
|
85
|
+
new Vector3(...face.normal),
|
|
86
|
+
)
|
|
87
|
+
.setPosition(...face.origin)
|
|
88
|
+
return { ...host, matrix: host.matrix.clone().multiply(local) }
|
|
89
|
+
}
|
|
90
|
+
const wallPose = (wall: WallNode, position: [number, number, number]) => {
|
|
91
|
+
const curve = getWallCurveFrameAt(wall, position[0] / (getWallCurveLength(wall) || 1))
|
|
92
|
+
const across = getWallBodyCenterOffset(wall) + position[2]
|
|
93
|
+
return {
|
|
94
|
+
position: [
|
|
95
|
+
curve.point.x + curve.normal.x * across,
|
|
96
|
+
position[1],
|
|
97
|
+
curve.point.y + curve.normal.y * across,
|
|
98
|
+
] as [number, number, number],
|
|
99
|
+
yaw: -Math.atan2(curve.tangent.y, curve.tangent.x),
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
const frame = (id: string): Frame | null => {
|
|
103
|
+
if (frames.has(id)) return frames.get(id) ?? null
|
|
104
|
+
const node = nodes[id]
|
|
105
|
+
if (!node || resolving.has(id)) return null
|
|
106
|
+
if (node.type === 'level') {
|
|
107
|
+
const result = { nodeId: id, levelId: id, matrix: new Matrix4() }
|
|
108
|
+
frames.set(id, result)
|
|
109
|
+
return result
|
|
110
|
+
}
|
|
111
|
+
resolving.add(id)
|
|
112
|
+
const parent = parentId(node)
|
|
113
|
+
const host = parent ? frame(parent) : null
|
|
114
|
+
let result: Frame | null = null
|
|
115
|
+
if (host) {
|
|
116
|
+
const data = fields(node)
|
|
117
|
+
let position: [number, number, number] = vector(data.position) ?? [0, 0, 0]
|
|
118
|
+
let rotation: [number, number, number] = vector(data.rotation) ?? [
|
|
119
|
+
0,
|
|
120
|
+
typeof data.rotation === 'number' ? data.rotation : 0,
|
|
121
|
+
0,
|
|
122
|
+
]
|
|
123
|
+
if (node.type === 'wall') {
|
|
124
|
+
const [dx, dz] = [node.end[0] - node.start[0], node.end[1] - node.start[1]]
|
|
125
|
+
position = [node.start[0], 0, node.start[1]]
|
|
126
|
+
rotation = [0, -Math.atan2(dz, dx), 0]
|
|
127
|
+
}
|
|
128
|
+
// Item scale belongs to its model, not the group that hosts its children.
|
|
129
|
+
const scale: [number, number, number] =
|
|
130
|
+
node.type === 'item' ? [1, 1, 1] : (vector(data.scale) ?? [1, 1, 1])
|
|
131
|
+
let basis = attachmentFrame(node, host).matrix
|
|
132
|
+
const hostNode = nodes[host.nodeId]
|
|
133
|
+
let orientation = new Quaternion().setFromEuler(new Euler(...rotation))
|
|
134
|
+
if (hostNode?.type === 'wall') {
|
|
135
|
+
const wallParent = parentId(hostNode)
|
|
136
|
+
const wallHost = wallParent ? frame(wallParent) : null
|
|
137
|
+
if (wallHost) {
|
|
138
|
+
const pose = wallPose(hostNode, position)
|
|
139
|
+
position = pose.position
|
|
140
|
+
basis = wallHost.matrix
|
|
141
|
+
orientation = new Quaternion()
|
|
142
|
+
.setFromEuler(new Euler(0, pose.yaw, 0))
|
|
143
|
+
.multiply(orientation)
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const local = new Matrix4().compose(
|
|
147
|
+
new Vector3(...position),
|
|
148
|
+
orientation,
|
|
149
|
+
new Vector3(...scale),
|
|
150
|
+
)
|
|
151
|
+
result = { nodeId: id, levelId: host.levelId, matrix: basis.clone().multiply(local) }
|
|
152
|
+
}
|
|
153
|
+
resolving.delete(id)
|
|
154
|
+
frames.set(id, result)
|
|
155
|
+
return result
|
|
156
|
+
}
|
|
157
|
+
const parentFrame = (node: AnyNode): Frame | null => {
|
|
158
|
+
const parent = parentId(node)
|
|
159
|
+
return parent ? frame(parent) : null
|
|
160
|
+
}
|
|
161
|
+
const transform = (point: [number, number, number], host: Frame): Point => {
|
|
162
|
+
const hostNode = nodes[host.nodeId]
|
|
163
|
+
if (hostNode?.type === 'wall') {
|
|
164
|
+
const wallHost = parentFrame(hostNode)
|
|
165
|
+
if (wallHost) return transform(wallPose(hostNode, point).position, wallHost)
|
|
166
|
+
}
|
|
167
|
+
const transformed = new Vector3(...point).applyMatrix4(host.matrix)
|
|
168
|
+
return [transformed.x, transformed.z]
|
|
169
|
+
}
|
|
170
|
+
const shape = (node: AnyNode, host: Frame): Shape => {
|
|
171
|
+
const data = fields(node)
|
|
172
|
+
const transformRing = (points: Point[]) => points.map(([x, z]) => transform([x, 0, z], host))
|
|
173
|
+
return {
|
|
174
|
+
polygon: transformRing(ring(data.polygon)),
|
|
175
|
+
holes: Array.isArray(data.holes) ? data.holes.map((hole) => transformRing(ring(hole))) : [],
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
const centres = (node: AnyNode, host: Frame): Point[] => {
|
|
179
|
+
host = attachmentFrame(node, host)
|
|
180
|
+
const data = fields(node)
|
|
181
|
+
const position = vector(data.position)
|
|
182
|
+
const floorPlaced = nodeRegistry.get(node.type)?.capabilities?.floorPlaced
|
|
183
|
+
if (
|
|
184
|
+
floorPlaced &&
|
|
185
|
+
(!floorPlaced.applies || floorPlaced.applies(node)) &&
|
|
186
|
+
(floorPlaced.footprint || floorPlaced.footprints)
|
|
187
|
+
) {
|
|
188
|
+
// Resolvers read this snapshot, never the current scene store. Their positions,
|
|
189
|
+
// like node.position, are in the parent's frame rather than offsets from it.
|
|
190
|
+
return getFloorPlacedFootprints(floorPlaced, node, { nodes }).flatMap((footprint) => {
|
|
191
|
+
const centre = vector(footprint.position) ?? position
|
|
192
|
+
return centre ? [transform(centre, host)] : []
|
|
193
|
+
})
|
|
194
|
+
}
|
|
195
|
+
if (position) return [transform(position, host)]
|
|
196
|
+
const polygonCentre = centroid(ring(data.polygon))
|
|
197
|
+
if (polygonCentre) return [transform([polygonCentre[0], 0, polygonCentre[1]], host)]
|
|
198
|
+
const start = ring([data.start])[0]
|
|
199
|
+
const end = ring([data.end])[0]
|
|
200
|
+
return start && end
|
|
201
|
+
? [transform([(start[0] + end[0]) / 2, 0, (start[1] + end[1]) / 2], host)]
|
|
202
|
+
: []
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const nodesByLevel = new Map<string, AnyNode[]>()
|
|
206
|
+
for (const node of Object.values(nodes)) {
|
|
207
|
+
const host = parentFrame(node)
|
|
208
|
+
if (!host) continue
|
|
209
|
+
const members = nodesByLevel.get(host.levelId) ?? []
|
|
210
|
+
members.push(node)
|
|
211
|
+
nodesByLevel.set(host.levelId, members)
|
|
212
|
+
}
|
|
213
|
+
const topology = new Map<string, ReturnType<typeof detectSpacesForLevel>>()
|
|
214
|
+
const rooms = (levelId: string) => {
|
|
215
|
+
let result = topology.get(levelId)
|
|
216
|
+
if (!result) {
|
|
217
|
+
const boundaries = (nodesByLevel.get(levelId) ?? []).flatMap<WallNode | SeparatorNode>(
|
|
218
|
+
(node) => {
|
|
219
|
+
if (node.type !== 'wall' && node.type !== 'separator') return []
|
|
220
|
+
const host = parentFrame(node)
|
|
221
|
+
if (!host) return []
|
|
222
|
+
return [
|
|
223
|
+
{
|
|
224
|
+
...node,
|
|
225
|
+
start: transform([node.start[0], 0, node.start[1]], host),
|
|
226
|
+
end: transform([node.end[0], 0, node.end[1]], host),
|
|
227
|
+
},
|
|
228
|
+
]
|
|
229
|
+
},
|
|
230
|
+
)
|
|
231
|
+
result = detectSpacesForLevel(levelId, boundaries)
|
|
232
|
+
topology.set(levelId, result)
|
|
233
|
+
}
|
|
234
|
+
return result
|
|
235
|
+
}
|
|
236
|
+
const seeds = new Set<string>(roomContentsOnly ? [] : targetIds)
|
|
237
|
+
const roomExcluded = (node: AnyNode) =>
|
|
238
|
+
node.type === 'zone' ||
|
|
239
|
+
node.type === 'level' ||
|
|
240
|
+
node.type === 'building' ||
|
|
241
|
+
node.type === 'site' ||
|
|
242
|
+
node.type === 'unit'
|
|
243
|
+
for (const id of targetIds) {
|
|
244
|
+
const zone = nodes[id]
|
|
245
|
+
if (zone?.type !== 'zone') continue
|
|
246
|
+
const data = fields(zone)
|
|
247
|
+
const host = parentFrame(zone)
|
|
248
|
+
if (!host) continue
|
|
249
|
+
for (const key of roomContentsOnly ? [] : ['boundaryWallIds', 'boundarySeparatorIds']) {
|
|
250
|
+
const ids = data[key]
|
|
251
|
+
if (Array.isArray(ids))
|
|
252
|
+
for (const boundaryId of ids) {
|
|
253
|
+
if (typeof boundaryId !== 'string') continue
|
|
254
|
+
const boundary = nodes[boundaryId]
|
|
255
|
+
if (
|
|
256
|
+
boundary &&
|
|
257
|
+
parentFrame(boundary)?.levelId === host.levelId &&
|
|
258
|
+
((key === 'boundaryWallIds' && boundary.type === 'wall') ||
|
|
259
|
+
(key === 'boundarySeparatorIds' && boundary.type === 'separator'))
|
|
260
|
+
)
|
|
261
|
+
seeds.add(boundaryId)
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
const outline = shape(zone, host)
|
|
265
|
+
if (outline.polygon.length < 3) continue
|
|
266
|
+
const levelNodes = nodesByLevel.get(host.levelId) ?? []
|
|
267
|
+
const inRoom = (node: AnyNode) => {
|
|
268
|
+
const nodeHost = parentFrame(node)
|
|
269
|
+
return nodeHost && centres(node, nodeHost).some((point) => covers(outline, point))
|
|
270
|
+
}
|
|
271
|
+
if (roomContentsOnly) {
|
|
272
|
+
for (const node of levelNodes) {
|
|
273
|
+
if (nodeRegistry.get(node.type)?.category === 'furnish' && inRoom(node)) seeds.add(node.id)
|
|
274
|
+
}
|
|
275
|
+
continue
|
|
276
|
+
}
|
|
277
|
+
const needsWalls = !Array.isArray(data.boundaryWallIds) || data.boundaryWallIds.length === 0
|
|
278
|
+
const needsSeparators =
|
|
279
|
+
!Array.isArray(data.boundarySeparatorIds) || data.boundarySeparatorIds.length === 0
|
|
280
|
+
if (
|
|
281
|
+
levelNodes.some(
|
|
282
|
+
(node) =>
|
|
283
|
+
(node.type === 'wall' && needsWalls) || (node.type === 'separator' && needsSeparators),
|
|
284
|
+
)
|
|
285
|
+
) {
|
|
286
|
+
const interior = polygonInteriorPoint(outline, true)
|
|
287
|
+
for (const room of rooms(host.levelId).rooms) {
|
|
288
|
+
const roomShape = {
|
|
289
|
+
polygon: room.polygon.map(({ x, y }): Point => [x, y]),
|
|
290
|
+
holes: room.holes,
|
|
291
|
+
}
|
|
292
|
+
if (!covers(roomShape, interior) && !covers(outline, polygonInteriorPoint(roomShape, true)))
|
|
293
|
+
continue
|
|
294
|
+
for (const span of room.spans) {
|
|
295
|
+
const boundary = nodes[span.boundaryId]
|
|
296
|
+
if (
|
|
297
|
+
(boundary?.type === 'wall' && needsWalls) ||
|
|
298
|
+
(boundary?.type === 'separator' && needsSeparators)
|
|
299
|
+
)
|
|
300
|
+
seeds.add(span.boundaryId)
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
for (const node of levelNodes) {
|
|
305
|
+
if (roomExcluded(node)) continue
|
|
306
|
+
if (!inRoom(node)) continue
|
|
307
|
+
seeds.add(node.id)
|
|
308
|
+
// A member hosted on a wall brings the geometry host, not its level ancestors.
|
|
309
|
+
const geometryHost = nodes[parentId(node) ?? '']
|
|
310
|
+
if (geometryHost?.type === 'wall') seeds.add(geometryHost.id)
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
// Contents are spatial members, not every descendant of a room or shared boundary wall.
|
|
314
|
+
if (roomContentsOnly) return seeds
|
|
315
|
+
const scope = new Set<string>()
|
|
316
|
+
const pending = [...seeds]
|
|
317
|
+
while (pending.length) {
|
|
318
|
+
const id = pending.pop()!
|
|
319
|
+
if (scope.has(id)) continue
|
|
320
|
+
scope.add(id)
|
|
321
|
+
for (const child of children.get(id) ?? []) pending.push(child)
|
|
322
|
+
}
|
|
323
|
+
return scope
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** Current furnishings use the same footprint centres and hosting frames as the pin's scope. */
|
|
327
|
+
export function pointRoomHasContents(roomId: string, nodes: Nodes): boolean {
|
|
328
|
+
return snapshotScope(nodes, [roomId], true).size > 0
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/** Target ancestry and room membership are evaluated independently in each snapshot. */
|
|
332
|
+
export function pointTargetScope(
|
|
333
|
+
targetIds: readonly string[],
|
|
334
|
+
before: Nodes,
|
|
335
|
+
current: Nodes,
|
|
336
|
+
): ReadonlySet<string> {
|
|
337
|
+
return new Set([...snapshotScope(before, targetIds), ...snapshotScope(current, targetIds)])
|
|
338
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Point and ask, scene side (the owner, 8 October): the shapes the pure modules, the store and the
|
|
2
|
+
// overlay share. Screen units are CSS pixels in viewport coordinates; world units are metres.
|
|
3
|
+
|
|
4
|
+
export type ScreenPoint = { x: number; y: number }
|
|
5
|
+
|
|
6
|
+
/** A screen rectangle by its edges. */
|
|
7
|
+
export type Rect = { x0: number; y0: number; x1: number; y1: number }
|
|
8
|
+
|
|
9
|
+
/** The sides a bubble can sit on beside its target, or `dock` when the target is off screen. */
|
|
10
|
+
export type BubbleSide = 'right' | 'left' | 'below' | 'above' | 'dock'
|
|
11
|
+
|
|
12
|
+
/** What the bubble's suggestions and placeholder are chosen by (spec 2.4). */
|
|
13
|
+
export type PointKind =
|
|
14
|
+
| 'room'
|
|
15
|
+
| 'wall'
|
|
16
|
+
| 'window'
|
|
17
|
+
| 'door'
|
|
18
|
+
| 'item'
|
|
19
|
+
| 'roof'
|
|
20
|
+
| 'stair'
|
|
21
|
+
| 'several'
|
|
22
|
+
| 'area'
|
|
23
|
+
| 'other'
|
|
24
|
+
|
|
25
|
+
/** One pointed element as the overlay holds it. */
|
|
26
|
+
export type PointTarget = {
|
|
27
|
+
/** The node id, authoritative. */
|
|
28
|
+
id: string
|
|
29
|
+
/** The node's type ('zone', 'wall', 'window', ...). */
|
|
30
|
+
type: string
|
|
31
|
+
kind: PointKind
|
|
32
|
+
/** "Kitchen", "Wall, north side of Kitchen", "Sofa". */
|
|
33
|
+
name: string
|
|
34
|
+
/** The chip's size line in the viewer's unit: "14.2 m²", "4.80 × 2.70 m", "1.20 × 1.40 m". */
|
|
35
|
+
size: string
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The pin's states (spec 2.6): a presence on the element while the ask runs, then its receipt. */
|
|
39
|
+
export type PinState =
|
|
40
|
+
| 'sent'
|
|
41
|
+
| 'queued'
|
|
42
|
+
| 'working'
|
|
43
|
+
| 'needs-input'
|
|
44
|
+
| 'done'
|
|
45
|
+
| 'answered'
|
|
46
|
+
| 'no-change'
|
|
47
|
+
| 'error'
|
|
48
|
+
| 'undone'
|
|
@@ -69,6 +69,34 @@ describe('portable vertex-color boundary', () => {
|
|
|
69
69
|
})
|
|
70
70
|
})
|
|
71
71
|
|
|
72
|
+
test('spreads a mesh with more faces than one atlas holds over several atlases', () => {
|
|
73
|
+
withCanvasCapture((capture) => {
|
|
74
|
+
const faceCount = 256 * 256 + 2
|
|
75
|
+
const colors = new Float32Array(faceCount * 3 * 4)
|
|
76
|
+
for (let vertex = 0; vertex < faceCount * 3; vertex++) {
|
|
77
|
+
colors.set(vertex < (faceCount - 2) * 3 ? [1, 0, 0, 1] : [0, 0, 1, 1], vertex * 4)
|
|
78
|
+
}
|
|
79
|
+
const geometry = new BufferGeometry()
|
|
80
|
+
geometry.setAttribute('position', new Float32BufferAttribute(faceCount * 9, 3))
|
|
81
|
+
geometry.setAttribute('color', new Float32BufferAttribute(colors, 4))
|
|
82
|
+
const mesh: Mesh = new Mesh(geometry, new MeshStandardMaterial({ vertexColors: true }))
|
|
83
|
+
const root = new Group()
|
|
84
|
+
root.add(mesh)
|
|
85
|
+
|
|
86
|
+
expect(normalizePortableScene(root)).toEqual([])
|
|
87
|
+
|
|
88
|
+
const materials = mesh.material as MeshStandardMaterial[]
|
|
89
|
+
expect(materials).toHaveLength(2)
|
|
90
|
+
expect(materials[0]!.map).not.toBe(materials[1]!.map)
|
|
91
|
+
expect(mesh.geometry.groups).toEqual([
|
|
92
|
+
{ start: 0, count: (faceCount - 2) * 3, materialIndex: 0 },
|
|
93
|
+
{ start: (faceCount - 2) * 3, count: 6, materialIndex: 1 },
|
|
94
|
+
])
|
|
95
|
+
// The capture holds the last atlas painted: the page with the two blue faces.
|
|
96
|
+
expect(portableColorAt(mesh, faceCount * 3 - 1, capture)).toEqual([0, 0, 255, 255])
|
|
97
|
+
})
|
|
98
|
+
})
|
|
99
|
+
|
|
72
100
|
test('does not warn when every channel is already portable', () => {
|
|
73
101
|
withCanvasCapture(() => {
|
|
74
102
|
const geometry = triangleGeometry(
|
|
@@ -300,7 +328,12 @@ function portableColorAt(mesh: Mesh, vertexIndex: number, capture: () => ImageDa
|
|
|
300
328
|
Math.round((colors.itemSize >= 4 ? colors.getW(vertexIndex) : 1) * 255),
|
|
301
329
|
]
|
|
302
330
|
}
|
|
303
|
-
const
|
|
331
|
+
const group = mesh.geometry.groups.find(
|
|
332
|
+
({ start, count }) => vertexIndex >= start && vertexIndex < start + count,
|
|
333
|
+
)
|
|
334
|
+
const material = (
|
|
335
|
+
Array.isArray(mesh.material) ? mesh.material[group?.materialIndex ?? 0] : mesh.material
|
|
336
|
+
) as MeshStandardMaterial
|
|
304
337
|
if (!material.map) throw new Error('Portable color has no material-connected carrier')
|
|
305
338
|
const channel = material.map.channel ?? 0
|
|
306
339
|
const uvName = channel === 0 ? 'uv' : `uv${channel}`
|
|
@@ -5,6 +5,7 @@ import { cloneExportUserData } from './export-user-data'
|
|
|
5
5
|
const VERTEX_COLOR_UV_CHANNEL = 0
|
|
6
6
|
const VERTEX_COLOR_TILE_SIZE = 8
|
|
7
7
|
const MAX_VERTEX_COLOR_ATLAS_SIZE = 2048
|
|
8
|
+
const MAX_VERTEX_COLOR_ATLAS_FACES = (MAX_VERTEX_COLOR_ATLAS_SIZE / VERTEX_COLOR_TILE_SIZE) ** 2
|
|
8
9
|
|
|
9
10
|
const MATERIAL_TEXTURE_SLOTS = [
|
|
10
11
|
'map',
|
|
@@ -260,14 +261,26 @@ function colorComponent(
|
|
|
260
261
|
return attribute.itemSize >= 4 ? attribute.getW(index) : 1
|
|
261
262
|
}
|
|
262
263
|
|
|
264
|
+
interface VertexColorAtlasPage {
|
|
265
|
+
/** First face of the expanded geometry painted on this page. */
|
|
266
|
+
firstFace: number
|
|
267
|
+
faceCount: number
|
|
268
|
+
texture: THREE.CanvasTexture
|
|
269
|
+
hasAlpha: boolean
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Paints each face of a vertex-colored geometry into its own atlas tile. A geometry with more faces
|
|
274
|
+
* than one export-sized atlas holds spreads over several pages, each covering a consecutive run of
|
|
275
|
+
* faces, so a large mesh exports at the same tile resolution as a small one.
|
|
276
|
+
*/
|
|
263
277
|
function buildVertexColorAtlas(
|
|
264
278
|
geometry: THREE.BufferGeometry,
|
|
265
279
|
materials: readonly THREE.Material[],
|
|
266
280
|
relocatedUvChannel?: number,
|
|
267
281
|
): {
|
|
268
282
|
geometry: THREE.BufferGeometry
|
|
269
|
-
|
|
270
|
-
hasAlpha: boolean
|
|
283
|
+
pages: VertexColorAtlasPage[]
|
|
271
284
|
clampedHdr: boolean
|
|
272
285
|
} {
|
|
273
286
|
const expanded = geometry.index ? geometry.toNonIndexed() : geometry.clone()
|
|
@@ -286,21 +299,6 @@ function buildVertexColorAtlas(
|
|
|
286
299
|
throw new Error('Vertex-color baking requires triangle geometry with a color attribute')
|
|
287
300
|
}
|
|
288
301
|
|
|
289
|
-
const faceCount = position.count / 3
|
|
290
|
-
const tilesPerRow = Math.ceil(Math.sqrt(faceCount))
|
|
291
|
-
const atlasSize = THREE.MathUtils.ceilPowerOfTwo(tilesPerRow * VERTEX_COLOR_TILE_SIZE)
|
|
292
|
-
if (atlasSize > MAX_VERTEX_COLOR_ATLAS_SIZE) {
|
|
293
|
-
expanded.dispose()
|
|
294
|
-
throw new Error(
|
|
295
|
-
`Vertex-color atlas requires ${atlasSize}px, exceeding the ${MAX_VERTEX_COLOR_ATLAS_SIZE}px export limit`,
|
|
296
|
-
)
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
const { canvas, context } = canvas2d(atlasSize, atlasSize)
|
|
300
|
-
const image = context.createImageData(atlasSize, atlasSize)
|
|
301
|
-
|
|
302
|
-
const uv = new Float32Array(position.count * 2)
|
|
303
|
-
let hasAlpha = false
|
|
304
302
|
let clampedHdr = false
|
|
305
303
|
for (let index = 0; index < color.count && !clampedHdr; index++) {
|
|
306
304
|
const triangleOffset = index - (index % 3)
|
|
@@ -312,9 +310,38 @@ function buildVertexColorAtlas(
|
|
|
312
310
|
clampedHdr = red < 0 || red > 1 || green < 0 || green > 1 || blue < 0 || blue > 1
|
|
313
311
|
}
|
|
314
312
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
313
|
+
const totalFaces = position.count / 3
|
|
314
|
+
const uv = new Float32Array(position.count * 2)
|
|
315
|
+
const pages: VertexColorAtlasPage[] = []
|
|
316
|
+
for (let firstFace = 0; firstFace < totalFaces; firstFace += MAX_VERTEX_COLOR_ATLAS_FACES) {
|
|
317
|
+
const faceCount = Math.min(MAX_VERTEX_COLOR_ATLAS_FACES, totalFaces - firstFace)
|
|
318
|
+
pages.push(paintVertexColorAtlasPage(expanded, materials, uv, firstFace, faceCount))
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
expanded.setAttribute(uvAttributeName(VERTEX_COLOR_UV_CHANNEL), new THREE.BufferAttribute(uv, 2))
|
|
322
|
+
expanded.deleteAttribute('color')
|
|
323
|
+
return { geometry: expanded, pages, clampedHdr }
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** Paints faces `firstFace … firstFace + faceCount` into one atlas and writes their tile UVs. */
|
|
327
|
+
function paintVertexColorAtlasPage(
|
|
328
|
+
expanded: THREE.BufferGeometry,
|
|
329
|
+
materials: readonly THREE.Material[],
|
|
330
|
+
uv: Float32Array,
|
|
331
|
+
firstFace: number,
|
|
332
|
+
faceCount: number,
|
|
333
|
+
): VertexColorAtlasPage {
|
|
334
|
+
const color = expanded.getAttribute('color')
|
|
335
|
+
const tilesPerRow = Math.ceil(Math.sqrt(faceCount))
|
|
336
|
+
const atlasSize = THREE.MathUtils.ceilPowerOfTwo(tilesPerRow * VERTEX_COLOR_TILE_SIZE)
|
|
337
|
+
const { canvas, context } = canvas2d(atlasSize, atlasSize)
|
|
338
|
+
const image = context.createImageData(atlasSize, atlasSize)
|
|
339
|
+
let hasAlpha = false
|
|
340
|
+
|
|
341
|
+
for (let tile = 0; tile < faceCount; tile++) {
|
|
342
|
+
const face = firstFace + tile
|
|
343
|
+
const tileX = (tile % tilesPerRow) * VERTEX_COLOR_TILE_SIZE
|
|
344
|
+
const tileY = Math.floor(tile / tilesPerRow) * VERTEX_COLOR_TILE_SIZE
|
|
318
345
|
const vertices = [face * 3, face * 3 + 1, face * 3 + 2]
|
|
319
346
|
const colors = vertices.map((vertex) => [
|
|
320
347
|
colorComponent(color, vertex, 0),
|
|
@@ -375,14 +402,14 @@ function buildVertexColorAtlas(
|
|
|
375
402
|
const tileColumn = Math.floor(x / VERTEX_COLOR_TILE_SIZE)
|
|
376
403
|
const tileIndex = tileRow * tilesPerRow + tileColumn
|
|
377
404
|
if (tileColumn < tilesPerRow && tileIndex < faceCount) continue
|
|
378
|
-
const
|
|
405
|
+
const sourceTile =
|
|
379
406
|
tileRow * tilesPerRow < faceCount
|
|
380
407
|
? Math.min((tileRow + 1) * tilesPerRow - 1, faceCount - 1)
|
|
381
408
|
: faceCount - 1
|
|
382
409
|
const sourceX =
|
|
383
|
-
(
|
|
410
|
+
(sourceTile % tilesPerRow) * VERTEX_COLOR_TILE_SIZE + (x % VERTEX_COLOR_TILE_SIZE)
|
|
384
411
|
const sourceY =
|
|
385
|
-
Math.floor(
|
|
412
|
+
Math.floor(sourceTile / tilesPerRow) * VERTEX_COLOR_TILE_SIZE + (y % VERTEX_COLOR_TILE_SIZE)
|
|
386
413
|
const sourcePixel = (sourceY * atlasSize + sourceX) * 4
|
|
387
414
|
const targetPixel = (y * atlasSize + x) * 4
|
|
388
415
|
image.data[targetPixel] = image.data[sourcePixel]!
|
|
@@ -393,16 +420,45 @@ function buildVertexColorAtlas(
|
|
|
393
420
|
}
|
|
394
421
|
|
|
395
422
|
context.putImageData(image, 0, 0)
|
|
396
|
-
expanded.setAttribute(uvAttributeName(VERTEX_COLOR_UV_CHANNEL), new THREE.BufferAttribute(uv, 2))
|
|
397
|
-
expanded.deleteAttribute('color')
|
|
398
423
|
const texture = new THREE.CanvasTexture(canvas)
|
|
399
424
|
texture.name = 'pascal_vertex_color_atlas'
|
|
400
425
|
texture.channel = VERTEX_COLOR_UV_CHANNEL
|
|
401
426
|
texture.colorSpace = THREE.SRGBColorSpace
|
|
402
427
|
texture.flipY = true
|
|
403
428
|
texture.needsUpdate = true
|
|
404
|
-
return {
|
|
429
|
+
return { firstFace, faceCount, texture, hasAlpha }
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Geometry groups for a geometry whose faces are spread over atlas pages: every source group is cut
|
|
434
|
+
* at page boundaries, and the piece on page `p` uses material `p * materialCount + its own index`.
|
|
435
|
+
*/
|
|
436
|
+
function pagedGroups(
|
|
437
|
+
geometry: THREE.BufferGeometry,
|
|
438
|
+
pages: readonly VertexColorAtlasPage[],
|
|
439
|
+
materialCount: number,
|
|
440
|
+
): { start: number; count: number; materialIndex: number }[] {
|
|
441
|
+
const vertexCount = geometry.getAttribute('position').count
|
|
442
|
+
const sources = geometry.groups.length
|
|
443
|
+
? geometry.groups
|
|
444
|
+
: [{ start: 0, count: vertexCount, materialIndex: 0 }]
|
|
445
|
+
const groups: { start: number; count: number; materialIndex: number }[] = []
|
|
446
|
+
for (const source of sources) {
|
|
447
|
+
const sourceEnd = Math.min(source.start + source.count, vertexCount)
|
|
448
|
+
pages.forEach((page, pageIndex) => {
|
|
449
|
+
const start = Math.max(source.start, page.firstFace * 3)
|
|
450
|
+
const end = Math.min(sourceEnd, (page.firstFace + page.faceCount) * 3)
|
|
451
|
+
if (end <= start) return
|
|
452
|
+
groups.push({
|
|
453
|
+
start,
|
|
454
|
+
count: end - start,
|
|
455
|
+
materialIndex: pageIndex * materialCount + (source.materialIndex ?? 0),
|
|
456
|
+
})
|
|
457
|
+
})
|
|
458
|
+
}
|
|
459
|
+
return groups
|
|
405
460
|
}
|
|
461
|
+
|
|
406
462
|
function hasDeformedGeometry(mesh: THREE.Mesh): boolean {
|
|
407
463
|
const skinnedMesh = mesh as THREE.SkinnedMesh
|
|
408
464
|
return (
|
|
@@ -466,36 +522,46 @@ function bakeVertexColors(root: THREE.Object3D, preserveDeformations = false): b
|
|
|
466
522
|
if (baked.clampedHdr) mesh.userData.pascalHdrVertexColor = 'clamped-to-portable-range'
|
|
467
523
|
clampedHdr ||= baked.clampedHdr
|
|
468
524
|
const relocatedTextures = new Map<THREE.Texture, THREE.Texture>()
|
|
469
|
-
const
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
const
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
525
|
+
const converted = baked.pages.flatMap((page) => {
|
|
526
|
+
const convertedMaterials = new Map<THREE.Material, THREE.Material>()
|
|
527
|
+
return materials.map((material) => {
|
|
528
|
+
const cached = convertedMaterials.get(material)
|
|
529
|
+
if (cached) return cached
|
|
530
|
+
if (!material.vertexColors && relocatedUvChannel === undefined) return material
|
|
531
|
+
const clone = material.clone() as TexturedMaterial
|
|
532
|
+
if (relocatedUvChannel !== undefined) {
|
|
533
|
+
for (const slot of MATERIAL_TEXTURE_SLOTS) {
|
|
534
|
+
const sourceTexture = clone[slot]
|
|
535
|
+
if (!(sourceTexture instanceof THREE.Texture) || sourceTexture.channel !== 0) continue
|
|
536
|
+
let relocatedTexture = relocatedTextures.get(sourceTexture)
|
|
537
|
+
if (!relocatedTexture) {
|
|
538
|
+
relocatedTexture = sourceTexture.clone()
|
|
539
|
+
relocatedTexture.channel = relocatedUvChannel
|
|
540
|
+
relocatedTexture.needsUpdate = true
|
|
541
|
+
relocatedTextures.set(sourceTexture, relocatedTexture)
|
|
542
|
+
rememberDiscarded(root, sourceTexture)
|
|
543
|
+
}
|
|
544
|
+
clone[slot] = relocatedTexture
|
|
486
545
|
}
|
|
487
|
-
clone[slot] = relocatedTexture
|
|
488
546
|
}
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
547
|
+
if (material.vertexColors) {
|
|
548
|
+
clone.vertexColors = false
|
|
549
|
+
clone.map = page.texture
|
|
550
|
+
if (page.hasAlpha) clone.transparent = true
|
|
551
|
+
}
|
|
552
|
+
convertedMaterials.set(material, clone)
|
|
553
|
+
return clone
|
|
554
|
+
})
|
|
497
555
|
})
|
|
498
|
-
|
|
556
|
+
if (baked.pages.length > 1) {
|
|
557
|
+
const groups = pagedGroups(mesh.geometry, baked.pages, materials.length)
|
|
558
|
+
mesh.geometry.clearGroups()
|
|
559
|
+
for (const group of groups)
|
|
560
|
+
mesh.geometry.addGroup(group.start, group.count, group.materialIndex)
|
|
561
|
+
mesh.material = converted
|
|
562
|
+
} else {
|
|
563
|
+
mesh.material = Array.isArray(mesh.material) ? converted : converted[0]!
|
|
564
|
+
}
|
|
499
565
|
rememberDiscarded(root, originalGeometry)
|
|
500
566
|
for (const material of materials) rememberDiscarded(root, material)
|
|
501
567
|
}
|