@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
@@ -0,0 +1,513 @@
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 test from 'node:test';
6
+ import assert from 'node:assert/strict';
7
+ import { validateScriptPreflight, validateScriptPreflightDetailed } from './script-preflight.js';
8
+
9
+ test('preflight accepts valid dedicated create methods', () => {
10
+ const code = `
11
+ const h = bim.create.project({ Name: "Schema Coverage" });
12
+ const s0 = bim.create.addIfcBuildingStorey(h, { Name: "Level 0", Elevation: 0 });
13
+ bim.create.addIfcWall(h, s0, {
14
+ Name: "Wall",
15
+ Start: [0, 0, 0],
16
+ End: [5, 0, 0],
17
+ Thickness: 0.2,
18
+ Height: 3,
19
+ });
20
+ bim.create.addIfcSlab(h, s0, {
21
+ Name: "Slab",
22
+ Position: [0, 0, 0],
23
+ Width: 5,
24
+ Depth: 4,
25
+ Thickness: 0.3,
26
+ });
27
+ `;
28
+
29
+ const errors = validateScriptPreflight(code);
30
+ assert.deepEqual(errors, []);
31
+ });
32
+
33
+ test('preflight suggests nearest method names for typos', () => {
34
+ const code = `
35
+ const h = bim.create.project({ Name: "Typo" });
36
+ const s0 = bim.create.addIfcBuildingStorey(h, { Name: "Level 0", Elevation: 0 });
37
+ bim.create.addIfcWal(h, s0, { Start: [0, 0, 0], End: [1, 0, 0], Thickness: 0.2, Height: 3 });
38
+ `;
39
+
40
+ const errors = validateScriptPreflight(code);
41
+ assert.equal(errors.length, 1);
42
+ assert.match(errors[0], /Did you mean `bim\.create\.addIfcWall\(\)`\?/);
43
+ });
44
+
45
+ test('preflight warns when standalone windows are used with walls but no wall openings exist', () => {
46
+ const code = `
47
+ const h = bim.create.project({ Name: "Window House" });
48
+ const s0 = bim.create.addIfcBuildingStorey(h, { Name: "Level 0", Elevation: 0 });
49
+ bim.create.addIfcWall(h, s0, {
50
+ Start: [0, 0, 0],
51
+ End: [5, 0, 0],
52
+ Thickness: 0.2,
53
+ Height: 3,
54
+ });
55
+ bim.create.addIfcWindow(h, s0, {
56
+ Position: [2.5, 0, 1.0],
57
+ Width: 1.2,
58
+ Height: 1.2,
59
+ });
60
+ `;
61
+ const errors = validateScriptPreflight(code);
62
+ assert.ok(errors.some((error) => error.includes('world-aligned standalone window')));
63
+ assert.ok(errors.some((error) => error.includes('addIfcWallWindow')));
64
+ });
65
+
66
+ test('preflight accepts wall openings without standalone windows', () => {
67
+ const code = `
68
+ const h = bim.create.project({ Name: "Window House" });
69
+ const s0 = bim.create.addIfcBuildingStorey(h, { Name: "Level 0", Elevation: 0 });
70
+ bim.create.addIfcWall(h, s0, {
71
+ Start: [0, 0, 0],
72
+ End: [5, 0, 0],
73
+ Thickness: 0.2,
74
+ Height: 3,
75
+ Openings: [
76
+ { Width: 1.2, Height: 1.2, Position: [2.5, 0, 1.0] },
77
+ ],
78
+ });
79
+ `;
80
+ const errors = validateScriptPreflight(code);
81
+ assert.deepEqual(errors, []);
82
+ });
83
+
84
+ test('preflight accepts hosted wall window helper usage', () => {
85
+ const code = `
86
+ const h = bim.create.project({ Name: "Window House" });
87
+ const s0 = bim.create.addIfcBuildingStorey(h, { Name: "Level 0", Elevation: 0 });
88
+ const wall = bim.create.addIfcWall(h, s0, {
89
+ Start: [0, 0, 0],
90
+ End: [5, 0, 0],
91
+ Thickness: 0.2,
92
+ Height: 3,
93
+ });
94
+ bim.create.addIfcWallWindow(h, wall, {
95
+ Position: [2.5, 0, 1.0],
96
+ Width: 1.2,
97
+ Height: 1.2,
98
+ });
99
+ `;
100
+ const errors = validateScriptPreflight(code);
101
+ assert.deepEqual(errors, []);
102
+ });
103
+
104
+ test('preflight emits one targeted diagnostic per standalone opening call', () => {
105
+ const diagnostics = validateScriptPreflightDetailed(`
106
+ const h = bim.create.project({ Name: "Window House" });
107
+ const s0 = bim.create.addIfcBuildingStorey(h, { Name: "Level 0", Elevation: 0 });
108
+ bim.create.addIfcWall(h, s0, {
109
+ Start: [0, 0, 0],
110
+ End: [5, 0, 0],
111
+ Thickness: 0.2,
112
+ Height: 3,
113
+ });
114
+ bim.create.addIfcDoor(h, s0, {
115
+ Position: [1.0, 0, 0],
116
+ Width: 1.0,
117
+ Height: 2.1,
118
+ });
119
+ bim.create.addIfcWindow(h, s0, {
120
+ Position: [2.5, 0, 1.0],
121
+ Width: 1.2,
122
+ Height: 1.2,
123
+ });
124
+ `);
125
+
126
+ const openingDiagnostics = diagnostics.filter((diagnostic) => diagnostic.code === 'wall_hosted_opening_pattern');
127
+ assert.equal(openingDiagnostics.length, 2);
128
+ assert.deepEqual(
129
+ openingDiagnostics.map((diagnostic) => diagnostic.data?.methodName).sort(),
130
+ ['addIfcDoor', 'addIfcWindow'],
131
+ );
132
+ assert.ok(openingDiagnostics.every((diagnostic) => diagnostic.rootCauseKey === 'placement_context_mismatch'));
133
+ assert.ok(openingDiagnostics.every((diagnostic) => diagnostic.repairScope === 'block'));
134
+ assert.ok(openingDiagnostics.every((diagnostic) => typeof diagnostic.data?.snippet === 'string'));
135
+ assert.ok(openingDiagnostics.every((diagnostic) => diagnostic.data?.range));
136
+ });
137
+
138
+ test('preflight can target an unterminated standalone opening fragment', () => {
139
+ const diagnostics = validateScriptPreflightDetailed(`
140
+ const h = bim.create.project({ Name: "Broken" });
141
+ const s0 = bim.create.addIfcBuildingStorey(h, { Name: "Level 0", Elevation: 0 });
142
+ bim.create.addIfcWall(h, s0, {
143
+ Start: [0, 0, 0],
144
+ End: [5, 0, 0],
145
+ Thickness: 0.2,
146
+ Height: 3,
147
+ });
148
+ bim.create.addIfcDoor(h, s0, {
149
+ Name: "Front Door",
150
+ // truncated repair debris
151
+ `);
152
+
153
+ const openingDiagnostic = diagnostics.find((diagnostic) => diagnostic.code === 'wall_hosted_opening_pattern');
154
+ assert.equal(openingDiagnostic?.data?.methodName, 'addIfcDoor');
155
+ assert.equal(openingDiagnostic?.data?.unterminated, true);
156
+ assert.match(String(openingDiagnostic?.data?.snippet ?? ''), /addIfcDoor/);
157
+ });
158
+
159
+ test('preflight rejects addIfcPlate slab-style contract misuse', () => {
160
+ const code = `
161
+ const h = bim.create.project({ Name: "Facade" });
162
+ const s0 = bim.create.addIfcBuildingStorey(h, { Name: "Level 0", Elevation: 0 });
163
+ bim.create.addIfcPlate(h, s0, {
164
+ Position: [0, 0, 0],
165
+ Width: 2.8,
166
+ Height: 3.0,
167
+ Thickness: 0.08,
168
+ });
169
+ `;
170
+ const errors = validateScriptPreflight(code);
171
+ assert.ok(errors.some((error) => error.includes('uses `Depth` and `Thickness`, not `Height`')));
172
+ assert.ok(errors.some((error) => error.includes('missing required key(s): `Depth`')));
173
+ });
174
+
175
+ test('preflight warns when repeated world-placement methods stay at ground level in a storey loop', () => {
176
+ const code = `
177
+ const h = bim.create.project({ Name: "Tower" });
178
+ const storeyHeight = 3.5;
179
+ const storeyCount = 10;
180
+ for (let i = 0; i < storeyCount; i++) {
181
+ const elevation = i * storeyHeight;
182
+ const storey = bim.create.addIfcBuildingStorey(h, { Name: "Level " + i, Elevation: elevation });
183
+ bim.create.addIfcCurtainWall(h, storey, {
184
+ Start: [0, -0.2, 0],
185
+ End: [30, -0.2, 0],
186
+ Height: storeyHeight,
187
+ Thickness: 0.15,
188
+ });
189
+ }
190
+ `;
191
+ const errors = validateScriptPreflight(code);
192
+ assert.ok(errors.some((error) => error.includes('Suspicious multi-level placement')));
193
+ });
194
+
195
+ test('preflight warns when material is queried via property sets', () => {
196
+ const code = `
197
+ const entities = bim.query.all();
198
+ for (const entity of entities) {
199
+ const material = bim.query.property(entity, "Pset_MaterialCommon", "Material")
200
+ ?? bim.query.property(entity, "Material", "Name");
201
+ console.log(material);
202
+ }
203
+ `;
204
+ const errors = validateScriptPreflight(code);
205
+ assert.ok(errors.some((error) => error.includes('Prefer `bim.query.materials(entity)`')));
206
+ });
207
+
208
+ test('preflight returns structured diagnostics with codes', () => {
209
+ const diagnostics = validateScriptPreflightDetailed(`
210
+ const entities = bim.query.all();
211
+ for (const entity of entities) {
212
+ const material = bim.query.property(entity, "Pset_MaterialCommon", "Material");
213
+ console.log(material);
214
+ }
215
+ `);
216
+
217
+ assert.ok(diagnostics.some((diagnostic) => diagnostic.code === 'metadata_query_pattern'));
218
+ assert.ok(diagnostics.some((diagnostic) => diagnostic.source === 'preflight'));
219
+ });
220
+
221
+ test('preflight diagnostics include method metadata when available', () => {
222
+ const diagnostics = validateScriptPreflightDetailed(`
223
+ const h = bim.create.project({ Name: "Facade" });
224
+ const s0 = bim.create.addIfcBuildingStorey(h, { Name: "Level 0", Elevation: 0 });
225
+ bim.create.addIfcPlate(h, s0, {
226
+ Position: [0, 0, 0],
227
+ Width: 2.8,
228
+ Height: 3.0,
229
+ Thickness: 0.08,
230
+ });
231
+ `);
232
+
233
+ const contractDiagnostic = diagnostics.find((diagnostic) => diagnostic.code === 'create_contract');
234
+ assert.equal(contractDiagnostic?.data?.methodName, 'addIfcPlate');
235
+ });
236
+
237
+ test('preflight warns on detached snippet scope for common facade variables', () => {
238
+ const code = `
239
+ for (let x = 0; x < width; x += 3) {
240
+ bim.create.addIfcMember(h, storey, {
241
+ Start: [x, -0.2, z],
242
+ End: [x, -0.2, z + 3.5],
243
+ Width: 0.25,
244
+ Height: 0.15,
245
+ });
246
+ }
247
+ `;
248
+ const errors = validateScriptPreflight(code);
249
+ assert.ok(errors.some((error) => error.includes('reference `h`')));
250
+ assert.ok(errors.some((error) => error.includes('reference `storey`')));
251
+ assert.ok(errors.some((error) => error.includes('references `width`')));
252
+ assert.ok(errors.some((error) => error.includes('references `z`')));
253
+ });
254
+
255
+ test('preflight detached snippet diagnostics carry structural repair metadata and evidence', () => {
256
+ const diagnostics = validateScriptPreflightDetailed(`
257
+ for (let x = 0; x < width; x += 3) {
258
+ bim.create.addIfcMember(h, storey, {
259
+ Start: [x, -0.2, z],
260
+ End: [x, -0.2, z + 3.5],
261
+ Width: 0.25,
262
+ Height: 0.15,
263
+ });
264
+ }
265
+ `);
266
+
267
+ const detached = diagnostics.filter((diagnostic) => diagnostic.code === 'detached_snippet_scope');
268
+ assert.ok(detached.length >= 3);
269
+ assert.ok(detached.every((diagnostic) => diagnostic.rootCauseKey === 'detached_fragment_rewrite'));
270
+ assert.ok(detached.every((diagnostic) => diagnostic.repairScope === 'structural'));
271
+ assert.ok(detached.every((diagnostic) => diagnostic.evidence?.length));
272
+ });
273
+
274
+ test('preflight does not flag destructured loop bindings in complete rewrite scripts as detached snippets', () => {
275
+ const diagnostics = validateScriptPreflightDetailed(`
276
+ const h = bim.create.project({ Name: "3-Story House" });
277
+
278
+ const storyHeight = 3.0;
279
+ const floorThickness = 0.3;
280
+ const wallThickness = 0.25;
281
+ const roofThickness = 0.25;
282
+ const windowWidth = 1.2;
283
+ const windowHeight = 1.5;
284
+ const windowSillHeight = 0.9;
285
+
286
+ const s0 = bim.create.addIfcBuildingStorey(h, { Name: "Ground Floor", Elevation: 0 });
287
+ const s1 = bim.create.addIfcBuildingStorey(h, { Name: "First Floor", Elevation: storyHeight });
288
+ const s2 = bim.create.addIfcBuildingStorey(h, { Name: "Second Floor", Elevation: storyHeight * 2 });
289
+
290
+ for (const [i, storey] of [s0, s1, s2].entries()) {
291
+ bim.create.addIfcWall(h, storey, {
292
+ Name: "North Wall Level " + i,
293
+ Start: [0, 0, 0],
294
+ End: [10, 0, 0],
295
+ Height: storyHeight,
296
+ Thickness: wallThickness,
297
+ Openings: [
298
+ {
299
+ Position: [2.5, 0, windowSillHeight],
300
+ Width: windowWidth,
301
+ Height: windowHeight
302
+ }
303
+ ]
304
+ });
305
+ }
306
+
307
+ bim.create.addIfcGableRoof(h, s2, {
308
+ Name: "Gable Roof",
309
+ Position: [0, 0, storyHeight],
310
+ Width: 10,
311
+ Depth: 8,
312
+ Thickness: roofThickness,
313
+ Slope: 30 * Math.PI / 180,
314
+ Overhang: 0.5
315
+ });
316
+
317
+ const result = bim.create.toIfc(h);
318
+ bim.model.loadIfc(result.content, "3-story-house-with-windows.ifc");
319
+ `);
320
+
321
+ const detached = diagnostics.filter((diagnostic) => diagnostic.code === 'detached_snippet_scope');
322
+ assert.equal(detached.length, 0);
323
+ });
324
+
325
+ test('preflight does not flag object destructuring or arrow params as detached snippets', () => {
326
+ const diagnostics = validateScriptPreflightDetailed(`
327
+ const h = bim.create.project({ Name: "Facade" });
328
+ const storey = bim.create.addIfcBuildingStorey(h, { Name: "Level 0", Elevation: 0 });
329
+ const footprint = { width: 10, depth: 8 };
330
+ const { width, depth } = footprint;
331
+ const buildWall = (z) => {
332
+ bim.create.addIfcWall(h, storey, {
333
+ Start: [0, 0, z],
334
+ End: [width, 0, z],
335
+ Height: 3,
336
+ Thickness: 0.25,
337
+ });
338
+ };
339
+ buildWall(0);
340
+
341
+ const result = bim.create.toIfc(h);
342
+ bim.model.loadIfc(result.content, "facade.ifc");
343
+ `);
344
+
345
+ const detached = diagnostics.filter((diagnostic) => diagnostic.code === 'detached_snippet_scope');
346
+ assert.equal(detached.length, 0);
347
+ });
348
+
349
+ test('preflight rejects unsupported rotation keys on windows', () => {
350
+ const code = `
351
+ const h = bim.create.project({ Name: "Rotated Window" });
352
+ const s0 = bim.create.addIfcBuildingStorey(h, { Name: "Level 0", Elevation: 0 });
353
+ bim.create.addIfcWindow(h, s0, {
354
+ Position: [2.5, 0, 1.0],
355
+ Width: 1.2,
356
+ Height: 1.2,
357
+ Rotation: Math.PI / 2,
358
+ });
359
+ `;
360
+ const errors = validateScriptPreflight(code);
361
+ assert.ok(errors.some((error) => error.includes('does not support rotation')));
362
+ });
363
+
364
+ test('preflight rejects unsupported rotation keys on hosted wall windows', () => {
365
+ const code = `
366
+ const h = bim.create.project({ Name: "Rotated Hosted Window" });
367
+ const s0 = bim.create.addIfcBuildingStorey(h, { Name: "Level 0", Elevation: 0 });
368
+ const wall = bim.create.addIfcWall(h, s0, {
369
+ Start: [0, 0, 0],
370
+ End: [5, 0, 0],
371
+ Thickness: 0.2,
372
+ Height: 3,
373
+ });
374
+ bim.create.addIfcWallWindow(h, wall, {
375
+ Position: [2.5, 0, 1.0],
376
+ Width: 1.2,
377
+ Height: 1.2,
378
+ Rotation: Math.PI / 2,
379
+ });
380
+ `;
381
+ const errors = validateScriptPreflight(code);
382
+ assert.ok(errors.some((error) => error.includes('auto-aligns to the host wall')));
383
+ });
384
+
385
+ test('preflight rejects invalid addElement contract shape', () => {
386
+ const code = `
387
+ const h = bim.create.project({ Name: "Bad Generic" });
388
+ const s0 = bim.create.addIfcBuildingStorey(h, { Name: "Level 0", Elevation: 0 });
389
+ bim.create.addElement(h, s0, {
390
+ Type: "IfcBuildingElementProxy",
391
+ Position: [0, 0, 0],
392
+ Profile: { kind: "rect", xDim: 1, yDim: 1 },
393
+ Height: 3,
394
+ });
395
+ `;
396
+ const errors = validateScriptPreflight(code);
397
+ assert.ok(errors.some((error) => error.includes('uses `IfcType`, not `Type`')));
398
+ assert.ok(errors.some((error) => error.includes('uses `Placement: { Location: [...] }`, not `Position`')));
399
+ assert.ok(errors.some((error) => error.includes('uses `Depth`, not `Height`')));
400
+ assert.ok(errors.some((error) => error.includes('ProfileType')));
401
+ });
402
+
403
+ test('preflight rejects unsupported roof profile geometry', () => {
404
+ const code = `
405
+ const h = bim.create.project({ Name: "Roof" });
406
+ const s0 = bim.create.addIfcBuildingStorey(h, { Name: "Level 0", Elevation: 0 });
407
+ bim.create.addIfcRoof(h, s0, {
408
+ Name: "Bad Roof",
409
+ Position: [0, 0, 3],
410
+ Profile: [[0, 0], [1, 0], [1, 1]],
411
+ Thickness: 0.2,
412
+ Width: 10,
413
+ Depth: 8,
414
+ });
415
+ `;
416
+ const errors = validateScriptPreflight(code);
417
+ assert.ok(errors.some((error) => error.includes('does not support `Profile`')));
418
+ });
419
+
420
+ test('preflight rejects roof slopes that look like degrees', () => {
421
+ const code = `
422
+ const h = bim.create.project({ Name: "Roof Degrees" });
423
+ const s0 = bim.create.addIfcBuildingStorey(h, { Name: "Level 0", Elevation: 0 });
424
+ bim.create.addIfcRoof(h, s0, {
425
+ Name: "Bad Roof",
426
+ Position: [0, 0, 3],
427
+ Width: 10,
428
+ Depth: 8,
429
+ Thickness: 0.2,
430
+ Slope: 15,
431
+ });
432
+ `;
433
+ const errors = validateScriptPreflight(code);
434
+ assert.ok(errors.some((error) => error.includes('convert them first')));
435
+ });
436
+
437
+ test('preflight steers gable intent to addIfcGableRoof', () => {
438
+ const code = `
439
+ const h = bim.create.project({ Name: "Gable" });
440
+ const s0 = bim.create.addIfcBuildingStorey(h, { Name: "Level 0", Elevation: 0 });
441
+ bim.create.addIfcRoof(h, s0, {
442
+ Name: "Main Gable Roof",
443
+ Position: [0, 0, 3],
444
+ Width: 10,
445
+ Depth: 8,
446
+ Thickness: 0.2,
447
+ Slope: Math.PI / 12,
448
+ });
449
+ `;
450
+ const errors = validateScriptPreflight(code);
451
+ assert.ok(errors.some((error) => error.includes('addIfcGableRoof')));
452
+ });
453
+
454
+ test('preflight accepts valid gable roof helper usage', () => {
455
+ const code = `
456
+ const h = bim.create.project({ Name: "Gable" });
457
+ const s0 = bim.create.addIfcBuildingStorey(h, { Name: "Level 0", Elevation: 0 });
458
+ bim.create.addIfcGableRoof(h, s0, {
459
+ Name: "Main Roof",
460
+ Position: [0, 0, 3],
461
+ Width: 10,
462
+ Depth: 8,
463
+ Thickness: 0.2,
464
+ Slope: Math.PI / 12,
465
+ Overhang: 0.3,
466
+ });
467
+ `;
468
+ const errors = validateScriptPreflight(code);
469
+ assert.deepEqual(errors, []);
470
+ });
471
+
472
+ test('preflight rejects slab missing footprint and profile', () => {
473
+ const code = `
474
+ const h = bim.create.project({ Name: "Slab" });
475
+ const s0 = bim.create.addIfcBuildingStorey(h, { Name: "Level 0", Elevation: 0 });
476
+ bim.create.addIfcSlab(h, s0, {
477
+ Position: [0, 0, 0],
478
+ Thickness: 0.3,
479
+ });
480
+ `;
481
+ const errors = validateScriptPreflight(code);
482
+ assert.ok(errors.some((error) => error.includes('requires one of')));
483
+ });
484
+
485
+ test('preflight rejects zero-length axis geometry', () => {
486
+ const code = `
487
+ const h = bim.create.project({ Name: "Axis" });
488
+ const s0 = bim.create.addIfcBuildingStorey(h, { Name: "Level 0", Elevation: 0 });
489
+ bim.create.addIfcWall(h, s0, {
490
+ Start: [0, 0, 0],
491
+ End: [0, 0, 0],
492
+ Thickness: 0.2,
493
+ Height: 3,
494
+ });
495
+ `;
496
+ const errors = validateScriptPreflight(code);
497
+ assert.ok(errors.some((error) => error.includes('non-zero axis')));
498
+ });
499
+
500
+ test('preflight rejects suspicious bare identifier values', () => {
501
+ const code = `
502
+ const h = bim.create.project({ Name: "Bare" });
503
+ const s0 = bim.create.addIfcBuildingStorey(h, { Name: "Level 0", Elevation: 0 });
504
+ bim.create.addIfcColumn(h, s0, {
505
+ Position: Position,
506
+ Width: 0.4,
507
+ Depth: 0.4,
508
+ Height: 3,
509
+ });
510
+ `;
511
+ const errors = validateScriptPreflight(code);
512
+ assert.ok(errors.some((error) => error.includes('Suspicious bare identifier value `Position`')));
513
+ });