@pascal-app/editor 1.1.0 → 1.1.1

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 (260) hide show
  1. package/package.json +9 -15
  2. package/src/components/editor/action-menu-button.tsx +1 -1
  3. package/src/components/editor/alignment-3d-guide-layer.tsx +1 -1
  4. package/src/components/editor/custom-camera-controls.tsx +11 -11
  5. package/src/components/editor/editor-layout-v2.tsx +1 -1
  6. package/src/components/editor/elevation-3d-guide-layer.tsx +1 -1
  7. package/src/components/editor/first-person-controls.tsx +20 -1981
  8. package/src/components/editor/floating-action-menu.tsx +1 -1
  9. package/src/components/editor/floating-building-action-menu.tsx +1 -1
  10. package/src/components/editor/floor-region-controls.tsx +6 -2
  11. package/src/components/editor/floorplan-navigation-presentation.test.ts +0 -40
  12. package/src/components/editor/floorplan-navigation-presentation.ts +0 -66
  13. package/src/components/editor/floorplan-panel.tsx +26 -22
  14. package/src/components/editor/floorplan-wall-typing-keys.ts +1 -1
  15. package/src/components/editor/follow-pascal.test.tsx +1 -2
  16. package/src/components/editor/follow-pascal.tsx +1 -1
  17. package/src/components/editor/footprint-height-handle.tsx +1 -1
  18. package/src/components/editor/group-actions.ts +1 -2
  19. package/src/components/editor/group-floating-action-menu.tsx +1 -1
  20. package/src/components/editor/group-move-3d.ts +1 -2
  21. package/src/components/editor/group-rotate-handle.tsx +1 -2
  22. package/src/components/editor/handles/use-handle-drag.ts +1 -2
  23. package/src/components/editor/index.tsx +24 -14
  24. package/src/components/editor/inspector-extension-actions.tsx +1 -1
  25. package/src/components/editor/node-arrow-handles.tsx +1 -1
  26. package/src/components/editor/open-wall-ends-3d-layer.tsx +1 -1
  27. package/src/components/editor/opening-guides-3d-layer.tsx +1 -1
  28. package/src/components/editor/room-controls.tsx +7 -4
  29. package/src/components/editor/room-handles-2d.tsx +2 -3
  30. package/src/components/editor/room-handles.tsx +1 -2
  31. package/src/components/editor/room-highlight.tsx +6 -105
  32. package/src/components/editor/room-labels.test.ts +8 -822
  33. package/src/components/editor/room-labels.tsx +40 -1671
  34. package/src/components/editor/room-webgpu-materials.test.ts +1 -1
  35. package/src/components/editor/selection-manager.tsx +5 -3
  36. package/src/components/editor/site-edge-labels.tsx +1 -1
  37. package/src/components/editor/snapshot-capture-overlay.tsx +2 -5
  38. package/src/components/editor/use-floorplan-background-placement.ts +2 -1
  39. package/src/components/editor/views/plugins-view.tsx +1 -1
  40. package/src/components/editor/views/view-bar.tsx +9 -9
  41. package/src/components/editor/wall-measurement-label.tsx +1 -1
  42. package/src/components/editor/wall-move-side-handles.tsx +1 -1
  43. package/src/components/editor/wall-pull-grip.tsx +2 -2
  44. package/src/components/editor-2d/floorplan-group-move.tsx +2 -3
  45. package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +1 -1
  46. package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +1 -1
  47. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +1 -2
  48. package/src/components/editor-2d/renderers/floorplan-annotation-layout.test.ts +0 -37
  49. package/src/components/editor-2d/renderers/floorplan-annotation-layout.ts +0 -30
  50. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +13 -3
  51. package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +1 -2
  52. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +8 -10
  53. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +25 -33
  54. package/src/components/systems/roof/roof-edit-system.tsx +4 -3
  55. package/src/components/tools/distribution/distribution-run-tool.tsx +1 -1
  56. package/src/components/tools/item/placement-history.test.tsx +1 -1
  57. package/src/components/tools/item/use-placement-coordinator.tsx +2 -3
  58. package/src/components/tools/paint-region/wall-paint-region-tool.tsx +6 -2
  59. package/src/components/tools/registry/move-registry-node-tool.tsx +1 -2
  60. package/src/components/tools/shared/cursor-sphere.tsx +1 -1
  61. package/src/components/tools/shared/draft-measurement-label.tsx +1 -1
  62. package/src/components/tools/shared/placement-box.tsx +1 -1
  63. package/src/components/tools/shared/placement-dimension-guides.tsx +1 -1
  64. package/src/components/tools/site/terrain-brush-cursor.tsx +1 -1
  65. package/src/components/ui/action-menu/camera-actions.tsx +2 -2
  66. package/src/components/ui/action-menu/control-modes.tsx +1 -1
  67. package/src/components/ui/action-menu/follow-pascal-pill.tsx +1 -1
  68. package/src/components/ui/action-menu/index.tsx +3 -3
  69. package/src/components/ui/action-menu/measurement-control.tsx +3 -3
  70. package/src/components/ui/action-menu/placement.ts +0 -18
  71. package/src/components/ui/action-menu/point-ask-button.tsx +1 -1
  72. package/src/components/ui/action-menu/view-toggles.tsx +5 -5
  73. package/src/components/ui/controls/finish-swatch.tsx +1 -1
  74. package/src/components/ui/controls/material-paint-panel.tsx +2 -2
  75. package/src/components/ui/controls/scene-material-list.tsx +2 -2
  76. package/src/components/ui/controls/select-control.tsx +1 -1
  77. package/src/components/ui/controls/terrain-sculpt-panel.tsx +1 -1
  78. package/src/components/ui/floating-level-selector.tsx +3 -3
  79. package/src/components/ui/helpers/contextual-helper-panel.tsx +1 -1
  80. package/src/components/ui/helpers/helper-manager.test.tsx +1 -1
  81. package/src/components/ui/helpers/helper-manager.tsx +1 -1
  82. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  83. package/src/components/ui/panels/collections/collections-popover.tsx +2 -2
  84. package/src/components/ui/panels/floor-foundation-panel.tsx +2 -2
  85. package/src/components/ui/panels/floor-opening-panel.tsx +4 -4
  86. package/src/components/ui/panels/node-display.ts +1 -1
  87. package/src/components/ui/panels/panel-manager.tsx +1 -1
  88. package/src/components/ui/panels/panel-wrapper.tsx +1 -1
  89. package/src/components/ui/panels/parametric-inspector.tsx +1 -1
  90. package/src/components/ui/panels/room-construction-rows.tsx +6 -6
  91. package/src/components/ui/panels/room-name-options.ts +1 -5
  92. package/src/components/ui/panels/room-panel.tsx +6 -8
  93. package/src/components/ui/primitives/color-dot.tsx +2 -2
  94. package/src/components/ui/primitives/opacity-control.tsx +2 -2
  95. package/src/components/ui/primitives/sidebar.tsx +3 -3
  96. package/src/components/ui/right-stack.tsx +1 -1
  97. package/src/components/ui/sidebar/icon-rail.tsx +1 -1
  98. package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +1 -1
  99. package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +2 -2
  100. package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.tsx +1 -1
  101. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +2 -2
  102. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +1 -1
  103. package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +1 -1
  104. package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +1 -1
  105. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +1 -1
  106. package/src/components/ui/sidebar/panels/site-panel/index.tsx +2 -2
  107. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +1 -1
  108. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +1 -1
  109. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +46 -7
  110. package/src/components/ui/sidebar/panels/site-panel/tree-node-actions.tsx +1 -1
  111. package/src/components/ui/sidebar/panels/site-panel/tree-node-drag.tsx +14 -5
  112. package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -2
  113. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +3 -3
  114. package/src/components/ui/sidebar/tab-bar.tsx +1 -1
  115. package/src/components/ui/wall-type/wall-type-editor-dialog.tsx +1 -1
  116. package/src/components/ui/wall-type/wall-type-menu.tsx +1 -1
  117. package/src/components/viewer-overlay.tsx +1 -2
  118. package/src/hooks/use-auto-frame.ts +1 -2
  119. package/src/hooks/use-keyboard.ts +1 -2
  120. package/src/hooks/use-selected-room.ts +5 -7
  121. package/src/i18n/floor-footprint-name.ts +1 -1
  122. package/src/i18n/locales/en/messages.po +180 -182
  123. package/src/i18n/locales/es/messages.po +180 -182
  124. package/src/i18n/locales/fr/messages.po +180 -182
  125. package/src/i18n/locales/ja/messages.po +180 -182
  126. package/src/i18n/locales/zh-CN/messages.po +180 -182
  127. package/src/i18n/locales.test.ts +1 -1
  128. package/src/index.tsx +109 -131
  129. package/src/lib/below-grade.test.tsx +4 -4
  130. package/src/lib/camera-facing.test.ts +1 -1
  131. package/src/lib/camera-facing.ts +2 -3
  132. package/src/lib/editor-level-display.ts +1 -1
  133. package/src/lib/floor-footprints.test.ts +2 -2
  134. package/src/lib/floor-opening-draft.ts +1 -1
  135. package/src/lib/floorplan/annotation-visibility.test.ts +1 -1
  136. package/src/lib/floorplan/annotation-visibility.ts +1 -1
  137. package/src/lib/floorplan/floorplan-export.test.ts +1 -1
  138. package/src/lib/floorplan/floorplan-export.tsx +8 -11
  139. package/src/lib/floorplan/floorplan-extension.ts +4 -157
  140. package/src/lib/floorplan/floorplan-mode.ts +2 -1
  141. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +1 -1
  142. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +8 -6
  143. package/src/lib/floorplan/floorplan-plugin-install.test.tsx +3 -4
  144. package/src/lib/floorplan/geometry.ts +2 -112
  145. package/src/lib/floorplan/index.ts +0 -9
  146. package/src/lib/floorplan/items.ts +2 -1
  147. package/src/lib/floorplan/schedules.ts +6 -3
  148. package/src/lib/floorplan/sheet-export.ts +7 -3
  149. package/src/lib/floorplan/site-plan/site-parts.ts +7 -1
  150. package/src/lib/floorplan/site-plan/site-plan-layer.tsx +1 -1
  151. package/src/lib/follow-pascal-presentation.ts +1 -1
  152. package/src/lib/glb-export.ts +1 -1
  153. package/src/lib/material-paint.ts +1 -1
  154. package/src/lib/measurements.test.ts +2 -1
  155. package/src/lib/mezzanine-selection.test.ts +1 -1
  156. package/src/lib/paint-region-mode.ts +1 -1
  157. package/src/lib/point-ask/session.ts +1 -2
  158. package/src/lib/roof-duplication.test.ts +100 -0
  159. package/src/lib/roof-duplication.ts +30 -5
  160. package/src/lib/room-handle-drag.ts +2 -12
  161. package/src/lib/room-name-catalog.test.ts +2 -1
  162. package/src/lib/room-name-messages.test.ts +2 -1
  163. package/src/lib/room-name-messages.ts +1 -118
  164. package/src/lib/room-name-storage.test.ts +2 -1
  165. package/src/lib/room-selection-commands.ts +2 -2
  166. package/src/lib/room-selection.test.ts +6 -6
  167. package/src/lib/room-transform-session.ts +1 -2
  168. package/src/lib/room-zone-routing.ts +2 -2
  169. package/src/lib/selection-routing.test.ts +0 -28
  170. package/src/lib/selection-routing.ts +1 -40
  171. package/src/lib/sfx-bus.ts +8 -32
  172. package/src/lib/sfx-player.test.ts +72 -1
  173. package/src/lib/sfx-player.ts +49 -337
  174. package/src/lib/viewer-selection.test.ts +38 -10
  175. package/src/lib/wall-pull.test.ts +1 -1
  176. package/src/lib/wall-pull.ts +1 -27
  177. package/src/store/use-editor.tsx +17 -26
  178. package/src/store/use-floorplan-annotation-visibility.ts +5 -5
  179. package/src/store/use-wall-region-handles-selection.ts +1 -2
  180. package/src/components/editor/__bench__/room-label-placement.bench.ts +0 -108
  181. package/src/components/editor/first-person/build-collider-world.test.ts +0 -596
  182. package/src/components/editor/first-person/build-collider-world.ts +0 -549
  183. package/src/components/editor/first-person/ground-holes.test.ts +0 -128
  184. package/src/components/editor/first-person/ground-holes.ts +0 -130
  185. package/src/components/editor/first-person/mechanism-targets.test.ts +0 -162
  186. package/src/components/editor/first-person/mechanism-targets.ts +0 -88
  187. package/src/components/editor/first-person/terrain-collider.test.ts +0 -132
  188. package/src/components/editor/first-person/terrain-collider.ts +0 -212
  189. package/src/components/editor/floorplan-camera-sync.test.ts +0 -249
  190. package/src/components/editor/floorplan-camera-sync.ts +0 -213
  191. package/src/components/editor/localized-html.tsx +0 -22
  192. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.test.tsx +0 -236
  193. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +0 -708
  194. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.test.tsx +0 -385
  195. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +0 -670
  196. package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +0 -89
  197. package/src/components/editor-2d/renderers/floorplan-label-angle.ts +0 -106
  198. package/src/components/editor-2d/svg-paths.ts +0 -124
  199. package/src/components/ui/action-menu/action-button.tsx +0 -89
  200. package/src/components/ui/action-menu/follow-pascal-toggle.tsx +0 -42
  201. package/src/components/ui/primitives/button.tsx +0 -69
  202. package/src/components/ui/primitives/dropdown-menu.tsx +0 -232
  203. package/src/components/ui/primitives/popover.tsx +0 -42
  204. package/src/components/ui/primitives/switch.tsx +0 -29
  205. package/src/components/ui/primitives/tooltip.tsx +0 -57
  206. package/src/components/ui/scene-loader.tsx +0 -73
  207. package/src/components/viewer/floorplan-compass-button.tsx +0 -67
  208. package/src/components/viewer/floorplan-preview-geometry.test.ts +0 -107
  209. package/src/components/viewer/floorplan-preview-geometry.ts +0 -281
  210. package/src/components/viewer/floorplan-preview-navigation.test.ts +0 -43
  211. package/src/components/viewer/floorplan-preview-navigation.ts +0 -62
  212. package/src/components/viewer/floorplan-preview-visibility.test.ts +0 -35
  213. package/src/components/viewer/floorplan-preview-visibility.ts +0 -19
  214. package/src/components/viewer/floorplan-preview.tsx +0 -1065
  215. package/src/components/viewer/render-settings-menu.tsx +0 -294
  216. package/src/components/viewer/sky-control.tsx +0 -710
  217. package/src/components/viewer/use-viewer-camera-navigation-sync.ts +0 -140
  218. package/src/components/viewer/use-viewer-ui-sounds.test.ts +0 -15
  219. package/src/components/viewer/use-viewer-ui-sounds.ts +0 -46
  220. package/src/components/viewer/viewer-controls-bar.tsx +0 -496
  221. package/src/components/viewer/viewer-hover-label.tsx +0 -42
  222. package/src/components/viewer/viewer-rooms.tsx +0 -248
  223. package/src/components/viewer/viewer-scene-header.tsx +0 -286
  224. package/src/components/viewer/viewer-selection-manager.tsx +0 -124
  225. package/src/components/viewer/viewer-stage-modes.test.ts +0 -38
  226. package/src/components/viewer/viewer-stage-modes.ts +0 -49
  227. package/src/components/viewer/viewer-stage-switcher.tsx +0 -97
  228. package/src/components/viewer/viewer-stage.test.tsx +0 -47
  229. package/src/components/viewer/viewer-stage.tsx +0 -221
  230. package/src/components/walkthrough-hud.tsx +0 -193
  231. package/src/hooks/use-mobile.ts +0 -19
  232. package/src/i18n/level-name.ts +0 -39
  233. package/src/i18n/scene-theme-name.ts +0 -16
  234. package/src/lib/below-grade.ts +0 -133
  235. package/src/lib/camera-pose.test.ts +0 -237
  236. package/src/lib/camera-pose.ts +0 -172
  237. package/src/lib/floorplan/floorplan-extension.test.ts +0 -42
  238. package/src/lib/floorplan/floorplan-readonly.test.ts +0 -47
  239. package/src/lib/floorplan/floorplan-readonly.ts +0 -95
  240. package/src/lib/floorplan/marks.test.ts +0 -213
  241. package/src/lib/floorplan/marks.ts +0 -238
  242. package/src/lib/measurements.ts +0 -8
  243. package/src/lib/node-display-label.ts +0 -37
  244. package/src/lib/room-assembly-overlay.test.ts +0 -275
  245. package/src/lib/room-assembly-overlay.ts +0 -265
  246. package/src/lib/room-selection.ts +0 -417
  247. package/src/lib/scene-bounds.test.ts +0 -327
  248. package/src/lib/scene-bounds.ts +0 -233
  249. package/src/lib/typing-target.ts +0 -9
  250. package/src/lib/viewer-selection.ts +0 -595
  251. package/src/lib/zone-content.ts +0 -133
  252. package/src/store/camera-pose-store.test.ts +0 -29
  253. package/src/store/camera-pose-store.ts +0 -27
  254. package/src/store/navigation-sync-pose-store.test.ts +0 -34
  255. package/src/store/navigation-sync-pose-store.ts +0 -27
  256. package/src/store/use-audio.test.ts +0 -146
  257. package/src/store/use-audio.tsx +0 -169
  258. package/src/store/use-first-person-hud.ts +0 -40
  259. package/src/store/use-follow-pascal.test.ts +0 -62
  260. package/src/store/use-follow-pascal.ts +0 -53
