@ifc-lite/viewer 1.14.2 → 1.14.4

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 (80) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/dist/assets/{Arrow.dom-CSgnLhN4.js → Arrow.dom-_vGzMMKs.js} +1 -1
  3. package/dist/assets/basketViewActivator-BZcoCL3V.js +1 -0
  4. package/dist/assets/{browser-qSKWrKQW.js → browser-Czmf34bo.js} +1 -1
  5. package/dist/assets/ifc-lite_bg-DyBKoGgk.wasm +0 -0
  6. package/dist/assets/index-CMQ_Dgkr.css +1 -0
  7. package/dist/assets/index-D7nEDctQ.js +229 -0
  8. package/dist/assets/{index-4Y4XaV8N.js → index-DX-Qf5fA.js} +72669 -61673
  9. package/dist/assets/{native-bridge-CSFDsEkg.js → native-bridge-DAOWftxE.js} +1 -1
  10. package/dist/assets/{wasm-bridge-Zf90ysEm.js → wasm-bridge-D7jYpn8a.js} +1 -1
  11. package/dist/index.html +2 -2
  12. package/package.json +21 -20
  13. package/src/App.tsx +17 -1
  14. package/src/components/viewer/BasketPresentationDock.tsx +8 -4
  15. package/src/components/viewer/ChatPanel.tsx +1402 -0
  16. package/src/components/viewer/CodeEditor.tsx +70 -4
  17. package/src/components/viewer/CommandPalette.tsx +1 -0
  18. package/src/components/viewer/HierarchyPanel.tsx +28 -13
  19. package/src/components/viewer/MainToolbar.tsx +113 -95
  20. package/src/components/viewer/ScriptPanel.tsx +351 -184
  21. package/src/components/viewer/UpgradePage.tsx +69 -0
  22. package/src/components/viewer/Viewport.tsx +23 -0
  23. package/src/components/viewer/chat/ChatMessage.tsx +144 -0
  24. package/src/components/viewer/chat/ExecutableCodeBlock.tsx +416 -0
  25. package/src/components/viewer/chat/ModelSelector.tsx +102 -0
  26. package/src/components/viewer/chat/renderTextContent.test.ts +23 -0
  27. package/src/components/viewer/chat/renderTextContent.ts +19 -0
  28. package/src/components/viewer/hierarchy/HierarchyNode.tsx +10 -3
  29. package/src/components/viewer/hierarchy/treeDataBuilder.test.ts +126 -0
  30. package/src/components/viewer/hierarchy/treeDataBuilder.ts +139 -38
  31. package/src/components/viewer/hierarchy/types.ts +6 -1
  32. package/src/components/viewer/hierarchy/useHierarchyTree.ts +27 -12
  33. package/src/hooks/useIfcCache.ts +1 -2
  34. package/src/hooks/useSandbox.ts +122 -6
  35. package/src/index.css +10 -0
  36. package/src/lib/attachments.ts +46 -0
  37. package/src/lib/llm/ClerkChatSync.tsx +74 -0
  38. package/src/lib/llm/clerk-auth.ts +62 -0
  39. package/src/lib/llm/code-extractor.ts +50 -0
  40. package/src/lib/llm/context-builder.test.ts +18 -0
  41. package/src/lib/llm/context-builder.ts +305 -0
  42. package/src/lib/llm/free-models.test.ts +118 -0
  43. package/src/lib/llm/message-capabilities.test.ts +131 -0
  44. package/src/lib/llm/message-capabilities.ts +94 -0
  45. package/src/lib/llm/models.ts +197 -0
  46. package/src/lib/llm/repair-loop.test.ts +91 -0
  47. package/src/lib/llm/repair-loop.ts +76 -0
  48. package/src/lib/llm/script-diagnostics.ts +445 -0
  49. package/src/lib/llm/script-edit-ops.test.ts +399 -0
  50. package/src/lib/llm/script-edit-ops.ts +954 -0
  51. package/src/lib/llm/script-preflight.test.ts +513 -0
  52. package/src/lib/llm/script-preflight.ts +990 -0
  53. package/src/lib/llm/script-preservation.test.ts +128 -0
  54. package/src/lib/llm/script-preservation.ts +152 -0
  55. package/src/lib/llm/stream-client.test.ts +97 -0
  56. package/src/lib/llm/stream-client.ts +410 -0
  57. package/src/lib/llm/system-prompt.test.ts +181 -0
  58. package/src/lib/llm/system-prompt.ts +665 -0
  59. package/src/lib/llm/types.ts +150 -0
  60. package/src/lib/scripts/templates/bim-globals.d.ts +226 -7
  61. package/src/lib/scripts/templates/create-building.ts +12 -12
  62. package/src/main.tsx +10 -1
  63. package/src/sdk/adapters/export-adapter.test.ts +24 -0
  64. package/src/sdk/adapters/export-adapter.ts +40 -16
  65. package/src/sdk/adapters/files-adapter.ts +39 -0
  66. package/src/sdk/adapters/model-compat.ts +1 -1
  67. package/src/sdk/adapters/mutate-adapter.ts +20 -6
  68. package/src/sdk/adapters/mutation-view.ts +112 -0
  69. package/src/sdk/adapters/query-adapter.ts +100 -4
  70. package/src/sdk/local-backend.ts +4 -0
  71. package/src/store/index.ts +15 -1
  72. package/src/store/slices/chatSlice.test.ts +325 -0
  73. package/src/store/slices/chatSlice.ts +468 -0
  74. package/src/store/slices/scriptSlice.test.ts +75 -0
  75. package/src/store/slices/scriptSlice.ts +256 -9
  76. package/src/vite-env.d.ts +10 -0
  77. package/vite.config.ts +21 -2
  78. package/dist/assets/ifc-lite_bg-BOvNXJA_.wasm +0 -0
  79. package/dist/assets/index-ByrFvN5A.css +0 -1
  80. package/dist/assets/index-CN7qDq7G.js +0 -216
