@pascal-app/editor 0.7.0 → 0.8.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 +6 -6
- package/src/components/editor/custom-camera-controls.tsx +2 -1
- package/src/components/editor/editor-layout-v2.tsx +4 -3
- package/src/components/editor/first-person/build-collider-world.ts +5 -7
- package/src/components/editor/first-person/bvh-ecctrl.tsx +119 -54
- package/src/components/editor/first-person-controls.tsx +11 -11
- package/src/components/editor/floating-action-menu.tsx +0 -0
- package/src/components/editor/floorplan-panel.tsx +44 -37
- package/src/components/editor/index.tsx +68 -53
- package/src/components/editor/selection-manager.tsx +2 -2
- package/src/components/editor/snapshot-capture-overlay.tsx +465 -0
- package/src/components/editor/thumbnail-generator.tsx +18 -61
- package/src/components/editor/use-floorplan-background-placement.ts +3 -3
- package/src/components/editor/wall-measurement-label.tsx +0 -0
- package/src/components/editor-2d/renderers/floorplan-draft-layer.tsx +6 -1
- package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +6 -1
- package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +5 -5
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +10 -12
- package/src/components/systems/roof/roof-edit-system.tsx +1 -1
- package/src/components/systems/stair/stair-edit-system.tsx +1 -1
- package/src/components/systems/zone/zone-label-editor-system.tsx +0 -0
- package/src/components/systems/zone/zone-system.tsx +0 -0
- package/src/components/tools/ceiling/move-ceiling-tool.tsx +9 -2
- package/src/components/tools/fence/curve-fence-tool.tsx +4 -5
- package/src/components/tools/fence/fence-tool.tsx +2 -2
- package/src/components/tools/fence/move-fence-endpoint-tool.tsx +11 -8
- package/src/components/tools/fence/move-fence-tool.tsx +13 -9
- package/src/components/tools/item/move-tool.tsx +3 -6
- package/src/components/tools/item/placement-math.ts +2 -4
- package/src/components/tools/item/placement-strategies.ts +11 -10
- package/src/components/tools/item/use-draft-node.ts +0 -1
- package/src/components/tools/item/use-placement-coordinator.tsx +9 -111
- package/src/components/tools/roof/move-roof-tool.tsx +7 -2
- package/src/components/tools/select/box-select-tool.tsx +12 -17
- package/src/components/tools/shared/segment-angle.ts +1 -1
- package/src/components/tools/tool-manager.tsx +12 -12
- package/src/components/tools/wall/curve-wall-tool.tsx +8 -6
- package/src/components/tools/wall/move-wall-endpoint-tool.tsx +11 -8
- package/src/components/tools/wall/move-wall-tool.tsx +6 -4
- package/src/components/tools/wall/wall-drafting.ts +0 -0
- package/src/components/tools/wall/wall-tool.tsx +3 -3
- package/src/components/tools/zone/zone-tool.tsx +20 -5
- package/src/components/ui/action-menu/camera-actions.tsx +0 -0
- package/src/components/ui/action-menu/control-modes.tsx +7 -1
- package/src/components/ui/action-menu/furnish-tools.tsx +6 -92
- package/src/components/ui/action-menu/index.tsx +35 -86
- package/src/components/ui/action-menu/view-toggles.tsx +19 -31
- package/src/components/ui/command-palette/editor-commands.tsx +6 -4
- package/src/components/ui/command-palette/index.tsx +4 -255
- package/src/components/ui/controls/material-picker.tsx +8 -5
- package/src/components/ui/floating-level-selector.tsx +1 -1
- package/src/components/ui/helpers/helper-manager.tsx +5 -0
- package/src/components/ui/item-catalog/catalog-items.tsx +1742 -315
- package/src/components/ui/item-catalog/item-catalog.tsx +88 -46
- package/src/components/ui/level-duplicate-dialog.tsx +3 -5
- package/src/components/ui/panels/ceiling-panel.tsx +2 -3
- package/src/components/ui/panels/column-panel.tsx +62 -18
- package/src/components/ui/panels/door-panel.tsx +272 -265
- package/src/components/ui/panels/fence-panel.tsx +0 -5
- package/src/components/ui/panels/paint-panel.tsx +66 -41
- package/src/components/ui/panels/panel-manager.tsx +3 -32
- package/src/components/ui/panels/reference-panel.tsx +28 -13
- package/src/components/ui/panels/roof-panel.tsx +52 -2
- package/src/components/ui/panels/roof-segment-panel.tsx +0 -0
- package/src/components/ui/panels/slab-panel.tsx +0 -0
- package/src/components/ui/panels/spawn-panel.tsx +10 -4
- package/src/components/ui/panels/stair-panel.tsx +66 -14
- package/src/components/ui/panels/wall-panel.tsx +97 -1
- package/src/components/ui/panels/window-panel.tsx +13 -5
- package/src/components/ui/primitives/number-input.tsx +1 -1
- package/src/components/ui/primitives/sidebar.tsx +0 -0
- package/src/components/ui/sidebar/app-sidebar.tsx +0 -0
- package/src/components/ui/sidebar/icon-rail.tsx +0 -0
- package/src/components/ui/sidebar/panels/items-panel/index.tsx +330 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +0 -0
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +0 -0
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +4 -6
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +0 -0
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -7
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +3 -1
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +3 -1
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +1 -1
- package/src/components/ui/slider.tsx +1 -1
- package/src/components/viewer-overlay.tsx +0 -0
- package/src/components/viewer-zone-system.tsx +0 -0
- package/src/hooks/use-auto-save.ts +14 -0
- package/src/hooks/use-keyboard.ts +10 -0
- package/src/index.tsx +8 -1
- package/src/lib/level-duplication.test.ts +0 -2
- package/src/lib/level-duplication.ts +1 -1
- package/src/lib/material-paint.ts +1 -1
- package/src/lib/roof-duplication.ts +1 -1
- package/src/lib/scene-bounds.ts +1 -1
- package/src/lib/scene.ts +0 -0
- package/src/lib/sfx-bus.ts +2 -0
- package/src/lib/sfx-player.ts +5 -5
- package/src/lib/stair-duplication.ts +2 -2
- package/src/store/use-editor.tsx +27 -59
- package/tsconfig.json +2 -1
- package/src/components/feedback-dialog.tsx +0 -265
- package/src/components/pascal-radio.tsx +0 -280
- package/src/components/preview-button.tsx +0 -16
- package/src/components/ui/viewer-toolbar.tsx +0 -436
|
@@ -192,7 +192,17 @@ function collectNodeIdsInBounds(bounds: Bounds): string[] {
|
|
|
192
192
|
|
|
193
193
|
const result: string[] = []
|
|
194
194
|
|
|
195
|
-
if (phase === 'structure' && structureLayer === '
|
|
195
|
+
if (phase === 'structure' && structureLayer === 'zones') {
|
|
196
|
+
for (const childId of levelNode.children) {
|
|
197
|
+
const node = nodes[childId as AnyNodeId]
|
|
198
|
+
if (!node || node.type !== 'zone') continue
|
|
199
|
+
const zone = node as ZoneNode
|
|
200
|
+
if (polygonIntersectsBounds(zone.polygon, bounds)) {
|
|
201
|
+
result.push(zone.id)
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
} else {
|
|
205
|
+
// structure (elements) and furnish: collect all node types
|
|
196
206
|
for (const childId of levelNode.children) {
|
|
197
207
|
const node = nodes[childId as AnyNodeId]
|
|
198
208
|
if (!node) continue
|
|
@@ -240,22 +250,7 @@ function collectNodeIdsInBounds(bounds: Bounds): string[] {
|
|
|
240
250
|
if (objectBoundsIntersectsBounds(node.id, bounds)) {
|
|
241
251
|
result.push(node.id)
|
|
242
252
|
}
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
} else if (phase === 'structure' && structureLayer === 'zones') {
|
|
246
|
-
for (const childId of levelNode.children) {
|
|
247
|
-
const node = nodes[childId as AnyNodeId]
|
|
248
|
-
if (!node || node.type !== 'zone') continue
|
|
249
|
-
const zone = node as ZoneNode
|
|
250
|
-
if (polygonIntersectsBounds(zone.polygon, bounds)) {
|
|
251
|
-
result.push(zone.id)
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
} else if (phase === 'furnish') {
|
|
255
|
-
for (const childId of levelNode.children) {
|
|
256
|
-
const node = nodes[childId as AnyNodeId]
|
|
257
|
-
if (!node) continue
|
|
258
|
-
if (node.type === 'item') {
|
|
253
|
+
} else if (node.type === 'item') {
|
|
259
254
|
const item = node as ItemNode
|
|
260
255
|
if (item.asset.category === 'door' || item.asset.category === 'window') continue
|
|
261
256
|
const xz = getNodeWorldXZ(item.id)
|
|
@@ -145,7 +145,7 @@ export function getSegmentAngleReferenceAtPoint(
|
|
|
145
145
|
}
|
|
146
146
|
|
|
147
147
|
const projected = getProjectedPointOnSegment(point, segment)
|
|
148
|
-
if (!projected
|
|
148
|
+
if (!(projected && pointsMatch(point, projected, SEGMENT_POINT_TOLERANCE))) {
|
|
149
149
|
return null
|
|
150
150
|
}
|
|
151
151
|
|
|
@@ -45,6 +45,7 @@ const tools: Record<Phase, Partial<Record<Tool, React.FC>>> = {
|
|
|
45
45
|
door: DoorTool,
|
|
46
46
|
item: ItemTool,
|
|
47
47
|
zone: ZoneTool,
|
|
48
|
+
spawn: SpawnTool,
|
|
48
49
|
window: WindowTool,
|
|
49
50
|
},
|
|
50
51
|
furnish: {
|
|
@@ -63,10 +64,9 @@ export const ToolManager: React.FC = () => {
|
|
|
63
64
|
const curvingFence = useEditor((state) => state.curvingFence)
|
|
64
65
|
const editingHole = useEditor((state) => state.editingHole)
|
|
65
66
|
const selectedZoneId = useViewer((state) => state.selection.zoneId)
|
|
66
|
-
const selectedLevelId = useViewer((state) => state.selection.levelId)
|
|
67
|
-
const buildingId = useViewer((state) => state.selection.buildingId)
|
|
68
67
|
const selectedIds = useViewer((state) => state.selection.selectedIds)
|
|
69
|
-
const
|
|
68
|
+
const buildingId = useViewer((state) => state.selection.buildingId)
|
|
69
|
+
const activeLevelId = useViewer((state) => state.selection.levelId)
|
|
70
70
|
const nodes = useScene((state) => state.nodes)
|
|
71
71
|
|
|
72
72
|
// Building transform for the local group — all building-relative tools live inside this group
|
|
@@ -128,13 +128,13 @@ export const ToolManager: React.FC = () => {
|
|
|
128
128
|
|
|
129
129
|
const BuildToolComponent = showBuildTool ? tools[phase]?.[tool] : null
|
|
130
130
|
const handlePlacedNodeSelected = (nodeId: AnyNodeId) => {
|
|
131
|
-
setSelection({ selectedIds: [nodeId] })
|
|
131
|
+
useViewer.getState().setSelection({ selectedIds: [nodeId] })
|
|
132
132
|
}
|
|
133
133
|
|
|
134
134
|
return (
|
|
135
135
|
<>
|
|
136
|
-
{showSiteBoundaryEditor && <SiteBoundaryEditor />}
|
|
137
136
|
{/* World-space tools: site boundary and building movement operate in world coordinates */}
|
|
137
|
+
{showSiteBoundaryEditor && <SiteBoundaryEditor />}
|
|
138
138
|
{movingNode?.type === 'building' && <MoveTool onSpawnMoved={handlePlacedNodeSelected} />}
|
|
139
139
|
|
|
140
140
|
{/* Building-local group: all other tools are relative to the selected building.
|
|
@@ -162,13 +162,13 @@ export const ToolManager: React.FC = () => {
|
|
|
162
162
|
{movingNode && movingNode.type !== 'building' && (
|
|
163
163
|
<MoveTool onSpawnMoved={handlePlacedNodeSelected} />
|
|
164
164
|
)}
|
|
165
|
-
{!movingNode &&
|
|
166
|
-
<SpawnTool currentLevelId={
|
|
167
|
-
)
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
165
|
+
{!movingNode && BuildToolComponent && tool === 'spawn' ? (
|
|
166
|
+
<SpawnTool currentLevelId={activeLevelId ?? null} onPlaced={handlePlacedNodeSelected} />
|
|
167
|
+
) : !movingNode && showBuildTool && tool === 'column' ? (
|
|
168
|
+
<ColumnTool currentLevelId={activeLevelId ?? null} onPlaced={handlePlacedNodeSelected} />
|
|
169
|
+
) : !movingNode && BuildToolComponent && tool !== 'column' ? (
|
|
170
|
+
<BuildToolComponent />
|
|
171
|
+
) : null}
|
|
172
172
|
</group>
|
|
173
173
|
</>
|
|
174
174
|
)
|
|
@@ -81,15 +81,17 @@ export const CurveWallTool: React.FC<{ node: WallNode }> = ({ node }) => {
|
|
|
81
81
|
? event.localPosition[2]
|
|
82
82
|
: snapScalarToGrid(event.localPosition[2], snapStep)
|
|
83
83
|
|
|
84
|
-
const offsetFromMidpoint =
|
|
85
|
-
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
)
|
|
84
|
+
const offsetFromMidpoint = -(
|
|
85
|
+
(localX - chord.midpoint.x) * chord.normal.x +
|
|
86
|
+
(localZ - chord.midpoint.y) * chord.normal.y
|
|
87
|
+
)
|
|
89
88
|
const snappedOffset = shiftPressedRef.current
|
|
90
89
|
? offsetFromMidpoint
|
|
91
90
|
: snapScalarToGrid(offsetFromMidpoint, snapStep)
|
|
92
|
-
const nextCurveOffset = normalizeWallCurveOffset(
|
|
91
|
+
const nextCurveOffset = normalizeWallCurveOffset(
|
|
92
|
+
node,
|
|
93
|
+
Math.max(-maxCurveOffset, Math.min(maxCurveOffset, snappedOffset)),
|
|
94
|
+
)
|
|
93
95
|
|
|
94
96
|
if (
|
|
95
97
|
previousCurveOffsetRef.current !== null &&
|
|
@@ -112,10 +112,12 @@ function getLinkedWallSnapshots(args: {
|
|
|
112
112
|
}
|
|
113
113
|
|
|
114
114
|
if (
|
|
115
|
-
!
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
115
|
+
!(
|
|
116
|
+
samePoint(node.start, originalStart) ||
|
|
117
|
+
samePoint(node.start, originalEnd) ||
|
|
118
|
+
samePoint(node.end, originalStart) ||
|
|
119
|
+
samePoint(node.end, originalEnd)
|
|
120
|
+
)
|
|
119
121
|
) {
|
|
120
122
|
continue
|
|
121
123
|
}
|
|
@@ -286,8 +288,9 @@ export const MoveWallEndpointTool: React.FC<{ target: MovingWallEndpoint }> = ({
|
|
|
286
288
|
}
|
|
287
289
|
|
|
288
290
|
const preview = previewRef.current ?? { start: originalStart, end: originalEnd }
|
|
289
|
-
const hasChanged =
|
|
290
|
-
|
|
291
|
+
const hasChanged = !(
|
|
292
|
+
samePoint(preview.start, originalStart) && samePoint(preview.end, originalEnd)
|
|
293
|
+
)
|
|
291
294
|
|
|
292
295
|
if (hasChanged && isWallLongEnough(preview.start, preview.end)) {
|
|
293
296
|
wasCommitted = true
|
|
@@ -391,7 +394,7 @@ export const MoveWallEndpointTool: React.FC<{ target: MovingWallEndpoint }> = ({
|
|
|
391
394
|
>
|
|
392
395
|
<div className="translate-y-10">
|
|
393
396
|
<div
|
|
394
|
-
className={`whitespace-nowrap rounded-full border px-2 py-1 text-[11px]
|
|
397
|
+
className={`whitespace-nowrap rounded-full border px-2 py-1 font-medium text-[11px] shadow-lg backdrop-blur-md transition-colors ${
|
|
395
398
|
altPressed
|
|
396
399
|
? 'border-amber-500/80 bg-amber-500/15 text-amber-100'
|
|
397
400
|
: 'border-border bg-background/95 text-muted-foreground'
|
|
@@ -415,7 +418,7 @@ function EndpointAngleLabel({
|
|
|
415
418
|
}) {
|
|
416
419
|
return (
|
|
417
420
|
<Html center position={position} style={{ pointerEvents: 'none' }} zIndexRange={[100, 0]}>
|
|
418
|
-
<div className="whitespace-nowrap rounded-full border border-border bg-background/95 px-2 py-1 font-mono text-[11px]
|
|
421
|
+
<div className="whitespace-nowrap rounded-full border border-border bg-background/95 px-2 py-1 font-mono font-semibold text-[11px] text-foreground shadow-lg backdrop-blur-md">
|
|
419
422
|
{label}
|
|
420
423
|
</div>
|
|
421
424
|
</Html>
|
|
@@ -63,10 +63,12 @@ function getLinkedWallSnapshots(args: {
|
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
if (
|
|
66
|
-
!
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
66
|
+
!(
|
|
67
|
+
samePoint(node.start, originalStart) ||
|
|
68
|
+
samePoint(node.start, originalEnd) ||
|
|
69
|
+
samePoint(node.end, originalStart) ||
|
|
70
|
+
samePoint(node.end, originalEnd)
|
|
71
|
+
)
|
|
70
72
|
) {
|
|
71
73
|
continue
|
|
72
74
|
}
|
|
File without changes
|
|
@@ -156,6 +156,8 @@ export const WallTool: React.FC = () => {
|
|
|
156
156
|
const unit = useViewer((state) => state.unit)
|
|
157
157
|
const cursorRef = useRef<Group>(null)
|
|
158
158
|
const wallPreviewRef = useRef<Mesh>(null!)
|
|
159
|
+
// All positions are building-local: this tool is inside the ToolManager building group,
|
|
160
|
+
// so local coords are used for both data and visual positioning.
|
|
159
161
|
const startingPoint = useRef(new Vector3(0, 0, 0))
|
|
160
162
|
const endingPoint = useRef(new Vector3(0, 0, 0))
|
|
161
163
|
const buildingState = useRef(0)
|
|
@@ -166,8 +168,6 @@ export const WallTool: React.FC = () => {
|
|
|
166
168
|
let gridPosition: WallPlanPoint = [0, 0]
|
|
167
169
|
let previousWallEnd: [number, number] | null = null
|
|
168
170
|
|
|
169
|
-
// All positions are building-local: this tool is inside the ToolManager building group,
|
|
170
|
-
// so local coords are used for both data and visual positioning.
|
|
171
171
|
const onGridMove = (event: GridEvent) => {
|
|
172
172
|
if (!(cursorRef.current && wallPreviewRef.current)) return
|
|
173
173
|
|
|
@@ -324,7 +324,7 @@ function DraftMeasurementLabel({
|
|
|
324
324
|
}) {
|
|
325
325
|
return (
|
|
326
326
|
<Html center position={position} style={{ pointerEvents: 'none' }} zIndexRange={[100, 0]}>
|
|
327
|
-
<div className="whitespace-nowrap rounded-full border border-border bg-background/95 px-2 py-1 font-mono text-[11px]
|
|
327
|
+
<div className="whitespace-nowrap rounded-full border border-border bg-background/95 px-2 py-1 font-mono font-semibold text-[11px] text-foreground shadow-lg backdrop-blur-md">
|
|
328
328
|
{label}
|
|
329
329
|
</div>
|
|
330
330
|
</Html>
|
|
@@ -3,6 +3,7 @@ import { useViewer } from '@pascal-app/viewer'
|
|
|
3
3
|
import { useEffect, useMemo, useRef, useState } from 'react'
|
|
4
4
|
import { BufferGeometry, DoubleSide, type Group, type Line, Shape, Vector3 } from 'three'
|
|
5
5
|
import { EDITOR_LAYER } from './../../../lib/constants'
|
|
6
|
+
import { sfxEmitter } from './../../../lib/sfx-bus'
|
|
6
7
|
import useEditor from './../../../store/use-editor'
|
|
7
8
|
import { CursorSphere } from '../shared/cursor-sphere'
|
|
8
9
|
|
|
@@ -67,6 +68,9 @@ const commitZoneDrawing = (levelId: LevelNode['id'], points: Array<[number, numb
|
|
|
67
68
|
|
|
68
69
|
// Select the newly created zone
|
|
69
70
|
useViewer.getState().setSelection({ zoneId: zone.id })
|
|
71
|
+
|
|
72
|
+
// Play structure build sound
|
|
73
|
+
sfxEmitter.emit('sfx:structure-build')
|
|
70
74
|
}
|
|
71
75
|
|
|
72
76
|
type PreviewState = {
|
|
@@ -86,6 +90,7 @@ export const ZoneTool: React.FC = () => {
|
|
|
86
90
|
const mainLineRef = useRef<Line>(null!)
|
|
87
91
|
const closingLineRef = useRef<Line>(null!)
|
|
88
92
|
const pointsRef = useRef<Array<[number, number]>>([])
|
|
93
|
+
const previousSnappedPointRef = useRef<[number, number] | null>(null)
|
|
89
94
|
const levelYRef = useRef(0) // Track current level Y position
|
|
90
95
|
const currentLevelId = useViewer((state) => state.selection.levelId)
|
|
91
96
|
const setTool = useEditor((state) => state.setTool)
|
|
@@ -181,12 +186,22 @@ export const ZoneTool: React.FC = () => {
|
|
|
181
186
|
|
|
182
187
|
// If we have points, snap to axis from last point
|
|
183
188
|
const lastPoint = pointsRef.current[pointsRef.current.length - 1]
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
+
const displayPoint = lastPoint
|
|
190
|
+
? calculateSnapPoint(lastPoint, cursorPosition)
|
|
191
|
+
: cursorPosition
|
|
192
|
+
|
|
193
|
+
// Play snap sound when the snapped position changes during drawing
|
|
194
|
+
if (
|
|
195
|
+
pointsRef.current.length > 0 &&
|
|
196
|
+
previousSnappedPointRef.current &&
|
|
197
|
+
(displayPoint[0] !== previousSnappedPointRef.current[0] ||
|
|
198
|
+
displayPoint[1] !== previousSnappedPointRef.current[1])
|
|
199
|
+
) {
|
|
200
|
+
sfxEmitter.emit('sfx:grid-snap')
|
|
189
201
|
}
|
|
202
|
+
previousSnappedPointRef.current = displayPoint
|
|
203
|
+
|
|
204
|
+
cursorRef.current.position.set(displayPoint[0], event.localPosition[1], displayPoint[1])
|
|
190
205
|
|
|
191
206
|
updatePreview()
|
|
192
207
|
}
|
|
File without changes
|
|
@@ -104,7 +104,9 @@ export function ControlModes() {
|
|
|
104
104
|
const setPhase = useEditor((state) => state.setPhase)
|
|
105
105
|
const setStructureLayer = useEditor((state) => state.setStructureLayer)
|
|
106
106
|
const setSelectionTool = useEditor((state) => state.setFloorplanSelectionTool)
|
|
107
|
-
const primeMaterialPaintFromSelection = useEditor(
|
|
107
|
+
const primeMaterialPaintFromSelection = useEditor(
|
|
108
|
+
(state) => state.primeMaterialPaintFromSelection,
|
|
109
|
+
)
|
|
108
110
|
const levelId = useViewer((s) => s.selection.levelId)
|
|
109
111
|
|
|
110
112
|
// Only subscribe to the primitive `level` number — when walls are added to
|
|
@@ -148,6 +150,8 @@ export function ControlModes() {
|
|
|
148
150
|
// setPhase('site') calls viewer.resetSelection() which clears levelId,
|
|
149
151
|
// breaking the 2D floorplan (it needs a level to render the SVG).
|
|
150
152
|
useEditor.setState({ phase: 'site', mode: 'select', tool: null, catalogCategory: null })
|
|
153
|
+
// Clear object selection so the polygon editor handles receive pointer events
|
|
154
|
+
useViewer.getState().setSelection({ selectedIds: [] })
|
|
151
155
|
}
|
|
152
156
|
return
|
|
153
157
|
}
|
|
@@ -188,6 +192,8 @@ export function ControlModes() {
|
|
|
188
192
|
} else {
|
|
189
193
|
setPhase('furnish')
|
|
190
194
|
setMode('build')
|
|
195
|
+
// Auto-switch sidebar to the items panel so the user can pick furniture
|
|
196
|
+
useEditor.getState().setActiveSidebarPanel('items')
|
|
191
197
|
}
|
|
192
198
|
} else if (id === 'zone') {
|
|
193
199
|
if (getIsActive('zone')) {
|
|
@@ -1,9 +1,4 @@
|
|
|
1
|
-
'use
|
|
2
|
-
|
|
3
|
-
import NextImage from 'next/image'
|
|
4
|
-
import { cn } from './../../../lib/utils'
|
|
5
|
-
import useEditor, { type CatalogCategory } from './../../../store/use-editor'
|
|
6
|
-
import { ActionButton } from './action-button'
|
|
1
|
+
import type { CatalogCategory } from './../../../store/use-editor'
|
|
7
2
|
|
|
8
3
|
export type FurnishToolConfig = {
|
|
9
4
|
id: 'item'
|
|
@@ -12,91 +7,10 @@ export type FurnishToolConfig = {
|
|
|
12
7
|
catalogCategory: CatalogCategory
|
|
13
8
|
}
|
|
14
9
|
|
|
15
|
-
// Furnish mode tools: furniture, appliances, decoration (painting is now a control mode)
|
|
16
10
|
export const furnishTools: FurnishToolConfig[] = [
|
|
17
|
-
{
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
},
|
|
23
|
-
{
|
|
24
|
-
id: 'item',
|
|
25
|
-
iconSrc: '/icons/appliance.png',
|
|
26
|
-
label: 'Appliance',
|
|
27
|
-
catalogCategory: 'appliance',
|
|
28
|
-
},
|
|
29
|
-
{
|
|
30
|
-
id: 'item',
|
|
31
|
-
iconSrc: '/icons/kitchen.png',
|
|
32
|
-
label: 'Kitchen',
|
|
33
|
-
catalogCategory: 'kitchen',
|
|
34
|
-
},
|
|
35
|
-
{
|
|
36
|
-
id: 'item',
|
|
37
|
-
iconSrc: '/icons/bathroom.png',
|
|
38
|
-
label: 'Bathroom',
|
|
39
|
-
catalogCategory: 'bathroom',
|
|
40
|
-
},
|
|
41
|
-
{
|
|
42
|
-
id: 'item',
|
|
43
|
-
iconSrc: '/icons/tree.png',
|
|
44
|
-
label: 'Outdoor',
|
|
45
|
-
catalogCategory: 'outdoor',
|
|
46
|
-
},
|
|
11
|
+
{ id: 'item', iconSrc: '/icons/couch.png', label: 'Furniture', catalogCategory: 'furniture' },
|
|
12
|
+
{ id: 'item', iconSrc: '/icons/appliance.png', label: 'Appliance', catalogCategory: 'appliance' },
|
|
13
|
+
{ id: 'item', iconSrc: '/icons/kitchen.png', label: 'Kitchen', catalogCategory: 'kitchen' },
|
|
14
|
+
{ id: 'item', iconSrc: '/icons/bathroom.png', label: 'Bathroom', catalogCategory: 'bathroom' },
|
|
15
|
+
{ id: 'item', iconSrc: '/icons/tree.png', label: 'Outdoor', catalogCategory: 'outdoor' },
|
|
47
16
|
]
|
|
48
|
-
|
|
49
|
-
export function FurnishTools() {
|
|
50
|
-
const mode = useEditor((state) => state.mode)
|
|
51
|
-
const activeTool = useEditor((state) => state.tool)
|
|
52
|
-
const setActiveTool = useEditor((state) => state.setTool)
|
|
53
|
-
const setMode = useEditor((state) => state.setMode)
|
|
54
|
-
const catalogCategory = useEditor((state) => state.catalogCategory)
|
|
55
|
-
const setCatalogCategory = useEditor((state) => state.setCatalogCategory)
|
|
56
|
-
|
|
57
|
-
const hasActiveTool = furnishTools.some(
|
|
58
|
-
(tool) => mode === 'build' && activeTool === 'item' && catalogCategory === tool.catalogCategory,
|
|
59
|
-
)
|
|
60
|
-
|
|
61
|
-
return (
|
|
62
|
-
<div className="flex items-center gap-1.5 px-1">
|
|
63
|
-
{furnishTools.map((tool, index) => {
|
|
64
|
-
// For item tools with catalog category, check both tool and category match
|
|
65
|
-
const isActive =
|
|
66
|
-
mode === 'build' && activeTool === 'item' && catalogCategory === tool.catalogCategory
|
|
67
|
-
|
|
68
|
-
return (
|
|
69
|
-
<ActionButton
|
|
70
|
-
className={cn(
|
|
71
|
-
'rounded-lg duration-300',
|
|
72
|
-
isActive
|
|
73
|
-
? 'z-10 scale-110 bg-black/40 hover:bg-black/40'
|
|
74
|
-
: 'scale-95 bg-transparent opacity-60 grayscale hover:bg-black/20 hover:opacity-100 hover:grayscale-0',
|
|
75
|
-
)}
|
|
76
|
-
key={`${tool.id}-${tool.catalogCategory ?? index}`}
|
|
77
|
-
label={tool.label}
|
|
78
|
-
onClick={() => {
|
|
79
|
-
if (!isActive) {
|
|
80
|
-
setCatalogCategory(tool.catalogCategory)
|
|
81
|
-
setActiveTool('item')
|
|
82
|
-
if (mode !== 'build') {
|
|
83
|
-
setMode('build')
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
}}
|
|
87
|
-
size="icon"
|
|
88
|
-
variant="ghost"
|
|
89
|
-
>
|
|
90
|
-
<NextImage
|
|
91
|
-
alt={tool.label}
|
|
92
|
-
className="size-full object-contain"
|
|
93
|
-
height={28}
|
|
94
|
-
src={tool.iconSrc}
|
|
95
|
-
width={28}
|
|
96
|
-
/>
|
|
97
|
-
</ActionButton>
|
|
98
|
-
)
|
|
99
|
-
})}
|
|
100
|
-
</div>
|
|
101
|
-
)
|
|
102
|
-
}
|
|
@@ -1,22 +1,25 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import { useScene } from '@pascal-app/core'
|
|
4
|
+
import { useViewer } from '@pascal-app/viewer'
|
|
4
5
|
import { AnimatePresence, motion } from 'motion/react'
|
|
5
6
|
import { useEffect, useMemo } from 'react'
|
|
6
|
-
import { useViewer } from '@pascal-app/viewer'
|
|
7
|
-
import { useReducedMotion } from './../../../hooks/use-reduced-motion'
|
|
8
|
-
import { useIsMobile } from './../../../hooks/use-mobile'
|
|
9
|
-
import { TooltipProvider } from './../../../components/ui/primitives/tooltip'
|
|
10
7
|
import { MaterialPicker } from './../../../components/ui/controls/material-picker'
|
|
8
|
+
import { TooltipProvider } from './../../../components/ui/primitives/tooltip'
|
|
9
|
+
import { useIsMobile } from './../../../hooks/use-mobile'
|
|
10
|
+
import { useReducedMotion } from './../../../hooks/use-reduced-motion'
|
|
11
11
|
import { resolvePaintTargetFromSelection } from './../../../lib/material-paint'
|
|
12
12
|
import { cn } from './../../../lib/utils'
|
|
13
13
|
import useEditor from './../../../store/use-editor'
|
|
14
|
-
import { ItemCatalog } from '../item-catalog/item-catalog'
|
|
15
14
|
import { CameraActions } from './camera-actions'
|
|
16
15
|
import { ControlModes } from './control-modes'
|
|
17
|
-
import { FurnishTools } from './furnish-tools'
|
|
18
16
|
import { StructureTools } from './structure-tools'
|
|
19
|
-
import {
|
|
17
|
+
import { GridSnapControl, SecondaryToggles } from './view-toggles'
|
|
18
|
+
|
|
19
|
+
// Mobile bottom offset matches the viewer's overlap behind the sheet's
|
|
20
|
+
// rounded corners (SHEET_OVERLAP_PX in editor-layout-mobile) so the menu sits
|
|
21
|
+
// just above that strip instead of inside it.
|
|
22
|
+
const MOBILE_BOTTOM_OFFSET = 24
|
|
20
23
|
|
|
21
24
|
function PaintMaterialTray() {
|
|
22
25
|
const activePaintMaterial = useEditor((state) => state.activePaintMaterial)
|
|
@@ -84,84 +87,16 @@ export function ActionMenu({ className }: { className?: string }) {
|
|
|
84
87
|
<TooltipProvider>
|
|
85
88
|
<motion.div
|
|
86
89
|
className={cn(
|
|
87
|
-
'
|
|
90
|
+
'left-1/2 z-50 -translate-x-1/2',
|
|
91
|
+
isMobile ? 'absolute origin-bottom scale-90' : 'fixed bottom-6',
|
|
88
92
|
'rounded-2xl border border-border bg-background/90 shadow-2xl backdrop-blur-md',
|
|
89
93
|
'transition-colors duration-200 ease-out',
|
|
90
94
|
className,
|
|
91
95
|
)}
|
|
92
96
|
layout
|
|
97
|
+
style={isMobile ? { bottom: MOBILE_BOTTOM_OFFSET } : undefined}
|
|
93
98
|
transition={transition}
|
|
94
99
|
>
|
|
95
|
-
{/* Item Catalog Row - Only show when in build mode with item tool */}
|
|
96
|
-
<AnimatePresence>
|
|
97
|
-
{mode === 'build' && tool === 'item' && catalogCategory && (
|
|
98
|
-
<motion.div
|
|
99
|
-
animate={{
|
|
100
|
-
opacity: 1,
|
|
101
|
-
maxHeight: 160,
|
|
102
|
-
paddingTop: 8,
|
|
103
|
-
paddingBottom: 8,
|
|
104
|
-
borderBottomWidth: 1,
|
|
105
|
-
}}
|
|
106
|
-
className={cn('overflow-hidden border-border border-b px-2 py-2')}
|
|
107
|
-
exit={{
|
|
108
|
-
opacity: 0,
|
|
109
|
-
maxHeight: 0,
|
|
110
|
-
paddingTop: 0,
|
|
111
|
-
paddingBottom: 0,
|
|
112
|
-
borderBottomWidth: 0,
|
|
113
|
-
}}
|
|
114
|
-
initial={{
|
|
115
|
-
opacity: 0,
|
|
116
|
-
maxHeight: 0,
|
|
117
|
-
paddingTop: 0,
|
|
118
|
-
paddingBottom: 0,
|
|
119
|
-
borderBottomWidth: 0,
|
|
120
|
-
}}
|
|
121
|
-
transition={transition}
|
|
122
|
-
>
|
|
123
|
-
<ItemCatalog category={catalogCategory} key={catalogCategory} />
|
|
124
|
-
</motion.div>
|
|
125
|
-
)}
|
|
126
|
-
</AnimatePresence>
|
|
127
|
-
|
|
128
|
-
<AnimatePresence>
|
|
129
|
-
{phase === 'furnish' && mode === 'build' && (
|
|
130
|
-
<motion.div
|
|
131
|
-
animate={{
|
|
132
|
-
opacity: 1,
|
|
133
|
-
maxHeight: 80,
|
|
134
|
-
paddingTop: 8,
|
|
135
|
-
paddingBottom: 8,
|
|
136
|
-
borderBottomWidth: 1,
|
|
137
|
-
}}
|
|
138
|
-
className={cn(
|
|
139
|
-
'overflow-hidden border-border',
|
|
140
|
-
'max-h-20 border-b px-2 py-2 opacity-100',
|
|
141
|
-
)}
|
|
142
|
-
exit={{
|
|
143
|
-
opacity: 0,
|
|
144
|
-
maxHeight: 0,
|
|
145
|
-
paddingTop: 0,
|
|
146
|
-
paddingBottom: 0,
|
|
147
|
-
borderBottomWidth: 0,
|
|
148
|
-
}}
|
|
149
|
-
initial={{
|
|
150
|
-
opacity: 0,
|
|
151
|
-
maxHeight: 0,
|
|
152
|
-
paddingTop: 0,
|
|
153
|
-
paddingBottom: 0,
|
|
154
|
-
borderBottomWidth: 0,
|
|
155
|
-
}}
|
|
156
|
-
transition={transition}
|
|
157
|
-
>
|
|
158
|
-
<div className="mx-auto w-max">
|
|
159
|
-
<FurnishTools />
|
|
160
|
-
</div>
|
|
161
|
-
</motion.div>
|
|
162
|
-
)}
|
|
163
|
-
</AnimatePresence>
|
|
164
|
-
|
|
165
100
|
{/* Structure Tools Row - Animated */}
|
|
166
101
|
<AnimatePresence>
|
|
167
102
|
{phase === 'structure' && mode === 'build' && (
|
|
@@ -228,14 +163,28 @@ export function ActionMenu({ className }: { className?: string }) {
|
|
|
228
163
|
</motion.div>
|
|
229
164
|
)}
|
|
230
165
|
</AnimatePresence>
|
|
231
|
-
{
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
166
|
+
{isMobile ? (
|
|
167
|
+
<div className="flex flex-col items-stretch gap-0.5 px-2 py-1.5">
|
|
168
|
+
{/* Row 1: control modes only */}
|
|
169
|
+
<div className="flex items-center justify-center gap-1">
|
|
170
|
+
<ControlModes />
|
|
171
|
+
</div>
|
|
172
|
+
{/* Row 2: grid snap + secondary toggles (orbit + top view hidden) */}
|
|
173
|
+
<div className="flex items-center justify-center gap-1 border-border/50 border-t pt-1">
|
|
174
|
+
<GridSnapControl />
|
|
175
|
+
<SecondaryToggles />
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
178
|
+
) : (
|
|
179
|
+
<div className="flex items-center justify-center gap-1 px-2 py-1.5">
|
|
180
|
+
<ControlModes />
|
|
181
|
+
<div className="mx-1 h-5 w-px bg-border" />
|
|
182
|
+
<GridSnapControl />
|
|
183
|
+
<SecondaryToggles />
|
|
184
|
+
<div className="mx-1 h-5 w-px bg-border" />
|
|
185
|
+
<CameraActions />
|
|
186
|
+
</div>
|
|
187
|
+
)}
|
|
239
188
|
</motion.div>
|
|
240
189
|
</TooltipProvider>
|
|
241
190
|
)
|