@ifc-lite/viewer 1.19.1 → 1.21.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 (106) hide show
  1. package/.turbo/turbo-build.log +59 -44
  2. package/.turbo/turbo-typecheck.log +1 -1
  3. package/CHANGELOG.md +488 -0
  4. package/dist/assets/{basketViewActivator-CA2CTcVo.js → basketViewActivator-Bzw51jhm.js} +6 -6
  5. package/dist/assets/decode-worker-t2EGKAxO.js +1708 -0
  6. package/dist/assets/drawing-2d-Bjy8YPrg.js +257 -0
  7. package/dist/assets/exporters-u0sz2Upj.js +259119 -0
  8. package/dist/assets/geometry-controller.worker-NH8pZmrU.js +7 -0
  9. package/dist/assets/geometry.worker-Bp4rW_R1.js +1 -0
  10. package/dist/assets/ids-B7AXEv7h.js +4067 -0
  11. package/dist/assets/ifc-lite-DfZHk36-.js +7 -0
  12. package/dist/assets/ifc-lite_bg-DlKs5-yM.wasm +0 -0
  13. package/dist/assets/ifc-lite_bg-PqmRe3Ph.wasm +0 -0
  14. package/dist/assets/index-CSWgTe1s.css +1 -0
  15. package/dist/assets/{index-D8Epw-e7.js → index-DVNSvEMh.js} +40146 -35823
  16. package/dist/assets/laz-perf-Cvr_Lepg.js +1 -0
  17. package/dist/assets/laz-perf-DnSyzVYH.wasm +0 -0
  18. package/dist/assets/{native-bridge-DKmx1z95.js → native-bridge-BiD01jI9.js} +1 -1
  19. package/dist/assets/parser.worker-Bnbrl6gy.js +182 -0
  20. package/dist/assets/{sandbox-tccwm5Bo.js → sandbox-DPD1ROr0.js} +4 -4
  21. package/dist/assets/{server-client-LoWPK1N2.js → server-client-DP8fMPY9.js} +1 -1
  22. package/dist/assets/{wasm-bridge-BsJGgPMs.js → wasm-bridge-CErti6zX.js} +1 -1
  23. package/dist/assets/workerHelpers-CBbWSJmd.js +36 -0
  24. package/dist/index.html +8 -8
  25. package/index.html +1 -1
  26. package/package.json +10 -10
  27. package/src/components/viewer/BasketPresentationDock.tsx +3 -0
  28. package/src/components/viewer/CesiumOverlay.tsx +165 -120
  29. package/src/components/viewer/DeviationPanel.tsx +172 -0
  30. package/src/components/viewer/HierarchyPanel.tsx +29 -3
  31. package/src/components/viewer/HoverTooltip.tsx +5 -0
  32. package/src/components/viewer/IDSAuditSummary.tsx +389 -0
  33. package/src/components/viewer/IDSPanel.tsx +80 -26
  34. package/src/components/viewer/MainToolbar.tsx +60 -7
  35. package/src/components/viewer/MergeLayersBanner.tsx +108 -0
  36. package/src/components/viewer/MobileToolbar.tsx +326 -0
  37. package/src/components/viewer/PointCloudClasses.tsx +111 -0
  38. package/src/components/viewer/PointCloudLegend.tsx +119 -0
  39. package/src/components/viewer/PointCloudPanel.tsx +52 -1
  40. package/src/components/viewer/PropertiesPanel.tsx +37 -6
  41. package/src/components/viewer/RectSelectionOverlay.tsx +48 -0
  42. package/src/components/viewer/StatusBar.tsx +14 -0
  43. package/src/components/viewer/ViewerLayout.tsx +288 -95
  44. package/src/components/viewer/Viewport.tsx +86 -18
  45. package/src/components/viewer/ViewportContainer.tsx +25 -11
  46. package/src/components/viewer/ViewportOverlays.tsx +41 -26
  47. package/src/components/viewer/mouseHandlerTypes.ts +22 -0
  48. package/src/components/viewer/properties/GeoreferencingPanel.tsx +77 -8
  49. package/src/components/viewer/properties/MaterialCard.tsx +2 -2
  50. package/src/components/viewer/selectionHandlers.ts +41 -0
  51. package/src/components/viewer/tools/SectionPanel.tsx +181 -24
  52. package/src/components/viewer/tools/SectionVisualization.tsx +384 -3
  53. package/src/components/viewer/useAnimationLoop.ts +22 -0
  54. package/src/components/viewer/useMouseControls.ts +296 -3
  55. package/src/components/viewer/usePointCloudSync.ts +8 -1
  56. package/src/components/viewer/useRenderUpdates.ts +21 -1
  57. package/src/components/viewer/useTouchControls.ts +100 -41
  58. package/src/hooks/federationLoadGate.test.ts +90 -0
  59. package/src/hooks/federationLoadGate.ts +127 -0
  60. package/src/hooks/ids/idsDataAccessor.ts +11 -259
  61. package/src/hooks/ingest/pointCloudIngest.ts +127 -16
  62. package/src/hooks/useDrawingGeneration.ts +81 -8
  63. package/src/hooks/useIDS.ts +90 -10
  64. package/src/hooks/useIfcFederation.ts +94 -16
  65. package/src/hooks/useIfcLoader.ts +289 -64
  66. package/src/hooks/useViewerSelectors.ts +10 -0
  67. package/src/lib/geo/cesium-bridge.ts +84 -67
  68. package/src/lib/geo/clamp-anchor.test.ts +80 -0
  69. package/src/lib/geo/clamp-anchor.ts +57 -0
  70. package/src/lib/geo/effective-georef.test.ts +79 -1
  71. package/src/lib/geo/effective-georef.ts +83 -0
  72. package/src/lib/geo/reproject.ts +26 -13
  73. package/src/lib/geo/terrain-elevation.ts +166 -0
  74. package/src/lib/lens/adapter.ts +1 -1
  75. package/src/lib/llm/context-builder.ts +1 -1
  76. package/src/lib/perf/memoryAccounting.test.ts +92 -0
  77. package/src/lib/perf/memoryAccounting.ts +235 -0
  78. package/src/sdk/adapters/mutation-view.ts +1 -1
  79. package/src/store/constants.ts +39 -2
  80. package/src/store/index.ts +6 -1
  81. package/src/store/slices/cesiumSlice.ts +1 -1
  82. package/src/store/slices/idsSlice.ts +24 -0
  83. package/src/store/slices/loadingSlice.ts +12 -0
  84. package/src/store/slices/pointCloudSlice.ts +72 -1
  85. package/src/store/slices/sectionSlice.test.ts +590 -1
  86. package/src/store/slices/sectionSlice.ts +344 -17
  87. package/src/store/slices/uiSlice.merge-layers.test.ts +217 -0
  88. package/src/store/slices/uiSlice.ts +60 -2
  89. package/src/store/types.ts +42 -0
  90. package/src/store.ts +13 -0
  91. package/src/utils/acquireFileBuffer.test.ts +231 -0
  92. package/src/utils/acquireFileBuffer.ts +128 -0
  93. package/src/utils/ifcConfig.ts +24 -0
  94. package/src/utils/nativeSpatialDataStore.ts +20 -2
  95. package/src/utils/spatialHierarchy.test.ts +116 -0
  96. package/src/utils/spatialHierarchy.ts +23 -0
  97. package/tailwind.config.js +5 -0
  98. package/tsconfig.json +1 -0
  99. package/vite.config.ts +6 -0
  100. package/dist/assets/decode-worker-Collf_X_.js +0 -1320
  101. package/dist/assets/drawing-2d-DoxKMqbO.js +0 -257
  102. package/dist/assets/exporters-xbXqEDlO.js +0 -81590
  103. package/dist/assets/geometry.worker-DQEZB2rB.js +0 -1
  104. package/dist/assets/ids-2WdONLlu.js +0 -2033
  105. package/dist/assets/ifc-lite_bg-4yUkDRD8.wasm +0 -0
  106. package/dist/assets/index-BXeEKqJG.css +0 -1
