@pascal-app/editor 1.0.0-beta.5 → 1.0.0
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/LICENSE +21 -0
- package/bunfig.toml +4 -0
- package/package.json +13 -7
- package/scripts/generate-print-golden-house.ts +89 -0
- package/src/components/editor/bake-exporter.tsx +1 -0
- package/src/components/editor/camera-dragging-lifecycle.test.ts +41 -0
- package/src/components/editor/camera-dragging-lifecycle.ts +10 -2
- package/src/components/editor/capture-camera-rig.tsx +59 -0
- package/src/components/editor/custom-camera-controls.tsx +20 -11
- package/src/components/editor/editor-layout-mobile.tsx +6 -6
- package/src/components/editor/editor-layout-v2.tsx +9 -2
- package/src/components/editor/export-manager.tsx +175 -33
- package/src/components/editor/first-person/build-collider-world.test.ts +30 -0
- package/src/components/editor/first-person-controls.tsx +180 -12
- package/src/components/editor/floating-action-menu.tsx +42 -51
- package/src/components/editor/floorplan-panel.tsx +76 -12
- package/src/components/editor/grid.tsx +12 -7
- package/src/components/editor/group-actions.ts +34 -50
- package/src/components/editor/group-move-3d.ts +51 -21
- package/src/components/editor/group-transform-shared.test.ts +78 -0
- package/src/components/editor/group-transform-shared.ts +74 -7
- package/src/components/editor/handles/handle-arrow-raycast.test.ts +86 -0
- package/src/components/editor/handles/handle-arrow.tsx +109 -48
- package/src/components/editor/handles/linear-resize-drag.ts +67 -0
- package/src/components/editor/handles/resize-snap.test.ts +46 -0
- package/src/components/editor/handles/resize-snap.ts +10 -1
- package/src/components/editor/handles/use-handle-drag.ts +18 -1
- package/src/components/editor/index.tsx +140 -21
- package/src/components/editor/node-action-menu.tsx +14 -1
- package/src/components/editor/node-arrow-handles.tsx +47 -58
- package/src/components/editor/selection-manager.tsx +104 -42
- package/src/components/editor/snapshot-capture-overlay.tsx +298 -39
- package/src/components/editor/snapshot-capture.test.ts +422 -0
- package/src/components/editor/snapshot-capture.ts +145 -0
- package/src/components/editor/thumbnail-generator.tsx +284 -237
- package/src/components/editor/use-floorplan-background-placement.ts +53 -5
- package/src/components/editor/wall-move-side-handles.tsx +299 -7
- package/src/components/editor-2d/floorplan-group-move.tsx +67 -33
- package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +3 -1
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +129 -38
- package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +14 -1
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.test.tsx +16 -0
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +2 -0
- package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +52 -3
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +196 -1
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +180 -32
- package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +402 -0
- package/src/components/systems/ceiling/ceiling-bracket-batch.ts +371 -0
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +347 -0
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +281 -305
- package/src/components/systems/roof/roof-edit-system.test.tsx +120 -0
- package/src/components/systems/roof/roof-edit-system.tsx +10 -19
- package/src/components/systems/selection-affordance-manager.tsx +59 -9
- package/src/components/systems/selection-affordance-services.ts +19 -0
- package/src/components/systems/stair/stair-edit-system.tsx +5 -2
- package/src/components/tools/fence/fence-drafting.test.ts +100 -0
- package/src/components/tools/fence/fence-drafting.ts +6 -25
- package/src/components/tools/item/draft-creation.test.ts +12 -0
- package/src/components/tools/item/draft-creation.ts +10 -0
- package/src/components/tools/item/face-host-commit.test.ts +65 -0
- package/src/components/tools/item/face-host-commit.ts +56 -0
- package/src/components/tools/item/face-host-preview.test.ts +77 -0
- package/src/components/tools/item/face-host-preview.ts +82 -0
- package/src/components/tools/item/move-tool.tsx +4 -3
- package/src/components/tools/item/placement-math.test.ts +11 -1
- package/src/components/tools/item/placement-math.ts +0 -20
- package/src/components/tools/item/placement-strategies.test.ts +331 -4
- package/src/components/tools/item/placement-strategies.ts +135 -2
- package/src/components/tools/item/placement-surface.test.ts +61 -0
- package/src/components/tools/item/placement-surface.ts +24 -0
- package/src/components/tools/item/placement-types.ts +6 -0
- package/src/components/tools/item/test-face-host.ts +121 -0
- package/src/components/tools/item/use-draft-node.test.tsx +336 -0
- package/src/components/tools/item/use-draft-node.ts +26 -4
- package/src/components/tools/item/use-placement-coordinator.tsx +428 -153
- package/src/components/tools/registered-tool-install-lifecycle.test.tsx +256 -0
- package/src/components/tools/registry/move-registry-node-tool.test.ts +10 -0
- package/src/components/tools/registry/move-registry-node-tool.tsx +306 -110
- package/src/components/tools/registry-tool-context.tsx +30 -0
- package/src/components/tools/select/box-select-tool.tsx +3 -1
- package/src/components/tools/select/marquee-footprint.test.ts +53 -0
- package/src/components/tools/select/marquee-footprint.ts +29 -0
- package/src/components/tools/shared/placement-box.tsx +57 -7
- package/src/components/tools/shared/placement-dimension-guides.tsx +121 -0
- package/src/components/tools/shared/pointer-support-cap.test.ts +319 -17
- package/src/components/tools/shared/pointer-support-cap.ts +45 -8
- package/src/components/tools/site/terrain-sculpt-grid.tsx +9 -5
- package/src/components/tools/site/terrain-sculpt-tool.tsx +1 -1
- package/src/components/tools/stair/stair-tool.tsx +127 -74
- package/src/components/tools/tool-manager.tsx +25 -8
- package/src/components/tools/wall/wall-drafting.test.ts +320 -2
- package/src/components/tools/wall/wall-drafting.ts +23 -114
- package/src/components/ui/action-menu/control-modes.tsx +9 -6
- package/src/components/ui/action-menu/index.tsx +11 -1
- package/src/components/ui/command-palette/editor-commands.tsx +9 -12
- package/src/components/ui/command-palette/index.tsx +7 -2
- package/src/components/ui/controls/material-paint-panel.tsx +6 -4
- package/src/components/ui/controls/material-picker.tsx +4 -4
- package/src/components/ui/controls/scene-material-list.tsx +6 -5
- package/src/components/ui/controls/segmented-control.tsx +8 -1
- package/src/components/ui/controls/slider-control.tsx +19 -3
- package/src/components/ui/controls/toggle-control.tsx +18 -5
- package/src/components/ui/controls/tool-options-panel.tsx +126 -0
- package/src/components/ui/floating-level-selector.tsx +22 -5
- package/src/components/ui/helpers/contextual-helper-panel.tsx +9 -0
- package/src/components/ui/helpers/helper-manager.tsx +47 -11
- package/src/components/ui/helpers/item-helper.tsx +1 -1
- package/src/components/ui/helpers/registered-tool-helper.tsx +21 -0
- package/src/components/ui/panels/homogeneous-selection.test.ts +80 -0
- package/src/components/ui/panels/homogeneous-selection.ts +47 -0
- package/src/components/ui/panels/mobile-selection-bar.tsx +11 -5
- package/src/components/ui/panels/multi-field-value.test.ts +209 -0
- package/src/components/ui/panels/multi-field-value.ts +163 -0
- package/src/components/ui/panels/multi-height-mode.tsx +185 -0
- package/src/components/ui/panels/multi-parametric-inspector.tsx +263 -0
- package/src/components/ui/panels/multi-selection-panel.tsx +48 -37
- package/src/components/ui/panels/node-display.ts +4 -0
- package/src/components/ui/panels/panel-manager.tsx +77 -3
- package/src/components/ui/panels/parametric-field-control.tsx +173 -0
- package/src/components/ui/panels/parametric-field-utils.ts +18 -0
- package/src/components/ui/panels/parametric-inspector.tsx +29 -162
- package/src/components/ui/panels/reference-panel.tsx +31 -1
- package/src/components/ui/primitives/shortcut-token.tsx +23 -1
- package/src/components/ui/scene-loader.tsx +35 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +390 -36
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +20 -23
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.test.ts +140 -0
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +109 -0
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +19 -5
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +116 -46
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +5 -2
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +28 -11
- package/src/components/ui/sidebar/panels/site-panel/tree-node.test.ts +8 -0
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +14 -6
- package/src/components/ui/sidebar/tab-bar.tsx +2 -0
- package/src/components/ui/sidebar/use-plugin-panels.tsx +17 -2
- package/src/components/viewer/viewer-scene-header.tsx +2 -1
- package/src/components/viewer/viewer-stage.tsx +4 -1
- package/src/components/viewer-overlay.tsx +1 -22
- package/src/components/viewer-zone-system.tsx +6 -2
- package/src/components/walkthrough-hud.tsx +19 -10
- package/src/hooks/use-auto-save.test.ts +73 -1
- package/src/hooks/use-auto-save.ts +76 -5
- package/src/hooks/use-ceiling-events.test.ts +122 -0
- package/src/hooks/use-drag-action.ts +17 -3
- package/src/hooks/use-grid-events.test.ts +240 -0
- package/src/hooks/use-grid-events.ts +190 -11
- package/src/hooks/use-keyboard.test.ts +244 -0
- package/src/hooks/use-keyboard.ts +129 -62
- package/src/hooks/use-registered-tool-enabled.ts +20 -0
- package/src/hooks/use-save-shortcut.ts +33 -0
- package/src/index.tsx +82 -4
- package/src/lib/active-placement-surface.test.ts +16 -0
- package/src/lib/active-placement-surface.ts +14 -0
- package/src/lib/ceiling-surface-raycast.test.ts +64 -0
- package/src/lib/ceiling-surface-raycast.ts +12 -0
- package/src/lib/contextual-help-extension.ts +17 -0
- package/src/lib/continuation.test.ts +11 -0
- package/src/lib/continuation.ts +8 -1
- package/src/lib/direct-manipulation.test.ts +132 -0
- package/src/lib/direct-manipulation.ts +38 -1
- package/src/lib/editor-api.ts +6 -3
- package/src/lib/elevation-guides.test.ts +28 -1
- package/src/lib/elevation-guides.ts +22 -7
- package/src/lib/floating-menu-scale.test.ts +29 -0
- package/src/lib/floating-menu-scale.ts +14 -0
- package/src/lib/floorplan/apply-alignment.test.ts +47 -2
- package/src/lib/floorplan/apply-alignment.ts +10 -1
- package/src/lib/floorplan/floorplan-export.test.ts +397 -1
- package/src/lib/floorplan/floorplan-export.tsx +111 -16
- package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +26 -0
- package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +32 -24
- package/src/lib/floorplan/floorplan-readonly.ts +1 -0
- package/src/lib/floorplan-grid-event-point.test.ts +29 -0
- package/src/lib/floorplan-grid-event-point.ts +13 -0
- package/src/lib/glb-export.test.ts +1169 -21
- package/src/lib/glb-export.ts +629 -101
- package/src/lib/grid-event-presentation.ts +16 -0
- package/src/lib/history.test.ts +528 -147
- package/src/lib/history.ts +81 -7
- package/src/lib/host-tree-children.test.ts +27 -0
- package/src/lib/host-tree-children.ts +79 -0
- package/src/lib/interaction/hot-set.test.ts +10 -1
- package/src/lib/interaction/hot-set.ts +3 -1
- package/src/lib/interaction/overlay-policy.test.ts +33 -1
- package/src/lib/interaction/overlay-policy.ts +16 -0
- package/src/lib/interaction/registered-drafting.ts +36 -0
- package/src/lib/interaction/scope.ts +31 -0
- package/src/lib/level-print-export.test.ts +484 -0
- package/src/lib/level-print-export.ts +526 -0
- package/src/lib/local-project-presentation-persistence.test.ts +302 -0
- package/src/lib/local-project-presentation-persistence.ts +335 -0
- package/src/lib/material-paint.ts +2 -0
- package/src/lib/model-export.ts +29 -0
- package/src/lib/paint-preview-owner.ts +71 -0
- package/src/lib/planar-cursor-placement.test.ts +178 -1
- package/src/lib/planar-cursor-placement.ts +61 -5
- package/src/lib/plugin-panels.test.ts +92 -1
- package/src/lib/plugin-panels.ts +48 -0
- package/src/lib/portable-export.test.ts +366 -0
- package/src/lib/portable-export.ts +969 -0
- package/src/lib/print-3mf.test.ts +86 -0
- package/src/lib/print-3mf.ts +161 -0
- package/src/lib/print-content-scope.test.ts +107 -0
- package/src/lib/print-content-scope.ts +90 -0
- package/src/lib/print-export.test.ts +294 -0
- package/src/lib/print-export.ts +735 -0
- package/src/lib/print-feature-thickness.test.ts +147 -0
- package/src/lib/print-feature-thickness.ts +180 -0
- package/src/lib/print-golden-house.test-fixture.ts +352 -0
- package/src/lib/print-golden-house.test.ts +249 -0
- package/src/lib/print-roof-solids.test.ts +133 -0
- package/src/lib/print-roof-solids.ts +630 -0
- package/src/lib/print-shell-compiler-baseline.test.ts +419 -0
- package/src/lib/print-shell-compiler-baseline.ts +225 -0
- package/src/lib/print-shell-compiler-manifold-core.ts +330 -0
- package/src/lib/print-shell-compiler-manifold-worker.ts +187 -0
- package/src/lib/print-shell-compiler-manifold.worker.ts +20 -0
- package/src/lib/print-shell-compiler-mesh-data.ts +32 -0
- package/src/lib/print-shell-compiler-protocol.ts +30 -0
- package/src/lib/print-shell-compiler.ts +273 -0
- package/src/lib/print-wall-solids.test.ts +194 -0
- package/src/lib/print-wall-solids.ts +328 -0
- package/src/lib/rigid-plan-svg-transform.test.ts +35 -0
- package/src/lib/rigid-plan-svg-transform.ts +24 -0
- package/src/lib/scene.ts +22 -8
- package/src/lib/selection-routing.test.ts +105 -1
- package/src/lib/selection-routing.ts +19 -0
- package/src/lib/sfx-bus.ts +4 -0
- package/src/lib/sfx-player.ts +8 -0
- package/src/lib/snapping-mode.test.ts +32 -4
- package/src/lib/snapping-mode.ts +11 -7
- package/src/lib/usdz-export.ts +38 -0
- package/src/lib/walkthrough-pointer-lock.ts +47 -0
- package/src/store/live-draft-preview-stores.test.ts +3 -0
- package/src/store/terrain-sculpt-mode.test.ts +3 -6
- package/src/store/tool-mode.test.ts +87 -0
- package/src/store/use-camera-hint-focus.ts +48 -0
- package/src/store/use-editor.tsx +299 -188
- package/src/store/use-floorplan-draft-preview.ts +9 -0
- package/src/store/use-interaction-scope.test.ts +23 -1
- package/src/store/use-interaction-scope.ts +63 -2
- package/src/store/use-placement-preview.ts +56 -3
- package/src/components/tools/roof/roof-tool.tsx +0 -656
- package/src/components/ui/helpers/roof-helper.tsx +0 -14
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
type RoofNode,
|
|
34
34
|
type RoofSegmentNode,
|
|
35
35
|
resolveSlabPlacementElevation,
|
|
36
|
+
resolveTerrainWallConstructionOptions,
|
|
36
37
|
type SiteNode,
|
|
37
38
|
type SlabNode,
|
|
38
39
|
SlabNode as SlabNodeSchema,
|
|
@@ -88,6 +89,8 @@ import {
|
|
|
88
89
|
type FloorplanNodeTransform as SharedFloorplanNodeTransform,
|
|
89
90
|
worldToFloorplanLocalPoint,
|
|
90
91
|
} from '../../lib/floorplan'
|
|
92
|
+
import { resolveGenericFloorplanGridEventPoint } from '../../lib/floorplan-grid-event-point'
|
|
93
|
+
import type { EditorGridEvent } from '../../lib/grid-event-presentation'
|
|
91
94
|
import { groundHeightAt } from '../../lib/ground-surface'
|
|
92
95
|
import { guideEmitter } from '../../lib/guide-events'
|
|
93
96
|
import { measurementHint, parseMeasurement } from '../../lib/measurement-parser'
|
|
@@ -148,6 +151,7 @@ import {
|
|
|
148
151
|
isBoxSelectPointerSuppressed,
|
|
149
152
|
markBoxSelectHandled,
|
|
150
153
|
} from '../tools/select/box-select-state'
|
|
154
|
+
import { marqueePolygon } from '../tools/select/marquee-footprint'
|
|
151
155
|
import {
|
|
152
156
|
type Point2 as MarqueePoint2,
|
|
153
157
|
polygonsIntersect as marqueePolygonsIntersect,
|
|
@@ -184,7 +188,6 @@ import {
|
|
|
184
188
|
chainEndJoinsExistingWall,
|
|
185
189
|
createWallOnCurrentLevel,
|
|
186
190
|
isSegmentLongEnough,
|
|
187
|
-
resolveTerrainWallConstructionOptions,
|
|
188
191
|
snapWallDraftPoint,
|
|
189
192
|
snapWallDraftPointDetailed,
|
|
190
193
|
snapPointToGrid as snapWallPointToGrid,
|
|
@@ -864,7 +867,8 @@ function collectFloorplanScreenSelectionIds(rect: ScreenRect, svg: SVGSVGElement
|
|
|
864
867
|
| { start?: unknown; end?: unknown; polygon?: unknown }
|
|
865
868
|
| undefined
|
|
866
869
|
if (!node) continue
|
|
867
|
-
const { start, end
|
|
870
|
+
const { start, end } = node
|
|
871
|
+
const polygon = marqueePolygon(node)
|
|
868
872
|
if (isMarqueeVec2(start) && isMarqueeVec2(end)) {
|
|
869
873
|
dataTested.add(id)
|
|
870
874
|
if (marqueeSegmentIntersectsPolygon(start, end, planQuad)) hitIdsFromData.add(id)
|
|
@@ -4673,6 +4677,7 @@ function FloorplanLinearDraftLayer({
|
|
|
4673
4677
|
const wallDraftEnd = useFloorplanDraftPreview((s) => s.wallDraftEnd)
|
|
4674
4678
|
const fenceDraftEnd = useFloorplanDraftPreview((s) => s.fenceDraftEnd)
|
|
4675
4679
|
const roofDraftEnd = useFloorplanDraftPreview((s) => s.roofDraftEnd)
|
|
4680
|
+
const roofDraftQuarterTurn = useFloorplanDraftPreview((s) => s.roofDraftQuarterTurn)
|
|
4676
4681
|
|
|
4677
4682
|
const draftPolygon = useMemo(() => {
|
|
4678
4683
|
if (
|
|
@@ -4709,6 +4714,21 @@ function FloorplanLinearDraftLayer({
|
|
|
4709
4714
|
return draftPolygon ? formatPolygonPoints(draftPolygon) : null
|
|
4710
4715
|
}, [draftPolygon, isRoofBuildActive, roofDraftEnd, roofDraftStart])
|
|
4711
4716
|
|
|
4717
|
+
const roofDraftDirectionLine = useMemo(() => {
|
|
4718
|
+
if (!(isRoofBuildActive && roofDraftStart && roofDraftEnd)) return null
|
|
4719
|
+
const minX = Math.min(roofDraftStart[0], roofDraftEnd[0])
|
|
4720
|
+
const maxX = Math.max(roofDraftStart[0], roofDraftEnd[0])
|
|
4721
|
+
const minY = Math.min(roofDraftStart[1], roofDraftEnd[1])
|
|
4722
|
+
const maxY = Math.max(roofDraftStart[1], roofDraftEnd[1])
|
|
4723
|
+
if (maxX - minX < 1e-6 || maxY - minY < 1e-6) return null
|
|
4724
|
+
|
|
4725
|
+
const centerX = (minX + maxX) / 2
|
|
4726
|
+
const centerY = (minY + maxY) / 2
|
|
4727
|
+
return roofDraftQuarterTurn
|
|
4728
|
+
? { x1: centerX, y1: minY, x2: centerX, y2: maxY }
|
|
4729
|
+
: { x1: minX, y1: centerY, x2: maxX, y2: centerY }
|
|
4730
|
+
}, [isRoofBuildActive, roofDraftEnd, roofDraftQuarterTurn, roofDraftStart])
|
|
4731
|
+
|
|
4712
4732
|
const fenceDraftSegment = useMemo(() => {
|
|
4713
4733
|
if (!(isFenceBuildActive && fenceDraftStart && fenceDraftEnd)) {
|
|
4714
4734
|
return null
|
|
@@ -4887,6 +4907,19 @@ function FloorplanLinearDraftLayer({
|
|
|
4887
4907
|
unitsPerPixel={unitsPerPixel}
|
|
4888
4908
|
/>
|
|
4889
4909
|
|
|
4910
|
+
{roofDraftDirectionLine && (
|
|
4911
|
+
<line
|
|
4912
|
+
pointerEvents="none"
|
|
4913
|
+
stroke={draftStroke}
|
|
4914
|
+
strokeLinecap="round"
|
|
4915
|
+
strokeWidth={unitsPerPixel * 1.5}
|
|
4916
|
+
x1={toSvgX(roofDraftDirectionLine.x1)}
|
|
4917
|
+
x2={toSvgX(roofDraftDirectionLine.x2)}
|
|
4918
|
+
y1={toSvgY(roofDraftDirectionLine.y1)}
|
|
4919
|
+
y2={toSvgY(roofDraftDirectionLine.y2)}
|
|
4920
|
+
/>
|
|
4921
|
+
)}
|
|
4922
|
+
|
|
4890
4923
|
{draftWallMeasurement && (
|
|
4891
4924
|
<FloorplanDraftWallMeasurement
|
|
4892
4925
|
labelBackground={isDark ? '#0f172a' : '#ffffff'}
|
|
@@ -5063,6 +5096,9 @@ export function FloorplanPanel({
|
|
|
5063
5096
|
if (building?.type !== 'building') return false
|
|
5064
5097
|
return building.children.some((cid) => state.nodes[cid]?.type === 'level')
|
|
5065
5098
|
})
|
|
5099
|
+
// The studio workspace (renders / materials / item builder) is a clean
|
|
5100
|
+
// stage — editor viewport chrome, compass included, stays out of it.
|
|
5101
|
+
const isStudioWorkspace = useEditor((s) => s.workspaceMode === 'studio')
|
|
5066
5102
|
const elevators = useScene(
|
|
5067
5103
|
useShallow((state) => {
|
|
5068
5104
|
const building = currentBuildingId ? state.nodes[currentBuildingId] : null
|
|
@@ -8931,12 +8967,29 @@ export function FloorplanPanel({
|
|
|
8931
8967
|
const groundY = groundHeightAt(worldX, worldZ, floorplanGridWorldY)
|
|
8932
8968
|
const worldY = groundY ?? floorplanGridWorldY
|
|
8933
8969
|
const localY = groundY === null ? floorplanGridLocalY : groundY - buildingPosition[1]
|
|
8970
|
+
const planScene =
|
|
8971
|
+
nativeEvent.currentTarget.querySelector<SVGGraphicsElement>('[data-floorplan-scene]')
|
|
8972
|
+
const screenMatrix = planScene?.getScreenCTM()
|
|
8934
8973
|
|
|
8935
|
-
|
|
8974
|
+
const gridEvent: EditorGridEvent = {
|
|
8936
8975
|
nativeEvent: nativeEvent.nativeEvent as any,
|
|
8937
8976
|
position: [worldX, worldY, worldZ],
|
|
8938
8977
|
localPosition: [planPoint[0], localY, planPoint[1]],
|
|
8939
|
-
|
|
8978
|
+
screenProjection: screenMatrix
|
|
8979
|
+
? {
|
|
8980
|
+
pointer: [nativeEvent.clientX, nativeEvent.clientY],
|
|
8981
|
+
localToScreen: [
|
|
8982
|
+
screenMatrix.a,
|
|
8983
|
+
screenMatrix.b,
|
|
8984
|
+
screenMatrix.c,
|
|
8985
|
+
screenMatrix.d,
|
|
8986
|
+
screenMatrix.e,
|
|
8987
|
+
screenMatrix.f,
|
|
8988
|
+
],
|
|
8989
|
+
}
|
|
8990
|
+
: undefined,
|
|
8991
|
+
}
|
|
8992
|
+
emitter.emit(`grid:${eventType}` as any, gridEvent)
|
|
8940
8993
|
},
|
|
8941
8994
|
[buildingPosition, buildingRotationY, floorplanGridLocalY, floorplanGridWorldY],
|
|
8942
8995
|
)
|
|
@@ -9400,10 +9453,14 @@ export function FloorplanPanel({
|
|
|
9400
9453
|
// this exclusion the catch-all would emit `grid:move` and re-drive the
|
|
9401
9454
|
// 3D MoveDoorTool's free-follow, fighting the overlay again.
|
|
9402
9455
|
if (!isWallBuildActive && !isOpeningMoveActive && isFloorplanGridInteractionActive) {
|
|
9403
|
-
const
|
|
9404
|
-
|
|
9456
|
+
const eventPoint = resolveGenericFloorplanGridEventPoint({
|
|
9457
|
+
point: planPoint,
|
|
9458
|
+
registryToolOwnsSnapping: isRegistryToolBuildActive,
|
|
9459
|
+
snap: getSnappedFloorplanPoint,
|
|
9460
|
+
})
|
|
9461
|
+
emitFloorplanGridEvent('move', eventPoint, event)
|
|
9405
9462
|
setCursorPoint((previousPoint) =>
|
|
9406
|
-
previousPoint && pointsEqual(previousPoint,
|
|
9463
|
+
previousPoint && pointsEqual(previousPoint, eventPoint) ? previousPoint : eventPoint,
|
|
9407
9464
|
)
|
|
9408
9465
|
return
|
|
9409
9466
|
}
|
|
@@ -9503,6 +9560,7 @@ export function FloorplanPanel({
|
|
|
9503
9560
|
// stale closure and float a door symbol while the window tool is armed.
|
|
9504
9561
|
showOpeningGhost,
|
|
9505
9562
|
isPolygonBuildActive,
|
|
9563
|
+
isRegistryToolBuildActive,
|
|
9506
9564
|
isRoofBuildActive,
|
|
9507
9565
|
isWallBuildActive,
|
|
9508
9566
|
levelId,
|
|
@@ -9849,6 +9907,7 @@ export function FloorplanPanel({
|
|
|
9849
9907
|
isOpeningPlacementActive: isOpeningBuildActive && !isOpeningMoveActive,
|
|
9850
9908
|
isPolygonBuildActive,
|
|
9851
9909
|
isRoofBuildActive,
|
|
9910
|
+
registryToolOwnsSnapping: isRegistryToolBuildActive,
|
|
9852
9911
|
isWallBuildActive,
|
|
9853
9912
|
isZoneBuildActive,
|
|
9854
9913
|
levelId,
|
|
@@ -10512,9 +10571,11 @@ export function FloorplanPanel({
|
|
|
10512
10571
|
}
|
|
10513
10572
|
|
|
10514
10573
|
if (useEditor.getState().phase !== 'site') {
|
|
10515
|
-
useEditor.
|
|
10574
|
+
useEditor.getState().setPhase('site')
|
|
10575
|
+
useEditor.getState().armToolMode({ mode: 'select' })
|
|
10576
|
+
} else {
|
|
10577
|
+
selectSiteFloorplanContext()
|
|
10516
10578
|
}
|
|
10517
|
-
selectSiteFloorplanContext()
|
|
10518
10579
|
|
|
10519
10580
|
const nextDraft = {
|
|
10520
10581
|
siteId,
|
|
@@ -10596,9 +10657,11 @@ export function FloorplanPanel({
|
|
|
10596
10657
|
]
|
|
10597
10658
|
|
|
10598
10659
|
if (useEditor.getState().phase !== 'site') {
|
|
10599
|
-
useEditor.
|
|
10660
|
+
useEditor.getState().setPhase('site')
|
|
10661
|
+
useEditor.getState().armToolMode({ mode: 'select' })
|
|
10662
|
+
} else {
|
|
10663
|
+
selectSiteFloorplanContext()
|
|
10600
10664
|
}
|
|
10601
|
-
selectSiteFloorplanContext()
|
|
10602
10665
|
|
|
10603
10666
|
const nextDraft = {
|
|
10604
10667
|
siteId,
|
|
@@ -11107,7 +11170,8 @@ export function FloorplanPanel({
|
|
|
11107
11170
|
<FloorplanRegistryActionMenu />
|
|
11108
11171
|
<FloorplanGroupActionMenu />
|
|
11109
11172
|
|
|
11110
|
-
{
|
|
11173
|
+
{!isStudioWorkspace &&
|
|
11174
|
+
(levelNode?.type === 'level' || hasAmbientBuildingLevel) &&
|
|
11111
11175
|
(compassHost ? (
|
|
11112
11176
|
createPortal(
|
|
11113
11177
|
<FloorplanCompassButton
|
|
@@ -9,7 +9,7 @@ import { color, float, fract, fwidth, mix, positionLocal, uniform } from 'three/
|
|
|
9
9
|
import { MeshBasicNodeMaterial } from 'three/webgpu'
|
|
10
10
|
import { useCeilingEvents } from '../../hooks/use-ceiling-events'
|
|
11
11
|
import { useGridEvents } from '../../hooks/use-grid-events'
|
|
12
|
-
import { getPlacementSurface } from '../../lib/active-placement-surface'
|
|
12
|
+
import { getPlacementSurface, usesOrientedPlacementPlane } from '../../lib/active-placement-surface'
|
|
13
13
|
import useEditor, { isGridSnapActive } from '../../store/use-editor'
|
|
14
14
|
import { getMovingNode } from '../../store/use-interaction-scope'
|
|
15
15
|
|
|
@@ -229,16 +229,21 @@ export const Grid = ({
|
|
|
229
229
|
}
|
|
230
230
|
|
|
231
231
|
const gridMesh = gridRef.current
|
|
232
|
-
const
|
|
233
|
-
if (
|
|
234
|
-
//
|
|
232
|
+
const onOrientedPlane = surfacePoint != null && usesOrientedPlacementPlane(surfaceNormal)
|
|
233
|
+
if (onOrientedPlane && surfacePoint) {
|
|
234
|
+
// Surface-anchored lattice: orient the plane into the host and pin the mesh to
|
|
235
235
|
// the plane's FOOT (the point on the wall plane closest to the world origin)
|
|
236
236
|
// — never the moving ghost. Sliding the opening along the wall then only
|
|
237
237
|
// moves the reveal patch (the cursor uniform); the snap lattice stays put.
|
|
238
238
|
// (Copying `surfacePoint` here made the grid follow the item — useless.)
|
|
239
239
|
gridMesh.quaternion.setFromUnitVectors(PLANE_LOCAL_NORMAL, surfaceNormal)
|
|
240
240
|
const planeOffset = surfacePoint.dot(surfaceNormal)
|
|
241
|
-
|
|
241
|
+
const latticeAnchor = published?.anchor ?? surfacePoint
|
|
242
|
+
gridMesh.position.copy(latticeAnchor)
|
|
243
|
+
gridMesh.position.addScaledVector(
|
|
244
|
+
surfaceNormal,
|
|
245
|
+
-latticeAnchor.dot(surfaceNormal) + planeOffset,
|
|
246
|
+
)
|
|
242
247
|
// Cursor → plane-local XY: rotate (ghost − anchor) by the inverse plane
|
|
243
248
|
// orientation. Both lie in the plane, so the resulting local Z is ~0.
|
|
244
249
|
invQuatRef.current.copy(gridMesh.quaternion).invert()
|
|
@@ -275,8 +280,8 @@ export const Grid = ({
|
|
|
275
280
|
// Floor grid depth-tests against the scene (ground occludes a sub-floor
|
|
276
281
|
// lattice); the wall grid ignores depth so it stays visible through the wall
|
|
277
282
|
// when the opening is being handled from the opposite side.
|
|
278
|
-
if (material.depthTest ===
|
|
279
|
-
material.depthTest = !
|
|
283
|
+
if (material.depthTest === onOrientedPlane) {
|
|
284
|
+
material.depthTest = !onOrientedPlane
|
|
280
285
|
material.needsUpdate = true
|
|
281
286
|
}
|
|
282
287
|
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
useLiveTransforms,
|
|
15
15
|
useScene,
|
|
16
16
|
} from '@pascal-app/core'
|
|
17
|
-
import { useViewer } from '@pascal-app/viewer'
|
|
17
|
+
import { markPerfAction, useViewer } from '@pascal-app/viewer'
|
|
18
18
|
import { Plane, Vector2, Vector3 } from 'three'
|
|
19
19
|
import { GROUP_MOVE_DRAG_LABEL } from '../../lib/contextual-help'
|
|
20
20
|
import { clientToPlan } from '../../lib/floorplan/plan-coords'
|
|
@@ -40,9 +40,11 @@ import {
|
|
|
40
40
|
collectParticipants,
|
|
41
41
|
computeGroupBox,
|
|
42
42
|
expandToComponent,
|
|
43
|
+
groupPlanBounds,
|
|
43
44
|
levelFrame,
|
|
44
|
-
|
|
45
|
+
planBoundsCenter,
|
|
45
46
|
rotateGroupSnapshots,
|
|
47
|
+
rotatePlanBounds,
|
|
46
48
|
translateGroupPatches,
|
|
47
49
|
type Vec2,
|
|
48
50
|
} from './group-transform-shared'
|
|
@@ -104,46 +106,15 @@ export function startGroupPickUp(
|
|
|
104
106
|
if (starts.length === 0) return false
|
|
105
107
|
const affectedIds: AnyNodeId[] = [...starts.map((s) => s.id), ...links.map((l) => l.id)]
|
|
106
108
|
|
|
107
|
-
// Rest bounds in the level frame. Prefer the mounted meshes' world box
|
|
108
|
-
// (footprint-accurate), but fall back to the participant DATA when the
|
|
109
|
-
// meshes aren't up yet — Duplicate starts the pick-up synchronously after
|
|
110
|
-
// `createNodes`, one frame before the clones' renderers mount.
|
|
111
109
|
const { inverse: frameInv } = levelFrame(levelId)
|
|
112
110
|
const restBox = computeGroupBox(fullIds)
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
let
|
|
117
|
-
|
|
118
|
-
const boxMin = restBox.min.clone().applyMatrix4(frameInv)
|
|
119
|
-
const boxMax = restBox.max.clone().applyMatrix4(frameInv)
|
|
120
|
-
minX = Math.min(boxMin.x, boxMax.x)
|
|
121
|
-
minZ = Math.min(boxMin.z, boxMax.z)
|
|
122
|
-
maxX = Math.max(boxMin.x, boxMax.x)
|
|
123
|
-
maxZ = Math.max(boxMin.z, boxMax.z)
|
|
124
|
-
} else {
|
|
125
|
-
const reach = (x: number, z: number) => {
|
|
126
|
-
minX = Math.min(minX, x)
|
|
127
|
-
minZ = Math.min(minZ, z)
|
|
128
|
-
maxX = Math.max(maxX, x)
|
|
129
|
-
maxZ = Math.max(maxZ, z)
|
|
130
|
-
}
|
|
131
|
-
for (const s of starts) {
|
|
132
|
-
if (s.kind === 'endpoint') {
|
|
133
|
-
reach(s.start[0], s.start[1])
|
|
134
|
-
reach(s.end[0], s.end[1])
|
|
135
|
-
} else if (s.kind === 'polygon') {
|
|
136
|
-
for (const [x, z] of s.polygon) {
|
|
137
|
-
reach(x, z)
|
|
138
|
-
}
|
|
139
|
-
} else {
|
|
140
|
-
reach(s.position[0], s.position[2])
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
if (!Number.isFinite(minX)) return false
|
|
111
|
+
const startBounds = groupPlanBounds(restBox, starts, frameInv)
|
|
112
|
+
if (!startBounds) return false
|
|
113
|
+
// Mutable: mid-carry R/T re-seeds the footprint around the same pivot.
|
|
114
|
+
let restBounds = startBounds
|
|
115
|
+
let carriedRotation = 0
|
|
145
116
|
// Rotation pivot for mid-carry R/T; stable across the whole pick-up.
|
|
146
|
-
const restCenter
|
|
117
|
+
const restCenter = planBoundsCenter(restBounds)
|
|
147
118
|
// Ground plane for the 3D surface: the meshes' base when available, floor
|
|
148
119
|
// level otherwise. Placements live in the level frame, so both surfaces
|
|
149
120
|
// resolve into it before measuring.
|
|
@@ -157,7 +128,13 @@ export function startGroupPickUp(
|
|
|
157
128
|
if (n && !movingIdSet.has(nid)) staticNodes[nid] = n
|
|
158
129
|
}
|
|
159
130
|
const candidates = collectAlignmentAnchors(staticNodes, '', levelId)
|
|
160
|
-
let restAnchors = bboxCornerAnchors(
|
|
131
|
+
let restAnchors = bboxCornerAnchors(
|
|
132
|
+
'group-move',
|
|
133
|
+
restBounds.minX,
|
|
134
|
+
restBounds.minZ,
|
|
135
|
+
restBounds.maxX,
|
|
136
|
+
restBounds.maxZ,
|
|
137
|
+
)
|
|
161
138
|
|
|
162
139
|
// Cursor → level-frame plan point, whichever surface the pointer is over.
|
|
163
140
|
const ndc = new Vector2()
|
|
@@ -274,18 +251,20 @@ export function startGroupPickUp(
|
|
|
274
251
|
// the current delta — the carried group turns exactly like the idle
|
|
275
252
|
// keyboard rotate, and the placement stays a single updateNodes.
|
|
276
253
|
const rotateCarried = (direction: 1 | -1) => {
|
|
277
|
-
const
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
{ x: restCenter[0], z: restCenter[1] },
|
|
281
|
-
-direction * (Math.PI / 4),
|
|
282
|
-
)
|
|
254
|
+
const pivot = { x: restCenter[0], z: restCenter[1] }
|
|
255
|
+
const delta = -direction * (Math.PI / 4)
|
|
256
|
+
const rotated = rotateGroupSnapshots(starts, links, pivot, delta)
|
|
283
257
|
starts = rotated.starts
|
|
284
258
|
links = rotated.links
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
259
|
+
carriedRotation += delta
|
|
260
|
+
restBounds = rotatePlanBounds(startBounds, pivot, carriedRotation)
|
|
261
|
+
restAnchors = bboxCornerAnchors(
|
|
262
|
+
'group-move',
|
|
263
|
+
restBounds.minX,
|
|
264
|
+
restBounds.minZ,
|
|
265
|
+
restBounds.maxX,
|
|
266
|
+
restBounds.maxZ,
|
|
267
|
+
)
|
|
289
268
|
sfxEmitter.emit('sfx:item-rotate')
|
|
290
269
|
applyDelta(lastDelta?.[0] ?? 0, lastDelta?.[1] ?? 0)
|
|
291
270
|
}
|
|
@@ -575,6 +554,11 @@ export function deleteSelection(): boolean {
|
|
|
575
554
|
if (selectedIds.length === 0) return false
|
|
576
555
|
|
|
577
556
|
const commitDelete = () => {
|
|
557
|
+
const detail =
|
|
558
|
+
selectedIds.length === 1
|
|
559
|
+
? (useScene.getState().nodes[selectedIds[0]!]?.type ?? selectedIds[0]!)
|
|
560
|
+
: String(selectedIds.length)
|
|
561
|
+
markPerfAction('delete', detail)
|
|
578
562
|
if (selectedIds.length === 1) {
|
|
579
563
|
emitDeleteSFX(useScene.getState().nodes[selectedIds[0]!]?.type)
|
|
580
564
|
} else {
|
|
@@ -32,9 +32,12 @@ import {
|
|
|
32
32
|
collectParticipants,
|
|
33
33
|
computeGroupBox,
|
|
34
34
|
expandToComponent,
|
|
35
|
+
type GroupPlanBounds,
|
|
36
|
+
groupPlanBounds,
|
|
35
37
|
levelFrame,
|
|
36
|
-
|
|
38
|
+
planBoundsCenter,
|
|
37
39
|
rotateGroupSnapshots,
|
|
40
|
+
rotatePlanBounds,
|
|
38
41
|
translateGroupPatches,
|
|
39
42
|
type Vec2,
|
|
40
43
|
} from './group-transform-shared'
|
|
@@ -88,6 +91,9 @@ export function armGroupMove3d(args: {
|
|
|
88
91
|
affectedIds: AnyNodeId[]
|
|
89
92
|
candidates: ReturnType<typeof collectAlignmentAnchors>
|
|
90
93
|
restAnchors: ReturnType<typeof bboxCornerAnchors>
|
|
94
|
+
startBounds: GroupPlanBounds
|
|
95
|
+
restBounds: GroupPlanBounds
|
|
96
|
+
rotation: number
|
|
91
97
|
restCenter: Vec2
|
|
92
98
|
plane: Plane
|
|
93
99
|
startLocal: Vector3
|
|
@@ -130,14 +136,14 @@ export function armGroupMove3d(args: {
|
|
|
130
136
|
if (n && !movingIdSet.has(nid)) staticNodes[nid] = n
|
|
131
137
|
}
|
|
132
138
|
const candidates = collectAlignmentAnchors(staticNodes, '', levelId)
|
|
133
|
-
const
|
|
134
|
-
|
|
139
|
+
const restBounds = groupPlanBounds(restBox, starts, frameInv)
|
|
140
|
+
if (!restBounds) return null
|
|
135
141
|
const restAnchors = bboxCornerAnchors(
|
|
136
142
|
'group-move',
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
143
|
+
restBounds.minX,
|
|
144
|
+
restBounds.minZ,
|
|
145
|
+
restBounds.maxX,
|
|
146
|
+
restBounds.maxZ,
|
|
141
147
|
)
|
|
142
148
|
|
|
143
149
|
for (const id of affectedIds) {
|
|
@@ -154,9 +160,11 @@ export function armGroupMove3d(args: {
|
|
|
154
160
|
nodeId,
|
|
155
161
|
handle: GROUP_MOVE_DRAG_LABEL,
|
|
156
162
|
})
|
|
157
|
-
// Rotation pivot for mid-drag R/T — the
|
|
158
|
-
|
|
159
|
-
|
|
163
|
+
// Rotation pivot for mid-drag R/T — the START footprint's center, the same
|
|
164
|
+
// point the idle keyboard rotate and the rotate gizmos orbit. Fixed for the
|
|
165
|
+
// whole session: the snapshots are start placements, and `applyDelta` adds
|
|
166
|
+
// the live drag delta on top of them.
|
|
167
|
+
const restCenter = planBoundsCenter(restBounds)
|
|
160
168
|
|
|
161
169
|
return {
|
|
162
170
|
starts,
|
|
@@ -164,6 +172,9 @@ export function armGroupMove3d(args: {
|
|
|
164
172
|
affectedIds,
|
|
165
173
|
candidates,
|
|
166
174
|
restAnchors,
|
|
175
|
+
startBounds: restBounds,
|
|
176
|
+
restBounds,
|
|
177
|
+
rotation: 0,
|
|
167
178
|
restCenter,
|
|
168
179
|
plane,
|
|
169
180
|
startLocal,
|
|
@@ -245,18 +256,22 @@ export function armGroupMove3d(args: {
|
|
|
245
256
|
// current delta — the carried group turns exactly like the idle keyboard
|
|
246
257
|
// rotate, and the commit stays a single updateNodes.
|
|
247
258
|
const rotateSession = (s: Session, direction: 1 | -1) => {
|
|
248
|
-
const
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
{ x: s.restCenter[0], z: s.restCenter[1] },
|
|
252
|
-
-direction * (Math.PI / 4),
|
|
253
|
-
)
|
|
259
|
+
const pivot = { x: s.restCenter[0], z: s.restCenter[1] }
|
|
260
|
+
const delta = -direction * (Math.PI / 4)
|
|
261
|
+
const rotated = rotateGroupSnapshots(s.starts, s.links, pivot, delta)
|
|
254
262
|
s.starts = rotated.starts
|
|
255
263
|
s.links = rotated.links
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
264
|
+
// Re-fit from the start footprint at the accumulated angle: rotating the
|
|
265
|
+
// previous axis-aligned fit would inflate the box every step.
|
|
266
|
+
s.rotation += delta
|
|
267
|
+
s.restBounds = rotatePlanBounds(s.startBounds, pivot, s.rotation)
|
|
268
|
+
s.restAnchors = bboxCornerAnchors(
|
|
269
|
+
'group-move',
|
|
270
|
+
s.restBounds.minX,
|
|
271
|
+
s.restBounds.minZ,
|
|
272
|
+
s.restBounds.maxX,
|
|
273
|
+
s.restBounds.maxZ,
|
|
274
|
+
)
|
|
260
275
|
sfxEmitter.emit('sfx:item-rotate')
|
|
261
276
|
applyDelta(s, s.lastDelta?.[0] ?? 0, s.lastDelta?.[1] ?? 0)
|
|
262
277
|
}
|
|
@@ -369,7 +384,22 @@ export function armGroupMove3d(args: {
|
|
|
369
384
|
const onKeyDown = (e: KeyboardEvent) => {
|
|
370
385
|
const key = e.key.toLowerCase()
|
|
371
386
|
if ((key === 'r' || key === 't') && !e.metaKey && !e.ctrlKey && !e.altKey && !e.shiftKey) {
|
|
372
|
-
|
|
387
|
+
// Armed but still under the drag threshold: engage first (exactly what
|
|
388
|
+
// the next pointer-move would do) so the rotation lands inside this
|
|
389
|
+
// session. Falling through to the global idle arm instead would write
|
|
390
|
+
// the scene behind snapshots already captured here, and the first
|
|
391
|
+
// `applyDelta` would republish them — undoing the rotation.
|
|
392
|
+
if (!session) {
|
|
393
|
+
session = engage()
|
|
394
|
+
if (!session) {
|
|
395
|
+
// No plane hit yet: swallow the chord and keep the gesture armed so
|
|
396
|
+
// the next pointer-move can still engage; the idle arm must not run
|
|
397
|
+
// behind the snapshots captured here.
|
|
398
|
+
e.preventDefault()
|
|
399
|
+
e.stopPropagation()
|
|
400
|
+
return
|
|
401
|
+
}
|
|
402
|
+
}
|
|
373
403
|
e.preventDefault()
|
|
374
404
|
e.stopPropagation()
|
|
375
405
|
rotateSession(session, key === 'r' ? 1 : -1)
|
|
@@ -4,7 +4,10 @@ import { z } from 'zod'
|
|
|
4
4
|
import {
|
|
5
5
|
classifyParticipant,
|
|
6
6
|
collectParticipants,
|
|
7
|
+
planBoundsCenter,
|
|
7
8
|
rotateGroupPatches,
|
|
9
|
+
rotateGroupSnapshots,
|
|
10
|
+
rotatePlanBounds,
|
|
8
11
|
translateGroupPatches,
|
|
9
12
|
} from './group-transform-shared'
|
|
10
13
|
|
|
@@ -40,6 +43,34 @@ function registerElevatorTestKind() {
|
|
|
40
43
|
} as AnyNodeDefinition)
|
|
41
44
|
}
|
|
42
45
|
|
|
46
|
+
// A level holding one of each rigid placement shape: an item ([x,y,z] rotation)
|
|
47
|
+
// and a column (numeric rotation).
|
|
48
|
+
function placedNodes() {
|
|
49
|
+
return {
|
|
50
|
+
building_test: { id: 'building_test', type: 'building', children: ['level_test'] },
|
|
51
|
+
level_test: {
|
|
52
|
+
id: 'level_test',
|
|
53
|
+
type: 'level',
|
|
54
|
+
parentId: 'building_test',
|
|
55
|
+
children: ['item_chair', 'column_post'],
|
|
56
|
+
},
|
|
57
|
+
item_chair: {
|
|
58
|
+
id: 'item_chair',
|
|
59
|
+
type: 'item',
|
|
60
|
+
parentId: 'level_test',
|
|
61
|
+
position: [1, 0, 3],
|
|
62
|
+
rotation: [0, 0.5, 0],
|
|
63
|
+
},
|
|
64
|
+
column_post: {
|
|
65
|
+
id: 'column_post',
|
|
66
|
+
type: 'column',
|
|
67
|
+
parentId: 'level_test',
|
|
68
|
+
position: [4, 0, -1],
|
|
69
|
+
rotation: 1.25,
|
|
70
|
+
},
|
|
71
|
+
} as unknown as Record<string, AnyNode>
|
|
72
|
+
}
|
|
73
|
+
|
|
43
74
|
describe('group transform participants', () => {
|
|
44
75
|
beforeAll(() => {
|
|
45
76
|
registerBuildingScopedTestKind()
|
|
@@ -362,6 +393,53 @@ describe('group transform participants', () => {
|
|
|
362
393
|
expect(lampPatch.position).toEqual([3, 2.4, 2])
|
|
363
394
|
})
|
|
364
395
|
|
|
396
|
+
test('translate patches carry the snapshot rotation for vec3 and scalar kinds', () => {
|
|
397
|
+
const { starts } = collectParticipants(
|
|
398
|
+
['item_chair', 'column_post'],
|
|
399
|
+
placedNodes(),
|
|
400
|
+
'level_test',
|
|
401
|
+
)
|
|
402
|
+
const patches = Object.fromEntries(translateGroupPatches(starts, [], 1, 2))
|
|
403
|
+
|
|
404
|
+
expect(patches.item_chair).toEqual({ position: [2, 0, 5], rotation: [0, 0.5, 0] })
|
|
405
|
+
expect(patches.column_post).toEqual({ position: [5, 0, 1], rotation: 1.25 })
|
|
406
|
+
})
|
|
407
|
+
|
|
408
|
+
test('a mid-drag rotation survives the next translate re-publish', () => {
|
|
409
|
+
const { starts } = collectParticipants(
|
|
410
|
+
['item_chair', 'column_post'],
|
|
411
|
+
placedNodes(),
|
|
412
|
+
'level_test',
|
|
413
|
+
)
|
|
414
|
+
// What a mid-drag R does: turn the snapshots, then re-apply the live delta.
|
|
415
|
+
const rotated = rotateGroupSnapshots(starts, [], { x: 0, z: 0 }, Math.PI / 2)
|
|
416
|
+
const patches = Object.fromEntries(
|
|
417
|
+
translateGroupPatches(rotated.starts, rotated.links, 1, 2),
|
|
418
|
+
) as Record<string, { position: number[]; rotation: number[] | number }>
|
|
419
|
+
|
|
420
|
+
// Orbited 90° in the atan2 x→z sense ((x, z) → (-z, x)), then slid.
|
|
421
|
+
expect(patches.item_chair!.position[0]).toBeCloseTo(-2)
|
|
422
|
+
expect(patches.item_chair!.position[2]).toBeCloseTo(3)
|
|
423
|
+
expect(patches.column_post!.position[0]).toBeCloseTo(2)
|
|
424
|
+
expect(patches.column_post!.position[2]).toBeCloseTo(6)
|
|
425
|
+
// …and the facings turned with it instead of reverting to the pre-drag yaw.
|
|
426
|
+
expect((patches.item_chair!.rotation as number[])[1]).toBeCloseTo(0.5 - Math.PI / 2)
|
|
427
|
+
expect(patches.column_post!.rotation as number).toBeCloseTo(1.25 - Math.PI / 2)
|
|
428
|
+
})
|
|
429
|
+
|
|
430
|
+
test('rotating the plan bounds keeps the footprint centred on the pivot', () => {
|
|
431
|
+
const bounds = { minX: 0, minZ: 0, maxX: 4, maxZ: 2 }
|
|
432
|
+
const [pivotX, pivotZ] = planBoundsCenter(bounds)
|
|
433
|
+
const rotated = rotatePlanBounds(bounds, { x: pivotX, z: pivotZ }, Math.PI / 2)
|
|
434
|
+
|
|
435
|
+
expect(rotated.minX).toBeCloseTo(1)
|
|
436
|
+
expect(rotated.maxX).toBeCloseTo(3)
|
|
437
|
+
expect(rotated.minZ).toBeCloseTo(-1)
|
|
438
|
+
expect(rotated.maxZ).toBeCloseTo(3)
|
|
439
|
+
expect(planBoundsCenter(rotated)[0]).toBeCloseTo(pivotX)
|
|
440
|
+
expect(planBoundsCenter(rotated)[1]).toBeCloseTo(pivotZ)
|
|
441
|
+
})
|
|
442
|
+
|
|
365
443
|
test('supports legacy level-parented elevators already loaded in the editor', () => {
|
|
366
444
|
const nodes = {
|
|
367
445
|
building_test: {
|