@ifc-lite/viewer 1.17.4 → 1.18.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 (196) hide show
  1. package/.turbo/turbo-build.log +20 -17
  2. package/.turbo/turbo-typecheck.log +1 -1
  3. package/CHANGELOG.md +630 -0
  4. package/DESKTOP_CONTRACT_VERSION +1 -1
  5. package/dist/assets/{basketViewActivator-BmnNtVfZ.js → basketViewActivator-Cm1QEk_R.js} +1 -1
  6. package/dist/assets/drawing-2d-DoxKMqbO.js +257 -0
  7. package/dist/assets/{exporters-ChAtBmlj.js → exporters-B_OBqIyD.js} +3479 -2845
  8. package/dist/assets/{geometry.worker-BQ0rzNo-.js → geometry.worker-xHHy-9DV.js} +1 -1
  9. package/dist/assets/ids-DQ5jY0E8.js +1 -0
  10. package/dist/assets/ifc-lite_bg-ADjKXSms.wasm +0 -0
  11. package/dist/assets/{index-Co8E2-FE.js → index-BKq-M3Mk.js} +55873 -40593
  12. package/dist/assets/index-COnQRuqY.css +1 -0
  13. package/dist/assets/{native-bridge-BRvbckFQ.js → native-bridge-SHXiQwFW.js} +104 -104
  14. package/dist/assets/sandbox-jez21HtV.js +9627 -0
  15. package/dist/assets/{server-client-BV8zHZ7Y.js → server-client-ncOQVNso.js} +1 -1
  16. package/dist/assets/{wasm-bridge-g01g7T9b.js → wasm-bridge-DyfBSB8z.js} +1 -1
  17. package/dist/index.html +8 -7
  18. package/index.html +1 -0
  19. package/package.json +13 -13
  20. package/src/App.tsx +16 -2
  21. package/src/apache-arrow.d.ts +30 -0
  22. package/src/components/viewer/AddElementPanel.tsx +758 -0
  23. package/src/components/viewer/BulkPropertyEditor.tsx +7 -0
  24. package/src/components/viewer/CesiumOverlay.tsx +62 -19
  25. package/src/components/viewer/ChatPanel.tsx +259 -93
  26. package/src/components/viewer/CommandPalette.tsx +56 -7
  27. package/src/components/viewer/EntityContextMenu.tsx +168 -4
  28. package/src/components/viewer/ExportChangesButton.tsx +25 -5
  29. package/src/components/viewer/ExportDialog.tsx +19 -1
  30. package/src/components/viewer/MainToolbar.tsx +73 -13
  31. package/src/components/viewer/PropertiesPanel.tsx +237 -23
  32. package/src/components/viewer/SearchInline.tsx +669 -0
  33. package/src/components/viewer/SearchModal.filter.builder.tsx +766 -0
  34. package/src/components/viewer/SearchModal.filter.tsx +514 -0
  35. package/src/components/viewer/SearchModal.text.tsx +388 -0
  36. package/src/components/viewer/SearchModal.tsx +235 -0
  37. package/src/components/viewer/SettingsPage.tsx +252 -101
  38. package/src/components/viewer/ThemeSwitch.tsx +63 -7
  39. package/src/components/viewer/ToolOverlays.tsx +5 -0
  40. package/src/components/viewer/ViewerLayout.tsx +25 -4
  41. package/src/components/viewer/Viewport.tsx +25 -3
  42. package/src/components/viewer/ViewportContainer.tsx +51 -64
  43. package/src/components/viewer/ViewportOverlays.tsx +5 -2
  44. package/src/components/viewer/annotations/AnnotationDropInput.tsx +203 -0
  45. package/src/components/viewer/annotations/AnnotationLayer.tsx +287 -0
  46. package/src/components/viewer/annotations/AnnotationPin.tsx +90 -0
  47. package/src/components/viewer/annotations/AnnotationPopover.tsx +296 -0
  48. package/src/components/viewer/bcf/BCFTopicDetail.tsx +4 -4
  49. package/src/components/viewer/chat/ModelSelector.tsx +90 -54
  50. package/src/components/viewer/lists/ListPanel.tsx +14 -21
  51. package/src/components/viewer/properties/GeoreferencingPanel.tsx +113 -51
  52. package/src/components/viewer/properties/LocationMap.tsx +9 -7
  53. package/src/components/viewer/properties/ModelMetadataPanel.tsx +1 -1
  54. package/src/components/viewer/properties/RawStepCard.tsx +332 -0
  55. package/src/components/viewer/properties/RawStepRow.tsx +261 -0
  56. package/src/components/viewer/properties/ScheduleCard.tsx +224 -0
  57. package/src/components/viewer/properties/TaskEditCard.tsx +510 -0
  58. package/src/components/viewer/properties/raw-step-format.ts +193 -0
  59. package/src/components/viewer/schedule/AnimationSettingsPopover.tsx +542 -0
  60. package/src/components/viewer/schedule/GanttDependencyArrows.tsx +89 -0
  61. package/src/components/viewer/schedule/GanttDragTooltip.tsx +48 -0
  62. package/src/components/viewer/schedule/GanttEmptyState.tsx +97 -0
  63. package/src/components/viewer/schedule/GanttPanel.tsx +295 -0
  64. package/src/components/viewer/schedule/GanttTaskBar.tsx +199 -0
  65. package/src/components/viewer/schedule/GanttTaskTree.tsx +250 -0
  66. package/src/components/viewer/schedule/GanttTimeline.tsx +305 -0
  67. package/src/components/viewer/schedule/GanttToolbar.tsx +406 -0
  68. package/src/components/viewer/schedule/GenerateAdvancedPanel.tsx +147 -0
  69. package/src/components/viewer/schedule/GenerateScheduleDialog.tsx +392 -0
  70. package/src/components/viewer/schedule/HeightStrategyPanel.tsx +120 -0
  71. package/src/components/viewer/schedule/generate-schedule.test.ts +439 -0
  72. package/src/components/viewer/schedule/generate-schedule.ts +648 -0
  73. package/src/components/viewer/schedule/schedule-animator.test.ts +452 -0
  74. package/src/components/viewer/schedule/schedule-animator.ts +488 -0
  75. package/src/components/viewer/schedule/schedule-selection.test.ts +148 -0
  76. package/src/components/viewer/schedule/schedule-selection.ts +163 -0
  77. package/src/components/viewer/schedule/schedule-utils.ts +223 -0
  78. package/src/components/viewer/schedule/useConstructionSequence.ts +156 -0
  79. package/src/components/viewer/schedule/useGanttBarDrag.test.ts +90 -0
  80. package/src/components/viewer/schedule/useGanttBarDrag.ts +305 -0
  81. package/src/components/viewer/schedule/useGanttSelection3DHighlight.ts +152 -0
  82. package/src/components/viewer/schedule/useOverlayCompositor.ts +108 -0
  83. package/src/components/viewer/selectionHandlers.ts +446 -0
  84. package/src/components/viewer/tools/AddElementOverlay.tsx +540 -0
  85. package/src/components/viewer/tools/SectionCapControls.tsx +237 -0
  86. package/src/components/viewer/tools/SectionPanel.tsx +39 -18
  87. package/src/components/viewer/useAnimationLoop.ts +9 -1
  88. package/src/components/viewer/useDuplicateShortcut.ts +77 -0
  89. package/src/components/viewer/useMouseControls.ts +9 -1
  90. package/src/components/viewer/useRenderUpdates.ts +1 -1
  91. package/src/hooks/ids/idsDataAccessor.ts +60 -24
  92. package/src/hooks/ingest/viewerModelIngest.ts +7 -2
  93. package/src/hooks/useIfcFederation.ts +326 -71
  94. package/src/hooks/useIfcLoader.ts +23 -10
  95. package/src/hooks/useKeyboardShortcuts.ts +25 -0
  96. package/src/hooks/useSandbox.ts +1 -1
  97. package/src/hooks/useSearchIndex.ts +125 -0
  98. package/src/hooks/useViewControls.ts +13 -5
  99. package/src/index.css +550 -10
  100. package/src/lib/desktop-entitlement.ts +2 -4
  101. package/src/lib/geo/cesium-bridge.ts +15 -7
  102. package/src/lib/geo/effective-georef.test.ts +73 -0
  103. package/src/lib/geo/effective-georef.ts +111 -0
  104. package/src/lib/geo/reproject.ts +105 -19
  105. package/src/lib/llm/byok-guard.test.ts +77 -0
  106. package/src/lib/llm/byok-guard.ts +39 -0
  107. package/src/lib/llm/free-models.test.ts +0 -6
  108. package/src/lib/llm/models.ts +104 -42
  109. package/src/lib/llm/stream-client.ts +74 -110
  110. package/src/lib/llm/stream-direct.test.ts +130 -0
  111. package/src/lib/llm/stream-direct.ts +316 -0
  112. package/src/lib/llm/system-prompt.test.ts +14 -0
  113. package/src/lib/llm/system-prompt.ts +102 -1
  114. package/src/lib/llm/types.ts +20 -2
  115. package/src/lib/recent-files.ts +38 -4
  116. package/src/lib/scripts/templates/bim-globals.d.ts +136 -114
  117. package/src/lib/scripts/templates/construction-schedule.ts +223 -0
  118. package/src/lib/scripts/templates.ts +7 -0
  119. package/src/lib/search/common-ifc-types.ts +36 -0
  120. package/src/lib/search/filter-evaluate.test.ts +537 -0
  121. package/src/lib/search/filter-evaluate.ts +610 -0
  122. package/src/lib/search/filter-rules.test.ts +119 -0
  123. package/src/lib/search/filter-rules.ts +198 -0
  124. package/src/lib/search/filter-schema.test.ts +233 -0
  125. package/src/lib/search/filter-schema.ts +146 -0
  126. package/src/lib/search/recent-searches.test.ts +116 -0
  127. package/src/lib/search/recent-searches.ts +93 -0
  128. package/src/lib/search/result-export.test.ts +101 -0
  129. package/src/lib/search/result-export.ts +104 -0
  130. package/src/lib/search/saved-filters.test.ts +118 -0
  131. package/src/lib/search/saved-filters.ts +154 -0
  132. package/src/lib/search/tier0-scan.test.ts +196 -0
  133. package/src/lib/search/tier0-scan.ts +237 -0
  134. package/src/lib/search/tier1-index.test.ts +242 -0
  135. package/src/lib/search/tier1-index.ts +448 -0
  136. package/src/main.tsx +1 -10
  137. package/src/sdk/adapters/export-adapter.test.ts +434 -1
  138. package/src/sdk/adapters/export-adapter.ts +404 -1
  139. package/src/sdk/adapters/export-schedule-splice.test.ts +127 -0
  140. package/src/sdk/adapters/export-schedule-splice.ts +87 -0
  141. package/src/sdk/adapters/model-compat.ts +8 -2
  142. package/src/sdk/adapters/schedule-adapter.ts +73 -0
  143. package/src/sdk/adapters/store-adapter.ts +201 -0
  144. package/src/sdk/adapters/visibility-adapter.ts +3 -0
  145. package/src/sdk/local-backend.ts +16 -8
  146. package/src/services/api-keys.ts +73 -0
  147. package/src/services/desktop-export.ts +3 -1
  148. package/src/services/desktop-native-metadata.ts +41 -18
  149. package/src/services/file-dialog.ts +4 -1
  150. package/src/services/tauri-modules.d.ts +25 -0
  151. package/src/store/basketVisibleSet.ts +3 -0
  152. package/src/store/constants.ts +20 -2
  153. package/src/store/globalId.ts +4 -1
  154. package/src/store/index.ts +82 -6
  155. package/src/store/slices/addElementMeshes.ts +365 -0
  156. package/src/store/slices/addElementSlice.ts +275 -0
  157. package/src/store/slices/annotationsSlice.test.ts +133 -0
  158. package/src/store/slices/annotationsSlice.ts +251 -0
  159. package/src/store/slices/cesiumSlice.ts +5 -0
  160. package/src/store/slices/chatSlice.test.ts +6 -76
  161. package/src/store/slices/chatSlice.ts +17 -58
  162. package/src/store/slices/dataSlice.test.ts +23 -4
  163. package/src/store/slices/dataSlice.ts +1 -1
  164. package/src/store/slices/modelSlice.test.ts +67 -9
  165. package/src/store/slices/modelSlice.ts +39 -7
  166. package/src/store/slices/mutationSlice.ts +964 -3
  167. package/src/store/slices/overlayCompositor.test.ts +164 -0
  168. package/src/store/slices/overlaySlice.test.ts +93 -0
  169. package/src/store/slices/overlaySlice.ts +151 -0
  170. package/src/store/slices/pinboardSlice.test.ts +6 -1
  171. package/src/store/slices/playbackSlice.ts +128 -0
  172. package/src/store/slices/schedule-edit-helpers.test.ts +97 -0
  173. package/src/store/slices/schedule-edit-helpers.ts +179 -0
  174. package/src/store/slices/scheduleSlice.test.ts +694 -0
  175. package/src/store/slices/scheduleSlice.ts +1330 -0
  176. package/src/store/slices/searchSlice.test.ts +342 -0
  177. package/src/store/slices/searchSlice.ts +341 -0
  178. package/src/store/slices/sectionSlice.test.ts +87 -7
  179. package/src/store/slices/sectionSlice.ts +151 -5
  180. package/src/store/slices/selectionSlice.test.ts +46 -0
  181. package/src/store/slices/selectionSlice.ts +20 -0
  182. package/src/store/slices/uiSlice.ts +28 -5
  183. package/src/store/types.ts +26 -0
  184. package/src/store.ts +14 -0
  185. package/src/utils/nativeSpatialDataStore.ts +4 -1
  186. package/src/utils/viewportUtils.ts +7 -2
  187. package/src/vite-env.d.ts +0 -4
  188. package/dist/assets/drawing-2d-gWfpdfYe.js +0 -257
  189. package/dist/assets/ids-B4jTqB1O.js +0 -1
  190. package/dist/assets/ifc-lite_bg-BX4E7TX8.wasm +0 -0
  191. package/dist/assets/index-DckuDqlv.css +0 -1
  192. package/dist/assets/sandbox-DZiNLNMk.js +0 -5933
  193. package/src/components/viewer/UpgradePage.tsx +0 -71
  194. package/src/lib/desktop/ClerkDesktopEntitlementSync.tsx +0 -175
  195. package/src/lib/llm/ClerkChatSync.tsx +0 -74
  196. package/src/lib/llm/clerk-auth.ts +0 -62
