@pascal-app/editor 0.5.1 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +8 -7
- package/src/components/editor/editor-layout-v2.tsx +9 -0
- package/src/components/editor/floating-action-menu.tsx +255 -34
- package/src/components/editor/floating-building-action-menu.tsx +4 -3
- package/src/components/editor/floorplan-panel.tsx +1323 -713
- package/src/components/editor/index.tsx +2 -0
- package/src/components/editor/node-action-menu.tsx +14 -1
- package/src/components/editor/selection-manager.tsx +200 -8
- package/src/components/editor/site-edge-labels.tsx +9 -3
- package/src/components/editor/thumbnail-generator.tsx +319 -157
- package/src/components/editor/wall-measurement-label.tsx +120 -32
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +272 -0
- package/src/components/systems/roof/roof-edit-system.tsx +5 -5
- package/src/components/tools/ceiling/ceiling-hole-editor.tsx +1 -0
- package/src/components/tools/ceiling/move-ceiling-tool.tsx +257 -0
- package/src/components/tools/door/door-tool.tsx +12 -0
- package/src/components/tools/door/move-door-tool.tsx +10 -0
- package/src/components/tools/fence/curve-fence-tool.tsx +179 -0
- package/src/components/tools/fence/fence-drafting.ts +19 -7
- package/src/components/tools/fence/move-fence-endpoint-tool.tsx +327 -0
- package/src/components/tools/fence/move-fence-tool.tsx +8 -0
- package/src/components/tools/item/move-tool.tsx +9 -0
- package/src/components/tools/item/placement-math.ts +14 -6
- package/src/components/tools/item/placement-strategies.ts +2 -2
- package/src/components/tools/item/use-placement-coordinator.tsx +42 -10
- package/src/components/tools/roof/move-roof-tool.tsx +89 -28
- package/src/components/tools/shared/polygon-editor.tsx +98 -8
- package/src/components/tools/slab/move-slab-tool.tsx +182 -0
- package/src/components/tools/slab/slab-hole-editor.tsx +1 -0
- package/src/components/tools/stair/stair-tool.tsx +11 -3
- package/src/components/tools/tool-manager.tsx +12 -0
- package/src/components/tools/wall/curve-wall-tool.tsx +176 -0
- package/src/components/tools/wall/move-wall-endpoint-tool.tsx +322 -0
- package/src/components/tools/wall/move-wall-tool.tsx +356 -0
- package/src/components/tools/wall/wall-drafting.ts +331 -9
- package/src/components/tools/window/move-window-tool.tsx +10 -0
- package/src/components/tools/window/window-tool.tsx +12 -0
- package/src/components/ui/action-menu/control-modes.tsx +9 -4
- package/src/components/ui/command-palette/editor-commands.tsx +9 -4
- package/src/components/ui/command-palette/index.tsx +0 -1
- package/src/components/ui/controls/material-picker.tsx +127 -94
- package/src/components/ui/controls/slider-control.tsx +28 -14
- package/src/components/ui/item-catalog/catalog-items.tsx +5 -0
- package/src/components/ui/panels/ceiling-panel.tsx +61 -17
- package/src/components/ui/panels/door-panel.tsx +5 -5
- package/src/components/ui/panels/fence-panel.tsx +97 -12
- package/src/components/ui/panels/item-panel.tsx +5 -5
- package/src/components/ui/panels/panel-manager.tsx +31 -29
- package/src/components/ui/panels/reference-panel.tsx +5 -4
- package/src/components/ui/panels/roof-panel.tsx +91 -22
- package/src/components/ui/panels/roof-segment-panel.tsx +23 -13
- package/src/components/ui/panels/slab-panel.tsx +63 -15
- package/src/components/ui/panels/stair-panel.tsx +173 -19
- package/src/components/ui/panels/stair-segment-panel.tsx +28 -17
- package/src/components/ui/panels/wall-panel.tsx +159 -11
- package/src/components/ui/panels/window-panel.tsx +5 -7
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +29 -32
- package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/tree-node-actions.tsx +3 -3
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +3 -3
- package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +7 -3
- package/src/components/ui/viewer-toolbar.tsx +55 -2
- package/src/components/viewer-overlay.tsx +25 -19
- package/src/hooks/use-contextual-tools.ts +14 -13
- package/src/hooks/use-keyboard.ts +3 -2
- package/src/index.tsx +2 -1
- package/src/lib/history.ts +20 -0
- package/src/lib/sfx-player.ts +96 -13
- package/src/store/use-editor.tsx +118 -10
|
@@ -5,17 +5,23 @@ import {
|
|
|
5
5
|
type AnyNode,
|
|
6
6
|
type AnyNodeId,
|
|
7
7
|
type BuildingNode,
|
|
8
|
+
type CeilingNode,
|
|
8
9
|
calculateLevelMiters,
|
|
9
10
|
DoorNode,
|
|
10
11
|
emitter,
|
|
11
12
|
type GridEvent,
|
|
12
13
|
type GuideNode,
|
|
13
14
|
getScaledDimensions,
|
|
15
|
+
getWallChordFrame,
|
|
16
|
+
getWallCurveLength,
|
|
17
|
+
getWallMidpointHandlePoint,
|
|
14
18
|
getWallPlanFootprint,
|
|
15
19
|
type ItemNode,
|
|
16
20
|
ItemNode as ItemNodeSchema,
|
|
21
|
+
isCurvedWall,
|
|
17
22
|
type LevelNode,
|
|
18
23
|
loadAssetUrl,
|
|
24
|
+
normalizeWallCurveOffset,
|
|
19
25
|
type Point2D,
|
|
20
26
|
type SiteNode,
|
|
21
27
|
SlabNode,
|
|
@@ -46,7 +52,7 @@ import { createPortal } from 'react-dom'
|
|
|
46
52
|
import { useShallow } from 'zustand/react/shallow'
|
|
47
53
|
import { sfxEmitter } from '../../lib/sfx-bus'
|
|
48
54
|
import { cn } from '../../lib/utils'
|
|
49
|
-
import useEditor from '../../store/use-editor'
|
|
55
|
+
import useEditor, { type FloorplanSelectionTool } from '../../store/use-editor'
|
|
50
56
|
import { snapToHalf } from '../tools/item/placement-math'
|
|
51
57
|
import {
|
|
52
58
|
DEFAULT_STAIR_ATTACHMENT_SIDE,
|
|
@@ -153,6 +159,12 @@ type FloorplanViewport = {
|
|
|
153
159
|
width: number
|
|
154
160
|
}
|
|
155
161
|
|
|
162
|
+
function floorplanViewportEquals(a: FloorplanViewport | null, b: FloorplanViewport | null) {
|
|
163
|
+
if (a === b) return true
|
|
164
|
+
if (!(a && b)) return false
|
|
165
|
+
return a.centerX === b.centerX && a.centerY === b.centerY && a.width === b.width
|
|
166
|
+
}
|
|
167
|
+
|
|
156
168
|
type SvgPoint = {
|
|
157
169
|
x: number
|
|
158
170
|
y: number
|
|
@@ -235,6 +247,12 @@ type WallEndpointDragState = {
|
|
|
235
247
|
currentPoint: WallPlanPoint
|
|
236
248
|
}
|
|
237
249
|
|
|
250
|
+
type WallCurveDragState = {
|
|
251
|
+
pointerId: number
|
|
252
|
+
wallId: WallNode['id']
|
|
253
|
+
currentCurveOffset: number
|
|
254
|
+
}
|
|
255
|
+
|
|
238
256
|
const GUIDE_CORNERS = ['nw', 'ne', 'se', 'sw'] as const
|
|
239
257
|
|
|
240
258
|
type GuideCorner = (typeof GUIDE_CORNERS)[number]
|
|
@@ -276,6 +294,11 @@ type WallEndpointDraft = {
|
|
|
276
294
|
end: WallPlanPoint
|
|
277
295
|
}
|
|
278
296
|
|
|
297
|
+
type WallCurveDraft = {
|
|
298
|
+
wallId: WallNode['id']
|
|
299
|
+
curveOffset: number
|
|
300
|
+
}
|
|
301
|
+
|
|
279
302
|
type SlabBoundaryDraft = {
|
|
280
303
|
slabId: SlabNode['id']
|
|
281
304
|
polygon: WallPlanPoint[]
|
|
@@ -2176,7 +2199,7 @@ function getWallMeasurementOverlay(
|
|
|
2176
2199
|
): WallMeasurementOverlay | null {
|
|
2177
2200
|
const dx = wall.end[0] - wall.start[0]
|
|
2178
2201
|
const dz = wall.end[1] - wall.start[1]
|
|
2179
|
-
const length =
|
|
2202
|
+
const length = getWallCurveLength(wall)
|
|
2180
2203
|
|
|
2181
2204
|
if (length < 0.1) {
|
|
2182
2205
|
return null
|
|
@@ -2425,13 +2448,19 @@ function buildGridPath(
|
|
|
2425
2448
|
function findClosestWallPoint(
|
|
2426
2449
|
point: WallPlanPoint,
|
|
2427
2450
|
walls: WallNode[],
|
|
2428
|
-
|
|
2451
|
+
options?: {
|
|
2452
|
+
maxDistance?: number
|
|
2453
|
+
canUseWall?: (wall: WallNode) => boolean
|
|
2454
|
+
},
|
|
2429
2455
|
): {
|
|
2430
2456
|
wall: WallNode
|
|
2431
2457
|
point: WallPlanPoint
|
|
2432
2458
|
t: number
|
|
2433
2459
|
normal: [number, number, number]
|
|
2434
2460
|
} | null {
|
|
2461
|
+
const maxDistance = options?.maxDistance ?? 0.5
|
|
2462
|
+
const canUseWall = options?.canUseWall
|
|
2463
|
+
|
|
2435
2464
|
let best: {
|
|
2436
2465
|
wall: WallNode
|
|
2437
2466
|
point: WallPlanPoint
|
|
@@ -2441,6 +2470,10 @@ function findClosestWallPoint(
|
|
|
2441
2470
|
let bestDistSq = maxDistance * maxDistance
|
|
2442
2471
|
|
|
2443
2472
|
for (const wall of walls) {
|
|
2473
|
+
if (canUseWall && !canUseWall(wall)) {
|
|
2474
|
+
continue
|
|
2475
|
+
}
|
|
2476
|
+
|
|
2444
2477
|
const [x1, z1] = wall.start
|
|
2445
2478
|
const [x2, z2] = wall.end
|
|
2446
2479
|
const dx = x2 - x1
|
|
@@ -4351,6 +4384,97 @@ const FloorplanWallEndpointLayer = memo(function FloorplanWallEndpointLayer({
|
|
|
4351
4384
|
)
|
|
4352
4385
|
})
|
|
4353
4386
|
|
|
4387
|
+
const FloorplanWallCurveHandleLayer = memo(function FloorplanWallCurveHandleLayer({
|
|
4388
|
+
curveHandles,
|
|
4389
|
+
hoveredHandleId,
|
|
4390
|
+
onHandleHoverChange,
|
|
4391
|
+
onWallCurvePointerDown,
|
|
4392
|
+
palette,
|
|
4393
|
+
}: {
|
|
4394
|
+
curveHandles: Array<{
|
|
4395
|
+
wall: WallNode
|
|
4396
|
+
point: WallPlanPoint
|
|
4397
|
+
isActive: boolean
|
|
4398
|
+
}>
|
|
4399
|
+
hoveredHandleId: string | null
|
|
4400
|
+
onHandleHoverChange: (handleId: string | null) => void
|
|
4401
|
+
onWallCurvePointerDown: (wall: WallNode, event: ReactPointerEvent<SVGCircleElement>) => void
|
|
4402
|
+
palette: FloorplanPalette
|
|
4403
|
+
}) {
|
|
4404
|
+
return (
|
|
4405
|
+
<>
|
|
4406
|
+
{curveHandles.map(({ wall, point, isActive }) => {
|
|
4407
|
+
const handleId = `curve:${wall.id}`
|
|
4408
|
+
const isHovered = hoveredHandleId === handleId
|
|
4409
|
+
const stroke = isActive ? palette.endpointHandleActiveStroke : palette.endpointHandleStroke
|
|
4410
|
+
const hoverStroke = isActive
|
|
4411
|
+
? palette.endpointHandleActiveStroke
|
|
4412
|
+
: palette.endpointHandleHoverStroke
|
|
4413
|
+
const svgPoint = toSvgPlanPoint(point)
|
|
4414
|
+
const radius = isActive ? 0.16 : 0.14
|
|
4415
|
+
|
|
4416
|
+
return (
|
|
4417
|
+
<g
|
|
4418
|
+
key={handleId}
|
|
4419
|
+
onClick={(event) => {
|
|
4420
|
+
event.stopPropagation()
|
|
4421
|
+
}}
|
|
4422
|
+
onPointerEnter={() => onHandleHoverChange(handleId)}
|
|
4423
|
+
onPointerLeave={() => onHandleHoverChange(null)}
|
|
4424
|
+
>
|
|
4425
|
+
<circle
|
|
4426
|
+
cx={svgPoint.x}
|
|
4427
|
+
cy={svgPoint.y}
|
|
4428
|
+
fill="none"
|
|
4429
|
+
pointerEvents="none"
|
|
4430
|
+
r={radius}
|
|
4431
|
+
stroke={hoverStroke}
|
|
4432
|
+
strokeOpacity={isActive ? 0.24 : 0.16}
|
|
4433
|
+
strokeWidth={FLOORPLAN_ENDPOINT_HOVER_GLOW_STROKE_WIDTH}
|
|
4434
|
+
style={{
|
|
4435
|
+
opacity: isHovered ? 1 : 0,
|
|
4436
|
+
transition: FLOORPLAN_HOVER_TRANSITION,
|
|
4437
|
+
}}
|
|
4438
|
+
vectorEffect="non-scaling-stroke"
|
|
4439
|
+
/>
|
|
4440
|
+
<circle
|
|
4441
|
+
cx={svgPoint.x}
|
|
4442
|
+
cy={svgPoint.y}
|
|
4443
|
+
fill={isActive ? palette.endpointHandleActiveFill : palette.endpointHandleFill}
|
|
4444
|
+
fillOpacity={0.96}
|
|
4445
|
+
pointerEvents="none"
|
|
4446
|
+
r={radius}
|
|
4447
|
+
stroke={stroke}
|
|
4448
|
+
strokeWidth="0.05"
|
|
4449
|
+
vectorEffect="non-scaling-stroke"
|
|
4450
|
+
/>
|
|
4451
|
+
<circle
|
|
4452
|
+
cx={svgPoint.x}
|
|
4453
|
+
cy={svgPoint.y}
|
|
4454
|
+
fill={stroke}
|
|
4455
|
+
pointerEvents="none"
|
|
4456
|
+
r={0.045}
|
|
4457
|
+
vectorEffect="non-scaling-stroke"
|
|
4458
|
+
/>
|
|
4459
|
+
<circle
|
|
4460
|
+
cx={svgPoint.x}
|
|
4461
|
+
cy={svgPoint.y}
|
|
4462
|
+
fill="transparent"
|
|
4463
|
+
onPointerDown={(event) => onWallCurvePointerDown(wall, event)}
|
|
4464
|
+
pointerEvents="all"
|
|
4465
|
+
r={radius}
|
|
4466
|
+
stroke="transparent"
|
|
4467
|
+
strokeWidth={FLOORPLAN_ENDPOINT_HIT_STROKE_WIDTH}
|
|
4468
|
+
style={{ cursor: EDITOR_CURSOR }}
|
|
4469
|
+
vectorEffect="non-scaling-stroke"
|
|
4470
|
+
/>
|
|
4471
|
+
</g>
|
|
4472
|
+
)
|
|
4473
|
+
})}
|
|
4474
|
+
</>
|
|
4475
|
+
)
|
|
4476
|
+
})
|
|
4477
|
+
|
|
4354
4478
|
const FloorplanPolygonHandleLayer = memo(function FloorplanPolygonHandleLayer({
|
|
4355
4479
|
hoveredHandleId,
|
|
4356
4480
|
midpointHandles,
|
|
@@ -4537,149 +4661,449 @@ const FloorplanPolygonHandleLayer = memo(function FloorplanPolygonHandleLayer({
|
|
|
4537
4661
|
)
|
|
4538
4662
|
})
|
|
4539
4663
|
|
|
4540
|
-
|
|
4541
|
-
|
|
4542
|
-
|
|
4543
|
-
const panStateRef = useRef<PanState | null>(null)
|
|
4544
|
-
const guideInteractionRef = useRef<GuideInteractionState | null>(null)
|
|
4545
|
-
const guideTransformDraftRef = useRef<GuideTransformDraft | null>(null)
|
|
4546
|
-
const wallEndpointDragRef = useRef<WallEndpointDragState | null>(null)
|
|
4547
|
-
const siteBoundaryDraftRef = useRef<SiteBoundaryDraft | null>(null)
|
|
4548
|
-
const slabBoundaryDraftRef = useRef<SlabBoundaryDraft | null>(null)
|
|
4549
|
-
const zoneBoundaryDraftRef = useRef<ZoneBoundaryDraft | null>(null)
|
|
4550
|
-
const gestureScaleRef = useRef(1)
|
|
4551
|
-
const panelInteractionRef = useRef<PanelInteractionState | null>(null)
|
|
4552
|
-
const panelBoundsRef = useRef<ViewportBounds | null>(null)
|
|
4553
|
-
const containerRef = useRef<HTMLDivElement>(null)
|
|
4554
|
-
const hasUserAdjustedViewportRef = useRef(false)
|
|
4555
|
-
const previousLevelIdRef = useRef<string | null>(null)
|
|
4556
|
-
const floorplanMarqueeSnapPointRef = useRef<WallPlanPoint | null>(null)
|
|
4557
|
-
const levelId = useViewer((state) => state.selection.levelId)
|
|
4558
|
-
const buildingId = useViewer((state) => state.selection.buildingId)
|
|
4559
|
-
const selectedZoneId = useViewer((state) => state.selection.zoneId)
|
|
4560
|
-
const selectedIds = useViewer((state) => state.selection.selectedIds)
|
|
4561
|
-
const previewSelectedIds = useViewer((state) => state.previewSelectedIds)
|
|
4562
|
-
const setSelection = useViewer((state) => state.setSelection)
|
|
4563
|
-
const setPreviewSelectedIds = useViewer((state) => state.setPreviewSelectedIds)
|
|
4564
|
-
const theme = useViewer((state) => state.theme)
|
|
4565
|
-
const unit = useViewer((state) => state.unit)
|
|
4566
|
-
const showGrid = useViewer((state) => state.showGrid)
|
|
4567
|
-
const showGuides = useViewer((state) => state.showGuides)
|
|
4568
|
-
const setShowGuides = useViewer((state) => state.setShowGuides)
|
|
4569
|
-
const catalogCategory = useEditor((state) => state.catalogCategory)
|
|
4570
|
-
const setCatalogCategory = useEditor((state) => state.setCatalogCategory)
|
|
4571
|
-
const selectedItem = useEditor((state) => state.selectedItem)
|
|
4664
|
+
type FloorplanSiteKeyHandlerProps = {
|
|
4665
|
+
onRestoreGroundLevel: () => void
|
|
4666
|
+
}
|
|
4572
4667
|
|
|
4668
|
+
const FloorplanSiteKeyHandler = memo(function FloorplanSiteKeyHandler({
|
|
4669
|
+
onRestoreGroundLevel,
|
|
4670
|
+
}: FloorplanSiteKeyHandlerProps) {
|
|
4573
4671
|
const isFloorplanHovered = useEditor((state) => state.isFloorplanHovered)
|
|
4574
|
-
const setFloorplanHovered = useEditor((state) => state.setFloorplanHovered)
|
|
4575
|
-
const selectedReferenceId = useEditor((state) => state.selectedReferenceId)
|
|
4576
|
-
const setSelectedReferenceId = useEditor((state) => state.setSelectedReferenceId)
|
|
4577
|
-
const setMode = useEditor((state) => state.setMode)
|
|
4578
|
-
const movingNode = useEditor((state) => state.movingNode)
|
|
4579
4672
|
const phase = useEditor((state) => state.phase)
|
|
4580
|
-
const
|
|
4581
|
-
|
|
4582
|
-
|
|
4583
|
-
|
|
4584
|
-
|
|
4585
|
-
|
|
4586
|
-
|
|
4587
|
-
|
|
4588
|
-
|
|
4589
|
-
|
|
4590
|
-
|
|
4591
|
-
|
|
4592
|
-
|
|
4593
|
-
|
|
4594
|
-
|
|
4595
|
-
|
|
4596
|
-
|
|
4597
|
-
|
|
4598
|
-
|
|
4599
|
-
|
|
4600
|
-
})
|
|
4601
|
-
const buildingRotationDeg = (buildingRotationY * 180) / Math.PI
|
|
4602
|
-
const site = useScene((state) => {
|
|
4603
|
-
for (const rootNodeId of state.rootNodeIds) {
|
|
4604
|
-
const node = state.nodes[rootNodeId]
|
|
4605
|
-
if (node?.type === 'site') {
|
|
4606
|
-
return node as SiteNode
|
|
4673
|
+
const setFloorplanSelectionTool = useEditor((state) => state.setFloorplanSelectionTool)
|
|
4674
|
+
|
|
4675
|
+
useEffect(() => {
|
|
4676
|
+
const handleKeyDown = (event: KeyboardEvent) => {
|
|
4677
|
+
const target = event.target as HTMLElement | null
|
|
4678
|
+
const isEditableTarget =
|
|
4679
|
+
target instanceof HTMLInputElement ||
|
|
4680
|
+
target instanceof HTMLTextAreaElement ||
|
|
4681
|
+
Boolean(target?.isContentEditable)
|
|
4682
|
+
|
|
4683
|
+
if (
|
|
4684
|
+
isEditableTarget ||
|
|
4685
|
+
!isFloorplanHovered ||
|
|
4686
|
+
phase !== 'site' ||
|
|
4687
|
+
event.metaKey ||
|
|
4688
|
+
event.ctrlKey ||
|
|
4689
|
+
event.altKey ||
|
|
4690
|
+
event.key.toLowerCase() !== 'v'
|
|
4691
|
+
) {
|
|
4692
|
+
return
|
|
4607
4693
|
}
|
|
4694
|
+
|
|
4695
|
+
setFloorplanSelectionTool('click')
|
|
4696
|
+
onRestoreGroundLevel()
|
|
4608
4697
|
}
|
|
4609
4698
|
|
|
4610
|
-
|
|
4611
|
-
|
|
4612
|
-
|
|
4613
|
-
|
|
4614
|
-
|
|
4615
|
-
return [] as LevelNode[]
|
|
4616
|
-
}
|
|
4699
|
+
window.addEventListener('keydown', handleKeyDown, true)
|
|
4700
|
+
return () => {
|
|
4701
|
+
window.removeEventListener('keydown', handleKeyDown, true)
|
|
4702
|
+
}
|
|
4703
|
+
}, [isFloorplanHovered, onRestoreGroundLevel, phase, setFloorplanSelectionTool])
|
|
4617
4704
|
|
|
4618
|
-
|
|
4619
|
-
|
|
4620
|
-
return [] as LevelNode[]
|
|
4621
|
-
}
|
|
4705
|
+
return null
|
|
4706
|
+
})
|
|
4622
4707
|
|
|
4623
|
-
|
|
4624
|
-
|
|
4625
|
-
|
|
4626
|
-
|
|
4627
|
-
}),
|
|
4628
|
-
)
|
|
4629
|
-
const walls = useScene(
|
|
4630
|
-
useShallow((state) => {
|
|
4631
|
-
if (!levelId) {
|
|
4632
|
-
return [] as WallNode[]
|
|
4633
|
-
}
|
|
4708
|
+
type FloorplanDuplicateHotkeyProps = {
|
|
4709
|
+
hasDuplicatable: boolean
|
|
4710
|
+
onDuplicateSelected: () => void
|
|
4711
|
+
}
|
|
4634
4712
|
|
|
4635
|
-
|
|
4636
|
-
|
|
4637
|
-
|
|
4638
|
-
|
|
4713
|
+
const FloorplanDuplicateHotkey = memo(function FloorplanDuplicateHotkey({
|
|
4714
|
+
hasDuplicatable,
|
|
4715
|
+
onDuplicateSelected,
|
|
4716
|
+
}: FloorplanDuplicateHotkeyProps) {
|
|
4717
|
+
const isFloorplanHovered = useEditor((state) => state.isFloorplanHovered)
|
|
4639
4718
|
|
|
4640
|
-
|
|
4641
|
-
|
|
4642
|
-
|
|
4643
|
-
|
|
4644
|
-
)
|
|
4645
|
-
const openings = useScene(
|
|
4646
|
-
useShallow((state) => {
|
|
4647
|
-
if (!levelId) {
|
|
4648
|
-
return [] as OpeningNode[]
|
|
4719
|
+
useEffect(() => {
|
|
4720
|
+
const handleKeyDown = (event: KeyboardEvent) => {
|
|
4721
|
+
if (!(event.metaKey || event.ctrlKey) || event.key.toLowerCase() !== 'c') {
|
|
4722
|
+
return
|
|
4649
4723
|
}
|
|
4650
4724
|
|
|
4651
|
-
|
|
4652
|
-
|
|
4653
|
-
return [] as OpeningNode[]
|
|
4725
|
+
if (!(isFloorplanHovered && hasDuplicatable)) {
|
|
4726
|
+
return
|
|
4654
4727
|
}
|
|
4655
4728
|
|
|
4656
|
-
const
|
|
4657
|
-
|
|
4658
|
-
|
|
4729
|
+
const target = event.target as HTMLElement | null
|
|
4730
|
+
const isEditableTarget =
|
|
4731
|
+
target instanceof HTMLInputElement ||
|
|
4732
|
+
target instanceof HTMLTextAreaElement ||
|
|
4733
|
+
Boolean(target?.isContentEditable)
|
|
4659
4734
|
|
|
4660
|
-
|
|
4661
|
-
|
|
4662
|
-
.map((childId) => state.nodes[childId])
|
|
4663
|
-
.filter((node): node is OpeningNode => node?.type === 'window' || node?.type === 'door'),
|
|
4664
|
-
)
|
|
4665
|
-
}),
|
|
4666
|
-
)
|
|
4667
|
-
const slabs = useScene(
|
|
4668
|
-
useShallow((state) => {
|
|
4669
|
-
if (!levelId) {
|
|
4670
|
-
return [] as SlabNode[]
|
|
4735
|
+
if (isEditableTarget) {
|
|
4736
|
+
return
|
|
4671
4737
|
}
|
|
4672
4738
|
|
|
4673
|
-
|
|
4674
|
-
|
|
4675
|
-
|
|
4676
|
-
}
|
|
4739
|
+
event.preventDefault()
|
|
4740
|
+
onDuplicateSelected()
|
|
4741
|
+
}
|
|
4677
4742
|
|
|
4678
|
-
|
|
4743
|
+
window.addEventListener('keydown', handleKeyDown, true)
|
|
4744
|
+
return () => {
|
|
4745
|
+
window.removeEventListener('keydown', handleKeyDown, true)
|
|
4746
|
+
}
|
|
4747
|
+
}, [hasDuplicatable, isFloorplanHovered, onDuplicateSelected])
|
|
4748
|
+
|
|
4749
|
+
return null
|
|
4750
|
+
})
|
|
4751
|
+
|
|
4752
|
+
type FloorplanActionMenuHandler = (event: ReactMouseEvent<HTMLButtonElement>) => void
|
|
4753
|
+
|
|
4754
|
+
type FloorplanActionMenuEntry = {
|
|
4755
|
+
position: SvgPoint | null
|
|
4756
|
+
onDelete: FloorplanActionMenuHandler
|
|
4757
|
+
onMove: FloorplanActionMenuHandler
|
|
4758
|
+
onDuplicate?: FloorplanActionMenuHandler
|
|
4759
|
+
}
|
|
4760
|
+
|
|
4761
|
+
type FloorplanActionMenuLayerProps = {
|
|
4762
|
+
item: FloorplanActionMenuEntry
|
|
4763
|
+
wall: FloorplanActionMenuEntry
|
|
4764
|
+
slab: FloorplanActionMenuEntry
|
|
4765
|
+
ceiling: FloorplanActionMenuEntry
|
|
4766
|
+
opening: FloorplanActionMenuEntry
|
|
4767
|
+
stair: FloorplanActionMenuEntry
|
|
4768
|
+
}
|
|
4769
|
+
|
|
4770
|
+
const FloorplanActionMenuLayer = memo(function FloorplanActionMenuLayer({
|
|
4771
|
+
item,
|
|
4772
|
+
wall,
|
|
4773
|
+
slab,
|
|
4774
|
+
ceiling,
|
|
4775
|
+
opening,
|
|
4776
|
+
stair,
|
|
4777
|
+
}: FloorplanActionMenuLayerProps) {
|
|
4778
|
+
const isFloorplanHovered = useEditor((state) => state.isFloorplanHovered)
|
|
4779
|
+
const movingNode = useEditor((state) => state.movingNode)
|
|
4780
|
+
const curvingWall = useEditor((state) => state.curvingWall)
|
|
4781
|
+
const curvingFence = useEditor((state) => state.curvingFence)
|
|
4782
|
+
|
|
4783
|
+
if (!isFloorplanHovered || movingNode || curvingWall || curvingFence) {
|
|
4784
|
+
return null
|
|
4785
|
+
}
|
|
4786
|
+
|
|
4787
|
+
const entries: FloorplanActionMenuEntry[] = [item, wall, slab, ceiling, opening, stair]
|
|
4788
|
+
|
|
4789
|
+
return (
|
|
4790
|
+
<>
|
|
4791
|
+
{entries.map((entry, index) =>
|
|
4792
|
+
entry.position ? (
|
|
4793
|
+
<div
|
|
4794
|
+
className="absolute z-30"
|
|
4795
|
+
key={index}
|
|
4796
|
+
style={{
|
|
4797
|
+
left: entry.position.x,
|
|
4798
|
+
top: entry.position.y,
|
|
4799
|
+
transform: `translate(-50%, calc(-100% - ${FLOORPLAN_ACTION_MENU_OFFSET_Y}px))`,
|
|
4800
|
+
}}
|
|
4801
|
+
>
|
|
4802
|
+
<NodeActionMenu
|
|
4803
|
+
onDelete={entry.onDelete}
|
|
4804
|
+
onDuplicate={entry.onDuplicate}
|
|
4805
|
+
onMove={entry.onMove}
|
|
4806
|
+
onPointerDown={(event) => event.stopPropagation()}
|
|
4807
|
+
onPointerUp={(event) => event.stopPropagation()}
|
|
4808
|
+
/>
|
|
4809
|
+
</div>
|
|
4810
|
+
) : null,
|
|
4811
|
+
)}
|
|
4812
|
+
</>
|
|
4813
|
+
)
|
|
4814
|
+
})
|
|
4815
|
+
|
|
4816
|
+
type FloorplanCursorIndicatorOverlayProps = {
|
|
4817
|
+
cursorPosition: SvgPoint | null
|
|
4818
|
+
cursorAnchorPosition: SvgPoint | null
|
|
4819
|
+
floorplanSelectionTool: FloorplanSelectionTool
|
|
4820
|
+
movingOpeningType: 'door' | 'window' | null
|
|
4821
|
+
isPanning: boolean
|
|
4822
|
+
cursorColor: string
|
|
4823
|
+
}
|
|
4824
|
+
|
|
4825
|
+
const FloorplanCursorIndicatorOverlay = memo(function FloorplanCursorIndicatorOverlay({
|
|
4826
|
+
cursorPosition,
|
|
4827
|
+
cursorAnchorPosition,
|
|
4828
|
+
floorplanSelectionTool,
|
|
4829
|
+
movingOpeningType,
|
|
4830
|
+
isPanning,
|
|
4831
|
+
cursorColor,
|
|
4832
|
+
}: FloorplanCursorIndicatorOverlayProps) {
|
|
4833
|
+
const mode = useEditor((state) => state.mode)
|
|
4834
|
+
const tool = useEditor((state) => state.tool)
|
|
4835
|
+
const structureLayer = useEditor((state) => state.structureLayer)
|
|
4836
|
+
const catalogCategory = useEditor((state) => state.catalogCategory)
|
|
4837
|
+
|
|
4838
|
+
const activeFloorplanToolConfig = useMemo(() => {
|
|
4839
|
+
if (movingOpeningType) {
|
|
4840
|
+
return structureTools.find((entry) => entry.id === movingOpeningType) ?? null
|
|
4841
|
+
}
|
|
4842
|
+
|
|
4843
|
+
if (mode !== 'build' || !tool) {
|
|
4844
|
+
return null
|
|
4845
|
+
}
|
|
4846
|
+
|
|
4847
|
+
if (tool === 'item' && catalogCategory) {
|
|
4848
|
+
return furnishTools.find((entry) => entry.catalogCategory === catalogCategory) ?? null
|
|
4849
|
+
}
|
|
4850
|
+
|
|
4851
|
+
return structureTools.find((entry) => entry.id === tool) ?? null
|
|
4852
|
+
}, [catalogCategory, mode, movingOpeningType, tool])
|
|
4853
|
+
|
|
4854
|
+
const indicator = useMemo<FloorplanCursorIndicator | null>(() => {
|
|
4855
|
+
if (activeFloorplanToolConfig) {
|
|
4856
|
+
return { kind: 'asset', iconSrc: activeFloorplanToolConfig.iconSrc }
|
|
4857
|
+
}
|
|
4858
|
+
|
|
4859
|
+
if (mode === 'select' && floorplanSelectionTool === 'marquee' && structureLayer !== 'zones') {
|
|
4860
|
+
return { kind: 'icon', icon: 'mdi:select-drag' }
|
|
4861
|
+
}
|
|
4862
|
+
|
|
4863
|
+
if (mode === 'delete') {
|
|
4864
|
+
return { kind: 'icon', icon: 'mdi:trash-can-outline' }
|
|
4865
|
+
}
|
|
4866
|
+
|
|
4867
|
+
return null
|
|
4868
|
+
}, [activeFloorplanToolConfig, floorplanSelectionTool, mode, structureLayer])
|
|
4869
|
+
|
|
4870
|
+
const position = mode === 'delete' ? cursorPosition : cursorAnchorPosition
|
|
4871
|
+
|
|
4872
|
+
if (!(indicator && position) || isPanning) {
|
|
4873
|
+
return null
|
|
4874
|
+
}
|
|
4875
|
+
|
|
4876
|
+
return (
|
|
4877
|
+
<div
|
|
4878
|
+
aria-hidden="true"
|
|
4879
|
+
className="pointer-events-none absolute z-20"
|
|
4880
|
+
style={{ left: position.x, top: position.y }}
|
|
4881
|
+
>
|
|
4882
|
+
{mode === 'delete' ? (
|
|
4883
|
+
<div
|
|
4884
|
+
className="flex h-8 w-8 items-center justify-center rounded-xl border border-white/5 bg-zinc-900/95 shadow-[0_8px_16px_-4px_rgba(0,0,0,0.3),0_4px_8px_-4px_rgba(0,0,0,0.2)]"
|
|
4885
|
+
style={{
|
|
4886
|
+
boxShadow: `0 8px 16px -4px rgba(0,0,0,0.3), 0 4px 8px -4px rgba(0,0,0,0.2), 0 0 18px ${cursorColor}22`,
|
|
4887
|
+
transform: `translate(${FLOORPLAN_CURSOR_BADGE_OFFSET_X}px, ${FLOORPLAN_CURSOR_BADGE_OFFSET_Y}px)`,
|
|
4888
|
+
}}
|
|
4889
|
+
>
|
|
4890
|
+
{indicator.kind === 'asset' ? (
|
|
4891
|
+
<img
|
|
4892
|
+
alt=""
|
|
4893
|
+
aria-hidden="true"
|
|
4894
|
+
className="h-5 w-5 object-contain drop-shadow-[0_2px_4px_rgba(0,0,0,0.5)]"
|
|
4895
|
+
src={indicator.iconSrc}
|
|
4896
|
+
/>
|
|
4897
|
+
) : (
|
|
4898
|
+
<Icon
|
|
4899
|
+
aria-hidden="true"
|
|
4900
|
+
className="drop-shadow-[0_2px_4px_rgba(0,0,0,0.5)]"
|
|
4901
|
+
color={cursorColor}
|
|
4902
|
+
height={18}
|
|
4903
|
+
icon={indicator.icon}
|
|
4904
|
+
width={18}
|
|
4905
|
+
/>
|
|
4906
|
+
)}
|
|
4907
|
+
</div>
|
|
4908
|
+
) : (
|
|
4909
|
+
<>
|
|
4910
|
+
<div
|
|
4911
|
+
className="absolute top-0 left-1/2 w-px -translate-x-1/2 -translate-y-full"
|
|
4912
|
+
style={{
|
|
4913
|
+
backgroundColor: cursorColor,
|
|
4914
|
+
boxShadow: `0 0 12px ${cursorColor}55`,
|
|
4915
|
+
height: FLOORPLAN_CURSOR_INDICATOR_LINE_HEIGHT,
|
|
4916
|
+
}}
|
|
4917
|
+
/>
|
|
4918
|
+
<div
|
|
4919
|
+
className="absolute top-0 left-1/2 flex h-8 w-8 items-center justify-center rounded-xl border border-white/5 bg-zinc-900/95 shadow-[0_8px_16px_-4px_rgba(0,0,0,0.3),0_4px_8px_-4px_rgba(0,0,0,0.2)]"
|
|
4920
|
+
style={{
|
|
4921
|
+
transform: `translate(-50%, calc(-100% - ${FLOORPLAN_CURSOR_INDICATOR_LINE_HEIGHT}px))`,
|
|
4922
|
+
}}
|
|
4923
|
+
>
|
|
4924
|
+
{indicator.kind === 'asset' ? (
|
|
4925
|
+
<img
|
|
4926
|
+
alt=""
|
|
4927
|
+
aria-hidden="true"
|
|
4928
|
+
className="h-5 w-5 object-contain drop-shadow-[0_2px_4px_rgba(0,0,0,0.5)]"
|
|
4929
|
+
src={indicator.iconSrc}
|
|
4930
|
+
/>
|
|
4931
|
+
) : (
|
|
4932
|
+
<Icon
|
|
4933
|
+
aria-hidden="true"
|
|
4934
|
+
className="drop-shadow-[0_2px_4px_rgba(0,0,0,0.5)]"
|
|
4935
|
+
color="white"
|
|
4936
|
+
height={18}
|
|
4937
|
+
icon={indicator.icon}
|
|
4938
|
+
width={18}
|
|
4939
|
+
/>
|
|
4940
|
+
)}
|
|
4941
|
+
</div>
|
|
4942
|
+
</>
|
|
4943
|
+
)}
|
|
4944
|
+
</div>
|
|
4945
|
+
)
|
|
4946
|
+
})
|
|
4947
|
+
|
|
4948
|
+
export function FloorplanPanel() {
|
|
4949
|
+
const viewportHostRef = useRef<HTMLDivElement>(null)
|
|
4950
|
+
const svgRef = useRef<SVGSVGElement>(null)
|
|
4951
|
+
const panStateRef = useRef<PanState | null>(null)
|
|
4952
|
+
const guideInteractionRef = useRef<GuideInteractionState | null>(null)
|
|
4953
|
+
const guideTransformDraftRef = useRef<GuideTransformDraft | null>(null)
|
|
4954
|
+
const wallEndpointDragRef = useRef<WallEndpointDragState | null>(null)
|
|
4955
|
+
const wallCurveDragRef = useRef<WallCurveDragState | null>(null)
|
|
4956
|
+
const siteBoundaryDraftRef = useRef<SiteBoundaryDraft | null>(null)
|
|
4957
|
+
const slabBoundaryDraftRef = useRef<SlabBoundaryDraft | null>(null)
|
|
4958
|
+
const zoneBoundaryDraftRef = useRef<ZoneBoundaryDraft | null>(null)
|
|
4959
|
+
const gestureScaleRef = useRef(1)
|
|
4960
|
+
const panelInteractionRef = useRef<PanelInteractionState | null>(null)
|
|
4961
|
+
const panelBoundsRef = useRef<ViewportBounds | null>(null)
|
|
4962
|
+
const containerRef = useRef<HTMLDivElement>(null)
|
|
4963
|
+
const hasUserAdjustedViewportRef = useRef(false)
|
|
4964
|
+
const previousLevelIdRef = useRef<string | null>(null)
|
|
4965
|
+
const floorplanMarqueeSnapPointRef = useRef<WallPlanPoint | null>(null)
|
|
4966
|
+
const levelId = useViewer((state) => state.selection.levelId)
|
|
4967
|
+
const buildingId = useViewer((state) => state.selection.buildingId)
|
|
4968
|
+
const selectedZoneId = useViewer((state) => state.selection.zoneId)
|
|
4969
|
+
const selectedIds = useViewer((state) => state.selection.selectedIds)
|
|
4970
|
+
const previewSelectedIds = useViewer((state) => state.previewSelectedIds)
|
|
4971
|
+
const setSelection = useViewer((state) => state.setSelection)
|
|
4972
|
+
const setPreviewSelectedIds = useViewer((state) => state.setPreviewSelectedIds)
|
|
4973
|
+
const theme = useViewer((state) => state.theme)
|
|
4974
|
+
const unit = useViewer((state) => state.unit)
|
|
4975
|
+
const showGrid = useViewer((state) => state.showGrid)
|
|
4976
|
+
const showGuides = useViewer((state) => state.showGuides)
|
|
4977
|
+
const setShowGuides = useViewer((state) => state.setShowGuides)
|
|
4978
|
+
const selectedItem = useEditor((state) => state.selectedItem)
|
|
4979
|
+
|
|
4980
|
+
const setFloorplanHovered = useEditor((state) => state.setFloorplanHovered)
|
|
4981
|
+
const selectedReferenceId = useEditor((state) => state.selectedReferenceId)
|
|
4982
|
+
const setSelectedReferenceId = useEditor((state) => state.setSelectedReferenceId)
|
|
4983
|
+
const setMode = useEditor((state) => state.setMode)
|
|
4984
|
+
const movingNode = useEditor((state) => state.movingNode)
|
|
4985
|
+
const curvingWall = useEditor((state) => state.curvingWall)
|
|
4986
|
+
const curvingFence = useEditor((state) => state.curvingFence)
|
|
4987
|
+
const phase = useEditor((state) => state.phase)
|
|
4988
|
+
const mode = useEditor((state) => state.mode)
|
|
4989
|
+
const setPhase = useEditor((state) => state.setPhase)
|
|
4990
|
+
const setMovingNode = useEditor((state) => state.setMovingNode)
|
|
4991
|
+
const structureLayer = useEditor((state) => state.structureLayer)
|
|
4992
|
+
const setStructureLayer = useEditor((state) => state.setStructureLayer)
|
|
4993
|
+
const setTool = useEditor((state) => state.setTool)
|
|
4994
|
+
const tool = useEditor((state) => state.tool)
|
|
4995
|
+
const deleteNode = useScene((state) => state.deleteNode)
|
|
4996
|
+
const updateNode = useScene((state) => state.updateNode)
|
|
4997
|
+
const levelNode = useScene((state) =>
|
|
4998
|
+
levelId ? (state.nodes[levelId] as LevelNode | undefined) : undefined,
|
|
4999
|
+
)
|
|
5000
|
+
const currentBuildingId =
|
|
5001
|
+
levelNode?.type === 'level' && levelNode.parentId
|
|
5002
|
+
? (levelNode.parentId as BuildingNode['id'])
|
|
5003
|
+
: (buildingId as BuildingNode['id'] | null)
|
|
5004
|
+
const buildingRotationY = useScene((state) => {
|
|
5005
|
+
if (!currentBuildingId) return 0
|
|
5006
|
+
const node = state.nodes[currentBuildingId]
|
|
5007
|
+
return node?.type === 'building' ? (node.rotation[1] ?? 0) : 0
|
|
5008
|
+
})
|
|
5009
|
+
const buildingRotationDeg = (buildingRotationY * 180) / Math.PI
|
|
5010
|
+
const site = useScene((state) => {
|
|
5011
|
+
for (const rootNodeId of state.rootNodeIds) {
|
|
5012
|
+
const node = state.nodes[rootNodeId]
|
|
5013
|
+
if (node?.type === 'site') {
|
|
5014
|
+
return node as SiteNode
|
|
5015
|
+
}
|
|
5016
|
+
}
|
|
5017
|
+
|
|
5018
|
+
return null
|
|
5019
|
+
})
|
|
5020
|
+
const floorplanLevels = useScene(
|
|
5021
|
+
useShallow((state) => {
|
|
5022
|
+
if (!currentBuildingId) {
|
|
5023
|
+
return [] as LevelNode[]
|
|
5024
|
+
}
|
|
5025
|
+
|
|
5026
|
+
const buildingNode = state.nodes[currentBuildingId]
|
|
5027
|
+
if (!buildingNode || buildingNode.type !== 'building') {
|
|
5028
|
+
return [] as LevelNode[]
|
|
5029
|
+
}
|
|
5030
|
+
|
|
5031
|
+
return buildingNode.children
|
|
5032
|
+
.map((childId) => state.nodes[childId])
|
|
5033
|
+
.filter((node): node is LevelNode => node?.type === 'level')
|
|
5034
|
+
.sort((a, b) => a.level - b.level)
|
|
5035
|
+
}),
|
|
5036
|
+
)
|
|
5037
|
+
const walls = useScene(
|
|
5038
|
+
useShallow((state) => {
|
|
5039
|
+
if (!levelId) {
|
|
5040
|
+
return [] as WallNode[]
|
|
5041
|
+
}
|
|
5042
|
+
|
|
5043
|
+
const nextLevelNode = state.nodes[levelId]
|
|
5044
|
+
if (!nextLevelNode || nextLevelNode.type !== 'level') {
|
|
5045
|
+
return [] as WallNode[]
|
|
5046
|
+
}
|
|
5047
|
+
|
|
5048
|
+
return nextLevelNode.children
|
|
5049
|
+
.map((childId) => state.nodes[childId])
|
|
5050
|
+
.filter((node): node is WallNode => node?.type === 'wall')
|
|
5051
|
+
}),
|
|
5052
|
+
)
|
|
5053
|
+
const openings = useScene(
|
|
5054
|
+
useShallow((state) => {
|
|
5055
|
+
if (!levelId) {
|
|
5056
|
+
return [] as OpeningNode[]
|
|
5057
|
+
}
|
|
5058
|
+
|
|
5059
|
+
const nextLevelNode = state.nodes[levelId]
|
|
5060
|
+
if (!nextLevelNode || nextLevelNode.type !== 'level') {
|
|
5061
|
+
return [] as OpeningNode[]
|
|
5062
|
+
}
|
|
5063
|
+
|
|
5064
|
+
const nextWalls = nextLevelNode.children
|
|
5065
|
+
.map((childId) => state.nodes[childId])
|
|
5066
|
+
.filter((node): node is WallNode => node?.type === 'wall')
|
|
5067
|
+
|
|
5068
|
+
return nextWalls.flatMap((wall) =>
|
|
5069
|
+
wall.children
|
|
5070
|
+
.map((childId) => state.nodes[childId])
|
|
5071
|
+
.filter((node): node is OpeningNode => node?.type === 'window' || node?.type === 'door'),
|
|
5072
|
+
)
|
|
5073
|
+
}),
|
|
5074
|
+
)
|
|
5075
|
+
const slabs = useScene(
|
|
5076
|
+
useShallow((state) => {
|
|
5077
|
+
if (!levelId) {
|
|
5078
|
+
return [] as SlabNode[]
|
|
5079
|
+
}
|
|
5080
|
+
|
|
5081
|
+
const nextLevelNode = state.nodes[levelId]
|
|
5082
|
+
if (!nextLevelNode || nextLevelNode.type !== 'level') {
|
|
5083
|
+
return [] as SlabNode[]
|
|
5084
|
+
}
|
|
5085
|
+
|
|
5086
|
+
return nextLevelNode.children
|
|
4679
5087
|
.map((childId) => state.nodes[childId])
|
|
4680
5088
|
.filter((node): node is SlabNode => node?.type === 'slab')
|
|
4681
5089
|
}),
|
|
4682
5090
|
)
|
|
5091
|
+
const ceilings = useScene(
|
|
5092
|
+
useShallow((state) => {
|
|
5093
|
+
if (!levelId) {
|
|
5094
|
+
return [] as CeilingNode[]
|
|
5095
|
+
}
|
|
5096
|
+
|
|
5097
|
+
const nextLevelNode = state.nodes[levelId]
|
|
5098
|
+
if (!nextLevelNode || nextLevelNode.type !== 'level') {
|
|
5099
|
+
return [] as CeilingNode[]
|
|
5100
|
+
}
|
|
5101
|
+
|
|
5102
|
+
return nextLevelNode.children
|
|
5103
|
+
.map((childId) => state.nodes[childId])
|
|
5104
|
+
.filter((node): node is CeilingNode => node?.type === 'ceiling')
|
|
5105
|
+
}),
|
|
5106
|
+
)
|
|
4683
5107
|
const levelGuides = useScene(
|
|
4684
5108
|
useShallow((state) => {
|
|
4685
5109
|
if (!levelId) {
|
|
@@ -4741,6 +5165,7 @@ export function FloorplanPanel() {
|
|
|
4741
5165
|
const [cursorPoint, setCursorPoint] = useState<WallPlanPoint | null>(null)
|
|
4742
5166
|
const [floorplanCursorPosition, setFloorplanCursorPosition] = useState<SvgPoint | null>(null)
|
|
4743
5167
|
const [wallEndpointDraft, setWallEndpointDraft] = useState<WallEndpointDraft | null>(null)
|
|
5168
|
+
const [wallCurveDraft, setWallCurveDraft] = useState<WallCurveDraft | null>(null)
|
|
4744
5169
|
const [hoveredOpeningId, setHoveredOpeningId] = useState<OpeningNode['id'] | null>(null)
|
|
4745
5170
|
const [hoveredWallId, setHoveredWallId] = useState<WallNode['id'] | null>(null)
|
|
4746
5171
|
const [hoveredSlabId, setHoveredSlabId] = useState<SlabNode['id'] | null>(null)
|
|
@@ -4748,6 +5173,7 @@ export function FloorplanPanel() {
|
|
|
4748
5173
|
const [hoveredStairId, setHoveredStairId] = useState<StairNode['id'] | null>(null)
|
|
4749
5174
|
const [hoveredZoneId, setHoveredZoneId] = useState<ZoneNodeType['id'] | null>(null)
|
|
4750
5175
|
const [hoveredEndpointId, setHoveredEndpointId] = useState<string | null>(null)
|
|
5176
|
+
const [hoveredWallCurveHandleId, setHoveredWallCurveHandleId] = useState<string | null>(null)
|
|
4751
5177
|
const [hoveredSiteHandleId, setHoveredSiteHandleId] = useState<string | null>(null)
|
|
4752
5178
|
const [hoveredSlabHandleId, setHoveredSlabHandleId] = useState<string | null>(null)
|
|
4753
5179
|
const [hoveredZoneHandleId, setHoveredZoneHandleId] = useState<string | null>(null)
|
|
@@ -4810,59 +5236,20 @@ export function FloorplanPanel() {
|
|
|
4810
5236
|
}
|
|
4811
5237
|
|
|
4812
5238
|
if (!(siteBoundaryDraft && siteBoundaryDraft.siteId === sitePolygonEntry.site.id)) {
|
|
4813
|
-
return sitePolygonEntry
|
|
4814
|
-
}
|
|
4815
|
-
|
|
4816
|
-
const polygon = siteBoundaryDraft.polygon.map(toPoint2D)
|
|
4817
|
-
|
|
4818
|
-
return {
|
|
4819
|
-
...sitePolygonEntry,
|
|
4820
|
-
polygon,
|
|
4821
|
-
points: formatPolygonPoints(polygon),
|
|
4822
|
-
}
|
|
4823
|
-
}, [siteBoundaryDraft, sitePolygonEntry])
|
|
4824
|
-
const movingOpeningType =
|
|
4825
|
-
movingNode?.type === 'door' || movingNode?.type === 'window' ? movingNode.type : null
|
|
4826
|
-
|
|
4827
|
-
const activeFloorplanToolConfig = useMemo(() => {
|
|
4828
|
-
if (movingOpeningType) {
|
|
4829
|
-
return structureTools.find((entry) => entry.id === movingOpeningType) ?? null
|
|
4830
|
-
}
|
|
4831
|
-
|
|
4832
|
-
if (mode !== 'build' || !tool) {
|
|
4833
|
-
return null
|
|
4834
|
-
}
|
|
4835
|
-
|
|
4836
|
-
if (tool === 'item' && catalogCategory) {
|
|
4837
|
-
return furnishTools.find((entry) => entry.catalogCategory === catalogCategory) ?? null
|
|
4838
|
-
}
|
|
4839
|
-
|
|
4840
|
-
return structureTools.find((entry) => entry.id === tool) ?? null
|
|
4841
|
-
}, [catalogCategory, mode, movingOpeningType, tool])
|
|
4842
|
-
const activeFloorplanCursorIndicator = useMemo<FloorplanCursorIndicator | null>(() => {
|
|
4843
|
-
if (activeFloorplanToolConfig) {
|
|
4844
|
-
return {
|
|
4845
|
-
kind: 'asset',
|
|
4846
|
-
iconSrc: activeFloorplanToolConfig.iconSrc,
|
|
4847
|
-
}
|
|
5239
|
+
return sitePolygonEntry
|
|
4848
5240
|
}
|
|
4849
5241
|
|
|
4850
|
-
|
|
4851
|
-
return {
|
|
4852
|
-
kind: 'icon',
|
|
4853
|
-
icon: 'mdi:select-drag',
|
|
4854
|
-
}
|
|
4855
|
-
}
|
|
5242
|
+
const polygon = siteBoundaryDraft.polygon.map(toPoint2D)
|
|
4856
5243
|
|
|
4857
|
-
|
|
4858
|
-
|
|
4859
|
-
|
|
4860
|
-
|
|
4861
|
-
}
|
|
5244
|
+
return {
|
|
5245
|
+
...sitePolygonEntry,
|
|
5246
|
+
polygon,
|
|
5247
|
+
points: formatPolygonPoints(polygon),
|
|
4862
5248
|
}
|
|
5249
|
+
}, [siteBoundaryDraft, sitePolygonEntry])
|
|
5250
|
+
const movingOpeningType =
|
|
5251
|
+
movingNode?.type === 'door' || movingNode?.type === 'window' ? movingNode.type : null
|
|
4863
5252
|
|
|
4864
|
-
return null
|
|
4865
|
-
}, [activeFloorplanToolConfig, floorplanSelectionTool, mode, structureLayer])
|
|
4866
5253
|
const visibleGuides = useMemo<GuideNode[]>(() => {
|
|
4867
5254
|
if (!showGuides) {
|
|
4868
5255
|
return []
|
|
@@ -4922,29 +5309,42 @@ export function FloorplanPanel() {
|
|
|
4922
5309
|
[floorplanWalls],
|
|
4923
5310
|
)
|
|
4924
5311
|
const displayWallById = useMemo(() => {
|
|
4925
|
-
if (!wallEndpointDraft) {
|
|
5312
|
+
if (!(wallEndpointDraft || wallCurveDraft)) {
|
|
4926
5313
|
return wallById
|
|
4927
5314
|
}
|
|
4928
5315
|
|
|
4929
|
-
const
|
|
4930
|
-
|
|
4931
|
-
|
|
5316
|
+
const nextWallById = new Map(wallById)
|
|
5317
|
+
|
|
5318
|
+
if (wallEndpointDraft) {
|
|
5319
|
+
const wall = nextWallById.get(wallEndpointDraft.wallId)
|
|
5320
|
+
if (wall) {
|
|
5321
|
+
nextWallById.set(
|
|
5322
|
+
wall.id,
|
|
5323
|
+
buildWallWithUpdatedEndpoints(wall, wallEndpointDraft.start, wallEndpointDraft.end),
|
|
5324
|
+
)
|
|
5325
|
+
}
|
|
4932
5326
|
}
|
|
4933
5327
|
|
|
4934
|
-
|
|
4935
|
-
|
|
4936
|
-
wall
|
|
4937
|
-
|
|
4938
|
-
|
|
5328
|
+
if (wallCurveDraft) {
|
|
5329
|
+
const wall = nextWallById.get(wallCurveDraft.wallId)
|
|
5330
|
+
if (wall) {
|
|
5331
|
+
nextWallById.set(wall.id, { ...wall, curveOffset: wallCurveDraft.curveOffset })
|
|
5332
|
+
}
|
|
5333
|
+
}
|
|
4939
5334
|
|
|
4940
5335
|
return nextWallById
|
|
4941
|
-
}, [wallById, wallEndpointDraft])
|
|
5336
|
+
}, [wallById, wallCurveDraft, wallEndpointDraft])
|
|
4942
5337
|
const displayFloorplanWallById = useMemo(() => {
|
|
4943
|
-
if (!wallEndpointDraft) {
|
|
5338
|
+
if (!(wallEndpointDraft || wallCurveDraft)) {
|
|
5339
|
+
return floorplanWallById
|
|
5340
|
+
}
|
|
5341
|
+
|
|
5342
|
+
const previewWallId = wallEndpointDraft?.wallId ?? wallCurveDraft?.wallId
|
|
5343
|
+
if (!previewWallId) {
|
|
4944
5344
|
return floorplanWallById
|
|
4945
5345
|
}
|
|
4946
5346
|
|
|
4947
|
-
const previewWall = displayWallById.get(
|
|
5347
|
+
const previewWall = displayWallById.get(previewWallId)
|
|
4948
5348
|
if (!previewWall) {
|
|
4949
5349
|
return floorplanWallById
|
|
4950
5350
|
}
|
|
@@ -4952,7 +5352,7 @@ export function FloorplanPanel() {
|
|
|
4952
5352
|
const nextFloorplanWallById = new Map(floorplanWallById)
|
|
4953
5353
|
nextFloorplanWallById.set(previewWall.id, getFloorplanWall(previewWall))
|
|
4954
5354
|
return nextFloorplanWallById
|
|
4955
|
-
}, [displayWallById, floorplanWallById, wallEndpointDraft])
|
|
5355
|
+
}, [displayWallById, floorplanWallById, wallCurveDraft, wallEndpointDraft])
|
|
4956
5356
|
const wallPolygons = useMemo(
|
|
4957
5357
|
() =>
|
|
4958
5358
|
walls.map((wall) => {
|
|
@@ -4967,11 +5367,16 @@ export function FloorplanPanel() {
|
|
|
4967
5367
|
[floorplanWallById, wallMiterData, walls],
|
|
4968
5368
|
)
|
|
4969
5369
|
const displayWallPolygons = useMemo(() => {
|
|
4970
|
-
if (!wallEndpointDraft) {
|
|
5370
|
+
if (!(wallEndpointDraft || wallCurveDraft)) {
|
|
4971
5371
|
return wallPolygons
|
|
4972
5372
|
}
|
|
4973
5373
|
|
|
4974
|
-
const
|
|
5374
|
+
const previewWallId = wallEndpointDraft?.wallId ?? wallCurveDraft?.wallId
|
|
5375
|
+
if (!previewWallId) {
|
|
5376
|
+
return wallPolygons
|
|
5377
|
+
}
|
|
5378
|
+
|
|
5379
|
+
const previewWall = displayWallById.get(previewWallId)
|
|
4975
5380
|
if (!previewWall) {
|
|
4976
5381
|
return wallPolygons
|
|
4977
5382
|
}
|
|
@@ -4990,7 +5395,7 @@ export function FloorplanPanel() {
|
|
|
4990
5395
|
}
|
|
4991
5396
|
: entry,
|
|
4992
5397
|
)
|
|
4993
|
-
}, [displayWallById, wallEndpointDraft, wallPolygons])
|
|
5398
|
+
}, [displayWallById, wallCurveDraft, wallEndpointDraft, wallPolygons])
|
|
4994
5399
|
|
|
4995
5400
|
const openingsPolygons = useMemo(
|
|
4996
5401
|
() =>
|
|
@@ -5046,6 +5451,29 @@ export function FloorplanPanel() {
|
|
|
5046
5451
|
: entry,
|
|
5047
5452
|
)
|
|
5048
5453
|
}, [slabBoundaryDraft, slabPolygons])
|
|
5454
|
+
const ceilingPolygons = useMemo(
|
|
5455
|
+
() =>
|
|
5456
|
+
ceilings.flatMap((ceiling) => {
|
|
5457
|
+
const polygon = toFloorplanPolygon(ceiling.polygon)
|
|
5458
|
+
if (polygon.length < 3) {
|
|
5459
|
+
return []
|
|
5460
|
+
}
|
|
5461
|
+
|
|
5462
|
+
const holes = (ceiling.holes ?? [])
|
|
5463
|
+
.map((hole) => toFloorplanPolygon(hole))
|
|
5464
|
+
.filter((hole) => hole.length >= 3)
|
|
5465
|
+
|
|
5466
|
+
return [
|
|
5467
|
+
{
|
|
5468
|
+
ceiling,
|
|
5469
|
+
polygon,
|
|
5470
|
+
holes,
|
|
5471
|
+
path: formatPolygonPath(polygon, holes),
|
|
5472
|
+
},
|
|
5473
|
+
]
|
|
5474
|
+
}),
|
|
5475
|
+
[ceilings],
|
|
5476
|
+
)
|
|
5049
5477
|
const zonePolygons = useMemo(
|
|
5050
5478
|
() =>
|
|
5051
5479
|
zones.flatMap((zone) => {
|
|
@@ -5176,6 +5604,13 @@ export function FloorplanPanel() {
|
|
|
5176
5604
|
|
|
5177
5605
|
return floorplanItemEntries.find(({ item }) => item.id === selectedIds[0]) ?? null
|
|
5178
5606
|
}, [floorplanItemEntries, selectedIds])
|
|
5607
|
+
const selectedWallEntry = useMemo(() => {
|
|
5608
|
+
if (selectedIds.length !== 1) {
|
|
5609
|
+
return null
|
|
5610
|
+
}
|
|
5611
|
+
|
|
5612
|
+
return displayWallPolygons.find(({ wall }) => wall.id === selectedIds[0]) ?? null
|
|
5613
|
+
}, [displayWallPolygons, selectedIds])
|
|
5179
5614
|
const selectedStairEntry = useMemo(() => {
|
|
5180
5615
|
if (selectedIds.length !== 1) {
|
|
5181
5616
|
return null
|
|
@@ -5192,6 +5627,13 @@ export function FloorplanPanel() {
|
|
|
5192
5627
|
|
|
5193
5628
|
return displaySlabPolygons.find(({ slab }) => slab.id === selectedIds[0]) ?? null
|
|
5194
5629
|
}, [displaySlabPolygons, selectedIds])
|
|
5630
|
+
const selectedCeilingEntry = useMemo(() => {
|
|
5631
|
+
if (selectedIds.length !== 1) {
|
|
5632
|
+
return null
|
|
5633
|
+
}
|
|
5634
|
+
|
|
5635
|
+
return ceilingPolygons.find(({ ceiling }) => ceiling.id === selectedIds[0]) ?? null
|
|
5636
|
+
}, [ceilingPolygons, selectedIds])
|
|
5195
5637
|
const selectedZoneEntry = useMemo(() => {
|
|
5196
5638
|
if (!selectedZoneId) {
|
|
5197
5639
|
return null
|
|
@@ -5212,12 +5654,25 @@ export function FloorplanPanel() {
|
|
|
5212
5654
|
const isOpeningPlacementActive = isOpeningBuildActive || isOpeningMoveActive
|
|
5213
5655
|
const isStairBuildActive = phase === 'structure' && mode === 'build' && tool === 'stair'
|
|
5214
5656
|
const isStairMoveActive = movingNode?.type === 'stair'
|
|
5657
|
+
const isSlabMoveActive = movingNode?.type === 'slab'
|
|
5658
|
+
const isCeilingMoveActive = movingNode?.type === 'ceiling'
|
|
5659
|
+
const isWallMoveActive = movingNode?.type === 'wall'
|
|
5660
|
+
const isWallCurveActive = curvingWall?.type === 'wall'
|
|
5661
|
+
const isFenceCurveActive = curvingFence?.type === 'fence'
|
|
5215
5662
|
const isItemPlacementPreviewActive =
|
|
5216
5663
|
(mode === 'build' && tool === 'item') || movingNode?.type === 'item'
|
|
5217
5664
|
const isFloorItemBuildActive = mode === 'build' && tool === 'item' && !selectedItem?.attachTo
|
|
5218
5665
|
const isFloorItemMoveActive = movingNode?.type === 'item' && !movingNode.asset.attachTo
|
|
5219
5666
|
const isFloorplanGridInteractionActive =
|
|
5220
|
-
isStairBuildActive ||
|
|
5667
|
+
isStairBuildActive ||
|
|
5668
|
+
isStairMoveActive ||
|
|
5669
|
+
isSlabMoveActive ||
|
|
5670
|
+
isCeilingMoveActive ||
|
|
5671
|
+
isWallMoveActive ||
|
|
5672
|
+
isWallCurveActive ||
|
|
5673
|
+
isFenceCurveActive ||
|
|
5674
|
+
isFloorItemBuildActive ||
|
|
5675
|
+
isFloorItemMoveActive
|
|
5221
5676
|
const floorplanPreviewStairSegment = useMemo(
|
|
5222
5677
|
() =>
|
|
5223
5678
|
StairSegmentNodeSchema.parse({
|
|
@@ -5399,6 +5854,56 @@ export function FloorplanPanel() {
|
|
|
5399
5854
|
shouldShowPersistentWallEndpointHandles,
|
|
5400
5855
|
wallEndpointDraft,
|
|
5401
5856
|
])
|
|
5857
|
+
const wallCurveHandles = useMemo(() => {
|
|
5858
|
+
if (
|
|
5859
|
+
isOpeningPlacementActive ||
|
|
5860
|
+
movingNode ||
|
|
5861
|
+
mode !== 'select' ||
|
|
5862
|
+
floorplanSelectionTool !== 'click' ||
|
|
5863
|
+
!selectedWallEntry
|
|
5864
|
+
) {
|
|
5865
|
+
return []
|
|
5866
|
+
}
|
|
5867
|
+
|
|
5868
|
+
const hasWallChildrenBlockingCurve = (selectedWallEntry.wall.children ?? []).some((childId) => {
|
|
5869
|
+
const childNode = levelDescendantNodeById.get(childId as AnyNodeId)
|
|
5870
|
+
if (!childNode) {
|
|
5871
|
+
return false
|
|
5872
|
+
}
|
|
5873
|
+
|
|
5874
|
+
if (childNode.type === 'door' || childNode.type === 'window') {
|
|
5875
|
+
return true
|
|
5876
|
+
}
|
|
5877
|
+
|
|
5878
|
+
if (childNode.type === 'item') {
|
|
5879
|
+
const attachTo = childNode.asset?.attachTo
|
|
5880
|
+
return attachTo === 'wall' || attachTo === 'wall-side'
|
|
5881
|
+
}
|
|
5882
|
+
|
|
5883
|
+
return false
|
|
5884
|
+
})
|
|
5885
|
+
if (hasWallChildrenBlockingCurve) {
|
|
5886
|
+
return []
|
|
5887
|
+
}
|
|
5888
|
+
|
|
5889
|
+
const centerPoint = getWallMidpointHandlePoint(selectedWallEntry.wall)
|
|
5890
|
+
|
|
5891
|
+
return [
|
|
5892
|
+
{
|
|
5893
|
+
wall: selectedWallEntry.wall,
|
|
5894
|
+
point: [centerPoint.x, centerPoint.y] as WallPlanPoint,
|
|
5895
|
+
isActive: wallCurveDraft?.wallId === selectedWallEntry.wall.id,
|
|
5896
|
+
},
|
|
5897
|
+
]
|
|
5898
|
+
}, [
|
|
5899
|
+
floorplanSelectionTool,
|
|
5900
|
+
isOpeningPlacementActive,
|
|
5901
|
+
mode,
|
|
5902
|
+
movingNode,
|
|
5903
|
+
levelDescendantNodeById,
|
|
5904
|
+
selectedWallEntry,
|
|
5905
|
+
wallCurveDraft,
|
|
5906
|
+
])
|
|
5402
5907
|
const slabVertexHandles = useMemo(() => {
|
|
5403
5908
|
if (!shouldShowSlabBoundaryHandles) {
|
|
5404
5909
|
return []
|
|
@@ -5660,23 +6165,15 @@ export function FloorplanPanel() {
|
|
|
5660
6165
|
if (levelChanged) {
|
|
5661
6166
|
previousLevelIdRef.current = levelId ?? null
|
|
5662
6167
|
hasUserAdjustedViewportRef.current = false
|
|
5663
|
-
setViewport(fittedViewport)
|
|
6168
|
+
setViewport((current) => (floorplanViewportEquals(current, fittedViewport) ? current : fittedViewport))
|
|
5664
6169
|
return
|
|
5665
6170
|
}
|
|
5666
6171
|
|
|
5667
6172
|
if (!hasUserAdjustedViewportRef.current) {
|
|
5668
|
-
setViewport(fittedViewport)
|
|
6173
|
+
setViewport((current) => (floorplanViewportEquals(current, fittedViewport) ? current : fittedViewport))
|
|
5669
6174
|
}
|
|
5670
6175
|
}, [fittedViewport, levelId])
|
|
5671
6176
|
|
|
5672
|
-
useEffect(() => {
|
|
5673
|
-
if (!(phase === 'site' && levelNode?.type === 'level')) {
|
|
5674
|
-
return
|
|
5675
|
-
}
|
|
5676
|
-
|
|
5677
|
-
setPhase('structure')
|
|
5678
|
-
}, [levelNode, phase, setPhase])
|
|
5679
|
-
|
|
5680
6177
|
const viewBox = useMemo(() => {
|
|
5681
6178
|
const currentViewport = viewport ?? fittedViewport
|
|
5682
6179
|
const width = currentViewport.width
|
|
@@ -5717,6 +6214,27 @@ export function FloorplanPanel() {
|
|
|
5717
6214
|
: null,
|
|
5718
6215
|
[selectedItemEntry, surfaceSize, viewBox],
|
|
5719
6216
|
)
|
|
6217
|
+
const selectedSlabActionMenuPosition = useMemo(
|
|
6218
|
+
() =>
|
|
6219
|
+
selectedSlabEntry
|
|
6220
|
+
? getFloorplanActionMenuPosition(selectedSlabEntry.polygon, viewBox, surfaceSize)
|
|
6221
|
+
: null,
|
|
6222
|
+
[selectedSlabEntry, surfaceSize, viewBox],
|
|
6223
|
+
)
|
|
6224
|
+
const selectedCeilingActionMenuPosition = useMemo(
|
|
6225
|
+
() =>
|
|
6226
|
+
selectedCeilingEntry
|
|
6227
|
+
? getFloorplanActionMenuPosition(selectedCeilingEntry.polygon, viewBox, surfaceSize)
|
|
6228
|
+
: null,
|
|
6229
|
+
[selectedCeilingEntry, surfaceSize, viewBox],
|
|
6230
|
+
)
|
|
6231
|
+
const selectedWallActionMenuPosition = useMemo(
|
|
6232
|
+
() =>
|
|
6233
|
+
selectedWallEntry
|
|
6234
|
+
? getFloorplanActionMenuPosition(selectedWallEntry.polygon, viewBox, surfaceSize)
|
|
6235
|
+
: null,
|
|
6236
|
+
[selectedWallEntry, surfaceSize, viewBox],
|
|
6237
|
+
)
|
|
5720
6238
|
const selectedStairActionMenuPosition = useMemo(
|
|
5721
6239
|
() =>
|
|
5722
6240
|
selectedStairEntry
|
|
@@ -6198,6 +6716,11 @@ export function FloorplanPanel() {
|
|
|
6198
6716
|
setWallEndpointDraft(null)
|
|
6199
6717
|
setHoveredEndpointId(null)
|
|
6200
6718
|
}, [])
|
|
6719
|
+
const clearWallCurveDrag = useCallback(() => {
|
|
6720
|
+
wallCurveDragRef.current = null
|
|
6721
|
+
setWallCurveDraft(null)
|
|
6722
|
+
setHoveredWallCurveHandleId(null)
|
|
6723
|
+
}, [])
|
|
6201
6724
|
const clearSiteBoundaryInteraction = useCallback(() => {
|
|
6202
6725
|
setSiteVertexDragState(null)
|
|
6203
6726
|
setSiteBoundaryDraft(null)
|
|
@@ -6219,11 +6742,13 @@ export function FloorplanPanel() {
|
|
|
6219
6742
|
clearSlabPlacementDraft()
|
|
6220
6743
|
clearZonePlacementDraft()
|
|
6221
6744
|
clearWallEndpointDrag()
|
|
6745
|
+
clearWallCurveDrag()
|
|
6222
6746
|
clearSiteBoundaryInteraction()
|
|
6223
6747
|
clearSlabBoundaryInteraction()
|
|
6224
6748
|
clearZoneBoundaryInteraction()
|
|
6225
6749
|
setCursorPoint(null)
|
|
6226
6750
|
}, [
|
|
6751
|
+
clearWallCurveDrag,
|
|
6227
6752
|
clearSiteBoundaryInteraction,
|
|
6228
6753
|
clearSlabBoundaryInteraction,
|
|
6229
6754
|
clearSlabPlacementDraft,
|
|
@@ -6430,51 +6955,85 @@ export function FloorplanPanel() {
|
|
|
6430
6955
|
}
|
|
6431
6956
|
|
|
6432
6957
|
const dragState = wallEndpointDragRef.current
|
|
6433
|
-
if (
|
|
6958
|
+
if (dragState && event.pointerId === dragState.pointerId) {
|
|
6959
|
+
event.preventDefault()
|
|
6960
|
+
|
|
6961
|
+
const planPoint = getPlanPointFromClientPoint(event.clientX, event.clientY)
|
|
6962
|
+
if (!planPoint) {
|
|
6963
|
+
return
|
|
6964
|
+
}
|
|
6965
|
+
|
|
6966
|
+
const snappedPoint = snapWallDraftPoint({
|
|
6967
|
+
point: planPoint,
|
|
6968
|
+
walls,
|
|
6969
|
+
start: dragState.fixedPoint,
|
|
6970
|
+
angleSnap: !shiftPressed,
|
|
6971
|
+
ignoreWallIds: [dragState.wallId],
|
|
6972
|
+
})
|
|
6973
|
+
|
|
6974
|
+
if (pointsEqual(dragState.currentPoint, snappedPoint)) {
|
|
6975
|
+
return
|
|
6976
|
+
}
|
|
6977
|
+
|
|
6978
|
+
dragState.currentPoint = snappedPoint
|
|
6979
|
+
setCursorPoint(snappedPoint)
|
|
6980
|
+
setWallEndpointDraft((previousDraft) => {
|
|
6981
|
+
const nextDraft = buildWallEndpointDraft(
|
|
6982
|
+
dragState.wallId,
|
|
6983
|
+
dragState.endpoint,
|
|
6984
|
+
dragState.fixedPoint,
|
|
6985
|
+
snappedPoint,
|
|
6986
|
+
)
|
|
6987
|
+
|
|
6988
|
+
if (
|
|
6989
|
+
!(
|
|
6990
|
+
previousDraft &&
|
|
6991
|
+
pointsEqual(previousDraft.start, nextDraft.start) &&
|
|
6992
|
+
pointsEqual(previousDraft.end, nextDraft.end)
|
|
6993
|
+
)
|
|
6994
|
+
) {
|
|
6995
|
+
sfxEmitter.emit('sfx:grid-snap')
|
|
6996
|
+
}
|
|
6997
|
+
|
|
6998
|
+
return nextDraft
|
|
6999
|
+
})
|
|
7000
|
+
return
|
|
7001
|
+
}
|
|
7002
|
+
|
|
7003
|
+
const curveDragState = wallCurveDragRef.current
|
|
7004
|
+
if (!curveDragState || event.pointerId !== curveDragState.pointerId) {
|
|
6434
7005
|
return
|
|
6435
7006
|
}
|
|
6436
7007
|
|
|
6437
7008
|
event.preventDefault()
|
|
6438
7009
|
|
|
6439
7010
|
const planPoint = getPlanPointFromClientPoint(event.clientX, event.clientY)
|
|
6440
|
-
|
|
7011
|
+
const wall = wallById.get(curveDragState.wallId)
|
|
7012
|
+
if (!(planPoint && wall)) {
|
|
6441
7013
|
return
|
|
6442
7014
|
}
|
|
6443
7015
|
|
|
6444
|
-
const
|
|
6445
|
-
|
|
6446
|
-
|
|
6447
|
-
|
|
6448
|
-
|
|
6449
|
-
|
|
6450
|
-
|
|
7016
|
+
const chord = getWallChordFrame(wall)
|
|
7017
|
+
const snappedPoint: WallPlanPoint = shiftPressed
|
|
7018
|
+
? planPoint
|
|
7019
|
+
: [snapToHalf(planPoint[0]), snapToHalf(planPoint[1])]
|
|
7020
|
+
const rawCurveOffset = -(
|
|
7021
|
+
(snappedPoint[0] - chord.midpoint.x) * chord.normal.x +
|
|
7022
|
+
(snappedPoint[1] - chord.midpoint.y) * chord.normal.y
|
|
7023
|
+
)
|
|
7024
|
+
const nextCurveOffset = normalizeWallCurveOffset(
|
|
7025
|
+
wall,
|
|
7026
|
+
shiftPressed ? rawCurveOffset : snapToHalf(rawCurveOffset),
|
|
7027
|
+
)
|
|
6451
7028
|
|
|
6452
|
-
if (
|
|
7029
|
+
if (curveDragState.currentCurveOffset === nextCurveOffset) {
|
|
6453
7030
|
return
|
|
6454
7031
|
}
|
|
6455
7032
|
|
|
6456
|
-
|
|
7033
|
+
curveDragState.currentCurveOffset = nextCurveOffset
|
|
7034
|
+
setWallCurveDraft({ wallId: wall.id, curveOffset: nextCurveOffset })
|
|
6457
7035
|
setCursorPoint(snappedPoint)
|
|
6458
|
-
|
|
6459
|
-
const nextDraft = buildWallEndpointDraft(
|
|
6460
|
-
dragState.wallId,
|
|
6461
|
-
dragState.endpoint,
|
|
6462
|
-
dragState.fixedPoint,
|
|
6463
|
-
snappedPoint,
|
|
6464
|
-
)
|
|
6465
|
-
|
|
6466
|
-
if (
|
|
6467
|
-
!(
|
|
6468
|
-
previousDraft &&
|
|
6469
|
-
pointsEqual(previousDraft.start, nextDraft.start) &&
|
|
6470
|
-
pointsEqual(previousDraft.end, nextDraft.end)
|
|
6471
|
-
)
|
|
6472
|
-
) {
|
|
6473
|
-
sfxEmitter.emit('sfx:grid-snap')
|
|
6474
|
-
}
|
|
6475
|
-
|
|
6476
|
-
return nextDraft
|
|
6477
|
-
})
|
|
7036
|
+
sfxEmitter.emit('sfx:grid-snap')
|
|
6478
7037
|
}
|
|
6479
7038
|
|
|
6480
7039
|
const commitGuideInteraction = (event: PointerEvent) => {
|
|
@@ -6559,6 +7118,26 @@ export function FloorplanPanel() {
|
|
|
6559
7118
|
setCursorPoint(null)
|
|
6560
7119
|
}
|
|
6561
7120
|
|
|
7121
|
+
const commitWallCurveDrag = (event: PointerEvent) => {
|
|
7122
|
+
const dragState = wallCurveDragRef.current
|
|
7123
|
+
if (!dragState || event.pointerId !== dragState.pointerId) {
|
|
7124
|
+
return
|
|
7125
|
+
}
|
|
7126
|
+
|
|
7127
|
+
const wall = wallById.get(dragState.wallId)
|
|
7128
|
+
if (wall) {
|
|
7129
|
+
const nextCurveOffset = normalizeWallCurveOffset(wall, dragState.currentCurveOffset)
|
|
7130
|
+
const currentCurveOffset = normalizeWallCurveOffset(wall, wall.curveOffset ?? 0)
|
|
7131
|
+
if (nextCurveOffset !== currentCurveOffset) {
|
|
7132
|
+
updateNode(wall.id, { curveOffset: nextCurveOffset })
|
|
7133
|
+
sfxEmitter.emit('sfx:structure-build')
|
|
7134
|
+
}
|
|
7135
|
+
}
|
|
7136
|
+
|
|
7137
|
+
clearWallCurveDrag()
|
|
7138
|
+
setCursorPoint(null)
|
|
7139
|
+
}
|
|
7140
|
+
|
|
6562
7141
|
const cancelWallEndpointDrag = (event: PointerEvent) => {
|
|
6563
7142
|
const dragState = wallEndpointDragRef.current
|
|
6564
7143
|
if (!dragState || event.pointerId !== dragState.pointerId) {
|
|
@@ -6569,11 +7148,23 @@ export function FloorplanPanel() {
|
|
|
6569
7148
|
setCursorPoint(null)
|
|
6570
7149
|
}
|
|
6571
7150
|
|
|
7151
|
+
const cancelWallCurveDrag = (event: PointerEvent) => {
|
|
7152
|
+
const dragState = wallCurveDragRef.current
|
|
7153
|
+
if (!dragState || event.pointerId !== dragState.pointerId) {
|
|
7154
|
+
return
|
|
7155
|
+
}
|
|
7156
|
+
|
|
7157
|
+
clearWallCurveDrag()
|
|
7158
|
+
setCursorPoint(null)
|
|
7159
|
+
}
|
|
7160
|
+
|
|
6572
7161
|
window.addEventListener('pointermove', handleWindowPointerMove)
|
|
6573
7162
|
window.addEventListener('pointerup', commitGuideInteraction)
|
|
6574
7163
|
window.addEventListener('pointercancel', cancelGuideInteraction)
|
|
6575
7164
|
window.addEventListener('pointerup', commitWallEndpointDrag)
|
|
6576
7165
|
window.addEventListener('pointercancel', cancelWallEndpointDrag)
|
|
7166
|
+
window.addEventListener('pointerup', commitWallCurveDrag)
|
|
7167
|
+
window.addEventListener('pointercancel', cancelWallCurveDrag)
|
|
6577
7168
|
|
|
6578
7169
|
return () => {
|
|
6579
7170
|
window.removeEventListener('pointermove', handleWindowPointerMove)
|
|
@@ -6581,8 +7172,11 @@ export function FloorplanPanel() {
|
|
|
6581
7172
|
window.removeEventListener('pointercancel', cancelGuideInteraction)
|
|
6582
7173
|
window.removeEventListener('pointerup', commitWallEndpointDrag)
|
|
6583
7174
|
window.removeEventListener('pointercancel', cancelWallEndpointDrag)
|
|
7175
|
+
window.removeEventListener('pointerup', commitWallCurveDrag)
|
|
7176
|
+
window.removeEventListener('pointercancel', cancelWallCurveDrag)
|
|
6584
7177
|
}
|
|
6585
7178
|
}, [
|
|
7179
|
+
clearWallCurveDrag,
|
|
6586
7180
|
clearGuideInteraction,
|
|
6587
7181
|
clearWallEndpointDrag,
|
|
6588
7182
|
getSvgPointFromClientPoint,
|
|
@@ -6596,7 +7190,8 @@ export function FloorplanPanel() {
|
|
|
6596
7190
|
|
|
6597
7191
|
useEffect(() => {
|
|
6598
7192
|
clearWallEndpointDrag()
|
|
6599
|
-
|
|
7193
|
+
clearWallCurveDrag()
|
|
7194
|
+
}, [clearWallCurveDrag, clearWallEndpointDrag, levelId])
|
|
6600
7195
|
|
|
6601
7196
|
useEffect(() => {
|
|
6602
7197
|
if (shouldShowSiteBoundaryHandles) {
|
|
@@ -7066,7 +7661,9 @@ export function FloorplanPanel() {
|
|
|
7066
7661
|
}
|
|
7067
7662
|
|
|
7068
7663
|
if (isOpeningPlacementActive) {
|
|
7069
|
-
const closest = findClosestWallPoint(planPoint, walls
|
|
7664
|
+
const closest = findClosestWallPoint(planPoint, walls, {
|
|
7665
|
+
canUseWall: (wall) => !isCurvedWall(wall),
|
|
7666
|
+
})
|
|
7070
7667
|
if (closest) {
|
|
7071
7668
|
const dx = closest.wall.end[0] - closest.wall.start[0]
|
|
7072
7669
|
const dz = closest.wall.end[1] - closest.wall.start[1]
|
|
@@ -7286,7 +7883,9 @@ export function FloorplanPanel() {
|
|
|
7286
7883
|
}
|
|
7287
7884
|
|
|
7288
7885
|
if (isOpeningPlacementActive) {
|
|
7289
|
-
const closest = findClosestWallPoint(planPoint, walls
|
|
7886
|
+
const closest = findClosestWallPoint(planPoint, walls, {
|
|
7887
|
+
canUseWall: (wall) => !isCurvedWall(wall),
|
|
7888
|
+
})
|
|
7290
7889
|
if (closest) {
|
|
7291
7890
|
const dx = closest.wall.end[0] - closest.wall.start[0]
|
|
7292
7891
|
const dz = closest.wall.end[1] - closest.wall.start[1]
|
|
@@ -7373,7 +7972,9 @@ export function FloorplanPanel() {
|
|
|
7373
7972
|
isOpeningPlacementActive,
|
|
7374
7973
|
isPolygonBuildActive,
|
|
7375
7974
|
isWallBuildActive,
|
|
7975
|
+
isWindowBuildActive,
|
|
7376
7976
|
isZoneBuildActive,
|
|
7977
|
+
movingOpeningType,
|
|
7377
7978
|
setSelectedReferenceId,
|
|
7378
7979
|
setSelection,
|
|
7379
7980
|
shiftPressed,
|
|
@@ -8095,6 +8696,96 @@ export function FloorplanPanel() {
|
|
|
8095
8696
|
},
|
|
8096
8697
|
[deleteNode, selectedItemEntry, setSelection],
|
|
8097
8698
|
)
|
|
8699
|
+
const handleSelectedWallMove = useCallback(
|
|
8700
|
+
(event: ReactMouseEvent<HTMLButtonElement>) => {
|
|
8701
|
+
event.stopPropagation()
|
|
8702
|
+
|
|
8703
|
+
const wall = selectedWallEntry?.wall
|
|
8704
|
+
if (!wall) {
|
|
8705
|
+
return
|
|
8706
|
+
}
|
|
8707
|
+
|
|
8708
|
+
sfxEmitter.emit('sfx:item-pick')
|
|
8709
|
+
setMovingNode(wall)
|
|
8710
|
+
setSelection({ selectedIds: [] })
|
|
8711
|
+
},
|
|
8712
|
+
[selectedWallEntry, setMovingNode, setSelection],
|
|
8713
|
+
)
|
|
8714
|
+
const handleSelectedWallDelete = useCallback(
|
|
8715
|
+
(event: ReactMouseEvent<HTMLButtonElement>) => {
|
|
8716
|
+
event.stopPropagation()
|
|
8717
|
+
|
|
8718
|
+
const wall = selectedWallEntry?.wall
|
|
8719
|
+
if (!wall) {
|
|
8720
|
+
return
|
|
8721
|
+
}
|
|
8722
|
+
|
|
8723
|
+
sfxEmitter.emit('sfx:item-delete')
|
|
8724
|
+
deleteNode(wall.id as AnyNodeId)
|
|
8725
|
+
setSelection({ selectedIds: [] })
|
|
8726
|
+
},
|
|
8727
|
+
[deleteNode, selectedWallEntry, setSelection],
|
|
8728
|
+
)
|
|
8729
|
+
const handleSelectedSlabMove = useCallback(
|
|
8730
|
+
(event: ReactMouseEvent<HTMLButtonElement>) => {
|
|
8731
|
+
event.stopPropagation()
|
|
8732
|
+
|
|
8733
|
+
const slab = selectedSlabEntry?.slab
|
|
8734
|
+
if (!slab) {
|
|
8735
|
+
return
|
|
8736
|
+
}
|
|
8737
|
+
|
|
8738
|
+
sfxEmitter.emit('sfx:item-pick')
|
|
8739
|
+
setMovingNode(slab)
|
|
8740
|
+
setSelection({ selectedIds: [] })
|
|
8741
|
+
},
|
|
8742
|
+
[selectedSlabEntry, setMovingNode, setSelection],
|
|
8743
|
+
)
|
|
8744
|
+
const handleSelectedSlabDelete = useCallback(
|
|
8745
|
+
(event: ReactMouseEvent<HTMLButtonElement>) => {
|
|
8746
|
+
event.stopPropagation()
|
|
8747
|
+
|
|
8748
|
+
const slab = selectedSlabEntry?.slab
|
|
8749
|
+
if (!slab) {
|
|
8750
|
+
return
|
|
8751
|
+
}
|
|
8752
|
+
|
|
8753
|
+
sfxEmitter.emit('sfx:item-delete')
|
|
8754
|
+
deleteNode(slab.id as AnyNodeId)
|
|
8755
|
+
setSelection({ selectedIds: [] })
|
|
8756
|
+
},
|
|
8757
|
+
[deleteNode, selectedSlabEntry, setSelection],
|
|
8758
|
+
)
|
|
8759
|
+
const handleSelectedCeilingMove = useCallback(
|
|
8760
|
+
(event: ReactMouseEvent<HTMLButtonElement>) => {
|
|
8761
|
+
event.stopPropagation()
|
|
8762
|
+
|
|
8763
|
+
const ceiling = selectedCeilingEntry?.ceiling
|
|
8764
|
+
if (!ceiling) {
|
|
8765
|
+
return
|
|
8766
|
+
}
|
|
8767
|
+
|
|
8768
|
+
sfxEmitter.emit('sfx:item-pick')
|
|
8769
|
+
setMovingNode(ceiling)
|
|
8770
|
+
setSelection({ selectedIds: [] })
|
|
8771
|
+
},
|
|
8772
|
+
[selectedCeilingEntry, setMovingNode, setSelection],
|
|
8773
|
+
)
|
|
8774
|
+
const handleSelectedCeilingDelete = useCallback(
|
|
8775
|
+
(event: ReactMouseEvent<HTMLButtonElement>) => {
|
|
8776
|
+
event.stopPropagation()
|
|
8777
|
+
|
|
8778
|
+
const ceiling = selectedCeilingEntry?.ceiling
|
|
8779
|
+
if (!ceiling) {
|
|
8780
|
+
return
|
|
8781
|
+
}
|
|
8782
|
+
|
|
8783
|
+
sfxEmitter.emit('sfx:item-delete')
|
|
8784
|
+
deleteNode(ceiling.id as AnyNodeId)
|
|
8785
|
+
setSelection({ selectedIds: [] })
|
|
8786
|
+
},
|
|
8787
|
+
[deleteNode, selectedCeilingEntry, setSelection],
|
|
8788
|
+
)
|
|
8098
8789
|
const handleStairDoubleClick = useCallback(
|
|
8099
8790
|
(stair: StairNode, event: ReactMouseEvent<SVGElement>) => {
|
|
8100
8791
|
emitFloorplanNodeClick(stair.id, 'double-click', event)
|
|
@@ -8304,6 +8995,39 @@ export function FloorplanPanel() {
|
|
|
8304
8995
|
},
|
|
8305
8996
|
[clearWallPlacementDraft, handleWallPlacementPoint, handleWallSelect, isWallBuildActive, mode],
|
|
8306
8997
|
)
|
|
8998
|
+
const handleWallCurvePointerDown = useCallback(
|
|
8999
|
+
(wall: WallNode, event: ReactPointerEvent<SVGCircleElement>) => {
|
|
9000
|
+
if (event.button !== 0) {
|
|
9001
|
+
return
|
|
9002
|
+
}
|
|
9003
|
+
|
|
9004
|
+
event.preventDefault()
|
|
9005
|
+
event.stopPropagation()
|
|
9006
|
+
setHoveredWallCurveHandleId(null)
|
|
9007
|
+
|
|
9008
|
+
if (isWallBuildActive || mode !== 'select') {
|
|
9009
|
+
return
|
|
9010
|
+
}
|
|
9011
|
+
|
|
9012
|
+
clearWallPlacementDraft()
|
|
9013
|
+
handleWallSelect(wall)
|
|
9014
|
+
clearWallEndpointDrag()
|
|
9015
|
+
|
|
9016
|
+
const currentCurveOffset = normalizeWallCurveOffset(wall, wall.curveOffset ?? 0)
|
|
9017
|
+
wallCurveDragRef.current = {
|
|
9018
|
+
pointerId: event.pointerId,
|
|
9019
|
+
wallId: wall.id,
|
|
9020
|
+
currentCurveOffset,
|
|
9021
|
+
}
|
|
9022
|
+
setWallCurveDraft({
|
|
9023
|
+
wallId: wall.id,
|
|
9024
|
+
curveOffset: currentCurveOffset,
|
|
9025
|
+
})
|
|
9026
|
+
const center = getWallMidpointHandlePoint(wall)
|
|
9027
|
+
setCursorPoint([center.x, center.y])
|
|
9028
|
+
},
|
|
9029
|
+
[clearWallEndpointDrag, clearWallPlacementDraft, handleWallSelect, isWallBuildActive, mode],
|
|
9030
|
+
)
|
|
8307
9031
|
const handleSlabVertexPointerDown = useCallback(
|
|
8308
9032
|
(slabId: SlabNode['id'], vertexIndex: number, event: ReactPointerEvent<SVGCircleElement>) => {
|
|
8309
9033
|
if (event.button !== 0) {
|
|
@@ -8650,10 +9374,20 @@ export function FloorplanPanel() {
|
|
|
8650
9374
|
zoneVertexDragState,
|
|
8651
9375
|
])
|
|
8652
9376
|
|
|
9377
|
+
// Lightweight flag that mirrors the conditions under which
|
|
9378
|
+
// FloorplanCursorIndicatorOverlay renders — used to gate cursor-position
|
|
9379
|
+
// tracking. Derived locally here (rather than duplicating the overlay's full
|
|
9380
|
+
// useMemos) so this handler doesn't need to know about catalogCategory.
|
|
9381
|
+
const hasFloorplanCursorIndicator =
|
|
9382
|
+
Boolean(movingOpeningType) ||
|
|
9383
|
+
(mode === 'build' && tool !== null) ||
|
|
9384
|
+
(mode === 'select' && floorplanSelectionTool === 'marquee' && structureLayer !== 'zones') ||
|
|
9385
|
+
mode === 'delete'
|
|
9386
|
+
|
|
8653
9387
|
const handleSvgPointerMove = useCallback(
|
|
8654
9388
|
(event: ReactPointerEvent<SVGSVGElement>) => {
|
|
8655
9389
|
if (
|
|
8656
|
-
|
|
9390
|
+
hasFloorplanCursorIndicator &&
|
|
8657
9391
|
!panStateRef.current &&
|
|
8658
9392
|
!guideInteractionRef.current &&
|
|
8659
9393
|
!wallEndpointDragRef.current &&
|
|
@@ -8662,19 +9396,28 @@ export function FloorplanPanel() {
|
|
|
8662
9396
|
!zoneVertexDragState
|
|
8663
9397
|
) {
|
|
8664
9398
|
const rect = event.currentTarget.getBoundingClientRect()
|
|
8665
|
-
|
|
9399
|
+
const nextPosition = {
|
|
8666
9400
|
x: event.clientX - rect.left,
|
|
8667
9401
|
y: event.clientY - rect.top,
|
|
8668
|
-
}
|
|
9402
|
+
}
|
|
9403
|
+
setFloorplanCursorPosition((currentPosition) =>
|
|
9404
|
+
currentPosition &&
|
|
9405
|
+
currentPosition.x === nextPosition.x &&
|
|
9406
|
+
currentPosition.y === nextPosition.y
|
|
9407
|
+
? currentPosition
|
|
9408
|
+
: nextPosition,
|
|
9409
|
+
)
|
|
8669
9410
|
} else {
|
|
8670
|
-
setFloorplanCursorPosition(
|
|
9411
|
+
setFloorplanCursorPosition((currentPosition) =>
|
|
9412
|
+
currentPosition === null ? currentPosition : null,
|
|
9413
|
+
)
|
|
8671
9414
|
}
|
|
8672
9415
|
|
|
8673
9416
|
handlePointerMove(event)
|
|
8674
9417
|
},
|
|
8675
9418
|
[
|
|
8676
|
-
activeFloorplanCursorIndicator,
|
|
8677
9419
|
handlePointerMove,
|
|
9420
|
+
hasFloorplanCursorIndicator,
|
|
8678
9421
|
siteVertexDragState,
|
|
8679
9422
|
slabVertexDragState,
|
|
8680
9423
|
zoneVertexDragState,
|
|
@@ -9044,84 +9787,25 @@ export function FloorplanPanel() {
|
|
|
9044
9787
|
setStructureLayer,
|
|
9045
9788
|
site,
|
|
9046
9789
|
])
|
|
9047
|
-
|
|
9048
|
-
|
|
9049
|
-
|
|
9050
|
-
|
|
9051
|
-
|
|
9052
|
-
|
|
9053
|
-
|
|
9054
|
-
|
|
9055
|
-
if (
|
|
9056
|
-
isEditableTarget ||
|
|
9057
|
-
!isFloorplanHovered ||
|
|
9058
|
-
phase !== 'site' ||
|
|
9059
|
-
event.metaKey ||
|
|
9060
|
-
event.ctrlKey ||
|
|
9061
|
-
event.altKey ||
|
|
9062
|
-
event.key.toLowerCase() !== 'v'
|
|
9063
|
-
) {
|
|
9064
|
-
return
|
|
9065
|
-
}
|
|
9066
|
-
|
|
9067
|
-
setFloorplanSelectionTool('click')
|
|
9068
|
-
restoreGroundLevelStructureSelection()
|
|
9069
|
-
}
|
|
9070
|
-
|
|
9071
|
-
window.addEventListener('keydown', handleKeyDown, true)
|
|
9072
|
-
|
|
9073
|
-
return () => {
|
|
9074
|
-
window.removeEventListener('keydown', handleKeyDown, true)
|
|
9790
|
+
const hasDuplicatableFloorplanSelection = Boolean(
|
|
9791
|
+
selectedItemEntry || selectedOpeningEntry || selectedStairEntry,
|
|
9792
|
+
)
|
|
9793
|
+
const handleDuplicateFloorplanSelection = useCallback(() => {
|
|
9794
|
+
if (selectedOpeningEntry) {
|
|
9795
|
+
duplicateSelectedOpening()
|
|
9796
|
+
return
|
|
9075
9797
|
}
|
|
9076
|
-
|
|
9077
|
-
|
|
9078
|
-
|
|
9079
|
-
if (!(event.metaKey || event.ctrlKey) || event.key.toLowerCase() !== 'c') {
|
|
9080
|
-
return
|
|
9081
|
-
}
|
|
9082
|
-
|
|
9083
|
-
if (
|
|
9084
|
-
!(isFloorplanHovered && (selectedItemEntry || selectedOpeningEntry || selectedStairEntry))
|
|
9085
|
-
) {
|
|
9086
|
-
return
|
|
9087
|
-
}
|
|
9088
|
-
|
|
9089
|
-
const target = event.target as HTMLElement | null
|
|
9090
|
-
const isEditableTarget =
|
|
9091
|
-
target instanceof HTMLInputElement ||
|
|
9092
|
-
target instanceof HTMLTextAreaElement ||
|
|
9093
|
-
Boolean(target?.isContentEditable)
|
|
9094
|
-
|
|
9095
|
-
if (isEditableTarget) {
|
|
9096
|
-
return
|
|
9097
|
-
}
|
|
9098
|
-
|
|
9099
|
-
event.preventDefault()
|
|
9100
|
-
if (selectedOpeningEntry) {
|
|
9101
|
-
duplicateSelectedOpening()
|
|
9102
|
-
return
|
|
9103
|
-
}
|
|
9104
|
-
|
|
9105
|
-
if (selectedItemEntry) {
|
|
9106
|
-
duplicateSelectedItem()
|
|
9107
|
-
return
|
|
9108
|
-
}
|
|
9109
|
-
|
|
9110
|
-
if (selectedStairEntry) {
|
|
9111
|
-
duplicateSelectedStair()
|
|
9112
|
-
}
|
|
9798
|
+
if (selectedItemEntry) {
|
|
9799
|
+
duplicateSelectedItem()
|
|
9800
|
+
return
|
|
9113
9801
|
}
|
|
9114
|
-
|
|
9115
|
-
|
|
9116
|
-
|
|
9117
|
-
return () => {
|
|
9118
|
-
window.removeEventListener('keydown', handleKeyDown, true)
|
|
9802
|
+
if (selectedStairEntry) {
|
|
9803
|
+
duplicateSelectedStair()
|
|
9119
9804
|
}
|
|
9120
9805
|
}, [
|
|
9121
9806
|
duplicateSelectedItem,
|
|
9122
9807
|
duplicateSelectedOpening,
|
|
9123
9808
|
duplicateSelectedStair,
|
|
9124
|
-
isFloorplanHovered,
|
|
9125
9809
|
selectedItemEntry,
|
|
9126
9810
|
selectedOpeningEntry,
|
|
9127
9811
|
selectedStairEntry,
|
|
@@ -9135,9 +9819,6 @@ export function FloorplanPanel() {
|
|
|
9135
9819
|
: activeDraftAnchorPoint
|
|
9136
9820
|
? palette.draftStroke
|
|
9137
9821
|
: palette.cursor
|
|
9138
|
-
const activeCursorIndicatorPosition =
|
|
9139
|
-
mode === 'delete' ? floorplanCursorPosition : floorplanCursorAnchorPosition
|
|
9140
|
-
|
|
9141
9822
|
return (
|
|
9142
9823
|
<div
|
|
9143
9824
|
className="pointer-events-auto flex h-full w-full flex-col overflow-hidden bg-background/95"
|
|
@@ -9148,80 +9829,20 @@ export function FloorplanPanel() {
|
|
|
9148
9829
|
}}
|
|
9149
9830
|
ref={containerRef}
|
|
9150
9831
|
>
|
|
9832
|
+
<FloorplanSiteKeyHandler onRestoreGroundLevel={restoreGroundLevelStructureSelection} />
|
|
9833
|
+
<FloorplanDuplicateHotkey
|
|
9834
|
+
hasDuplicatable={hasDuplicatableFloorplanSelection}
|
|
9835
|
+
onDuplicateSelected={handleDuplicateFloorplanSelection}
|
|
9836
|
+
/>
|
|
9151
9837
|
<div className="relative min-h-0 flex-1" ref={viewportHostRef}>
|
|
9152
|
-
|
|
9153
|
-
|
|
9154
|
-
|
|
9155
|
-
|
|
9156
|
-
|
|
9157
|
-
|
|
9158
|
-
|
|
9159
|
-
|
|
9160
|
-
>
|
|
9161
|
-
{mode === 'delete' ? (
|
|
9162
|
-
<div
|
|
9163
|
-
className="flex h-8 w-8 items-center justify-center rounded-xl border border-white/5 bg-zinc-900/95 shadow-[0_8px_16px_-4px_rgba(0,0,0,0.3),0_4px_8px_-4px_rgba(0,0,0,0.2)]"
|
|
9164
|
-
style={{
|
|
9165
|
-
boxShadow: `0 8px 16px -4px rgba(0,0,0,0.3), 0 4px 8px -4px rgba(0,0,0,0.2), 0 0 18px ${floorplanCursorColor}22`,
|
|
9166
|
-
transform: `translate(${FLOORPLAN_CURSOR_BADGE_OFFSET_X}px, ${FLOORPLAN_CURSOR_BADGE_OFFSET_Y}px)`,
|
|
9167
|
-
}}
|
|
9168
|
-
>
|
|
9169
|
-
{activeFloorplanCursorIndicator.kind === 'asset' ? (
|
|
9170
|
-
<img
|
|
9171
|
-
alt=""
|
|
9172
|
-
aria-hidden="true"
|
|
9173
|
-
className="h-5 w-5 object-contain drop-shadow-[0_2px_4px_rgba(0,0,0,0.5)]"
|
|
9174
|
-
src={activeFloorplanCursorIndicator.iconSrc}
|
|
9175
|
-
/>
|
|
9176
|
-
) : (
|
|
9177
|
-
<Icon
|
|
9178
|
-
aria-hidden="true"
|
|
9179
|
-
className="drop-shadow-[0_2px_4px_rgba(0,0,0,0.5)]"
|
|
9180
|
-
color={floorplanCursorColor}
|
|
9181
|
-
height={18}
|
|
9182
|
-
icon={activeFloorplanCursorIndicator.icon}
|
|
9183
|
-
width={18}
|
|
9184
|
-
/>
|
|
9185
|
-
)}
|
|
9186
|
-
</div>
|
|
9187
|
-
) : (
|
|
9188
|
-
<>
|
|
9189
|
-
<div
|
|
9190
|
-
className="absolute top-0 left-1/2 w-px -translate-x-1/2 -translate-y-full"
|
|
9191
|
-
style={{
|
|
9192
|
-
backgroundColor: floorplanCursorColor,
|
|
9193
|
-
boxShadow: `0 0 12px ${floorplanCursorColor}55`,
|
|
9194
|
-
height: FLOORPLAN_CURSOR_INDICATOR_LINE_HEIGHT,
|
|
9195
|
-
}}
|
|
9196
|
-
/>
|
|
9197
|
-
<div
|
|
9198
|
-
className="absolute top-0 left-1/2 flex h-8 w-8 items-center justify-center rounded-xl border border-white/5 bg-zinc-900/95 shadow-[0_8px_16px_-4px_rgba(0,0,0,0.3),0_4px_8px_-4px_rgba(0,0,0,0.2)]"
|
|
9199
|
-
style={{
|
|
9200
|
-
transform: `translate(-50%, calc(-100% - ${FLOORPLAN_CURSOR_INDICATOR_LINE_HEIGHT}px))`,
|
|
9201
|
-
}}
|
|
9202
|
-
>
|
|
9203
|
-
{activeFloorplanCursorIndicator.kind === 'asset' ? (
|
|
9204
|
-
<img
|
|
9205
|
-
alt=""
|
|
9206
|
-
aria-hidden="true"
|
|
9207
|
-
className="h-5 w-5 object-contain drop-shadow-[0_2px_4px_rgba(0,0,0,0.5)]"
|
|
9208
|
-
src={activeFloorplanCursorIndicator.iconSrc}
|
|
9209
|
-
/>
|
|
9210
|
-
) : (
|
|
9211
|
-
<Icon
|
|
9212
|
-
aria-hidden="true"
|
|
9213
|
-
className="drop-shadow-[0_2px_4px_rgba(0,0,0,0.5)]"
|
|
9214
|
-
color="white"
|
|
9215
|
-
height={18}
|
|
9216
|
-
icon={activeFloorplanCursorIndicator.icon}
|
|
9217
|
-
width={18}
|
|
9218
|
-
/>
|
|
9219
|
-
)}
|
|
9220
|
-
</div>
|
|
9221
|
-
</>
|
|
9222
|
-
)}
|
|
9223
|
-
</div>
|
|
9224
|
-
)}
|
|
9838
|
+
<FloorplanCursorIndicatorOverlay
|
|
9839
|
+
cursorAnchorPosition={floorplanCursorAnchorPosition}
|
|
9840
|
+
cursorColor={floorplanCursorColor}
|
|
9841
|
+
cursorPosition={floorplanCursorPosition}
|
|
9842
|
+
floorplanSelectionTool={floorplanSelectionTool}
|
|
9843
|
+
isPanning={isPanning}
|
|
9844
|
+
movingOpeningType={movingOpeningType}
|
|
9845
|
+
/>
|
|
9225
9846
|
{showGuides && canInteractWithGuides && selectedGuide && (
|
|
9226
9847
|
<FloorplanGuideHandleHint
|
|
9227
9848
|
anchor={guideHandleHintAnchor}
|
|
@@ -9230,60 +9851,41 @@ export function FloorplanPanel() {
|
|
|
9230
9851
|
rotationModifierPressed={rotationModifierPressed}
|
|
9231
9852
|
/>
|
|
9232
9853
|
)}
|
|
9233
|
-
|
|
9234
|
-
|
|
9235
|
-
|
|
9236
|
-
|
|
9237
|
-
|
|
9238
|
-
|
|
9239
|
-
|
|
9240
|
-
|
|
9241
|
-
|
|
9242
|
-
|
|
9243
|
-
|
|
9244
|
-
|
|
9245
|
-
|
|
9246
|
-
|
|
9247
|
-
|
|
9248
|
-
|
|
9249
|
-
|
|
9250
|
-
|
|
9251
|
-
|
|
9252
|
-
|
|
9253
|
-
|
|
9254
|
-
|
|
9255
|
-
|
|
9256
|
-
|
|
9257
|
-
|
|
9258
|
-
|
|
9259
|
-
|
|
9260
|
-
|
|
9261
|
-
|
|
9262
|
-
|
|
9263
|
-
|
|
9264
|
-
|
|
9265
|
-
|
|
9266
|
-
|
|
9267
|
-
|
|
9268
|
-
)}
|
|
9269
|
-
{selectedStairActionMenuPosition && isFloorplanHovered && !movingNode && (
|
|
9270
|
-
<div
|
|
9271
|
-
className="absolute z-30"
|
|
9272
|
-
style={{
|
|
9273
|
-
left: selectedStairActionMenuPosition.x,
|
|
9274
|
-
top: selectedStairActionMenuPosition.y,
|
|
9275
|
-
transform: `translate(-50%, calc(-100% - ${FLOORPLAN_ACTION_MENU_OFFSET_Y}px))`,
|
|
9276
|
-
}}
|
|
9277
|
-
>
|
|
9278
|
-
<NodeActionMenu
|
|
9279
|
-
onDelete={handleSelectedStairDelete}
|
|
9280
|
-
onDuplicate={handleSelectedStairDuplicate}
|
|
9281
|
-
onMove={handleSelectedStairMove}
|
|
9282
|
-
onPointerDown={(event) => event.stopPropagation()}
|
|
9283
|
-
onPointerUp={(event) => event.stopPropagation()}
|
|
9284
|
-
/>
|
|
9285
|
-
</div>
|
|
9286
|
-
)}
|
|
9854
|
+
<FloorplanActionMenuLayer
|
|
9855
|
+
ceiling={{
|
|
9856
|
+
position: selectedCeilingActionMenuPosition,
|
|
9857
|
+
onDelete: handleSelectedCeilingDelete,
|
|
9858
|
+
onMove: handleSelectedCeilingMove,
|
|
9859
|
+
}}
|
|
9860
|
+
item={{
|
|
9861
|
+
position: selectedItemActionMenuPosition,
|
|
9862
|
+
onDelete: handleSelectedItemDelete,
|
|
9863
|
+
onDuplicate: handleSelectedItemDuplicate,
|
|
9864
|
+
onMove: handleSelectedItemMove,
|
|
9865
|
+
}}
|
|
9866
|
+
opening={{
|
|
9867
|
+
position: selectedOpeningActionMenuPosition,
|
|
9868
|
+
onDelete: handleSelectedOpeningDelete,
|
|
9869
|
+
onDuplicate: handleSelectedOpeningDuplicate,
|
|
9870
|
+
onMove: handleSelectedOpeningMove,
|
|
9871
|
+
}}
|
|
9872
|
+
slab={{
|
|
9873
|
+
position: selectedSlabActionMenuPosition,
|
|
9874
|
+
onDelete: handleSelectedSlabDelete,
|
|
9875
|
+
onMove: handleSelectedSlabMove,
|
|
9876
|
+
}}
|
|
9877
|
+
stair={{
|
|
9878
|
+
position: selectedStairActionMenuPosition,
|
|
9879
|
+
onDelete: handleSelectedStairDelete,
|
|
9880
|
+
onDuplicate: handleSelectedStairDuplicate,
|
|
9881
|
+
onMove: handleSelectedStairMove,
|
|
9882
|
+
}}
|
|
9883
|
+
wall={{
|
|
9884
|
+
position: selectedWallActionMenuPosition,
|
|
9885
|
+
onDelete: handleSelectedWallDelete,
|
|
9886
|
+
onMove: handleSelectedWallMove,
|
|
9887
|
+
}}
|
|
9888
|
+
/>
|
|
9287
9889
|
|
|
9288
9890
|
{!levelNode || levelNode.type !== 'level' ? (
|
|
9289
9891
|
<div className="flex h-full items-center justify-center px-6 text-center text-muted-foreground text-sm">
|
|
@@ -9316,308 +9918,316 @@ export function FloorplanPanel() {
|
|
|
9316
9918
|
<FloorplanGridLayer
|
|
9317
9919
|
majorGridPath={majorGridPath}
|
|
9318
9920
|
minorGridPath={minorGridPath}
|
|
9319
|
-
|
|
9320
|
-
|
|
9321
|
-
|
|
9322
|
-
|
|
9323
|
-
<FloorplanGuideLayer
|
|
9324
|
-
activeGuideInteractionGuideId={activeGuideInteractionGuideId}
|
|
9325
|
-
activeGuideInteractionMode={activeGuideInteractionMode}
|
|
9326
|
-
guides={displayGuides}
|
|
9327
|
-
isInteractive={canInteractWithGuides}
|
|
9328
|
-
onGuideSelect={handleGuideSelect}
|
|
9329
|
-
onGuideTranslateStart={handleGuideTranslateStart}
|
|
9330
|
-
selectedGuideId={selectedGuideId}
|
|
9331
|
-
/>
|
|
9921
|
+
palette={palette}
|
|
9922
|
+
showGrid={showGrid}
|
|
9923
|
+
/>
|
|
9332
9924
|
|
|
9333
|
-
|
|
9334
|
-
|
|
9335
|
-
|
|
9336
|
-
|
|
9337
|
-
|
|
9338
|
-
|
|
9339
|
-
|
|
9340
|
-
|
|
9341
|
-
|
|
9342
|
-
hoveredWallId={hoveredWallId}
|
|
9343
|
-
isDeleteMode={isDeleteMode}
|
|
9344
|
-
onOpeningDoubleClick={handleOpeningDoubleClick}
|
|
9345
|
-
onOpeningHoverChange={handleOpeningHoverChange}
|
|
9346
|
-
onOpeningPointerDown={handleOpeningPointerDown}
|
|
9347
|
-
onOpeningSelect={handleOpeningSelect}
|
|
9348
|
-
onSlabDoubleClick={handleSlabDoubleClick}
|
|
9349
|
-
onSlabHoverChange={handleSlabHoverChange}
|
|
9350
|
-
onSlabSelect={handleSlabSelect}
|
|
9351
|
-
onWallClick={handleWallClick}
|
|
9352
|
-
onWallDoubleClick={handleWallDoubleClick}
|
|
9353
|
-
onWallHoverChange={handleWallHoverChange}
|
|
9354
|
-
openingsPolygons={openingsPolygons}
|
|
9355
|
-
palette={palette}
|
|
9356
|
-
selectedIdSet={selectedIdSet}
|
|
9357
|
-
slabPolygons={displaySlabPolygons}
|
|
9358
|
-
unit={unit}
|
|
9359
|
-
wallPolygons={displayWallPolygons}
|
|
9360
|
-
/>
|
|
9925
|
+
<FloorplanGuideLayer
|
|
9926
|
+
activeGuideInteractionGuideId={activeGuideInteractionGuideId}
|
|
9927
|
+
activeGuideInteractionMode={activeGuideInteractionMode}
|
|
9928
|
+
guides={displayGuides}
|
|
9929
|
+
isInteractive={canInteractWithGuides}
|
|
9930
|
+
onGuideSelect={handleGuideSelect}
|
|
9931
|
+
onGuideTranslateStart={handleGuideTranslateStart}
|
|
9932
|
+
selectedGuideId={selectedGuideId}
|
|
9933
|
+
/>
|
|
9361
9934
|
|
|
9362
|
-
|
|
9363
|
-
|
|
9364
|
-
|
|
9365
|
-
|
|
9366
|
-
|
|
9367
|
-
|
|
9368
|
-
|
|
9369
|
-
|
|
9370
|
-
|
|
9371
|
-
|
|
9935
|
+
<FloorplanSiteLayer isEditing={isSiteEditActive} sitePolygon={visibleSitePolygon} />
|
|
9936
|
+
|
|
9937
|
+
<FloorplanGeometryLayer
|
|
9938
|
+
canFocusGeometry={canSelectElementFloorplanGeometry}
|
|
9939
|
+
canSelectGeometry={canInteractElementFloorplanGeometry}
|
|
9940
|
+
canSelectSlabs={canInteractFloorplanSlabs}
|
|
9941
|
+
highlightedIdSet={highlightedFloorplanIdSet}
|
|
9942
|
+
hoveredOpeningId={hoveredOpeningId}
|
|
9943
|
+
hoveredSlabId={hoveredSlabId}
|
|
9944
|
+
hoveredWallId={hoveredWallId}
|
|
9945
|
+
isDeleteMode={isDeleteMode}
|
|
9946
|
+
onOpeningDoubleClick={handleOpeningDoubleClick}
|
|
9947
|
+
onOpeningHoverChange={handleOpeningHoverChange}
|
|
9948
|
+
onOpeningPointerDown={handleOpeningPointerDown}
|
|
9949
|
+
onOpeningSelect={handleOpeningSelect}
|
|
9950
|
+
onSlabDoubleClick={handleSlabDoubleClick}
|
|
9951
|
+
onSlabHoverChange={handleSlabHoverChange}
|
|
9952
|
+
onSlabSelect={handleSlabSelect}
|
|
9953
|
+
onWallClick={handleWallClick}
|
|
9954
|
+
onWallDoubleClick={handleWallDoubleClick}
|
|
9955
|
+
onWallHoverChange={handleWallHoverChange}
|
|
9956
|
+
openingsPolygons={openingsPolygons}
|
|
9957
|
+
palette={palette}
|
|
9958
|
+
selectedIdSet={selectedIdSet}
|
|
9959
|
+
slabPolygons={displaySlabPolygons}
|
|
9960
|
+
unit={unit}
|
|
9961
|
+
wallPolygons={displayWallPolygons}
|
|
9962
|
+
/>
|
|
9372
9963
|
|
|
9373
|
-
|
|
9374
|
-
|
|
9375
|
-
|
|
9376
|
-
|
|
9377
|
-
|
|
9378
|
-
|
|
9379
|
-
|
|
9380
|
-
|
|
9381
|
-
|
|
9382
|
-
|
|
9383
|
-
itemEntries={floorplanItemEntries}
|
|
9384
|
-
onItemDoubleClick={handleItemDoubleClick}
|
|
9385
|
-
onItemHoverChange={handleItemHoverChange}
|
|
9386
|
-
onItemHoverEnter={handleFloorplanItemHoverEnter}
|
|
9387
|
-
onItemPointerDown={handleItemPointerDown}
|
|
9388
|
-
onItemSelect={handleItemSelect}
|
|
9389
|
-
onStairDoubleClick={handleStairDoubleClick}
|
|
9390
|
-
onStairHoverChange={handleStairHoverChange}
|
|
9391
|
-
onStairHoverEnter={handleFloorplanStairHoverEnter}
|
|
9392
|
-
onStairSelect={handleStairSelect}
|
|
9393
|
-
palette={palette}
|
|
9394
|
-
selectedIdSet={selectedIdSet}
|
|
9395
|
-
stairEntries={renderedFloorplanStairEntries}
|
|
9396
|
-
/>
|
|
9964
|
+
<FloorplanZoneLayer
|
|
9965
|
+
canSelectZones={canInteractFloorplanZones}
|
|
9966
|
+
hoveredZoneId={hoveredZoneId}
|
|
9967
|
+
isDeleteMode={isDeleteMode}
|
|
9968
|
+
onZoneHoverChange={handleZoneHoverChange}
|
|
9969
|
+
onZoneSelect={handleZoneSelect}
|
|
9970
|
+
palette={palette}
|
|
9971
|
+
selectedZoneId={selectedZoneId}
|
|
9972
|
+
zonePolygons={visibleZonePolygons}
|
|
9973
|
+
/>
|
|
9397
9974
|
|
|
9398
|
-
|
|
9399
|
-
|
|
9400
|
-
|
|
9401
|
-
|
|
9402
|
-
|
|
9403
|
-
|
|
9404
|
-
|
|
9405
|
-
|
|
9406
|
-
|
|
9975
|
+
<FloorplanNodeLayer
|
|
9976
|
+
canFocusItems={canFocusFloorplanItems}
|
|
9977
|
+
canFocusStairs={canFocusFloorplanStairs}
|
|
9978
|
+
canSelectItems={canSelectFloorplanItems}
|
|
9979
|
+
canSelectStairs={canSelectFloorplanStairs}
|
|
9980
|
+
highlightedIdSet={highlightedFloorplanIdSet}
|
|
9981
|
+
hoveredItemId={hoveredItemId}
|
|
9982
|
+
hoveredStairId={hoveredStairId}
|
|
9983
|
+
isDeleteMode={isDeleteMode}
|
|
9984
|
+
isFurnishContextActive={isFloorplanFurnishContextActive}
|
|
9985
|
+
itemEntries={floorplanItemEntries}
|
|
9986
|
+
onItemDoubleClick={handleItemDoubleClick}
|
|
9987
|
+
onItemHoverChange={handleItemHoverChange}
|
|
9988
|
+
onItemHoverEnter={handleFloorplanItemHoverEnter}
|
|
9989
|
+
onItemPointerDown={handleItemPointerDown}
|
|
9990
|
+
onItemSelect={handleItemSelect}
|
|
9991
|
+
onStairDoubleClick={handleStairDoubleClick}
|
|
9992
|
+
onStairHoverChange={handleStairHoverChange}
|
|
9993
|
+
onStairHoverEnter={handleFloorplanStairHoverEnter}
|
|
9994
|
+
onStairSelect={handleStairSelect}
|
|
9995
|
+
palette={palette}
|
|
9996
|
+
selectedIdSet={selectedIdSet}
|
|
9997
|
+
stairEntries={renderedFloorplanStairEntries}
|
|
9998
|
+
/>
|
|
9407
9999
|
|
|
9408
|
-
|
|
9409
|
-
|
|
9410
|
-
|
|
9411
|
-
|
|
9412
|
-
|
|
9413
|
-
|
|
9414
|
-
|
|
9415
|
-
|
|
9416
|
-
|
|
9417
|
-
}
|
|
9418
|
-
onVertexPointerDown={(nodeId, vertexIndex, event) =>
|
|
9419
|
-
handleSiteVertexPointerDown(nodeId as SiteNode['id'], vertexIndex, event)
|
|
9420
|
-
}
|
|
9421
|
-
palette={palette}
|
|
9422
|
-
vertexHandles={siteVertexHandles}
|
|
9423
|
-
/>
|
|
10000
|
+
{/* Zone labels: always visible so users can click to select zones from any mode */}
|
|
10001
|
+
<FloorplanZoneLabelLayer
|
|
10002
|
+
onLabelHoverChange={handleZoneHoverChange}
|
|
10003
|
+
onZoneLabelClick={handleZoneLabelClick}
|
|
10004
|
+
selectedZoneId={selectedZoneId}
|
|
10005
|
+
svgRef={svgRef}
|
|
10006
|
+
viewBox={viewBox}
|
|
10007
|
+
zonePolygons={displayZonePolygons}
|
|
10008
|
+
/>
|
|
9424
10009
|
|
|
9425
|
-
|
|
9426
|
-
|
|
9427
|
-
|
|
9428
|
-
|
|
9429
|
-
|
|
9430
|
-
event
|
|
9431
|
-
|
|
9432
|
-
|
|
9433
|
-
|
|
9434
|
-
|
|
9435
|
-
|
|
9436
|
-
|
|
9437
|
-
|
|
9438
|
-
|
|
9439
|
-
|
|
9440
|
-
onPointerUp={handleMarqueePointerUp}
|
|
9441
|
-
style={{ cursor: EDITOR_CURSOR }}
|
|
9442
|
-
width={viewBox.width}
|
|
9443
|
-
x={viewBox.minX}
|
|
9444
|
-
y={viewBox.minY}
|
|
10010
|
+
<FloorplanPolygonHandleLayer
|
|
10011
|
+
hoveredHandleId={hoveredSiteHandleId}
|
|
10012
|
+
midpointHandles={siteMidpointHandles}
|
|
10013
|
+
onHandleHoverChange={setHoveredSiteHandleId}
|
|
10014
|
+
onMidpointPointerDown={(nodeId, edgeIndex, event) =>
|
|
10015
|
+
handleSiteMidpointPointerDown(nodeId as SiteNode['id'], edgeIndex, event)
|
|
10016
|
+
}
|
|
10017
|
+
onVertexDoubleClick={(nodeId, vertexIndex, event) =>
|
|
10018
|
+
handleSiteVertexDoubleClick(nodeId as SiteNode['id'], vertexIndex, event)
|
|
10019
|
+
}
|
|
10020
|
+
onVertexPointerDown={(nodeId, vertexIndex, event) =>
|
|
10021
|
+
handleSiteVertexPointerDown(nodeId as SiteNode['id'], vertexIndex, event)
|
|
10022
|
+
}
|
|
10023
|
+
palette={palette}
|
|
10024
|
+
vertexHandles={siteVertexHandles}
|
|
9445
10025
|
/>
|
|
9446
|
-
)}
|
|
9447
10026
|
|
|
9448
|
-
|
|
9449
|
-
<>
|
|
10027
|
+
{isMarqueeSelectionToolActive && (
|
|
9450
10028
|
<rect
|
|
9451
|
-
fill=
|
|
9452
|
-
|
|
9453
|
-
|
|
9454
|
-
|
|
9455
|
-
|
|
9456
|
-
|
|
9457
|
-
|
|
10029
|
+
fill="transparent"
|
|
10030
|
+
height={viewBox.height}
|
|
10031
|
+
onClick={(event) => {
|
|
10032
|
+
event.preventDefault()
|
|
10033
|
+
event.stopPropagation()
|
|
10034
|
+
}}
|
|
10035
|
+
onDoubleClick={(event) => {
|
|
10036
|
+
event.preventDefault()
|
|
10037
|
+
event.stopPropagation()
|
|
10038
|
+
}}
|
|
10039
|
+
onPointerCancel={handleMarqueePointerCancel}
|
|
10040
|
+
onPointerDown={handleMarqueePointerDown}
|
|
10041
|
+
onPointerMove={handleMarqueePointerMove}
|
|
10042
|
+
onPointerUp={handleMarqueePointerUp}
|
|
10043
|
+
style={{ cursor: EDITOR_CURSOR }}
|
|
10044
|
+
width={viewBox.width}
|
|
10045
|
+
x={viewBox.minX}
|
|
10046
|
+
y={viewBox.minY}
|
|
10047
|
+
/>
|
|
10048
|
+
)}
|
|
10049
|
+
|
|
10050
|
+
{visibleSvgMarqueeBounds && (
|
|
10051
|
+
<>
|
|
10052
|
+
<rect
|
|
10053
|
+
fill={palette.cursor}
|
|
10054
|
+
fillOpacity={0.12}
|
|
10055
|
+
height={visibleSvgMarqueeBounds.height}
|
|
10056
|
+
pointerEvents="none"
|
|
10057
|
+
stroke={palette.cursor}
|
|
10058
|
+
strokeOpacity={0.26}
|
|
10059
|
+
strokeWidth={FLOORPLAN_MARQUEE_GLOW_WIDTH}
|
|
10060
|
+
vectorEffect="non-scaling-stroke"
|
|
10061
|
+
width={visibleSvgMarqueeBounds.width}
|
|
10062
|
+
x={visibleSvgMarqueeBounds.x}
|
|
10063
|
+
y={visibleSvgMarqueeBounds.y}
|
|
10064
|
+
/>
|
|
10065
|
+
<rect
|
|
10066
|
+
fill="none"
|
|
10067
|
+
height={visibleSvgMarqueeBounds.height}
|
|
10068
|
+
pointerEvents="none"
|
|
10069
|
+
stroke={palette.cursor}
|
|
10070
|
+
strokeOpacity={0.96}
|
|
10071
|
+
strokeWidth={FLOORPLAN_MARQUEE_OUTLINE_WIDTH}
|
|
10072
|
+
vectorEffect="non-scaling-stroke"
|
|
10073
|
+
width={visibleSvgMarqueeBounds.width}
|
|
10074
|
+
x={visibleSvgMarqueeBounds.x}
|
|
10075
|
+
y={visibleSvgMarqueeBounds.y}
|
|
10076
|
+
/>
|
|
10077
|
+
</>
|
|
10078
|
+
)}
|
|
10079
|
+
|
|
10080
|
+
{draftPolygon && (
|
|
10081
|
+
<polygon
|
|
10082
|
+
fill={palette.draftFill}
|
|
10083
|
+
fillOpacity={0.35}
|
|
10084
|
+
points={draftPolygonPoints ?? undefined}
|
|
10085
|
+
stroke={palette.draftStroke}
|
|
10086
|
+
strokeDasharray="0.24 0.12"
|
|
10087
|
+
strokeWidth="0.07"
|
|
9458
10088
|
vectorEffect="non-scaling-stroke"
|
|
9459
|
-
width={visibleSvgMarqueeBounds.width}
|
|
9460
|
-
x={visibleSvgMarqueeBounds.x}
|
|
9461
|
-
y={visibleSvgMarqueeBounds.y}
|
|
9462
10089
|
/>
|
|
9463
|
-
|
|
10090
|
+
)}
|
|
10091
|
+
|
|
10092
|
+
{polygonDraftPolygonPoints && (
|
|
10093
|
+
<polygon
|
|
10094
|
+
fill={palette.draftFill}
|
|
10095
|
+
fillOpacity={0.2}
|
|
10096
|
+
points={polygonDraftPolygonPoints}
|
|
10097
|
+
stroke="none"
|
|
10098
|
+
/>
|
|
10099
|
+
)}
|
|
10100
|
+
|
|
10101
|
+
{polygonDraftPolylinePoints && (
|
|
10102
|
+
<polyline
|
|
9464
10103
|
fill="none"
|
|
9465
|
-
|
|
9466
|
-
|
|
9467
|
-
|
|
9468
|
-
|
|
9469
|
-
strokeWidth=
|
|
10104
|
+
points={polygonDraftPolylinePoints}
|
|
10105
|
+
stroke={palette.draftStroke}
|
|
10106
|
+
strokeLinecap="round"
|
|
10107
|
+
strokeLinejoin="round"
|
|
10108
|
+
strokeWidth="0.08"
|
|
9470
10109
|
vectorEffect="non-scaling-stroke"
|
|
9471
|
-
width={visibleSvgMarqueeBounds.width}
|
|
9472
|
-
x={visibleSvgMarqueeBounds.x}
|
|
9473
|
-
y={visibleSvgMarqueeBounds.y}
|
|
9474
10110
|
/>
|
|
9475
|
-
|
|
9476
|
-
)}
|
|
10111
|
+
)}
|
|
9477
10112
|
|
|
9478
|
-
|
|
9479
|
-
|
|
9480
|
-
|
|
9481
|
-
|
|
9482
|
-
|
|
9483
|
-
|
|
9484
|
-
|
|
9485
|
-
|
|
9486
|
-
|
|
9487
|
-
|
|
9488
|
-
|
|
10113
|
+
{polygonDraftClosingSegment && (
|
|
10114
|
+
<line
|
|
10115
|
+
stroke={palette.draftStroke}
|
|
10116
|
+
strokeDasharray="0.16 0.1"
|
|
10117
|
+
strokeLinecap="round"
|
|
10118
|
+
strokeOpacity={0.75}
|
|
10119
|
+
strokeWidth="0.05"
|
|
10120
|
+
vectorEffect="non-scaling-stroke"
|
|
10121
|
+
x1={polygonDraftClosingSegment.x1}
|
|
10122
|
+
x2={polygonDraftClosingSegment.x2}
|
|
10123
|
+
y1={polygonDraftClosingSegment.y1}
|
|
10124
|
+
y2={polygonDraftClosingSegment.y2}
|
|
10125
|
+
/>
|
|
10126
|
+
)}
|
|
9489
10127
|
|
|
9490
|
-
|
|
9491
|
-
|
|
9492
|
-
|
|
9493
|
-
|
|
9494
|
-
|
|
9495
|
-
|
|
9496
|
-
|
|
9497
|
-
|
|
10128
|
+
{activePolygonDraftPoints.map((point, index) => (
|
|
10129
|
+
<circle
|
|
10130
|
+
cx={toSvgX(point[0])}
|
|
10131
|
+
cy={toSvgY(point[1])}
|
|
10132
|
+
fill={index === 0 ? palette.anchor : palette.draftStroke}
|
|
10133
|
+
fillOpacity={0.95}
|
|
10134
|
+
key={`polygon-draft-${index}`}
|
|
10135
|
+
pointerEvents="none"
|
|
10136
|
+
r={index === 0 ? 0.12 : 0.1}
|
|
10137
|
+
vectorEffect="non-scaling-stroke"
|
|
10138
|
+
/>
|
|
10139
|
+
))}
|
|
9498
10140
|
|
|
9499
|
-
|
|
9500
|
-
|
|
9501
|
-
|
|
9502
|
-
|
|
9503
|
-
|
|
9504
|
-
|
|
9505
|
-
strokeLinejoin="round"
|
|
9506
|
-
strokeWidth="0.08"
|
|
9507
|
-
vectorEffect="non-scaling-stroke"
|
|
10141
|
+
<FloorplanWallEndpointLayer
|
|
10142
|
+
endpointHandles={wallEndpointHandles}
|
|
10143
|
+
hoveredEndpointId={hoveredEndpointId}
|
|
10144
|
+
onEndpointHoverChange={setHoveredEndpointId}
|
|
10145
|
+
onWallEndpointPointerDown={handleWallEndpointPointerDown}
|
|
10146
|
+
palette={palette}
|
|
9508
10147
|
/>
|
|
9509
|
-
)}
|
|
9510
10148
|
|
|
9511
|
-
|
|
9512
|
-
|
|
9513
|
-
|
|
9514
|
-
|
|
9515
|
-
|
|
9516
|
-
|
|
9517
|
-
strokeWidth="0.05"
|
|
9518
|
-
vectorEffect="non-scaling-stroke"
|
|
9519
|
-
x1={polygonDraftClosingSegment.x1}
|
|
9520
|
-
x2={polygonDraftClosingSegment.x2}
|
|
9521
|
-
y1={polygonDraftClosingSegment.y1}
|
|
9522
|
-
y2={polygonDraftClosingSegment.y2}
|
|
10149
|
+
<FloorplanWallCurveHandleLayer
|
|
10150
|
+
curveHandles={wallCurveHandles}
|
|
10151
|
+
hoveredHandleId={hoveredWallCurveHandleId}
|
|
10152
|
+
onHandleHoverChange={setHoveredWallCurveHandleId}
|
|
10153
|
+
onWallCurvePointerDown={handleWallCurvePointerDown}
|
|
10154
|
+
palette={palette}
|
|
9523
10155
|
/>
|
|
9524
|
-
)}
|
|
9525
10156
|
|
|
9526
|
-
|
|
9527
|
-
|
|
9528
|
-
|
|
9529
|
-
|
|
9530
|
-
|
|
9531
|
-
|
|
9532
|
-
|
|
9533
|
-
|
|
9534
|
-
|
|
9535
|
-
|
|
10157
|
+
<FloorplanPolygonHandleLayer
|
|
10158
|
+
hoveredHandleId={hoveredSlabHandleId}
|
|
10159
|
+
midpointHandles={slabMidpointHandles}
|
|
10160
|
+
onHandleHoverChange={setHoveredSlabHandleId}
|
|
10161
|
+
onMidpointPointerDown={(nodeId, edgeIndex, event) =>
|
|
10162
|
+
handleSlabMidpointPointerDown(nodeId as SlabNode['id'], edgeIndex, event)
|
|
10163
|
+
}
|
|
10164
|
+
onVertexDoubleClick={(nodeId, vertexIndex, event) =>
|
|
10165
|
+
handleSlabVertexDoubleClick(nodeId as SlabNode['id'], vertexIndex, event)
|
|
10166
|
+
}
|
|
10167
|
+
onVertexPointerDown={(nodeId, vertexIndex, event) =>
|
|
10168
|
+
handleSlabVertexPointerDown(nodeId as SlabNode['id'], vertexIndex, event)
|
|
10169
|
+
}
|
|
10170
|
+
palette={palette}
|
|
10171
|
+
vertexHandles={slabVertexHandles}
|
|
9536
10172
|
/>
|
|
9537
|
-
))}
|
|
9538
|
-
|
|
9539
|
-
<FloorplanWallEndpointLayer
|
|
9540
|
-
endpointHandles={wallEndpointHandles}
|
|
9541
|
-
hoveredEndpointId={hoveredEndpointId}
|
|
9542
|
-
onEndpointHoverChange={setHoveredEndpointId}
|
|
9543
|
-
onWallEndpointPointerDown={handleWallEndpointPointerDown}
|
|
9544
|
-
palette={palette}
|
|
9545
|
-
/>
|
|
9546
|
-
|
|
9547
|
-
<FloorplanPolygonHandleLayer
|
|
9548
|
-
hoveredHandleId={hoveredSlabHandleId}
|
|
9549
|
-
midpointHandles={slabMidpointHandles}
|
|
9550
|
-
onHandleHoverChange={setHoveredSlabHandleId}
|
|
9551
|
-
onMidpointPointerDown={(nodeId, edgeIndex, event) =>
|
|
9552
|
-
handleSlabMidpointPointerDown(nodeId as SlabNode['id'], edgeIndex, event)
|
|
9553
|
-
}
|
|
9554
|
-
onVertexDoubleClick={(nodeId, vertexIndex, event) =>
|
|
9555
|
-
handleSlabVertexDoubleClick(nodeId as SlabNode['id'], vertexIndex, event)
|
|
9556
|
-
}
|
|
9557
|
-
onVertexPointerDown={(nodeId, vertexIndex, event) =>
|
|
9558
|
-
handleSlabVertexPointerDown(nodeId as SlabNode['id'], vertexIndex, event)
|
|
9559
|
-
}
|
|
9560
|
-
palette={palette}
|
|
9561
|
-
vertexHandles={slabVertexHandles}
|
|
9562
|
-
/>
|
|
9563
10173
|
|
|
9564
|
-
|
|
9565
|
-
|
|
9566
|
-
|
|
9567
|
-
|
|
9568
|
-
|
|
9569
|
-
|
|
9570
|
-
|
|
9571
|
-
|
|
9572
|
-
|
|
9573
|
-
|
|
9574
|
-
|
|
9575
|
-
|
|
9576
|
-
|
|
9577
|
-
|
|
9578
|
-
|
|
9579
|
-
/>
|
|
9580
|
-
|
|
9581
|
-
{selectedGuide && showGuides && (
|
|
9582
|
-
<FloorplanGuideSelectionOverlay
|
|
9583
|
-
guide={selectedGuide}
|
|
9584
|
-
isDarkMode={theme === 'dark'}
|
|
9585
|
-
onCornerHoverChange={setHoveredGuideCorner}
|
|
9586
|
-
onCornerPointerDown={handleGuideCornerPointerDown}
|
|
9587
|
-
rotationModifierPressed={rotationModifierPressed}
|
|
9588
|
-
showHandles={canInteractWithGuides}
|
|
10174
|
+
<FloorplanPolygonHandleLayer
|
|
10175
|
+
hoveredHandleId={hoveredZoneHandleId}
|
|
10176
|
+
midpointHandles={zoneMidpointHandles}
|
|
10177
|
+
onHandleHoverChange={setHoveredZoneHandleId}
|
|
10178
|
+
onMidpointPointerDown={(nodeId, edgeIndex, event) =>
|
|
10179
|
+
handleZoneMidpointPointerDown(nodeId as ZoneNodeType['id'], edgeIndex, event)
|
|
10180
|
+
}
|
|
10181
|
+
onVertexDoubleClick={(nodeId, vertexIndex, event) =>
|
|
10182
|
+
handleZoneVertexDoubleClick(nodeId as ZoneNodeType['id'], vertexIndex, event)
|
|
10183
|
+
}
|
|
10184
|
+
onVertexPointerDown={(nodeId, vertexIndex, event) =>
|
|
10185
|
+
handleZoneVertexPointerDown(nodeId as ZoneNodeType['id'], vertexIndex, event)
|
|
10186
|
+
}
|
|
10187
|
+
palette={palette}
|
|
10188
|
+
vertexHandles={zoneVertexHandles}
|
|
9589
10189
|
/>
|
|
9590
|
-
)}
|
|
9591
10190
|
|
|
9592
|
-
|
|
9593
|
-
|
|
9594
|
-
|
|
9595
|
-
|
|
9596
|
-
|
|
9597
|
-
|
|
9598
|
-
|
|
9599
|
-
|
|
10191
|
+
{selectedGuide && showGuides && (
|
|
10192
|
+
<FloorplanGuideSelectionOverlay
|
|
10193
|
+
guide={selectedGuide}
|
|
10194
|
+
isDarkMode={theme === 'dark'}
|
|
10195
|
+
onCornerHoverChange={setHoveredGuideCorner}
|
|
10196
|
+
onCornerPointerDown={handleGuideCornerPointerDown}
|
|
10197
|
+
rotationModifierPressed={rotationModifierPressed}
|
|
10198
|
+
showHandles={canInteractWithGuides}
|
|
9600
10199
|
/>
|
|
10200
|
+
)}
|
|
10201
|
+
|
|
10202
|
+
{cursorPoint && (
|
|
10203
|
+
<g>
|
|
10204
|
+
<circle
|
|
10205
|
+
cx={toSvgX(cursorPoint[0])}
|
|
10206
|
+
cy={toSvgY(cursorPoint[1])}
|
|
10207
|
+
fill={floorplanCursorColor}
|
|
10208
|
+
fillOpacity={0.25}
|
|
10209
|
+
r={FLOORPLAN_CURSOR_MARKER_GLOW_RADIUS}
|
|
10210
|
+
/>
|
|
10211
|
+
<circle
|
|
10212
|
+
cx={toSvgX(cursorPoint[0])}
|
|
10213
|
+
cy={toSvgY(cursorPoint[1])}
|
|
10214
|
+
fill={floorplanCursorColor}
|
|
10215
|
+
fillOpacity={0.9}
|
|
10216
|
+
r={FLOORPLAN_CURSOR_MARKER_CORE_RADIUS}
|
|
10217
|
+
/>
|
|
10218
|
+
</g>
|
|
10219
|
+
)}
|
|
10220
|
+
|
|
10221
|
+
{activeDraftAnchorPoint && (
|
|
9601
10222
|
<circle
|
|
9602
|
-
cx={toSvgX(
|
|
9603
|
-
cy={toSvgY(
|
|
9604
|
-
fill={
|
|
9605
|
-
fillOpacity={0.
|
|
9606
|
-
r=
|
|
10223
|
+
cx={toSvgX(activeDraftAnchorPoint[0])}
|
|
10224
|
+
cy={toSvgY(activeDraftAnchorPoint[1])}
|
|
10225
|
+
fill={palette.anchor}
|
|
10226
|
+
fillOpacity={0.95}
|
|
10227
|
+
r="0.14"
|
|
10228
|
+
vectorEffect="non-scaling-stroke"
|
|
9607
10229
|
/>
|
|
9608
|
-
|
|
9609
|
-
)}
|
|
9610
|
-
|
|
9611
|
-
{activeDraftAnchorPoint && (
|
|
9612
|
-
<circle
|
|
9613
|
-
cx={toSvgX(activeDraftAnchorPoint[0])}
|
|
9614
|
-
cy={toSvgY(activeDraftAnchorPoint[1])}
|
|
9615
|
-
fill={palette.anchor}
|
|
9616
|
-
fillOpacity={0.95}
|
|
9617
|
-
r="0.14"
|
|
9618
|
-
vectorEffect="non-scaling-stroke"
|
|
9619
|
-
/>
|
|
9620
|
-
)}
|
|
10230
|
+
)}
|
|
9621
10231
|
</g>
|
|
9622
10232
|
</svg>
|
|
9623
10233
|
)}
|