@pascal-app/editor 0.4.0 → 0.5.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 +5 -5
- package/src/components/editor/floating-action-menu.tsx +101 -29
- package/src/components/editor/floating-building-action-menu.tsx +69 -0
- package/src/components/editor/floorplan-panel.tsx +31 -13
- package/src/components/editor/index.tsx +219 -167
- package/src/components/editor/node-action-menu.tsx +26 -10
- package/src/components/editor/selection-manager.tsx +38 -2
- package/src/components/editor/thumbnail-generator.tsx +245 -64
- package/src/components/systems/stair/stair-edit-system.tsx +27 -5
- package/src/components/tools/building/move-building-tool.tsx +157 -0
- package/src/components/tools/door/door-math.ts +1 -1
- package/src/components/tools/door/door-tool.tsx +19 -7
- package/src/components/tools/door/move-door-tool.tsx +17 -8
- package/src/components/tools/fence/fence-drafting.ts +125 -0
- package/src/components/tools/fence/fence-tool.tsx +190 -0
- package/src/components/tools/fence/move-fence-tool.tsx +223 -0
- package/src/components/tools/item/item-tool.tsx +3 -3
- package/src/components/tools/item/move-tool.tsx +7 -0
- package/src/components/tools/item/placement-strategies.ts +15 -7
- package/src/components/tools/item/use-placement-coordinator.tsx +89 -14
- package/src/components/tools/roof/move-roof-tool.tsx +5 -2
- package/src/components/tools/roof/roof-tool.tsx +6 -6
- package/src/components/tools/select/box-select-tool.tsx +2 -2
- package/src/components/tools/shared/polygon-editor.tsx +2 -2
- package/src/components/tools/slab/slab-tool.tsx +4 -4
- package/src/components/tools/stair/stair-defaults.ts +10 -0
- package/src/components/tools/stair/stair-tool.tsx +29 -6
- package/src/components/tools/tool-manager.tsx +42 -14
- package/src/components/tools/wall/wall-tool.tsx +19 -29
- package/src/components/tools/window/move-window-tool.tsx +17 -8
- package/src/components/tools/window/window-math.ts +1 -1
- package/src/components/tools/window/window-tool.tsx +19 -7
- package/src/components/tools/zone/zone-tool.tsx +7 -7
- package/src/components/ui/action-menu/structure-tools.tsx +1 -0
- package/src/components/ui/helpers/building-helper.tsx +32 -0
- package/src/components/ui/helpers/helper-manager.tsx +2 -0
- package/src/components/ui/panels/fence-panel.tsx +184 -0
- package/src/components/ui/panels/panel-manager.tsx +3 -0
- package/src/components/ui/panels/stair-panel.tsx +206 -33
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +22 -15
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +60 -52
- package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +35 -24
- package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +65 -0
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +59 -40
- package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +14 -13
- package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +59 -52
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +27 -22
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +66 -49
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +35 -36
- package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +66 -49
- package/src/components/ui/sidebar/panels/site-panel/tree-node-actions.tsx +11 -11
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +17 -14
- package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +57 -53
- package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +35 -24
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +22 -27
- package/src/components/viewer-overlay.tsx +1 -0
- package/src/hooks/use-auto-save.ts +3 -6
- package/src/hooks/use-contextual-tools.ts +10 -2
- package/src/hooks/use-grid-events.ts +13 -1
- package/src/hooks/use-keyboard.ts +4 -0
- package/src/store/use-editor.tsx +7 -0
|
@@ -1,62 +1,57 @@
|
|
|
1
|
-
import { useScene, type ZoneNode } from '@pascal-app/core'
|
|
1
|
+
import { type AnyNodeId, useScene, type ZoneNode } from '@pascal-app/core'
|
|
2
2
|
import { useViewer } from '@pascal-app/viewer'
|
|
3
|
-
import { useState } from 'react'
|
|
3
|
+
import { useCallback, useState } from 'react'
|
|
4
4
|
import { ColorDot } from './../../../../../components/ui/primitives/color-dot'
|
|
5
5
|
import { InlineRenameInput } from './inline-rename-input'
|
|
6
6
|
import { focusTreeNode, TreeNodeWrapper } from './tree-node'
|
|
7
7
|
import { TreeNodeActions } from './tree-node-actions'
|
|
8
8
|
|
|
9
9
|
interface ZoneTreeNodeProps {
|
|
10
|
-
|
|
10
|
+
nodeId: AnyNodeId
|
|
11
11
|
depth: number
|
|
12
12
|
isLast?: boolean
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
export function ZoneTreeNode({
|
|
15
|
+
export function ZoneTreeNode({ nodeId, depth, isLast }: ZoneTreeNodeProps) {
|
|
16
16
|
const [isEditing, setIsEditing] = useState(false)
|
|
17
17
|
const updateNode = useScene((state) => state.updateNode)
|
|
18
|
-
const
|
|
19
|
-
const
|
|
18
|
+
const isVisible = useScene((s) => s.nodes[nodeId]?.visible !== false)
|
|
19
|
+
const color = useScene((s) => (s.nodes[nodeId] as ZoneNode | undefined)?.color)
|
|
20
|
+
const polygon = useScene((s) => (s.nodes[nodeId] as ZoneNode | undefined)?.polygon ?? [])
|
|
21
|
+
const isSelected = useViewer((state) => state.selection.zoneId === nodeId)
|
|
22
|
+
const isHovered = useViewer((state) => state.hoveredId === nodeId)
|
|
20
23
|
const setSelection = useViewer((state) => state.setSelection)
|
|
21
24
|
const setHoveredId = useViewer((state) => state.setHoveredId)
|
|
22
25
|
|
|
23
|
-
const handleClick = () => {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const handleMouseEnter = () => {
|
|
32
|
-
setHoveredId(node.id)
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const handleMouseLeave = () => {
|
|
36
|
-
setHoveredId(null)
|
|
37
|
-
}
|
|
26
|
+
const handleClick = useCallback(() => setSelection({ zoneId: nodeId }), [nodeId, setSelection])
|
|
27
|
+
const handleDoubleClick = useCallback(() => focusTreeNode(nodeId), [nodeId])
|
|
28
|
+
const handleMouseEnter = useCallback(() => setHoveredId(nodeId), [nodeId, setHoveredId])
|
|
29
|
+
const handleMouseLeave = useCallback(() => setHoveredId(null), [setHoveredId])
|
|
30
|
+
const handleStartEditing = useCallback(() => setIsEditing(true), [])
|
|
31
|
+
const handleStopEditing = useCallback(() => setIsEditing(false), [])
|
|
38
32
|
|
|
39
33
|
// Calculate approximate area from polygon
|
|
40
|
-
const area = calculatePolygonArea(
|
|
34
|
+
const area = calculatePolygonArea(polygon).toFixed(1)
|
|
41
35
|
const defaultName = `Zone (${area}m²)`
|
|
42
36
|
|
|
43
37
|
return (
|
|
44
38
|
<TreeNodeWrapper
|
|
45
|
-
actions={<TreeNodeActions
|
|
39
|
+
actions={<TreeNodeActions nodeId={nodeId} />}
|
|
46
40
|
depth={depth}
|
|
47
41
|
expanded={false}
|
|
48
42
|
hasChildren={false}
|
|
49
|
-
icon={<ColorDot color={
|
|
43
|
+
icon={<ColorDot color={color} onChange={(c) => updateNode(nodeId, { color: c })} />}
|
|
50
44
|
isHovered={isHovered}
|
|
51
45
|
isLast={isLast}
|
|
52
46
|
isSelected={isSelected}
|
|
47
|
+
isVisible={isVisible}
|
|
53
48
|
label={
|
|
54
49
|
<InlineRenameInput
|
|
55
50
|
defaultName={defaultName}
|
|
56
51
|
isEditing={isEditing}
|
|
57
|
-
|
|
58
|
-
onStartEditing={
|
|
59
|
-
onStopEditing={
|
|
52
|
+
nodeId={nodeId}
|
|
53
|
+
onStartEditing={handleStartEditing}
|
|
54
|
+
onStopEditing={handleStopEditing}
|
|
60
55
|
/>
|
|
61
56
|
}
|
|
62
57
|
onClick={handleClick}
|
|
@@ -62,6 +62,7 @@ const wallModeConfig = {
|
|
|
62
62
|
const getNodeName = (node: AnyNode): string => {
|
|
63
63
|
if ('name' in node && node.name) return node.name
|
|
64
64
|
if (node.type === 'wall') return 'Wall'
|
|
65
|
+
if (node.type === 'fence') return 'Fence'
|
|
65
66
|
if (node.type === 'item') return (node as { asset: { name: string } }).asset?.name || 'Item'
|
|
66
67
|
if (node.type === 'slab') return 'Slab'
|
|
67
68
|
if (node.type === 'ceiling') return 'Ceiling'
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import { useScene } from '@pascal-app/core'
|
|
4
|
-
import { type MutableRefObject, useCallback, useEffect, useRef
|
|
4
|
+
import { type MutableRefObject, useCallback, useEffect, useRef } from 'react'
|
|
5
5
|
import { type SceneGraph, saveSceneToLocalStorage } from '../lib/scene'
|
|
6
6
|
|
|
7
7
|
const AUTOSAVE_DEBOUNCE_MS = 1000
|
|
@@ -26,9 +26,7 @@ export function useAutoSave({
|
|
|
26
26
|
onDirty,
|
|
27
27
|
onSaveStatusChange,
|
|
28
28
|
isVersionPreviewMode = false,
|
|
29
|
-
}: UseAutoSaveOptions): {
|
|
30
|
-
const [saveStatus, _setSaveStatus] = useState<SaveStatus>('idle')
|
|
31
|
-
|
|
29
|
+
}: UseAutoSaveOptions): { isLoadingSceneRef: MutableRefObject<boolean> } {
|
|
32
30
|
const saveTimeoutRef = useRef<NodeJS.Timeout | undefined>(undefined)
|
|
33
31
|
const isSavingRef = useRef(false)
|
|
34
32
|
const isLoadingSceneRef = useRef(false)
|
|
@@ -56,7 +54,6 @@ export function useAutoSave({
|
|
|
56
54
|
}, [isVersionPreviewMode])
|
|
57
55
|
|
|
58
56
|
const setSaveStatus = useCallback((status: SaveStatus) => {
|
|
59
|
-
_setSaveStatus(status)
|
|
60
57
|
onSaveStatusChangeRef.current?.(status)
|
|
61
58
|
}, [])
|
|
62
59
|
|
|
@@ -190,5 +187,5 @@ export function useAutoSave({
|
|
|
190
187
|
setSaveStatus('saved')
|
|
191
188
|
}, [isVersionPreviewMode, setSaveStatus])
|
|
192
189
|
|
|
193
|
-
return {
|
|
190
|
+
return { isLoadingSceneRef }
|
|
194
191
|
}
|
|
@@ -16,7 +16,15 @@ export function useContextualTools() {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
// Default tools when nothing is selected
|
|
19
|
-
const defaultTools: StructureTool[] = [
|
|
19
|
+
const defaultTools: StructureTool[] = [
|
|
20
|
+
'wall',
|
|
21
|
+
'fence',
|
|
22
|
+
'slab',
|
|
23
|
+
'ceiling',
|
|
24
|
+
'roof',
|
|
25
|
+
'door',
|
|
26
|
+
'window',
|
|
27
|
+
]
|
|
20
28
|
|
|
21
29
|
if (selection.selectedIds.length === 0) {
|
|
22
30
|
return defaultTools
|
|
@@ -29,7 +37,7 @@ export function useContextualTools() {
|
|
|
29
37
|
|
|
30
38
|
// If a wall is selected, prioritize wall-hosted elements
|
|
31
39
|
if (selectedTypes.has('wall')) {
|
|
32
|
-
return ['window', 'door', 'wall'] as StructureTool[]
|
|
40
|
+
return ['window', 'door', 'wall', 'fence'] as StructureTool[]
|
|
33
41
|
}
|
|
34
42
|
|
|
35
43
|
// If a slab is selected, prioritize slab editing
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
type AnyNodeId,
|
|
3
|
+
type EventSuffix,
|
|
4
|
+
emitter,
|
|
5
|
+
type GridEvent,
|
|
6
|
+
sceneRegistry,
|
|
7
|
+
} from '@pascal-app/core'
|
|
2
8
|
import { useViewer } from '@pascal-app/viewer'
|
|
3
9
|
import { useThree } from '@react-three/fiber'
|
|
4
10
|
import { useEffect, useRef } from 'react'
|
|
@@ -44,9 +50,15 @@ export function useGridEvents(gridY: number) {
|
|
|
44
50
|
const point = getIntersection(nativeEvent)
|
|
45
51
|
if (!point) return
|
|
46
52
|
|
|
53
|
+
// Convert world-space point to building-local for tools that live inside a building.
|
|
54
|
+
const buildingId = useViewer.getState().selection.buildingId
|
|
55
|
+
const buildingMesh = buildingId ? sceneRegistry.nodes.get(buildingId as AnyNodeId) : null
|
|
56
|
+
const localPoint = buildingMesh ? buildingMesh.worldToLocal(point.clone()) : point
|
|
57
|
+
|
|
47
58
|
const eventKey = `grid:${suffix}` as `grid:${EventSuffix}`
|
|
48
59
|
const payload: GridEvent = {
|
|
49
60
|
position: [point.x, point.y, point.z],
|
|
61
|
+
localPosition: [localPoint.x, localPoint.y, localPoint.z],
|
|
50
62
|
nativeEvent: nativeEvent as any, // Type compatibility with ThreeEvent
|
|
51
63
|
}
|
|
52
64
|
|
|
@@ -84,6 +84,10 @@ export const useKeyboard = ({ isVersionPreviewMode = false } = {}) => {
|
|
|
84
84
|
useEditor.getState().setPhase('structure')
|
|
85
85
|
useEditor.getState().setStructureLayer('elements')
|
|
86
86
|
useEditor.getState().setMode('build')
|
|
87
|
+
} else if (e.key === 'd' && !e.metaKey && !e.ctrlKey) {
|
|
88
|
+
if (isVersionPreviewMode) return
|
|
89
|
+
e.preventDefault()
|
|
90
|
+
useEditor.getState().setMode('delete')
|
|
87
91
|
} else if (e.key === 'z' && (e.metaKey || e.ctrlKey)) {
|
|
88
92
|
if (isVersionPreviewMode) return
|
|
89
93
|
e.preventDefault()
|
package/src/store/use-editor.tsx
CHANGED
|
@@ -4,6 +4,7 @@ import type { AssetInput } from '@pascal-app/core'
|
|
|
4
4
|
import {
|
|
5
5
|
type BuildingNode,
|
|
6
6
|
type DoorNode,
|
|
7
|
+
type FenceNode,
|
|
7
8
|
type ItemNode,
|
|
8
9
|
type LevelNode,
|
|
9
10
|
type RoofNode,
|
|
@@ -33,6 +34,7 @@ export type Mode = 'select' | 'edit' | 'delete' | 'build'
|
|
|
33
34
|
// Structure mode tools (building elements)
|
|
34
35
|
export type StructureTool =
|
|
35
36
|
| 'wall'
|
|
37
|
+
| 'fence'
|
|
36
38
|
| 'room'
|
|
37
39
|
| 'custom-room'
|
|
38
40
|
| 'slab'
|
|
@@ -85,20 +87,24 @@ type EditorState = {
|
|
|
85
87
|
| ItemNode
|
|
86
88
|
| WindowNode
|
|
87
89
|
| DoorNode
|
|
90
|
+
| FenceNode
|
|
88
91
|
| RoofNode
|
|
89
92
|
| RoofSegmentNode
|
|
90
93
|
| StairNode
|
|
91
94
|
| StairSegmentNode
|
|
95
|
+
| BuildingNode
|
|
92
96
|
| null
|
|
93
97
|
setMovingNode: (
|
|
94
98
|
node:
|
|
95
99
|
| ItemNode
|
|
96
100
|
| WindowNode
|
|
97
101
|
| DoorNode
|
|
102
|
+
| FenceNode
|
|
98
103
|
| RoofNode
|
|
99
104
|
| RoofSegmentNode
|
|
100
105
|
| StairNode
|
|
101
106
|
| StairSegmentNode
|
|
107
|
+
| BuildingNode
|
|
102
108
|
| null,
|
|
103
109
|
) => void
|
|
104
110
|
selectedReferenceId: string | null
|
|
@@ -428,6 +434,7 @@ const useEditor = create<EditorState>()(
|
|
|
428
434
|
| RoofSegmentNode
|
|
429
435
|
| StairNode
|
|
430
436
|
| StairSegmentNode
|
|
437
|
+
| BuildingNode
|
|
431
438
|
| null,
|
|
432
439
|
setMovingNode: (node) => set({ movingNode: node }),
|
|
433
440
|
selectedReferenceId: null,
|