@pascal-app/editor 0.8.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 +8 -9
- package/src/components/editor/alignment-3d-guide-layer.tsx +160 -0
- package/src/components/editor/custom-camera-controls.tsx +754 -25
- package/src/components/editor/editor-layout-mobile.tsx +3 -3
- package/src/components/editor/editor-layout-v2.tsx +58 -51
- 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 +201 -83
- package/src/components/editor/first-person/bvh-ecctrl.tsx +8 -8
- package/src/components/editor/first-person-controls.tsx +825 -20
- 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 +5829 -12005
- 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 +178 -149
- 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 +433 -156
- 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 +136 -62
- package/src/components/editor/thumbnail-generator.tsx +9 -3
- package/src/components/editor/use-floorplan-background-placement.ts +119 -32
- 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-geometry-renderer.tsx +266 -0
- package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +108 -66
- 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 +24 -9
- package/src/components/editor-2d/svg-paths.ts +10 -2
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +26 -3
- 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 +1 -1
- 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 +41 -105
- package/src/components/tools/item/placement-math.test.ts +10 -0
- package/src/components/tools/item/placement-math.ts +31 -5
- package/src/components/tools/item/placement-strategies.ts +190 -15
- package/src/components/tools/item/placement-types.ts +10 -1
- package/src/components/tools/item/use-draft-node.ts +20 -1
- package/src/components/tools/item/use-placement-coordinator.tsx +694 -191
- 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 -538
- 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 +56 -0
- 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 +16 -6
- package/src/components/ui/action-menu/action-button.tsx +21 -1
- package/src/components/ui/action-menu/control-modes.tsx +5 -82
- package/src/components/ui/action-menu/index.tsx +1 -113
- package/src/components/ui/action-menu/structure-tools.tsx +13 -88
- package/src/components/ui/action-menu/view-toggles.tsx +243 -6
- package/src/components/ui/command-palette/editor-commands.tsx +18 -13
- package/src/components/ui/command-palette/index.tsx +2 -1
- package/src/components/ui/controls/material-paint-panel.tsx +83 -0
- package/src/components/ui/controls/material-picker.tsx +11 -32
- package/src/components/ui/controls/metric-control.tsx +33 -11
- package/src/components/ui/floating-level-selector.tsx +47 -10
- package/src/components/ui/helpers/helper-manager.tsx +16 -17
- package/src/components/ui/helpers/registered-tool-helper.tsx +25 -0
- package/src/components/ui/item-catalog/catalog-items.tsx +1 -0
- package/src/components/ui/item-catalog/item-catalog.tsx +3 -0
- package/src/components/ui/level-duplicate-dialog.tsx +2 -1
- package/src/components/ui/panels/node-display.ts +1 -0
- package/src/components/ui/panels/panel-manager.tsx +35 -47
- package/src/components/ui/panels/panel-wrapper.tsx +217 -12
- package/src/components/ui/panels/parametric-inspector.tsx +469 -0
- package/src/components/ui/primitives/dropdown-menu.tsx +12 -8
- package/src/components/ui/scene-loader.tsx +1 -3
- package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +263 -0
- package/src/components/ui/sidebar/panels/items-panel/index.tsx +82 -8
- 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 +4 -6
- 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 +7 -1
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +68 -36
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +51 -2
- package/src/components/ui/sidebar/tab-bar.tsx +61 -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 +10 -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 +115 -62
- package/src/hooks/use-selection.ts +64 -0
- package/src/index.tsx +237 -5
- 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.ts +54 -42
- package/src/lib/level-name.ts +11 -0
- package/src/lib/material-paint.ts +211 -19
- 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 +1 -1
- package/src/lib/scene-clipboard.ts +267 -0
- package/src/lib/scene.ts +13 -4
- package/src/lib/sfx-bus.ts +23 -1
- package/src/lib/sfx-player.ts +70 -22
- package/src/lib/site-boundary.ts +1 -0
- 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 +320 -37
- 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/src/components/editor/preset-thumbnail-generator.tsx +0 -125
- package/src/components/editor-2d/renderers/floorplan-roof-layer.tsx +0 -113
- 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 -264
- 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 -178
- package/src/components/tools/fence/fence-tool.tsx +0 -346
- package/src/components/tools/fence/move-fence-endpoint-tool.tsx +0 -441
- package/src/components/tools/fence/move-fence-tool.tsx +0 -302
- package/src/components/tools/item/item-tool.tsx +0 -26
- package/src/components/tools/roof/move-roof-tool.tsx +0 -364
- 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 -178
- package/src/components/tools/wall/move-wall-endpoint-tool.tsx +0 -426
- package/src/components/tools/wall/move-wall-tool.tsx +0 -358
- 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 -249
- package/src/components/ui/panels/column-panel.tsx +0 -759
- package/src/components/ui/panels/door-panel.tsx +0 -1299
- package/src/components/ui/panels/fence-panel.tsx +0 -222
- 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 -280
- 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 -161
- package/src/components/ui/panels/stair-panel.tsx +0 -577
- package/src/components/ui/panels/stair-segment-panel.tsx +0 -325
- package/src/components/ui/panels/wall-panel.tsx +0 -273
- package/src/components/ui/panels/window-panel.tsx +0 -968
- package/src/contexts/presets-context.tsx +0 -121
- package/src/hooks/use-contextual-tools.ts +0 -61
|
@@ -7,7 +7,11 @@ import { useIsMobile } from '../../hooks/use-mobile'
|
|
|
7
7
|
import { triggerSFX } from '../../lib/sfx-bus'
|
|
8
8
|
import useEditor from '../../store/use-editor'
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
// Local crop-mode enum — distinct from `useEditor.captureMode` (which
|
|
11
|
+
// describes *why* a capture is happening, e.g. `preset`). This one says
|
|
12
|
+
// HOW the captured pixels are cropped: full-frame 16:9 (`standard`),
|
|
13
|
+
// raw canvas viewport, or user-dragged area.
|
|
14
|
+
type CropMode = 'standard' | 'viewport' | 'area'
|
|
11
15
|
type CaptureState = 'idle' | 'capturing' | 'saved'
|
|
12
16
|
|
|
13
17
|
interface DragPoint {
|
|
@@ -21,7 +25,7 @@ interface Drag {
|
|
|
21
25
|
}
|
|
22
26
|
|
|
23
27
|
function getResolution(
|
|
24
|
-
mode:
|
|
28
|
+
mode: CropMode,
|
|
25
29
|
overlayEl: HTMLDivElement | null,
|
|
26
30
|
drag: Drag | null,
|
|
27
31
|
): { w: number; h: number } | null {
|
|
@@ -47,10 +51,15 @@ function getResolution(
|
|
|
47
51
|
|
|
48
52
|
export function SnapshotCaptureOverlay({ projectId }: { projectId: string }) {
|
|
49
53
|
const isCaptureMode = useEditor((s) => s.isCaptureMode)
|
|
54
|
+
const captureMode = useEditor((s) => s.captureMode)
|
|
50
55
|
const setCaptureMode = useEditor((s) => s.setCaptureMode)
|
|
51
56
|
const isMobile = useIsMobile()
|
|
57
|
+
// `preset` capture mode locks the overlay to a square area crop with
|
|
58
|
+
// a transparent background — the user picks framing but not the
|
|
59
|
+
// crop shape. Matches the unified preset-thumbnail capture flow.
|
|
60
|
+
const isPreset = captureMode.mode === 'preset'
|
|
52
61
|
|
|
53
|
-
const [mode, setMode] = useState<
|
|
62
|
+
const [mode, setMode] = useState<CropMode>('standard')
|
|
54
63
|
const [drag, setDrag] = useState<Drag | null>(null)
|
|
55
64
|
const [isDragging, setIsDragging] = useState(false)
|
|
56
65
|
const [captureState, setCaptureState] = useState<CaptureState>('idle')
|
|
@@ -66,15 +75,29 @@ export function SnapshotCaptureOverlay({ projectId }: { projectId: string }) {
|
|
|
66
75
|
return () => window.removeEventListener('keydown', onKey)
|
|
67
76
|
}, [isCaptureMode, setCaptureMode])
|
|
68
77
|
|
|
69
|
-
// Reset local state when entering capture mode
|
|
78
|
+
// Reset local state when entering capture mode. Preset mode also
|
|
79
|
+
// auto-stages a centered square crop sized to ~75% of the shorter
|
|
80
|
+
// viewport dimension so the user can capture immediately — the
|
|
81
|
+
// overlay's pan/move/resize handles still apply if they want to
|
|
82
|
+
// tweak the framing, but they don't have to draw the rect first.
|
|
70
83
|
useEffect(() => {
|
|
71
|
-
if (isCaptureMode)
|
|
72
|
-
|
|
84
|
+
if (!isCaptureMode) return
|
|
85
|
+
setMode(isPreset ? 'area' : 'standard')
|
|
86
|
+
setIsDragging(false)
|
|
87
|
+
setCaptureState('idle')
|
|
88
|
+
if (isPreset && overlayRef.current) {
|
|
89
|
+
const rect = overlayRef.current.getBoundingClientRect()
|
|
90
|
+
const side = Math.min(rect.width, rect.height) * 0.75
|
|
91
|
+
const cx = rect.width / 2
|
|
92
|
+
const cy = rect.height / 2
|
|
93
|
+
setDrag({
|
|
94
|
+
start: { x: cx - side / 2, y: cy - side / 2 },
|
|
95
|
+
end: { x: cx + side / 2, y: cy + side / 2 },
|
|
96
|
+
})
|
|
97
|
+
} else {
|
|
73
98
|
setDrag(null)
|
|
74
|
-
setIsDragging(false)
|
|
75
|
-
setCaptureState('idle')
|
|
76
99
|
}
|
|
77
|
-
}, [isCaptureMode])
|
|
100
|
+
}, [isCaptureMode, isPreset])
|
|
78
101
|
|
|
79
102
|
// Listen for snapshot saved to show feedback then exit
|
|
80
103
|
useEffect(() => {
|
|
@@ -142,11 +165,28 @@ export function SnapshotCaptureOverlay({ projectId }: { projectId: string }) {
|
|
|
142
165
|
start: { x: snapshot.start.x + dx, y: snapshot.start.y + dy },
|
|
143
166
|
end: { x: snapshot.end.x + dx, y: snapshot.end.y + dy },
|
|
144
167
|
})
|
|
168
|
+
} else if (isPreset) {
|
|
169
|
+
// Preset mode locks the rect to a square — use the smaller
|
|
170
|
+
// axis to keep the drag predictable, sign-correct so the user
|
|
171
|
+
// can still drag in any quadrant.
|
|
172
|
+
setDrag((d) => {
|
|
173
|
+
if (!d) return null
|
|
174
|
+
const dx = pt.x - d.start.x
|
|
175
|
+
const dy = pt.y - d.start.y
|
|
176
|
+
const side = Math.min(Math.abs(dx), Math.abs(dy))
|
|
177
|
+
return {
|
|
178
|
+
start: d.start,
|
|
179
|
+
end: {
|
|
180
|
+
x: d.start.x + Math.sign(dx || 1) * side,
|
|
181
|
+
y: d.start.y + Math.sign(dy || 1) * side,
|
|
182
|
+
},
|
|
183
|
+
}
|
|
184
|
+
})
|
|
145
185
|
} else {
|
|
146
186
|
setDrag((d) => (d ? { start: d.start, end: pt } : null))
|
|
147
187
|
}
|
|
148
188
|
},
|
|
149
|
-
[isDragging],
|
|
189
|
+
[isDragging, isPreset],
|
|
150
190
|
)
|
|
151
191
|
|
|
152
192
|
const onPointerUp = useCallback(() => {
|
|
@@ -225,8 +265,12 @@ export function SnapshotCaptureOverlay({ projectId }: { projectId: string }) {
|
|
|
225
265
|
projectId,
|
|
226
266
|
captureMode: mode,
|
|
227
267
|
cropRegion,
|
|
268
|
+
// In preset mode, the ThumbnailGenerator should keep the alpha
|
|
269
|
+
// channel transparent so the saved preset thumbnail composes
|
|
270
|
+
// cleanly onto any palette background.
|
|
271
|
+
transparent: isPreset,
|
|
228
272
|
})
|
|
229
|
-
}, [captureState, mode, drag, projectId])
|
|
273
|
+
}, [captureState, mode, drag, projectId, isPreset])
|
|
230
274
|
|
|
231
275
|
if (!isCaptureMode) return null
|
|
232
276
|
|
|
@@ -250,31 +294,47 @@ export function SnapshotCaptureOverlay({ projectId }: { projectId: string }) {
|
|
|
250
294
|
|
|
251
295
|
return (
|
|
252
296
|
<div className="pointer-events-none absolute inset-0 z-40" ref={overlayRef}>
|
|
253
|
-
{/* Area mode: dim layer + crosshair cursor + drag-to-select
|
|
297
|
+
{/* Area mode: dim layer + crosshair cursor + drag-to-select.
|
|
298
|
+
*
|
|
299
|
+
* Preset mode reuses the same DOM but stays click-through: the
|
|
300
|
+
* crop frame is auto-staged and locked, so the user adjusts the
|
|
301
|
+
* camera (orbit / pan / zoom) instead of dragging the rect. The
|
|
302
|
+
* dim letterbox + dashed border still render via the inline
|
|
303
|
+
* `box-shadow` on the selection rect — they're cosmetic. */}
|
|
254
304
|
{mode === 'area' && (
|
|
255
305
|
<div
|
|
256
|
-
className=
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
306
|
+
className={
|
|
307
|
+
isPreset
|
|
308
|
+
? 'pointer-events-none absolute inset-0'
|
|
309
|
+
: 'pointer-events-auto absolute inset-0 bg-black/30'
|
|
310
|
+
}
|
|
311
|
+
onPointerDown={isPreset ? undefined : onPointerDown}
|
|
312
|
+
onPointerMove={
|
|
313
|
+
isPreset
|
|
314
|
+
? undefined
|
|
315
|
+
: (e) => {
|
|
316
|
+
onPointerMove(e)
|
|
317
|
+
// Update cursor: 'move' when hovering inside an existing selection
|
|
318
|
+
if (!isDragging && drag && overlayRef.current) {
|
|
319
|
+
const rect = overlayRef.current.getBoundingClientRect()
|
|
320
|
+
const px = e.clientX - rect.left
|
|
321
|
+
const py = e.clientY - rect.top
|
|
322
|
+
const x0 = Math.min(drag.start.x, drag.end.x)
|
|
323
|
+
const y0 = Math.min(drag.start.y, drag.end.y)
|
|
324
|
+
const x1 = Math.max(drag.start.x, drag.end.x)
|
|
325
|
+
const y1 = Math.max(drag.start.y, drag.end.y)
|
|
326
|
+
e.currentTarget.style.cursor =
|
|
327
|
+
px >= x0 && px <= x1 && py >= y0 && py <= y1 ? 'move' : 'crosshair'
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
onPointerUp={isPreset ? undefined : onPointerUp}
|
|
332
|
+
style={isPreset ? undefined : { cursor: 'crosshair' }}
|
|
275
333
|
>
|
|
276
|
-
{/* "No selection" hint
|
|
277
|
-
|
|
334
|
+
{/* "No selection" hint — only when the user has to draw the
|
|
335
|
+
area themselves (`standard` capture). Preset mode always
|
|
336
|
+
has a pre-staged square, so we never show it there. */}
|
|
337
|
+
{!selectionStyle && !isPreset && (
|
|
278
338
|
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
|
|
279
339
|
<span className="rounded-full bg-black/40 px-4 py-2 text-sm text-white backdrop-blur-sm">
|
|
280
340
|
Drag the area you want to capture
|
|
@@ -297,31 +357,34 @@ export function SnapshotCaptureOverlay({ projectId }: { projectId: string }) {
|
|
|
297
357
|
background: 'rgba(255,255,255,0.04)',
|
|
298
358
|
}}
|
|
299
359
|
>
|
|
300
|
-
{/* Corner handles
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
360
|
+
{/* Corner handles — preset mode locks the frame to the
|
|
361
|
+
auto-staged centered square; the user adjusts the
|
|
362
|
+
camera instead. */}
|
|
363
|
+
{!isPreset &&
|
|
364
|
+
(
|
|
365
|
+
[
|
|
366
|
+
{ pos: { top: -5, left: -5 }, cursor: 'nwse-resize' },
|
|
367
|
+
{ pos: { top: -5, right: -5 }, cursor: 'nesw-resize' },
|
|
368
|
+
{ pos: { bottom: -5, left: -5 }, cursor: 'nesw-resize' },
|
|
369
|
+
{ pos: { bottom: -5, right: -5 }, cursor: 'nwse-resize' },
|
|
370
|
+
] as const
|
|
371
|
+
).map(({ pos, cursor }, i) => (
|
|
372
|
+
<div
|
|
373
|
+
key={i}
|
|
374
|
+
onPointerDown={(e) => onCornerPointerDown(e, i)}
|
|
375
|
+
style={{
|
|
376
|
+
position: 'absolute',
|
|
377
|
+
width: 10,
|
|
378
|
+
height: 10,
|
|
379
|
+
borderRadius: '50%',
|
|
380
|
+
background: 'white',
|
|
381
|
+
boxShadow: '0 1px 4px rgba(0,0,0,0.4)',
|
|
382
|
+
pointerEvents: 'auto',
|
|
383
|
+
cursor,
|
|
384
|
+
...pos,
|
|
385
|
+
}}
|
|
386
|
+
/>
|
|
387
|
+
))}
|
|
325
388
|
</div>
|
|
326
389
|
)}
|
|
327
390
|
</div>
|
|
@@ -343,7 +406,10 @@ export function SnapshotCaptureOverlay({ projectId }: { projectId: string }) {
|
|
|
343
406
|
{/* Bottom-center mode toolbar */}
|
|
344
407
|
<div className="pointer-events-auto absolute bottom-6 left-1/2 -translate-x-1/2">
|
|
345
408
|
{(() => {
|
|
346
|
-
|
|
409
|
+
// Preset capture mode locks both the crop shape (square) and
|
|
410
|
+
// the transparent-background output — hide the per-shape
|
|
411
|
+
// mode buttons so the user has nothing to second-guess.
|
|
412
|
+
const modeButtons = isPreset ? null : (
|
|
347
413
|
<>
|
|
348
414
|
<ModeButton
|
|
349
415
|
active={mode === 'standard'}
|
|
@@ -408,8 +474,16 @@ export function SnapshotCaptureOverlay({ projectId }: { projectId: string }) {
|
|
|
408
474
|
if (isMobile) {
|
|
409
475
|
return (
|
|
410
476
|
<div className="flex flex-col items-stretch gap-2 rounded-2xl border border-white/10 bg-neutral-900/95 px-2 py-2 shadow-xl backdrop-blur-md">
|
|
411
|
-
|
|
412
|
-
|
|
477
|
+
{modeButtons && (
|
|
478
|
+
<div className="flex items-center justify-center gap-1">{modeButtons}</div>
|
|
479
|
+
)}
|
|
480
|
+
<div
|
|
481
|
+
className={
|
|
482
|
+
modeButtons
|
|
483
|
+
? 'flex items-center justify-center gap-2 border-white/10 border-t pt-2'
|
|
484
|
+
: 'flex items-center justify-center gap-2'
|
|
485
|
+
}
|
|
486
|
+
>
|
|
413
487
|
{resolutionDisplay}
|
|
414
488
|
{captureButton}
|
|
415
489
|
</div>
|
|
@@ -420,7 +494,7 @@ export function SnapshotCaptureOverlay({ projectId }: { projectId: string }) {
|
|
|
420
494
|
return (
|
|
421
495
|
<div className="flex items-center gap-1 rounded-full border border-white/10 bg-neutral-900/95 px-2 py-2 shadow-xl backdrop-blur-md">
|
|
422
496
|
{modeButtons}
|
|
423
|
-
<div className="mx-1 h-4 w-px bg-white/10" />
|
|
497
|
+
{modeButtons && <div className="mx-1 h-4 w-px bg-white/10" />}
|
|
424
498
|
{resolutionDisplay}
|
|
425
499
|
<div className="mx-1 h-4 w-px bg-white/10" />
|
|
426
500
|
{captureButton}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import { emitter, sceneRegistry } from '@pascal-app/core'
|
|
4
|
-
import { SSGI_PARAMS, snapLevelsToTruePositions, useViewer } from '@pascal-app/viewer'
|
|
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'
|
|
@@ -62,6 +62,7 @@ export const ThumbnailGenerator = ({ onThumbnailCapture }: ThumbnailGeneratorPro
|
|
|
62
62
|
useEffect(() => {
|
|
63
63
|
const cam = new THREE.PerspectiveCamera(60, THUMBNAIL_WIDTH / THUMBNAIL_HEIGHT, 0.1, 1000)
|
|
64
64
|
cam.layers.disable(EDITOR_LAYER)
|
|
65
|
+
cam.layers.disable(GRID_LAYER)
|
|
65
66
|
thumbnailCameraRef.current = cam
|
|
66
67
|
|
|
67
68
|
let mounted = true
|
|
@@ -73,7 +74,7 @@ export const ThumbnailGenerator = ({ onThumbnailCapture }: ThumbnailGeneratorPro
|
|
|
73
74
|
|
|
74
75
|
// pass() handles MRT internally for all material types, including custom
|
|
75
76
|
// shaders — unlike renderer.setMRT() which crashes on non-NodeMaterials.
|
|
76
|
-
// pass() also respects camera.layers, so EDITOR_LAYER objects are filtered.
|
|
77
|
+
// pass() also respects camera.layers, so EDITOR_LAYER + GRID_LAYER objects are filtered.
|
|
77
78
|
const scenePass = pass(scene, cam)
|
|
78
79
|
scenePass.setMRT(
|
|
79
80
|
mrt({
|
|
@@ -207,7 +208,7 @@ export const ThumbnailGenerator = ({ onThumbnailCapture }: ThumbnailGeneratorPro
|
|
|
207
208
|
const restoreNodeVisibility = (() => {
|
|
208
209
|
const saved = new Map<THREE.Object3D, boolean>()
|
|
209
210
|
for (const type of ['scan', 'guide'] as const) {
|
|
210
|
-
const ids = sceneRegistry.byType[type]
|
|
211
|
+
const ids = sceneRegistry.byType[type]!
|
|
211
212
|
ids.forEach((id) => {
|
|
212
213
|
const node = sceneRegistry.nodes.get(id)
|
|
213
214
|
if (node) {
|
|
@@ -460,6 +461,11 @@ export const ThumbnailGenerator = ({ onThumbnailCapture }: ThumbnailGeneratorPro
|
|
|
460
461
|
captureMode?: 'standard' | 'viewport' | 'area'
|
|
461
462
|
cropRegion?: { x: number; y: number; width: number; height: number }
|
|
462
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
|
|
463
469
|
}) => {
|
|
464
470
|
await generate(event.snapLevels === true, event.captureMode, event.cropRegion)
|
|
465
471
|
}
|
|
@@ -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,20 +148,28 @@ 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
|
|
|
@@ -154,16 +183,26 @@ export function useFloorplanBackgroundPlacement({
|
|
|
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
|
|