@pascal-app/editor 1.0.0-beta.5 → 1.0.1
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 +15 -8
- package/scripts/generate-print-golden-house.ts +89 -0
- package/src/components/editor/bake-exporter.tsx +5 -0
- package/src/components/editor/bake-thumbnail.tsx +4 -1
- 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 +55 -51
- package/src/components/editor/floorplan-panel.tsx +99 -15
- 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/handle-placement.test.ts +111 -0
- package/src/components/editor/handles/handle-placement.ts +69 -0
- package/src/components/editor/handles/linear-resize-drag.ts +78 -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 +23 -3
- package/src/components/editor/index.tsx +151 -24
- package/src/components/editor/node-action-menu.tsx +14 -1
- package/src/components/editor/node-arrow-handles.tsx +67 -119
- package/src/components/editor/selection-manager.tsx +153 -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 +295 -239
- 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-action-menu.tsx +12 -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 +226 -35
- package/src/components/procedural-items/inspector.tsx +223 -0
- 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/ceiling/ceiling-system.tsx +3 -1
- 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/item-surface-placement.test.ts +175 -0
- package/src/components/tools/item/item-surface-strategy-resolver.test.ts +210 -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 +304 -171
- 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 +10 -0
- package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +257 -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 +116 -44
- package/src/components/tools/item/use-placement-coordinator.tsx +672 -256
- package/src/components/tools/registered-tool-install-lifecycle.test.tsx +256 -0
- package/src/components/tools/registry/item-surface-child-frame.test.ts +90 -0
- package/src/components/tools/registry/item-surface-move.test.ts +501 -0
- package/src/components/tools/registry/item-surface-move.ts +441 -0
- package/src/components/tools/registry/item-surface-resolver.test.ts +257 -0
- package/src/components/tools/registry/move-registry-node-tool.test.ts +10 -0
- package/src/components/tools/registry/move-registry-node-tool.tsx +505 -164
- 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 +56 -17
- package/src/components/tools/shared/shelf-stickiness.ts +54 -0
- package/src/components/tools/shared/surface-hit.test.ts +56 -0
- package/src/components/tools/shared/surface-hit.ts +23 -0
- package/src/components/tools/shared/surface-rejection.test.tsx +106 -0
- package/src/components/tools/shared/surface-rejection.tsx +29 -0
- 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/tools/zone/zone-tool.tsx +13 -4
- 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 +63 -13
- 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/metric-control.tsx +29 -13
- 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 +44 -21
- 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 +103 -10
- 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 +266 -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 +35 -164
- package/src/components/ui/panels/reference-panel.tsx +34 -4
- package/src/components/ui/primitives/checkbox.tsx +34 -0
- 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 +502 -31
- 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 +167 -0
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +128 -0
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +8 -1
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +19 -5
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +222 -50
- package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +2 -7
- 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/shelf-tree-node.tsx +3 -8
- 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 +20 -7
- package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +224 -0
- package/src/components/ui/sidebar/panels/site-panel/zone-membership-checkbox.tsx +22 -0
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +24 -7
- package/src/components/ui/sidebar/tab-bar.tsx +2 -0
- package/src/components/ui/sidebar/use-plugin-panels.tsx +17 -2
- package/src/components/ui/snap-target-badge.tsx +2 -0
- package/src/components/viewer/viewer-scene-header.tsx +5 -1
- package/src/components/viewer/viewer-stage.tsx +4 -1
- package/src/components/viewer/viewer-units-panel.tsx +116 -0
- 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 +144 -0
- package/src/hooks/use-ceiling-events.ts +5 -1
- 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 +132 -63
- package/src/hooks/use-registered-tool-enabled.ts +20 -0
- package/src/hooks/use-save-shortcut.ts +33 -0
- package/src/index.tsx +87 -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/export-texture-utils.test.ts +167 -0
- package/src/lib/export-texture-utils.ts +121 -0
- package/src/lib/export-user-data.test.ts +48 -0
- package/src/lib/export-user-data.ts +69 -0
- 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 +536 -2
- package/src/lib/floorplan/floorplan-export.tsx +120 -19
- 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 +5 -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/fresh-planar-placement.ts +13 -5
- package/src/lib/glb-export-serialize.test.ts +280 -0
- package/src/lib/glb-export.test.ts +1375 -21
- package/src/lib/glb-export.ts +781 -106
- 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-duplication.test.ts +73 -0
- package/src/lib/level-duplication.ts +32 -2
- package/src/lib/level-print-export.test.ts +484 -0
- package/src/lib/level-print-export.ts +526 -0
- package/src/lib/linear-display.test.ts +59 -0
- package/src/lib/linear-display.ts +39 -0
- package/src/lib/local-guide-image.test.ts +24 -0
- package/src/lib/local-guide-image.ts +34 -1
- 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/node-action-movement.ts +21 -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 +455 -0
- package/src/lib/portable-export.ts +1042 -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 +379 -0
- package/src/lib/print-shell-compiler-manifold-worker.ts +200 -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 +38 -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-clipboard.ts +1 -1
- 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/send-to-app.test.ts +108 -0
- package/src/lib/send-to-app.ts +121 -0
- package/src/lib/sfx/movement-tick.ts +37 -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/surface-attachment.ts +77 -0
- package/src/lib/units.ts +254 -0
- package/src/lib/usdz-export.ts +65 -0
- package/src/lib/use-linear-display.ts +8 -32
- 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-handle-group.ts +4 -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/store/use-send-to-app.ts +21 -0
- package/src/components/tools/roof/roof-tool.tsx +0 -656
- package/src/components/ui/helpers/roof-helper.tsx +0 -14
|
@@ -33,6 +33,8 @@ import {
|
|
|
33
33
|
type RoofNode,
|
|
34
34
|
type RoofSegmentNode,
|
|
35
35
|
resolveSlabPlacementElevation,
|
|
36
|
+
resolveTerrainWallConstructionOptions,
|
|
37
|
+
runAsSingleSceneHistoryStep,
|
|
36
38
|
type SiteNode,
|
|
37
39
|
type SlabNode,
|
|
38
40
|
SlabNode as SlabNodeSchema,
|
|
@@ -88,6 +90,8 @@ import {
|
|
|
88
90
|
type FloorplanNodeTransform as SharedFloorplanNodeTransform,
|
|
89
91
|
worldToFloorplanLocalPoint,
|
|
90
92
|
} from '../../lib/floorplan'
|
|
93
|
+
import { resolveGenericFloorplanGridEventPoint } from '../../lib/floorplan-grid-event-point'
|
|
94
|
+
import type { EditorGridEvent } from '../../lib/grid-event-presentation'
|
|
91
95
|
import { groundHeightAt } from '../../lib/ground-surface'
|
|
92
96
|
import { guideEmitter } from '../../lib/guide-events'
|
|
93
97
|
import { measurementHint, parseMeasurement } from '../../lib/measurement-parser'
|
|
@@ -95,6 +99,7 @@ import { formatLinearMeasurement, linearUnitToMeters } from '../../lib/measureme
|
|
|
95
99
|
import { sfxEmitter } from '../../lib/sfx-bus'
|
|
96
100
|
import { SITE_BOUNDARY_DRAG_LABEL, siteBoundaryHandlesEnabled } from '../../lib/site-boundary'
|
|
97
101
|
import { resolveSlabPlanPointSnap } from '../../lib/slab-plan-snap'
|
|
102
|
+
import { cancelPendingZonePaint, focusedUnitNode, paintZoneMembership } from '../../lib/units'
|
|
98
103
|
import { cn } from '../../lib/utils'
|
|
99
104
|
import { snapBuildingLocalToWorldGrid } from '../../lib/world-grid-snap'
|
|
100
105
|
import { subscribeNavigationSyncPose } from '../../store/navigation-sync-pose-store'
|
|
@@ -148,6 +153,7 @@ import {
|
|
|
148
153
|
isBoxSelectPointerSuppressed,
|
|
149
154
|
markBoxSelectHandled,
|
|
150
155
|
} from '../tools/select/box-select-state'
|
|
156
|
+
import { marqueePolygon } from '../tools/select/marquee-footprint'
|
|
151
157
|
import {
|
|
152
158
|
type Point2 as MarqueePoint2,
|
|
153
159
|
polygonsIntersect as marqueePolygonsIntersect,
|
|
@@ -184,7 +190,6 @@ import {
|
|
|
184
190
|
chainEndJoinsExistingWall,
|
|
185
191
|
createWallOnCurrentLevel,
|
|
186
192
|
isSegmentLongEnough,
|
|
187
|
-
resolveTerrainWallConstructionOptions,
|
|
188
193
|
snapWallDraftPoint,
|
|
189
194
|
snapWallDraftPointDetailed,
|
|
190
195
|
snapPointToGrid as snapWallPointToGrid,
|
|
@@ -864,7 +869,8 @@ function collectFloorplanScreenSelectionIds(rect: ScreenRect, svg: SVGSVGElement
|
|
|
864
869
|
| { start?: unknown; end?: unknown; polygon?: unknown }
|
|
865
870
|
| undefined
|
|
866
871
|
if (!node) continue
|
|
867
|
-
const { start, end
|
|
872
|
+
const { start, end } = node
|
|
873
|
+
const polygon = marqueePolygon(node)
|
|
868
874
|
if (isMarqueeVec2(start) && isMarqueeVec2(end)) {
|
|
869
875
|
dataTested.add(id)
|
|
870
876
|
if (marqueeSegmentIntersectsPolygon(start, end, planQuad)) hitIdsFromData.add(id)
|
|
@@ -4673,6 +4679,7 @@ function FloorplanLinearDraftLayer({
|
|
|
4673
4679
|
const wallDraftEnd = useFloorplanDraftPreview((s) => s.wallDraftEnd)
|
|
4674
4680
|
const fenceDraftEnd = useFloorplanDraftPreview((s) => s.fenceDraftEnd)
|
|
4675
4681
|
const roofDraftEnd = useFloorplanDraftPreview((s) => s.roofDraftEnd)
|
|
4682
|
+
const roofDraftQuarterTurn = useFloorplanDraftPreview((s) => s.roofDraftQuarterTurn)
|
|
4676
4683
|
|
|
4677
4684
|
const draftPolygon = useMemo(() => {
|
|
4678
4685
|
if (
|
|
@@ -4709,6 +4716,21 @@ function FloorplanLinearDraftLayer({
|
|
|
4709
4716
|
return draftPolygon ? formatPolygonPoints(draftPolygon) : null
|
|
4710
4717
|
}, [draftPolygon, isRoofBuildActive, roofDraftEnd, roofDraftStart])
|
|
4711
4718
|
|
|
4719
|
+
const roofDraftDirectionLine = useMemo(() => {
|
|
4720
|
+
if (!(isRoofBuildActive && roofDraftStart && roofDraftEnd)) return null
|
|
4721
|
+
const minX = Math.min(roofDraftStart[0], roofDraftEnd[0])
|
|
4722
|
+
const maxX = Math.max(roofDraftStart[0], roofDraftEnd[0])
|
|
4723
|
+
const minY = Math.min(roofDraftStart[1], roofDraftEnd[1])
|
|
4724
|
+
const maxY = Math.max(roofDraftStart[1], roofDraftEnd[1])
|
|
4725
|
+
if (maxX - minX < 1e-6 || maxY - minY < 1e-6) return null
|
|
4726
|
+
|
|
4727
|
+
const centerX = (minX + maxX) / 2
|
|
4728
|
+
const centerY = (minY + maxY) / 2
|
|
4729
|
+
return roofDraftQuarterTurn
|
|
4730
|
+
? { x1: centerX, y1: minY, x2: centerX, y2: maxY }
|
|
4731
|
+
: { x1: minX, y1: centerY, x2: maxX, y2: centerY }
|
|
4732
|
+
}, [isRoofBuildActive, roofDraftEnd, roofDraftQuarterTurn, roofDraftStart])
|
|
4733
|
+
|
|
4712
4734
|
const fenceDraftSegment = useMemo(() => {
|
|
4713
4735
|
if (!(isFenceBuildActive && fenceDraftStart && fenceDraftEnd)) {
|
|
4714
4736
|
return null
|
|
@@ -4887,6 +4909,19 @@ function FloorplanLinearDraftLayer({
|
|
|
4887
4909
|
unitsPerPixel={unitsPerPixel}
|
|
4888
4910
|
/>
|
|
4889
4911
|
|
|
4912
|
+
{roofDraftDirectionLine && (
|
|
4913
|
+
<line
|
|
4914
|
+
pointerEvents="none"
|
|
4915
|
+
stroke={draftStroke}
|
|
4916
|
+
strokeLinecap="round"
|
|
4917
|
+
strokeWidth={unitsPerPixel * 1.5}
|
|
4918
|
+
x1={toSvgX(roofDraftDirectionLine.x1)}
|
|
4919
|
+
x2={toSvgX(roofDraftDirectionLine.x2)}
|
|
4920
|
+
y1={toSvgY(roofDraftDirectionLine.y1)}
|
|
4921
|
+
y2={toSvgY(roofDraftDirectionLine.y2)}
|
|
4922
|
+
/>
|
|
4923
|
+
)}
|
|
4924
|
+
|
|
4890
4925
|
{draftWallMeasurement && (
|
|
4891
4926
|
<FloorplanDraftWallMeasurement
|
|
4892
4927
|
labelBackground={isDark ? '#0f172a' : '#ffffff'}
|
|
@@ -5063,6 +5098,9 @@ export function FloorplanPanel({
|
|
|
5063
5098
|
if (building?.type !== 'building') return false
|
|
5064
5099
|
return building.children.some((cid) => state.nodes[cid]?.type === 'level')
|
|
5065
5100
|
})
|
|
5101
|
+
// The studio workspace (renders / materials / item builder) is a clean
|
|
5102
|
+
// stage — editor viewport chrome, compass included, stays out of it.
|
|
5103
|
+
const isStudioWorkspace = useEditor((s) => s.workspaceMode === 'studio')
|
|
5066
5104
|
const elevators = useScene(
|
|
5067
5105
|
useShallow((state) => {
|
|
5068
5106
|
const building = currentBuildingId ? state.nodes[currentBuildingId] : null
|
|
@@ -7916,7 +7954,7 @@ export function FloorplanPanel({
|
|
|
7916
7954
|
return null
|
|
7917
7955
|
}
|
|
7918
7956
|
|
|
7919
|
-
const { createNode, nodes } = useScene.getState()
|
|
7957
|
+
const { createNode, updateNode, nodes } = useScene.getState()
|
|
7920
7958
|
const zoneCount = Object.values(nodes).filter((node) => node.type === 'zone').length
|
|
7921
7959
|
const zone = ZoneNodeSchema.parse({
|
|
7922
7960
|
color: PALETTE_COLORS[zoneCount % PALETTE_COLORS.length],
|
|
@@ -7924,9 +7962,15 @@ export function FloorplanPanel({
|
|
|
7924
7962
|
polygon: points.map(([x, z]) => [x, z] as [number, number]),
|
|
7925
7963
|
})
|
|
7926
7964
|
|
|
7927
|
-
|
|
7965
|
+
// Joining the focused unit rides in the zone's own undo step; selecting
|
|
7966
|
+
// the zone would end focus, so a painted unit keeps it instead.
|
|
7967
|
+
const focusedUnit = focusedUnitNode()
|
|
7968
|
+
runAsSingleSceneHistoryStep(useScene, () => {
|
|
7969
|
+
createNode(zone, levelId)
|
|
7970
|
+
if (focusedUnit) updateNode(focusedUnit.id, { members: [...focusedUnit.members, zone.id] })
|
|
7971
|
+
})
|
|
7928
7972
|
sfxEmitter.emit('sfx:structure-build')
|
|
7929
|
-
setSelection({ zoneId: zone.id })
|
|
7973
|
+
if (!focusedUnit) setSelection({ zoneId: zone.id })
|
|
7930
7974
|
return zone.id
|
|
7931
7975
|
},
|
|
7932
7976
|
[levelId, setSelection],
|
|
@@ -8931,12 +8975,29 @@ export function FloorplanPanel({
|
|
|
8931
8975
|
const groundY = groundHeightAt(worldX, worldZ, floorplanGridWorldY)
|
|
8932
8976
|
const worldY = groundY ?? floorplanGridWorldY
|
|
8933
8977
|
const localY = groundY === null ? floorplanGridLocalY : groundY - buildingPosition[1]
|
|
8978
|
+
const planScene =
|
|
8979
|
+
nativeEvent.currentTarget.querySelector<SVGGraphicsElement>('[data-floorplan-scene]')
|
|
8980
|
+
const screenMatrix = planScene?.getScreenCTM()
|
|
8934
8981
|
|
|
8935
|
-
|
|
8982
|
+
const gridEvent: EditorGridEvent = {
|
|
8936
8983
|
nativeEvent: nativeEvent.nativeEvent as any,
|
|
8937
8984
|
position: [worldX, worldY, worldZ],
|
|
8938
8985
|
localPosition: [planPoint[0], localY, planPoint[1]],
|
|
8939
|
-
|
|
8986
|
+
screenProjection: screenMatrix
|
|
8987
|
+
? {
|
|
8988
|
+
pointer: [nativeEvent.clientX, nativeEvent.clientY],
|
|
8989
|
+
localToScreen: [
|
|
8990
|
+
screenMatrix.a,
|
|
8991
|
+
screenMatrix.b,
|
|
8992
|
+
screenMatrix.c,
|
|
8993
|
+
screenMatrix.d,
|
|
8994
|
+
screenMatrix.e,
|
|
8995
|
+
screenMatrix.f,
|
|
8996
|
+
],
|
|
8997
|
+
}
|
|
8998
|
+
: undefined,
|
|
8999
|
+
}
|
|
9000
|
+
emitter.emit(`grid:${eventType}` as any, gridEvent)
|
|
8940
9001
|
},
|
|
8941
9002
|
[buildingPosition, buildingRotationY, floorplanGridLocalY, floorplanGridWorldY],
|
|
8942
9003
|
)
|
|
@@ -9400,10 +9461,14 @@ export function FloorplanPanel({
|
|
|
9400
9461
|
// this exclusion the catch-all would emit `grid:move` and re-drive the
|
|
9401
9462
|
// 3D MoveDoorTool's free-follow, fighting the overlay again.
|
|
9402
9463
|
if (!isWallBuildActive && !isOpeningMoveActive && isFloorplanGridInteractionActive) {
|
|
9403
|
-
const
|
|
9404
|
-
|
|
9464
|
+
const eventPoint = resolveGenericFloorplanGridEventPoint({
|
|
9465
|
+
point: planPoint,
|
|
9466
|
+
registryToolOwnsSnapping: isRegistryToolBuildActive,
|
|
9467
|
+
snap: getSnappedFloorplanPoint,
|
|
9468
|
+
})
|
|
9469
|
+
emitFloorplanGridEvent('move', eventPoint, event)
|
|
9405
9470
|
setCursorPoint((previousPoint) =>
|
|
9406
|
-
previousPoint && pointsEqual(previousPoint,
|
|
9471
|
+
previousPoint && pointsEqual(previousPoint, eventPoint) ? previousPoint : eventPoint,
|
|
9407
9472
|
)
|
|
9408
9473
|
return
|
|
9409
9474
|
}
|
|
@@ -9503,6 +9568,7 @@ export function FloorplanPanel({
|
|
|
9503
9568
|
// stale closure and float a door symbol while the window tool is armed.
|
|
9504
9569
|
showOpeningGhost,
|
|
9505
9570
|
isPolygonBuildActive,
|
|
9571
|
+
isRegistryToolBuildActive,
|
|
9506
9572
|
isRoofBuildActive,
|
|
9507
9573
|
isWallBuildActive,
|
|
9508
9574
|
levelId,
|
|
@@ -9849,6 +9915,7 @@ export function FloorplanPanel({
|
|
|
9849
9915
|
isOpeningPlacementActive: isOpeningBuildActive && !isOpeningMoveActive,
|
|
9850
9916
|
isPolygonBuildActive,
|
|
9851
9917
|
isRoofBuildActive,
|
|
9918
|
+
registryToolOwnsSnapping: isRegistryToolBuildActive,
|
|
9852
9919
|
isWallBuildActive,
|
|
9853
9920
|
isZoneBuildActive,
|
|
9854
9921
|
levelId,
|
|
@@ -9949,6 +10016,18 @@ export function FloorplanPanel({
|
|
|
9949
10016
|
setSelectedReferenceId(null)
|
|
9950
10017
|
|
|
9951
10018
|
if (backgroundSelection.kind === 'select-zone') {
|
|
10019
|
+
// Unit focus: a click paints membership, a double-click selects the
|
|
10020
|
+
// zone and keeps focus (the SVG gets the second click as detail 2).
|
|
10021
|
+
const focusedUnitId = useViewer.getState().focusedUnitId
|
|
10022
|
+
if (focusedUnitId) {
|
|
10023
|
+
if (event.detail >= 2) {
|
|
10024
|
+
cancelPendingZonePaint(backgroundSelection.zoneId)
|
|
10025
|
+
setSelection({ zoneId: backgroundSelection.zoneId })
|
|
10026
|
+
} else {
|
|
10027
|
+
paintZoneMembership(focusedUnitId, backgroundSelection.zoneId)
|
|
10028
|
+
}
|
|
10029
|
+
return
|
|
10030
|
+
}
|
|
9952
10031
|
setSelection({ zoneId: backgroundSelection.zoneId })
|
|
9953
10032
|
return
|
|
9954
10033
|
}
|
|
@@ -10512,9 +10591,11 @@ export function FloorplanPanel({
|
|
|
10512
10591
|
}
|
|
10513
10592
|
|
|
10514
10593
|
if (useEditor.getState().phase !== 'site') {
|
|
10515
|
-
useEditor.
|
|
10594
|
+
useEditor.getState().setPhase('site')
|
|
10595
|
+
useEditor.getState().armToolMode({ mode: 'select' })
|
|
10596
|
+
} else {
|
|
10597
|
+
selectSiteFloorplanContext()
|
|
10516
10598
|
}
|
|
10517
|
-
selectSiteFloorplanContext()
|
|
10518
10599
|
|
|
10519
10600
|
const nextDraft = {
|
|
10520
10601
|
siteId,
|
|
@@ -10596,9 +10677,11 @@ export function FloorplanPanel({
|
|
|
10596
10677
|
]
|
|
10597
10678
|
|
|
10598
10679
|
if (useEditor.getState().phase !== 'site') {
|
|
10599
|
-
useEditor.
|
|
10680
|
+
useEditor.getState().setPhase('site')
|
|
10681
|
+
useEditor.getState().armToolMode({ mode: 'select' })
|
|
10682
|
+
} else {
|
|
10683
|
+
selectSiteFloorplanContext()
|
|
10600
10684
|
}
|
|
10601
|
-
selectSiteFloorplanContext()
|
|
10602
10685
|
|
|
10603
10686
|
const nextDraft = {
|
|
10604
10687
|
siteId,
|
|
@@ -11107,7 +11190,8 @@ export function FloorplanPanel({
|
|
|
11107
11190
|
<FloorplanRegistryActionMenu />
|
|
11108
11191
|
<FloorplanGroupActionMenu />
|
|
11109
11192
|
|
|
11110
|
-
{
|
|
11193
|
+
{!isStudioWorkspace &&
|
|
11194
|
+
(levelNode?.type === 'level' || hasAmbientBuildingLevel) &&
|
|
11111
11195
|
(compassHost ? (
|
|
11112
11196
|
createPortal(
|
|
11113
11197
|
<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: {
|