@ifc-lite/viewer 1.27.0 → 1.28.1

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 (162) hide show
  1. package/.turbo/turbo-build.log +35 -42
  2. package/CHANGELOG.md +74 -0
  3. package/dist/assets/{basketViewActivator-B3CdrLsb.js → basketViewActivator-Ce38DhXd.js} +8 -8
  4. package/dist/assets/{bcf-QeHK_Aud.js → bcf-Cv_O3JfD.js} +56 -56
  5. package/dist/assets/{decode-worker-CgM1iNSK.js → decode-worker-Cjign7Zh.js} +1 -1
  6. package/dist/assets/{deflate-B-d0SYQM.js → deflate-HbyMq59o.js} +1 -1
  7. package/dist/assets/drawing-2d-DW98umlt.js +257 -0
  8. package/dist/assets/e57-source-2wI9jkCA.js +1 -0
  9. package/dist/assets/{exporters-B4LbZFeT.js → exporters-BuD3XRzB.js} +1309 -1153
  10. package/dist/assets/geometry.worker-TH3fCCoY.js +1 -0
  11. package/dist/assets/{geotiff-CrVtDRFq.js → geotiff-B2HA8Bwm.js} +10 -10
  12. package/dist/assets/{ids-DjsGFN10.js → ids-DYUFMd5f.js} +952 -945
  13. package/dist/assets/{ifc-lite_bg-DsYUIHm3.wasm → ifc-lite_bg-BEA5DLmg.wasm} +0 -0
  14. package/dist/assets/index-E9wB0zWt.css +1 -0
  15. package/dist/assets/{index-COYokSKc.js → index-n5O1QJMM.js} +37877 -38126
  16. package/dist/assets/{index.es-CY202jA3.js → index.es-BKVIpZgL.js} +9 -9
  17. package/dist/assets/{jpeg-D4wOkf5h.js → jpeg-C7hjKjPX.js} +1 -1
  18. package/dist/assets/{jspdf.es.min-DIGb9BHN.js → jspdf.es.min-oWlFc42Y.js} +4 -4
  19. package/dist/assets/lens-C4p1kQ0p.js +1 -0
  20. package/dist/assets/{lerc-DmW0_tgf.js → lerc-BfIOGhQz.js} +1 -1
  21. package/dist/assets/{lzw-oWetY-d6.js → lzw-B0jRuuW5.js} +1 -1
  22. package/dist/assets/{native-bridge-BX8_tHXE.js → native-bridge-DpB-dtEn.js} +6 -3
  23. package/dist/assets/{packbits-F8Nkp4NY.js → packbits-DVvBTC39.js} +1 -1
  24. package/dist/assets/parser.worker-BDsWQ6rc.js +182 -0
  25. package/dist/assets/{pdf-Dsh3HPZB.js → pdf-dVIqI5ac.js} +10 -10
  26. package/dist/assets/raw-C0ZJYGmN.js +1 -0
  27. package/dist/assets/{sandbox-BAC3a-eN.js → sandbox-qpJlrNN0.js} +2962 -2554
  28. package/dist/assets/server-client-DVZ2huNS.js +719 -0
  29. package/dist/assets/{webimage-BLV1dgmd.js → webimage-B394g0Tw.js} +1 -1
  30. package/dist/assets/{xlsx-Bc2HTrjC.js → xlsx-D-oHO76J.js} +8 -8
  31. package/dist/assets/{zstd-C_1HxVrA.js → zstd-Bf38MwV2.js} +1 -1
  32. package/dist/index.html +9 -9
  33. package/package.json +24 -23
  34. package/src/App.tsx +1 -3
  35. package/src/components/mcp/playground-dispatcher.ts +3 -0
  36. package/src/components/mcp/playground-files.ts +33 -1
  37. package/src/components/viewer/BCFPanel.tsx +1 -16
  38. package/src/components/viewer/ChatPanel.tsx +11 -46
  39. package/src/components/viewer/CommandPalette.tsx +6 -1
  40. package/src/components/viewer/ComparePanel.tsx +420 -0
  41. package/src/components/viewer/HierarchyPanel.tsx +48 -183
  42. package/src/components/viewer/IDSPanel.tsx +1 -26
  43. package/src/components/viewer/MainToolbar.tsx +94 -187
  44. package/src/components/viewer/MobileToolbar.tsx +1 -9
  45. package/src/components/viewer/PropertiesPanel.tsx +98 -127
  46. package/src/components/viewer/ScriptPanel.tsx +8 -34
  47. package/src/components/viewer/Section2DPanel.tsx +32 -1
  48. package/src/components/viewer/ViewerLayout.tsx +5 -2
  49. package/src/components/viewer/Viewport.tsx +3 -0
  50. package/src/components/viewer/ViewportContainer.tsx +24 -42
  51. package/src/components/viewer/ViewportOverlays.tsx +1 -4
  52. package/src/components/viewer/hierarchy/HierarchyNode.tsx +3 -3
  53. package/src/components/viewer/hierarchy/ifc-icons.ts +9 -0
  54. package/src/components/viewer/hierarchy/treeDataBuilder.ts +87 -0
  55. package/src/components/viewer/hierarchy/types.ts +1 -0
  56. package/src/components/viewer/hierarchy/useHierarchyTree.ts +6 -2
  57. package/src/components/viewer/properties/MaterialTotalsPanel.tsx +283 -0
  58. package/src/components/viewer/useGeometryStreaming.ts +0 -2
  59. package/src/hooks/federationLoadGate.test.ts +12 -2
  60. package/src/hooks/federationLoadGate.ts +9 -2
  61. package/src/hooks/ingest/federationAlign.ts +488 -0
  62. package/src/hooks/ingest/viewerModelIngest.ts +3 -212
  63. package/src/hooks/useCompare.ts +0 -0
  64. package/src/hooks/useCompareOverlay.ts +119 -0
  65. package/src/hooks/useDrawingGeneration.ts +234 -14
  66. package/src/hooks/useIfc.ts +1 -1
  67. package/src/hooks/useIfcCache.ts +100 -24
  68. package/src/hooks/useIfcFederation.ts +42 -811
  69. package/src/hooks/useIfcLoader.ts +349 -1517
  70. package/src/hooks/useIfcServer.ts +3 -0
  71. package/src/hooks/useLens.ts +5 -1
  72. package/src/hooks/useSymbolicAnnotations.ts +70 -38
  73. package/src/lib/compare/buildFingerprints.ts +173 -0
  74. package/src/lib/compare/describeChange.ts +0 -0
  75. package/src/lib/compare/geometricData.test.ts +54 -0
  76. package/src/lib/compare/geometricData.ts +37 -0
  77. package/src/lib/compare/overlay.test.ts +99 -0
  78. package/src/lib/compare/overlay.ts +91 -0
  79. package/src/lib/geo/cesium-placement.ts +1 -1
  80. package/src/lib/geo/reproject.ts +4 -1
  81. package/src/lib/llm/script-edit-ops.ts +23 -0
  82. package/src/lib/llm/stream-client.ts +8 -1
  83. package/src/lib/search/result-export.ts +7 -1
  84. package/src/sdk/adapters/export-adapter.ts +6 -1
  85. package/src/services/cacheService.ts +9 -25
  86. package/src/services/desktop-export.ts +2 -59
  87. package/src/services/file-dialog.ts +8 -142
  88. package/src/store/constants.ts +23 -0
  89. package/src/store/globalId.ts +15 -13
  90. package/src/store/index.ts +19 -6
  91. package/src/store/slices/cesiumSlice.ts +8 -1
  92. package/src/store/slices/compareSlice.ts +96 -0
  93. package/src/store/slices/drawing2DSlice.ts +8 -0
  94. package/src/store/slices/lensSlice.ts +8 -0
  95. package/src/store/slices/visibilitySlice.ts +22 -1
  96. package/src/store/types.ts +1 -71
  97. package/src/utils/acquireFileBuffer.test.ts +12 -4
  98. package/src/utils/ifcConfig.ts +0 -12
  99. package/src/utils/loadingUtils.ts +32 -0
  100. package/src/utils/spatialHierarchy.test.ts +53 -1
  101. package/src/utils/spatialHierarchy.ts +42 -2
  102. package/src/vite-env.d.ts +2 -0
  103. package/vite.config.ts +6 -3
  104. package/DESKTOP_CONTRACT_VERSION +0 -1
  105. package/dist/assets/drawing-2d-C71b8Ugx.js +0 -257
  106. package/dist/assets/e57-source-CQHxE8n3.js +0 -1
  107. package/dist/assets/event-B0kAzHa-.js +0 -1
  108. package/dist/assets/geometry.worker-BdH-E6NB.js +0 -1
  109. package/dist/assets/index-ajK6D32J.css +0 -1
  110. package/dist/assets/lens-PYsLu_MA.js +0 -1
  111. package/dist/assets/parser.worker-D591Zu_-.js +0 -182
  112. package/dist/assets/raw-D9iw0tmc.js +0 -1
  113. package/dist/assets/server-client-Cjwnm7il.js +0 -706
  114. package/dist/assets/tauri-core-stub-D8Fa-u43.js +0 -1
  115. package/dist/assets/tauri-dialog-stub-r7Wksg7o.js +0 -1
  116. package/dist/assets/tauri-fs-stub-BdeRC7aK.js +0 -1
  117. package/src/components/viewer/DesktopEntitlementBanner.tsx +0 -74
  118. package/src/components/viewer/SettingsPage.tsx +0 -581
  119. package/src/hooks/ingest/resolveDataStoreOrAbort.test.ts +0 -61
  120. package/src/hooks/ingest/resolveDataStoreOrAbort.ts +0 -28
  121. package/src/hooks/ingest/watchedGeometryStream.test.ts +0 -78
  122. package/src/hooks/ingest/watchedGeometryStream.ts +0 -76
  123. package/src/lib/desktop/desktopEntitlementEvents.ts +0 -39
  124. package/src/lib/desktop-entitlement.ts +0 -43
  125. package/src/lib/desktop-product.ts +0 -130
  126. package/src/lib/platform.ts +0 -23
  127. package/src/services/desktop-cache.ts +0 -186
  128. package/src/services/desktop-harness.ts +0 -196
  129. package/src/services/desktop-logger.ts +0 -20
  130. package/src/services/desktop-native-metadata.ts +0 -230
  131. package/src/services/desktop-panel-actions.ts +0 -43
  132. package/src/services/desktop-preferences.ts +0 -44
  133. package/src/services/fs-cache.ts +0 -212
  134. package/src/services/tauri-core-stub.ts +0 -7
  135. package/src/services/tauri-dialog-stub.ts +0 -7
  136. package/src/services/tauri-fs-stub.ts +0 -7
  137. package/src/services/tauri-modules.d.ts +0 -50
  138. package/src/store/slices/desktopEntitlementSlice.ts +0 -86
  139. package/src/utils/desktopModelSnapshot.ts +0 -358
  140. package/src/utils/nativeSpatialDataStore.ts +0 -277
  141. package/src-tauri/Cargo.toml +0 -29
  142. package/src-tauri/build.rs +0 -7
  143. package/src-tauri/capabilities/default.json +0 -18
  144. package/src-tauri/icons/128x128.png +0 -0
  145. package/src-tauri/icons/128x128@2x.png +0 -0
  146. package/src-tauri/icons/32x32.png +0 -0
  147. package/src-tauri/icons/Square107x107Logo.png +0 -0
  148. package/src-tauri/icons/Square142x142Logo.png +0 -0
  149. package/src-tauri/icons/Square150x150Logo.png +0 -0
  150. package/src-tauri/icons/Square284x284Logo.png +0 -0
  151. package/src-tauri/icons/Square30x30Logo.png +0 -0
  152. package/src-tauri/icons/Square310x310Logo.png +0 -0
  153. package/src-tauri/icons/Square44x44Logo.png +0 -0
  154. package/src-tauri/icons/Square71x71Logo.png +0 -0
  155. package/src-tauri/icons/Square89x89Logo.png +0 -0
  156. package/src-tauri/icons/StoreLogo.png +0 -0
  157. package/src-tauri/icons/icon.icns +0 -0
  158. package/src-tauri/icons/icon.ico +0 -0
  159. package/src-tauri/icons/icon.png +0 -0
  160. package/src-tauri/src/lib.rs +0 -21
  161. package/src-tauri/src/main.rs +0 -10
  162. package/src-tauri/tauri.conf.json +0 -39
