@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.
Files changed (122) hide show
  1. package/package.json +9 -5
  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 +20 -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 +32 -55
  10. package/src/components/editor/floorplan-background-selection.ts +113 -0
  11. package/src/components/editor/floorplan-panel.tsx +9855 -3298
  12. package/src/components/editor/index.tsx +269 -21
  13. package/src/components/editor/selection-manager.tsx +575 -13
  14. package/src/components/editor/thumbnail-generator.tsx +38 -7
  15. package/src/components/editor/use-floorplan-background-placement.ts +257 -0
  16. package/src/components/editor/use-floorplan-hit-testing.ts +171 -0
  17. package/src/components/editor/use-floorplan-scene-data.ts +189 -0
  18. package/src/components/editor/wall-measurement-label.tsx +267 -36
  19. package/src/components/editor-2d/floorplan-action-menu-layer.tsx +95 -0
  20. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +160 -0
  21. package/src/components/editor-2d/floorplan-hotkey-handlers.tsx +92 -0
  22. package/src/components/editor-2d/renderers/floorplan-draft-layer.tsx +119 -0
  23. package/src/components/editor-2d/renderers/floorplan-marquee-layer.tsx +58 -0
  24. package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +197 -0
  25. package/src/components/editor-2d/renderers/floorplan-roof-layer.tsx +113 -0
  26. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +474 -0
  27. package/src/components/editor-2d/svg-paths.ts +119 -0
  28. package/src/components/tools/ceiling/ceiling-boundary-editor.tsx +1 -0
  29. package/src/components/tools/ceiling/ceiling-hole-editor.tsx +1 -0
  30. package/src/components/tools/ceiling/ceiling-tool.tsx +5 -5
  31. package/src/components/tools/column/column-tool.tsx +97 -0
  32. package/src/components/tools/column/move-column-tool.tsx +105 -0
  33. package/src/components/tools/door/door-tool.tsx +7 -0
  34. package/src/components/tools/door/move-door-tool.tsx +28 -8
  35. package/src/components/tools/fence/fence-drafting.ts +10 -3
  36. package/src/components/tools/fence/fence-tool.tsx +159 -3
  37. package/src/components/tools/fence/move-fence-endpoint-tool.tsx +129 -18
  38. package/src/components/tools/fence/move-fence-tool.tsx +101 -34
  39. package/src/components/tools/item/move-tool.tsx +10 -1
  40. package/src/components/tools/item/placement-math.ts +30 -1
  41. package/src/components/tools/item/placement-strategies.ts +109 -31
  42. package/src/components/tools/item/placement-types.ts +7 -0
  43. package/src/components/tools/item/use-draft-node.ts +2 -0
  44. package/src/components/tools/item/use-placement-coordinator.tsx +660 -52
  45. package/src/components/tools/roof/move-roof-tool.tsx +22 -15
  46. package/src/components/tools/shared/polygon-editor.tsx +153 -28
  47. package/src/components/tools/shared/segment-angle.ts +156 -0
  48. package/src/components/tools/slab/slab-boundary-editor.tsx +1 -0
  49. package/src/components/tools/slab/slab-hole-editor.tsx +1 -0
  50. package/src/components/tools/spawn/move-spawn-tool.tsx +101 -0
  51. package/src/components/tools/spawn/spawn-tool.tsx +130 -0
  52. package/src/components/tools/tool-manager.tsx +18 -3
  53. package/src/components/tools/wall/move-wall-endpoint-tool.tsx +121 -20
  54. package/src/components/tools/wall/wall-drafting.ts +18 -9
  55. package/src/components/tools/wall/wall-tool.tsx +134 -2
  56. package/src/components/tools/window/move-window-tool.tsx +18 -0
  57. package/src/components/tools/window/window-tool.tsx +5 -0
  58. package/src/components/ui/action-menu/camera-actions.tsx +37 -33
  59. package/src/components/ui/action-menu/control-modes.tsx +28 -1
  60. package/src/components/ui/action-menu/index.tsx +91 -1
  61. package/src/components/ui/action-menu/structure-tools.tsx +2 -0
  62. package/src/components/ui/action-menu/view-toggles.tsx +424 -35
  63. package/src/components/ui/command-palette/editor-commands.tsx +18 -1
  64. package/src/components/ui/controls/material-picker.tsx +152 -165
  65. package/src/components/ui/controls/slider-control.tsx +66 -18
  66. package/src/components/ui/floating-level-selector.tsx +286 -55
  67. package/src/components/ui/helpers/helper-manager.tsx +5 -0
  68. package/src/components/ui/item-catalog/catalog-items.tsx +1116 -1219
  69. package/src/components/ui/item-catalog/item-catalog.tsx +42 -175
  70. package/src/components/ui/level-duplicate-dialog.tsx +115 -0
  71. package/src/components/ui/panels/ceiling-panel.tsx +1 -25
  72. package/src/components/ui/panels/column-panel.tsx +715 -0
  73. package/src/components/ui/panels/door-panel.tsx +981 -289
  74. package/src/components/ui/panels/fence-panel.tsx +3 -45
  75. package/src/components/ui/panels/mobile-panel-sheet.tsx +108 -0
  76. package/src/components/ui/panels/mobile-selection-bar.tsx +100 -0
  77. package/src/components/ui/panels/node-display.ts +39 -0
  78. package/src/components/ui/panels/paint-panel.tsx +138 -0
  79. package/src/components/ui/panels/panel-manager.tsx +210 -1
  80. package/src/components/ui/panels/panel-wrapper.tsx +48 -39
  81. package/src/components/ui/panels/reference-panel.tsx +238 -5
  82. package/src/components/ui/panels/roof-panel.tsx +4 -105
  83. package/src/components/ui/panels/roof-segment-panel.tsx +0 -25
  84. package/src/components/ui/panels/slab-panel.tsx +4 -30
  85. package/src/components/ui/panels/spawn-panel.tsx +155 -0
  86. package/src/components/ui/panels/stair-panel.tsx +11 -117
  87. package/src/components/ui/panels/stair-segment-panel.tsx +0 -25
  88. package/src/components/ui/panels/wall-panel.tsx +1 -95
  89. package/src/components/ui/panels/window-panel.tsx +660 -139
  90. package/src/components/ui/sidebar/mobile-tab-bar.tsx +46 -0
  91. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +2 -2
  92. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +2 -2
  93. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +77 -0
  94. package/src/components/ui/sidebar/panels/site-panel/index.tsx +109 -24
  95. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +2 -2
  96. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +82 -0
  97. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +9 -3
  98. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +8 -5
  99. package/src/components/ui/sidebar/tab-bar.tsx +3 -0
  100. package/src/components/ui/viewer-toolbar.tsx +42 -1
  101. package/src/hooks/use-auto-frame.ts +45 -0
  102. package/src/hooks/use-keyboard.ts +64 -7
  103. package/src/hooks/use-mobile.ts +12 -12
  104. package/src/lib/door-interaction.ts +88 -0
  105. package/src/lib/floorplan/geometry.ts +263 -0
  106. package/src/lib/floorplan/index.ts +38 -0
  107. package/src/lib/floorplan/items.ts +179 -0
  108. package/src/lib/floorplan/selection-tool.ts +231 -0
  109. package/src/lib/floorplan/stairs.ts +478 -0
  110. package/src/lib/floorplan/types.ts +57 -0
  111. package/src/lib/floorplan/walls.ts +23 -0
  112. package/src/lib/guide-events.ts +10 -0
  113. package/src/lib/level-duplication.test.ts +72 -0
  114. package/src/lib/level-duplication.ts +153 -0
  115. package/src/lib/local-guide-image.ts +42 -0
  116. package/src/lib/material-paint.ts +284 -0
  117. package/src/lib/roof-duplication.ts +214 -0
  118. package/src/lib/scene-bounds.test.ts +183 -0
  119. package/src/lib/scene-bounds.ts +169 -0
  120. package/src/lib/stair-duplication.ts +126 -0
  121. package/src/lib/window-interaction.ts +86 -0
  122. package/src/store/use-editor.tsx +164 -8
