@pascal-app/editor 0.4.0 → 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 +341 -48
- package/src/components/editor/floating-building-action-menu.tsx +70 -0
- package/src/components/editor/floorplan-panel.tsx +1350 -722
- package/src/components/editor/index.tsx +221 -167
- package/src/components/editor/node-action-menu.tsx +40 -11
- package/src/components/editor/selection-manager.tsx +238 -10
- package/src/components/editor/site-edge-labels.tsx +9 -3
- package/src/components/editor/thumbnail-generator.tsx +422 -79
- 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/systems/stair/stair-edit-system.tsx +27 -5
- package/src/components/tools/building/move-building-tool.tsx +157 -0
- 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-math.ts +1 -1
- package/src/components/tools/door/door-tool.tsx +31 -7
- package/src/components/tools/door/move-door-tool.tsx +27 -8
- package/src/components/tools/fence/curve-fence-tool.tsx +179 -0
- package/src/components/tools/fence/fence-drafting.ts +137 -0
- package/src/components/tools/fence/fence-tool.tsx +190 -0
- package/src/components/tools/fence/move-fence-endpoint-tool.tsx +327 -0
- package/src/components/tools/fence/move-fence-tool.tsx +231 -0
- package/src/components/tools/item/item-tool.tsx +3 -3
- package/src/components/tools/item/move-tool.tsx +16 -0
- package/src/components/tools/item/placement-math.ts +14 -6
- package/src/components/tools/item/placement-strategies.ts +17 -9
- package/src/components/tools/item/use-placement-coordinator.tsx +123 -16
- package/src/components/tools/roof/move-roof-tool.tsx +90 -26
- package/src/components/tools/roof/roof-tool.tsx +6 -6
- package/src/components/tools/select/box-select-tool.tsx +2 -2
- 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/slab/slab-tool.tsx +4 -4
- package/src/components/tools/stair/stair-defaults.ts +10 -0
- package/src/components/tools/stair/stair-tool.tsx +39 -8
- package/src/components/tools/tool-manager.tsx +54 -14
- 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/wall/wall-tool.tsx +19 -29
- package/src/components/tools/window/move-window-tool.tsx +27 -8
- package/src/components/tools/window/window-math.ts +1 -1
- package/src/components/tools/window/window-tool.tsx +31 -7
- package/src/components/tools/zone/zone-tool.tsx +7 -7
- package/src/components/ui/action-menu/control-modes.tsx +9 -4
- package/src/components/ui/action-menu/structure-tools.tsx +1 -0
- 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/helpers/building-helper.tsx +32 -0
- package/src/components/ui/helpers/helper-manager.tsx +2 -0
- 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 +269 -0
- package/src/components/ui/panels/item-panel.tsx +5 -5
- package/src/components/ui/panels/panel-manager.tsx +32 -27
- 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 +377 -50
- 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 +28 -17
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +65 -53
- package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +40 -25
- package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +69 -0
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +88 -72
- package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +14 -13
- package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +64 -53
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +32 -23
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +72 -51
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +40 -37
- package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +72 -51
- package/src/components/ui/sidebar/panels/site-panel/tree-node-actions.tsx +13 -13
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +20 -17
- package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +62 -54
- package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +40 -25
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +27 -28
- package/src/components/ui/viewer-toolbar.tsx +55 -2
- package/src/components/viewer-overlay.tsx +26 -19
- package/src/hooks/use-auto-save.ts +3 -6
- package/src/hooks/use-contextual-tools.ts +25 -16
- package/src/hooks/use-grid-events.ts +13 -1
- package/src/hooks/use-keyboard.ts +7 -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 +125 -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,145 +4661,451 @@ 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
|
-
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
|
|
4601
4693
|
}
|
|
4694
|
+
|
|
4695
|
+
setFloorplanSelectionTool('click')
|
|
4696
|
+
onRestoreGroundLevel()
|
|
4602
4697
|
}
|
|
4603
4698
|
|
|
4604
|
-
|
|
4605
|
-
|
|
4606
|
-
|
|
4607
|
-
|
|
4608
|
-
|
|
4609
|
-
return [] as LevelNode[]
|
|
4610
|
-
}
|
|
4699
|
+
window.addEventListener('keydown', handleKeyDown, true)
|
|
4700
|
+
return () => {
|
|
4701
|
+
window.removeEventListener('keydown', handleKeyDown, true)
|
|
4702
|
+
}
|
|
4703
|
+
}, [isFloorplanHovered, onRestoreGroundLevel, phase, setFloorplanSelectionTool])
|
|
4611
4704
|
|
|
4612
|
-
|
|
4613
|
-
|
|
4614
|
-
return [] as LevelNode[]
|
|
4615
|
-
}
|
|
4705
|
+
return null
|
|
4706
|
+
})
|
|
4616
4707
|
|
|
4617
|
-
|
|
4618
|
-
|
|
4619
|
-
|
|
4620
|
-
|
|
4621
|
-
}),
|
|
4622
|
-
)
|
|
4623
|
-
const walls = useScene(
|
|
4624
|
-
useShallow((state) => {
|
|
4625
|
-
if (!levelId) {
|
|
4626
|
-
return [] as WallNode[]
|
|
4627
|
-
}
|
|
4708
|
+
type FloorplanDuplicateHotkeyProps = {
|
|
4709
|
+
hasDuplicatable: boolean
|
|
4710
|
+
onDuplicateSelected: () => void
|
|
4711
|
+
}
|
|
4628
4712
|
|
|
4629
|
-
|
|
4630
|
-
|
|
4631
|
-
|
|
4632
|
-
|
|
4713
|
+
const FloorplanDuplicateHotkey = memo(function FloorplanDuplicateHotkey({
|
|
4714
|
+
hasDuplicatable,
|
|
4715
|
+
onDuplicateSelected,
|
|
4716
|
+
}: FloorplanDuplicateHotkeyProps) {
|
|
4717
|
+
const isFloorplanHovered = useEditor((state) => state.isFloorplanHovered)
|
|
4633
4718
|
|
|
4634
|
-
|
|
4635
|
-
|
|
4636
|
-
|
|
4637
|
-
|
|
4638
|
-
)
|
|
4639
|
-
const openings = useScene(
|
|
4640
|
-
useShallow((state) => {
|
|
4641
|
-
if (!levelId) {
|
|
4642
|
-
return [] as OpeningNode[]
|
|
4719
|
+
useEffect(() => {
|
|
4720
|
+
const handleKeyDown = (event: KeyboardEvent) => {
|
|
4721
|
+
if (!(event.metaKey || event.ctrlKey) || event.key.toLowerCase() !== 'c') {
|
|
4722
|
+
return
|
|
4643
4723
|
}
|
|
4644
4724
|
|
|
4645
|
-
|
|
4646
|
-
|
|
4647
|
-
return [] as OpeningNode[]
|
|
4725
|
+
if (!(isFloorplanHovered && hasDuplicatable)) {
|
|
4726
|
+
return
|
|
4648
4727
|
}
|
|
4649
4728
|
|
|
4650
|
-
const
|
|
4651
|
-
|
|
4652
|
-
|
|
4729
|
+
const target = event.target as HTMLElement | null
|
|
4730
|
+
const isEditableTarget =
|
|
4731
|
+
target instanceof HTMLInputElement ||
|
|
4732
|
+
target instanceof HTMLTextAreaElement ||
|
|
4733
|
+
Boolean(target?.isContentEditable)
|
|
4653
4734
|
|
|
4654
|
-
|
|
4655
|
-
|
|
4656
|
-
.map((childId) => state.nodes[childId])
|
|
4657
|
-
.filter((node): node is OpeningNode => node?.type === 'window' || node?.type === 'door'),
|
|
4658
|
-
)
|
|
4659
|
-
}),
|
|
4660
|
-
)
|
|
4661
|
-
const slabs = useScene(
|
|
4662
|
-
useShallow((state) => {
|
|
4663
|
-
if (!levelId) {
|
|
4664
|
-
return [] as SlabNode[]
|
|
4735
|
+
if (isEditableTarget) {
|
|
4736
|
+
return
|
|
4665
4737
|
}
|
|
4666
4738
|
|
|
4667
|
-
|
|
4668
|
-
|
|
4669
|
-
|
|
4670
|
-
}
|
|
4739
|
+
event.preventDefault()
|
|
4740
|
+
onDuplicateSelected()
|
|
4741
|
+
}
|
|
4671
4742
|
|
|
4672
|
-
|
|
4673
|
-
|
|
4674
|
-
|
|
4675
|
-
}
|
|
4676
|
-
)
|
|
4677
|
-
|
|
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
|
|
5087
|
+
.map((childId) => state.nodes[childId])
|
|
5088
|
+
.filter((node): node is SlabNode => node?.type === 'slab')
|
|
5089
|
+
}),
|
|
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
|
+
)
|
|
5107
|
+
const levelGuides = useScene(
|
|
5108
|
+
useShallow((state) => {
|
|
4679
5109
|
if (!levelId) {
|
|
4680
5110
|
return [] as GuideNode[]
|
|
4681
5111
|
}
|
|
@@ -4735,6 +5165,7 @@ export function FloorplanPanel() {
|
|
|
4735
5165
|
const [cursorPoint, setCursorPoint] = useState<WallPlanPoint | null>(null)
|
|
4736
5166
|
const [floorplanCursorPosition, setFloorplanCursorPosition] = useState<SvgPoint | null>(null)
|
|
4737
5167
|
const [wallEndpointDraft, setWallEndpointDraft] = useState<WallEndpointDraft | null>(null)
|
|
5168
|
+
const [wallCurveDraft, setWallCurveDraft] = useState<WallCurveDraft | null>(null)
|
|
4738
5169
|
const [hoveredOpeningId, setHoveredOpeningId] = useState<OpeningNode['id'] | null>(null)
|
|
4739
5170
|
const [hoveredWallId, setHoveredWallId] = useState<WallNode['id'] | null>(null)
|
|
4740
5171
|
const [hoveredSlabId, setHoveredSlabId] = useState<SlabNode['id'] | null>(null)
|
|
@@ -4742,6 +5173,7 @@ export function FloorplanPanel() {
|
|
|
4742
5173
|
const [hoveredStairId, setHoveredStairId] = useState<StairNode['id'] | null>(null)
|
|
4743
5174
|
const [hoveredZoneId, setHoveredZoneId] = useState<ZoneNodeType['id'] | null>(null)
|
|
4744
5175
|
const [hoveredEndpointId, setHoveredEndpointId] = useState<string | null>(null)
|
|
5176
|
+
const [hoveredWallCurveHandleId, setHoveredWallCurveHandleId] = useState<string | null>(null)
|
|
4745
5177
|
const [hoveredSiteHandleId, setHoveredSiteHandleId] = useState<string | null>(null)
|
|
4746
5178
|
const [hoveredSlabHandleId, setHoveredSlabHandleId] = useState<string | null>(null)
|
|
4747
5179
|
const [hoveredZoneHandleId, setHoveredZoneHandleId] = useState<string | null>(null)
|
|
@@ -4810,53 +5242,14 @@ export function FloorplanPanel() {
|
|
|
4810
5242
|
const polygon = siteBoundaryDraft.polygon.map(toPoint2D)
|
|
4811
5243
|
|
|
4812
5244
|
return {
|
|
4813
|
-
...sitePolygonEntry,
|
|
4814
|
-
polygon,
|
|
4815
|
-
points: formatPolygonPoints(polygon),
|
|
4816
|
-
}
|
|
4817
|
-
}, [siteBoundaryDraft, sitePolygonEntry])
|
|
4818
|
-
const movingOpeningType =
|
|
4819
|
-
movingNode?.type === 'door' || movingNode?.type === 'window' ? movingNode.type : null
|
|
4820
|
-
|
|
4821
|
-
const activeFloorplanToolConfig = useMemo(() => {
|
|
4822
|
-
if (movingOpeningType) {
|
|
4823
|
-
return structureTools.find((entry) => entry.id === movingOpeningType) ?? null
|
|
4824
|
-
}
|
|
4825
|
-
|
|
4826
|
-
if (mode !== 'build' || !tool) {
|
|
4827
|
-
return null
|
|
4828
|
-
}
|
|
4829
|
-
|
|
4830
|
-
if (tool === 'item' && catalogCategory) {
|
|
4831
|
-
return furnishTools.find((entry) => entry.catalogCategory === catalogCategory) ?? null
|
|
4832
|
-
}
|
|
4833
|
-
|
|
4834
|
-
return structureTools.find((entry) => entry.id === tool) ?? null
|
|
4835
|
-
}, [catalogCategory, mode, movingOpeningType, tool])
|
|
4836
|
-
const activeFloorplanCursorIndicator = useMemo<FloorplanCursorIndicator | null>(() => {
|
|
4837
|
-
if (activeFloorplanToolConfig) {
|
|
4838
|
-
return {
|
|
4839
|
-
kind: 'asset',
|
|
4840
|
-
iconSrc: activeFloorplanToolConfig.iconSrc,
|
|
4841
|
-
}
|
|
4842
|
-
}
|
|
4843
|
-
|
|
4844
|
-
if (mode === 'select' && floorplanSelectionTool === 'marquee' && structureLayer !== 'zones') {
|
|
4845
|
-
return {
|
|
4846
|
-
kind: 'icon',
|
|
4847
|
-
icon: 'mdi:select-drag',
|
|
4848
|
-
}
|
|
4849
|
-
}
|
|
4850
|
-
|
|
4851
|
-
if (mode === 'delete') {
|
|
4852
|
-
return {
|
|
4853
|
-
kind: 'icon',
|
|
4854
|
-
icon: 'mdi:trash-can-outline',
|
|
4855
|
-
}
|
|
5245
|
+
...sitePolygonEntry,
|
|
5246
|
+
polygon,
|
|
5247
|
+
points: formatPolygonPoints(polygon),
|
|
4856
5248
|
}
|
|
5249
|
+
}, [siteBoundaryDraft, sitePolygonEntry])
|
|
5250
|
+
const movingOpeningType =
|
|
5251
|
+
movingNode?.type === 'door' || movingNode?.type === 'window' ? movingNode.type : null
|
|
4857
5252
|
|
|
4858
|
-
return null
|
|
4859
|
-
}, [activeFloorplanToolConfig, floorplanSelectionTool, mode, structureLayer])
|
|
4860
5253
|
const visibleGuides = useMemo<GuideNode[]>(() => {
|
|
4861
5254
|
if (!showGuides) {
|
|
4862
5255
|
return []
|
|
@@ -4916,29 +5309,42 @@ export function FloorplanPanel() {
|
|
|
4916
5309
|
[floorplanWalls],
|
|
4917
5310
|
)
|
|
4918
5311
|
const displayWallById = useMemo(() => {
|
|
4919
|
-
if (!wallEndpointDraft) {
|
|
5312
|
+
if (!(wallEndpointDraft || wallCurveDraft)) {
|
|
4920
5313
|
return wallById
|
|
4921
5314
|
}
|
|
4922
5315
|
|
|
4923
|
-
const
|
|
4924
|
-
|
|
4925
|
-
|
|
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
|
+
}
|
|
4926
5326
|
}
|
|
4927
5327
|
|
|
4928
|
-
|
|
4929
|
-
|
|
4930
|
-
wall
|
|
4931
|
-
|
|
4932
|
-
|
|
5328
|
+
if (wallCurveDraft) {
|
|
5329
|
+
const wall = nextWallById.get(wallCurveDraft.wallId)
|
|
5330
|
+
if (wall) {
|
|
5331
|
+
nextWallById.set(wall.id, { ...wall, curveOffset: wallCurveDraft.curveOffset })
|
|
5332
|
+
}
|
|
5333
|
+
}
|
|
4933
5334
|
|
|
4934
5335
|
return nextWallById
|
|
4935
|
-
}, [wallById, wallEndpointDraft])
|
|
5336
|
+
}, [wallById, wallCurveDraft, wallEndpointDraft])
|
|
4936
5337
|
const displayFloorplanWallById = useMemo(() => {
|
|
4937
|
-
if (!wallEndpointDraft) {
|
|
5338
|
+
if (!(wallEndpointDraft || wallCurveDraft)) {
|
|
5339
|
+
return floorplanWallById
|
|
5340
|
+
}
|
|
5341
|
+
|
|
5342
|
+
const previewWallId = wallEndpointDraft?.wallId ?? wallCurveDraft?.wallId
|
|
5343
|
+
if (!previewWallId) {
|
|
4938
5344
|
return floorplanWallById
|
|
4939
5345
|
}
|
|
4940
5346
|
|
|
4941
|
-
const previewWall = displayWallById.get(
|
|
5347
|
+
const previewWall = displayWallById.get(previewWallId)
|
|
4942
5348
|
if (!previewWall) {
|
|
4943
5349
|
return floorplanWallById
|
|
4944
5350
|
}
|
|
@@ -4946,7 +5352,7 @@ export function FloorplanPanel() {
|
|
|
4946
5352
|
const nextFloorplanWallById = new Map(floorplanWallById)
|
|
4947
5353
|
nextFloorplanWallById.set(previewWall.id, getFloorplanWall(previewWall))
|
|
4948
5354
|
return nextFloorplanWallById
|
|
4949
|
-
}, [displayWallById, floorplanWallById, wallEndpointDraft])
|
|
5355
|
+
}, [displayWallById, floorplanWallById, wallCurveDraft, wallEndpointDraft])
|
|
4950
5356
|
const wallPolygons = useMemo(
|
|
4951
5357
|
() =>
|
|
4952
5358
|
walls.map((wall) => {
|
|
@@ -4961,11 +5367,16 @@ export function FloorplanPanel() {
|
|
|
4961
5367
|
[floorplanWallById, wallMiterData, walls],
|
|
4962
5368
|
)
|
|
4963
5369
|
const displayWallPolygons = useMemo(() => {
|
|
4964
|
-
if (!wallEndpointDraft) {
|
|
5370
|
+
if (!(wallEndpointDraft || wallCurveDraft)) {
|
|
5371
|
+
return wallPolygons
|
|
5372
|
+
}
|
|
5373
|
+
|
|
5374
|
+
const previewWallId = wallEndpointDraft?.wallId ?? wallCurveDraft?.wallId
|
|
5375
|
+
if (!previewWallId) {
|
|
4965
5376
|
return wallPolygons
|
|
4966
5377
|
}
|
|
4967
5378
|
|
|
4968
|
-
const previewWall = displayWallById.get(
|
|
5379
|
+
const previewWall = displayWallById.get(previewWallId)
|
|
4969
5380
|
if (!previewWall) {
|
|
4970
5381
|
return wallPolygons
|
|
4971
5382
|
}
|
|
@@ -4984,7 +5395,7 @@ export function FloorplanPanel() {
|
|
|
4984
5395
|
}
|
|
4985
5396
|
: entry,
|
|
4986
5397
|
)
|
|
4987
|
-
}, [displayWallById, wallEndpointDraft, wallPolygons])
|
|
5398
|
+
}, [displayWallById, wallCurveDraft, wallEndpointDraft, wallPolygons])
|
|
4988
5399
|
|
|
4989
5400
|
const openingsPolygons = useMemo(
|
|
4990
5401
|
() =>
|
|
@@ -5040,6 +5451,29 @@ export function FloorplanPanel() {
|
|
|
5040
5451
|
: entry,
|
|
5041
5452
|
)
|
|
5042
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
|
+
)
|
|
5043
5477
|
const zonePolygons = useMemo(
|
|
5044
5478
|
() =>
|
|
5045
5479
|
zones.flatMap((zone) => {
|
|
@@ -5170,6 +5604,13 @@ export function FloorplanPanel() {
|
|
|
5170
5604
|
|
|
5171
5605
|
return floorplanItemEntries.find(({ item }) => item.id === selectedIds[0]) ?? null
|
|
5172
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])
|
|
5173
5614
|
const selectedStairEntry = useMemo(() => {
|
|
5174
5615
|
if (selectedIds.length !== 1) {
|
|
5175
5616
|
return null
|
|
@@ -5186,6 +5627,13 @@ export function FloorplanPanel() {
|
|
|
5186
5627
|
|
|
5187
5628
|
return displaySlabPolygons.find(({ slab }) => slab.id === selectedIds[0]) ?? null
|
|
5188
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])
|
|
5189
5637
|
const selectedZoneEntry = useMemo(() => {
|
|
5190
5638
|
if (!selectedZoneId) {
|
|
5191
5639
|
return null
|
|
@@ -5206,12 +5654,25 @@ export function FloorplanPanel() {
|
|
|
5206
5654
|
const isOpeningPlacementActive = isOpeningBuildActive || isOpeningMoveActive
|
|
5207
5655
|
const isStairBuildActive = phase === 'structure' && mode === 'build' && tool === 'stair'
|
|
5208
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'
|
|
5209
5662
|
const isItemPlacementPreviewActive =
|
|
5210
5663
|
(mode === 'build' && tool === 'item') || movingNode?.type === 'item'
|
|
5211
5664
|
const isFloorItemBuildActive = mode === 'build' && tool === 'item' && !selectedItem?.attachTo
|
|
5212
5665
|
const isFloorItemMoveActive = movingNode?.type === 'item' && !movingNode.asset.attachTo
|
|
5213
5666
|
const isFloorplanGridInteractionActive =
|
|
5214
|
-
isStairBuildActive ||
|
|
5667
|
+
isStairBuildActive ||
|
|
5668
|
+
isStairMoveActive ||
|
|
5669
|
+
isSlabMoveActive ||
|
|
5670
|
+
isCeilingMoveActive ||
|
|
5671
|
+
isWallMoveActive ||
|
|
5672
|
+
isWallCurveActive ||
|
|
5673
|
+
isFenceCurveActive ||
|
|
5674
|
+
isFloorItemBuildActive ||
|
|
5675
|
+
isFloorItemMoveActive
|
|
5215
5676
|
const floorplanPreviewStairSegment = useMemo(
|
|
5216
5677
|
() =>
|
|
5217
5678
|
StairSegmentNodeSchema.parse({
|
|
@@ -5393,6 +5854,56 @@ export function FloorplanPanel() {
|
|
|
5393
5854
|
shouldShowPersistentWallEndpointHandles,
|
|
5394
5855
|
wallEndpointDraft,
|
|
5395
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
|
+
])
|
|
5396
5907
|
const slabVertexHandles = useMemo(() => {
|
|
5397
5908
|
if (!shouldShowSlabBoundaryHandles) {
|
|
5398
5909
|
return []
|
|
@@ -5654,23 +6165,15 @@ export function FloorplanPanel() {
|
|
|
5654
6165
|
if (levelChanged) {
|
|
5655
6166
|
previousLevelIdRef.current = levelId ?? null
|
|
5656
6167
|
hasUserAdjustedViewportRef.current = false
|
|
5657
|
-
setViewport(fittedViewport)
|
|
6168
|
+
setViewport((current) => (floorplanViewportEquals(current, fittedViewport) ? current : fittedViewport))
|
|
5658
6169
|
return
|
|
5659
6170
|
}
|
|
5660
6171
|
|
|
5661
6172
|
if (!hasUserAdjustedViewportRef.current) {
|
|
5662
|
-
setViewport(fittedViewport)
|
|
6173
|
+
setViewport((current) => (floorplanViewportEquals(current, fittedViewport) ? current : fittedViewport))
|
|
5663
6174
|
}
|
|
5664
6175
|
}, [fittedViewport, levelId])
|
|
5665
6176
|
|
|
5666
|
-
useEffect(() => {
|
|
5667
|
-
if (!(phase === 'site' && levelNode?.type === 'level' && levelNode.level > 0)) {
|
|
5668
|
-
return
|
|
5669
|
-
}
|
|
5670
|
-
|
|
5671
|
-
setPhase('structure')
|
|
5672
|
-
}, [levelNode, phase, setPhase])
|
|
5673
|
-
|
|
5674
6177
|
const viewBox = useMemo(() => {
|
|
5675
6178
|
const currentViewport = viewport ?? fittedViewport
|
|
5676
6179
|
const width = currentViewport.width
|
|
@@ -5711,6 +6214,27 @@ export function FloorplanPanel() {
|
|
|
5711
6214
|
: null,
|
|
5712
6215
|
[selectedItemEntry, surfaceSize, viewBox],
|
|
5713
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
|
+
)
|
|
5714
6238
|
const selectedStairActionMenuPosition = useMemo(
|
|
5715
6239
|
() =>
|
|
5716
6240
|
selectedStairEntry
|
|
@@ -5963,9 +6487,14 @@ export function FloorplanPanel() {
|
|
|
5963
6487
|
return null
|
|
5964
6488
|
}
|
|
5965
6489
|
|
|
6490
|
+
if (buildingRotationY !== 0) {
|
|
6491
|
+
const [unrotX, unrotY] = rotatePlanVector(svgPoint.x, svgPoint.y, buildingRotationY)
|
|
6492
|
+
return toPlanPointFromSvgPoint({ x: unrotX, y: unrotY })
|
|
6493
|
+
}
|
|
6494
|
+
|
|
5966
6495
|
return toPlanPointFromSvgPoint(svgPoint)
|
|
5967
6496
|
},
|
|
5968
|
-
[getSvgPointFromClientPoint],
|
|
6497
|
+
[getSvgPointFromClientPoint, buildingRotationY],
|
|
5969
6498
|
)
|
|
5970
6499
|
useEffect(() => {
|
|
5971
6500
|
siteBoundaryDraftRef.current = siteBoundaryDraft
|
|
@@ -6187,6 +6716,11 @@ export function FloorplanPanel() {
|
|
|
6187
6716
|
setWallEndpointDraft(null)
|
|
6188
6717
|
setHoveredEndpointId(null)
|
|
6189
6718
|
}, [])
|
|
6719
|
+
const clearWallCurveDrag = useCallback(() => {
|
|
6720
|
+
wallCurveDragRef.current = null
|
|
6721
|
+
setWallCurveDraft(null)
|
|
6722
|
+
setHoveredWallCurveHandleId(null)
|
|
6723
|
+
}, [])
|
|
6190
6724
|
const clearSiteBoundaryInteraction = useCallback(() => {
|
|
6191
6725
|
setSiteVertexDragState(null)
|
|
6192
6726
|
setSiteBoundaryDraft(null)
|
|
@@ -6208,11 +6742,13 @@ export function FloorplanPanel() {
|
|
|
6208
6742
|
clearSlabPlacementDraft()
|
|
6209
6743
|
clearZonePlacementDraft()
|
|
6210
6744
|
clearWallEndpointDrag()
|
|
6745
|
+
clearWallCurveDrag()
|
|
6211
6746
|
clearSiteBoundaryInteraction()
|
|
6212
6747
|
clearSlabBoundaryInteraction()
|
|
6213
6748
|
clearZoneBoundaryInteraction()
|
|
6214
6749
|
setCursorPoint(null)
|
|
6215
6750
|
}, [
|
|
6751
|
+
clearWallCurveDrag,
|
|
6216
6752
|
clearSiteBoundaryInteraction,
|
|
6217
6753
|
clearSlabBoundaryInteraction,
|
|
6218
6754
|
clearSlabPlacementDraft,
|
|
@@ -6419,51 +6955,85 @@ export function FloorplanPanel() {
|
|
|
6419
6955
|
}
|
|
6420
6956
|
|
|
6421
6957
|
const dragState = wallEndpointDragRef.current
|
|
6422
|
-
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) {
|
|
6423
7005
|
return
|
|
6424
7006
|
}
|
|
6425
7007
|
|
|
6426
7008
|
event.preventDefault()
|
|
6427
7009
|
|
|
6428
7010
|
const planPoint = getPlanPointFromClientPoint(event.clientX, event.clientY)
|
|
6429
|
-
|
|
7011
|
+
const wall = wallById.get(curveDragState.wallId)
|
|
7012
|
+
if (!(planPoint && wall)) {
|
|
6430
7013
|
return
|
|
6431
7014
|
}
|
|
6432
7015
|
|
|
6433
|
-
const
|
|
6434
|
-
|
|
6435
|
-
|
|
6436
|
-
|
|
6437
|
-
|
|
6438
|
-
|
|
6439
|
-
|
|
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
|
+
)
|
|
6440
7028
|
|
|
6441
|
-
if (
|
|
7029
|
+
if (curveDragState.currentCurveOffset === nextCurveOffset) {
|
|
6442
7030
|
return
|
|
6443
7031
|
}
|
|
6444
7032
|
|
|
6445
|
-
|
|
7033
|
+
curveDragState.currentCurveOffset = nextCurveOffset
|
|
7034
|
+
setWallCurveDraft({ wallId: wall.id, curveOffset: nextCurveOffset })
|
|
6446
7035
|
setCursorPoint(snappedPoint)
|
|
6447
|
-
|
|
6448
|
-
const nextDraft = buildWallEndpointDraft(
|
|
6449
|
-
dragState.wallId,
|
|
6450
|
-
dragState.endpoint,
|
|
6451
|
-
dragState.fixedPoint,
|
|
6452
|
-
snappedPoint,
|
|
6453
|
-
)
|
|
6454
|
-
|
|
6455
|
-
if (
|
|
6456
|
-
!(
|
|
6457
|
-
previousDraft &&
|
|
6458
|
-
pointsEqual(previousDraft.start, nextDraft.start) &&
|
|
6459
|
-
pointsEqual(previousDraft.end, nextDraft.end)
|
|
6460
|
-
)
|
|
6461
|
-
) {
|
|
6462
|
-
sfxEmitter.emit('sfx:grid-snap')
|
|
6463
|
-
}
|
|
6464
|
-
|
|
6465
|
-
return nextDraft
|
|
6466
|
-
})
|
|
7036
|
+
sfxEmitter.emit('sfx:grid-snap')
|
|
6467
7037
|
}
|
|
6468
7038
|
|
|
6469
7039
|
const commitGuideInteraction = (event: PointerEvent) => {
|
|
@@ -6548,6 +7118,26 @@ export function FloorplanPanel() {
|
|
|
6548
7118
|
setCursorPoint(null)
|
|
6549
7119
|
}
|
|
6550
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
|
+
|
|
6551
7141
|
const cancelWallEndpointDrag = (event: PointerEvent) => {
|
|
6552
7142
|
const dragState = wallEndpointDragRef.current
|
|
6553
7143
|
if (!dragState || event.pointerId !== dragState.pointerId) {
|
|
@@ -6558,11 +7148,23 @@ export function FloorplanPanel() {
|
|
|
6558
7148
|
setCursorPoint(null)
|
|
6559
7149
|
}
|
|
6560
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
|
+
|
|
6561
7161
|
window.addEventListener('pointermove', handleWindowPointerMove)
|
|
6562
7162
|
window.addEventListener('pointerup', commitGuideInteraction)
|
|
6563
7163
|
window.addEventListener('pointercancel', cancelGuideInteraction)
|
|
6564
7164
|
window.addEventListener('pointerup', commitWallEndpointDrag)
|
|
6565
7165
|
window.addEventListener('pointercancel', cancelWallEndpointDrag)
|
|
7166
|
+
window.addEventListener('pointerup', commitWallCurveDrag)
|
|
7167
|
+
window.addEventListener('pointercancel', cancelWallCurveDrag)
|
|
6566
7168
|
|
|
6567
7169
|
return () => {
|
|
6568
7170
|
window.removeEventListener('pointermove', handleWindowPointerMove)
|
|
@@ -6570,8 +7172,11 @@ export function FloorplanPanel() {
|
|
|
6570
7172
|
window.removeEventListener('pointercancel', cancelGuideInteraction)
|
|
6571
7173
|
window.removeEventListener('pointerup', commitWallEndpointDrag)
|
|
6572
7174
|
window.removeEventListener('pointercancel', cancelWallEndpointDrag)
|
|
7175
|
+
window.removeEventListener('pointerup', commitWallCurveDrag)
|
|
7176
|
+
window.removeEventListener('pointercancel', cancelWallCurveDrag)
|
|
6573
7177
|
}
|
|
6574
7178
|
}, [
|
|
7179
|
+
clearWallCurveDrag,
|
|
6575
7180
|
clearGuideInteraction,
|
|
6576
7181
|
clearWallEndpointDrag,
|
|
6577
7182
|
getSvgPointFromClientPoint,
|
|
@@ -6585,7 +7190,8 @@ export function FloorplanPanel() {
|
|
|
6585
7190
|
|
|
6586
7191
|
useEffect(() => {
|
|
6587
7192
|
clearWallEndpointDrag()
|
|
6588
|
-
|
|
7193
|
+
clearWallCurveDrag()
|
|
7194
|
+
}, [clearWallCurveDrag, clearWallEndpointDrag, levelId])
|
|
6589
7195
|
|
|
6590
7196
|
useEffect(() => {
|
|
6591
7197
|
if (shouldShowSiteBoundaryHandles) {
|
|
@@ -6973,6 +7579,7 @@ export function FloorplanPanel() {
|
|
|
6973
7579
|
emitter.emit(`grid:${eventType}` as any, {
|
|
6974
7580
|
nativeEvent: nativeEvent.nativeEvent as any,
|
|
6975
7581
|
position: [snappedPoint[0], worldY, snappedPoint[1]],
|
|
7582
|
+
localPosition: [snappedPoint[0], worldY, snappedPoint[1]],
|
|
6976
7583
|
})
|
|
6977
7584
|
|
|
6978
7585
|
return snappedPoint
|
|
@@ -7054,7 +7661,9 @@ export function FloorplanPanel() {
|
|
|
7054
7661
|
}
|
|
7055
7662
|
|
|
7056
7663
|
if (isOpeningPlacementActive) {
|
|
7057
|
-
const closest = findClosestWallPoint(planPoint, walls
|
|
7664
|
+
const closest = findClosestWallPoint(planPoint, walls, {
|
|
7665
|
+
canUseWall: (wall) => !isCurvedWall(wall),
|
|
7666
|
+
})
|
|
7058
7667
|
if (closest) {
|
|
7059
7668
|
const dx = closest.wall.end[0] - closest.wall.start[0]
|
|
7060
7669
|
const dz = closest.wall.end[1] - closest.wall.start[1]
|
|
@@ -7274,7 +7883,9 @@ export function FloorplanPanel() {
|
|
|
7274
7883
|
}
|
|
7275
7884
|
|
|
7276
7885
|
if (isOpeningPlacementActive) {
|
|
7277
|
-
const closest = findClosestWallPoint(planPoint, walls
|
|
7886
|
+
const closest = findClosestWallPoint(planPoint, walls, {
|
|
7887
|
+
canUseWall: (wall) => !isCurvedWall(wall),
|
|
7888
|
+
})
|
|
7278
7889
|
if (closest) {
|
|
7279
7890
|
const dx = closest.wall.end[0] - closest.wall.start[0]
|
|
7280
7891
|
const dz = closest.wall.end[1] - closest.wall.start[1]
|
|
@@ -7361,7 +7972,9 @@ export function FloorplanPanel() {
|
|
|
7361
7972
|
isOpeningPlacementActive,
|
|
7362
7973
|
isPolygonBuildActive,
|
|
7363
7974
|
isWallBuildActive,
|
|
7975
|
+
isWindowBuildActive,
|
|
7364
7976
|
isZoneBuildActive,
|
|
7977
|
+
movingOpeningType,
|
|
7365
7978
|
setSelectedReferenceId,
|
|
7366
7979
|
setSelection,
|
|
7367
7980
|
shiftPressed,
|
|
@@ -8051,6 +8664,7 @@ export function FloorplanPanel() {
|
|
|
8051
8664
|
...(typeof cloned.metadata === 'object' && cloned.metadata !== null ? cloned.metadata : {}),
|
|
8052
8665
|
isNew: true,
|
|
8053
8666
|
}
|
|
8667
|
+
cloned.children = []
|
|
8054
8668
|
|
|
8055
8669
|
try {
|
|
8056
8670
|
const duplicate = ItemNodeSchema.parse(cloned)
|
|
@@ -8082,6 +8696,96 @@ export function FloorplanPanel() {
|
|
|
8082
8696
|
},
|
|
8083
8697
|
[deleteNode, selectedItemEntry, setSelection],
|
|
8084
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
|
+
)
|
|
8085
8789
|
const handleStairDoubleClick = useCallback(
|
|
8086
8790
|
(stair: StairNode, event: ReactMouseEvent<SVGElement>) => {
|
|
8087
8791
|
emitFloorplanNodeClick(stair.id, 'double-click', event)
|
|
@@ -8179,8 +8883,8 @@ export function FloorplanPanel() {
|
|
|
8179
8883
|
delete cloned.id
|
|
8180
8884
|
cloned.metadata = {
|
|
8181
8885
|
...(typeof cloned.metadata === 'object' && cloned.metadata !== null ? cloned.metadata : {}),
|
|
8182
|
-
isNew: true,
|
|
8183
8886
|
}
|
|
8887
|
+
delete (cloned.metadata as Record<string, unknown>).isNew
|
|
8184
8888
|
|
|
8185
8889
|
const nextPosition =
|
|
8186
8890
|
Array.isArray(cloned.position) && cloned.position.length >= 3
|
|
@@ -8195,9 +8899,11 @@ export function FloorplanPanel() {
|
|
|
8195
8899
|
|
|
8196
8900
|
try {
|
|
8197
8901
|
const duplicate = StairNodeSchema.parse(cloned)
|
|
8198
|
-
useScene.getState().createNode(duplicate, stair.parentId as AnyNodeId)
|
|
8199
|
-
|
|
8200
8902
|
const nodesState = useScene.getState().nodes
|
|
8903
|
+
const createOps: { node: AnyNode; parentId?: AnyNodeId }[] = [
|
|
8904
|
+
{ node: duplicate, parentId: stair.parentId as AnyNodeId },
|
|
8905
|
+
]
|
|
8906
|
+
|
|
8201
8907
|
for (const childId of stair.children ?? []) {
|
|
8202
8908
|
const childNode = nodesState[childId]
|
|
8203
8909
|
if (childNode?.type !== 'stair-segment') {
|
|
@@ -8210,19 +8916,20 @@ export function FloorplanPanel() {
|
|
|
8210
8916
|
...(typeof childClone.metadata === 'object' && childClone.metadata !== null
|
|
8211
8917
|
? childClone.metadata
|
|
8212
8918
|
: {}),
|
|
8213
|
-
isNew: true,
|
|
8214
8919
|
}
|
|
8920
|
+
delete (childClone.metadata as Record<string, unknown>).isNew
|
|
8215
8921
|
|
|
8216
8922
|
const childDuplicate = StairSegmentNodeSchema.parse(childClone)
|
|
8217
|
-
|
|
8923
|
+
createOps.push({ node: childDuplicate, parentId: duplicate.id as AnyNodeId })
|
|
8218
8924
|
}
|
|
8219
8925
|
|
|
8220
|
-
|
|
8221
|
-
|
|
8926
|
+
useScene.getState().createNodes(createOps)
|
|
8927
|
+
|
|
8928
|
+
setSelection({ selectedIds: [duplicate.id as AnyNodeId] })
|
|
8222
8929
|
} catch (error) {
|
|
8223
8930
|
console.error('Failed to duplicate stair', error)
|
|
8224
8931
|
}
|
|
8225
|
-
}, [selectedStairEntry,
|
|
8932
|
+
}, [selectedStairEntry, setSelection])
|
|
8226
8933
|
const handleSelectedStairDuplicate = useCallback(
|
|
8227
8934
|
(event: ReactMouseEvent<HTMLButtonElement>) => {
|
|
8228
8935
|
event.stopPropagation()
|
|
@@ -8288,6 +8995,39 @@ export function FloorplanPanel() {
|
|
|
8288
8995
|
},
|
|
8289
8996
|
[clearWallPlacementDraft, handleWallPlacementPoint, handleWallSelect, isWallBuildActive, mode],
|
|
8290
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
|
+
)
|
|
8291
9031
|
const handleSlabVertexPointerDown = useCallback(
|
|
8292
9032
|
(slabId: SlabNode['id'], vertexIndex: number, event: ReactPointerEvent<SVGCircleElement>) => {
|
|
8293
9033
|
if (event.button !== 0) {
|
|
@@ -8634,10 +9374,20 @@ export function FloorplanPanel() {
|
|
|
8634
9374
|
zoneVertexDragState,
|
|
8635
9375
|
])
|
|
8636
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
|
+
|
|
8637
9387
|
const handleSvgPointerMove = useCallback(
|
|
8638
9388
|
(event: ReactPointerEvent<SVGSVGElement>) => {
|
|
8639
9389
|
if (
|
|
8640
|
-
|
|
9390
|
+
hasFloorplanCursorIndicator &&
|
|
8641
9391
|
!panStateRef.current &&
|
|
8642
9392
|
!guideInteractionRef.current &&
|
|
8643
9393
|
!wallEndpointDragRef.current &&
|
|
@@ -8646,19 +9396,28 @@ export function FloorplanPanel() {
|
|
|
8646
9396
|
!zoneVertexDragState
|
|
8647
9397
|
) {
|
|
8648
9398
|
const rect = event.currentTarget.getBoundingClientRect()
|
|
8649
|
-
|
|
9399
|
+
const nextPosition = {
|
|
8650
9400
|
x: event.clientX - rect.left,
|
|
8651
9401
|
y: event.clientY - rect.top,
|
|
8652
|
-
}
|
|
9402
|
+
}
|
|
9403
|
+
setFloorplanCursorPosition((currentPosition) =>
|
|
9404
|
+
currentPosition &&
|
|
9405
|
+
currentPosition.x === nextPosition.x &&
|
|
9406
|
+
currentPosition.y === nextPosition.y
|
|
9407
|
+
? currentPosition
|
|
9408
|
+
: nextPosition,
|
|
9409
|
+
)
|
|
8653
9410
|
} else {
|
|
8654
|
-
setFloorplanCursorPosition(
|
|
9411
|
+
setFloorplanCursorPosition((currentPosition) =>
|
|
9412
|
+
currentPosition === null ? currentPosition : null,
|
|
9413
|
+
)
|
|
8655
9414
|
}
|
|
8656
9415
|
|
|
8657
9416
|
handlePointerMove(event)
|
|
8658
9417
|
},
|
|
8659
9418
|
[
|
|
8660
|
-
activeFloorplanCursorIndicator,
|
|
8661
9419
|
handlePointerMove,
|
|
9420
|
+
hasFloorplanCursorIndicator,
|
|
8662
9421
|
siteVertexDragState,
|
|
8663
9422
|
slabVertexDragState,
|
|
8664
9423
|
zoneVertexDragState,
|
|
@@ -9028,84 +9787,25 @@ export function FloorplanPanel() {
|
|
|
9028
9787
|
setStructureLayer,
|
|
9029
9788
|
site,
|
|
9030
9789
|
])
|
|
9031
|
-
|
|
9032
|
-
|
|
9033
|
-
|
|
9034
|
-
|
|
9035
|
-
|
|
9036
|
-
|
|
9037
|
-
|
|
9038
|
-
|
|
9039
|
-
if (
|
|
9040
|
-
isEditableTarget ||
|
|
9041
|
-
!isFloorplanHovered ||
|
|
9042
|
-
phase !== 'site' ||
|
|
9043
|
-
event.metaKey ||
|
|
9044
|
-
event.ctrlKey ||
|
|
9045
|
-
event.altKey ||
|
|
9046
|
-
event.key.toLowerCase() !== 'v'
|
|
9047
|
-
) {
|
|
9048
|
-
return
|
|
9049
|
-
}
|
|
9050
|
-
|
|
9051
|
-
setFloorplanSelectionTool('click')
|
|
9052
|
-
restoreGroundLevelStructureSelection()
|
|
9053
|
-
}
|
|
9054
|
-
|
|
9055
|
-
window.addEventListener('keydown', handleKeyDown, true)
|
|
9056
|
-
|
|
9057
|
-
return () => {
|
|
9058
|
-
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
|
|
9059
9797
|
}
|
|
9060
|
-
|
|
9061
|
-
|
|
9062
|
-
|
|
9063
|
-
if (!(event.metaKey || event.ctrlKey) || event.key.toLowerCase() !== 'c') {
|
|
9064
|
-
return
|
|
9065
|
-
}
|
|
9066
|
-
|
|
9067
|
-
if (
|
|
9068
|
-
!(isFloorplanHovered && (selectedItemEntry || selectedOpeningEntry || selectedStairEntry))
|
|
9069
|
-
) {
|
|
9070
|
-
return
|
|
9071
|
-
}
|
|
9072
|
-
|
|
9073
|
-
const target = event.target as HTMLElement | null
|
|
9074
|
-
const isEditableTarget =
|
|
9075
|
-
target instanceof HTMLInputElement ||
|
|
9076
|
-
target instanceof HTMLTextAreaElement ||
|
|
9077
|
-
Boolean(target?.isContentEditable)
|
|
9078
|
-
|
|
9079
|
-
if (isEditableTarget) {
|
|
9080
|
-
return
|
|
9081
|
-
}
|
|
9082
|
-
|
|
9083
|
-
event.preventDefault()
|
|
9084
|
-
if (selectedOpeningEntry) {
|
|
9085
|
-
duplicateSelectedOpening()
|
|
9086
|
-
return
|
|
9087
|
-
}
|
|
9088
|
-
|
|
9089
|
-
if (selectedItemEntry) {
|
|
9090
|
-
duplicateSelectedItem()
|
|
9091
|
-
return
|
|
9092
|
-
}
|
|
9093
|
-
|
|
9094
|
-
if (selectedStairEntry) {
|
|
9095
|
-
duplicateSelectedStair()
|
|
9096
|
-
}
|
|
9798
|
+
if (selectedItemEntry) {
|
|
9799
|
+
duplicateSelectedItem()
|
|
9800
|
+
return
|
|
9097
9801
|
}
|
|
9098
|
-
|
|
9099
|
-
|
|
9100
|
-
|
|
9101
|
-
return () => {
|
|
9102
|
-
window.removeEventListener('keydown', handleKeyDown, true)
|
|
9802
|
+
if (selectedStairEntry) {
|
|
9803
|
+
duplicateSelectedStair()
|
|
9103
9804
|
}
|
|
9104
9805
|
}, [
|
|
9105
9806
|
duplicateSelectedItem,
|
|
9106
9807
|
duplicateSelectedOpening,
|
|
9107
9808
|
duplicateSelectedStair,
|
|
9108
|
-
isFloorplanHovered,
|
|
9109
9809
|
selectedItemEntry,
|
|
9110
9810
|
selectedOpeningEntry,
|
|
9111
9811
|
selectedStairEntry,
|
|
@@ -9119,9 +9819,6 @@ export function FloorplanPanel() {
|
|
|
9119
9819
|
: activeDraftAnchorPoint
|
|
9120
9820
|
? palette.draftStroke
|
|
9121
9821
|
: palette.cursor
|
|
9122
|
-
const activeCursorIndicatorPosition =
|
|
9123
|
-
mode === 'delete' ? floorplanCursorPosition : floorplanCursorAnchorPosition
|
|
9124
|
-
|
|
9125
9822
|
return (
|
|
9126
9823
|
<div
|
|
9127
9824
|
className="pointer-events-auto flex h-full w-full flex-col overflow-hidden bg-background/95"
|
|
@@ -9132,80 +9829,20 @@ export function FloorplanPanel() {
|
|
|
9132
9829
|
}}
|
|
9133
9830
|
ref={containerRef}
|
|
9134
9831
|
>
|
|
9832
|
+
<FloorplanSiteKeyHandler onRestoreGroundLevel={restoreGroundLevelStructureSelection} />
|
|
9833
|
+
<FloorplanDuplicateHotkey
|
|
9834
|
+
hasDuplicatable={hasDuplicatableFloorplanSelection}
|
|
9835
|
+
onDuplicateSelected={handleDuplicateFloorplanSelection}
|
|
9836
|
+
/>
|
|
9135
9837
|
<div className="relative min-h-0 flex-1" ref={viewportHostRef}>
|
|
9136
|
-
|
|
9137
|
-
|
|
9138
|
-
|
|
9139
|
-
|
|
9140
|
-
|
|
9141
|
-
|
|
9142
|
-
|
|
9143
|
-
|
|
9144
|
-
>
|
|
9145
|
-
{mode === 'delete' ? (
|
|
9146
|
-
<div
|
|
9147
|
-
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)]"
|
|
9148
|
-
style={{
|
|
9149
|
-
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`,
|
|
9150
|
-
transform: `translate(${FLOORPLAN_CURSOR_BADGE_OFFSET_X}px, ${FLOORPLAN_CURSOR_BADGE_OFFSET_Y}px)`,
|
|
9151
|
-
}}
|
|
9152
|
-
>
|
|
9153
|
-
{activeFloorplanCursorIndicator.kind === 'asset' ? (
|
|
9154
|
-
<img
|
|
9155
|
-
alt=""
|
|
9156
|
-
aria-hidden="true"
|
|
9157
|
-
className="h-5 w-5 object-contain drop-shadow-[0_2px_4px_rgba(0,0,0,0.5)]"
|
|
9158
|
-
src={activeFloorplanCursorIndicator.iconSrc}
|
|
9159
|
-
/>
|
|
9160
|
-
) : (
|
|
9161
|
-
<Icon
|
|
9162
|
-
aria-hidden="true"
|
|
9163
|
-
className="drop-shadow-[0_2px_4px_rgba(0,0,0,0.5)]"
|
|
9164
|
-
color={floorplanCursorColor}
|
|
9165
|
-
height={18}
|
|
9166
|
-
icon={activeFloorplanCursorIndicator.icon}
|
|
9167
|
-
width={18}
|
|
9168
|
-
/>
|
|
9169
|
-
)}
|
|
9170
|
-
</div>
|
|
9171
|
-
) : (
|
|
9172
|
-
<>
|
|
9173
|
-
<div
|
|
9174
|
-
className="absolute top-0 left-1/2 w-px -translate-x-1/2 -translate-y-full"
|
|
9175
|
-
style={{
|
|
9176
|
-
backgroundColor: floorplanCursorColor,
|
|
9177
|
-
boxShadow: `0 0 12px ${floorplanCursorColor}55`,
|
|
9178
|
-
height: FLOORPLAN_CURSOR_INDICATOR_LINE_HEIGHT,
|
|
9179
|
-
}}
|
|
9180
|
-
/>
|
|
9181
|
-
<div
|
|
9182
|
-
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)]"
|
|
9183
|
-
style={{
|
|
9184
|
-
transform: `translate(-50%, calc(-100% - ${FLOORPLAN_CURSOR_INDICATOR_LINE_HEIGHT}px))`,
|
|
9185
|
-
}}
|
|
9186
|
-
>
|
|
9187
|
-
{activeFloorplanCursorIndicator.kind === 'asset' ? (
|
|
9188
|
-
<img
|
|
9189
|
-
alt=""
|
|
9190
|
-
aria-hidden="true"
|
|
9191
|
-
className="h-5 w-5 object-contain drop-shadow-[0_2px_4px_rgba(0,0,0,0.5)]"
|
|
9192
|
-
src={activeFloorplanCursorIndicator.iconSrc}
|
|
9193
|
-
/>
|
|
9194
|
-
) : (
|
|
9195
|
-
<Icon
|
|
9196
|
-
aria-hidden="true"
|
|
9197
|
-
className="drop-shadow-[0_2px_4px_rgba(0,0,0,0.5)]"
|
|
9198
|
-
color="white"
|
|
9199
|
-
height={18}
|
|
9200
|
-
icon={activeFloorplanCursorIndicator.icon}
|
|
9201
|
-
width={18}
|
|
9202
|
-
/>
|
|
9203
|
-
)}
|
|
9204
|
-
</div>
|
|
9205
|
-
</>
|
|
9206
|
-
)}
|
|
9207
|
-
</div>
|
|
9208
|
-
)}
|
|
9838
|
+
<FloorplanCursorIndicatorOverlay
|
|
9839
|
+
cursorAnchorPosition={floorplanCursorAnchorPosition}
|
|
9840
|
+
cursorColor={floorplanCursorColor}
|
|
9841
|
+
cursorPosition={floorplanCursorPosition}
|
|
9842
|
+
floorplanSelectionTool={floorplanSelectionTool}
|
|
9843
|
+
isPanning={isPanning}
|
|
9844
|
+
movingOpeningType={movingOpeningType}
|
|
9845
|
+
/>
|
|
9209
9846
|
{showGuides && canInteractWithGuides && selectedGuide && (
|
|
9210
9847
|
<FloorplanGuideHandleHint
|
|
9211
9848
|
anchor={guideHandleHintAnchor}
|
|
@@ -9214,60 +9851,41 @@ export function FloorplanPanel() {
|
|
|
9214
9851
|
rotationModifierPressed={rotationModifierPressed}
|
|
9215
9852
|
/>
|
|
9216
9853
|
)}
|
|
9217
|
-
|
|
9218
|
-
|
|
9219
|
-
|
|
9220
|
-
|
|
9221
|
-
|
|
9222
|
-
|
|
9223
|
-
|
|
9224
|
-
|
|
9225
|
-
|
|
9226
|
-
|
|
9227
|
-
|
|
9228
|
-
|
|
9229
|
-
|
|
9230
|
-
|
|
9231
|
-
|
|
9232
|
-
|
|
9233
|
-
|
|
9234
|
-
|
|
9235
|
-
|
|
9236
|
-
|
|
9237
|
-
|
|
9238
|
-
|
|
9239
|
-
|
|
9240
|
-
|
|
9241
|
-
|
|
9242
|
-
|
|
9243
|
-
|
|
9244
|
-
|
|
9245
|
-
|
|
9246
|
-
|
|
9247
|
-
|
|
9248
|
-
|
|
9249
|
-
|
|
9250
|
-
|
|
9251
|
-
|
|
9252
|
-
)}
|
|
9253
|
-
{selectedStairActionMenuPosition && isFloorplanHovered && !movingNode && (
|
|
9254
|
-
<div
|
|
9255
|
-
className="absolute z-30"
|
|
9256
|
-
style={{
|
|
9257
|
-
left: selectedStairActionMenuPosition.x,
|
|
9258
|
-
top: selectedStairActionMenuPosition.y,
|
|
9259
|
-
transform: `translate(-50%, calc(-100% - ${FLOORPLAN_ACTION_MENU_OFFSET_Y}px))`,
|
|
9260
|
-
}}
|
|
9261
|
-
>
|
|
9262
|
-
<NodeActionMenu
|
|
9263
|
-
onDelete={handleSelectedStairDelete}
|
|
9264
|
-
onDuplicate={handleSelectedStairDuplicate}
|
|
9265
|
-
onMove={handleSelectedStairMove}
|
|
9266
|
-
onPointerDown={(event) => event.stopPropagation()}
|
|
9267
|
-
onPointerUp={(event) => event.stopPropagation()}
|
|
9268
|
-
/>
|
|
9269
|
-
</div>
|
|
9270
|
-
)}
|
|
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
|
+
/>
|
|
9271
9889
|
|
|
9272
9890
|
{!levelNode || levelNode.type !== 'level' ? (
|
|
9273
9891
|
<div className="flex h-full items-center justify-center px-6 text-center text-muted-foreground text-sm">
|
|
@@ -9296,311 +9914,321 @@ export function FloorplanPanel() {
|
|
|
9296
9914
|
y={viewBox.minY}
|
|
9297
9915
|
/>
|
|
9298
9916
|
|
|
9299
|
-
<
|
|
9300
|
-
|
|
9301
|
-
|
|
9302
|
-
|
|
9303
|
-
|
|
9304
|
-
|
|
9305
|
-
|
|
9306
|
-
<FloorplanGuideLayer
|
|
9307
|
-
activeGuideInteractionGuideId={activeGuideInteractionGuideId}
|
|
9308
|
-
activeGuideInteractionMode={activeGuideInteractionMode}
|
|
9309
|
-
guides={displayGuides}
|
|
9310
|
-
isInteractive={canInteractWithGuides}
|
|
9311
|
-
onGuideSelect={handleGuideSelect}
|
|
9312
|
-
onGuideTranslateStart={handleGuideTranslateStart}
|
|
9313
|
-
selectedGuideId={selectedGuideId}
|
|
9314
|
-
/>
|
|
9917
|
+
<g transform={buildingRotationDeg !== 0 ? `rotate(${buildingRotationDeg})` : undefined}>
|
|
9918
|
+
<FloorplanGridLayer
|
|
9919
|
+
majorGridPath={majorGridPath}
|
|
9920
|
+
minorGridPath={minorGridPath}
|
|
9921
|
+
palette={palette}
|
|
9922
|
+
showGrid={showGrid}
|
|
9923
|
+
/>
|
|
9315
9924
|
|
|
9316
|
-
|
|
9317
|
-
|
|
9318
|
-
|
|
9319
|
-
|
|
9320
|
-
|
|
9321
|
-
|
|
9322
|
-
|
|
9323
|
-
|
|
9324
|
-
|
|
9325
|
-
hoveredWallId={hoveredWallId}
|
|
9326
|
-
isDeleteMode={isDeleteMode}
|
|
9327
|
-
onOpeningDoubleClick={handleOpeningDoubleClick}
|
|
9328
|
-
onOpeningHoverChange={handleOpeningHoverChange}
|
|
9329
|
-
onOpeningPointerDown={handleOpeningPointerDown}
|
|
9330
|
-
onOpeningSelect={handleOpeningSelect}
|
|
9331
|
-
onSlabDoubleClick={handleSlabDoubleClick}
|
|
9332
|
-
onSlabHoverChange={handleSlabHoverChange}
|
|
9333
|
-
onSlabSelect={handleSlabSelect}
|
|
9334
|
-
onWallClick={handleWallClick}
|
|
9335
|
-
onWallDoubleClick={handleWallDoubleClick}
|
|
9336
|
-
onWallHoverChange={handleWallHoverChange}
|
|
9337
|
-
openingsPolygons={openingsPolygons}
|
|
9338
|
-
palette={palette}
|
|
9339
|
-
selectedIdSet={selectedIdSet}
|
|
9340
|
-
slabPolygons={displaySlabPolygons}
|
|
9341
|
-
unit={unit}
|
|
9342
|
-
wallPolygons={displayWallPolygons}
|
|
9343
|
-
/>
|
|
9925
|
+
<FloorplanGuideLayer
|
|
9926
|
+
activeGuideInteractionGuideId={activeGuideInteractionGuideId}
|
|
9927
|
+
activeGuideInteractionMode={activeGuideInteractionMode}
|
|
9928
|
+
guides={displayGuides}
|
|
9929
|
+
isInteractive={canInteractWithGuides}
|
|
9930
|
+
onGuideSelect={handleGuideSelect}
|
|
9931
|
+
onGuideTranslateStart={handleGuideTranslateStart}
|
|
9932
|
+
selectedGuideId={selectedGuideId}
|
|
9933
|
+
/>
|
|
9344
9934
|
|
|
9345
|
-
|
|
9346
|
-
|
|
9347
|
-
|
|
9348
|
-
|
|
9349
|
-
|
|
9350
|
-
|
|
9351
|
-
|
|
9352
|
-
|
|
9353
|
-
|
|
9354
|
-
|
|
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
|
+
/>
|
|
9355
9963
|
|
|
9356
|
-
|
|
9357
|
-
|
|
9358
|
-
|
|
9359
|
-
|
|
9360
|
-
|
|
9361
|
-
|
|
9362
|
-
|
|
9363
|
-
|
|
9364
|
-
|
|
9365
|
-
|
|
9366
|
-
itemEntries={floorplanItemEntries}
|
|
9367
|
-
onItemDoubleClick={handleItemDoubleClick}
|
|
9368
|
-
onItemHoverChange={handleItemHoverChange}
|
|
9369
|
-
onItemHoverEnter={handleFloorplanItemHoverEnter}
|
|
9370
|
-
onItemPointerDown={handleItemPointerDown}
|
|
9371
|
-
onItemSelect={handleItemSelect}
|
|
9372
|
-
onStairDoubleClick={handleStairDoubleClick}
|
|
9373
|
-
onStairHoverChange={handleStairHoverChange}
|
|
9374
|
-
onStairHoverEnter={handleFloorplanStairHoverEnter}
|
|
9375
|
-
onStairSelect={handleStairSelect}
|
|
9376
|
-
palette={palette}
|
|
9377
|
-
selectedIdSet={selectedIdSet}
|
|
9378
|
-
stairEntries={renderedFloorplanStairEntries}
|
|
9379
|
-
/>
|
|
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
|
+
/>
|
|
9380
9974
|
|
|
9381
|
-
|
|
9382
|
-
|
|
9383
|
-
|
|
9384
|
-
|
|
9385
|
-
|
|
9386
|
-
|
|
9387
|
-
|
|
9388
|
-
|
|
9389
|
-
|
|
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
|
+
/>
|
|
9390
9999
|
|
|
9391
|
-
|
|
9392
|
-
|
|
9393
|
-
|
|
9394
|
-
|
|
9395
|
-
|
|
9396
|
-
|
|
9397
|
-
|
|
9398
|
-
|
|
9399
|
-
|
|
9400
|
-
}
|
|
9401
|
-
onVertexPointerDown={(nodeId, vertexIndex, event) =>
|
|
9402
|
-
handleSiteVertexPointerDown(nodeId as SiteNode['id'], vertexIndex, event)
|
|
9403
|
-
}
|
|
9404
|
-
palette={palette}
|
|
9405
|
-
vertexHandles={siteVertexHandles}
|
|
9406
|
-
/>
|
|
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
|
+
/>
|
|
9407
10009
|
|
|
9408
|
-
|
|
9409
|
-
|
|
9410
|
-
|
|
9411
|
-
|
|
9412
|
-
|
|
9413
|
-
event
|
|
9414
|
-
|
|
9415
|
-
|
|
9416
|
-
|
|
9417
|
-
|
|
9418
|
-
|
|
9419
|
-
|
|
9420
|
-
|
|
9421
|
-
|
|
9422
|
-
|
|
9423
|
-
onPointerUp={handleMarqueePointerUp}
|
|
9424
|
-
style={{ cursor: EDITOR_CURSOR }}
|
|
9425
|
-
width={viewBox.width}
|
|
9426
|
-
x={viewBox.minX}
|
|
9427
|
-
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}
|
|
9428
10025
|
/>
|
|
9429
|
-
)}
|
|
9430
10026
|
|
|
9431
|
-
|
|
9432
|
-
<>
|
|
10027
|
+
{isMarqueeSelectionToolActive && (
|
|
9433
10028
|
<rect
|
|
9434
|
-
fill=
|
|
9435
|
-
|
|
9436
|
-
|
|
9437
|
-
|
|
9438
|
-
|
|
9439
|
-
|
|
9440
|
-
|
|
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"
|
|
9441
10088
|
vectorEffect="non-scaling-stroke"
|
|
9442
|
-
width={visibleSvgMarqueeBounds.width}
|
|
9443
|
-
x={visibleSvgMarqueeBounds.x}
|
|
9444
|
-
y={visibleSvgMarqueeBounds.y}
|
|
9445
10089
|
/>
|
|
9446
|
-
|
|
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
|
|
9447
10103
|
fill="none"
|
|
9448
|
-
|
|
9449
|
-
|
|
9450
|
-
|
|
9451
|
-
|
|
9452
|
-
strokeWidth=
|
|
10104
|
+
points={polygonDraftPolylinePoints}
|
|
10105
|
+
stroke={palette.draftStroke}
|
|
10106
|
+
strokeLinecap="round"
|
|
10107
|
+
strokeLinejoin="round"
|
|
10108
|
+
strokeWidth="0.08"
|
|
9453
10109
|
vectorEffect="non-scaling-stroke"
|
|
9454
|
-
width={visibleSvgMarqueeBounds.width}
|
|
9455
|
-
x={visibleSvgMarqueeBounds.x}
|
|
9456
|
-
y={visibleSvgMarqueeBounds.y}
|
|
9457
10110
|
/>
|
|
9458
|
-
|
|
9459
|
-
)}
|
|
10111
|
+
)}
|
|
9460
10112
|
|
|
9461
|
-
|
|
9462
|
-
|
|
9463
|
-
|
|
9464
|
-
|
|
9465
|
-
|
|
9466
|
-
|
|
9467
|
-
|
|
9468
|
-
|
|
9469
|
-
|
|
9470
|
-
|
|
9471
|
-
|
|
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
|
+
)}
|
|
9472
10127
|
|
|
9473
|
-
|
|
9474
|
-
|
|
9475
|
-
|
|
9476
|
-
|
|
9477
|
-
|
|
9478
|
-
|
|
9479
|
-
|
|
9480
|
-
|
|
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
|
+
))}
|
|
9481
10140
|
|
|
9482
|
-
|
|
9483
|
-
|
|
9484
|
-
|
|
9485
|
-
|
|
9486
|
-
|
|
9487
|
-
|
|
9488
|
-
strokeLinejoin="round"
|
|
9489
|
-
strokeWidth="0.08"
|
|
9490
|
-
vectorEffect="non-scaling-stroke"
|
|
10141
|
+
<FloorplanWallEndpointLayer
|
|
10142
|
+
endpointHandles={wallEndpointHandles}
|
|
10143
|
+
hoveredEndpointId={hoveredEndpointId}
|
|
10144
|
+
onEndpointHoverChange={setHoveredEndpointId}
|
|
10145
|
+
onWallEndpointPointerDown={handleWallEndpointPointerDown}
|
|
10146
|
+
palette={palette}
|
|
9491
10147
|
/>
|
|
9492
|
-
)}
|
|
9493
10148
|
|
|
9494
|
-
|
|
9495
|
-
|
|
9496
|
-
|
|
9497
|
-
|
|
9498
|
-
|
|
9499
|
-
|
|
9500
|
-
strokeWidth="0.05"
|
|
9501
|
-
vectorEffect="non-scaling-stroke"
|
|
9502
|
-
x1={polygonDraftClosingSegment.x1}
|
|
9503
|
-
x2={polygonDraftClosingSegment.x2}
|
|
9504
|
-
y1={polygonDraftClosingSegment.y1}
|
|
9505
|
-
y2={polygonDraftClosingSegment.y2}
|
|
10149
|
+
<FloorplanWallCurveHandleLayer
|
|
10150
|
+
curveHandles={wallCurveHandles}
|
|
10151
|
+
hoveredHandleId={hoveredWallCurveHandleId}
|
|
10152
|
+
onHandleHoverChange={setHoveredWallCurveHandleId}
|
|
10153
|
+
onWallCurvePointerDown={handleWallCurvePointerDown}
|
|
10154
|
+
palette={palette}
|
|
9506
10155
|
/>
|
|
9507
|
-
)}
|
|
9508
10156
|
|
|
9509
|
-
|
|
9510
|
-
|
|
9511
|
-
|
|
9512
|
-
|
|
9513
|
-
|
|
9514
|
-
|
|
9515
|
-
|
|
9516
|
-
|
|
9517
|
-
|
|
9518
|
-
|
|
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}
|
|
9519
10172
|
/>
|
|
9520
|
-
))}
|
|
9521
10173
|
|
|
9522
|
-
|
|
9523
|
-
|
|
9524
|
-
|
|
9525
|
-
|
|
9526
|
-
|
|
9527
|
-
|
|
9528
|
-
|
|
9529
|
-
|
|
9530
|
-
|
|
9531
|
-
|
|
9532
|
-
|
|
9533
|
-
|
|
9534
|
-
|
|
9535
|
-
|
|
9536
|
-
|
|
9537
|
-
onVertexDoubleClick={(nodeId, vertexIndex, event) =>
|
|
9538
|
-
handleSlabVertexDoubleClick(nodeId as SlabNode['id'], vertexIndex, event)
|
|
9539
|
-
}
|
|
9540
|
-
onVertexPointerDown={(nodeId, vertexIndex, event) =>
|
|
9541
|
-
handleSlabVertexPointerDown(nodeId as SlabNode['id'], vertexIndex, event)
|
|
9542
|
-
}
|
|
9543
|
-
palette={palette}
|
|
9544
|
-
vertexHandles={slabVertexHandles}
|
|
9545
|
-
/>
|
|
9546
|
-
|
|
9547
|
-
<FloorplanPolygonHandleLayer
|
|
9548
|
-
hoveredHandleId={hoveredZoneHandleId}
|
|
9549
|
-
midpointHandles={zoneMidpointHandles}
|
|
9550
|
-
onHandleHoverChange={setHoveredZoneHandleId}
|
|
9551
|
-
onMidpointPointerDown={(nodeId, edgeIndex, event) =>
|
|
9552
|
-
handleZoneMidpointPointerDown(nodeId as ZoneNodeType['id'], edgeIndex, event)
|
|
9553
|
-
}
|
|
9554
|
-
onVertexDoubleClick={(nodeId, vertexIndex, event) =>
|
|
9555
|
-
handleZoneVertexDoubleClick(nodeId as ZoneNodeType['id'], vertexIndex, event)
|
|
9556
|
-
}
|
|
9557
|
-
onVertexPointerDown={(nodeId, vertexIndex, event) =>
|
|
9558
|
-
handleZoneVertexPointerDown(nodeId as ZoneNodeType['id'], vertexIndex, event)
|
|
9559
|
-
}
|
|
9560
|
-
palette={palette}
|
|
9561
|
-
vertexHandles={zoneVertexHandles}
|
|
9562
|
-
/>
|
|
9563
|
-
|
|
9564
|
-
{selectedGuide && showGuides && (
|
|
9565
|
-
<FloorplanGuideSelectionOverlay
|
|
9566
|
-
guide={selectedGuide}
|
|
9567
|
-
isDarkMode={theme === 'dark'}
|
|
9568
|
-
onCornerHoverChange={setHoveredGuideCorner}
|
|
9569
|
-
onCornerPointerDown={handleGuideCornerPointerDown}
|
|
9570
|
-
rotationModifierPressed={rotationModifierPressed}
|
|
9571
|
-
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}
|
|
9572
10189
|
/>
|
|
9573
|
-
)}
|
|
9574
10190
|
|
|
9575
|
-
|
|
9576
|
-
|
|
9577
|
-
|
|
9578
|
-
|
|
9579
|
-
|
|
9580
|
-
|
|
9581
|
-
|
|
9582
|
-
|
|
10191
|
+
{selectedGuide && showGuides && (
|
|
10192
|
+
<FloorplanGuideSelectionOverlay
|
|
10193
|
+
guide={selectedGuide}
|
|
10194
|
+
isDarkMode={theme === 'dark'}
|
|
10195
|
+
onCornerHoverChange={setHoveredGuideCorner}
|
|
10196
|
+
onCornerPointerDown={handleGuideCornerPointerDown}
|
|
10197
|
+
rotationModifierPressed={rotationModifierPressed}
|
|
10198
|
+
showHandles={canInteractWithGuides}
|
|
9583
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 && (
|
|
9584
10222
|
<circle
|
|
9585
|
-
cx={toSvgX(
|
|
9586
|
-
cy={toSvgY(
|
|
9587
|
-
fill={
|
|
9588
|
-
fillOpacity={0.
|
|
9589
|
-
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"
|
|
9590
10229
|
/>
|
|
9591
|
-
|
|
9592
|
-
|
|
9593
|
-
|
|
9594
|
-
{activeDraftAnchorPoint && (
|
|
9595
|
-
<circle
|
|
9596
|
-
cx={toSvgX(activeDraftAnchorPoint[0])}
|
|
9597
|
-
cy={toSvgY(activeDraftAnchorPoint[1])}
|
|
9598
|
-
fill={palette.anchor}
|
|
9599
|
-
fillOpacity={0.95}
|
|
9600
|
-
r="0.14"
|
|
9601
|
-
vectorEffect="non-scaling-stroke"
|
|
9602
|
-
/>
|
|
9603
|
-
)}
|
|
10230
|
+
)}
|
|
10231
|
+
</g>
|
|
9604
10232
|
</svg>
|
|
9605
10233
|
)}
|
|
9606
10234
|
</div>
|