@ifc-lite/viewer 1.25.2 → 1.27.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 (116) hide show
  1. package/.turbo/turbo-build.log +40 -30
  2. package/CHANGELOG.md +110 -0
  3. package/dist/assets/{basketViewActivator-CTgyKI3U.js → basketViewActivator-B3CdrLsb.js} +7 -7
  4. package/dist/assets/{bcf-7jQby1qi.js → bcf-QeHK_Aud.js} +5 -5
  5. package/dist/assets/{browser-DXS29_v9.js → browser-BIoDDfBW.js} +1 -1
  6. package/dist/assets/{cesium-BoVuJvTC.js → cesium-CzZn5yVA.js} +319 -319
  7. package/dist/assets/{deflate-Cfp9t1Df.js → deflate-B-d0SYQM.js} +1 -1
  8. package/dist/assets/exceljs.min-DsuzKYnj.js +29 -0
  9. package/dist/assets/{exporters-DfSvJPi4.js → exporters-B4LbZFeT.js} +1434 -1179
  10. package/dist/assets/geometry.worker-BdH-E6NB.js +1 -0
  11. package/dist/assets/{geotiff-xZoE8BkO.js → geotiff-CrVtDRFq.js} +10 -10
  12. package/dist/assets/html2canvas.esm-Ge7aVWlp.js +5 -0
  13. package/dist/assets/{ids-Cu73hD0Y.js → ids-DjsGFN10.js} +21 -21
  14. package/dist/assets/ifc-lite_bg-DsYUIHm3.wasm +0 -0
  15. package/dist/assets/{index-WSbA5iy6.js → index-COYokSKc.js} +44122 -38782
  16. package/dist/assets/index-ajK6D32J.css +1 -0
  17. package/dist/assets/index.es-CY202jA3.js +6866 -0
  18. package/dist/assets/{jpeg-DhwFEbqb.js → jpeg-D4wOkf5h.js} +1 -1
  19. package/dist/assets/jspdf.es.min-DIGb9BHN.js +19571 -0
  20. package/dist/assets/jspdf.plugin.autotable-BBLUVd7n.js +2 -0
  21. package/dist/assets/{lerc-Dz6BXOVb.js → lerc-DmW0_tgf.js} +1 -1
  22. package/dist/assets/{lzw-C9z0fG2o.js → lzw-oWetY-d6.js} +1 -1
  23. package/dist/assets/{maplibre-gl-Do6O5tDc.js → maplibre-gl-BF3Z0idw.js} +1 -1
  24. package/dist/assets/{native-bridge-RvDmzO-2.js → native-bridge-BX8_tHXE.js} +1 -1
  25. package/dist/assets/{packbits-jfwifz7C.js → packbits-F8Nkp4NY.js} +1 -1
  26. package/dist/assets/{pako.esm-Cram60i4.js → pako.esm-n3Pgozwg.js} +1 -1
  27. package/dist/assets/{parser.worker-C594dWxH.js → parser.worker-D591Zu_-.js} +3 -3
  28. package/dist/assets/pdf-Dsh3HPZB.js +135 -0
  29. package/dist/assets/raw-D9iw0tmc.js +1 -0
  30. package/dist/assets/{sandbox-DDSZ7rek.js → sandbox-BAC3a-eN.js} +4235 -2716
  31. package/dist/assets/server-client-Cjwnm7il.js +706 -0
  32. package/dist/assets/{webimage-XFHVyVtC.js → webimage-BLV1dgmd.js} +1 -1
  33. package/dist/assets/xlsx-Bc2HTrjC.js +142 -0
  34. package/dist/assets/{zip-BJqVbRkU.js → zip-DFgP-l20.js} +1 -1
  35. package/dist/assets/{zstd-3q5qcl5V.js → zstd-C_1HxVrA.js} +1 -1
  36. package/dist/index.html +8 -8
  37. package/package.json +13 -9
  38. package/src/components/extensions/FlavorDialog.tsx +18 -2
  39. package/src/components/extensions/FlavorListView.tsx +12 -3
  40. package/src/components/mcp/PlaygroundChat.tsx +1 -0
  41. package/src/components/mcp/data.ts +6 -0
  42. package/src/components/mcp/playground-dispatcher.ts +277 -0
  43. package/src/components/mcp/types.ts +2 -1
  44. package/src/components/ui/combo-input.tsx +163 -0
  45. package/src/components/ui/tabs.tsx +1 -1
  46. package/src/components/viewer/ClashBcfExportDialog.tsx +271 -0
  47. package/src/components/viewer/ClashPanel.tsx +370 -0
  48. package/src/components/viewer/ClashSettingsDialog.tsx +407 -0
  49. package/src/components/viewer/CommandPalette.tsx +14 -15
  50. package/src/components/viewer/MainToolbar.tsx +155 -175
  51. package/src/components/viewer/PropertiesPanel.tsx +13 -6
  52. package/src/components/viewer/SearchInline.tsx +62 -2
  53. package/src/components/viewer/SearchModal.filter.builder.tsx +24 -393
  54. package/src/components/viewer/SearchModal.filter.editors.tsx +503 -0
  55. package/src/components/viewer/SearchModal.filter.tsx +64 -1
  56. package/src/components/viewer/SearchModal.tsx +19 -6
  57. package/src/components/viewer/ViewerLayout.tsx +5 -0
  58. package/src/components/viewer/Viewport.tsx +64 -9
  59. package/src/components/viewer/ViewportContainer.tsx +45 -3
  60. package/src/components/viewer/bcf/BCFOverlay.tsx +5 -4
  61. package/src/components/viewer/lists/ColumnHeaderMenu.tsx +84 -0
  62. package/src/components/viewer/lists/ListBuilder.tsx +789 -280
  63. package/src/components/viewer/lists/ListGroupingBar.tsx +72 -0
  64. package/src/components/viewer/lists/ListPanel.tsx +49 -5
  65. package/src/components/viewer/lists/ListResultsTable.tsx +270 -176
  66. package/src/components/viewer/lists/list-table-utils.ts +123 -0
  67. package/src/components/viewer/useGeometryStreaming.ts +21 -1
  68. package/src/generated/mcp-catalog.json +4 -0
  69. package/src/hooks/ingest/streamCleanup.test.ts +41 -0
  70. package/src/hooks/ingest/streamCleanup.ts +45 -0
  71. package/src/hooks/ingest/viewerModelIngest.ts +64 -42
  72. package/src/hooks/ingest/watchedGeometryStream.test.ts +78 -0
  73. package/src/hooks/ingest/watchedGeometryStream.ts +76 -0
  74. package/src/hooks/source-key.ts +35 -0
  75. package/src/hooks/useAlignmentLines3D.ts +139 -0
  76. package/src/hooks/useClash.ts +420 -0
  77. package/src/hooks/useGridLines3D.ts +140 -0
  78. package/src/hooks/useIfcFederation.ts +16 -2
  79. package/src/hooks/useIfcLoader.ts +5 -7
  80. package/src/lib/clash/persistence.ts +308 -0
  81. package/src/lib/geo/effective-georef.test.ts +66 -0
  82. package/src/lib/length-unit-scale.ts +41 -0
  83. package/src/lib/lists/adapter.ts +136 -11
  84. package/src/lib/lists/export/csv.ts +47 -0
  85. package/src/lib/lists/export/index.ts +49 -0
  86. package/src/lib/lists/export/model.ts +111 -0
  87. package/src/lib/lists/export/pdf.ts +67 -0
  88. package/src/lib/lists/export/xlsx.ts +83 -0
  89. package/src/lib/lists/index.ts +2 -0
  90. package/src/lib/search/filter-evaluate.test.ts +81 -0
  91. package/src/lib/search/filter-evaluate.ts +59 -87
  92. package/src/lib/search/filter-match.ts +167 -0
  93. package/src/lib/search/filter-rules.test.ts +25 -0
  94. package/src/lib/search/filter-rules.ts +75 -2
  95. package/src/lib/search/filter-schema.ts +0 -0
  96. package/src/lib/slab-edit.test.ts +72 -0
  97. package/src/lib/slab-edit.ts +159 -19
  98. package/src/sdk/adapters/export-adapter.ts +3 -3
  99. package/src/sdk/adapters/query-adapter.ts +3 -3
  100. package/src/services/extensions/host.ts +13 -0
  101. package/src/store/constants.ts +33 -25
  102. package/src/store/index.ts +29 -8
  103. package/src/store/slices/clashSlice.ts +251 -0
  104. package/src/store/slices/listSlice.ts +6 -0
  105. package/src/store/slices/mutationSlice.ts +14 -6
  106. package/src/store/slices/searchSlice.ts +29 -3
  107. package/src/store/slices/visibilitySlice.test.ts +23 -5
  108. package/src/store/slices/visibilitySlice.ts +18 -8
  109. package/src/utils/nativeSpatialDataStore.ts +6 -0
  110. package/src/utils/serverDataModel.test.ts +6 -0
  111. package/src/utils/serverDataModel.ts +7 -0
  112. package/dist/assets/geometry.worker-Cyn5BybV.js +0 -1
  113. package/dist/assets/ifc-lite_bg-ksLBP5cA.wasm +0 -0
  114. package/dist/assets/index-Bws3UAkj.css +0 -1
  115. package/dist/assets/raw-R2QfzPAR.js +0 -1
  116. package/dist/assets/server-client-Ctk8_Bof.js +0 -626