@@ -4,7 +4,10 @@
4
4
 
5
5
  import type { EntityRef, FederatedModel } from './types.js';
6
6
 
7
- type ForwardModelMapLike = ReadonlyMap<string, { idOffset?: number }>;
7
+ /** Shape accepted by `toGlobalIdFromModels` re-export it so consumers can
8
+ * drop the `as unknown as Map<...>` cast when threading the store's
9
+ * federated `models` map through downstream helpers. */
10
+ export type ForwardModelMapLike = ReadonlyMap<string, { idOffset?: number }>;
8
11
  type ReverseModelMapLike = ReadonlyMap<string, Pick<FederatedModel, 'idOffset' | 'maxExpressId'>>;
9
12
 
10
13
  /**
@@ -34,6 +34,12 @@ import { createScriptSlice, type ScriptSlice } from './slices/scriptSlice.js';
34
34
  import { createChatSlice, type ChatSlice } from './slices/chatSlice.js';
35
35
  import { createCesiumSlice, type CesiumSlice } from './slices/cesiumSlice.js';
36
36
  import { createDesktopEntitlementSlice, type DesktopEntitlementSlice } from './slices/desktopEntitlementSlice.js';
37
+ import { createScheduleSlice, type ScheduleSlice } from './slices/scheduleSlice.js';
38
+ import { createPlaybackSlice, type PlaybackSlice } from './slices/playbackSlice.js';
39
+ import { createOverlaySlice, type OverlaySlice } from './slices/overlaySlice.js';
40
+ import { createSearchSlice, type SearchSlice } from './slices/searchSlice.js';
41
+ import { createAnnotationsSlice, type AnnotationsSlice } from './slices/annotationsSlice.js';
42
+ import { createAddElementSlice, type AddElementSlice } from './slices/addElementSlice.js';
37
43
  import { invalidateVisibleBasketCache } from './basketVisibleSet.js';
38
44
 
39
45
  // Import constants for reset function
@@ -43,7 +49,7 @@ import { CAMERA_DEFAULTS, SECTION_PLANE_DEFAULTS, UI_DEFAULTS, TYPE_VISIBILITY_D
43
49
  export type * from './types.js';
44
50
 
45
51
  // Explicitly re-export multi-model types that need to be imported by name
46
- export type { EntityRef, SchemaVersion, FederatedModel, MeasurementConstraintEdge, OrthogonalAxis } from './types.js';
52
+ export type { EntityRef, SchemaVersion, FederatedModel, MeasurementConstraintEdge, OrthogonalAxis, SectionCapStyle, SectionCapHatchId, SectionPlane, SectionPlaneAxis } from './types.js';
47
53
 
48
54
  // Re-export utility functions for entity references
49
55
  export { entityRefToString, stringToEntityRef, entityRefEquals, isIfcxDataStore } from './types.js';
@@ -83,6 +89,21 @@ export type { DesktopEntitlementSlice } from './slices/desktopEntitlementSlice.j
83
89
  // Re-export Cesium types
84
90
  export type { CesiumSlice, CesiumDataSource } from './slices/cesiumSlice.js';
85
91
 
92
+ // Re-export Schedule (4D) types + selectors
93
+ export type { ScheduleSlice, ScheduleTimeRange, GanttTimeScale } from './slices/scheduleSlice.js';
94
+ export type { PlaybackSlice } from './slices/playbackSlice.js';
95
+ export type { OverlaySlice, OverlayLayer, RGBA as OverlayRGBA } from './slices/overlaySlice.js';
96
+ export { composeLayers as composeOverlayLayers } from './slices/overlaySlice.js';
97
+ export {
98
+ computeScheduleRange,
99
+ computeHiddenProductIds,
100
+ computeActiveProductIds,
101
+ taskStartEpoch,
102
+ taskFinishEpoch,
103
+ parseIsoDate,
104
+ } from './slices/scheduleSlice.js';
105
+ export { resolveScheduleSourceModelId } from './slices/schedule-edit-helpers.js';
106
+
86
107
  // Combined store type
87
108
  export type ViewerState = LoadingSlice &
88
109
  SelectionSlice &
@@ -105,7 +126,13 @@ export type ViewerState = LoadingSlice &
105
126
  ScriptSlice &
106
127
  ChatSlice &
107
128
  CesiumSlice &
108
- DesktopEntitlementSlice & {
129
+ DesktopEntitlementSlice &
130
+ ScheduleSlice &
131
+ PlaybackSlice &
132
+ OverlaySlice &
133
+ SearchSlice &
134
+ AnnotationsSlice &
135
+ AddElementSlice & {
109
136
  resetViewerState: () => void;
110
137
  };
111
138
 
@@ -136,6 +163,12 @@ const createViewerStore = () => create<ViewerState>()((...args) => ({
136
163
  ...createChatSlice(...args),
137
164
  ...createCesiumSlice(...args),
138
165
  ...createDesktopEntitlementSlice(...args),
166
+ ...createScheduleSlice(...args),
167
+ ...createPlaybackSlice(...args),
168
+ ...createOverlaySlice(...args),
169
+ ...createSearchSlice(...args),
170
+ ...createAnnotationsSlice(...args),
171
+ ...createAddElementSlice(...args),
139
172
 
140
173
  // Reset all viewer state when loading new file
141
174
  // Note: Does NOT clear models - use clearAllModels() for that
@@ -195,12 +228,18 @@ const createViewerStore = () => create<ViewerState>()((...args) => ({
195
228
  cornerValence: 0,
196
229
  },
197
230
 
198
- // Section plane
231
+ // Section plane: reset axis/position/enabled/flipped (those are
232
+ // model-relative and meaningless when switching files), but PRESERVE
233
+ // the user's cap appearance preferences (showCap, showOutlines,
234
+ // capStyle). Those round-trip to localStorage via the slice's
235
+ // persistence helpers; clobbering them here was the cause of "my
236
+ // hatch / colour resets to defaults every time I open a file".
199
237
  sectionPlane: {
200
- axis: SECTION_PLANE_DEFAULTS.AXIS,
238
+ ...get().sectionPlane,
239
+ axis: SECTION_PLANE_DEFAULTS.AXIS,
201
240
  position: SECTION_PLANE_DEFAULTS.POSITION,
202
- enabled: SECTION_PLANE_DEFAULTS.ENABLED,
203
- flipped: SECTION_PLANE_DEFAULTS.FLIPPED,
241
+ enabled: SECTION_PLANE_DEFAULTS.ENABLED,
242
+ flipped: SECTION_PLANE_DEFAULTS.FLIPPED,
204
243
  },
205
244
 
206
245
  // Camera
@@ -224,6 +263,7 @@ const createViewerStore = () => create<ViewerState>()((...args) => ({
224
263
  separationLinesRadius: UI_DEFAULTS.SEPARATION_LINES_RADIUS,
225
264
 
226
265
  // Cesium
266
+ cesiumAvailable: false,
227
267
  cesiumEnabled: false,
228
268
  cesiumTerrainHeight: null,
229
269
  cesiumTerrainClamp: false,
@@ -331,6 +371,21 @@ const createViewerStore = () => create<ViewerState>()((...args) => ({
331
371
  chatError: null,
332
372
  chatAbortController: null,
333
373
 
374
+ // Schedule (4D) - drop panel + data; definitions are re-extracted on
375
+ // next load. `playbackSpeed`, `playbackLoop`, and `ganttTimeScale` are
376
+ // intentionally preserved as user preferences that survive file loads.
377
+ ganttPanelVisible: false,
378
+ generateScheduleDialogOpen: false,
379
+ scheduleData: null,
380
+ scheduleRange: null,
381
+ activeWorkScheduleId: '',
382
+ expandedTaskGlobalIds: new Set<string>(),
383
+ hoveredTaskGlobalId: null,
384
+ selectedTaskGlobalIds: new Set<string>(),
385
+ animationEnabled: false,
386
+ playbackIsPlaying: false,
387
+ playbackTime: 0,
388
+
334
389
  // Mutations - clear all mutation state so stale changes don't carry over
335
390
  mutationViews: new Map(),
336
391
  changeSets: new Map(),
@@ -339,6 +394,27 @@ const createViewerStore = () => create<ViewerState>()((...args) => ({
339
394
  redoStacks: new Map(),
340
395
  dirtyModels: new Set(),
341
396
  mutationVersion: get().mutationVersion + 1,
397
+
398
+ // Search - results reference the previous model's expressIds, drop them.
399
+ searchQuery: '',
400
+ searchOpen: false,
401
+ searchHighlightIndex: 0,
402
+ searchIndexes: new Map(),
403
+ searchVimCycle: null,
404
+ searchModalOpen: false,
405
+ searchFieldFilter: 'all',
406
+ searchModelFilter: null,
407
+ searchFilterResult: null,
408
+ searchFilterRunning: false,
409
+ searchFilterError: null,
410
+ searchFilter: { rules: [], combinator: 'AND', limit: 500 },
411
+ searchFilterSchema: new Map(),
412
+
413
+ // Annotations — drop draft + selection so a new file doesn't
414
+ // inherit the previous file's pin authoring state. Persisted
415
+ // pins themselves stay in localStorage (cross-file workspace).
416
+ draft: null,
417
+ selectedAnnotationId: null,
342
418
  });
343
419
  },
344
420
  }));
@@ -0,0 +1,365 @@
1
+ /* This Source Code Form is subject to the terms of the Mozilla Public
2
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
+
5
+ /**
6
+ * Build simple "instant preview" meshes for newly-added elements so
7
+ * the user sees them in 3D the moment the builder commits — without
8
+ * waiting for an export+re-parse round-trip.
9
+ *
10
+ * Coordinate-system note (matches the rest of the viewer):
11
+ * - Builder params are in **IFC storey-local** metres (Z-up).
12
+ * - The renderer is **Y-up** with `viewer.y = ifc.z + storeyElevation`,
13
+ * `viewer.x = ifc.x`, `viewer.z = -ifc.y`.
14
+ * - We emit positions directly in renderer-frame so the mesh slots
15
+ * into the standard mesh-list pipeline.
16
+ *
17
+ * What we don't try to do here: cut openings, host walls, model door
18
+ * leaves correctly, tessellate non-convex polygons. The preview is a
19
+ * faithful extrusion of the user's parametric input, not a final
20
+ * presentation render — when the user exports, the IFC pipeline emits
21
+ * the proper sub-graph and a re-parse yields the canonical geometry.
22
+ */
23
+
24
+ import type { MeshData } from '@ifc-lite/geometry';
25
+ import type {
26
+ AddElementType,
27
+ AddElementWallParams,
28
+ AddElementSlabParams,
29
+ AddElementBeamParams,
30
+ AddElementColumnParams,
31
+ AddElementDoorParams,
32
+ AddElementWindowParams,
33
+ AddElementSpaceParams,
34
+ AddElementRoofParams,
35
+ AddElementPlateParams,
36
+ AddElementMemberParams,
37
+ } from './addElementSlice';
38
+
39
+ type Vec3 = [number, number, number];
40
+
41
+ /** Per-type colour palette for the preview mesh. RGBA, 0..1. */
42
+ const COLORS: Record<AddElementType, [number, number, number, number]> = {
43
+ wall: [0.85, 0.85, 0.82, 1.0],
44
+ slab: [0.78, 0.78, 0.78, 1.0],
45
+ beam: [0.65, 0.50, 0.35, 1.0],
46
+ column: [0.65, 0.50, 0.35, 1.0],
47
+ door: [0.55, 0.35, 0.20, 1.0],
48
+ window: [0.45, 0.65, 0.85, 0.45],
49
+ space: [0.30, 0.85, 0.55, 0.18],
50
+ roof: [0.55, 0.35, 0.30, 1.0],
51
+ plate: [0.70, 0.70, 0.72, 1.0],
52
+ member: [0.55, 0.55, 0.50, 1.0],
53
+ };
54
+
55
+ const IFC_TYPE: Record<AddElementType, string> = {
56
+ wall: 'IfcWall',
57
+ slab: 'IfcSlab',
58
+ beam: 'IfcBeam',
59
+ column: 'IfcColumn',
60
+ door: 'IfcDoor',
61
+ window: 'IfcWindow',
62
+ space: 'IfcSpace',
63
+ roof: 'IfcRoof',
64
+ plate: 'IfcPlate',
65
+ member: 'IfcMember',
66
+ };
67
+
68
+ export interface ElementBuildContext {
69
+ type: AddElementType;
70
+ /** New entity's globalId (federation-aware). Tags every vertex. */
71
+ globalId: number;
72
+ /** Storey elevation in IFC Z (metres) — added to vertex Y in renderer. */
73
+ storeyElevation: number;
74
+ /** Per-element-type discriminated params + click points. */
75
+ payload: ElementMeshPayload;
76
+ }
77
+
78
+ export type ElementMeshPayload =
79
+ | { type: 'wall'; params: AddElementWallParams; start: Vec3; end: Vec3 }
80
+ | { type: 'beam'; params: AddElementBeamParams; start: Vec3; end: Vec3 }
81
+ | { type: 'member'; params: AddElementMemberParams; start: Vec3; end: Vec3 }
82
+ | { type: 'column'; params: AddElementColumnParams; position: Vec3 }
83
+ | { type: 'door'; params: AddElementDoorParams; position: Vec3 }
84
+ | { type: 'window'; params: AddElementWindowParams; position: Vec3 }
85
+ | { type: 'slab'; params: AddElementSlabParams; corners: Vec3[] }
86
+ | { type: 'space'; params: AddElementSpaceParams; corners: Vec3[] }
87
+ | { type: 'roof'; params: AddElementRoofParams; corners: Vec3[] }
88
+ | { type: 'plate'; params: AddElementPlateParams; corners: Vec3[] };
89
+
90
+ /**
91
+ * Build a renderer-frame `MeshData` for a freshly-added element.
92
+ * Returns `null` when the payload is degenerate (zero-length wall etc.).
93
+ */
94
+ export function buildElementMesh(ctx: ElementBuildContext): MeshData | null {
95
+ const { type, globalId, storeyElevation, payload } = ctx;
96
+ switch (payload.type) {
97
+ case 'wall':
98
+ case 'beam':
99
+ case 'member': {
100
+ // Linear extrusion: a (length × thickness × height) box centred
101
+ // along the click→click axis, sitting on the storey floor.
102
+ const thickness = 'Thickness' in payload.params
103
+ ? payload.params.Thickness
104
+ : payload.params.Width;
105
+ const height = 'Height' in payload.params ? payload.params.Height : 0.1;
106
+ return buildLinearBox(globalId, type, payload.start, payload.end, thickness, height, storeyElevation);
107
+ }
108
+ case 'column': {
109
+ const { Width, Depth, Height } = payload.params;
110
+ return buildAxisBox(globalId, type, payload.position, Width, Depth, Height, storeyElevation);
111
+ }
112
+ case 'door': {
113
+ const { Width, Height, FrameThickness } = payload.params;
114
+ return buildAxisBox(globalId, type, payload.position, Width, FrameThickness, Height, storeyElevation);
115
+ }
116
+ case 'window': {
117
+ const { Width, Height, FrameThickness } = payload.params;
118
+ return buildAxisBox(globalId, type, payload.position, Width, FrameThickness, Height, storeyElevation);
119
+ }
120
+ case 'slab':
121
+ case 'roof':
122
+ case 'plate': {
123
+ const thickness = payload.params.Thickness;
124
+ return buildPolygonExtrusion(globalId, type, payload.corners, thickness, storeyElevation, /* extrudeUp */ true);
125
+ }
126
+ case 'space': {
127
+ const height = payload.params.Height;
128
+ return buildPolygonExtrusion(globalId, type, payload.corners, height, storeyElevation, /* extrudeUp */ true);
129
+ }
130
+ }
131
+ }
132
+
133
+ /**
134
+ * Linear segment extruded into a thickness × height box (wall / beam /
135
+ * member shape). The bottom ring follows the segment's actual start/end
136
+ * Z so a sloped beam previews as a sloped prism instead of pinning to
137
+ * `startIfc[2]`. Walls reject sloped axes upstream
138
+ * (`addWallToStore` enforces planar XY); beams / members do not, which
139
+ * is why this routine has to honour both endpoints' Z.
140
+ */
141
+ function buildLinearBox(
142
+ globalId: number,
143
+ type: AddElementType,
144
+ startIfc: Vec3,
145
+ endIfc: Vec3,
146
+ thickness: number,
147
+ height: number,
148
+ storeyElevation: number,
149
+ ): MeshData | null {
150
+ const dx = endIfc[0] - startIfc[0];
151
+ const dy = endIfc[1] - startIfc[1];
152
+ // Cross-section plane is perpendicular to the ground-plane axis;
153
+ // even on sloped segments we keep the cross-section vertical so
154
+ // walls/beams/members read like building elements (extrusion is
155
+ // along +Z, not perpendicular to the segment direction).
156
+ const lenXY = Math.hypot(dx, dy);
157
+ if (lenXY < 1e-6) return null;
158
+ const ax = dx / lenXY;
159
+ const ay = dy / lenXY;
160
+ const nx = -ay;
161
+ const ny = ax;
162
+ const half = thickness / 2;
163
+ const startBaseZ = startIfc[2];
164
+ const endBaseZ = endIfc[2];
165
+ const startTopZ = startBaseZ + height;
166
+ const endTopZ = endBaseZ + height;
167
+ const ifcCorners: Vec3[] = [
168
+ [startIfc[0] + nx * half, startIfc[1] + ny * half, startBaseZ],
169
+ [endIfc[0] + nx * half, endIfc[1] + ny * half, endBaseZ],
170
+ [endIfc[0] - nx * half, endIfc[1] - ny * half, endBaseZ],
171
+ [startIfc[0] - nx * half, startIfc[1] - ny * half, startBaseZ],
172
+ [startIfc[0] + nx * half, startIfc[1] + ny * half, startTopZ],
173
+ [endIfc[0] + nx * half, endIfc[1] + ny * half, endTopZ],
174
+ [endIfc[0] - nx * half, endIfc[1] - ny * half, endTopZ],
175
+ [startIfc[0] - nx * half, startIfc[1] - ny * half, startTopZ],
176
+ ];
177
+ return buildBoxFromIfcCorners(globalId, type, ifcCorners, storeyElevation);
178
+ }
179
+
180
+ /** Axis-aligned box centred on a single point (column / door / window shape). */
181
+ function buildAxisBox(
182
+ globalId: number,
183
+ type: AddElementType,
184
+ centerIfc: Vec3,
185
+ sizeX: number,
186
+ sizeY: number,
187
+ sizeZ: number,
188
+ storeyElevation: number,
189
+ ): MeshData {
190
+ const hx = sizeX / 2;
191
+ const hy = sizeY / 2;
192
+ const baseZ = centerIfc[2];
193
+ const topZ = baseZ + sizeZ;
194
+ const cx = centerIfc[0];
195
+ const cy = centerIfc[1];
196
+ const ifcCorners: Vec3[] = [
197
+ [cx - hx, cy - hy, baseZ],
198
+ [cx + hx, cy - hy, baseZ],
199
+ [cx + hx, cy + hy, baseZ],
200
+ [cx - hx, cy + hy, baseZ],
201
+ [cx - hx, cy - hy, topZ],
202
+ [cx + hx, cy - hy, topZ],
203
+ [cx + hx, cy + hy, topZ],
204
+ [cx - hx, cy + hy, topZ],
205
+ ];
206
+ return buildBoxFromIfcCorners(globalId, type, ifcCorners, storeyElevation);
207
+ }
208
+
209
+ /** Polygon footprint extruded vertically (slab / space / roof / plate). */
210
+ function buildPolygonExtrusion(
211
+ globalId: number,
212
+ type: AddElementType,
213
+ ifcFootprint: Vec3[],
214
+ thickness: number,
215
+ storeyElevation: number,
216
+ extrudeUp: boolean,
217
+ ): MeshData | null {
218
+ const n = ifcFootprint.length;
219
+ if (n < 3 || thickness <= 0) return null;
220
+ const baseZ = ifcFootprint[0][2];
221
+ const topZ = baseZ + (extrudeUp ? thickness : -thickness);
222
+
223
+ // Vertex layout: bottom ring [0..n-1], top ring [n..2n-1]. Side
224
+ // quads built from consecutive ring pairs. Cap fans triangulate
225
+ // both rings around vertex 0 — fine for convex profiles, tolerable
226
+ // for slightly concave (the export still emits the proper polygon).
227
+ const vertCount = 2 * n;
228
+ const positions = new Float32Array(vertCount * 3);
229
+ const normals = new Float32Array(vertCount * 3);
230
+ for (let i = 0; i < n; i++) {
231
+ const [ix, iy] = ifcFootprint[i];
232
+ // Bottom ring (renderer-frame)
233
+ positions[i * 3 + 0] = ix;
234
+ positions[i * 3 + 1] = baseZ + storeyElevation;
235
+ positions[i * 3 + 2] = -iy;
236
+ normals[i * 3 + 0] = 0;
237
+ normals[i * 3 + 1] = -1;
238
+ normals[i * 3 + 2] = 0;
239
+ // Top ring
240
+ const j = (n + i) * 3;
241
+ positions[j + 0] = ix;
242
+ positions[j + 1] = topZ + storeyElevation;
243
+ positions[j + 2] = -iy;
244
+ normals[j + 0] = 0;
245
+ normals[j + 1] = 1;
246
+ normals[j + 2] = 0;
247
+ }
248
+
249
+ // Triangle counts: 2(n-2) for caps + 2n for sides = 4n - 4 triangles.
250
+ const triCount = 4 * n - 4;
251
+ const indices = new Uint32Array(triCount * 3);
252
+ let k = 0;
253
+
254
+ // Bottom cap — fan around vertex 0 (CW so it faces -Y / down).
255
+ for (let i = 1; i < n - 1; i++) {
256
+ indices[k++] = 0;
257
+ indices[k++] = i + 1;
258
+ indices[k++] = i;
259
+ }
260
+ // Top cap — fan around vertex n (CCW so it faces +Y / up).
261
+ for (let i = 1; i < n - 1; i++) {
262
+ indices[k++] = n;
263
+ indices[k++] = n + i;
264
+ indices[k++] = n + i + 1;
265
+ }
266
+ // Side quads — two triangles per edge.
267
+ for (let i = 0; i < n; i++) {
268
+ const i0 = i;
269
+ const i1 = (i + 1) % n;
270
+ const t0 = n + i0;
271
+ const t1 = n + i1;
272
+ indices[k++] = i0;
273
+ indices[k++] = i1;
274
+ indices[k++] = t1;
275
+ indices[k++] = i0;
276
+ indices[k++] = t1;
277
+ indices[k++] = t0;
278
+ }
279
+
280
+ const entityIds = new Uint32Array(vertCount);
281
+ entityIds.fill(globalId);
282
+
283
+ return {
284
+ expressId: globalId,
285
+ ifcType: IFC_TYPE[type],
286
+ positions,
287
+ normals,
288
+ indices,
289
+ color: COLORS[type],
290
+ entityIds,
291
+ };
292
+ }
293
+
294
+ /**
295
+ * Box mesh from 8 IFC-frame corners (bottom ring 0..3, top ring 4..7).
296
+ * Emits 12 triangles with face-aligned normals.
297
+ */
298
+ function buildBoxFromIfcCorners(
299
+ globalId: number,
300
+ type: AddElementType,
301
+ ifcCorners: Vec3[],
302
+ storeyElevation: number,
303
+ ): MeshData {
304
+ // Each face has 4 unique vertices (normal welded per face) → 24 verts.
305
+ // Faces: bottom, top, +U, +V, -U, -V (where U/V are the two sides).
306
+ const faces: Array<{ corners: number[]; normal: Vec3 }> = [
307
+ { corners: [0, 1, 2, 3], normal: [0, 0, -1] }, // bottom (IFC -Z)
308
+ { corners: [4, 7, 6, 5], normal: [0, 0, 1] }, // top (IFC +Z)
309
+ { corners: [0, 4, 5, 1], normal: faceNormal(ifcCorners, 0, 4, 1) },
310
+ { corners: [1, 5, 6, 2], normal: faceNormal(ifcCorners, 1, 5, 2) },
311
+ { corners: [2, 6, 7, 3], normal: faceNormal(ifcCorners, 2, 6, 3) },
312
+ { corners: [3, 7, 4, 0], normal: faceNormal(ifcCorners, 3, 7, 0) },
313
+ ];
314
+ const positions = new Float32Array(24 * 3);
315
+ const normals = new Float32Array(24 * 3);
316
+ const indices = new Uint32Array(36);
317
+ let v = 0;
318
+ let i = 0;
319
+ for (const face of faces) {
320
+ const baseV = v;
321
+ for (const ci of face.corners) {
322
+ const [ix, iy, iz] = ifcCorners[ci];
323
+ positions[v * 3 + 0] = ix;
324
+ positions[v * 3 + 1] = iz + storeyElevation;
325
+ positions[v * 3 + 2] = -iy;
326
+ // IFC-frame normal → renderer-frame.
327
+ normals[v * 3 + 0] = face.normal[0];
328
+ normals[v * 3 + 1] = face.normal[2];
329
+ normals[v * 3 + 2] = -face.normal[1];
330
+ v++;
331
+ }
332
+ // Two triangles per face — split as (0,1,2) + (0,2,3).
333
+ indices[i++] = baseV;
334
+ indices[i++] = baseV + 1;
335
+ indices[i++] = baseV + 2;
336
+ indices[i++] = baseV;
337
+ indices[i++] = baseV + 2;
338
+ indices[i++] = baseV + 3;
339
+ }
340
+ const entityIds = new Uint32Array(24);
341
+ entityIds.fill(globalId);
342
+ return {
343
+ expressId: globalId,
344
+ ifcType: IFC_TYPE[type],
345
+ positions,
346
+ normals,
347
+ indices,
348
+ color: COLORS[type],
349
+ entityIds,
350
+ };
351
+ }
352
+
353
+ function faceNormal(corners: Vec3[], a: number, b: number, c: number): Vec3 {
354
+ const ux = corners[b][0] - corners[a][0];
355
+ const uy = corners[b][1] - corners[a][1];
356
+ const uz = corners[b][2] - corners[a][2];
357
+ const vx = corners[c][0] - corners[a][0];
358
+ const vy = corners[c][1] - corners[a][1];
359
+ const vz = corners[c][2] - corners[a][2];
360
+ const nx = uy * vz - uz * vy;
361
+ const ny = uz * vx - ux * vz;
362
+ const nz = ux * vy - uy * vx;
363
+ const len = Math.hypot(nx, ny, nz) || 1;
364
+ return [nx / len, ny / len, nz / len];
365
+ }