@ifc-lite/viewer 1.13.0 → 1.14.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 (28) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/assets/{Arrow.dom-VW5W1XFO.js → Arrow.dom-CNguvlQi.js} +1 -1
  3. package/dist/assets/{browser-C6mwD6n0.js → browser-D6lgLpkA.js} +1 -1
  4. package/dist/assets/{index-DQE23JyT.js → index-BMwpw264.js} +4 -4
  5. package/dist/assets/index-Qp8stcGO.css +1 -0
  6. package/dist/assets/{index-BzoX4cQC.js → index-UaDsJsCR.js} +24458 -22069
  7. package/dist/assets/{native-bridge-BibEEmFV.js → native-bridge-DqELq4X0.js} +1 -1
  8. package/dist/assets/{wasm-bridge-CYzUd3Io.js → wasm-bridge-CVWvHlfH.js} +1 -1
  9. package/dist/index.html +2 -2
  10. package/package.json +19 -19
  11. package/src/components/viewer/BulkPropertyEditor.tsx +8 -1
  12. package/src/components/viewer/DataConnector.tsx +8 -1
  13. package/src/components/viewer/ExportChangesButton.tsx +8 -1
  14. package/src/components/viewer/ExportDialog.tsx +8 -1
  15. package/src/components/viewer/PropertiesPanel.tsx +209 -15
  16. package/src/components/viewer/properties/BsddCard.tsx +507 -0
  17. package/src/components/viewer/properties/QuantitySetCard.tsx +1 -0
  18. package/src/index.css +7 -0
  19. package/src/lib/scripts/templates/bim-globals.d.ts +33 -0
  20. package/src/lib/scripts/templates/create-building.ts +491 -0
  21. package/src/lib/scripts/templates.ts +8 -0
  22. package/src/sdk/adapters/export-adapter.ts +84 -0
  23. package/src/sdk/adapters/model-adapter.ts +8 -0
  24. package/src/services/bsdd.ts +262 -0
  25. package/src/store/index.ts +2 -2
  26. package/src/store/slices/mutationSlice.ts +155 -1
  27. package/vite.config.ts +7 -0
  28. package/dist/assets/index-Cx134arv.css +0 -1
