@pascal-app/editor 0.8.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 +8 -9
- package/src/components/editor/alignment-3d-guide-layer.tsx +160 -0
- package/src/components/editor/custom-camera-controls.tsx +754 -25
- package/src/components/editor/editor-layout-mobile.tsx +3 -3
- package/src/components/editor/editor-layout-v2.tsx +58 -51
- 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 +201 -83
- package/src/components/editor/first-person/bvh-ecctrl.tsx +8 -8
- package/src/components/editor/first-person-controls.tsx +825 -20
- 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 +5829 -12005
- 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 +178 -149
- 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 +433 -156
- 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 +136 -62
- package/src/components/editor/thumbnail-generator.tsx +9 -3
- package/src/components/editor/use-floorplan-background-placement.ts +119 -32
- 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-geometry-renderer.tsx +266 -0
- package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +108 -66
- 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 +24 -9
- package/src/components/editor-2d/svg-paths.ts +10 -2
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +26 -3
- 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 +1 -1
- 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 +41 -105
- package/src/components/tools/item/placement-math.test.ts +10 -0
- package/src/components/tools/item/placement-math.ts +31 -5
- package/src/components/tools/item/placement-strategies.ts +190 -15
- package/src/components/tools/item/placement-types.ts +10 -1
- package/src/components/tools/item/use-draft-node.ts +20 -1
- package/src/components/tools/item/use-placement-coordinator.tsx +694 -191
- 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 -538
- 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 +56 -0
- 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 +16 -6
- package/src/components/ui/action-menu/action-button.tsx +21 -1
- package/src/components/ui/action-menu/control-modes.tsx +5 -82
- package/src/components/ui/action-menu/index.tsx +1 -113
- package/src/components/ui/action-menu/structure-tools.tsx +13 -88
- package/src/components/ui/action-menu/view-toggles.tsx +243 -6
- package/src/components/ui/command-palette/editor-commands.tsx +18 -13
- package/src/components/ui/command-palette/index.tsx +2 -1
- package/src/components/ui/controls/material-paint-panel.tsx +83 -0
- package/src/components/ui/controls/material-picker.tsx +11 -32
- package/src/components/ui/controls/metric-control.tsx +33 -11
- package/src/components/ui/floating-level-selector.tsx +47 -10
- package/src/components/ui/helpers/helper-manager.tsx +16 -17
- package/src/components/ui/helpers/registered-tool-helper.tsx +25 -0
- package/src/components/ui/item-catalog/catalog-items.tsx +1 -0
- package/src/components/ui/item-catalog/item-catalog.tsx +3 -0
- package/src/components/ui/level-duplicate-dialog.tsx +2 -1
- package/src/components/ui/panels/node-display.ts +1 -0
- package/src/components/ui/panels/panel-manager.tsx +35 -47
- package/src/components/ui/panels/panel-wrapper.tsx +217 -12
- package/src/components/ui/panels/parametric-inspector.tsx +469 -0
- package/src/components/ui/primitives/dropdown-menu.tsx +12 -8
- package/src/components/ui/scene-loader.tsx +1 -3
- package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +263 -0
- package/src/components/ui/sidebar/panels/items-panel/index.tsx +82 -8
- 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 +4 -6
- 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 +7 -1
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +68 -36
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +51 -2
- package/src/components/ui/sidebar/tab-bar.tsx +61 -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 +10 -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 +115 -62
- package/src/hooks/use-selection.ts +64 -0
- package/src/index.tsx +237 -5
- 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.ts +54 -42
- package/src/lib/level-name.ts +11 -0
- package/src/lib/material-paint.ts +211 -19
- 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 +1 -1
- package/src/lib/scene-clipboard.ts +267 -0
- package/src/lib/scene.ts +13 -4
- package/src/lib/sfx-bus.ts +23 -1
- package/src/lib/sfx-player.ts +70 -22
- package/src/lib/site-boundary.ts +1 -0
- 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 +320 -37
- 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/src/components/editor/preset-thumbnail-generator.tsx +0 -125
- package/src/components/editor-2d/renderers/floorplan-roof-layer.tsx +0 -113
- 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 -264
- 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 -178
- package/src/components/tools/fence/fence-tool.tsx +0 -346
- package/src/components/tools/fence/move-fence-endpoint-tool.tsx +0 -441
- package/src/components/tools/fence/move-fence-tool.tsx +0 -302
- package/src/components/tools/item/item-tool.tsx +0 -26
- package/src/components/tools/roof/move-roof-tool.tsx +0 -364
- 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 -178
- package/src/components/tools/wall/move-wall-endpoint-tool.tsx +0 -426
- package/src/components/tools/wall/move-wall-tool.tsx +0 -358
- 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 -249
- package/src/components/ui/panels/column-panel.tsx +0 -759
- package/src/components/ui/panels/door-panel.tsx +0 -1299
- package/src/components/ui/panels/fence-panel.tsx +0 -222
- 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 -280
- 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 -161
- package/src/components/ui/panels/stair-panel.tsx +0 -577
- package/src/components/ui/panels/stair-segment-panel.tsx +0 -325
- package/src/components/ui/panels/wall-panel.tsx +0 -273
- package/src/components/ui/panels/window-panel.tsx +0 -968
- package/src/contexts/presets-context.tsx +0 -121
- package/src/hooks/use-contextual-tools.ts +0 -61
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { create } from 'zustand'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Ephemeral preview state for wall move's bridge walls.
|
|
5
|
+
*
|
|
6
|
+
* When the user drags a wall whose corner neighbours are off-axis to
|
|
7
|
+
* the move direction, the junction planner emits a `bridgePlan` for
|
|
8
|
+
* each — a new wall that would be inserted between the original and
|
|
9
|
+
* new corner on commit. The 3D `MoveWallTool` already renders these
|
|
10
|
+
* as translucent ghost meshes mid-drag; the 2D `wallFloorplanMoveTarget`
|
|
11
|
+
* writes here so the floor-plan SVG layer can render the same hint.
|
|
12
|
+
*
|
|
13
|
+
* Writer: `packages/nodes/src/wall/floorplan-move.ts` (on each `apply`,
|
|
14
|
+
* cleared on `commit` and by the move overlay's cleanup).
|
|
15
|
+
* Reader: `packages/editor/src/components/editor-2d/floorplan-wall-move-ghost-layer.tsx`.
|
|
16
|
+
*/
|
|
17
|
+
export type WallMoveGhostBridge = {
|
|
18
|
+
id: string
|
|
19
|
+
start: [number, number]
|
|
20
|
+
end: [number, number]
|
|
21
|
+
/** Plan-space thickness already passed through `getFloorplanWallThickness`. */
|
|
22
|
+
thickness: number
|
|
23
|
+
color: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
type WallMoveGhostsState = {
|
|
27
|
+
bridges: WallMoveGhostBridge[]
|
|
28
|
+
setBridges: (bridges: WallMoveGhostBridge[]) => void
|
|
29
|
+
clear: () => void
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const useWallMoveGhosts = create<WallMoveGhostsState>((set) => ({
|
|
33
|
+
bridges: [],
|
|
34
|
+
setBridges: (bridges) => set({ bridges }),
|
|
35
|
+
clear: () => set({ bridges: [] }),
|
|
36
|
+
}))
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Ephemeral store for the "magnetic" wall-snap beacon shown during wall
|
|
2
|
+
// drafting. The wall tool writes the active snap point on pointermove when
|
|
3
|
+
// the draft endpoint locks onto existing wall geometry (a corner or a point
|
|
4
|
+
// along a wall); the 3D beacon layer subscribes and draws a vertical marker
|
|
5
|
+
// there. Cleared on commit, cancel, and unmount — same lifecycle as
|
|
6
|
+
// `use-alignment-guides`.
|
|
7
|
+
|
|
8
|
+
import { create } from 'zustand'
|
|
9
|
+
|
|
10
|
+
/** Which kind of wall geometry the draft point snapped to. */
|
|
11
|
+
export type WallSnapKind = 'endpoint' | 'midpoint' | 'intersection' | 'wall'
|
|
12
|
+
|
|
13
|
+
export type WallSnapPoint = {
|
|
14
|
+
/** Building-local plan coordinates (XZ meters). */
|
|
15
|
+
x: number
|
|
16
|
+
z: number
|
|
17
|
+
kind: WallSnapKind
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
type WallSnapIndicatorState = {
|
|
21
|
+
point: WallSnapPoint | null
|
|
22
|
+
set(point: WallSnapPoint | null): void
|
|
23
|
+
clear(): void
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const useWallSnapIndicator = create<WallSnapIndicatorState>((set) => ({
|
|
27
|
+
point: null,
|
|
28
|
+
set: (point) => set({ point }),
|
|
29
|
+
clear: () => set({ point: null }),
|
|
30
|
+
}))
|
|
31
|
+
|
|
32
|
+
export default useWallSnapIndicator
|
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import { emitter, sceneRegistry } from '@pascal-app/core'
|
|
4
|
-
import { useThree } from '@react-three/fiber'
|
|
5
|
-
import { useCallback, useEffect } from 'react'
|
|
6
|
-
import * as THREE from 'three'
|
|
7
|
-
import { usePresetsAdapter } from '../../contexts/presets-context'
|
|
8
|
-
|
|
9
|
-
const THUMBNAIL_SIZE = 1080
|
|
10
|
-
const CAMERA_FOV = 45
|
|
11
|
-
|
|
12
|
-
export const PresetThumbnailGenerator = () => {
|
|
13
|
-
const gl = useThree((state) => state.gl)
|
|
14
|
-
const scene = useThree((state) => state.scene)
|
|
15
|
-
const adapter = usePresetsAdapter()
|
|
16
|
-
|
|
17
|
-
const generate = useCallback(
|
|
18
|
-
async ({ presetId, nodeId }: { presetId: string; nodeId: string }) => {
|
|
19
|
-
const target = sceneRegistry.nodes.get(nodeId)
|
|
20
|
-
if (!target) {
|
|
21
|
-
console.error('❌ PresetThumbnail: node not found', nodeId)
|
|
22
|
-
return
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// Compute each mesh's transform relative to the target node (cancels world
|
|
26
|
-
// position/rotation), so the item is always rendered at origin with a known
|
|
27
|
-
// neutral orientation regardless of where it's placed in the scene.
|
|
28
|
-
target.updateWorldMatrix(true, true)
|
|
29
|
-
const targetInverse = new THREE.Matrix4().copy(target.matrixWorld).invert()
|
|
30
|
-
const relMatrix = new THREE.Matrix4()
|
|
31
|
-
|
|
32
|
-
const clones: THREE.Object3D[] = []
|
|
33
|
-
target.traverse((obj) => {
|
|
34
|
-
if (
|
|
35
|
-
!(obj instanceof THREE.Mesh || obj instanceof THREE.Line || obj instanceof THREE.Points)
|
|
36
|
-
)
|
|
37
|
-
return
|
|
38
|
-
const c = obj.clone(false) // shallow clone: copies geometry, material, visible — no children
|
|
39
|
-
relMatrix.multiplyMatrices(targetInverse, obj.matrixWorld)
|
|
40
|
-
relMatrix.decompose(c.position, c.quaternion, c.scale)
|
|
41
|
-
scene.add(c)
|
|
42
|
-
clones.push(c)
|
|
43
|
-
})
|
|
44
|
-
|
|
45
|
-
if (clones.length === 0) {
|
|
46
|
-
console.error('❌ PresetThumbnail: no renderable objects found', nodeId)
|
|
47
|
-
return
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// Combined bounding box across all clones
|
|
51
|
-
const box = new THREE.Box3()
|
|
52
|
-
for (const c of clones) box.expandByObject(c)
|
|
53
|
-
|
|
54
|
-
if (box.isEmpty()) {
|
|
55
|
-
for (const c of clones) scene.remove(c)
|
|
56
|
-
console.error('❌ PresetThumbnail: empty bounding box', nodeId)
|
|
57
|
-
return
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const sphere = new THREE.Sphere()
|
|
61
|
-
box.getBoundingSphere(sphere)
|
|
62
|
-
|
|
63
|
-
// Camera: aspect matches canvas (center-cropped to square after render)
|
|
64
|
-
const { width, height } = gl.domElement
|
|
65
|
-
const camera = new THREE.PerspectiveCamera(CAMERA_FOV, width / height, 0.01, 1000)
|
|
66
|
-
const dir = new THREE.Vector3(-0.5, 0.5, 0.5).normalize()
|
|
67
|
-
const fovRad = (CAMERA_FOV * Math.PI) / 180
|
|
68
|
-
const dist = (sphere.radius / Math.tan(fovRad / 2)) * 1.3
|
|
69
|
-
camera.position.copy(sphere.center).addScaledVector(dir, dist)
|
|
70
|
-
camera.lookAt(sphere.center)
|
|
71
|
-
camera.updateProjectionMatrix()
|
|
72
|
-
|
|
73
|
-
// Hide all scene geometry except the clones — leave lights, cameras, etc. intact
|
|
74
|
-
const cloneSet = new Set<THREE.Object3D>(clones)
|
|
75
|
-
const snapshot = new Map<THREE.Object3D, boolean>()
|
|
76
|
-
scene.traverse((obj) => {
|
|
77
|
-
if (cloneSet.has(obj)) return
|
|
78
|
-
if (
|
|
79
|
-
!(obj instanceof THREE.Mesh || obj instanceof THREE.Line || obj instanceof THREE.Points)
|
|
80
|
-
)
|
|
81
|
-
return
|
|
82
|
-
snapshot.set(obj, obj.visible)
|
|
83
|
-
obj.visible = false
|
|
84
|
-
})
|
|
85
|
-
|
|
86
|
-
gl.render(scene, camera)
|
|
87
|
-
|
|
88
|
-
// Restore visibility and remove clones
|
|
89
|
-
snapshot.forEach((wasVisible, obj) => {
|
|
90
|
-
obj.visible = wasVisible
|
|
91
|
-
})
|
|
92
|
-
for (const c of clones) scene.remove(c)
|
|
93
|
-
|
|
94
|
-
// Center-crop to square and scale to THUMBNAIL_SIZE
|
|
95
|
-
const minDim = Math.min(width, height)
|
|
96
|
-
const sx = Math.round((width - minDim) / 2)
|
|
97
|
-
const sy = Math.round((height - minDim) / 2)
|
|
98
|
-
const offscreen = document.createElement('canvas')
|
|
99
|
-
offscreen.width = THUMBNAIL_SIZE
|
|
100
|
-
offscreen.height = THUMBNAIL_SIZE
|
|
101
|
-
const ctx = offscreen.getContext('2d')!
|
|
102
|
-
ctx.drawImage(gl.domElement, sx, sy, minDim, minDim, 0, 0, THUMBNAIL_SIZE, THUMBNAIL_SIZE)
|
|
103
|
-
|
|
104
|
-
offscreen.toBlob(async (blob) => {
|
|
105
|
-
if (!blob) {
|
|
106
|
-
console.error('❌ PresetThumbnail: failed to create blob')
|
|
107
|
-
return
|
|
108
|
-
}
|
|
109
|
-
if (!adapter.uploadPresetThumbnail) return
|
|
110
|
-
const thumbnailUrl = await adapter.uploadPresetThumbnail(presetId, blob)
|
|
111
|
-
if (thumbnailUrl) {
|
|
112
|
-
emitter.emit('preset:thumbnail-updated', { presetId, thumbnailUrl })
|
|
113
|
-
}
|
|
114
|
-
}, 'image/png')
|
|
115
|
-
},
|
|
116
|
-
[gl, scene, adapter],
|
|
117
|
-
)
|
|
118
|
-
|
|
119
|
-
useEffect(() => {
|
|
120
|
-
emitter.on('preset:generate-thumbnail', generate)
|
|
121
|
-
return () => emitter.off('preset:generate-thumbnail', generate)
|
|
122
|
-
}, [generate])
|
|
123
|
-
|
|
124
|
-
return null
|
|
125
|
-
}
|
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import type { Point2D, RoofNode, RoofSegmentNode } from '@pascal-app/core'
|
|
4
|
-
import { memo } from 'react'
|
|
5
|
-
import { toSvgX, toSvgY } from '../svg-paths'
|
|
6
|
-
|
|
7
|
-
type FloorplanLineSegment = {
|
|
8
|
-
start: Point2D
|
|
9
|
-
end: Point2D
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
type FloorplanRoofSegmentEntry = {
|
|
13
|
-
segment: RoofSegmentNode
|
|
14
|
-
points: string
|
|
15
|
-
ridgeLine: FloorplanLineSegment | null
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
type FloorplanRoofEntry = {
|
|
19
|
-
roof: RoofNode
|
|
20
|
-
segments: FloorplanRoofSegmentEntry[]
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
type FloorplanRoofPalette = {
|
|
24
|
-
roofFill: string
|
|
25
|
-
roofActiveFill: string
|
|
26
|
-
roofSelectedFill: string
|
|
27
|
-
roofStroke: string
|
|
28
|
-
roofActiveStroke: string
|
|
29
|
-
roofSelectedStroke: string
|
|
30
|
-
roofRidgeStroke: string
|
|
31
|
-
roofSelectedRidgeStroke: string
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
type FloorplanRoofLayerProps = {
|
|
35
|
-
highlightedIdSet: ReadonlySet<string>
|
|
36
|
-
palette: FloorplanRoofPalette
|
|
37
|
-
roofEntries: FloorplanRoofEntry[]
|
|
38
|
-
selectedIdSet: ReadonlySet<string>
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export const FloorplanRoofLayer = memo(function FloorplanRoofLayer({
|
|
42
|
-
highlightedIdSet,
|
|
43
|
-
palette,
|
|
44
|
-
roofEntries,
|
|
45
|
-
selectedIdSet,
|
|
46
|
-
}: FloorplanRoofLayerProps) {
|
|
47
|
-
if (roofEntries.length === 0) {
|
|
48
|
-
return null
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
return (
|
|
52
|
-
<>
|
|
53
|
-
{roofEntries.map(({ roof, segments }) => {
|
|
54
|
-
const roofSelected = selectedIdSet.has(roof.id)
|
|
55
|
-
const roofHighlighted = highlightedIdSet.has(roof.id)
|
|
56
|
-
const hasSelectedSegment = segments.some(({ segment }) => selectedIdSet.has(segment.id))
|
|
57
|
-
const hasHighlightedSegment = segments.some(({ segment }) =>
|
|
58
|
-
highlightedIdSet.has(segment.id),
|
|
59
|
-
)
|
|
60
|
-
const isRoofActive =
|
|
61
|
-
roofSelected || roofHighlighted || hasSelectedSegment || hasHighlightedSegment
|
|
62
|
-
|
|
63
|
-
return (
|
|
64
|
-
<g key={roof.id} pointerEvents="none">
|
|
65
|
-
{segments.map(({ points, ridgeLine, segment }) => {
|
|
66
|
-
const isSegmentSelected = selectedIdSet.has(segment.id)
|
|
67
|
-
const isSegmentHighlighted = highlightedIdSet.has(segment.id)
|
|
68
|
-
const isSegmentActive = isSegmentSelected || isSegmentHighlighted
|
|
69
|
-
|
|
70
|
-
return (
|
|
71
|
-
<g key={segment.id}>
|
|
72
|
-
<polygon
|
|
73
|
-
fill={
|
|
74
|
-
isSegmentActive
|
|
75
|
-
? palette.roofSelectedFill
|
|
76
|
-
: isRoofActive
|
|
77
|
-
? palette.roofActiveFill
|
|
78
|
-
: palette.roofFill
|
|
79
|
-
}
|
|
80
|
-
points={points}
|
|
81
|
-
stroke={
|
|
82
|
-
isSegmentActive
|
|
83
|
-
? palette.roofSelectedStroke
|
|
84
|
-
: isRoofActive
|
|
85
|
-
? palette.roofActiveStroke
|
|
86
|
-
: palette.roofStroke
|
|
87
|
-
}
|
|
88
|
-
strokeWidth={isSegmentActive ? '2.25' : isRoofActive ? '1.75' : '1.1'}
|
|
89
|
-
vectorEffect="non-scaling-stroke"
|
|
90
|
-
/>
|
|
91
|
-
{ridgeLine ? (
|
|
92
|
-
<line
|
|
93
|
-
fill="none"
|
|
94
|
-
stroke={
|
|
95
|
-
isSegmentActive ? palette.roofSelectedRidgeStroke : palette.roofRidgeStroke
|
|
96
|
-
}
|
|
97
|
-
strokeWidth={isSegmentActive ? '2' : '1.4'}
|
|
98
|
-
vectorEffect="non-scaling-stroke"
|
|
99
|
-
x1={toSvgX(ridgeLine.start.x)}
|
|
100
|
-
x2={toSvgX(ridgeLine.end.x)}
|
|
101
|
-
y1={toSvgY(ridgeLine.start.y)}
|
|
102
|
-
y2={toSvgY(ridgeLine.end.y)}
|
|
103
|
-
/>
|
|
104
|
-
) : null}
|
|
105
|
-
</g>
|
|
106
|
-
)
|
|
107
|
-
})}
|
|
108
|
-
</g>
|
|
109
|
-
)
|
|
110
|
-
})}
|
|
111
|
-
</>
|
|
112
|
-
)
|
|
113
|
-
})
|
|
@@ -1,157 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
type BuildingNode,
|
|
5
|
-
emitter,
|
|
6
|
-
type GridEvent,
|
|
7
|
-
sceneRegistry,
|
|
8
|
-
useScene,
|
|
9
|
-
} from '@pascal-app/core'
|
|
10
|
-
import { useViewer } from '@pascal-app/viewer'
|
|
11
|
-
import { useFrame } from '@react-three/fiber'
|
|
12
|
-
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
13
|
-
import * as THREE from 'three'
|
|
14
|
-
import { markToolCancelConsumed } from '../../../hooks/use-keyboard'
|
|
15
|
-
import { sfxEmitter } from '../../../lib/sfx-bus'
|
|
16
|
-
import useEditor from '../../../store/use-editor'
|
|
17
|
-
import { CursorSphere } from '../shared/cursor-sphere'
|
|
18
|
-
|
|
19
|
-
export function MoveBuildingContent({ node }: { node: BuildingNode }) {
|
|
20
|
-
const previousGridPosRef = useRef<[number, number] | null>(null)
|
|
21
|
-
|
|
22
|
-
// Stable refs so the effect never needs node in its dependency array
|
|
23
|
-
const nodeIdRef = useRef(node.id)
|
|
24
|
-
const originalPositionRef = useRef<[number, number, number]>([...node.position] as [
|
|
25
|
-
number,
|
|
26
|
-
number,
|
|
27
|
-
number,
|
|
28
|
-
])
|
|
29
|
-
const originalRotationRef = useRef<number>(node.rotation[1] ?? 0)
|
|
30
|
-
const pendingRotationRef = useRef<number>(node.rotation[1] ?? 0)
|
|
31
|
-
|
|
32
|
-
const [cursorWorldPos, setCursorWorldPos] = useState<[number, number, number]>(() => {
|
|
33
|
-
const obj = sceneRegistry.nodes.get(node.id)
|
|
34
|
-
if (obj) {
|
|
35
|
-
const pos = new THREE.Vector3()
|
|
36
|
-
obj.getWorldPosition(pos)
|
|
37
|
-
return [pos.x, pos.y, pos.z]
|
|
38
|
-
}
|
|
39
|
-
return [node.position[0], node.position[1], node.position[2]]
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
const exitMoveMode = useCallback(() => {
|
|
43
|
-
useEditor.getState().setMovingNode(null)
|
|
44
|
-
}, [])
|
|
45
|
-
|
|
46
|
-
useEffect(() => {
|
|
47
|
-
const nodeId = nodeIdRef.current
|
|
48
|
-
const originalPosition = originalPositionRef.current
|
|
49
|
-
|
|
50
|
-
useScene.temporal.getState().pause()
|
|
51
|
-
|
|
52
|
-
let wasCommitted = false
|
|
53
|
-
|
|
54
|
-
const onKeyDown = (event: KeyboardEvent) => {
|
|
55
|
-
if (event.target instanceof HTMLInputElement || event.target instanceof HTMLTextAreaElement) {
|
|
56
|
-
return
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const ROTATION_STEP = Math.PI / 2
|
|
60
|
-
let rotationDelta = 0
|
|
61
|
-
if (event.key === 'r' || event.key === 'R') rotationDelta = ROTATION_STEP
|
|
62
|
-
else if (event.key === 't' || event.key === 'T') rotationDelta = -ROTATION_STEP
|
|
63
|
-
|
|
64
|
-
if (rotationDelta !== 0) {
|
|
65
|
-
event.preventDefault()
|
|
66
|
-
sfxEmitter.emit('sfx:item-rotate')
|
|
67
|
-
pendingRotationRef.current += rotationDelta
|
|
68
|
-
|
|
69
|
-
const mesh = sceneRegistry.nodes.get(nodeId)
|
|
70
|
-
if (mesh) mesh.rotation.y = pendingRotationRef.current
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const onGridMove = (event: GridEvent) => {
|
|
75
|
-
const gridX = Math.round(event.position[0] * 2) / 2
|
|
76
|
-
const gridZ = Math.round(event.position[2] * 2) / 2
|
|
77
|
-
|
|
78
|
-
if (
|
|
79
|
-
previousGridPosRef.current &&
|
|
80
|
-
(gridX !== previousGridPosRef.current[0] || gridZ !== previousGridPosRef.current[1])
|
|
81
|
-
) {
|
|
82
|
-
sfxEmitter.emit('sfx:grid-snap')
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
previousGridPosRef.current = [gridX, gridZ]
|
|
86
|
-
setCursorWorldPos([gridX, 0, gridZ])
|
|
87
|
-
|
|
88
|
-
// Directly update the Three.js group — no store update during drag
|
|
89
|
-
const mesh = sceneRegistry.nodes.get(nodeId)
|
|
90
|
-
if (mesh) {
|
|
91
|
-
mesh.position.x = gridX
|
|
92
|
-
mesh.position.z = gridZ
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const onGridClick = (event: GridEvent) => {
|
|
97
|
-
const gridX = Math.round(event.position[0] * 2) / 2
|
|
98
|
-
const gridZ = Math.round(event.position[2] * 2) / 2
|
|
99
|
-
|
|
100
|
-
wasCommitted = true
|
|
101
|
-
|
|
102
|
-
useScene.temporal.getState().resume()
|
|
103
|
-
useScene.getState().updateNode(nodeId, {
|
|
104
|
-
position: [gridX, originalPosition[1], gridZ],
|
|
105
|
-
rotation: [0, pendingRotationRef.current, 0],
|
|
106
|
-
})
|
|
107
|
-
useScene.temporal.getState().pause()
|
|
108
|
-
|
|
109
|
-
sfxEmitter.emit('sfx:item-place')
|
|
110
|
-
useViewer.getState().setSelection({ buildingId: nodeId as BuildingNode['id'] })
|
|
111
|
-
exitMoveMode()
|
|
112
|
-
event.nativeEvent?.stopPropagation?.()
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
const onCancel = () => {
|
|
116
|
-
// Revert mesh position and rotation immediately
|
|
117
|
-
const mesh = sceneRegistry.nodes.get(nodeId)
|
|
118
|
-
if (mesh) {
|
|
119
|
-
mesh.position.x = originalPosition[0]
|
|
120
|
-
mesh.position.z = originalPosition[2]
|
|
121
|
-
mesh.rotation.y = originalRotationRef.current
|
|
122
|
-
}
|
|
123
|
-
pendingRotationRef.current = originalRotationRef.current
|
|
124
|
-
// Restore building selection
|
|
125
|
-
useViewer.getState().setSelection({ buildingId: nodeId as BuildingNode['id'] })
|
|
126
|
-
useScene.temporal.getState().resume()
|
|
127
|
-
// Tell the keyboard handler we handled this, so it doesn't also clear the selection
|
|
128
|
-
markToolCancelConsumed()
|
|
129
|
-
exitMoveMode()
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
emitter.on('grid:move', onGridMove)
|
|
133
|
-
emitter.on('grid:click', onGridClick)
|
|
134
|
-
emitter.on('tool:cancel', onCancel)
|
|
135
|
-
window.addEventListener('keydown', onKeyDown)
|
|
136
|
-
|
|
137
|
-
return () => {
|
|
138
|
-
if (!wasCommitted) {
|
|
139
|
-
useScene.getState().updateNode(nodeId, {
|
|
140
|
-
position: originalPosition,
|
|
141
|
-
rotation: [0, originalRotationRef.current, 0],
|
|
142
|
-
})
|
|
143
|
-
}
|
|
144
|
-
useScene.temporal.getState().resume()
|
|
145
|
-
emitter.off('grid:move', onGridMove)
|
|
146
|
-
emitter.off('grid:click', onGridClick)
|
|
147
|
-
emitter.off('tool:cancel', onCancel)
|
|
148
|
-
window.removeEventListener('keydown', onKeyDown)
|
|
149
|
-
}
|
|
150
|
-
}, [exitMoveMode]) // stable — node values captured via refs at mount
|
|
151
|
-
|
|
152
|
-
return (
|
|
153
|
-
<group>
|
|
154
|
-
<CursorSphere position={cursorWorldPos} showTooltip={false} />
|
|
155
|
-
</group>
|
|
156
|
-
)
|
|
157
|
-
}
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import { type CeilingNode, resolveLevelId, useScene } from '@pascal-app/core'
|
|
2
|
-
import { useViewer } from '@pascal-app/viewer'
|
|
3
|
-
import { useCallback } from 'react'
|
|
4
|
-
import { PolygonEditor } from '../shared/polygon-editor'
|
|
5
|
-
|
|
6
|
-
interface CeilingBoundaryEditorProps {
|
|
7
|
-
ceilingId: CeilingNode['id']
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Ceiling boundary editor - allows editing ceiling polygon vertices for a specific ceiling
|
|
12
|
-
* Uses the generic PolygonEditor component
|
|
13
|
-
*/
|
|
14
|
-
export const CeilingBoundaryEditor: React.FC<CeilingBoundaryEditorProps> = ({ ceilingId }) => {
|
|
15
|
-
const ceilingNode = useScene((state) => state.nodes[ceilingId])
|
|
16
|
-
const updateNode = useScene((state) => state.updateNode)
|
|
17
|
-
const setSelection = useViewer((state) => state.setSelection)
|
|
18
|
-
|
|
19
|
-
const ceiling = ceilingNode?.type === 'ceiling' ? (ceilingNode as CeilingNode) : null
|
|
20
|
-
|
|
21
|
-
const handlePolygonChange = useCallback(
|
|
22
|
-
(newPolygon: Array<[number, number]>) => {
|
|
23
|
-
updateNode(ceilingId, { polygon: newPolygon })
|
|
24
|
-
// Re-assert selection so the ceiling stays selected after the edit
|
|
25
|
-
setSelection({ selectedIds: [ceilingId] })
|
|
26
|
-
},
|
|
27
|
-
[ceilingId, updateNode, setSelection],
|
|
28
|
-
)
|
|
29
|
-
|
|
30
|
-
if (!ceiling?.polygon || ceiling.polygon.length < 3) return null
|
|
31
|
-
|
|
32
|
-
return (
|
|
33
|
-
<PolygonEditor
|
|
34
|
-
allowEdgeMove
|
|
35
|
-
color="#d4d4d4"
|
|
36
|
-
levelId={resolveLevelId(ceiling, useScene.getState().nodes)}
|
|
37
|
-
minVertices={3}
|
|
38
|
-
onPolygonChange={handlePolygonChange}
|
|
39
|
-
polygon={ceiling.polygon}
|
|
40
|
-
surfaceHeight={ceiling.height ?? 2.5}
|
|
41
|
-
/>
|
|
42
|
-
)
|
|
43
|
-
}
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import { type CeilingNode, resolveLevelId, useScene } from '@pascal-app/core'
|
|
2
|
-
import { useViewer } from '@pascal-app/viewer'
|
|
3
|
-
import { useCallback } from 'react'
|
|
4
|
-
import { PolygonEditor } from '../shared/polygon-editor'
|
|
5
|
-
|
|
6
|
-
interface CeilingHoleEditorProps {
|
|
7
|
-
ceilingId: CeilingNode['id']
|
|
8
|
-
holeIndex: number
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Ceiling hole editor - allows editing a specific hole polygon within a ceiling
|
|
13
|
-
* Uses the generic PolygonEditor component
|
|
14
|
-
*/
|
|
15
|
-
export const CeilingHoleEditor: React.FC<CeilingHoleEditorProps> = ({ ceilingId, holeIndex }) => {
|
|
16
|
-
const ceilingNode = useScene((state) => state.nodes[ceilingId])
|
|
17
|
-
const updateNode = useScene((state) => state.updateNode)
|
|
18
|
-
const setSelection = useViewer((state) => state.setSelection)
|
|
19
|
-
|
|
20
|
-
const ceiling = ceilingNode?.type === 'ceiling' ? (ceilingNode as CeilingNode) : null
|
|
21
|
-
const holes = ceiling?.holes || []
|
|
22
|
-
const hole = holes[holeIndex]
|
|
23
|
-
|
|
24
|
-
const handlePolygonChange = useCallback(
|
|
25
|
-
(newPolygon: Array<[number, number]>) => {
|
|
26
|
-
const updatedHoles = [...holes]
|
|
27
|
-
updatedHoles[holeIndex] = newPolygon
|
|
28
|
-
updateNode(ceilingId, { holes: updatedHoles })
|
|
29
|
-
// Re-assert selection so the ceiling stays selected after the edit
|
|
30
|
-
setSelection({ selectedIds: [ceilingId] })
|
|
31
|
-
},
|
|
32
|
-
[ceilingId, holeIndex, holes, updateNode, setSelection],
|
|
33
|
-
)
|
|
34
|
-
|
|
35
|
-
if (!(ceiling && hole) || hole.length < 3) return null
|
|
36
|
-
|
|
37
|
-
return (
|
|
38
|
-
<PolygonEditor
|
|
39
|
-
allowEdgeMove
|
|
40
|
-
allowPolygonMove
|
|
41
|
-
color="#ef4444"
|
|
42
|
-
levelId={resolveLevelId(ceiling, useScene.getState().nodes)} // red for holes
|
|
43
|
-
minVertices={3}
|
|
44
|
-
onPolygonChange={handlePolygonChange}
|
|
45
|
-
polygon={hole}
|
|
46
|
-
surfaceHeight={ceiling.height ?? 2.5}
|
|
47
|
-
/>
|
|
48
|
-
)
|
|
49
|
-
}
|