@ifc-lite/cli 0.40.2 → 0.42.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 (75) hide show
  1. package/README.md +12 -1
  2. package/dist/commands/diff-engine.d.ts.map +1 -1
  3. package/dist/commands/diff-engine.js +9 -3
  4. package/dist/commands/diff-engine.js.map +1 -1
  5. package/dist/commands/doctor.d.ts +3 -0
  6. package/dist/commands/doctor.d.ts.map +1 -0
  7. package/dist/commands/doctor.js +24 -0
  8. package/dist/commands/doctor.js.map +1 -0
  9. package/dist/commands/export-rust-formats.d.ts.map +1 -1
  10. package/dist/commands/export-rust-formats.js +37 -41
  11. package/dist/commands/export-rust-formats.js.map +1 -1
  12. package/dist/commands/extract-entities.d.ts.map +1 -1
  13. package/dist/commands/extract-entities.js +7 -11
  14. package/dist/commands/extract-entities.js.map +1 -1
  15. package/dist/commands/flow-host.d.ts.map +1 -1
  16. package/dist/commands/flow-host.js +20 -0
  17. package/dist/commands/flow-host.js.map +1 -1
  18. package/dist/commands/flow.d.ts.map +1 -1
  19. package/dist/commands/flow.js +3 -2
  20. package/dist/commands/flow.js.map +1 -1
  21. package/dist/commands/gym.d.ts.map +1 -1
  22. package/dist/commands/gym.js +2 -10
  23. package/dist/commands/gym.js.map +1 -1
  24. package/dist/commands/mutate.d.ts +6 -0
  25. package/dist/commands/mutate.d.ts.map +1 -1
  26. package/dist/commands/mutate.js +16 -19
  27. package/dist/commands/mutate.js.map +1 -1
  28. package/dist/commands/mutation-base.d.ts +4 -0
  29. package/dist/commands/mutation-base.d.ts.map +1 -0
  30. package/dist/commands/mutation-base.js +11 -0
  31. package/dist/commands/mutation-base.js.map +1 -0
  32. package/dist/commands/step-args.d.ts +0 -119
  33. package/dist/commands/step-args.d.ts.map +1 -1
  34. package/dist/commands/step-args.js +54 -49
  35. package/dist/commands/step-args.js.map +1 -1
  36. package/dist/commands/storey-selection.d.ts.map +1 -1
  37. package/dist/commands/storey-selection.js +16 -9
  38. package/dist/commands/storey-selection.js.map +1 -1
  39. package/dist/commands/subset-relations.d.ts +0 -76
  40. package/dist/commands/subset-relations.d.ts.map +1 -1
  41. package/dist/commands/subset-relations.js +88 -24
  42. package/dist/commands/subset-relations.js.map +1 -1
  43. package/dist/commands/test-fixtures/type-mutation.d.ts +18 -0
  44. package/dist/commands/test-fixtures/type-mutation.d.ts.map +1 -0
  45. package/dist/commands/test-fixtures/type-mutation.js +45 -0
  46. package/dist/commands/test-fixtures/type-mutation.js.map +1 -0
  47. package/dist/commands/validate.d.ts.map +1 -1
  48. package/dist/commands/validate.js +6 -0
  49. package/dist/commands/validate.js.map +1 -1
  50. package/dist/diagnostics.d.ts +33 -0
  51. package/dist/diagnostics.d.ts.map +1 -0
  52. package/dist/diagnostics.js +113 -0
  53. package/dist/diagnostics.js.map +1 -0
  54. package/dist/headless-backend.d.ts +30 -75
  55. package/dist/headless-backend.d.ts.map +1 -1
  56. package/dist/headless-backend.js +54 -534
  57. package/dist/headless-backend.js.map +1 -1
  58. package/dist/help.d.ts.map +1 -1
  59. package/dist/help.js +1 -0
  60. package/dist/help.js.map +1 -1
  61. package/dist/index.js +51 -82
  62. package/dist/index.js.map +1 -1
  63. package/dist/loader.d.ts +7 -1
  64. package/dist/loader.d.ts.map +1 -1
  65. package/dist/loader.js +34 -50
  66. package/dist/loader.js.map +1 -1
  67. package/package.json +22 -21
  68. package/dist/query-overlay-relations.d.ts +0 -9
  69. package/dist/query-overlay-relations.d.ts.map +0 -1
  70. package/dist/query-overlay-relations.js +0 -78
  71. package/dist/query-overlay-relations.js.map +0 -1
  72. package/dist/query-overlay.d.ts +0 -46
  73. package/dist/query-overlay.d.ts.map +0 -1
  74. package/dist/query-overlay.js +0 -141
  75. package/dist/query-overlay.js.map +0 -1
