@ifc-lite/viewer 1.7.0 → 1.9.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 (95) hide show
  1. package/CHANGELOG.md +88 -0
  2. package/dist/assets/{Arrow.dom-BGPQieQQ.js → Arrow.dom-CusgkT03.js} +1 -1
  3. package/dist/assets/browser-BXNIkE8a.js +694 -0
  4. package/dist/assets/emscripten-module-BTRCZGcB.wasm +0 -0
  5. package/dist/assets/emscripten-module-CGIn_cMh.wasm +0 -0
  6. package/dist/assets/emscripten-module-DYvzWiHh.wasm +0 -0
  7. package/dist/assets/emscripten-module-NWak2PoB.wasm +0 -0
  8. package/dist/assets/emscripten-module.browser-CY5t0Vfq.js +1 -0
  9. package/dist/assets/esbuild-COv63sf-.js +1 -0
  10. package/dist/assets/esbuild-Cpd5nU_H.wasm +0 -0
  11. package/dist/assets/ffi-DlhRHxHv.js +1 -0
  12. package/dist/assets/index-6Mr3byM-.js +216 -0
  13. package/dist/assets/index-CGbokkQ9.css +1 -0
  14. package/dist/assets/index-huvR-kGC.js +98305 -0
  15. package/dist/assets/module-6F3E5H7Y-tx0BadV3.js +6 -0
  16. package/dist/assets/{native-bridge-DD0SNyQ5.js → native-bridge-DsHOKdgD.js} +1 -1
  17. package/dist/assets/{wasm-bridge-D54YMO7X.js → wasm-bridge-Bd73HXn-.js} +1 -1
  18. package/dist/index.html +12 -3
  19. package/index.html +10 -1
  20. package/package.json +30 -21
  21. package/src/App.tsx +6 -1
  22. package/src/components/ui/dialog.tsx +8 -6
  23. package/src/components/viewer/CodeEditor.tsx +309 -0
  24. package/src/components/viewer/CommandPalette.tsx +597 -0
  25. package/src/components/viewer/Drawing2DCanvas.tsx +364 -1
  26. package/src/components/viewer/EntityContextMenu.tsx +47 -20
  27. package/src/components/viewer/ExportDialog.tsx +166 -17
  28. package/src/components/viewer/HierarchyPanel.tsx +3 -1
  29. package/src/components/viewer/LensPanel.tsx +848 -85
  30. package/src/components/viewer/MainToolbar.tsx +145 -84
  31. package/src/components/viewer/ScriptPanel.tsx +416 -0
  32. package/src/components/viewer/Section2DPanel.tsx +269 -29
  33. package/src/components/viewer/TextAnnotationEditor.tsx +112 -0
  34. package/src/components/viewer/ViewerLayout.tsx +63 -11
  35. package/src/components/viewer/Viewport.tsx +58 -23
  36. package/src/components/viewer/ViewportContainer.tsx +2 -0
  37. package/src/components/viewer/hierarchy/HierarchyNode.tsx +1 -1
  38. package/src/components/viewer/hierarchy/types.ts +1 -1
  39. package/src/components/viewer/lists/ListResultsTable.tsx +53 -19
  40. package/src/components/viewer/tools/cloudPathGenerator.test.ts +118 -0
  41. package/src/components/viewer/tools/cloudPathGenerator.ts +275 -0
  42. package/src/components/viewer/tools/computePolygonArea.test.ts +165 -0
  43. package/src/components/viewer/tools/computePolygonArea.ts +72 -0
  44. package/src/components/viewer/useGeometryStreaming.ts +25 -5
  45. package/src/hooks/ids/idsExportService.ts +1 -1
  46. package/src/hooks/useAnnotation2D.ts +551 -0
  47. package/src/hooks/useDrawingExport.ts +83 -1
  48. package/src/hooks/useKeyboardShortcuts.ts +114 -14
  49. package/src/hooks/useLens.ts +40 -55
  50. package/src/hooks/useLensDiscovery.ts +46 -0
  51. package/src/hooks/useModelSelection.ts +5 -22
  52. package/src/hooks/useSandbox.ts +113 -0
  53. package/src/index.css +7 -1
  54. package/src/lib/lens/adapter.ts +127 -1
  55. package/src/lib/lists/columnToAutoColor.ts +33 -0
  56. package/src/lib/recent-files.ts +122 -0
  57. package/src/lib/scripts/persistence.ts +132 -0
  58. package/src/lib/scripts/templates/bim-globals.d.ts +111 -0
  59. package/src/lib/scripts/templates/data-quality-audit.ts +149 -0
  60. package/src/lib/scripts/templates/envelope-check.ts +164 -0
  61. package/src/lib/scripts/templates/federation-compare.ts +189 -0
  62. package/src/lib/scripts/templates/fire-safety-check.ts +161 -0
  63. package/src/lib/scripts/templates/mep-equipment-schedule.ts +175 -0
  64. package/src/lib/scripts/templates/quantity-takeoff.ts +145 -0
  65. package/src/lib/scripts/templates/reset-view.ts +6 -0
  66. package/src/lib/scripts/templates/space-validation.ts +189 -0
  67. package/src/lib/scripts/templates/tsconfig.json +13 -0
  68. package/src/lib/scripts/templates.ts +86 -0
  69. package/src/sdk/BimProvider.tsx +50 -0
  70. package/src/sdk/adapters/export-adapter.ts +283 -0
  71. package/src/sdk/adapters/lens-adapter.ts +44 -0
  72. package/src/sdk/adapters/model-adapter.ts +32 -0
  73. package/src/sdk/adapters/model-compat.ts +80 -0
  74. package/src/sdk/adapters/mutate-adapter.ts +45 -0
  75. package/src/sdk/adapters/query-adapter.ts +241 -0
  76. package/src/sdk/adapters/selection-adapter.ts +29 -0
  77. package/src/sdk/adapters/spatial-adapter.ts +37 -0
  78. package/src/sdk/adapters/types.ts +11 -0
  79. package/src/sdk/adapters/viewer-adapter.ts +103 -0
  80. package/src/sdk/adapters/visibility-adapter.ts +61 -0
  81. package/src/sdk/local-backend.ts +144 -0
  82. package/src/sdk/useBimHost.ts +69 -0
  83. package/src/store/constants.ts +10 -2
  84. package/src/store/index.ts +28 -2
  85. package/src/store/resolveEntityRef.ts +44 -0
  86. package/src/store/slices/drawing2DSlice.ts +321 -0
  87. package/src/store/slices/lensSlice.ts +46 -4
  88. package/src/store/slices/pinboardSlice.ts +171 -42
  89. package/src/store/slices/scriptSlice.ts +218 -0
  90. package/src/store/slices/uiSlice.ts +2 -0
  91. package/src/store.ts +3 -0
  92. package/tsconfig.json +5 -2
  93. package/vite.config.ts +8 -0
  94. package/dist/assets/index-dgdgiQ9p.js +0 -75456
  95. package/dist/assets/index-yTqs8kgX.css +0 -1
