@pascal-app/editor 0.7.0 → 0.9.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/package.json +12 -13
- package/src/components/editor/alignment-3d-guide-layer.tsx +160 -0
- package/src/components/editor/custom-camera-controls.tsx +755 -25
- package/src/components/editor/editor-layout-mobile.tsx +3 -3
- package/src/components/editor/editor-layout-v2.tsx +62 -54
- package/src/components/editor/export-manager.tsx +32 -3
- package/src/components/editor/first-person/build-collider-world.test.ts +149 -0
- package/src/components/editor/first-person/build-collider-world.ts +203 -87
- package/src/components/editor/first-person/bvh-ecctrl.tsx +127 -62
- package/src/components/editor/first-person-controls.tsx +836 -31
- package/src/components/editor/floating-action-menu.tsx +287 -196
- package/src/components/editor/floating-building-action-menu.tsx +1 -1
- package/src/components/editor/floorplan-panel.tsx +5837 -12006
- package/src/components/editor/grid.tsx +51 -17
- package/src/components/editor/group-move-handle.tsx +316 -0
- package/src/components/editor/group-rotate-handle.tsx +382 -0
- package/src/components/editor/group-transform-shared.test.ts +205 -0
- package/src/components/editor/group-transform-shared.ts +256 -0
- package/src/components/editor/handles/handle-arrow.tsx +523 -0
- package/src/components/editor/handles/use-handle-drag.ts +213 -0
- package/src/components/editor/index.tsx +234 -190
- package/src/components/editor/measurement-pill.tsx +75 -0
- package/src/components/editor/node-arrow-handles.tsx +1453 -0
- package/src/components/editor/selection-manager.tsx +435 -158
- package/src/components/editor/site-edge-labels.tsx +44 -20
- package/src/components/editor/slab-hole-highlights.tsx +466 -0
- package/src/components/editor/snapshot-capture-overlay.tsx +539 -0
- package/src/components/editor/thumbnail-generator.tsx +27 -64
- package/src/components/editor/use-floorplan-background-placement.ts +122 -35
- package/src/components/editor/use-floorplan-hit-testing.ts +24 -0
- package/src/components/editor/use-floorplan-scene-data.ts +14 -2
- package/src/components/editor/wall-measurement-label.tsx +11 -6
- package/src/components/editor/wall-move-side-handles.tsx +855 -0
- package/src/components/editor/wall-opening-highlights.tsx +181 -0
- package/src/components/editor/wall-snap-beacon-layer.tsx +150 -0
- package/src/components/editor-2d/floorplan-action-menu-layer.tsx +6 -1
- package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +142 -0
- package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +5 -3
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +227 -0
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +616 -0
- package/src/components/editor-2d/floorplan-render-context.tsx +62 -0
- package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +75 -0
- package/src/components/editor-2d/floorplan-wall-move-ghost-layer.tsx +47 -0
- package/src/components/editor-2d/renderers/floorplan-draft-layer.tsx +6 -1
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +266 -0
- package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +108 -61
- package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +59 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +1963 -0
- package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +29 -14
- package/src/components/editor-2d/svg-paths.ts +10 -2
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +30 -9
- package/src/components/systems/ceiling/ceiling-system.tsx +12 -8
- package/src/components/systems/roof/roof-edit-system.tsx +86 -12
- package/src/components/systems/stair/stair-edit-system.tsx +2 -2
- package/src/components/systems/zone/zone-label-editor-system.tsx +0 -0
- package/src/components/systems/zone/zone-system.tsx +0 -0
- package/src/components/tools/elevator/elevator-defaults.ts +8 -0
- package/src/components/tools/elevator/elevator-tool.tsx +349 -0
- package/src/components/tools/elevator/move-elevator-tool.tsx +252 -0
- package/src/components/tools/fence/fence-drafting.ts +64 -22
- package/src/components/tools/item/move-tool.tsx +40 -107
- package/src/components/tools/item/placement-math.test.ts +10 -0
- package/src/components/tools/item/placement-math.ts +33 -9
- package/src/components/tools/item/placement-strategies.ts +201 -25
- package/src/components/tools/item/placement-types.ts +10 -1
- package/src/components/tools/item/use-draft-node.ts +20 -2
- package/src/components/tools/item/use-placement-coordinator.tsx +700 -299
- package/src/components/tools/registry/move-registry-node-tool.tsx +602 -0
- package/src/components/tools/roof/roof-tool.tsx +108 -12
- package/src/components/tools/select/box-select-state.ts +77 -0
- package/src/components/tools/select/box-select-tool.tsx +324 -543
- package/src/components/tools/select/plane-box-select-tool.tsx +577 -0
- package/src/components/tools/select/screen-rectangle-selection.ts +84 -0
- package/src/components/tools/select/select-candidates.test.ts +109 -0
- package/src/components/tools/select/select-candidates.ts +85 -0
- package/src/components/tools/shared/affordance-dispatch.ts +30 -0
- package/src/components/tools/shared/drag-bounding-box.tsx +171 -0
- package/src/components/tools/shared/floor-stack-preview.ts +25 -0
- package/src/components/tools/shared/fresh-placement-visibility.ts +61 -0
- package/src/components/tools/shared/placement-box-geometry.ts +123 -0
- package/src/components/tools/shared/placement-box.tsx +123 -0
- package/src/components/tools/shared/polygon-editor.tsx +617 -167
- package/src/components/tools/shared/segment-angle.ts +57 -1
- package/src/components/tools/site/site-boundary-editor.tsx +464 -12
- package/src/components/tools/site/site-flag-model.ts +18 -0
- package/src/components/tools/stair/stair-defaults.ts +1 -1
- package/src/components/tools/stair/stair-tool.tsx +269 -35
- package/src/components/tools/tool-manager.tsx +172 -56
- package/src/components/tools/wall/wall-drafting.ts +98 -98
- package/src/components/tools/wall/wall-snap-geometry.test.ts +79 -0
- package/src/components/tools/wall/wall-snap-geometry.ts +270 -0
- package/src/components/tools/zone/zone-tool.tsx +36 -11
- package/src/components/ui/action-menu/action-button.tsx +21 -1
- package/src/components/ui/action-menu/camera-actions.tsx +0 -0
- package/src/components/ui/action-menu/control-modes.tsx +5 -76
- package/src/components/ui/action-menu/furnish-tools.tsx +6 -92
- package/src/components/ui/action-menu/index.tsx +33 -196
- package/src/components/ui/action-menu/structure-tools.tsx +13 -88
- package/src/components/ui/action-menu/view-toggles.tsx +262 -37
- package/src/components/ui/command-palette/editor-commands.tsx +24 -17
- package/src/components/ui/command-palette/index.tsx +6 -256
- package/src/components/ui/controls/material-paint-panel.tsx +83 -0
- package/src/components/ui/controls/material-picker.tsx +17 -35
- package/src/components/ui/controls/metric-control.tsx +33 -11
- package/src/components/ui/floating-level-selector.tsx +48 -11
- package/src/components/ui/helpers/helper-manager.tsx +21 -17
- package/src/components/ui/helpers/registered-tool-helper.tsx +25 -0
- package/src/components/ui/item-catalog/catalog-items.tsx +1743 -315
- package/src/components/ui/item-catalog/item-catalog.tsx +91 -46
- package/src/components/ui/level-duplicate-dialog.tsx +5 -6
- package/src/components/ui/panels/node-display.ts +1 -0
- package/src/components/ui/panels/paint-panel.tsx +66 -41
- package/src/components/ui/panels/panel-manager.tsx +33 -74
- package/src/components/ui/panels/panel-wrapper.tsx +217 -12
- package/src/components/ui/panels/parametric-inspector.tsx +469 -0
- package/src/components/ui/panels/reference-panel.tsx +28 -13
- package/src/components/ui/primitives/dropdown-menu.tsx +12 -8
- package/src/components/ui/primitives/number-input.tsx +1 -1
- package/src/components/ui/primitives/sidebar.tsx +0 -0
- package/src/components/ui/scene-loader.tsx +1 -3
- package/src/components/ui/sidebar/app-sidebar.tsx +0 -0
- package/src/components/ui/sidebar/icon-rail.tsx +0 -0
- package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +263 -0
- package/src/components/ui/sidebar/panels/items-panel/index.tsx +404 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +59 -9
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +6 -1
- package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +259 -0
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +3 -1
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +5 -2
- package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +83 -0
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +89 -0
- package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +75 -0
- package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +2 -2
- package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +83 -0
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +8 -12
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +2 -1
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +93 -0
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +41 -5
- package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +128 -0
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +5 -2
- package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +83 -0
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +68 -34
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +3 -1
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +52 -3
- package/src/components/ui/sidebar/tab-bar.tsx +61 -1
- package/src/components/ui/slider.tsx +1 -1
- package/src/components/viewer-overlay.tsx +169 -52
- package/src/components/viewer-zone-system.tsx +1 -1
- package/src/hooks/use-auto-save.ts +24 -3
- package/src/hooks/use-ceiling-events.ts +175 -0
- package/src/hooks/use-drag-action.ts +124 -0
- package/src/hooks/use-keyboard.ts +125 -62
- package/src/hooks/use-selection.ts +64 -0
- package/src/index.tsx +243 -4
- package/src/lib/constants.ts +6 -2
- package/src/lib/editor-api.ts +62 -0
- package/src/lib/elevator-support.ts +96 -0
- package/src/lib/floorplan/apply-alignment.ts +111 -0
- package/src/lib/floorplan/index.ts +7 -0
- package/src/lib/floorplan/selection-tool.ts +40 -0
- package/src/lib/floorplan/stairs.ts +12 -1
- package/src/lib/fresh-planar-placement.test.ts +102 -0
- package/src/lib/fresh-planar-placement.ts +63 -0
- package/src/lib/history.ts +2 -1
- package/src/lib/level-duplication.test.ts +0 -2
- package/src/lib/level-duplication.ts +55 -43
- package/src/lib/level-name.ts +11 -0
- package/src/lib/material-paint.ts +212 -20
- package/src/lib/placement-metadata.ts +29 -0
- package/src/lib/planar-cursor-placement.test.ts +43 -0
- package/src/lib/planar-cursor-placement.ts +42 -0
- package/src/lib/roof-duplication.ts +2 -2
- package/src/lib/scene-bounds.ts +1 -1
- package/src/lib/scene-clipboard.ts +267 -0
- package/src/lib/scene.ts +13 -4
- package/src/lib/sfx-bus.ts +25 -1
- package/src/lib/sfx-player.ts +75 -27
- package/src/lib/site-boundary.ts +1 -0
- package/src/lib/stair-duplication.ts +2 -2
- package/src/lib/stair-levels.test.ts +146 -0
- package/src/lib/stair-levels.ts +177 -0
- package/src/lib/world-grid-snap.ts +199 -0
- package/src/lib/zone-content.ts +130 -0
- package/src/store/use-alignment-guides.ts +21 -0
- package/src/store/use-editor.tsx +342 -91
- package/src/store/use-placement-preview.ts +31 -0
- package/src/store/use-wall-move-ghosts.ts +36 -0
- package/src/store/use-wall-snap-indicator.ts +32 -0
- package/tsconfig.json +2 -1
- package/src/components/editor/preset-thumbnail-generator.tsx +0 -125
- package/src/components/editor-2d/renderers/floorplan-roof-layer.tsx +0 -113
- package/src/components/feedback-dialog.tsx +0 -265
- package/src/components/pascal-radio.tsx +0 -280
- package/src/components/preview-button.tsx +0 -16
- package/src/components/tools/building/move-building-tool.tsx +0 -157
- package/src/components/tools/ceiling/ceiling-boundary-editor.tsx +0 -43
- package/src/components/tools/ceiling/ceiling-hole-editor.tsx +0 -49
- package/src/components/tools/ceiling/ceiling-tool.tsx +0 -465
- package/src/components/tools/ceiling/move-ceiling-tool.tsx +0 -257
- package/src/components/tools/column/column-tool.tsx +0 -97
- package/src/components/tools/column/move-column-tool.tsx +0 -105
- package/src/components/tools/door/door-math.ts +0 -110
- package/src/components/tools/door/door-tool.tsx +0 -324
- package/src/components/tools/door/move-door-tool.tsx +0 -412
- package/src/components/tools/fence/curve-fence-tool.tsx +0 -179
- package/src/components/tools/fence/fence-tool.tsx +0 -346
- package/src/components/tools/fence/move-fence-endpoint-tool.tsx +0 -438
- package/src/components/tools/fence/move-fence-tool.tsx +0 -298
- package/src/components/tools/item/item-tool.tsx +0 -26
- package/src/components/tools/roof/move-roof-tool.tsx +0 -359
- package/src/components/tools/slab/move-slab-tool.tsx +0 -182
- package/src/components/tools/slab/slab-boundary-editor.tsx +0 -43
- package/src/components/tools/slab/slab-hole-editor.tsx +0 -49
- package/src/components/tools/slab/slab-tool.tsx +0 -322
- package/src/components/tools/spawn/move-spawn-tool.tsx +0 -101
- package/src/components/tools/spawn/spawn-tool.tsx +0 -130
- package/src/components/tools/wall/curve-wall-tool.tsx +0 -176
- package/src/components/tools/wall/move-wall-endpoint-tool.tsx +0 -423
- package/src/components/tools/wall/move-wall-tool.tsx +0 -356
- package/src/components/tools/wall/wall-tool.tsx +0 -332
- package/src/components/tools/window/move-window-tool.tsx +0 -447
- package/src/components/tools/window/window-math.ts +0 -117
- package/src/components/tools/window/window-tool.tsx +0 -332
- package/src/components/ui/helpers/ceiling-helper.tsx +0 -20
- package/src/components/ui/helpers/slab-helper.tsx +0 -20
- package/src/components/ui/helpers/wall-helper.tsx +0 -20
- package/src/components/ui/panels/ceiling-panel.tsx +0 -250
- package/src/components/ui/panels/column-panel.tsx +0 -715
- package/src/components/ui/panels/door-panel.tsx +0 -1292
- package/src/components/ui/panels/fence-panel.tsx +0 -227
- package/src/components/ui/panels/item-panel.tsx +0 -306
- package/src/components/ui/panels/presets/presets-popover.tsx +0 -511
- package/src/components/ui/panels/roof-panel.tsx +0 -230
- package/src/components/ui/panels/roof-segment-panel.tsx +0 -311
- package/src/components/ui/panels/slab-panel.tsx +0 -250
- package/src/components/ui/panels/spawn-panel.tsx +0 -155
- package/src/components/ui/panels/stair-panel.tsx +0 -525
- package/src/components/ui/panels/stair-segment-panel.tsx +0 -325
- package/src/components/ui/panels/wall-panel.tsx +0 -177
- package/src/components/ui/panels/window-panel.tsx +0 -960
- package/src/components/ui/viewer-toolbar.tsx +0 -436
- package/src/contexts/presets-context.tsx +0 -121
- package/src/hooks/use-contextual-tools.ts +0 -61
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { emitter, sceneRegistry
|
|
4
|
-
import { SSGI_PARAMS, snapLevelsToTruePositions, useViewer } from '@pascal-app/viewer'
|
|
3
|
+
import { emitter, sceneRegistry } from '@pascal-app/core'
|
|
4
|
+
import { GRID_LAYER, SSGI_PARAMS, snapLevelsToTruePositions, useViewer } from '@pascal-app/viewer'
|
|
5
5
|
import type { CameraControls } from '@react-three/drei'
|
|
6
6
|
import { useThree } from '@react-three/fiber'
|
|
7
7
|
import { useCallback, useEffect, useRef } from 'react'
|
|
@@ -28,7 +28,6 @@ import { EDITOR_LAYER } from '../../lib/constants'
|
|
|
28
28
|
|
|
29
29
|
const THUMBNAIL_WIDTH = 1920
|
|
30
30
|
const THUMBNAIL_HEIGHT = 1080
|
|
31
|
-
const AUTO_SAVE_DELAY = 10_000
|
|
32
31
|
|
|
33
32
|
export interface SnapshotCameraData {
|
|
34
33
|
position: [number, number, number]
|
|
@@ -49,8 +48,6 @@ export const ThumbnailGenerator = ({ onThumbnailCapture }: ThumbnailGeneratorPro
|
|
|
49
48
|
const mainCamera = useThree((state) => state.camera)
|
|
50
49
|
const controls = useThree((state) => state.controls) as CameraControls | null
|
|
51
50
|
const isGenerating = useRef(false)
|
|
52
|
-
const debounceTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
53
|
-
const pendingAutoRef = useRef(false)
|
|
54
51
|
const onThumbnailCaptureRef = useRef(onThumbnailCapture)
|
|
55
52
|
|
|
56
53
|
const thumbnailCameraRef = useRef<THREE.PerspectiveCamera | null>(null)
|
|
@@ -61,10 +58,11 @@ export const ThumbnailGenerator = ({ onThumbnailCapture }: ThumbnailGeneratorPro
|
|
|
61
58
|
onThumbnailCaptureRef.current = onThumbnailCapture
|
|
62
59
|
}, [onThumbnailCapture])
|
|
63
60
|
|
|
64
|
-
// Build the thumbnail camera, SSGI pipeline, and render target once
|
|
61
|
+
// Build the thumbnail camera, SSGI pipeline, and render target once — reused on every capture.
|
|
65
62
|
useEffect(() => {
|
|
66
63
|
const cam = new THREE.PerspectiveCamera(60, THUMBNAIL_WIDTH / THUMBNAIL_HEIGHT, 0.1, 1000)
|
|
67
64
|
cam.layers.disable(EDITOR_LAYER)
|
|
65
|
+
cam.layers.disable(GRID_LAYER)
|
|
68
66
|
thumbnailCameraRef.current = cam
|
|
69
67
|
|
|
70
68
|
let mounted = true
|
|
@@ -75,8 +73,8 @@ export const ThumbnailGenerator = ({ onThumbnailCapture }: ThumbnailGeneratorPro
|
|
|
75
73
|
if (!mounted) return
|
|
76
74
|
|
|
77
75
|
// pass() handles MRT internally for all material types, including custom
|
|
78
|
-
// shaders
|
|
79
|
-
// pass() also respects camera.layers, so EDITOR_LAYER objects are filtered.
|
|
76
|
+
// shaders — unlike renderer.setMRT() which crashes on non-NodeMaterials.
|
|
77
|
+
// pass() also respects camera.layers, so EDITOR_LAYER + GRID_LAYER objects are filtered.
|
|
80
78
|
const scenePass = pass(scene, cam)
|
|
81
79
|
scenePass.setMRT(
|
|
82
80
|
mrt({
|
|
@@ -117,15 +115,15 @@ export const ThumbnailGenerator = ({ onThumbnailCapture }: ThumbnailGeneratorPro
|
|
|
117
115
|
const ao = (denoisePass as any).r
|
|
118
116
|
const finalOutput = vec4(scenePassColor.rgb.mul(ao), scenePassColor.a)
|
|
119
117
|
|
|
120
|
-
// FXAA requires a texture node as input
|
|
121
|
-
// into an intermediate RT so FXAA can sample it with
|
|
118
|
+
// FXAA requires a texture node as input; convertToTexture renders finalOutput
|
|
119
|
+
// into an intermediate RT so FXAA can sample it with neighbour UV offsets.
|
|
122
120
|
const aaOutput = fxaa(convertToTexture(finalOutput))
|
|
123
121
|
|
|
124
122
|
const pipeline = new RenderPipeline(gl as unknown as WebGPURenderer)
|
|
125
123
|
pipeline.outputNode = aaOutput
|
|
126
124
|
pipelineRef.current = pipeline
|
|
127
125
|
|
|
128
|
-
// Dedicated render target
|
|
126
|
+
// Dedicated render target — pipeline outputs here instead of the canvas,
|
|
129
127
|
// so R3F's main render loop can never overwrite our capture.
|
|
130
128
|
const { width, height } = gl.domElement
|
|
131
129
|
renderTargetRef.current = new RenderTarget(width, height, { depthBuffer: true })
|
|
@@ -176,7 +174,7 @@ export const ThumbnailGenerator = ({ onThumbnailCapture }: ThumbnailGeneratorPro
|
|
|
176
174
|
thumbnailCamera.aspect = width / height
|
|
177
175
|
thumbnailCamera.updateProjectionMatrix()
|
|
178
176
|
|
|
179
|
-
// Capture camera data for snapshot storage
|
|
177
|
+
// Capture camera data for snapshot storage
|
|
180
178
|
const pos = mainCamera.position
|
|
181
179
|
let tgt: [number, number, number] | null = null
|
|
182
180
|
if (controls && 'getTarget' in controls) {
|
|
@@ -192,7 +190,7 @@ export const ThumbnailGenerator = ({ onThumbnailCapture }: ThumbnailGeneratorPro
|
|
|
192
190
|
...(isOrtho && { zoom: (mainCamera as THREE.OrthographicCamera).zoom }),
|
|
193
191
|
}
|
|
194
192
|
|
|
195
|
-
// For auto-save
|
|
193
|
+
// For auto-save: snap levels to stacked positions and reset levelMode
|
|
196
194
|
let restoreLevelMode: (() => void) | null = null
|
|
197
195
|
let restoreLevels: () => void = () => {}
|
|
198
196
|
if (snapLevels) {
|
|
@@ -205,12 +203,12 @@ export const ThumbnailGenerator = ({ onThumbnailCapture }: ThumbnailGeneratorPro
|
|
|
205
203
|
}
|
|
206
204
|
|
|
207
205
|
// Hide scan and guide nodes directly so they are excluded from the
|
|
208
|
-
// thumbnail regardless of whether ScanSystem
|
|
206
|
+
// thumbnail regardless of whether ScanSystem/GuideSystem listeners are
|
|
209
207
|
// registered. Returns a function that restores the original visibility.
|
|
210
208
|
const restoreNodeVisibility = (() => {
|
|
211
209
|
const saved = new Map<THREE.Object3D, boolean>()
|
|
212
210
|
for (const type of ['scan', 'guide'] as const) {
|
|
213
|
-
const ids = sceneRegistry.byType[type]
|
|
211
|
+
const ids = sceneRegistry.byType[type]!
|
|
214
212
|
ids.forEach((id) => {
|
|
215
213
|
const node = sceneRegistry.nodes.get(id)
|
|
216
214
|
if (node) {
|
|
@@ -231,7 +229,7 @@ export const ThumbnailGenerator = ({ onThumbnailCapture }: ThumbnailGeneratorPro
|
|
|
231
229
|
if (pipelineRef.current && renderTargetRef.current) {
|
|
232
230
|
const rt = renderTargetRef.current
|
|
233
231
|
|
|
234
|
-
// Resize RT if the canvas dimensions changed
|
|
232
|
+
// Resize RT if the canvas dimensions changed
|
|
235
233
|
if (rt.width !== width || rt.height !== height) {
|
|
236
234
|
rt.setSize(width, height)
|
|
237
235
|
}
|
|
@@ -248,7 +246,7 @@ export const ThumbnailGenerator = ({ onThumbnailCapture }: ThumbnailGeneratorPro
|
|
|
248
246
|
emitter.emit('thumbnail:after-capture', undefined)
|
|
249
247
|
|
|
250
248
|
// Restore level positions, levelMode, and node visibility immediately after the
|
|
251
|
-
// render
|
|
249
|
+
// render — before the async GPU readback.
|
|
252
250
|
restoreLevels()
|
|
253
251
|
restoreLevelMode?.()
|
|
254
252
|
restoreNodeVisibility()
|
|
@@ -339,6 +337,7 @@ export const ThumbnailGenerator = ({ onThumbnailCapture }: ThumbnailGeneratorPro
|
|
|
339
337
|
offscreen.getContext('2d')!.drawImage(srcCanvas, sx, sy, outW, outH, 0, 0, outW, outH)
|
|
340
338
|
blob = await offscreen.convertToBlob({ type: 'image/png' })
|
|
341
339
|
} else {
|
|
340
|
+
// Standard: center-crop to 1920×1080 aspect ratio
|
|
342
341
|
const srcAspect = width / height
|
|
343
342
|
const dstAspect = THUMBNAIL_WIDTH / THUMBNAIL_HEIGHT
|
|
344
343
|
let sx = 0,
|
|
@@ -364,7 +363,7 @@ export const ThumbnailGenerator = ({ onThumbnailCapture }: ThumbnailGeneratorPro
|
|
|
364
363
|
if (captureMode !== undefined) cameraData.captureMode = captureMode
|
|
365
364
|
cameraData.resolution = { w: outW, h: outH }
|
|
366
365
|
} else {
|
|
367
|
-
// Fallback: plain render directly to the canvas
|
|
366
|
+
// Fallback: plain render directly to the canvas
|
|
368
367
|
emitter.emit('thumbnail:before-capture', undefined)
|
|
369
368
|
gl.render(scene, thumbnailCamera)
|
|
370
369
|
emitter.emit('thumbnail:after-capture', undefined)
|
|
@@ -450,10 +449,11 @@ export const ThumbnailGenerator = ({ onThumbnailCapture }: ThumbnailGeneratorPro
|
|
|
450
449
|
)
|
|
451
450
|
|
|
452
451
|
// Thumbnail request via emitter. Two call shapes:
|
|
453
|
-
// - user-driven capture: `{ projectId, captureMode, cropRegion }
|
|
452
|
+
// - user-driven capture: `{ projectId, captureMode, cropRegion }` — captures
|
|
454
453
|
// the current pose with the supplied crop.
|
|
455
|
-
// - auto-save
|
|
456
|
-
// their true positions first for a consistent auto-thumbnail angle.
|
|
454
|
+
// - host-driven auto-save: `{ projectId, snapLevels: true }` — snaps levels
|
|
455
|
+
// to their true positions first for a consistent auto-thumbnail angle.
|
|
456
|
+
// The caller owns policy (when to fire, whether the tab is visible).
|
|
457
457
|
useEffect(() => {
|
|
458
458
|
if (!onThumbnailCapture) return
|
|
459
459
|
|
|
@@ -461,6 +461,11 @@ export const ThumbnailGenerator = ({ onThumbnailCapture }: ThumbnailGeneratorPro
|
|
|
461
461
|
captureMode?: 'standard' | 'viewport' | 'area'
|
|
462
462
|
cropRegion?: { x: number; y: number; width: number; height: number }
|
|
463
463
|
snapLevels?: boolean
|
|
464
|
+
// `transparent` is informational here — the render pipeline already
|
|
465
|
+
// captures with alpha (see `setClearAlpha(0)` above) — the flag is
|
|
466
|
+
// forwarded so future tweaks (suppressing the ground occluder, theme
|
|
467
|
+
// background bits) can branch on it without touching the emitter.
|
|
468
|
+
transparent?: boolean
|
|
464
469
|
}) => {
|
|
465
470
|
await generate(event.snapLevels === true, event.captureMode, event.cropRegion)
|
|
466
471
|
}
|
|
@@ -469,49 +474,7 @@ export const ThumbnailGenerator = ({ onThumbnailCapture }: ThumbnailGeneratorPro
|
|
|
469
474
|
return () => emitter.off('camera-controls:generate-thumbnail', handleGenerateThumbnail)
|
|
470
475
|
}, [generate, onThumbnailCapture])
|
|
471
476
|
|
|
472
|
-
//
|
|
473
|
-
// community host-side autosave hook is not part of this repo.
|
|
474
|
-
useEffect(() => {
|
|
475
|
-
if (!onThumbnailCapture) return
|
|
476
|
-
|
|
477
|
-
const triggerNow = () => {
|
|
478
|
-
void generate(true)
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
const scheduleOrDefer = () => {
|
|
482
|
-
if (document.visibilityState === 'visible') {
|
|
483
|
-
triggerNow()
|
|
484
|
-
} else {
|
|
485
|
-
pendingAutoRef.current = true
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
const onSceneChange = () => {
|
|
490
|
-
if (debounceTimerRef.current) clearTimeout(debounceTimerRef.current)
|
|
491
|
-
debounceTimerRef.current = setTimeout(scheduleOrDefer, AUTO_SAVE_DELAY)
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
const onVisibilityChange = () => {
|
|
495
|
-
if (document.visibilityState === 'visible' && pendingAutoRef.current) {
|
|
496
|
-
pendingAutoRef.current = false
|
|
497
|
-
triggerNow()
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
const unsubscribe = useScene.subscribe((state, prevState) => {
|
|
502
|
-
if (state.nodes !== prevState.nodes) onSceneChange()
|
|
503
|
-
})
|
|
504
|
-
|
|
505
|
-
document.addEventListener('visibilitychange', onVisibilityChange)
|
|
506
|
-
|
|
507
|
-
return () => {
|
|
508
|
-
if (debounceTimerRef.current) clearTimeout(debounceTimerRef.current)
|
|
509
|
-
unsubscribe()
|
|
510
|
-
document.removeEventListener('visibilitychange', onVisibilityChange)
|
|
511
|
-
}
|
|
512
|
-
}, [generate, onThumbnailCapture])
|
|
513
|
-
|
|
514
|
-
// Go-to-camera: animate camera to a saved snapshot position or target.
|
|
477
|
+
// Go-to-camera: animate camera to a saved snapshot position/target
|
|
515
478
|
useEffect(() => {
|
|
516
479
|
const handler = ({
|
|
517
480
|
position,
|
|
@@ -2,9 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
import { emitter, type FenceNode, isCurvedWall, type WallNode } from '@pascal-app/core'
|
|
4
4
|
import { type MouseEvent as ReactMouseEvent, useCallback } from 'react'
|
|
5
|
-
import { getPlanPointDistance } from '../../lib/floorplan'
|
|
5
|
+
import { alignFloorplanDraftPoint, getPlanPointDistance } from '../../lib/floorplan'
|
|
6
6
|
import { snapFenceDraftPoint } from '../tools/fence/fence-drafting'
|
|
7
|
-
import
|
|
7
|
+
import {
|
|
8
|
+
WALL_FINE_GRID_STEP,
|
|
9
|
+
WALL_GRID_STEP,
|
|
10
|
+
type WallPlanPoint,
|
|
11
|
+
} from '../tools/wall/wall-drafting'
|
|
8
12
|
|
|
9
13
|
type UseFloorplanBackgroundPlacementArgs = {
|
|
10
14
|
activePolygonDraftPoints: WallPlanPoint[]
|
|
@@ -30,11 +34,16 @@ type UseFloorplanBackgroundPlacementArgs = {
|
|
|
30
34
|
} | null
|
|
31
35
|
floorplanOpeningLocalY: number
|
|
32
36
|
getSnappedFloorplanPoint: (point: WallPlanPoint) => WallPlanPoint
|
|
37
|
+
handleCeilingItemPlacementClick: (
|
|
38
|
+
planPoint: WallPlanPoint,
|
|
39
|
+
nativeEvent: ReactMouseEvent<SVGSVGElement>,
|
|
40
|
+
) => boolean
|
|
33
41
|
handleCeilingPlacementPoint: (point: WallPlanPoint) => void
|
|
34
42
|
handleSlabPlacementPoint: (point: WallPlanPoint) => void
|
|
35
|
-
handleWallPlacementPoint: (point: WallPlanPoint) => void
|
|
43
|
+
handleWallPlacementPoint: (point: WallPlanPoint, options?: { singleWall?: boolean }) => void
|
|
36
44
|
handleZonePlacementPoint: (point: WallPlanPoint) => void
|
|
37
45
|
isCeilingBuildActive: boolean
|
|
46
|
+
isCeilingItemPlacementActive: boolean
|
|
38
47
|
isFenceBuildActive: boolean
|
|
39
48
|
isFloorplanGridInteractionActive: boolean
|
|
40
49
|
isOpeningPlacementActive: boolean
|
|
@@ -53,7 +62,9 @@ type UseFloorplanBackgroundPlacementArgs = {
|
|
|
53
62
|
point: WallPlanPoint
|
|
54
63
|
walls: WallNode[]
|
|
55
64
|
start?: WallPlanPoint
|
|
56
|
-
angleSnap
|
|
65
|
+
angleSnap?: boolean
|
|
66
|
+
step?: number
|
|
67
|
+
gridSnap?: (point: WallPlanPoint) => WallPlanPoint
|
|
57
68
|
}) => WallPlanPoint
|
|
58
69
|
snapPolygonDraftPoint: (args: {
|
|
59
70
|
point: WallPlanPoint
|
|
@@ -62,6 +73,13 @@ type UseFloorplanBackgroundPlacementArgs = {
|
|
|
62
73
|
}) => WallPlanPoint
|
|
63
74
|
toPoint2D: (point: WallPlanPoint) => { x: number; y: number }
|
|
64
75
|
walls: WallNode[]
|
|
76
|
+
/**
|
|
77
|
+
* Snap a building-local plan point to the world XZ grid at `step`.
|
|
78
|
+
* Injected so the hook doesn't have to know the building's rotation
|
|
79
|
+
* or position — used by wall / fence branches that snap at variable
|
|
80
|
+
* step (Shift = fine).
|
|
81
|
+
*/
|
|
82
|
+
worldGridSnap: (point: WallPlanPoint, step: number) => WallPlanPoint
|
|
65
83
|
}
|
|
66
84
|
|
|
67
85
|
export function useFloorplanBackgroundPlacement({
|
|
@@ -75,11 +93,13 @@ export function useFloorplanBackgroundPlacement({
|
|
|
75
93
|
findClosestWallPoint,
|
|
76
94
|
floorplanOpeningLocalY,
|
|
77
95
|
getSnappedFloorplanPoint,
|
|
96
|
+
handleCeilingItemPlacementClick,
|
|
78
97
|
handleCeilingPlacementPoint,
|
|
79
98
|
handleSlabPlacementPoint,
|
|
80
99
|
handleWallPlacementPoint,
|
|
81
100
|
handleZonePlacementPoint,
|
|
82
101
|
isCeilingBuildActive,
|
|
102
|
+
isCeilingItemPlacementActive,
|
|
83
103
|
isFenceBuildActive,
|
|
84
104
|
isFloorplanGridInteractionActive,
|
|
85
105
|
isOpeningPlacementActive,
|
|
@@ -98,6 +118,7 @@ export function useFloorplanBackgroundPlacement({
|
|
|
98
118
|
snapPolygonDraftPoint,
|
|
99
119
|
toPoint2D,
|
|
100
120
|
walls,
|
|
121
|
+
worldGridSnap,
|
|
101
122
|
}: UseFloorplanBackgroundPlacementArgs) {
|
|
102
123
|
const handleBackgroundPlacementClick = useCallback(
|
|
103
124
|
(
|
|
@@ -127,43 +148,61 @@ export function useFloorplanBackgroundPlacement({
|
|
|
127
148
|
}
|
|
128
149
|
|
|
129
150
|
if (isCeilingBuildActive) {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
151
|
+
// Align the committed vertex the same way the move-preview did, so
|
|
152
|
+
// the placed point matches what the user saw. Skip when angle snap
|
|
153
|
+
// owns the vertex (matches the move branch).
|
|
154
|
+
const angleSnap = ceilingDraftPoints.length > 0 && !shiftPressed
|
|
155
|
+
let snappedPoint = snapPolygonDraftPoint({
|
|
133
156
|
point: planPoint,
|
|
134
157
|
start: ceilingDraftPoints[ceilingDraftPoints.length - 1],
|
|
135
|
-
angleSnap
|
|
158
|
+
angleSnap,
|
|
136
159
|
})
|
|
160
|
+
if (!angleSnap) {
|
|
161
|
+
snappedPoint = alignFloorplanDraftPoint(snappedPoint, { bypass: event.altKey })
|
|
162
|
+
}
|
|
137
163
|
|
|
164
|
+
emitFloorplanGridEvent('click', snappedPoint, event)
|
|
138
165
|
handleCeilingPlacementPoint(snappedPoint)
|
|
139
166
|
return true
|
|
140
167
|
}
|
|
141
168
|
|
|
142
169
|
if (isRoofBuildActive) {
|
|
143
|
-
const snappedPoint = getSnappedFloorplanPoint(planPoint)
|
|
170
|
+
const snappedPoint = alignFloorplanDraftPoint(getSnappedFloorplanPoint(planPoint), {
|
|
171
|
+
bypass: event.altKey,
|
|
172
|
+
})
|
|
144
173
|
emitFloorplanGridEvent('click', snappedPoint, event)
|
|
145
174
|
setCursorPoint(snappedPoint)
|
|
146
175
|
|
|
147
|
-
if (
|
|
176
|
+
if (roofDraftStart) {
|
|
177
|
+
clearRoofPlacementDraft()
|
|
178
|
+
} else {
|
|
148
179
|
setRoofDraftStart(snappedPoint)
|
|
149
180
|
setRoofDraftEnd(snappedPoint)
|
|
150
|
-
} else {
|
|
151
|
-
clearRoofPlacementDraft()
|
|
152
181
|
}
|
|
153
182
|
return true
|
|
154
183
|
}
|
|
155
184
|
|
|
156
185
|
if (isFenceBuildActive) {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
186
|
+
// Fence draft: grid snap (+ existing-wall/fence endpoint snap), then
|
|
187
|
+
// Figma alignment — endpoint snap wins (same precedence as move).
|
|
188
|
+
// `gridSnap` keeps the snap on the world XZ grid even when the
|
|
189
|
+
// building is rotated.
|
|
190
|
+
const fenceStep = shiftPressed ? WALL_FINE_GRID_STEP : WALL_GRID_STEP
|
|
191
|
+
const fenceSnapped = snapFenceDraftPoint({
|
|
160
192
|
point: planPoint,
|
|
161
193
|
walls,
|
|
162
194
|
fences,
|
|
163
|
-
|
|
164
|
-
|
|
195
|
+
step: shiftPressed ? WALL_FINE_GRID_STEP : undefined,
|
|
196
|
+
gridSnap: (p) => worldGridSnap(p, fenceStep),
|
|
165
197
|
})
|
|
198
|
+
const fenceGridBase = worldGridSnap(planPoint, fenceStep)
|
|
199
|
+
const fenceLocked =
|
|
200
|
+
fenceSnapped[0] !== fenceGridBase[0] || fenceSnapped[1] !== fenceGridBase[1]
|
|
201
|
+
const snappedPoint = fenceLocked
|
|
202
|
+
? fenceSnapped
|
|
203
|
+
: alignFloorplanDraftPoint(fenceSnapped, { bypass: event.altKey })
|
|
166
204
|
|
|
205
|
+
emitFloorplanGridEvent('click', snappedPoint, event)
|
|
167
206
|
setCursorPoint(snappedPoint)
|
|
168
207
|
|
|
169
208
|
if (!fenceDraftStart) {
|
|
@@ -179,18 +218,28 @@ export function useFloorplanBackgroundPlacement({
|
|
|
179
218
|
return true
|
|
180
219
|
}
|
|
181
220
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
221
|
+
// Slab / zone polygon build — local draft state + grid emit.
|
|
222
|
+
// Must run BEFORE the `isFloorplanGridInteractionActive` catch-all
|
|
223
|
+
// (since slab is registry-driven, the catch-all would otherwise
|
|
224
|
+
// swallow the click and skip local draft state updates — leaving
|
|
225
|
+
// the 2D draft polygon invisible while the 3D tool builds fine).
|
|
188
226
|
if (isPolygonBuildActive) {
|
|
189
|
-
const
|
|
227
|
+
const angleSnap = activePolygonDraftPoints.length > 0 && !shiftPressed
|
|
228
|
+
let snappedPoint = snapPolygonDraftPoint({
|
|
190
229
|
point: planPoint,
|
|
191
230
|
start: activePolygonDraftPoints[activePolygonDraftPoints.length - 1],
|
|
192
|
-
angleSnap
|
|
231
|
+
angleSnap,
|
|
193
232
|
})
|
|
233
|
+
if (!angleSnap) {
|
|
234
|
+
snappedPoint = alignFloorplanDraftPoint(snappedPoint, { bypass: event.altKey })
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Emit the grid event so the registry-driven slab tool also
|
|
238
|
+
// sees the click (parity with ceiling / fence / roof branches
|
|
239
|
+
// above). Zone has no registry tool — emit-or-not is irrelevant.
|
|
240
|
+
if (!isZoneBuildActive) {
|
|
241
|
+
emitFloorplanGridEvent('click', snappedPoint, event)
|
|
242
|
+
}
|
|
194
243
|
|
|
195
244
|
if (isZoneBuildActive) {
|
|
196
245
|
handleZonePlacementPoint(snappedPoint)
|
|
@@ -200,19 +249,54 @@ export function useFloorplanBackgroundPlacement({
|
|
|
200
249
|
return true
|
|
201
250
|
}
|
|
202
251
|
|
|
203
|
-
|
|
204
|
-
|
|
252
|
+
// Wall placement — local draft state + grid emit. Same reasoning
|
|
253
|
+
// as slab above: wall is registry-driven, so without this branch
|
|
254
|
+
// the catch-all would swallow the click and the local draftStart
|
|
255
|
+
// / draftEnd state in the floor plan would never update, leaving
|
|
256
|
+
// the dashed-line draft preview invisible.
|
|
257
|
+
if (isWallBuildActive) {
|
|
258
|
+
// Wall draft: grid snap (+ existing-wall endpoint/join snap), then
|
|
259
|
+
// Figma alignment — endpoint/join snap wins (same precedence as the
|
|
260
|
+
// move-preview branch), so committing onto a corner still works.
|
|
261
|
+
// `gridSnap` keeps the snap on the world XZ grid even when the
|
|
262
|
+
// building is rotated.
|
|
263
|
+
const wallStep = shiftPressed ? WALL_FINE_GRID_STEP : WALL_GRID_STEP
|
|
264
|
+
const wallSnapped = snapWallDraftPoint({
|
|
265
|
+
point: planPoint,
|
|
266
|
+
walls,
|
|
267
|
+
step: shiftPressed ? WALL_FINE_GRID_STEP : undefined,
|
|
268
|
+
gridSnap: (p) => worldGridSnap(p, wallStep),
|
|
269
|
+
})
|
|
270
|
+
const wallGridBase = worldGridSnap(planPoint, wallStep)
|
|
271
|
+
const wallLocked = wallSnapped[0] !== wallGridBase[0] || wallSnapped[1] !== wallGridBase[1]
|
|
272
|
+
const snappedPoint = wallLocked
|
|
273
|
+
? wallSnapped
|
|
274
|
+
: alignFloorplanDraftPoint(wallSnapped, { bypass: false })
|
|
275
|
+
|
|
276
|
+
emitFloorplanGridEvent('click', snappedPoint, event)
|
|
277
|
+
handleWallPlacementPoint(snappedPoint, { singleWall: event.altKey })
|
|
278
|
+
return true
|
|
205
279
|
}
|
|
206
280
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
281
|
+
// Ceiling-attached item placement (lights, fans). Routes the click
|
|
282
|
+
// through `ceiling:click` instead of `grid:click` so the placement
|
|
283
|
+
// strategy parents the new item to the ceiling at the correct
|
|
284
|
+
// height — mirrors the pointer-move handler in `floorplan-panel`.
|
|
285
|
+
if (isCeilingItemPlacementActive) {
|
|
286
|
+
handleCeilingItemPlacementClick(planPoint, event)
|
|
287
|
+
return true
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Generic catch-all — registry-driven tool whose kind has no
|
|
291
|
+
// local floor-plan draft handler (column / spawn / shelf / etc.).
|
|
292
|
+
// The tool's `grid:click` subscriber owns the placement.
|
|
293
|
+
if (isFloorplanGridInteractionActive) {
|
|
294
|
+
const snappedPoint = emitFloorplanGridEvent('click', planPoint, event)
|
|
295
|
+
setCursorPoint(snappedPoint)
|
|
296
|
+
return true
|
|
297
|
+
}
|
|
213
298
|
|
|
214
|
-
|
|
215
|
-
return true
|
|
299
|
+
return false
|
|
216
300
|
},
|
|
217
301
|
[
|
|
218
302
|
activePolygonDraftPoints,
|
|
@@ -225,10 +309,12 @@ export function useFloorplanBackgroundPlacement({
|
|
|
225
309
|
findClosestWallPoint,
|
|
226
310
|
floorplanOpeningLocalY,
|
|
227
311
|
getSnappedFloorplanPoint,
|
|
312
|
+
handleCeilingItemPlacementClick,
|
|
228
313
|
handleCeilingPlacementPoint,
|
|
229
314
|
handleSlabPlacementPoint,
|
|
230
315
|
handleZonePlacementPoint,
|
|
231
316
|
isCeilingBuildActive,
|
|
317
|
+
isCeilingItemPlacementActive,
|
|
232
318
|
isFenceBuildActive,
|
|
233
319
|
isFloorplanGridInteractionActive,
|
|
234
320
|
isOpeningPlacementActive,
|
|
@@ -248,6 +334,7 @@ export function useFloorplanBackgroundPlacement({
|
|
|
248
334
|
toPoint2D,
|
|
249
335
|
walls,
|
|
250
336
|
handleWallPlacementPoint,
|
|
337
|
+
worldGridSnap,
|
|
251
338
|
],
|
|
252
339
|
)
|
|
253
340
|
|
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
import type {
|
|
4
4
|
AnyNode,
|
|
5
5
|
CeilingNode,
|
|
6
|
+
ColumnNode,
|
|
6
7
|
DoorNode,
|
|
8
|
+
ElevatorNode,
|
|
7
9
|
ItemNode,
|
|
8
10
|
Point2D,
|
|
9
11
|
RoofNode,
|
|
@@ -46,6 +48,16 @@ type CeilingPolygonEntry = {
|
|
|
46
48
|
holes: Point2D[][]
|
|
47
49
|
}
|
|
48
50
|
|
|
51
|
+
type ColumnPolygonEntry = {
|
|
52
|
+
column: ColumnNode
|
|
53
|
+
polygon: Point2D[]
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
type ElevatorPolygonEntry = {
|
|
57
|
+
elevator: ElevatorNode
|
|
58
|
+
polygon: Point2D[]
|
|
59
|
+
}
|
|
60
|
+
|
|
49
61
|
type FloorplanRoofEntry = {
|
|
50
62
|
roof: RoofNode
|
|
51
63
|
segments: Array<{
|
|
@@ -72,8 +84,10 @@ type FloorplanStairEntry = {
|
|
|
72
84
|
|
|
73
85
|
type UseFloorplanHitTestingArgs = {
|
|
74
86
|
ceilingPolygons: CeilingPolygonEntry[]
|
|
87
|
+
columnPolygons: ColumnPolygonEntry[]
|
|
75
88
|
displaySlabPolygons: SlabPolygonEntry[]
|
|
76
89
|
displayWallPolygons: WallPolygonEntry[]
|
|
90
|
+
floorplanElevatorEntries: ElevatorPolygonEntry[]
|
|
77
91
|
floorplanItemEntries: FloorplanItemEntry[]
|
|
78
92
|
floorplanOpeningHitTolerance: number
|
|
79
93
|
floorplanRoofEntries: FloorplanRoofEntry[]
|
|
@@ -88,8 +102,10 @@ type UseFloorplanHitTestingArgs = {
|
|
|
88
102
|
|
|
89
103
|
export function useFloorplanHitTesting({
|
|
90
104
|
ceilingPolygons,
|
|
105
|
+
columnPolygons,
|
|
91
106
|
displaySlabPolygons,
|
|
92
107
|
displayWallPolygons,
|
|
108
|
+
floorplanElevatorEntries,
|
|
93
109
|
floorplanItemEntries,
|
|
94
110
|
floorplanOpeningHitTolerance,
|
|
95
111
|
floorplanRoofEntries,
|
|
@@ -113,18 +129,22 @@ export function useFloorplanHitTesting({
|
|
|
113
129
|
openings: openingsPolygons,
|
|
114
130
|
roofs: floorplanRoofEntries,
|
|
115
131
|
stairs: floorplanStairEntries,
|
|
132
|
+
elevators: floorplanElevatorEntries,
|
|
116
133
|
walls: displayWallPolygons,
|
|
117
134
|
slabs: displaySlabPolygons,
|
|
118
135
|
openingHitTolerance: floorplanOpeningHitTolerance,
|
|
119
136
|
wallHitTolerance: floorplanWallHitTolerance,
|
|
137
|
+
columns: columnPolygons,
|
|
120
138
|
getOpeningCenterLine,
|
|
121
139
|
})
|
|
122
140
|
},
|
|
123
141
|
[
|
|
124
142
|
ceilingPolygons,
|
|
143
|
+
columnPolygons,
|
|
125
144
|
displaySlabPolygons,
|
|
126
145
|
displayWallPolygons,
|
|
127
146
|
floorplanItemEntries,
|
|
147
|
+
floorplanElevatorEntries,
|
|
128
148
|
floorplanOpeningHitTolerance,
|
|
129
149
|
floorplanRoofEntries,
|
|
130
150
|
floorplanStairEntries,
|
|
@@ -149,13 +169,17 @@ export function useFloorplanHitTesting({
|
|
|
149
169
|
openings: openingsPolygons,
|
|
150
170
|
roofs: floorplanRoofEntries,
|
|
151
171
|
slabs: displaySlabPolygons,
|
|
172
|
+
columns: columnPolygons,
|
|
173
|
+
elevators: floorplanElevatorEntries,
|
|
152
174
|
stairs: floorplanStairEntries,
|
|
153
175
|
}),
|
|
154
176
|
[
|
|
155
177
|
ceilingPolygons,
|
|
178
|
+
columnPolygons,
|
|
156
179
|
displaySlabPolygons,
|
|
157
180
|
displayWallPolygons,
|
|
158
181
|
floorplanItemEntries,
|
|
182
|
+
floorplanElevatorEntries,
|
|
159
183
|
floorplanRoofEntries,
|
|
160
184
|
floorplanStairEntries,
|
|
161
185
|
isFloorplanItemContextActive,
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
type SiteNode,
|
|
13
13
|
type SlabNode,
|
|
14
14
|
type SpawnNode,
|
|
15
|
+
useLiveTransforms,
|
|
15
16
|
useScene,
|
|
16
17
|
type WallNode,
|
|
17
18
|
type WindowNode,
|
|
@@ -59,13 +60,22 @@ export function useFloorplanSceneData({
|
|
|
59
60
|
? (levelNode.parentId as BuildingNode['id'])
|
|
60
61
|
: buildingId
|
|
61
62
|
|
|
62
|
-
|
|
63
|
+
// Live transform override — when the building is mid-drag (the move
|
|
64
|
+
// tool publishes per-frame pose to useLiveTransforms), the floor-plan
|
|
65
|
+
// follows that pose so the dimmed reference floor tracks the cursor
|
|
66
|
+
// instead of snapping only on commit.
|
|
67
|
+
const buildingLiveTransform = useLiveTransforms((state) =>
|
|
68
|
+
currentBuildingId ? state.transforms.get(currentBuildingId) : undefined,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
const committedBuildingRotationY = useScene((state) => {
|
|
63
72
|
if (!currentBuildingId) return 0
|
|
64
73
|
const node = state.nodes[currentBuildingId]
|
|
65
74
|
return node?.type === 'building' ? (node.rotation[1] ?? 0) : 0
|
|
66
75
|
})
|
|
76
|
+
const buildingRotationY = buildingLiveTransform?.rotation ?? committedBuildingRotationY
|
|
67
77
|
|
|
68
|
-
const
|
|
78
|
+
const committedBuildingPosition = useScene((state) => {
|
|
69
79
|
if (!currentBuildingId) {
|
|
70
80
|
return DEFAULT_BUILDING_POSITION
|
|
71
81
|
}
|
|
@@ -75,6 +85,7 @@ export function useFloorplanSceneData({
|
|
|
75
85
|
? (node.position as [number, number, number])
|
|
76
86
|
: DEFAULT_BUILDING_POSITION
|
|
77
87
|
})
|
|
88
|
+
const buildingPosition = buildingLiveTransform?.position ?? committedBuildingPosition
|
|
78
89
|
|
|
79
90
|
const site = useScene((state) => {
|
|
80
91
|
for (const rootNodeId of state.rootNodeIds) {
|
|
@@ -170,6 +181,7 @@ export function useFloorplanSceneData({
|
|
|
170
181
|
|
|
171
182
|
return {
|
|
172
183
|
buildingPosition,
|
|
184
|
+
committedBuildingPosition,
|
|
173
185
|
buildingRotationY,
|
|
174
186
|
currentBuildingId,
|
|
175
187
|
ceilings,
|
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
type WallMiterData,
|
|
19
19
|
type WallNode,
|
|
20
20
|
} from '@pascal-app/core'
|
|
21
|
-
import { useViewer } from '@pascal-app/viewer'
|
|
21
|
+
import { getSceneTheme, useViewer } from '@pascal-app/viewer'
|
|
22
22
|
import { Html } from '@react-three/drei'
|
|
23
23
|
import { createPortal, useFrame } from '@react-three/fiber'
|
|
24
24
|
import { useMemo, useState } from 'react'
|
|
@@ -32,6 +32,12 @@ const HEIGHT_TICK_HALF_LENGTH = 0.14
|
|
|
32
32
|
const HEIGHT_GUIDE_OUTSIDE_OFFSET = 0.16
|
|
33
33
|
|
|
34
34
|
const BAR_AXIS = new THREE.Vector3(0, 1, 0)
|
|
35
|
+
// Shared unit cube — each MeasurementBar scales it to BAR_THICKNESS × length
|
|
36
|
+
// × BAR_THICKNESS instead of constructing a fresh BoxGeometry every frame.
|
|
37
|
+
// Per-frame `<boxGeometry args={[..., length, ...]}/>` triggers R3F to
|
|
38
|
+
// rebuild the geometry whenever the wall moves, and the WebGPU backend
|
|
39
|
+
// flags the in-flight buffer churn as "Vertex buffer slot N ... was not set".
|
|
40
|
+
const SHARED_BAR_GEOMETRY = new THREE.BoxGeometry(1, 1, 1)
|
|
35
41
|
|
|
36
42
|
type Vec3 = [number, number, number]
|
|
37
43
|
|
|
@@ -73,8 +79,7 @@ export function WallMeasurementLabel() {
|
|
|
73
79
|
|
|
74
80
|
const selectedId = selectedIds.length === 1 ? selectedIds[0] : null
|
|
75
81
|
const selectedNode = selectedId ? nodes[selectedId as AnyNodeId] : null
|
|
76
|
-
const measurableNode =
|
|
77
|
-
selectedNode?.type === 'wall' || selectedNode?.type === 'item' ? selectedNode : null
|
|
82
|
+
const measurableNode = selectedNode?.type === 'item' ? selectedNode : null
|
|
78
83
|
|
|
79
84
|
const [objectState, setObjectState] = useState<{
|
|
80
85
|
id: AnyNodeId
|
|
@@ -447,11 +452,12 @@ function MeasurementBar({ start, end, color }: { start: Vec3; end: Vec3; color:
|
|
|
447
452
|
|
|
448
453
|
return (
|
|
449
454
|
<mesh
|
|
455
|
+
geometry={SHARED_BAR_GEOMETRY}
|
|
450
456
|
position={[segment.position.x, segment.position.y, segment.position.z]}
|
|
451
457
|
quaternion={segment.quaternion}
|
|
452
458
|
renderOrder={1000}
|
|
459
|
+
scale={[BAR_THICKNESS, segment.length, BAR_THICKNESS]}
|
|
453
460
|
>
|
|
454
|
-
<boxGeometry args={[BAR_THICKNESS, segment.length, BAR_THICKNESS]} />
|
|
455
461
|
<meshBasicMaterial
|
|
456
462
|
color={color}
|
|
457
463
|
depthTest={false}
|
|
@@ -515,9 +521,8 @@ function SelectedMeasurementAnnotation({ node }: { node: WallNode | ItemNode })
|
|
|
515
521
|
|
|
516
522
|
function WallMeasurementAnnotation({ wall }: { wall: WallNode }) {
|
|
517
523
|
const nodes = useScene((state) => state.nodes)
|
|
518
|
-
const theme = useViewer((state) => state.theme)
|
|
519
524
|
const unit = useViewer((state) => state.unit)
|
|
520
|
-
const isNight =
|
|
525
|
+
const isNight = useViewer((state) => getSceneTheme(state.sceneTheme).appearance === 'dark')
|
|
521
526
|
const color = isNight ? '#ffffff' : '#111111'
|
|
522
527
|
const shadowColor = isNight ? '#111111' : '#ffffff'
|
|
523
528
|
|