@pascal-app/editor 0.9.2 → 1.0.0-beta.2
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 +9 -7
- package/src/components/editor/bake-exporter.tsx +3 -1
- package/src/components/editor/bake-thumbnail.tsx +95 -0
- package/src/components/editor/camera-dragging-lifecycle.test.ts +49 -0
- package/src/components/editor/camera-dragging-lifecycle.ts +41 -0
- package/src/components/editor/custom-camera-controls.tsx +307 -212
- package/src/components/editor/delete-confirmation-dialog.tsx +54 -0
- package/src/components/editor/elevation-3d-guide-layer.tsx +120 -0
- package/src/components/editor/first-person/build-collider-world.test.ts +66 -2
- package/src/components/editor/first-person/build-collider-world.ts +20 -0
- package/src/components/editor/first-person/terrain-collider.test.ts +132 -0
- package/src/components/editor/first-person/terrain-collider.ts +212 -0
- package/src/components/editor/first-person-controls.tsx +295 -132
- package/src/components/editor/floating-action-menu.tsx +59 -65
- package/src/components/editor/floorplan-background-selection.test.ts +16 -2
- package/src/components/editor/floorplan-background-selection.ts +4 -18
- package/src/components/editor/floorplan-camera-sync.test.ts +249 -0
- package/src/components/editor/floorplan-camera-sync.ts +213 -0
- package/src/components/editor/floorplan-mode-coordinator.tsx +82 -0
- package/src/components/editor/floorplan-navigation-presentation.test.ts +142 -0
- package/src/components/editor/floorplan-navigation-presentation.ts +156 -0
- package/src/components/editor/floorplan-panel.tsx +931 -861
- package/src/components/editor/group-actions.ts +155 -15
- package/src/components/editor/group-floating-action-menu.tsx +1 -1
- package/src/components/editor/group-rotate-handle.tsx +1 -1
- package/src/components/editor/group-selection-box-3d.tsx +1 -1
- package/src/components/editor/handles/handle-drag-history.test.ts +96 -0
- package/src/components/editor/handles/handle-drag-history.ts +14 -0
- package/src/components/editor/handles/preview-overrides.test.ts +26 -0
- package/src/components/editor/handles/preview-overrides.ts +13 -0
- package/src/components/editor/handles/resize-snap.test.ts +52 -0
- package/src/components/editor/handles/resize-snap.ts +23 -0
- package/src/components/editor/handles/use-handle-drag.ts +17 -7
- package/src/components/editor/index.tsx +42 -9
- package/src/components/editor/measurement-pill.tsx +6 -1
- package/src/components/editor/node-arrow-handles.tsx +51 -15
- package/src/components/editor/quick-measurement-card.tsx +9 -3
- package/src/components/editor/quick-measurement-hud.tsx +7 -1
- package/src/components/editor/selection-manager.tsx +3 -1
- package/src/components/editor/site-edge-labels.tsx +24 -2
- package/src/components/editor/snapshot-capture-overlay.tsx +4 -1
- package/src/components/editor/thumbnail-generator.tsx +66 -294
- package/src/components/editor/use-floorplan-hit-testing.test.ts +82 -0
- package/src/components/editor/use-floorplan-hit-testing.ts +144 -161
- package/src/components/editor/use-mesh-settle-epoch.ts +1 -0
- package/src/components/editor/wall-measurement-label.tsx +8 -16
- package/src/components/editor/wall-move-side-handles.tsx +193 -18
- package/src/components/editor/wall-opening-highlights.tsx +63 -32
- package/src/components/editor/wall-snap-beacon-layer.tsx +6 -4
- package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +2 -1
- package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +39 -8
- package/src/components/editor-2d/floorplan-cursor-indicator-position.test.ts +31 -0
- package/src/components/editor-2d/floorplan-cursor-indicator-position.ts +39 -0
- package/src/components/editor-2d/floorplan-group-move.tsx +47 -4
- package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +21 -5
- package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +72 -0
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +93 -59
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +27 -0
- package/src/components/editor-2d/floorplan-render-context.test.ts +27 -0
- package/src/components/editor-2d/floorplan-render-context.tsx +113 -8
- package/src/components/editor-2d/renderers/floorplan-annotation-layout.test.ts +285 -0
- package/src/components/editor-2d/renderers/floorplan-annotation-layout.ts +516 -0
- package/src/components/editor-2d/renderers/floorplan-dimension-renderer.test.tsx +236 -0
- package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +618 -0
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.test.tsx +369 -0
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +431 -32
- package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +75 -1
- package/src/components/editor-2d/renderers/floorplan-label-angle.ts +92 -0
- package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +93 -28
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +279 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +621 -281
- package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +20 -10
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +12 -7
- package/src/components/tools/fence/fence-drafting.ts +27 -0
- package/src/components/tools/item/use-draft-node.ts +131 -72
- package/src/components/tools/item/use-placement-coordinator.tsx +52 -4
- package/src/components/tools/registry/move-registry-node-tool.tsx +75 -4
- package/src/components/tools/roof/roof-tool.tsx +16 -0
- package/src/components/tools/shared/floor-stack-preview.ts +4 -0
- package/src/components/tools/shared/horizontal-construction-plane.ts +123 -0
- package/src/components/tools/shared/pointer-support-cap.test.ts +98 -0
- package/src/components/tools/shared/pointer-support-cap.ts +273 -0
- package/src/components/tools/site/site-boundary-editor.tsx +13 -6
- package/src/components/tools/site/stroke-pointer-ownership.test.ts +91 -0
- package/src/components/tools/site/stroke-pointer-ownership.ts +57 -0
- package/src/components/tools/site/terrain-brush-cursor.tsx +247 -0
- package/src/components/tools/site/terrain-sculpt-grid.tsx +192 -0
- package/src/components/tools/site/terrain-sculpt-tool.tsx +356 -0
- package/src/components/tools/stair/stair-click-guard.test.ts +58 -0
- package/src/components/tools/stair/stair-click-guard.ts +72 -0
- package/src/components/tools/stair/stair-tool.tsx +98 -47
- package/src/components/tools/tool-manager.tsx +67 -9
- package/src/components/tools/wall/wall-drafting.test.ts +118 -1
- package/src/components/tools/wall/wall-drafting.ts +128 -9
- package/src/components/tools/wall/wall-snap-geometry.test.ts +8 -0
- package/src/components/tools/wall/wall-snap-geometry.ts +70 -3
- package/src/components/tools/zone/zone-tool.tsx +16 -0
- package/src/components/ui/action-menu/control-modes.tsx +4 -1
- package/src/components/ui/action-menu/measurement-control.tsx +107 -28
- package/src/components/ui/command-palette/editor-commands.tsx +14 -1
- package/src/components/ui/controls/material-paint-panel.tsx +7 -1
- package/src/components/ui/controls/material-picker.tsx +86 -7
- package/src/components/ui/controls/terrain-sculpt-panel.tsx +193 -0
- package/src/components/ui/floating-level-selector.tsx +73 -19
- package/src/components/ui/helpers/helper-manager.tsx +46 -0
- package/src/components/ui/panels/multi-selection-panel.tsx +57 -0
- package/src/components/ui/panels/panel-manager.tsx +14 -1
- package/src/components/ui/panels/parametric-inspector.tsx +4 -2
- package/src/components/ui/panels/selection-breakdown.test.ts +24 -0
- package/src/components/ui/panels/selection-breakdown.ts +20 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +14 -4
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +16 -0
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +2 -1
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +2 -0
- package/src/components/viewer/viewer-controls-bar.tsx +466 -0
- package/src/components/viewer/viewer-scene-header.tsx +235 -0
- package/src/components/viewer-overlay.tsx +20 -655
- package/src/components/walkthrough-hud.tsx +111 -0
- package/src/hooks/use-grid-events.ts +24 -0
- package/src/hooks/use-keyboard.ts +53 -30
- package/src/index.tsx +137 -4
- package/src/lib/active-placement-surface.test.ts +24 -0
- package/src/lib/active-placement-surface.ts +8 -1
- package/src/lib/camera-pose.test.ts +237 -0
- package/src/lib/camera-pose.ts +172 -0
- package/src/lib/direct-manipulation.test.ts +83 -0
- package/src/lib/direct-manipulation.ts +9 -0
- package/src/lib/door-interaction.ts +1 -1
- package/src/lib/elevation-guides.test.ts +107 -0
- package/src/lib/elevation-guides.ts +265 -0
- package/src/lib/floorplan/annotation-visibility.test.ts +327 -0
- package/src/lib/floorplan/annotation-visibility.ts +133 -0
- package/src/lib/floorplan/drawing-coordination.test.ts +46 -0
- package/src/lib/floorplan/drawing-coordination.ts +13 -0
- package/src/lib/floorplan/floorplan-export.test.ts +319 -0
- package/src/lib/floorplan/floorplan-export.tsx +745 -131
- package/src/lib/floorplan/floorplan-extension.test.ts +43 -0
- package/src/lib/floorplan/floorplan-extension.ts +181 -0
- package/src/lib/floorplan/floorplan-mode.test.ts +120 -0
- package/src/lib/floorplan/floorplan-mode.ts +73 -0
- package/src/lib/floorplan/floorplan-pdfkit-document.ts +194 -0
- package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +107 -0
- package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +680 -0
- package/src/lib/floorplan/geometry.ts +2 -3
- package/src/lib/fresh-planar-placement.test.ts +44 -1
- package/src/lib/fresh-planar-placement.ts +21 -4
- package/src/lib/glb-export.test.ts +116 -1
- package/src/lib/glb-export.ts +204 -7
- package/src/lib/ground-surface.ts +95 -0
- package/src/lib/history.test.ts +96 -0
- package/src/lib/history.ts +22 -0
- package/src/lib/interaction/overlay-policy.test.ts +2 -1
- package/src/lib/interaction/scope.ts +40 -8
- package/src/lib/level-duplication.test.ts +2 -1
- package/src/lib/measurements.test.ts +5 -0
- package/src/lib/measurements.ts +12 -1
- package/src/lib/quick-action-feedback.ts +33 -0
- package/src/lib/quick-action-nodes.test.ts +94 -0
- package/src/lib/quick-action-nodes.ts +47 -0
- package/src/lib/scene-clipboard.test.ts +130 -1
- package/src/lib/scene-clipboard.ts +237 -13
- package/src/lib/selection-routing.test.ts +16 -1
- package/src/lib/selection-routing.ts +5 -0
- package/src/lib/sfx-bus.ts +33 -2
- package/src/lib/sfx-player.test.ts +148 -6
- package/src/lib/sfx-player.ts +156 -8
- package/src/lib/site-boundary.test.ts +53 -0
- package/src/lib/site-boundary.ts +27 -0
- package/src/lib/snapping-mode.test.ts +29 -2
- package/src/lib/snapping-mode.ts +15 -3
- package/src/lib/stair-duplication.test.ts +103 -0
- package/src/lib/stair-duplication.ts +57 -85
- package/src/lib/stair-levels.ts +2 -0
- package/src/lib/terrain-sculpt.test.ts +429 -0
- package/src/lib/terrain-sculpt.ts +301 -0
- package/src/lib/terrain-verb-color.test.ts +51 -0
- package/src/lib/terrain-verb-color.ts +58 -0
- package/src/lib/touch-gesture-priority.test.ts +45 -0
- package/src/lib/touch-gesture-priority.ts +30 -0
- package/src/lib/window-interaction.ts +1 -1
- package/src/store/camera-pose-store.test.ts +29 -0
- package/src/store/camera-pose-store.ts +27 -0
- package/src/store/live-draft-preview-stores.test.ts +136 -0
- package/src/store/navigation-sync-pose-store.test.ts +34 -0
- package/src/store/navigation-sync-pose-store.ts +27 -0
- package/src/store/terrain-sculpt-mode.test.ts +223 -0
- package/src/store/use-delete-confirmation.ts +27 -0
- package/src/store/use-drawing-view.test.ts +23 -0
- package/src/store/use-drawing-view.ts +84 -0
- package/src/store/use-editor.tsx +184 -15
- package/src/store/use-elevation-guides.ts +40 -0
- package/src/store/use-fence-curve-draft.ts +38 -8
- package/src/store/use-first-person-hud.ts +40 -0
- package/src/store/use-floorplan-annotation-visibility.ts +60 -0
- package/src/store/use-floorplan-draft-preview.ts +54 -4
- package/src/store/use-floorplan-mode.ts +110 -0
- package/src/store/use-interaction-scope.test.ts +23 -3
- package/src/store/use-interaction-scope.ts +8 -0
- package/src/store/use-measurement-draft.test.ts +29 -0
- package/src/store/use-measurement-draft.ts +33 -6
- package/src/store/use-path-draft-preview.ts +90 -0
- package/src/store/use-stair-build-preview.ts +12 -2
- package/src/lib/floorplan/selection-tool.ts +0 -271
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
|
+
type AnyNode,
|
|
4
5
|
type AnyNodeId,
|
|
5
|
-
DEFAULT_WALL_HEIGHT,
|
|
6
6
|
type FenceNode,
|
|
7
|
+
getWallBaseElevationForNodes,
|
|
7
8
|
getWallCurveFrameAt,
|
|
9
|
+
getWallEffectiveHeightForNodes,
|
|
8
10
|
getWallThickness,
|
|
9
11
|
isCurvedWall,
|
|
12
|
+
MIN_WALL_HEIGHT,
|
|
10
13
|
sceneRegistry,
|
|
11
14
|
useLiveNodeOverrides,
|
|
12
15
|
useScene,
|
|
@@ -32,19 +35,27 @@ import {
|
|
|
32
35
|
} from 'three'
|
|
33
36
|
import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js'
|
|
34
37
|
import { MeshBasicNodeMaterial } from 'three/webgpu'
|
|
38
|
+
import {
|
|
39
|
+
clearStructuralElevationGuide,
|
|
40
|
+
publishStructuralElevationGuide,
|
|
41
|
+
resolveStructuralElevationSnap,
|
|
42
|
+
} from '../../lib/elevation-guides'
|
|
35
43
|
import { isHistoryShortcut } from '../../lib/history'
|
|
36
44
|
import { endpointReshapeScope } from '../../lib/interaction/scope'
|
|
37
45
|
import { sfxEmitter } from '../../lib/sfx-bus'
|
|
38
|
-
import useEditor from '../../store/use-editor'
|
|
46
|
+
import useEditor, { isGridSnapActive, isMagneticSnapActive } from '../../store/use-editor'
|
|
39
47
|
import useInteractionScope, {
|
|
40
48
|
useEndpointReshape,
|
|
41
49
|
useIsCurveReshape,
|
|
42
50
|
useMovingNode,
|
|
43
51
|
} from '../../store/use-interaction-scope'
|
|
44
52
|
import { suppressBoxSelectForPointer } from '../tools/select/box-select-state'
|
|
53
|
+
import { resolveResizeSnapValue } from './handles/resize-snap'
|
|
54
|
+
import { type HandleDragControls, useHandleDrag } from './handles/use-handle-drag'
|
|
45
55
|
import {
|
|
46
56
|
createArrowHitAreaGeometry,
|
|
47
57
|
createEndpointHitAreaGeometry,
|
|
58
|
+
HandleArrow,
|
|
48
59
|
InvisibleHandleHitArea,
|
|
49
60
|
NO_RAYCAST,
|
|
50
61
|
useInvisibleHitAreaMaterial,
|
|
@@ -55,7 +66,6 @@ const HANDLE_MIN_OFFSET = 0.33
|
|
|
55
66
|
const HANDLE_MIN_HEIGHT = 0.4
|
|
56
67
|
const HANDLE_TOP_INSET = 0.08
|
|
57
68
|
const HEIGHT_HANDLE_OFFSET = 0.26
|
|
58
|
-
const MIN_WALL_HEIGHT = 0.5
|
|
59
69
|
const ARROW_COLOR = '#8381ed'
|
|
60
70
|
const ARROW_HOVER_COLOR = '#a5b4fc'
|
|
61
71
|
// Match the door arrows: scale the rendered chevron down to ~two-thirds
|
|
@@ -154,6 +164,7 @@ export function WallMoveSideHandles() {
|
|
|
154
164
|
}
|
|
155
165
|
|
|
156
166
|
function WallMoveSideHandlesForWall({ wall }: { wall: WallNode }) {
|
|
167
|
+
const nodes = useScene((state) => state.nodes)
|
|
157
168
|
// Merge the in-flight drag override so every handle (side-move arrows,
|
|
158
169
|
// height arrow, corner leaders) tracks the live height in real time
|
|
159
170
|
// during a height drag — the scene store stays at the pre-drag value
|
|
@@ -197,24 +208,34 @@ function WallMoveSideHandlesForWall({ wall }: { wall: WallNode }) {
|
|
|
197
208
|
}
|
|
198
209
|
}, [wall.parentId])
|
|
199
210
|
|
|
200
|
-
const
|
|
211
|
+
const baseElevation = getWallBaseElevationForNodes(effectiveWall, nodes)
|
|
212
|
+
const handles = useMemo(() => getWallMoveHandles(effectiveWall, nodes), [effectiveWall, nodes])
|
|
201
213
|
|
|
202
214
|
if (!levelObject || handles.length === 0) return null
|
|
203
215
|
|
|
204
216
|
return createPortal(
|
|
205
|
-
|
|
206
|
-
{
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
217
|
+
<>
|
|
218
|
+
<group position={[0, baseElevation, 0]}>
|
|
219
|
+
{handles.map((handle) => (
|
|
220
|
+
<WallMoveArrowHandle handle={handle} key={handle.key} wall={effectiveWall} />
|
|
221
|
+
))}
|
|
222
|
+
<WallHeightArrowHandle wall={effectiveWall} />
|
|
223
|
+
<WallCornerLeaderHandle endpoint="start" wall={effectiveWall} />
|
|
224
|
+
<WallCornerLeaderHandle endpoint="end" wall={effectiveWall} />
|
|
225
|
+
</group>
|
|
226
|
+
<WallBaseElevationHandle
|
|
227
|
+
baseElevation={baseElevation}
|
|
228
|
+
levelObject={levelObject}
|
|
229
|
+
wall={effectiveWall}
|
|
230
|
+
/>
|
|
231
|
+
</>,
|
|
213
232
|
levelObject,
|
|
214
233
|
)
|
|
215
234
|
}
|
|
216
235
|
|
|
217
236
|
function buildDashedVerticalGeometry(height: number) {
|
|
237
|
+
if (!(Number.isFinite(height) && height > 0)) return new BufferGeometry()
|
|
238
|
+
|
|
218
239
|
// Build each dash as a thin cylinder section so thickness is
|
|
219
240
|
// controllable — native `lineSegments` lock to 1px on WebGL/WebGPU.
|
|
220
241
|
const dashes: BufferGeometry[] = []
|
|
@@ -227,7 +248,10 @@ function buildDashedVerticalGeometry(height: number) {
|
|
|
227
248
|
dashes.push(cylinder)
|
|
228
249
|
y = end + CORNER_GAP_SIZE
|
|
229
250
|
}
|
|
230
|
-
const merged =
|
|
251
|
+
const merged =
|
|
252
|
+
dashes.length > 0
|
|
253
|
+
? (mergeGeometries(dashes, false) ?? new BufferGeometry())
|
|
254
|
+
: new BufferGeometry()
|
|
231
255
|
for (const dash of dashes) dash.dispose()
|
|
232
256
|
return merged
|
|
233
257
|
}
|
|
@@ -244,7 +268,7 @@ function WallCornerLeaderHandle({ wall, endpoint }: { wall: WallNode; endpoint:
|
|
|
244
268
|
const corner = endpoint === 'start' ? wall.start : wall.end
|
|
245
269
|
const x = corner[0]
|
|
246
270
|
const z = corner[1]
|
|
247
|
-
const wallHeight = wall.
|
|
271
|
+
const wallHeight = getWallEffectiveHeightForNodes(wall, useScene.getState().nodes)
|
|
248
272
|
|
|
249
273
|
const dashedGeometry = useMemo(() => buildDashedVerticalGeometry(wallHeight), [wallHeight])
|
|
250
274
|
const hitGeometry = useMemo(() => createEndpointHitAreaGeometry(CORNER_HEX_RADIUS), [])
|
|
@@ -381,6 +405,155 @@ function WallCornerLeaderHandle({ wall, endpoint }: { wall: WallNode; endpoint:
|
|
|
381
405
|
)
|
|
382
406
|
}
|
|
383
407
|
|
|
408
|
+
function WallBaseElevationHandle({
|
|
409
|
+
wall,
|
|
410
|
+
baseElevation,
|
|
411
|
+
levelObject,
|
|
412
|
+
}: {
|
|
413
|
+
wall: WallNode
|
|
414
|
+
baseElevation: number
|
|
415
|
+
levelObject: Object3D
|
|
416
|
+
}) {
|
|
417
|
+
const [isHovered, setIsHovered] = useState(false)
|
|
418
|
+
const [isDragging, setIsDragging] = useState(false)
|
|
419
|
+
const { camera } = useThree()
|
|
420
|
+
const zoom = camera instanceof OrthographicCamera ? 1 / camera.zoom : 1
|
|
421
|
+
const curveFrame = isCurvedWall(wall) ? getWallCurveFrameAt(wall, 0.5) : null
|
|
422
|
+
const midpoint: [number, number] = curveFrame
|
|
423
|
+
? [curveFrame.point.x, curveFrame.point.y]
|
|
424
|
+
: [(wall.start[0] + wall.end[0]) / 2, (wall.start[1] + wall.end[1]) / 2]
|
|
425
|
+
const elevationGuideSource = {
|
|
426
|
+
nodeId: wall.id,
|
|
427
|
+
levelId: wall.parentId,
|
|
428
|
+
anchor: midpoint,
|
|
429
|
+
}
|
|
430
|
+
const leaderBottom = Math.min(0, baseElevation)
|
|
431
|
+
const leaderHeight = Math.abs(baseElevation)
|
|
432
|
+
const dashedGeometry = useMemo(
|
|
433
|
+
() => (leaderHeight > 0.0001 ? buildDashedVerticalGeometry(leaderHeight) : null),
|
|
434
|
+
[leaderHeight],
|
|
435
|
+
)
|
|
436
|
+
const dashMaterial = useMemo(
|
|
437
|
+
() =>
|
|
438
|
+
new MeshBasicNodeMaterial({
|
|
439
|
+
color: new Color(ARROW_COLOR),
|
|
440
|
+
transparent: true,
|
|
441
|
+
opacity: 0.85,
|
|
442
|
+
depthTest: false,
|
|
443
|
+
depthWrite: false,
|
|
444
|
+
}),
|
|
445
|
+
[],
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
const isActive = isHovered || isDragging
|
|
449
|
+
useEffect(() => {
|
|
450
|
+
dashMaterial.color.set(isActive ? ARROW_HOVER_COLOR : ARROW_COLOR)
|
|
451
|
+
}, [dashMaterial, isActive])
|
|
452
|
+
useEffect(() => () => dashedGeometry?.dispose(), [dashedGeometry])
|
|
453
|
+
useEffect(() => () => dashMaterial.dispose(), [dashMaterial])
|
|
454
|
+
const dragControls = useMemo<HandleDragControls>(
|
|
455
|
+
() => ({
|
|
456
|
+
onStart: () => {},
|
|
457
|
+
onEnd: () => {},
|
|
458
|
+
}),
|
|
459
|
+
[],
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
const activateElevationMove = useHandleDrag({
|
|
463
|
+
kind: 'drag',
|
|
464
|
+
cursor: 'ns-resize',
|
|
465
|
+
dragControls,
|
|
466
|
+
handleIndex: 0,
|
|
467
|
+
node: wall,
|
|
468
|
+
rideObject: levelObject,
|
|
469
|
+
setIsDragging,
|
|
470
|
+
onStart: ({ event, initialNode, intersectPlane, nodeId, sceneApi }) => {
|
|
471
|
+
if (initialNode.type !== 'wall') return null
|
|
472
|
+
|
|
473
|
+
levelObject.updateWorldMatrix(true, false)
|
|
474
|
+
const initialBase = getWallBaseElevationForNodes(initialNode, sceneApi.nodes())
|
|
475
|
+
const supportBase = initialBase - (initialNode.supportOffset ?? 0)
|
|
476
|
+
const initialHeight = getWallEffectiveHeightForNodes(initialNode, sceneApi.nodes())
|
|
477
|
+
const midpointWorld = new Vector3(midpoint[0], initialBase, midpoint[1]).applyMatrix4(
|
|
478
|
+
levelObject.matrixWorld,
|
|
479
|
+
)
|
|
480
|
+
const planeNormal = new Vector3().subVectors(camera.position, midpointWorld).setY(0)
|
|
481
|
+
if (planeNormal.lengthSq() === 0) return null
|
|
482
|
+
planeNormal.normalize()
|
|
483
|
+
const plane = new Plane().setFromNormalAndCoplanarPoint(planeNormal, midpointWorld)
|
|
484
|
+
const initialHit = new Vector3()
|
|
485
|
+
if (
|
|
486
|
+
!intersectPlane(event.nativeEvent.clientX, event.nativeEvent.clientY, plane, initialHit)
|
|
487
|
+
) {
|
|
488
|
+
return null
|
|
489
|
+
}
|
|
490
|
+
const initialPointerY = levelObject.worldToLocal(initialHit).y
|
|
491
|
+
|
|
492
|
+
return {
|
|
493
|
+
onBegin: () => {
|
|
494
|
+
useInteractionScope.getState().begin({ kind: 'handle-drag', nodeId, handle: 'elevation' })
|
|
495
|
+
},
|
|
496
|
+
onEnd: () => {
|
|
497
|
+
useInteractionScope.getState().endIf((scope) => scope.kind === 'handle-drag')
|
|
498
|
+
clearStructuralElevationGuide(initialNode.id)
|
|
499
|
+
},
|
|
500
|
+
move: ({ event: moveEvent, intersectPlane: intersectMovePlane }) => {
|
|
501
|
+
const hit = new Vector3()
|
|
502
|
+
if (!intersectMovePlane(moveEvent.clientX, moveEvent.clientY, plane, hit)) return null
|
|
503
|
+
const pointerY = levelObject.worldToLocal(hit).y
|
|
504
|
+
const nextBase = resolveResizeSnapValue({
|
|
505
|
+
rawValue: initialBase + pointerY - initialPointerY,
|
|
506
|
+
gridSnapEnabled: true,
|
|
507
|
+
gridSnapActive: isGridSnapActive(),
|
|
508
|
+
gridSnapStep: useEditor.getState().gridSnapStep,
|
|
509
|
+
magneticSnapActive: isMagneticSnapActive(),
|
|
510
|
+
magneticSnap: (value) =>
|
|
511
|
+
resolveStructuralElevationSnap(elevationGuideSource, value, sceneApi.nodes()),
|
|
512
|
+
})
|
|
513
|
+
publishStructuralElevationGuide(elevationGuideSource, nextBase, sceneApi.nodes())
|
|
514
|
+
if (Math.abs(nextBase - initialBase) < 1e-6) {
|
|
515
|
+
return {
|
|
516
|
+
supportOffset: initialNode.supportOffset,
|
|
517
|
+
height: initialNode.height,
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
const nextOffset = nextBase - supportBase
|
|
521
|
+
return {
|
|
522
|
+
supportOffset: Math.abs(nextOffset) < 1e-6 ? undefined : nextOffset,
|
|
523
|
+
height: initialHeight,
|
|
524
|
+
}
|
|
525
|
+
},
|
|
526
|
+
}
|
|
527
|
+
},
|
|
528
|
+
})
|
|
529
|
+
|
|
530
|
+
return (
|
|
531
|
+
<>
|
|
532
|
+
{dashedGeometry ? (
|
|
533
|
+
<mesh
|
|
534
|
+
frustumCulled={false}
|
|
535
|
+
geometry={dashedGeometry}
|
|
536
|
+
material={dashMaterial}
|
|
537
|
+
position={[midpoint[0], leaderBottom, midpoint[1]]}
|
|
538
|
+
renderOrder={1001}
|
|
539
|
+
/>
|
|
540
|
+
) : null}
|
|
541
|
+
<HandleArrow
|
|
542
|
+
cursor="ns-resize"
|
|
543
|
+
hover={isActive}
|
|
544
|
+
hoverScale={1.25}
|
|
545
|
+
onHoverChange={setIsHovered}
|
|
546
|
+
onPointerDown={activateElevationMove}
|
|
547
|
+
placement={{
|
|
548
|
+
position: [midpoint[0], baseElevation, midpoint[1]],
|
|
549
|
+
baseScale: zoom,
|
|
550
|
+
}}
|
|
551
|
+
shape="tracker"
|
|
552
|
+
/>
|
|
553
|
+
</>
|
|
554
|
+
)
|
|
555
|
+
}
|
|
556
|
+
|
|
384
557
|
function WallHeightArrowHandle({ wall }: { wall: WallNode }) {
|
|
385
558
|
const [isHovered, setIsHovered] = useState(false)
|
|
386
559
|
const arrowGeometry = useMemo(() => createArrowHandleGeometry(), [])
|
|
@@ -433,7 +606,7 @@ function WallHeightArrowHandle({ wall }: { wall: WallNode }) {
|
|
|
433
606
|
const wallAngle = Math.atan2(-dirZ, dirX)
|
|
434
607
|
// `wall` is the override-merged effective wall (see
|
|
435
608
|
// WallMoveSideHandlesForWall), so this height is already live during a drag.
|
|
436
|
-
const wallHeight = wall.
|
|
609
|
+
const wallHeight = getWallEffectiveHeightForNodes(wall, useScene.getState().nodes)
|
|
437
610
|
const handleY = wallHeight + HEIGHT_HANDLE_OFFSET
|
|
438
611
|
|
|
439
612
|
const activateHeightResize = (event: ThreeEvent<PointerEvent>) => {
|
|
@@ -466,7 +639,9 @@ function WallHeightArrowHandle({ wall }: { wall: WallNode }) {
|
|
|
466
639
|
const hit = new Vector3()
|
|
467
640
|
if (!raycaster.ray.intersectPlane(plane, hit)) return
|
|
468
641
|
|
|
469
|
-
|
|
642
|
+
// Dragging the top makes the wall custom-height; seed from the resolved
|
|
643
|
+
// effective height so a plane-bound wall's drag starts at its real top.
|
|
644
|
+
const initialHeight = getWallEffectiveHeightForNodes(wall, useScene.getState().nodes)
|
|
470
645
|
const initialY = hit.y
|
|
471
646
|
const wallId = wall.id as AnyNodeId
|
|
472
647
|
let pendingHeight = initialHeight
|
|
@@ -758,7 +933,7 @@ function FenceMoveArrowHandle({ fence, handle }: { fence: FenceNode; handle: Wal
|
|
|
758
933
|
)
|
|
759
934
|
}
|
|
760
935
|
|
|
761
|
-
function getWallMoveHandles(wall: WallNode): WallMoveHandle[] {
|
|
936
|
+
function getWallMoveHandles(wall: WallNode, nodes: Record<string, AnyNode>): WallMoveHandle[] {
|
|
762
937
|
const dx = wall.end[0] - wall.start[0]
|
|
763
938
|
const dz = wall.end[1] - wall.start[1]
|
|
764
939
|
const length = Math.hypot(dx, dz)
|
|
@@ -774,7 +949,7 @@ function getWallMoveHandles(wall: WallNode): WallMoveHandle[] {
|
|
|
774
949
|
const midpoint: [number, number] = frame
|
|
775
950
|
? [frame.point.x, frame.point.y]
|
|
776
951
|
: [(wall.start[0] + wall.end[0]) / 2, (wall.start[1] + wall.end[1]) / 2]
|
|
777
|
-
const wallHeight = wall
|
|
952
|
+
const wallHeight = getWallEffectiveHeightForNodes(wall, nodes)
|
|
778
953
|
const handleHeight = Math.max(wallHeight - HANDLE_TOP_INSET, HANDLE_MIN_HEIGHT)
|
|
779
954
|
const offset = Math.max(getWallThickness(wall) / 2 + HANDLE_OFFSET, HANDLE_MIN_OFFSET)
|
|
780
955
|
|
|
@@ -3,8 +3,16 @@
|
|
|
3
3
|
import { type AnyNodeId, sceneRegistry, useLiveNodeOverrides, useScene } from '@pascal-app/core'
|
|
4
4
|
import { useViewer } from '@pascal-app/viewer'
|
|
5
5
|
import { createPortal, useFrame, useThree } from '@react-three/fiber'
|
|
6
|
-
import { useEffect, useMemo, useRef } from 'react'
|
|
7
|
-
import {
|
|
6
|
+
import { useEffect, useLayoutEffect, useMemo, useRef } from 'react'
|
|
7
|
+
import {
|
|
8
|
+
BoxGeometry,
|
|
9
|
+
type BufferGeometry,
|
|
10
|
+
EdgesGeometry,
|
|
11
|
+
type Group,
|
|
12
|
+
type Mesh,
|
|
13
|
+
MeshBasicMaterial,
|
|
14
|
+
Vector3,
|
|
15
|
+
} from 'three'
|
|
8
16
|
import { LineBasicNodeMaterial, MeshBasicNodeMaterial } from 'three/webgpu'
|
|
9
17
|
import { EDITOR_LAYER } from '../../lib/constants'
|
|
10
18
|
|
|
@@ -41,6 +49,13 @@ const fillMaterial = new MeshBasicNodeMaterial({
|
|
|
41
49
|
depthWrite: false,
|
|
42
50
|
})
|
|
43
51
|
|
|
52
|
+
// Keep the cutout in the render list for the outliner's override material
|
|
53
|
+
// without letting its source material alter the normal scene pass.
|
|
54
|
+
const outlineProxyMaterial = new MeshBasicMaterial({
|
|
55
|
+
colorWrite: false,
|
|
56
|
+
depthWrite: false,
|
|
57
|
+
})
|
|
58
|
+
|
|
44
59
|
function makeOutlineGeometry(width: number, height: number, depth: number): BufferGeometry {
|
|
45
60
|
const box = new BoxGeometry(width + PAD, height + PAD, depth + PAD)
|
|
46
61
|
const edges = new EdgesGeometry(box)
|
|
@@ -60,26 +75,60 @@ function makeOutlineGeometry(width: number, height: number, depth: number): Buff
|
|
|
60
75
|
*/
|
|
61
76
|
export function WallOpeningHighlights() {
|
|
62
77
|
const selectedIds = useViewer((state) => state.selection.selectedIds)
|
|
78
|
+
const hoveredId = useViewer((state) => state.hoveredId)
|
|
63
79
|
const { scene } = useThree()
|
|
80
|
+
const outlineProxyIds = Array.from(new Set(hoveredId ? [...selectedIds, hoveredId] : selectedIds))
|
|
64
81
|
|
|
65
|
-
if (selectedIds.length === 0) return null
|
|
82
|
+
if (selectedIds.length === 0 && !hoveredId) return null
|
|
66
83
|
|
|
67
|
-
return
|
|
84
|
+
return (
|
|
68
85
|
<>
|
|
69
|
-
{
|
|
70
|
-
<
|
|
86
|
+
{outlineProxyIds.map((id) => (
|
|
87
|
+
<OpeningOutlineProxy key={id} openingId={id} />
|
|
71
88
|
))}
|
|
72
|
-
|
|
73
|
-
|
|
89
|
+
{selectedIds.length > 0 &&
|
|
90
|
+
createPortal(
|
|
91
|
+
<>
|
|
92
|
+
{selectedIds.map((id) => (
|
|
93
|
+
<SelectionOpeningHighlights key={id} selectedId={id} />
|
|
94
|
+
))}
|
|
95
|
+
</>,
|
|
96
|
+
scene,
|
|
97
|
+
)}
|
|
98
|
+
</>
|
|
74
99
|
)
|
|
75
100
|
}
|
|
76
101
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
102
|
+
function OpeningOutlineProxy({ openingId }: { openingId: string }) {
|
|
103
|
+
const node = useScene((state) => state.nodes[openingId as AnyNodeId])
|
|
104
|
+
const geometryRevision = useViewer((state) => state.geometryRevision)
|
|
105
|
+
const proxyRef = useRef<Mesh | null>(null)
|
|
106
|
+
const isFramelessOpening = node?.type === 'door' && node.openingKind === 'opening'
|
|
107
|
+
|
|
108
|
+
useLayoutEffect(() => {
|
|
109
|
+
void geometryRevision
|
|
110
|
+
if (!isFramelessOpening) return
|
|
111
|
+
const root = sceneRegistry.nodes.get(openingId as AnyNodeId)
|
|
112
|
+
const proxy = root?.getObjectByName('cutout') as Mesh | undefined
|
|
113
|
+
if (!proxy) return
|
|
114
|
+
|
|
115
|
+
proxy.material = outlineProxyMaterial
|
|
116
|
+
proxy.visible = true
|
|
117
|
+
proxyRef.current = proxy
|
|
118
|
+
|
|
119
|
+
return () => {
|
|
120
|
+
proxy.visible = false
|
|
121
|
+
proxyRef.current = null
|
|
122
|
+
}
|
|
123
|
+
}, [geometryRevision, isFramelessOpening, openingId])
|
|
124
|
+
|
|
125
|
+
useFrame(() => {
|
|
126
|
+
if (proxyRef.current) proxyRef.current.visible = true
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
return null
|
|
130
|
+
}
|
|
131
|
+
|
|
83
132
|
function SelectionOpeningHighlights({ selectedId }: { selectedId: string }) {
|
|
84
133
|
const node = useScene((state) => state.nodes[selectedId as AnyNodeId])
|
|
85
134
|
|
|
@@ -94,27 +143,9 @@ function SelectionOpeningHighlights({ selectedId }: { selectedId: string }) {
|
|
|
94
143
|
)
|
|
95
144
|
}
|
|
96
145
|
|
|
97
|
-
if (node?.type === 'door' && node.openingKind === 'opening') {
|
|
98
|
-
return <SelectedOpeningHighlight openingId={selectedId} parentId={node.parentId} />
|
|
99
|
-
}
|
|
100
|
-
|
|
101
146
|
return null
|
|
102
147
|
}
|
|
103
148
|
|
|
104
|
-
// A frameless opening selected on its own. Pulls the cutout depth from its
|
|
105
|
-
// host wall's thickness so the fill block matches the wall it sits in.
|
|
106
|
-
function SelectedOpeningHighlight({
|
|
107
|
-
openingId,
|
|
108
|
-
parentId,
|
|
109
|
-
}: {
|
|
110
|
-
openingId: string
|
|
111
|
-
parentId: string | null
|
|
112
|
-
}) {
|
|
113
|
-
const parent = useScene((state) => (parentId ? state.nodes[parentId as AnyNodeId] : undefined))
|
|
114
|
-
const depth = parent?.type === 'wall' ? (parent.thickness ?? 0.1) : 0.1
|
|
115
|
-
return <OpeningHighlight depth={depth} openingId={openingId} />
|
|
116
|
-
}
|
|
117
|
-
|
|
118
149
|
function OpeningHighlight({ openingId, depth }: { openingId: string; depth: number }) {
|
|
119
150
|
const node = useScene((state) => state.nodes[openingId as AnyNodeId])
|
|
120
151
|
// Resize arrows publish width/height to the live-override store during the
|
|
@@ -3,12 +3,13 @@
|
|
|
3
3
|
import {
|
|
4
4
|
type AnyNode,
|
|
5
5
|
type AnyNodeId,
|
|
6
|
-
DEFAULT_WALL_HEIGHT,
|
|
7
6
|
getWallCurveFrameAt,
|
|
8
7
|
getWallCurveLength,
|
|
8
|
+
getWallPlaneTop,
|
|
9
9
|
getWallThickness,
|
|
10
10
|
isCurvedWall,
|
|
11
11
|
resolveLevelId,
|
|
12
|
+
resolveWallTop,
|
|
12
13
|
sceneRegistry,
|
|
13
14
|
spatialGridManager,
|
|
14
15
|
useScene,
|
|
@@ -147,15 +148,16 @@ type WallTopHighlightSegment = {
|
|
|
147
148
|
|
|
148
149
|
function getWallTopY(wall: WallNode, nodes: Readonly<Record<string, AnyNode>>) {
|
|
149
150
|
const levelId = resolveLevelId(wall, nodes as Record<string, AnyNode>)
|
|
150
|
-
const
|
|
151
|
+
const support = spatialGridManager.getSlabSupportForWall(
|
|
151
152
|
levelId,
|
|
152
153
|
wall.start,
|
|
153
154
|
wall.end,
|
|
154
155
|
wall.curveOffset ?? 0,
|
|
155
156
|
wall.thickness,
|
|
157
|
+
wall.supportSlabId,
|
|
156
158
|
)
|
|
157
|
-
const
|
|
158
|
-
return (
|
|
159
|
+
const planeTop = getWallPlaneTop(wall, levelId, nodes as Record<string, AnyNode>)
|
|
160
|
+
return resolveWallTop(wall, planeTop, support.elevation) + WALL_TOP_HIGHLIGHT_LIFT
|
|
159
161
|
}
|
|
160
162
|
|
|
161
163
|
function buildHighlightSegment(start: [number, number], end: [number, number]) {
|
|
@@ -28,6 +28,7 @@ import { useFloorplanRender } from './floorplan-render-context'
|
|
|
28
28
|
export const FloorplanAlignmentGuideLayer = memo(function FloorplanAlignmentGuideLayer() {
|
|
29
29
|
const guides = useAlignmentGuides((s) => s.guides)
|
|
30
30
|
const unit = useViewer((s) => s.unit)
|
|
31
|
+
const metricNotation = useViewer((s) => s.metricNotation)
|
|
31
32
|
const ctx = useFloorplanRender()
|
|
32
33
|
|
|
33
34
|
if (guides.length === 0) return null
|
|
@@ -61,7 +62,7 @@ export const FloorplanAlignmentGuideLayer = memo(function FloorplanAlignmentGuid
|
|
|
61
62
|
// offset along X.
|
|
62
63
|
const pillX = axis === 'x' ? midX + pillOffset : midX
|
|
63
64
|
const pillZ = axis === 'z' ? midZ + pillOffset : midZ
|
|
64
|
-
const distLabel = formatMeasurement(distMeters, unit)
|
|
65
|
+
const distLabel = formatMeasurement(distMeters, unit, metricNotation)
|
|
65
66
|
const charWidth = pillFontSize * 0.55
|
|
66
67
|
const pillWidth = distLabel.length * charWidth + pillPadX * 2
|
|
67
68
|
const pillHeight = pillFontSize + pillPadY * 2
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import { Icon } from '@iconify/react'
|
|
4
|
-
import { memo, useMemo } from 'react'
|
|
4
|
+
import { memo, useLayoutEffect, useMemo, useRef, useState } from 'react'
|
|
5
5
|
import useEditor, { type FloorplanSelectionTool } from '../../store/use-editor'
|
|
6
|
+
import { useFloorplanDraftPreview } from '../../store/use-floorplan-draft-preview'
|
|
6
7
|
import { furnishTools } from '../ui/action-menu/furnish-tools'
|
|
7
8
|
import { tools as structureTools } from '../ui/action-menu/structure-tools'
|
|
8
|
-
|
|
9
|
-
type
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
}
|
|
9
|
+
import {
|
|
10
|
+
type FloorplanCursorPoint,
|
|
11
|
+
projectFloorplanCursorPoint,
|
|
12
|
+
resolveFloorplanCursorIndicatorPosition,
|
|
13
|
+
} from './floorplan-cursor-indicator-position'
|
|
13
14
|
|
|
14
15
|
type FloorplanCursorIndicator =
|
|
15
16
|
| {
|
|
@@ -22,7 +23,7 @@ type FloorplanCursorIndicator =
|
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
type FloorplanCursorIndicatorOverlayProps = {
|
|
25
|
-
cursorPosition:
|
|
26
|
+
cursorPosition: FloorplanCursorPoint | null
|
|
26
27
|
floorplanSelectionTool: FloorplanSelectionTool
|
|
27
28
|
movingOpeningType: 'door' | 'window' | null
|
|
28
29
|
isPanning: boolean
|
|
@@ -46,6 +47,10 @@ export const FloorplanCursorIndicatorOverlay = memo(function FloorplanCursorIndi
|
|
|
46
47
|
const tool = useEditor((state) => state.tool)
|
|
47
48
|
const structureLayer = useEditor((state) => state.structureLayer)
|
|
48
49
|
const catalogCategory = useEditor((state) => state.catalogCategory)
|
|
50
|
+
const cursorPoint = useFloorplanDraftPreview((state) => state.cursorPoint)
|
|
51
|
+
const anchorRef = useRef<HTMLDivElement>(null)
|
|
52
|
+
const [projectedCursorPosition, setProjectedCursorPosition] =
|
|
53
|
+
useState<FloorplanCursorPoint | null>(null)
|
|
49
54
|
|
|
50
55
|
const activeFloorplanToolConfig = useMemo(() => {
|
|
51
56
|
if (movingOpeningType) {
|
|
@@ -83,7 +88,32 @@ export const FloorplanCursorIndicatorOverlay = memo(function FloorplanCursorIndi
|
|
|
83
88
|
return null
|
|
84
89
|
}, [activeFloorplanToolConfig, floorplanSelectionTool, mode, structureLayer])
|
|
85
90
|
|
|
86
|
-
|
|
91
|
+
useLayoutEffect(() => {
|
|
92
|
+
const anchor = anchorRef.current
|
|
93
|
+
const overlayHost = anchor?.parentElement
|
|
94
|
+
const scene = overlayHost?.querySelector<SVGGElement>('[data-floorplan-scene]')
|
|
95
|
+
const sceneToViewport = scene?.getScreenCTM()
|
|
96
|
+
|
|
97
|
+
if (!(cursorPoint && cursorPosition && overlayHost && sceneToViewport)) {
|
|
98
|
+
setProjectedCursorPosition(null)
|
|
99
|
+
return
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const overlayRect = overlayHost.getBoundingClientRect()
|
|
103
|
+
const nextPosition = projectFloorplanCursorPoint(cursorPoint, sceneToViewport, {
|
|
104
|
+
x: overlayRect.left,
|
|
105
|
+
y: overlayRect.top,
|
|
106
|
+
})
|
|
107
|
+
setProjectedCursorPosition((currentPosition) =>
|
|
108
|
+
currentPosition &&
|
|
109
|
+
currentPosition.x === nextPosition.x &&
|
|
110
|
+
currentPosition.y === nextPosition.y
|
|
111
|
+
? currentPosition
|
|
112
|
+
: nextPosition,
|
|
113
|
+
)
|
|
114
|
+
}, [cursorPoint, cursorPosition])
|
|
115
|
+
|
|
116
|
+
const position = resolveFloorplanCursorIndicatorPosition(cursorPosition, projectedCursorPosition)
|
|
87
117
|
|
|
88
118
|
if (!(indicator && position) || isPanning) {
|
|
89
119
|
return null
|
|
@@ -93,6 +123,7 @@ export const FloorplanCursorIndicatorOverlay = memo(function FloorplanCursorIndi
|
|
|
93
123
|
<div
|
|
94
124
|
aria-hidden="true"
|
|
95
125
|
className="pointer-events-none absolute z-20"
|
|
126
|
+
ref={anchorRef}
|
|
96
127
|
style={{ left: position.x, top: position.y }}
|
|
97
128
|
>
|
|
98
129
|
{mode === 'delete' ? (
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import {
|
|
3
|
+
projectFloorplanCursorPoint,
|
|
4
|
+
resolveFloorplanCursorIndicatorPosition,
|
|
5
|
+
} from './floorplan-cursor-indicator-position'
|
|
6
|
+
|
|
7
|
+
describe('projectFloorplanCursorPoint', () => {
|
|
8
|
+
test('projects a snapped plan point into overlay-local screen coordinates', () => {
|
|
9
|
+
expect(
|
|
10
|
+
projectFloorplanCursorPoint(
|
|
11
|
+
[3, 4],
|
|
12
|
+
{
|
|
13
|
+
a: 0,
|
|
14
|
+
b: 10,
|
|
15
|
+
c: -10,
|
|
16
|
+
d: 0,
|
|
17
|
+
e: 128,
|
|
18
|
+
f: 40,
|
|
19
|
+
},
|
|
20
|
+
{ x: 30, y: 20 },
|
|
21
|
+
),
|
|
22
|
+
).toEqual({ x: 58, y: 50 })
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
test('anchors the placement pin to the projected snap point instead of the raw pointer', () => {
|
|
26
|
+
expect(resolveFloorplanCursorIndicatorPosition({ x: 88, y: 97 }, { x: 58, y: 70 })).toEqual({
|
|
27
|
+
x: 58,
|
|
28
|
+
y: 70,
|
|
29
|
+
})
|
|
30
|
+
})
|
|
31
|
+
})
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export type FloorplanCursorPoint = {
|
|
2
|
+
x: number
|
|
3
|
+
y: number
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
type Matrix2D = {
|
|
7
|
+
a: number
|
|
8
|
+
b: number
|
|
9
|
+
c: number
|
|
10
|
+
d: number
|
|
11
|
+
e: number
|
|
12
|
+
f: number
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function projectFloorplanCursorPoint(
|
|
16
|
+
point: readonly [number, number],
|
|
17
|
+
sceneToViewport: Matrix2D,
|
|
18
|
+
overlayOrigin: FloorplanCursorPoint,
|
|
19
|
+
): FloorplanCursorPoint {
|
|
20
|
+
return {
|
|
21
|
+
x:
|
|
22
|
+
sceneToViewport.a * point[0] +
|
|
23
|
+
sceneToViewport.c * point[1] +
|
|
24
|
+
sceneToViewport.e -
|
|
25
|
+
overlayOrigin.x,
|
|
26
|
+
y:
|
|
27
|
+
sceneToViewport.b * point[0] +
|
|
28
|
+
sceneToViewport.d * point[1] +
|
|
29
|
+
sceneToViewport.f -
|
|
30
|
+
overlayOrigin.y,
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function resolveFloorplanCursorIndicatorPosition(
|
|
35
|
+
cursorPosition: FloorplanCursorPoint | null,
|
|
36
|
+
projectedCursorPosition: FloorplanCursorPoint | null,
|
|
37
|
+
): FloorplanCursorPoint | null {
|
|
38
|
+
return projectedCursorPosition ?? cursorPosition
|
|
39
|
+
}
|