@pascal-app/editor 0.5.1 → 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.
Files changed (150) hide show
  1. package/package.json +12 -7
  2. package/src/components/editor/bottom-sheet.tsx +149 -0
  3. package/src/components/editor/custom-camera-controls.tsx +75 -7
  4. package/src/components/editor/editor-layout-mobile.tsx +264 -0
  5. package/src/components/editor/editor-layout-v2.tsx +29 -0
  6. package/src/components/editor/first-person/build-collider-world.ts +365 -0
  7. package/src/components/editor/first-person/bvh-ecctrl.tsx +795 -0
  8. package/src/components/editor/first-person-controls.tsx +496 -143
  9. package/src/components/editor/floating-action-menu.tsx +281 -83
  10. package/src/components/editor/floating-building-action-menu.tsx +4 -3
  11. package/src/components/editor/floorplan-background-selection.ts +113 -0
  12. package/src/components/editor/floorplan-panel.tsx +10442 -3275
  13. package/src/components/editor/index.tsx +270 -20
  14. package/src/components/editor/node-action-menu.tsx +14 -1
  15. package/src/components/editor/selection-manager.tsx +766 -12
  16. package/src/components/editor/site-edge-labels.tsx +9 -3
  17. package/src/components/editor/thumbnail-generator.tsx +350 -157
  18. package/src/components/editor/use-floorplan-background-placement.ts +257 -0
  19. package/src/components/editor/use-floorplan-hit-testing.ts +171 -0
  20. package/src/components/editor/use-floorplan-scene-data.ts +189 -0
  21. package/src/components/editor/wall-measurement-label.tsx +377 -58
  22. package/src/components/editor-2d/floorplan-action-menu-layer.tsx +95 -0
  23. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +160 -0
  24. package/src/components/editor-2d/floorplan-hotkey-handlers.tsx +92 -0
  25. package/src/components/editor-2d/renderers/floorplan-draft-layer.tsx +119 -0
  26. package/src/components/editor-2d/renderers/floorplan-marquee-layer.tsx +58 -0
  27. package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +197 -0
  28. package/src/components/editor-2d/renderers/floorplan-roof-layer.tsx +113 -0
  29. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +474 -0
  30. package/src/components/editor-2d/svg-paths.ts +119 -0
  31. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +272 -0
  32. package/src/components/systems/roof/roof-edit-system.tsx +5 -5
  33. package/src/components/tools/ceiling/ceiling-boundary-editor.tsx +1 -0
  34. package/src/components/tools/ceiling/ceiling-hole-editor.tsx +2 -0
  35. package/src/components/tools/ceiling/ceiling-tool.tsx +5 -5
  36. package/src/components/tools/ceiling/move-ceiling-tool.tsx +257 -0
  37. package/src/components/tools/column/column-tool.tsx +97 -0
  38. package/src/components/tools/column/move-column-tool.tsx +105 -0
  39. package/src/components/tools/door/door-tool.tsx +19 -0
  40. package/src/components/tools/door/move-door-tool.tsx +38 -8
  41. package/src/components/tools/fence/curve-fence-tool.tsx +179 -0
  42. package/src/components/tools/fence/fence-drafting.ts +27 -8
  43. package/src/components/tools/fence/fence-tool.tsx +159 -3
  44. package/src/components/tools/fence/move-fence-endpoint-tool.tsx +438 -0
  45. package/src/components/tools/fence/move-fence-tool.tsx +102 -27
  46. package/src/components/tools/item/move-tool.tsx +19 -1
  47. package/src/components/tools/item/placement-math.ts +44 -7
  48. package/src/components/tools/item/placement-strategies.ts +111 -33
  49. package/src/components/tools/item/placement-types.ts +7 -0
  50. package/src/components/tools/item/use-draft-node.ts +2 -0
  51. package/src/components/tools/item/use-placement-coordinator.tsx +701 -61
  52. package/src/components/tools/roof/move-roof-tool.tsx +111 -43
  53. package/src/components/tools/shared/polygon-editor.tsx +244 -29
  54. package/src/components/tools/shared/segment-angle.ts +156 -0
  55. package/src/components/tools/slab/move-slab-tool.tsx +182 -0
  56. package/src/components/tools/slab/slab-boundary-editor.tsx +1 -0
  57. package/src/components/tools/slab/slab-hole-editor.tsx +2 -0
  58. package/src/components/tools/spawn/move-spawn-tool.tsx +101 -0
  59. package/src/components/tools/spawn/spawn-tool.tsx +130 -0
  60. package/src/components/tools/stair/stair-tool.tsx +11 -3
  61. package/src/components/tools/tool-manager.tsx +30 -3
  62. package/src/components/tools/wall/curve-wall-tool.tsx +176 -0
  63. package/src/components/tools/wall/move-wall-endpoint-tool.tsx +423 -0
  64. package/src/components/tools/wall/move-wall-tool.tsx +356 -0
  65. package/src/components/tools/wall/wall-drafting.ts +348 -17
  66. package/src/components/tools/wall/wall-tool.tsx +134 -2
  67. package/src/components/tools/window/move-window-tool.tsx +28 -0
  68. package/src/components/tools/window/window-tool.tsx +17 -0
  69. package/src/components/ui/action-menu/camera-actions.tsx +37 -33
  70. package/src/components/ui/action-menu/control-modes.tsx +37 -5
  71. package/src/components/ui/action-menu/index.tsx +91 -1
  72. package/src/components/ui/action-menu/structure-tools.tsx +2 -0
  73. package/src/components/ui/action-menu/view-toggles.tsx +424 -35
  74. package/src/components/ui/command-palette/editor-commands.tsx +27 -5
  75. package/src/components/ui/command-palette/index.tsx +0 -1
  76. package/src/components/ui/controls/material-picker.tsx +189 -169
  77. package/src/components/ui/controls/slider-control.tsx +88 -26
  78. package/src/components/ui/floating-level-selector.tsx +286 -55
  79. package/src/components/ui/helpers/helper-manager.tsx +5 -0
  80. package/src/components/ui/item-catalog/catalog-items.tsx +1121 -1219
  81. package/src/components/ui/item-catalog/item-catalog.tsx +42 -175
  82. package/src/components/ui/level-duplicate-dialog.tsx +115 -0
  83. package/src/components/ui/panels/ceiling-panel.tsx +47 -27
  84. package/src/components/ui/panels/column-panel.tsx +715 -0
  85. package/src/components/ui/panels/door-panel.tsx +986 -294
  86. package/src/components/ui/panels/fence-panel.tsx +55 -12
  87. package/src/components/ui/panels/item-panel.tsx +5 -5
  88. package/src/components/ui/panels/mobile-panel-sheet.tsx +108 -0
  89. package/src/components/ui/panels/mobile-selection-bar.tsx +100 -0
  90. package/src/components/ui/panels/node-display.ts +39 -0
  91. package/src/components/ui/panels/paint-panel.tsx +138 -0
  92. package/src/components/ui/panels/panel-manager.tsx +241 -30
  93. package/src/components/ui/panels/panel-wrapper.tsx +48 -39
  94. package/src/components/ui/panels/reference-panel.tsx +243 -9
  95. package/src/components/ui/panels/roof-panel.tsx +30 -62
  96. package/src/components/ui/panels/roof-segment-panel.tsx +8 -23
  97. package/src/components/ui/panels/slab-panel.tsx +46 -24
  98. package/src/components/ui/panels/spawn-panel.tsx +155 -0
  99. package/src/components/ui/panels/stair-panel.tsx +117 -69
  100. package/src/components/ui/panels/stair-segment-panel.tsx +13 -27
  101. package/src/components/ui/panels/wall-panel.tsx +71 -17
  102. package/src/components/ui/panels/window-panel.tsx +665 -146
  103. package/src/components/ui/sidebar/mobile-tab-bar.tsx +46 -0
  104. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +2 -2
  105. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +9 -5
  106. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +7 -3
  107. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +77 -0
  108. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +7 -3
  109. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +7 -3
  110. package/src/components/ui/sidebar/panels/site-panel/index.tsx +138 -56
  111. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +7 -3
  112. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +9 -5
  113. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +7 -3
  114. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +7 -3
  115. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +82 -0
  116. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +7 -3
  117. package/src/components/ui/sidebar/panels/site-panel/tree-node-actions.tsx +3 -3
  118. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +12 -6
  119. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +7 -3
  120. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +7 -3
  121. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +15 -8
  122. package/src/components/ui/sidebar/tab-bar.tsx +3 -0
  123. package/src/components/ui/viewer-toolbar.tsx +96 -2
  124. package/src/components/viewer-overlay.tsx +25 -19
  125. package/src/hooks/use-auto-frame.ts +45 -0
  126. package/src/hooks/use-contextual-tools.ts +14 -13
  127. package/src/hooks/use-keyboard.ts +67 -9
  128. package/src/hooks/use-mobile.ts +12 -12
  129. package/src/index.tsx +2 -1
  130. package/src/lib/door-interaction.ts +88 -0
  131. package/src/lib/floorplan/geometry.ts +263 -0
  132. package/src/lib/floorplan/index.ts +38 -0
  133. package/src/lib/floorplan/items.ts +179 -0
  134. package/src/lib/floorplan/selection-tool.ts +231 -0
  135. package/src/lib/floorplan/stairs.ts +478 -0
  136. package/src/lib/floorplan/types.ts +57 -0
  137. package/src/lib/floorplan/walls.ts +23 -0
  138. package/src/lib/guide-events.ts +10 -0
  139. package/src/lib/history.ts +20 -0
  140. package/src/lib/level-duplication.test.ts +72 -0
  141. package/src/lib/level-duplication.ts +153 -0
  142. package/src/lib/local-guide-image.ts +42 -0
  143. package/src/lib/material-paint.ts +284 -0
  144. package/src/lib/roof-duplication.ts +214 -0
  145. package/src/lib/scene-bounds.test.ts +183 -0
  146. package/src/lib/scene-bounds.ts +169 -0
  147. package/src/lib/sfx-player.ts +96 -13
  148. package/src/lib/stair-duplication.ts +126 -0
  149. package/src/lib/window-interaction.ts +86 -0
  150. package/src/store/use-editor.tsx +279 -15