@@ -2,16 +2,11 @@
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
- import { IfcParser, parseIfcx, type IfcDataStore, type PointCloudExtraction } from '@ifc-lite/parser';
6
- import { WorkerParser } from '@ifc-lite/parser/browser';
7
- import { GeometryProcessor, GeometryQuality, type CoordinateInfo, type DynamicBatchConfig, type GeometryResult, type MeshData, type PointCloudAsset } from '@ifc-lite/geometry';
5
+ import { parseIfcx, type IfcDataStore, type PointCloudExtraction } from '@ifc-lite/parser';
6
+ import { type GeometryResult, type MeshData, type PointCloudAsset } from '@ifc-lite/geometry';
8
7
  import { loadGLBToMeshData } from '@ifc-lite/cache';
9
8
  import type { SchemaVersion } from '../../store/types.js';
10
- import { calculateMeshBounds, calculateStoreyHeights, createCoordinateInfo, normalizeColor } from '../../utils/localParsingUtils.js';
11
- import { resolveDataStoreOrAbort } from './resolveDataStoreOrAbort.js';
12
- import { watchedGeometryStream } from './watchedGeometryStream.js';
13
-
14
- type RgbaColor = [number, number, number, number];
9
+ import { calculateMeshBounds, createCoordinateInfo, normalizeColor } from '../../utils/localParsingUtils.js';
15
10
 