@@ -6,7 +6,8 @@ import type { StoreApi } from './types.js';
6
6
  import type { EntityRef, EntityData, PropertySetData, QuantitySetData, ExportBackendMethods } from '@ifc-lite/sdk';
7
7
  import { EntityNode } from '@ifc-lite/query';
8
8
  import { StepExporter, type StepExportOptions } from '@ifc-lite/export';
9
- import { getModelForRef } from './model-compat.js';
9
+ import { getModelForRef, LEGACY_MODEL_ID } from './model-compat.js';
10
+ import { applyAttributeMutationsToEntityData, getMutationViewForModel } from './mutation-view.js';
10
11
 
11
12
  /** Options for CSV export */
12
13
  interface CsvOptions {
@@ -78,6 +79,28 @@ function normalizeRefs(raw: unknown[]): EntityRef[] {
78
79
  });
79
80
  }
80
81
 
82
+ export function resolveVisibilityFilterSets(
83
+ state: StoreApi['getState'] extends () => infer T ? T : never,
84
+ modelId: string,
85
+ selectedExpressIds: Set<number>,
86
+ entityCount: number,
87
+ ): { visibleOnly: boolean; hiddenEntityIds: Set<number>; isolatedEntityIds: Set<number> | null } {
88
+ const shouldLimitToSelection = selectedExpressIds.size < entityCount;
89
+ const isLegacyModel = state.models.size === 0 && (modelId === LEGACY_MODEL_ID || modelId === 'legacy');
90
+ const modelHidden = state.hiddenEntitiesByModel.get(modelId) ?? (isLegacyModel ? state.hiddenEntities : undefined);
91
+ const modelIsolated = state.isolatedEntitiesByModel.get(modelId) ?? (isLegacyModel ? state.isolatedEntities : null);
92
+
93
+ return {
94
+ visibleOnly: shouldLimitToSelection,
95
+ hiddenEntityIds: shouldLimitToSelection
96
+ ? new Set<number>()
97
+ : new Set<number>(modelHidden ?? []),
98
+ isolatedEntityIds: shouldLimitToSelection
99
+ ? selectedExpressIds
100
+ : modelIsolated,
101
+ };
102
+ }
103
+
81
104
  /**
82
105
  * Escape a CSV cell value — wrap in quotes if it contains the separator,
83
106
  * double-quotes, or newlines.
@@ -104,14 +127,14 @@ export function createExportAdapter(store: StoreApi): ExportBackendMethods {
104
127
  if (!model?.ifcDataStore) return null;
105
128
 
106
129
  const node = new EntityNode(model.ifcDataStore, ref.expressId);
107
- return {
130
+ return applyAttributeMutationsToEntityData(store, ref.modelId, ref.expressId, {
108
131
  ref,
109
132
  globalId: node.globalId,
110
133
  name: node.name,
111
134
  type: node.type,
112
135
  description: node.description,
113
136
  objectType: node.objectType,
114
- };
137
+ });
115
138
  }
116
139
 
117
140
  /** Resolve property sets for an entity */