@@ -1,59 +1,74 @@
1
1
 
2
- > @ifc-lite/viewer@1.19.1 build /home/runner/work/ifc-lite/ifc-lite/apps/viewer
2
+ > @ifc-lite/viewer@1.21.0 build /home/runner/work/ifc-lite/ifc-lite/apps/viewer
3
3
  > vite build
4
4
 
5
5
  vite v5.4.21 building for production...
6
6
  transforming...
7
- ✓ 4459 modules transformed.
7
+ ✓ 4517 modules transformed.
8
8
  ../../node_modules/.pnpm/protobufjs@8.0.0/node_modules/protobufjs/dist/minimal/protobuf.js (662:18): Use of eval in "../../node_modules/.pnpm/protobufjs@8.0.0/node_modules/protobufjs/dist/minimal/protobuf.js" is strongly discouraged as it poses security risks and may cause issues with minification.
9
9
  rendering chunks...
10
10
  [plugin:vite:reporter] [plugin vite:reporter]
11
+ (!) /home/runner/work/ifc-lite/ifc-lite/apps/viewer/src/utils/ifcConfig.ts is dynamically imported by /home/runner/work/ifc-lite/ifc-lite/apps/viewer/src/utils/acquireFileBuffer.ts but also statically imported by /home/runner/work/ifc-lite/ifc-lite/apps/viewer/src/hooks/useIfcFederation.ts, /home/runner/work/ifc-lite/ifc-lite/apps/viewer/src/hooks/useIfcLoader.ts, /home/runner/work/ifc-lite/ifc-lite/apps/viewer/src/hooks/useIfcServer.ts, /home/runner/work/ifc-lite/ifc-lite/apps/viewer/src/services/cacheService.ts, dynamic import will not move module into another chunk.
12
+ 
13
+ [plugin:vite:reporter] [plugin vite:reporter]
11
14
  (!) /home/runner/work/ifc-lite/ifc-lite/apps/viewer/src/services/desktop-cache.ts is dynamically imported by /home/runner/work/ifc-lite/ifc-lite/apps/viewer/src/hooks/useIfcLoader.ts, /home/runner/work/ifc-lite/ifc-lite/apps/viewer/src/hooks/useIfcLoader.ts, /home/runner/work/ifc-lite/ifc-lite/apps/viewer/src/hooks/useIfcLoader.ts, /home/runner/work/ifc-lite/ifc-lite/apps/viewer/src/hooks/useIfcLoader.ts, /home/runner/work/ifc-lite/ifc-lite/apps/viewer/src/hooks/useIfcLoader.ts, /home/runner/work/ifc-lite/ifc-lite/apps/viewer/src/hooks/useIfcLoader.ts, /home/runner/work/ifc-lite/ifc-lite/apps/viewer/src/services/cacheService.ts but also statically imported by /home/runner/work/ifc-lite/ifc-lite/apps/viewer/src/services/desktop-native-metadata.ts, dynamic import will not move module into another chunk.
12
15
  
13
16
  [plugin:vite:reporter] [plugin vite:reporter]
14
17
  (!) /home/runner/work/ifc-lite/ifc-lite/apps/viewer/src/components/viewer/selectionHandlers.ts is dynamically imported by /home/runner/work/ifc-lite/ifc-lite/apps/viewer/src/hooks/useKeyboardShortcuts.ts but also statically imported by /home/runner/work/ifc-lite/ifc-lite/apps/viewer/src/components/viewer/useMouseControls.ts, dynamic import will not move module into another chunk.
15
18
  
16
19
  computing gzip size...
