@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,160 @@
1
+ 'use client'
2
+
3
+ import { Icon } from '@iconify/react'
4
+ import { memo, useMemo } from 'react'
5
+ import useEditor, { type FloorplanSelectionTool } from '../../store/use-editor'
6
+ import { furnishTools } from '../ui/action-menu/furnish-tools'
7
+ import { tools as structureTools } from '../ui/action-menu/structure-tools'
8
+
9
+ type SvgPoint = {
10
+ x: number
11
+ y: number
12
+ }
13
+
14
+ type FloorplanCursorIndicator =
15
+ | {
16
+ kind: 'asset'
17
+ iconSrc: string
18
+ }
19
+ | {
20
+ kind: 'icon'
21
+ icon: string
22
+ }
23
+
24
+ type FloorplanCursorIndicatorOverlayProps = {
25
+ cursorPosition: SvgPoint | null
26
+ cursorAnchorPosition: SvgPoint | null
27
+ floorplanSelectionTool: FloorplanSelectionTool
28
+ movingOpeningType: 'door' | 'window' | null
29
+ isPanning: boolean
30
+ cursorColor: string
31
+ indicatorLineHeight?: number
32
+ indicatorBadgeOffsetX?: number
33
+ indicatorBadgeOffsetY?: number
34
+ }
35
+
36
+ export const FloorplanCursorIndicatorOverlay = memo(function FloorplanCursorIndicatorOverlay({
37
+ cursorPosition,
38
+ cursorAnchorPosition,
39
+ floorplanSelectionTool,
40
+ movingOpeningType,
41
+ isPanning,
42
+ cursorColor,
43
+ indicatorLineHeight = 18,
44
+ indicatorBadgeOffsetX = 14,
45
+ indicatorBadgeOffsetY = 14,
46
+ }: FloorplanCursorIndicatorOverlayProps) {
47
+ const mode = useEditor((state) => state.mode)
48
+ const tool = useEditor((state) => state.tool)
49
+ const structureLayer = useEditor((state) => state.structureLayer)
50
+ const catalogCategory = useEditor((state) => state.catalogCategory)
51
+
52
+ const activeFloorplanToolConfig = useMemo(() => {
53
+ if (movingOpeningType) {
54
+ return structureTools.find((entry) => entry.id === movingOpeningType) ?? null
55
+ }
56
+
57
+ if (mode !== 'build' || !tool) {
58
+ return null
59
+ }
60
+
61
+ if (tool === 'item' && catalogCategory) {
62
+ return furnishTools.find((entry) => entry.catalogCategory === catalogCategory) ?? null
63
+ }
64
+
65
+ return structureTools.find((entry) => entry.id === tool) ?? null
66
+ }, [catalogCategory, mode, movingOpeningType, tool])
67
+
68
+ const indicator = useMemo<FloorplanCursorIndicator | null>(() => {
69
+ if (activeFloorplanToolConfig) {
70
+ return { kind: 'asset', iconSrc: activeFloorplanToolConfig.iconSrc }
71
+ }
72
+
73
+ if (mode === 'select' && floorplanSelectionTool === 'marquee' && structureLayer !== 'zones') {
74
+ return { kind: 'icon', icon: 'mdi:select-drag' }
75
+ }
76
+
77
+ if (mode === 'delete') {
78
+ return { kind: 'icon', icon: 'mdi:trash-can-outline' }
79
+ }
80
+
81
+ return null
82
+ }, [activeFloorplanToolConfig, floorplanSelectionTool, mode, structureLayer])
83
+
84
+ const position = mode === 'delete' ? cursorPosition : cursorAnchorPosition
85
+
86
+ if (!(indicator && position) || isPanning) {
87
+ return null
88
+ }
89
+
90
+ return (
91
+ <div
92
+ aria-hidden="true"
93
+ className="pointer-events-none absolute z-20"
94
+ style={{ left: position.x, top: position.y }}
95
+ >
96
+ {mode === 'delete' ? (
97
+ <div
98
+ className="flex h-8 w-8 items-center justify-center rounded-xl border border-white/5 bg-zinc-900/95 shadow-[0_8px_16px_-4px_rgba(0,0,0,0.3),0_4px_8px_-4px_rgba(0,0,0,0.2)]"
99
+ style={{
100
+ boxShadow: `0 8px 16px -4px rgba(0,0,0,0.3), 0 4px 8px -4px rgba(0,0,0,0.2), 0 0 18px ${cursorColor}22`,
101
+ transform: `translate(${indicatorBadgeOffsetX}px, ${indicatorBadgeOffsetY}px)`,
102
+ }}
103
+ >
104
+ {indicator.kind === 'asset' ? (
105
+ <img
106
+ alt=""
107
+ aria-hidden="true"
108
+ className="h-5 w-5 object-contain drop-shadow-[0_2px_4px_rgba(0,0,0,0.5)]"
109
+ src={indicator.iconSrc}
110
+ />
111
+ ) : (
112
+ <Icon
113
+ aria-hidden="true"
114
+ className="drop-shadow-[0_2px_4px_rgba(0,0,0,0.5)]"
115
+ color={cursorColor}
116
+ height={18}
117
+ icon={indicator.icon}
118
+ width={18}
119
+ />
120
+ )}
121
+ </div>
122
+ ) : (
123
+ <>
124
+ <div
125
+ className="absolute top-0 left-1/2 w-px -translate-x-1/2 -translate-y-full"
126
+ style={{
127
+ backgroundColor: cursorColor,
128
+ boxShadow: `0 0 12px ${cursorColor}55`,
129
+ height: indicatorLineHeight,
130
+ }}
131
+ />
132
+ <div
133
+ className="absolute top-0 left-1/2 flex h-8 w-8 items-center justify-center rounded-xl border border-white/5 bg-zinc-900/95 shadow-[0_8px_16px_-4px_rgba(0,0,0,0.3),0_4px_8px_-4px_rgba(0,0,0,0.2)]"
134
+ style={{
135
+ transform: `translate(-50%, calc(-100% - ${indicatorLineHeight}px))`,
136
+ }}
137
+ >
138
+ {indicator.kind === 'asset' ? (
139
+ <img
140
+ alt=""
141
+ aria-hidden="true"
142
+ className="h-5 w-5 object-contain drop-shadow-[0_2px_4px_rgba(0,0,0,0.5)]"
143
+ src={indicator.iconSrc}
144
+ />
145
+ ) : (
146
+ <Icon
147
+ aria-hidden="true"
148
+ className="drop-shadow-[0_2px_4px_rgba(0,0,0,0.5)]"
149
+ color="white"
150
+ height={18}
151
+ icon={indicator.icon}
152
+ width={18}
153
+ />
154
+ )}
155
+ </div>
156
+ </>
157
+ )}
158
+ </div>
159
+ )
160
+ })
@@ -0,0 +1,92 @@
1
+ 'use client'
2
+
3
+ import { memo, useEffect } from 'react'
4
+ import useEditor from '../../store/use-editor'
5
+
6
+ type FloorplanSiteKeyHandlerProps = {
7
+ onRestoreGroundLevel: () => void
8
+ }
9
+
10
+ export const FloorplanSiteKeyHandler = memo(function FloorplanSiteKeyHandler({
11
+ onRestoreGroundLevel,
12
+ }: FloorplanSiteKeyHandlerProps) {
13
+ const isFloorplanHovered = useEditor((state) => state.isFloorplanHovered)
14
+ const phase = useEditor((state) => state.phase)
15
+ const setFloorplanSelectionTool = useEditor((state) => state.setFloorplanSelectionTool)
16
+
17
+ useEffect(() => {
18
+ const handleKeyDown = (event: KeyboardEvent) => {
19
+ const target = event.target as HTMLElement | null
20
+ const isEditableTarget =
21
+ target instanceof HTMLInputElement ||
22
+ target instanceof HTMLTextAreaElement ||
23
+ Boolean(target?.isContentEditable)
24
+
25
+ if (
26
+ isEditableTarget ||
27
+ !isFloorplanHovered ||
28
+ phase !== 'site' ||
29
+ event.metaKey ||
30
+ event.ctrlKey ||
31
+ event.altKey ||
32
+ event.key.toLowerCase() !== 'v'
33
+ ) {
34
+ return
35
+ }
36
+
37
+ setFloorplanSelectionTool('click')
38
+ onRestoreGroundLevel()
39
+ }
40
+
41
+ window.addEventListener('keydown', handleKeyDown, true)
42
+ return () => {
43
+ window.removeEventListener('keydown', handleKeyDown, true)
44
+ }
45
+ }, [isFloorplanHovered, onRestoreGroundLevel, phase, setFloorplanSelectionTool])
46
+
47
+ return null
48
+ })
49
+
50
+ type FloorplanDuplicateHotkeyProps = {
51
+ hasDuplicatable: boolean
52
+ onDuplicateSelected: () => void
53
+ }
54
+
55
+ export const FloorplanDuplicateHotkey = memo(function FloorplanDuplicateHotkey({
56
+ hasDuplicatable,
57
+ onDuplicateSelected,
58
+ }: FloorplanDuplicateHotkeyProps) {
59
+ const isFloorplanHovered = useEditor((state) => state.isFloorplanHovered)
60
+
61
+ useEffect(() => {
62
+ const handleKeyDown = (event: KeyboardEvent) => {
63
+ if (!(event.metaKey || event.ctrlKey) || event.key.toLowerCase() !== 'c') {
64
+ return
65
+ }
66
+
67
+ if (!(isFloorplanHovered && hasDuplicatable)) {
68
+ return
69
+ }
70
+
71
+ const target = event.target as HTMLElement | null
72
+ const isEditableTarget =
73
+ target instanceof HTMLInputElement ||
74
+ target instanceof HTMLTextAreaElement ||
75
+ Boolean(target?.isContentEditable)
76
+
77
+ if (isEditableTarget) {
78
+ return
79
+ }
80
+
81
+ event.preventDefault()
82
+ onDuplicateSelected()
83
+ }
84
+
85
+ window.addEventListener('keydown', handleKeyDown, true)
86
+ return () => {
87
+ window.removeEventListener('keydown', handleKeyDown, true)
88
+ }
89
+ }, [hasDuplicatable, isFloorplanHovered, onDuplicateSelected])
90
+
91
+ return null
92
+ })
@@ -0,0 +1,119 @@
1
+ 'use client'
2
+
3
+ import { memo } from 'react'
4
+
5
+ type SvgLine = {
6
+ x1: number
7
+ y1: number
8
+ x2: number
9
+ y2: number
10
+ }
11
+
12
+ type FloorplanDraftLayerProps = {
13
+ draftPolygonPoints: string | null
14
+ linearDraftSegment: SvgLine | null
15
+ polygonDraftPolygonPoints: string | null
16
+ polygonDraftPolylinePoints: string | null
17
+ polygonDraftClosingSegment: SvgLine | null
18
+ draftAnchorPoints: Array<{ x: number; y: number; isPrimary: boolean }>
19
+ draftFill: string
20
+ draftStroke: string
21
+ polygonDraftStroke?: string
22
+ polygonDraftStrokeWidth?: string
23
+ anchorFill: string
24
+ unitsPerPixel: number
25
+ }
26
+
27
+ export const FloorplanDraftLayer = memo(function FloorplanDraftLayer({
28
+ draftPolygonPoints,
29
+ linearDraftSegment,
30
+ polygonDraftPolygonPoints,
31
+ polygonDraftPolylinePoints,
32
+ polygonDraftClosingSegment,
33
+ draftAnchorPoints,
34
+ draftFill,
35
+ draftStroke,
36
+ polygonDraftStroke,
37
+ polygonDraftStrokeWidth = '0.08',
38
+ anchorFill,
39
+ unitsPerPixel,
40
+ }: FloorplanDraftLayerProps) {
41
+ const primaryAnchorRadius = 6 * unitsPerPixel
42
+ const secondaryAnchorRadius = 5 * unitsPerPixel
43
+ const activePolygonDraftStroke = polygonDraftStroke ?? draftStroke
44
+
45
+ return (
46
+ <>
47
+ {draftPolygonPoints && (
48
+ <polygon
49
+ fill={draftFill}
50
+ fillOpacity={0.35}
51
+ points={draftPolygonPoints}
52
+ stroke={draftStroke}
53
+ strokeDasharray="0.24 0.12"
54
+ strokeWidth="0.07"
55
+ vectorEffect="non-scaling-stroke"
56
+ />
57
+ )}
58
+
59
+ {linearDraftSegment && (
60
+ <line
61
+ stroke={draftStroke}
62
+ strokeDasharray="0.2 0.12"
63
+ strokeLinecap="round"
64
+ strokeOpacity={0.95}
65
+ strokeWidth="0.08"
66
+ vectorEffect="non-scaling-stroke"
67
+ x1={linearDraftSegment.x1}
68
+ x2={linearDraftSegment.x2}
69
+ y1={linearDraftSegment.y1}
70
+ y2={linearDraftSegment.y2}
71
+ />
72
+ )}
73
+
74
+ {polygonDraftPolygonPoints && (
75
+ <polygon fill={draftFill} fillOpacity={0.2} points={polygonDraftPolygonPoints} stroke="none" />
76
+ )}
77
+
78
+ {polygonDraftPolylinePoints && (
79
+ <polyline
80
+ fill="none"
81
+ points={polygonDraftPolylinePoints}
82
+ stroke={activePolygonDraftStroke}
83
+ strokeLinecap="round"
84
+ strokeLinejoin="round"
85
+ strokeWidth={polygonDraftStrokeWidth}
86
+ vectorEffect="non-scaling-stroke"
87
+ />
88
+ )}
89
+
90
+ {polygonDraftClosingSegment && (
91
+ <line
92
+ stroke={activePolygonDraftStroke}
93
+ strokeDasharray="0.16 0.1"
94
+ strokeLinecap="round"
95
+ strokeOpacity={0.75}
96
+ strokeWidth={polygonDraftStrokeWidth}
97
+ vectorEffect="non-scaling-stroke"
98
+ x1={polygonDraftClosingSegment.x1}
99
+ x2={polygonDraftClosingSegment.x2}
100
+ y1={polygonDraftClosingSegment.y1}
101
+ y2={polygonDraftClosingSegment.y2}
102
+ />
103
+ )}
104
+
105
+ {draftAnchorPoints.map((point, index) => (
106
+ <circle
107
+ cx={point.x}
108
+ cy={point.y}
109
+ fill={point.isPrimary ? anchorFill : draftStroke}
110
+ fillOpacity={0.95}
111
+ key={`polygon-draft-${index}`}
112
+ pointerEvents="none"
113
+ r={point.isPrimary ? primaryAnchorRadius : secondaryAnchorRadius}
114
+ vectorEffect="non-scaling-stroke"
115
+ />
116
+ ))}
117
+ </>
118
+ )
119
+ })
@@ -0,0 +1,58 @@
1
+ 'use client'
2
+
3
+ import { memo } from 'react'
4
+
5
+ type SvgSelectionBounds = {
6
+ x: number
7
+ y: number
8
+ width: number
9
+ height: number
10
+ }
11
+
12
+ type FloorplanMarqueeLayerProps = {
13
+ bounds: SvgSelectionBounds | null
14
+ cursorColor: string
15
+ outlineWidth: number
16
+ glowWidth: number
17
+ }
18
+
19
+ export const FloorplanMarqueeLayer = memo(function FloorplanMarqueeLayer({
20
+ bounds,
21
+ cursorColor,
22
+ outlineWidth,
23
+ glowWidth,
24
+ }: FloorplanMarqueeLayerProps) {
25
+ if (!bounds) {
26
+ return null
27
+ }
28
+
29
+ return (
30
+ <>
31
+ <rect
32
+ fill={cursorColor}
33
+ fillOpacity={0.12}
34
+ height={bounds.height}
35
+ pointerEvents="none"
36
+ stroke={cursorColor}
37
+ strokeOpacity={0.26}
38
+ strokeWidth={glowWidth}
39
+ vectorEffect="non-scaling-stroke"
40
+ width={bounds.width}
41
+ x={bounds.x}
42
+ y={bounds.y}
43
+ />
44
+ <rect
45
+ fill="none"
46
+ height={bounds.height}
47
+ pointerEvents="none"
48
+ stroke={cursorColor}
49
+ strokeOpacity={0.96}
50
+ strokeWidth={outlineWidth}
51
+ vectorEffect="non-scaling-stroke"
52
+ width={bounds.width}
53
+ x={bounds.x}
54
+ y={bounds.y}
55
+ />
56
+ </>
57
+ )
58
+ })
@@ -0,0 +1,197 @@
1
+ 'use client'
2
+
3
+ import { memo } from 'react'
4
+
5
+ const FLOORPLAN_MEASUREMENT_LINE_WIDTH = 1.35
6
+ const FLOORPLAN_MEASUREMENT_LINE_OPACITY = 0.95
7
+ const FLOORPLAN_MEASUREMENT_LABEL_FONT_SIZE = 0.15
8
+ const FLOORPLAN_MEASUREMENT_LABEL_OPACITY = 0.98
9
+ const FLOORPLAN_MEASUREMENT_EXTENSION_DASH = '0.08 0.12'
10
+ const FLOORPLAN_MEASUREMENT_END_TICK = 0.18
11
+
12
+ export type LinearMeasurementOverlay = {
13
+ dashedExtensions?: boolean
14
+ id: string
15
+ dimensionLineEnd: { x1: number; y1: number; x2: number; y2: number }
16
+ dimensionLineStart: { x1: number; y1: number; x2: number; y2: number }
17
+ extensionStart: { x1: number; y1: number; x2: number; y2: number }
18
+ extensionEnd: { x1: number; y1: number; x2: number; y2: number }
19
+ label: string
20
+ labelX: number
21
+ labelY: number
22
+ labelAngleDeg: number
23
+ extensionStroke?: string
24
+ isSelected?: boolean
25
+ labelFill?: string
26
+ showTicks?: boolean
27
+ stroke?: string
28
+ }
29
+
30
+ type FloorplanMeasurementPalette = {
31
+ measurementStroke: string
32
+ }
33
+
34
+ type FloorplanMeasurementLineProps = {
35
+ palette: FloorplanMeasurementPalette
36
+ segment: { x1: number; y1: number; x2: number; y2: number }
37
+ isSelected?: boolean
38
+ dashed?: boolean
39
+ stroke?: string
40
+ }
41
+
42
+ function FloorplanMeasurementLine({
43
+ palette,
44
+ segment,
45
+ isSelected,
46
+ dashed = false,
47
+ stroke,
48
+ }: FloorplanMeasurementLineProps) {
49
+ const lineOpacity = isSelected
50
+ ? FLOORPLAN_MEASUREMENT_LINE_OPACITY
51
+ : FLOORPLAN_MEASUREMENT_LINE_OPACITY * 0.4
52
+
53
+ return (
54
+ <line
55
+ shapeRendering="geometricPrecision"
56
+ stroke={stroke ?? palette.measurementStroke}
57
+ strokeDasharray={dashed ? FLOORPLAN_MEASUREMENT_EXTENSION_DASH : undefined}
58
+ strokeLinecap="round"
59
+ strokeOpacity={lineOpacity}
60
+ strokeWidth={FLOORPLAN_MEASUREMENT_LINE_WIDTH}
61
+ vectorEffect="non-scaling-stroke"
62
+ x1={segment.x1}
63
+ x2={segment.x2}
64
+ y1={segment.y1}
65
+ y2={segment.y2}
66
+ />
67
+ )
68
+ }
69
+
70
+ type FloorplanMeasurementTickProps = {
71
+ palette: FloorplanMeasurementPalette
72
+ x: number
73
+ y: number
74
+ angleDeg: number
75
+ isSelected?: boolean
76
+ stroke?: string
77
+ }
78
+
79
+ function FloorplanMeasurementTick({
80
+ palette,
81
+ x,
82
+ y,
83
+ angleDeg,
84
+ isSelected,
85
+ stroke,
86
+ }: FloorplanMeasurementTickProps) {
87
+ const radians = (angleDeg * Math.PI) / 180
88
+ const nx = -Math.sin(radians)
89
+ const ny = Math.cos(radians)
90
+ const half = FLOORPLAN_MEASUREMENT_END_TICK / 2
91
+
92
+ return (
93
+ <line
94
+ shapeRendering="geometricPrecision"
95
+ stroke={stroke ?? palette.measurementStroke}
96
+ strokeLinecap="round"
97
+ strokeOpacity={
98
+ isSelected ? FLOORPLAN_MEASUREMENT_LINE_OPACITY : FLOORPLAN_MEASUREMENT_LINE_OPACITY * 0.4
99
+ }
100
+ strokeWidth={FLOORPLAN_MEASUREMENT_LINE_WIDTH}
101
+ vectorEffect="non-scaling-stroke"
102
+ x1={x - nx * half}
103
+ x2={x + nx * half}
104
+ y1={y - ny * half}
105
+ y2={y + ny * half}
106
+ />
107
+ )
108
+ }
109
+
110
+ type FloorplanMeasurementsLayerProps = {
111
+ className: string
112
+ measurements: LinearMeasurementOverlay[]
113
+ palette: FloorplanMeasurementPalette
114
+ }
115
+
116
+ export const FloorplanMeasurementsLayer = memo(function FloorplanMeasurementsLayer({
117
+ className,
118
+ measurements,
119
+ palette,
120
+ }: FloorplanMeasurementsLayerProps) {
121
+ if (measurements.length === 0) {
122
+ return null
123
+ }
124
+
125
+ return (
126
+ <>
127
+ {measurements.map((measurement) => (
128
+ <g className={className} key={measurement.id} pointerEvents="none" style={{ userSelect: 'none' }}>
129
+ <FloorplanMeasurementLine
130
+ dashed={measurement.dashedExtensions ?? true}
131
+ isSelected={measurement.isSelected}
132
+ palette={palette}
133
+ segment={measurement.extensionStart}
134
+ stroke={measurement.extensionStroke}
135
+ />
136
+ <FloorplanMeasurementLine
137
+ isSelected={measurement.isSelected}
138
+ palette={palette}
139
+ segment={measurement.dimensionLineStart}
140
+ stroke={measurement.stroke}
141
+ />
142
+ <FloorplanMeasurementLine
143
+ isSelected={measurement.isSelected}
144
+ palette={palette}
145
+ segment={measurement.dimensionLineEnd}
146
+ stroke={measurement.stroke}
147
+ />
148
+ <FloorplanMeasurementLine
149
+ dashed={measurement.dashedExtensions ?? true}
150
+ isSelected={measurement.isSelected}
151
+ palette={palette}
152
+ segment={measurement.extensionEnd}
153
+ stroke={measurement.extensionStroke}
154
+ />
155
+ {measurement.showTicks !== false ? (
156
+ <>
157
+ <FloorplanMeasurementTick
158
+ angleDeg={measurement.labelAngleDeg}
159
+ isSelected={measurement.isSelected}
160
+ palette={palette}
161
+ stroke={measurement.stroke}
162
+ x={measurement.dimensionLineStart.x1}
163
+ y={measurement.dimensionLineStart.y1}
164
+ />
165
+ <FloorplanMeasurementTick
166
+ angleDeg={measurement.labelAngleDeg}
167
+ isSelected={measurement.isSelected}
168
+ palette={palette}
169
+ stroke={measurement.stroke}
170
+ x={measurement.dimensionLineEnd.x2}
171
+ y={measurement.dimensionLineEnd.y2}
172
+ />
173
+ </>
174
+ ) : null}
175
+ <text
176
+ dominantBaseline="central"
177
+ fill={measurement.labelFill ?? palette.measurementStroke}
178
+ fillOpacity={
179
+ measurement.isSelected
180
+ ? FLOORPLAN_MEASUREMENT_LABEL_OPACITY
181
+ : FLOORPLAN_MEASUREMENT_LABEL_OPACITY * 0.4
182
+ }
183
+ fontFamily="ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace"
184
+ fontSize={FLOORPLAN_MEASUREMENT_LABEL_FONT_SIZE}
185
+ fontWeight="600"
186
+ textAnchor="middle"
187
+ transform={`rotate(${measurement.labelAngleDeg} ${measurement.labelX} ${measurement.labelY}) translate(0, -0.04)`}
188
+ x={measurement.labelX}
189
+ y={measurement.labelY}
190
+ >
191
+ {measurement.label}
192
+ </text>
193
+ </g>
194
+ ))}
195
+ </>
196
+ )
197
+ })