@@ -0,0 +1,262 @@
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
+ * bSDD (buildingSMART Data Dictionary) API client.
7
+ *
8
+ * Fetches IFC class definitions, property sets, and properties from the
9
+ * bSDD REST API so that users can discover schema-conform properties
10
+ * for a selected IFC entity type and add them in one click.
11
+ *
12
+ * API docs: https://app.swaggerhub.com/apis/buildingSMART/Dictionaries/v1
13
+ */
14
+
15
+ // Proxy through our own origin to avoid CORS issues.
16
+ // In dev Vite proxies /api/bsdd → https://api.bsdd.buildingsmart.org,
17
+ // in production Vercel rewrites do the same.
18
+ const BSDD_API = '/api/bsdd';
19
+ const IFC_DICTIONARY_URI =
20
+ 'https://identifier.buildingsmart.org/uri/buildingsmart/ifc/4.3';
21
+
22
+ // ---------------------------------------------------------------------------
23
+ // Types
24
+ // ---------------------------------------------------------------------------
25
+
26
+ export interface BsddClassProperty {
27
+ /** Property name, e.g. "IsExternal" */
28
+ name: string;
29
+ /** URI of the property definition */
30
+ uri: string;
31
+ /** Human-readable description */
32
+ description: string | null;
33
+ /** bSDD data type, e.g. "Boolean", "Real", "String" */
34
+ dataType: string | null;
35
+ /** Name of the property set this property belongs to */
36
+ propertySet: string | null;
37
+ /** Allowed values (enum constraints) */
38
+ allowedValues: Array<{ uri?: string; value: string; description?: string }> | null;
39
+ /** Units */
40
+ units: string[] | null;
41
+ /** Whether this is from the IFC standard dictionary */
42
+ isIfcStandard: boolean;
43
+ }
44
+
45
+ export interface BsddClassInfo {
46
+ /** Class URI */
47
+ uri: string;
48
+ /** IFC entity code, e.g. "IfcWall" */
49
+ code: string;
50
+ /** Human-readable name */
51
+ name: string;
52
+ /** Description / definition */
53
+ definition: string | null;
54
+ /** Parent class URI */
55
+ parentClassUri: string | null;
56
+ /** Properties defined for this class */
57
+ classProperties: BsddClassProperty[];
58
+ /** Related IFC entity names */
59
+ relatedIfcEntityNames: string[] | null;
60
+ }
61
+
62
+ export interface BsddSearchResult {
63
+ uri: string;
64
+ code: string;
65
+ name: string;
66
+ definition: string | null;
67
+ dictionaryUri: string;
68
+ }
69
+
70
+ // ---------------------------------------------------------------------------
71
+ // In-memory cache (keyed by class URI)
72
+ // ---------------------------------------------------------------------------
73
+
74
+ const classCache = new Map<string, { data: BsddClassInfo; ts: number }>();
75
+ const CACHE_TTL_MS = 10 * 60 * 1000; // 10 minutes
76
+
77
+ function getCached(key: string): BsddClassInfo | null {
78
+ const entry = classCache.get(key);
79
+ if (entry && Date.now() - entry.ts < CACHE_TTL_MS) return entry.data;
80
+ if (entry) classCache.delete(key);
81
+ return null;
82
+ }
83
+
84
+ function setCache(key: string, data: BsddClassInfo) {
85
+ classCache.set(key, { data, ts: Date.now() });
86
+ }
87
+
88
+ // ---------------------------------------------------------------------------
89
+ // API helpers
90
+ // ---------------------------------------------------------------------------
91
+
92
+ async function fetchJson<T>(url: string): Promise<T> {
93
+ const res = await fetch(url, {
94
+ headers: { Accept: 'application/json' },
95
+ });
96
+ if (!res.ok) {
97
+ throw new Error(`bSDD API ${res.status}: ${res.statusText}`);
98
+ }
99
+ return res.json() as Promise<T>;
100
+ }
101
+
102
+ // ---------------------------------------------------------------------------
103
+ // Public API
104
+ // ---------------------------------------------------------------------------
105
+
106
+ /**
107
+ * Build the bSDD class URI for an IFC entity type.
108
+ * e.g. "IfcWall" -> "https://identifier.buildingsmart.org/uri/buildingsmart/ifc/4.3/class/IfcWall"
109
+ */
110
+ export function ifcClassUri(ifcType: string): string {
111
+ // Use the type name as-is. IFC parsers typically produce PascalCase
112
+ // names (e.g. "IfcWall") which match the bSDD URI scheme directly.
113
+ // Previous best-effort lowercasing corrupted multi-word names like
114
+ // IFCWALLSTANDARDCASE → "IfcWallstandardcase", so we no longer attempt
115
+ // case normalisation — the bSDD API will simply 404 for unknown names
116
+ // and we handle that gracefully.
117
+ return `${IFC_DICTIONARY_URI}/class/${ifcType}`;
118
+ }
119
+
120
+ /**
121
+ * Fetch full class info (including properties) for an IFC entity type.
122
+ *
123
+ * Uses the `/api/Class/v1` endpoint with `IncludeClassProperties=true`
124
+ * (PascalCase parameter names per the bSDD OpenAPI spec).
125
+ * Falls back to the paginated `/api/Class/Properties/v1` endpoint when
126
+ * the inline property list comes back empty.
127
+ */
128
+ export async function fetchClassInfo(
129
+ ifcType: string,
130
+ ): Promise<BsddClassInfo | null> {
131
+ const uri = ifcClassUri(ifcType);
132
+ const cached = getCached(uri);
133
+ if (cached) return cached;
134
+
135
+ try {
136
+ // Parameter names must be PascalCase per the bSDD OpenAPI spec
137
+ const raw = await fetchJson<Record<string, unknown>>(
138
+ `${BSDD_API}/api/Class/v1?Uri=${encodeURIComponent(uri)}&IncludeClassProperties=true&IncludeClassRelations=true`,
139
+ );
140
+
141
+ let info = mapClassResponse(raw, true);
142
+
143
+ // Fallback: if inline classProperties came back empty, try the
144
+ // dedicated paginated properties endpoint
145
+ if (info.classProperties.length === 0) {
146
+ const propsRaw = await fetchJson<Record<string, unknown>>(
147
+ `${BSDD_API}/api/Class/Properties/v1?ClassUri=${encodeURIComponent(uri)}`,
148
+ ).catch(() => null);
149
+
150
+ if (propsRaw) {
151
+ const propsList = propsRaw.classProperties as Array<Record<string, unknown>> | undefined;
152
+ if (propsList && propsList.length > 0) {
153
+ info = {
154
+ ...info,
155
+ classProperties: propsList.map((p) => ({
156
+ name: String(p.name ?? p.propertyCode ?? ''),
157
+ uri: String(p.propertyUri ?? p.uri ?? ''),
158
+ description: p.description ? String(p.description) : null,
159
+ dataType: p.dataType ? String(p.dataType) : null,
160
+ propertySet: p.propertySet ? String(p.propertySet) : null,
161
+ allowedValues: Array.isArray(p.allowedValues)
162
+ ? p.allowedValues.map((v: Record<string, unknown>) => ({
163
+ uri: v.uri ? String(v.uri) : undefined,
164
+ value: String(v.value ?? ''),
165
+ description: v.description ? String(v.description) : undefined,
166
+ }))
167
+ : null,
168
+ units: Array.isArray(p.units) ? (p.units as string[]) : null,
169
+ isIfcStandard: true,
170
+ })),
171
+ };
172
+ }
173
+ }
174
+ }
175
+
176
+ setCache(uri, info);
177
+ return info;
178
+ } catch {
179
+ // Silently return null – bSDD may not have data for every type
180
+ return null;
181
+ }
182
+ }
183
+
184
+ /**
185
+ * Search bSDD for classes related to a given IFC entity type across all
186
+ * dictionaries (not just the IFC dictionary).
187
+ *
188
+ * Uses `/api/Class/Search/v1` with a RelatedIfcEntities filter.
189
+ * Returns lightweight results. Call `fetchClassInfo` on a specific result
190
+ * to get full properties.
191
+ */
192
+ export async function searchRelatedClasses(
193
+ ifcType: string,
194
+ ): Promise<BsddSearchResult[]> {
195
+ try {
196
+ const raw = await fetchJson<{
197
+ classes?: Array<Record<string, unknown>>;
198
+ }>(
199
+ `${BSDD_API}/api/Class/Search/v1?SearchText=${encodeURIComponent(ifcType)}&RelatedIfcEntities=${encodeURIComponent(ifcType)}`,
200
+ );
201
+ return (raw.classes ?? []).map((c) => ({
202
+ uri: String(c.uri ?? ''),
203
+ code: String(c.code ?? c.name ?? ''),
204
+ name: String(c.name ?? ''),
205
+ definition: c.definition ? String(c.definition) : null,
206
+ dictionaryUri: String(c.dictionaryUri ?? ''),
207
+ }));
208
+ } catch {
209
+ return [];
210
+ }
211
+ }
212
+
213
+ // ---------------------------------------------------------------------------
214
+ // Response mapping
215
+ // ---------------------------------------------------------------------------
216
+
217
+ function mapClassResponse(
218
+ raw: Record<string, unknown>,
219
+ isIfcStandard: boolean,
220
+ ): BsddClassInfo {
221
+ const props = raw.classProperties as Array<Record<string, unknown>> | undefined;
222
+
223
+ return {
224
+ uri: String(raw.uri ?? ''),
225
+ code: String(raw.code ?? raw.name ?? ''),
226
+ name: String(raw.name ?? ''),
227
+ definition: raw.definition ? String(raw.definition) : null,
228
+ parentClassUri: raw.parentClassReference
229
+ ? String((raw.parentClassReference as Record<string, unknown>).uri ?? '')
230
+ : null,
231
+ relatedIfcEntityNames: raw.relatedIfcEntityNames as string[] | null,
232
+ classProperties: (props ?? []).map((p) => ({
233
+ name: String(p.name ?? p.propertyCode ?? ''),
234
+ uri: String(p.propertyUri ?? p.uri ?? ''),
235
+ description: p.description ? String(p.description) : null,
236
+ dataType: p.dataType ? String(p.dataType) : null,
237
+ propertySet: p.propertySet ? String(p.propertySet) : null,
238
+ allowedValues: Array.isArray(p.allowedValues)
239
+ ? p.allowedValues.map((v: Record<string, unknown>) => ({
240
+ uri: v.uri ? String(v.uri) : undefined,
241
+ value: String(v.value ?? ''),
242
+ description: v.description ? String(v.description) : undefined,
243
+ }))
244
+ : null,
245
+ units: Array.isArray(p.units) ? (p.units as string[]) : null,
246
+ isIfcStandard,
247
+ })),
248
+ };
249
+ }
250
+
251
+ /**
252
+ * Map bSDD dataType string to a human-friendly label.
253
+ */
254
+ export function bsddDataTypeLabel(dt: string | null): string {
255
+ if (!dt) return 'String';
256
+ const lower = dt.toLowerCase();
257
+ if (lower === 'boolean') return 'Boolean';
258
+ if (lower === 'real' || lower === 'number') return 'Real';
259
+ if (lower === 'integer') return 'Integer';
260
+ if (lower === 'string' || lower === 'character') return 'String';
261
+ return dt;
262
+ }
@@ -275,8 +275,8 @@ export const useViewerStore = create<ViewerState>()((...args) => ({
275
275
  basketPresentationVisible: false,
276
276
  hierarchyBasketSelection: new Set<string>(),
277
277
 
278
- // Script - reset execution state but keep saved scripts and editor content
279
- scriptPanelVisible: false,
278
+ // Script - reset execution state but keep saved scripts, editor content, and panel visibility
279
+ // (scripts that create-and-load a model should not close the panel)
280
280
  scriptExecutionState: 'idle' as const,
281
281
  scriptLastResult: null,
282
282
  scriptLastError: null,
@@ -10,7 +10,7 @@ import { type StateCreator } from 'zustand';
10
10
  import type { ViewerState } from '../index.js';
11
11
  import type { MutablePropertyView } from '@ifc-lite/mutations';
12
12
  import type { Mutation, ChangeSet, PropertyValue } from '@ifc-lite/mutations';
13
- import { PropertyValueType } from '@ifc-lite/data';
13
+ import { PropertyValueType, QuantityType } from '@ifc-lite/data';
14
14
 
15
15
  export interface MutationSlice {
16
16
  // State
@@ -68,6 +68,35 @@ export interface MutationSlice {
68
68
  psetName: string
69
69
  ) => Mutation | null;
70
70
 
71
+ // Actions - Quantity Mutations
72
+ /** Set a quantity value */
73
+ setQuantity: (
74
+ modelId: string,
75
+ entityId: number,
76
+ qsetName: string,
77
+ quantName: string,
78
+ value: number,
79
+ quantityType?: QuantityType,
80
+ unit?: string
81
+ ) => Mutation | null;
82
+ /** Create a new quantity set */
83
+ createQuantitySet: (
84
+ modelId: string,
85
+ entityId: number,
86
+ qsetName: string,
87
+ quantities: Array<{ name: string; value: number; quantityType: QuantityType; unit?: string }>
88
+ ) => Mutation | null;
89
+
90
+ // Actions - Attribute Mutations
91
+ /** Set an entity attribute value */
92
+ setAttribute: (
93
+ modelId: string,
94
+ entityId: number,
95
+ attrName: string,
96
+ value: string,
97
+ oldValue?: string
98
+ ) => Mutation | null;
99
+
71
100
  // Actions - Undo/Redo
72
101
  /** Undo last mutation for a model */
73
102
  undo: (modelId: string) => void;
@@ -266,6 +295,92 @@ export const createMutationSlice: StateCreator<
266
295
  return mutation;
267
296
  },
268
297
 
298
+ // Quantity Mutations
299
+ setQuantity: (modelId, entityId, qsetName, quantName, value, quantityType = QuantityType.Count, unit) => {
300
+ const view = get().mutationViews.get(modelId);
301
+ if (!view) return null;
302
+
303
+ const mutation = view.setQuantity(entityId, qsetName, quantName, value, quantityType, unit);
304
+
305
+ set((state) => {
306
+ const newUndoStacks = new Map(state.undoStacks);
307
+ const stack = newUndoStacks.get(modelId) || [];
308
+ newUndoStacks.set(modelId, [...stack, mutation]);
309
+
310
+ const newRedoStacks = new Map(state.redoStacks);
311
+ newRedoStacks.set(modelId, []);
312
+
313
+ const newDirty = new Set(state.dirtyModels);
314
+ newDirty.add(modelId);
315
+
316
+ return {
317
+ undoStacks: newUndoStacks,
318
+ redoStacks: newRedoStacks,
319
+ dirtyModels: newDirty,
320
+ mutationVersion: state.mutationVersion + 1,
321
+ };
322
+ });
323
+
324
+ return mutation;
325
+ },
326
+
327
+ createQuantitySet: (modelId, entityId, qsetName, quantities) => {
328
+ const view = get().mutationViews.get(modelId);
329
+ if (!view) return null;
330
+
331
+ const mutation = view.createQuantitySet(entityId, qsetName, quantities);
332
+
333
+ set((state) => {
334
+ const newUndoStacks = new Map(state.undoStacks);
335
+ const stack = newUndoStacks.get(modelId) || [];
336
+ newUndoStacks.set(modelId, [...stack, mutation]);
337
+
338
+ const newRedoStacks = new Map(state.redoStacks);
339
+ newRedoStacks.set(modelId, []);
340
+
341
+ const newDirty = new Set(state.dirtyModels);
342
+ newDirty.add(modelId);
343
+
344
+ return {
345
+ undoStacks: newUndoStacks,
346
+ redoStacks: newRedoStacks,
347
+ dirtyModels: newDirty,
348
+ mutationVersion: state.mutationVersion + 1,
349
+ };
350
+ });
351
+
352
+ return mutation;
353
+ },
354
+
355
+ // Attribute Mutations
356
+ setAttribute: (modelId, entityId, attrName, value, oldValue) => {
357
+ const view = get().mutationViews.get(modelId);
358
+ if (!view) return null;
359
+
360
+ const mutation = view.setAttribute(entityId, attrName, value, oldValue);
361
+
362
+ set((state) => {
363
+ const newUndoStacks = new Map(state.undoStacks);
364
+ const stack = newUndoStacks.get(modelId) || [];
365
+ newUndoStacks.set(modelId, [...stack, mutation]);
366
+
367
+ const newRedoStacks = new Map(state.redoStacks);
368
+ newRedoStacks.set(modelId, []);
369
+
370
+ const newDirty = new Set(state.dirtyModels);
371
+ newDirty.add(modelId);
372
+
373
+ return {
374
+ undoStacks: newUndoStacks,
375
+ redoStacks: newRedoStacks,
376
+ dirtyModels: newDirty,
377
+ mutationVersion: state.mutationVersion + 1,
378
+ };
379
+ });
380
+
381
+ return mutation;
382
+ },
383
+
269
384
  // Undo/Redo
270
385
  undo: (modelId) => {
271
386
  const state = get();
@@ -303,6 +418,29 @@ export const createMutationSlice: StateCreator<
303
418
  true // skipHistory
304
419
  );
305
420
  }
421
+ } else if (mutation.type === 'CREATE_QUANTITY') {
422
+ // Undo creation: remove the quantity mutation
423
+ view.removeQuantityMutation(mutation.entityId, mutation.psetName!, mutation.propName);
424
+ } else if (mutation.type === 'UPDATE_QUANTITY') {
425
+ if (mutation.psetName && mutation.propName && mutation.oldValue !== undefined && mutation.oldValue !== null) {
426
+ view.setQuantity(
427
+ mutation.entityId,
428
+ mutation.psetName,
429
+ mutation.propName,
430
+ Number(mutation.oldValue),
431
+ undefined,
432
+ undefined,
433
+ true // skipHistory
434
+ );
435
+ }
436
+ } else if (mutation.type === 'UPDATE_ATTRIBUTE') {
437
+ if (mutation.attributeName) {
438
+ if (mutation.oldValue !== undefined && mutation.oldValue !== null) {
439
+ view.setAttribute(mutation.entityId, mutation.attributeName, String(mutation.oldValue), undefined, true);
440
+ } else {
441
+ view.removeAttributeMutation(mutation.entityId, mutation.attributeName);
442
+ }
443
+ }
306
444
  }
307
445
 
308
446
  set((s) => {
@@ -347,6 +485,22 @@ export const createMutationSlice: StateCreator<
347
485
  if (mutation.psetName && mutation.propName) {
348
486
  view.deleteProperty(mutation.entityId, mutation.psetName, mutation.propName, true);
349
487
  }
488
+ } else if (mutation.type === 'CREATE_QUANTITY' || mutation.type === 'UPDATE_QUANTITY') {
489
+ if (mutation.psetName && mutation.propName && mutation.newValue !== undefined) {
490
+ view.setQuantity(
491
+ mutation.entityId,
492
+ mutation.psetName,
493
+ mutation.propName,
494
+ Number(mutation.newValue),
495
+ undefined,
496
+ undefined,
497
+ true // skipHistory
498
+ );
499
+ }
500
+ } else if (mutation.type === 'UPDATE_ATTRIBUTE') {
501
+ if (mutation.attributeName && mutation.newValue !== undefined) {
502
+ view.setAttribute(mutation.entityId, mutation.attributeName, String(mutation.newValue), undefined, true);
503
+ }
350
504
  }
351
505
 
352
506
  set((s) => {
package/vite.config.ts CHANGED
@@ -201,6 +201,13 @@ export default defineConfig({
201
201
  fs: {
202
202
  allow: ['../..'],
203
203
  },
204
+ proxy: {
205
+ '/api/bsdd': {
206
+ target: 'https://api.bsdd.buildingsmart.org',
207
+ changeOrigin: true,
208
+ rewrite: (p) => p.replace(/^\/api\/bsdd/, ''),
209
+ },
210
+ },
204
211
  },
205
212
  build: {
206
213
  target: 'esnext',