@@ -0,0 +1,46 @@
1
+ 'use client'
2
+
3
+ import { cn } from './../../../lib/utils'
4
+ import type { SidebarTab } from './tab-bar'
5
+
6
+ interface MobileTabBarProps {
7
+ tabs: SidebarTab[]
8
+ activeTab: string
9
+ onTabPress: (id: string) => void
10
+ }
11
+
12
+ export function MobileTabBar({ tabs, activeTab, onTabPress }: MobileTabBarProps) {
13
+ return (
14
+ <div
15
+ className="z-50 flex h-14 shrink-0 border-border/50 border-t bg-sidebar text-sidebar-foreground"
16
+ style={{
17
+ // Cap the safe-area inset — iOS Chrome can report its bottom UI bar
18
+ // (50–100px) as part of the safe area which would balloon the tab bar.
19
+ // 34px matches the iPhone home-indicator height (the typical max).
20
+ paddingBottom: 'min(env(safe-area-inset-bottom, 0px), 34px)',
21
+ }}
22
+ >
23
+ {tabs.map((tab) => {
24
+ const isActive = activeTab === tab.id
25
+ return (
26
+ <button
27
+ className={cn(
28
+ 'flex flex-1 flex-col items-center justify-center gap-0.5 text-xs transition-colors',
29
+ isActive ? 'text-foreground' : 'text-muted-foreground',
30
+ )}
31
+ key={tab.id}
32
+ onClick={() => onTabPress(tab.id)}
33
+ type="button"
34
+ >
35
+ {tab.mobileIcon ? (
36
+ <span className={cn('flex h-5 w-5 items-center justify-center')}>
37
+ {tab.mobileIcon}
38
+ </span>
39
+ ) : null}
40
+ <span className="font-medium">{tab.label}</span>
41
+ </button>
42
+ )
43
+ })}
44
+ </div>
45
+ )
46
+ }
@@ -88,8 +88,8 @@ const SHORTCUT_CATEGORIES: ShortcutCategory[] = [
88
88
  {
89
89
  title: 'Item Placement',
90
90
  shortcuts: [
91
- { keys: ['R'], action: 'Rotate item clockwise by 90 degrees' },
92
- { keys: ['T'], action: 'Rotate item counter-clockwise by 90 degrees' },
91
+ { keys: ['R'], action: 'Rotate item clockwise, or toggle selected door open/closed' },
92
+ { keys: ['T'], action: 'Rotate item counter-clockwise, or close selected door' },
93
93
  {
94
94
  keys: ['Shift'],
95
95
  action: 'Temporarily bypass placement validation constraints',
@@ -1,7 +1,7 @@
1
- import { type AnyNodeId, type BuildingNode, LevelNode, useScene } from '@pascal-app/core'
1
+ import { type BuildingNode, LevelNode, useScene } from '@pascal-app/core'
2
2
  import { useViewer } from '@pascal-app/viewer'
3
3
  import { Building2, Plus } from 'lucide-react'
4
- import { useState } from 'react'
4
+ import { memo, useState } from 'react'
5
5
  import { useShallow } from 'zustand/react/shallow'
6
6
  import {
7
7
  Tooltip,
@@ -12,12 +12,16 @@ import { focusTreeNode, TreeNode, TreeNodeWrapper } from './tree-node'
12
12
  import { TreeNodeActions } from './tree-node-actions'
13
13
 
14
14
  interface BuildingTreeNodeProps {
15
- nodeId: AnyNodeId
15
+ nodeId: BuildingNode['id']
16
16
  depth: number
17
17
  isLast?: boolean
18
18
  }
19
19
 
20
- export function BuildingTreeNode({ nodeId, depth, isLast }: BuildingTreeNodeProps) {
20
+ export const BuildingTreeNode = memo(function BuildingTreeNode({
21
+ nodeId,
22
+ depth,
23
+ isLast,
24
+ }: BuildingTreeNodeProps) {
21
25
  const [expanded, setExpanded] = useState(true)
22
26
  const createNode = useScene((state) => state.createNode)
23
27
  const isVisible = useScene((s) => s.nodes[nodeId]?.visible !== false)
@@ -84,4 +88,4 @@ export function BuildingTreeNode({ nodeId, depth, isLast }: BuildingTreeNodeProp
84
88
  ))}
85
89
  </TreeNodeWrapper>
86
90
  )
87
- }
91
+ })
@@ -1,7 +1,7 @@
1
1
  import { type AnyNodeId, type CeilingNode, useScene } from '@pascal-app/core'
2
2
  import { useViewer } from '@pascal-app/viewer'
3
3
  import Image from 'next/image'
4
- import { useCallback, useEffect, useState } from 'react'
4
+ import { memo, useCallback, useEffect, useState } from 'react'
5
5
  import { useShallow } from 'zustand/react/shallow'
6
6
  import useEditor from './../../../../../store/use-editor'
7
7
  import { InlineRenameInput } from './inline-rename-input'
@@ -14,7 +14,11 @@ interface CeilingTreeNodeProps {
14
14
  isLast?: boolean
15
15
  }
16
16
 
17
- export function CeilingTreeNode({ nodeId, depth, isLast }: CeilingTreeNodeProps) {
17
+ export const CeilingTreeNode = memo(function CeilingTreeNode({
18
+ nodeId,
19
+ depth,
20
+ isLast,
21
+ }: CeilingTreeNodeProps) {
18
22
  const [expanded, setExpanded] = useState(false)
19
23
  const [isEditing, setIsEditing] = useState(false)
20
24
  const isVisible = useScene((s) => s.nodes[nodeId as AnyNodeId]?.visible !== false)
@@ -113,7 +117,7 @@ export function CeilingTreeNode({ nodeId, depth, isLast }: CeilingTreeNodeProps)
113
117
  ))}
114
118
  </TreeNodeWrapper>
115
119
  )
116
- }
120
+ })
117
121
 
