@ifc-lite/parser 5.2.0 → 6.1.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 (151) hide show
  1. package/dist/columnar-entity-preparation.d.ts +3 -0
  2. package/dist/columnar-entity-preparation.d.ts.map +1 -1
  3. package/dist/columnar-entity-preparation.js +101 -11
  4. package/dist/columnar-entity-preparation.js.map +1 -1
  5. package/dist/columnar-parser-indexes.d.ts +17 -1
  6. package/dist/columnar-parser-indexes.d.ts.map +1 -1
  7. package/dist/columnar-parser-indexes.js +31 -21
  8. package/dist/columnar-parser-indexes.js.map +1 -1
  9. package/dist/columnar-parser.d.ts +10 -1
  10. package/dist/columnar-parser.d.ts.map +1 -1
  11. package/dist/columnar-parser.js +14 -2
  12. package/dist/columnar-parser.js.map +1 -1
  13. package/dist/cost-extractor.d.ts +10 -0
  14. package/dist/cost-extractor.d.ts.map +1 -0
  15. package/dist/cost-extractor.js +369 -0
  16. package/dist/cost-extractor.js.map +1 -0
  17. package/dist/cost-schedule-2x3.d.ts +31 -0
  18. package/dist/cost-schedule-2x3.d.ts.map +1 -0
  19. package/dist/cost-schedule-2x3.js +75 -0
  20. package/dist/cost-schedule-2x3.js.map +1 -0
  21. package/dist/cost-types.d.ts +105 -0
  22. package/dist/cost-types.d.ts.map +1 -0
  23. package/dist/cost-types.js +5 -0
  24. package/dist/cost-types.js.map +1 -0
  25. package/dist/data-store-transport.d.ts +1 -0
  26. package/dist/data-store-transport.d.ts.map +1 -1
  27. package/dist/data-store-transport.js +5 -29
  28. package/dist/data-store-transport.js.map +1 -1
  29. package/dist/drop-census.d.ts +136 -0
  30. package/dist/drop-census.d.ts.map +1 -0
  31. package/dist/drop-census.js +53 -0
  32. package/dist/drop-census.js.map +1 -0
  33. package/dist/entity-scanner.d.ts.map +1 -1
  34. package/dist/entity-scanner.js +10 -5
  35. package/dist/entity-scanner.js.map +1 -1
  36. package/dist/generated/ifc2x3/schema-registry.d.ts +50 -0
  37. package/dist/generated/ifc2x3/schema-registry.d.ts.map +1 -0
  38. package/dist/generated/ifc2x3/schema-registry.js +50052 -0
  39. package/dist/generated/ifc2x3/schema-registry.js.map +1 -0
  40. package/dist/generated/ifc4x3/schema-registry.d.ts +50 -0
  41. package/dist/generated/ifc4x3/schema-registry.d.ts.map +1 -0
  42. package/dist/generated/ifc4x3/schema-registry.js +71299 -0
  43. package/dist/generated/ifc4x3/schema-registry.js.map +1 -0
  44. package/dist/generated/schema-registry-by-version.d.ts +61 -0
  45. package/dist/generated/schema-registry-by-version.d.ts.map +1 -0
  46. package/dist/generated/schema-registry-by-version.js +86 -0
  47. package/dist/generated/schema-registry-by-version.js.map +1 -0
  48. package/dist/group-assignment-factor.d.ts +13 -0
  49. package/dist/group-assignment-factor.d.ts.map +1 -0
  50. package/dist/group-assignment-factor.js +30 -0
  51. package/dist/group-assignment-factor.js.map +1 -0
  52. package/dist/ifczip.d.ts +6 -0
  53. package/dist/ifczip.d.ts.map +1 -1
  54. package/dist/ifczip.js +20 -11
  55. package/dist/ifczip.js.map +1 -1
  56. package/dist/index.d.ts +7 -2
  57. package/dist/index.d.ts.map +1 -1
  58. package/dist/index.js +13 -2
  59. package/dist/index.js.map +1 -1
  60. package/dist/on-demand-extractors.d.ts +1 -0
  61. package/dist/on-demand-extractors.d.ts.map +1 -1
  62. package/dist/on-demand-extractors.js +2 -1
  63. package/dist/on-demand-extractors.js.map +1 -1
  64. package/dist/property-value-parser.d.ts +10 -0
  65. package/dist/property-value-parser.d.ts.map +1 -1
  66. package/dist/property-value-parser.js +40 -1
  67. package/dist/property-value-parser.js.map +1 -1
  68. package/dist/scan-entities-balanced.d.ts.map +1 -1
  69. package/dist/scan-entities-balanced.js +8 -6
  70. package/dist/scan-entities-balanced.js.map +1 -1
  71. package/dist/scan-worker-lexing.d.ts +14 -0
  72. package/dist/scan-worker-lexing.d.ts.map +1 -0
  73. package/dist/scan-worker-lexing.js +101 -0
  74. package/dist/scan-worker-lexing.js.map +1 -0
  75. package/dist/scan-worker-source.d.ts +1 -1
  76. package/dist/scan-worker-source.d.ts.map +1 -1
  77. package/dist/scan-worker-source.js +61 -59
  78. package/dist/scan-worker-source.js.map +1 -1
  79. package/dist/schedule-extractor.d.ts +2 -80
  80. package/dist/schedule-extractor.d.ts.map +1 -1
  81. package/dist/schedule-extractor.js +100 -225
  82. package/dist/schedule-extractor.js.map +1 -1
  83. package/dist/schedule-serializer-builders.d.ts +28 -0
  84. package/dist/schedule-serializer-builders.d.ts.map +1 -0
  85. package/dist/schedule-serializer-builders.js +163 -0
  86. package/dist/schedule-serializer-builders.js.map +1 -0
  87. package/dist/schedule-serializer.d.ts +6 -0
  88. package/dist/schedule-serializer.d.ts.map +1 -1
  89. package/dist/schedule-serializer.js +26 -165
  90. package/dist/schedule-serializer.js.map +1 -1
  91. package/dist/schedule-types.d.ts +211 -0
  92. package/dist/schedule-types.d.ts.map +1 -0
  93. package/dist/schedule-types.js +221 -0
  94. package/dist/schedule-types.js.map +1 -0
  95. package/dist/spatial-hierarchy-ambiguity.d.ts +39 -0
  96. package/dist/spatial-hierarchy-ambiguity.d.ts.map +1 -0
  97. package/dist/spatial-hierarchy-ambiguity.js +25 -0
  98. package/dist/spatial-hierarchy-ambiguity.js.map +1 -0
  99. package/dist/spatial-hierarchy-attributes.d.ts +54 -0
  100. package/dist/spatial-hierarchy-attributes.d.ts.map +1 -0
  101. package/dist/spatial-hierarchy-attributes.js +145 -0
  102. package/dist/spatial-hierarchy-attributes.js.map +1 -0
  103. package/dist/spatial-hierarchy-builder.d.ts +0 -30
  104. package/dist/spatial-hierarchy-builder.d.ts.map +1 -1
  105. package/dist/spatial-hierarchy-builder.js +45 -167
  106. package/dist/spatial-hierarchy-builder.js.map +1 -1
  107. package/dist/spatial-hierarchy-canonical-parent.d.ts +52 -2
  108. package/dist/spatial-hierarchy-canonical-parent.d.ts.map +1 -1
  109. package/dist/spatial-hierarchy-canonical-parent.js +153 -4
  110. package/dist/spatial-hierarchy-canonical-parent.js.map +1 -1
  111. package/dist/step-lexing.d.ts +1 -1
  112. package/dist/step-lexing.d.ts.map +1 -1
  113. package/dist/step-lexing.js +1 -56
  114. package/dist/step-lexing.js.map +1 -1
  115. package/dist/step-record-boundary.d.ts +28 -0
  116. package/dist/step-record-boundary.d.ts.map +1 -0
  117. package/dist/step-record-boundary.js +108 -0
  118. package/dist/step-record-boundary.js.map +1 -0
  119. package/dist/step-record-boundary.vectors.d.ts +58 -0
  120. package/dist/step-record-boundary.vectors.d.ts.map +1 -0
  121. package/dist/step-record-boundary.vectors.js +80 -0
  122. package/dist/step-record-boundary.vectors.js.map +1 -0
  123. package/dist/structural-extractor.d.ts +9 -0
  124. package/dist/structural-extractor.d.ts.map +1 -0
  125. package/dist/structural-extractor.js +310 -0
  126. package/dist/structural-extractor.js.map +1 -0
  127. package/dist/structural-load-extractor.d.ts +147 -0
  128. package/dist/structural-load-extractor.d.ts.map +1 -0
  129. package/dist/structural-load-extractor.js +178 -0
  130. package/dist/structural-load-extractor.js.map +1 -0
  131. package/dist/structural-step-values.d.ts +51 -0
  132. package/dist/structural-step-values.d.ts.map +1 -0
  133. package/dist/structural-step-values.js +109 -0
  134. package/dist/structural-step-values.js.map +1 -0
  135. package/dist/structural-types.d.ts +145 -0
  136. package/dist/structural-types.d.ts.map +1 -0
  137. package/dist/structural-types.js +5 -0
  138. package/dist/structural-types.js.map +1 -0
  139. package/dist/tokenizer.d.ts +11 -5
  140. package/dist/tokenizer.d.ts.map +1 -1
  141. package/dist/tokenizer.js +75 -21
  142. package/dist/tokenizer.js.map +1 -1
  143. package/dist/wasm-init-retry.d.ts +6 -42
  144. package/dist/wasm-init-retry.d.ts.map +1 -1
  145. package/dist/wasm-init-retry.js +6 -99
  146. package/dist/wasm-init-retry.js.map +1 -1
  147. package/dist/wasm-panic-forward.d.ts +7 -63
  148. package/dist/wasm-panic-forward.d.ts.map +1 -1
  149. package/dist/wasm-panic-forward.js +7 -100
  150. package/dist/wasm-panic-forward.js.map +1 -1
  151. package/package.json +5 -4