17
- dist/index.html  3.74 kB │ gzip: 1.23 kB
18
- dist/assets/geometry.worker-DQEZB2rB.js  42.60 kB
19
- dist/assets/index-XwKzDuw6.js  54.12 kB
20
- dist/assets/decode-worker-Collf_X_.js  56.14 kB
21
- dist/assets/emscripten-module-NWak2PoB.wasm  518.88 kB
22
- dist/assets/ifc-lite_bg-4yUkDRD8.wasm  1,059.81 kB
23
- dist/assets/emscripten-module-CGIn_cMh.wasm  1,070.86 kB
24
- dist/assets/emscripten-module-DYvzWiHh.wasm  1,561.30 kB
25
- dist/assets/arrow2_bg-4Y7xYo54.wasm  5,000.39 kB
26
- dist/assets/arrow2_bg-BoXCojjR.wasm  5,001.30 kB
27
- dist/assets/emscripten-module-BTRCZGcB.wasm  6,623.52 kB
28
- dist/assets/esbuild-Cpd5nU_H.wasm 13,524.82 kB
29
- dist/assets/cesium-ADbP7waU.css  24.30 kB │ gzip: 5.49 kB
30
- dist/assets/index-BXeEKqJG.css  241.24 kB │ gzip: 35.39 kB
31
- dist/assets/tauri-dialog-stub-r7Wksg7o.js  0.05 kB │ gzip: 0.07 kB
32
- dist/assets/esbuild-COv63sf-.js  0.06 kB │ gzip: 0.08 kB
33
- dist/assets/tauri-core-stub-D8Fa-u43.js  0.11 kB │ gzip: 0.12 kB
34
- dist/assets/tauri-fs-stub-BdeRC7aK.js  0.13 kB │ gzip: 0.13 kB
35
- dist/assets/event-DIOks52T.js  1.42 kB │ gzip: 0.66 kB
36
- dist/assets/wasm-bridge-BsJGgPMs.js  1.66 kB │ gzip: 0.80 kB
37
- dist/assets/basketViewActivator-CA2CTcVo.js  2.31 kB │ gzip: 0.81 kB
38
- dist/assets/ifc-cache-BAN4vcd4.js  2.77 kB │ gzip: 0.94 kB
39
- dist/assets/ffi-DlhRHxHv.js  6.14 kB │ gzip: 0.98 kB
40
- dist/assets/lens-CSASnhAL.js  9.07 kB │ gzip: 2.81 kB
41
- dist/assets/emscripten-module.browser-CY5t0Vfq.js  12.70 kB │ gzip: 5.09 kB
42
- dist/assets/native-bridge-DKmx1z95.js  18.70 kB │ gzip: 3.80 kB
43
- dist/assets/parquet-CEXmQNRO.js  29.67 kB │ gzip: 6.89 kB
44
- dist/assets/server-client-LoWPK1N2.js  31.43 kB │ gzip: 7.05 kB
45
- dist/assets/bcf-4K724hw0.js  41.08 kB │ gzip: 12.74 kB
46
- dist/assets/browser-C5TFR7sH.js  67.65 kB │ gzip: 18.94 kB
47
- dist/assets/drawing-2d-DoxKMqbO.js  71.45 kB │ gzip: 20.98 kB
48
- dist/assets/ids-2WdONLlu.js  89.26 kB │ gzip: 15.06 kB
49
- dist/assets/zip-DBEtpeu6.js  97.03 kB │ gzip: 30.09 kB
50
- dist/assets/arrow-CZ5kQ26f.js  208.84 kB │ gzip: 50.09 kB
51
- dist/assets/sandbox-tccwm5Bo.js  386.03 kB │ gzip: 62.78 kB
52
- dist/assets/three-CDRZThFA.js  527.54 kB │ gzip: 132.19 kB
53
- dist/assets/maplibre-gl-CGLcoNXc.js  1,046.19 kB │ gzip: 282.97 kB
54
- dist/assets/exporters-xbXqEDlO.js  2,914.44 kB │ gzip: 163.67 kB
55
- dist/assets/epsg-index.generated-BjJrt_0S.js  4,284.21 kB │ gzip: 440.44 kB
56
- dist/assets/cesium-DUOzBlqv.js  5,543.11 kB │ gzip: 1,373.81 kB
57
- dist/assets/index-D8Epw-e7.js  5,984.89 kB │ gzip: 1,082.26 kB
20
+ 
21
+ dist/assets/laz-perf-Cvr_Lepg.js  0.06 kB
22
+ (!) Some chunks are larger than 6000 kB after minification. Consider:
23
+ dist/assets/workerHelpers-CBbWSJmd.js  1.21 kB
24
+ - Using dynamic import() to code-split the application
25
+ dist/index.html  3.79 kB │ gzip: 1.26 kB
26
+ - Use build.rollupOptions.output.manualChunks to improve chunking: https://rollupjs.org/configuration-options/#output-manualchunks
27
+ dist/assets/ifc-lite-DfZHk36-.js  20.18 kB
28
+ - Adjust chunk size limit for this warning via build.chunkSizeWarningLimit.
29
+ dist/assets/geometry.worker-Bp4rW_R1.js  47.29 kB
30
+ dist/assets/geometry-controller.worker-NH8pZmrU.js  52.16 kB
31
+ dist/assets/index-XwKzDuw6.js  54.12 kB
32
+ dist/assets/decode-worker-t2EGKAxO.js  69.76 kB
33
+ dist/assets/laz-perf-DnSyzVYH.wasm  214.62 kB
34
+ dist/assets/emscripten-module-NWak2PoB.wasm  518.88 kB
35
+ dist/assets/parser.worker-Bnbrl6gy.js  937.91 kB
36
+ dist/assets/emscripten-module-CGIn_cMh.wasm  1,070.86 kB
37
+ dist/assets/ifc-lite_bg-DlKs5-yM.wasm  1,149.83 kB
38
+ dist/assets/ifc-lite_bg-PqmRe3Ph.wasm  1,210.81 kB
39
+ dist/assets/emscripten-module-DYvzWiHh.wasm  1,561.30 kB
40
+ dist/assets/arrow2_bg-4Y7xYo54.wasm  5,000.39 kB
41
+ dist/assets/arrow2_bg-BoXCojjR.wasm  5,001.30 kB
42
+ dist/assets/emscripten-module-BTRCZGcB.wasm  6,623.52 kB
43
+ dist/assets/esbuild-Cpd5nU_H.wasm 13,524.82 kB
44
+ dist/assets/cesium-ADbP7waU.css  24.30 kB │ gzip: 5.49 kB
45
+ dist/assets/index-CSWgTe1s.css  245.71 kB │ gzip: 35.88 kB
46
+ dist/assets/tauri-dialog-stub-r7Wksg7o.js  0.05 kB │ gzip: 0.07 kB
47
+ dist/assets/esbuild-COv63sf-.js  0.06 kB │ gzip: 0.08 kB
48
+ dist/assets/tauri-core-stub-D8Fa-u43.js  0.11 kB │ gzip: 0.12 kB
49
+ dist/assets/tauri-fs-stub-BdeRC7aK.js  0.13 kB │ gzip: 0.13 kB
50
+ dist/assets/event-DIOks52T.js  1.42 kB │ gzip: 0.66 kB
51
+ dist/assets/wasm-bridge-CErti6zX.js  1.66 kB │ gzip: 0.80 kB
52
+ dist/assets/basketViewActivator-Bzw51jhm.js  2.31 kB │ gzip: 0.81 kB
53
+ dist/assets/ifc-cache-BAN4vcd4.js  2.77 kB │ gzip: 0.94 kB
54
+ dist/assets/ffi-DlhRHxHv.js  6.14 kB │ gzip: 0.98 kB
55
+ dist/assets/lens-CSASnhAL.js  9.07 kB │ gzip: 2.81 kB
56
+ dist/assets/emscripten-module.browser-CY5t0Vfq.js  12.70 kB │ gzip: 5.09 kB
57
+ dist/assets/native-bridge-BiD01jI9.js  18.70 kB │ gzip: 3.80 kB
58
+ dist/assets/parquet-CEXmQNRO.js  29.67 kB │ gzip: 6.89 kB
59
+ dist/assets/server-client-DP8fMPY9.js  31.43 kB │ gzip: 7.05 kB
60
+ dist/assets/bcf-4K724hw0.js  41.08 kB │ gzip: 12.74 kB
61
+ dist/assets/browser-C5TFR7sH.js  67.65 kB │ gzip: 18.94 kB
62
+ dist/assets/drawing-2d-Bjy8YPrg.js  71.89 kB │ gzip: 21.08 kB
63
+ dist/assets/zip-DBEtpeu6.js  97.03 kB │ gzip: 30.09 kB
64
+ dist/assets/ids-B7AXEv7h.js  164.04 kB │ gzip: 29.28 kB
65
+ dist/assets/arrow-CZ5kQ26f.js  208.84 kB │ gzip: 50.09 kB
66
+ dist/assets/sandbox-DPD1ROr0.js  386.03 kB │ gzip: 62.78 kB
67
+ dist/assets/three-CDRZThFA.js  527.54 kB │ gzip: 132.19 kB
68
+ dist/assets/maplibre-gl-CGLcoNXc.js  1,046.19 kB │ gzip: 282.97 kB
69
+ dist/assets/epsg-index.generated-BjJrt_0S.js  4,284.21 kB │ gzip: 440.44 kB
70
+ dist/assets/cesium-DUOzBlqv.js  5,543.11 kB │ gzip: 1,373.81 kB
71
+ dist/assets/index-DVNSvEMh.js  6,171.81 kB │ gzip: 1,119.61 kB
72
+ dist/assets/exporters-u0sz2Upj.js  8,313.73 kB │ gzip: 486.31 kB
58
73
  [vite-plugin-static-copy] Copied 4 items.