@@ -1,50 +1,18 @@
1
1
  /* This Source Code Form is subject to the terms of the Mozilla Public
2
2
  * License, v. 2.0. If a copy of the MPL was not distributed with this
3
3
  * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
- import { createCostBackend, createEffectiveEntityCheck, createHeadlessMutateAdapter, resolveLiveOwnerHistoryId } from '@ifc-lite/sdk';
4
+ import { resolveLiveOwnerHistoryId } from '@ifc-lite/sdk';
5
+ import { HeadlessBackend as SharedHeadlessBackend, createQueryAdapter } from '@ifc-lite/sdk/headless';
6
+ import { expandTypes, isQueryableObjectType } from '@ifc-lite/parser';
7
+ import { normalizeBooleanValue } from '@ifc-lite/query';
8
+ import { addDoorToStore, addWindowToStore, resolveSpatialAnchor, generateSpaces, listStoreys } from '@ifc-lite/create';
5
9
  import { createStoreAuthoring } from './headless-backend-store-authoring.js';
6
- import { MutablePropertyView, StoreEditor, storeHasSourceEntity } from '@ifc-lite/mutations';
7
- import { addDoorToStore, applyStylesInStore, addWindowToStore, resolveSpatialAnchor, generateSpaces, listStoreys, } from '@ifc-lite/create';
8
- import { EntityNode, findPropertyInSets, findQuantityInSets, normalizeBooleanValue, matchesPropertyFilter } from '@ifc-lite/query';
9
- import { extractAllEntityAttributes, extractClassificationsOnDemand, extractMaterialsOnDemand, extractPropertiesOnDemand, extractQuantitiesOnDemand, extractTypePropertiesOnDemand, extractDocumentsOnDemand, extractRelationshipsOnDemand, extractExactRelatedIds, expandTypes, QUERY_REL_TYPE_MAP, isQueryableObjectType, } from '@ifc-lite/parser';
10
- import { escapeCsvCell, exportToStep, StepExporter } from '@ifc-lite/export';
11
10
  import { createStructuralAdapter } from './headless-backend-structural.js';
12
11
  import { createScheduleAdapter } from './headless-backend-schedule.js';
13
12
  import { exportHbjson, exportDfjson } from './energy-export.js';
14
- import { foldQueuedRelated, foldQueuedRelationshipData, supersededRelationshipIds } from './query-overlay-relations.js';
15
- import { applyParsedEntityOverrides, overlayEntityData, overlayProperties, overlayQuantities } from './query-overlay.js';
16
- import { iterateEffectiveEntityIds } from '@ifc-lite/mutations';
17
- // `expandTypes` used to be defined here; it now comes from `@ifc-lite/parser`,
18
- // shared with the other query backends (see `query-backend-maps.ts`). Re-exported
19
- // so this module's consumers are unaffected by where it lives.
20
- export { expandTypes };
21
- const MODEL_ID = 'default';
22
- /**
23
- * Strip a real IFC source-file extension from a filename-derived model name.
24
- * Only applied to the `modelName` fallback: an explicitly supplied export name
25
- * is a display name where a trailing dotted segment is meaningful (`Tower.v2`).
26
- */
27
- function stripIfcExtension(name) {
28
- return name.replace(/\.(ifc|ifcx|ifczip)$/i, '');
29
- }
30
- /**
31
- * Which classes an unfiltered query answers with.
32
- *
33
- * Thin alias: the predicate is schema logic and lives in `@ifc-lite/parser`, so
34
- * the CLI and MCP backends cannot drift apart on it. Kept as a named export
35
- * here because both packages already publish it under this name.
36
- */
13
+ export { expandTypes, normalizeBooleanValue };
37
14
  export const isProductType = isQueryableObjectType;
