@ifc-lite/viewer 1.26.0 → 1.28.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 +45 -38
- package/CHANGELOG.md +93 -0
- package/dist/assets/{basketViewActivator-ZpTYWE3K.js → basketViewActivator-BNRDNuUJ.js} +9 -9
- package/dist/assets/{bcf-Ctcu_Sc2.js → bcf-DCwCuP7n.js} +56 -56
- package/dist/assets/{browser-DXS29_v9.js → browser-BIoDDfBW.js} +1 -1
- package/dist/assets/{cesium-BoVuJvTC.js → cesium-CzZn5yVA.js} +319 -319
- package/dist/assets/{decode-worker-CgM1iNSK.js → decode-worker-Cjign7Zh.js} +1 -1
- package/dist/assets/deflate-DNGgs8Ur.js +1 -0
- package/dist/assets/drawing-2d-D0dDf6Lh.js +257 -0
- package/dist/assets/e57-source-2wI9jkCA.js +1 -0
- package/dist/assets/exceljs.min-DsuzKYnj.js +29 -0
- package/dist/assets/{exporters-DSq76AVM.js → exporters-B9v81gi9.js} +1861 -1524
- package/dist/assets/geometry.worker-Bpa3115V.js +1 -0
- package/dist/assets/{geotiff-A5UjhI6L.js → geotiff-D-YCLS4g.js} +10 -10
- package/dist/assets/html2canvas.esm-Ge7aVWlp.js +5 -0
- package/dist/assets/{ids-DiLcGTer.js → ids-CCpq-5d3.js} +952 -945
- package/dist/assets/ifc-lite_bg-DbgS5EUA.wasm +0 -0
- package/dist/assets/{index-BAH8IJVR.js → index-Bgb3_Pu_.js} +47682 -42474
- package/dist/assets/index-BtbXFKsX.css +1 -0
- package/dist/assets/index.es-CWfqZyyr.js +6866 -0
- package/dist/assets/{jpeg-BzSkwo5D.js → jpeg-DGOAeUqU.js} +1 -1
- package/dist/assets/jspdf.es.min-XPLU2Wkq.js +19571 -0
- package/dist/assets/jspdf.plugin.autotable-BBLUVd7n.js +2 -0
- package/dist/assets/lens-C4p1kQ0p.js +1 -0
- package/dist/assets/{lerc-Cg2Rz-D5.js → lerc-1PMSCHwX.js} +1 -1
- package/dist/assets/{lzw-BBPPLW-0.js → lzw-C65U9lNM.js} +1 -1
- package/dist/assets/{maplibre-gl-Do6O5tDc.js → maplibre-gl-BF3Z0idw.js} +1 -1
- package/dist/assets/{native-bridge-CPojOeGE.js → native-bridge-XxXos6yI.js} +2 -2
- package/dist/assets/{packbits-yLSpjW-V.js → packbits-BdMWXC3m.js} +1 -1
- package/dist/assets/{pako.esm-Cram60i4.js → pako.esm-n3Pgozwg.js} +1 -1
- package/dist/assets/parser.worker-Ddwo3_06.js +182 -0
- package/dist/assets/pdf-CRwaZf3s.js +135 -0
- package/dist/assets/raw-CJgQdyuZ.js +1 -0
- package/dist/assets/{sandbox-CsRXlgCO.js → sandbox-0sDo3g3m.js} +3037 -2554
- package/dist/assets/server-client-cTCJ-853.js +719 -0
- package/dist/assets/{webimage-YafxjjGr.js → webimage-BtakWX7W.js} +1 -1
- package/dist/assets/xlsx-B1YOg2QB.js +142 -0
- package/dist/assets/{zip-BJqVbRkU.js → zip-DFgP-l20.js} +1 -1
- package/dist/assets/{zstd-CkSLOiuu.js → zstd-CmwsbxmM.js} +1 -1
- package/dist/index.html +10 -10
- package/package.json +27 -23
- package/src/components/mcp/PlaygroundChat.tsx +1 -0
- package/src/components/mcp/data.ts +6 -0
- package/src/components/mcp/playground-dispatcher.ts +280 -0
- package/src/components/mcp/playground-files.ts +33 -1
- package/src/components/mcp/types.ts +2 -1
- package/src/components/ui/combo-input.tsx +163 -0
- package/src/components/ui/tabs.tsx +1 -1
- package/src/components/viewer/CommandPalette.tsx +6 -1
- package/src/components/viewer/ComparePanel.tsx +420 -0
- package/src/components/viewer/HierarchyPanel.tsx +46 -7
- package/src/components/viewer/MainToolbar.tsx +19 -2
- package/src/components/viewer/PropertiesPanel.tsx +84 -8
- package/src/components/viewer/SearchInline.tsx +62 -2
- package/src/components/viewer/SearchModal.filter.builder.tsx +24 -393
- package/src/components/viewer/SearchModal.filter.editors.tsx +503 -0
- package/src/components/viewer/SearchModal.filter.tsx +64 -1
- package/src/components/viewer/SearchModal.tsx +19 -6
- package/src/components/viewer/ViewerLayout.tsx +5 -0
- package/src/components/viewer/Viewport.tsx +18 -0
- package/src/components/viewer/hierarchy/HierarchyNode.tsx +3 -3
- package/src/components/viewer/hierarchy/ifc-icons.ts +9 -0
- package/src/components/viewer/hierarchy/treeDataBuilder.ts +87 -0
- package/src/components/viewer/hierarchy/types.ts +1 -0
- package/src/components/viewer/hierarchy/useHierarchyTree.ts +6 -2
- package/src/components/viewer/lists/ColumnHeaderMenu.tsx +84 -0
- package/src/components/viewer/lists/ListBuilder.tsx +789 -280
- package/src/components/viewer/lists/ListGroupingBar.tsx +72 -0
- package/src/components/viewer/lists/ListPanel.tsx +49 -5
- package/src/components/viewer/lists/ListResultsTable.tsx +270 -176
- package/src/components/viewer/lists/list-table-utils.ts +123 -0
- package/src/components/viewer/properties/MaterialTotalsPanel.tsx +283 -0
- package/src/generated/mcp-catalog.json +4 -0
- package/src/hooks/federationLoadGate.test.ts +12 -2
- package/src/hooks/federationLoadGate.ts +9 -2
- package/src/hooks/ingest/federationAlign.ts +481 -0
- package/src/hooks/ingest/viewerModelIngest.ts +3 -212
- package/src/hooks/source-key.ts +35 -0
- package/src/hooks/useAlignmentLines3D.ts +1 -26
- package/src/hooks/useCompare.ts +0 -0
- package/src/hooks/useCompareOverlay.ts +119 -0
- package/src/hooks/useDrawingGeneration.ts +23 -1
- package/src/hooks/useGridLines3D.ts +140 -0
- package/src/hooks/useIfc.ts +1 -1
- package/src/hooks/useIfcCache.ts +32 -9
- package/src/hooks/useIfcFederation.ts +42 -810
- package/src/hooks/useIfcLoader.ts +361 -488
- package/src/hooks/useIfcServer.ts +3 -0
- package/src/hooks/useLens.ts +5 -1
- package/src/hooks/useSymbolicAnnotations.ts +70 -38
- package/src/lib/compare/buildFingerprints.ts +173 -0
- package/src/lib/compare/describeChange.ts +0 -0
- package/src/lib/compare/geometricData.test.ts +54 -0
- package/src/lib/compare/geometricData.ts +37 -0
- package/src/lib/compare/overlay.test.ts +99 -0
- package/src/lib/compare/overlay.ts +91 -0
- package/src/lib/geo/cesium-placement.ts +1 -1
- package/src/lib/geo/reproject.ts +4 -1
- package/src/lib/length-unit-scale.ts +41 -0
- package/src/lib/lists/adapter.ts +136 -11
- package/src/lib/lists/export/csv.ts +47 -0
- package/src/lib/lists/export/index.ts +49 -0
- package/src/lib/lists/export/model.ts +111 -0
- package/src/lib/lists/export/pdf.ts +67 -0
- package/src/lib/lists/export/xlsx.ts +83 -0
- package/src/lib/lists/index.ts +2 -0
- package/src/lib/llm/script-edit-ops.ts +23 -0
- package/src/lib/llm/stream-client.ts +8 -1
- package/src/lib/search/filter-evaluate.test.ts +81 -0
- package/src/lib/search/filter-evaluate.ts +59 -87
- package/src/lib/search/filter-match.ts +167 -0
- package/src/lib/search/filter-rules.test.ts +25 -0
- package/src/lib/search/filter-rules.ts +75 -2
- package/src/lib/search/filter-schema.ts +0 -0
- package/src/lib/search/result-export.ts +7 -1
- package/src/lib/slab-edit.test.ts +72 -0
- package/src/lib/slab-edit.ts +159 -19
- package/src/sdk/adapters/export-adapter.ts +9 -4
- package/src/sdk/adapters/query-adapter.ts +3 -3
- package/src/store/globalId.ts +15 -13
- package/src/store/index.ts +16 -1
- package/src/store/slices/cesiumSlice.ts +8 -1
- package/src/store/slices/compareSlice.ts +96 -0
- package/src/store/slices/lensSlice.ts +8 -0
- package/src/store/slices/listSlice.ts +6 -0
- package/src/store/slices/mutationSlice.ts +14 -6
- package/src/store/slices/searchSlice.ts +29 -3
- package/src/utils/acquireFileBuffer.test.ts +12 -4
- package/src/utils/desktopModelSnapshot.ts +2 -1
- package/src/utils/loadingUtils.ts +32 -0
- package/src/utils/nativeSpatialDataStore.ts +6 -0
- package/src/utils/serverDataModel.test.ts +6 -0
- package/src/utils/serverDataModel.ts +7 -0
- package/src/utils/spatialHierarchy.test.ts +53 -1
- package/src/utils/spatialHierarchy.ts +42 -2
- package/src/vite-env.d.ts +2 -0
- package/dist/assets/deflate-Cnx0il6E.js +0 -1
- package/dist/assets/drawing-2d-C71b8Ugx.js +0 -257
- package/dist/assets/e57-source-CQHxE8n3.js +0 -1
- package/dist/assets/geometry.worker-0Q9qEa6p.js +0 -1
- package/dist/assets/ifc-lite_bg-CEZnhM2e.wasm +0 -0
- package/dist/assets/index-B9Ug2EqU.css +0 -1
- package/dist/assets/lens-PYsLu_MA.js +0 -1
- package/dist/assets/parser.worker-8md211IW.js +0 -182
- package/dist/assets/raw-BQrAgxwT.js +0 -1
- package/dist/assets/server-client-Bk4c1CPO.js +0 -626
- package/src/hooks/ingest/resolveDataStoreOrAbort.test.ts +0 -61
- package/src/hooks/ingest/resolveDataStoreOrAbort.ts +0 -28
- package/src/hooks/ingest/watchedGeometryStream.test.ts +0 -78
- package/src/hooks/ingest/watchedGeometryStream.ts +0 -76
|
@@ -46,6 +46,7 @@ import {
|
|
|
46
46
|
type SectionClipForGrid,
|
|
47
47
|
} from '../../hooks/useSymbolicAnnotations.js';
|
|
48
48
|
import { useAlignmentLines3D } from '../../hooks/useAlignmentLines3D.js';
|
|
49
|
+
import { useGridLines3D } from '../../hooks/useGridLines3D.js';
|
|
49
50
|
|
|
50
51
|
interface ViewportProps {
|
|
51
52
|
geometry: MeshData[] | null;
|
|
@@ -807,6 +808,9 @@ export function Viewport({
|
|
|
807
808
|
}
|
|
808
809
|
setIsInitialized(false);
|
|
809
810
|
rendererRef.current = null;
|
|
811
|
+
// Free all WebGPU resources held by this renderer instance.
|
|
812
|
+
// destroy() is idempotent, so this is safe even if init() rejected.
|
|
813
|
+
renderer.destroy();
|
|
810
814
|
// Clear BCF global refs to prevent memory leaks
|
|
811
815
|
clearGlobalRefs();
|
|
812
816
|
};
|
|
@@ -901,6 +905,20 @@ export function Viewport({
|
|
|
901
905
|
}
|
|
902
906
|
}, [alignmentVertices3D, isInitialized]);
|
|
903
907
|
|
|
908
|
+
// Structural-grid (IfcGridAxis) lines, gated by the `ifcGrid` type-visibility
|
|
909
|
+
// toggle (issue #967). Parsed once per source + cached; only the upload/clear
|
|
910
|
+
// is toggled so flipping visibility doesn't re-parse.
|
|
911
|
+
const gridVertices3D = useGridLines3D();
|
|
912
|
+
useEffect(() => {
|
|
913
|
+
const renderer = rendererRef.current;
|
|
914
|
+
if (!renderer || !isInitialized) return;
|
|
915
|
+
if (!ifcGridVisible || gridVertices3D.length === 0) {
|
|
916
|
+
renderer.clearGridLines3D();
|
|
917
|
+
} else {
|
|
918
|
+
renderer.uploadGridLines3D(gridVertices3D);
|
|
919
|
+
}
|
|
920
|
+
}, [gridVertices3D, ifcGridVisible, isInitialized]);
|
|
921
|
+
|
|
904
922
|
// Upload IfcAnnotation text + fill data for the WebGPU symbolic overlay
|
|
905
923
|
// pipelines. Map the hook's per-annotation records into the SymbolicFillInput
|
|
906
924
|
// / SymbolicTextInput shape the renderer expects. Empty arrays clear cleanly.
|
|
@@ -275,7 +275,7 @@ export function HierarchyNode({
|
|
|
275
275
|
{/* Name */}
|
|
276
276
|
<span className={cn(
|
|
277
277
|
'flex-1 text-sm truncate ml-1.5',
|
|
278
|
-
isSpatialContainer(node.type) || node.type === 'IfcBuildingStorey' || node.type === 'IfcSpace' || node.type === 'unified-storey' || node.type === 'type-group'
|
|
278
|
+
isSpatialContainer(node.type) || node.type === 'IfcBuildingStorey' || node.type === 'IfcSpace' || node.type === 'unified-storey' || node.type === 'type-group' || node.type === 'material-group'
|
|
279
279
|
? 'font-medium text-zinc-900 dark:text-zinc-100'
|
|
280
280
|
: 'text-zinc-700 dark:text-zinc-300',
|
|
281
281
|
nodeHidden && 'line-through decoration-zinc-400 dark:decoration-zinc-600'
|
|
@@ -306,11 +306,11 @@ export function HierarchyNode({
|
|
|
306
306
|
<Tooltip>
|
|
307
307
|
<TooltipTrigger asChild>
|
|
308
308
|
<span className="text-[10px] font-mono bg-zinc-100 dark:bg-zinc-950 px-1.5 py-0.5 border border-zinc-200 dark:border-zinc-800 text-zinc-500 dark:text-zinc-400 rounded-none">
|
|
309
|
-
{node.elementCount}
|
|
309
|
+
{node.elementCount.toLocaleString()}
|
|
310
310
|
</span>
|
|
311
311
|
</TooltipTrigger>
|
|
312
312
|
<TooltipContent>
|
|
313
|
-
<p className="text-xs">{node.elementCount} {node.elementCount === 1 ? 'element' : 'elements'}</p>
|
|
313
|
+
<p className="text-xs">{node.elementCount.toLocaleString()} {node.elementCount === 1 ? 'element' : 'elements'}</p>
|
|
314
314
|
</TooltipContent>
|
|
315
315
|
</Tooltip>
|
|
316
316
|
)}
|
|
@@ -140,6 +140,15 @@ export const IFC_ICON_CODEPOINTS: Record<string, string> = {
|
|
|
140
140
|
IfcTrackElement: '\ue260', // "linear_scale"
|
|
141
141
|
IfcVehicle: '\ue531', // "directions_car"
|
|
142
142
|
|
|
143
|
+
// Materials (Materials hierarchy tab)
|
|
144
|
+
IfcMaterial: '\ue4f4', // "texture"
|
|
145
|
+
IfcMaterialLayerSet: '\ue8fe', // "layers"
|
|
146
|
+
IfcMaterialLayerSetUsage: '\ue8fe',
|
|
147
|
+
IfcMaterialProfileSet: '\ue8fe',
|
|
148
|
+
IfcMaterialProfileSetUsage: '\ue8fe',
|
|
149
|
+
IfcMaterialConstituentSet: '\ue4f4',
|
|
150
|
+
IfcMaterialList: '\ue4f4',
|
|
151
|
+
|
|
143
152
|
// Proxy / generic fallback
|
|
144
153
|
IfcProduct: '\ue047',
|
|
145
154
|
IfcBuildingElementProxy: '\ue047',
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
type SpatialNode,
|
|
13
13
|
} from '@ifc-lite/data';
|
|
14
14
|
import type { IfcDataStore } from '@ifc-lite/parser';
|
|
15
|
+
import { buildMaterialUsageIndex } from '@ifc-lite/parser';
|
|
15
16
|
import { useViewerStore, type FederatedModel } from '@/store';
|
|
16
17
|
import { toGlobalIdFromModels } from '@/store/globalId';
|
|
17
18
|
import type { TreeNode, NodeType, StoreyData, UnifiedStorey } from './types';
|
|
@@ -712,6 +713,92 @@ export function buildIfcTypeTree(
|
|
|
712
713
|
return nodes;
|
|
713
714
|
}
|
|
714
715
|
|
|
716
|
+
/**
|
|
717
|
+
* Build a flat "By Material" tree: one row per base material (IfcMaterial),
|
|
718
|
+
* grouped by name so the same-named material across federated models merges.
|
|
719
|
+
* Each row carries the using elements' global ids for click-to-isolate and the
|
|
720
|
+
* representative material express id for the properties panel. Mirrors
|
|
721
|
+
* {@link buildIfcTypeTree} but keyed on the parser's material usage index.
|
|
722
|
+
*/
|
|
723
|
+
export function buildMaterialTree(
|
|
724
|
+
models: Map<string, FederatedModel>,
|
|
725
|
+
ifcDataStore: IfcDataStore | null | undefined,
|
|
726
|
+
_expandedNodes: Set<string>,
|
|
727
|
+
_isMultiModel: boolean,
|
|
728
|
+
geometricIds?: Set<number>,
|
|
729
|
+
): TreeNode[] {
|
|
730
|
+
interface MatEntry {
|
|
731
|
+
name: string;
|
|
732
|
+
ifcClass: string;
|
|
733
|
+
materialId: number; // representative material express id
|
|
734
|
+
modelIds: Set<string>; // contributing models (insertion order)
|
|
735
|
+
elements: Map<number, number>; // globalId -> expressId (deduped)
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
const byName = new Map<string, MatEntry>();
|
|
739
|
+
const applyGeomFilter = !!geometricIds && geometricIds.size > 0;
|
|
740
|
+
|
|
741
|
+
const processDataStore = (dataStore: IfcDataStore, modelId: string) => {
|
|
742
|
+
const usage = buildMaterialUsageIndex(dataStore);
|
|
743
|
+
for (const u of usage.values()) {
|
|
744
|
+
let entry = byName.get(u.name);
|
|
745
|
+
if (!entry) {
|
|
746
|
+
// Invariant: the representative `materialId` and the first entry in
|
|
747
|
+
// `modelIds` come from the SAME (first-contributing) model, so the click
|
|
748
|
+
// handler's `node.modelIds[0]` + `node.entityExpressId` always resolve a
|
|
749
|
+
// valid (model, material) pair. Sets preserve insertion order.
|
|
750
|
+
entry = {
|
|
751
|
+
name: u.name,
|
|
752
|
+
ifcClass: u.ifcClass,
|
|
753
|
+
materialId: u.id,
|
|
754
|
+
modelIds: new Set([modelId]),
|
|
755
|
+
elements: new Map(),
|
|
756
|
+
};
|
|
757
|
+
byName.set(u.name, entry);
|
|
758
|
+
} else {
|
|
759
|
+
entry.modelIds.add(modelId);
|
|
760
|
+
}
|
|
761
|
+
for (const { entityId } of u.entries) {
|
|
762
|
+
const globalId = resolveTreeGlobalId(modelId, entityId, models);
|
|
763
|
+
if (applyGeomFilter && !geometricIds!.has(globalId)) continue;
|
|
764
|
+
entry.elements.set(globalId, entityId);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
};
|
|
768
|
+
|
|
769
|
+
if (models.size > 0) {
|
|
770
|
+
for (const [modelId, model] of models) {
|
|
771
|
+
if (model.ifcDataStore) processDataStore(model.ifcDataStore, modelId);
|
|
772
|
+
}
|
|
773
|
+
} else if (ifcDataStore) {
|
|
774
|
+
processDataStore(ifcDataStore, 'legacy');
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
const nodes: TreeNode[] = [];
|
|
778
|
+
const names = Array.from(byName.keys()).sort((a, b) => a.localeCompare(b));
|
|
779
|
+
for (const name of names) {
|
|
780
|
+
const entry = byName.get(name)!;
|
|
781
|
+
if (entry.elements.size === 0) continue; // skip materials with no visible elements (dead clicks)
|
|
782
|
+
nodes.push({
|
|
783
|
+
id: `material-${name}`,
|
|
784
|
+
expressIds: Array.from(entry.elements.values()),
|
|
785
|
+
globalIds: Array.from(entry.elements.keys()),
|
|
786
|
+
entityExpressId: entry.materialId,
|
|
787
|
+
modelIds: Array.from(entry.modelIds),
|
|
788
|
+
name,
|
|
789
|
+
type: 'material-group',
|
|
790
|
+
ifcType: entry.ifcClass,
|
|
791
|
+
depth: 0,
|
|
792
|
+
hasChildren: false,
|
|
793
|
+
isExpanded: false,
|
|
794
|
+
isVisible: true,
|
|
795
|
+
elementCount: entry.elements.size,
|
|
796
|
+
});
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
return nodes;
|
|
800
|
+
}
|
|
801
|
+
|
|
715
802
|
/** Filter nodes based on search query */
|
|
716
803
|
export function filterNodes(nodes: TreeNode[], searchQuery: string): TreeNode[] {
|
|
717
804
|
if (!searchQuery.trim()) return nodes;
|
|
@@ -22,6 +22,7 @@ export type NodeType =
|
|
|
22
22
|
| 'IfcSpace' // Space node
|
|
23
23
|
| 'type-group' // IFC class grouping header (e.g., "IfcWall (47)")
|
|
24
24
|
| 'ifc-type' // IFC type entity node (e.g., "IfcWallType/W01")
|
|
25
|
+
| 'material-group' // Material grouping (e.g., "Concrete (47)") from the Materials tab
|
|
25
26
|
| 'element'; // Individual element
|
|
26
27
|
|
|
27
28
|
export interface TreeNode {
|
|
@@ -13,11 +13,12 @@ import {
|
|
|
13
13
|
buildTreeData,
|
|
14
14
|
buildTypeTree,
|
|
15
15
|
buildIfcTypeTree,
|
|
16
|
+
buildMaterialTree,
|
|
16
17
|
filterNodes,
|
|
17
18
|
splitNodes,
|
|
18
19
|
} from './treeDataBuilder';
|
|
19
20
|
|
|
20
|
-
export type GroupingMode = 'spatial' | 'type' | 'ifc-type';
|
|
21
|
+
export type GroupingMode = 'spatial' | 'type' | 'ifc-type' | 'material';
|
|
21
22
|
|
|
22
23
|
interface UseHierarchyTreeParams {
|
|
23
24
|
models: Map<string, FederatedModel>;
|
|
@@ -192,6 +193,9 @@ export function useHierarchyTree({ models, ifcDataStore, isMultiModel, geometryR
|
|
|
192
193
|
if (groupingMode === 'ifc-type') {
|
|
193
194
|
return buildIfcTypeTree(models, ifcDataStore, expandedNodes, isMultiModel, geometricIds);
|
|
194
195
|
}
|
|
196
|
+
if (groupingMode === 'material') {
|
|
197
|
+
return buildMaterialTree(models, ifcDataStore, expandedNodes, isMultiModel, geometricIds);
|
|
198
|
+
}
|
|
195
199
|
return buildTreeData(models, ifcDataStore, expandedNodes, isMultiModel, unifiedStoreys);
|
|
196
200
|
},
|
|
197
201
|
[models, ifcDataStore, expandedNodes, isMultiModel, unifiedStoreys, groupingMode, geometricIds]
|
|
@@ -223,7 +227,7 @@ export function useHierarchyTree({ models, ifcDataStore, isMultiModel, geometryR
|
|
|
223
227
|
|
|
224
228
|
// Get all elements for a node (handles type groups, ifc-type, unified storeys, single storeys, model contributions, and elements)
|
|
225
229
|
const getNodeElements = useCallback((node: TreeNode): number[] => {
|
|
226
|
-
if (node.type === 'type-group' || node.type === 'ifc-type') {
|
|
230
|
+
if (node.type === 'type-group' || node.type === 'ifc-type' || node.type === 'material-group') {
|
|
227
231
|
// GlobalIds are pre-stored on the node during tree construction — O(1)
|
|
228
232
|
return node.globalIds;
|
|
229
233
|
}
|
|
@@ -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
|
+
}
|