16
11
  interface RawIfcxMesh {
17
12
  expressId?: number;
@@ -31,40 +26,6 @@ export interface ViewerModelPayload {
31
26
  schemaVersion: SchemaVersion;
32
27
  }
33
28
 
34
- export interface StepBatchEvent {
35
- batchIndex: number;
36
- estimatedTotal: number;
37
- totalSoFar: number;
38
- meshes: MeshData[];
39
- coordinateInfo?: CoordinateInfo | null;
40
- }
41
-
42
- export interface StepRtcOffsetEvent {
43
- rtcOffset: { x: number; y: number; z: number };
44
- }
45
-
46
- export interface StepBufferIngestOptions {
47
- fileName: string;
48
- buffer: ArrayBuffer;
49
- fileSizeMB: number;
50
- getDynamicBatchSize: (fileSizeMB: number) => number | DynamicBatchConfig;
51
- onProgress?: (progress: { phase: string; percent: number }) => void;
52
- onBatch?: (event: StepBatchEvent) => void;
53
- onColorUpdate?: (updates: Map<number, RgbaColor>) => void;
54
- onSpatialReady?: (dataStore: IfcDataStore) => void;
55
- onRtcOffset?: (event: StepRtcOffsetEvent) => void;
56
- shouldAbort?: () => boolean;
57
- /** Shared RTC offset from first federated model (IFC Z-up coords).
58
- * When set, this model uses the same RTC as the first model instead of
59
- * computing its own, ensuring all models share the same coordinate space. */
60
- sharedRtcOffset?: { x: number; y: number; z: number };
61
- }
62
-
63
- export interface StepBufferIngestResult extends ViewerModelPayload {
64
- allMeshes: MeshData[];
65
- cumulativeColorUpdates: Map<number, RgbaColor>;
66
- }
67
-
68
29
  export function convertIfcxMeshes(rawMeshes: RawIfcxMesh[]): MeshData[] {
69
30
  return rawMeshes.map((mesh) => {
70
31
  const positions = mesh.positions instanceof Float32Array ? mesh.positions : new Float32Array(mesh.positions || []);
@@ -99,16 +60,6 @@ export function createMinimalGlbDataStore(buffer: ArrayBuffer, meshCount: number
99
60
  } as unknown as IfcDataStore;
100
61
  }
101
62
 
102
- export function normalizeDataStoreStoreys(dataStore: IfcDataStore): IfcDataStore {
103
- if (dataStore.spatialHierarchy && dataStore.spatialHierarchy.storeyHeights.size === 0 && dataStore.spatialHierarchy.storeyElevations.size > 1) {
104
- const calculatedHeights = calculateStoreyHeights(dataStore.spatialHierarchy.storeyElevations);
105
- for (const [storeyId, height] of calculatedHeights) {
106
- dataStore.spatialHierarchy.storeyHeights.set(storeyId, height);
107
- }
108
- }
109
- return dataStore;
110
- }
111
-
112
63
  export function getMaxExpressId(dataStore: IfcDataStore, meshes: MeshData[]): number {
113
64
  const maxExpressIdFromMeshes = meshes.reduce((max, mesh) => Math.max(max, mesh.expressId), 0);
114
65
  let maxExpressIdFromEntities = 0;
@@ -213,163 +164,3 @@ export async function parseGlbViewerModel(buffer: ArrayBuffer): Promise<ViewerMo
213
164
  schemaVersion: 'IFC4',
214
165
  };
215
166
  }
216
-
217
- export async function parseStepBufferViewerModel(options: StepBufferIngestOptions): Promise<StepBufferIngestResult> {
218
- const geometryProcessor = new GeometryProcessor({ quality: GeometryQuality.Balanced });
219
- await geometryProcessor.init();
220
-
221
- const parser = new IfcParser();
222
- const wasmApi = geometryProcessor.getApi();
223
- const canShareSource = WorkerParser.isSupported();
224
- const sharedSource = canShareSource ? new SharedArrayBuffer(options.buffer.byteLength) : null;
225
- if (sharedSource) {
226
- new Uint8Array(sharedSource).set(new Uint8Array(options.buffer));
227
- }
228
- const geometryWillEmitEntityIndex =
229
- sharedSource !== null
230
- && options.fileSizeMB >= 2
231
- && typeof Worker !== 'undefined'
232
- && typeof navigator !== 'undefined'
233
- && (navigator.hardwareConcurrency ?? 1) > 1;
234
- let workerParser: WorkerParser | null = null;
235
- const allMeshes: MeshData[] = [];
236
- const cumulativeColorUpdates = new Map<number, RgbaColor>();
237
- let finalCoordinateInfo: CoordinateInfo | null = null;
238
- let batchIndex = 0;
239
- let estimatedTotal = 0;
240
- let capturedRtcOffset: { x: number; y: number; z: number } | null = null;
241
-
242
- const handleSpatialReady = (partialStore: IfcDataStore) => {
243
- if (options.shouldAbort?.()) {
244
- return;
245
- }
246
- options.onSpatialReady?.(normalizeDataStoreStoreys(partialStore));
247
- };
248
- const dataStorePromise = sharedSource
249
- ? (() => {
250
- workerParser = new WorkerParser();
251
- return workerParser.parseColumnar(sharedSource, {
252
- waitForEntityIndex: geometryWillEmitEntityIndex,
253
- onSpatialReady: handleSpatialReady,
254
- }).catch((error) => {
255
- console.warn('[viewerModelIngest] Parser worker failed, falling back to main-thread parse:', error);
256
- return parser.parseColumnar(options.buffer, {
257
- wasmApi: wasmApi ?? undefined,
258
- onSpatialReady: handleSpatialReady,
259
- });
260
- });
261
- })()
262
- : parser.parseColumnar(options.buffer, {
263
- wasmApi: wasmApi ?? undefined,
264
- onSpatialReady: handleSpatialReady,
265
- });
266
-
267
- const geometryView = sharedSource ? new Uint8Array(sharedSource) : new Uint8Array(options.buffer);
268
- const geometryStream = geometryProcessor.processAdaptive(geometryView, {
269
- sizeThreshold: 2 * 1024 * 1024,
270
- batchSize: options.getDynamicBatchSize(options.fileSizeMB),
271
- sharedRtcOffset: options.sharedRtcOffset,
272
- existingSab: sharedSource ?? undefined,
273
- onEntityIndex: (ids, starts, lengths) => {
274
- workerParser?.setEntityIndex(ids, starts, lengths);
275
- },
276
- });
277
- let lastTotalMeshes = 0;
278
- // The federated/added-model path was missing the size-aware stream watchdog
279
- // the single-model loader has, so a geometry worker that failed to spawn would
280
- // hang the load forever on "Processing geometry (N meshes)" instead of
281
- // surfacing a recoverable error. watchedGeometryStream re-yields each event
282
- // under that watchdog and bounds iterator teardown on every exit path.
283
- try {
284
- for await (const event of watchedGeometryStream(geometryStream, {
285
- fileName: options.fileName,
286
- fileSizeMB: options.fileSizeMB,
287
- shouldAbort: options.shouldAbort,
288
- getBatchCount: () => batchIndex,
289
- getLastTotalMeshes: () => lastTotalMeshes,
290
- })) {
291
- switch (event.type) {
292
- case 'start':
293
- estimatedTotal = event.totalEstimate;
294
- break;
295
- case 'colorUpdate':
296
- for (const [expressId, color] of event.updates) {
297
- cumulativeColorUpdates.set(expressId, color);
298
- }
299
- options.onColorUpdate?.(event.updates);
300
- break;
301
- case 'rtcOffset':
302
- if (event.hasRtc) {
303
- capturedRtcOffset = event.rtcOffset;
304
- options.onRtcOffset?.({ rtcOffset: event.rtcOffset });
305
- }
306
- break;
307
- case 'batch':
308
- batchIndex += 1;
309
- for (let i = 0; i < event.meshes.length; i++) {
310
- allMeshes.push(event.meshes[i]);
311
- }
312
- finalCoordinateInfo = event.coordinateInfo ?? null;
313
- lastTotalMeshes = event.totalSoFar;
314
- options.onBatch?.({
315
- batchIndex,
316
- estimatedTotal,
317
- totalSoFar: event.totalSoFar,
318
- meshes: event.meshes,
319
- coordinateInfo: event.coordinateInfo ?? null,
320
- });
321
- options.onProgress?.({
322
- phase: `Processing geometry (${event.totalSoFar} meshes)`,
323
- percent: 10 + Math.min(80, (allMeshes.length / 1000) * 0.8),
324
- });
325
- break;
326
- case 'complete':
327
- finalCoordinateInfo = event.coordinateInfo ?? null;
328
- break;
329
- }
330
- }
331
- } catch (err) {
332
- // Watchdog stall (or other stream error): the parser worker may be
333
- // blocked in `waitForEntityIndex`, which only the geometry pre-pass would
334
- // unblock. Terminate it here so it doesn't leak — the normal path below
335
- // still awaits it via resolveDataStoreOrAbort. watchedGeometryStream's
336
- // finally has already bounded teardown of the geometry iterator itself.
337
- workerParser?.terminate();
338
- throw err;
339
- }
340
-
341
- // If the load was cancelled, don't await dataStorePromise: a worker parse
342
- // started with waitForEntityIndex blocks until the geometry pre-pass hands
343
- // over the entity index, which never happens once the geometry loop has been
344
- // aborted above. resolveDataStoreOrAbort terminates the worker and throws an
345
- // AbortError instead of hanging here.
346
- const dataStore = normalizeDataStoreStoreys(
347
- await resolveDataStoreOrAbort(dataStorePromise, {
348
- aborted: options.shouldAbort?.() ?? false,
349
- terminate: () => workerParser?.terminate(),
350
- }),
351
- );
352
- if (!finalCoordinateInfo) {
353
- finalCoordinateInfo = createCoordinateInfo(calculateMeshBounds(allMeshes).bounds);
354
- }
355
- if (capturedRtcOffset) {
356
- finalCoordinateInfo.wasmRtcOffset = capturedRtcOffset;
357
- }
358
-
359
- return {
360
- dataStore,
361
- geometryResult: {
362
- meshes: allMeshes,
363
- totalVertices: allMeshes.reduce((sum, mesh) => sum + mesh.positions.length / 3, 0),
364
- totalTriangles: allMeshes.reduce((sum, mesh) => sum + mesh.indices.length / 3, 0),
365
- coordinateInfo: finalCoordinateInfo,
366
- },
367
- schemaVersion: dataStore.schemaVersion === 'IFC4X3'
368
- ? 'IFC4X3'
369
- : dataStore.schemaVersion === 'IFC4'
370
- ? 'IFC4'
371
- : 'IFC2X3',
372
- allMeshes,
373
- cumulativeColorUpdates,
374
- };
375
- }
Binary file
@@ -0,0 +1,119 @@
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
+ /**
6
+ * Applies the model-comparison result to the 3D viewport (issue #924):
7
+ * per-entity colour overrides (added/modified/deleted/unchanged) plus a hidden
8
+ * set that suppresses the duplicated base-model geometry.
9
+ *
10
+ * Colours go through the same single overlay channel `useLens` uses
11
+ * (`setPendingColorUpdates` → `scene.setColorOverrides` — overlay batches drawn
12
+ * over the original geometry, instant to clear). The hidden set is reconciled
13
+ * with ownership tracking lifted from `useOverlayCompositor`: we remember which
14
+ * ids we hid and whether the user had already hidden them, so teardown only
15
+ * un-hides what we actually contributed.
16
+ *
17
+ * Mounted inside `ComparePanel`, so closing the panel (or clearing the result)
18
+ * restores the scene automatically.
19
+ */
20
+
21
+ import { useEffect, useRef } from 'react';
22
+ import { useViewerStore } from '@/store';
23
+ import { buildCompareOverlay } from '@/lib/compare/overlay';
24
+
25
+ type ViewerStore = ReturnType<typeof useViewerStore.getState>;
26
+
27
+ /** Hand the shared colour channel back to its prior owner when compare lets go:
28
+ * if a lens is still active, restore its overlay; otherwise clear. Prevents the
29
+ * compare panel from blanking lens colours on close (an active lens only has
30
+ * its panel hidden, not deactivated). */
31
+ function handBackColorChannel(store: ViewerStore): void {
32
+ const lensColors = store.lensAppliedColors;
33
+ store.setPendingColorUpdates(lensColors && lensColors.size > 0 ? new Map(lensColors) : new Map());
34
+ }
35
+
36
+ /** Re-show ids we hid that the user wasn't already hiding, then drop ownership. */
37
+ function restoreOwnedHidden(owned: Map<number, boolean>, store: ViewerStore): void {
38
+ if (owned.size === 0) return;
39
+ const toShow: number[] = [];
40
+ for (const [id, wasHidden] of owned) {
41
+ if (wasHidden === false) toShow.push(id);
42
+ }
43
+ if (toShow.length > 0) store.showEntities(toShow);
44
+ owned.clear();
45
+ }
46
+
47
+ /** Reconcile the desired hidden set against what we previously contributed. */
48
+ function reconcileHidden(
49
+ nextHidden: Set<number>,
50
+ ownedRef: { current: Map<number, boolean> },
51
+ store: ViewerStore,
52
+ ): void {
53
+ const prev = ownedRef.current;
54
+ const currentlyHidden = store.hiddenEntities ?? new Set<number>();
55
+
56
+ const toShow: number[] = [];
57
+ for (const [id, wasHidden] of prev) {
58
+ if (!nextHidden.has(id) && wasHidden === false) toShow.push(id);
59
+ }
60
+
61
+ const next = new Map<number, boolean>();
62
+ const toHide: number[] = [];
63
+ for (const id of nextHidden) {
64
+ if (prev.has(id)) {
65
+ next.set(id, prev.get(id)!);
66
+ } else {
67
+ const wasHidden = currentlyHidden.has(id);
68
+ next.set(id, wasHidden);
69
+ if (!wasHidden) toHide.push(id);
70
+ }
71
+ }
72
+
73
+ if (toShow.length > 0) store.showEntities(toShow);
74
+ if (toHide.length > 0) store.hideEntities(toHide);
75
+ ownedRef.current = next;
76
+ }
77
+
78
+ export function useCompareOverlay(): void {
79
+ // global id → "was the user already hiding this when we took over?"
80
+ const ownedHiddenRef = useRef<Map<number, boolean>>(new Map());
81
+ const colorActiveRef = useRef(false);
82
+
83
+ const compareResult = useViewerStore((s) => s.compareResult);
84
+ const showUnchanged = useViewerStore((s) => s.compareShowUnchanged);
85
+
86
+ useEffect(() => {
87
+ const store = useViewerStore.getState();
88
+
89
+ if (!compareResult) {
90
+ if (colorActiveRef.current) {
91
+ handBackColorChannel(store);
92
+ colorActiveRef.current = false;
93
+ }
94
+ restoreOwnedHidden(ownedHiddenRef.current, store);
95
+ return;
96
+ }
97
+
98
+ const { colorOverrides, hiddenIds } = buildCompareOverlay(compareResult.diff, showUnchanged);
99
+ reconcileHidden(hiddenIds, ownedHiddenRef, store);
100
+ // Empty map signals the consumer to clear overlays (lens contract).
101
+ store.setPendingColorUpdates(colorOverrides);
102
+ // We own the colour channel whenever a comparison is shown — even an empty
103
+ // override map clobbered any prior lens colours — so teardown must hand the
104
+ // channel back regardless of the map size (don't gate on `.size`).
105
+ colorActiveRef.current = true;
106
+ }, [compareResult, showUnchanged]);
107
+
108
+ // Teardown on unmount (panel closed) — restore the scene we touched.
109
+ useEffect(() => {
110
+ return () => {
111
+ const store = useViewerStore.getState();
112
+ if (colorActiveRef.current) {
113
+ handBackColorChannel(store);
114
+ colorActiveRef.current = false;
115
+ }
116
+ restoreOwnedHidden(ownedHiddenRef.current, store);
117
+ };
118
+ }, []);
119
+ }