@@ -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
+ })
@@ -0,0 +1,113 @@
1
+ 'use client'
2
+
3
+ import type { Point2D, RoofNode, RoofSegmentNode } from '@pascal-app/core'
4
+ import { memo } from 'react'
5
+ import { toSvgX, toSvgY } from '../svg-paths'
6
+
7
+ type FloorplanLineSegment = {
8
+ start: Point2D
9
+ end: Point2D
10
+ }
11
+
12
+ type FloorplanRoofSegmentEntry = {
13
+ segment: RoofSegmentNode
14
+ points: string
15
+ ridgeLine: FloorplanLineSegment | null
16
+ }
17
+
18
+ type FloorplanRoofEntry = {
19
+ roof: RoofNode
20
+ segments: FloorplanRoofSegmentEntry[]
21
+ }
22
+
23
+ type FloorplanRoofPalette = {
24
+ roofFill: string
25
+ roofActiveFill: string
26
+ roofSelectedFill: string
27
+ roofStroke: string
28
+ roofActiveStroke: string
29
+ roofSelectedStroke: string
30
+ roofRidgeStroke: string
31
+ roofSelectedRidgeStroke: string
32
+ }
33
+
34
+ type FloorplanRoofLayerProps = {
35
+ highlightedIdSet: ReadonlySet<string>
36
+ palette: FloorplanRoofPalette
37
+ roofEntries: FloorplanRoofEntry[]
38
+ selectedIdSet: ReadonlySet<string>
39
+ }
40
+
41
+ export const FloorplanRoofLayer = memo(function FloorplanRoofLayer({
42
+ highlightedIdSet,
43
+ palette,
44
+ roofEntries,
45
+ selectedIdSet,
46
+ }: FloorplanRoofLayerProps) {
47
+ if (roofEntries.length === 0) {
48
+ return null
49
+ }
50
+
51
+ return (
52
+ <>
53
+ {roofEntries.map(({ roof, segments }) => {
54
+ const roofSelected = selectedIdSet.has(roof.id)
55
+ const roofHighlighted = highlightedIdSet.has(roof.id)
56
+ const hasSelectedSegment = segments.some(({ segment }) => selectedIdSet.has(segment.id))
57
+ const hasHighlightedSegment = segments.some(({ segment }) =>
58
+ highlightedIdSet.has(segment.id),
59
+ )
60
+ const isRoofActive =
61
+ roofSelected || roofHighlighted || hasSelectedSegment || hasHighlightedSegment
62
+
63
+ return (
64
+ <g key={roof.id} pointerEvents="none">
65
+ {segments.map(({ points, ridgeLine, segment }) => {
66
+ const isSegmentSelected = selectedIdSet.has(segment.id)
67
+ const isSegmentHighlighted = highlightedIdSet.has(segment.id)
68
+ const isSegmentActive = isSegmentSelected || isSegmentHighlighted
69
+
70
+ return (
71
+ <g key={segment.id}>
72
+ <polygon
73
+ fill={
74
+ isSegmentActive
75
+ ? palette.roofSelectedFill
76
+ : isRoofActive
77
+ ? palette.roofActiveFill
78
+ : palette.roofFill
79
+ }
80
+ points={points}
81
+ stroke={
82
+ isSegmentActive
83
+ ? palette.roofSelectedStroke
84
+ : isRoofActive
85
+ ? palette.roofActiveStroke
86
+ : palette.roofStroke
87
+ }
88
+ strokeWidth={isSegmentActive ? '2.25' : isRoofActive ? '1.75' : '1.1'}
89
+ vectorEffect="non-scaling-stroke"
90
+ />
91
+ {ridgeLine ? (
92
+ <line
93
+ fill="none"
94
+ stroke={
95
+ isSegmentActive ? palette.roofSelectedRidgeStroke : palette.roofRidgeStroke
96
+ }
97
+ strokeWidth={isSegmentActive ? '2' : '1.4'}
98
+ vectorEffect="non-scaling-stroke"
99
+ x1={toSvgX(ridgeLine.start.x)}
100
+ x2={toSvgX(ridgeLine.end.x)}
101
+ y1={toSvgY(ridgeLine.start.y)}
102
+ y2={toSvgY(ridgeLine.end.y)}
103
+ />
104
+ ) : null}
105
+ </g>
106
+ )
107
+ })}
108
+ </g>
109
+ )
110
+ })}
111
+ </>
112
+ )
113
+ })