@@ -1,670 +0,0 @@
1
- 'use client'
2
-
3
- import { type FloorplanGeometry, loadAssetUrl } from '@pascal-app/core'
4
- import { memo, useEffect, useState } from 'react'
5
- import { readFloorplanGeometryMetadata } from '../../../lib/floorplan/floorplan-extension'
6
- import {
7
- floorplanAnnotationObstacleMode,
8
- isFloorplanAnnotationObstacleGeometry,
9
- } from './floorplan-annotation-layout'
10
- import {
11
- FloorplanDimensionRenderer,
12
- FloorplanDimensionStringRenderer,
13
- } from './floorplan-dimension-renderer'
14
- import { resolveFloorplanLabelAngle } from './floorplan-label-angle'
15
-
16
- const STATIC_LABEL_UNITS_PER_PIXEL = 0.01
17
- const DOCUMENT_DEFAULT_TEXT_SIZE_PT = 8
18
- const DOCUMENT_ROOM_NAME_TEXT_SIZE_PT = 8
19
- const DOCUMENT_ROOM_NUMBER_TEXT_SIZE_PT = 7
20
- const DOCUMENT_ROOM_DETAIL_TEXT_SIZE_PT = 5.5
21
- const DOCUMENT_COLUMN_MARK_TEXT_SIZE_PT = 7
22
- const DOCUMENT_DEFAULT_STROKE_WIDTH_PT = 0.5
23
- const DOCUMENT_TEXT_OUTLINE_MIN_WIDTH_PT = 0.75
24
- const DOCUMENT_MARK_HEIGHT_PT = 14
25
- const PDF_ANNOTATION_STROKE_WIDTH_PT = 0.5
26
-
27
- type FloorplanRenderMode = 'screen' | 'pdf'
28
-
29
- /**
30
- * Pure-data → SVG converter. Walks a `FloorplanGeometry` tree returned by
31
- * `def.floorplan(node, ctx)` and emits the matching React-SVG nodes.
32
- *
33
- * Coordinates are level-local meters. The wrapping floor-plan panel
34
- * applies the world→SVG transform via its viewBox, so kinds emit
35
- * geometry in the same units they reason about in 3D.
36
- *
37
- * Group transforms compose: `transform={translate(x y) rotate(deg)}`.
38
- * Rotations are radians at the data layer (consistent with three.js
39
- * conventions used by `def.geometry`) and converted to degrees for SVG
40
- * here — kinds never touch units.
41
- *
42
- * Styling props map straight onto SVG attributes. Builders that need
43
- * theme colors should declare them inline or expose them as registry
44
- * tokens later (deferred until a real need surfaces — AI-authored kinds
45
- * can pick safe defaults today).
46
- */
47
- export const FloorplanGeometryRenderer = memo(function FloorplanGeometryRenderer({
48
- geometry,
49
- pointerEventsOverride,
50
- sceneRotationDeg = 0,
51
- annotationUnitsPerPoint,
52
- screenUnitsPerPixel,
53
- renderMode = 'screen',
54
- }: {
55
- geometry: FloorplanGeometry
56
- pointerEventsOverride?: string
57
- sceneRotationDeg?: number
58
- annotationUnitsPerPoint?: number
59
- screenUnitsPerPixel?: number
60
- renderMode?: FloorplanRenderMode
61
- }) {
62
- return renderNode(
63
- geometry,
64
- 0,
65
- pointerEventsOverride,
66
- sceneRotationDeg,
67
- annotationUnitsPerPoint,
68
- screenUnitsPerPixel,
69
- renderMode,
70
- )
71
- })
72
-
73
- function styleAttrs(
74
- g: FloorplanGeometry & { kind: Exclude<FloorplanGeometry['kind'], 'group'> },
75
- pointerEventsOverride?: string,
76
- annotationUnitsPerPoint?: number,
77
- renderMode: FloorplanRenderMode = 'screen',
78
- ) {
79
- // Shared SVG attribute mapping for any styled primitive. Keeps the per-
80
- // primitive switch arms terse and ensures new style fields land
81
- // everywhere at once. `as any` avoids re-asserting every variant
82
- // includes the style fields — they all do, except `group` (which is
83
- // filtered out by the caller's type bound).
84
- const s = g as unknown as {
85
- fill?: string
86
- fillRule?: 'nonzero' | 'evenodd'
87
- fillOpacity?: number
88
- stroke?: string
89
- strokeWidth?: number
90
- strokeDasharray?: string
91
- strokeLinecap?: 'butt' | 'round' | 'square'
92
- strokeLinejoin?: 'miter' | 'round' | 'bevel'
93
- strokeOpacity?: number
94
- opacity?: number
95
- vectorEffect?: 'non-scaling-stroke'
96
- pointerEvents?: string
97
- cursor?: string
98
- }
99
- const annotationMetadata = readFloorplanGeometryMetadata(g)
100
- const documentStyle = resolveDocumentFloorplanAnnotationStyle(g, annotationUnitsPerPoint)
101
- const vectorEffect = documentStyle.vectorEffect ?? s.vectorEffect
102
- const resolvedStrokeWidth = documentStyle.strokeWidth ?? s.strokeWidth
103
- const strokeWidth =
104
- renderMode === 'pdf' &&
105
- vectorEffect === 'non-scaling-stroke' &&
106
- resolvedStrokeWidth !== undefined
107
- ? Math.min(PDF_ANNOTATION_STROKE_WIDTH_PT, resolvedStrokeWidth)
108
- : resolvedStrokeWidth
109
- return {
110
- 'data-floorplan-annotation-obstacle': floorplanAnnotationObstacleMode(g),
111
- 'data-floorplan-annotation-role': annotationMetadata.annotationRole,
112
- fill: documentStyle.fill ?? s.fill ?? 'none',
113
- fillRule: s.fillRule,
114
- fillOpacity: s.fillOpacity,
115
- stroke: documentStyle.stroke ?? s.stroke,
116
- strokeWidth,
117
- strokeDasharray: s.strokeDasharray,
118
- strokeLinecap: s.strokeLinecap,
119
- strokeLinejoin: s.strokeLinejoin,
120
- strokeOpacity: s.strokeOpacity,
121
- opacity: s.opacity,
122
- vectorEffect,
123
- pointerEvents: pointerEventsOverride ?? s.pointerEvents,
124
- style: s.cursor ? { cursor: s.cursor } : undefined,
125
- }
126
- }
127
-
128
- export function resolveDocumentFloorplanAnnotationStyle(
129
- geometry: FloorplanGeometry & { kind: Exclude<FloorplanGeometry['kind'], 'group'> },
130
- annotationUnitsPerPoint?: number,
131
- ): {
132
- fill?: string
133
- stroke?: string
134
- strokeWidth?: number
135
- vectorEffect?: 'non-scaling-stroke'
136
- } {
137
- if (annotationUnitsPerPoint === undefined) return {}
138
- const styled = geometry as FloorplanGeometry & {
139
- stroke?: string
140
- strokeWidth?: number
141
- vectorEffect?: 'non-scaling-stroke'
142
- }
143
- if (!styled.stroke && geometry.kind !== 'text') return {}
144
-
145
- if (geometry.kind === 'text') {
146
- const sourceFontSize = Math.max(geometry.fontSize, 1e-6)
147
- const sourceStrokeWidth = geometry.strokeWidth ?? 0
148
- const outlineRatio = sourceStrokeWidth > 0 ? sourceStrokeWidth / sourceFontSize : 0
149
- const fontSize = documentTextFontSize(geometry, annotationUnitsPerPoint)
150
- return {
151
- strokeWidth:
152
- geometry.stroke && outlineRatio > 0
153
- ? Math.max(
154
- DOCUMENT_TEXT_OUTLINE_MIN_WIDTH_PT * annotationUnitsPerPoint,
155
- fontSize * outlineRatio,
156
- )
157
- : undefined,
158
- }
159
- }
160
-
161
- return {
162
- strokeWidth: documentStrokeWidth(styled, annotationUnitsPerPoint),
163
- vectorEffect: 'non-scaling-stroke',
164
- }
165
- }
166
-
167
- function documentTextFontSize(
168
- geometry: Extract<FloorplanGeometry, { kind: 'text' }>,
169
- annotationUnitsPerPoint: number,
170
- ): number {
171
- return documentTextSizePt(geometry) * annotationUnitsPerPoint
172
- }
173
-
174
- function documentTextSizePt(geometry: Extract<FloorplanGeometry, { kind: 'text' }>): number {
175
- const metadata = readFloorplanGeometryMetadata(geometry)
176
- // an explicit paper size wins over the role's (a sheet's room tag sets its own)
177
- if (typeof metadata.textSizePt === 'number' && metadata.textSizePt > 0) return metadata.textSizePt
178
- switch (metadata.annotationRole) {
179
- case 'room-label':
180
- if (geometry.fontSize >= 0.18) return DOCUMENT_ROOM_NAME_TEXT_SIZE_PT
181
- if (geometry.fontSize >= 0.145) return DOCUMENT_ROOM_NUMBER_TEXT_SIZE_PT
182
- return DOCUMENT_ROOM_DETAIL_TEXT_SIZE_PT
183
- case 'column-center':
184
- case 'stair-annotation':
185
- return DOCUMENT_COLUMN_MARK_TEXT_SIZE_PT
186
- default:
187
- return DOCUMENT_DEFAULT_TEXT_SIZE_PT
188
- }
189
- }
190
-
191
- function documentStrokeWidth(
192
- geometry: { strokeWidth?: number },
193
- annotationUnitsPerPoint: number,
194
- ): number {
195
- return Math.max(DOCUMENT_DEFAULT_STROKE_WIDTH_PT, Math.min(1.2, geometry.strokeWidth ?? 0.5))
196
- }
197
-
198
- export function documentRectGeometryAttrs(
199
- geometry: Extract<FloorplanGeometry, { kind: 'rect' }>,
200
- annotationUnitsPerPoint?: number,
201
- ) {
202
- if (annotationUnitsPerPoint === undefined || !isAnnotationMarkRect(geometry)) {
203
- return {
204
- x: geometry.x,
205
- y: geometry.y,
206
- width: geometry.width,
207
- height: geometry.height,
208
- rx: geometry.rx,
209
- ry: geometry.ry,
210
- }
211
- }
212
-
213
- const centerX = geometry.x + geometry.width / 2
214
- const centerY = geometry.y + geometry.height / 2
215
- const height = DOCUMENT_MARK_HEIGHT_PT * annotationUnitsPerPoint
216
- const width = Math.max(height * 1.6, (geometry.width / Math.max(geometry.height, 1e-6)) * height)
217
- return {
218
- x: centerX - width / 2,
219
- y: centerY - height / 2,
220
- width,
221
- height,
222
- rx: height / 2,
223
- ry: height / 2,
224
- }
225
- }
226
-
227
- function isAnnotationMarkRect(geometry: Extract<FloorplanGeometry, { kind: 'rect' }>): boolean {
228
- return geometry.fill === '#ffffff' && !!geometry.stroke && geometry.height <= 0.5
229
- }
230
-
231
- export function documentCircleGeometryAttrs(
232
- geometry: Extract<FloorplanGeometry, { kind: 'circle' }>,
233
- annotationUnitsPerPoint?: number,
234
- ) {
235
- if (annotationUnitsPerPoint === undefined || !isAnnotationMarkCircle(geometry)) {
236
- return { r: geometry.r }
237
- }
238
- return { r: Math.max(geometry.r, (DOCUMENT_MARK_HEIGHT_PT / 2) * annotationUnitsPerPoint) }
239
- }
240
-
241
- function isAnnotationMarkCircle(geometry: Extract<FloorplanGeometry, { kind: 'circle' }>): boolean {
242
- return geometry.fill === '#ffffff' && !!geometry.stroke && geometry.r <= 0.25
243
- }
244
-
245
- export function resolveDocumentAnnotationGroupChildren(
246
- children: FloorplanGeometry[],
247
- annotationUnitsPerPoint?: number,
248
- ): FloorplanGeometry[] {
249
- if (annotationUnitsPerPoint === undefined) return children
250
-
251
- const next = [...children]
252
- let start = 0
253
- while (start < next.length) {
254
- const first = next[start]
255
- if (!isDocumentTextLine(first)) {
256
- start++
257
- continue
258
- }
259
-
260
- let end = start + 1
261
- while (end < next.length && isSameDocumentTextRun(first, next[end])) end++
262
- if (end - start > 1) {
263
- const run = next.slice(start, end) as Extract<FloorplanGeometry, { kind: 'text' }>[]
264
- const adjusted = positionDocumentTextRun(run, annotationUnitsPerPoint)
265
- for (let index = 0; index < adjusted.length; index++) {
266
- const line = adjusted[index]
267
- if (line) next[start + index] = line
268
- }
269
- }
270
- start = end
271
- }
272
-
273
- return next
274
- }
275
-
276
- function isDocumentTextLine(
277
- geometry: FloorplanGeometry | undefined,
278
- ): geometry is Extract<FloorplanGeometry, { kind: 'text' }> {
279
- return geometry?.kind === 'text' && geometry.upright === true
280
- }
281
-
282
- function isSameDocumentTextRun(
283
- first: Extract<FloorplanGeometry, { kind: 'text' }>,
284
- candidate: FloorplanGeometry | undefined,
285
- ): candidate is Extract<FloorplanGeometry, { kind: 'text' }> {
286
- return (
287
- isDocumentTextLine(candidate) &&
288
- Math.abs(candidate.x - first.x) < 1e-6 &&
289
- candidate.textAnchor === first.textAnchor &&
290
- readFloorplanGeometryMetadata(candidate).annotationRole ===
291
- readFloorplanGeometryMetadata(first).annotationRole
292
- )
293
- }
294
-
295
- function positionDocumentTextRun(
296
- run: Extract<FloorplanGeometry, { kind: 'text' }>[],
297
- annotationUnitsPerPoint: number,
298
- ): Extract<FloorplanGeometry, { kind: 'text' }>[] {
299
- const centerY = run.reduce((sum, line) => sum + line.y, 0) / run.length
300
- const steps = run.slice(0, -1).map((line, index) => {
301
- const next = run[index + 1] ?? line
302
- const largerFontPt = Math.max(documentTextSizePt(line), documentTextSizePt(next))
303
- return largerFontPt * 1.25 * annotationUnitsPerPoint
304
- })
305
- const totalHeight = steps.reduce((sum, step) => sum + step, 0)
306
- let y = centerY - totalHeight / 2
307
- return run.map((line, index) => {
308
- if (index > 0) y += steps[index - 1] ?? 0
309
- return { ...line, y }
310
- })
311
- }
312
-
313
- function renderNode(
314
- g: FloorplanGeometry,
315
- keyHint: number,
316
- pointerEventsOverride?: string,
317
- sceneRotationDeg = 0,
318
- annotationUnitsPerPoint?: number,
319
- screenUnitsPerPixel?: number,
320
- renderMode: FloorplanRenderMode = 'screen',
321
- ): React.ReactElement | null {
322
- switch (g.kind) {
323
- case 'path':
324
- return (
325
- <path
326
- d={g.d}
327
- key={keyHint}
328
- {...styleAttrs(g, pointerEventsOverride, annotationUnitsPerPoint, renderMode)}
329
- />
330
- )
331
-
332
- case 'polygon':
333
- return (
334
- <polygon
335
- key={keyHint}
336
- points={pointsToAttr(g.points)}
337
- {...styleAttrs(g, pointerEventsOverride, annotationUnitsPerPoint, renderMode)}
338
- />
339
- )
340
-
341
- case 'polyline':
342
- return (
343
- <polyline
344
- key={keyHint}
345
- points={pointsToAttr(g.points)}
346
- {...styleAttrs(g, pointerEventsOverride, annotationUnitsPerPoint, renderMode)}
347
- />
348
- )
349
-
350
- case 'rect': {
351
- const attrs = documentRectGeometryAttrs(g, annotationUnitsPerPoint)
352
- return (
353
- <rect
354
- height={attrs.height}
355
- key={keyHint}
356
- rx={attrs.rx}
357
- ry={attrs.ry}
358
- width={attrs.width}
359
- x={attrs.x}
360
- y={attrs.y}
361
- {...styleAttrs(g, pointerEventsOverride, annotationUnitsPerPoint, renderMode)}
362
- />
363
- )
364
- }
365
-
366
- case 'circle': {
367
- const attrs = documentCircleGeometryAttrs(g, annotationUnitsPerPoint)
368
- return (
369
- <circle
370
- cx={g.cx}
371
- cy={g.cy}
372
- key={keyHint}
373
- r={attrs.r}
374
- {...styleAttrs(g, pointerEventsOverride, annotationUnitsPerPoint, renderMode)}
375
- />
376
- )
377
- }
378
-
379
- case 'line':
380
- return (
381
- <line
382
- key={keyHint}
383
- x1={g.x1}
384
- x2={g.x2}
385
- y1={g.y1}
386
- y2={g.y2}
387
- {...styleAttrs(g, pointerEventsOverride, annotationUnitsPerPoint, renderMode)}
388
- />
389
- )
390
-
391
- case 'text': {
392
- const fontSize =
393
- annotationUnitsPerPoint !== undefined
394
- ? documentTextFontSize(g, annotationUnitsPerPoint)
395
- : g.fontSize
396
- const textStyle = resolveDocumentFloorplanAnnotationStyle(g, annotationUnitsPerPoint)
397
- const pdfOutlinedText = renderMode === 'pdf' && g.paintOrder === 'stroke' && !!g.stroke
398
- const fill =
399
- pdfOutlinedText && g.fill?.toLocaleLowerCase() === '#ffffff'
400
- ? g.stroke
401
- : (g.fill ?? '#171717')
402
- if (g.upright) {
403
- return (
404
- <g
405
- data-floorplan-annotation-obstacle={floorplanAnnotationObstacleMode(g)}
406
- key={keyHint}
407
- transform={`translate(${g.x} ${g.y}) rotate(${-sceneRotationDeg})`}
408
- >
409
- <text
410
- dominantBaseline={g.dominantBaseline ?? 'middle'}
411
- fill={fill}
412
- fontFamily={g.fontFamily}
413
- fontSize={fontSize}
414
- fontWeight={g.fontWeight}
415
- opacity={g.opacity}
416
- paintOrder={pdfOutlinedText ? undefined : g.paintOrder}
417
- pointerEvents={pointerEventsOverride}
418
- stroke={pdfOutlinedText ? undefined : g.stroke}
419
- strokeLinecap={!pdfOutlinedText && g.stroke ? 'round' : undefined}
420
- strokeLinejoin={!pdfOutlinedText && g.stroke ? 'round' : undefined}
421
- strokeWidth={pdfOutlinedText ? undefined : (textStyle.strokeWidth ?? g.strokeWidth)}
422
- textAnchor={g.textAnchor ?? 'start'}
423
- x={0}
424
- y={0}
425
- >
426
- {g.text}
427
- </text>
428
- </g>
429
- )
430
- }
431
- return (
432
- <text
433
- data-floorplan-annotation-obstacle={floorplanAnnotationObstacleMode(g)}
434
- dominantBaseline={g.dominantBaseline ?? 'middle'}
435
- fill={fill}
436
- fontFamily={g.fontFamily}
437
- fontSize={fontSize}
438
- fontWeight={g.fontWeight}
439
- key={keyHint}
440
- opacity={g.opacity}
441
- paintOrder={pdfOutlinedText ? undefined : g.paintOrder}
442
- stroke={pdfOutlinedText ? undefined : g.stroke}
443
- strokeLinecap={!pdfOutlinedText && g.stroke ? 'round' : undefined}
444
- strokeLinejoin={!pdfOutlinedText && g.stroke ? 'round' : undefined}
445
- strokeWidth={pdfOutlinedText ? undefined : (textStyle.strokeWidth ?? g.strokeWidth)}
446
- textAnchor={g.textAnchor ?? 'start'}
447
- pointerEvents={pointerEventsOverride}
448
- x={g.x}
449
- y={g.y}
450
- >
451
- {g.text}
452
- </text>
453
- )
454
- }
455
-
456
- case 'dimension':
457
- return (
458
- <FloorplanDimensionRenderer
459
- geometry={g}
460
- key={keyHint}
461
- sceneRotationDeg={sceneRotationDeg}
462
- annotationUnitsPerPoint={annotationUnitsPerPoint}
463
- renderMode={renderMode}
464
- />
465
- )
466
-
467
- case 'dimension-string':
468
- return (
469
- <FloorplanDimensionStringRenderer
470
- annotationUnitsPerPoint={annotationUnitsPerPoint}
471
- geometry={g}
472
- key={keyHint}
473
- renderMode={renderMode}
474
- sceneRotationDeg={sceneRotationDeg}
475
- />
476
- )
477
-
478
- case 'dimension-label': {
479
- const unitsPerPixel =
480
- annotationUnitsPerPoint ?? screenUnitsPerPixel ?? STATIC_LABEL_UNITS_PER_PIXEL
481
- const documentMode = annotationUnitsPerPoint !== undefined
482
- const outlined = g.appearance === 'outlined'
483
- const pdfOutlined = outlined && renderMode === 'pdf'
484
- const padX = unitsPerPixel * 6
485
- const padY = unitsPerPixel * 3
486
- const fontSize = unitsPerPixel * (documentMode ? 8 : outlined ? 12 : 10)
487
- const textWidth = g.text.length * unitsPerPixel * 6.2
488
- const plateW = textWidth + padX * 2
489
- const plateH = fontSize + padY * 2
490
- const degrees = resolveFloorplanLabelAngle(g.angle, sceneRotationDeg, g.screenUpright)
491
- const labelTransform = `translate(${g.cx} ${g.cy}) rotate(${degrees}) translate(0 ${-(g.offsetPx ?? 0) * unitsPerPixel})`
492
-
493
- return (
494
- <g
495
- data-floorplan-annotation-angle-radians={g.angle}
496
- data-floorplan-annotation-default-transform={labelTransform}
497
- data-floorplan-annotation-label=""
498
- data-floorplan-annotation-priority="20"
499
- data-floorplan-annotation-screen-upright={g.screenUpright ? 'true' : undefined}
500
- data-floorplan-annotation-transform-after-rotation={`translate(0 ${
501
- -(g.offsetPx ?? 0) * unitsPerPixel
502
- })`}
503
- data-floorplan-annotation-transform-before-rotation={`translate(${g.cx} ${g.cy})`}
504
- key={keyHint}
505
- pointerEvents="none"
506
- transform={labelTransform}
507
- >
508
- {outlined && !pdfOutlined ? null : (
509
- <rect
510
- data-floorplan-dimension-label-plate={pdfOutlined ? '' : undefined}
511
- fill="#ffffff"
512
- height={plateH}
513
- opacity={0.92}
514
- rx={unitsPerPixel * 3}
515
- ry={unitsPerPixel * 3}
516
- stroke={pdfOutlined ? undefined : '#334155'}
517
- strokeWidth={pdfOutlined ? undefined : unitsPerPixel * 0.5}
518
- width={plateW}
519
- x={-plateW / 2}
520
- y={-plateH / 2}
521
- />
522
- )}
523
- <text
524
- dominantBaseline="middle"
525
- fill={pdfOutlined ? '#111827' : outlined ? '#ffffff' : '#111827'}
526
- fontFamily={
527
- outlined && !pdfOutlined
528
- ? 'system-ui, -apple-system, sans-serif'
529
- : 'ui-monospace, SFMono-Regular, Menlo, monospace'
530
- }
531
- fontSize={fontSize}
532
- fontWeight={outlined ? 500 : 600}
533
- paintOrder={outlined && !pdfOutlined ? 'stroke' : undefined}
534
- stroke={outlined && !pdfOutlined ? '#334155' : undefined}
535
- strokeLinecap={outlined && !pdfOutlined ? 'round' : undefined}
536
- strokeLinejoin={outlined && !pdfOutlined ? 'round' : undefined}
537
- strokeWidth={outlined && !pdfOutlined ? fontSize * 0.35 : undefined}
538
- textAnchor="middle"
539
- x={0}
540
- y={0}
541
- >
542
- {g.text}
543
- </text>
544
- </g>
545
- )
546
- }
547
-
548
- case 'image':
549
- return (
550
- <FloorplanImage
551
- center={g.center}
552
- height={g.height}
553
- key={keyHint}
554
- opacity={g.opacity}
555
- preserveAspectRatio={g.preserveAspectRatio ?? 'xMidYMid meet'}
556
- rotation={g.rotation ?? 0}
557
- url={g.url}
558
- width={g.width}
559
- />
560
- )
561
-
562
- case 'group': {
563
- const transform = formatTransform(g.transform)
564
- const children = resolveDocumentAnnotationGroupChildren(g.children, annotationUnitsPerPoint)
565
- return (
566
- <g
567
- data-floorplan-annotation-obstacle={
568
- isFloorplanAnnotationObstacleGeometry(g) ? '' : undefined
569
- }
570
- key={keyHint}
571
- transform={transform}
572
- >
573
- {children.map((child, i) =>
574
- renderNode(
575
- child,
576
- i,
577
- pointerEventsOverride,
578
- sceneRotationDeg,
579
- annotationUnitsPerPoint,
580
- screenUnitsPerPixel,
581
- renderMode,
582
- ),
583
- )}
584
- </g>
585
- )
586
- }
587
-
588
- // The remaining interactive primitives (hatch / hit-line / endpoint-handle)
589
- // need the SVG context + theme palette + units-per-
590
- // pixel that only the registry layer has access to. They're rendered
591
- // by `floorplan-registry-layer.tsx`'s interactive walker instead. If
592
- // a caller routes one of these through this pure renderer it
593
- // silently drops — the static renderer is for static output.
594
- default:
595
- return null
596
- }
597
- }
598
-
599
- function pointsToAttr(points: readonly (readonly [number, number])[]): string {
600
- return points.map(([x, y]) => `${x},${y}`).join(' ')
601
- }
602
-
603
- function formatTransform(t?: {
604
- translate?: readonly [number, number]
605
- rotate?: number
606
- }): string | undefined {
607
- if (!t) return undefined
608
- const parts: string[] = []
609
- if (t.translate) parts.push(`translate(${t.translate[0]} ${t.translate[1]})`)
610
- if (t.rotate !== undefined) parts.push(`rotate(${(t.rotate * 180) / Math.PI})`)
611
- return parts.length > 0 ? parts.join(' ') : undefined
612
- }
613
-
614
- /**
615
- * `image` primitive renderer. Resolves the URL asynchronously via
616
- * `loadAssetUrl` (handles CDN / Supabase storage) and renders an SVG
617
- * `<image>` centered at `center`, rotated around it, sized in plan-local
618
- * metres. While the resolution is in flight, renders nothing.
619
- */
620
- function FloorplanImage({
621
- url,
622
- center,
623
- width,
624
- height,
625
- rotation,
626
- preserveAspectRatio,
627
- opacity,
628
- }: {
629
- url: string
630
- center: readonly [number, number]
631
- width: number
632
- height: number
633
- rotation: number
634
- preserveAspectRatio: string
635
- opacity?: number
636
- }) {
637
- const [resolvedUrl, setResolvedUrl] = useState<string | null>(null)
638
- useEffect(() => {
639
- if (!url) {
640
- setResolvedUrl(null)
641
- return
642
- }
643
- let cancelled = false
644
- setResolvedUrl(null)
645
- loadAssetUrl(url).then((next) => {
646
- if (!cancelled) setResolvedUrl(next)
647
- })
648
- return () => {
649
- cancelled = true
650
- }
651
- }, [url])
652
- if (!resolvedUrl) return null
653
- const rotationDeg = (rotation * 180) / Math.PI
654
- return (
655
- <g
656
- pointerEvents="none"
657
- transform={`translate(${center[0]} ${center[1]}) rotate(${rotationDeg})`}
658
- >
659
- <image
660
- height={height}
661
- href={resolvedUrl}
662
- opacity={opacity}
663
- preserveAspectRatio={preserveAspectRatio}
664
- width={width}
665
- x={-width / 2}
666
- y={-height / 2}
667
- />
668
- </g>
669
- )
670
- }