@ifc-lite/viewer 1.17.4 → 1.18.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.
- package/.turbo/turbo-build.log +20 -17
- package/.turbo/turbo-typecheck.log +1 -1
- package/CHANGELOG.md +630 -0
- package/DESKTOP_CONTRACT_VERSION +1 -1
- package/dist/assets/{basketViewActivator-BmnNtVfZ.js → basketViewActivator-Cm1QEk_R.js} +1 -1
- package/dist/assets/drawing-2d-DoxKMqbO.js +257 -0
- package/dist/assets/{exporters-ChAtBmlj.js → exporters-B_OBqIyD.js} +3479 -2845
- package/dist/assets/{geometry.worker-BQ0rzNo-.js → geometry.worker-xHHy-9DV.js} +1 -1
- package/dist/assets/ids-DQ5jY0E8.js +1 -0
- package/dist/assets/ifc-lite_bg-ADjKXSms.wasm +0 -0
- package/dist/assets/{index-Co8E2-FE.js → index-BKq-M3Mk.js} +55873 -40593
- package/dist/assets/index-COnQRuqY.css +1 -0
- package/dist/assets/{native-bridge-BRvbckFQ.js → native-bridge-SHXiQwFW.js} +104 -104
- package/dist/assets/sandbox-jez21HtV.js +9627 -0
- package/dist/assets/{server-client-BV8zHZ7Y.js → server-client-ncOQVNso.js} +1 -1
- package/dist/assets/{wasm-bridge-g01g7T9b.js → wasm-bridge-DyfBSB8z.js} +1 -1
- package/dist/index.html +8 -7
- package/index.html +1 -0
- package/package.json +13 -13
- package/src/App.tsx +16 -2
- package/src/apache-arrow.d.ts +30 -0
- package/src/components/viewer/AddElementPanel.tsx +758 -0
- package/src/components/viewer/BulkPropertyEditor.tsx +7 -0
- package/src/components/viewer/CesiumOverlay.tsx +62 -19
- package/src/components/viewer/ChatPanel.tsx +259 -93
- package/src/components/viewer/CommandPalette.tsx +56 -7
- package/src/components/viewer/EntityContextMenu.tsx +168 -4
- package/src/components/viewer/ExportChangesButton.tsx +25 -5
- package/src/components/viewer/ExportDialog.tsx +19 -1
- package/src/components/viewer/MainToolbar.tsx +73 -13
- package/src/components/viewer/PropertiesPanel.tsx +237 -23
- package/src/components/viewer/SearchInline.tsx +669 -0
- package/src/components/viewer/SearchModal.filter.builder.tsx +766 -0
- package/src/components/viewer/SearchModal.filter.tsx +514 -0
- package/src/components/viewer/SearchModal.text.tsx +388 -0
- package/src/components/viewer/SearchModal.tsx +235 -0
- package/src/components/viewer/SettingsPage.tsx +252 -101
- package/src/components/viewer/ThemeSwitch.tsx +63 -7
- package/src/components/viewer/ToolOverlays.tsx +5 -0
- package/src/components/viewer/ViewerLayout.tsx +25 -4
- package/src/components/viewer/Viewport.tsx +25 -3
- package/src/components/viewer/ViewportContainer.tsx +51 -64
- package/src/components/viewer/ViewportOverlays.tsx +5 -2
- package/src/components/viewer/annotations/AnnotationDropInput.tsx +203 -0
- package/src/components/viewer/annotations/AnnotationLayer.tsx +287 -0
- package/src/components/viewer/annotations/AnnotationPin.tsx +90 -0
- package/src/components/viewer/annotations/AnnotationPopover.tsx +296 -0
- package/src/components/viewer/bcf/BCFTopicDetail.tsx +4 -4
- package/src/components/viewer/chat/ModelSelector.tsx +90 -54
- package/src/components/viewer/lists/ListPanel.tsx +14 -21
- package/src/components/viewer/properties/GeoreferencingPanel.tsx +113 -51
- package/src/components/viewer/properties/LocationMap.tsx +9 -7
- package/src/components/viewer/properties/ModelMetadataPanel.tsx +1 -1
- package/src/components/viewer/properties/RawStepCard.tsx +332 -0
- package/src/components/viewer/properties/RawStepRow.tsx +261 -0
- package/src/components/viewer/properties/ScheduleCard.tsx +224 -0
- package/src/components/viewer/properties/TaskEditCard.tsx +510 -0
- package/src/components/viewer/properties/raw-step-format.ts +193 -0
- package/src/components/viewer/schedule/AnimationSettingsPopover.tsx +542 -0
- package/src/components/viewer/schedule/GanttDependencyArrows.tsx +89 -0
- package/src/components/viewer/schedule/GanttDragTooltip.tsx +48 -0
- package/src/components/viewer/schedule/GanttEmptyState.tsx +97 -0
- package/src/components/viewer/schedule/GanttPanel.tsx +295 -0
- package/src/components/viewer/schedule/GanttTaskBar.tsx +199 -0
- package/src/components/viewer/schedule/GanttTaskTree.tsx +250 -0
- package/src/components/viewer/schedule/GanttTimeline.tsx +305 -0
- package/src/components/viewer/schedule/GanttToolbar.tsx +406 -0
- package/src/components/viewer/schedule/GenerateAdvancedPanel.tsx +147 -0
- package/src/components/viewer/schedule/GenerateScheduleDialog.tsx +392 -0
- package/src/components/viewer/schedule/HeightStrategyPanel.tsx +120 -0
- package/src/components/viewer/schedule/generate-schedule.test.ts +439 -0
- package/src/components/viewer/schedule/generate-schedule.ts +648 -0
- package/src/components/viewer/schedule/schedule-animator.test.ts +452 -0
- package/src/components/viewer/schedule/schedule-animator.ts +488 -0
- package/src/components/viewer/schedule/schedule-selection.test.ts +148 -0
- package/src/components/viewer/schedule/schedule-selection.ts +163 -0
- package/src/components/viewer/schedule/schedule-utils.ts +223 -0
- package/src/components/viewer/schedule/useConstructionSequence.ts +156 -0
- package/src/components/viewer/schedule/useGanttBarDrag.test.ts +90 -0
- package/src/components/viewer/schedule/useGanttBarDrag.ts +305 -0
- package/src/components/viewer/schedule/useGanttSelection3DHighlight.ts +152 -0
- package/src/components/viewer/schedule/useOverlayCompositor.ts +108 -0
- package/src/components/viewer/selectionHandlers.ts +446 -0
- package/src/components/viewer/tools/AddElementOverlay.tsx +540 -0
- package/src/components/viewer/tools/SectionCapControls.tsx +237 -0
- package/src/components/viewer/tools/SectionPanel.tsx +39 -18
- package/src/components/viewer/useAnimationLoop.ts +9 -1
- package/src/components/viewer/useDuplicateShortcut.ts +77 -0
- package/src/components/viewer/useMouseControls.ts +9 -1
- package/src/components/viewer/useRenderUpdates.ts +1 -1
- package/src/hooks/ids/idsDataAccessor.ts +60 -24
- package/src/hooks/ingest/viewerModelIngest.ts +7 -2
- package/src/hooks/useIfcFederation.ts +326 -71
- package/src/hooks/useIfcLoader.ts +23 -10
- package/src/hooks/useKeyboardShortcuts.ts +25 -0
- package/src/hooks/useSandbox.ts +1 -1
- package/src/hooks/useSearchIndex.ts +125 -0
- package/src/hooks/useViewControls.ts +13 -5
- package/src/index.css +550 -10
- package/src/lib/desktop-entitlement.ts +2 -4
- package/src/lib/geo/cesium-bridge.ts +15 -7
- package/src/lib/geo/effective-georef.test.ts +73 -0
- package/src/lib/geo/effective-georef.ts +111 -0
- package/src/lib/geo/reproject.ts +105 -19
- package/src/lib/llm/byok-guard.test.ts +77 -0
- package/src/lib/llm/byok-guard.ts +39 -0
- package/src/lib/llm/free-models.test.ts +0 -6
- package/src/lib/llm/models.ts +104 -42
- package/src/lib/llm/stream-client.ts +74 -110
- package/src/lib/llm/stream-direct.test.ts +130 -0
- package/src/lib/llm/stream-direct.ts +316 -0
- package/src/lib/llm/system-prompt.test.ts +14 -0
- package/src/lib/llm/system-prompt.ts +102 -1
- package/src/lib/llm/types.ts +20 -2
- package/src/lib/recent-files.ts +38 -4
- package/src/lib/scripts/templates/bim-globals.d.ts +136 -114
- package/src/lib/scripts/templates/construction-schedule.ts +223 -0
- package/src/lib/scripts/templates.ts +7 -0
- package/src/lib/search/common-ifc-types.ts +36 -0
- package/src/lib/search/filter-evaluate.test.ts +537 -0
- package/src/lib/search/filter-evaluate.ts +610 -0
- package/src/lib/search/filter-rules.test.ts +119 -0
- package/src/lib/search/filter-rules.ts +198 -0
- package/src/lib/search/filter-schema.test.ts +233 -0
- package/src/lib/search/filter-schema.ts +146 -0
- package/src/lib/search/recent-searches.test.ts +116 -0
- package/src/lib/search/recent-searches.ts +93 -0
- package/src/lib/search/result-export.test.ts +101 -0
- package/src/lib/search/result-export.ts +104 -0
- package/src/lib/search/saved-filters.test.ts +118 -0
- package/src/lib/search/saved-filters.ts +154 -0
- package/src/lib/search/tier0-scan.test.ts +196 -0
- package/src/lib/search/tier0-scan.ts +237 -0
- package/src/lib/search/tier1-index.test.ts +242 -0
- package/src/lib/search/tier1-index.ts +448 -0
- package/src/main.tsx +1 -10
- package/src/sdk/adapters/export-adapter.test.ts +434 -1
- package/src/sdk/adapters/export-adapter.ts +404 -1
- package/src/sdk/adapters/export-schedule-splice.test.ts +127 -0
- package/src/sdk/adapters/export-schedule-splice.ts +87 -0
- package/src/sdk/adapters/model-compat.ts +8 -2
- package/src/sdk/adapters/schedule-adapter.ts +73 -0
- package/src/sdk/adapters/store-adapter.ts +201 -0
- package/src/sdk/adapters/visibility-adapter.ts +3 -0
- package/src/sdk/local-backend.ts +16 -8
- package/src/services/api-keys.ts +73 -0
- package/src/services/desktop-export.ts +3 -1
- package/src/services/desktop-native-metadata.ts +41 -18
- package/src/services/file-dialog.ts +4 -1
- package/src/services/tauri-modules.d.ts +25 -0
- package/src/store/basketVisibleSet.ts +3 -0
- package/src/store/constants.ts +20 -2
- package/src/store/globalId.ts +4 -1
- package/src/store/index.ts +82 -6
- package/src/store/slices/addElementMeshes.ts +365 -0
- package/src/store/slices/addElementSlice.ts +275 -0
- package/src/store/slices/annotationsSlice.test.ts +133 -0
- package/src/store/slices/annotationsSlice.ts +251 -0
- package/src/store/slices/cesiumSlice.ts +5 -0
- package/src/store/slices/chatSlice.test.ts +6 -76
- package/src/store/slices/chatSlice.ts +17 -58
- package/src/store/slices/dataSlice.test.ts +23 -4
- package/src/store/slices/dataSlice.ts +1 -1
- package/src/store/slices/modelSlice.test.ts +67 -9
- package/src/store/slices/modelSlice.ts +39 -7
- package/src/store/slices/mutationSlice.ts +964 -3
- package/src/store/slices/overlayCompositor.test.ts +164 -0
- package/src/store/slices/overlaySlice.test.ts +93 -0
- package/src/store/slices/overlaySlice.ts +151 -0
- package/src/store/slices/pinboardSlice.test.ts +6 -1
- package/src/store/slices/playbackSlice.ts +128 -0
- package/src/store/slices/schedule-edit-helpers.test.ts +97 -0
- package/src/store/slices/schedule-edit-helpers.ts +179 -0
- package/src/store/slices/scheduleSlice.test.ts +694 -0
- package/src/store/slices/scheduleSlice.ts +1330 -0
- package/src/store/slices/searchSlice.test.ts +342 -0
- package/src/store/slices/searchSlice.ts +341 -0
- package/src/store/slices/sectionSlice.test.ts +87 -7
- package/src/store/slices/sectionSlice.ts +151 -5
- package/src/store/slices/selectionSlice.test.ts +46 -0
- package/src/store/slices/selectionSlice.ts +20 -0
- package/src/store/slices/uiSlice.ts +28 -5
- package/src/store/types.ts +26 -0
- package/src/store.ts +14 -0
- package/src/utils/nativeSpatialDataStore.ts +4 -1
- package/src/utils/viewportUtils.ts +7 -2
- package/src/vite-env.d.ts +0 -4
- package/dist/assets/drawing-2d-gWfpdfYe.js +0 -257
- package/dist/assets/ids-B4jTqB1O.js +0 -1
- package/dist/assets/ifc-lite_bg-BX4E7TX8.wasm +0 -0
- package/dist/assets/index-DckuDqlv.css +0 -1
- package/dist/assets/sandbox-DZiNLNMk.js +0 -5933
- package/src/components/viewer/UpgradePage.tsx +0 -71
- package/src/lib/desktop/ClerkDesktopEntitlementSync.tsx +0 -175
- package/src/lib/llm/ClerkChatSync.tsx +0 -74
- package/src/lib/llm/clerk-auth.ts +0 -62
|
@@ -0,0 +1,439 @@
|
|
|
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 { describe, it } from 'node:test';
|
|
6
|
+
import assert from 'node:assert';
|
|
7
|
+
import type { IfcDataStore } from '@ifc-lite/parser';
|
|
8
|
+
import {
|
|
9
|
+
generateScheduleFromSpatialHierarchy,
|
|
10
|
+
canGenerateScheduleFrom,
|
|
11
|
+
DEFAULT_OPTIONS,
|
|
12
|
+
toLocalIso,
|
|
13
|
+
} from './generate-schedule.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Build a minimal mock IfcDataStore whose spatialHierarchy fixture has three
|
|
17
|
+
* storeys with 3 / 2 / 1 contained elements respectively. Elevations are set
|
|
18
|
+
* so bottom-up ordering is deterministic.
|
|
19
|
+
*/
|
|
20
|
+
function buildMockStore(): IfcDataStore {
|
|
21
|
+
const entitiesByExpressId = new Map<number, { name: string; globalId: string }>([
|
|
22
|
+
[100, { name: 'Ground', globalId: 'storey-0000' }],
|
|
23
|
+
[101, { name: 'Level 1', globalId: 'storey-1111' }],
|
|
24
|
+
[102, { name: 'Roof', globalId: 'storey-2222' }],
|
|
25
|
+
[1, { name: 'Wall A', globalId: 'wall-A' }],
|
|
26
|
+
[2, { name: 'Wall B', globalId: 'wall-B' }],
|
|
27
|
+
[3, { name: 'Slab G', globalId: 'slab-G' }],
|
|
28
|
+
[4, { name: 'Column 1', globalId: 'col-1' }],
|
|
29
|
+
[5, { name: 'Window', globalId: 'win-1' }],
|
|
30
|
+
[6, { name: 'Roof Slab', globalId: 'slab-R' }],
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
spatialHierarchy: {
|
|
35
|
+
project: { expressId: 0, type: 0, name: 'Project', children: [], elements: [] },
|
|
36
|
+
byStorey: new Map([
|
|
37
|
+
[100, [1, 2, 3]],
|
|
38
|
+
[101, [4, 5]],
|
|
39
|
+
[102, [6]],
|
|
40
|
+
]),
|
|
41
|
+
byBuilding: new Map([[99, [1, 2, 3, 4, 5, 6]]]),
|
|
42
|
+
bySite: new Map(),
|
|
43
|
+
bySpace: new Map(),
|
|
44
|
+
storeyElevations: new Map([[100, 0], [101, 3], [102, 6.5]]),
|
|
45
|
+
storeyHeights: new Map(),
|
|
46
|
+
elementToStorey: new Map(),
|
|
47
|
+
getStoreyElements: () => [],
|
|
48
|
+
getStoreyByElevation: () => null,
|
|
49
|
+
getContainingSpace: () => null,
|
|
50
|
+
getPath: () => [],
|
|
51
|
+
},
|
|
52
|
+
entities: {
|
|
53
|
+
getName: (id: number) => entitiesByExpressId.get(id)?.name ?? '',
|
|
54
|
+
getGlobalId: (id: number) => entitiesByExpressId.get(id)?.globalId ?? '',
|
|
55
|
+
},
|
|
56
|
+
} as unknown as IfcDataStore;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
describe('canGenerateScheduleFrom', () => {
|
|
60
|
+
it('returns false for null/missing hierarchy', () => {
|
|
61
|
+
assert.strictEqual(canGenerateScheduleFrom(null), false);
|
|
62
|
+
assert.strictEqual(canGenerateScheduleFrom(undefined), false);
|
|
63
|
+
assert.strictEqual(
|
|
64
|
+
canGenerateScheduleFrom({ spatialHierarchy: undefined } as unknown as IfcDataStore),
|
|
65
|
+
false,
|
|
66
|
+
);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('returns true when storeys or buildings exist', () => {
|
|
70
|
+
assert.strictEqual(canGenerateScheduleFrom(buildMockStore()), true);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe('generateScheduleFromSpatialHierarchy — storey strategy', () => {
|
|
75
|
+
it('produces one task per storey, bottom-up, with product assignments', () => {
|
|
76
|
+
const preview = generateScheduleFromSpatialHierarchy(buildMockStore(), {
|
|
77
|
+
...DEFAULT_OPTIONS,
|
|
78
|
+
startDate: '2024-05-01T08:00:00',
|
|
79
|
+
daysPerGroup: 5,
|
|
80
|
+
lagDays: 0,
|
|
81
|
+
linkSequences: true,
|
|
82
|
+
order: 'bottom-up',
|
|
83
|
+
});
|
|
84
|
+
assert.strictEqual(preview.empty, false);
|
|
85
|
+
assert.strictEqual(preview.groupCount, 3);
|
|
86
|
+
assert.strictEqual(preview.productCount, 6);
|
|
87
|
+
assert.strictEqual(preview.extraction.tasks.length, 3);
|
|
88
|
+
assert.deepStrictEqual(
|
|
89
|
+
preview.extraction.tasks.map(t => t.name),
|
|
90
|
+
['Ground', 'Level 1', 'Roof'],
|
|
91
|
+
);
|
|
92
|
+
assert.deepStrictEqual(
|
|
93
|
+
preview.extraction.tasks[0].productExpressIds,
|
|
94
|
+
[1, 2, 3],
|
|
95
|
+
);
|
|
96
|
+
assert.deepStrictEqual(
|
|
97
|
+
preview.extraction.tasks[0].productGlobalIds,
|
|
98
|
+
['wall-A', 'wall-B', 'slab-G'],
|
|
99
|
+
);
|
|
100
|
+
// Finish-Start sequences between consecutive storeys.
|
|
101
|
+
assert.strictEqual(preview.extraction.sequences.length, 2);
|
|
102
|
+
assert.strictEqual(
|
|
103
|
+
preview.extraction.sequences[0].sequenceType,
|
|
104
|
+
'FINISH_START',
|
|
105
|
+
);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('top-down order reverses the task list', () => {
|
|
109
|
+
const preview = generateScheduleFromSpatialHierarchy(buildMockStore(), {
|
|
110
|
+
...DEFAULT_OPTIONS,
|
|
111
|
+
startDate: '2024-05-01T08:00:00',
|
|
112
|
+
order: 'top-down',
|
|
113
|
+
});
|
|
114
|
+
assert.deepStrictEqual(
|
|
115
|
+
preview.extraction.tasks.map(t => t.name),
|
|
116
|
+
['Roof', 'Level 1', 'Ground'],
|
|
117
|
+
);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('laying out dates — no lag', () => {
|
|
121
|
+
const preview = generateScheduleFromSpatialHierarchy(buildMockStore(), {
|
|
122
|
+
...DEFAULT_OPTIONS,
|
|
123
|
+
startDate: '2024-05-01T00:00:00',
|
|
124
|
+
daysPerGroup: 5,
|
|
125
|
+
lagDays: 0,
|
|
126
|
+
});
|
|
127
|
+
const starts = preview.extraction.tasks.map(t => t.taskTime?.scheduleStart);
|
|
128
|
+
assert.deepStrictEqual(starts, [
|
|
129
|
+
'2024-05-01T00:00:00',
|
|
130
|
+
'2024-05-06T00:00:00',
|
|
131
|
+
'2024-05-11T00:00:00',
|
|
132
|
+
]);
|
|
133
|
+
assert.strictEqual(preview.finishDate, '2024-05-16T00:00:00');
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('laying out dates — 2-day lag', () => {
|
|
137
|
+
const preview = generateScheduleFromSpatialHierarchy(buildMockStore(), {
|
|
138
|
+
...DEFAULT_OPTIONS,
|
|
139
|
+
startDate: '2024-05-01T00:00:00',
|
|
140
|
+
daysPerGroup: 5,
|
|
141
|
+
lagDays: 2,
|
|
142
|
+
});
|
|
143
|
+
const starts = preview.extraction.tasks.map(t => t.taskTime?.scheduleStart);
|
|
144
|
+
assert.deepStrictEqual(starts, [
|
|
145
|
+
'2024-05-01T00:00:00',
|
|
146
|
+
'2024-05-08T00:00:00',
|
|
147
|
+
'2024-05-15T00:00:00',
|
|
148
|
+
]);
|
|
149
|
+
// Sequence edges get the lag duration attached.
|
|
150
|
+
assert.strictEqual(
|
|
151
|
+
preview.extraction.sequences[0].timeLagDuration,
|
|
152
|
+
'P2D',
|
|
153
|
+
);
|
|
154
|
+
assert.strictEqual(
|
|
155
|
+
preview.extraction.sequences[0].timeLagSeconds,
|
|
156
|
+
2 * 86_400,
|
|
157
|
+
);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('skipEmptyGroups drops storeys with no products', () => {
|
|
161
|
+
const store = buildMockStore();
|
|
162
|
+
// Replace the Roof storey with an empty one.
|
|
163
|
+
(store.spatialHierarchy!.byStorey as Map<number, number[]>).set(102, []);
|
|
164
|
+
|
|
165
|
+
const preview = generateScheduleFromSpatialHierarchy(store, {
|
|
166
|
+
...DEFAULT_OPTIONS,
|
|
167
|
+
skipEmptyGroups: true,
|
|
168
|
+
});
|
|
169
|
+
assert.strictEqual(preview.groupCount, 2);
|
|
170
|
+
assert.deepStrictEqual(
|
|
171
|
+
preview.extraction.tasks.map(t => t.name),
|
|
172
|
+
['Ground', 'Level 1'],
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
const preview2 = generateScheduleFromSpatialHierarchy(store, {
|
|
176
|
+
...DEFAULT_OPTIONS,
|
|
177
|
+
skipEmptyGroups: false,
|
|
178
|
+
});
|
|
179
|
+
assert.strictEqual(preview2.groupCount, 3);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('linkSequences=false produces a flat list of tasks', () => {
|
|
183
|
+
const preview = generateScheduleFromSpatialHierarchy(buildMockStore(), {
|
|
184
|
+
...DEFAULT_OPTIONS,
|
|
185
|
+
linkSequences: false,
|
|
186
|
+
});
|
|
187
|
+
assert.strictEqual(preview.extraction.sequences.length, 0);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('attaches every task to the generated work schedule', () => {
|
|
191
|
+
const preview = generateScheduleFromSpatialHierarchy(buildMockStore(), DEFAULT_OPTIONS);
|
|
192
|
+
const scheduleGid = preview.extraction.workSchedules[0].globalId;
|
|
193
|
+
for (const task of preview.extraction.tasks) {
|
|
194
|
+
assert.ok(task.controllingScheduleGlobalIds.includes(scheduleGid));
|
|
195
|
+
}
|
|
196
|
+
assert.strictEqual(
|
|
197
|
+
preview.extraction.workSchedules[0].taskGlobalIds.length,
|
|
198
|
+
preview.extraction.tasks.length,
|
|
199
|
+
);
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
describe('generateScheduleFromSpatialHierarchy — building strategy', () => {
|
|
204
|
+
it('produces one task per building rolling up all products', () => {
|
|
205
|
+
const preview = generateScheduleFromSpatialHierarchy(buildMockStore(), {
|
|
206
|
+
...DEFAULT_OPTIONS,
|
|
207
|
+
strategy: 'IfcBuilding',
|
|
208
|
+
});
|
|
209
|
+
assert.strictEqual(preview.groupCount, 1);
|
|
210
|
+
assert.strictEqual(preview.productCount, 6);
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
describe('empty / degenerate inputs', () => {
|
|
215
|
+
it('returns empty preview for null store', () => {
|
|
216
|
+
const preview = generateScheduleFromSpatialHierarchy(null, DEFAULT_OPTIONS);
|
|
217
|
+
assert.strictEqual(preview.empty, true);
|
|
218
|
+
assert.strictEqual(preview.extraction.hasSchedule, false);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('returns empty preview when every storey is empty and skipEmpty=true', () => {
|
|
222
|
+
const store = buildMockStore();
|
|
223
|
+
const by = store.spatialHierarchy!.byStorey as Map<number, number[]>;
|
|
224
|
+
by.set(100, []); by.set(101, []); by.set(102, []);
|
|
225
|
+
const preview = generateScheduleFromSpatialHierarchy(store, {
|
|
226
|
+
...DEFAULT_OPTIONS,
|
|
227
|
+
strategy: 'IfcBuildingStorey',
|
|
228
|
+
skipEmptyGroups: true,
|
|
229
|
+
});
|
|
230
|
+
// byBuilding still has products so the helper isn't technically empty —
|
|
231
|
+
// it just has 0 storey groups. Assert groupCount explicitly.
|
|
232
|
+
assert.strictEqual(preview.groupCount, 0);
|
|
233
|
+
assert.strictEqual(preview.extraction.tasks.length, 0);
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
describe('deterministic globalIds', () => {
|
|
238
|
+
it('re-running against the same model produces identical task IDs', () => {
|
|
239
|
+
const a = generateScheduleFromSpatialHierarchy(buildMockStore(), DEFAULT_OPTIONS);
|
|
240
|
+
const b = generateScheduleFromSpatialHierarchy(buildMockStore(), DEFAULT_OPTIONS);
|
|
241
|
+
assert.deepStrictEqual(
|
|
242
|
+
a.extraction.tasks.map(t => t.globalId),
|
|
243
|
+
b.extraction.tasks.map(t => t.globalId),
|
|
244
|
+
);
|
|
245
|
+
assert.strictEqual(
|
|
246
|
+
a.extraction.workSchedules[0].globalId,
|
|
247
|
+
b.extraction.workSchedules[0].globalId,
|
|
248
|
+
);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it('different models produce different task IDs', () => {
|
|
252
|
+
// Two models with disjoint container globalIds must not collide.
|
|
253
|
+
const storeA = buildMockStore();
|
|
254
|
+
const storeB = buildMockStore();
|
|
255
|
+
// Re-key storeB's storey ids so `entities.getGlobalId` returns new values.
|
|
256
|
+
const storeyRemap = new Map<number, string>([
|
|
257
|
+
[100, 'DIFF-ground'], [101, 'DIFF-L1'], [102, 'DIFF-roof'],
|
|
258
|
+
]);
|
|
259
|
+
const originalGetGlobalId = storeB.entities.getGlobalId.bind(storeB.entities);
|
|
260
|
+
(storeB.entities as unknown as { getGlobalId: (id: number) => string }).getGlobalId = (id: number) =>
|
|
261
|
+
storeyRemap.get(id) ?? originalGetGlobalId(id);
|
|
262
|
+
|
|
263
|
+
const a = generateScheduleFromSpatialHierarchy(storeA, DEFAULT_OPTIONS);
|
|
264
|
+
const b = generateScheduleFromSpatialHierarchy(storeB, DEFAULT_OPTIONS);
|
|
265
|
+
const idsA = new Set(a.extraction.tasks.map(t => t.globalId));
|
|
266
|
+
const idsB = new Set(b.extraction.tasks.map(t => t.globalId));
|
|
267
|
+
for (const id of idsB) assert.ok(!idsA.has(id), `id ${id} collided across models`);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('100 distinct seeds map to 100 distinct GlobalIds (hash-collision regression)', () => {
|
|
271
|
+
// Regression: the single-stream 32-bit FNV variant we shipped
|
|
272
|
+
// collided on real-world 30-task schedules — duplicate task
|
|
273
|
+
// GlobalIds caused downstream Gantt bars to overwrite each other,
|
|
274
|
+
// producing bars scattered across months instead of a clean
|
|
275
|
+
// sequence. The two-stream mixer is sized so 100 seeds never
|
|
276
|
+
// collide; this guards against regressing to the weaker variant.
|
|
277
|
+
const entitiesByExpressId = new Map<number, { name: string; globalId: string }>();
|
|
278
|
+
const byStorey = new Map<number, number[]>();
|
|
279
|
+
const storeyElevations = new Map<number, number>();
|
|
280
|
+
for (let i = 0; i < 100; i++) {
|
|
281
|
+
const storeyId = 1000 + i;
|
|
282
|
+
// Use realistic IFC 22-char GUIDs so we're stressing the same
|
|
283
|
+
// input shape as production (not short "storey-0001" stubs).
|
|
284
|
+
const gid = `Storey-${i.toString().padStart(17, 'A')}`;
|
|
285
|
+
entitiesByExpressId.set(storeyId, { name: `Level ${i}`, globalId: gid });
|
|
286
|
+
byStorey.set(storeyId, [i + 1]); // one element per storey so none are skipped
|
|
287
|
+
storeyElevations.set(storeyId, i * 3.0);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const stressStore = {
|
|
291
|
+
spatialHierarchy: {
|
|
292
|
+
project: { expressId: 0, type: 0, name: 'Project', children: [], elements: [] },
|
|
293
|
+
byStorey,
|
|
294
|
+
byBuilding: new Map([[99, Array.from(byStorey.values()).flat()]]),
|
|
295
|
+
bySite: new Map(),
|
|
296
|
+
bySpace: new Map(),
|
|
297
|
+
storeyElevations,
|
|
298
|
+
storeyHeights: new Map(),
|
|
299
|
+
elementToStorey: new Map(),
|
|
300
|
+
getStoreyElements: () => [],
|
|
301
|
+
getStoreyByElevation: () => null,
|
|
302
|
+
getContainingSpace: () => null,
|
|
303
|
+
getPath: () => [],
|
|
304
|
+
},
|
|
305
|
+
entities: {
|
|
306
|
+
getName: (id: number) => entitiesByExpressId.get(id)?.name ?? '',
|
|
307
|
+
getGlobalId: (id: number) => entitiesByExpressId.get(id)?.globalId ?? '',
|
|
308
|
+
},
|
|
309
|
+
// Required IfcDataStore surface — unused by this codepath but
|
|
310
|
+
// satisfies the type so we don't have to cast through `unknown`.
|
|
311
|
+
properties: null,
|
|
312
|
+
entityCount: 0,
|
|
313
|
+
schemaVersion: 'IFC4',
|
|
314
|
+
} as unknown as IfcDataStore;
|
|
315
|
+
|
|
316
|
+
const preview = generateScheduleFromSpatialHierarchy(stressStore, DEFAULT_OPTIONS);
|
|
317
|
+
const ids = preview.extraction.tasks.map(t => t.globalId);
|
|
318
|
+
assert.strictEqual(ids.length, 100, 'expected 100 tasks');
|
|
319
|
+
const unique = new Set(ids);
|
|
320
|
+
assert.strictEqual(unique.size, 100, `GlobalId collisions: ${ids.length - unique.size}`);
|
|
321
|
+
// Workschedule id must also be distinct from every task id.
|
|
322
|
+
const wsId = preview.extraction.workSchedules[0]!.globalId;
|
|
323
|
+
assert.ok(!unique.has(wsId), `workschedule id ${wsId} collides with a task`);
|
|
324
|
+
});
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
describe('generateScheduleFromSpatialHierarchy — IfcElement (Z slice) strategy', () => {
|
|
328
|
+
// Build a synthetic mesh for a product — vertical coord on Y (WebGL
|
|
329
|
+
// Y-up, matching the parser's `convertZUpToYUp` output) controls the
|
|
330
|
+
// bin; ifcType controls the "class" subgroup; name/type routed
|
|
331
|
+
// through the entities shim.
|
|
332
|
+
const makeMesh = (expressId: number, y: number, ifcType = 'IfcWall') => ({
|
|
333
|
+
expressId,
|
|
334
|
+
ifcType,
|
|
335
|
+
// 3 vertices at (x, y, z) = (0,y,0), (1,y,0), (0,y,1) — all at the
|
|
336
|
+
// same vertical so the centroid equals `y` exactly.
|
|
337
|
+
positions: new Float32Array([0, y, 0, 1, y, 0, 0, y, 1]),
|
|
338
|
+
normals: new Float32Array(9),
|
|
339
|
+
indices: new Uint32Array([0, 1, 2]),
|
|
340
|
+
color: [1, 1, 1, 1] as [number, number, number, number],
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
const baseStore = (): IfcDataStore => {
|
|
344
|
+
const nameByLocal = new Map<number, { name: string; globalId: string; typeName: string }>([
|
|
345
|
+
[1, { name: 'Wall A', globalId: 'W-A', typeName: 'WallType-100' }],
|
|
346
|
+
[2, { name: 'Wall B', globalId: 'W-B', typeName: 'WallType-100' }],
|
|
347
|
+
[3, { name: 'Slab L1', globalId: 'S-L1', typeName: 'SlabType-S1' }],
|
|
348
|
+
[4, { name: 'Slab L2', globalId: 'S-L2', typeName: 'SlabType-S1' }],
|
|
349
|
+
[5, { name: 'Col High', globalId: 'C-Hi', typeName: 'ColType-X' }],
|
|
350
|
+
]);
|
|
351
|
+
return {
|
|
352
|
+
spatialHierarchy: undefined,
|
|
353
|
+
entities: {
|
|
354
|
+
getName: (id: number) => nameByLocal.get(id)?.name ?? '',
|
|
355
|
+
getGlobalId: (id: number) => nameByLocal.get(id)?.globalId ?? '',
|
|
356
|
+
getTypeName: (id: number) => nameByLocal.get(id)?.typeName ?? '',
|
|
357
|
+
},
|
|
358
|
+
} as unknown as IfcDataStore;
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
it('is empty when no geometry is supplied', () => {
|
|
362
|
+
const preview = generateScheduleFromSpatialHierarchy(
|
|
363
|
+
baseStore(),
|
|
364
|
+
{ ...DEFAULT_OPTIONS, strategy: 'IfcElement' },
|
|
365
|
+
);
|
|
366
|
+
assert.strictEqual(preview.empty, true);
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
it('bins by mesh centroid Z (tolerance = slice height), subgroup=none', () => {
|
|
370
|
+
const meshes = [
|
|
371
|
+
makeMesh(1, 0.2), makeMesh(2, 1.1), // bin 0 (0-3 m)
|
|
372
|
+
makeMesh(3, 3.5), makeMesh(4, 4.9), // bin 1 (3-6 m)
|
|
373
|
+
makeMesh(5, 9.8), // bin 3 (9-12 m) — note gap is fine
|
|
374
|
+
];
|
|
375
|
+
const preview = generateScheduleFromSpatialHierarchy(
|
|
376
|
+
baseStore(),
|
|
377
|
+
{ ...DEFAULT_OPTIONS, strategy: 'IfcElement', heightTolerance: 3, elementZSubgroup: 'none' },
|
|
378
|
+
{ meshes: meshes as unknown as import('@ifc-lite/geometry').MeshData[], idOffset: 0 },
|
|
379
|
+
);
|
|
380
|
+
assert.strictEqual(preview.empty, false);
|
|
381
|
+
assert.strictEqual(preview.groupCount, 3);
|
|
382
|
+
// Products in the first bin: 1 and 2.
|
|
383
|
+
const task0 = preview.extraction.tasks[0]!;
|
|
384
|
+
assert.deepEqual(task0.productExpressIds.sort(), [1, 2]);
|
|
385
|
+
const task1 = preview.extraction.tasks[1]!;
|
|
386
|
+
assert.deepEqual(task1.productExpressIds.sort(), [3, 4]);
|
|
387
|
+
const task2 = preview.extraction.tasks[2]!;
|
|
388
|
+
assert.deepEqual(task2.productExpressIds, [5]);
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
it('subdivides each slice by IFC class when subgroup=class', () => {
|
|
392
|
+
const meshes = [
|
|
393
|
+
makeMesh(1, 0.5, 'IfcWall'),
|
|
394
|
+
makeMesh(2, 0.8, 'IfcWall'),
|
|
395
|
+
makeMesh(3, 1.2, 'IfcSlab'),
|
|
396
|
+
makeMesh(4, 4.5, 'IfcWall'),
|
|
397
|
+
];
|
|
398
|
+
const preview = generateScheduleFromSpatialHierarchy(
|
|
399
|
+
baseStore(),
|
|
400
|
+
{ ...DEFAULT_OPTIONS, strategy: 'IfcElement', heightTolerance: 3, elementZSubgroup: 'class' },
|
|
401
|
+
{ meshes: meshes as unknown as import('@ifc-lite/geometry').MeshData[], idOffset: 0 },
|
|
402
|
+
);
|
|
403
|
+
// bin 0 × { IfcWall, IfcSlab } + bin 1 × { IfcWall } = 3 tasks.
|
|
404
|
+
assert.strictEqual(preview.groupCount, 3);
|
|
405
|
+
const walls0 = preview.extraction.tasks.find(t => t.name.startsWith('IfcWall') && t.productExpressIds.includes(1));
|
|
406
|
+
assert.ok(walls0, 'expected IfcWall task in bin 0');
|
|
407
|
+
assert.deepEqual(walls0!.productExpressIds.sort(), [1, 2]);
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
it('respects idOffset when converting mesh.expressId to local', () => {
|
|
411
|
+
// idOffset=1000: mesh with expressId=1001 → local=1.
|
|
412
|
+
const meshes = [makeMesh(1001, 0.5)];
|
|
413
|
+
const preview = generateScheduleFromSpatialHierarchy(
|
|
414
|
+
baseStore(),
|
|
415
|
+
{ ...DEFAULT_OPTIONS, strategy: 'IfcElement', heightTolerance: 3, elementZSubgroup: 'none' },
|
|
416
|
+
{ meshes: meshes as unknown as import('@ifc-lite/geometry').MeshData[], idOffset: 1000 },
|
|
417
|
+
);
|
|
418
|
+
assert.deepEqual(preview.extraction.tasks[0]!.productExpressIds, [1]);
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
it('emits unique task globalIds across bins and subkeys', () => {
|
|
422
|
+
const meshes: ReturnType<typeof makeMesh>[] = [];
|
|
423
|
+
for (let i = 0; i < 20; i++) meshes.push(makeMesh(i + 1, i * 0.5, i % 2 === 0 ? 'IfcWall' : 'IfcSlab'));
|
|
424
|
+
const preview = generateScheduleFromSpatialHierarchy(
|
|
425
|
+
baseStore(),
|
|
426
|
+
{ ...DEFAULT_OPTIONS, strategy: 'IfcElement', heightTolerance: 2, elementZSubgroup: 'class' },
|
|
427
|
+
{ meshes: meshes as unknown as import('@ifc-lite/geometry').MeshData[], idOffset: 0 },
|
|
428
|
+
);
|
|
429
|
+
const ids = preview.extraction.tasks.map(t => t.globalId);
|
|
430
|
+
assert.strictEqual(new Set(ids).size, ids.length, `${ids.length - new Set(ids).size} collisions`);
|
|
431
|
+
});
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
describe('toLocalIso', () => {
|
|
435
|
+
it('emits a stable zero-padded local-timezone ISO string', () => {
|
|
436
|
+
const d = new Date(2024, 4, 1, 8, 5, 9); // May 1, 08:05:09
|
|
437
|
+
assert.strictEqual(toLocalIso(d), '2024-05-01T08:05:09');
|
|
438
|
+
});
|
|
439
|
+
});
|