@ifc-lite/sandbox 1.14.2 → 1.14.3

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.
@@ -7,22 +7,42 @@ import { IfcCreator } from '@ifc-lite/sdk';
7
7
  // ============================================================================
8
8
  /** Simple registry for IfcCreator instances managed by the sandbox */
9
9
  const creatorRegistry = (() => {
10
- let nextHandle = 1;
11
- const creators = new Map();
10
+ const nextHandleBySession = new Map();
11
+ const creatorsBySession = new Map();
12
+ function getSessionCreators(sessionId) {
13
+ let sessionCreators = creatorsBySession.get(sessionId);
14
+ if (!sessionCreators) {
15
+ sessionCreators = new Map();
16
+ creatorsBySession.set(sessionId, sessionCreators);
17
+ }
18
+ return sessionCreators;
19
+ }
12
20
  return {
13
- register(creator) {
14
- const handle = nextHandle++;
15
- creators.set(handle, creator);
21
+ registerForSession(sessionId, creator) {
22
+ const handle = nextHandleBySession.get(sessionId) ?? 1;
23
+ nextHandleBySession.set(sessionId, handle + 1);
24
+ getSessionCreators(sessionId).set(handle, creator);
16
25
  return handle;
17
26
  },
18
- get(handle) {
19
- const creator = creators.get(handle);
27
+ getForSession(sessionId, handle) {
28
+ const creator = creatorsBySession.get(sessionId)?.get(handle);
20
29
  if (!creator)
21
30
  throw new Error(`Invalid creator handle: ${handle}`);
22
31
  return creator;
23
32
  },
24
- remove(handle) {
25
- creators.delete(handle);
33
+ removeForSession(sessionId, handle) {
34
+ const sessionCreators = creatorsBySession.get(sessionId);
35
+ if (!sessionCreators)
36
+ return;
37
+ sessionCreators.delete(handle);
38
+ if (sessionCreators.size === 0) {
39
+ creatorsBySession.delete(sessionId);
40
+ nextHandleBySession.delete(sessionId);
41
+ }
42
+ },
43
+ removeSession(sessionId) {
44
+ creatorsBySession.delete(sessionId);
45
+ nextHandleBySession.delete(sessionId);
26
46
  },
27
47
  };
28
48
  })();
@@ -62,6 +82,487 @@ function toRef(raw) {
62
82
  }
63
83
  return null;
64
84
  }
85
+ function mapNamedProperties(properties) {
86
+ return properties.map((property) => ({
87
+ name: property.name, Name: property.name,
88
+ value: property.value, Value: property.value,
89
+ NominalValue: property.value,
90
+ type: property.type, Type: property.type,
91
+ }));
92
+ }
93
+ /** Methods with non-standard signatures that need hand-written wiring */
94
+ const SPECIAL_METHODS = new Set([
95
+ 'constructor', 'toIfc', 'setColor',
96
+ ]);
97
+ /**
98
+ * Explicit allow-list of IfcCreator methods exposed in the sandbox.
99
+ * Only methods in this set (or in SPECIAL_METHODS/ELEMENT_METHODS/ZERO_ARG_METHODS)
100
+ * are wired. This prevents accidental exposure of private/internal helpers.
101
+ */
102
+ const ALLOWED_METHODS = new Set([
103
+ // Spatial structure
104
+ 'addIfcBuildingStorey',
105
+ // Building elements
106
+ 'addIfcWall', 'addIfcSlab', 'addIfcColumn', 'addIfcBeam',
107
+ 'addIfcStair', 'addIfcRoof', 'addIfcGableRoof', 'addIfcWallDoor', 'addIfcWallWindow', 'addIfcDoor', 'addIfcWindow',
108
+ 'addIfcRamp', 'addIfcRailing', 'addIfcPlate', 'addIfcMember',
109
+ 'addIfcFooting', 'addIfcPile', 'addIfcSpace', 'addIfcCurtainWall',
110
+ 'addIfcFurnishingElement', 'addIfcBuildingElementProxy',
111
+ // Specialized profiles
112
+ 'addIfcCircularColumn', 'addIfcIShapeBeam',
113
+ 'addIfcLShapeMember', 'addIfcTShapeMember', 'addIfcUShapeMember',
114
+ 'addIfcHollowCircularColumn', 'addIfcRectangleHollowBeam',
115
+ // Generic element creation
116
+ 'addElement', 'addAxisElement', 'createProfile',
117
+ // Properties and materials
118
+ 'addIfcPropertySet', 'addIfcElementQuantity', 'addIfcMaterial',
119
+ // Low-level geometry
120
+ 'getWorldPlacementId',
121
+ ]);
122
+ /** Methods that take (elementId, def) instead of (storeyId, params) */
123
+ const ELEMENT_METHODS = new Set([
124
+ 'addIfcWallDoor', 'addIfcWallWindow',
125
+ 'addIfcPropertySet', 'addIfcElementQuantity', 'addIfcMaterial',
126
+ ]);
127
+ /** Methods that take a single params object after the handle */
128
+ const SINGLE_DUMP_METHODS = new Set([
129
+ 'createProfile',
130
+ ]);
131
+ /** Methods with zero args (just need the handle) */
132
+ const ZERO_ARG_METHODS = new Set([
133
+ 'getWorldPlacementId',
134
+ ]);
135
+ function classifyMethod(name, _fn) {
136
+ if (SPECIAL_METHODS.has(name))
137
+ return 'special';
138
+ if (ZERO_ARG_METHODS.has(name))
139
+ return 'no-args';
140
+ if (SINGLE_DUMP_METHODS.has(name))
141
+ return 'single-dump';
142
+ if (ELEMENT_METHODS.has(name))
143
+ return 'element-params';
144
+ return 'storey-params';
145
+ }
146
+ /** Humanize a method name for the doc string */
147
+ function methodDoc(name) {
148
+ // addIfcWall → 'Add an IfcWall'
149
+ // addElement → 'Add a generic element'
150
+ // createProfile → 'Create a profile from a ProfileDef'
151
+ if (name === 'addIfcBuildingStorey')
152
+ return 'Add a building storey. Returns storey expressId.';
153
+ if (name === 'addIfcGableRoof')
154
+ return 'Add a dual-pitch gable roof. `Slope` is in radians. Returns roof expressId.';
155
+ if (name === 'addIfcWallDoor')
156
+ return 'Add a door hosted in a wall opening. Position is wall-local [alongWall, 0, baseHeight]. Returns door expressId.';
157
+ if (name === 'addIfcWallWindow')
158
+ return 'Add a window hosted in a wall opening. Position is wall-local [alongWall, 0, sillHeight]. Returns window expressId.';
159
+ if (name === 'addElement')
160
+ return 'Create ANY IFC type with a profile at a placement. Returns expressId.';
161
+ if (name === 'addAxisElement')
162
+ return 'Create ANY IFC type extruded along a Start→End axis. Returns expressId.';
163
+ if (name === 'createProfile')
164
+ return 'Create a profile from a ProfileDef union. Returns profile ID.';
165
+ if (name === 'getWorldPlacementId')
166
+ return 'Get the world placement ID for use with addLocalPlacement.';
167
+ if (name.startsWith('addIfc')) {
168
+ const entity = name.slice(3); // remove 'add', keep 'IfcWall' etc.
169
+ return `Add ${entity}. Returns expressId.`;
170
+ }
171
+ if (name.startsWith('add')) {
172
+ const what = name.slice(3);
173
+ return `Add ${what}. Returns ID.`;
174
+ }
175
+ return `Call ${name} on the creator.`;
176
+ }
177
+ const CREATE_METHOD_SEMANTICS = {
178
+ project: {
179
+ taskTags: ['create', 'repair'],
180
+ useWhen: 'Start a new generated IFC model before creating storeys and elements.',
181
+ },
182
+ toIfc: {
183
+ taskTags: ['create', 'export'],
184
+ useWhen: 'Finalize the in-memory IFC model and produce STEP content for preview or download.',
185
+ },
186
+ setColor: {
187
+ taskTags: ['visualize', 'repair'],
188
+ useWhen: 'Assign a named RGB color to a created element using [r, g, b] values between 0 and 1.',
189
+ },
190
+ addIfcBuildingStorey: {
191
+ taskTags: ['create', 'repair'],
192
+ requiredKeys: ['Elevation'],
193
+ useWhen: 'Create a building storey container before adding level-based geometry.',
194
+ },
195
+ addIfcWall: {
196
+ taskTags: ['create', 'repair'],
197
+ placement: 'storey-relative',
198
+ requiredKeys: ['Start', 'End', 'Thickness', 'Height'],
199
+ positiveKeys: ['Thickness', 'Height'],
200
+ pointArity: { Start: 3, End: 3 },
201
+ axisPair: ['Start', 'End'],
202
+ useWhen: 'Create a wall from a start/end axis on the current storey.',
203
+ },
204
+ addIfcSlab: {
205
+ taskTags: ['create', 'repair'],
206
+ placement: 'storey-relative',
207
+ requiredKeys: ['Position', 'Thickness'],
208
+ anyOfKeys: [['Profile'], ['Width', 'Depth']],
209
+ positiveKeys: ['Thickness', 'Width', 'Depth'],
210
+ pointArity: { Position: 3 },
211
+ customValidationId: 'slab-shape',
212
+ useWhen: 'Create a slab from a rectangular footprint or a 2D point-array profile.',
213
+ },
214
+ addIfcColumn: {
215
+ taskTags: ['create', 'repair'],
216
+ placement: 'storey-relative',
217
+ requiredKeys: ['Position', 'Width', 'Depth', 'Height'],
218
+ positiveKeys: ['Width', 'Depth', 'Height'],
219
+ pointArity: { Position: 3 },
220
+ useWhen: 'Create a vertical column from a base position and dimensions.',
221
+ },
222
+ addIfcBeam: {
223
+ taskTags: ['create', 'repair'],
224
+ placement: 'storey-relative',
225
+ requiredKeys: ['Start', 'End', 'Width', 'Height'],
226
+ positiveKeys: ['Width', 'Height'],
227
+ pointArity: { Start: 3, End: 3 },
228
+ axisPair: ['Start', 'End'],
229
+ useWhen: 'Create a beam from an axis on the current storey.',
230
+ },
231
+ addIfcMember: {
232
+ taskTags: ['create', 'repair'],
233
+ placement: 'world',
234
+ requiredKeys: ['Start', 'End', 'Width', 'Height'],
235
+ positiveKeys: ['Width', 'Height'],
236
+ pointArity: { Start: 3, End: 3 },
237
+ axisPair: ['Start', 'End'],
238
+ useWhen: 'Create mullions, braces, or facade members with explicit world coordinates.',
239
+ cautions: [
240
+ 'Inside storey loops, include the current storey elevation in Start/End Z for facade members.',
241
+ ],
242
+ },
243
+ addIfcPlate: {
244
+ taskTags: ['create', 'repair'],
245
+ placement: 'world',
246
+ requiredKeys: ['Position', 'Width', 'Depth', 'Thickness'],
247
+ positiveKeys: ['Width', 'Depth', 'Thickness'],
248
+ pointArity: { Position: 3 },
249
+ forbiddenKeys: [
250
+ { key: 'Height', message: '`bim.create.addIfcPlate(...)` uses `Depth` and `Thickness`, not `Height`.' },
251
+ { key: 'Start', message: '`bim.create.addIfcPlate(...)` uses `Position`, not `Start`/`End`.' },
252
+ { key: 'End', message: '`bim.create.addIfcPlate(...)` uses `Position`, not `Start`/`End`.' },
253
+ ],
254
+ useWhen: 'Create thin world-placement panels or facade plates from a base point.',
255
+ cautions: [
256
+ 'Facade plates repeated by storey usually need absolute Z = elevation + localOffset.',
257
+ ],
258
+ },
259
+ addIfcCurtainWall: {
260
+ taskTags: ['create', 'repair'],
261
+ placement: 'world',
262
+ requiredKeys: ['Start', 'End', 'Height'],
263
+ positiveKeys: ['Height', 'Thickness'],
264
+ pointArity: { Start: 3, End: 3 },
265
+ axisPair: ['Start', 'End'],
266
+ useWhen: 'Create a world-placement curtain wall segment between two points.',
267
+ cautions: [
268
+ 'Inside storey loops, include the current storey elevation in Start/End Z.',
269
+ ],
270
+ },
271
+ addIfcRailing: {
272
+ taskTags: ['create', 'repair'],
273
+ placement: 'world',
274
+ requiredKeys: ['Start', 'End', 'Height'],
275
+ positiveKeys: ['Height', 'Width'],
276
+ pointArity: { Start: 3, End: 3 },
277
+ axisPair: ['Start', 'End'],
278
+ useWhen: 'Create a world-placement railing along an axis.',
279
+ },
280
+ addIfcStair: {
281
+ taskTags: ['create', 'repair'],
282
+ placement: 'storey-relative',
283
+ requiredKeys: ['Position', 'NumberOfRisers', 'RiserHeight', 'TreadLength', 'Width'],
284
+ positiveKeys: ['NumberOfRisers', 'RiserHeight', 'TreadLength', 'Width'],
285
+ pointArity: { Position: 3 },
286
+ useWhen: 'Create a stair from a base position and riser/tread definition.',
287
+ },
288
+ addIfcRoof: {
289
+ taskTags: ['create', 'repair'],
290
+ placement: 'storey-relative',
291
+ requiredKeys: ['Position', 'Width', 'Depth', 'Thickness'],
292
+ positiveKeys: ['Width', 'Depth', 'Thickness', 'Slope'],
293
+ pointArity: { Position: 3 },
294
+ forbiddenKeys: [
295
+ { key: 'Profile', message: '`bim.create.addIfcRoof(...)` does not support `Profile`. Use `Position`, `Width`, `Depth`, `Thickness`, and optional `Slope`.' },
296
+ { key: 'ExtrusionHeight', message: '`bim.create.addIfcRoof(...)` uses `Depth`, not `ExtrusionHeight`.' },
297
+ { key: 'Height', message: '`bim.create.addIfcRoof(...)` uses `Thickness` and `Depth`, not `Height`.' },
298
+ { key: 'Overhang', message: '`bim.create.addIfcRoof(...)` does not support `Overhang`. Use `addIfcGableRoof(...)` for a house-style roof with pitch and overhang.' },
299
+ ],
300
+ customValidationId: 'roof-shape',
301
+ useWhen: 'Create flat or mono-pitch roof slabs only.',
302
+ cautions: [
303
+ 'Slope is in radians.',
304
+ 'Use addIfcGableRoof for house, pitched-roof, or gable-roof requests.',
305
+ ],
306
+ },
307
+ addIfcGableRoof: {
308
+ taskTags: ['create', 'repair'],
309
+ placement: 'storey-relative',
310
+ requiredKeys: ['Position', 'Width', 'Depth', 'Thickness', 'Slope'],
311
+ positiveKeys: ['Width', 'Depth', 'Thickness', 'Slope'],
312
+ pointArity: { Position: 3 },
313
+ forbiddenKeys: [
314
+ { key: 'Profile', message: '`bim.create.addIfcGableRoof(...)` does not support `Profile`. Use `Position`, `Width`, `Depth`, `Thickness`, `Slope`, and optional `Overhang`.' },
315
+ { key: 'ExtrusionHeight', message: '`bim.create.addIfcGableRoof(...)` uses `Thickness`, not `ExtrusionHeight`.' },
316
+ { key: 'Height', message: '`bim.create.addIfcGableRoof(...)` uses `Thickness` for roof thickness and derives ridge height from `Slope`.' },
317
+ ],
318
+ customValidationId: 'roof-shape',
319
+ useWhen: 'Create standard dual-pitch house roofs.',
320
+ cautions: [
321
+ 'Slope is in radians.',
322
+ ],
323
+ },
324
+ addIfcWallDoor: {
325
+ taskTags: ['create', 'repair'],
326
+ placement: 'wall-local',
327
+ requiredKeys: ['Position', 'Width', 'Height'],
328
+ positiveKeys: ['Width', 'Height', 'Thickness'],
329
+ pointArity: { Position: 3 },
330
+ forbiddenKeys: [
331
+ { key: 'Start', message: '`bim.create.addIfcWallDoor(...)` uses wall-local `Position`, not `Start`/`End`.' },
332
+ { key: 'End', message: '`bim.create.addIfcWallDoor(...)` uses wall-local `Position`, not `Start`/`End`.' },
333
+ { key: 'Rotation', message: '`bim.create.addIfcWallDoor(...)` auto-aligns to the host wall. Do not pass `Rotation`.' },
334
+ { key: 'Direction', message: '`bim.create.addIfcWallDoor(...)` auto-aligns to the host wall. Do not pass `Direction`.' },
335
+ { key: 'Axis', message: '`bim.create.addIfcWallDoor(...)` auto-aligns to the host wall. Do not pass `Axis`.' },
336
+ { key: 'RefDirection', message: '`bim.create.addIfcWallDoor(...)` auto-aligns to the host wall. Do not pass `RefDirection`.' },
337
+ { key: 'Placement', message: '`bim.create.addIfcWallDoor(...)` uses wall-local `Position`, not `Placement`.' },
338
+ ],
339
+ useWhen: 'Create a wall-hosted door aligned to a host wall.',
340
+ },
341
+ addIfcWallWindow: {
342
+ taskTags: ['create', 'repair'],
343
+ placement: 'wall-local',
344
+ requiredKeys: ['Position', 'Width', 'Height'],
345
+ positiveKeys: ['Width', 'Height', 'Thickness'],
346
+ pointArity: { Position: 3 },
347
+ forbiddenKeys: [
348
+ { key: 'Start', message: '`bim.create.addIfcWallWindow(...)` uses wall-local `Position`, not `Start`/`End`.' },
349
+ { key: 'End', message: '`bim.create.addIfcWallWindow(...)` uses wall-local `Position`, not `Start`/`End`.' },
350
+ { key: 'Rotation', message: '`bim.create.addIfcWallWindow(...)` auto-aligns to the host wall. Do not pass `Rotation`.' },
351
+ { key: 'Direction', message: '`bim.create.addIfcWallWindow(...)` auto-aligns to the host wall. Do not pass `Direction`.' },
352
+ { key: 'Axis', message: '`bim.create.addIfcWallWindow(...)` auto-aligns to the host wall. Do not pass `Axis`.' },
353
+ { key: 'RefDirection', message: '`bim.create.addIfcWallWindow(...)` auto-aligns to the host wall. Do not pass `RefDirection`.' },
354
+ { key: 'Placement', message: '`bim.create.addIfcWallWindow(...)` uses wall-local `Position`, not `Placement`.' },
355
+ ],
356
+ useWhen: 'Create a wall-hosted window aligned to a host wall.',
357
+ },
358
+ addIfcDoor: {
359
+ taskTags: ['create', 'repair'],
360
+ placement: 'world',
361
+ requiredKeys: ['Position', 'Width', 'Height'],
362
+ positiveKeys: ['Width', 'Height', 'Thickness'],
363
+ pointArity: { Position: 3 },
364
+ forbiddenKeys: [
365
+ { key: 'Start', message: '`bim.create.addIfcDoor(...)` uses `Position`, not `Start`/`End`.' },
366
+ { key: 'End', message: '`bim.create.addIfcDoor(...)` uses `Position`, not `Start`/`End`.' },
367
+ { key: 'Direction', message: '`bim.create.addIfcDoor(...)` does not support wall-axis rotation. It creates a world-aligned standalone door element.' },
368
+ { key: 'Rotation', message: '`bim.create.addIfcDoor(...)` does not support rotation. For wall-hosted inserts, use `bim.create.addIfcWallDoor(...)` or wall `Openings`.' },
369
+ { key: 'Axis', message: '`bim.create.addIfcDoor(...)` does not accept `Axis`. It is not a generic placement API.' },
370
+ { key: 'RefDirection', message: '`bim.create.addIfcDoor(...)` does not accept `RefDirection`. It is not auto-aligned to wall direction.' },
371
+ { key: 'Placement', message: '`bim.create.addIfcDoor(...)` uses `Position`, not `Placement`.' },
372
+ ],
373
+ useWhen: 'Create a standalone world-aligned door element.',
374
+ cautions: [
375
+ 'For wall-hosted inserts, use addIfcWallDoor or wall Openings instead.',
376
+ ],
377
+ },
378
+ addIfcWindow: {
379
+ taskTags: ['create', 'repair'],
380
+ placement: 'world',
381
+ requiredKeys: ['Position', 'Width', 'Height'],
382
+ positiveKeys: ['Width', 'Height', 'Thickness'],
383
+ pointArity: { Position: 3 },
384
+ forbiddenKeys: [
385
+ { key: 'Start', message: '`bim.create.addIfcWindow(...)` uses `Position`, not `Start`/`End`.' },
386
+ { key: 'End', message: '`bim.create.addIfcWindow(...)` uses `Position`, not `Start`/`End`.' },
387
+ { key: 'Direction', message: '`bim.create.addIfcWindow(...)` does not support wall-axis rotation. It creates a world-aligned standalone window element.' },
388
+ { key: 'Rotation', message: '`bim.create.addIfcWindow(...)` does not support rotation. For wall-hosted inserts, use `bim.create.addIfcWallWindow(...)` or wall `Openings`.' },
389
+ { key: 'Axis', message: '`bim.create.addIfcWindow(...)` does not accept `Axis`. It is not a generic placement API.' },
390
+ { key: 'RefDirection', message: '`bim.create.addIfcWindow(...)` does not accept `RefDirection`. It is not auto-aligned to wall direction.' },
391
+ { key: 'Placement', message: '`bim.create.addIfcWindow(...)` uses `Position`, not `Placement`.' },
392
+ ],
393
+ useWhen: 'Create a standalone world-aligned window element.',
394
+ cautions: [
395
+ 'For wall-hosted inserts, use addIfcWallWindow or wall Openings instead.',
396
+ ],
397
+ },
398
+ addElement: {
399
+ taskTags: ['create', 'repair'],
400
+ placement: 'explicit-placement',
401
+ requiredKeys: ['IfcType', 'Placement', 'Profile', 'Depth'],
402
+ positiveKeys: ['Depth'],
403
+ customValidationId: 'generic-element',
404
+ useWhen: 'Create advanced IFC entities only when no dedicated helper exists.',
405
+ },
406
+ addAxisElement: {
407
+ taskTags: ['create', 'repair'],
408
+ placement: 'world',
409
+ requiredKeys: ['IfcType', 'Start', 'End', 'Profile'],
410
+ pointArity: { Start: 3, End: 3 },
411
+ axisPair: ['Start', 'End'],
412
+ customValidationId: 'axis-element',
413
+ useWhen: 'Create advanced axis-based IFC entities when no dedicated helper exists.',
414
+ },
415
+ };
416
+ /**
417
+ * Build all bim.create method schemas by discovering public methods
418
+ * on IfcCreator.prototype. New methods are automatically exposed.
419
+ */
420
+ function buildCreateMethods() {
421
+ const methods = [];
422
+ // ── Special: project (creates IfcCreator, returns handle) ──
423
+ methods.push({
424
+ name: 'project',
425
+ doc: 'Create a new IFC project. Returns a creator handle (number).',
426
+ args: ['dump'],
427
+ paramNames: ['params'],
428
+ tsParamTypes: ['{ Name?: string; Description?: string; Schema?: string; LengthUnit?: string; Author?: string; Organization?: string }'],
429
+ tsReturn: 'number',
430
+ call: (_sdk, args, context) => {
431
+ const params = (args[0] ?? {});
432
+ const creator = new IfcCreator(params);
433
+ return creatorRegistry.registerForSession(context.sandboxSessionId, creator);
434
+ },
435
+ returns: 'value',
436
+ llmSemantics: CREATE_METHOD_SEMANTICS.project,
437
+ });
438
+ // ── Special: toIfc (finalizes + cleans up handle) ──
439
+ methods.push({
440
+ name: 'toIfc',
441
+ doc: 'Generate the IFC STEP file content. Returns { content, entities, stats }.',
442
+ args: ['number'],
443
+ paramNames: ['handle'],
444
+ tsReturn: '{ content: string; entities: Array<{ expressId: number; type: string; Name?: string }>; stats: { entityCount: number; fileSize: number } }',
445
+ call: (_sdk, args, context) => {
446
+ const handle = args[0];
447
+ try {
448
+ const creator = creatorRegistry.getForSession(context.sandboxSessionId, handle);
449
+ return creator.toIfc();
450
+ }
451
+ finally {
452
+ creatorRegistry.removeForSession(context.sandboxSessionId, handle);
453
+ }
454
+ },
455
+ returns: 'value',
456
+ llmSemantics: CREATE_METHOD_SEMANTICS.toIfc,
457
+ });
458
+ // ── Special: setColor (unique signature: handle, elementId, name, rgb) ──
459
+ methods.push({
460
+ name: 'setColor',
461
+ doc: 'Assign a named colour to an element. Call before toIfc().',
462
+ args: ['number', 'number', 'string', 'dump'],
463
+ paramNames: ['handle', 'elementId', 'name', 'rgb'],
464
+ tsReturn: 'void',
465
+ call: (_sdk, args, context) => {
466
+ const creator = creatorRegistry.getForSession(context.sandboxSessionId, args[0]);
467
+ creator.setColor(args[1], args[2], args[3]);
468
+ },
469
+ returns: 'void',
470
+ llmSemantics: CREATE_METHOD_SEMANTICS.setColor,
471
+ });
472
+ // ── Auto-discover all other public methods from IfcCreator.prototype ──
473
+ const proto = IfcCreator.prototype;
474
+ const methodNames = Object.getOwnPropertyNames(proto)
475
+ .filter(name => typeof proto[name] === 'function' && ALLOWED_METHODS.has(name))
476
+ .sort();
477
+ for (const name of methodNames) {
478
+ const pattern = classifyMethod(name, proto[name]);
479
+ if (pattern === 'special')
480
+ continue; // already handled above
481
+ switch (pattern) {
482
+ case 'storey-params':
483
+ // addIfcBuildingStorey takes (params) — 1 arg after handle
484
+ if (name === 'addIfcBuildingStorey') {
485
+ methods.push({
486
+ name,
487
+ doc: methodDoc(name),
488
+ args: ['number', 'dump'],
489
+ paramNames: ['handle', 'params'],
490
+ tsReturn: 'number',
491
+ call: (_sdk, args, context) => {
492
+ const creator = creatorRegistry.getForSession(context.sandboxSessionId, args[0]);
493
+ return creator[name](args[1]);
494
+ },
495
+ returns: 'value',
496
+ llmSemantics: CREATE_METHOD_SEMANTICS[name],
497
+ });
498
+ }
499
+ else {
500
+ // Standard: (storeyId, params) — 2 args after handle
501
+ methods.push({
502
+ name,
503
+ doc: methodDoc(name),
504
+ args: ['number', 'number', 'dump'],
505
+ paramNames: ['handle', 'storeyId', 'params'],
506
+ tsReturn: 'number',
507
+ call: (_sdk, args, context) => {
508
+ const creator = creatorRegistry.getForSession(context.sandboxSessionId, args[0]);
509
+ return creator[name](args[1], args[2]);
510
+ },
511
+ returns: 'value',
512
+ llmSemantics: CREATE_METHOD_SEMANTICS[name],
513
+ });
514
+ }
515
+ break;
516
+ case 'element-params':
517
+ // (elementId, def) — 2 args after handle, may return number or void
518
+ methods.push({
519
+ name,
520
+ doc: methodDoc(name),
521
+ args: ['number', 'number', 'dump'],
522
+ paramNames: ['handle', name === 'addIfcWallDoor' || name === 'addIfcWallWindow' ? 'wallId' : 'elementId', 'params'],
523
+ tsReturn: name === 'addIfcMaterial' ? 'void' : 'number',
524
+ call: (_sdk, args, context) => {
525
+ const creator = creatorRegistry.getForSession(context.sandboxSessionId, args[0]);
526
+ return creator[name](args[1], args[2]);
527
+ },
528
+ returns: name === 'addIfcMaterial' ? 'void' : 'value',
529
+ llmSemantics: CREATE_METHOD_SEMANTICS[name],
530
+ });
531
+ break;
532
+ case 'single-dump':
533
+ methods.push({
534
+ name,
535
+ doc: methodDoc(name),
536
+ args: ['number', 'dump'],
537
+ paramNames: ['handle', 'profile'],
538
+ tsReturn: 'number',
539
+ call: (_sdk, args, context) => {
540
+ const creator = creatorRegistry.getForSession(context.sandboxSessionId, args[0]);
541
+ return creator[name](args[1]);
542
+ },
543
+ returns: 'value',
544
+ llmSemantics: CREATE_METHOD_SEMANTICS[name],
545
+ });
546
+ break;
547
+ case 'no-args':
548
+ methods.push({
549
+ name,
550
+ doc: methodDoc(name),
551
+ args: ['number'],
552
+ paramNames: ['handle'],
553
+ tsReturn: 'number',
554
+ call: (_sdk, args, context) => {
555
+ const creator = creatorRegistry.getForSession(context.sandboxSessionId, args[0]);
556
+ return creator[name]();
557
+ },
558
+ returns: 'value',
559
+ llmSemantics: CREATE_METHOD_SEMANTICS[name],
560
+ });
561
+ break;
562
+ }
563
+ }
564
+ return methods;
565
+ }
65
566
  // ============================================================================
66
567
  // Schema Definitions
67
568
  // ============================================================================
@@ -151,6 +652,26 @@ export const NAMESPACE_SCHEMAS = [
151
652
  },
152
653
  returns: 'value',
153
654
  },
655
+ {
656
+ name: 'attributes',
657
+ doc: 'Get all named string/enum attributes for an entity',
658
+ args: ['dump'],
659
+ paramNames: ['entity'],
660
+ tsParamTypes: ['BimEntity'],
661
+ tsReturn: 'BimAttribute[]',
662
+ call: (sdk, args) => {
663
+ const ref = toRef(args[0]);
664
+ if (!ref)
665
+ return [];
666
+ return sdk.attributes(ref);
667
+ },
668
+ returns: 'value',
669
+ llmSemantics: {
670
+ taskTags: ['inspect', 'repair'],
671
+ inspectFirst: true,
672
+ useWhen: 'Inspect raw IFC occurrence attributes before guessing metadata names.',
673
+ },
674
+ },
154
675
  {
155
676
  name: 'properties',
156
677
  doc: 'Get all IfcPropertySet data for an entity',
@@ -162,9 +683,22 @@ export const NAMESPACE_SCHEMAS = [
162
683
  const ref = toRef(args[0]);
163
684
  if (!ref)
164
685
  return [];
165
- return sdk.properties(ref);
686
+ return sdk.properties(ref).map(pset => {
687
+ const mappedProperties = mapNamedProperties(pset.properties);
688
+ return {
689
+ name: pset.name, Name: pset.name,
690
+ globalId: pset.globalId, GlobalId: pset.globalId,
691
+ properties: mappedProperties,
692
+ Properties: mappedProperties,
693
+ };
694
+ });
166
695
  },
167
696
  returns: 'value',
697
+ llmSemantics: {
698
+ taskTags: ['inspect', 'repair'],
699
+ inspectFirst: true,
700
+ useWhen: 'Inspect all property sets on an occurrence before guessing individual property names.',
701
+ },
168
702
  },
169
703
  {
170
704
  name: 'quantities',
@@ -177,9 +711,314 @@ export const NAMESPACE_SCHEMAS = [
177
711
  const ref = toRef(args[0]);
178
712
  if (!ref)
179
713
  return [];
180
- return sdk.quantities(ref);
714
+ return sdk.quantities(ref).map(qset => {
715
+ const mappedQuantities = mapNamedProperties(qset.quantities);
716
+ return {
717
+ name: qset.name, Name: qset.name,
718
+ quantities: mappedQuantities,
719
+ Quantities: mappedQuantities,
720
+ };
721
+ });
181
722
  },
182
723
  returns: 'value',
724
+ llmSemantics: {
725
+ taskTags: ['inspect', 'repair'],
726
+ inspectFirst: true,
727
+ useWhen: 'Inspect all quantity sets on an occurrence.',
728
+ },
729
+ },
730
+ {
731
+ name: 'property',
732
+ doc: 'Get a single property value from an entity',
733
+ args: ['dump', 'string', 'string'],
734
+ paramNames: ['entity', 'psetName', 'propName'],
735
+ tsParamTypes: ['BimEntity'],
736
+ tsReturn: 'string | number | boolean | null',
737
+ call: (sdk, args) => {
738
+ const ref = toRef(args[0]);
739
+ if (!ref)
740
+ return null;
741
+ return sdk.property(ref, args[1], args[2]);
742
+ },
743
+ returns: 'value',
744
+ llmSemantics: {
745
+ taskTags: ['inspect', 'repair'],
746
+ inspectFirst: true,
747
+ useWhen: 'Read one known property when you already know the exact property-set and property names.',
748
+ },
749
+ },
750
+ {
751
+ name: 'classifications',
752
+ doc: 'Get classification references for an entity',
753
+ args: ['dump'],
754
+ paramNames: ['entity'],
755
+ tsParamTypes: ['BimEntity'],
756
+ tsReturn: 'BimClassification[]',
757
+ call: (sdk, args) => {
758
+ const ref = toRef(args[0]);
759
+ if (!ref)
760
+ return [];
761
+ return sdk.classifications(ref);
762
+ },
763
+ returns: 'value',
764
+ llmSemantics: {
765
+ taskTags: ['inspect', 'repair'],
766
+ inspectFirst: true,
767
+ useWhen: 'Read relationship-based classification references.',
768
+ cautions: ['Prefer this over guessing ad-hoc classification property names.'],
769
+ },
770
+ },
771
+ {
772
+ name: 'materials',
773
+ doc: 'Get material assignment for an entity',
774
+ args: ['dump'],
775
+ paramNames: ['entity'],
776
+ tsParamTypes: ['BimEntity'],
777
+ tsReturn: 'BimMaterial | null',
778
+ call: (sdk, args) => {
779
+ const ref = toRef(args[0]);
780
+ if (!ref)
781
+ return null;
782
+ return sdk.materials(ref);
783
+ },
784
+ returns: 'value',
785
+ llmSemantics: {
786
+ taskTags: ['inspect', 'repair'],
787
+ inspectFirst: true,
788
+ useWhen: 'Read assigned material data for an entity.',
789
+ cautions: ['Prefer this over querying Pset_MaterialCommon or Material.Name as ordinary property sets.'],
790
+ },
791
+ },
792
+ {
793
+ name: 'typeProperties',
794
+ doc: 'Get type-level property sets for an entity',
795
+ args: ['dump'],
796
+ paramNames: ['entity'],
797
+ tsParamTypes: ['BimEntity'],
798
+ tsReturn: 'BimTypeProperties | null',
799
+ call: (sdk, args) => {
800
+ const ref = toRef(args[0]);
801
+ if (!ref)
802
+ return null;
803
+ return sdk.typeProperties(ref);
804
+ },
805
+ returns: 'value',
806
+ llmSemantics: {
807
+ taskTags: ['inspect', 'repair'],
808
+ inspectFirst: true,
809
+ useWhen: 'Inspect type-level properties when occurrence-level property sets are missing expected data.',
810
+ },
811
+ },
812
+ {
813
+ name: 'documents',
814
+ doc: 'Get linked document references for an entity',
815
+ args: ['dump'],
816
+ paramNames: ['entity'],
817
+ tsParamTypes: ['BimEntity'],
818
+ tsReturn: 'BimDocument[]',
819
+ call: (sdk, args) => {
820
+ const ref = toRef(args[0]);
821
+ if (!ref)
822
+ return [];
823
+ return sdk.documents(ref);
824
+ },
825
+ returns: 'value',
826
+ llmSemantics: {
827
+ taskTags: ['inspect', 'repair'],
828
+ inspectFirst: true,
829
+ useWhen: 'Inspect linked document references and external documentation.',
830
+ },
831
+ },
832
+ {
833
+ name: 'relationships',
834
+ doc: 'Get structural relationship summary for an entity',
835
+ args: ['dump'],
836
+ paramNames: ['entity'],
837
+ tsParamTypes: ['BimEntity'],
838
+ tsReturn: 'BimRelationships',
839
+ call: (sdk, args) => {
840
+ const ref = toRef(args[0]);
841
+ if (!ref)
842
+ return { voids: [], fills: [], groups: [], connections: [] };
843
+ return sdk.relationships(ref);
844
+ },
845
+ returns: 'value',
846
+ llmSemantics: {
847
+ taskTags: ['inspect', 'repair'],
848
+ inspectFirst: true,
849
+ useWhen: 'Inspect structural and semantic relationships such as voids, fills, groups, and connections.',
850
+ },
851
+ },
852
+ {
853
+ name: 'quantity',
854
+ doc: 'Get a single quantity value from an entity',
855
+ args: ['dump', 'string', 'string'],
856
+ paramNames: ['entity', 'qsetName', 'quantityName'],
857
+ tsParamTypes: ['BimEntity'],
858
+ tsReturn: 'number | null',
859
+ call: (sdk, args) => {
860
+ const ref = toRef(args[0]);
861
+ if (!ref)
862
+ return null;
863
+ return sdk.quantity(ref, args[1], args[2]);
864
+ },
865
+ returns: 'value',
866
+ },
867
+ {
868
+ name: 'related',
869
+ doc: 'Get related entities by IFC relationship type',
870
+ args: ['dump', 'string', 'string'],
871
+ paramNames: ['entity', 'relType', 'direction'],
872
+ tsParamTypes: ['BimEntity', undefined, "'forward' | 'inverse'"],
873
+ tsReturn: 'BimEntity[]',
874
+ call: (sdk, args) => {
875
+ const ref = toRef(args[0]);
876
+ if (!ref)
877
+ return [];
878
+ return sdk.related(ref, args[1], args[2]).map(withAliases);
879
+ },
880
+ returns: 'value',
881
+ llmSemantics: {
882
+ taskTags: ['inspect', 'repair'],
883
+ inspectFirst: true,
884
+ useWhen: 'Traverse a known IFC relationship type in forward or inverse direction.',
885
+ },
886
+ },
887
+ {
888
+ name: 'containedIn',
889
+ doc: 'Get the spatial container of an entity',
890
+ args: ['dump'],
891
+ paramNames: ['entity'],
892
+ tsParamTypes: ['BimEntity'],
893
+ tsReturn: 'BimEntity | null',
894
+ call: (sdk, args) => {
895
+ const ref = toRef(args[0]);
896
+ if (!ref)
897
+ return null;
898
+ const entity = sdk.containedIn(ref);
899
+ return entity ? withAliases(entity) : null;
900
+ },
901
+ returns: 'value',
902
+ },
903
+ {
904
+ name: 'contains',
905
+ doc: 'Get entities contained in a spatial container',
906
+ args: ['dump'],
907
+ paramNames: ['entity'],
908
+ tsParamTypes: ['BimEntity'],
909
+ tsReturn: 'BimEntity[]',
910
+ call: (sdk, args) => {
911
+ const ref = toRef(args[0]);
912
+ if (!ref)
913
+ return [];
914
+ return sdk.contains(ref).map(withAliases);
915
+ },
916
+ returns: 'value',
917
+ },
918
+ {
919
+ name: 'decomposedBy',
920
+ doc: 'Get the parent aggregate of an entity',
921
+ args: ['dump'],
922
+ paramNames: ['entity'],
923
+ tsParamTypes: ['BimEntity'],
924
+ tsReturn: 'BimEntity | null',
925
+ call: (sdk, args) => {
926
+ const ref = toRef(args[0]);
927
+ if (!ref)
928
+ return null;
929
+ const entity = sdk.decomposedBy(ref);
930
+ return entity ? withAliases(entity) : null;
931
+ },
932
+ returns: 'value',
933
+ },
934
+ {
935
+ name: 'decomposes',
936
+ doc: 'Get aggregated children of an entity',
937
+ args: ['dump'],
938
+ paramNames: ['entity'],
939
+ tsParamTypes: ['BimEntity'],
940
+ tsReturn: 'BimEntity[]',
941
+ call: (sdk, args) => {
942
+ const ref = toRef(args[0]);
943
+ if (!ref)
944
+ return [];
945
+ return sdk.decomposes(ref).map(withAliases);
946
+ },
947
+ returns: 'value',
948
+ },
949
+ {
950
+ name: 'storey',
951
+ doc: 'Get the containing building storey of an entity',
952
+ args: ['dump'],
953
+ paramNames: ['entity'],
954
+ tsParamTypes: ['BimEntity'],
955
+ tsReturn: 'BimEntity | null',
956
+ call: (sdk, args) => {
957
+ const ref = toRef(args[0]);
958
+ if (!ref)
959
+ return null;
960
+ const entity = sdk.storey(ref);
961
+ return entity ? withAliases(entity) : null;
962
+ },
963
+ returns: 'value',
964
+ llmSemantics: {
965
+ taskTags: ['inspect', 'repair'],
966
+ inspectFirst: true,
967
+ useWhen: 'Resolve which building storey currently contains an entity.',
968
+ },
969
+ },
970
+ {
971
+ name: 'path',
972
+ doc: 'Get the spatial/aggregation path from project to entity',
973
+ args: ['dump'],
974
+ paramNames: ['entity'],
975
+ tsParamTypes: ['BimEntity'],
976
+ tsReturn: 'BimEntity[]',
977
+ call: (sdk, args) => {
978
+ const ref = toRef(args[0]);
979
+ if (!ref)
980
+ return [];
981
+ return sdk.path(ref).map(withAliases);
982
+ },
983
+ returns: 'value',
984
+ llmSemantics: {
985
+ taskTags: ['inspect', 'repair'],
986
+ inspectFirst: true,
987
+ useWhen: 'Inspect the full project-to-entity spatial path before generating hierarchy-aware edits.',
988
+ },
989
+ },
990
+ {
991
+ name: 'storeys',
992
+ doc: 'List all building storeys',
993
+ args: [],
994
+ tsReturn: 'BimEntity[]',
995
+ call: (sdk) => {
996
+ return sdk.storeys().map(withAliases);
997
+ },
998
+ returns: 'value',
999
+ llmSemantics: {
1000
+ taskTags: ['inspect', 'repair'],
1001
+ inspectFirst: true,
1002
+ useWhen: 'List all building storeys and use their actual names/elevations as generation targets.',
1003
+ },
1004
+ },
1005
+ {
1006
+ name: 'selection',
1007
+ doc: 'Get the current viewer selection as entities',
1008
+ args: [],
1009
+ tsReturn: 'BimEntity[]',
1010
+ call: (sdk) => {
1011
+ return sdk.viewer.getSelection()
1012
+ .map((ref) => sdk.entity(ref))
1013
+ .filter((entity) => Boolean(entity))
1014
+ .map(withAliases);
1015
+ },
1016
+ returns: 'value',
1017
+ llmSemantics: {
1018
+ taskTags: ['inspect', 'repair'],
1019
+ inspectFirst: true,
1020
+ useWhen: 'Inspect what the user currently selected in the viewer before proposing targeted edits or analysis.',
1021
+ },
183
1022
  },
184
1023
  ],