118
122
  /**
119
123
  * Calculate the area of a polygon using the shoelace formula
@@ -0,0 +1,77 @@
1
+ import { type AnyNodeId, type ColumnNode, useScene } from '@pascal-app/core'
2
+ import { useViewer } from '@pascal-app/viewer'
3
+ import Image from 'next/image'
4
+ import { memo, useCallback, useState } from 'react'
5
+ import useEditor from './../../../../../store/use-editor'
6
+ import { InlineRenameInput } from './inline-rename-input'
7
+ import { focusTreeNode, handleTreeSelection, TreeNodeWrapper } from './tree-node'
8
+ import { TreeNodeActions } from './tree-node-actions'
9
+
10
+ interface ColumnTreeNodeProps {
11
+ nodeId: AnyNodeId
12
+ depth: number
13
+ isLast?: boolean
14
+ }
15
+
16
+ export const ColumnTreeNode = memo(function ColumnTreeNode({
17
+ nodeId,
18
+ depth,
19
+ isLast,
20
+ }: ColumnTreeNodeProps) {
21
+ const [isEditing, setIsEditing] = useState(false)
22
+ const isVisible = useScene((s) => s.nodes[nodeId]?.visible !== false)
23
+ const node = useScene((s) => s.nodes[nodeId] as ColumnNode | undefined)
24
+ const isSelected = useViewer((state) => state.selection.selectedIds.includes(nodeId))
25
+ const isHovered = useViewer((state) => state.hoveredId === nodeId)
26
+ const setSelection = useViewer((state) => state.setSelection)
27
+ const setHoveredId = useViewer((state) => state.setHoveredId)
28
+
29
+ const handleClick = useCallback(
30
+ (e: React.MouseEvent) => {
31
+ e.stopPropagation()
32
+ const handled = handleTreeSelection(
33
+ e,
34
+ nodeId,
35
+ useViewer.getState().selection.selectedIds,
36
+ setSelection,
37
+ )
38
+ if (!handled && useEditor.getState().phase === 'furnish') {
39
+ useEditor.getState().setPhase('structure')
40
+ }
41
+ },
42
+ [nodeId, setSelection],
43
+ )
44
+
45
+ const defaultName = node?.name || 'Column'
46
+
47
+ return (
48
+ <TreeNodeWrapper
49
+ actions={<TreeNodeActions nodeId={nodeId} />}
50
+ depth={depth}
51
+ expanded={false}
52
+ hasChildren={false}
53
+ icon={
54
+ <Image alt="" className="object-contain" height={14} src="/icons/column.png" width={14} />
55
+ }
56
+ isHovered={isHovered}
57
+ isLast={isLast}
58
+ isSelected={isSelected}
59
+ isVisible={isVisible}
60
+ label={
61
+ <InlineRenameInput
62
+ defaultName={defaultName}
63
+ isEditing={isEditing}
64
+ nodeId={nodeId}
65
+ onStartEditing={() => setIsEditing(true)}
66
+ onStopEditing={() => setIsEditing(false)}
67
+ />
68
+ }
69
+ nodeId={nodeId}
70
+ onClick={handleClick}
71
+ onDoubleClick={() => focusTreeNode(nodeId)}
72
+ onMouseEnter={() => setHoveredId(nodeId)}
73
+ onMouseLeave={() => setHoveredId(null)}
74
+ onToggle={() => {}}
75
+ />
76
+ )
77
+ })
@@ -3,7 +3,7 @@
3
3
  import { type AnyNodeId, useScene } from '@pascal-app/core'
4
4
  import { useViewer } from '@pascal-app/viewer'
5
5
  import Image from 'next/image'
6
- import { useCallback, useState } from 'react'
6
+ import { memo, useCallback, useState } from 'react'
7
7
  import useEditor from './../../../../../store/use-editor'
8
8
  import { InlineRenameInput } from './inline-rename-input'
9
9
  import { focusTreeNode, handleTreeSelection, TreeNodeWrapper } from './tree-node'
@@ -15,7 +15,11 @@ interface DoorTreeNodeProps {
15
15
  isLast?: boolean
16
16
  }
17
17
 
18
- export function DoorTreeNode({ nodeId, depth, isLast }: DoorTreeNodeProps) {
18
+ export const DoorTreeNode = memo(function DoorTreeNode({
19
+ nodeId,
20
+ depth,
21
+ isLast,
22
+ }: DoorTreeNodeProps) {
19
23
  const [isEditing, setIsEditing] = useState(false)
20
24
  const isVisible = useScene((s) => s.nodes[nodeId as AnyNodeId]?.visible !== false)
21
25
  const isSelected = useViewer((state) => state.selection.selectedIds.includes(nodeId))
@@ -72,4 +76,4 @@ export function DoorTreeNode({ nodeId, depth, isLast }: DoorTreeNodeProps) {
72
76
  onToggle={() => {}}
73
77
  />
74
78
  )
75
- }
79
+ })
@@ -1,7 +1,7 @@
1
1
  import { type AnyNodeId, type FenceNode, useScene } from '@pascal-app/core'
2
2
  import { useViewer } from '@pascal-app/viewer'
3
3
  import Image from 'next/image'
4
- import { useState } from 'react'
4
+ import { memo, useState } from 'react'
5
5
  import useEditor from '../../../../../store/use-editor'
6
6
  import { InlineRenameInput } from './inline-rename-input'
7
7
  import { focusTreeNode, handleTreeSelection, TreeNodeWrapper } from './tree-node'
@@ -13,7 +13,11 @@ interface FenceTreeNodeProps {
13
13
  isLast?: boolean
14
14
  }
15
15
 
16
- export function FenceTreeNode({ nodeId, depth, isLast }: FenceTreeNodeProps) {
16
+ export const FenceTreeNode = memo(function FenceTreeNode({
17
+ nodeId,
18
+ depth,
19
+ isLast,
20
+ }: FenceTreeNodeProps) {
17
21
  const node = useScene((state) => state.nodes[nodeId]) as FenceNode | undefined
18
22
  const [isEditing, setIsEditing] = useState(false)
19
23
  const selectedIds = useViewer((state) => state.selection.selectedIds)
@@ -62,4 +66,4 @@ export function FenceTreeNode({ nodeId, depth, isLast }: FenceTreeNodeProps) {
62
66
  onToggle={() => {}}
63
67
  />
64
68
  )
65
- }
69
+ })