@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
|
@@ -1,9 +1,21 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
|
|
3
|
+
import {
|
|
4
|
+
emitter,
|
|
5
|
+
type FenceNode,
|
|
6
|
+
isCurvedWall,
|
|
7
|
+
nodeRegistry,
|
|
8
|
+
type WallNode,
|
|
9
|
+
} from '@pascal-app/core'
|
|
10
|
+
import {
|
|
11
|
+
type MouseEvent as ReactMouseEvent,
|
|
12
|
+
useCallback,
|
|
13
|
+
useEffect,
|
|
14
|
+
useSyncExternalStore,
|
|
15
|
+
} from 'react'
|
|
5
16
|
import { resolveCeilingPlanPointSnap } from '../../lib/ceiling-plan-snap'
|
|
6
17
|
import { alignFloorplanDraftPoint, getPlanPointDistance } from '../../lib/floorplan'
|
|
18
|
+
import { resolveGenericFloorplanGridEventPoint } from '../../lib/floorplan-grid-event-point'
|
|
7
19
|
import { resolveSlabPlanPointSnap } from '../../lib/slab-plan-snap'
|
|
8
20
|
import useAlignmentGuides from '../../store/use-alignment-guides'
|
|
9
21
|
import useEditor, { isAngleSnapActive, isMagneticSnapActive } from '../../store/use-editor'
|
|
@@ -12,6 +24,9 @@ import useSegmentDraftChain from '../../store/use-segment-draft-chain'
|
|
|
12
24
|
import { snapFenceDraftPoint } from '../tools/fence/fence-drafting'
|
|
13
25
|
import { getSegmentGridStep, type WallPlanPoint } from '../tools/wall/wall-drafting'
|
|
14
26
|
|
|
27
|
+
const NOOP_SUBSCRIBE = () => () => {}
|
|
28
|
+
const DEFAULT_ROOF_FOOTPRINT_CHOICE = () => 'draw'
|
|
29
|
+
|
|
15
30
|
type UseFloorplanBackgroundPlacementArgs = {
|
|
16
31
|
activePolygonDraftPoints: WallPlanPoint[]
|
|
17
32
|
ceilingDraftPoints: WallPlanPoint[]
|
|
@@ -55,6 +70,7 @@ type UseFloorplanBackgroundPlacementArgs = {
|
|
|
55
70
|
isWallBuildActive: boolean
|
|
56
71
|
isZoneBuildActive: boolean
|
|
57
72
|
levelId: string | null
|
|
73
|
+
registryToolOwnsSnapping: boolean
|
|
58
74
|
roofDraftStart: WallPlanPoint | null
|
|
59
75
|
setCursorPoint: React.Dispatch<React.SetStateAction<WallPlanPoint | null>>
|
|
60
76
|
setFenceDraftEnd: React.Dispatch<React.SetStateAction<WallPlanPoint | null>>
|
|
@@ -113,6 +129,7 @@ export function useFloorplanBackgroundPlacement({
|
|
|
113
129
|
isWallBuildActive,
|
|
114
130
|
isZoneBuildActive,
|
|
115
131
|
levelId,
|
|
132
|
+
registryToolOwnsSnapping,
|
|
116
133
|
roofDraftStart,
|
|
117
134
|
setCursorPoint,
|
|
118
135
|
setFenceDraftEnd,
|
|
@@ -125,6 +142,26 @@ export function useFloorplanBackgroundPlacement({
|
|
|
125
142
|
walls,
|
|
126
143
|
worldGridSnap,
|
|
127
144
|
}: UseFloorplanBackgroundPlacementArgs) {
|
|
145
|
+
// Read the roof's footprint-source option through the registry, not
|
|
146
|
+
// `@pascal-app/nodes`: this file lands in the nodes package's program via
|
|
147
|
+
// its editor imports, so a direct nodes import would cycle onto nodes' own
|
|
148
|
+
// dist output.
|
|
149
|
+
const roofFootprintOption = nodeRegistry
|
|
150
|
+
.get('roof')
|
|
151
|
+
?.toolOptions?.find((option) => option.id === 'footprintSource')
|
|
152
|
+
const roofFootprintChoice = useSyncExternalStore(
|
|
153
|
+
roofFootprintOption?.subscribe ?? NOOP_SUBSCRIBE,
|
|
154
|
+
roofFootprintOption?.value ?? DEFAULT_ROOF_FOOTPRINT_CHOICE,
|
|
155
|
+
roofFootprintOption?.value ?? DEFAULT_ROOF_FOOTPRINT_CHOICE,
|
|
156
|
+
)
|
|
157
|
+
// Conical always builds from a curved wall pick, regardless of the choice.
|
|
158
|
+
const roofIsConical = useEditor((state) => state.toolDefaults.roof?.roofType === 'conical')
|
|
159
|
+
const roofFootprintSource = roofIsConical ? 'walls' : roofFootprintChoice
|
|
160
|
+
|
|
161
|
+
useEffect(() => {
|
|
162
|
+
if (isRoofBuildActive && roofFootprintSource !== 'draw') clearRoofPlacementDraft()
|
|
163
|
+
}, [clearRoofPlacementDraft, isRoofBuildActive, roofFootprintSource])
|
|
164
|
+
|
|
128
165
|
const handleBackgroundPlacementClick = useCallback(
|
|
129
166
|
(
|
|
130
167
|
planPoint: WallPlanPoint,
|
|
@@ -188,6 +225,11 @@ export function useFloorplanBackgroundPlacement({
|
|
|
188
225
|
emitFloorplanGridEvent('click', snappedPoint, event)
|
|
189
226
|
setCursorPoint(snappedPoint)
|
|
190
227
|
|
|
228
|
+
if (roofFootprintSource !== 'draw') {
|
|
229
|
+
clearRoofPlacementDraft()
|
|
230
|
+
return true
|
|
231
|
+
}
|
|
232
|
+
|
|
191
233
|
if (roofDraftStart) {
|
|
192
234
|
clearRoofPlacementDraft()
|
|
193
235
|
} else {
|
|
@@ -357,9 +399,13 @@ export function useFloorplanBackgroundPlacement({
|
|
|
357
399
|
// local floor-plan draft handler (column / spawn / shelf / etc.).
|
|
358
400
|
// The tool's `grid:click` subscriber owns the placement.
|
|
359
401
|
if (isFloorplanGridInteractionActive) {
|
|
360
|
-
const
|
|
361
|
-
|
|
362
|
-
|
|
402
|
+
const eventPoint = resolveGenericFloorplanGridEventPoint({
|
|
403
|
+
point: planPoint,
|
|
404
|
+
registryToolOwnsSnapping,
|
|
405
|
+
snap: getSnappedFloorplanPoint,
|
|
406
|
+
})
|
|
407
|
+
emitFloorplanGridEvent('click', eventPoint, event)
|
|
408
|
+
setCursorPoint(eventPoint)
|
|
363
409
|
return true
|
|
364
410
|
}
|
|
365
411
|
|
|
@@ -392,6 +438,8 @@ export function useFloorplanBackgroundPlacement({
|
|
|
392
438
|
isZoneBuildActive,
|
|
393
439
|
levelId,
|
|
394
440
|
roofDraftStart,
|
|
441
|
+
registryToolOwnsSnapping,
|
|
442
|
+
roofFootprintSource,
|
|
395
443
|
setCursorPoint,
|
|
396
444
|
setFenceDraftEnd,
|
|
397
445
|
setFenceDraftStart,
|
|
@@ -4,8 +4,11 @@ import {
|
|
|
4
4
|
type AnyNode,
|
|
5
5
|
type AnyNodeId,
|
|
6
6
|
type FenceNode,
|
|
7
|
+
getFenceCenterlineFrameAt,
|
|
8
|
+
getFenceCenterlineLength,
|
|
7
9
|
getWallBaseElevationForNodes,
|
|
8
10
|
getWallCurveFrameAt,
|
|
11
|
+
getWallCurveLength,
|
|
9
12
|
getWallEffectiveHeightForNodes,
|
|
10
13
|
getWallThickness,
|
|
11
14
|
isCurvedWall,
|
|
@@ -16,6 +19,7 @@ import {
|
|
|
16
19
|
type WallNode,
|
|
17
20
|
} from '@pascal-app/core'
|
|
18
21
|
import { useViewer } from '@pascal-app/viewer'
|
|
22
|
+
import { Html } from '@react-three/drei'
|
|
19
23
|
import { createPortal, type ThreeEvent, useFrame, useThree } from '@react-three/fiber'
|
|
20
24
|
import { useEffect, useMemo, useRef, useState } from 'react'
|
|
21
25
|
import {
|
|
@@ -29,6 +33,7 @@ import {
|
|
|
29
33
|
OrthographicCamera,
|
|
30
34
|
Plane,
|
|
31
35
|
Quaternion,
|
|
36
|
+
Ray,
|
|
32
37
|
Shape,
|
|
33
38
|
Vector2,
|
|
34
39
|
Vector3,
|
|
@@ -37,6 +42,7 @@ import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js
|
|
|
37
42
|
import { MeshBasicNodeMaterial } from 'three/webgpu'
|
|
38
43
|
import {
|
|
39
44
|
clearStructuralElevationGuide,
|
|
45
|
+
getFenceBaseElevationForNodes,
|
|
40
46
|
publishStructuralElevationGuide,
|
|
41
47
|
resolveStructuralElevationSnap,
|
|
42
48
|
} from '../../lib/elevation-guides'
|
|
@@ -52,6 +58,7 @@ import useInteractionScope, {
|
|
|
52
58
|
import { suppressBoxSelectForPointer } from '../tools/select/box-select-state'
|
|
53
59
|
import { resolveResizeSnapValue } from './handles/resize-snap'
|
|
54
60
|
import { type HandleDragControls, useHandleDrag } from './handles/use-handle-drag'
|
|
61
|
+
import { MeasurementPill } from './measurement-pill'
|
|
55
62
|
import {
|
|
56
63
|
createArrowHitAreaGeometry,
|
|
57
64
|
createEndpointHitAreaGeometry,
|
|
@@ -76,6 +83,8 @@ const CORNER_DASH_SIZE = 0.1
|
|
|
76
83
|
const CORNER_GAP_SIZE = 0.07
|
|
77
84
|
const CORNER_DASH_THICKNESS = 0.006
|
|
78
85
|
const CORNER_FLOOR_OFFSET = 0.01
|
|
86
|
+
const THICKNESS_HANDLE_RADIUS = 0.075
|
|
87
|
+
const MIN_WALL_THICKNESS = 0.05
|
|
79
88
|
|
|
80
89
|
type WallMoveHandle = {
|
|
81
90
|
key: string
|
|
@@ -140,14 +149,11 @@ export function WallMoveSideHandles() {
|
|
|
140
149
|
const isCurveReshape = useIsCurveReshape()
|
|
141
150
|
|
|
142
151
|
const selectedId = selectedIds.length === 1 ? selectedIds[0] : null
|
|
143
|
-
//
|
|
144
|
-
//
|
|
145
|
-
// Only walls still need the legacy renderer here — the registry path
|
|
146
|
-
// didn't render correctly for walls specifically and was reverted in
|
|
147
|
-
// commit 0e207a7f; revisit once that's diagnosed.
|
|
152
|
+
// Walls still use this legacy handle renderer. Fences retain their registry
|
|
153
|
+
// handles and mount only the matching thickness dots from this component.
|
|
148
154
|
const selectedNode = useScene((state) => {
|
|
149
155
|
const node = selectedId ? state.nodes[selectedId as AnyNodeId] : null
|
|
150
|
-
return node?.type === 'wall' ? node : null
|
|
156
|
+
return node?.type === 'wall' || node?.type === 'fence' ? node : null
|
|
151
157
|
})
|
|
152
158
|
|
|
153
159
|
const shouldRender =
|
|
@@ -160,7 +166,11 @@ export function WallMoveSideHandles() {
|
|
|
160
166
|
|
|
161
167
|
if (!shouldRender || !selectedNode) return null
|
|
162
168
|
|
|
163
|
-
return
|
|
169
|
+
return selectedNode.type === 'wall' ? (
|
|
170
|
+
<WallMoveSideHandlesForWall wall={selectedNode} />
|
|
171
|
+
) : (
|
|
172
|
+
<FenceThicknessHandles fence={selectedNode} />
|
|
173
|
+
)
|
|
164
174
|
}
|
|
165
175
|
|
|
166
176
|
function WallMoveSideHandlesForWall({ wall }: { wall: WallNode }) {
|
|
@@ -220,6 +230,18 @@ function WallMoveSideHandlesForWall({ wall }: { wall: WallNode }) {
|
|
|
220
230
|
<WallMoveArrowHandle handle={handle} key={handle.key} wall={effectiveWall} />
|
|
221
231
|
))}
|
|
222
232
|
<WallHeightArrowHandle wall={effectiveWall} />
|
|
233
|
+
<StructureThicknessHandle
|
|
234
|
+
baseElevation={baseElevation}
|
|
235
|
+
levelObject={levelObject}
|
|
236
|
+
node={effectiveWall}
|
|
237
|
+
side={1}
|
|
238
|
+
/>
|
|
239
|
+
<StructureThicknessHandle
|
|
240
|
+
baseElevation={baseElevation}
|
|
241
|
+
levelObject={levelObject}
|
|
242
|
+
node={effectiveWall}
|
|
243
|
+
side={-1}
|
|
244
|
+
/>
|
|
223
245
|
<WallCornerLeaderHandle endpoint="start" wall={effectiveWall} />
|
|
224
246
|
<WallCornerLeaderHandle endpoint="end" wall={effectiveWall} />
|
|
225
247
|
</group>
|
|
@@ -233,6 +255,276 @@ function WallMoveSideHandlesForWall({ wall }: { wall: WallNode }) {
|
|
|
233
255
|
)
|
|
234
256
|
}
|
|
235
257
|
|
|
258
|
+
function closestAxisParameterToRay(axisOrigin: Vector3, axisDirection: Vector3, ray: Ray) {
|
|
259
|
+
const originToRay = new Vector3().subVectors(axisOrigin, ray.origin)
|
|
260
|
+
const directionDot = axisDirection.dot(ray.direction)
|
|
261
|
+
const axisDot = axisDirection.dot(originToRay)
|
|
262
|
+
const rayDot = ray.direction.dot(originToRay)
|
|
263
|
+
const denominator = 1 - directionDot * directionDot
|
|
264
|
+
if (Math.abs(denominator) < 1e-6) return -axisDot
|
|
265
|
+
|
|
266
|
+
const axisParameter = (directionDot * rayDot - axisDot) / denominator
|
|
267
|
+
const rayParameter = rayDot + directionDot * axisParameter
|
|
268
|
+
return rayParameter < 0 ? -axisDot : axisParameter
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function StructureThicknessHandle({
|
|
272
|
+
node,
|
|
273
|
+
side,
|
|
274
|
+
baseElevation,
|
|
275
|
+
levelObject,
|
|
276
|
+
}: {
|
|
277
|
+
node: WallNode | FenceNode
|
|
278
|
+
side: 1 | -1
|
|
279
|
+
baseElevation: number
|
|
280
|
+
levelObject: Object3D
|
|
281
|
+
}) {
|
|
282
|
+
const [isHovered, setIsHovered] = useState(false)
|
|
283
|
+
const [isDragging, setIsDragging] = useState(false)
|
|
284
|
+
const { camera } = useThree()
|
|
285
|
+
const unit = useViewer((state) => state.unit)
|
|
286
|
+
const zoom = camera instanceof OrthographicCamera ? 1 / camera.zoom : 1
|
|
287
|
+
const frame =
|
|
288
|
+
node.type === 'fence' ? getFenceCenterlineFrameAt(node, 0.5) : getWallCurveFrameAt(node, 0.5)
|
|
289
|
+
const thickness = node.type === 'fence' ? (node.thickness ?? 0.08) : getWallThickness(node)
|
|
290
|
+
const structureHeight =
|
|
291
|
+
node.type === 'fence'
|
|
292
|
+
? (node.height ?? 1.8)
|
|
293
|
+
: getWallEffectiveHeightForNodes(node, useScene.getState().nodes)
|
|
294
|
+
const outward = new Vector2(frame.normal.x * side, frame.normal.y * side)
|
|
295
|
+
const faceOffset = thickness / 2 + 0.006
|
|
296
|
+
const position: [number, number, number] = [
|
|
297
|
+
frame.point.x + outward.x * faceOffset,
|
|
298
|
+
structureHeight / 2,
|
|
299
|
+
frame.point.y + outward.y * faceOffset,
|
|
300
|
+
]
|
|
301
|
+
const rotationY = Math.atan2(outward.x, outward.y)
|
|
302
|
+
const isActive = isHovered || isDragging
|
|
303
|
+
const scale = zoom * (isActive ? 1.18 : 1)
|
|
304
|
+
const hitGeometry = useMemo(() => createEndpointHitAreaGeometry(0.13), [])
|
|
305
|
+
const hitMaterial = useInvisibleHitAreaMaterial()
|
|
306
|
+
const dotMaterial = useMemo(
|
|
307
|
+
() =>
|
|
308
|
+
new MeshBasicNodeMaterial({
|
|
309
|
+
color: new Color(ARROW_COLOR),
|
|
310
|
+
side: DoubleSide,
|
|
311
|
+
depthTest: false,
|
|
312
|
+
depthWrite: false,
|
|
313
|
+
}),
|
|
314
|
+
[],
|
|
315
|
+
)
|
|
316
|
+
const ringMaterial = useMemo(
|
|
317
|
+
() =>
|
|
318
|
+
new MeshBasicNodeMaterial({
|
|
319
|
+
color: new Color(ARROW_HOVER_COLOR),
|
|
320
|
+
side: DoubleSide,
|
|
321
|
+
depthTest: false,
|
|
322
|
+
depthWrite: false,
|
|
323
|
+
}),
|
|
324
|
+
[],
|
|
325
|
+
)
|
|
326
|
+
const dragControls = useMemo<HandleDragControls>(
|
|
327
|
+
() => ({
|
|
328
|
+
onStart: () => {},
|
|
329
|
+
onEnd: () => {},
|
|
330
|
+
}),
|
|
331
|
+
[],
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
useEffect(() => {
|
|
335
|
+
dotMaterial.color.set(isActive ? ARROW_HOVER_COLOR : ARROW_COLOR)
|
|
336
|
+
}, [dotMaterial, isActive])
|
|
337
|
+
useEffect(() => () => hitGeometry.dispose(), [hitGeometry])
|
|
338
|
+
useEffect(() => () => dotMaterial.dispose(), [dotMaterial])
|
|
339
|
+
useEffect(() => () => ringMaterial.dispose(), [ringMaterial])
|
|
340
|
+
useEffect(
|
|
341
|
+
() => () => {
|
|
342
|
+
if (document.body.style.cursor === 'ew-resize') document.body.style.cursor = ''
|
|
343
|
+
},
|
|
344
|
+
[],
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
const activateThicknessResize = useHandleDrag({
|
|
348
|
+
kind: 'drag',
|
|
349
|
+
cursor: 'ew-resize',
|
|
350
|
+
dragControls,
|
|
351
|
+
handleIndex: side > 0 ? 0 : 1,
|
|
352
|
+
node,
|
|
353
|
+
rideObject: levelObject,
|
|
354
|
+
setIsDragging,
|
|
355
|
+
onStart: ({ event, getPointerRay, initialNode, nodeId, rideObject }) => {
|
|
356
|
+
if (initialNode.type !== 'wall' && initialNode.type !== 'fence') return null
|
|
357
|
+
|
|
358
|
+
rideObject.updateWorldMatrix(true, false)
|
|
359
|
+
const initialFrame =
|
|
360
|
+
initialNode.type === 'fence'
|
|
361
|
+
? getFenceCenterlineFrameAt(initialNode, 0.5)
|
|
362
|
+
: getWallCurveFrameAt(initialNode, 0.5)
|
|
363
|
+
const localAxis = new Vector3(initialFrame.normal.x * side, 0, initialFrame.normal.y * side)
|
|
364
|
+
const initialStructureHeight =
|
|
365
|
+
initialNode.type === 'fence'
|
|
366
|
+
? (initialNode.height ?? 1.8)
|
|
367
|
+
: getWallEffectiveHeightForNodes(initialNode, useScene.getState().nodes)
|
|
368
|
+
const initialStructureThickness =
|
|
369
|
+
initialNode.type === 'fence'
|
|
370
|
+
? (initialNode.thickness ?? 0.08)
|
|
371
|
+
: getWallThickness(initialNode)
|
|
372
|
+
const minimumThickness = initialNode.type === 'fence' ? 0.03 : MIN_WALL_THICKNESS
|
|
373
|
+
const localOrigin = new Vector3(
|
|
374
|
+
initialFrame.point.x + localAxis.x * (initialStructureThickness / 2 + 0.006),
|
|
375
|
+
baseElevation + initialStructureHeight / 2,
|
|
376
|
+
initialFrame.point.y + localAxis.z * (initialStructureThickness / 2 + 0.006),
|
|
377
|
+
)
|
|
378
|
+
const worldOrigin = localOrigin.clone().applyMatrix4(rideObject.matrixWorld)
|
|
379
|
+
const worldAxisEnd = localOrigin.clone().add(localAxis).applyMatrix4(rideObject.matrixWorld)
|
|
380
|
+
const worldAxis = worldAxisEnd.sub(worldOrigin)
|
|
381
|
+
const axisScale = worldAxis.length()
|
|
382
|
+
if (axisScale < 1e-6) return null
|
|
383
|
+
worldAxis.normalize()
|
|
384
|
+
|
|
385
|
+
const pointerRay = new Ray()
|
|
386
|
+
const initialPointer =
|
|
387
|
+
closestAxisParameterToRay(
|
|
388
|
+
worldOrigin,
|
|
389
|
+
worldAxis,
|
|
390
|
+
getPointerRay(event.nativeEvent.clientX, event.nativeEvent.clientY, pointerRay),
|
|
391
|
+
) / axisScale
|
|
392
|
+
let lastThickness = initialStructureThickness
|
|
393
|
+
|
|
394
|
+
return {
|
|
395
|
+
onBegin: () => {
|
|
396
|
+
useInteractionScope.getState().begin({
|
|
397
|
+
kind: 'handle-drag',
|
|
398
|
+
nodeId,
|
|
399
|
+
handle: 'thickness',
|
|
400
|
+
})
|
|
401
|
+
},
|
|
402
|
+
onEnd: () => {
|
|
403
|
+
useInteractionScope.getState().endIf((scope) => scope.kind === 'handle-drag')
|
|
404
|
+
},
|
|
405
|
+
move: ({ event: moveEvent, getPointerRay: getMovePointerRay }) => {
|
|
406
|
+
const currentPointer =
|
|
407
|
+
closestAxisParameterToRay(
|
|
408
|
+
worldOrigin,
|
|
409
|
+
worldAxis,
|
|
410
|
+
getMovePointerRay(moveEvent.clientX, moveEvent.clientY, pointerRay),
|
|
411
|
+
) / axisScale
|
|
412
|
+
const rawThickness = initialStructureThickness + (currentPointer - initialPointer) * 2
|
|
413
|
+
const nextThickness = Math.max(
|
|
414
|
+
minimumThickness,
|
|
415
|
+
resolveResizeSnapValue({
|
|
416
|
+
rawValue: rawThickness,
|
|
417
|
+
fallbackValue: lastThickness,
|
|
418
|
+
gridSnapEnabled: true,
|
|
419
|
+
gridSnapActive: isGridSnapActive(),
|
|
420
|
+
gridSnapStep: useEditor.getState().gridSnapStep,
|
|
421
|
+
magneticSnapActive: false,
|
|
422
|
+
}),
|
|
423
|
+
)
|
|
424
|
+
if (nextThickness !== lastThickness) sfxEmitter.emit('sfx:resize')
|
|
425
|
+
lastThickness = nextThickness
|
|
426
|
+
return { thickness: nextThickness }
|
|
427
|
+
},
|
|
428
|
+
}
|
|
429
|
+
},
|
|
430
|
+
})
|
|
431
|
+
|
|
432
|
+
return (
|
|
433
|
+
<>
|
|
434
|
+
<group position={position} rotation={[0, rotationY, 0]} scale={scale}>
|
|
435
|
+
<InvisibleHandleHitArea
|
|
436
|
+
geometry={hitGeometry}
|
|
437
|
+
material={hitMaterial}
|
|
438
|
+
onPointerDown={activateThicknessResize}
|
|
439
|
+
onPointerEnter={(event) => {
|
|
440
|
+
event.stopPropagation()
|
|
441
|
+
setIsHovered(true)
|
|
442
|
+
document.body.style.cursor = 'ew-resize'
|
|
443
|
+
}}
|
|
444
|
+
onPointerLeave={(event) => {
|
|
445
|
+
event.stopPropagation()
|
|
446
|
+
setIsHovered(false)
|
|
447
|
+
if (!isDragging && document.body.style.cursor === 'ew-resize') {
|
|
448
|
+
document.body.style.cursor = ''
|
|
449
|
+
}
|
|
450
|
+
}}
|
|
451
|
+
scale={1}
|
|
452
|
+
/>
|
|
453
|
+
<mesh material={dotMaterial} raycast={NO_RAYCAST} renderOrder={1003}>
|
|
454
|
+
<circleGeometry args={[THICKNESS_HANDLE_RADIUS, 32]} />
|
|
455
|
+
</mesh>
|
|
456
|
+
<mesh material={ringMaterial} raycast={NO_RAYCAST} renderOrder={1002}>
|
|
457
|
+
<ringGeometry args={[THICKNESS_HANDLE_RADIUS, THICKNESS_HANDLE_RADIUS * 1.18, 32]} />
|
|
458
|
+
</mesh>
|
|
459
|
+
</group>
|
|
460
|
+
{isDragging ? (
|
|
461
|
+
<Html
|
|
462
|
+
center
|
|
463
|
+
position={[position[0], position[1] + 0.22, position[2]]}
|
|
464
|
+
style={{ pointerEvents: 'none', userSelect: 'none' }}
|
|
465
|
+
zIndexRange={[25, 0]}
|
|
466
|
+
>
|
|
467
|
+
<MeasurementPill
|
|
468
|
+
height={structureHeight}
|
|
469
|
+
length={
|
|
470
|
+
node.type === 'fence' ? getFenceCenterlineLength(node) : getWallCurveLength(node)
|
|
471
|
+
}
|
|
472
|
+
primary="thickness"
|
|
473
|
+
thickness={thickness}
|
|
474
|
+
unit={unit}
|
|
475
|
+
/>
|
|
476
|
+
</Html>
|
|
477
|
+
) : null}
|
|
478
|
+
</>
|
|
479
|
+
)
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function FenceThicknessHandles({ fence }: { fence: FenceNode }) {
|
|
483
|
+
const nodes = useScene((state) => state.nodes)
|
|
484
|
+
const liveOverride = useLiveNodeOverrides((state) => state.overrides.get(fence.id))
|
|
485
|
+
const effectiveFence = useMemo(
|
|
486
|
+
() => (liveOverride ? ({ ...fence, ...liveOverride } as FenceNode) : fence),
|
|
487
|
+
[fence, liveOverride],
|
|
488
|
+
)
|
|
489
|
+
const [levelObject, setLevelObject] = useState<Object3D | null>(() =>
|
|
490
|
+
fence.parentId ? (sceneRegistry.nodes.get(fence.parentId) ?? null) : null,
|
|
491
|
+
)
|
|
492
|
+
|
|
493
|
+
useEffect(() => {
|
|
494
|
+
let frameId = 0
|
|
495
|
+
const resolveLevelObject = () => {
|
|
496
|
+
const next = fence.parentId ? (sceneRegistry.nodes.get(fence.parentId) ?? null) : null
|
|
497
|
+
setLevelObject(next)
|
|
498
|
+
if (!next) frameId = window.requestAnimationFrame(resolveLevelObject)
|
|
499
|
+
}
|
|
500
|
+
resolveLevelObject()
|
|
501
|
+
return () => {
|
|
502
|
+
if (frameId) window.cancelAnimationFrame(frameId)
|
|
503
|
+
}
|
|
504
|
+
}, [fence.parentId])
|
|
505
|
+
|
|
506
|
+
if (!levelObject) return null
|
|
507
|
+
|
|
508
|
+
const baseElevation = getFenceBaseElevationForNodes(effectiveFence, nodes)
|
|
509
|
+
return createPortal(
|
|
510
|
+
<group position={[0, baseElevation, 0]}>
|
|
511
|
+
<StructureThicknessHandle
|
|
512
|
+
baseElevation={baseElevation}
|
|
513
|
+
levelObject={levelObject}
|
|
514
|
+
node={effectiveFence}
|
|
515
|
+
side={1}
|
|
516
|
+
/>
|
|
517
|
+
<StructureThicknessHandle
|
|
518
|
+
baseElevation={baseElevation}
|
|
519
|
+
levelObject={levelObject}
|
|
520
|
+
node={effectiveFence}
|
|
521
|
+
side={-1}
|
|
522
|
+
/>
|
|
523
|
+
</group>,
|
|
524
|
+
levelObject,
|
|
525
|
+
)
|
|
526
|
+
}
|
|
527
|
+
|
|
236
528
|
function buildDashedVerticalGeometry(height: number) {
|
|
237
529
|
if (!(Number.isFinite(height) && height > 0)) return new BufferGeometry()
|
|
238
530
|
|
|
@@ -39,10 +39,13 @@ import {
|
|
|
39
39
|
collectParticipants,
|
|
40
40
|
computeGroupBox,
|
|
41
41
|
expandToComponent,
|
|
42
|
+
type GroupPlanBounds,
|
|
43
|
+
groupPlanBounds,
|
|
42
44
|
levelFrame,
|
|
43
|
-
|
|
45
|
+
planBoundsCenter,
|
|
44
46
|
rotateGroupPatches,
|
|
45
47
|
rotateGroupSnapshots,
|
|
48
|
+
rotatePlanBounds,
|
|
46
49
|
translateGroupPatches,
|
|
47
50
|
type Vec2,
|
|
48
51
|
} from '../editor/group-transform-shared'
|
|
@@ -112,6 +115,9 @@ export function startFloorplanGroupMove(
|
|
|
112
115
|
affectedIds: AnyNodeId[]
|
|
113
116
|
candidates: ReturnType<typeof collectAlignmentAnchors>
|
|
114
117
|
restAnchors: ReturnType<typeof bboxCornerAnchors>
|
|
118
|
+
startBounds: GroupPlanBounds
|
|
119
|
+
restBounds: GroupPlanBounds
|
|
120
|
+
rotation: number
|
|
115
121
|
restCenter: Vec2
|
|
116
122
|
lastDelta: Vec2 | null
|
|
117
123
|
}
|
|
@@ -140,20 +146,16 @@ export function startFloorplanGroupMove(
|
|
|
140
146
|
// The group aligns as one rigid footprint: its bbox corners + center are
|
|
141
147
|
// the moving anchors. `computeGroupBox` is world-space (the 3D scene stays
|
|
142
148
|
// mounted under every view mode); plan coords are level-frame, so convert.
|
|
143
|
-
const restBox = computeGroupBox(fullIds)
|
|
144
149
|
const { inverse: frameInv } = levelFrame(levelId)
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
const restAnchors =
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
Math.max(boxMin.z, boxMax.z),
|
|
155
|
-
)
|
|
156
|
-
: []
|
|
150
|
+
const restBounds = groupPlanBounds(computeGroupBox(fullIds), starts, frameInv)
|
|
151
|
+
if (!restBounds) return null
|
|
152
|
+
const restAnchors = bboxCornerAnchors(
|
|
153
|
+
'group-move',
|
|
154
|
+
restBounds.minX,
|
|
155
|
+
restBounds.minZ,
|
|
156
|
+
restBounds.maxX,
|
|
157
|
+
restBounds.maxZ,
|
|
158
|
+
)
|
|
157
159
|
|
|
158
160
|
for (const id of affectedIds) {
|
|
159
161
|
useLiveTransforms.getState().clear(id)
|
|
@@ -169,12 +171,23 @@ export function startFloorplanGroupMove(
|
|
|
169
171
|
nodeId,
|
|
170
172
|
handle: GROUP_MOVE_DRAG_LABEL,
|
|
171
173
|
})
|
|
172
|
-
// Rotation pivot for mid-drag R/T — the
|
|
173
|
-
//
|
|
174
|
-
|
|
175
|
-
const restCenter
|
|
174
|
+
// Rotation pivot for mid-drag R/T — the START footprint's center, the same
|
|
175
|
+
// point the 3D body drag, the idle keyboard rotate and the rotate gizmos
|
|
176
|
+
// orbit. Stable across the drag; rotations re-seed around the same point.
|
|
177
|
+
const restCenter = planBoundsCenter(restBounds)
|
|
176
178
|
|
|
177
|
-
return {
|
|
179
|
+
return {
|
|
180
|
+
starts,
|
|
181
|
+
links,
|
|
182
|
+
affectedIds,
|
|
183
|
+
candidates,
|
|
184
|
+
restAnchors,
|
|
185
|
+
startBounds: restBounds,
|
|
186
|
+
restBounds,
|
|
187
|
+
rotation: 0,
|
|
188
|
+
restCenter,
|
|
189
|
+
lastDelta: null,
|
|
190
|
+
}
|
|
178
191
|
}
|
|
179
192
|
|
|
180
193
|
const applyMove = (e: PointerEvent, s: Session) => {
|
|
@@ -241,18 +254,20 @@ export function startFloorplanGroupMove(
|
|
|
241
254
|
// current delta — the carried group turns exactly like the idle keyboard
|
|
242
255
|
// rotate, and the commit stays a single updateNodes.
|
|
243
256
|
const rotateSession = (s: Session, direction: 1 | -1) => {
|
|
244
|
-
const
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
{ x: s.restCenter[0], z: s.restCenter[1] },
|
|
248
|
-
-direction * (Math.PI / 4),
|
|
249
|
-
)
|
|
257
|
+
const pivot = { x: s.restCenter[0], z: s.restCenter[1] }
|
|
258
|
+
const delta = -direction * (Math.PI / 4)
|
|
259
|
+
const rotated = rotateGroupSnapshots(s.starts, s.links, pivot, delta)
|
|
250
260
|
s.starts = rotated.starts
|
|
251
261
|
s.links = rotated.links
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
262
|
+
s.rotation += delta
|
|
263
|
+
s.restBounds = rotatePlanBounds(s.startBounds, pivot, s.rotation)
|
|
264
|
+
s.restAnchors = bboxCornerAnchors(
|
|
265
|
+
'group-move',
|
|
266
|
+
s.restBounds.minX,
|
|
267
|
+
s.restBounds.minZ,
|
|
268
|
+
s.restBounds.maxX,
|
|
269
|
+
s.restBounds.maxZ,
|
|
270
|
+
)
|
|
256
271
|
sfxEmitter.emit('sfx:item-rotate')
|
|
257
272
|
applyDelta(s, s.lastDelta?.[0] ?? 0, s.lastDelta?.[1] ?? 0)
|
|
258
273
|
}
|
|
@@ -358,7 +373,22 @@ export function startFloorplanGroupMove(
|
|
|
358
373
|
const onKeyDown = (e: KeyboardEvent) => {
|
|
359
374
|
const key = e.key.toLowerCase()
|
|
360
375
|
if ((key === 'r' || key === 't') && !e.metaKey && !e.ctrlKey && !e.altKey && !e.shiftKey) {
|
|
361
|
-
|
|
376
|
+
// Armed but still under the drag threshold: engage first (exactly what
|
|
377
|
+
// the next pointer-move would do) so the rotation lands inside this
|
|
378
|
+
// session. Falling through to the global idle arm instead would write
|
|
379
|
+
// the scene behind snapshots already captured here, and the first
|
|
380
|
+
// `applyDelta` would republish them — undoing the rotation.
|
|
381
|
+
if (!session) {
|
|
382
|
+
session = engage()
|
|
383
|
+
if (!session) {
|
|
384
|
+
// No plane hit yet: swallow the chord and keep the gesture armed so
|
|
385
|
+
// the next pointer-move can still engage; the idle arm must not run
|
|
386
|
+
// behind the snapshots captured here.
|
|
387
|
+
e.preventDefault()
|
|
388
|
+
e.stopPropagation()
|
|
389
|
+
return
|
|
390
|
+
}
|
|
391
|
+
}
|
|
362
392
|
e.preventDefault()
|
|
363
393
|
e.stopPropagation()
|
|
364
394
|
rotateSession(session, key === 'r' ? 1 : -1)
|
|
@@ -415,9 +445,13 @@ export function startFloorplanGroupRotate(event: {
|
|
|
415
445
|
const { starts, links } = collectParticipants(fullIds, nodes, levelId)
|
|
416
446
|
if (starts.length === 0) return false
|
|
417
447
|
const affectedIds: AnyNodeId[] = [...starts.map((s) => s.id), ...links.map((l) => l.id)]
|
|
418
|
-
const
|
|
419
|
-
|
|
420
|
-
|
|
448
|
+
const { inverse: frameInv } = levelFrame(levelId)
|
|
449
|
+
const bounds = groupPlanBounds(computeGroupBox(fullIds), starts, frameInv)
|
|
450
|
+
if (!bounds) return false
|
|
451
|
+
// Same pivot as the dashed box the handles hang off (and as the 3D rotate
|
|
452
|
+
// gizmo): its centre, not the anchor points' centre.
|
|
453
|
+
const [pivotX, pivotZ] = planBoundsCenter(bounds)
|
|
454
|
+
const pivot = { x: pivotX, z: pivotZ }
|
|
421
455
|
const startPlan = clientToPlan(event.clientX, event.clientY)
|
|
422
456
|
if (!startPlan) return false
|
|
423
457
|
// Bearing around the pivot in the plan frame — the same atan2 x→z sense
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { createSceneApi, nodeRegistry, useScene } from '@pascal-app/core'
|
|
4
4
|
import { useViewer } from '@pascal-app/viewer'
|
|
5
5
|
import { type ComponentType, lazy, Suspense, useCallback, useMemo } from 'react'
|
|
6
|
+
import { useRegisteredToolEnabled } from '../../hooks/use-registered-tool-enabled'
|
|
6
7
|
import {
|
|
7
8
|
type FloorplanToolContext,
|
|
8
9
|
getFloorplanNodeExtension,
|
|
@@ -35,6 +36,7 @@ function registeredFloorplanTool(
|
|
|
35
36
|
export function FloorplanRegisteredToolLayer() {
|
|
36
37
|
const mode = useEditor((state) => state.mode)
|
|
37
38
|
const tool = useEditor((state) => state.tool)
|
|
39
|
+
const registeredToolEnabled = useRegisteredToolEnabled(tool)
|
|
38
40
|
const floorplanMode = useFloorplanMode((state) => state.mode)
|
|
39
41
|
const gridSnapStep = useEditor((state) => state.gridSnapStep)
|
|
40
42
|
const toolDefaults = useEditor((state) =>
|
|
@@ -54,7 +56,7 @@ export function FloorplanRegisteredToolLayer() {
|
|
|
54
56
|
useEditor.getState().setMode('select')
|
|
55
57
|
}, [])
|
|
56
58
|
if (mode !== 'build') return null
|
|
57
|
-
const Tool = registeredFloorplanTool(tool, floorplanMode)
|
|
59
|
+
const Tool = registeredToolEnabled ? registeredFloorplanTool(tool, floorplanMode) : null
|
|
58
60
|
return Tool ? (
|
|
59
61
|
<Suspense fallback={null}>
|
|
60
62
|
<Tool
|