@pascal-app/editor 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +9 -5
- package/src/components/editor/bottom-sheet.tsx +149 -0
- package/src/components/editor/custom-camera-controls.tsx +75 -7
- package/src/components/editor/editor-layout-mobile.tsx +264 -0
- package/src/components/editor/editor-layout-v2.tsx +20 -0
- package/src/components/editor/first-person/build-collider-world.ts +365 -0
- package/src/components/editor/first-person/bvh-ecctrl.tsx +795 -0
- package/src/components/editor/first-person-controls.tsx +496 -143
- package/src/components/editor/floating-action-menu.tsx +32 -55
- package/src/components/editor/floorplan-background-selection.ts +113 -0
- package/src/components/editor/floorplan-panel.tsx +9855 -3298
- package/src/components/editor/index.tsx +269 -21
- package/src/components/editor/selection-manager.tsx +575 -13
- package/src/components/editor/thumbnail-generator.tsx +38 -7
- package/src/components/editor/use-floorplan-background-placement.ts +257 -0
- package/src/components/editor/use-floorplan-hit-testing.ts +171 -0
- package/src/components/editor/use-floorplan-scene-data.ts +189 -0
- package/src/components/editor/wall-measurement-label.tsx +267 -36
- package/src/components/editor-2d/floorplan-action-menu-layer.tsx +95 -0
- package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +160 -0
- package/src/components/editor-2d/floorplan-hotkey-handlers.tsx +92 -0
- package/src/components/editor-2d/renderers/floorplan-draft-layer.tsx +119 -0
- package/src/components/editor-2d/renderers/floorplan-marquee-layer.tsx +58 -0
- package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +197 -0
- package/src/components/editor-2d/renderers/floorplan-roof-layer.tsx +113 -0
- package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +474 -0
- package/src/components/editor-2d/svg-paths.ts +119 -0
- package/src/components/tools/ceiling/ceiling-boundary-editor.tsx +1 -0
- package/src/components/tools/ceiling/ceiling-hole-editor.tsx +1 -0
- package/src/components/tools/ceiling/ceiling-tool.tsx +5 -5
- package/src/components/tools/column/column-tool.tsx +97 -0
- package/src/components/tools/column/move-column-tool.tsx +105 -0
- package/src/components/tools/door/door-tool.tsx +7 -0
- package/src/components/tools/door/move-door-tool.tsx +28 -8
- package/src/components/tools/fence/fence-drafting.ts +10 -3
- package/src/components/tools/fence/fence-tool.tsx +159 -3
- package/src/components/tools/fence/move-fence-endpoint-tool.tsx +129 -18
- package/src/components/tools/fence/move-fence-tool.tsx +101 -34
- package/src/components/tools/item/move-tool.tsx +10 -1
- package/src/components/tools/item/placement-math.ts +30 -1
- package/src/components/tools/item/placement-strategies.ts +109 -31
- package/src/components/tools/item/placement-types.ts +7 -0
- package/src/components/tools/item/use-draft-node.ts +2 -0
- package/src/components/tools/item/use-placement-coordinator.tsx +660 -52
- package/src/components/tools/roof/move-roof-tool.tsx +22 -15
- package/src/components/tools/shared/polygon-editor.tsx +153 -28
- package/src/components/tools/shared/segment-angle.ts +156 -0
- package/src/components/tools/slab/slab-boundary-editor.tsx +1 -0
- package/src/components/tools/slab/slab-hole-editor.tsx +1 -0
- package/src/components/tools/spawn/move-spawn-tool.tsx +101 -0
- package/src/components/tools/spawn/spawn-tool.tsx +130 -0
- package/src/components/tools/tool-manager.tsx +18 -3
- package/src/components/tools/wall/move-wall-endpoint-tool.tsx +121 -20
- package/src/components/tools/wall/wall-drafting.ts +18 -9
- package/src/components/tools/wall/wall-tool.tsx +134 -2
- package/src/components/tools/window/move-window-tool.tsx +18 -0
- package/src/components/tools/window/window-tool.tsx +5 -0
- package/src/components/ui/action-menu/camera-actions.tsx +37 -33
- package/src/components/ui/action-menu/control-modes.tsx +28 -1
- package/src/components/ui/action-menu/index.tsx +91 -1
- package/src/components/ui/action-menu/structure-tools.tsx +2 -0
- package/src/components/ui/action-menu/view-toggles.tsx +424 -35
- package/src/components/ui/command-palette/editor-commands.tsx +18 -1
- package/src/components/ui/controls/material-picker.tsx +152 -165
- package/src/components/ui/controls/slider-control.tsx +66 -18
- package/src/components/ui/floating-level-selector.tsx +286 -55
- package/src/components/ui/helpers/helper-manager.tsx +5 -0
- package/src/components/ui/item-catalog/catalog-items.tsx +1116 -1219
- package/src/components/ui/item-catalog/item-catalog.tsx +42 -175
- package/src/components/ui/level-duplicate-dialog.tsx +115 -0
- package/src/components/ui/panels/ceiling-panel.tsx +1 -25
- package/src/components/ui/panels/column-panel.tsx +715 -0
- package/src/components/ui/panels/door-panel.tsx +981 -289
- package/src/components/ui/panels/fence-panel.tsx +3 -45
- package/src/components/ui/panels/mobile-panel-sheet.tsx +108 -0
- package/src/components/ui/panels/mobile-selection-bar.tsx +100 -0
- package/src/components/ui/panels/node-display.ts +39 -0
- package/src/components/ui/panels/paint-panel.tsx +138 -0
- package/src/components/ui/panels/panel-manager.tsx +210 -1
- package/src/components/ui/panels/panel-wrapper.tsx +48 -39
- package/src/components/ui/panels/reference-panel.tsx +238 -5
- package/src/components/ui/panels/roof-panel.tsx +4 -105
- package/src/components/ui/panels/roof-segment-panel.tsx +0 -25
- package/src/components/ui/panels/slab-panel.tsx +4 -30
- package/src/components/ui/panels/spawn-panel.tsx +155 -0
- package/src/components/ui/panels/stair-panel.tsx +11 -117
- package/src/components/ui/panels/stair-segment-panel.tsx +0 -25
- package/src/components/ui/panels/wall-panel.tsx +1 -95
- package/src/components/ui/panels/window-panel.tsx +660 -139
- package/src/components/ui/sidebar/mobile-tab-bar.tsx +46 -0
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +2 -2
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +2 -2
- package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +77 -0
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +109 -24
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +2 -2
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +82 -0
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +9 -3
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +8 -5
- package/src/components/ui/sidebar/tab-bar.tsx +3 -0
- package/src/components/ui/viewer-toolbar.tsx +42 -1
- package/src/hooks/use-auto-frame.ts +45 -0
- package/src/hooks/use-keyboard.ts +64 -7
- package/src/hooks/use-mobile.ts +12 -12
- package/src/lib/door-interaction.ts +88 -0
- package/src/lib/floorplan/geometry.ts +263 -0
- package/src/lib/floorplan/index.ts +38 -0
- package/src/lib/floorplan/items.ts +179 -0
- package/src/lib/floorplan/selection-tool.ts +231 -0
- package/src/lib/floorplan/stairs.ts +478 -0
- package/src/lib/floorplan/types.ts +57 -0
- package/src/lib/floorplan/walls.ts +23 -0
- package/src/lib/guide-events.ts +10 -0
- package/src/lib/level-duplication.test.ts +72 -0
- package/src/lib/level-duplication.ts +153 -0
- package/src/lib/local-guide-image.ts +42 -0
- package/src/lib/material-paint.ts +284 -0
- package/src/lib/roof-duplication.ts +214 -0
- package/src/lib/scene-bounds.test.ts +183 -0
- package/src/lib/scene-bounds.ts +169 -0
- package/src/lib/stair-duplication.ts +126 -0
- package/src/lib/window-interaction.ts +86 -0
- package/src/store/use-editor.tsx +164 -8
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import type { AssetInput } from '@pascal-app/core'
|
|
4
4
|
import { resolveCdnUrl } from '@pascal-app/viewer'
|
|
5
5
|
import Image from 'next/image'
|
|
6
|
-
import { useEffect
|
|
6
|
+
import { useEffect } from 'react'
|
|
7
7
|
import {
|
|
8
8
|
Tooltip,
|
|
9
9
|
TooltipContent,
|
|
@@ -13,54 +13,19 @@ import { cn } from './../../../lib/utils'
|
|
|
13
13
|
import useEditor, { type CatalogCategory } from './../../../store/use-editor'
|
|
14
14
|
import { CATALOG_ITEMS } from './catalog-items'
|
|
15
15
|
|
|
16
|
-
const PLACEMENT_TAGS = new Set(['floor', 'wall', 'ceiling', 'countertop'])
|
|
17
|
-
|
|
18
16
|
export function ItemCatalog({ category }: { category: CatalogCategory }) {
|
|
19
17
|
const selectedItem = useEditor((state) => state.selectedItem)
|
|
20
18
|
const setSelectedItem = useEditor((state) => state.setSelectedItem)
|
|
21
|
-
const [activePlacementTag, setActivePlacementTag] = useState<string | null>(null)
|
|
22
|
-
const [activeFunctionalTag, setActiveFunctionalTag] = useState<string | null>(null)
|
|
23
19
|
|
|
24
20
|
const categoryItems = CATALOG_ITEMS.filter((item) => item.category === category)
|
|
25
21
|
|
|
26
|
-
//
|
|
27
|
-
const allTags = Array.from(new Set(categoryItems.flatMap((item) => item.tags ?? [])))
|
|
28
|
-
const placementTags = allTags.filter((t) => PLACEMENT_TAGS.has(t))
|
|
29
|
-
const functionalTags = allTags.filter((t) => !PLACEMENT_TAGS.has(t))
|
|
30
|
-
const hasFilters = allTags.length > 1
|
|
31
|
-
|
|
32
|
-
// Count items for a placement tag given the current functional filter
|
|
33
|
-
const placementCount = (tag: string | null) =>
|
|
34
|
-
categoryItems.filter((item) => {
|
|
35
|
-
const tags = item.tags ?? []
|
|
36
|
-
if (tag !== null && !tags.includes(tag)) return false
|
|
37
|
-
if (activeFunctionalTag && !tags.includes(activeFunctionalTag)) return false
|
|
38
|
-
return true
|
|
39
|
-
}).length
|
|
40
|
-
|
|
41
|
-
// Count items for a functional tag given the current placement filter
|
|
42
|
-
const functionalCount = (tag: string) =>
|
|
43
|
-
categoryItems.filter((item) => {
|
|
44
|
-
const tags = item.tags ?? []
|
|
45
|
-
if (!tags.includes(tag)) return false
|
|
46
|
-
if (activePlacementTag && !tags.includes(activePlacementTag)) return false
|
|
47
|
-
return true
|
|
48
|
-
}).length
|
|
49
|
-
|
|
50
|
-
const filteredItems = categoryItems.filter((item) => {
|
|
51
|
-
const tags = item.tags ?? []
|
|
52
|
-
if (activePlacementTag && !tags.includes(activePlacementTag)) return false
|
|
53
|
-
if (activeFunctionalTag && !tags.includes(activeFunctionalTag)) return false
|
|
54
|
-
return true
|
|
55
|
-
})
|
|
56
|
-
|
|
57
|
-
// Auto-select first item if current selection is not in the filtered list
|
|
22
|
+
// Auto-select first item if current selection is not in this category
|
|
58
23
|
useEffect(() => {
|
|
59
|
-
const isCurrentItemInCategory =
|
|
60
|
-
if (!isCurrentItemInCategory &&
|
|
61
|
-
setSelectedItem(
|
|
24
|
+
const isCurrentItemInCategory = categoryItems.some((item) => item.src === selectedItem?.src)
|
|
25
|
+
if (!isCurrentItemInCategory && categoryItems.length > 0) {
|
|
26
|
+
setSelectedItem(categoryItems[0] as AssetInput)
|
|
62
27
|
}
|
|
63
|
-
}, [
|
|
28
|
+
}, [categoryItems, selectedItem?.src, setSelectedItem])
|
|
64
29
|
|
|
65
30
|
// Get attachment icon based on attachTo type
|
|
66
31
|
const getAttachmentIcon = (attachTo: AssetInput['attachTo']) => {
|
|
@@ -74,146 +39,48 @@ export function ItemCatalog({ category }: { category: CatalogCategory }) {
|
|
|
74
39
|
}
|
|
75
40
|
|
|
76
41
|
return (
|
|
77
|
-
<div className="flex
|
|
78
|
-
{
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
{
|
|
83
|
-
<
|
|
42
|
+
<div className="-mx-2 -my-2 flex max-w-xl gap-2 overflow-x-auto p-2">
|
|
43
|
+
{categoryItems.map((item, index) => {
|
|
44
|
+
const isSelected = selectedItem?.src === item?.src
|
|
45
|
+
const attachmentIcon = getAttachmentIcon(item?.attachTo)
|
|
46
|
+
return (
|
|
47
|
+
<Tooltip key={index}>
|
|
48
|
+
<TooltipTrigger asChild>
|
|
84
49
|
<button
|
|
85
50
|
className={cn(
|
|
86
|
-
'
|
|
87
|
-
|
|
88
|
-
? 'bg-blue-500 text-white'
|
|
89
|
-
: 'bg-blue-950/50 text-blue-300 hover:bg-blue-900/60 hover:text-blue-200',
|
|
51
|
+
'relative aspect-square h-14 min-h-14 w-14 min-w-14 shrink-0 flex-col gap-px rounded-lg transition-all duration-200 ease-out hover:scale-105 hover:cursor-pointer',
|
|
52
|
+
isSelected && 'ring-2 ring-primary-foreground',
|
|
90
53
|
)}
|
|
91
|
-
onClick={() =>
|
|
54
|
+
onClick={() => setSelectedItem(item)}
|
|
92
55
|
type="button"
|
|
93
56
|
>
|
|
94
|
-
|
|
57
|
+
<Image
|
|
58
|
+
alt={item.name}
|
|
59
|
+
className="rounded-lg object-cover"
|
|
60
|
+
fill
|
|
61
|
+
loading="eager"
|
|
62
|
+
sizes="56px"
|
|
63
|
+
src={resolveCdnUrl(item.thumbnail) || ''}
|
|
64
|
+
/>
|
|
65
|
+
{attachmentIcon && (
|
|
66
|
+
<div className="absolute right-0.5 bottom-0.5 flex h-4 w-4 items-center justify-center rounded bg-black/60">
|
|
67
|
+
<Image
|
|
68
|
+
alt={item.attachTo === 'ceiling' ? 'Ceiling attachment' : 'Wall attachment'}
|
|
69
|
+
className="h-4 w-4"
|
|
70
|
+
height={16}
|
|
71
|
+
src={attachmentIcon}
|
|
72
|
+
width={16}
|
|
73
|
+
/>
|
|
74
|
+
</div>
|
|
75
|
+
)}
|
|
95
76
|
</button>
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
'inline-flex cursor-pointer items-center gap-1 rounded-md py-0.5 pr-1.5 pl-2 font-medium text-xs capitalize transition-colors',
|
|
104
|
-
isActive
|
|
105
|
-
? 'bg-blue-500 text-white'
|
|
106
|
-
: isEmpty
|
|
107
|
-
? 'cursor-not-allowed bg-zinc-800 text-zinc-500'
|
|
108
|
-
: 'bg-blue-950/50 text-blue-300 hover:bg-blue-900/60 hover:text-blue-200',
|
|
109
|
-
)}
|
|
110
|
-
disabled={isEmpty}
|
|
111
|
-
key={tag}
|
|
112
|
-
onClick={() => setActivePlacementTag(isActive ? null : tag)}
|
|
113
|
-
type="button"
|
|
114
|
-
>
|
|
115
|
-
{tag}
|
|
116
|
-
<span
|
|
117
|
-
className={cn(
|
|
118
|
-
'text-[10px]',
|
|
119
|
-
isActive ? 'text-blue-200' : isEmpty ? 'text-zinc-600' : 'text-blue-500/70',
|
|
120
|
-
)}
|
|
121
|
-
>
|
|
122
|
-
{count}
|
|
123
|
-
</span>
|
|
124
|
-
</button>
|
|
125
|
-
)
|
|
126
|
-
})}
|
|
127
|
-
</div>
|
|
128
|
-
)}
|
|
129
|
-
|
|
130
|
-
{/* Functional row */}
|
|
131
|
-
{functionalTags.length > 0 && (
|
|
132
|
-
<div className="flex flex-wrap gap-1">
|
|
133
|
-
{functionalTags.map((tag) => {
|
|
134
|
-
const count = functionalCount(tag)
|
|
135
|
-
const isActive = activeFunctionalTag === tag
|
|
136
|
-
const isEmpty = count === 0 && !isActive
|
|
137
|
-
return (
|
|
138
|
-
<button
|
|
139
|
-
className={cn(
|
|
140
|
-
'inline-flex cursor-pointer items-center gap-1 rounded-md py-0.5 pr-1.5 pl-2 font-medium text-xs capitalize transition-colors',
|
|
141
|
-
isActive
|
|
142
|
-
? 'bg-violet-500 text-white'
|
|
143
|
-
: isEmpty
|
|
144
|
-
? 'cursor-not-allowed bg-zinc-800 text-zinc-500'
|
|
145
|
-
: 'bg-muted text-muted-foreground hover:bg-muted/80 hover:text-foreground',
|
|
146
|
-
)}
|
|
147
|
-
disabled={isEmpty}
|
|
148
|
-
key={tag}
|
|
149
|
-
onClick={() => setActiveFunctionalTag(isActive ? null : tag)}
|
|
150
|
-
type="button"
|
|
151
|
-
>
|
|
152
|
-
{tag}
|
|
153
|
-
<span
|
|
154
|
-
className={cn(
|
|
155
|
-
'text-[10px]',
|
|
156
|
-
isActive
|
|
157
|
-
? 'text-violet-200'
|
|
158
|
-
: isEmpty
|
|
159
|
-
? 'text-zinc-600'
|
|
160
|
-
: 'text-zinc-500/70',
|
|
161
|
-
)}
|
|
162
|
-
>
|
|
163
|
-
{count}
|
|
164
|
-
</span>
|
|
165
|
-
</button>
|
|
166
|
-
)
|
|
167
|
-
})}
|
|
168
|
-
</div>
|
|
169
|
-
)}
|
|
170
|
-
</div>
|
|
171
|
-
)}
|
|
172
|
-
|
|
173
|
-
{/* Items */}
|
|
174
|
-
<div className="-mx-2 -my-2 flex max-w-xl gap-2 overflow-x-auto p-2">
|
|
175
|
-
{filteredItems.map((item, index) => {
|
|
176
|
-
const isSelected = selectedItem?.src === item?.src
|
|
177
|
-
const attachmentIcon = getAttachmentIcon(item?.attachTo)
|
|
178
|
-
return (
|
|
179
|
-
<Tooltip key={index}>
|
|
180
|
-
<TooltipTrigger asChild>
|
|
181
|
-
<button
|
|
182
|
-
className={cn(
|
|
183
|
-
'relative aspect-square h-14 min-h-14 w-14 min-w-14 shrink-0 flex-col gap-px rounded-lg transition-all duration-200 ease-out hover:scale-105 hover:cursor-pointer',
|
|
184
|
-
isSelected && 'ring-2 ring-primary-foreground',
|
|
185
|
-
)}
|
|
186
|
-
onClick={() => setSelectedItem(item)}
|
|
187
|
-
type="button"
|
|
188
|
-
>
|
|
189
|
-
<Image
|
|
190
|
-
alt={item.name}
|
|
191
|
-
className="rounded-lg object-cover"
|
|
192
|
-
fill
|
|
193
|
-
loading="eager"
|
|
194
|
-
sizes="56px"
|
|
195
|
-
src={resolveCdnUrl(item.thumbnail) || ''}
|
|
196
|
-
/>
|
|
197
|
-
{attachmentIcon && (
|
|
198
|
-
<div className="absolute right-0.5 bottom-0.5 flex h-4 w-4 items-center justify-center rounded bg-black/60">
|
|
199
|
-
<Image
|
|
200
|
-
alt={item.attachTo === 'ceiling' ? 'Ceiling attachment' : 'Wall attachment'}
|
|
201
|
-
className="h-4 w-4"
|
|
202
|
-
height={16}
|
|
203
|
-
src={attachmentIcon}
|
|
204
|
-
width={16}
|
|
205
|
-
/>
|
|
206
|
-
</div>
|
|
207
|
-
)}
|
|
208
|
-
</button>
|
|
209
|
-
</TooltipTrigger>
|
|
210
|
-
<TooltipContent className="text-xs" side="top">
|
|
211
|
-
{item.name}
|
|
212
|
-
</TooltipContent>
|
|
213
|
-
</Tooltip>
|
|
214
|
-
)
|
|
215
|
-
})}
|
|
216
|
-
</div>
|
|
77
|
+
</TooltipTrigger>
|
|
78
|
+
<TooltipContent className="text-xs" side="top">
|
|
79
|
+
{item.name}
|
|
80
|
+
</TooltipContent>
|
|
81
|
+
</Tooltip>
|
|
82
|
+
)
|
|
83
|
+
})}
|
|
217
84
|
</div>
|
|
218
85
|
)
|
|
219
86
|
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import type { LevelNode } from '@pascal-app/core'
|
|
4
|
+
import { useEffect, useState } from 'react'
|
|
5
|
+
import { cn } from '../../lib/utils'
|
|
6
|
+
import type { LevelDuplicatePreset } from '../../lib/level-duplication'
|
|
7
|
+
import {
|
|
8
|
+
Dialog,
|
|
9
|
+
DialogContent,
|
|
10
|
+
DialogDescription,
|
|
11
|
+
DialogFooter,
|
|
12
|
+
DialogHeader,
|
|
13
|
+
DialogTitle,
|
|
14
|
+
} from './primitives/dialog'
|
|
15
|
+
|
|
16
|
+
const DUPLICATE_PRESETS: Array<{
|
|
17
|
+
id: LevelDuplicatePreset
|
|
18
|
+
label: string
|
|
19
|
+
description: string
|
|
20
|
+
}> = [
|
|
21
|
+
{
|
|
22
|
+
id: 'everything',
|
|
23
|
+
label: 'Everything',
|
|
24
|
+
description: 'Structure, materials, furniture, and references.',
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
id: 'structure',
|
|
28
|
+
label: 'Structure only',
|
|
29
|
+
description: 'Walls, slabs, roofs, stairs, windows, and doors without finishes.',
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
id: 'structure-materials',
|
|
33
|
+
label: 'Structure + materials',
|
|
34
|
+
description: 'Structure with the current material and finish assignments.',
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
id: 'structure-furniture',
|
|
38
|
+
label: 'Structure + furniture',
|
|
39
|
+
description: 'Structure, finishes, and placed items, without guide references.',
|
|
40
|
+
},
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
function getLevelLabel(level: LevelNode | null) {
|
|
44
|
+
if (!level) return 'this level'
|
|
45
|
+
return level.name || `Level ${level.level}`
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function LevelDuplicateDialog({
|
|
49
|
+
open,
|
|
50
|
+
level,
|
|
51
|
+
onConfirm,
|
|
52
|
+
onOpenChange,
|
|
53
|
+
}: {
|
|
54
|
+
open: boolean
|
|
55
|
+
level: LevelNode | null
|
|
56
|
+
onConfirm: (preset: LevelDuplicatePreset) => void
|
|
57
|
+
onOpenChange: (open: boolean) => void
|
|
58
|
+
}) {
|
|
59
|
+
const [preset, setPreset] = useState<LevelDuplicatePreset>('everything')
|
|
60
|
+
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
if (open) {
|
|
63
|
+
setPreset('everything')
|
|
64
|
+
}
|
|
65
|
+
}, [open])
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<Dialog onOpenChange={onOpenChange} open={open}>
|
|
69
|
+
<DialogContent className="sm:max-w-md" showCloseButton={false}>
|
|
70
|
+
<DialogHeader>
|
|
71
|
+
<DialogTitle>Duplicate Level</DialogTitle>
|
|
72
|
+
<DialogDescription>
|
|
73
|
+
Choose what to copy from {getLevelLabel(level)}.
|
|
74
|
+
</DialogDescription>
|
|
75
|
+
</DialogHeader>
|
|
76
|
+
|
|
77
|
+
<div className="grid gap-2">
|
|
78
|
+
{DUPLICATE_PRESETS.map((option) => (
|
|
79
|
+
<button
|
|
80
|
+
className={cn(
|
|
81
|
+
'cursor-pointer rounded-xl border px-3 py-3 text-left transition-colors',
|
|
82
|
+
preset === option.id
|
|
83
|
+
? 'border-primary bg-primary/10 text-foreground'
|
|
84
|
+
: 'border-border bg-background hover:bg-accent/40',
|
|
85
|
+
)}
|
|
86
|
+
key={option.id}
|
|
87
|
+
onClick={() => setPreset(option.id)}
|
|
88
|
+
type="button"
|
|
89
|
+
>
|
|
90
|
+
<div className="font-medium text-sm">{option.label}</div>
|
|
91
|
+
<div className="mt-1 text-muted-foreground text-xs">{option.description}</div>
|
|
92
|
+
</button>
|
|
93
|
+
))}
|
|
94
|
+
</div>
|
|
95
|
+
|
|
96
|
+
<DialogFooter>
|
|
97
|
+
<button
|
|
98
|
+
className="cursor-pointer rounded-md px-4 py-2 text-sm text-muted-foreground transition-colors hover:bg-accent"
|
|
99
|
+
onClick={() => onOpenChange(false)}
|
|
100
|
+
type="button"
|
|
101
|
+
>
|
|
102
|
+
Cancel
|
|
103
|
+
</button>
|
|
104
|
+
<button
|
|
105
|
+
className="cursor-pointer rounded-md bg-primary px-4 py-2 text-primary-foreground text-sm transition-opacity hover:opacity-90"
|
|
106
|
+
onClick={() => onConfirm(preset)}
|
|
107
|
+
type="button"
|
|
108
|
+
>
|
|
109
|
+
Duplicate
|
|
110
|
+
</button>
|
|
111
|
+
</DialogFooter>
|
|
112
|
+
</DialogContent>
|
|
113
|
+
</Dialog>
|
|
114
|
+
)
|
|
115
|
+
}
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { type AnyNode, type CeilingNode,
|
|
3
|
+
import { type AnyNode, type CeilingNode, useScene } from '@pascal-app/core'
|
|
4
4
|
import { useViewer } from '@pascal-app/viewer'
|
|
5
5
|
import { Edit, Move, Plus, Trash2 } from 'lucide-react'
|
|
6
6
|
import { useCallback, useEffect } from 'react'
|
|
7
7
|
import { sfxEmitter } from '../../../lib/sfx-bus'
|
|
8
8
|
import useEditor from '../../../store/use-editor'
|
|
9
9
|
import { ActionButton, ActionGroup } from '../controls/action-button'
|
|
10
|
-
import { MaterialPicker } from '../controls/material-picker'
|
|
11
10
|
import { PanelSection } from '../controls/panel-section'
|
|
12
11
|
import { SliderControl } from '../controls/slider-control'
|
|
13
12
|
import { PanelWrapper } from './panel-wrapper'
|
|
@@ -32,20 +31,6 @@ export function CeilingPanel() {
|
|
|
32
31
|
[selectedId, updateNode],
|
|
33
32
|
)
|
|
34
33
|
|
|
35
|
-
const handleMaterialChange = useCallback(
|
|
36
|
-
(material: MaterialSchema) => {
|
|
37
|
-
handleUpdate({ material, materialPreset: undefined })
|
|
38
|
-
},
|
|
39
|
-
[handleUpdate],
|
|
40
|
-
)
|
|
41
|
-
|
|
42
|
-
const handleMaterialPresetChange = useCallback(
|
|
43
|
-
(materialPreset: string) => {
|
|
44
|
-
handleUpdate({ materialPreset, material: undefined })
|
|
45
|
-
},
|
|
46
|
-
[handleUpdate],
|
|
47
|
-
)
|
|
48
|
-
|
|
49
34
|
const handleClose = useCallback(() => {
|
|
50
35
|
setSelection({ selectedIds: [] })
|
|
51
36
|
setEditingHole(null)
|
|
@@ -257,15 +242,6 @@ export function CeilingPanel() {
|
|
|
257
242
|
</div>
|
|
258
243
|
</PanelSection>
|
|
259
244
|
|
|
260
|
-
<PanelSection title="Material">
|
|
261
|
-
<MaterialPicker
|
|
262
|
-
nodeType="ceiling"
|
|
263
|
-
onChange={handleMaterialChange}
|
|
264
|
-
onSelectMaterialPreset={handleMaterialPresetChange}
|
|
265
|
-
selectedMaterialPreset={node.materialPreset}
|
|
266
|
-
value={node.material}
|
|
267
|
-
/>
|
|
268
|
-
</PanelSection>
|
|
269
245
|
<ActionGroup>
|
|
270
246
|
<ActionButton icon={<Move className="h-3.5 w-3.5" />} label="Move" onClick={handleMove} />
|
|
271
247
|
</ActionGroup>
|