59
- ✓ built in 1m 4s
74
+ ✓ built in 1m 15s
@@ -1,4 +1,4 @@
1
1
 
2
- > @ifc-lite/viewer@1.19.1 typecheck /home/runner/work/ifc-lite/ifc-lite/apps/viewer
2
+ > @ifc-lite/viewer@1.21.0 typecheck /home/runner/work/ifc-lite/ifc-lite/apps/viewer
3
3
  > tsc --noEmit
4
4
 
package/CHANGELOG.md CHANGED
@@ -1,5 +1,493 @@
1
1
  # @ifc-lite/viewer
2
2
 
3
+ ## 1.21.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#650](https://github.com/louistrue/ifc-lite/pull/650) [`2ff772d`](https://github.com/louistrue/ifc-lite/commit/2ff772d0174f8cd6657f7e4090e15bc7744e8158) Thanks [@louistrue](https://github.com/louistrue)! - Arbitrary-normal section planes with face-pick (Bonsai-style) and a
8
+ properly-rendered cap on tilted planes (#243). Click any face in the
9
+ section tool's "Pick" mode to cut through it; the kept half-space
10
+ defaults to the side facing the camera. The cardinal "Down / Front /
11
+ Side" presets are unchanged.
12
+
13
+ Renderer:
14
+
15
+ - New `planeBasis(normal)` + `nearestCardinalAxis(normal)` exports
16
+ derive a deterministic in-plane basis used by both the cap renderer
17
+ and the 2D cutter — without a single shared derivation the cap hatch
18
+ rotated when state was reconstructed.
19
+ - `SectionPlaneRenderOptions` and `SectionPlane` gain optional
20
+ `normal` + `distance` fields. When set, the shader clips on that
21
+ plane verbatim (no axis mapping, no building-rotation, no
22
+ position-percentage math) and the gizmo renders as a violet quad
23
+ oriented from `planeBasis(normal)`.
24
+ - `Section2DOverlayRenderer.uploadDrawing` accepts an optional
25
+ `customPlane = { origin, tangent, bitangent }`. When supplied it
26
+ replaces the cardinal-axis 2D→3D coordinate swap with
27
+ `origin + tangent·x + bitangent·y`, so the cap silhouette lands
28
+ exactly on the tilted plane (the bug PR #581 hid by suppressing the
29
+ cap entirely for non-cardinal planes).
30
+
31
+ Drawing-2d:
32
+
33
+ - `SectionPlaneConfig` gains an optional `customPlane`. `SectionCutter`
34
+ uses it verbatim for the plane equation and projects intersections
35
+ to 2D via `(dot(p − origin, tangent), dot(p − origin, bitangent))`,
36
+ matching the cap renderer's lift exactly.
37
+ - `DrawingGenerator` now rebuilds the CPU cutter on each `generate()`
38
+ call so a switch from cardinal to custom (or between custom planes)
39
+ takes effect immediately.
40
+
41
+ Tests: 11 new viewer tests covering normalisation, sign-preserving
42
+ cardinal mapping, basis orthonormality, half-space flip, slice
43
+ clearing on cardinal preset, and degenerate-normal handling. 6 new
44
+ renderer tests covering basis derivation across cardinal axes,
45
+ near-axis tilts, and the +Y / −Y reference-axis boundary.
46
+
47
+ ### Patch Changes
48
+
49
+ - Updated dependencies [[`2ff772d`](https://github.com/louistrue/ifc-lite/commit/2ff772d0174f8cd6657f7e4090e15bc7744e8158)]:
50
+ - @ifc-lite/renderer@1.20.0
51
+ - @ifc-lite/drawing-2d@1.16.0
52
+
53
+ ## 1.20.0
54
+
55
+ ### Minor Changes
56
+
57
+ - [#614](https://github.com/louistrue/ifc-lite/pull/614) [`7efc878`](https://github.com/louistrue/ifc-lite/commit/7efc8783314559b674509131f1e203ae7c1fda8e) Thanks [@louistrue](https://github.com/louistrue)! - Per-class visibility toggles for ASPRS-classified point clouds.
58
+
59
+ A new "Classes" section in the point cloud panel exposes a checkbox
60
+ list of every LAS 1.4 standard class (Ground, Vegetation, Building,
61
+ Water, Wires, Bridge deck, ...). Toggling a class hides every point
62
+ with that classification. Works in any colour mode; the swatch
63
+ colours mirror the splat shader's classification palette so the UI
64
+ matches what's on screen.
65
+
66
+ Implementation:
67
+
68
+ - New `pointCloudClassMask: number` (u32 bitmask, default
69
+ `0xFFFFFFFF`) on the point cloud slice. `togglePointCloudClass(id)`
70
+ flips a single bit; `setPointCloudClassMask(mask)` replaces all 32.
71
+ - `PointCloudRenderOptions.classMask` plumbed through the renderer.
72
+ Stored in uniform slot `flags.w` (was unused).
73
+ - Splat shader checks `(flags.w >> classId) & 1` per vertex; hidden
74
+ classes get a degenerate `clipPos = vec4(0, 0, -2, 1)` so they're
75
+ culled before rasterisation rather than wasted on a fragment-stage
76
+ discard.
77
+ - New `PointCloudClasses` component in the panel renders a
78
+ `<details>` collapsible with "Show all" + per-class toggles. A
79
+ badge surfaces "N of 32 visible" when not all are on.
80
+ - `usePointCloudSync` forwards the mask to
81
+ `setPointCloudOptions({ classMask })`.
82
+
83
+ Class ids ≥32 always show — the mask only covers the standard
84
+ range. Custom-labelled scans need a richer UI (deferred).
85
+
86
+ - [#614](https://github.com/louistrue/ifc-lite/pull/614) [`7efc878`](https://github.com/louistrue/ifc-lite/commit/7efc8783314559b674509131f1e203ae7c1fda8e) Thanks [@louistrue](https://github.com/louistrue)! - BIM ↔ scan deviation heatmap — GPU compute pipeline that colours each
87
+ scan point by signed distance to the nearest mesh surface. Works with
88
+ every IFC ingest path (STEP / IFCx / GLB / federated) and with every
89
+ point cloud format (inline IFCx + streamed LAS / LAZ / PLY / PCD / E57
90
+ / PTS / XYZ — anywhere `Scene.forEachMeshData` reaches and any node
91
+ the splat pipeline already renders).
92
+
93
+ Pipeline:
94
+
95
+ 1. **Per-triangle BVH** built from `Scene.forEachMeshData()` —
96
+ reaches every CPU-side `MeshData` regardless of source. Median
97
+ split along longest axis, max 16 tris per leaf, flattened to a
98
+ `Float32Array` of 32-byte nodes during the build (no second
99
+ pass).
100
+ 2. **Two GPU storage buffers** — nodes + triangles — uploaded once
101
+ per mesh-set change. Cached by a `(meshCount, totalPositions)`
102
+ fingerprint so re-running deviation against the same model is a
103
+ pure dispatch.
104
+ 3. **Compute shader** with stack-based BVH descent (workgroup-size
105
+ 64). Per point: descend BVH pruning by squared point-to-AABB
106
+ distance, run Ericson §5.1.5 closest-point-on-triangle on every
107
+ leaf candidate, output signed distance via the closest face's
108
+ precomputed normal.
109
+ 4. **Per-chunk deviation buffer** allocated alongside the splat
110
+ vertex buffer (`STORAGE | VERTEX | COPY_DST`, 4 bytes per point,
111
+ zero-initialised). Compute reads the vertex buffer's positions
112
+ directly — no CPU copy of streamed clouds needed.
113
+ 5. **Splat shader** gains a 2nd vertex buffer (location 4 = `f32`
114
+ deviation), a new `deviation` color mode, and a diverging
115
+ blue → white → red `deviation_ramp`. Uniform block grows by 16
116
+ bytes (new `deviationRange: vec4<f32>` slot for centre + half-
117
+ range), `POINT_UNIFORM_SIZE` 208 → 224.
118
+ 6. **Public API** — `Renderer.computeDeviations({ maxRange?,
119
+ forceRebuild? })` returns `{ bvhTriangles, bvhNodes,
120
+ chunksProcessed, pointsProcessed, bounds, suggestedHalfRange }`.
121
+ Awaits `queue.onSubmittedWorkDone` so callers see populated
122
+ buffers when the promise resolves.
123
+ 7. **UI** — new `DeviationPanel` inside `PointCloudPanel`. Compute
124
+ button (gated on `triangleCount > 0`), live progress + duration
125
+ readout, range slider in millimetres (1 mm to 1 m), inline
126
+ blue-white-red legend. Auto-suggests a half-range from the BVH
127
+ bbox (±max-extent / 1000) and auto-switches the colour mode to
128
+ `deviation` on success.
129
+ 8. **Slice** — `pointCloudColorMode` gains `'deviation'`, plus
130
+ `pointCloudDeviationCenterOffset`, `pointCloudDeviationHalfRange`
131
+ (default ±5 cm), and `pointCloudDeviationComputed`. Sync hook
132
+ forwards the range to the renderer uniform.
133
+
134
+ Sign convention: positive = scan point is on the outward-normal
135
+ side of the closest triangle (typical "scan overshoots wall by
136
+ 5 mm"). Negative = inside / behind. Non-watertight BIM (typical
137
+ IFC) means "inside the building" isn't globally defined, but
138
+ per-surface front/back is always meaningful.
139
+
140
+ Limitations / future work:
141
+
142
+ - The dispatch processes every uploaded point against every
143
+ triangle in the scene; isolated / hidden meshes still contribute
144
+ to the BVH. A `meshFilter` predicate is a natural follow-up.
145
+ - Histogram + auto-range from p5/p95 not yet implemented — the
146
+ default half-range suggestion is a coarse bbox/1000 heuristic.
147
+ Phase B will add a 2nd compute pass with atomic histogram.
148
+ - The BVH walk uses a 64-deep per-thread stack. Pathologically
149
+ unbalanced trees (>64 deep) silently drop the deepest branch.
150
+ Real BIMs don't get there; SAH or surface-area cost would help
151
+ if we ever hit it.
152
+
153
+ Verified: full repo typecheck (24/24), 655 viewer tests, viewer
154
+ Vite build green.
155
+
156
+ - [#614](https://github.com/louistrue/ifc-lite/pull/614) [`7efc878`](https://github.com/louistrue/ifc-lite/commit/7efc8783314559b674509131f1e203ae7c1fda8e) Thanks [@louistrue](https://github.com/louistrue)! - Near-term UX features from #611.
157
+
158
+ **Hover XYZ readback.** GPU pick now also samples the depth texel at
159
+ the click position and unprojects it through the inverse view-
160
+ projection. `PickResult` carries an optional `worldXYZ`. Reverse-Z is
161
+ honoured (depth=1 = near, 0 = far / miss). The hover tooltip shows
162
+ `x, y, z` (2 decimals) under the entity id. Useful for measurement
163
+ hooks and point-cloud picks where the synthetic entity has no
164
+ surface property to display.
165
+
166
+ **Solid-color picker.** When the point-cloud panel's colour mode is
167
+ set to `fixed`, a native `<input type="color">` swatch appears.
168
+ Hex round-trips through the existing `[r,g,b,a]` store tuple.
169
+
170
+ **Colour-mode legend.** A new `PointCloudLegend` component renders
171
+ inline beneath the colour-mode buttons:
172
+
173
+ - Classification → list of ASPRS LAS 1.4 class id / colour swatch /
174
+ label (Ground, Vegetation, Building, ...). Palette mirrors
175
+ `point-shader.wgsl.ts` exactly.
176
+ - Intensity → black-to-white gradient bar with low/high labels.
177
+ - Height → cool-warm gradient bar (blue → cyan → green → yellow →
178
+ red), matching the shader's `height_ramp`.
179
+ RGB and Solid don't render a legend.
180
+
181
+ **Cancel button for in-flight streams.** New
182
+ `activeStreamCanceller` field on the loading slice. Both ingest
183
+ sites (`useIfcLoader`, `useIfcFederation`) register
184
+ `() => streamHandle.cancel()` after starting and clear on success /
185
+ error. `StatusBar` shows a Cancel button while the canceller is
186
+ non-null. AbortError on cancel is reported as "Cancelled" rather
187
+ than a scary error string.
188
+
189
+ - [#614](https://github.com/louistrue/ifc-lite/pull/614) [`7efc878`](https://github.com/louistrue/ifc-lite/commit/7efc8783314559b674509131f1e203ae7c1fda8e) Thanks [@louistrue](https://github.com/louistrue)! - PTS / XYZ ASCII point cloud reader.
190
+
191
+ Both formats are line-oriented plain-text scans common in legacy
192
+ survey workflows. They share the same syntax — they differ only in
193
+ the optional first-line point count (PTS may have one; XYZ never
194
+ does). One shared decoder + streaming source handles both.
195
+
196
+ Auto-detected per-line layouts (by column count of the first data
197
+ line):
198
+
199
+ - 3 cols → `X Y Z`
200
+ - 4 cols → `X Y Z I` (intensity)
201
+ - 6 cols → `X Y Z R G B`
202
+ - 7 cols → `X Y Z I R G B` (canonical PTS)
203
+ - 9 cols → `X Y Z R G B Nx Ny Nz` (XYZ-with-normals; normals dropped)
204
+ - 10 cols → `X Y Z I R G B Nx Ny Nz` (PTS-with-normals; normals dropped)
205
+ - For XYZ with unknown column counts ≥3 we still emit positions and
206
+ skip the rest, so weird custom exports load instead of erroring.
207
+
208
+ Other behaviour:
209
+
210
+ - Comment lines (`#`, `//`) and blank lines are skipped.
211
+ - Intensity normalisation: 0..1 vs 0..255 vs raw sensor detected from
212
+ the observed maximum, then mapped to u16.
213
+ - RGB normalisation: same heuristic (>1.0 → 0..255 source).
214
+ - Whole-file decode wrapped in `AsciiPointsStreamingSource`; the
215
+ streaming host's 25M-point cap stride-downsamples on the way out.
216
+
217
+ Wired into the decode worker, format detection
218
+ (`detectPointCloudFormat` returns `'pts'` / `'xyz'`), the file
219
+ picker accept lists, drop handlers, and both `useIfcLoader` /
220
+ `useIfcFederation` ingest branches. The "PTS / XYZ ASCII points —
221
+ not yet supported" toast is removed from `describeUnsupportedFormat`.
222
+
223
+ 10 new unit tests cover layout probing, decoder round-trips for the
224
+ common shapes, and the comment / header-count edge cases.
225
+
226
+ - [#614](https://github.com/louistrue/ifc-lite/pull/614) [`7efc878`](https://github.com/louistrue/ifc-lite/commit/7efc8783314559b674509131f1e203ae7c1fda8e) Thanks [@louistrue](https://github.com/louistrue)! - GPU rectangle pick (marquee select) — meshes + point clouds.
227
+
228
+ Hold `Ctrl` (or `⌘` on macOS) and drag with the left mouse button
229
+ in the select tool to draw a rectangle. On release, every entity
230
+ (mesh or point cloud) whose pixel falls inside the rect becomes
231
+ the new selection. A teal-dashed SVG outline tracks the drag.
232
+
233
+ Implementation:
234
+
235
+ - `Picker.pickRect(x0, y0, x1, y1, …) → Set<expressId>` renders the
236
+ same pick pass as `pick()` and reads back the texel rect, deduping
237
+ hits to a Set. Mesh + point splats both participate (point splats
238
+ share the depth buffer in the pick pass).
239
+ - A new private `Picker.renderPickPass` extracts the shared render-
240
+ pass setup so single-pixel `pick` and rect `pickRect` don't drift.
241
+ - `PickingManager.pickRect` applies the same visibility filtering
242
+ (`hiddenIds`, `isolatedIds`) as `pick`. The CPU-raycast and
243
+ dynamic-mesh-creation fallbacks `pick` uses for very large batched
244
+ models are skipped — rect pick only sees already-hydrated meshes.
245
+ - `Renderer.pickRect` exposes the manager's API.
246
+ - New `RectSelectionOverlay` component renders the dashed SVG box
247
+ while dragging; lives inside `Viewport.tsx` as a sibling of the
248
+ canvas.
249
+ - `useMouseControls` tracks a new `mouseState.isRectSelecting` flag,
250
+ suppresses orbit/pan during the drag, and on mouseup runs
251
+ `renderer.pickRect(...)` and feeds the result into
252
+ `setSelectedEntityIds`. A 4-pixel minimum rect size avoids
253
+ clobbering selection on a stray Ctrl-click.
254
+ - `MouseState.isRectSelecting?: boolean` and a new
255
+ `setRectSelection?` callback added to `UseMouseControlsParams`.
256
+
257
+ Lasso (polygonal) pick still pending — covered by issue #611's
258
+ mid-term list. Per-class isolation for points is a separate
259
+ follow-up.
260
+
261
+ - [#614](https://github.com/louistrue/ifc-lite/pull/614) [`7efc878`](https://github.com/louistrue/ifc-lite/commit/7efc8783314559b674509131f1e203ae7c1fda8e) Thanks [@louistrue](https://github.com/louistrue)! - Section-plane drag preview — render at 1/4 density during slider
262
+ drag for responsive section-cutting on huge point clouds.
263
+
264
+ The splat shader gains a `previewStride` uniform that culls
265
+ `(instance_index % stride) != 0` at the start of `vs_main`. The
266
+ section-plane position slider wires `onPointerDown` to set
267
+ `previewStride: 4` and `onPointerUp` to restore `1`, so scans of
268
+ millions of points stay responsive while the user drags.
269
+
270
+ Implementation:
271
+
272
+ - `POINT_UNIFORM_SIZE` bumped from 208 → 224 to add a new
273
+ `extras: vec4<u32>` slot. `extras.x` carries `previewStride`;
274
+ `yzw` reserved for future per-frame state.
275
+ - `PointCloudRenderOptions.previewStride?: number` clamped to
276
+ [1, 256] in the renderer.
277
+ - Vertex shader culls hidden instances by writing
278
+ `clipPos = vec4(0, 0, -2, 1)` (outside reverse-Z `[0, 1]`) so they
279
+ drop pre-rasterisation.
280
+ - New `pointCloudPreviewStride` field on the point cloud slice
281
+ (default 1) with `setPointCloudPreviewStride` action.
282
+ - `usePointCloudSync` forwards the stride to
283
+ `setPointCloudOptions`.
284
+ - `SectionOverlay`'s position slider triggers stride 4 on
285
+ drag start (pointer + keyboard), 1 on release. Only flips when
286
+ `pointCloudAssetCount > 0` so IFC-only sessions are unaffected.
287
+
288
+ Triangle meshes ignore the stride — they're cheap enough that
289
+ section drag was already smooth.
290
+
291
+ Verified: full repo typecheck (24/24), 655 viewer tests, viewer
292
+ Vite build green.
293
+
294
+ ### Patch Changes
295
+
296
+ - [#614](https://github.com/louistrue/ifc-lite/pull/614) [`7efc878`](https://github.com/louistrue/ifc-lite/commit/7efc8783314559b674509131f1e203ae7c1fda8e) Thanks [@louistrue](https://github.com/louistrue)! - Fix LAZ load failing with `WebAssembly: Response has unsupported MIME
297
+ type 'text/plain'` on real-world files (e.g. autzen-classified.laz).
298
+
299
+ `laz-perf`'s emscripten shim resolves the wasm via `locateFile()` and
300
+ calls `fetch("laz-perf.wasm")` relative to its own script directory.
301
+ In a Vite-bundled module worker that path becomes `/assets/<chunk>/…`
302
+ or just `/laz-perf.wasm` — both 404, and the SPA fallback returns
303
+ `index.html` as `text/plain`, which `instantiateStreaming` rightly
304
+ rejects. The async fallback then 404s the same way and aborts.
305
+
306
+ `loadLazPerf` now resolves the wasm asset URL through Vite's
307
+ `?url` import (`laz-perf/lib/web/laz-perf.wasm?url`), pre-fetches the
308
+ bytes itself, and hands them to emscripten as `Module.wasmBinary` so
309
+ the shim's own fetch is bypassed entirely. Failure modes (asset
310
+ resolution, fetch HTTP error) now produce a precise error message
311
+ naming the URL and status instead of the opaque emscripten "Aborted".
312
+
313
+ - [#614](https://github.com/louistrue/ifc-lite/pull/614) [`7efc878`](https://github.com/louistrue/ifc-lite/commit/7efc8783314559b674509131f1e203ae7c1fda8e) Thanks [@louistrue](https://github.com/louistrue)! - Near-term batch — correctness + robustness items from #611.
314
+
315
+ **`computeBBox` empty / non-finite guards.** Both `e57.ts` and
316
+ `ifcx-points.ts` now return `{0,0,0}/{0,0,0}` for empty arrays and
317
+ skip non-finite triplets. Previously a zero-point or NaN-poisoned
318
+ chunk produced ±Infinity bounds that broke camera fit-to-view and
319
+ section-plane sliders.
320
+
321
+ **Magic-byte-first format detection.** `detectPointCloudFormat` now
322
+ probes the buffer (E57 magic, LASF magic, "ply" / "#" / ".PCD"
323
+ ASCII tokens) before falling back to extension. A LAS file
324
+ mistakenly named `*.ply` no longer goes down the wrong decoder. LAS
325
+ vs LAZ still uses the extension to disambiguate (they share the
326
+ LASF magic).
327
+
328
+ **E57 packet-bounds + per-stream guards.** Validate that the
329
+ DataPacket header, bytestream-length table, and each individual
330
+ bytestream stay inside `payloadEnd = packetEnd - 4` before reading.
331
+ Corrupt files now fail with a precise "bytestream X runs past
332
+ packet payload" error instead of silently reading into the next
333
+ packet.
334
+
335
+ **`e57.ts` split (631 → 4 files).** `e57-page.ts` (header / page CRC
336
+ / section-header resolver), `e57-xml.ts` (prototype + Data3D
337
+ parser), `e57-decode.ts` (per-scan binary decoder), `e57.ts`
338
+ (orchestrator + re-exports). All four under the AGENTS ~400-line
339
+ guideline.
340
+
341
+ **`point-cloud-renderer.ts` extract.** Pulled the uniform-block
342
+ writer into `point-cloud-uniforms.ts` (`writePointCloudUniforms` +
343
+ mode index maps). Renderer drops below 400 lines.
344
+
345
+ Verified: 62 pointcloud unit tests pass, full repo typecheck
346
+ (24/24).
347
+
348
+ - [#614](https://github.com/louistrue/ifc-lite/pull/614) [`7efc878`](https://github.com/louistrue/ifc-lite/commit/7efc8783314559b674509131f1e203ae7c1fda8e) Thanks [@louistrue](https://github.com/louistrue)! - Round 2 of CodeRabbit feedback on PR #614:
349
+
350
+ - **E57 stride downsampling drops classifications.** `applyStride` rebuilt
351
+ positions / colors / intensities into new arrays but never copied the
352
+ per-point class IDs, so any non-default stride (`{ stride: 2 }` and up)
353
+ silently lost them and `hasClassification` flipped to false.
354
+ - **Federation abort can stomp a newer load.** The AbortError handler in
355
+ `useIfcFederation.addModel()` wrote `progress`, `error`, and `loading`
356
+ unconditionally — if a second `addModel()` started after the first was
357
+ cancelled, it lost its spinner and progress to the cancelled load's
358
+ cleanup. Added a `loadSessionRef` token (mirrors `useIfcLoader`) and
359
+ gate state writes on `loadSessionRef.current === currentSession`.
360
+ - **E57 Integer classification subtracts `minimum`.** Class IDs are
361
+ absolute labels (ASPRS LAS 1.4 0..31), not range-normalised offsets.
362
+ `raw - minimum` was corrupting class IDs whenever a producer declared
363
+ a non-zero `minimum` on the Integer-encoded classification field. The
364
+ Integer branch now matches the ScaledInteger branch's intent: keep
365
+ the raw byte, clamp to 0..255.
366
+ - **PCD probe missed `VERSION` / `FIELDS` headers.** The magic-byte
367
+ detector only recognised `# .PCD …` comment-style headers. Real PCDs
368
+ emitted by PCL's `pcl_io` and a few third-party tools start directly
369
+ with `VERSION 0.7\n…` or `FIELDS x y z\n…` — these now route through
370
+ the PCD decoder instead of falling through to extension-based
371
+ detection (which would mis-route a renamed PCD).
372
+ - **Catch-block logging.** Per repo convention, log point-cloud ingest
373
+ failures in `useIfcLoader.ts` before the early return so abort vs.
374
+ real-failure vs. stale-session paths are distinguishable in console
375
+ triage.
376
+
377
+ Test cleanup: drop the shadowed (and unused) ScaledInteger packet
378
+ buffer in `e57.test.ts` so only the live `fullBuf` setup remains.
379
+
380
+ - Updated dependencies [[`8408c88`](https://github.com/louistrue/ifc-lite/commit/8408c88c4c0a1e848fade6c60474952eca1a4149), [`2334993`](https://github.com/louistrue/ifc-lite/commit/2334993827839b9f5b96ca8008c49543fb597660), [`ba7553a`](https://github.com/louistrue/ifc-lite/commit/ba7553af693939896a840074999b5f6806a94815), [`2ab0e4c`](https://github.com/louistrue/ifc-lite/commit/2ab0e4c0eafc21feb22bfc7cd96c467b8b9ff599), [`7efc878`](https://github.com/louistrue/ifc-lite/commit/7efc8783314559b674509131f1e203ae7c1fda8e), [`7efc878`](https://github.com/louistrue/ifc-lite/commit/7efc8783314559b674509131f1e203ae7c1fda8e), [`7efc878`](https://github.com/louistrue/ifc-lite/commit/7efc8783314559b674509131f1e203ae7c1fda8e), [`7efc878`](https://github.com/louistrue/ifc-lite/commit/7efc8783314559b674509131f1e203ae7c1fda8e), [`7efc878`](https://github.com/louistrue/ifc-lite/commit/7efc8783314559b674509131f1e203ae7c1fda8e), [`7efc878`](https://github.com/louistrue/ifc-lite/commit/7efc8783314559b674509131f1e203ae7c1fda8e), [`7efc878`](https://github.com/louistrue/ifc-lite/commit/7efc8783314559b674509131f1e203ae7c1fda8e), [`7efc878`](https://github.com/louistrue/ifc-lite/commit/7efc8783314559b674509131f1e203ae7c1fda8e), [`7efc878`](https://github.com/louistrue/ifc-lite/commit/7efc8783314559b674509131f1e203ae7c1fda8e), [`7efc878`](https://github.com/louistrue/ifc-lite/commit/7efc8783314559b674509131f1e203ae7c1fda8e), [`7efc878`](https://github.com/louistrue/ifc-lite/commit/7efc8783314559b674509131f1e203ae7c1fda8e)]:
381
+ - @ifc-lite/wasm@1.16.9
382
+ - @ifc-lite/geometry@1.18.0
383
+ - @ifc-lite/parser@2.4.0
384
+ - @ifc-lite/data@1.17.0
385
+ - @ifc-lite/renderer@1.19.0
386
+ - @ifc-lite/pointcloud@0.3.0
387
+ - @ifc-lite/ids@1.15.1
388
+ - @ifc-lite/lists@1.14.12
389
+
390
+ ## 1.19.2
391
+
392
+ ### Patch Changes
393
+
394
+ - [#622](https://github.com/louistrue/ifc-lite/pull/622) [`28db7df`](https://github.com/louistrue/ifc-lite/commit/28db7df0fa64dc8cab0d08f4948fb1d9b67e0f70) Thanks [@louistrue](https://github.com/louistrue)! - Cesium overlay: precomputed terrain placement, ground-floor clamping,
395
+ and a refactored camera path.
396
+
397
+ **Placement is now resolved before the bridge is built** (no more
398
+ "model loads at IFC OrthogonalHeight, then jumps to terrain"):
399
+
400
+ - `terrain-elevation.ts` (new module) tries sources in fast-first
401
+ order — sync `globe.getHeight`, sync `scene.sampleHeight`, async
402
+ `scene.sampleHeightMostDetailed` with a 3.5 s timeout, then
403
+ Open-Meteo as a bare-earth fallback. Implausible elevations
404
+ (e.g. depth-buffer noise from Google Photorealistic 3D Tiles
405
+ returning `-69184 m`) are range-checked against terrestrial bounds.
406
+ Results are cached per-session via `clearTerrainElevationCache()`.
407
+ - `sampleHeightMostDetailed` runs _before_ Open-Meteo so the model
408
+ lands on the same surface the user actually sees in 3D Tiles
409
+ (street decks, podiums) rather than the bare-earth DEM.
410
+ - `createCesiumBridge` accepts a `placementHeightOverride` so the
411
+ computed placement is baked into the `enuToEcef` origin altitude
412
+ for both camera frame and model matrix from creation.
413
+
414
+ **`findClampAnchorY` (new helper, 9 unit tests)** picks the anchor
415
+ viewer-Y that auto-clamp pins to terrain. Primary: the
416
+ `IfcBuildingStorey` whose elevation is closest to 0 (ground floor),
417
+ within the model AABB. Fallback: `bounds.min.y`. Without this,
418
+ basements and foundations dragged the model deep below the terrain
419
+ surface.
420
+
421
+ **`oHeightForBaseAltitude`** in the Georeferencing panel now mirrors
422
+ the auto-clamp formula (anchor-aware, shift- and RTC-aware), so the
423
+ "Set OrthogonalHeight to Cesium terrain elevation" button produces
424
+ the same world position as toggling the clamp.
425
+
426
+ **UX behaviours**
427
+
428
+ - `cesiumTerrainClamp` defaults to `true` (slice + reset path).
429
+ - Clamp toggle is now actually uncheckable — dropped the auto-toggle
430
+ branch that fought the user's setting.
431
+ - Editing OrthogonalHeight directly auto-releases the clamp so the
432
+ edit takes effect (with clamp on, placement is intentionally
433
+ terrain-anchored regardless of OrthogonalHeight).
434
+ - Stale `terrainHeight` / `terrainClipY` are cleared when a re-query
435
+ fails so the clip plane doesn't drift relative to the new bridge.
436
+ - Effect 2d depends on `bridgeVersion` so the model matrix refreshes
437
+ after an async bridge rebuild.
438
+
439
+ **Camera navigation refactor.** Reported symptom: orbit/zoom
440
+ restricted to the terrain plane. Two coupled root causes:
441
+
442
+ 1. `screenSpaceCameraController.enableInputs` was still default-true.
443
+ Any input slipping past the overlay's `pointer-events: none`
444
+ reached Cesium and got processed in the locked frame, fighting
445
+ our externally-driven pose. Now flipped to `false` (master kill-
446
+ switch) on top of the per-mode flags.
447
+ 2. `syncCamera` used `lookAtTransform(viewerToEcef)` to write
448
+ position/direction/up in viewer-space. `lookAtTransform` _locks_
449
+ Cesium's reference frame; rotate/tilt/zoom operations are then
450
+ constrained to that local frame — the "stuck to terrain plane"
451
+ behaviour. Refactored to clear `lookAtTransform` with
452
+ `Matrix4.IDENTITY` and write position/direction/up directly in
453
+ ECEF (Cesium's RTC handles shader precision for primitives).
454
+
455
+ **Network hygiene.** `queryTerrainElevation` (Open-Meteo) gets a 5 s
456
+ `AbortController` timeout and a `console.warn` so failures are
457
+ visible instead of silently swallowed.
458
+
459
+ - [#622](https://github.com/louistrue/ifc-lite/pull/622) [`28db7df`](https://github.com/louistrue/ifc-lite/commit/28db7df0fa64dc8cab0d08f4948fb1d9b67e0f70) Thanks [@louistrue](https://github.com/louistrue)! - Apply IfcMapConversion.Scale per IFC schema (issue #595).
460
+
461
+ Scale converts local engineering coordinates (in the project length unit)
462
+ to map CRS units (e.g. `0.001` for a millimetre project with a metre map).
463
+ ifc-lite's geometry pipeline already converts vertices to metres during
464
+ extraction, so applying the raw Scale to viewer-space coordinates double-
465
+ scaled the model — making the Cesium 3D world context unusable for files
466
+ authored per spec.
467
+
468
+ Introduces `getEffectiveHorizontalScale(scale, mapUnitScale, lengthUnitScale)`
469
+ which returns `(scale × mapUnitScale) / lengthUnitScale` — the correct
470
+ multiplier for metre-converted geometry. For files where Scale is set
471
+ consistently with the unit difference this evaluates to 1.0 and the
472
+ geometry passes through unchanged. Wired through:
473
+
474
+ - `cesium-bridge.ts` — 3D model origin and the viewer→ENU rotation.
475
+ - `CesiumOverlay.tsx::buildModelMatrix` — GLB placement.
476
+ - `reproject.ts` — 2D map centre, footprint, and reverse-pick.
477
+ - `useIfcFederation.ts` — multi-model alignment transform.
478
+
479
+ Adds a visible amber warning in the Georeferencing panel when
480
+ `Scale × mapUnitScale ≠ lengthUnitScale` (the IFC schema invariant) so
481
+ authoring errors are discoverable. The warning surfaces both inline (in
482
+ the expanded Coordinate Operation section) and as a small indicator on
483
+ the collapsed section header.
484
+
485
+ - Updated dependencies [[`7c85376`](https://github.com/louistrue/ifc-lite/commit/7c853760ef96e6f0f88ebdc29c17aefae724ff43), [`7c85376`](https://github.com/louistrue/ifc-lite/commit/7c853760ef96e6f0f88ebdc29c17aefae724ff43), [`5439cce`](https://github.com/louistrue/ifc-lite/commit/5439cce34edaff1c050ce8975a330163167df6fd)]:
486
+ - @ifc-lite/data@1.16.0
487
+ - @ifc-lite/ids@1.15.0
488
+ - @ifc-lite/geometry@1.17.1
489
+ - @ifc-lite/lists@1.14.11
490
+
3
491
  ## 1.19.1
4
492
 
5
493
  ### Patch Changes