@pascal-app/editor 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +6 -6
- package/src/components/editor/custom-camera-controls.tsx +2 -1
- package/src/components/editor/editor-layout-v2.tsx +4 -3
- package/src/components/editor/first-person/build-collider-world.ts +5 -7
- package/src/components/editor/first-person/bvh-ecctrl.tsx +119 -54
- package/src/components/editor/first-person-controls.tsx +11 -11
- package/src/components/editor/floating-action-menu.tsx +0 -0
- package/src/components/editor/floorplan-panel.tsx +44 -37
- package/src/components/editor/index.tsx +68 -53
- package/src/components/editor/selection-manager.tsx +2 -2
- package/src/components/editor/snapshot-capture-overlay.tsx +465 -0
- package/src/components/editor/thumbnail-generator.tsx +18 -61
- package/src/components/editor/use-floorplan-background-placement.ts +3 -3
- package/src/components/editor/wall-measurement-label.tsx +0 -0
- package/src/components/editor-2d/renderers/floorplan-draft-layer.tsx +6 -1
- package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +6 -1
- package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +5 -5
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +10 -12
- package/src/components/systems/roof/roof-edit-system.tsx +1 -1
- package/src/components/systems/stair/stair-edit-system.tsx +1 -1
- package/src/components/systems/zone/zone-label-editor-system.tsx +0 -0
- package/src/components/systems/zone/zone-system.tsx +0 -0
- package/src/components/tools/ceiling/move-ceiling-tool.tsx +9 -2
- package/src/components/tools/fence/curve-fence-tool.tsx +4 -5
- package/src/components/tools/fence/fence-tool.tsx +2 -2
- package/src/components/tools/fence/move-fence-endpoint-tool.tsx +11 -8
- package/src/components/tools/fence/move-fence-tool.tsx +13 -9
- package/src/components/tools/item/move-tool.tsx +3 -6
- package/src/components/tools/item/placement-math.ts +2 -4
- package/src/components/tools/item/placement-strategies.ts +11 -10
- package/src/components/tools/item/use-draft-node.ts +0 -1
- package/src/components/tools/item/use-placement-coordinator.tsx +9 -111
- package/src/components/tools/roof/move-roof-tool.tsx +7 -2
- package/src/components/tools/select/box-select-tool.tsx +12 -17
- package/src/components/tools/shared/segment-angle.ts +1 -1
- package/src/components/tools/tool-manager.tsx +12 -12
- package/src/components/tools/wall/curve-wall-tool.tsx +8 -6
- package/src/components/tools/wall/move-wall-endpoint-tool.tsx +11 -8
- package/src/components/tools/wall/move-wall-tool.tsx +6 -4
- package/src/components/tools/wall/wall-drafting.ts +0 -0
- package/src/components/tools/wall/wall-tool.tsx +3 -3
- package/src/components/tools/zone/zone-tool.tsx +20 -5
- package/src/components/ui/action-menu/camera-actions.tsx +0 -0
- package/src/components/ui/action-menu/control-modes.tsx +7 -1
- package/src/components/ui/action-menu/furnish-tools.tsx +6 -92
- package/src/components/ui/action-menu/index.tsx +35 -86
- package/src/components/ui/action-menu/view-toggles.tsx +19 -31
- package/src/components/ui/command-palette/editor-commands.tsx +6 -4
- package/src/components/ui/command-palette/index.tsx +4 -255
- package/src/components/ui/controls/material-picker.tsx +8 -5
- package/src/components/ui/floating-level-selector.tsx +1 -1
- package/src/components/ui/helpers/helper-manager.tsx +5 -0
- package/src/components/ui/item-catalog/catalog-items.tsx +1742 -315
- package/src/components/ui/item-catalog/item-catalog.tsx +88 -46
- package/src/components/ui/level-duplicate-dialog.tsx +3 -5
- package/src/components/ui/panels/ceiling-panel.tsx +2 -3
- package/src/components/ui/panels/column-panel.tsx +62 -18
- package/src/components/ui/panels/door-panel.tsx +272 -265
- package/src/components/ui/panels/fence-panel.tsx +0 -5
- package/src/components/ui/panels/paint-panel.tsx +66 -41
- package/src/components/ui/panels/panel-manager.tsx +3 -32
- package/src/components/ui/panels/reference-panel.tsx +28 -13
- package/src/components/ui/panels/roof-panel.tsx +52 -2
- package/src/components/ui/panels/roof-segment-panel.tsx +0 -0
- package/src/components/ui/panels/slab-panel.tsx +0 -0
- package/src/components/ui/panels/spawn-panel.tsx +10 -4
- package/src/components/ui/panels/stair-panel.tsx +66 -14
- package/src/components/ui/panels/wall-panel.tsx +97 -1
- package/src/components/ui/panels/window-panel.tsx +13 -5
- package/src/components/ui/primitives/number-input.tsx +1 -1
- package/src/components/ui/primitives/sidebar.tsx +0 -0
- package/src/components/ui/sidebar/app-sidebar.tsx +0 -0
- package/src/components/ui/sidebar/icon-rail.tsx +0 -0
- package/src/components/ui/sidebar/panels/items-panel/index.tsx +330 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +0 -0
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +0 -0
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +4 -6
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +0 -0
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -7
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +3 -1
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +3 -1
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +1 -1
- package/src/components/ui/slider.tsx +1 -1
- package/src/components/viewer-overlay.tsx +0 -0
- package/src/components/viewer-zone-system.tsx +0 -0
- package/src/hooks/use-auto-save.ts +14 -0
- package/src/hooks/use-keyboard.ts +10 -0
- package/src/index.tsx +8 -1
- package/src/lib/level-duplication.test.ts +0 -2
- package/src/lib/level-duplication.ts +1 -1
- package/src/lib/material-paint.ts +1 -1
- package/src/lib/roof-duplication.ts +1 -1
- package/src/lib/scene-bounds.ts +1 -1
- package/src/lib/scene.ts +0 -0
- package/src/lib/sfx-bus.ts +2 -0
- package/src/lib/sfx-player.ts +5 -5
- package/src/lib/stair-duplication.ts +2 -2
- package/src/store/use-editor.tsx +27 -59
- package/tsconfig.json +2 -1
- package/src/components/feedback-dialog.tsx +0 -265
- package/src/components/pascal-radio.tsx +0 -280
- package/src/components/preview-button.tsx +0 -16
- package/src/components/ui/viewer-toolbar.tsx +0 -436
|
@@ -13,13 +13,53 @@ 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
|
-
export function ItemCatalog({
|
|
16
|
+
export function ItemCatalog({
|
|
17
|
+
category,
|
|
18
|
+
items: itemsOverride,
|
|
19
|
+
activePlacementTag = null,
|
|
20
|
+
activeFunctionalTag = null,
|
|
21
|
+
search = '',
|
|
22
|
+
overrideItems,
|
|
23
|
+
leadingTile,
|
|
24
|
+
emptyState,
|
|
25
|
+
}: {
|
|
26
|
+
category: CatalogCategory
|
|
27
|
+
items?: AssetInput[]
|
|
28
|
+
activePlacementTag?: string | null
|
|
29
|
+
activeFunctionalTag?: string | null
|
|
30
|
+
search?: string
|
|
31
|
+
/** When set, bypasses all filtering and displays these items directly (used for server search results) */
|
|
32
|
+
overrideItems?: AssetInput[]
|
|
33
|
+
/** Rendered as the first grid cell, always visible when there are items. */
|
|
34
|
+
leadingTile?: React.ReactNode
|
|
35
|
+
/** Rendered when there are no items to show. Replaces the empty grid. */
|
|
36
|
+
emptyState?: React.ReactNode
|
|
37
|
+
}) {
|
|
17
38
|
const selectedItem = useEditor((state) => state.selectedItem)
|
|
18
39
|
const setSelectedItem = useEditor((state) => state.setSelectedItem)
|
|
40
|
+
const setMode = useEditor((state) => state.setMode)
|
|
41
|
+
const setTool = useEditor((state) => state.setTool)
|
|
19
42
|
|
|
20
|
-
const
|
|
43
|
+
const sourceItems = itemsOverride ?? CATALOG_ITEMS
|
|
44
|
+
// Server-provided results bypass all local filtering; otherwise filter by category/search/tags
|
|
45
|
+
const filteredItems =
|
|
46
|
+
overrideItems ??
|
|
47
|
+
(() => {
|
|
48
|
+
const categoryItems = search
|
|
49
|
+
? sourceItems
|
|
50
|
+
: sourceItems.filter((item) => item.category === category)
|
|
51
|
+
return categoryItems.filter((item) => {
|
|
52
|
+
const tags = item.tags ?? []
|
|
53
|
+
if (activePlacementTag && !tags.includes(activePlacementTag)) return false
|
|
54
|
+
if (activeFunctionalTag && !tags.includes(activeFunctionalTag)) return false
|
|
55
|
+
if (search && !item.name.toLowerCase().includes(search.toLowerCase())) return false
|
|
56
|
+
return true
|
|
57
|
+
})
|
|
58
|
+
})()
|
|
21
59
|
|
|
22
|
-
|
|
60
|
+
const categoryItems = filteredItems
|
|
61
|
+
|
|
62
|
+
// Auto-select first item if current selection is not in the filtered list
|
|
23
63
|
useEffect(() => {
|
|
24
64
|
const isCurrentItemInCategory = categoryItems.some((item) => item.src === selectedItem?.src)
|
|
25
65
|
if (!isCurrentItemInCategory && categoryItems.length > 0) {
|
|
@@ -27,58 +67,60 @@ export function ItemCatalog({ category }: { category: CatalogCategory }) {
|
|
|
27
67
|
}
|
|
28
68
|
}, [categoryItems, selectedItem?.src, setSelectedItem])
|
|
29
69
|
|
|
30
|
-
// Get attachment icon based on attachTo type
|
|
31
70
|
const getAttachmentIcon = (attachTo: AssetInput['attachTo']) => {
|
|
32
|
-
if (attachTo === 'wall' || attachTo === 'wall-side')
|
|
33
|
-
|
|
34
|
-
}
|
|
35
|
-
if (attachTo === 'ceiling') {
|
|
36
|
-
return '/icons/ceiling.png'
|
|
37
|
-
}
|
|
71
|
+
if (attachTo === 'wall' || attachTo === 'wall-side') return '/icons/wall.png'
|
|
72
|
+
if (attachTo === 'ceiling') return '/icons/ceiling.png'
|
|
38
73
|
return null
|
|
39
74
|
}
|
|
40
75
|
|
|
76
|
+
if (filteredItems.length === 0 && emptyState) {
|
|
77
|
+
return <>{emptyState}</>
|
|
78
|
+
}
|
|
79
|
+
|
|
41
80
|
return (
|
|
42
|
-
<div
|
|
43
|
-
|
|
81
|
+
<div
|
|
82
|
+
className="grid gap-2"
|
|
83
|
+
style={{ gridTemplateColumns: 'repeat(auto-fill, minmax(90px, 1fr))' }}
|
|
84
|
+
>
|
|
85
|
+
{leadingTile}
|
|
86
|
+
{filteredItems.map((item, index) => {
|
|
44
87
|
const isSelected = selectedItem?.src === item?.src
|
|
45
88
|
const attachmentIcon = getAttachmentIcon(item?.attachTo)
|
|
46
89
|
return (
|
|
47
|
-
<
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
{
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
<TooltipContent className="text-xs" side="top">
|
|
90
|
+
<button
|
|
91
|
+
className={cn(
|
|
92
|
+
'group relative flex flex-col gap-1.5 rounded-xl p-1.5 transition-colors hover:cursor-pointer hover:bg-sidebar-accent',
|
|
93
|
+
isSelected && 'bg-sidebar-accent ring-2 ring-primary-foreground',
|
|
94
|
+
)}
|
|
95
|
+
key={index}
|
|
96
|
+
onClick={() => {
|
|
97
|
+
setSelectedItem(item)
|
|
98
|
+
setTool('item')
|
|
99
|
+
setMode('build')
|
|
100
|
+
}}
|
|
101
|
+
type="button"
|
|
102
|
+
>
|
|
103
|
+
<div className="relative aspect-square w-full overflow-hidden rounded-lg">
|
|
104
|
+
<img
|
|
105
|
+
alt={item.name}
|
|
106
|
+
className="h-full w-full object-cover"
|
|
107
|
+
loading="eager"
|
|
108
|
+
src={resolveCdnUrl(item.thumbnail) || ''}
|
|
109
|
+
/>
|
|
110
|
+
{attachmentIcon && (
|
|
111
|
+
<div className="absolute right-1 bottom-1 flex h-4 w-4 items-center justify-center rounded bg-black/60">
|
|
112
|
+
<img
|
|
113
|
+
alt={item.attachTo === 'ceiling' ? 'Ceiling attachment' : 'Wall attachment'}
|
|
114
|
+
className="h-4 w-4"
|
|
115
|
+
src={attachmentIcon}
|
|
116
|
+
/>
|
|
117
|
+
</div>
|
|
118
|
+
)}
|
|
119
|
+
</div>
|
|
120
|
+
<span className="truncate px-0.5 text-left font-medium text-[11px] text-muted-foreground group-hover:text-foreground">
|
|
79
121
|
{item.name}
|
|
80
|
-
</
|
|
81
|
-
</
|
|
122
|
+
</span>
|
|
123
|
+
</button>
|
|
82
124
|
)
|
|
83
125
|
})}
|
|
84
126
|
</div>
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
import type { LevelNode } from '@pascal-app/core'
|
|
4
4
|
import { useEffect, useState } from 'react'
|
|
5
|
-
import { cn } from '../../lib/utils'
|
|
6
5
|
import type { LevelDuplicatePreset } from '../../lib/level-duplication'
|
|
6
|
+
import { cn } from '../../lib/utils'
|
|
7
7
|
import {
|
|
8
8
|
Dialog,
|
|
9
9
|
DialogContent,
|
|
@@ -69,9 +69,7 @@ export function LevelDuplicateDialog({
|
|
|
69
69
|
<DialogContent className="sm:max-w-md" showCloseButton={false}>
|
|
70
70
|
<DialogHeader>
|
|
71
71
|
<DialogTitle>Duplicate Level</DialogTitle>
|
|
72
|
-
<DialogDescription>
|
|
73
|
-
Choose what to copy from {getLevelLabel(level)}.
|
|
74
|
-
</DialogDescription>
|
|
72
|
+
<DialogDescription>Choose what to copy from {getLevelLabel(level)}.</DialogDescription>
|
|
75
73
|
</DialogHeader>
|
|
76
74
|
|
|
77
75
|
<div className="grid gap-2">
|
|
@@ -95,7 +93,7 @@ export function LevelDuplicateDialog({
|
|
|
95
93
|
|
|
96
94
|
<DialogFooter>
|
|
97
95
|
<button
|
|
98
|
-
className="cursor-pointer rounded-md px-4 py-2 text-
|
|
96
|
+
className="cursor-pointer rounded-md px-4 py-2 text-muted-foreground text-sm transition-colors hover:bg-accent"
|
|
99
97
|
onClick={() => onOpenChange(false)}
|
|
100
98
|
type="button"
|
|
101
99
|
>
|
|
@@ -120,9 +120,8 @@ export function CeilingPanel() {
|
|
|
120
120
|
const n = polygon.length
|
|
121
121
|
for (let i = 0; i < n; i++) {
|
|
122
122
|
const j = (i + 1) % n
|
|
123
|
-
const current = polygon[i]
|
|
124
|
-
const next = polygon[j]
|
|
125
|
-
if (!(current && next)) continue
|
|
123
|
+
const current = polygon[i]!
|
|
124
|
+
const next = polygon[j]!
|
|
126
125
|
area += current[0] * next[1]
|
|
127
126
|
area -= next[0] * current[1]
|
|
128
127
|
}
|
|
@@ -70,10 +70,12 @@ const COLUMN_PROPORTION_PRESETS = {
|
|
|
70
70
|
|
|
71
71
|
type ColumnProportionPresetId = keyof typeof COLUMN_PROPORTION_PRESETS
|
|
72
72
|
|
|
73
|
-
const COLUMN_PROPORTION_OPTIONS = Object.entries(COLUMN_PROPORTION_PRESETS).map(
|
|
74
|
-
value
|
|
75
|
-
|
|
76
|
-
|
|
73
|
+
const COLUMN_PROPORTION_OPTIONS = Object.entries(COLUMN_PROPORTION_PRESETS).map(
|
|
74
|
+
([value, preset]) => ({
|
|
75
|
+
value: value as ColumnProportionPresetId,
|
|
76
|
+
label: preset.label,
|
|
77
|
+
}),
|
|
78
|
+
)
|
|
77
79
|
|
|
78
80
|
function clamp(value: number, min: number, max: number) {
|
|
79
81
|
return Math.min(max, Math.max(min, value))
|
|
@@ -201,7 +203,12 @@ export function ColumnPanel() {
|
|
|
201
203
|
const shaftProfile = node.shaftProfile ?? 'straight'
|
|
202
204
|
|
|
203
205
|
return (
|
|
204
|
-
<PanelWrapper
|
|
206
|
+
<PanelWrapper
|
|
207
|
+
icon="/icons/column.png"
|
|
208
|
+
onClose={handleClose}
|
|
209
|
+
title={node.name || 'Column'}
|
|
210
|
+
width={300}
|
|
211
|
+
>
|
|
205
212
|
<PanelSection title="Preset">
|
|
206
213
|
<select
|
|
207
214
|
className={SELECT_CLASS}
|
|
@@ -223,7 +230,9 @@ export function ColumnPanel() {
|
|
|
223
230
|
<PanelSection title="Shape">
|
|
224
231
|
<select
|
|
225
232
|
className={SELECT_CLASS}
|
|
226
|
-
onChange={(event) =>
|
|
233
|
+
onChange={(event) =>
|
|
234
|
+
handleUpdate({ crossSection: event.target.value as ColumnNode['crossSection'] })
|
|
235
|
+
}
|
|
227
236
|
value={node.crossSection}
|
|
228
237
|
>
|
|
229
238
|
<option value="round">Round</option>
|
|
@@ -313,7 +322,9 @@ export function ColumnPanel() {
|
|
|
313
322
|
<PanelSection title="Shaft">
|
|
314
323
|
<select
|
|
315
324
|
className={SELECT_CLASS}
|
|
316
|
-
onChange={(event) =>
|
|
325
|
+
onChange={(event) =>
|
|
326
|
+
handleUpdate(shaftProfileUpdates(event.target.value as ColumnNode['shaftProfile']))
|
|
327
|
+
}
|
|
317
328
|
value={shaftProfile}
|
|
318
329
|
>
|
|
319
330
|
<option value="straight">Straight</option>
|
|
@@ -487,10 +498,22 @@ export function ColumnPanel() {
|
|
|
487
498
|
? {}
|
|
488
499
|
: {
|
|
489
500
|
capitalHeight: Math.max(node.capitalHeight, 0.12),
|
|
490
|
-
capitalTierCount:
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
501
|
+
capitalTierCount:
|
|
502
|
+
capitalStyle === 'stepped'
|
|
503
|
+
? Math.max(node.capitalTierCount ?? 3, 3)
|
|
504
|
+
: node.capitalTierCount,
|
|
505
|
+
capitalWidthScale: Math.max(
|
|
506
|
+
node.capitalWidthScale ?? 1.3,
|
|
507
|
+
capitalStyle === 'stepped' ? 1.42 : 1.28,
|
|
508
|
+
),
|
|
509
|
+
capitalDepthScale: Math.max(
|
|
510
|
+
node.capitalDepthScale ?? 1.3,
|
|
511
|
+
capitalStyle === 'stepped' ? 1.42 : 1.28,
|
|
512
|
+
),
|
|
513
|
+
capitalStepSpread:
|
|
514
|
+
capitalStyle === 'stepped'
|
|
515
|
+
? Math.max(node.capitalStepSpread ?? 0.34, 0.34)
|
|
516
|
+
: node.capitalStepSpread,
|
|
494
517
|
}),
|
|
495
518
|
})
|
|
496
519
|
}}
|
|
@@ -572,13 +595,34 @@ export function ColumnPanel() {
|
|
|
572
595
|
? {}
|
|
573
596
|
: {
|
|
574
597
|
baseHeight: Math.max(node.baseHeight, 0.12),
|
|
575
|
-
baseTierCount:
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
598
|
+
baseTierCount:
|
|
599
|
+
baseStyle === 'stepped-square'
|
|
600
|
+
? Math.max(node.baseTierCount ?? 3, 3)
|
|
601
|
+
: node.baseTierCount,
|
|
602
|
+
baseWidthScale: Math.max(
|
|
603
|
+
node.baseWidthScale ?? 1.24,
|
|
604
|
+
baseStyle === 'stepped-square' ? 1.42 : 1.24,
|
|
605
|
+
),
|
|
606
|
+
baseDepthScale: Math.max(
|
|
607
|
+
node.baseDepthScale ?? 1.24,
|
|
608
|
+
baseStyle === 'stepped-square' ? 1.42 : 1.24,
|
|
609
|
+
),
|
|
610
|
+
baseStepSpread:
|
|
611
|
+
baseStyle === 'stepped-square'
|
|
612
|
+
? Math.max(node.baseStepSpread ?? 0.34, 0.34)
|
|
613
|
+
: node.baseStepSpread,
|
|
614
|
+
basePlinthHeightRatio:
|
|
615
|
+
baseStyle === 'round-rings'
|
|
616
|
+
? (node.basePlinthHeightRatio ?? 0.44)
|
|
617
|
+
: node.basePlinthHeightRatio,
|
|
618
|
+
baseRoundBandScale:
|
|
619
|
+
baseStyle === 'round-rings'
|
|
620
|
+
? (node.baseRoundBandScale ?? 0.92)
|
|
621
|
+
: node.baseRoundBandScale,
|
|
622
|
+
baseNeckScale:
|
|
623
|
+
baseStyle === 'round-rings'
|
|
624
|
+
? (node.baseNeckScale ?? 0.72)
|
|
625
|
+
: node.baseNeckScale,
|
|
582
626
|
}),
|
|
583
627
|
})
|
|
584
628
|
}}
|