@pascal-app/editor 1.0.5 → 1.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +20 -17
- package/src/components/editor/action-menu-button.tsx +4 -1
- package/src/components/editor/alignment-3d-guide-layer.tsx +102 -11
- package/src/components/editor/bake-thumbnail.tsx +3 -2
- package/src/components/editor/custom-camera-controls.tsx +20 -8
- package/src/components/editor/elevation-3d-guide-layer.tsx +1 -1
- package/src/components/editor/first-person-controls.tsx +72 -61
- package/src/components/editor/floating-action-menu.tsx +27 -9
- package/src/components/editor/floating-building-action-menu.tsx +1 -1
- package/src/components/editor/floorplan-panel.test.ts +220 -0
- package/src/components/editor/floorplan-panel.tsx +285 -132
- package/src/components/editor/footprint-height-handle.tsx +1 -1
- package/src/components/editor/grid.tsx +24 -15
- package/src/components/editor/group-actions.ts +19 -1
- package/src/components/editor/group-floating-action-menu.tsx +1 -1
- package/src/components/editor/group-move-3d.ts +27 -2
- package/src/components/editor/group-selection-box-3d.tsx +4 -1
- package/src/components/editor/group-transform-shared.ts +20 -0
- package/src/components/editor/index.tsx +130 -20
- package/src/components/editor/inspector-extension-actions.tsx +36 -0
- package/src/components/editor/localized-html.tsx +22 -0
- package/src/components/editor/node-action-menu.tsx +46 -12
- package/src/components/editor/node-arrow-handles.tsx +1 -2
- package/src/components/editor/open-wall-ends-3d-layer.tsx +1 -1
- package/src/components/editor/opening-guides-3d-layer.tsx +1 -1
- package/src/components/editor/registry-action-contributions.test.tsx +67 -6
- package/src/components/editor/registry-action-contributions.tsx +115 -15
- package/src/components/editor/riser-diagram-panel.tsx +59 -9
- package/src/components/editor/room-controls.tsx +1 -1
- package/src/components/editor/room-handles-2d.tsx +156 -6
- package/src/components/editor/room-handles.tsx +241 -51
- package/src/components/editor/room-highlight.tsx +2 -0
- package/src/components/editor/room-labels.tsx +29 -15
- package/src/components/editor/selection-manager.tsx +15 -102
- package/src/components/editor/separator-end-grip.tsx +74 -0
- package/src/components/editor/site-edge-labels.tsx +1 -1
- package/src/components/editor/thumbnail-generator.test.ts +15 -6
- package/src/components/editor/thumbnail-generator.tsx +26 -1
- package/src/components/editor/use-floorplan-background-placement.ts +55 -52
- package/src/components/editor/views/view-bar.tsx +111 -26
- package/src/components/editor/wall-measurement-label.tsx +1 -1
- package/src/components/editor/wall-move-side-handles.tsx +5 -1
- package/src/components/editor/wall-pull-grip.tsx +193 -0
- package/src/components/editor/wall-snap-beacon-layer.tsx +81 -1
- package/src/components/editor-2d/floorplan-adjacent-walls-layer.test.ts +76 -0
- package/src/components/editor-2d/floorplan-adjacent-walls-layer.tsx +186 -0
- package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +63 -3
- package/src/components/editor-2d/floorplan-group-move.tsx +31 -3
- package/src/components/editor-2d/floorplan-measurement-tool-layer.test.ts +6 -0
- package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +22 -13
- package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +11 -3
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +7 -1
- package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +96 -4
- package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +52 -12
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +12 -1
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +197 -37
- package/src/components/procedural-items/inspector.tsx +59 -12
- package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +2 -0
- package/src/components/systems/ceiling/ceiling-system.test.ts +141 -0
- package/src/components/systems/ceiling/ceiling-system.tsx +53 -18
- package/src/components/tools/distribution/distribution-run-tool.tsx +50 -26
- package/src/components/tools/item/placement-history.test.tsx +1 -1
- package/src/components/tools/item/placement-strategies.test.ts +64 -1
- package/src/components/tools/item/placement-strategies.ts +5 -9
- package/src/components/tools/item/use-placement-coordinator.tsx +30 -34
- package/src/components/tools/registry/item-surface-move.test.ts +11 -0
- package/src/components/tools/select/select-candidates.test.ts +45 -0
- package/src/components/tools/select/select-candidates.ts +13 -2
- package/src/components/tools/shared/cursor-sphere.tsx +10 -3
- package/src/components/tools/shared/draft-measurement-label.tsx +1 -1
- package/src/components/tools/shared/placement-box.tsx +1 -1
- package/src/components/tools/shared/placement-dimension-guides.tsx +1 -1
- package/src/components/tools/site/terrain-brush-cursor.tsx +1 -1
- package/src/components/tools/wall/wall-drafting.test.ts +299 -23
- package/src/components/tools/wall/wall-drafting.ts +132 -24
- package/src/components/tools/wall/wall-pull-snap.test.ts +274 -0
- package/src/components/tools/wall/wall-pull-snap.ts +563 -0
- package/src/components/tools/wall/wall-snap-geometry.test.ts +113 -1
- package/src/components/tools/wall/wall-snap-geometry.ts +137 -0
- package/src/components/tools/zone/zone-tool.tsx +3 -3
- package/src/components/ui/action-menu/action-button.tsx +2 -0
- package/src/components/ui/action-menu/camera-actions.tsx +8 -7
- package/src/components/ui/action-menu/control-modes.tsx +9 -5
- package/src/components/ui/action-menu/follow-pascal-pill.tsx +3 -1
- package/src/components/ui/action-menu/follow-pascal-toggle.tsx +7 -1
- package/src/components/ui/action-menu/furnish-tools.tsx +7 -6
- package/src/components/ui/action-menu/index.tsx +1 -0
- package/src/components/ui/action-menu/measurement-control.tsx +35 -29
- package/src/components/ui/action-menu/point-ask-button.tsx +4 -3
- package/src/components/ui/action-menu/structure-tools.tsx +25 -23
- package/src/components/ui/action-menu/view-toggles.test.ts +18 -0
- package/src/components/ui/action-menu/view-toggles.tsx +256 -51
- package/src/components/ui/build-panel/build-panel.tsx +163 -25
- package/src/components/ui/command-palette/editor-commands.tsx +38 -11
- package/src/components/ui/controls/finish-swatch.tsx +45 -19
- package/src/components/ui/controls/material-picker.tsx +24 -17
- package/src/components/ui/controls/metric-control.test.ts +52 -0
- package/src/components/ui/controls/metric-control.tsx +62 -24
- package/src/components/ui/controls/segmented-control.tsx +5 -2
- package/src/components/ui/controls/slider-control.test.tsx +23 -0
- package/src/components/ui/controls/slider-control.tsx +30 -14
- package/src/components/ui/floating-level-selector.tsx +100 -48
- package/src/components/ui/helpers/building-helper.tsx +5 -3
- package/src/components/ui/helpers/contextual-helper-panel.tsx +97 -37
- package/src/components/ui/helpers/helper-manager.test.tsx +53 -3
- package/src/components/ui/helpers/helper-manager.tsx +82 -57
- package/src/components/ui/helpers/item-helper.tsx +6 -4
- package/src/components/ui/helpers/registered-tool-helper.tsx +3 -1
- package/src/components/ui/level-duplicate-dialog.tsx +32 -23
- package/src/components/ui/level-wall-copy-prompt.tsx +124 -0
- package/src/components/ui/panels/collections/collections-popover.tsx +11 -11
- package/src/components/ui/panels/deck-boards-fields.tsx +136 -0
- package/src/components/ui/panels/floor-foundation-panel.tsx +113 -38
- package/src/components/ui/panels/floor-opening-panel.tsx +73 -25
- package/src/components/ui/panels/inspector-extensions.tsx +66 -0
- package/src/components/ui/panels/mobile-panel-sheet.tsx +3 -1
- package/src/components/ui/panels/mobile-selection-bar.tsx +8 -6
- package/src/components/ui/panels/multi-field-value.test.ts +27 -0
- package/src/components/ui/panels/multi-field-value.ts +20 -1
- package/src/components/ui/panels/multi-height-mode.tsx +8 -7
- package/src/components/ui/panels/multi-parametric-inspector.tsx +122 -34
- package/src/components/ui/panels/multi-selection-panel.tsx +75 -35
- package/src/components/ui/panels/node-display.ts +35 -24
- package/src/components/ui/panels/panel-manager.tsx +5 -3
- package/src/components/ui/panels/panel-wrapper.tsx +22 -109
- package/src/components/ui/panels/parametric-field-control.tsx +13 -10
- package/src/components/ui/panels/parametric-inspector.tsx +18 -12
- package/src/components/ui/panels/reference-panel.tsx +51 -35
- package/src/components/ui/panels/room-construction-rows.tsx +464 -65
- package/src/components/ui/panels/room-name-options.test.ts +36 -3
- package/src/components/ui/panels/room-name-options.ts +110 -11
- package/src/components/ui/panels/room-openings-field.tsx +21 -12
- package/src/components/ui/panels/room-panel.tsx +206 -99
- package/src/components/ui/panels/shape-choice.tsx +7 -5
- package/src/components/ui/panels/wall-region-list.tsx +7 -7
- package/src/components/ui/primitives/shortcut-token.tsx +10 -6
- package/src/components/ui/sidebar/icon-rail.tsx +11 -9
- package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +10 -10
- package/src/components/ui/sidebar/panels/items-panel/index.tsx +10 -9
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +3 -0
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +102 -32
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +4 -3
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +7 -5
- package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/default-names.ts +75 -0
- package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +117 -64
- package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +36 -3
- package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +7 -4
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +8 -2
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +8 -5
- package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +8 -3
- package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +8 -7
- package/src/components/ui/sidebar/panels/site-panel/tree-node-actions.tsx +7 -6
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +16 -1
- package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +15 -8
- package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/zone-membership-checkbox.tsx +3 -1
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +18 -8
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +25 -14
- package/src/components/ui/sidebar/tab-bar.tsx +4 -1
- package/src/components/ui/sidebar/use-plugin-panels.tsx +19 -10
- package/src/components/ui/snap-target-badge.tsx +7 -6
- package/src/components/ui/wall-type/wall-type-editor-dialog.tsx +721 -0
- package/src/components/ui/wall-type/wall-type-menu.tsx +167 -0
- package/src/components/viewer/floorplan-compass-button.tsx +12 -3
- package/src/components/viewer/floorplan-preview.tsx +27 -19
- package/src/components/viewer/render-settings-menu.tsx +294 -0
- package/src/components/viewer/sky-control.tsx +710 -0
- package/src/components/viewer/use-viewer-ui-sounds.test.ts +15 -0
- package/src/components/viewer/use-viewer-ui-sounds.ts +46 -0
- package/src/components/viewer/viewer-controls-bar.tsx +278 -260
- package/src/components/viewer/viewer-hover-label.tsx +12 -4
- package/src/components/viewer/viewer-rooms.tsx +61 -4
- package/src/components/viewer/viewer-scene-header.tsx +28 -17
- package/src/components/viewer/viewer-stage-switcher.tsx +4 -2
- package/src/components/viewer-overlay.tsx +8 -0
- package/src/components/walkthrough-hud.tsx +75 -2
- package/src/hooks/use-keyboard.test.ts +85 -4
- package/src/hooks/use-keyboard.ts +65 -7
- package/src/hooks/use-selected-room.ts +12 -1
- package/src/i18n/floor-footprint-name.ts +30 -0
- package/src/i18n/level-name.ts +39 -0
- package/src/i18n/locales/en/messages.po +15117 -0
- package/src/i18n/locales/en/messages.ts +1 -0
- package/src/i18n/locales/en-XA/messages.ts +1 -0
- package/src/i18n/locales/es/messages.po +37501 -0
- package/src/i18n/locales/es/messages.ts +1 -0
- package/src/i18n/locales/fr/messages.po +37501 -0
- package/src/i18n/locales/fr/messages.ts +1 -0
- package/src/i18n/locales/ja/messages.po +37501 -0
- package/src/i18n/locales/ja/messages.ts +1 -0
- package/src/i18n/locales/zh-CN/messages.po +37501 -0
- package/src/i18n/locales/zh-CN/messages.ts +1 -0
- package/src/i18n/locales.test.ts +63 -0
- package/src/i18n/locales.ts +32 -0
- package/src/i18n/message-slot.tsx +25 -0
- package/src/i18n/node-messages.ts +169 -0
- package/src/i18n/pseudo-locale.ts +17 -0
- package/src/i18n/published-entry.test.ts +17 -0
- package/src/i18n/scene-theme-name.ts +16 -0
- package/src/index.tsx +156 -4
- package/src/lib/add-storey.test.ts +3 -3
- package/src/lib/contextual-help.test.ts +20 -1
- package/src/lib/contextual-help.ts +95 -19
- package/src/lib/continuation.ts +38 -11
- package/src/lib/floor-footprints.test.ts +2 -1
- package/src/lib/floor-foundation-support.test.ts +7 -3
- package/src/lib/floorplan/dimension-drive.test.ts +36 -2
- package/src/lib/floorplan/dimension-drive.ts +68 -3
- package/src/lib/floorplan/floorplan-export.test.ts +16 -5
- package/src/lib/floorplan/floorplan-export.tsx +13 -16
- package/src/lib/floorplan/floorplan-extension.test.ts +11 -12
- package/src/lib/floorplan/floorplan-extension.ts +37 -18
- package/src/lib/floorplan/floorplan-mode.test.ts +13 -6
- package/src/lib/floorplan/floorplan-mode.ts +14 -12
- package/src/lib/floorplan/floorplan-plugin-install.test.tsx +1 -1
- package/src/lib/floorplan/floorplan-readonly.ts +3 -3
- package/src/lib/floorplan/schedules.ts +10 -6
- package/src/lib/floorplan/site-plan/site-annotations.test.ts +46 -1
- package/src/lib/floorplan/site-plan/site-annotations.ts +3 -0
- package/src/lib/floorplan/site-plan/site-parts.ts +6 -3
- package/src/lib/geometry-script/author.test.ts +114 -0
- package/src/lib/geometry-script/author.ts +55 -23
- package/src/lib/geometry-script/client.ts +3 -0
- package/src/lib/geometry-script/finish-scripts.ts +7 -7
- package/src/lib/glb-export.test.ts +327 -0
- package/src/lib/glb-export.ts +70 -2
- package/src/lib/grid-frame.test.ts +56 -3
- package/src/lib/grid-frame.ts +28 -1
- package/src/lib/history.test.ts +2 -1
- package/src/lib/hud-title.ts +48 -24
- package/src/lib/ifc-export.test.ts +41 -1
- package/src/lib/inspector-card-mode.test.ts +3 -31
- package/src/lib/inspector-card-mode.ts +5 -10
- package/src/lib/inspector-expanded.test.ts +13 -0
- package/src/lib/inspector-expanded.ts +13 -1
- package/src/lib/keyboard-pan.test.ts +7 -0
- package/src/lib/keyboard-pan.ts +9 -2
- package/src/lib/layer-thickness.test.ts +22 -0
- package/src/lib/layer-thickness.ts +79 -0
- package/src/lib/lot/drop-in.test.ts +59 -1
- package/src/lib/lot/drop-in.ts +42 -50
- package/src/lib/material-swatch-style.ts +24 -0
- package/src/lib/measurement-parser.ts +7 -3
- package/src/lib/measurements.test.ts +17 -0
- package/src/lib/measurements.ts +20 -5
- package/src/lib/mezzanine-selection.test.ts +3 -1
- package/src/lib/orbit-near-plane.test.ts +12 -0
- package/src/lib/orbit-near-plane.ts +13 -0
- package/src/lib/paint-regions.test.ts +8 -5
- package/src/lib/paint-scope.test.ts +53 -0
- package/src/lib/paint-scope.ts +18 -4
- package/src/lib/panel-rows.ts +17 -8
- package/src/lib/reference-scale.ts +83 -0
- package/src/lib/room-built-on.test.ts +26 -12
- package/src/lib/room-built-on.ts +20 -2
- package/src/lib/room-construction.test.ts +28 -1
- package/src/lib/room-construction.ts +14 -0
- package/src/lib/room-controls-phase6.test.ts +140 -0
- package/src/lib/room-handle-drag.ts +557 -42
- package/src/lib/room-name-catalog.test.ts +8 -0
- package/src/lib/room-name-catalog.ts +8 -1
- package/src/lib/room-name-messages.test.ts +132 -0
- package/src/lib/room-name-messages.ts +147 -0
- package/src/lib/room-name-storage.test.ts +334 -0
- package/src/lib/room-push-dimensions.test.ts +37 -0
- package/src/lib/room-push-dimensions.ts +226 -2
- package/src/lib/room-selection.test.ts +23 -6
- package/src/lib/scene-clipboard.test.ts +70 -49
- package/src/lib/scene-clipboard.ts +9 -4
- package/src/lib/selection-cycle.ts +63 -0
- package/src/lib/separator-edit.test.ts +431 -0
- package/src/lib/separator-edit.ts +179 -0
- package/src/lib/separator-lines.ts +38 -0
- package/src/lib/sfx-bus.ts +27 -1
- package/src/lib/sfx-player.ts +16 -0
- package/src/lib/tool-cursor-icon.ts +5 -1
- package/src/lib/tool-shortcuts.ts +130 -0
- package/src/lib/viewer-selection.test.ts +28 -24
- package/src/lib/viewer-selection.ts +20 -5
- package/src/lib/wall-draw-variant.test.ts +7 -6
- package/src/lib/wall-draw-variant.ts +107 -6
- package/src/lib/wall-nudge.test.ts +44 -0
- package/src/lib/wall-nudge.ts +131 -0
- package/src/lib/wall-parallel-snap.test.ts +83 -0
- package/src/lib/wall-parallel-snap.ts +176 -0
- package/src/lib/wall-placement-pointer.test.ts +23 -0
- package/src/lib/wall-placement-pointer.ts +18 -0
- package/src/lib/wall-pull.test.ts +245 -0
- package/src/lib/wall-pull.ts +209 -0
- package/src/lib/wall-push-line-up.test.ts +132 -0
- package/src/lib/wall-region-list.ts +14 -5
- package/src/lib/wall-run-snap.test.ts +188 -0
- package/src/lib/wall-run-snap.ts +83 -0
- package/src/lib/wall-type.test.ts +125 -0
- package/src/lib/wall-type.ts +119 -0
- package/src/lib/window-interaction.test.ts +42 -0
- package/src/lib/window-interaction.ts +47 -5
- package/src/lib/world-grid-snap.test.ts +139 -2
- package/src/lib/world-grid-snap.ts +102 -105
- package/src/store/use-alignment-guides.ts +27 -1
- package/src/store/use-editor.tsx +48 -0
- package/src/store/use-floorplan-annotation-visibility.test.ts +64 -0
- package/src/store/use-floorplan-annotation-visibility.ts +47 -22
- package/src/store/use-wall-snap-indicator.ts +15 -2
|
@@ -15,11 +15,14 @@ import {
|
|
|
15
15
|
SiteNode,
|
|
16
16
|
sceneRegistry,
|
|
17
17
|
useScene,
|
|
18
|
+
WindowNode,
|
|
18
19
|
} from '@pascal-app/core'
|
|
19
20
|
import { evaluateRecipe, ProceduralItemNode, parseRecipe } from '@pascal-app/core/procedural-items'
|
|
20
21
|
import {
|
|
21
22
|
buildDoorPreviewMesh,
|
|
23
|
+
buildWindowPreviewMesh,
|
|
22
24
|
markViewerPresentationTextureBorrowed,
|
|
25
|
+
poseWindowMovingParts,
|
|
23
26
|
type ViewerPresentationContribution,
|
|
24
27
|
viewerPresentationRegistry,
|
|
25
28
|
} from '@pascal-app/viewer'
|
|
@@ -1164,6 +1167,210 @@ describe('prepareSceneForExport', () => {
|
|
|
1164
1167
|
expect(hasMesh).toBe(false)
|
|
1165
1168
|
})
|
|
1166
1169
|
|
|
1170
|
+
test.each(['tilt', 'turn'] as const)(
|
|
1171
|
+
'bakes tilt-turn %s with attached hardware and grids at 0, 0.5 and 1',
|
|
1172
|
+
(tiltTurnMode) => {
|
|
1173
|
+
const node = WindowNode.parse({
|
|
1174
|
+
windowType: 'tilt-turn',
|
|
1175
|
+
tiltTurnMode,
|
|
1176
|
+
hingesSide: 'right',
|
|
1177
|
+
operationState: 0.6,
|
|
1178
|
+
sill: false,
|
|
1179
|
+
sashGrid: true,
|
|
1180
|
+
columnRatios: [1, 1],
|
|
1181
|
+
rollerShutter: 'box',
|
|
1182
|
+
rollerShutterOpen: 0.5,
|
|
1183
|
+
})
|
|
1184
|
+
const mesh = buildWindowPreviewMesh(node)
|
|
1185
|
+
const original = mesh.getObjectByName('tilt-turn-window-sash')!.quaternion.clone()
|
|
1186
|
+
const root = new THREE.Group().add(mesh)
|
|
1187
|
+
sceneRegistry.nodes.set(node.id, mesh)
|
|
1188
|
+
const prepared = prepareSceneForExport(root, { [node.id]: node })
|
|
1189
|
+
expect(prepared.animations).toHaveLength(2)
|
|
1190
|
+
const clip = prepared.animations.find((clip) => clip.name === `${node.id}: open`)!
|
|
1191
|
+
expect(clip.tracks).toHaveLength(1)
|
|
1192
|
+
const target = prepared.scene.getObjectByProperty(
|
|
1193
|
+
'uuid',
|
|
1194
|
+
clip.tracks[0]!.name.replace('.quaternion', ''),
|
|
1195
|
+
)!
|
|
1196
|
+
expect(target.getObjectByName('window-handle')).toBeDefined()
|
|
1197
|
+
expect(target.getObjectByName('window-sash-grid')).toBeDefined()
|
|
1198
|
+
const mixer = new THREE.AnimationMixer(prepared.scene)
|
|
1199
|
+
const action = mixer.clipAction(clip)
|
|
1200
|
+
action.setLoop(THREE.LoopOnce, 1)
|
|
1201
|
+
action.clampWhenFinished = true
|
|
1202
|
+
action.play()
|
|
1203
|
+
for (const value of [0, 0.5, 1]) {
|
|
1204
|
+
mixer.setTime(value)
|
|
1205
|
+
const live = buildWindowPreviewMesh({ ...node, operationState: value })
|
|
1206
|
+
const liveSash = live.getObjectByName('tilt-turn-window-sash')!
|
|
1207
|
+
expect(target.quaternion.clone().normalize().angleTo(liveSash.quaternion)).toBeLessThan(
|
|
1208
|
+
1e-6,
|
|
1209
|
+
)
|
|
1210
|
+
}
|
|
1211
|
+
expect(
|
|
1212
|
+
mesh.getObjectByName('tilt-turn-window-sash')!.quaternion.angleTo(original),
|
|
1213
|
+
).toBeCloseTo(0)
|
|
1214
|
+
mixer.stopAllAction()
|
|
1215
|
+
prepared.dispose()
|
|
1216
|
+
},
|
|
1217
|
+
)
|
|
1218
|
+
|
|
1219
|
+
test.each(
|
|
1220
|
+
(['box', 'concealed'] as const).flatMap((rollerShutter) =>
|
|
1221
|
+
[0, 0.5, 1].map((shutterOpen) => ({ rollerShutter, shutterOpen })),
|
|
1222
|
+
),
|
|
1223
|
+
)(
|
|
1224
|
+
'bakes saved bifold shutters and $rollerShutter roller at $shutterOpen without changing live previews',
|
|
1225
|
+
({ shutterOpen, rollerShutter }) => {
|
|
1226
|
+
const node = WindowNode.parse({
|
|
1227
|
+
windowType: 'casement',
|
|
1228
|
+
shutters: 'panel',
|
|
1229
|
+
shutterOpen,
|
|
1230
|
+
width: 2,
|
|
1231
|
+
rollerShutter,
|
|
1232
|
+
rollerShutterOpen: shutterOpen,
|
|
1233
|
+
})
|
|
1234
|
+
const mesh = buildWindowPreviewMesh(node)
|
|
1235
|
+
const saved = mesh.getObjectByName('shutter-left')!.quaternion.clone()
|
|
1236
|
+
poseWindowMovingParts(node, mesh, 0, 1 - shutterOpen)
|
|
1237
|
+
const preview = mesh.getObjectByName('shutter-left')!.quaternion.clone()
|
|
1238
|
+
// Simulate a live roller preview whose topology differs from the saved node.
|
|
1239
|
+
mesh.getObjectByName('window-roller-curtain')!.clear()
|
|
1240
|
+
sceneRegistry.nodes.set(node.id, mesh)
|
|
1241
|
+
const prepared = prepareSceneForExport(new THREE.Group().add(mesh), { [node.id]: node })
|
|
1242
|
+
expect(prepared.animations.map((clip) => clip.name)).toEqual([
|
|
1243
|
+
`${node.id}: open`,
|
|
1244
|
+
`${node.id}: shutters`,
|
|
1245
|
+
])
|
|
1246
|
+
expect(prepared.animations[0]!.tracks).toHaveLength(1)
|
|
1247
|
+
const extras = prepared.scene.getObjectByName(node.id)!.userData
|
|
1248
|
+
expect(extras.shutters).toBe(true)
|
|
1249
|
+
expect(extras.shuttersOpen).toBe(shutterOpen)
|
|
1250
|
+
expect(extras.openable).toBe(true)
|
|
1251
|
+
expect(extras.clips).toEqual(prepared.animations.map((clip) => clip.name))
|
|
1252
|
+
const shutters = prepared.animations[1]!
|
|
1253
|
+
expect(shutters.duration).toBe(1)
|
|
1254
|
+
const leaves: THREE.Object3D[] = []
|
|
1255
|
+
prepared.scene.traverse((part) => {
|
|
1256
|
+
if (['shutter-left', 'shutter-right', 'shutter-outer'].includes(part.name))
|
|
1257
|
+
leaves.push(part)
|
|
1258
|
+
})
|
|
1259
|
+
for (const leaf of leaves)
|
|
1260
|
+
expect(shutters.tracks.some((track) => track.name === `${leaf.uuid}.quaternion`)).toBe(true)
|
|
1261
|
+
const baked = prepared.scene.getObjectByName('shutter-left')!
|
|
1262
|
+
const outer = baked.getObjectByName('shutter-outer')!
|
|
1263
|
+
const expectedOuter = buildWindowPreviewMesh(node).getObjectByName('shutter-outer')!
|
|
1264
|
+
expect(outer.quaternion.angleTo(expectedOuter.quaternion)).toBeCloseTo(0)
|
|
1265
|
+
expect(baked.quaternion.angleTo(saved)).toBeCloseTo(0)
|
|
1266
|
+
expect(mesh.getObjectByName('shutter-left')!.quaternion.angleTo(preview)).toBeCloseTo(0)
|
|
1267
|
+
const curtain = prepared.scene.getObjectByName('window-roller-curtain')!
|
|
1268
|
+
expect(curtain.children).toHaveLength(1)
|
|
1269
|
+
expect(curtain.position.y).toBe(node.height / 2)
|
|
1270
|
+
expect(curtain.scale.y).toBeCloseTo(1 - 0.98 * shutterOpen)
|
|
1271
|
+
const panel = curtain.children[0] as THREE.Mesh
|
|
1272
|
+
expect(panel.geometry.index!.count).toBe(Math.floor(node.height / 0.04) * 36)
|
|
1273
|
+
const track = shutters.tracks.find((track) => track.name === `${curtain.uuid}.scale`)!
|
|
1274
|
+
expect(Array.from(track.values)).toEqual([1, 1, 1, 1, expect.closeTo(0.02), 1])
|
|
1275
|
+
expect(extras.rollerShutterAnimation).toBe('top-pivot-scale-y')
|
|
1276
|
+
const mixer = new THREE.AnimationMixer(prepared.scene)
|
|
1277
|
+
const action = mixer.clipAction(shutters).setLoop(THREE.LoopOnce, 1)
|
|
1278
|
+
action.clampWhenFinished = true
|
|
1279
|
+
action.play()
|
|
1280
|
+
for (const fraction of [0, 0.25, 0.5, 0.75, 1]) {
|
|
1281
|
+
mixer.setTime(fraction)
|
|
1282
|
+
const expected = buildWindowPreviewMesh({ ...node, shutterOpen: fraction })
|
|
1283
|
+
expect(
|
|
1284
|
+
baked.quaternion
|
|
1285
|
+
.clone()
|
|
1286
|
+
.normalize()
|
|
1287
|
+
.angleTo(expected.getObjectByName('shutter-left')!.quaternion),
|
|
1288
|
+
).toBeLessThan(1e-6)
|
|
1289
|
+
expect(
|
|
1290
|
+
outer.quaternion
|
|
1291
|
+
.clone()
|
|
1292
|
+
.normalize()
|
|
1293
|
+
.angleTo(expected.getObjectByName('shutter-outer')!.quaternion),
|
|
1294
|
+
).toBeLessThan(1e-6)
|
|
1295
|
+
}
|
|
1296
|
+
mixer.stopAllAction()
|
|
1297
|
+
prepared.dispose()
|
|
1298
|
+
},
|
|
1299
|
+
)
|
|
1300
|
+
|
|
1301
|
+
test('a shuttered casement exports independent sash and leaf rotations at its saved pose', () => {
|
|
1302
|
+
const node = WindowNode.parse({
|
|
1303
|
+
windowType: 'casement',
|
|
1304
|
+
width: 1,
|
|
1305
|
+
shutters: 'panel',
|
|
1306
|
+
shutterOpen: 0.25,
|
|
1307
|
+
rollerShutter: 'box',
|
|
1308
|
+
rollerShutterOpen: 0.75,
|
|
1309
|
+
})
|
|
1310
|
+
const mesh = buildWindowPreviewMesh(node)
|
|
1311
|
+
sceneRegistry.nodes.set(node.id, mesh)
|
|
1312
|
+
const prepared = prepareSceneForExport(new THREE.Group().add(mesh), { [node.id]: node })
|
|
1313
|
+
expect(prepared.animations.map((clip) => clip.name)).toEqual([
|
|
1314
|
+
`${node.id}: open`,
|
|
1315
|
+
`${node.id}: shutters`,
|
|
1316
|
+
])
|
|
1317
|
+
const window = prepared.scene.getObjectByName(node.id)!
|
|
1318
|
+
expect(window.userData.shuttersOpen).toBe(0.25)
|
|
1319
|
+
expect(window.userData.rollerShutterOpen).toBe(0.75)
|
|
1320
|
+
const curtain = window.getObjectByName('window-roller-curtain')!
|
|
1321
|
+
expect(curtain.scale.y).toBeCloseTo(1 - 0.98 * 0.75)
|
|
1322
|
+
const top = new THREE.Box3().setFromObject(curtain).max.y
|
|
1323
|
+
const leaves = ['shutter-left', 'shutter-right'].map((name) => window.getObjectByName(name)!)
|
|
1324
|
+
const clip = prepared.animations[1]!
|
|
1325
|
+
expect(clip.tracks.map((track) => track.name)).toEqual([
|
|
1326
|
+
...leaves.map((leaf) => `${leaf.uuid}.quaternion`),
|
|
1327
|
+
`${curtain.uuid}.scale`,
|
|
1328
|
+
])
|
|
1329
|
+
for (const leaf of leaves)
|
|
1330
|
+
expect(leaf.quaternion.angleTo(mesh.getObjectByName(leaf.name)!.quaternion)).toBeCloseTo(0)
|
|
1331
|
+
const mixer = new THREE.AnimationMixer(prepared.scene)
|
|
1332
|
+
const action = mixer.clipAction(clip).setLoop(THREE.LoopOnce, 1)
|
|
1333
|
+
action.clampWhenFinished = true
|
|
1334
|
+
action.play()
|
|
1335
|
+
for (const fraction of [0, 0.25, 0.5, 0.75, 1]) {
|
|
1336
|
+
mixer.setTime(fraction)
|
|
1337
|
+
const expected = buildWindowPreviewMesh({ ...node, shutterOpen: fraction })
|
|
1338
|
+
for (const leaf of leaves)
|
|
1339
|
+
expect(
|
|
1340
|
+
leaf.quaternion
|
|
1341
|
+
.clone()
|
|
1342
|
+
.normalize()
|
|
1343
|
+
.angleTo(expected.getObjectByName(leaf.name)!.quaternion),
|
|
1344
|
+
).toBeLessThan(1e-6)
|
|
1345
|
+
expect(new THREE.Box3().setFromObject(curtain).max.y).toBeCloseTo(top)
|
|
1346
|
+
}
|
|
1347
|
+
mixer.stopAllAction()
|
|
1348
|
+
prepared.dispose()
|
|
1349
|
+
})
|
|
1350
|
+
|
|
1351
|
+
test.each(['panel', 'roller'] as const)(
|
|
1352
|
+
'a fixed window with %s shutters exports only its shutters clip',
|
|
1353
|
+
(kind) => {
|
|
1354
|
+
const node = WindowNode.parse({
|
|
1355
|
+
windowType: 'fixed',
|
|
1356
|
+
...(kind === 'panel'
|
|
1357
|
+
? { shutters: 'panel', shutterOpen: 0.3 }
|
|
1358
|
+
: { rollerShutter: 'box', rollerShutterOpen: 0.3 }),
|
|
1359
|
+
})
|
|
1360
|
+
const mesh = buildWindowPreviewMesh(node)
|
|
1361
|
+
sceneRegistry.nodes.set(node.id, mesh)
|
|
1362
|
+
const prepared = prepareSceneForExport(new THREE.Group().add(mesh), { [node.id]: node })
|
|
1363
|
+
expect(prepared.animations.map((clip) => clip.name)).toEqual([`${node.id}: shutters`])
|
|
1364
|
+
expect(mesh.userData.openable).toBeUndefined()
|
|
1365
|
+
const extras = prepared.scene.getObjectByName(node.id)!.userData
|
|
1366
|
+
expect(extras.openable).toBeUndefined()
|
|
1367
|
+
expect(extras.shutters).toBe(true)
|
|
1368
|
+
expect(extras.shuttersOpen).toBe(0.3)
|
|
1369
|
+
expect(extras.clips).toEqual([`${node.id}: shutters`])
|
|
1370
|
+
prepared.dispose()
|
|
1371
|
+
},
|
|
1372
|
+
)
|
|
1373
|
+
|
|
1167
1374
|
test('bakes a swing door into an open quaternion clip', () => {
|
|
1168
1375
|
const root = new THREE.Group()
|
|
1169
1376
|
const doorGroup = new THREE.Group()
|
|
@@ -1203,6 +1410,64 @@ describe('prepareSceneForExport', () => {
|
|
|
1203
1410
|
expect(closed.angleTo(new THREE.Quaternion())).toBeCloseTo(0)
|
|
1204
1411
|
})
|
|
1205
1412
|
|
|
1413
|
+
for (const doorType of ['hinged', 'double', 'french'] as const) {
|
|
1414
|
+
test(`bakes ${doorType} hardware with the leaf through closed/45/90° poses`, () => {
|
|
1415
|
+
const node = DoorNode.parse({
|
|
1416
|
+
id: `door_hardware_${doorType}`,
|
|
1417
|
+
doorType,
|
|
1418
|
+
swingAngle: Math.PI / 3,
|
|
1419
|
+
position: [2, 1.05, 3],
|
|
1420
|
+
rotation: [0, 0.4, 0],
|
|
1421
|
+
})
|
|
1422
|
+
const mesh = buildDoorPreviewMesh(node)
|
|
1423
|
+
const root = new THREE.Group()
|
|
1424
|
+
root.add(mesh)
|
|
1425
|
+
sceneRegistry.nodes.set(node.id, mesh)
|
|
1426
|
+
const { scene, animations } = prepareSceneForExport(root, { [node.id]: node })
|
|
1427
|
+
expect(animations).toHaveLength(1)
|
|
1428
|
+
const clip = animations[0]!
|
|
1429
|
+
expect(clip.tracks).toHaveLength(doorType === 'hinged' ? 1 : 2)
|
|
1430
|
+
const mixer = new THREE.AnimationMixer(scene)
|
|
1431
|
+
const action = mixer.clipAction(clip)
|
|
1432
|
+
action.setLoop(THREE.LoopOnce, 1)
|
|
1433
|
+
action.clampWhenFinished = true
|
|
1434
|
+
action.play()
|
|
1435
|
+
for (const amount of [0, 0.5, 1]) {
|
|
1436
|
+
mixer.setTime(amount)
|
|
1437
|
+
scene.updateMatrixWorld(true)
|
|
1438
|
+
const live = buildDoorPreviewMesh({ ...node, swingAngle: (amount * Math.PI) / 2 })
|
|
1439
|
+
live.updateMatrixWorld(true)
|
|
1440
|
+
const leaves = live.children.filter((part) => part.userData.pascalSwingLeaf)
|
|
1441
|
+
for (const [i, track] of clip.tracks.entries()) {
|
|
1442
|
+
const target = scene.getObjectByProperty('uuid', track.name.replace('.quaternion', ''))!
|
|
1443
|
+
const exportedHardware = target.children.filter(
|
|
1444
|
+
(part) => part.userData.slotId === 'hardware',
|
|
1445
|
+
)
|
|
1446
|
+
const liveHardware = leaves[i]!.children.filter(
|
|
1447
|
+
(part) => part.userData.slotId === 'hardware',
|
|
1448
|
+
)
|
|
1449
|
+
expect(exportedHardware).toHaveLength(2)
|
|
1450
|
+
for (const [j, part] of exportedHardware.entries()) {
|
|
1451
|
+
const source = liveHardware[j]!
|
|
1452
|
+
expect(
|
|
1453
|
+
part
|
|
1454
|
+
.getWorldPosition(new THREE.Vector3())
|
|
1455
|
+
.distanceTo(source.getWorldPosition(new THREE.Vector3())),
|
|
1456
|
+
).toBeLessThan(1e-6)
|
|
1457
|
+
expect(
|
|
1458
|
+
part
|
|
1459
|
+
.getWorldQuaternion(new THREE.Quaternion())
|
|
1460
|
+
.angleTo(source.getWorldQuaternion(new THREE.Quaternion())),
|
|
1461
|
+
).toBeLessThan(1e-6)
|
|
1462
|
+
expect((part as THREE.Mesh).geometry).not.toBe((source as THREE.Mesh).geometry)
|
|
1463
|
+
expect((part as THREE.Mesh).geometry.userData.shared).toBeUndefined()
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
mixer.stopAllAction()
|
|
1468
|
+
})
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1206
1471
|
test('bakes registry-owned open clips and stamps the node openable', () => {
|
|
1207
1472
|
const root = new THREE.Group()
|
|
1208
1473
|
const nodeGroup = new THREE.Group()
|
|
@@ -2461,3 +2726,65 @@ test('exports a plugin LOD once without invoking its argument-requiring construc
|
|
|
2461
2726
|
expect(meshes[0]!.geometry).not.toBe(mesh.geometry)
|
|
2462
2727
|
expect(lod.levels).toHaveLength(2)
|
|
2463
2728
|
})
|
|
2729
|
+
|
|
2730
|
+
for (const config of [
|
|
2731
|
+
{ doorType: 'sliding', leafCount: 3, slidingStyle: 'patio', tracks: 1 },
|
|
2732
|
+
{ doorType: 'sliding', leafCount: 4, slidingStyle: 'patio', tracks: 2 },
|
|
2733
|
+
{ doorType: 'sliding', leafCount: 4, slidingStyle: 'bypass', tracks: 2 },
|
|
2734
|
+
{ doorType: 'folding', leafCount: 6, tracks: 6 },
|
|
2735
|
+
{ doorType: 'folding', leafCount: 8, tracks: 8 },
|
|
2736
|
+
]) {
|
|
2737
|
+
test(`GLB samples ${config.slidingStyle ?? ''} ${config.doorType} with ${config.leafCount} panels`, () => {
|
|
2738
|
+
const node = DoorNode.parse({
|
|
2739
|
+
...config,
|
|
2740
|
+
width: 4,
|
|
2741
|
+
slideDirection: 'right',
|
|
2742
|
+
operationState: 0.5,
|
|
2743
|
+
})
|
|
2744
|
+
const mesh = buildDoorPreviewMesh(node)
|
|
2745
|
+
const root = new THREE.Group()
|
|
2746
|
+
root.add(mesh)
|
|
2747
|
+
sceneRegistry.nodes.set(node.id, mesh)
|
|
2748
|
+
try {
|
|
2749
|
+
const { animations } = prepareSceneForExport(root, { [node.id]: node })
|
|
2750
|
+
expect(animations).toHaveLength(1)
|
|
2751
|
+
expect(animations[0]!.tracks).toHaveLength(config.tracks)
|
|
2752
|
+
for (const track of animations[0]!.tracks) {
|
|
2753
|
+
expect(track.times[0]).toBe(0)
|
|
2754
|
+
expect(track.times[8]).toBe(0.5)
|
|
2755
|
+
expect(track.times[16]).toBe(1)
|
|
2756
|
+
}
|
|
2757
|
+
} finally {
|
|
2758
|
+
sceneRegistry.nodes.delete(node.id)
|
|
2759
|
+
}
|
|
2760
|
+
})
|
|
2761
|
+
}
|
|
2762
|
+
|
|
2763
|
+
test('GLB entry-door clip moves its leaf and keeps sidelights and transom static', () => {
|
|
2764
|
+
const node = DoorNode.parse({
|
|
2765
|
+
width: 1,
|
|
2766
|
+
height: 2.1,
|
|
2767
|
+
sidelightLeft: 0.3,
|
|
2768
|
+
sidelightRight: 0.5,
|
|
2769
|
+
transomHeight: 0.4,
|
|
2770
|
+
swingAngle: 0.6,
|
|
2771
|
+
})
|
|
2772
|
+
const mesh = buildDoorPreviewMesh(node)
|
|
2773
|
+
const root = new THREE.Group()
|
|
2774
|
+
root.add(mesh)
|
|
2775
|
+
sceneRegistry.nodes.set(node.id, mesh)
|
|
2776
|
+
try {
|
|
2777
|
+
const { scene, animations } = prepareSceneForExport(root, { [node.id]: node })
|
|
2778
|
+
expect(animations).toHaveLength(1)
|
|
2779
|
+
const fixed = scene.getObjectByName('door-fixed-lights')!
|
|
2780
|
+
expect(fixed).toBeDefined()
|
|
2781
|
+
const staticIds = new Set<string>()
|
|
2782
|
+
fixed.traverse((part) => staticIds.add(part.uuid))
|
|
2783
|
+
expect(animations[0]!.tracks.length).toBeGreaterThan(0)
|
|
2784
|
+
expect(animations[0]!.tracks.some((track) => staticIds.has(track.name.split('.')[0]!))).toBe(
|
|
2785
|
+
false,
|
|
2786
|
+
)
|
|
2787
|
+
} finally {
|
|
2788
|
+
sceneRegistry.nodes.delete(node.id)
|
|
2789
|
+
}
|
|
2790
|
+
})
|
package/src/lib/glb-export.ts
CHANGED
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
isViewerPresentationTextureBorrowed,
|
|
27
27
|
poseDoorMovingParts,
|
|
28
28
|
poseWindowMovingParts,
|
|
29
|
+
restoreWindowRollerShutter,
|
|
29
30
|
SCENE_LAYER,
|
|
30
31
|
snapLevelsToTruePositions,
|
|
31
32
|
type ViewerPresentationContribution,
|
|
@@ -914,6 +915,8 @@ function cloneSceneForExport(
|
|
|
914
915
|
let geometry = cache.geometries.get(renderable.geometry)
|
|
915
916
|
if (!geometry) {
|
|
916
917
|
geometry = renderable.geometry.clone()
|
|
918
|
+
// Export owns its clones even when the live geometry belongs to a shared cache.
|
|
919
|
+
delete geometry.userData.shared
|
|
917
920
|
cache.geometries.set(renderable.geometry, geometry)
|
|
918
921
|
}
|
|
919
922
|
renderableClone.geometry = geometry
|
|
@@ -1710,14 +1713,68 @@ function openClip(id: string, tracks: THREE.KeyframeTrack[]): THREE.AnimationCli
|
|
|
1710
1713
|
return clip
|
|
1711
1714
|
}
|
|
1712
1715
|
|
|
1716
|
+
/** Shutters share a toggle, independently of the sash's open clip. */
|
|
1717
|
+
function bakeWindowClip(
|
|
1718
|
+
id: string,
|
|
1719
|
+
node: WindowNode,
|
|
1720
|
+
windowObject: THREE.Object3D,
|
|
1721
|
+
): THREE.AnimationClip[] {
|
|
1722
|
+
const clips: THREE.AnimationClip[] = []
|
|
1723
|
+
restoreWindowRollerShutter(node, windowObject)
|
|
1724
|
+
const sash = bakeWindowOpenClip(id, node, windowObject)
|
|
1725
|
+
if (sash) clips.push(sash)
|
|
1726
|
+
const hinged = Boolean(node.shutters && node.shutters !== 'none')
|
|
1727
|
+
const roller = node.rollerShutter === 'box' || node.rollerShutter === 'concealed'
|
|
1728
|
+
if (!hinged && !roller) return clips
|
|
1729
|
+
|
|
1730
|
+
const leaves: THREE.Object3D[] = []
|
|
1731
|
+
windowObject.traverse((part) => {
|
|
1732
|
+
if (['shutter-left', 'shutter-right', 'shutter-outer'].includes(part.name)) leaves.push(part)
|
|
1733
|
+
})
|
|
1734
|
+
const rotations = new Map(leaves.map((leaf) => [leaf, [] as number[]]))
|
|
1735
|
+
// A midpoint disambiguates the direction of a half-turn for quaternion interpolation.
|
|
1736
|
+
for (const value of [0, 0.5, 1]) {
|
|
1737
|
+
poseWindowMovingParts(node, windowObject, 0, value)
|
|
1738
|
+
for (const leaf of leaves) rotations.get(leaf)!.push(...leaf.quaternion.toArray())
|
|
1739
|
+
}
|
|
1740
|
+
poseWindowMovingParts(node, windowObject, 0)
|
|
1741
|
+
const tracks: THREE.KeyframeTrack[] = leaves.map(
|
|
1742
|
+
(leaf) =>
|
|
1743
|
+
new THREE.QuaternionKeyframeTrack(
|
|
1744
|
+
`${leaf.uuid}.quaternion`,
|
|
1745
|
+
[0, 0.5, 1],
|
|
1746
|
+
rotations.get(leaf)!,
|
|
1747
|
+
),
|
|
1748
|
+
)
|
|
1749
|
+
if (roller) {
|
|
1750
|
+
restoreWindowRollerShutter({ ...node, rollerShutterOpen: 0 }, windowObject)
|
|
1751
|
+
const curtain = windowObject.getObjectByName('window-roller-curtain')
|
|
1752
|
+
if (curtain) {
|
|
1753
|
+
// Fixed topology: compress the full curtain towards its top, inside the lintel/box.
|
|
1754
|
+
curtain.position.y = node.height / 2
|
|
1755
|
+
for (const child of curtain.children) child.position.y -= node.height / 2
|
|
1756
|
+
curtain.scale.y = 1 - 0.98 * (node.rollerShutterOpen ?? 1)
|
|
1757
|
+
tracks.push(
|
|
1758
|
+
new THREE.VectorKeyframeTrack(`${curtain.uuid}.scale`, [0, 1], [1, 1, 1, 1, 0.02, 1]),
|
|
1759
|
+
)
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
if (tracks.length) {
|
|
1763
|
+
const clip = new THREE.AnimationClip(`${id}: shutters`, 1, tracks)
|
|
1764
|
+
clip.userData = { loop: false }
|
|
1765
|
+
clips.push(clip)
|
|
1766
|
+
}
|
|
1767
|
+
return clips
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1713
1770
|
/**
|
|
1714
1771
|
* Bake a window's open motion generically: snapshot every part's pose closed,
|
|
1715
1772
|
* pose the subtree open, and emit a track for whichever parts actually moved
|
|
1716
1773
|
* (translation for sliding/hung sashes, rotation for casement/awning/louvre).
|
|
1717
1774
|
* Reusing the live `poseWindowMovingParts` keeps one source of truth for window
|
|
1718
|
-
* kinematics. The
|
|
1775
|
+
* kinematics. The sash is left closed; shutters keep their saved rest pose.
|
|
1719
1776
|
*/
|
|
1720
|
-
function
|
|
1777
|
+
function bakeWindowOpenClip(
|
|
1721
1778
|
id: string,
|
|
1722
1779
|
node: WindowNode,
|
|
1723
1780
|
windowObject: THREE.Object3D,
|
|
@@ -1855,6 +1912,17 @@ function stampIdentity(
|
|
|
1855
1912
|
const clipNames = clipNamesByNode.get(id)
|
|
1856
1913
|
if (clipNames?.length) {
|
|
1857
1914
|
if (clipNames.some((name) => name.endsWith(': open'))) extras.openable = true
|
|
1915
|
+
if (node.type === 'window' && clipNames.includes(`${id}: shutters`)) {
|
|
1916
|
+
extras.shutters = true
|
|
1917
|
+
extras.shuttersOpen =
|
|
1918
|
+
node.shutters && node.shutters !== 'none'
|
|
1919
|
+
? (node.shutterOpen ?? 1)
|
|
1920
|
+
: (node.rollerShutterOpen ?? 1)
|
|
1921
|
+
if (node.rollerShutter === 'box' || node.rollerShutter === 'concealed') {
|
|
1922
|
+
extras.rollerShutterOpen = node.rollerShutterOpen ?? 1
|
|
1923
|
+
extras.rollerShutterAnimation = 'top-pivot-scale-y'
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1858
1926
|
extras.clips = clipNames
|
|
1859
1927
|
}
|
|
1860
1928
|
if (node.type === 'zone') {
|
|
@@ -1,6 +1,11 @@
|
|
|
1
|
-
import { expect, test } from 'bun:test'
|
|
2
|
-
import { Group, Vector3 } from 'three'
|
|
3
|
-
import {
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { Group, Quaternion, Vector3 } from 'three'
|
|
3
|
+
import {
|
|
4
|
+
gridLocalNormal,
|
|
5
|
+
gridLocalPoint,
|
|
6
|
+
HORIZONTAL_GRID_QUATERNION,
|
|
7
|
+
layHorizontalGrid,
|
|
8
|
+
} from './grid-frame'
|
|
4
9
|
|
|
5
10
|
test('grid floor and cursor stay in model coordinates after God-view pan, scale and yaw', () => {
|
|
6
11
|
const root = new Group()
|
|
@@ -17,3 +22,51 @@ test('grid floor and cursor stay in model coordinates after God-view pan, scale
|
|
|
17
22
|
).toBeLessThan(1e-8)
|
|
18
23
|
expect(world.distanceTo(local)).toBeGreaterThan(1)
|
|
19
24
|
})
|
|
25
|
+
|
|
26
|
+
// The grid plane's lattice lines run at multiples of the cell size in its own
|
|
27
|
+
// XY, from its origin: where does plane-local (u, v) land?
|
|
28
|
+
function planePoint(position: Vector3, quaternion: Quaternion, u: number, v: number) {
|
|
29
|
+
return new Vector3(u, v, 0).applyQuaternion(quaternion).add(position)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe('layHorizontalGrid', () => {
|
|
33
|
+
test("lays the lattice on a turned, offset building's own frame", () => {
|
|
34
|
+
const yaw = (17 * Math.PI) / 180
|
|
35
|
+
const position = new Vector3()
|
|
36
|
+
const quaternion = new Quaternion()
|
|
37
|
+
layHorizontalGrid({ position: [12.37, 0, -8.91], rotationY: yaw }, 0.301, position, quaternion)
|
|
38
|
+
|
|
39
|
+
expect(position.toArray()).toEqual([12.37, 0.301, -8.91])
|
|
40
|
+
// Flat…
|
|
41
|
+
expect(new Vector3(0, 0, 1).applyQuaternion(quaternion).y).toBeCloseTo(1, 12)
|
|
42
|
+
// …and each lattice point is a building-local lattice point: plane (1, -1.5)
|
|
43
|
+
// is building-local (1, 1.5), wherever the building's yaw puts it.
|
|
44
|
+
const point = planePoint(position, quaternion, 1, -1.5)
|
|
45
|
+
const cos = Math.cos(yaw)
|
|
46
|
+
const sin = Math.sin(yaw)
|
|
47
|
+
expect(point.x).toBeCloseTo(12.37 + cos + 1.5 * sin, 12)
|
|
48
|
+
expect(point.y).toBeCloseTo(0.301, 12)
|
|
49
|
+
expect(point.z).toBeCloseTo(-8.91 - sin + 1.5 * cos, 12)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
test('with no building the world grid stays anchored at the origin, as before', () => {
|
|
53
|
+
const position = new Vector3(5, 5, 5)
|
|
54
|
+
const quaternion = new Quaternion()
|
|
55
|
+
layHorizontalGrid(null, 0.001, position, quaternion)
|
|
56
|
+
|
|
57
|
+
expect(position.toArray()).toEqual([0, 0.001, 0])
|
|
58
|
+
expect(Math.abs(quaternion.dot(HORIZONTAL_GRID_QUATERNION))).toBeCloseTo(1, 12)
|
|
59
|
+
const point = planePoint(position, quaternion, 2, -3)
|
|
60
|
+
expect(point.x).toBeCloseTo(2, 12)
|
|
61
|
+
expect(point.z).toBeCloseTo(3, 12)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
test('an unturned building at the origin draws the same grid as no building', () => {
|
|
65
|
+
const position = new Vector3()
|
|
66
|
+
const quaternion = new Quaternion()
|
|
67
|
+
layHorizontalGrid({ position: [0, 0, 0], rotationY: 0 }, 0.001, position, quaternion)
|
|
68
|
+
|
|
69
|
+
expect(position.toArray()).toEqual([0, 0.001, 0])
|
|
70
|
+
expect(Math.abs(quaternion.dot(HORIZONTAL_GRID_QUATERNION))).toBeCloseTo(1, 12)
|
|
71
|
+
})
|
|
72
|
+
})
|
package/src/lib/grid-frame.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { BuildingPose } from '@pascal-app/core'
|
|
2
|
+
import { type Object3D, Quaternion, Vector3 } from 'three'
|
|
2
3
|
|
|
3
4
|
// Placement surfaces are world-space; the grid lives under the scalable XR
|
|
4
5
|
// scene root. Keep its lattice and reveal in that root's model coordinates.
|
|
@@ -16,3 +17,29 @@ export function gridLocalNormal(parent: Object3D | null, normal: Vector3, target
|
|
|
16
17
|
if (parent) target.transformDirection(parent.matrixWorld.clone().invert())
|
|
17
18
|
return target
|
|
18
19
|
}
|
|
20
|
+
|
|
21
|
+
const UP = new Vector3(0, 1, 0)
|
|
22
|
+
|
|
23
|
+
/** Lays the grid's PlaneGeometry (which faces +Z) flat, its normal → +Y. */
|
|
24
|
+
export const HORIZONTAL_GRID_QUATERNION = new Quaternion().setFromUnitVectors(
|
|
25
|
+
new Vector3(0, 0, 1),
|
|
26
|
+
UP,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Lay the horizontal grid plane on the lattice the active tools snap on: a
|
|
31
|
+
* building's own frame (`pose`, its yaw and offset — the lattice
|
|
32
|
+
* `snapWorldXZForActiveBuilding` snaps to), or with `pose` null the world XZ
|
|
33
|
+
* grid anchored at (0, 0). The lattice lines run at multiples of the cell size
|
|
34
|
+
* from the plane's origin, so a turned house's walls sit square on them. `y`
|
|
35
|
+
* is the plane's height in the grid parent's frame.
|
|
36
|
+
*/
|
|
37
|
+
export function layHorizontalGrid(
|
|
38
|
+
pose: BuildingPose | null,
|
|
39
|
+
y: number,
|
|
40
|
+
position: Vector3,
|
|
41
|
+
quaternion: Quaternion,
|
|
42
|
+
) {
|
|
43
|
+
position.set(pose?.position[0] ?? 0, y, pose?.position[2] ?? 0)
|
|
44
|
+
quaternion.setFromAxisAngle(UP, pose?.rotationY ?? 0).multiply(HORIZONTAL_GRID_QUATERNION)
|
|
45
|
+
}
|
package/src/lib/history.test.ts
CHANGED
|
@@ -488,7 +488,8 @@ describe('standalone history source invalidation', () => {
|
|
|
488
488
|
const effects = []
|
|
489
489
|
mockShared('react', () => ({ ...react, useEffect: effect => effects.push(effect), useRef: current => ({ current }) }))
|
|
490
490
|
const segment = core.StairSegmentNode.parse({ id: 'sseg_history', parentId: 'stair_history', height: 2.5 })
|
|
491
|
-
|
|
491
|
+
// Off the slab: the flight rises from the level base, whatever elects the stair's support.
|
|
492
|
+
const stair = core.StairNode.parse({ id: 'stair_history', parentId: level.id, position: [10, 0, 10], children: [segment.id] })
|
|
492
493
|
scene.setState({ nodes: { ...baseline, [level.id]: { ...level, height: 2.5, children: [stair.id] }, [stair.id]: stair, [segment.id]: segment } })
|
|
493
494
|
const { StairOpeningSystem } = await import(${JSON.stringify(resolve(import.meta.dir, '../../../core/src/systems/stair/stair-opening-system.tsx'))})
|
|
494
495
|
StairOpeningSystem()
|