@@ -11,6 +11,7 @@
11
11
  * function each mirrors.
12
12
  */
13
13
  import { MAX_EXPRESS_ID } from './express-id.js';
14
+ import { WORKER_LEXING } from './scan-worker-lexing.js';
14
15
  /**
15
16
  * Self-contained entity scanner code (runs inside Web Worker).
16
17
  * This is the same algorithm as StepTokenizer.scanEntitiesFast() but
@@ -36,51 +37,7 @@ self.onmessage = function(e) {
36
37
  // (#3395). The worker runs from a Blob URL and cannot import at runtime, so
37
38
  // the bound below is interpolated from express-id.ts when this template is
38
39
  // evaluated -- one home for the number, not a copy that can drift.
39
- // Whether a STEP comment opens at p.
40
- function opensCommentAt(p) {
41
- return p + 1 < len && buf[p] === 0x2F && buf[p + 1] === 0x2A;
42
- }
43
-
44
- // Index just past the '*/' closing the comment at p, or -1 when it never
45
- // closes. Counts the newlines it crosses so line numbers stay in step.
46
- //
47
- // Kept behaviourally identical to skipComment/skipTrivia in step-lexing.ts,
48
- // which this cannot import: the worker source is a string, so this copy of
49
- // the rule has to live here. Comments do not nest, per ISO 10303-21.
50
- function skipCommentAt(p) {
51
- var q = p + 2;
52
- while (q + 1 < len) {
53
- if (buf[q] === 0x2A && buf[q + 1] === 0x2F) return q + 2;
54
- if (buf[q] === 0x0A) line++;
55
- q++;
56
- }
57
- return -1;
58
- }
59
-
60
- // Skip whitespace, comments, and any run of the two -- 10303-21 allows a
61
- // comment wherever whitespace is allowed, INCLUDING inside a record.
62
- // Returns -1 when a comment opens and never closes: everything from there on
63
- // is inside it, so there is nothing left to find.
64
- //
65
- // The whitespace byte set (space, tab, CR, LF, form feed, vertical tab) is
66
- // kept byte-for-byte in sync with isSpaceByte in step-lexing.ts and its
67
- // three inline twins in tokenizer.ts's scanEntitiesFast -- this file is a
68
- // string because a Blob worker cannot import at runtime, not a reason for
69
- // the rule itself to drift.
70
- function skipTriviaAt(p) {
71
- for (;;) {
72
- while (p < len) {
73
- var t = buf[p];
74
- if (t === 0x20 || t === 0x09 || t === 0x0D || t === 0x0C || t === 0x0B) { p++; }
75
- else if (t === 0x0A) { line++; p++; }
76
- else break;
77
- }
78
- if (!opensCommentAt(p)) return p;
79
- var e = skipCommentAt(p);
80
- if (e < 0) return -1;
81
- p = e;
82
- }
83
- }
40
+ ${WORKER_LEXING}
84
41
 
