@ifc-lite/viewer 1.17.6 → 1.19.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 -15
- package/.turbo/turbo-typecheck.log +1 -1
- package/CHANGELOG.md +949 -0
- package/dist/assets/{basketViewActivator-86rgogji.js → basketViewActivator-RZy5c3Td.js} +1 -1
- package/dist/assets/decode-worker-Collf_X_.js +1320 -0
- package/dist/assets/{exporters-CcPS9MK5.js → exporters-BraHBeoi.js} +4194 -3025
- package/dist/assets/{geometry.worker-BFUYA08u.js → geometry.worker-DQEZB2rB.js} +1 -1
- package/dist/assets/ifc-lite_bg-4yUkDRD8.wasm +0 -0
- package/dist/assets/index-0XpVr_S5.css +1 -0
- package/dist/assets/{index-Bfms9I4A.js → index-BOi3BuUI.js} +46423 -31181
- package/dist/assets/index-XwKzDuw6.js +22 -0
- package/dist/assets/{native-bridge-DUyLCMZS.js → native-bridge-CpBeOPQa.js} +1 -1
- package/dist/assets/sandbox-Baez7n-t.js +9682 -0
- package/dist/assets/{server-client-BuZK7OST.js → server-client-BB6cMAXE.js} +1 -1
- package/dist/assets/{wasm-bridge-JsqEGDV8.js → wasm-bridge-CAYCUHbE.js} +1 -1
- package/dist/index.html +6 -6
- package/package.json +11 -10
- 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/ChatPanel.tsx +64 -2
- 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 -12
- package/src/components/viewer/PointCloudPanel.tsx +174 -0
- package/src/components/viewer/PropertiesPanel.tsx +222 -22
- 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/ToolOverlays.tsx +5 -0
- package/src/components/viewer/ViewerLayout.tsx +24 -4
- package/src/components/viewer/Viewport.tsx +29 -2
- package/src/components/viewer/ViewportContainer.tsx +45 -5
- package/src/components/viewer/ViewportOverlays.tsx +13 -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 +1 -1
- package/src/components/viewer/lists/ListPanel.tsx +14 -21
- 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 +581 -0
- package/src/components/viewer/useDuplicateShortcut.ts +77 -0
- package/src/components/viewer/useMouseControls.ts +9 -1
- package/src/components/viewer/usePointCloudLifecycle.ts +64 -0
- package/src/components/viewer/usePointCloudSync.ts +98 -0
- package/src/hooks/ingest/pointCloudIngest.ts +391 -0
- package/src/hooks/ingest/viewerModelIngest.ts +32 -3
- package/src/hooks/useIfcFederation.ts +72 -3
- package/src/hooks/useIfcLoader.ts +89 -13
- package/src/hooks/useKeyboardShortcuts.ts +25 -0
- package/src/hooks/useSandbox.ts +1 -1
- package/src/hooks/useSearchIndex.ts +125 -0
- package/src/index.css +66 -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 +6 -0
- 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/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/desktop-export.ts +3 -1
- package/src/services/desktop-native-metadata.ts +41 -18
- package/src/services/file-dialog.ts +8 -3
- package/src/services/tauri-modules.d.ts +25 -0
- package/src/store/basketVisibleSet.ts +3 -0
- package/src/store/globalId.ts +4 -1
- package/src/store/index.ts +79 -1
- 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/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/pointCloudSlice.ts +102 -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/selectionSlice.test.ts +46 -0
- package/src/store/slices/selectionSlice.ts +20 -0
- package/src/store/types.ts +7 -0
- package/src/store.ts +14 -0
- package/vite.config.ts +1 -0
- package/dist/assets/ifc-lite_bg-BINvzoCP.wasm +0 -0
- package/dist/assets/index-_bfZsDCC.css +0 -1
- package/dist/assets/sandbox-C8575tul.js +0 -5951
|
@@ -0,0 +1,452 @@
|
|
|
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/strict';
|
|
7
|
+
import type { ScheduleExtraction, ScheduleTaskInfo } from '@ifc-lite/parser';
|
|
8
|
+
import {
|
|
9
|
+
computeAnimationFrame,
|
|
10
|
+
DEFAULT_ANIMATION_SETTINGS,
|
|
11
|
+
DEFAULT_PALETTE,
|
|
12
|
+
type AnimationSettings,
|
|
13
|
+
type RGBA,
|
|
14
|
+
} from './schedule-animator.js';
|
|
15
|
+
|
|
16
|
+
const DAY = 86_400_000;
|
|
17
|
+
|
|
18
|
+
function parseDate(iso: string): number {
|
|
19
|
+
return Date.parse(iso.endsWith('Z') || /[+-]\d{2}:?\d{2}$/.test(iso) ? iso : `${iso}Z`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function makeTask(overrides: Partial<ScheduleTaskInfo>): ScheduleTaskInfo {
|
|
23
|
+
return {
|
|
24
|
+
expressId: 0,
|
|
25
|
+
globalId: 'task-x',
|
|
26
|
+
name: 'Task',
|
|
27
|
+
isMilestone: false,
|
|
28
|
+
childGlobalIds: [],
|
|
29
|
+
productExpressIds: [],
|
|
30
|
+
productGlobalIds: [],
|
|
31
|
+
controllingScheduleGlobalIds: [],
|
|
32
|
+
...overrides,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function makeSchedule(tasks: ScheduleTaskInfo[]): ScheduleExtraction {
|
|
37
|
+
return { hasSchedule: true, workSchedules: [], sequences: [], tasks };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Tests exercise the colour pipeline, which requires at least one
|
|
41
|
+
// colour-overlay flag to be on. Default settings have all three off
|
|
42
|
+
// (the "minimal" preset). `phased()` turns on the task-type colour
|
|
43
|
+
// flag — the minimum needed to emit non-null `result.color` in
|
|
44
|
+
// `computeTaskPhase`. Tests asserting minimal behaviour use `settings()`.
|
|
45
|
+
const settings = (over: Partial<AnimationSettings> = {}): AnimationSettings => ({
|
|
46
|
+
...DEFAULT_ANIMATION_SETTINGS,
|
|
47
|
+
...over,
|
|
48
|
+
});
|
|
49
|
+
const phased = (over: Partial<AnimationSettings> = {}): AnimationSettings => ({
|
|
50
|
+
...DEFAULT_ANIMATION_SETTINGS,
|
|
51
|
+
colorizeByTaskType: true,
|
|
52
|
+
// The new minimal default has `paletteIntensity: 0`; `phased()` mirrors
|
|
53
|
+
// the Popover's Phased preset (0.6) so tests exercise the real paint
|
|
54
|
+
// pipeline rather than a silently-zeroed alpha.
|
|
55
|
+
paletteIntensity: 0.6,
|
|
56
|
+
...over,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// ─── upcoming / preparation / active / complete ────────────────────────
|
|
60
|
+
|
|
61
|
+
describe('computeAnimationFrame — standard lifecycle', () => {
|
|
62
|
+
const task = makeTask({
|
|
63
|
+
predefinedType: 'CONSTRUCTION',
|
|
64
|
+
productExpressIds: [1, 2],
|
|
65
|
+
taskTime: { scheduleStart: '2024-05-10T08:00:00Z', scheduleFinish: '2024-05-20T17:00:00Z' },
|
|
66
|
+
});
|
|
67
|
+
const data = makeSchedule([task]);
|
|
68
|
+
|
|
69
|
+
it('hides products outside the preparation window when hideBeforePreparation=true', () => {
|
|
70
|
+
const t = parseDate('2024-05-01T00:00:00Z'); // far before start
|
|
71
|
+
const frame = computeAnimationFrame(data, t, settings({ preparationDays: 2 }));
|
|
72
|
+
assert.ok(frame.hiddenIds.has(1));
|
|
73
|
+
assert.ok(frame.hiddenIds.has(2));
|
|
74
|
+
assert.equal(frame.stats['upcoming-far'], 2);
|
|
75
|
+
assert.equal(frame.colorOverrides.size, 0);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('does not hide when hideBeforePreparation=false', () => {
|
|
79
|
+
const t = parseDate('2024-05-01T00:00:00Z');
|
|
80
|
+
const frame = computeAnimationFrame(data, t, settings({ hideBeforePreparation: false }));
|
|
81
|
+
assert.equal(frame.hiddenIds.size, 0);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('paints the preparation ghost inside the look-ahead window (when opted in)', () => {
|
|
85
|
+
// Default style is minimal + ghost-off — users must opt into both.
|
|
86
|
+
const t = parseDate('2024-05-09T08:00:00Z');
|
|
87
|
+
const frame = computeAnimationFrame(
|
|
88
|
+
data, t, phased({ preparationDays: 2, showPreparationGhost: true }),
|
|
89
|
+
);
|
|
90
|
+
assert.equal(frame.hiddenIds.size, 0);
|
|
91
|
+
assert.equal(frame.stats['upcoming-preparation'], 2);
|
|
92
|
+
const color = frame.colorOverrides.get(1);
|
|
93
|
+
assert.ok(color);
|
|
94
|
+
// Preparation entry in the palette, verbatim (intensity doesn't affect it).
|
|
95
|
+
assert.deepEqual(color, DEFAULT_PALETTE.PREPARATION);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('defaults hide upcoming products without drawing a ghost', () => {
|
|
99
|
+
const t = parseDate('2024-05-09T08:00:00Z');
|
|
100
|
+
const frame = computeAnimationFrame(data, t, settings({ preparationDays: 2 }));
|
|
101
|
+
// Default style is 'minimal' — hideBeforePreparation=true kicks in.
|
|
102
|
+
assert.ok(frame.hiddenIds.has(1));
|
|
103
|
+
assert.equal(frame.colorOverrides.size, 0);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('paints with task-type colour (scaled by paletteIntensity) in the middle of the active window', () => {
|
|
107
|
+
const t = parseDate('2024-05-15T00:00:00Z'); // ~mid-window
|
|
108
|
+
// Explicit intensity — don't piggyback on a preset default, keep
|
|
109
|
+
// the assertion's math self-contained.
|
|
110
|
+
const cfg = phased({ paletteIntensity: 0.6 });
|
|
111
|
+
const frame = computeAnimationFrame(data, t, cfg);
|
|
112
|
+
assert.equal(frame.stats.active, 2);
|
|
113
|
+
const color = frame.colorOverrides.get(1)!;
|
|
114
|
+
const expectedAlpha = DEFAULT_PALETTE.CONSTRUCTION[3] * cfg.paletteIntensity;
|
|
115
|
+
assert.deepEqual(color.slice(0, 3), DEFAULT_PALETTE.CONSTRUCTION.slice(0, 3));
|
|
116
|
+
assert.equal(color[3], expectedAlpha);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('paletteIntensity=1 restores full-strength task-type colour', () => {
|
|
120
|
+
const t = parseDate('2024-05-15T00:00:00Z');
|
|
121
|
+
const frame = computeAnimationFrame(data, t, phased({ paletteIntensity: 1 }));
|
|
122
|
+
assert.deepEqual(frame.colorOverrides.get(1), DEFAULT_PALETTE.CONSTRUCTION);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('paletteIntensity=0 suppresses the active-phase override entirely (but phase stats still populate)', () => {
|
|
126
|
+
const t = parseDate('2024-05-15T00:00:00Z');
|
|
127
|
+
const frame = computeAnimationFrame(data, t, phased({ paletteIntensity: 0 }));
|
|
128
|
+
assert.equal(frame.stats.active, 2);
|
|
129
|
+
assert.equal(frame.colorOverrides.size, 0);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('ramps opacity up during the first rampInFraction of the window', () => {
|
|
133
|
+
// 5% into the 10-day window: ~0.5 day from start. rampInFraction default 0.08 → inside ramp.
|
|
134
|
+
const start = parseDate('2024-05-10T08:00:00Z');
|
|
135
|
+
const t = start + 0.5 * DAY;
|
|
136
|
+
const frame = computeAnimationFrame(data, t, phased({ paletteIntensity: 1 }));
|
|
137
|
+
assert.equal(frame.stats['active-ramp-in'], 2);
|
|
138
|
+
const color = frame.colorOverrides.get(1)!;
|
|
139
|
+
// Ramp-in alpha < 1 and > 0 at intensity=1
|
|
140
|
+
assert.ok(color[3] > 0);
|
|
141
|
+
assert.ok(color[3] < 1);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('fades out (override alpha) during the last fadeOutFraction of the window', () => {
|
|
145
|
+
const finish = parseDate('2024-05-20T17:00:00Z');
|
|
146
|
+
const t = finish - 0.2 * DAY; // ~last 2 %
|
|
147
|
+
const frame = computeAnimationFrame(
|
|
148
|
+
data, t, phased({ fadeOutFraction: 0.10, paletteIntensity: 1 }),
|
|
149
|
+
);
|
|
150
|
+
assert.equal(frame.stats['active-settling'], 2);
|
|
151
|
+
const color = frame.colorOverrides.get(1)!;
|
|
152
|
+
assert.ok(color[3] < 1);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('emits no override after the task finishes', () => {
|
|
156
|
+
const t = parseDate('2024-06-01T00:00:00Z');
|
|
157
|
+
const frame = computeAnimationFrame(data, t, settings());
|
|
158
|
+
assert.equal(frame.stats.complete, 2);
|
|
159
|
+
assert.equal(frame.colorOverrides.size, 0);
|
|
160
|
+
assert.equal(frame.hiddenIds.size, 0);
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// ─── removal tasks (DEMOLITION etc.) ────────────────────────────────────
|
|
165
|
+
|
|
166
|
+
describe('computeAnimationFrame — removal tasks invert the lifecycle', () => {
|
|
167
|
+
const task = makeTask({
|
|
168
|
+
globalId: 'demo-1',
|
|
169
|
+
predefinedType: 'DEMOLITION',
|
|
170
|
+
productExpressIds: [9],
|
|
171
|
+
taskTime: { scheduleStart: '2024-05-10T00:00:00Z', scheduleFinish: '2024-05-12T00:00:00Z' },
|
|
172
|
+
});
|
|
173
|
+
const data = makeSchedule([task]);
|
|
174
|
+
|
|
175
|
+
it('leaves the product visible (no override) before the task starts', () => {
|
|
176
|
+
const frame = computeAnimationFrame(data, parseDate('2024-05-01T00:00:00Z'), settings());
|
|
177
|
+
assert.equal(frame.hiddenIds.size, 0);
|
|
178
|
+
assert.equal(frame.colorOverrides.size, 0);
|
|
179
|
+
assert.equal(frame.stats['upcoming-far'], 1);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('fades out with the removal tint while active (phased mode)', () => {
|
|
183
|
+
const frame = computeAnimationFrame(data, parseDate('2024-05-11T00:00:00Z'), phased());
|
|
184
|
+
assert.equal(frame.stats['removal-active'], 1);
|
|
185
|
+
const color = frame.colorOverrides.get(9)!;
|
|
186
|
+
assert.deepEqual(color.slice(0, 3), DEFAULT_PALETTE.DEMOLITION.slice(0, 3));
|
|
187
|
+
assert.ok(color[3] > 0 && color[3] < 1);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('hides the product after demolition is complete', () => {
|
|
191
|
+
const frame = computeAnimationFrame(data, parseDate('2024-05-20T00:00:00Z'), settings());
|
|
192
|
+
assert.ok(frame.hiddenIds.has(9));
|
|
193
|
+
assert.equal(frame.stats['removal-complete'], 1);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('skips inversion when animateDemolition=false', () => {
|
|
197
|
+
const frame = computeAnimationFrame(
|
|
198
|
+
data,
|
|
199
|
+
parseDate('2024-05-11T00:00:00Z'),
|
|
200
|
+
phased({ animateDemolition: false, paletteIntensity: 1 }),
|
|
201
|
+
);
|
|
202
|
+
// Falls through to the standard construction lifecycle, which paints the
|
|
203
|
+
// DEMOLITION palette colour at full strength (intensity=1) during the
|
|
204
|
+
// active window.
|
|
205
|
+
assert.equal(frame.stats.active, 1);
|
|
206
|
+
assert.deepEqual(frame.colorOverrides.get(9), DEFAULT_PALETTE.DEMOLITION);
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// ─── minimal style / settings flags ─────────────────────────────────────
|
|
211
|
+
|
|
212
|
+
describe('computeAnimationFrame — style / flag behaviour', () => {
|
|
213
|
+
const data = makeSchedule([makeTask({
|
|
214
|
+
productExpressIds: [1],
|
|
215
|
+
predefinedType: 'INSTALLATION',
|
|
216
|
+
taskTime: { scheduleStart: '2024-05-10T08:00:00Z', scheduleFinish: '2024-05-20T17:00:00Z' },
|
|
217
|
+
})]);
|
|
218
|
+
|
|
219
|
+
it('style=minimal emits visibility-timing hiddenIds but no colour overrides', () => {
|
|
220
|
+
// Active mid-window: nothing is hidden (timing says "visible"), no colour.
|
|
221
|
+
const frame = computeAnimationFrame(
|
|
222
|
+
data, parseDate('2024-05-15T00:00:00Z'),
|
|
223
|
+
settings(),
|
|
224
|
+
);
|
|
225
|
+
assert.equal(frame.colorOverrides.size, 0);
|
|
226
|
+
assert.equal(frame.hiddenIds.size, 0);
|
|
227
|
+
assert.equal(frame.stats.active, 1);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('style=minimal still hides upcoming products (timing layer always active)', () => {
|
|
231
|
+
const frame = computeAnimationFrame(
|
|
232
|
+
data, parseDate('2024-05-01T00:00:00Z'), // before task start
|
|
233
|
+
settings({ hideBeforePreparation: true }),
|
|
234
|
+
);
|
|
235
|
+
assert.ok(frame.hiddenIds.has(1));
|
|
236
|
+
assert.equal(frame.colorOverrides.size, 0);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('style=minimal still removes demolished products after their task finishes', () => {
|
|
240
|
+
const demo: ScheduleExtraction = {
|
|
241
|
+
hasSchedule: true, workSchedules: [], sequences: [],
|
|
242
|
+
tasks: [{
|
|
243
|
+
expressId: 0, globalId: 'd', name: 'Demo', isMilestone: false,
|
|
244
|
+
predefinedType: 'DEMOLITION', childGlobalIds: [],
|
|
245
|
+
productExpressIds: [7], productGlobalIds: [],
|
|
246
|
+
controllingScheduleGlobalIds: [],
|
|
247
|
+
taskTime: {
|
|
248
|
+
scheduleStart: '2024-05-01T00:00:00Z',
|
|
249
|
+
scheduleFinish: '2024-05-05T00:00:00Z',
|
|
250
|
+
},
|
|
251
|
+
}],
|
|
252
|
+
};
|
|
253
|
+
const frame = computeAnimationFrame(
|
|
254
|
+
demo, parseDate('2024-05-15T00:00:00Z'), // well after the task finishes
|
|
255
|
+
settings({ animateDemolition: true }),
|
|
256
|
+
);
|
|
257
|
+
assert.ok(frame.hiddenIds.has(7));
|
|
258
|
+
assert.equal(frame.colorOverrides.size, 0);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it('colorizeByTaskType=false suppresses the active-window override (phased)', () => {
|
|
262
|
+
const frame = computeAnimationFrame(
|
|
263
|
+
data, parseDate('2024-05-15T00:00:00Z'),
|
|
264
|
+
phased({ colorizeByTaskType: false }),
|
|
265
|
+
);
|
|
266
|
+
assert.equal(frame.stats.active, 1);
|
|
267
|
+
assert.equal(frame.colorOverrides.size, 0);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('showPreparationGhost=false respects hideBeforePreparation in the look-ahead window', () => {
|
|
271
|
+
const frame = computeAnimationFrame(
|
|
272
|
+
data, parseDate('2024-05-09T12:00:00Z'),
|
|
273
|
+
phased({ showPreparationGhost: false, hideBeforePreparation: true }),
|
|
274
|
+
);
|
|
275
|
+
assert.ok(frame.hiddenIds.has(1));
|
|
276
|
+
assert.equal(frame.colorOverrides.size, 0);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('applies a user-customized palette entry instead of the default', () => {
|
|
280
|
+
const custom = phased({
|
|
281
|
+
paletteIntensity: 1,
|
|
282
|
+
palette: {
|
|
283
|
+
...DEFAULT_PALETTE,
|
|
284
|
+
INSTALLATION: [1, 0, 1, 1], // full magenta override
|
|
285
|
+
},
|
|
286
|
+
});
|
|
287
|
+
const frame = computeAnimationFrame(
|
|
288
|
+
data, parseDate('2024-05-15T00:00:00Z'), custom,
|
|
289
|
+
);
|
|
290
|
+
assert.deepEqual(frame.colorOverrides.get(1), [1, 0, 1, 1]);
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
// ─── palette / multi-task resolution ───────────────────────────────────
|
|
295
|
+
|
|
296
|
+
describe('computeAnimationFrame — multi-task resolution', () => {
|
|
297
|
+
it('picks the highest-priority phase when two tasks control the same product', () => {
|
|
298
|
+
// Same product in a prep-phase task and an active-phase task — active wins.
|
|
299
|
+
const prepTask = makeTask({
|
|
300
|
+
globalId: 'prep',
|
|
301
|
+
predefinedType: 'CONSTRUCTION',
|
|
302
|
+
productExpressIds: [42],
|
|
303
|
+
taskTime: { scheduleStart: '2024-05-20T00:00:00Z', scheduleFinish: '2024-05-21T00:00:00Z' },
|
|
304
|
+
});
|
|
305
|
+
const activeTask = makeTask({
|
|
306
|
+
globalId: 'active',
|
|
307
|
+
predefinedType: 'INSTALLATION',
|
|
308
|
+
productExpressIds: [42],
|
|
309
|
+
taskTime: { scheduleStart: '2024-05-10T00:00:00Z', scheduleFinish: '2024-05-30T00:00:00Z' },
|
|
310
|
+
});
|
|
311
|
+
const frame = computeAnimationFrame(
|
|
312
|
+
makeSchedule([prepTask, activeTask]),
|
|
313
|
+
parseDate('2024-05-18T12:00:00Z'),
|
|
314
|
+
phased({ paletteIntensity: 1 }),
|
|
315
|
+
);
|
|
316
|
+
// Active task (INSTALLATION) wins; intensity=1 so the emitted colour
|
|
317
|
+
// equals the palette entry byte-for-byte.
|
|
318
|
+
const colour = frame.colorOverrides.get(42) as RGBA;
|
|
319
|
+
assert.deepEqual(colour, DEFAULT_PALETTE.INSTALLATION);
|
|
320
|
+
assert.equal(frame.stats.active, 1);
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
describe('computeAnimationFrame — schedule filter', () => {
|
|
325
|
+
const tasks = [
|
|
326
|
+
makeTask({
|
|
327
|
+
globalId: 'A', predefinedType: 'CONSTRUCTION',
|
|
328
|
+
productExpressIds: [1],
|
|
329
|
+
controllingScheduleGlobalIds: ['sched-1'],
|
|
330
|
+
taskTime: { scheduleStart: '2024-05-10T00:00:00Z', scheduleFinish: '2024-05-20T00:00:00Z' },
|
|
331
|
+
}),
|
|
332
|
+
makeTask({
|
|
333
|
+
globalId: 'B', predefinedType: 'CONSTRUCTION',
|
|
334
|
+
productExpressIds: [2],
|
|
335
|
+
controllingScheduleGlobalIds: ['sched-2'],
|
|
336
|
+
taskTime: { scheduleStart: '2024-05-10T00:00:00Z', scheduleFinish: '2024-05-20T00:00:00Z' },
|
|
337
|
+
}),
|
|
338
|
+
];
|
|
339
|
+
it('respects activeWorkScheduleId filter', () => {
|
|
340
|
+
const frame = computeAnimationFrame(
|
|
341
|
+
makeSchedule(tasks),
|
|
342
|
+
parseDate('2024-05-15T00:00:00Z'),
|
|
343
|
+
phased(),
|
|
344
|
+
'sched-1',
|
|
345
|
+
);
|
|
346
|
+
assert.ok(frame.colorOverrides.has(1));
|
|
347
|
+
assert.ok(!frame.colorOverrides.has(2));
|
|
348
|
+
});
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
describe('computeAnimationFrame — defensive inputs', () => {
|
|
352
|
+
it('returns empty frame for null data', () => {
|
|
353
|
+
const frame = computeAnimationFrame(null, Date.now(), settings());
|
|
354
|
+
assert.equal(frame.colorOverrides.size, 0);
|
|
355
|
+
assert.equal(frame.hiddenIds.size, 0);
|
|
356
|
+
});
|
|
357
|
+
it('skips tasks without scheduled times', () => {
|
|
358
|
+
const frame = computeAnimationFrame(
|
|
359
|
+
makeSchedule([makeTask({ productExpressIds: [5] /* no taskTime */ })]),
|
|
360
|
+
Date.now(),
|
|
361
|
+
settings(),
|
|
362
|
+
);
|
|
363
|
+
assert.equal(frame.colorOverrides.size, 0);
|
|
364
|
+
});
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
// ─── untasked-product hide (coverage-gap workaround) ─────────────────────
|
|
368
|
+
|
|
369
|
+
describe('computeAnimationFrame — hideUntaskedProducts', () => {
|
|
370
|
+
const taskedProduct = 100;
|
|
371
|
+
const untaskedProduct = 200;
|
|
372
|
+
const data = makeSchedule([
|
|
373
|
+
makeTask({
|
|
374
|
+
productExpressIds: [taskedProduct],
|
|
375
|
+
predefinedType: 'CONSTRUCTION',
|
|
376
|
+
taskTime: { scheduleStart: '2024-05-10T00:00:00Z', scheduleFinish: '2024-05-20T00:00:00Z' },
|
|
377
|
+
}),
|
|
378
|
+
]);
|
|
379
|
+
|
|
380
|
+
it('hides products not covered by any task when enabled and allProductIds supplied', () => {
|
|
381
|
+
const frame = computeAnimationFrame(
|
|
382
|
+
data, parseDate('2024-05-15T00:00:00Z'),
|
|
383
|
+
settings({ hideUntaskedProducts: true }),
|
|
384
|
+
null,
|
|
385
|
+
[taskedProduct, untaskedProduct],
|
|
386
|
+
);
|
|
387
|
+
// Untasked product is added to hidden; tasked product is active (visible).
|
|
388
|
+
assert.ok(frame.hiddenIds.has(untaskedProduct));
|
|
389
|
+
assert.ok(!frame.hiddenIds.has(taskedProduct));
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
it('leaves untasked products untouched when the setting is off', () => {
|
|
393
|
+
const frame = computeAnimationFrame(
|
|
394
|
+
data, parseDate('2024-05-15T00:00:00Z'),
|
|
395
|
+
settings({ hideUntaskedProducts: false }),
|
|
396
|
+
null,
|
|
397
|
+
[taskedProduct, untaskedProduct],
|
|
398
|
+
);
|
|
399
|
+
assert.ok(!frame.hiddenIds.has(untaskedProduct));
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
it('is a no-op when allProductIds is not supplied (caller opt-in)', () => {
|
|
403
|
+
const frame = computeAnimationFrame(
|
|
404
|
+
data, parseDate('2024-05-15T00:00:00Z'),
|
|
405
|
+
settings({ hideUntaskedProducts: true }),
|
|
406
|
+
// no 5th arg — animator can't know the model universe
|
|
407
|
+
);
|
|
408
|
+
assert.equal(frame.hiddenIds.size, 0);
|
|
409
|
+
});
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
// ─── completed tint ──────────────────────────────────────────────────────
|
|
413
|
+
|
|
414
|
+
describe('computeAnimationFrame — showCompletedTint', () => {
|
|
415
|
+
const data = makeSchedule([
|
|
416
|
+
makeTask({
|
|
417
|
+
productExpressIds: [1],
|
|
418
|
+
predefinedType: 'CONSTRUCTION',
|
|
419
|
+
taskTime: { scheduleStart: '2024-05-10T00:00:00Z', scheduleFinish: '2024-05-20T00:00:00Z' },
|
|
420
|
+
}),
|
|
421
|
+
]);
|
|
422
|
+
|
|
423
|
+
it('emits the COMPLETED palette colour after a task finishes (phased + opt-in)', () => {
|
|
424
|
+
const frame = computeAnimationFrame(
|
|
425
|
+
data, parseDate('2024-06-01T00:00:00Z'),
|
|
426
|
+
phased({ showCompletedTint: true }),
|
|
427
|
+
);
|
|
428
|
+
assert.equal(frame.stats.complete, 1);
|
|
429
|
+
assert.deepEqual(frame.colorOverrides.get(1), DEFAULT_PALETTE.COMPLETED);
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
it('emits no override after finish when the tint is off (default)', () => {
|
|
433
|
+
const frame = computeAnimationFrame(
|
|
434
|
+
data, parseDate('2024-06-01T00:00:00Z'),
|
|
435
|
+
phased({ showCompletedTint: false }),
|
|
436
|
+
);
|
|
437
|
+
assert.equal(frame.colorOverrides.size, 0);
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
it('emits the completed tint whenever its flag is on, regardless of other colour flags', () => {
|
|
441
|
+
// Post-P1.3: `emitColours` is derived — ANY colour flag being on
|
|
442
|
+
// turns on the colour pipeline. So a tint-only configuration emits
|
|
443
|
+
// its colour without needing `colorizeByTaskType` or a separate
|
|
444
|
+
// `style` mode.
|
|
445
|
+
const frame = computeAnimationFrame(
|
|
446
|
+
data, parseDate('2024-06-01T00:00:00Z'),
|
|
447
|
+
settings({ showCompletedTint: true }),
|
|
448
|
+
);
|
|
449
|
+
assert.equal(frame.colorOverrides.size, 1);
|
|
450
|
+
assert.deepEqual(frame.colorOverrides.get(1), DEFAULT_PALETTE.COMPLETED);
|
|
451
|
+
});
|
|
452
|
+
});
|