@@ -317,19 +340,20 @@ export function createExportAdapter(store: StoreApi): ExportBackendMethods {
317
340
 
318
341
  const options = candidateOptions;
319
342
  const selectedExpressIds = new Set(refs.map(ref => ref.expressId));
320
- const modelHidden = state.hiddenEntitiesByModel.get(modelId);
321
- const modelIsolated = state.isolatedEntitiesByModel.get(modelId) ?? null;
322
-
323
- const shouldLimitToSelection = selectedExpressIds.size < model.ifcDataStore.entityCount;
324
- const visibleOnly = options.visibleOnly === true || shouldLimitToSelection;
325
- const hiddenEntityIds = shouldLimitToSelection
326
- ? new Set<number>()
327
- : new Set<number>(modelHidden ?? []);
328
- const isolatedEntityIds = shouldLimitToSelection
329
- ? selectedExpressIds
330
- : modelIsolated;
331
-
332
- const exporter = new StepExporter(model.ifcDataStore);
343
+ const visibilityFilters = resolveVisibilityFilterSets(
344
+ state,
345
+ modelId,
346
+ selectedExpressIds,
347
+ model.ifcDataStore.entityCount,
348
+ );
349
+ const visibleOnly = options.visibleOnly === true || visibilityFilters.visibleOnly;
350
+ const hiddenEntityIds = visibleOnly ? visibilityFilters.hiddenEntityIds : new Set<number>();
351
+ const isolatedEntityIds = visibleOnly ? visibilityFilters.isolatedEntityIds : null;
352
+
353
+ const exporter = new StepExporter(
354
+ model.ifcDataStore,
355
+ options.includeMutations === false ? undefined : getMutationViewForModel(store, modelId) ?? undefined,
356
+ );
333
357
  const exportOptions: StepExportOptions = {
334
358
  schema: options.schema ?? model.ifcDataStore.schemaVersion,
335
359
  includeGeometry: true,
@@ -0,0 +1,39 @@
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 { FileAttachmentInfo, FilesBackendMethods } from '@ifc-lite/sdk';
6
+ import { collectActiveFileAttachments } from '@/lib/attachments';
7
+ import type { StoreApi } from './types.js';
8
+
9
+ function getAttachments(store: StoreApi) {
10
+ const state = store.getState();
11
+ return collectActiveFileAttachments(state.chatMessages, state.chatAttachments);
12
+ }
13
+
14
+ export function createFilesAdapter(store: StoreApi): FilesBackendMethods {
15
+ return {
16
+ list(): FileAttachmentInfo[] {
17
+ return getAttachments(store).map((attachment) => ({
18
+ name: attachment.name,
19
+ type: attachment.type,
20
+ size: attachment.size,
21
+ rowCount: attachment.csvData?.length,
22
+ columns: attachment.csvColumns,
23
+ hasTextContent: typeof attachment.textContent === 'string',
24
+ }));
25
+ },
26
+ text(name: string): string | null {
27
+ const attachment = getAttachments(store).find((item) => item.name === name);
28
+ return attachment?.textContent ?? null;
29
+ },
30
+ csv(name: string): Record<string, string>[] | null {
31
+ const attachment = getAttachments(store).find((item) => item.name === name);
32
+ return attachment?.csvData ?? null;
33
+ },
34
+ csvColumns(name: string): string[] {
35
+ const attachment = getAttachments(store).find((item) => item.name === name);
36
+ return attachment?.csvColumns ?? [];
37
+ },
38
+ };
39
+ }
@@ -42,7 +42,7 @@ export function getModelForRef(state: ViewerState, modelId: string): ModelLike |
42
42
  if (model) return model;
43
43
 
44
44
  // Legacy single-model fallback
45
- if (modelId === LEGACY_MODEL_ID && state.models.size === 0 && state.ifcDataStore) {
45
+ if ((modelId === LEGACY_MODEL_ID || modelId === 'legacy') && state.models.size === 0 && state.ifcDataStore) {
46
46
  return buildLegacyModel(state.ifcDataStore);
47
47
  }
48
48
 
@@ -4,31 +4,45 @@
4
4
 
5
5
  import type { EntityRef, MutateBackendMethods } from '@ifc-lite/sdk';
6
6
  import type { StoreApi } from './types.js';
7
+ import { getOrCreateMutationView, normalizeMutationModelId } from './mutation-view.js';
7
8
 
8
9
  export function createMutateAdapter(store: StoreApi): MutateBackendMethods {
9
10
  return {
10
11
  setProperty(ref: EntityRef, psetName: string, propName: string, value: string | number | boolean) {
11
12
  const state = store.getState();
12
- state.setProperty?.(ref.modelId, ref.expressId, psetName, propName, value);
13
+ const normalizedModelId = normalizeMutationModelId(state, ref.modelId);
14
+ if (!getOrCreateMutationView(store, ref.modelId)) return undefined;
15
+ state.setProperty?.(normalizedModelId, ref.expressId, psetName, propName, value);
16
+ return undefined;
17
+ },
18
+ setAttribute(ref: EntityRef, attrName: string, value: string) {
19
+ const state = store.getState();
20
+ const normalizedModelId = normalizeMutationModelId(state, ref.modelId);
21
+ if (!getOrCreateMutationView(store, ref.modelId)) return undefined;
22
+ state.setAttribute?.(normalizedModelId, ref.expressId, attrName, value);
13
23
  return undefined;
14
24
  },
15
25
  deleteProperty(ref: EntityRef, psetName: string, propName: string) {
16
26
  const state = store.getState();
17
- state.deleteProperty?.(ref.modelId, ref.expressId, psetName, propName);
27
+ const normalizedModelId = normalizeMutationModelId(state, ref.modelId);
28
+ if (!getOrCreateMutationView(store, ref.modelId)) return undefined;
29
+ state.deleteProperty?.(normalizedModelId, ref.expressId, psetName, propName);
18
30
  return undefined;
19
31
  },
20
32
  undo(modelId: string) {
21
33
  const state = store.getState();
22
- if (state.canUndo?.(modelId)) {
23
- state.undo?.(modelId);
34
+ const normalizedModelId = normalizeMutationModelId(state, modelId);
35
+ if (state.canUndo?.(normalizedModelId)) {
36
+ state.undo?.(normalizedModelId);
24
37
  return true;
25
38
  }
26
39
  return false;
27
40
  },
28
41
  redo(modelId: string) {
29
42
  const state = store.getState();
30
- if (state.canRedo?.(modelId)) {
31
- state.redo?.(modelId);
43
+ const normalizedModelId = normalizeMutationModelId(state, modelId);
44
+ if (state.canRedo?.(normalizedModelId)) {
45
+ state.redo?.(normalizedModelId);
32
46
  return true;
33
47
  }
34
48
  return false;
@@ -0,0 +1,112 @@
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 { MutablePropertyView } from '@ifc-lite/mutations';
6
+ import { extractPropertiesOnDemand, extractQuantitiesOnDemand } from '@ifc-lite/parser';
7
+ import type { EntityAttributeData, EntityData } from '@ifc-lite/sdk';
8
+ import type { ViewerState } from '../../store/index.js';
9
+ import { getModelForRef, LEGACY_MODEL_ID } from './model-compat.js';
10
+ import type { StoreApi } from './types.js';
11
+
12
+ export const LEGACY_MUTATION_MODEL_ID = '__legacy__';
13
+
14
+ function isLegacyMutationRef(state: ViewerState, modelId: string): boolean {
15
+ return state.models.size === 0 && (modelId === 'legacy' || modelId === LEGACY_MODEL_ID || modelId === LEGACY_MUTATION_MODEL_ID);
16
+ }
17
+
18
+ export function normalizeMutationModelId(state: ViewerState, modelId: string): string {
19
+ if (isLegacyMutationRef(state, modelId)) {
20
+ return LEGACY_MUTATION_MODEL_ID;
21
+ }
22
+ return modelId;
23
+ }
24
+
25
+ export function getMutationViewForModel(store: StoreApi, modelId: string): MutablePropertyView | null {
26
+ const state = store.getState();
27
+ return state.getMutationView?.(normalizeMutationModelId(state, modelId)) ?? null;
28
+ }
29
+
30
+ export function getOrCreateMutationView(store: StoreApi, modelId: string): MutablePropertyView | null {
31
+ const state = store.getState();
32
+ const normalizedModelId = normalizeMutationModelId(state, modelId);
33
+ const existing = state.getMutationView?.(normalizedModelId);
34
+ if (existing) return existing;
35
+
36
+ const modelRefId = isLegacyMutationRef(state, modelId) ? LEGACY_MODEL_ID : modelId;
37
+ const model = getModelForRef(state, modelRefId);
38
+ const dataStore = model?.ifcDataStore;
39
+ if (!dataStore) return null;
40
+
41
+ const mutationView = new MutablePropertyView(dataStore.properties || null, normalizedModelId);
42
+
43
+ if (dataStore.onDemandPropertyMap && dataStore.source?.length) {
44
+ mutationView.setOnDemandExtractor((entityId: number) => (
45
+ extractPropertiesOnDemand(dataStore, entityId)
46
+ ));
47
+ }
48
+
49
+ if (dataStore.onDemandQuantityMap && dataStore.source?.length) {
50
+ mutationView.setQuantityExtractor((entityId: number) => (
51
+ extractQuantitiesOnDemand(dataStore, entityId)
52
+ ));
53
+ }
54
+
55
+ state.registerMutationView?.(normalizedModelId, mutationView);
56
+ return mutationView;
57
+ }
58
+
59
+ export function applyAttributeMutationsToEntityData(
60
+ store: StoreApi,
61
+ modelId: string,
62
+ expressId: number,
63
+ data: EntityData,
64
+ ): EntityData {
65
+ const mutationView = getMutationViewForModel(store, modelId);
66
+ if (!mutationView) return data;
67
+
68
+ const mutations = mutationView.getAttributeMutationsForEntity(expressId);
69
+ if (mutations.length === 0) return data;
70
+
71
+ const next = { ...data };
72
+ for (const mutation of mutations) {
73
+ switch (mutation.name) {
74
+ case 'GlobalId':
75
+ next.globalId = mutation.value;
76
+ break;
77
+ case 'Name':
78
+ next.name = mutation.value;
79
+ break;
80
+ case 'Description':
81
+ next.description = mutation.value;
82
+ break;
83
+ case 'ObjectType':
84
+ next.objectType = mutation.value;
85
+ break;
86
+ }
87
+ }
88
+ return next;
89
+ }
90
+
91
+ export function mergeAttributeMutations(
92
+ baseAttributes: EntityAttributeData[],
93
+ store: StoreApi,
94
+ modelId: string,
95
+ expressId: number,
96
+ ): EntityAttributeData[] {
97
+ const mutationView = getMutationViewForModel(store, modelId);
98
+ if (!mutationView) return baseAttributes;
99
+
100
+ const mutations = mutationView.getAttributeMutationsForEntity(expressId);
101
+ if (mutations.length === 0) return baseAttributes;
102
+
103
+ const merged = new Map<string, string>();
104
+ for (const attr of baseAttributes) {
105
+ merged.set(attr.name, attr.value);
106
+ }
107
+ for (const mutation of mutations) {
108
+ merged.set(mutation.name, mutation.value);
109
+ }
110
+
111
+ return [...merged.entries()].map(([name, value]) => ({ name, value }));
112
+ }
@@ -5,8 +5,14 @@
5
5
  import type {
6
6
  EntityRef,
7
7
  EntityData,
8
+ EntityAttributeData,
8
9
  PropertySetData,
9
10
  QuantitySetData,
11
+ ClassificationData,
12
+ MaterialData,
13
+ TypePropertiesData,
14
+ DocumentData,
15
+ EntityRelationshipsData,
10
16
  QueryDescriptor,
11
17
  QueryBackendMethods,
12
18
  } from '@ifc-lite/sdk';
@@ -14,6 +20,15 @@ import type { StoreApi } from './types.js';
14
20
  import { EntityNode } from '@ifc-lite/query';
15
21
  import { RelationshipType, IfcTypeEnum, IfcTypeEnumFromString } from '@ifc-lite/data';
16
22
  import { getModelForRef, getAllModelEntries } from './model-compat.js';
23
+ import {
24
+ extractAllEntityAttributes,
25
+ extractClassificationsOnDemand,
26
+ extractMaterialsOnDemand,
27
+ extractTypePropertiesOnDemand,
28
+ extractDocumentsOnDemand,
29
+ extractRelationshipsOnDemand,
30
+ } from '@ifc-lite/parser';
31
+ import { applyAttributeMutationsToEntityData, mergeAttributeMutations } from './mutation-view.js';
17
32
 
18
33
  /** Map IFC relationship entity names to internal RelationshipType enum.
19
34
  * Keys use proper IFC schema names (e.g. IfcRelAggregates, not "Aggregates"). */
@@ -86,6 +101,18 @@ function isProductType(type: string): boolean {
86
101
  return true;
87
102
  }
88
103
 
104
+ function normalizePropertyValue(value: unknown): string | number | boolean | null {
105
+ if (value == null) return null;
106
+ if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
107
+ return value;
108
+ }
109
+ try {
110
+ return JSON.stringify(value);
111
+ } catch {
112
+ return String(value);
113
+ }
114
+ }
115
+
89
116
  export function createQueryAdapter(store: StoreApi): QueryBackendMethods {
90
117
  function getEntityData(ref: EntityRef): EntityData | null {
91
118
  const state = store.getState();
@@ -93,14 +120,14 @@ export function createQueryAdapter(store: StoreApi): QueryBackendMethods {
93
120
  if (!model?.ifcDataStore) return null;
94
121
 
95
122
  const node = new EntityNode(model.ifcDataStore, ref.expressId);
96
- return {
123
+ return applyAttributeMutationsToEntityData(store, ref.modelId, ref.expressId, {
97
124
  ref,
98
125
  globalId: node.globalId,
99
126
  name: node.name,
100
127
  type: node.type,
101
128
  description: node.description,
102
129
  objectType: node.objectType,
103
- };
130
+ });
104
131
  }
105
132
 
106
133
  function getProperties(ref: EntityRef): PropertySetData[] {
@@ -120,6 +147,18 @@ export function createQueryAdapter(store: StoreApi): QueryBackendMethods {
120
147
  }));
121
148
  }
122
149
 
150
+ function getAttributes(ref: EntityRef): EntityAttributeData[] {
151
+ const state = store.getState();
152
+ const model = getModelForRef(state, ref.modelId);
153
+ if (!model?.ifcDataStore) return [];
154
+ return mergeAttributeMutations(
155
+ extractAllEntityAttributes(model.ifcDataStore, ref.expressId),
156
+ store,
157
+ ref.modelId,
158
+ ref.expressId,
159
+ );
160
+ }
161
+
123
162
  function getQuantities(ref: EntityRef): QuantitySetData[] {
124
163
  const state = store.getState();
125
164
  const model = getModelForRef(state, ref.modelId);
@@ -136,6 +175,57 @@ export function createQueryAdapter(store: StoreApi): QueryBackendMethods {
136
175
  }));
137
176
  }
138
177
 
178
+ function getClassifications(ref: EntityRef): ClassificationData[] {
179
+ const state = store.getState();
180
+ const model = getModelForRef(state, ref.modelId);
181
+ if (!model?.ifcDataStore) return [];
182
+ return extractClassificationsOnDemand(model.ifcDataStore, ref.expressId);
183
+ }
184
+
185
+ function getMaterials(ref: EntityRef): MaterialData | null {
186
+ const state = store.getState();
187
+ const model = getModelForRef(state, ref.modelId);
188
+ if (!model?.ifcDataStore) return null;
189
+ return extractMaterialsOnDemand(model.ifcDataStore, ref.expressId);
190
+ }
191
+
192
+ function getTypeProperties(ref: EntityRef): TypePropertiesData | null {
193
+ const state = store.getState();
194
+ const model = getModelForRef(state, ref.modelId);
195
+ if (!model?.ifcDataStore) return null;
196
+ const info = extractTypePropertiesOnDemand(model.ifcDataStore, ref.expressId);
197
+ if (!info) return null;
198
+ return {
199
+ typeName: info.typeName,
200
+ typeId: info.typeId,
201
+ properties: info.properties.map((pset) => ({
202
+ name: pset.name,
203
+ globalId: pset.globalId,
204
+ properties: pset.properties.map((prop) => ({
205
+ name: prop.name,
206
+ type: prop.type,
207
+ value: normalizePropertyValue(prop.value),
208
+ })),
209
+ })),
210
+ };
211
+ }
212
+
213
+ function getDocuments(ref: EntityRef): DocumentData[] {
214
+ const state = store.getState();
215
+ const model = getModelForRef(state, ref.modelId);
216
+ if (!model?.ifcDataStore) return [];
217
+ return extractDocumentsOnDemand(model.ifcDataStore, ref.expressId);
218
+ }
219
+
220
+ function getRelationships(ref: EntityRef): EntityRelationshipsData {
221
+ const state = store.getState();
222
+ const model = getModelForRef(state, ref.modelId);
223
+ if (!model?.ifcDataStore) {
224
+ return { voids: [], fills: [], groups: [], connections: [] };
225
+ }
226
+ return extractRelationshipsOnDemand(model.ifcDataStore, ref.expressId);
227
+ }
228
+
139
229
  function queryEntities(descriptor: QueryDescriptor): EntityData[] {
140
230
  const state = store.getState();
141
231
  const results: EntityData[] = [];
@@ -167,14 +257,14 @@ export function createQueryAdapter(store: StoreApi): QueryBackendMethods {
167
257
  for (const expressId of entityIds) {
168
258
  if (expressId === 0) continue;
169
259
  const node = new EntityNode(model.ifcDataStore, expressId);
170
- results.push({
260
+ results.push(applyAttributeMutationsToEntityData(store, modelId, expressId, {
171
261
  ref: { modelId, expressId },
172
262
  globalId: node.globalId,
173
263
  name: node.name,
174
264
  type: node.type,
175
265
  description: node.description,
176
266
  objectType: node.objectType,
177
- });
267
+ }));
178
268
  }
179
269
  }
180
270
 
@@ -226,8 +316,14 @@ export function createQueryAdapter(store: StoreApi): QueryBackendMethods {
226
316
  return {
227
317
  entities: queryEntities,
228
318
  entityData: getEntityData,
319
+ attributes: getAttributes,
229
320
  properties: getProperties,
230
321
  quantities: getQuantities,
322
+ classifications: getClassifications,
323
+ materials: getMaterials,
324
+ typeProperties: getTypeProperties,
325
+ documents: getDocuments,
326
+ relationships: getRelationships,
231
327
  related(ref: EntityRef, relType: string, direction: 'forward' | 'inverse') {
232
328
  const state = store.getState();
233
329
  const model = getModelForRef(state, ref.modelId);
@@ -21,6 +21,7 @@ import type {
21
21
  SpatialBackendMethods,
22
22
  ExportBackendMethods,
23
23
  LensBackendMethods,
24
+ FilesBackendMethods,
24
25
  } from '@ifc-lite/sdk';
25
26
  import type { StoreApi } from './adapters/types.js';
26
27
  import { LEGACY_MODEL_ID } from './adapters/model-compat.js';
@@ -33,6 +34,7 @@ import { createMutateAdapter } from './adapters/mutate-adapter.js';
33
34
  import { createSpatialAdapter } from './adapters/spatial-adapter.js';
34
35
  import { createLensAdapter } from './adapters/lens-adapter.js';
35
36
  import { createExportAdapter } from './adapters/export-adapter.js';
37
+ import { createFilesAdapter } from './adapters/files-adapter.js';
36
38
 
37
39
  export class LocalBackend implements BimBackend {
38
40
  readonly model: ModelBackendMethods;
@@ -44,6 +46,7 @@ export class LocalBackend implements BimBackend {
44
46
  readonly spatial: SpatialBackendMethods;
45
47
  readonly export: ExportBackendMethods;
46
48
  readonly lens: LensBackendMethods;
49
+ readonly files: FilesBackendMethods;
47
50
 
48
51
  private store: StoreApi;
49
52
 
@@ -58,6 +61,7 @@ export class LocalBackend implements BimBackend {
58
61
  this.spatial = createSpatialAdapter(store);
59
62
  this.lens = createLensAdapter(store);
60
63
  this.export = createExportAdapter(store);
64
+ this.files = createFilesAdapter(store);
61
65
  }
62
66
 
63
67
  subscribe(event: BimEventType, handler: (data: unknown) => void): () => void {
@@ -31,6 +31,7 @@ import { createListSlice, type ListSlice } from './slices/listSlice.js';
31
31
  import { createPinboardSlice, type PinboardSlice } from './slices/pinboardSlice.js';
32
32
  import { createLensSlice, type LensSlice } from './slices/lensSlice.js';
33
33
  import { createScriptSlice, type ScriptSlice } from './slices/scriptSlice.js';
34
+ import { createChatSlice, type ChatSlice } from './slices/chatSlice.js';
34
35
  import { invalidateVisibleBasketCache } from './basketVisibleSet.js';
35
36
 
36
37
  // Import constants for reset function
@@ -72,6 +73,9 @@ export type { LensSlice, Lens, LensRule, LensCriteria } from './slices/lensSlice
72
73
  // Re-export Script types
73
74
  export type { ScriptSlice } from './slices/scriptSlice.js';
74
75
 
76
+ // Re-export Chat types
77
+ export type { ChatSlice } from './slices/chatSlice.js';
78
+
75
79
  // Combined store type
76
80
  export type ViewerState = LoadingSlice &
77
81
  SelectionSlice &
@@ -91,7 +95,8 @@ export type ViewerState = LoadingSlice &
91
95
  ListSlice &
92
96
  PinboardSlice &
93
97
  LensSlice &
94
- ScriptSlice & {
98
+ ScriptSlice &
99
+ ChatSlice & {
95
100
  resetViewerState: () => void;
96
101
  };
97
102
 
@@ -119,6 +124,7 @@ export const useViewerStore = create<ViewerState>()((...args) => ({
119
124
  ...createPinboardSlice(...args),
120
125
  ...createLensSlice(...args),
121
126
  ...createScriptSlice(...args),
127
+ ...createChatSlice(...args),
122
128
 
123
129
  // Reset all viewer state when loading new file
124
130
  // Note: Does NOT clear models - use clearAllModels() for that
@@ -280,6 +286,8 @@ export const useViewerStore = create<ViewerState>()((...args) => ({
280
286
  scriptExecutionState: 'idle' as const,
281
287
  scriptLastResult: null,
282
288
  scriptLastError: null,
289
+ scriptLastDiagnostics: [],
290
+ scriptAssistantTurnSnapshot: null,
283
291
  scriptDeleteConfirmId: null,
284
292
 
285
293
  // Lens - deactivate but keep saved lenses
@@ -289,6 +297,12 @@ export const useViewerStore = create<ViewerState>()((...args) => ({
289
297
  lensHiddenIds: new Set<number>(),
290
298
  lensRuleCounts: new Map<string, number>(),
291
299
  lensRuleEntityIds: new Map<string, number[]>(),
300
+
301
+ // Chat - keep messages and panel visible, reset streaming state
302
+ chatStatus: 'idle' as const,
303
+ chatStreamingContent: '',
304
+ chatError: null,
305
+ chatAbortController: null,
292
306
  });
293
307
  },
294
308
  }));