38
- /**
39
- * Normalize boolean-like values for comparison.
40
- * IFC STEP files store booleans as .T./.F., but users pass true/false.
41
- *
42
- * Re-exported from `@ifc-lite/query` for existing importers of this module;
43
- * the actual comparison the query filter runs lives there too
44
- * (`compareFilterValue`), shared with the viewer and MCP `QueryBackendMethods`
45
- * implementations so the three can't drift apart on `where()` semantics again.
46
- */
47
- export { normalizeBooleanValue };
15
+ const MODEL_ID = 'default';
48
16
  export function normalizePropertyValue(value) {
49
17
  if (value == null)
50
18
  return null;
@@ -63,13 +31,12 @@ export function normalizePropertyValue(value) {
63
31
  }
64
32
  }
65
33
  export class HeadlessBackend {
34
+ dataStore;
66
35
  model;
67
36
  query;
68
37
  selection;
69
- visibility;
70
- viewer;
71
38
  mutate;
72
- store;
39
+ style;
73
40
  spatial;
74
41
  export;
75
42
  lens;
@@ -77,70 +44,57 @@ export class HeadlessBackend {
77
44
  schedule;
78
45
  structural;
79
46
  cost;
47
+ store;
80
48
  spaces;
81
- style;
82
- dataStore;
83
- modelName;
84
- /** Every spelling of the one model this backend answers for, written once:
85
- * the schedule assert, the `bim.mutate.*` guard and the `bim.store.add*`
86
- * ref-minting sites all have to give the SAME answer, or a ref is minted
87
- * under an id the very next write refuses (#3764). */
88
- acceptedModelIds;
89
- mutationView = null;
90
- storeEditor = null;
91
- constructor(store, modelName) {
92
- this.dataStore = store;
93
- this.modelName = modelName;
94
- this.acceptedModelIds = modelName === MODEL_ID ? [MODEL_ID] : [MODEL_ID, modelName];
95
- this.model = this.createModelAdapter();
96
- this.query = this.createQueryAdapter();
97
- this.selection = this.createSelectionAdapter();
98
- this.visibility = this.createVisibilityAdapter();
99
- this.viewer = this.createViewerAdapter();
100
- this.mutate = this.createMutateAdapter();
101
- this.store = this.createStoreAdapter();
102
- this.spatial = this.createSpatialAdapter();
103
- this.export = this.createExportAdapter();
104
- this.lens = this.createLensAdapter();
105
- this.files = this.createFilesAdapter();
106
- this.schedule = createScheduleAdapter(this.dataStore, modelId => this.assertKnownModelId(modelId));
107
- this.structural = createStructuralAdapter(this.dataStore, modelId => this.assertKnownModelId(modelId));
108
- this.cost = createCostBackend(modelId => {
109
- if (modelId)
110
- this.assertKnownModelId(modelId);
111
- // Lazily created overlay (#4857): a `bim.store.addCost*` authored
112
- // entity, or any pending edit, is only visible to `bim.cost` once this
113
- // is non-null, and it stays null until the first mutating call.
114
- return { modelId: MODEL_ID, store: this.dataStore, mutationView: this.mutationView ?? undefined };
49
+ viewer;
50
+ visibility;
51
+ shared;
52
+ constructor(dataStore, modelName) {
53
+ this.dataStore = dataStore;
54
+ this.shared = new SharedHeadlessBackend(dataStore, modelName, MODEL_ID, {}, modelName === MODEL_ID ? [] : [modelName]);
55
+ this.model = this.shared.model;
56
+ this.selection = this.shared.selection;
57
+ this.mutate = this.shared.mutate;
58
+ this.style = this.shared.style;
59
+ this.spatial = this.shared.spatial;
60
+ this.lens = this.shared.lens;
61
+ this.files = this.shared.files;
62
+ this.cost = this.shared.cost;
63
+ this.viewer = this.shared.viewer;
64
+ this.visibility = this.shared.visibility;
65
+ this.query = createQueryAdapter(dataStore, MODEL_ID, () => this.shared.pendingOverlay(), () => this.getMutationView(), {
66
+ paginationError: TypeError,
67
+ normalizeTypePropertyValue: normalizePropertyValue,
115
68
  });
69
+ this.store = this.createStoreAdapter();
70
+ this.schedule = createScheduleAdapter(dataStore, id => this.assertKnownModelId(id));
71
+ this.structural = createStructuralAdapter(dataStore, id => this.assertKnownModelId(id));
116
72
  this.spaces = this.createSpacesAdapter();
117
- this.style = this.createStyleAdapter();
118
- }
119
- /** Whether `modelId` names the one model this backend holds. */
120
- acceptsModelId(modelId) {
121
- return this.acceptedModelIds.includes(modelId);
73
+ // Explicit display names retain dots; only the filename-derived fallback loses its IFC suffix.
74
+ const fallbackName = modelName.replace(/\.(ifc|ifcx|ifczip)$/i, '');
75
+ this.export = {
76
+ ...this.shared.export,
77
+ hbjson: name => exportHbjson(dataStore, this.mutationView, name ?? fallbackName),
78
+ dfjson: name => exportDfjson(dataStore, this.mutationView, name ?? fallbackName),
79
+ };
122
80
  }
123
- /** The accepted spellings as an error-message clause: `'default' or 'a.ifc'`. */
124
- acceptedModelIdList() {
125
- return this.acceptedModelIds.map(id => `'${id}'`).join(' or ');
81
+ get mutationView() { return this.shared.getMutationView(); }
82
+ getMutationView() { return this.shared.getMutationView(); }
83
+ getOrCreateMutationView() { return this.shared.getOrCreateMutationView(); }
84
+ getOrCreateStoreEditor() { return this.shared.ensureEditor(); }
85
+ assertKnownModelId(modelId) { this.shared.assertKnownModelId(modelId); }
86
+ tableAccess(modelId) {
87
+ if (modelId)
88
+ this.assertKnownModelId(modelId);
89
+ return { entities: this.dataStore.entities, mutationView: this.getOrCreateMutationView(), strings: this.dataStore.strings ?? null };
126
90
  }
127
- /** Refuse an unknown model id loudly, at whichever surface was handed it. */
128
- assertKnownModelId(modelId) {
129
- if (this.acceptsModelId(modelId))
130
- return;
131
- throw new Error(`Unknown modelId '${modelId}': this backend answers for ${this.acceptedModelIdList()}`);
91
+ subscribe(event, handler) {
92
+ return this.shared.subscribe(event, handler);
132
93
  }
133
- createStyleAdapter() {
134
- return {
135
- // Same arrangement as the spaces adapter: the work happens in
136
- // @ifc-lite/create against the shared StoreEditor, so the new entities
137
- // land in the overlay this backend's export adapter already reads.
138
- applyColors: (batches, options) => applyStylesInStore(this.getOrCreateStoreEditor(), this.dataStore, batches.map(batch => ({
139
- products: batch.refs.map(r => r.expressId),
140
- color: batch.color,
141
- name: batch.name,
142
- })), options),
143
- };
94
+ attachStreamingAdapters(viewer, visibility) {
95
+ this.shared.attachStreamingAdapters(viewer, visibility);
96
+ this.viewer = viewer;
97
+ this.visibility = visibility;
144
98
  }
145
99
  createSpacesAdapter() {
146
100
  return {
@@ -155,304 +109,6 @@ export class HeadlessBackend {
155
109
  },
156
110
  };
157
111
  }
158
- subscribe(_event, _handler) {
159
- return () => { };
160
- }
161
- createModelAdapter() {
162
- const store = this.dataStore;
163
- const name = this.modelName;
164
- return {
165
- list() {
166
- return [{
167
- id: MODEL_ID,
168
- name,
169
- schema: store.schemaVersion,
170
- schemaVersion: store.schemaVersion,
171
- entityCount: store.entityCount,
172
- fileSize: store.fileSize,
173
- loadedAt: Date.now(),
174
- }];
175
- },
176
- activeId() { return MODEL_ID; },
177
- loadIfc() { },
178
- };
179
- }
180
- createQueryAdapter() {
181
- const store = this.dataStore;
182
- // Lazy — a read-only session's `related()` stays on the store-only path.
183
- const getMutationView = () => this.mutationView;
184
- function getEntityData(ref) {
185
- const overlay = overlayEntityData(getMutationView(), ref, store.schemaVersion);
186
- if (overlay !== undefined)
187
- return overlay;
188
- // @raw-entity-enumeration-ok overlayEntityData already handles deleted and created ids; this is source membership for one remaining ref
189
- if (!store.entityIndex.byId.has(ref.expressId))
190
- return null; // not parsed either
191
- const node = new EntityNode(store, ref.expressId);
192
- const type = node.type;
193
- if (!type || type === 'Unknown')
194
- return null;
195
- return applyParsedEntityOverrides(getMutationView(), ref.expressId, type, store.schemaVersion, {
196
- ref,
197
- globalId: node.globalId,
198
- name: node.name,
199
- type,
200
- description: node.description,
201
- objectType: node.objectType,
202
- });
203
- }
204
- function getProperties(ref) {
205
- const overlay = overlayProperties(getMutationView(), ref);
206
- if (overlay !== undefined)
207
- return overlay;
208
- const node = new EntityNode(store, ref.expressId);
209
- return node.properties().map((pset) => ({
210
- name: pset.name,
211
- globalId: pset.globalId,
212
- properties: pset.properties.map((p) => ({
213
- name: p.name,
214
- type: p.type,
215
- value: p.value,
216
- })),
217
- }));
218
- }
219
- function getQuantities(ref) {
220
- const overlay = overlayQuantities(getMutationView(), ref);
221
- if (overlay !== undefined)
222
- return overlay;
223
- const node = new EntityNode(store, ref.expressId);
224
- return node.quantities().map(qset => ({
225
- name: qset.name,
226
- quantities: qset.quantities.map(q => ({
227
- name: q.name,
228
- type: q.type,
229
- value: q.value,
230
- })),
231
- }));
232
- }
233
- return {
234
- entities(descriptor) {
235
- const results = [];
236
- const view = getMutationView();
237
- const types = descriptor.types && descriptor.types.length > 0
238
- ? expandTypes(descriptor.types, store.schemaVersion) : undefined;
239
- for (const { expressId, type, overlayCreated } of iterateEffectiveEntityIds(store, view, types)) {
240
- if (!types && !isProductType(type))
241
- continue;
242
- const ref = { modelId: MODEL_ID, expressId };
243
- if (overlayCreated) {
244
- const created = overlayEntityData(view, ref, store.schemaVersion);
245
- if (created)
246
- results.push(created);
247
- continue;
248
- }
249
- const node = new EntityNode(store, expressId);
250
- results.push({
251
- ref,
252
- globalId: node.globalId,
253
- name: node.name,
254
- type: view?.getEntityTypeMutation(expressId)?.newType ?? node.type,
255
- description: node.description,
256
- objectType: node.objectType,
257
- });
258
- }
259
- let filtered = results;
260
- if (descriptor.filters && descriptor.filters.length > 0) {
261
- const propsCache = new Map();
262
- const getCachedProps = (ref) => {
263
- let cached = propsCache.get(ref.expressId);
264
- if (!cached) {
265
- cached = getProperties(ref);
266
- propsCache.set(ref.expressId, cached);
267
- }
268
- return cached;
269
- };
270
- // A `Qto_` filter (or any psetName with no matching property set)
271
- // falls back to quantity sets — see `matchesPropertyFilter` in
272
- // `@ifc-lite/query`'s `property-filter-match.ts`. Cached the same
273
- // way as `propsCache`; only populated on the fallback path since
274
- // most filters resolve from properties alone.
275
- const qsetsCache = new Map();
276
- const getCachedQuantities = (ref) => {
277
- let cached = qsetsCache.get(ref.expressId);
278
- if (!cached) {
279
- cached = getQuantities(ref);
280
- qsetsCache.set(ref.expressId, cached);
281
- }
282
- return cached;
283
- };
284
- for (const filter of descriptor.filters) {
285
- filtered = filtered.filter(entity => {
286
- const props = getCachedProps(entity.ref);
287
- return matchesPropertyFilter(props, filter, getCachedQuantities(entity.ref));
288
- });
289
- }
290
- }
291
- // `!= null` alone lets a NaN offset/limit through (neither null nor
292
- // undefined); a bare `> 0` then silently drops it (every NaN
293
- // comparison is false) instead of rejecting it, and by the same
294
- // reasoning silently ignored a deliberate `limit: 0`. Reject
295
- // non-finite/negative values loudly instead of quietly serving the
296
- // wrong slice. The CLI's own `--limit`/`--offset` flags are validated
297
- // before reaching this descriptor, so this guards direct SDK callers
298
- // (`@ifc-lite/mcp`'s parallel `backend-query.ts` ported the identical
299
- // fix independently — same defect shape, own tests and release
300
- // cadence).
301
- if (descriptor.offset != null) {
302
- if (!Number.isFinite(descriptor.offset) || descriptor.offset < 0) {
303
- throw new TypeError(`Invalid offset: ${descriptor.offset} (must be a non-negative finite number)`);
304
- }
305
- if (descriptor.offset > 0)
306
- filtered = filtered.slice(descriptor.offset);
307
- }
308
- if (descriptor.limit != null) {
309
- if (!Number.isFinite(descriptor.limit) || descriptor.limit < 0) {
310
- throw new TypeError(`Invalid limit: ${descriptor.limit} (must be a non-negative finite number)`);
311
- }
312
- filtered = filtered.slice(0, descriptor.limit);
313
- }
314
- return filtered;
315
- },
316
- // Headless contexts have no interactive viewer filter, so there is never
317
- // an "active filter" to report (issue #1107).
318
- entitiesMatchingActiveFilter: () => null,
319
- entityData: getEntityData,
320
- attributes(ref) {
321
- return extractAllEntityAttributes(store, ref.expressId);
322
- },
323
- properties: getProperties,
324
- quantities: getQuantities,
325
- classifications(ref) {
326
- return extractClassificationsOnDemand(store, ref.expressId);
327
- },
328
- materials(ref) {
329
- return extractMaterialsOnDemand(store, ref.expressId);
330
- },
331
- typeProperties(ref) {
332
- const info = extractTypePropertiesOnDemand(store, ref.expressId);
333
- if (!info)
334
- return null;
335
- return {
336
- typeName: info.typeName,
337
- typeId: info.typeId,
338
- properties: info.properties.map((pset) => ({
339
- name: pset.name,
340
- globalId: pset.globalId,
341
- properties: pset.properties.map((prop) => ({
342
- name: prop.name,
343
- type: prop.type,
344
- value: normalizePropertyValue(prop.value),
345
- })),
346
- })),
347
- };
348
- },
349
- documents(ref) {
350
- return extractDocumentsOnDemand(store, ref.expressId);
351
- },
352
- relationships(ref) {
353
- const result = extractRelationshipsOnDemand(store, ref.expressId);
354
- const view = getMutationView();
355
- if (!view)
356
- return result;
357
- return foldQueuedRelationshipData(store, view, result, ref, getEntityData);
358
- },
359
- // Folds queued `IfcRel…` creates in (query-overlay-relations.ts, mirrors #2014).
360
- related(ref, relType, direction) {
361
- const relEnum = QUERY_REL_TYPE_MAP[relType];
362
- if (relEnum === undefined)
363
- return [];
364
- const view = getMutationView();
365
- if (view?.isDeleted(ref.expressId))
366
- return []; // deleted relates to nothing
367
- const out = [];
368
- const seen = new Set();
369
- const take = (id) => {
370
- if (view?.isDeleted(id) || seen.has(id))
371
- return;
372
- seen.add(id);
373
- out.push(id);
374
- };
375
- const superseded = view ? supersededRelationshipIds(store, view) : new Set();
376
- const isDeleted = view ? (id) => view.isDeleted(id) || superseded.has(id) : () => false;
377
- for (const id of extractExactRelatedIds(store, ref.expressId, relType, direction, isDeleted))
378
- take(id);
379
- if (view)
380
- for (const t of foldQueuedRelated(store, view, relType, direction, ref.expressId))
381
- take(t);
382
- return out.map((expressId) => ({ modelId: ref.modelId, expressId }));
383
- },
384
- };
385
- }
386
- createSelectionAdapter() {
387
- let selection = [];
388
- return {
389
- get() { return selection; },
390
- set(refs) { selection = refs; },
391
- };
392
- }
393
- createVisibilityAdapter() {
394
- return {
395
- hide() { },
396
- show() { },
397
- isolate() { },
398
- reset() { },
399
- };
400
- }
401
- createViewerAdapter() {
402
- return {
403
- colorize() { },
404
- colorizeAll() { },
405
- resetColors() { },
406
- flyTo() { },
407
- setSection() { },
408
- getSection() { return null; },
409
- setCamera() { },
410
- getCamera() { return { mode: 'perspective' }; },
411
- };
412
- }
413
- createMutateAdapter() {
414
- return createHeadlessMutateAdapter(() => this.getOrCreateMutationView(), createEffectiveEntityCheck({
415
- acceptedModelIds: this.acceptedModelIds,
416
- // Both halves of the source index (byId + deferred property atoms, #5222).
417
- hasSourceEntity: id => storeHasSourceEntity(this.dataStore, id),
418
- overlay: () => this.mutationView,
419
- }));
420
- }
421
- /**
422
- * The overlay every write goes through, created on first use by
423
- * `getOrCreateStoreEditor` so its extractors are wired exactly once.
424
- */
425
- getOrCreateMutationView() {
426
- this.getOrCreateStoreEditor();
427
- // Non-null immediately after: both fields are assigned together and never cleared.
428
- return this.mutationView;
429
- }
430
- /**
431
- * The bulk entity-table access `table.joinByKey` needs to reuse
432
- * `@ifc-lite/mutations`' `csv-match.ts` tag/property index (#5167, #5230)
433
- * rather than re-implement it. Same lazily-created overlay as `bim.cost`.
434
- */
435
- tableAccess(modelId) {
436
- if (modelId)
437
- this.assertKnownModelId(modelId);
438
- return { entities: this.dataStore.entities, mutationView: this.getOrCreateMutationView(), strings: this.dataStore.strings ?? null };
439
- }
440
- getOrCreateStoreEditor() {
441
- if (this.storeEditor)
442
- return this.storeEditor;
443
- this.mutationView = new MutablePropertyView(this.dataStore.properties || null, MODEL_ID);
444
- // Give the overlay a base to merge against — the columnar parser serves
445
- // properties on demand, so without this `getForEntity` would answer with
446
- // only the one edited pset and `StepExporter` would drop every sibling
447
- // property on save. Same wiring as `packages/mcp/src/headless-backend.ts`
448
- // and `apps/viewer/src/utils/configureMutationView.ts` (#2000, #2004).
449
- if (this.dataStore.source?.length > 0) {
450
- this.mutationView.setOnDemandExtractor((entityId) => extractPropertiesOnDemand(this.dataStore, entityId));
451
- this.mutationView.setQuantityExtractor((entityId) => extractQuantitiesOnDemand(this.dataStore, entityId));
452
- }
453
- this.storeEditor = new StoreEditor(this.dataStore, this.mutationView);
454
- return this.storeEditor;
455
- }
456
112
  createStoreAdapter() {
457
113
  const get = () => this.getOrCreateStoreEditor();
458
114
  const dataStore = () => this.dataStore;
@@ -500,141 +156,5 @@ export class HeadlessBackend {
500
156
  }),
501
157
  };
502
158
  }
503
- createSpatialAdapter() {
504
- return {
505
- queryBounds() { return []; },
506
- raycast() { return []; },
507
- queryFrustum() { return []; },
508
- };
509
- }
510
- createExportAdapter() {
511
- const store = this.dataStore;
512
- const modelName = this.modelName;
513
- const queryAdapter = this.query;
514
- /**
515
- * RFC 4180 quoting + the CWE-1236 formula-injection guard, delegated to
516
- * `@ifc-lite/export`'s single escaper. The copy that used to live here
517
- * tested the trigger anchored at offset 0, so a BOM/ZWSP/LRM/NBSP/U+2028
518
- * in front of `=` walked past it.
519
- */
520
- function escapeCsv(value, sep) {
521
- return escapeCsvCell(value, { delimiter: sep });
522
- }
523
- function resolveColumn(data, col, props, qsets) {
524
- if (col === 'Name' || col === 'name')
525
- return data.name;
526
- if (col === 'Type' || col === 'type')
527
- return data.type;
528
- if (col === 'GlobalId' || col === 'globalId')
529
- return data.globalId;
530
- if (col === 'Description' || col === 'description')
531
- return data.description;
532
- if (col === 'ObjectType' || col === 'objectType')
533
- return data.objectType;
534
- const dotIdx = col.indexOf('.');
535
- if (dotIdx > 0) {
536
- const setName = col.slice(0, dotIdx);
537
- const valueName = col.slice(dotIdx + 1);
538
- if (props) {
539
- const prop = findPropertyInSets(props, setName, valueName);
540
- if (prop?.value != null)
541
- return String(prop.value);
542
- }
543
- if (qsets) {
544
- const qty = findQuantityInSets(qsets, setName, valueName);
545
- if (qty?.value != null)
546
- return String(qty.value);
547
- }
548
- }
549
- return '';
550
- }
551
- return {
552
- csv(refs, options) {
553
- const entityRefs = refs;
554
- const opts = options;
555
- const columns = opts.columns;
556
- const sep = opts.separator ?? ',';
557
- const hasDotColumns = columns.some(c => c.indexOf('.') > 0);
558
- const rows = [columns];
559
- for (const ref of entityRefs) {
560
- const data = queryAdapter.entityData(ref);
561
- if (!data)
562
- continue;
563
- const props = hasDotColumns ? queryAdapter.properties(ref) : null;
564
- const qsets = hasDotColumns ? queryAdapter.quantities(ref) : null;
565
- rows.push(columns.map(col => resolveColumn(data, col, props, qsets)));
566
- }
567
- return rows.map(r => r.map(cell => escapeCsv(cell, sep)).join(sep)).join('\n');
568
- },
569
- json(refs, columns) {
570
- const entityRefs = refs;
571
- const cols = columns;
572
- const hasDotColumns = cols.some(c => c.indexOf('.') > 0);
573
- const result = [];
574
- for (const ref of entityRefs) {
575
- const data = queryAdapter.entityData(ref);
576
- if (!data)
577
- continue;
578
- const props = hasDotColumns ? queryAdapter.properties(ref) : null;
579
- const qsets = hasDotColumns ? queryAdapter.quantities(ref) : null;
580
- const row = {};
581
- for (const col of cols) {
582
- const val = resolveColumn(data, col, props, qsets);
583
- row[col] = val || null;
584
- }
585
- result.push(row);
586
- }
587
- return result;
588
- },
589
- ifc: (refs, options) => {
590
- const opts = (options ?? {});
591
- const schema = opts.schema ?? store.schemaVersion ?? 'IFC4';
592
- const exportOpts = { schema };
593
- // `undefined` is the only "no isolation filter": an empty list is a filter
594
- // that matched nothing, refused above in `ExportNamespace.ifc` (#4738).
595
- if (refs != null) {
596
- const isolatedIds = new Set(refs.map(r => r.expressId));
597
- exportOpts.visibleOnly = true;
598
- exportOpts.isolatedEntityIds = isolatedIds;
599
- exportOpts.hiddenEntityIds = new Set();
600
- }
601
- // Route through StepExporter directly so any bim.store.* / bim.mutate.*
602
- // overlay state on this backend's MutablePropertyView is included.
603
- if (this.mutationView) {
604
- const exporter = new StepExporter(store, this.mutationView);
605
- const result = exporter.export({ schema, ...exportOpts });
606
- return new TextDecoder().decode(result.content);
607
- }
608
- return exportToStep(store, exportOpts);
609
- },
610
- // Both energy formats apply the mutation view — see energy-export.ts
611
- // (issues #1908, #1344). An explicit `name` is a display/model name, so
612
- // it is kept verbatim (dotted identifiers like `Tower.v2` are valid);
613
- // only the `modelName` fallback is a filename, so only it gets a real
614
- // IFC extension stripped.
615
- hbjson: (name) => exportHbjson(store, this.mutationView, name ?? stripIfcExtension(modelName)),
616
- dfjson: (name) => exportDfjson(store, this.mutationView, name ?? stripIfcExtension(modelName)),
617
- download(_content, _filename, _mimeType) {
618
- /* no-op — CLI writes to stdout/file directly */
619
- },
620
- };
621
- }
622
- createLensAdapter() {
623
- return {
624
- presets() { return []; },
625
- create() { return null; },
626
- activate() { },
627
- deactivate() { },
628
- getActive() { return null; },
629
- };
630
- }
631
- createFilesAdapter() {
632
- return {
633
- list() { return []; },
634
- text() { return null; },
635
- csv() { return null; },
636
- csvColumns() { return []; },
637
- };
638
- }
639
159
  }
640
160
  //# sourceMappingURL=headless-backend.js.map