@pascal-app/editor 0.7.0 → 0.9.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 +12 -13
- package/src/components/editor/alignment-3d-guide-layer.tsx +160 -0
- package/src/components/editor/custom-camera-controls.tsx +755 -25
- package/src/components/editor/editor-layout-mobile.tsx +3 -3
- package/src/components/editor/editor-layout-v2.tsx +62 -54
- package/src/components/editor/export-manager.tsx +32 -3
- package/src/components/editor/first-person/build-collider-world.test.ts +149 -0
- package/src/components/editor/first-person/build-collider-world.ts +203 -87
- package/src/components/editor/first-person/bvh-ecctrl.tsx +127 -62
- package/src/components/editor/first-person-controls.tsx +836 -31
- package/src/components/editor/floating-action-menu.tsx +287 -196
- package/src/components/editor/floating-building-action-menu.tsx +1 -1
- package/src/components/editor/floorplan-panel.tsx +5837 -12006
- package/src/components/editor/grid.tsx +51 -17
- package/src/components/editor/group-move-handle.tsx +316 -0
- package/src/components/editor/group-rotate-handle.tsx +382 -0
- package/src/components/editor/group-transform-shared.test.ts +205 -0
- package/src/components/editor/group-transform-shared.ts +256 -0
- package/src/components/editor/handles/handle-arrow.tsx +523 -0
- package/src/components/editor/handles/use-handle-drag.ts +213 -0
- package/src/components/editor/index.tsx +234 -190
- package/src/components/editor/measurement-pill.tsx +75 -0
- package/src/components/editor/node-arrow-handles.tsx +1453 -0
- package/src/components/editor/selection-manager.tsx +435 -158
- package/src/components/editor/site-edge-labels.tsx +44 -20
- package/src/components/editor/slab-hole-highlights.tsx +466 -0
- package/src/components/editor/snapshot-capture-overlay.tsx +539 -0
- package/src/components/editor/thumbnail-generator.tsx +27 -64
- package/src/components/editor/use-floorplan-background-placement.ts +122 -35
- package/src/components/editor/use-floorplan-hit-testing.ts +24 -0
- package/src/components/editor/use-floorplan-scene-data.ts +14 -2
- package/src/components/editor/wall-measurement-label.tsx +11 -6
- package/src/components/editor/wall-move-side-handles.tsx +855 -0
- package/src/components/editor/wall-opening-highlights.tsx +181 -0
- package/src/components/editor/wall-snap-beacon-layer.tsx +150 -0
- package/src/components/editor-2d/floorplan-action-menu-layer.tsx +6 -1
- package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +142 -0
- package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +5 -3
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +227 -0
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +616 -0
- package/src/components/editor-2d/floorplan-render-context.tsx +62 -0
- package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +75 -0
- package/src/components/editor-2d/floorplan-wall-move-ghost-layer.tsx +47 -0
- package/src/components/editor-2d/renderers/floorplan-draft-layer.tsx +6 -1
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +266 -0
- package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +108 -61
- package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +59 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +1963 -0
- package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +29 -14
- package/src/components/editor-2d/svg-paths.ts +10 -2
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +30 -9
- package/src/components/systems/ceiling/ceiling-system.tsx +12 -8
- package/src/components/systems/roof/roof-edit-system.tsx +86 -12
- package/src/components/systems/stair/stair-edit-system.tsx +2 -2
- 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/elevator/elevator-defaults.ts +8 -0
- package/src/components/tools/elevator/elevator-tool.tsx +349 -0
- package/src/components/tools/elevator/move-elevator-tool.tsx +252 -0
- package/src/components/tools/fence/fence-drafting.ts +64 -22
- package/src/components/tools/item/move-tool.tsx +40 -107
- package/src/components/tools/item/placement-math.test.ts +10 -0
- package/src/components/tools/item/placement-math.ts +33 -9
- package/src/components/tools/item/placement-strategies.ts +201 -25
- package/src/components/tools/item/placement-types.ts +10 -1
- package/src/components/tools/item/use-draft-node.ts +20 -2
- package/src/components/tools/item/use-placement-coordinator.tsx +700 -299
- package/src/components/tools/registry/move-registry-node-tool.tsx +602 -0
- package/src/components/tools/roof/roof-tool.tsx +108 -12
- package/src/components/tools/select/box-select-state.ts +77 -0
- package/src/components/tools/select/box-select-tool.tsx +324 -543
- package/src/components/tools/select/plane-box-select-tool.tsx +577 -0
- package/src/components/tools/select/screen-rectangle-selection.ts +84 -0
- package/src/components/tools/select/select-candidates.test.ts +109 -0
- package/src/components/tools/select/select-candidates.ts +85 -0
- package/src/components/tools/shared/affordance-dispatch.ts +30 -0
- package/src/components/tools/shared/drag-bounding-box.tsx +171 -0
- package/src/components/tools/shared/floor-stack-preview.ts +25 -0
- package/src/components/tools/shared/fresh-placement-visibility.ts +61 -0
- package/src/components/tools/shared/placement-box-geometry.ts +123 -0
- package/src/components/tools/shared/placement-box.tsx +123 -0
- package/src/components/tools/shared/polygon-editor.tsx +617 -167
- package/src/components/tools/shared/segment-angle.ts +57 -1
- package/src/components/tools/site/site-boundary-editor.tsx +464 -12
- package/src/components/tools/site/site-flag-model.ts +18 -0
- package/src/components/tools/stair/stair-defaults.ts +1 -1
- package/src/components/tools/stair/stair-tool.tsx +269 -35
- package/src/components/tools/tool-manager.tsx +172 -56
- package/src/components/tools/wall/wall-drafting.ts +98 -98
- package/src/components/tools/wall/wall-snap-geometry.test.ts +79 -0
- package/src/components/tools/wall/wall-snap-geometry.ts +270 -0
- package/src/components/tools/zone/zone-tool.tsx +36 -11
- package/src/components/ui/action-menu/action-button.tsx +21 -1
- package/src/components/ui/action-menu/camera-actions.tsx +0 -0
- package/src/components/ui/action-menu/control-modes.tsx +5 -76
- package/src/components/ui/action-menu/furnish-tools.tsx +6 -92
- package/src/components/ui/action-menu/index.tsx +33 -196
- package/src/components/ui/action-menu/structure-tools.tsx +13 -88
- package/src/components/ui/action-menu/view-toggles.tsx +262 -37
- package/src/components/ui/command-palette/editor-commands.tsx +24 -17
- package/src/components/ui/command-palette/index.tsx +6 -256
- package/src/components/ui/controls/material-paint-panel.tsx +83 -0
- package/src/components/ui/controls/material-picker.tsx +17 -35
- package/src/components/ui/controls/metric-control.tsx +33 -11
- package/src/components/ui/floating-level-selector.tsx +48 -11
- package/src/components/ui/helpers/helper-manager.tsx +21 -17
- package/src/components/ui/helpers/registered-tool-helper.tsx +25 -0
- package/src/components/ui/item-catalog/catalog-items.tsx +1743 -315
- package/src/components/ui/item-catalog/item-catalog.tsx +91 -46
- package/src/components/ui/level-duplicate-dialog.tsx +5 -6
- package/src/components/ui/panels/node-display.ts +1 -0
- package/src/components/ui/panels/paint-panel.tsx +66 -41
- package/src/components/ui/panels/panel-manager.tsx +33 -74
- package/src/components/ui/panels/panel-wrapper.tsx +217 -12
- package/src/components/ui/panels/parametric-inspector.tsx +469 -0
- package/src/components/ui/panels/reference-panel.tsx +28 -13
- package/src/components/ui/primitives/dropdown-menu.tsx +12 -8
- package/src/components/ui/primitives/number-input.tsx +1 -1
- package/src/components/ui/primitives/sidebar.tsx +0 -0
- package/src/components/ui/scene-loader.tsx +1 -3
- 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/function-tree-panel.tsx +263 -0
- package/src/components/ui/sidebar/panels/items-panel/index.tsx +404 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +59 -9
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +6 -1
- package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +259 -0
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +3 -1
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +5 -2
- package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +83 -0
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +89 -0
- package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +75 -0
- package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +2 -2
- package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +83 -0
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +8 -12
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +2 -1
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +93 -0
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +41 -5
- package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +128 -0
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +5 -2
- package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +83 -0
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +68 -34
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +3 -1
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +52 -3
- package/src/components/ui/sidebar/tab-bar.tsx +61 -1
- package/src/components/ui/slider.tsx +1 -1
- package/src/components/viewer-overlay.tsx +169 -52
- package/src/components/viewer-zone-system.tsx +1 -1
- package/src/hooks/use-auto-save.ts +24 -3
- package/src/hooks/use-ceiling-events.ts +175 -0
- package/src/hooks/use-drag-action.ts +124 -0
- package/src/hooks/use-keyboard.ts +125 -62
- package/src/hooks/use-selection.ts +64 -0
- package/src/index.tsx +243 -4
- package/src/lib/constants.ts +6 -2
- package/src/lib/editor-api.ts +62 -0
- package/src/lib/elevator-support.ts +96 -0
- package/src/lib/floorplan/apply-alignment.ts +111 -0
- package/src/lib/floorplan/index.ts +7 -0
- package/src/lib/floorplan/selection-tool.ts +40 -0
- package/src/lib/floorplan/stairs.ts +12 -1
- package/src/lib/fresh-planar-placement.test.ts +102 -0
- package/src/lib/fresh-planar-placement.ts +63 -0
- package/src/lib/history.ts +2 -1
- package/src/lib/level-duplication.test.ts +0 -2
- package/src/lib/level-duplication.ts +55 -43
- package/src/lib/level-name.ts +11 -0
- package/src/lib/material-paint.ts +212 -20
- package/src/lib/placement-metadata.ts +29 -0
- package/src/lib/planar-cursor-placement.test.ts +43 -0
- package/src/lib/planar-cursor-placement.ts +42 -0
- package/src/lib/roof-duplication.ts +2 -2
- package/src/lib/scene-bounds.ts +1 -1
- package/src/lib/scene-clipboard.ts +267 -0
- package/src/lib/scene.ts +13 -4
- package/src/lib/sfx-bus.ts +25 -1
- package/src/lib/sfx-player.ts +75 -27
- package/src/lib/site-boundary.ts +1 -0
- package/src/lib/stair-duplication.ts +2 -2
- package/src/lib/stair-levels.test.ts +146 -0
- package/src/lib/stair-levels.ts +177 -0
- package/src/lib/world-grid-snap.ts +199 -0
- package/src/lib/zone-content.ts +130 -0
- package/src/store/use-alignment-guides.ts +21 -0
- package/src/store/use-editor.tsx +342 -91
- package/src/store/use-placement-preview.ts +31 -0
- package/src/store/use-wall-move-ghosts.ts +36 -0
- package/src/store/use-wall-snap-indicator.ts +32 -0
- package/tsconfig.json +2 -1
- package/src/components/editor/preset-thumbnail-generator.tsx +0 -125
- package/src/components/editor-2d/renderers/floorplan-roof-layer.tsx +0 -113
- 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/tools/building/move-building-tool.tsx +0 -157
- package/src/components/tools/ceiling/ceiling-boundary-editor.tsx +0 -43
- package/src/components/tools/ceiling/ceiling-hole-editor.tsx +0 -49
- package/src/components/tools/ceiling/ceiling-tool.tsx +0 -465
- package/src/components/tools/ceiling/move-ceiling-tool.tsx +0 -257
- package/src/components/tools/column/column-tool.tsx +0 -97
- package/src/components/tools/column/move-column-tool.tsx +0 -105
- package/src/components/tools/door/door-math.ts +0 -110
- package/src/components/tools/door/door-tool.tsx +0 -324
- package/src/components/tools/door/move-door-tool.tsx +0 -412
- package/src/components/tools/fence/curve-fence-tool.tsx +0 -179
- package/src/components/tools/fence/fence-tool.tsx +0 -346
- package/src/components/tools/fence/move-fence-endpoint-tool.tsx +0 -438
- package/src/components/tools/fence/move-fence-tool.tsx +0 -298
- package/src/components/tools/item/item-tool.tsx +0 -26
- package/src/components/tools/roof/move-roof-tool.tsx +0 -359
- package/src/components/tools/slab/move-slab-tool.tsx +0 -182
- package/src/components/tools/slab/slab-boundary-editor.tsx +0 -43
- package/src/components/tools/slab/slab-hole-editor.tsx +0 -49
- package/src/components/tools/slab/slab-tool.tsx +0 -322
- package/src/components/tools/spawn/move-spawn-tool.tsx +0 -101
- package/src/components/tools/spawn/spawn-tool.tsx +0 -130
- package/src/components/tools/wall/curve-wall-tool.tsx +0 -176
- package/src/components/tools/wall/move-wall-endpoint-tool.tsx +0 -423
- package/src/components/tools/wall/move-wall-tool.tsx +0 -356
- package/src/components/tools/wall/wall-tool.tsx +0 -332
- package/src/components/tools/window/move-window-tool.tsx +0 -447
- package/src/components/tools/window/window-math.ts +0 -117
- package/src/components/tools/window/window-tool.tsx +0 -332
- package/src/components/ui/helpers/ceiling-helper.tsx +0 -20
- package/src/components/ui/helpers/slab-helper.tsx +0 -20
- package/src/components/ui/helpers/wall-helper.tsx +0 -20
- package/src/components/ui/panels/ceiling-panel.tsx +0 -250
- package/src/components/ui/panels/column-panel.tsx +0 -715
- package/src/components/ui/panels/door-panel.tsx +0 -1292
- package/src/components/ui/panels/fence-panel.tsx +0 -227
- package/src/components/ui/panels/item-panel.tsx +0 -306
- package/src/components/ui/panels/presets/presets-popover.tsx +0 -511
- package/src/components/ui/panels/roof-panel.tsx +0 -230
- package/src/components/ui/panels/roof-segment-panel.tsx +0 -311
- package/src/components/ui/panels/slab-panel.tsx +0 -250
- package/src/components/ui/panels/spawn-panel.tsx +0 -155
- package/src/components/ui/panels/stair-panel.tsx +0 -525
- package/src/components/ui/panels/stair-segment-panel.tsx +0 -325
- package/src/components/ui/panels/wall-panel.tsx +0 -177
- package/src/components/ui/panels/window-panel.tsx +0 -960
- package/src/components/ui/viewer-toolbar.tsx +0 -436
- package/src/contexts/presets-context.tsx +0 -121
- package/src/hooks/use-contextual-tools.ts +0 -61
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
// Pure geometry for "magnetic" wall-draft snapping — no store / viewer / React
|
|
2
|
+
// deps, so it's unit-testable in isolation. Coordinates are XZ plan points
|
|
3
|
+
// (building-local meters). `wall-drafting.ts` layers grid/angle snapping and
|
|
4
|
+
// scene access on top of these primitives.
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
getWallCurveFrameAt,
|
|
8
|
+
getWallCurveLength,
|
|
9
|
+
isCurvedWall,
|
|
10
|
+
type WallNode,
|
|
11
|
+
} from '@pascal-app/core'
|
|
12
|
+
|
|
13
|
+
export type WallPlanPoint = [number, number]
|
|
14
|
+
|
|
15
|
+
/** Which kind of existing-geometry snap produced a drafted point. */
|
|
16
|
+
export type WallDraftSnapKind = 'endpoint' | 'midpoint' | 'intersection' | 'wall'
|
|
17
|
+
|
|
18
|
+
export type WallDraftSnapResult = {
|
|
19
|
+
point: WallPlanPoint
|
|
20
|
+
/**
|
|
21
|
+
* Set when `point` locked onto existing wall geometry (a corner, midpoint,
|
|
22
|
+
* crossing, or wall body) rather than a plain grid/angle position. This is
|
|
23
|
+
* the "magnetic" snap the beacon visualises; `null` for grid/angle-only.
|
|
24
|
+
*/
|
|
25
|
+
snap: WallDraftSnapKind | null
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const WALL_JOIN_SNAP_RADIUS = 0.35
|
|
29
|
+
// Generous radius for snapping to an *existing* wall's endpoint while
|
|
30
|
+
// drafting. Larger than `WALL_JOIN_SNAP_RADIUS` because endpoint snap
|
|
31
|
+
// is the strongest user intent (closing a polygon, attaching to a
|
|
32
|
+
// corner) and the cursor never lands pixel-perfect on a corner.
|
|
33
|
+
export const WALL_ENDPOINT_SNAP_RADIUS = 0.7
|
|
34
|
+
// Discrete "special point" snaps taken from the raw cursor (like the
|
|
35
|
+
// endpoint snap) but slightly tighter — a corner is the strongest intent,
|
|
36
|
+
// a midpoint / crossing is the next tier down.
|
|
37
|
+
export const WALL_MIDPOINT_SNAP_RADIUS = 0.5
|
|
38
|
+
export const WALL_INTERSECTION_SNAP_RADIUS = 0.5
|
|
39
|
+
|
|
40
|
+
export function distanceSquared(a: WallPlanPoint, b: WallPlanPoint): number {
|
|
41
|
+
const dx = a[0] - b[0]
|
|
42
|
+
const dz = a[1] - b[1]
|
|
43
|
+
return dx * dx + dz * dz
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function projectPointOntoWall(point: WallPlanPoint, wall: WallNode): WallPlanPoint | null {
|
|
47
|
+
const [x1, z1] = wall.start
|
|
48
|
+
const [x2, z2] = wall.end
|
|
49
|
+
const dx = x2 - x1
|
|
50
|
+
const dz = z2 - z1
|
|
51
|
+
const lengthSquared = dx * dx + dz * dz
|
|
52
|
+
if (lengthSquared < 1e-9) {
|
|
53
|
+
return null
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const t = ((point[0] - x1) * dx + (point[1] - z1) * dz) / lengthSquared
|
|
57
|
+
if (t <= 0 || t >= 1) {
|
|
58
|
+
return null
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return [x1 + dx * t, z1 + dz * t]
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function findWallSnapTarget(
|
|
65
|
+
point: WallPlanPoint,
|
|
66
|
+
walls: WallNode[],
|
|
67
|
+
options?: { ignoreWallIds?: string[]; radius?: number },
|
|
68
|
+
): WallPlanPoint | null {
|
|
69
|
+
const ignoreWallIds = new Set(options?.ignoreWallIds ?? [])
|
|
70
|
+
const radiusSquared = (options?.radius ?? WALL_JOIN_SNAP_RADIUS) ** 2
|
|
71
|
+
let bestTarget: WallPlanPoint | null = null
|
|
72
|
+
let bestDistanceSquared = Number.POSITIVE_INFINITY
|
|
73
|
+
|
|
74
|
+
for (const wall of walls) {
|
|
75
|
+
if (ignoreWallIds.has(wall.id)) {
|
|
76
|
+
continue
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const candidates: Array<WallPlanPoint | null> = [wall.start, wall.end]
|
|
80
|
+
|
|
81
|
+
if (isCurvedWall(wall)) {
|
|
82
|
+
const sampleCount = Math.max(8, Math.ceil(getWallCurveLength(wall) / 0.3))
|
|
83
|
+
for (let index = 0; index <= sampleCount; index += 1) {
|
|
84
|
+
const frame = getWallCurveFrameAt(wall, index / sampleCount)
|
|
85
|
+
candidates.push([frame.point.x, frame.point.y])
|
|
86
|
+
}
|
|
87
|
+
} else {
|
|
88
|
+
candidates.push(projectPointOntoWall(point, wall))
|
|
89
|
+
}
|
|
90
|
+
for (const candidate of candidates) {
|
|
91
|
+
if (!candidate) {
|
|
92
|
+
continue
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const candidateDistanceSquared = distanceSquared(point, candidate)
|
|
96
|
+
if (
|
|
97
|
+
candidateDistanceSquared > radiusSquared ||
|
|
98
|
+
candidateDistanceSquared >= bestDistanceSquared
|
|
99
|
+
) {
|
|
100
|
+
continue
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
bestTarget = candidate
|
|
104
|
+
bestDistanceSquared = candidateDistanceSquared
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return bestTarget
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Endpoint-only snap from the *raw* cursor (no grid pre-snap), with a
|
|
113
|
+
* generous radius. Use this before `findWallSnapTarget` so the strong
|
|
114
|
+
* "attach to an existing wall corner" intent isn't accidentally pushed
|
|
115
|
+
* out of range by an interim grid snap that moved the cursor away from
|
|
116
|
+
* the endpoint.
|
|
117
|
+
*/
|
|
118
|
+
export function findWallEndpointFromRaw(
|
|
119
|
+
point: WallPlanPoint,
|
|
120
|
+
walls: WallNode[],
|
|
121
|
+
ignoreWallIds?: string[],
|
|
122
|
+
): WallPlanPoint | null {
|
|
123
|
+
const ignored = new Set(ignoreWallIds ?? [])
|
|
124
|
+
const radiusSquared = WALL_ENDPOINT_SNAP_RADIUS ** 2
|
|
125
|
+
let best: WallPlanPoint | null = null
|
|
126
|
+
let bestDistSq = Number.POSITIVE_INFINITY
|
|
127
|
+
|
|
128
|
+
for (const wall of walls) {
|
|
129
|
+
if (ignored.has(wall.id)) continue
|
|
130
|
+
for (const corner of [wall.start, wall.end] as WallPlanPoint[]) {
|
|
131
|
+
const d = distanceSquared(point, corner)
|
|
132
|
+
if (d <= radiusSquared && d < bestDistSq) {
|
|
133
|
+
best = corner
|
|
134
|
+
bestDistSq = d
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return best
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Midpoint of a wall — curve midpoint for curved walls, segment midpoint otherwise. */
|
|
142
|
+
function wallMidpoint(wall: WallNode): WallPlanPoint {
|
|
143
|
+
if (isCurvedWall(wall)) {
|
|
144
|
+
const frame = getWallCurveFrameAt(wall, 0.5)
|
|
145
|
+
return [frame.point.x, frame.point.y]
|
|
146
|
+
}
|
|
147
|
+
return [(wall.start[0] + wall.end[0]) / 2, (wall.start[1] + wall.end[1]) / 2]
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Nearest wall midpoint to the raw cursor, within `WALL_MIDPOINT_SNAP_RADIUS`. */
|
|
151
|
+
export function findWallMidpointFromRaw(
|
|
152
|
+
point: WallPlanPoint,
|
|
153
|
+
walls: WallNode[],
|
|
154
|
+
ignoreWallIds?: string[],
|
|
155
|
+
): WallPlanPoint | null {
|
|
156
|
+
const ignored = new Set(ignoreWallIds ?? [])
|
|
157
|
+
const radiusSquared = WALL_MIDPOINT_SNAP_RADIUS ** 2
|
|
158
|
+
let best: WallPlanPoint | null = null
|
|
159
|
+
let bestDistSq = Number.POSITIVE_INFINITY
|
|
160
|
+
|
|
161
|
+
for (const wall of walls) {
|
|
162
|
+
if (ignored.has(wall.id)) continue
|
|
163
|
+
const mid = wallMidpoint(wall)
|
|
164
|
+
const d = distanceSquared(point, mid)
|
|
165
|
+
if (d <= radiusSquared && d < bestDistSq) {
|
|
166
|
+
best = mid
|
|
167
|
+
bestDistSq = d
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return best
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Crossing point of two straight segments, or null if they don't intersect within both. */
|
|
174
|
+
function segmentIntersection(
|
|
175
|
+
a1: WallPlanPoint,
|
|
176
|
+
a2: WallPlanPoint,
|
|
177
|
+
b1: WallPlanPoint,
|
|
178
|
+
b2: WallPlanPoint,
|
|
179
|
+
): WallPlanPoint | null {
|
|
180
|
+
const rx = a2[0] - a1[0]
|
|
181
|
+
const rz = a2[1] - a1[1]
|
|
182
|
+
const sx = b2[0] - b1[0]
|
|
183
|
+
const sz = b2[1] - b1[1]
|
|
184
|
+
const denom = rx * sz - rz * sx
|
|
185
|
+
if (Math.abs(denom) < 1e-9) return null // parallel / collinear
|
|
186
|
+
|
|
187
|
+
const qpx = b1[0] - a1[0]
|
|
188
|
+
const qpz = b1[1] - a1[1]
|
|
189
|
+
const t = (qpx * sz - qpz * sx) / denom
|
|
190
|
+
const u = (qpx * rz - qpz * rx) / denom
|
|
191
|
+
if (t < 0 || t > 1 || u < 0 || u > 1) return null
|
|
192
|
+
|
|
193
|
+
return [a1[0] + t * rx, a1[1] + t * rz]
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Nearest point where two existing straight walls cross, within
|
|
198
|
+
* `WALL_INTERSECTION_SNAP_RADIUS`. Curved walls are skipped. O(n²) over the
|
|
199
|
+
* level's walls — fine at editor scale.
|
|
200
|
+
*/
|
|
201
|
+
export function findWallIntersectionFromRaw(
|
|
202
|
+
point: WallPlanPoint,
|
|
203
|
+
walls: WallNode[],
|
|
204
|
+
ignoreWallIds?: string[],
|
|
205
|
+
): WallPlanPoint | null {
|
|
206
|
+
const ignored = new Set(ignoreWallIds ?? [])
|
|
207
|
+
const straight = walls.filter((wall) => !ignored.has(wall.id) && !isCurvedWall(wall))
|
|
208
|
+
const radiusSquared = WALL_INTERSECTION_SNAP_RADIUS ** 2
|
|
209
|
+
let best: WallPlanPoint | null = null
|
|
210
|
+
let bestDistSq = Number.POSITIVE_INFINITY
|
|
211
|
+
|
|
212
|
+
for (let i = 0; i < straight.length; i += 1) {
|
|
213
|
+
for (let j = i + 1; j < straight.length; j += 1) {
|
|
214
|
+
const crossing = segmentIntersection(
|
|
215
|
+
straight[i]!.start,
|
|
216
|
+
straight[i]!.end,
|
|
217
|
+
straight[j]!.start,
|
|
218
|
+
straight[j]!.end,
|
|
219
|
+
)
|
|
220
|
+
if (!crossing) continue
|
|
221
|
+
const d = distanceSquared(point, crossing)
|
|
222
|
+
if (d <= radiusSquared && d < bestDistSq) {
|
|
223
|
+
best = crossing
|
|
224
|
+
bestDistSq = d
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return best
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** Pick the candidate nearest to `point`, ignoring nulls. */
|
|
232
|
+
function nearestCandidate(
|
|
233
|
+
point: WallPlanPoint,
|
|
234
|
+
candidates: Array<WallDraftSnapResult | null | false>,
|
|
235
|
+
): WallDraftSnapResult | null {
|
|
236
|
+
let best: WallDraftSnapResult | null = null
|
|
237
|
+
let bestDistSq = Number.POSITIVE_INFINITY
|
|
238
|
+
for (const candidate of candidates) {
|
|
239
|
+
if (!candidate) continue
|
|
240
|
+
const d = distanceSquared(point, candidate.point)
|
|
241
|
+
if (d < bestDistSq) {
|
|
242
|
+
best = candidate
|
|
243
|
+
bestDistSq = d
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return best
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Discrete "special point" snap from the raw cursor, in priority order:
|
|
251
|
+
* 1. corners (endpoints) — strongest intent, largest radius
|
|
252
|
+
* 2. midpoints / crossings — next tier; the nearer of the two wins
|
|
253
|
+
* A corner within range always wins over a midpoint/crossing. Returns null
|
|
254
|
+
* when no special point is in range (caller falls back to grid/edge snap).
|
|
255
|
+
*/
|
|
256
|
+
export function findWallSpecialPointSnap(
|
|
257
|
+
point: WallPlanPoint,
|
|
258
|
+
walls: WallNode[],
|
|
259
|
+
ignoreWallIds?: string[],
|
|
260
|
+
): WallDraftSnapResult | null {
|
|
261
|
+
const endpoint = findWallEndpointFromRaw(point, walls, ignoreWallIds)
|
|
262
|
+
if (endpoint) return { point: endpoint, snap: 'endpoint' }
|
|
263
|
+
|
|
264
|
+
const midpoint = findWallMidpointFromRaw(point, walls, ignoreWallIds)
|
|
265
|
+
const intersection = findWallIntersectionFromRaw(point, walls, ignoreWallIds)
|
|
266
|
+
return nearestCandidate(point, [
|
|
267
|
+
midpoint && { point: midpoint, snap: 'midpoint' },
|
|
268
|
+
intersection && { point: intersection, snap: 'intersection' },
|
|
269
|
+
])
|
|
270
|
+
}
|
|
@@ -3,6 +3,8 @@ 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'
|
|
7
|
+
import { snapWorldXZForActiveBuilding } from './../../../lib/world-grid-snap'
|
|
6
8
|
import useEditor from './../../../store/use-editor'
|
|
7
9
|
import { CursorSphere } from '../shared/cursor-sphere'
|
|
8
10
|
|
|
@@ -67,6 +69,9 @@ const commitZoneDrawing = (levelId: LevelNode['id'], points: Array<[number, numb
|
|
|
67
69
|
|
|
68
70
|
// Select the newly created zone
|
|
69
71
|
useViewer.getState().setSelection({ zoneId: zone.id })
|
|
72
|
+
|
|
73
|
+
// Play structure build sound
|
|
74
|
+
sfxEmitter.emit('sfx:structure-build')
|
|
70
75
|
}
|
|
71
76
|
|
|
72
77
|
type PreviewState = {
|
|
@@ -86,6 +91,7 @@ export const ZoneTool: React.FC = () => {
|
|
|
86
91
|
const mainLineRef = useRef<Line>(null!)
|
|
87
92
|
const closingLineRef = useRef<Line>(null!)
|
|
88
93
|
const pointsRef = useRef<Array<[number, number]>>([])
|
|
94
|
+
const previousSnappedPointRef = useRef<[number, number] | null>(null)
|
|
89
95
|
const levelYRef = useRef(0) // Track current level Y position
|
|
90
96
|
const currentLevelId = useViewer((state) => state.selection.levelId)
|
|
91
97
|
const setTool = useEditor((state) => state.setTool)
|
|
@@ -173,20 +179,34 @@ export const ZoneTool: React.FC = () => {
|
|
|
173
179
|
const onGridMove = (event: GridEvent) => {
|
|
174
180
|
if (!cursorRef.current) return
|
|
175
181
|
|
|
176
|
-
//
|
|
177
|
-
|
|
178
|
-
const gridZ =
|
|
182
|
+
// World-grid snap projected into building-local; rotated buildings
|
|
183
|
+
// used to pull the snap off the visible grid lines.
|
|
184
|
+
const [gridX, gridZ] = snapWorldXZForActiveBuilding(
|
|
185
|
+
event.position[0],
|
|
186
|
+
event.position[2],
|
|
187
|
+
0.5,
|
|
188
|
+
).local
|
|
179
189
|
cursorPosition = [gridX, gridZ]
|
|
180
190
|
levelYRef.current = event.localPosition[1]
|
|
181
191
|
|
|
182
192
|
// If we have points, snap to axis from last point
|
|
183
193
|
const lastPoint = pointsRef.current[pointsRef.current.length - 1]
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
194
|
+
const displayPoint = lastPoint
|
|
195
|
+
? calculateSnapPoint(lastPoint, cursorPosition)
|
|
196
|
+
: cursorPosition
|
|
197
|
+
|
|
198
|
+
// Play snap sound when the snapped position changes during drawing
|
|
199
|
+
if (
|
|
200
|
+
pointsRef.current.length > 0 &&
|
|
201
|
+
previousSnappedPointRef.current &&
|
|
202
|
+
(displayPoint[0] !== previousSnappedPointRef.current[0] ||
|
|
203
|
+
displayPoint[1] !== previousSnappedPointRef.current[1])
|
|
204
|
+
) {
|
|
205
|
+
sfxEmitter.emit('sfx:grid-snap')
|
|
189
206
|
}
|
|
207
|
+
previousSnappedPointRef.current = displayPoint
|
|
208
|
+
|
|
209
|
+
cursorRef.current.position.set(displayPoint[0], event.localPosition[1], displayPoint[1])
|
|
190
210
|
|
|
191
211
|
updatePreview()
|
|
192
212
|
}
|
|
@@ -194,8 +214,11 @@ export const ZoneTool: React.FC = () => {
|
|
|
194
214
|
const onGridClick = (event: GridEvent) => {
|
|
195
215
|
if (!currentLevelId) return
|
|
196
216
|
|
|
197
|
-
const gridX =
|
|
198
|
-
|
|
217
|
+
const [gridX, gridZ] = snapWorldXZForActiveBuilding(
|
|
218
|
+
event.position[0],
|
|
219
|
+
event.position[2],
|
|
220
|
+
0.5,
|
|
221
|
+
).local
|
|
199
222
|
let clickPoint: [number, number] = [gridX, gridZ]
|
|
200
223
|
|
|
201
224
|
// Snap to axis from last point
|
|
@@ -221,7 +244,9 @@ export const ZoneTool: React.FC = () => {
|
|
|
221
244
|
mainLineRef.current.visible = false
|
|
222
245
|
closingLineRef.current.visible = false
|
|
223
246
|
} else {
|
|
224
|
-
// Add point to polygon
|
|
247
|
+
// Add point to polygon. Every non-closing vertex is a "start" tick;
|
|
248
|
+
// closing the polygon above fires the structure-build (end) cue.
|
|
249
|
+
sfxEmitter.emit('sfx:structure-build-start')
|
|
225
250
|
pointsRef.current = [...pointsRef.current, clickPoint]
|
|
226
251
|
updatePreview()
|
|
227
252
|
}
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
TooltipContent,
|
|
6
6
|
TooltipTrigger,
|
|
7
7
|
} from './../../../components/ui/primitives/tooltip'
|
|
8
|
+
import { triggerSFX } from './../../../lib/sfx-bus'
|
|
8
9
|
import { cn } from './../../../lib/utils'
|
|
9
10
|
|
|
10
11
|
interface ActionButtonProps extends React.ComponentProps<typeof Button> {
|
|
@@ -17,7 +18,18 @@ interface ActionButtonProps extends React.ComponentProps<typeof Button> {
|
|
|
17
18
|
|
|
18
19
|
export const ActionButton = React.forwardRef<HTMLButtonElement, ActionButtonProps>(
|
|
19
20
|
(
|
|
20
|
-
{
|
|
21
|
+
{
|
|
22
|
+
className,
|
|
23
|
+
children,
|
|
24
|
+
label,
|
|
25
|
+
shortcut,
|
|
26
|
+
isActive,
|
|
27
|
+
tooltipContent,
|
|
28
|
+
tooltipSide,
|
|
29
|
+
onClick,
|
|
30
|
+
onMouseEnter,
|
|
31
|
+
...props
|
|
32
|
+
},
|
|
21
33
|
ref,
|
|
22
34
|
) => {
|
|
23
35
|
return (
|
|
@@ -25,6 +37,14 @@ export const ActionButton = React.forwardRef<HTMLButtonElement, ActionButtonProp
|
|
|
25
37
|
<TooltipTrigger asChild>
|
|
26
38
|
<Button
|
|
27
39
|
className={cn('relative h-11 w-11 transition-all', className)}
|
|
40
|
+
onClick={(event) => {
|
|
41
|
+
triggerSFX('sfx:menu-click')
|
|
42
|
+
onClick?.(event)
|
|
43
|
+
}}
|
|
44
|
+
onMouseEnter={(event) => {
|
|
45
|
+
triggerSFX('sfx:menu-hover')
|
|
46
|
+
onMouseEnter?.(event)
|
|
47
|
+
}}
|
|
28
48
|
ref={ref}
|
|
29
49
|
{...props}
|
|
30
50
|
>
|
|
File without changes
|
|
@@ -6,18 +6,10 @@ import { useViewer } from '@pascal-app/viewer'
|
|
|
6
6
|
import { type LucideIcon, Trash2 } from 'lucide-react'
|
|
7
7
|
import Image from 'next/image'
|
|
8
8
|
import { cn } from './../../../lib/utils'
|
|
9
|
-
import useEditor from './../../../store/use-editor'
|
|
9
|
+
import useEditor, { selectSiteFloorplanContext } from './../../../store/use-editor'
|
|
10
10
|
import { ActionButton } from './action-button'
|
|
11
11
|
|
|
12
|
-
type ControlId =
|
|
13
|
-
| 'select'
|
|
14
|
-
| 'box-select'
|
|
15
|
-
| 'site-edit'
|
|
16
|
-
| 'build'
|
|
17
|
-
| 'material-paint'
|
|
18
|
-
| 'furnish'
|
|
19
|
-
| 'zone'
|
|
20
|
-
| 'delete'
|
|
12
|
+
type ControlId = 'select' | 'box-select' | 'site-edit' | 'zone' | 'delete'
|
|
21
13
|
|
|
22
14
|
type ControlConfig = {
|
|
23
15
|
id: ControlId
|
|
@@ -40,44 +32,13 @@ const controls: ControlConfig[] = [
|
|
|
40
32
|
color: 'hover:bg-blue-500/20 hover:text-blue-400',
|
|
41
33
|
activeColor: 'bg-blue-500/20 text-blue-400',
|
|
42
34
|
},
|
|
43
|
-
{
|
|
44
|
-
id: 'box-select',
|
|
45
|
-
iconifyIcon: 'mdi:select-drag',
|
|
46
|
-
label: 'Box select',
|
|
47
|
-
color: 'hover:bg-white/5',
|
|
48
|
-
activeColor: 'bg-white/10 hover:bg-white/10',
|
|
49
|
-
},
|
|
50
35
|
{
|
|
51
36
|
id: 'site-edit',
|
|
52
|
-
imageSrc: '/icons/site.png',
|
|
37
|
+
imageSrc: '/icons/site-flag.png',
|
|
53
38
|
label: 'Edit site',
|
|
54
39
|
color: 'hover:bg-white/5',
|
|
55
40
|
activeColor: 'bg-white/10 hover:bg-white/10',
|
|
56
41
|
},
|
|
57
|
-
{
|
|
58
|
-
id: 'build',
|
|
59
|
-
imageSrc: '/icons/build.png',
|
|
60
|
-
label: 'Build',
|
|
61
|
-
shortcut: 'B',
|
|
62
|
-
color: 'hover:bg-green-500/20 hover:text-green-400',
|
|
63
|
-
activeColor: 'bg-green-500/20 text-green-400',
|
|
64
|
-
},
|
|
65
|
-
{
|
|
66
|
-
id: 'material-paint',
|
|
67
|
-
imageSrc: '/icons/paint.png',
|
|
68
|
-
label: 'Material Paint',
|
|
69
|
-
shortcut: 'P',
|
|
70
|
-
color: 'hover:bg-amber-500/20 hover:text-amber-400',
|
|
71
|
-
activeColor: 'bg-amber-500/20 text-amber-400',
|
|
72
|
-
},
|
|
73
|
-
{
|
|
74
|
-
id: 'furnish',
|
|
75
|
-
imageSrc: '/icons/couch.png',
|
|
76
|
-
label: 'Furnish',
|
|
77
|
-
shortcut: 'F',
|
|
78
|
-
color: 'hover:bg-green-500/20 hover:text-green-400',
|
|
79
|
-
activeColor: 'bg-green-500/20 text-green-400',
|
|
80
|
-
},
|
|
81
42
|
{
|
|
82
43
|
id: 'zone',
|
|
83
44
|
imageSrc: '/icons/zone.png',
|
|
@@ -90,7 +51,7 @@ const controls: ControlConfig[] = [
|
|
|
90
51
|
id: 'delete',
|
|
91
52
|
icon: Trash2,
|
|
92
53
|
label: 'Delete',
|
|
93
|
-
shortcut: '
|
|
54
|
+
shortcut: 'X',
|
|
94
55
|
color: 'hover:bg-red-500/20 hover:text-red-400',
|
|
95
56
|
activeColor: 'bg-red-500/20 text-red-400',
|
|
96
57
|
},
|
|
@@ -104,7 +65,6 @@ export function ControlModes() {
|
|
|
104
65
|
const setPhase = useEditor((state) => state.setPhase)
|
|
105
66
|
const setStructureLayer = useEditor((state) => state.setStructureLayer)
|
|
106
67
|
const setSelectionTool = useEditor((state) => state.setFloorplanSelectionTool)
|
|
107
|
-
const primeMaterialPaintFromSelection = useEditor((state) => state.primeMaterialPaintFromSelection)
|
|
108
68
|
const levelId = useViewer((s) => s.selection.levelId)
|
|
109
69
|
|
|
110
70
|
// Only subscribe to the primitive `level` number — when walls are added to
|
|
@@ -127,10 +87,6 @@ export function ControlModes() {
|
|
|
127
87
|
if (id === 'select') return mode === 'select' && selectionTool === 'click'
|
|
128
88
|
if (id === 'box-select') return mode === 'select' && selectionTool === 'marquee'
|
|
129
89
|
if (id === 'site-edit') return false
|
|
130
|
-
if (id === 'build')
|
|
131
|
-
return mode === 'build' && phase === 'structure' && structureLayer === 'elements'
|
|
132
|
-
if (id === 'material-paint') return mode === 'material-paint'
|
|
133
|
-
if (id === 'furnish') return mode === 'build' && phase === 'furnish'
|
|
134
90
|
if (id === 'zone')
|
|
135
91
|
return mode === 'build' && phase === 'structure' && structureLayer === 'zones'
|
|
136
92
|
return mode === id
|
|
@@ -144,10 +100,8 @@ export function ControlModes() {
|
|
|
144
100
|
setMode('select')
|
|
145
101
|
setStructureLayer('elements')
|
|
146
102
|
} else if (isGroundFloor) {
|
|
147
|
-
// Enter site editing — set state directly to preserve level selection.
|
|
148
|
-
// setPhase('site') calls viewer.resetSelection() which clears levelId,
|
|
149
|
-
// breaking the 2D floorplan (it needs a level to render the SVG).
|
|
150
103
|
useEditor.setState({ phase: 'site', mode: 'select', tool: null, catalogCategory: null })
|
|
104
|
+
selectSiteFloorplanContext()
|
|
151
105
|
}
|
|
152
106
|
return
|
|
153
107
|
}
|
|
@@ -164,31 +118,6 @@ export function ControlModes() {
|
|
|
164
118
|
} else if (id === 'box-select') {
|
|
165
119
|
setMode('select')
|
|
166
120
|
setSelectionTool('marquee')
|
|
167
|
-
} else if (id === 'build') {
|
|
168
|
-
// Toggle: if already in structure build, go back to select
|
|
169
|
-
if (getIsActive('build')) {
|
|
170
|
-
setMode('select')
|
|
171
|
-
} else {
|
|
172
|
-
setPhase('structure')
|
|
173
|
-
setStructureLayer('elements')
|
|
174
|
-
setMode('build')
|
|
175
|
-
}
|
|
176
|
-
} else if (id === 'material-paint') {
|
|
177
|
-
if (getIsActive('material-paint')) {
|
|
178
|
-
setMode('select')
|
|
179
|
-
} else {
|
|
180
|
-
primeMaterialPaintFromSelection()
|
|
181
|
-
setPhase('structure')
|
|
182
|
-
setStructureLayer('elements')
|
|
183
|
-
setMode('material-paint')
|
|
184
|
-
}
|
|
185
|
-
} else if (id === 'furnish') {
|
|
186
|
-
if (getIsActive('furnish')) {
|
|
187
|
-
setMode('select')
|
|
188
|
-
} else {
|
|
189
|
-
setPhase('furnish')
|
|
190
|
-
setMode('build')
|
|
191
|
-
}
|
|
192
121
|
} else if (id === 'zone') {
|
|
193
122
|
if (getIsActive('zone')) {
|
|
194
123
|
setMode('select')
|
|
@@ -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
|
-
}
|