@@ -0,0 +1,241 @@
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
+ import type {
6
+ EntityRef,
7
+ EntityData,
8
+ PropertySetData,
9
+ QuantitySetData,
10
+ QueryDescriptor,
11
+ QueryBackendMethods,
12
+ } from '@ifc-lite/sdk';
13
+ import type { StoreApi } from './types.js';
14
+ import { EntityNode } from '@ifc-lite/query';
15
+ import { RelationshipType, IfcTypeEnum, IfcTypeEnumFromString } from '@ifc-lite/data';
16
+ import { getModelForRef, getAllModelEntries } from './model-compat.js';
17
+
18
+ /** Map IFC relationship entity names to internal RelationshipType enum.
19
+ * Keys use proper IFC schema names (e.g. IfcRelAggregates, not "Aggregates"). */
20
+ const REL_TYPE_MAP: Record<string, RelationshipType> = {
21
+ IfcRelContainedInSpatialStructure: RelationshipType.ContainsElements,
22
+ IfcRelAggregates: RelationshipType.Aggregates,
23
+ IfcRelDefinesByType: RelationshipType.DefinesByType,
24
+ IfcRelVoidsElement: RelationshipType.VoidsElement,
25
+ IfcRelFillsElement: RelationshipType.FillsElement,
26
+ };
27
+
28
+ /**
29
+ * IFC4 subtype map — maps parent types to their StandardCase/ElementedCase subtypes.
30
+ * In IFC4, many element types have *StandardCase subtypes that the parser stores
31
+ * as the full type name. This map lets byType('IfcWall') also find IfcWallStandardCase.
32
+ *
33
+ * Keys and values are UPPERCASE because entityIndex.byType uses UPPERCASE keys
34
+ * (raw STEP type names, e.g. IFCWALLSTANDARDCASE).
35
+ */
36
+ const IFC_SUBTYPES: Record<string, string[]> = {
37
+ IFCWALL: ['IFCWALLSTANDARDCASE', 'IFCWALLELEMENTEDCASE'],
38
+ IFCBEAM: ['IFCBEAMSTANDARDCASE'],
39
+ IFCCOLUMN: ['IFCCOLUMNSTANDARDCASE'],
40
+ IFCDOOR: ['IFCDOORSTANDARDCASE'],
41
+ IFCWINDOW: ['IFCWINDOWSTANDARDCASE'],
42
+ IFCSLAB: ['IFCSLABSTANDARDCASE', 'IFCSLABELEMENTEDCASE'],
43
+ IFCMEMBER: ['IFCMEMBERSTANDARDCASE'],
44
+ IFCPLATE: ['IFCPLATESTANDARDCASE'],
45
+ IFCOPENINGELEMENT: ['IFCOPENINGSTANDARDCASE'],
46
+ };
47
+
48
+ /**
49
+ * Expand a type list to include known IFC subtypes.
50
+ * Converts PascalCase input (e.g. 'IfcWall') to UPPERCASE for entityIndex lookup.
51
+ */
52
+ function expandTypes(types: string[]): string[] {
53
+ const result: string[] = [];
54
+ for (const type of types) {
55
+ const upper = type.toUpperCase();
56
+ result.push(upper);
57
+ const subtypes = IFC_SUBTYPES[upper];
58
+ if (subtypes) {
59
+ for (const sub of subtypes) result.push(sub);
60
+ }
61
+ }
62
+ return result;
63
+ }
64
+
65
+ /**
66
+ * Check if a type name represents a product/spatial entity.
67
+ *
68
+ * Uses IfcTypeEnum as a whitelist — only known IFC types pass.
69
+ * Excludes relationships, properties, quantities, element quantities,
70
+ * and type objects (IfcWallType, IfcDoorType, etc.).
71
+ *
72
+ * Type names from entityIndex.byType are UPPERCASE (e.g. IFCWALLSTANDARDCASE).
73
+ */
74
+ function isProductType(type: string): boolean {
75
+ const enumVal = IfcTypeEnumFromString(type);
76
+ // Unknown = not a recognized product/spatial type (geometry definitions, placements, etc.)
77
+ if (enumVal === IfcTypeEnum.Unknown) return false;
78
+ // Exclude relationships, properties, quantities
79
+ const upper = type.toUpperCase();
80
+ if (upper.startsWith('IFCREL')) return false;
81
+ if (upper.startsWith('IFCPROPERTY')) return false;
82
+ if (upper.startsWith('IFCQUANTITY')) return false;
83
+ if (upper === 'IFCELEMENTQUANTITY') return false;
84
+ // Exclude type objects (IfcWallType, IfcDoorType, etc.) — metadata, not instances
85
+ if (upper.endsWith('TYPE')) return false;
86
+ return true;
87
+ }
88
+
89
+ export function createQueryAdapter(store: StoreApi): QueryBackendMethods {
90
+ function getEntityData(ref: EntityRef): EntityData | null {
91
+ const state = store.getState();
92
+ const model = getModelForRef(state, ref.modelId);
93
+ if (!model?.ifcDataStore) return null;
94
+
95
+ const node = new EntityNode(model.ifcDataStore, ref.expressId);
96
+ return {
97
+ ref,
98
+ globalId: node.globalId,
99
+ name: node.name,
100
+ type: node.type,
101
+ description: node.description,
102
+ objectType: node.objectType,
103
+ };
104
+ }
105
+
106
+ function getProperties(ref: EntityRef): PropertySetData[] {
107
+ const state = store.getState();
108
+ const model = getModelForRef(state, ref.modelId);
109
+ if (!model?.ifcDataStore) return [];
110
+
111
+ const node = new EntityNode(model.ifcDataStore, ref.expressId);
112
+ return node.properties().map((pset: { name: string; globalId?: string; properties: Array<{ name: string; type: number; value: string | number | boolean | null }> }) => ({
113
+ name: pset.name,
114
+ globalId: pset.globalId,
115
+ properties: pset.properties.map((p: { name: string; type: number; value: string | number | boolean | null }) => ({
116
+ name: p.name,
117
+ type: p.type,
118
+ value: p.value,
119
+ })),
120
+ }));
121
+ }
122
+
123
+ function getQuantities(ref: EntityRef): QuantitySetData[] {
124
+ const state = store.getState();
125
+ const model = getModelForRef(state, ref.modelId);
126
+ if (!model?.ifcDataStore) return [];
127
+
128
+ const node = new EntityNode(model.ifcDataStore, ref.expressId);
129
+ return node.quantities().map(qset => ({
130
+ name: qset.name,
131
+ quantities: qset.quantities.map(q => ({
132
+ name: q.name,
133
+ type: q.type,
134
+ value: q.value,
135
+ })),
136
+ }));
137
+ }
138
+
139
+ function queryEntities(descriptor: QueryDescriptor): EntityData[] {
140
+ const state = store.getState();
141
+ const results: EntityData[] = [];
142
+
143
+ const modelEntries = descriptor.modelId
144
+ ? [[descriptor.modelId, getModelForRef(state, descriptor.modelId)] as const].filter(([, m]) => m)
145
+ : getAllModelEntries(state);
146
+
147
+ for (const [modelId, model] of modelEntries) {
148
+ if (!model?.ifcDataStore) continue;
149
+
150
+ let entityIds: number[];
151
+ if (descriptor.types && descriptor.types.length > 0) {
152
+ // Expand types to include IFC4 subtypes (e.g., IfcWall → IfcWallStandardCase)
153
+ entityIds = [];
154
+ for (const type of expandTypes(descriptor.types)) {
155
+ const typeIds = model.ifcDataStore.entityIndex.byType.get(type) ?? [];
156
+ for (const id of typeIds) entityIds.push(id);
157
+ }
158
+ } else {
159
+ // No type filter — return product entities only (skip relationships, property defs)
160
+ entityIds = [];
161
+ for (const [typeName, ids] of model.ifcDataStore.entityIndex.byType) {
162
+ if (isProductType(typeName)) {
163
+ for (const id of ids) entityIds.push(id);
164
+ }
165
+ }
166
+ }
167
+ for (const expressId of entityIds) {
168
+ if (expressId === 0) continue;
169
+ const node = new EntityNode(model.ifcDataStore, expressId);
170
+ results.push({
171
+ ref: { modelId, expressId },
172
+ globalId: node.globalId,
173
+ name: node.name,
174
+ type: node.type,
175
+ description: node.description,
176
+ objectType: node.objectType,
177
+ });
178
+ }
179
+ }
180
+
181
+ // Apply property filters
182
+ let filtered = results;
183
+ if (descriptor.filters && descriptor.filters.length > 0) {
184
+ // Cache properties per entity to avoid O(n²) re-extraction per filter
185
+ const propsCache = new Map<string, PropertySetData[]>();
186
+ const getCachedProps = (ref: EntityRef): PropertySetData[] => {
187
+ const key = `${ref.modelId}:${ref.expressId}`;
188
+ let cached = propsCache.get(key);
189
+ if (!cached) {
190
+ cached = getProperties(ref);
191
+ propsCache.set(key, cached);
192
+ }
193
+ return cached;
194
+ };
195
+
196
+ for (const filter of descriptor.filters) {
197
+ filtered = filtered.filter(entity => {
198
+ const props = getCachedProps(entity.ref);
199
+ const pset = props.find(p => p.name === filter.psetName);
200
+ if (!pset) return false;
201
+ const prop = pset.properties.find(p => p.name === filter.propName);
202
+ if (!prop) return false;
203
+ if (filter.operator === 'exists') return true;
204
+
205
+ const val = prop.value;
206
+ switch (filter.operator) {
207
+ case '=': return String(val) === String(filter.value);
208
+ case '!=': return String(val) !== String(filter.value);
209
+ case '>': return Number(val) > Number(filter.value);
210
+ case '<': return Number(val) < Number(filter.value);
211
+ case '>=': return Number(val) >= Number(filter.value);
212
+ case '<=': return Number(val) <= Number(filter.value);
213
+ case 'contains': return String(val).includes(String(filter.value));
214
+ default: return false;
215
+ }
216
+ });
217
+ }
218
+ }
219
+
220
+ if (descriptor.offset != null && descriptor.offset > 0) filtered = filtered.slice(descriptor.offset);
221
+ if (descriptor.limit != null && descriptor.limit > 0) filtered = filtered.slice(0, descriptor.limit);
222
+
223
+ return filtered;
224
+ }
225
+
226
+ return {
227
+ entities: queryEntities,
228
+ entityData: getEntityData,
229
+ properties: getProperties,
230
+ quantities: getQuantities,
231
+ related(ref: EntityRef, relType: string, direction: 'forward' | 'inverse') {
232
+ const state = store.getState();
233
+ const model = getModelForRef(state, ref.modelId);
234
+ if (!model?.ifcDataStore) return [];
235
+ const relEnum = REL_TYPE_MAP[relType];
236
+ if (relEnum === undefined) return [];
237
+ const targets = model.ifcDataStore.relationships.getRelated(ref.expressId, relEnum, direction);
238
+ return targets.map((expressId: number) => ({ modelId: ref.modelId, expressId }));
239
+ },
240
+ };
241
+ }
@@ -0,0 +1,29 @@
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
+ import type { EntityRef, SelectionBackendMethods } from '@ifc-lite/sdk';
6
+ import type { StoreApi } from './types.js';
7
+
8
+ export function createSelectionAdapter(store: StoreApi): SelectionBackendMethods {
9
+ return {
10
+ get() {
11
+ const state = store.getState();
12
+ return state.selectedEntities ?? [];
13
+ },
14
+ set(refs: EntityRef[]) {
15
+ const state = store.getState();
16
+ if (refs.length === 0) {
17
+ state.clearEntitySelection?.();
18
+ } else if (refs.length === 1) {
19
+ state.setSelectedEntity?.(refs[0]);
20
+ } else {
21
+ state.clearEntitySelection?.();
22
+ for (const ref of refs) {
23
+ state.addEntityToSelection?.(ref);
24
+ }
25
+ }
26
+ return undefined;
27
+ },
28
+ };
29
+ }
@@ -0,0 +1,37 @@
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
+ import type { AABB, SpatialFrustum, SpatialBackendMethods } from '@ifc-lite/sdk';
6
+ import type { StoreApi } from './types.js';
7
+ import { getModelForRef } from './model-compat.js';
8
+
9
+ export function createSpatialAdapter(store: StoreApi): SpatialBackendMethods {
10
+ return {
11
+ queryBounds(modelId: string, bounds: AABB) {
12
+ const state = store.getState();
13
+ const model = getModelForRef(state, modelId);
14
+ if (!model?.ifcDataStore?.spatialIndex) return [];
15
+ const expressIds = model.ifcDataStore.spatialIndex.queryAABB(bounds);
16
+ return expressIds.map(expressId => ({ modelId, expressId }));
17
+ },
18
+ raycast(modelId: string, origin: [number, number, number], direction: [number, number, number]) {
19
+ const state = store.getState();
20
+ const model = getModelForRef(state, modelId);
21
+ if (!model?.ifcDataStore?.spatialIndex) return [];
22
+ const expressIds = model.ifcDataStore.spatialIndex.raycast(origin, direction);
23
+ return expressIds.map(expressId => ({ modelId, expressId }));
24
+ },
25
+ queryFrustum(modelId: string, frustum: SpatialFrustum) {
26
+ const state = store.getState();
27
+ const model = getModelForRef(state, modelId);
28
+ if (!model?.ifcDataStore?.spatialIndex) return [];
29
+ const index = model.ifcDataStore.spatialIndex as {
30
+ queryFrustum?: (frustum: SpatialFrustum) => number[];
31
+ };
32
+ if (!index.queryFrustum) return [];
33
+ const expressIds = index.queryFrustum(frustum);
34
+ return expressIds.map((expressId: number) => ({ modelId, expressId }));
35
+ },
36
+ };
37
+ }
@@ -0,0 +1,11 @@
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
+ import type { ViewerState } from '../../store/index.js';
6
+
7
+ /** Store API surface needed by adapters */
8
+ export type StoreApi = {
9
+ getState: () => ViewerState;
10
+ subscribe: (listener: (state: ViewerState, prevState: ViewerState) => void) => () => void;
11
+ };
@@ -0,0 +1,103 @@
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
+ import type { EntityRef, SectionPlane, CameraState, ViewerBackendMethods, RGBAColor } from '@ifc-lite/sdk';
6
+ import type { StoreApi } from './types.js';
7
+ import { getModelForRef } from './model-compat.js';
8
+
9
+ const AXIS_TO_STORE: Record<string, 'down' | 'front' | 'side'> = {
10
+ x: 'side',
11
+ y: 'down',
12
+ z: 'front',
13
+ };
14
+ const STORE_TO_AXIS: Record<string, 'x' | 'y' | 'z'> = {
15
+ side: 'x',
16
+ down: 'y',
17
+ front: 'z',
18
+ };
19
+
20
+ export function createViewerAdapter(store: StoreApi): ViewerBackendMethods {
21
+ return {
22
+ colorize(refs: EntityRef[], color: [number, number, number, number]) {
23
+ const state = store.getState();
24
+ // Merge with existing pending colors (supports multiple colorize calls per script)
25
+ const existing = state.pendingColorUpdates;
26
+ const colorMap = existing ? new Map(existing) : new Map<number, [number, number, number, number]>();
27
+ for (const ref of refs) {
28
+ const model = getModelForRef(state, ref.modelId);
29
+ if (model) {
30
+ const globalId = ref.expressId + model.idOffset;
31
+ colorMap.set(globalId, color);
32
+ }
33
+ }
34
+ state.setPendingColorUpdates(colorMap);
35
+ return undefined;
36
+ },
37
+ colorizeAll(batches: Array<{ refs: EntityRef[]; color: [number, number, number, number] }>) {
38
+ const state = store.getState();
39
+ // Batch colorize: build the complete color map in a single call.
40
+ // Avoids accumulation issues when React effects fire between calls.
41
+ const batchMap = new Map<number, [number, number, number, number]>();
42
+ for (const batch of batches) {
43
+ for (const ref of batch.refs) {
44
+ const model = getModelForRef(state, ref.modelId);
45
+ if (model) {
46
+ batchMap.set(ref.expressId + model.idOffset, batch.color);
47
+ }
48
+ }
49
+ }
50
+ state.setPendingColorUpdates(batchMap);
51
+ return undefined;
52
+ },
53
+ resetColors() {
54
+ const state = store.getState();
55
+ // Set empty map to trigger scene.clearColorOverrides() (null skips the effect)
56
+ state.setPendingColorUpdates(new Map());
57
+ return undefined;
58
+ },
59
+ flyTo() {
60
+ // flyTo requires renderer access — wired via useBimHost
61
+ return undefined;
62
+ },
63
+ setSection(section: SectionPlane | null) {
64
+ const state = store.getState();
65
+ if (section) {
66
+ state.setSectionPlaneAxis?.(AXIS_TO_STORE[section.axis] ?? 'down');
67
+ state.setSectionPlanePosition?.(section.position);
68
+ if (section.flipped !== undefined && state.sectionPlane?.flipped !== section.flipped) {
69
+ state.flipSectionPlane?.();
70
+ }
71
+ if (state.sectionPlane?.enabled !== section.enabled) {
72
+ state.toggleSectionPlane?.();
73
+ }
74
+ } else {
75
+ if (state.sectionPlane?.enabled) {
76
+ state.toggleSectionPlane?.();
77
+ }
78
+ }
79
+ return undefined;
80
+ },
81
+ getSection() {
82
+ const state = store.getState();
83
+ if (!state.sectionPlane?.enabled) return null;
84
+ return {
85
+ axis: STORE_TO_AXIS[state.sectionPlane.axis] ?? 'y',
86
+ position: state.sectionPlane.position,
87
+ enabled: state.sectionPlane.enabled,
88
+ flipped: state.sectionPlane.flipped,
89
+ };
90
+ },
91
+ setCamera(cameraState: Partial<CameraState>) {
92
+ const state = store.getState();
93
+ if (cameraState.mode) {
94
+ state.setProjectionMode?.(cameraState.mode);
95
+ }
96
+ return undefined;
97
+ },
98
+ getCamera() {
99
+ const state = store.getState();
100
+ return { mode: state.projectionMode ?? 'perspective' };
101
+ },
102
+ };
103
+ }
@@ -0,0 +1,61 @@
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
+ import type { EntityRef, VisibilityBackendMethods } from '@ifc-lite/sdk';
6
+ import type { StoreApi } from './types.js';
7
+ import { getModelForRef } from './model-compat.js';
8
+
9
+ export function createVisibilityAdapter(store: StoreApi): VisibilityBackendMethods {
10
+ return {
11
+ hide(refs: EntityRef[]) {
12
+ const state = store.getState();
13
+ // Convert EntityRef to global IDs — the renderer subscribes to the flat
14
+ // hiddenEntities set (global IDs), not hiddenEntitiesByModel.
15
+ const globalIds: number[] = [];
16
+ for (const ref of refs) {
17
+ const model = getModelForRef(state, ref.modelId);
18
+ if (model) {
19
+ globalIds.push(ref.expressId + model.idOffset);
20
+ }
21
+ }
22
+ if (globalIds.length > 0) {
23
+ state.hideEntities(globalIds);
24
+ }
25
+ return undefined;
26
+ },
27
+ show(refs: EntityRef[]) {
28
+ const state = store.getState();
29
+ const globalIds: number[] = [];
30
+ for (const ref of refs) {
31
+ const model = getModelForRef(state, ref.modelId);
32
+ if (model) {
33
+ globalIds.push(ref.expressId + model.idOffset);
34
+ }
35
+ }
36
+ if (globalIds.length > 0) {
37
+ state.showEntities(globalIds);
38
+ }
39
+ return undefined;
40
+ },
41
+ isolate(refs: EntityRef[]) {
42
+ const state = store.getState();
43
+ const globalIds: number[] = [];
44
+ for (const ref of refs) {
45
+ const model = getModelForRef(state, ref.modelId);
46
+ if (model) {
47
+ globalIds.push(ref.expressId + model.idOffset);
48
+ }
49
+ }
50
+ if (globalIds.length > 0) {
51
+ state.isolateEntities?.(globalIds);
52
+ }
53
+ return undefined;
54
+ },
55
+ reset() {
56
+ const state = store.getState();
57
+ state.showAllInAllModels?.();
58
+ return undefined;
59
+ },
60
+ };
61
+ }
@@ -0,0 +1,144 @@
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
+ * LocalBackend — implements BimBackend via per-namespace adapters.
7
+ *
8
+ * This is the viewer's internal backend: zero serialization overhead.
9
+ * Each namespace is a typed property with named methods.
10
+ */
11
+
12
+ import type {
13
+ BimBackend,
14
+ BimEventType,
15
+ ModelBackendMethods,
16
+ QueryBackendMethods,
17
+ SelectionBackendMethods,
18
+ VisibilityBackendMethods,
19
+ ViewerBackendMethods,
20
+ MutateBackendMethods,
21
+ SpatialBackendMethods,
22
+ ExportBackendMethods,
23
+ LensBackendMethods,
24
+ } from '@ifc-lite/sdk';
25
+ import type { StoreApi } from './adapters/types.js';
26
+ import { LEGACY_MODEL_ID } from './adapters/model-compat.js';
27
+ import { createModelAdapter } from './adapters/model-adapter.js';
28
+ import { createQueryAdapter } from './adapters/query-adapter.js';
29
+ import { createSelectionAdapter } from './adapters/selection-adapter.js';
30
+ import { createVisibilityAdapter } from './adapters/visibility-adapter.js';
31
+ import { createViewerAdapter } from './adapters/viewer-adapter.js';
32
+ import { createMutateAdapter } from './adapters/mutate-adapter.js';
33
+ import { createSpatialAdapter } from './adapters/spatial-adapter.js';
34
+ import { createLensAdapter } from './adapters/lens-adapter.js';
35
+ import { createExportAdapter } from './adapters/export-adapter.js';
36
+
37
+ export class LocalBackend implements BimBackend {
38
+ readonly model: ModelBackendMethods;
39
+ readonly query: QueryBackendMethods;
40
+ readonly selection: SelectionBackendMethods;
41
+ readonly visibility: VisibilityBackendMethods;
42
+ readonly viewer: ViewerBackendMethods;
43
+ readonly mutate: MutateBackendMethods;
44
+ readonly spatial: SpatialBackendMethods;
45
+ readonly export: ExportBackendMethods;
46
+ readonly lens: LensBackendMethods;
47
+
48
+ private store: StoreApi;
49
+
50
+ constructor(store: StoreApi) {
51
+ this.store = store;
52
+ this.model = createModelAdapter(store);
53
+ this.query = createQueryAdapter(store);
54
+ this.selection = createSelectionAdapter(store);
55
+ this.visibility = createVisibilityAdapter(store);
56
+ this.viewer = createViewerAdapter(store);
57
+ this.mutate = createMutateAdapter(store);
58
+ this.spatial = createSpatialAdapter(store);
59
+ this.lens = createLensAdapter(store);
60
+ this.export = createExportAdapter(store);
61
+ }
62
+
63
+ subscribe(event: BimEventType, handler: (data: unknown) => void): () => void {
64
+ switch (event) {
65
+ case 'selection:changed':
66
+ return this.store.subscribe((state, prev) => {
67
+ if (state.selectedEntities !== prev.selectedEntities) {
68
+ handler({ refs: state.selectedEntities ?? [] });
69
+ }
70
+ });
71
+
72
+ case 'model:loaded':
73
+ return this.store.subscribe((state, prev) => {
74
+ if (state.models.size > prev.models.size) {
75
+ for (const [id, model] of state.models) {
76
+ if (!prev.models.has(id)) {
77
+ handler({
78
+ model: {
79
+ id: model.id,
80
+ name: model.name,
81
+ schemaVersion: model.schemaVersion,
82
+ entityCount: model.ifcDataStore?.entities?.count ?? 0,
83
+ fileSize: model.fileSize,
84
+ loadedAt: model.loadedAt,
85
+ },
86
+ });
87
+ }
88
+ }
89
+ }
90
+ if (state.ifcDataStore && !prev.ifcDataStore && state.models.size === 0) {
91
+ handler({
92
+ model: {
93
+ id: LEGACY_MODEL_ID,
94
+ name: 'Model',
95
+ schemaVersion: state.ifcDataStore.schemaVersion ?? 'IFC4',
96
+ entityCount: state.ifcDataStore.entities?.count ?? 0,
97
+ fileSize: state.ifcDataStore.source?.byteLength ?? 0,
98
+ loadedAt: 0,
99
+ },
100
+ });
101
+ }
102
+ });
103
+
104
+ case 'model:removed':
105
+ return this.store.subscribe((state, prev) => {
106
+ if (state.models.size < prev.models.size) {
107
+ for (const id of prev.models.keys()) {
108
+ if (!state.models.has(id)) {
109
+ handler({ modelId: id });
110
+ }
111
+ }
112
+ }
113
+ });
114
+
115
+ case 'visibility:changed':
116
+ return this.store.subscribe((state, prev) => {
117
+ if (
118
+ state.hiddenEntities !== prev.hiddenEntities ||
119
+ state.isolatedEntities !== prev.isolatedEntities ||
120
+ state.hiddenEntitiesByModel !== prev.hiddenEntitiesByModel
121
+ ) {
122
+ handler({});
123
+ }
124
+ });
125
+
126
+ case 'mutation:changed':
127
+ return this.store.subscribe((state, prev) => {
128
+ if (state.mutationVersion !== prev.mutationVersion) {
129
+ handler({});
130
+ }
131
+ });
132
+
133
+ case 'lens:changed':
134
+ return this.store.subscribe((state, prev) => {
135
+ if (state.activeLensId !== prev.activeLensId) {
136
+ handler({ lensId: state.activeLensId });
137
+ }
138
+ });
139
+
140
+ default:
141
+ return () => {};
142
+ }
143
+ }
144
+ }