85
42
  var ids = new Uint32Array(estimatedCount);
86
43
  var offsets = new Uint32Array(estimatedCount);
@@ -254,9 +211,22 @@ self.onmessage = function(e) {
254
211
  if (buf[pos] !== 0x28) { declOpen = false; continue; }
255
212
  declOpen = false; // Header complete: '(' found.
256
213
 
257
- // Skip to semicolon (handling strings)
214
+ // Skip to semicolon (handling strings), bounded to THIS record's own
215
+ // body so a record missing its ';' cannot latch onto a later one and
216
+ // swallow what lies between (#4179): the ';' must be preceded, modulo
217
+ // trivia, by the ')' closing the parameter list, and no '=' may come
218
+ // before it outside a string or comment. On either failure the record is
219
+ // DROPPED and the scan resumes at that ')' rather than abandoning the
220
+ // file's tail. close_step_record in rust/core/src/parser/lexical.rs
221
+ // argues both rules and the recovery; this is a hand-duplicated copy
222
+ // because a Blob worker cannot import at runtime. Change them together.
223
+ var parenPos = pos;
258
224
  var inString = false;
259
225
  var foundTerminator = false;
226
+ // Memoised across the exact ';' check and the recovery below, which
227
+ // would otherwise balance the same record twice. -3 = not computed;
228
+ // -1 = unbalanced but readable; -2 = a literal/comment never closed.
229
+ var recordClose = -3;
260
230
  while (pos < len) {
261
231
  var c6 = buf[pos];
262
232
  if (c6 === 0x27) { // quote
@@ -271,15 +241,26 @@ self.onmessage = function(e) {
271
241
  // and parens inside it text -- the other direction of the rule the
272
242
  // quote branch above gives for a '/*' inside a literal.
273
243
  var ce = skipCommentAt(pos);
274
- if (ce < 0) {
275
- // Unterminated: this record has no terminator, and neither has
276
- // anything after it. Drop it and stop.
277
- pos = len;
278
- break;
279
- }
244
+ if (ce < 0) break; // Unterminated: recovery below finds no ')'.
280
245
  pos = ce;
281
246
  continue;
282
247
  } else if (c6 === 0x3B && !inString) { // semicolon
248
+ // ')' modulo whitespace settles it for every record a real file
249
+ // holds. Anything else -- including the '/' of a trailing '*/',
250
+ // which a backwards walk cannot see through -- goes to the cold,
251
+ // exact balance-and-skip-trivia check.
252
+ var w = pos;
253
+ while (w > parenPos && isSpaceByteAt(w - 1)) w--;
254
+ if (buf[w - 1] !== 0x29) {
255
+ var savedBalLine = line;
256
+ recordClose = findEntityLengthAt(parenPos, startOffset);
257
+ line = savedBalLine;
258
+ if (recordClose <= 0) break;
259
+ recordClose += startOffset;
260
+ var after = skipTriviaAt(recordClose);
261
+ line = savedBalLine;
262
+ if (after < 0 || after !== pos) break;
263
+ }
283
264
  var entityLength = pos - startOffset + 1;
284
265
 
285
266
  // Grow if needed
@@ -295,19 +276,40 @@ self.onmessage = function(e) {
295
276
  pos++;
296
277
  foundTerminator = true;
297
278
  break;
279
+ } else if (c6 === 0x3D && !inString) {
280
+ break; // '=': the NEXT declaration already started.
298
281
  } else if (c6 === 0x0A) {
299
282
  line++;
300
283
  }
301
284
  pos++;
302
285
  }
303
286
 
304
- // Ran off the end without an unquoted ';' -- usually an unescaped
305
- // quote left open, or the unterminated-comment break above (mirrors
306
- // tokenizer.ts's scanEntitiesFast). Not resynced: with no known
307
- // terminator, guessing a resume point risks fabricating entities from
308
- // misaligned bytes. Recorded in 'stopped', not incremented here -- see
309
- // the post-loop check below.
310
- if (!foundTerminator) { stopped = true; }
287
+ // No ';' of this record's own. EVERY such exit lands here, so the
288
+ // recovery lives here once rather than at each break -- mirrors
289
+ // tokenizer.ts's scanEntitiesFast. Resume at the ')' balancing this
290
+ // record's own '(' when there is one, dropping just this record;
291
+ // otherwise run to len, ending the scan un-resynced rather than guessing
292
+ // a resume point from misaligned bytes.
293
+ if (!foundTerminator) {
294
+ stopped = true;
295
+ if (recordClose === -3) {
296
+ var savedRecLine = line;
297
+ recordClose = findEntityLengthAt(parenPos, startOffset);
298
+ line = savedRecLine;
299
+ if (recordClose > 0) recordClose += startOffset;
300
+ }
301
+ if (recordClose > 0) {
302
+ pos = recordClose;
303
+ line = startLine;
304
+ for (var q = startOffset; q < pos; q++) if (buf[q] === 0x0A) line++;
305
+ } else if (recordClose === -1) {
306
+ // Unbalanced but readable: re-hunt from past this record's '#'.
307
+ pos = startOffset + 1;
308
+ line = startLine;
309
+ } else {
310
+ pos = len; // Nothing to resume from (#3695).
311
+ }
312
+ }
311
313
  } else if (ch === 0x0A) {
312
314
  line++;
313
315
  pos++;
@@ -1 +1 @@
1
- {"version":3,"file":"scan-worker-source.js","sourceRoot":"","sources":["../src/scan-worker-source.ts"],"names":[],"mappings":"AAAA;;+DAE+D;AAE/D;;;;;;;;GAQG;AAEH,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAEjD;;;;GAIG;AACH;4DAC4D;AAC5D,MAAM,CAAC,MAAM,WAAW,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;wBAwKH,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAiMrC,CAAC"}
1
+ {"version":3,"file":"scan-worker-source.js","sourceRoot":"","sources":["../src/scan-worker-source.ts"],"names":[],"mappings":"AAAA;;+DAE+D;AAE/D;;;;;;;;GAQG;AAEH,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAExD;;;;GAIG;AACH;4DAC4D;AAC5D,MAAM,CAAC,MAAM,WAAW,GAAG;;;;;;;;;;;;;;;;;;EAkBzB,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;wBA0GS,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8OrC,CAAC"}
@@ -1,86 +1,8 @@
1
1
  import type { IfcDataStore } from './columnar-parser.js';
2
2
  import { parseIso8601Duration } from './iso8601-duration.js';
3
+ import type { SequenceTypeEnum, TaskDurationType, ScheduleTaskTimeInfo, ScheduleTaskInfo, ScheduleSequenceInfo, WorkScheduleInfo, ScheduleExtraction } from './schedule-types.js';
3
4
  export { parseIso8601Duration };
4
- export type SequenceTypeEnum = 'START_START' | 'START_FINISH' | 'FINISH_START' | 'FINISH_FINISH' | 'USERDEFINED' | 'NOTDEFINED';
5
- export type TaskDurationType = 'WORKTIME' | 'ELAPSEDTIME' | 'NOTDEFINED';
6
- export interface ScheduleTaskTimeInfo {
7
- scheduleStart?: string;
8
- scheduleFinish?: string;
9
- scheduleDuration?: string;
10
- actualStart?: string;
11
- actualFinish?: string;
12
- actualDuration?: string;
13
- earlyStart?: string;
14
- earlyFinish?: string;
15
- lateStart?: string;
16
- lateFinish?: string;
17
- freeFloat?: string;
18
- totalFloat?: string;
19
- remainingTime?: string;
20
- durationType?: TaskDurationType;
21
- statusTime?: string;
22
- isCritical?: boolean;
23
- completion?: number;
24
- }
25
- export interface ScheduleTaskInfo {
26
- expressId: number;
27
- globalId: string;
28
- name: string;
29
- description?: string;
30
- objectType?: string;
31
- identification?: string;
32
- longDescription?: string;
33
- status?: string;
34
- workMethod?: string;
35
- isMilestone: boolean;
36
- priority?: number;
37
- predefinedType?: string;
38
- taskTime?: ScheduleTaskTimeInfo;
39
- /** Parent task globalId (from IfcRelNests where this task is in RelatedObjects). */
40
- parentGlobalId?: string;
41
- /** Child task globalIds (from IfcRelNests where this task is RelatingObject). */
42
- childGlobalIds: string[];
43
- /** expressIds of products assigned to this task via IfcRelAssignsToProcess. */
44
- productExpressIds: number[];
45
- /** globalIds of the same products (aligned with productExpressIds by index). */
46
- productGlobalIds: string[];
47
- /** WorkSchedule globalIds that control this task via IfcRelAssignsToControl. */
48
- controllingScheduleGlobalIds: string[];
49
- }
50
- export interface ScheduleSequenceInfo {
51
- globalId: string;
52
- relatingTaskGlobalId: string;
53
- relatedTaskGlobalId: string;
54
- sequenceType: SequenceTypeEnum;
55
- userDefinedSequenceType?: string;
56
- /** Lag value expressed in seconds if it resolves to an IfcDuration; otherwise undefined. */
57
- timeLagSeconds?: number;
58
- /** Original lag duration string (ISO 8601 like 'P1D'), if available. */
59
- timeLagDuration?: string;
60
- }
61
- export interface WorkScheduleInfo {
62
- expressId: number;
63
- globalId: string;
64
- kind: 'WorkSchedule' | 'WorkPlan';
65
- name: string;
66
- description?: string;
67
- identification?: string;
68
- creationDate?: string;
69
- purpose?: string;
70
- duration?: string;
71
- startTime?: string;
72
- finishTime?: string;
73
- predefinedType?: string;
74
- /** Root task globalIds directly assigned via IfcRelAssignsToControl. */
75
- taskGlobalIds: string[];
76
- }
77
- export interface ScheduleExtraction {
78
- workSchedules: WorkScheduleInfo[];
79
- tasks: ScheduleTaskInfo[];
80
- sequences: ScheduleSequenceInfo[];
81
- /** True if we encountered any scheduling entity (useful for empty-state UI). */
82
- hasSchedule: boolean;
83
- }
5
+ export type { SequenceTypeEnum, TaskDurationType, ScheduleTaskTimeInfo, ScheduleTaskInfo, ScheduleSequenceInfo, WorkScheduleInfo, ScheduleExtraction, };
84
6
  /**
85
7
  * Extract all scheduling data from a parsed IFC store.
86
8
  *
@@ -1 +1 @@
1
- {"version":3,"file":"schedule-extractor.d.ts","sourceRoot":"","sources":["../src/schedule-extractor.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"schedule-extractor.d.ts","sourceRoot":"","sources":["../src/schedule-extractor.ts"],"names":[],"mappings":"AAuBA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AACzD,OAAO,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAC;AAoB7D,OAAO,KAAK,EACV,gBAAgB,EAChB,gBAAgB,EAChB,oBAAoB,EACpB,gBAAgB,EAChB,oBAAoB,EACpB,gBAAgB,EAChB,kBAAkB,EACnB,MAAM,qBAAqB,CAAC;AAQ7B,OAAO,EAAE,oBAAoB,EAAE,CAAC;AAKhC,YAAY,EACV,gBAAgB,EAChB,gBAAgB,EAChB,oBAAoB,EACpB,gBAAgB,EAChB,oBAAoB,EACpB,gBAAgB,EAChB,kBAAkB,GACnB,CAAC;AAEF;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,YAAY,GAAG,kBAAkB,CA4T/E"}
@@ -2,17 +2,25 @@
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
4
  /**
5
- * Schedule (4D) extractor — parses IfcTask, IfcTaskTime, IfcRelSequence,
5
+ * Schedule (4D) extractor — walks IfcTask, IfcTaskTime, IfcRelSequence,
6
6
  * IfcRelAssignsToProcess, IfcRelAssignsToControl, IfcRelNests, IfcWorkSchedule,
7
- * IfcWorkPlan, IfcLagTime from a parsed IfcDataStore and returns a normalized
8
- * ScheduleExtraction that the viewer can drive a Gantt/4D animation from.
7
+ * IfcWorkPlan, IfcLagTime entities in a parsed IfcDataStore and assembles a
8
+ * normalized ScheduleExtraction that the viewer can drive a Gantt/4D
9
+ * animation from.
9
10
  *
10
11
  * Handles IFC4 / IFC4X3. IFC2X3 has a different IfcTask layout (no TaskTime
11
12
  * attribute, ScheduleStart/ScheduleFinish/TaskOwner instead) and is supported
12
13
  * with best-effort degradation.
14
+ *
15
+ * STEP attribute-index layouts, the record types themselves, and the small
16
+ * pure/single-entity helpers (`asString` et al., `extractTaskTime`,
17
+ * `extractLagTimeSeconds`) live in `schedule-types.ts` — this file owns only
18
+ * the cross-entity orchestration (the multi-pass walk below that wires those
19
+ * records together via their IFC relationships).
13
20
  */
14
21
  import { EntityExtractor } from './entity-extractor.js';
15
22
  import { parseIso8601Duration } from './iso8601-duration.js';
23
+ import { TASK_ATTR, TASK_ATTR_2X3, REL_SEQUENCE_ATTR, REL_ASSIGNS_TO_PROCESS_ATTR, REL_ASSIGNS_TO_CONTROL_ATTR, REL_NESTS_ATTR, WORK_SCHEDULE_ATTR, WORK_PLAN_ATTR, asString, asNumber, asBoolean, asEnum, asRef, asRefList, sequenceTypeFromString, extractTaskTime, extractLagTimeSeconds, } from './schedule-types.js';
16
24
  // Re-exported for backward compatibility — this is where consumers
17
25
  // (including this package's own public surface, see index.ts) have always
18
26
  // imported it from. The implementation itself now lives in
@@ -20,222 +28,6 @@ import { parseIso8601Duration } from './iso8601-duration.js';
20
28
  // `secondsToIso8601Duration`, so the round-trip property between the two is
21
29
  // visible in one place.
22
30
  export { parseIso8601Duration };
23
- /** IFC4 STEP attribute indices for IfcTask. */
24
- const TASK_ATTR = {
25
- GlobalId: 0,
26
- Name: 2,
27
- Description: 3,
28
- ObjectType: 4,
29
- Identification: 5,
30
- LongDescription: 6,
31
- Status: 7,
32
- WorkMethod: 8,
33
- IsMilestone: 9,
34
- Priority: 10,
35
- TaskTime: 11,
36
- PredefinedType: 12,
37
- };
38
- /**
39
- * IFC2X3 IfcTask layout — the attributes IfcTask itself adds over IfcObject
40
- * (TaskId, Status, WorkMethod, IsMilestone, Priority). IFC2X3 schedule times
41
- * live on `IfcScheduleTimeControl` and link to IfcTask via `IfcRelAssignsTasks`
42
- * — we don't resolve those here yet; best-effort 2x3 models only surface task
43
- * metadata without dates.
44
- */
45
- const TASK_ATTR_2X3 = {
46
- GlobalId: 0,
47
- Name: 2,
48
- Description: 3,
49
- ObjectType: 4,
50
- TaskId: 5,
51
- Status: 6,
52
- WorkMethod: 7,
53
- IsMilestone: 8,
54
- Priority: 9,
55
- };
56
- const TASK_TIME_ATTR = {
57
- Name: 0,
58
- DurationType: 3,
59
- ScheduleDuration: 4,
60
- ScheduleStart: 5,
61
- ScheduleFinish: 6,
62
- EarlyStart: 7,
63
- EarlyFinish: 8,
64
- LateStart: 9,
65
- LateFinish: 10,
66
- FreeFloat: 11,
67
- TotalFloat: 12,
68
- IsCritical: 13,
69
- StatusTime: 14,
70
- ActualDuration: 15,
71
- ActualStart: 16,
72
- ActualFinish: 17,
73
- RemainingTime: 18,
74
- Completion: 19,
75
- };
76
- const REL_SEQUENCE_ATTR = {
77
- GlobalId: 0,
78
- RelatingProcess: 4,
79
- RelatedProcess: 5,
80
- TimeLag: 6,
81
- SequenceType: 7,
82
- UserDefinedSequenceType: 8,
83
- };
84
- const REL_ASSIGNS_TO_PROCESS_ATTR = {
85
- RelatedObjects: 4,
86
- RelatingProcess: 6,
87
- };
88
- const REL_ASSIGNS_TO_CONTROL_ATTR = {
89
- RelatedObjects: 4,
90
- RelatingControl: 6,
91
- };
92
- const REL_NESTS_ATTR = {
93
- RelatingObject: 4,
94
- RelatedObjects: 5,
95
- };
96
- const WORK_SCHEDULE_ATTR = {
97
- GlobalId: 0,
98
- Name: 2,
99
- Description: 3,
100
- ObjectType: 4,
101
- Identification: 5,
102
- CreationDate: 6,
103
- Purpose: 8,
104
- Duration: 9,
105
- TotalFloat: 10,
106
- StartTime: 11,
107
- FinishTime: 12,
108
- PredefinedType: 13,
109
- };
110
- const WORK_PLAN_ATTR = WORK_SCHEDULE_ATTR;
111
- const LAG_TIME_ATTR = {
112
- LagValue: 3,
113
- DurationType: 4,
114
- };
115
- function asString(v) {
116
- if (typeof v === 'string' && v.length > 0)
117
- return v;
118
- return undefined;
119
- }
120
- function asNumber(v) {
121
- if (typeof v === 'number' && Number.isFinite(v))
122
- return v;
123
- return undefined;
124
- }
125
- function asBoolean(v) {
126
- if (typeof v === 'boolean')
127
- return v;
128
- // STEP booleans are typically stored as '.T.' / '.F.' after parsing
129
- if (typeof v === 'string') {
130
- if (v === '.T.' || v === 'T')
131
- return true;
132
- if (v === '.F.' || v === 'F')
133
- return false;
134
- }
135
- return undefined;
136
- }
137
- function asEnum(v) {
138
- if (typeof v !== 'string' || v.length === 0)
139
- return undefined;
140
- // STEP enum looks like .PLANNED.
141
- const match = v.match(/^\.([A-Z_]+)\.$/);
142
- return match ? match[1] : undefined;
143
- }
144
- function asRef(v) {
145
- if (typeof v === 'number' && Number.isInteger(v) && v > 0)
146
- return v;
147
- return undefined;
148
- }
149
- function asRefList(v) {
150
- if (!Array.isArray(v))
151
- return [];
152
- const out = [];
153
- for (const x of v) {
154
- const id = asRef(x);
155
- if (id !== undefined)
156
- out.push(id);
157
- }
158
- return out;
159
- }
160
- function sequenceTypeFromString(s) {
161
- switch (s) {
162
- case 'START_START':
163
- case 'START_FINISH':
164
- case 'FINISH_START':
165
- case 'FINISH_FINISH':
166
- case 'USERDEFINED':
167
- case 'NOTDEFINED':
168
- return s;
169
- default:
170
- return 'FINISH_START';
171
- }
172
- }
173
- function durationTypeFromString(s) {
174
- switch (s) {
175
- case 'WORKTIME':
176
- case 'ELAPSEDTIME':
177
- case 'NOTDEFINED':
178
- return s;
179
- default:
180
- return undefined;
181
- }
182
- }
183
- function extractTaskTime(extractor, store, taskTimeId) {
184
- const ref = store.entityIndex.byId.get(taskTimeId);
185
- if (!ref)
186
- return undefined;
187
- const entity = extractor.extractEntity(ref);
188
- if (!entity)
189
- return undefined;
190
- const t = entity.type.toUpperCase();
191
- if (t !== 'IFCTASKTIME' && t !== 'IFCTASKTIMERECURRING')
192
- return undefined;
193
- const a = entity.attributes || [];
194
- return {
195
- durationType: durationTypeFromString(asEnum(a[TASK_TIME_ATTR.DurationType])),
196
- scheduleDuration: asString(a[TASK_TIME_ATTR.ScheduleDuration]),
197
- scheduleStart: asString(a[TASK_TIME_ATTR.ScheduleStart]),
198
- scheduleFinish: asString(a[TASK_TIME_ATTR.ScheduleFinish]),
199
- earlyStart: asString(a[TASK_TIME_ATTR.EarlyStart]),
200
- earlyFinish: asString(a[TASK_TIME_ATTR.EarlyFinish]),
201
- lateStart: asString(a[TASK_TIME_ATTR.LateStart]),
202
- lateFinish: asString(a[TASK_TIME_ATTR.LateFinish]),
203
- freeFloat: asString(a[TASK_TIME_ATTR.FreeFloat]),
204
- totalFloat: asString(a[TASK_TIME_ATTR.TotalFloat]),
205
- isCritical: asBoolean(a[TASK_TIME_ATTR.IsCritical]),
206
- statusTime: asString(a[TASK_TIME_ATTR.StatusTime]),
207
- actualDuration: asString(a[TASK_TIME_ATTR.ActualDuration]),
208
- actualStart: asString(a[TASK_TIME_ATTR.ActualStart]),
209
- actualFinish: asString(a[TASK_TIME_ATTR.ActualFinish]),
210
- remainingTime: asString(a[TASK_TIME_ATTR.RemainingTime]),
211
- completion: asNumber(a[TASK_TIME_ATTR.Completion]),
212
- };
213
- }
214
- function extractLagTimeSeconds(extractor, store, lagId) {
215
- const ref = store.entityIndex.byId.get(lagId);
216
- if (!ref)
217
- return {};
218
- const entity = extractor.extractEntity(ref);
219
- if (!entity)
220
- return {};
221
- if (entity.type.toUpperCase() !== 'IFCLAGTIME')
222
- return {};
223
- const a = entity.attributes || [];
224
- const lagValue = a[LAG_TIME_ATTR.LagValue];
225
- // lagValue is an IfcTimeOrRatioSelect — either a typed wrapper like
226
- // ['IFCDURATION', 'P1D'] or a direct ratio number.
227
- if (Array.isArray(lagValue) && lagValue.length === 2) {
228
- const typeName = String(lagValue[0]).toUpperCase();
229
- const inner = lagValue[1];
230
- if (typeName === 'IFCDURATION' && typeof inner === 'string') {
231
- return { seconds: parseIso8601Duration(inner), duration: inner };
232
- }
233
- }
234
- else if (typeof lagValue === 'string') {
235
- return { seconds: parseIso8601Duration(lagValue), duration: lagValue };
236
- }
237
- return {};
238
- }
239
31
  /**
240
32
  * Extract all scheduling data from a parsed IFC store.
241
33
  *
@@ -344,18 +136,27 @@ export function extractScheduleOnDemand(store) {
344
136
  continue;
345
137
  const parentTask = taskByExpressId.get(parent);
346
138
  if (!parentTask)
347
- continue; // nesting over non-task entities ignore
139
+ continue; // parent isn't a task — task/subtask nesting only; IfcWorkPlan -> IfcWorkSchedule nesting is handled in a separate pass below, once schedules are extracted
348
140
  for (const childId of children) {
349
141
  const childTask = taskByExpressId.get(childId);
350
142
  if (!childTask)
351
143
  continue;
352
- parentTask.childGlobalIds.push(childTask.globalId);
144
+ // A source file may repeat the relation; guard as Pass 4b/5 do below.
145
+ if (!parentTask.childGlobalIds.includes(childTask.globalId)) {
146
+ parentTask.childGlobalIds.push(childTask.globalId);
147
+ }
353
148
  if (!childTask.parentGlobalId) {
354
149
  childTask.parentGlobalId = parentTask.globalId;
355
150
  }
356
151
  }
357
152
  }
358
153
  // Pass 3: resolve IfcRelAssignsToProcess — products assigned to tasks.
154
+ // NOT deduped, deliberately: IfcRelAssignsToProcess carries a
155
+ // QuantityInProcess attribute (not extracted here, but real in the
156
+ // schema), so the same product can legitimately repeat across relations
157
+ // to the same task as separate quantity assignments. Unlike the identity
158
+ // lists elsewhere in this file, this pair is a multiset — dedup would
159
+ // silently drop a legitimate repeated assignment.
359
160
  for (const relId of relAssignsProcessIds) {
360
161
  const ref = store.entityIndex.byId.get(relId);
361
162
  if (!ref)
@@ -407,6 +208,7 @@ export function extractScheduleOnDemand(store) {
407
208
  finishTime: asString(a[layout.FinishTime]),
408
209
  predefinedType: asEnum(a[layout.PredefinedType]),
409
210
  taskGlobalIds: [],
211
+ childScheduleGlobalIds: [],
410
212
  };
411
213
  if (globalId)
412
214
  globalIdByExpressId.set(expressId, globalId);
@@ -426,7 +228,59 @@ export function extractScheduleOnDemand(store) {
426
228
  scheduleByExpressId.set(id, info);
427
229
  }
428
230
  }
429
- // Pass 5: IfcRelAssignsToControlmap schedules to tasks.
231
+ // Pass 4b: walk IfcRelNests again IfcWorkPlan nesting IfcWorkSchedule.
232
+ // Real-world files use IfcRelNests for this grouping (confirmed against the
233
+ // buildingSMART IFC4 spec's own reference example), distinct from Pass 2's
234
+ // task/subtask hierarchy above, which only resolves parents through
235
+ // taskByExpressId and silently skips a WorkPlan RelatingObject.
236
+ for (const relId of relNestsIds) {
237
+ const ref = store.entityIndex.byId.get(relId);
238
+ if (!ref)
239
+ continue;
240
+ const entity = extractor.extractEntity(ref);
241
+ if (!entity)
242
+ continue;
243
+ const a = entity.attributes || [];
244
+ const parent = asRef(a[REL_NESTS_ATTR.RelatingObject]);
245
+ const children = asRefList(a[REL_NESTS_ATTR.RelatedObjects]);
246
+ if (parent === undefined)
247
+ continue;
248
+ const parentPlan = scheduleByExpressId.get(parent);
249
+ if (!parentPlan || parentPlan.kind !== 'WorkPlan')
250
+ continue; // not a work-plan nest
251
+ for (const childId of children) {
252
+ const childSchedule = scheduleByExpressId.get(childId);
253
+ if (!childSchedule || childSchedule.kind !== 'WorkSchedule')
254
+ continue;
255
+ // This extractor is the sole producer of `parentPlan`: every
256
+ // WorkScheduleInfo it builds above sets `childScheduleGlobalIds: []`,
257
+ // so the field is never actually absent here. `??=` makes that
258
+ // invariant visible to the checker without a non-null assertion, and
259
+ // without treating "absent" any differently from "empty" — both still
260
+ // mean "no nested schedules yet" everywhere else that reads it.
261
+ // Two distinct IfcRelNests entities can nest the same WorkPlan/
262
+ // WorkSchedule pair (a source file may repeat the relation), so guard
263
+ // the push the same way Pass 5 below guards its own append.
264
+ const childGlobalIds = (parentPlan.childScheduleGlobalIds ??= []);
265
+ if (!childGlobalIds.includes(childSchedule.globalId)) {
266
+ childGlobalIds.push(childSchedule.globalId);
267
+ }
268
+ if (!childSchedule.parentPlanGlobalId) {
269
+ childSchedule.parentPlanGlobalId = parentPlan.globalId;
270
+ }
271
+ }
272
+ }
273
+ // Pass 5: IfcRelAssignsToControl — map schedules to tasks, and (a second,
274
+ // distinct grouping path from Pass 4b's IfcRelNests) IfcWorkPlan grouping
275
+ // IfcWorkSchedule. The SDK's scripting bridge
276
+ // (`assignSchedulesToWorkPlan` in packages/create/src/ifc-creator.ts)
277
+ // emits exactly this relation — RelatingControl the plan, RelatedObjects
278
+ // the schedules — not IfcRelNests, so a plan grouped through that bridge
279
+ // must resolve here too or the SDK-authored grouping never reads back.
280
+ // If a source file expresses the same WorkPlan->WorkSchedule pair through
281
+ // *both* relations, Pass 4b (IfcRelNests) runs first and wins:
282
+ // `parentPlanGlobalId` is set-once (guarded below and in Pass 4b), and
283
+ // `childScheduleGlobalIds` is deduped so the pair is not double-counted.
430
284
  for (const relId of relAssignsControlIds) {
431
285
  const ref = store.entityIndex.byId.get(relId);
432
286
  if (!ref)
@@ -444,10 +298,31 @@ export function extractScheduleOnDemand(store) {
444
298
  continue;
445
299
  for (const objId of objects) {
446
300
  const task = taskByExpressId.get(objId);
447
- if (!task)
301
+ if (task) {
302
+ // A source file may repeat the relation; guard both paired arrays
303
+ // on one predicate to keep them in lockstep. IfcRelAssignsToControl
304
+ // carries no quantity attribute, so this pair is a pure identity
305
+ // edge, unlike Pass 3's productExpressIds/productGlobalIds above.
306
+ if (!schedule.taskGlobalIds.includes(task.globalId)) {
307
+ schedule.taskGlobalIds.push(task.globalId);
308
+ task.controllingScheduleGlobalIds.push(schedule.globalId);
309
+ }
448
310
  continue;
449
- schedule.taskGlobalIds.push(task.globalId);
450
- task.controllingScheduleGlobalIds.push(schedule.globalId);
311
+ }
312
+ // Not a task — check whether this is a WorkPlan grouping a
313
+ // WorkSchedule via IfcRelAssignsToControl instead of IfcRelNests.
314
+ if (schedule.kind !== 'WorkPlan')
315
+ continue;
316
+ const childSchedule = scheduleByExpressId.get(objId);
317
+ if (!childSchedule || childSchedule.kind !== 'WorkSchedule')
318
+ continue;
319
+ const siblings = (schedule.childScheduleGlobalIds ??= []);
320
+ if (!siblings.includes(childSchedule.globalId)) {
321
+ siblings.push(childSchedule.globalId);
322
+ }
323
+ if (!childSchedule.parentPlanGlobalId) {
324
+ childSchedule.parentPlanGlobalId = schedule.globalId;
325
+ }
451
326
  }
452
327
  }
453
328
  // Pass 6: IfcRelSequence — dependency edges between tasks.