185
1024
  },
@@ -296,11 +1135,29 @@ export const NAMESPACE_SCHEMAS = [
296
1135
  methods: [
297
1136
  {
298
1137
  name: 'setProperty',
299
- doc: 'Set a property value',
1138
+ doc: 'Set an IfcPropertySet or quantity value (not a root IFC attribute)',
300
1139
  args: ['dump', 'string', 'string', 'dump'],
301
1140
  paramNames: ['entity', 'psetName', 'propName', 'value'],
302
1141
  call: (sdk, args) => {
303
- sdk.mutate.setProperty(args[0], args[1], args[2], args[3]);
1142
+ const ref = toRef(args[0]);
1143
+ if (!ref) {
1144
+ throw new Error('bim.mutate.setProperty: invalid entity reference');
1145
+ }
1146
+ sdk.mutate.setProperty(ref, args[1], args[2], args[3]);
1147
+ },
1148
+ returns: 'void',
1149
+ },
1150
+ {
1151
+ name: 'setAttribute',
1152
+ doc: 'Set a root IFC attribute such as Name, Description, ObjectType, or Tag',
1153
+ args: ['dump', 'string', 'string'],
1154
+ paramNames: ['entity', 'attrName', 'value'],
1155
+ call: (sdk, args) => {
1156
+ const ref = toRef(args[0]);
1157
+ if (!ref) {
1158
+ throw new Error('bim.mutate.setAttribute: invalid entity reference');
1159
+ }
1160
+ sdk.mutate.setAttribute(ref, args[1], args[2]);
304
1161
  },
305
1162
  returns: 'void',
306
1163
  },
@@ -310,7 +1167,11 @@ export const NAMESPACE_SCHEMAS = [
310
1167
  args: ['dump', 'string', 'string'],
311
1168
  paramNames: ['entity', 'psetName', 'propName'],
312
1169
  call: (sdk, args) => {
313
- sdk.mutate.deleteProperty(args[0], args[1], args[2]);
1170
+ const ref = toRef(args[0]);
1171
+ if (!ref) {
1172
+ throw new Error('bim.mutate.deleteProperty: invalid entity reference');
1173
+ }
1174
+ sdk.mutate.deleteProperty(ref, args[1], args[2]);
314
1175
  },
315
1176
  returns: 'void',
316
1177
  },
@@ -356,190 +1217,68 @@ export const NAMESPACE_SCHEMAS = [
356
1217
  ],
357
1218
  },
358
1219
  // ── bim.create ─────────────────────────────────────────────
1220
+ //
1221
+ // Auto-discovered from IfcCreator.prototype at module load.
1222
+ // Adding a new public method to IfcCreator automatically exposes it
1223
+ // in the sandbox — no manual bridge wiring needed.
1224
+ //
359
1225
  {
360
1226
  name: 'create',
361
1227
  doc: 'IFC creation from scratch',
362
1228
  permission: 'export', // reuses export permission — creation produces files
1229
+ methods: buildCreateMethods(),
1230
+ },
1231
+ // ── bim.export ─────────────────────────────────────────────
1232
+ {
1233
+ name: 'files',
1234
+ doc: 'Uploaded file attachments',
1235
+ permission: 'files',
363
1236
  methods: [
364
1237
  {
365
- name: 'project',
366
- doc: 'Create a new IFC project. Returns a creator handle (number).',
367
- args: ['dump'],
368
- paramNames: ['params'],
369
- tsParamTypes: ['{ Name?: string; Description?: string; Schema?: string; LengthUnit?: string; Author?: string; Organization?: string }'],
370
- tsReturn: 'number',
371
- call: (_sdk, args) => {
372
- const params = (args[0] ?? {});
373
- const creator = new IfcCreator(params);
374
- return creatorRegistry.register(creator);
375
- },
376
- returns: 'value',
377
- },
378
- {
379
- name: 'addIfcBuildingStorey',
380
- doc: 'Add a building storey. Returns storey expressId.',
381
- args: ['number', 'dump'],
382
- paramNames: ['handle', 'params'],
383
- tsParamTypes: [undefined, '{ Name?: string; Description?: string; Elevation: number }'],
384
- tsReturn: 'number',
385
- call: (_sdk, args) => {
386
- const creator = creatorRegistry.get(args[0]);
387
- return creator.addIfcBuildingStorey(args[1]);
388
- },
389
- returns: 'value',
390
- },
391
- {
392
- name: 'addIfcWall',
393
- doc: 'Add a wall to a storey. Returns wall expressId.',
394
- args: ['number', 'number', 'dump'],
395
- paramNames: ['handle', 'storeyId', 'params'],
396
- tsParamTypes: [undefined, undefined, '{ Start: [number,number,number]; End: [number,number,number]; Thickness: number; Height: number; Name?: string; Openings?: Array<{ Width: number; Height: number; Position: [number,number,number]; Name?: string }> }'],
397
- tsReturn: 'number',
398
- call: (_sdk, args) => {
399
- const creator = creatorRegistry.get(args[0]);
400
- return creator.addIfcWall(args[1], args[2]);
401
- },
402
- returns: 'value',
403
- },
404
- {
405
- name: 'addIfcSlab',
406
- doc: 'Add a slab to a storey. Returns slab expressId.',
407
- args: ['number', 'number', 'dump'],
408
- paramNames: ['handle', 'storeyId', 'params'],
409
- tsParamTypes: [undefined, undefined, '{ Position: [number,number,number]; Thickness: number; Width?: number; Depth?: number; Profile?: [number,number][]; Name?: string; Openings?: Array<{ Width: number; Height: number; Position: [number,number,number]; Name?: string }> }'],
410
- tsReturn: 'number',
411
- call: (_sdk, args) => {
412
- const creator = creatorRegistry.get(args[0]);
413
- return creator.addIfcSlab(args[1], args[2]);
414
- },
415
- returns: 'value',
416
- },
417
- {
418
- name: 'addIfcColumn',
419
- doc: 'Add a column to a storey. Returns column expressId.',
420
- args: ['number', 'number', 'dump'],
421
- paramNames: ['handle', 'storeyId', 'params'],
422
- tsParamTypes: [undefined, undefined, '{ Position: [number,number,number]; Width: number; Depth: number; Height: number; Name?: string }'],
423
- tsReturn: 'number',
424
- call: (_sdk, args) => {
425
- const creator = creatorRegistry.get(args[0]);
426
- return creator.addIfcColumn(args[1], args[2]);
427
- },
428
- returns: 'value',
429
- },
430
- {
431
- name: 'addIfcBeam',
432
- doc: 'Add a beam to a storey. Returns beam expressId.',
433
- args: ['number', 'number', 'dump'],
434
- paramNames: ['handle', 'storeyId', 'params'],
435
- tsParamTypes: [undefined, undefined, '{ Start: [number,number,number]; End: [number,number,number]; Width: number; Height: number; Name?: string }'],
436
- tsReturn: 'number',
437
- call: (_sdk, args) => {
438
- const creator = creatorRegistry.get(args[0]);
439
- return creator.addIfcBeam(args[1], args[2]);
440
- },
441
- returns: 'value',
442
- },
443
- {
444
- name: 'addIfcStair',
445
- doc: 'Add a stair to a storey. Returns stair expressId.',
446
- args: ['number', 'number', 'dump'],
447
- paramNames: ['handle', 'storeyId', 'params'],
448
- tsParamTypes: [undefined, undefined, '{ Position: [number,number,number]; NumberOfRisers: number; RiserHeight: number; TreadLength: number; Width: number; Direction?: number; Name?: string }'],
449
- tsReturn: 'number',
450
- call: (_sdk, args) => {
451
- const creator = creatorRegistry.get(args[0]);
452
- return creator.addIfcStair(args[1], args[2]);
453
- },
1238
+ name: 'list',
1239
+ doc: 'List uploaded file attachments available to scripts',
1240
+ args: [],
1241
+ tsReturn: 'BimFileAttachment[]',
1242
+ call: (sdk) => sdk.files.list(),
454
1243
  returns: 'value',
455
1244
  },
456
1245
  {
457
- name: 'addIfcRoof',
458
- doc: 'Add a roof to a storey. Returns roof expressId.',
459
- args: ['number', 'number', 'dump'],
460
- paramNames: ['handle', 'storeyId', 'params'],
461
- tsParamTypes: [undefined, undefined, '{ Position: [number,number,number]; Width: number; Depth: number; Thickness: number; Slope?: number; Name?: string }'],
462
- tsReturn: 'number',
463
- call: (_sdk, args) => {
464
- const creator = creatorRegistry.get(args[0]);
465
- return creator.addIfcRoof(args[1], args[2]);
466
- },
1246
+ name: 'text',
1247
+ doc: 'Get raw text content for an uploaded attachment by file name',
1248
+ args: ['string'],
1249
+ paramNames: ['name'],
1250
+ tsReturn: 'string | null',
1251
+ call: (sdk, args) => sdk.files.text(args[0]),
467
1252
  returns: 'value',
468
- },
469
- {
470
- name: 'setColor',
471
- doc: 'Assign a named colour to an element. Call before toIfc().',
472
- args: ['number', 'number', 'string', 'dump'],
473
- paramNames: ['handle', 'elementId', 'name', 'rgb'],
474
- tsParamTypes: [undefined, undefined, undefined, '[number, number, number]'],
475
- tsReturn: 'void',
476
- call: (_sdk, args) => {
477
- const creator = creatorRegistry.get(args[0]);
478
- creator.setColor(args[1], args[2], args[3]);
479
- },
480
- returns: 'void',
481
- },
482
- {
483
- name: 'addIfcMaterial',
484
- doc: 'Assign an IFC material (simple or layered) to an element.',
485
- args: ['number', 'number', 'dump'],
486
- paramNames: ['handle', 'elementId', 'material'],
487
- tsParamTypes: [undefined, undefined, '{ Name: string; Category?: string; Layers?: Array<{ Name: string; Thickness: number; Category?: string; IsVentilated?: boolean }> }'],
488
- tsReturn: 'void',
489
- call: (_sdk, args) => {
490
- const creator = creatorRegistry.get(args[0]);
491
- creator.addIfcMaterial(args[1], args[2]);
1253
+ llmSemantics: {
1254
+ taskTags: ['inspect', 'modify', 'repair', 'export'],
1255
+ useWhen: 'Read uploaded CSV, TSV, JSON, or text attachments without using fetch().',
492
1256
  },
493
- returns: 'void',
494
1257
  },
495
1258
  {
496
- name: 'addIfcPropertySet',
497
- doc: 'Attach a property set to an element. Returns pset expressId.',
498
- args: ['number', 'number', 'dump'],
499
- paramNames: ['handle', 'elementId', 'pset'],
500
- tsParamTypes: [undefined, undefined, '{ Name: string; Properties: Array<{ Name: string; NominalValue: string | number | boolean; Type?: string }> }'],
501
- tsReturn: 'number',
502
- call: (_sdk, args) => {
503
- const creator = creatorRegistry.get(args[0]);
504
- return creator.addIfcPropertySet(args[1], args[2]);
505
- },
1259
+ name: 'csv',
1260
+ doc: 'Get parsed CSV/TSV rows for an uploaded attachment by file name',
1261
+ args: ['string'],
1262
+ paramNames: ['name'],
1263
+ tsReturn: 'Record<string, string>[] | null',
1264
+ call: (sdk, args) => sdk.files.csv(args[0]),
506
1265
  returns: 'value',
507
- },
508
- {
509
- name: 'addIfcElementQuantity',
510
- doc: 'Attach element quantities to an element. Returns qset expressId.',
511
- args: ['number', 'number', 'dump'],
512
- paramNames: ['handle', 'elementId', 'qset'],
513
- tsParamTypes: [undefined, undefined, "{ Name: string; Quantities: Array<{ Name: string; Value: number; Kind: 'IfcQuantityLength' | 'IfcQuantityArea' | 'IfcQuantityVolume' | 'IfcQuantityCount' | 'IfcQuantityWeight' }> }"],
514
- tsReturn: 'number',
515
- call: (_sdk, args) => {
516
- const creator = creatorRegistry.get(args[0]);
517
- return creator.addIfcElementQuantity(args[1], args[2]);
1266
+ llmSemantics: {
1267
+ taskTags: ['inspect', 'modify', 'repair', 'export'],
1268
+ useWhen: 'Load uploaded CSV rows directly inside a script and join them against model entities.',
518
1269
  },
519
- returns: 'value',
520
1270
  },
521
1271
  {
522
- name: 'toIfc',
523
- doc: 'Generate the IFC STEP file content. Returns { content, entities, stats }.',
524
- args: ['number'],
525
- paramNames: ['handle'],
526
- tsReturn: '{ content: string; entities: Array<{ expressId: number; type: string; Name?: string }>; stats: { entityCount: number; fileSize: number } }',
527
- call: (_sdk, args) => {
528
- const handle = args[0];
529
- try {
530
- const creator = creatorRegistry.get(handle);
531
- return creator.toIfc();
532
- }
533
- finally {
534
- // Always clean up the creator, even if toIfc() throws
535
- creatorRegistry.remove(handle);
536
- }
537
- },
1272
+ name: 'csvColumns',
1273
+ doc: 'Get parsed CSV column names for an uploaded attachment by file name',
1274
+ args: ['string'],
1275
+ paramNames: ['name'],
1276
+ tsReturn: 'string[]',
1277
+ call: (sdk, args) => sdk.files.csvColumns(args[0]),
538
1278
  returns: 'value',
539
1279
  },
540
1280
  ],
541
1281
  },
542
- // ── bim.export ─────────────────────────────────────────────
543
1282
  {
544
1283
  name: 'export',
545
1284
  doc: 'Data export',
@@ -569,6 +1308,18 @@ export const NAMESPACE_SCHEMAS = [
569
1308
  },
570
1309
  returns: 'value',
571
1310
  },
1311
+ {
1312
+ name: 'ifc',
1313
+ doc: 'Export entities to IFC STEP text. Pass filename to auto-download a valid .ifc file',
1314
+ args: ['entityRefs', 'dump'],
1315
+ paramNames: ['entities', 'options'],
1316
+ tsParamTypes: [undefined, '{ schema?: "IFC2X3" | "IFC4" | "IFC4X3"; filename?: string; includeMutations?: boolean; visibleOnly?: boolean }'],
1317
+ tsReturn: 'string',
1318
+ call: (sdk, args) => {
1319
+ return sdk.export.ifc(args[0], args[1]);
1320
+ },
1321
+ returns: 'string',
1322
+ },
572
1323
  {
573
1324
  name: 'download',
574
1325
  doc: 'Trigger a browser file download with the given content',
@@ -589,21 +1340,21 @@ export const NAMESPACE_SCHEMAS = [
589
1340
  * Build all schema-defined namespaces on the `bim` handle.
590
1341
  * Skips namespaces whose permission is disabled.
591
1342
  */
592
- export function buildSchemaNamespaces(vm, bimHandle, sdk, permissions) {
1343
+ export function buildSchemaNamespaces(vm, bimHandle, sdk, permissions, context) {
593
1344
  for (const schema of NAMESPACE_SCHEMAS) {
594
1345
  if (!permissions[schema.permission])
595
1346
  continue;
596
- buildNamespace(vm, bimHandle, sdk, schema);
1347
+ buildNamespace(vm, bimHandle, sdk, schema, context);
597
1348
  }
598
1349
  }
599
- function buildNamespace(vm, bimHandle, sdk, schema) {
1350
+ function buildNamespace(vm, bimHandle, sdk, schema, context) {
600
1351
  const nsHandle = vm.newObject();
601
1352
  for (const method of schema.methods) {
602
1353
  const fn = vm.newFunction(method.name, (...handles) => {
603
1354
  // Unmarshal arguments
604
1355
  const nativeArgs = unmarshalArgs(vm, handles, method.args);
605
1356
  // Call the SDK
606
- const result = method.call(sdk, nativeArgs);
1357
+ const result = method.call(sdk, nativeArgs, context);
607
1358
  // Marshal return value
608
1359
  return marshalReturn(vm, result, method.returns);
609
1360
  });
@@ -613,6 +1364,9 @@ function buildNamespace(vm, bimHandle, sdk, schema) {
613
1364
  vm.setProp(bimHandle, schema.name, nsHandle);
614
1365
  nsHandle.dispose();
615
1366
  }
1367
+ export function disposeSchemaNamespaceSession(context) {
1368
+ creatorRegistry.removeSession(context.sandboxSessionId);
1369
+ }
616
1370
  /** Unmarshal QuickJS handles to native JS values based on arg schema */
617
1371
  function unmarshalArgs(vm, handles, argTypes) {
618
1372
  const result = [];