@@ -45,6 +45,8 @@ import {
45
45
  useSymbolicAnnotationsRichData,
46
46
  type SectionClipForGrid,
47
47
  } from '../../hooks/useSymbolicAnnotations.js';
48
+ import { useAlignmentLines3D } from '../../hooks/useAlignmentLines3D.js';
49
+ import { useGridLines3D } from '../../hooks/useGridLines3D.js';
48
50
 
49
51
  interface ViewportProps {
50
52
  geometry: MeshData[] | null;
@@ -652,19 +654,43 @@ export function Viewport({
652
654
  calculateScale();
653
655
  },
654
656
  frameSelection: () => {
655
- // Frame selection - zoom to fit selected element
656
- const selectedId = selectedEntityIdRef.current;
657
+ // Frame the current selection. Prefer the full multi-selection set
658
+ // (Ctrl-click, box-select, a clash pair) so the camera encloses EVERY
659
+ // selected element; fall back to the single primary id. The set is
660
+ // kept in sync with selection (cleared on a plain click), so the
661
+ // union is always an accurate frame of what's highlighted.
657
662
  const geom = geometryRef.current;
658
- if (selectedId !== null && geom) {
659
- const bounds = getEntityBounds(geom, selectedId);
660
- if (bounds) {
661
- camera.frameBounds(bounds.min, bounds.max, 300);
662
- calculateScale();
663
+ const set = selectedEntityIdsRef.current;
664
+ const single = selectedEntityIdRef.current;
665
+ const ids = set && set.size > 0
666
+ ? Array.from(set)
667
+ : single !== null ? [single] : [];
668
+ if (!geom || ids.length === 0) {
669
+ console.warn('[Viewport] frameSelection: No selection or geometry');
670
+ return;
671
+ }
672
+ let min: { x: number; y: number; z: number } | null = null;
673
+ let max: { x: number; y: number; z: number } | null = null;
674
+ for (const id of ids) {
675
+ const b = getEntityBounds(geom, id);
676
+ if (!b) continue;
677
+ if (!min || !max) {
678
+ min = { x: b.min.x, y: b.min.y, z: b.min.z };
679
+ max = { x: b.max.x, y: b.max.y, z: b.max.z };
663
680
  } else {
664
- console.warn('[Viewport] frameSelection: Could not get bounds for selected element');
681
+ min.x = Math.min(min.x, b.min.x);
682
+ min.y = Math.min(min.y, b.min.y);
683
+ min.z = Math.min(min.z, b.min.z);
684
+ max.x = Math.max(max.x, b.max.x);
685
+ max.y = Math.max(max.y, b.max.y);
686
+ max.z = Math.max(max.z, b.max.z);
665
687
  }
688
+ }
689
+ if (min && max) {
690
+ camera.frameBounds(min, max, 300);
691
+ calculateScale();
666
692
  } else {
667
- console.warn('[Viewport] frameSelection: No selection or geometry');
693
+ console.warn('[Viewport] frameSelection: Could not get bounds for selected element');
668
694
  }
669
695
  },
670
696
  orbit: (deltaX: number, deltaY: number) => {
@@ -862,6 +888,34 @@ export function Viewport({
862
888
  }
863
889
  }, [annotationVertices3D, isInitialized]);
864
890
 
891
+ // IfcAlignment centerlines render as thin lines (not a ribbon mesh), always
892
+ // on — see useAlignmentLines3D. Upload/clear mirrors the annotation overlay;
893
+ // a separate renderer buffer keeps alignment visibility independent.
894
+ const alignmentVertices3D = useAlignmentLines3D();
895
+ useEffect(() => {
896
+ const renderer = rendererRef.current;
897
+ if (!renderer || !isInitialized) return;
898
+ if (alignmentVertices3D.length === 0) {
899
+ renderer.clearAlignmentLines3D();
900
+ } else {
901
+ renderer.uploadAlignmentLines3D(alignmentVertices3D);
902
+ }
903
+ }, [alignmentVertices3D, isInitialized]);
904
+
905
+ // Structural-grid (IfcGridAxis) lines, gated by the `ifcGrid` type-visibility
906
+ // toggle (issue #967). Parsed once per source + cached; only the upload/clear
907
+ // is toggled so flipping visibility doesn't re-parse.
908
+ const gridVertices3D = useGridLines3D();
909
+ useEffect(() => {
910
+ const renderer = rendererRef.current;
911
+ if (!renderer || !isInitialized) return;
912
+ if (!ifcGridVisible || gridVertices3D.length === 0) {
913
+ renderer.clearGridLines3D();
914
+ } else {
915
+ renderer.uploadGridLines3D(gridVertices3D);
916
+ }
917
+ }, [gridVertices3D, ifcGridVisible, isInitialized]);
918
+
865
919
  // Upload IfcAnnotation text + fill data for the WebGPU symbolic overlay
866
920
  // pipelines. Map the hook's per-annotation records into the SymbolicFillInput
867
921
  // / SymbolicTextInput shape the renderer expects. Empty arrays clear cleanly.
@@ -1056,6 +1110,7 @@ export function Viewport({
1056
1110
  geometryContentVersion,
1057
1111
  coordinateInfo,
1058
1112
  isStreaming,
1113
+ modelCount: modelIdToIndex?.size ?? 0,
1059
1114
  geometryBoundsRef,
1060
1115
  pendingColorUpdates,
1061
1116
  pendingMeshColorUpdates,
@@ -39,6 +39,26 @@ const DEFAULT_COORDINATE_INFO: CoordinateInfo = {
39
39
  hasLargeCoordinates: false,
40
40
  };
41
41
 
42
+ type Vec3Bounds = { min: { x: number; y: number; z: number }; max: { x: number; y: number; z: number } };
43
+
44
+ /** True for a real (non-placeholder, non-degenerate) bounds box. */
45
+ function isUsableBounds(b: Vec3Bounds | undefined): b is Vec3Bounds {
46
+ if (!b) return false;
47
+ return (
48
+ b.max.x > b.min.x || b.max.y > b.min.y || b.max.z > b.min.z
49
+ );
50
+ }
51
+
52
+ /** Axis-aligned union of two bounds boxes (either may be undefined). */
53
+ function unionBounds(acc: Vec3Bounds | undefined, b: Vec3Bounds | undefined): Vec3Bounds | undefined {
54
+ if (!isUsableBounds(b)) return acc;
55
+ if (!acc) return { min: { ...b.min }, max: { ...b.max } };
56
+ return {
57
+ min: { x: Math.min(acc.min.x, b.min.x), y: Math.min(acc.min.y, b.min.y), z: Math.min(acc.min.z, b.min.z) },
58
+ max: { x: Math.max(acc.max.x, b.max.x), y: Math.max(acc.max.y, b.max.y), z: Math.max(acc.max.z, b.max.z) },
59
+ };
60
+ }
61
+
42
62
  export function ViewportContainer() {
43
63
  // Drive Stacked / Solo / Exploded level display from the slice.
44
64
  // Mount-once hook — it self-gates on mode + gap + model changes.
@@ -121,7 +141,16 @@ export function ViewportContainer() {
121
141
  if (storeModels.size > 1) {
122
142
  let totalVertices = 0;
123
143
  let totalTriangles = 0;
124
- let mergedCoordinateInfo: CoordinateInfo | undefined;
144
+ // The merged coordinateInfo must cover ALL visible models, not just the
145
+ // first one — the renderer fits the camera to `shiftedBounds`, so a
146
+ // first-wins box left every model after the first off-screen (it only
147
+ // showed its 2D grid overlay). Union the bounds across visible models;
148
+ // keep the first model's frame metadata (originShift / RTC) since
149
+ // federated models share a coordinate frame.
150
+ let baseCoordInfo: CoordinateInfo | undefined;
151
+ let unionedShifted: Vec3Bounds | undefined;
152
+ let unionedOriginal: Vec3Bounds | undefined;
153
+ let anyLargeCoords = false;
125
154
  let shouldRebuild = false;
126
155
 
127
156
  if (mergedLengthsRef.current.size !== storeModels.size) {
@@ -142,8 +171,12 @@ export function ViewportContainer() {
142
171
  const meshCount = model.visible ? (modelGeometry?.meshes.length ?? 0) : 0;
143
172
  totalVertices += model.visible ? (modelGeometry?.totalVertices ?? 0) : 0;
144
173
  totalTriangles += model.visible ? (modelGeometry?.totalTriangles ?? 0) : 0;
145
- if (!mergedCoordinateInfo && model.visible && modelGeometry?.coordinateInfo) {
146
- mergedCoordinateInfo = modelGeometry.coordinateInfo;
174
+ if (model.visible && modelGeometry?.coordinateInfo) {
175
+ const ci = modelGeometry.coordinateInfo;
176
+ if (!baseCoordInfo) baseCoordInfo = ci;
177
+ anyLargeCoords = anyLargeCoords || !!ci.hasLargeCoordinates;
178
+ unionedShifted = unionBounds(unionedShifted, ci.shiftedBounds);
179
+ unionedOriginal = unionBounds(unionedOriginal, ci.originalBounds);
147
180
  }
148
181
 
149
182
  if (
@@ -187,6 +220,15 @@ export function ViewportContainer() {
187
220
  }
188
221
  }
189
222
 
223
+ const mergedCoordinateInfo: CoordinateInfo | undefined = baseCoordInfo
224
+ ? {
225
+ ...baseCoordInfo,
226
+ originalBounds: unionedOriginal ?? baseCoordInfo.originalBounds,
227
+ shiftedBounds: unionedShifted ?? baseCoordInfo.shiftedBounds,
228
+ hasLargeCoordinates: anyLargeCoords,
229
+ }
230
+ : undefined;
231
+
190
232
  return {
191
233
  meshes: mergedCacheRef.current,
192
234
  totalVertices,
@@ -128,7 +128,7 @@ export function BCFOverlay() {
128
128
  const bcfProject = useViewerStore((s) => s.bcfProject);
129
129
  const activeTopicId = useViewerStore((s) => s.activeTopicId);
130
130
  const setActiveTopic = useViewerStore((s) => s.setActiveTopic);
131
- const setBcfPanelVisible = useViewerStore((s) => s.setBcfPanelVisible);
131
+ const openWorkspacePanel = useViewerStore((s) => s.openWorkspacePanel);
132
132
  const models = useViewerStore((s) => s.models);
133
133
  const loading = useViewerStore((s) => s.loading);
134
134
  const ifcDataStore = useViewerStore((s) => s.ifcDataStore);
@@ -239,10 +239,11 @@ export function BCFOverlay() {
239
239
  if (!overlay) return;
240
240
  return overlay.onMarkerClick((topicGuid) => {
241
241
  setActiveTopic(topicGuid);
242
- const panelVisible = useViewerStore.getState().bcfPanelVisible;
243
- if (!panelVisible) setBcfPanelVisible(true);
242
+ // Open BCF exclusively so clicking a marker brings it to the front over any
243
+ // other right panel (e.g. clash), instead of leaving it behind.
244
+ openWorkspacePanel('bcf');
244
245
  });
245
- }, [overlayReady, setActiveTopic, setBcfPanelVisible]);
246
+ }, [overlayReady, setActiveTopic, openWorkspacePanel]);
246
247
 
247
248
  return (
248
249
  <div
@@ -0,0 +1,84 @@
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
+ * Per-column actions menu for the Lists results table header. Brings
7
+ * grouping / aggregation / sorting onto the table itself so the user never
8
+ * has to round-trip through the list settings.
9
+ */
10
+
11
+ import { ArrowUp, ArrowDown, Group, Ungroup, Sigma, Palette, MoreVertical } from 'lucide-react';
12
+ import {
13
+ DropdownMenu,
14
+ DropdownMenuTrigger,
15
+ DropdownMenuContent,
16
+ DropdownMenuItem,
17
+ DropdownMenuCheckboxItem,
18
+ DropdownMenuSeparator,
19
+ } from '@/components/ui/dropdown-menu';
20
+ import { cn } from '@/lib/utils';
21
+
22
+ interface ColumnHeaderMenuProps {
23
+ isNumeric: boolean;
24
+ isGroupedBy: boolean;
25
+ isSummed: boolean;
26
+ active: boolean;
27
+ onSort: (dir: 'asc' | 'desc') => void;
28
+ onToggleGroup: () => void;
29
+ onToggleSum: () => void;
30
+ onColorBy: () => void;
31
+ }
32
+
33
+ export function ColumnHeaderMenu({
34
+ isNumeric, isGroupedBy, isSummed, active,
35
+ onSort, onToggleGroup, onToggleSum, onColorBy,
36
+ }: ColumnHeaderMenuProps) {
37
+ return (
38
+ <DropdownMenu>
39
+ <DropdownMenuTrigger asChild>
40
+ <button
41
+ aria-label="Column options"
42
+ onClick={(e) => e.stopPropagation()}
43
+ className={cn(
44
+ 'shrink-0 rounded-sm p-0.5 transition-opacity hover:text-foreground',
45
+ active
46
+ ? 'text-primary opacity-100'
47
+ : 'text-muted-foreground opacity-0 group-hover/col:opacity-100 data-[state=open]:opacity-100',
48
+ )}
49
+ >
50
+ <MoreVertical className="h-3 w-3" />
51
+ </button>
52
+ </DropdownMenuTrigger>
53
+ <DropdownMenuContent align="start" className="w-52">
54
+ <DropdownMenuItem className="gap-2 text-xs" onClick={() => onSort('asc')}>
55
+ <ArrowUp className="h-3.5 w-3.5" /> Sort ascending
56
+ </DropdownMenuItem>
57
+ <DropdownMenuItem className="gap-2 text-xs" onClick={() => onSort('desc')}>
58
+ <ArrowDown className="h-3.5 w-3.5" /> Sort descending
59
+ </DropdownMenuItem>
60
+ <DropdownMenuSeparator />
61
+ <DropdownMenuItem className="gap-2 text-xs" onClick={onToggleGroup}>
62
+ {isGroupedBy
63
+ ? (<><Ungroup className="h-3.5 w-3.5" /> Remove grouping</>)
64
+ : (<><Group className="h-3.5 w-3.5" /> Group by this column</>)}
65
+ </DropdownMenuItem>
66
+ <DropdownMenuCheckboxItem
67
+ className="text-xs"
68
+ checked={isSummed}
69
+ disabled={!isNumeric}
70
+ onCheckedChange={onToggleSum}
71
+ >
72
+ <span className="flex items-center gap-2">
73
+ <Sigma className="h-3.5 w-3.5" />
74
+ {isNumeric ? 'Sum / total this column' : 'Sum (numeric only)'}
75
+ </span>
76
+ </DropdownMenuCheckboxItem>
77
+ <DropdownMenuSeparator />
78
+ <DropdownMenuItem className="gap-2 text-xs" onClick={onColorBy}>
79
+ <Palette className="h-3.5 w-3.5" /> Colour by this column
80
+ </DropdownMenuItem>
81
+ </DropdownMenuContent>
82
+ </DropdownMenu>
83
+ );
84
+ }