@pascal-app/editor 0.9.2 → 1.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +9 -7
- package/src/components/editor/bake-exporter.tsx +3 -1
- package/src/components/editor/bake-thumbnail.tsx +95 -0
- package/src/components/editor/camera-dragging-lifecycle.test.ts +49 -0
- package/src/components/editor/camera-dragging-lifecycle.ts +41 -0
- package/src/components/editor/custom-camera-controls.tsx +307 -212
- package/src/components/editor/delete-confirmation-dialog.tsx +54 -0
- package/src/components/editor/elevation-3d-guide-layer.tsx +120 -0
- package/src/components/editor/first-person/build-collider-world.test.ts +66 -2
- package/src/components/editor/first-person/build-collider-world.ts +20 -0
- package/src/components/editor/first-person/terrain-collider.test.ts +132 -0
- package/src/components/editor/first-person/terrain-collider.ts +212 -0
- package/src/components/editor/first-person-controls.tsx +295 -132
- package/src/components/editor/floating-action-menu.tsx +59 -65
- package/src/components/editor/floorplan-background-selection.test.ts +16 -2
- package/src/components/editor/floorplan-background-selection.ts +4 -18
- package/src/components/editor/floorplan-camera-sync.test.ts +249 -0
- package/src/components/editor/floorplan-camera-sync.ts +213 -0
- package/src/components/editor/floorplan-mode-coordinator.tsx +82 -0
- package/src/components/editor/floorplan-navigation-presentation.test.ts +142 -0
- package/src/components/editor/floorplan-navigation-presentation.ts +156 -0
- package/src/components/editor/floorplan-panel.tsx +931 -861
- package/src/components/editor/group-actions.ts +155 -15
- package/src/components/editor/group-floating-action-menu.tsx +1 -1
- package/src/components/editor/group-rotate-handle.tsx +1 -1
- package/src/components/editor/group-selection-box-3d.tsx +1 -1
- package/src/components/editor/handles/handle-drag-history.test.ts +96 -0
- package/src/components/editor/handles/handle-drag-history.ts +14 -0
- package/src/components/editor/handles/preview-overrides.test.ts +26 -0
- package/src/components/editor/handles/preview-overrides.ts +13 -0
- package/src/components/editor/handles/resize-snap.test.ts +52 -0
- package/src/components/editor/handles/resize-snap.ts +23 -0
- package/src/components/editor/handles/use-handle-drag.ts +17 -7
- package/src/components/editor/index.tsx +42 -9
- package/src/components/editor/measurement-pill.tsx +6 -1
- package/src/components/editor/node-arrow-handles.tsx +51 -15
- package/src/components/editor/quick-measurement-card.tsx +9 -3
- package/src/components/editor/quick-measurement-hud.tsx +7 -1
- package/src/components/editor/selection-manager.tsx +3 -1
- package/src/components/editor/site-edge-labels.tsx +24 -2
- package/src/components/editor/snapshot-capture-overlay.tsx +4 -1
- package/src/components/editor/thumbnail-generator.tsx +66 -294
- package/src/components/editor/use-floorplan-hit-testing.test.ts +82 -0
- package/src/components/editor/use-floorplan-hit-testing.ts +144 -161
- package/src/components/editor/use-mesh-settle-epoch.ts +1 -0
- package/src/components/editor/wall-measurement-label.tsx +8 -16
- package/src/components/editor/wall-move-side-handles.tsx +193 -18
- package/src/components/editor/wall-opening-highlights.tsx +63 -32
- package/src/components/editor/wall-snap-beacon-layer.tsx +6 -4
- package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +2 -1
- package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +39 -8
- package/src/components/editor-2d/floorplan-cursor-indicator-position.test.ts +31 -0
- package/src/components/editor-2d/floorplan-cursor-indicator-position.ts +39 -0
- package/src/components/editor-2d/floorplan-group-move.tsx +47 -4
- package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +21 -5
- package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +72 -0
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +93 -59
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +27 -0
- package/src/components/editor-2d/floorplan-render-context.test.ts +27 -0
- package/src/components/editor-2d/floorplan-render-context.tsx +113 -8
- package/src/components/editor-2d/renderers/floorplan-annotation-layout.test.ts +285 -0
- package/src/components/editor-2d/renderers/floorplan-annotation-layout.ts +516 -0
- package/src/components/editor-2d/renderers/floorplan-dimension-renderer.test.tsx +236 -0
- package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +618 -0
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.test.tsx +369 -0
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +431 -32
- package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +75 -1
- package/src/components/editor-2d/renderers/floorplan-label-angle.ts +92 -0
- package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +93 -28
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +279 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +621 -281
- package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +20 -10
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +12 -7
- package/src/components/tools/fence/fence-drafting.ts +27 -0
- package/src/components/tools/item/use-draft-node.ts +131 -72
- package/src/components/tools/item/use-placement-coordinator.tsx +52 -4
- package/src/components/tools/registry/move-registry-node-tool.tsx +75 -4
- package/src/components/tools/roof/roof-tool.tsx +16 -0
- package/src/components/tools/shared/floor-stack-preview.ts +4 -0
- package/src/components/tools/shared/horizontal-construction-plane.ts +123 -0
- package/src/components/tools/shared/pointer-support-cap.test.ts +98 -0
- package/src/components/tools/shared/pointer-support-cap.ts +273 -0
- package/src/components/tools/site/site-boundary-editor.tsx +13 -6
- package/src/components/tools/site/stroke-pointer-ownership.test.ts +91 -0
- package/src/components/tools/site/stroke-pointer-ownership.ts +57 -0
- package/src/components/tools/site/terrain-brush-cursor.tsx +247 -0
- package/src/components/tools/site/terrain-sculpt-grid.tsx +192 -0
- package/src/components/tools/site/terrain-sculpt-tool.tsx +356 -0
- package/src/components/tools/stair/stair-click-guard.test.ts +58 -0
- package/src/components/tools/stair/stair-click-guard.ts +72 -0
- package/src/components/tools/stair/stair-tool.tsx +98 -47
- package/src/components/tools/tool-manager.tsx +67 -9
- package/src/components/tools/wall/wall-drafting.test.ts +118 -1
- package/src/components/tools/wall/wall-drafting.ts +128 -9
- package/src/components/tools/wall/wall-snap-geometry.test.ts +8 -0
- package/src/components/tools/wall/wall-snap-geometry.ts +70 -3
- package/src/components/tools/zone/zone-tool.tsx +16 -0
- package/src/components/ui/action-menu/control-modes.tsx +4 -1
- package/src/components/ui/action-menu/measurement-control.tsx +107 -28
- package/src/components/ui/command-palette/editor-commands.tsx +14 -1
- package/src/components/ui/controls/material-paint-panel.tsx +7 -1
- package/src/components/ui/controls/material-picker.tsx +86 -7
- package/src/components/ui/controls/terrain-sculpt-panel.tsx +193 -0
- package/src/components/ui/floating-level-selector.tsx +73 -19
- package/src/components/ui/helpers/helper-manager.tsx +46 -0
- package/src/components/ui/panels/multi-selection-panel.tsx +57 -0
- package/src/components/ui/panels/panel-manager.tsx +14 -1
- package/src/components/ui/panels/parametric-inspector.tsx +4 -2
- package/src/components/ui/panels/selection-breakdown.test.ts +24 -0
- package/src/components/ui/panels/selection-breakdown.ts +20 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +14 -4
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +16 -0
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +2 -1
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +2 -0
- package/src/components/viewer/viewer-controls-bar.tsx +466 -0
- package/src/components/viewer/viewer-scene-header.tsx +235 -0
- package/src/components/viewer-overlay.tsx +20 -655
- package/src/components/walkthrough-hud.tsx +111 -0
- package/src/hooks/use-grid-events.ts +24 -0
- package/src/hooks/use-keyboard.ts +53 -30
- package/src/index.tsx +137 -4
- package/src/lib/active-placement-surface.test.ts +24 -0
- package/src/lib/active-placement-surface.ts +8 -1
- package/src/lib/camera-pose.test.ts +237 -0
- package/src/lib/camera-pose.ts +172 -0
- package/src/lib/direct-manipulation.test.ts +83 -0
- package/src/lib/direct-manipulation.ts +9 -0
- package/src/lib/door-interaction.ts +1 -1
- package/src/lib/elevation-guides.test.ts +107 -0
- package/src/lib/elevation-guides.ts +236 -0
- package/src/lib/floorplan/annotation-visibility.test.ts +327 -0
- package/src/lib/floorplan/annotation-visibility.ts +133 -0
- package/src/lib/floorplan/drawing-coordination.test.ts +46 -0
- package/src/lib/floorplan/drawing-coordination.ts +13 -0
- package/src/lib/floorplan/floorplan-export.test.ts +319 -0
- package/src/lib/floorplan/floorplan-export.tsx +745 -131
- package/src/lib/floorplan/floorplan-extension.test.ts +43 -0
- package/src/lib/floorplan/floorplan-extension.ts +181 -0
- package/src/lib/floorplan/floorplan-mode.test.ts +120 -0
- package/src/lib/floorplan/floorplan-mode.ts +73 -0
- package/src/lib/floorplan/floorplan-pdfkit-document.ts +194 -0
- package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +107 -0
- package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +680 -0
- package/src/lib/floorplan/geometry.ts +2 -3
- package/src/lib/fresh-planar-placement.test.ts +44 -1
- package/src/lib/fresh-planar-placement.ts +21 -4
- package/src/lib/glb-export.test.ts +116 -1
- package/src/lib/glb-export.ts +204 -7
- package/src/lib/ground-surface.ts +95 -0
- package/src/lib/history.test.ts +96 -0
- package/src/lib/history.ts +22 -0
- package/src/lib/interaction/overlay-policy.test.ts +2 -1
- package/src/lib/interaction/scope.ts +40 -8
- package/src/lib/level-duplication.test.ts +2 -1
- package/src/lib/measurements.test.ts +5 -0
- package/src/lib/measurements.ts +12 -1
- package/src/lib/quick-action-feedback.ts +33 -0
- package/src/lib/quick-action-nodes.test.ts +94 -0
- package/src/lib/quick-action-nodes.ts +47 -0
- package/src/lib/scene-clipboard.test.ts +130 -1
- package/src/lib/scene-clipboard.ts +237 -13
- package/src/lib/selection-routing.test.ts +16 -1
- package/src/lib/selection-routing.ts +5 -0
- package/src/lib/sfx-bus.ts +33 -2
- package/src/lib/sfx-player.test.ts +148 -6
- package/src/lib/sfx-player.ts +156 -8
- package/src/lib/site-boundary.test.ts +53 -0
- package/src/lib/site-boundary.ts +27 -0
- package/src/lib/snapping-mode.test.ts +29 -2
- package/src/lib/snapping-mode.ts +15 -3
- package/src/lib/stair-duplication.test.ts +103 -0
- package/src/lib/stair-duplication.ts +57 -85
- package/src/lib/stair-levels.ts +2 -0
- package/src/lib/terrain-sculpt.test.ts +429 -0
- package/src/lib/terrain-sculpt.ts +301 -0
- package/src/lib/terrain-verb-color.test.ts +51 -0
- package/src/lib/terrain-verb-color.ts +58 -0
- package/src/lib/touch-gesture-priority.test.ts +45 -0
- package/src/lib/touch-gesture-priority.ts +30 -0
- package/src/lib/window-interaction.ts +1 -1
- package/src/store/camera-pose-store.test.ts +29 -0
- package/src/store/camera-pose-store.ts +27 -0
- package/src/store/live-draft-preview-stores.test.ts +136 -0
- package/src/store/navigation-sync-pose-store.test.ts +34 -0
- package/src/store/navigation-sync-pose-store.ts +27 -0
- package/src/store/terrain-sculpt-mode.test.ts +223 -0
- package/src/store/use-delete-confirmation.ts +27 -0
- package/src/store/use-drawing-view.test.ts +23 -0
- package/src/store/use-drawing-view.ts +84 -0
- package/src/store/use-editor.tsx +184 -15
- package/src/store/use-elevation-guides.ts +40 -0
- package/src/store/use-fence-curve-draft.ts +38 -8
- package/src/store/use-first-person-hud.ts +40 -0
- package/src/store/use-floorplan-annotation-visibility.ts +60 -0
- package/src/store/use-floorplan-draft-preview.ts +54 -4
- package/src/store/use-floorplan-mode.ts +110 -0
- package/src/store/use-interaction-scope.test.ts +23 -3
- package/src/store/use-interaction-scope.ts +8 -0
- package/src/store/use-measurement-draft.test.ts +29 -0
- package/src/store/use-measurement-draft.ts +33 -6
- package/src/store/use-path-draft-preview.ts +90 -0
- package/src/store/use-stair-build-preview.ts +12 -2
- package/src/lib/floorplan/selection-tool.ts +0 -271
|
@@ -2,11 +2,16 @@ import {
|
|
|
2
2
|
type AnyNode,
|
|
3
3
|
type AnyNodeId,
|
|
4
4
|
DEFAULT_ANGLE_STEP,
|
|
5
|
+
DEFAULT_LEVEL_HEIGHT,
|
|
5
6
|
type DoorNode,
|
|
7
|
+
GROUND_SUPPORT_ID,
|
|
6
8
|
getScaledDimensions,
|
|
7
9
|
type ItemNode,
|
|
10
|
+
resolveWallSupportSlabPatch,
|
|
8
11
|
runAsSingleSceneHistoryStep,
|
|
9
12
|
snapPointAlongAngleRay,
|
|
13
|
+
spatialGridManager,
|
|
14
|
+
terrainSupportLift,
|
|
10
15
|
useScene,
|
|
11
16
|
type WallNode,
|
|
12
17
|
WallNode as WallSchema,
|
|
@@ -26,6 +31,7 @@ import {
|
|
|
26
31
|
type WallDraftSnapResult,
|
|
27
32
|
type WallPlanPoint,
|
|
28
33
|
type WallSnapRadii,
|
|
34
|
+
wallIdsAtSnapPoint,
|
|
29
35
|
} from './wall-snap-geometry'
|
|
30
36
|
|
|
31
37
|
// The pure snap geometry lives in `./wall-snap-geometry`; re-exported here so
|
|
@@ -72,7 +78,11 @@ export function snapPointToGrid(point: WallPlanPoint, step = WALL_GRID_STEP): Wa
|
|
|
72
78
|
return [snapScalarToGrid(point[0], step), snapScalarToGrid(point[1], step)]
|
|
73
79
|
}
|
|
74
80
|
|
|
75
|
-
function splitWallAtPoint(
|
|
81
|
+
function splitWallAtPoint(
|
|
82
|
+
wall: WallNode,
|
|
83
|
+
splitPoint: WallPlanPoint,
|
|
84
|
+
nodes: ReturnType<typeof useScene.getState>['nodes'],
|
|
85
|
+
): [WallNode, WallNode] {
|
|
76
86
|
const { id: _id, parentId: _parentId, children, ...rest } = wall
|
|
77
87
|
|
|
78
88
|
const first = WallSchema.parse({
|
|
@@ -88,7 +98,24 @@ function splitWallAtPoint(wall: WallNode, splitPoint: WallPlanPoint): [WallNode,
|
|
|
88
98
|
children: [],
|
|
89
99
|
})
|
|
90
100
|
|
|
91
|
-
|
|
101
|
+
if (wall.supportSlabId !== GROUND_SUPPORT_ID || !wall.parentId) {
|
|
102
|
+
return [first, second]
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const levelId = wall.parentId
|
|
106
|
+
const originalElevation =
|
|
107
|
+
(terrainSupportLift(nodes, levelId, wall.start[0], wall.start[1]) ?? 0) +
|
|
108
|
+
(wall.supportOffset ?? 0)
|
|
109
|
+
const rebase = (segment: WallNode): WallNode => {
|
|
110
|
+
const terrainElevation =
|
|
111
|
+
terrainSupportLift(nodes, levelId, segment.start[0], segment.start[1]) ?? 0
|
|
112
|
+
const supportOffset = originalElevation - terrainElevation
|
|
113
|
+
return {
|
|
114
|
+
...segment,
|
|
115
|
+
supportOffset: Math.abs(supportOffset) > 1e-6 ? supportOffset : undefined,
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return [rebase(first), rebase(second)]
|
|
92
119
|
}
|
|
93
120
|
|
|
94
121
|
function pointsEqual(a: WallPlanPoint, b: WallPlanPoint, tolerance = 1e-6): boolean {
|
|
@@ -287,7 +314,7 @@ function splitWallIfNeeded(
|
|
|
287
314
|
return { walls, point: intersection.point }
|
|
288
315
|
}
|
|
289
316
|
|
|
290
|
-
const [first, second] = splitWallAtPoint(wallToSplit, intersection.point)
|
|
317
|
+
const [first, second] = splitWallAtPoint(wallToSplit, intersection.point, nodes)
|
|
291
318
|
const attachmentUpdates = buildAttachmentMigrationPlan(
|
|
292
319
|
wallToSplit,
|
|
293
320
|
intersection.point,
|
|
@@ -393,7 +420,7 @@ export function snapWallDraftPointDetailed(args: SnapWallDraftArgs): WallDraftSn
|
|
|
393
420
|
snapRadii,
|
|
394
421
|
} = args
|
|
395
422
|
|
|
396
|
-
if (bypassSnap) return { point, snap: null }
|
|
423
|
+
if (bypassSnap) return { point, snap: null, targetWallIds: [] }
|
|
397
424
|
|
|
398
425
|
// Discrete special points (corner / midpoint / crossing) are taken from the
|
|
399
426
|
// raw cursor so an interim grid snap can't mask them. A corner always wins,
|
|
@@ -419,8 +446,14 @@ export function snapWallDraftPointDetailed(args: SnapWallDraftArgs): WallDraftSn
|
|
|
419
446
|
ignoreWallIds,
|
|
420
447
|
radius: snapRadii?.wall,
|
|
421
448
|
})
|
|
422
|
-
if (wallSnap)
|
|
423
|
-
|
|
449
|
+
if (wallSnap) {
|
|
450
|
+
return {
|
|
451
|
+
point: wallSnap,
|
|
452
|
+
snap: 'wall',
|
|
453
|
+
targetWallIds: wallIdsAtSnapPoint(wallSnap, walls, ignoreWallIds),
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
return { point: basePoint, snap: null, targetWallIds: [] }
|
|
424
457
|
}
|
|
425
458
|
|
|
426
459
|
// Non-magnetic modes (grid / off / angles): connectivity still sticks so a
|
|
@@ -440,9 +473,15 @@ export function snapWallDraftPointDetailed(args: SnapWallDraftArgs): WallDraftSn
|
|
|
440
473
|
ignoreWallIds,
|
|
441
474
|
radius: WALL_CONNECT_SNAP_RADIUS,
|
|
442
475
|
})
|
|
443
|
-
if (connectWall)
|
|
476
|
+
if (connectWall) {
|
|
477
|
+
return {
|
|
478
|
+
point: connectWall,
|
|
479
|
+
snap: 'wall',
|
|
480
|
+
targetWallIds: wallIdsAtSnapPoint(connectWall, walls, ignoreWallIds),
|
|
481
|
+
}
|
|
482
|
+
}
|
|
444
483
|
|
|
445
|
-
return { point: basePoint, snap: null }
|
|
484
|
+
return { point: basePoint, snap: null, targetWallIds: [] }
|
|
446
485
|
}
|
|
447
486
|
|
|
448
487
|
export function snapWallDraftPoint(args: SnapWallDraftArgs): WallPlanPoint {
|
|
@@ -453,9 +492,45 @@ export function isSegmentLongEnough(start: WallPlanPoint, end: WallPlanPoint): b
|
|
|
453
492
|
return distanceSquared(start, end) >= WALL_MIN_LENGTH * WALL_MIN_LENGTH
|
|
454
493
|
}
|
|
455
494
|
|
|
495
|
+
export type WallConstructionOptions = {
|
|
496
|
+
/** Pointer-decided maximum support elevation in level-local metres. */
|
|
497
|
+
supportCap?: number | null
|
|
498
|
+
/** Support source selected by the first click or inherited from a snapped wall. */
|
|
499
|
+
preferredSupportSlabId?: string | null
|
|
500
|
+
/** Frozen level-local Y shown by the draft ghost. */
|
|
501
|
+
constructionElevation?: number | null
|
|
502
|
+
/** Height shown by the draft ghost. */
|
|
503
|
+
constructionHeight?: number | null
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
export function resolveTerrainWallConstructionOptions(
|
|
507
|
+
nodes: Record<string, AnyNode>,
|
|
508
|
+
levelId: string,
|
|
509
|
+
point: WallPlanPoint,
|
|
510
|
+
defaults?: Record<string, unknown>,
|
|
511
|
+
): WallConstructionOptions | undefined {
|
|
512
|
+
const constructionElevation = terrainSupportLift(nodes, levelId, point[0], point[1])
|
|
513
|
+
if (constructionElevation == null) return undefined
|
|
514
|
+
|
|
515
|
+
const level = nodes[levelId]
|
|
516
|
+
const constructionHeight =
|
|
517
|
+
typeof defaults?.height === 'number'
|
|
518
|
+
? defaults.height
|
|
519
|
+
: level?.type === 'level'
|
|
520
|
+
? (level.height ?? DEFAULT_LEVEL_HEIGHT)
|
|
521
|
+
: DEFAULT_LEVEL_HEIGHT
|
|
522
|
+
|
|
523
|
+
return {
|
|
524
|
+
constructionElevation,
|
|
525
|
+
constructionHeight,
|
|
526
|
+
supportCap: constructionElevation,
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
456
530
|
export function createWallOnCurrentLevel(
|
|
457
531
|
start: WallPlanPoint,
|
|
458
532
|
end: WallPlanPoint,
|
|
533
|
+
options?: WallConstructionOptions,
|
|
459
534
|
): WallNode | null {
|
|
460
535
|
const currentLevelId = useViewer.getState().selection.levelId
|
|
461
536
|
const { createNode, createNodes, deleteNode, nodes } = useScene.getState()
|
|
@@ -541,8 +616,52 @@ export function createWallOnCurrentLevel(
|
|
|
541
616
|
})
|
|
542
617
|
|
|
543
618
|
createNode(wall, currentLevelId)
|
|
619
|
+
const createdWall = useScene.getState().nodes[wall.id]
|
|
620
|
+
if (createdWall?.type === 'wall') {
|
|
621
|
+
const terrainBase = terrainSupportLift(
|
|
622
|
+
useScene.getState().nodes,
|
|
623
|
+
currentLevelId,
|
|
624
|
+
createdWall.start[0],
|
|
625
|
+
createdWall.start[1],
|
|
626
|
+
)
|
|
627
|
+
const preferredSupportSlabId =
|
|
628
|
+
options?.preferredSupportSlabId ??
|
|
629
|
+
(options?.constructionElevation != null && terrainBase != null ? GROUND_SUPPORT_ID : null)
|
|
630
|
+
const supportPatch = resolveWallSupportSlabPatch(createdWall, useScene.getState().nodes, {
|
|
631
|
+
maxElevation: options?.supportCap ?? null,
|
|
632
|
+
preferredSlabId: preferredSupportSlabId,
|
|
633
|
+
})
|
|
634
|
+
const supportSlabId = supportPatch.supportSlabId
|
|
635
|
+
const sourceSupport = spatialGridManager.getSlabSupportForWall(
|
|
636
|
+
currentLevelId,
|
|
637
|
+
createdWall.start,
|
|
638
|
+
createdWall.end,
|
|
639
|
+
createdWall.curveOffset,
|
|
640
|
+
createdWall.thickness,
|
|
641
|
+
supportSlabId,
|
|
642
|
+
options?.supportCap ?? null,
|
|
643
|
+
)
|
|
644
|
+
const supportOffset =
|
|
645
|
+
options?.constructionElevation == null
|
|
646
|
+
? undefined
|
|
647
|
+
: options.constructionElevation - sourceSupport.elevation
|
|
648
|
+
const preserveDraftHeight =
|
|
649
|
+
createdWall.height == null &&
|
|
650
|
+
options?.constructionHeight != null &&
|
|
651
|
+
options.constructionElevation != null &&
|
|
652
|
+
(terrainBase != null || Math.abs(options.constructionElevation) > 1e-6)
|
|
653
|
+
useScene.getState().updateNode(createdWall.id, {
|
|
654
|
+
...supportPatch,
|
|
655
|
+
height: preserveDraftHeight
|
|
656
|
+
? (options?.constructionHeight ?? createdWall.height)
|
|
657
|
+
: createdWall.height,
|
|
658
|
+
supportOffset:
|
|
659
|
+
supportOffset != null && Math.abs(supportOffset) > 1e-6 ? supportOffset : undefined,
|
|
660
|
+
})
|
|
661
|
+
}
|
|
544
662
|
sfxEmitter.emit('sfx:structure-build')
|
|
545
663
|
|
|
546
|
-
|
|
664
|
+
const committedWall = useScene.getState().nodes[wall.id]
|
|
665
|
+
return committedWall?.type === 'wall' ? committedWall : wall
|
|
547
666
|
})
|
|
548
667
|
}
|
|
@@ -48,6 +48,14 @@ describe('findWallSpecialPointSnap', () => {
|
|
|
48
48
|
expect(result?.snap).toBe('intersection')
|
|
49
49
|
expect(result?.point[0]).toBeCloseTo(1, 6)
|
|
50
50
|
expect(result?.point[1]).toBeCloseTo(0, 6)
|
|
51
|
+
expect(result?.targetWallIds).toEqual(['a', 'b'])
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
test('keeps every wall that shares a snapped endpoint as provenance', () => {
|
|
55
|
+
const walls = [makeWall([0, 0], [4, 0], 'a'), makeWall([0, 0], [0, 4], 'b')]
|
|
56
|
+
const result = findWallSpecialPointSnap([0.1, 0.1], walls)
|
|
57
|
+
expect(result?.point).toEqual([0, 0])
|
|
58
|
+
expect(result?.targetWallIds).toEqual(['a', 'b'])
|
|
51
59
|
})
|
|
52
60
|
|
|
53
61
|
test('corner wins over a midpoint when both are in range', () => {
|
|
@@ -25,6 +25,12 @@ export type WallDraftSnapResult = {
|
|
|
25
25
|
* the "magnetic" snap the beacon visualises; `null` for grid/angle-only.
|
|
26
26
|
*/
|
|
27
27
|
snap: WallDraftSnapKind | null
|
|
28
|
+
/**
|
|
29
|
+
* Walls whose geometry produced the snap. Kept separate from `snap` so a
|
|
30
|
+
* caller can use geometry for XZ alignment while independently deciding
|
|
31
|
+
* whether the target is allowed to transfer its construction plane.
|
|
32
|
+
*/
|
|
33
|
+
targetWallIds: string[]
|
|
28
34
|
}
|
|
29
35
|
|
|
30
36
|
export const WALL_JOIN_SNAP_RADIUS = 0.35
|
|
@@ -118,6 +124,53 @@ export function findWallSnapTarget(
|
|
|
118
124
|
return bestTarget
|
|
119
125
|
}
|
|
120
126
|
|
|
127
|
+
/**
|
|
128
|
+
* Wall ids that contain an already-resolved snap point.
|
|
129
|
+
*
|
|
130
|
+
* This is provenance, not another snap pass: the tolerance only absorbs float
|
|
131
|
+
* drift around a point the snap pipeline already chose.
|
|
132
|
+
*/
|
|
133
|
+
export function wallIdsAtSnapPoint(
|
|
134
|
+
point: WallPlanPoint,
|
|
135
|
+
walls: WallNode[],
|
|
136
|
+
ignoreWallIds?: string[],
|
|
137
|
+
tolerance = 1e-6,
|
|
138
|
+
): string[] {
|
|
139
|
+
const ignored = new Set(ignoreWallIds ?? [])
|
|
140
|
+
const toleranceSquared = tolerance * tolerance
|
|
141
|
+
const ids: string[] = []
|
|
142
|
+
|
|
143
|
+
for (const wall of walls) {
|
|
144
|
+
if (ignored.has(wall.id)) continue
|
|
145
|
+
if (
|
|
146
|
+
distanceSquared(point, wall.start) <= toleranceSquared ||
|
|
147
|
+
distanceSquared(point, wall.end) <= toleranceSquared
|
|
148
|
+
) {
|
|
149
|
+
ids.push(wall.id)
|
|
150
|
+
continue
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (isCurvedWall(wall)) {
|
|
154
|
+
const sampleCount = Math.max(8, Math.ceil(getWallCurveLength(wall) / 0.3))
|
|
155
|
+
for (let index = 1; index < sampleCount; index += 1) {
|
|
156
|
+
const frame = getWallCurveFrameAt(wall, index / sampleCount)
|
|
157
|
+
if (distanceSquared(point, [frame.point.x, frame.point.y]) <= toleranceSquared) {
|
|
158
|
+
ids.push(wall.id)
|
|
159
|
+
break
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
continue
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const projected = projectPointOntoWall(point, wall)
|
|
166
|
+
if (projected && distanceSquared(point, projected) <= toleranceSquared) {
|
|
167
|
+
ids.push(wall.id)
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return ids
|
|
172
|
+
}
|
|
173
|
+
|
|
121
174
|
/**
|
|
122
175
|
* Endpoint-only snap from the *raw* cursor (no grid pre-snap), with a
|
|
123
176
|
* generous radius. Use this before `findWallSnapTarget` so the strong
|
|
@@ -320,12 +373,26 @@ export function findWallSpecialPointSnap(
|
|
|
320
373
|
radii?: WallSnapRadii,
|
|
321
374
|
): WallDraftSnapResult | null {
|
|
322
375
|
const endpoint = findWallEndpointFromRaw(point, walls, ignoreWallIds, radii?.endpoint)
|
|
323
|
-
if (endpoint)
|
|
376
|
+
if (endpoint) {
|
|
377
|
+
return {
|
|
378
|
+
point: endpoint,
|
|
379
|
+
snap: 'endpoint',
|
|
380
|
+
targetWallIds: wallIdsAtSnapPoint(endpoint, walls, ignoreWallIds),
|
|
381
|
+
}
|
|
382
|
+
}
|
|
324
383
|
|
|
325
384
|
const midpoint = findWallMidpointFromRaw(point, walls, ignoreWallIds, radii?.midpoint)
|
|
326
385
|
const intersection = findWallIntersectionFromRaw(point, walls, ignoreWallIds, radii?.intersection)
|
|
327
386
|
return nearestCandidate(point, [
|
|
328
|
-
midpoint && {
|
|
329
|
-
|
|
387
|
+
midpoint && {
|
|
388
|
+
point: midpoint,
|
|
389
|
+
snap: 'midpoint',
|
|
390
|
+
targetWallIds: wallIdsAtSnapPoint(midpoint, walls, ignoreWallIds),
|
|
391
|
+
},
|
|
392
|
+
intersection && {
|
|
393
|
+
point: intersection,
|
|
394
|
+
snap: 'intersection',
|
|
395
|
+
targetWallIds: wallIdsAtSnapPoint(intersection, walls, ignoreWallIds),
|
|
396
|
+
},
|
|
330
397
|
])
|
|
331
398
|
}
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
} from './../../../lib/surface-plan-snap'
|
|
19
19
|
import { snapWorldXZForActiveBuilding } from './../../../lib/world-grid-snap'
|
|
20
20
|
import useEditor, { isAngleSnapActive, isGridSnapActive } from './../../../store/use-editor'
|
|
21
|
+
import { useFloorplanDraftPreview } from './../../../store/use-floorplan-draft-preview'
|
|
21
22
|
import { CursorSphere } from '../shared/cursor-sphere'
|
|
22
23
|
|
|
23
24
|
const Y_OFFSET = 0.02
|
|
@@ -79,6 +80,20 @@ export const ZoneTool: React.FC = () => {
|
|
|
79
80
|
levelY: 0,
|
|
80
81
|
})
|
|
81
82
|
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
useFloorplanDraftPreview.getState().setPolygonDraft('zone', preview.points)
|
|
85
|
+
}, [preview.points])
|
|
86
|
+
useEffect(
|
|
87
|
+
() => () => {
|
|
88
|
+
const draftPreview = useFloorplanDraftPreview.getState()
|
|
89
|
+
if (draftPreview.polygonDraftType === 'zone') {
|
|
90
|
+
draftPreview.setPolygonDraft(null, [])
|
|
91
|
+
}
|
|
92
|
+
draftPreview.setCursorPoint(null)
|
|
93
|
+
},
|
|
94
|
+
[],
|
|
95
|
+
)
|
|
96
|
+
|
|
82
97
|
useEffect(() => {
|
|
83
98
|
if (!currentLevelId) return
|
|
84
99
|
|
|
@@ -192,6 +207,7 @@ export const ZoneTool: React.FC = () => {
|
|
|
192
207
|
event.localPosition[2],
|
|
193
208
|
])
|
|
194
209
|
snappedCursorPosition = displayPoint
|
|
210
|
+
useFloorplanDraftPreview.getState().setCursorPoint(displayPoint)
|
|
195
211
|
|
|
196
212
|
// Play snap sound when the snapped position changes during drawing — only
|
|
197
213
|
// when a quantizing mode is active (off / lines move continuously).
|
|
@@ -72,7 +72,10 @@ export function ControlModes() {
|
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
const handleClick = (id: ControlId) => {
|
|
75
|
-
// Exit site editing first if needed
|
|
75
|
+
// Exit site editing first if needed. Sculpting is a site-phase mode, so
|
|
76
|
+
// leaving the phase is exactly the right way to leave the brush — but the
|
|
77
|
+
// order matters: `setPhase` resets the mode, and setting the mode first
|
|
78
|
+
// would have it overwritten below.
|
|
76
79
|
if (isSiteEditing) {
|
|
77
80
|
setPhase('structure')
|
|
78
81
|
setStructureLayer('elements')
|
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import type {
|
|
4
|
+
ConstructionDimensionChainMode,
|
|
5
|
+
ConstructionDimensionMode,
|
|
6
|
+
} from '@pascal-app/core'
|
|
4
7
|
import {
|
|
5
8
|
Box,
|
|
6
9
|
Check,
|
|
7
10
|
ChevronDown,
|
|
8
|
-
|
|
9
|
-
|
|
11
|
+
CircleIcon,
|
|
12
|
+
Crosshair,
|
|
13
|
+
Grid2X2,
|
|
14
|
+
Minus,
|
|
10
15
|
Ruler,
|
|
11
16
|
ScanSearch,
|
|
12
17
|
Square,
|
|
@@ -17,6 +22,7 @@ import { useState } from 'react'
|
|
|
17
22
|
import type { CreatableMeasurementKind } from '../../../lib/measurement-kind'
|
|
18
23
|
import { cn } from '../../../lib/utils'
|
|
19
24
|
import useEditor from '../../../store/use-editor'
|
|
25
|
+
import useFloorplanMode from '../../../store/use-floorplan-mode'
|
|
20
26
|
import { Popover, PopoverContent, PopoverTrigger } from '../primitives/popover'
|
|
21
27
|
import { ActionButton } from './action-button'
|
|
22
28
|
|
|
@@ -37,27 +43,65 @@ const measurementMenuOptions = [
|
|
|
37
43
|
...measurementOptions,
|
|
38
44
|
] as const
|
|
39
45
|
|
|
46
|
+
const constructionDimensionOptions = [
|
|
47
|
+
{ mode: 'linear', chainMode: 'point-to-point', label: 'Linear dimension', icon: Ruler },
|
|
48
|
+
{ mode: 'linear', chainMode: 'continuous', label: 'Continuous dimension', icon: Waypoints },
|
|
49
|
+
{ mode: 'radius', chainMode: 'point-to-point', label: 'Radius dimension', icon: CircleIcon },
|
|
50
|
+
{ mode: 'diameter', chainMode: 'point-to-point', label: 'Diameter dimension', icon: CircleIcon },
|
|
51
|
+
{ mode: 'center-mark', chainMode: 'point-to-point', label: 'Center mark', icon: Crosshair },
|
|
52
|
+
{ mode: 'chord', chainMode: 'point-to-point', label: 'Chord dimension', icon: Minus },
|
|
53
|
+
{ mode: 'arc-length', chainMode: 'point-to-point', label: 'Arc length', icon: CircleIcon },
|
|
54
|
+
{ mode: 'angular', chainMode: 'point-to-point', label: 'Angular dimension', icon: Triangle },
|
|
55
|
+
{ mode: 'coordinate', chainMode: 'continuous', label: 'Coordinate dimensions', icon: Grid2X2 },
|
|
56
|
+
] as const satisfies readonly {
|
|
57
|
+
mode: ConstructionDimensionMode
|
|
58
|
+
chainMode: ConstructionDimensionChainMode
|
|
59
|
+
label: string
|
|
60
|
+
icon: typeof Ruler
|
|
61
|
+
}[]
|
|
62
|
+
|
|
40
63
|
export function MeasurementControl() {
|
|
41
64
|
const [isOpen, setIsOpen] = useState(false)
|
|
42
65
|
const mode = useEditor((state) => state.mode)
|
|
43
66
|
const tool = useEditor((state) => state.tool)
|
|
67
|
+
const floorplanMode = useFloorplanMode((state) => state.mode)
|
|
44
68
|
const selectedKind = useEditor((state) => state.lastMeasurementKind)
|
|
45
69
|
const activeToolKind = useEditor((state) => state.toolDefaults.measurement?.kind)
|
|
70
|
+
const constructionDimensionChainMode = useEditor(
|
|
71
|
+
(state) => state.toolDefaults['construction-dimension']?.chainMode,
|
|
72
|
+
)
|
|
73
|
+
const constructionDimensionMode = useEditor(
|
|
74
|
+
(state) => state.toolDefaults['construction-dimension']?.mode,
|
|
75
|
+
)
|
|
46
76
|
const setMode = useEditor((state) => state.setMode)
|
|
47
77
|
const setPhase = useEditor((state) => state.setPhase)
|
|
48
78
|
const setLastMeasurementKind = useEditor((state) => state.setLastMeasurementKind)
|
|
49
79
|
const setStructureLayer = useEditor((state) => state.setStructureLayer)
|
|
50
80
|
const setTool = useEditor((state) => state.setTool)
|
|
51
81
|
const setToolDefaults = useEditor((state) => state.setToolDefaults)
|
|
52
|
-
const
|
|
53
|
-
const setShowMeasurements = useViewer((state) => state.setShowMeasurements)
|
|
82
|
+
const setViewMode = useEditor((state) => state.setViewMode)
|
|
54
83
|
|
|
55
84
|
const selectedOption =
|
|
56
85
|
measurementOptions.find((option) => option.kind === selectedKind) ?? measurementOptions[0]
|
|
57
86
|
const isActive = mode === 'build' && tool === 'measurement'
|
|
87
|
+
const isConstructionDimensionActive = mode === 'build' && tool === 'construction-dimension'
|
|
88
|
+
const activeConstructionDimensionOption = constructionDimensionOptions.find(
|
|
89
|
+
(option) =>
|
|
90
|
+
option.mode === (constructionDimensionMode ?? 'linear') &&
|
|
91
|
+
option.chainMode === (constructionDimensionChainMode ?? 'point-to-point'),
|
|
92
|
+
)
|
|
93
|
+
const isControlActive = isActive || isConstructionDimensionActive
|
|
58
94
|
const isSmartActive = isActive && activeToolKind === 'smart'
|
|
59
|
-
const SelectedIcon =
|
|
60
|
-
|
|
95
|
+
const SelectedIcon = isConstructionDimensionActive
|
|
96
|
+
? (activeConstructionDimensionOption?.icon ?? Ruler)
|
|
97
|
+
: isSmartActive
|
|
98
|
+
? ScanSearch
|
|
99
|
+
: selectedOption.icon
|
|
100
|
+
const selectedLabel = isConstructionDimensionActive
|
|
101
|
+
? (activeConstructionDimensionOption?.label ?? 'Linear dimension')
|
|
102
|
+
: isSmartActive
|
|
103
|
+
? 'Smart'
|
|
104
|
+
: selectedOption.label
|
|
61
105
|
|
|
62
106
|
const activateMeasurement = (kind: CreatableMeasurementKind) => {
|
|
63
107
|
setPhase('structure')
|
|
@@ -69,7 +113,7 @@ export function MeasurementControl() {
|
|
|
69
113
|
}
|
|
70
114
|
|
|
71
115
|
const handlePrimaryClick = () => {
|
|
72
|
-
if (
|
|
116
|
+
if (isControlActive) {
|
|
73
117
|
setMode('select')
|
|
74
118
|
return
|
|
75
119
|
}
|
|
@@ -84,15 +128,31 @@ export function MeasurementControl() {
|
|
|
84
128
|
setTool('measurement')
|
|
85
129
|
}
|
|
86
130
|
|
|
131
|
+
const activateConstructionDimension = (
|
|
132
|
+
dimensionMode: ConstructionDimensionMode,
|
|
133
|
+
chainMode: ConstructionDimensionChainMode,
|
|
134
|
+
) => {
|
|
135
|
+
if (useFloorplanMode.getState().mode !== 'expert') {
|
|
136
|
+
useFloorplanMode.getState().showExpertModeNotice('Construction Dimension')
|
|
137
|
+
return
|
|
138
|
+
}
|
|
139
|
+
setPhase('structure')
|
|
140
|
+
setStructureLayer('elements')
|
|
141
|
+
setViewMode('2d')
|
|
142
|
+
setToolDefaults('construction-dimension', { chainMode, mode: dimensionMode })
|
|
143
|
+
setMode('build')
|
|
144
|
+
setTool('construction-dimension')
|
|
145
|
+
}
|
|
146
|
+
|
|
87
147
|
return (
|
|
88
148
|
<Popover onOpenChange={setIsOpen} open={isOpen}>
|
|
89
149
|
<div className="flex items-center">
|
|
90
150
|
<ActionButton
|
|
91
151
|
aria-label={`Measure: ${selectedLabel}`}
|
|
92
|
-
aria-pressed={
|
|
152
|
+
aria-pressed={isControlActive}
|
|
93
153
|
className={cn(
|
|
94
154
|
'rounded-r-none p-0 text-muted-foreground',
|
|
95
|
-
|
|
155
|
+
isControlActive
|
|
96
156
|
? 'bg-cyan-500/20 text-cyan-400 hover:bg-cyan-500/20'
|
|
97
157
|
: 'hover:bg-cyan-500/15 hover:text-cyan-400',
|
|
98
158
|
)}
|
|
@@ -128,7 +188,7 @@ export function MeasurementControl() {
|
|
|
128
188
|
|
|
129
189
|
<PopoverContent
|
|
130
190
|
align="center"
|
|
131
|
-
className="w-
|
|
191
|
+
className="max-h-[70vh] w-64 overflow-y-auto rounded-lg border-border/45 bg-background/96 p-2 shadow-elevation-3 backdrop-blur-xl"
|
|
132
192
|
side="top"
|
|
133
193
|
sideOffset={14}
|
|
134
194
|
>
|
|
@@ -138,7 +198,7 @@ export function MeasurementControl() {
|
|
|
138
198
|
const isSmart = option.kind === 'smart'
|
|
139
199
|
const isSelected = isSmart
|
|
140
200
|
? isSmartActive
|
|
141
|
-
: !isSmartActive && option.kind === selectedKind
|
|
201
|
+
: !isConstructionDimensionActive && !isSmartActive && option.kind === selectedKind
|
|
142
202
|
return (
|
|
143
203
|
<button
|
|
144
204
|
aria-checked={isSelected}
|
|
@@ -164,23 +224,42 @@ export function MeasurementControl() {
|
|
|
164
224
|
)
|
|
165
225
|
})}
|
|
166
226
|
|
|
167
|
-
|
|
227
|
+
{floorplanMode === 'expert' ? (
|
|
228
|
+
<>
|
|
229
|
+
<div className="my-1.5 h-px bg-border/60" />
|
|
230
|
+
<div className="px-2.5 pt-1 pb-0.5 font-semibold text-[10px] text-muted-foreground uppercase tracking-wider">
|
|
231
|
+
Floor plan
|
|
232
|
+
</div>
|
|
168
233
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
234
|
+
{constructionDimensionOptions.map((option) => {
|
|
235
|
+
const OptionIcon = option.icon
|
|
236
|
+
const isSelected =
|
|
237
|
+
isConstructionDimensionActive && activeConstructionDimensionOption === option
|
|
238
|
+
return (
|
|
239
|
+
<button
|
|
240
|
+
aria-checked={isSelected}
|
|
241
|
+
className={cn(
|
|
242
|
+
'flex h-9 w-full items-center gap-2 rounded-md px-2.5 text-left text-sm transition-colors',
|
|
243
|
+
isSelected
|
|
244
|
+
? 'bg-white/10 text-foreground'
|
|
245
|
+
: 'text-muted-foreground hover:bg-white/8 hover:text-foreground',
|
|
246
|
+
)}
|
|
247
|
+
key={`${option.mode}-${option.chainMode}`}
|
|
248
|
+
onClick={() => {
|
|
249
|
+
activateConstructionDimension(option.mode, option.chainMode)
|
|
250
|
+
setIsOpen(false)
|
|
251
|
+
}}
|
|
252
|
+
role="menuitemradio"
|
|
253
|
+
type="button"
|
|
254
|
+
>
|
|
255
|
+
<OptionIcon aria-hidden="true" className="h-4 w-4" />
|
|
256
|
+
<span>{option.label}</span>
|
|
257
|
+
{isSelected ? <Check aria-hidden="true" className="ml-auto h-4 w-4" /> : null}
|
|
258
|
+
</button>
|
|
259
|
+
)
|
|
260
|
+
})}
|
|
261
|
+
</>
|
|
262
|
+
) : null}
|
|
184
263
|
</div>
|
|
185
264
|
</PopoverContent>
|
|
186
265
|
</Popover>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import type { AnyNodeId } from '@pascal-app/core'
|
|
4
|
-
import { LevelNode, useScene } from '@pascal-app/core'
|
|
4
|
+
import { DEFAULT_LEVEL_HEIGHT, LevelNode, useScene } from '@pascal-app/core'
|
|
5
5
|
import { useViewer } from '@pascal-app/viewer'
|
|
6
6
|
import {
|
|
7
7
|
AppWindow,
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
Minimize2,
|
|
23
23
|
Moon,
|
|
24
24
|
MousePointer2,
|
|
25
|
+
Mountain,
|
|
25
26
|
Package,
|
|
26
27
|
PaintBucket,
|
|
27
28
|
PencilLine,
|
|
@@ -168,6 +169,17 @@ export function EditorCommands() {
|
|
|
168
169
|
setMode('material-paint')
|
|
169
170
|
}),
|
|
170
171
|
},
|
|
172
|
+
{
|
|
173
|
+
id: 'editor.mode.terrain-sculpt',
|
|
174
|
+
label: 'Sculpt Terrain',
|
|
175
|
+
group: 'Scene',
|
|
176
|
+
icon: <Mountain className="h-4 w-4" />,
|
|
177
|
+
keywords: ['terrain', 'ground', 'elevation', 'sculpt', 'hill', 'slope', 'grade', 'dig'],
|
|
178
|
+
shortcut: ['G'],
|
|
179
|
+
// No `setPhase`: `setMode` moves to the site phase itself, and doing it
|
|
180
|
+
// here would set the phase twice with a mode reset in between.
|
|
181
|
+
execute: () => run(() => setMode('terrain-sculpt')),
|
|
182
|
+
},
|
|
171
183
|
|
|
172
184
|
// ── Levels ───────────────────────────────────────────────────────────
|
|
173
185
|
{
|
|
@@ -196,6 +208,7 @@ export function EditorCommands() {
|
|
|
196
208
|
).length
|
|
197
209
|
const newLevel = LevelNode.parse({
|
|
198
210
|
level: levelCount,
|
|
211
|
+
height: DEFAULT_LEVEL_HEIGHT,
|
|
199
212
|
children: [],
|
|
200
213
|
parentId: building.id,
|
|
201
214
|
})
|
|
@@ -27,7 +27,12 @@ import { SceneMaterialList } from './scene-material-list'
|
|
|
27
27
|
* fixed control/category header, a single scrolling catalog grid, and a fixed
|
|
28
28
|
* scene-material footer (always visible, with a `+` to add a custom material).
|
|
29
29
|
*/
|
|
30
|
-
export
|
|
30
|
+
export type MaterialPaintPanelProps = {
|
|
31
|
+
/** When provided, the catalog grid leads with a "New material" tile that invokes it. */
|
|
32
|
+
onCreateMaterialRequest?: () => void
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function MaterialPaintPanel({ onCreateMaterialRequest }: MaterialPaintPanelProps) {
|
|
31
36
|
const activePaintMaterial = useEditor((state) => state.activePaintMaterial)
|
|
32
37
|
const activePaintTarget = useEditor((state) => state.activePaintTarget)
|
|
33
38
|
const setActivePaintMaterial = useEditor((state) => state.setActivePaintMaterial)
|
|
@@ -109,6 +114,7 @@ export function MaterialPaintPanel() {
|
|
|
109
114
|
{/* Scrolls: category tabs (fixed inside) + catalog grid (the scroll). */}
|
|
110
115
|
<div className="min-h-0 flex-1">
|
|
111
116
|
<MaterialPicker
|
|
117
|
+
onCreateMaterialRequest={onCreateMaterialRequest}
|
|
112
118
|
onSelectMaterialPreset={(materialPreset) => {
|
|
113
119
|
setActivePaintMaterial({ materialPreset, sourceTarget: activePaintTarget })
|
|
114
120
|
}}
|