@ifc-lite/viewer 1.25.1 → 1.26.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 +83 -85
- package/CHANGELOG.md +104 -0
- package/dist/assets/{basketViewActivator-Dkn92C04.js → basketViewActivator-ZpTYWE3K.js} +6 -6
- package/dist/assets/{bcf-DP2AK1-_.js → bcf-Ctcu_Sc2.js} +5 -5
- package/dist/assets/{deflate-BYqYwhkl.js → deflate-Cnx0il6E.js} +1 -1
- package/dist/assets/exporters-DSq76AVM.js +4687 -0
- package/dist/assets/geometry.worker-0Q9qEa6p.js +1 -0
- package/dist/assets/{geotiff-By06vdeL.js → geotiff-A5UjhI6L.js} +10 -10
- package/dist/assets/{ids-DDkkb4mo.js → ids-DiLcGTer.js} +4 -4
- package/dist/assets/ifc-lite_bg-CEZnhM2e.wasm +0 -0
- package/dist/assets/index-B9Ug2EqU.css +1 -0
- package/dist/assets/{index-CqBdDOAZ.js → index-BAH8IJVR.js} +39550 -36936
- package/dist/assets/{jpeg-B4IBTphL.js → jpeg-BzSkwo5D.js} +1 -1
- package/dist/assets/{lerc-DQ3jI0Ke.js → lerc-Cg2Rz-D5.js} +1 -1
- package/dist/assets/{lzw-CtdH775t.js → lzw-BBPPLW-0.js} +1 -1
- package/dist/assets/{native-bridge-DA8wxaN_.js → native-bridge-CPojOeGE.js} +1 -1
- package/dist/assets/{packbits-DG3zn49C.js → packbits-yLSpjW-V.js} +1 -1
- package/dist/assets/parser.worker-8md211IW.js +182 -0
- package/dist/assets/raw-BQrAgxwT.js +1 -0
- package/dist/assets/{sandbox-D1pQT-5R.js → sandbox-CsRXlgCO.js} +4715 -3081
- package/dist/assets/{server-client-D9xO_8yX.js → server-client-Bk4c1CPO.js} +1 -1
- package/dist/assets/{webimage-_-qCDjkn.js → webimage-YafxjjGr.js} +1 -1
- package/dist/assets/{zstd-DlfgC8gA.js → zstd-CkSLOiuu.js} +1 -1
- package/dist/index.html +7 -7
- package/package.json +23 -21
- package/src/App.tsx +4 -0
- package/src/components/extensions/FlavorDialog.tsx +18 -2
- package/src/components/extensions/FlavorListView.tsx +12 -3
- package/src/components/viewer/ClashBcfExportDialog.tsx +271 -0
- package/src/components/viewer/ClashPanel.tsx +370 -0
- package/src/components/viewer/ClashSettingsDialog.tsx +407 -0
- package/src/components/viewer/CommandPalette.tsx +19 -16
- package/src/components/viewer/MainToolbar.tsx +155 -153
- package/src/components/viewer/ViewerLayout.tsx +5 -0
- package/src/components/viewer/Viewport.tsx +97 -12
- package/src/components/viewer/ViewportContainer.tsx +45 -3
- package/src/components/viewer/bcf/BCFOverlay.tsx +5 -4
- package/src/components/viewer/hierarchy/ifc-icons.ts +60 -0
- package/src/components/viewer/useGeometryStreaming.ts +134 -19
- package/src/hooks/ingest/resolveDataStoreOrAbort.test.ts +61 -0
- package/src/hooks/ingest/resolveDataStoreOrAbort.ts +28 -0
- package/src/hooks/ingest/streamCleanup.test.ts +41 -0
- package/src/hooks/ingest/streamCleanup.ts +45 -0
- package/src/hooks/ingest/viewerModelIngest.ts +118 -52
- package/src/hooks/ingest/watchedGeometryStream.test.ts +78 -0
- package/src/hooks/ingest/watchedGeometryStream.ts +76 -0
- package/src/hooks/useAlignmentLines3D.ts +164 -0
- package/src/hooks/useClash.ts +420 -0
- package/src/hooks/useIfcCache.ts +44 -18
- package/src/hooks/useIfcFederation.ts +16 -2
- package/src/hooks/useIfcLoader.ts +6 -30
- package/src/hooks/useSymbolicAnnotations.ts +170 -35
- package/src/lib/clash/persistence.ts +308 -0
- package/src/lib/geo/effective-georef.test.ts +66 -0
- package/src/services/extensions/host.ts +13 -0
- package/src/store/constants.ts +38 -14
- package/src/store/index.ts +29 -7
- package/src/store/slices/clashSlice.ts +251 -0
- package/src/store/slices/visibilitySlice.test.ts +23 -5
- package/src/store/slices/visibilitySlice.ts +19 -8
- package/src/store/types.ts +9 -0
- package/src/utils/serverDataModel.test.ts +51 -1
- package/src/utils/serverDataModel.ts +2 -26
- package/vite.config.ts +0 -5
- package/dist/assets/exporters-CZe0D8N-.js +0 -5957
- package/dist/assets/geometry-controller.worker-pD49_fH6.js +0 -7
- package/dist/assets/geometry.worker-D4c-06r5.js +0 -1
- package/dist/assets/ifc-lite-DxGqDbjO.js +0 -7
- package/dist/assets/ifc-lite_bg-BNeu7R_V.wasm +0 -0
- package/dist/assets/ifc-lite_bg-DuxUZomW.wasm +0 -0
- package/dist/assets/index-Bws3UAkj.css +0 -1
- package/dist/assets/parser.worker-BZZcO7DB.js +0 -182
- package/dist/assets/raw-DY7Y_acr.js +0 -1
- package/dist/assets/wasm-bridge-DMX8Acuf.js +0 -1
- package/dist/assets/workerHelpers-Crstj4Oa.js +0 -36
|
@@ -52,6 +52,7 @@ import {
|
|
|
52
52
|
Redo2,
|
|
53
53
|
} from 'lucide-react';
|
|
54
54
|
import { Button } from '@/components/ui/button';
|
|
55
|
+
import { Switch } from '@/components/ui/switch';
|
|
55
56
|
import { Separator } from '@/components/ui/separator';
|
|
56
57
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
|
57
58
|
import {
|
|
@@ -158,6 +159,39 @@ function ToolButton({
|
|
|
158
159
|
);
|
|
159
160
|
}
|
|
160
161
|
|
|
162
|
+
interface ClassVisibilityRowProps {
|
|
163
|
+
/** Colored class glyph (caller sets the tint). */
|
|
164
|
+
icon: React.ReactNode;
|
|
165
|
+
label: string;
|
|
166
|
+
/** One-line plain-language hint about what the IFC class covers. */
|
|
167
|
+
description: string;
|
|
168
|
+
checked: boolean;
|
|
169
|
+
onChange: (next: boolean) => void;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* One row of the Visibility panel: colored class icon + label/description
|
|
174
|
+
* on the left, a Switch on the right. The whole row is a <label>, so a
|
|
175
|
+
* click anywhere toggles the switch and — because it isn't a menu item —
|
|
176
|
+
* the dropdown stays open for flipping several classes in a row. The left
|
|
177
|
+
* cluster dims when off so on/off reads from saturation as well as the
|
|
178
|
+
* switch position.
|
|
179
|
+
*/
|
|
180
|
+
function ClassVisibilityRow({ icon, label, description, checked, onChange }: ClassVisibilityRowProps) {
|
|
181
|
+
return (
|
|
182
|
+
<label className="group flex items-center justify-between gap-3 rounded-md px-2 py-1.5 cursor-pointer hover:bg-muted/50 transition-colors">
|
|
183
|
+
<span className={cn('flex items-center gap-2.5 min-w-0 transition-opacity', !checked && 'opacity-50')}>
|
|
184
|
+
{icon}
|
|
185
|
+
<span className="grid gap-0.5 min-w-0">
|
|
186
|
+
<span className="text-sm leading-tight truncate">{label}</span>
|
|
187
|
+
<span className="text-[10px] leading-tight text-muted-foreground truncate">{description}</span>
|
|
188
|
+
</span>
|
|
189
|
+
</span>
|
|
190
|
+
<Switch checked={checked} onCheckedChange={onChange} />
|
|
191
|
+
</label>
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
161
195
|
/**
|
|
162
196
|
* Stacked / Exploded / Solo level display dropdown. Pinned next
|
|
163
197
|
* to the Quick Floorplan dropdown so storey-related controls
|
|
@@ -505,6 +539,16 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
505
539
|
const toggleHoverTooltips = useViewerStore((state) => state.toggleHoverTooltips);
|
|
506
540
|
const typeVisibility = useViewerStore((state) => state.typeVisibility);
|
|
507
541
|
const toggleTypeVisibility = useViewerStore((state) => state.toggleTypeVisibility);
|
|
542
|
+
const resetTypeVisibility = useViewerStore((state) => state.resetTypeVisibility);
|
|
543
|
+
// How many of the five class toggles are on — surfaced in the menu
|
|
544
|
+
// header so the user sees scene state at a glance.
|
|
545
|
+
const visibleClassCount = [
|
|
546
|
+
typeVisibility.spaces,
|
|
547
|
+
typeVisibility.openings,
|
|
548
|
+
typeVisibility.site,
|
|
549
|
+
typeVisibility.ifcAnnotations,
|
|
550
|
+
typeVisibility.ifcGrid,
|
|
551
|
+
].filter(Boolean).length;
|
|
508
552
|
// Issue #540: load-time toggle that asks the WASM bridge to merge
|
|
509
553
|
// Revit-style multilayer walls. We surface this in the Class
|
|
510
554
|
// Visibility dropdown so users discover it next to the other
|
|
@@ -516,6 +560,8 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
516
560
|
const setBcfPanelVisible = useViewerStore((state) => state.setBcfPanelVisible);
|
|
517
561
|
const idsPanelVisible = useViewerStore((state) => state.idsPanelVisible);
|
|
518
562
|
const setIdsPanelVisible = useViewerStore((state) => state.setIdsPanelVisible);
|
|
563
|
+
const clashPanelVisible = useViewerStore((state) => state.clashPanelVisible);
|
|
564
|
+
const setClashPanelVisible = useViewerStore((state) => state.setClashPanelVisible);
|
|
519
565
|
const listPanelVisible = useViewerStore((state) => state.listPanelVisible);
|
|
520
566
|
const setListPanelVisible = useViewerStore((state) => state.setListPanelVisible);
|
|
521
567
|
const setRightPanelCollapsed = useViewerStore((state) => state.setRightPanelCollapsed);
|
|
@@ -562,94 +608,11 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
562
608
|
);
|
|
563
609
|
const desktopShell = isTauri();
|
|
564
610
|
|
|
565
|
-
//
|
|
566
|
-
//
|
|
567
|
-
//
|
|
568
|
-
//
|
|
569
|
-
//
|
|
570
|
-
const typeGeomScanRef = useRef({
|
|
571
|
-
spaces: false, openings: false, site: false,
|
|
572
|
-
legacyLastLen: 0,
|
|
573
|
-
modelLastLen: new Map<string | number, number>(),
|
|
574
|
-
});
|
|
575
|
-
const meshLen = geometryResult?.meshes.length ?? 0;
|
|
576
|
-
const typeGeometryExists = useMemo(() => {
|
|
577
|
-
const scan = typeGeomScanRef.current;
|
|
578
|
-
|
|
579
|
-
// Reset if legacy meshes array shrunk (new file loaded)
|
|
580
|
-
if (meshLen < scan.legacyLastLen) {
|
|
581
|
-
scan.spaces = false;
|
|
582
|
-
scan.openings = false;
|
|
583
|
-
scan.site = false;
|
|
584
|
-
scan.legacyLastLen = 0;
|
|
585
|
-
scan.modelLastLen.clear();
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
// Already found all types — nothing to do
|
|
589
|
-
if (scan.spaces && scan.openings && scan.site) {
|
|
590
|
-
return { spaces: scan.spaces, openings: scan.openings, site: scan.site };
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
// Check federated models (scan only new meshes per model)
|
|
594
|
-
if (models.size > 0) {
|
|
595
|
-
for (const [modelId, model] of models) {
|
|
596
|
-
const meshes = model.geometryResult?.meshes;
|
|
597
|
-
if (!meshes) continue;
|
|
598
|
-
const modelStart = scan.modelLastLen.get(modelId) ?? 0;
|
|
599
|
-
// Reset cursor if model was reloaded (mesh array shrunk)
|
|
600
|
-
const start = meshes.length < modelStart ? 0 : modelStart;
|
|
601
|
-
for (let i = start; i < meshes.length; i++) {
|
|
602
|
-
const t = meshes[i].ifcType;
|
|
603
|
-
if (t === 'IfcSpace') scan.spaces = true;
|
|
604
|
-
else if (t === 'IfcOpeningElement') scan.openings = true;
|
|
605
|
-
else if (t === 'IfcSite') scan.site = true;
|
|
606
|
-
if (scan.spaces && scan.openings && scan.site) break;
|
|
607
|
-
}
|
|
608
|
-
scan.modelLastLen.set(modelId, meshes.length);
|
|
609
|
-
if (scan.spaces && scan.openings && scan.site) break;
|
|
610
|
-
}
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
// Legacy single-model path (scan only new meshes)
|
|
614
|
-
if (geometryResult?.meshes) {
|
|
615
|
-
const meshes = geometryResult.meshes;
|
|
616
|
-
for (let i = scan.legacyLastLen; i < meshes.length; i++) {
|
|
617
|
-
const t = meshes[i].ifcType;
|
|
618
|
-
if (t === 'IfcSpace') scan.spaces = true;
|
|
619
|
-
else if (t === 'IfcOpeningElement') scan.openings = true;
|
|
620
|
-
else if (t === 'IfcSite') scan.site = true;
|
|
621
|
-
if (scan.spaces && scan.openings && scan.site) break;
|
|
622
|
-
}
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
scan.legacyLastLen = meshLen;
|
|
626
|
-
return { spaces: scan.spaces, openings: scan.openings, site: scan.site };
|
|
627
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps -- meshLen is a stable proxy for geometryResult
|
|
628
|
-
}, [models, meshLen]);
|
|
629
|
-
|
|
630
|
-
// IfcAnnotation has no body mesh, so it can't be detected via the mesh scan.
|
|
631
|
-
// Look up the entity table directly. byType keys are uppercase STEP names
|
|
632
|
-
// ('IFCANNOTATION') but cache loads sometimes preserve PascalCase too.
|
|
633
|
-
// Symbolic 2D overlays cover BOTH IfcAnnotation (text, dimensions, leader
|
|
634
|
-
// lines, filled regions) AND IfcGrid (axis lines + synthesized bubble +
|
|
635
|
-
// tag). Some files ship only grids (Snowdon Towers Structural is the
|
|
636
|
-
// canonical example — no IfcAnnotation at all), so the toggle must
|
|
637
|
-
// surface for either entity type or grid-only models get no way to hide
|
|
638
|
-
// the overlay.
|
|
639
|
-
const hasIfcAnnotations = useMemo(() => {
|
|
640
|
-
const has = (store: typeof ifcDataStore | undefined) => {
|
|
641
|
-
const byType = store?.entityIndex?.byType;
|
|
642
|
-
if (!byType) return false;
|
|
643
|
-
return (byType.get('IFCANNOTATION')?.length ?? 0) > 0
|
|
644
|
-
|| (byType.get('IfcAnnotation')?.length ?? 0) > 0
|
|
645
|
-
|| (byType.get('IFCGRID')?.length ?? 0) > 0
|
|
646
|
-
|| (byType.get('IfcGrid')?.length ?? 0) > 0;
|
|
647
|
-
};
|
|
648
|
-
if (models.size > 0) {
|
|
649
|
-
for (const [, m] of models) if (has(m.ifcDataStore)) return true;
|
|
650
|
-
}
|
|
651
|
-
return has(ifcDataStore);
|
|
652
|
-
}, [models, ifcDataStore]);
|
|
611
|
+
// NOTE: The Class Visibility dropdown used to gate each toggle on whether
|
|
612
|
+
// the loaded model actually contained that class (scanning meshes for
|
|
613
|
+
// Spaces/Openings/Site and probing the entity table for Annotations/Grids).
|
|
614
|
+
// That gating was removed: the toggles are persisted user preferences, so
|
|
615
|
+
// they now render unconditionally and stay sticky across models and reloads.
|
|
653
616
|
|
|
654
617
|
const handleFileSelect = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
|
655
618
|
const files = e.target.files;
|
|
@@ -798,7 +761,7 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
798
761
|
setScriptPanelVisible,
|
|
799
762
|
]);
|
|
800
763
|
|
|
801
|
-
const handleToggleRightPanel = useCallback((panel: 'bcf' | 'ids' | 'lens' | 'addElement' | 'extensions') => {
|
|
764
|
+
const handleToggleRightPanel = useCallback((panel: 'bcf' | 'ids' | 'lens' | 'clash' | 'addElement' | 'extensions') => {
|
|
802
765
|
if (activeAnalysisExtension?.placement !== 'bottom') {
|
|
803
766
|
closeActiveAnalysisExtension();
|
|
804
767
|
}
|
|
@@ -815,6 +778,7 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
815
778
|
const nextBcfVisible = panel === 'bcf' ? !bcfPanelVisible : false;
|
|
816
779
|
const nextIdsVisible = panel === 'ids' ? !idsPanelVisible : false;
|
|
817
780
|
const nextLensVisible = panel === 'lens' ? !lensPanelVisible : false;
|
|
781
|
+
const nextClashVisible = panel === 'clash' ? !clashPanelVisible : false;
|
|
818
782
|
const nextExtensionsVisible = panel === 'extensions' ? !extensionsPanelVisible : false;
|
|
819
783
|
const isAddElementActive = activeTool === 'addElement';
|
|
820
784
|
const nextAddElementActive = panel === 'addElement' ? !isAddElementActive : false;
|
|
@@ -822,6 +786,7 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
822
786
|
setBcfPanelVisible(nextBcfVisible);
|
|
823
787
|
setIdsPanelVisible(nextIdsVisible);
|
|
824
788
|
setLensPanelVisible(nextLensVisible);
|
|
789
|
+
setClashPanelVisible(nextClashVisible);
|
|
825
790
|
setExtensionsPanelVisible(nextExtensionsVisible);
|
|
826
791
|
|
|
827
792
|
if (panel === 'addElement') {
|
|
@@ -830,19 +795,21 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
830
795
|
setActiveTool('select');
|
|
831
796
|
}
|
|
832
797
|
|
|
833
|
-
if (nextBcfVisible || nextIdsVisible || nextLensVisible || nextExtensionsVisible || nextAddElementActive) {
|
|
798
|
+
if (nextBcfVisible || nextIdsVisible || nextLensVisible || nextClashVisible || nextExtensionsVisible || nextAddElementActive) {
|
|
834
799
|
setRightPanelCollapsed(false);
|
|
835
800
|
}
|
|
836
801
|
}, [
|
|
837
802
|
activeAnalysisExtension?.placement,
|
|
838
803
|
activeTool,
|
|
839
804
|
bcfPanelVisible,
|
|
805
|
+
clashPanelVisible,
|
|
840
806
|
extensionsPanelVisible,
|
|
841
807
|
idsPanelVisible,
|
|
842
808
|
lensPanelVisible,
|
|
843
809
|
requireDesktopFeature,
|
|
844
810
|
setActiveTool,
|
|
845
811
|
setBcfPanelVisible,
|
|
812
|
+
setClashPanelVisible,
|
|
846
813
|
setExtensionsPanelVisible,
|
|
847
814
|
setIdsPanelVisible,
|
|
848
815
|
setLensPanelVisible,
|
|
@@ -876,6 +843,7 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
876
843
|
setBcfPanelVisible(false);
|
|
877
844
|
setIdsPanelVisible(false);
|
|
878
845
|
setLensPanelVisible(false);
|
|
846
|
+
setClashPanelVisible(false);
|
|
879
847
|
setExtensionsPanelVisible(false);
|
|
880
848
|
// The right slot is single-tenant: when an analysis extension takes
|
|
881
849
|
// it over, the AddElement tool must release it too, otherwise its 3D
|
|
@@ -890,6 +858,7 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
890
858
|
analysisExtensionState.extensions,
|
|
891
859
|
setActiveTool,
|
|
892
860
|
setBcfPanelVisible,
|
|
861
|
+
setClashPanelVisible,
|
|
893
862
|
setExtensionsPanelVisible,
|
|
894
863
|
setGanttPanelVisible,
|
|
895
864
|
setIdsPanelVisible,
|
|
@@ -907,6 +876,7 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
907
876
|
if (bcfPanelVisible) panels.add('bcf');
|
|
908
877
|
if (idsPanelVisible) panels.add('ids');
|
|
909
878
|
if (lensPanelVisible) panels.add('lens');
|
|
879
|
+
if (clashPanelVisible) panels.add('clash');
|
|
910
880
|
if (extensionsPanelVisible) panels.add('extensions');
|
|
911
881
|
if (activeTool === 'addElement') panels.add('addElement');
|
|
912
882
|
if (analysisExtensionState.activeId) panels.add(analysisExtensionState.activeId);
|
|
@@ -915,6 +885,7 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
915
885
|
activeTool,
|
|
916
886
|
analysisExtensionState.activeId,
|
|
917
887
|
bcfPanelVisible,
|
|
888
|
+
clashPanelVisible,
|
|
918
889
|
extensionsPanelVisible,
|
|
919
890
|
ganttPanelVisible,
|
|
920
891
|
idsPanelVisible,
|
|
@@ -932,6 +903,7 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
932
903
|
if (activeWorkspacePanels.has('bcf')) return 'BCF Issues';
|
|
933
904
|
if (activeWorkspacePanels.has('ids')) return 'IDS Validation';
|
|
934
905
|
if (activeWorkspacePanels.has('lens')) return 'Lens Rules';
|
|
906
|
+
if (activeWorkspacePanels.has('clash')) return 'Clash Detection';
|
|
935
907
|
if (activeWorkspacePanels.has('extensions')) return 'Extensions';
|
|
936
908
|
if (activeWorkspacePanels.has('addElement')) return 'Add Element';
|
|
937
909
|
return activeAnalysisExtension?.label ?? 'Analysis';
|
|
@@ -1286,6 +1258,13 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
1286
1258
|
<Palette className="h-4 w-4 mr-2" />
|
|
1287
1259
|
Lens Rules
|
|
1288
1260
|
</DropdownMenuCheckboxItem>
|
|
1261
|
+
<DropdownMenuCheckboxItem
|
|
1262
|
+
checked={activeWorkspacePanels.has('clash')}
|
|
1263
|
+
onCheckedChange={() => handleToggleRightPanel('clash')}
|
|
1264
|
+
>
|
|
1265
|
+
<Crosshair className="h-4 w-4 mr-2" />
|
|
1266
|
+
Clash Detection
|
|
1267
|
+
</DropdownMenuCheckboxItem>
|
|
1289
1268
|
<DropdownMenuSeparator />
|
|
1290
1269
|
<DropdownMenuLabel className="text-[10px] uppercase tracking-wide text-muted-foreground">
|
|
1291
1270
|
Author
|
|
@@ -1521,9 +1500,9 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
1521
1500
|
// Stay enabled even with no model loaded — the dropdown
|
|
1522
1501
|
// also exposes load-time settings (Merge Multilayer
|
|
1523
1502
|
// Walls) that the user should be able to set BEFORE
|
|
1524
|
-
// opening a file.
|
|
1525
|
-
//
|
|
1526
|
-
aria-label={mergeLayers ? '
|
|
1503
|
+
// opening a file. The class toggles are persisted
|
|
1504
|
+
// preferences, so they always render too.
|
|
1505
|
+
aria-label={mergeLayers ? 'Visibility (Merge Multilayer Walls is on)' : 'Visibility'}
|
|
1527
1506
|
className="relative"
|
|
1528
1507
|
>
|
|
1529
1508
|
<Filter className="h-4 w-4" />
|
|
@@ -1540,70 +1519,93 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
1540
1519
|
</DropdownMenuTrigger>
|
|
1541
1520
|
</TooltipTrigger>
|
|
1542
1521
|
<TooltipContent>
|
|
1543
|
-
{mergeLayers ? '
|
|
1522
|
+
{mergeLayers ? 'Visibility · Merge Multilayer Walls is on' : 'Visibility'}
|
|
1544
1523
|
</TooltipContent>
|
|
1545
1524
|
</Tooltip>
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
<
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
>
|
|
1561
|
-
<
|
|
1562
|
-
|
|
1563
|
-
</DropdownMenuCheckboxItem>
|
|
1564
|
-
)}
|
|
1565
|
-
{typeGeometryExists.site && (
|
|
1566
|
-
<DropdownMenuCheckboxItem
|
|
1567
|
-
checked={typeVisibility.site}
|
|
1568
|
-
onCheckedChange={() => toggleTypeVisibility('site')}
|
|
1569
|
-
>
|
|
1570
|
-
<Building2 className="h-4 w-4 mr-2" style={{ color: '#66cc4d' }} />
|
|
1571
|
-
Show Site
|
|
1572
|
-
</DropdownMenuCheckboxItem>
|
|
1573
|
-
)}
|
|
1574
|
-
{hasIfcAnnotations && (
|
|
1575
|
-
<DropdownMenuCheckboxItem
|
|
1576
|
-
checked={typeVisibility.ifcAnnotations}
|
|
1577
|
-
onCheckedChange={() => toggleTypeVisibility('ifcAnnotations')}
|
|
1578
|
-
>
|
|
1579
|
-
<Pencil className="h-4 w-4 mr-2" style={{ color: '#e4b400' }} />
|
|
1580
|
-
Show Annotations & Grids
|
|
1581
|
-
</DropdownMenuCheckboxItem>
|
|
1582
|
-
)}
|
|
1583
|
-
|
|
1584
|
-
{/* Load-time toggles live below the runtime visibility
|
|
1585
|
-
switches — they apply on next model open rather than
|
|
1586
|
-
affecting the current scene. The subheader makes that
|
|
1587
|
-
boundary visible at a glance. */}
|
|
1588
|
-
<DropdownMenuSeparator />
|
|
1589
|
-
<DropdownMenuLabel className="px-2 pt-1 pb-0.5 text-[10px] font-semibold uppercase tracking-wider text-muted-foreground">
|
|
1590
|
-
Load Settings
|
|
1591
|
-
</DropdownMenuLabel>
|
|
1592
|
-
<DropdownMenuCheckboxItem
|
|
1593
|
-
checked={mergeLayers}
|
|
1594
|
-
onCheckedChange={(next) => setMergeLayers(next === true)}
|
|
1595
|
-
// Use items-start so the checkmark and icon line up with
|
|
1596
|
-
// the primary label while the description wraps below.
|
|
1597
|
-
className="items-start gap-2 py-2"
|
|
1598
|
-
>
|
|
1599
|
-
<Layers2 className="h-4 w-4 mr-2 mt-0.5 shrink-0 text-primary" />
|
|
1600
|
-
<div className="flex flex-col gap-0.5 min-w-0">
|
|
1601
|
-
<span className="text-sm font-medium leading-tight">Merge Multilayer Walls</span>
|
|
1602
|
-
<span className="text-[11px] leading-tight text-muted-foreground">
|
|
1603
|
-
Render walls as 1 solid · Applies on reload
|
|
1525
|
+
{/*
|
|
1526
|
+
Settings-style panel (not a list of menu-items): each row is a
|
|
1527
|
+
plain <label> wrapping a right-aligned Switch, so toggling does
|
|
1528
|
+
NOT close the menu — users routinely flip several classes in one
|
|
1529
|
+
pass. State reads two ways: the switch position and the row
|
|
1530
|
+
dimming when off. All five render unconditionally (persisted
|
|
1531
|
+
preferences, sticky across models/reloads); toggling a class the
|
|
1532
|
+
model lacks is a no-op.
|
|
1533
|
+
*/}
|
|
1534
|
+
<DropdownMenuContent align="start" className="w-[300px] p-1.5">
|
|
1535
|
+
<div className="flex items-center justify-between gap-2 px-1.5 pb-1 pt-0.5">
|
|
1536
|
+
<span className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground">
|
|
1537
|
+
Visibility
|
|
1538
|
+
</span>
|
|
1539
|
+
<div className="flex items-center gap-1">
|
|
1540
|
+
<span className="text-[11px] tabular-nums text-muted-foreground/80">
|
|
1541
|
+
{visibleClassCount}/5
|
|
1604
1542
|
</span>
|
|
1543
|
+
<Button
|
|
1544
|
+
variant="ghost"
|
|
1545
|
+
size="sm"
|
|
1546
|
+
className="h-6 px-1.5 text-[11px] font-medium text-muted-foreground hover:text-foreground"
|
|
1547
|
+
onClick={resetTypeVisibility}
|
|
1548
|
+
>
|
|
1549
|
+
Reset
|
|
1550
|
+
</Button>
|
|
1605
1551
|
</div>
|
|
1606
|
-
</
|
|
1552
|
+
</div>
|
|
1553
|
+
|
|
1554
|
+
<ClassVisibilityRow
|
|
1555
|
+
icon={<Box className="h-4 w-4 shrink-0" style={{ color: '#33d9ff' }} />}
|
|
1556
|
+
label="Spaces"
|
|
1557
|
+
description="Room & zone volumes"
|
|
1558
|
+
checked={typeVisibility.spaces}
|
|
1559
|
+
onChange={() => toggleTypeVisibility('spaces')}
|
|
1560
|
+
/>
|
|
1561
|
+
<ClassVisibilityRow
|
|
1562
|
+
icon={<SquareX className="h-4 w-4 shrink-0" style={{ color: '#ff6b4a' }} />}
|
|
1563
|
+
label="Openings"
|
|
1564
|
+
description="Door & window voids"
|
|
1565
|
+
checked={typeVisibility.openings}
|
|
1566
|
+
onChange={() => toggleTypeVisibility('openings')}
|
|
1567
|
+
/>
|
|
1568
|
+
<ClassVisibilityRow
|
|
1569
|
+
icon={<Building2 className="h-4 w-4 shrink-0" style={{ color: '#66cc4d' }} />}
|
|
1570
|
+
label="Site"
|
|
1571
|
+
description="Terrain & context"
|
|
1572
|
+
checked={typeVisibility.site}
|
|
1573
|
+
onChange={() => toggleTypeVisibility('site')}
|
|
1574
|
+
/>
|
|
1575
|
+
<ClassVisibilityRow
|
|
1576
|
+
icon={<Pencil className="h-4 w-4 shrink-0" style={{ color: '#e4b400' }} />}
|
|
1577
|
+
label="Annotations"
|
|
1578
|
+
description="Text, dimensions, leaders"
|
|
1579
|
+
checked={typeVisibility.ifcAnnotations}
|
|
1580
|
+
onChange={() => toggleTypeVisibility('ifcAnnotations')}
|
|
1581
|
+
/>
|
|
1582
|
+
<ClassVisibilityRow
|
|
1583
|
+
icon={<Grid3x3 className="h-4 w-4 shrink-0" style={{ color: '#e4b400' }} />}
|
|
1584
|
+
label="Grids"
|
|
1585
|
+
description="Structural axes"
|
|
1586
|
+
checked={typeVisibility.ifcGrid}
|
|
1587
|
+
onChange={() => toggleTypeVisibility('ifcGrid')}
|
|
1588
|
+
/>
|
|
1589
|
+
|
|
1590
|
+
<DropdownMenuSeparator className="my-1" />
|
|
1591
|
+
|
|
1592
|
+
{/* Merge multilayer walls rebuilds geometry, so unlike the live
|
|
1593
|
+
toggles above it only takes effect on the next model load.
|
|
1594
|
+
The "· on reload" suffix carries that nuance inline — keeps
|
|
1595
|
+
the row identical in shape to the others (no header, no chip
|
|
1596
|
+
crowding the long label). */}
|
|
1597
|
+
<label className="group flex items-center justify-between gap-3 rounded-md px-2 py-1.5 cursor-pointer hover:bg-muted/50 transition-colors">
|
|
1598
|
+
<span className={cn('flex items-center gap-2.5 min-w-0 transition-opacity', !mergeLayers && 'opacity-50')}>
|
|
1599
|
+
<Layers2 className="h-4 w-4 shrink-0 text-primary" />
|
|
1600
|
+
<span className="grid gap-0.5 min-w-0">
|
|
1601
|
+
<span className="text-sm leading-tight truncate">Merge multilayer walls</span>
|
|
1602
|
+
<span className="text-[10px] leading-tight text-muted-foreground truncate">
|
|
1603
|
+
Render walls as one solid · on reload
|
|
1604
|
+
</span>
|
|
1605
|
+
</span>
|
|
1606
|
+
</span>
|
|
1607
|
+
<Switch checked={mergeLayers} onCheckedChange={(next) => setMergeLayers(next === true)} />
|
|
1608
|
+
</label>
|
|
1607
1609
|
</DropdownMenuContent>
|
|
1608
1610
|
</DropdownMenu>
|
|
1609
1611
|
|
|
@@ -28,6 +28,7 @@ import { HoverTooltip } from './HoverTooltip';
|
|
|
28
28
|
import { BCFPanel } from './BCFPanel';
|
|
29
29
|
import { IDSPanel } from './IDSPanel';
|
|
30
30
|
import { LensPanel } from './LensPanel';
|
|
31
|
+
import { ClashPanel } from './ClashPanel';
|
|
31
32
|
import { ListPanel } from './lists/ListPanel';
|
|
32
33
|
import { ScriptPanel } from './ScriptPanel';
|
|
33
34
|
import { GanttPanel } from './schedule/GanttPanel';
|
|
@@ -132,6 +133,8 @@ export function ViewerLayout() {
|
|
|
132
133
|
const setListPanelVisible = useViewerStore((s) => s.setListPanelVisible);
|
|
133
134
|
const lensPanelVisible = useViewerStore((s) => s.lensPanelVisible);
|
|
134
135
|
const setLensPanelVisible = useViewerStore((s) => s.setLensPanelVisible);
|
|
136
|
+
const clashPanelVisible = useViewerStore((s) => s.clashPanelVisible);
|
|
137
|
+
const setClashPanelVisible = useViewerStore((s) => s.setClashPanelVisible);
|
|
135
138
|
const scriptPanelVisible = useViewerStore((s) => s.scriptPanelVisible);
|
|
136
139
|
const setScriptPanelVisible = useViewerStore((s) => s.setScriptPanelVisible);
|
|
137
140
|
const ganttPanelVisible = useViewerStore((s) => s.ganttPanelVisible);
|
|
@@ -342,6 +345,8 @@ export function ViewerLayout() {
|
|
|
342
345
|
<AddElementPanel onClose={() => setActiveTool('select')} />
|
|
343
346
|
) : lensPanelVisible ? (
|
|
344
347
|
<LensPanel onClose={() => setLensPanelVisible(false)} />
|
|
348
|
+
) : clashPanelVisible ? (
|
|
349
|
+
<ClashPanel onClose={() => setClashPanelVisible(false)} />
|
|
345
350
|
) : idsPanelVisible ? (
|
|
346
351
|
<IDSPanel onClose={() => setIdsPanelVisible(false)} />
|
|
347
352
|
) : bcfPanelVisible ? (
|
|
@@ -40,7 +40,12 @@ import { useGeometryStreaming } from './useGeometryStreaming.js';
|
|
|
40
40
|
import { usePointCloudSync } from './usePointCloudSync.js';
|
|
41
41
|
import { usePointCloudLifecycle } from './usePointCloudLifecycle.js';
|
|
42
42
|
import { useRenderUpdates } from './useRenderUpdates.js';
|
|
43
|
-
import {
|
|
43
|
+
import {
|
|
44
|
+
useSymbolicAnnotations,
|
|
45
|
+
useSymbolicAnnotationsRichData,
|
|
46
|
+
type SectionClipForGrid,
|
|
47
|
+
} from '../../hooks/useSymbolicAnnotations.js';
|
|
48
|
+
import { useAlignmentLines3D } from '../../hooks/useAlignmentLines3D.js';
|
|
44
49
|
|
|
45
50
|
interface ViewportProps {
|
|
46
51
|
geometry: MeshData[] | null;
|
|
@@ -622,8 +627,19 @@ export function Viewport({
|
|
|
622
627
|
calculateScale();
|
|
623
628
|
},
|
|
624
629
|
home: () => {
|
|
625
|
-
//
|
|
626
|
-
|
|
630
|
+
// Adaptive home: compact buildings get the historical SE isometric
|
|
631
|
+
// pose (1:1 with the old behaviour), linear infrastructure gets a
|
|
632
|
+
// side-on view at a distance where signals / referents are visible
|
|
633
|
+
// instead of receding to sub-pixel. The policy is computed from
|
|
634
|
+
// the current bbox shape so a federation that swaps from one
|
|
635
|
+
// building to a railway picks the right pose on Home press.
|
|
636
|
+
// See packages/renderer/src/camera-fit-policy.ts.
|
|
637
|
+
const canvas = rendererRef.current?.getCanvas();
|
|
638
|
+
const canvasShort = Math.min(canvas?.height ?? 0, canvas?.width ?? 0);
|
|
639
|
+
camera.fitBoundsAdaptive(
|
|
640
|
+
{ min: geometryBoundsRef.current.min, max: geometryBoundsRef.current.max },
|
|
641
|
+
{ animate: true, duration: 500, viewportShortPx: canvasShort > 0 ? canvasShort : undefined },
|
|
642
|
+
);
|
|
627
643
|
calculateScale();
|
|
628
644
|
},
|
|
629
645
|
zoomIn: () => {
|
|
@@ -637,19 +653,43 @@ export function Viewport({
|
|
|
637
653
|
calculateScale();
|
|
638
654
|
},
|
|
639
655
|
frameSelection: () => {
|
|
640
|
-
// Frame selection
|
|
641
|
-
|
|
656
|
+
// Frame the current selection. Prefer the full multi-selection set
|
|
657
|
+
// (Ctrl-click, box-select, a clash pair) so the camera encloses EVERY
|
|
658
|
+
// selected element; fall back to the single primary id. The set is
|
|
659
|
+
// kept in sync with selection (cleared on a plain click), so the
|
|
660
|
+
// union is always an accurate frame of what's highlighted.
|
|
642
661
|
const geom = geometryRef.current;
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
662
|
+
const set = selectedEntityIdsRef.current;
|
|
663
|
+
const single = selectedEntityIdRef.current;
|
|
664
|
+
const ids = set && set.size > 0
|
|
665
|
+
? Array.from(set)
|
|
666
|
+
: single !== null ? [single] : [];
|
|
667
|
+
if (!geom || ids.length === 0) {
|
|
668
|
+
console.warn('[Viewport] frameSelection: No selection or geometry');
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
let min: { x: number; y: number; z: number } | null = null;
|
|
672
|
+
let max: { x: number; y: number; z: number } | null = null;
|
|
673
|
+
for (const id of ids) {
|
|
674
|
+
const b = getEntityBounds(geom, id);
|
|
675
|
+
if (!b) continue;
|
|
676
|
+
if (!min || !max) {
|
|
677
|
+
min = { x: b.min.x, y: b.min.y, z: b.min.z };
|
|
678
|
+
max = { x: b.max.x, y: b.max.y, z: b.max.z };
|
|
648
679
|
} else {
|
|
649
|
-
|
|
680
|
+
min.x = Math.min(min.x, b.min.x);
|
|
681
|
+
min.y = Math.min(min.y, b.min.y);
|
|
682
|
+
min.z = Math.min(min.z, b.min.z);
|
|
683
|
+
max.x = Math.max(max.x, b.max.x);
|
|
684
|
+
max.y = Math.max(max.y, b.max.y);
|
|
685
|
+
max.z = Math.max(max.z, b.max.z);
|
|
650
686
|
}
|
|
687
|
+
}
|
|
688
|
+
if (min && max) {
|
|
689
|
+
camera.frameBounds(min, max, 300);
|
|
690
|
+
calculateScale();
|
|
651
691
|
} else {
|
|
652
|
-
console.warn('[Viewport] frameSelection:
|
|
692
|
+
console.warn('[Viewport] frameSelection: Could not get bounds for selected element');
|
|
653
693
|
}
|
|
654
694
|
},
|
|
655
695
|
orbit: (deltaX: number, deltaY: number) => {
|
|
@@ -787,6 +827,11 @@ export function Viewport({
|
|
|
787
827
|
// storey model shows all storeys' annotations layered correctly in 3D
|
|
788
828
|
// (issue #653). Parsing is lazy and only runs while the toggle is on.
|
|
789
829
|
const ifcAnnotationsVisible = useViewerStore((s) => s.typeVisibility.ifcAnnotations);
|
|
830
|
+
// Issue #862: IfcGrid is a separate toggle from IfcAnnotation. Default
|
|
831
|
+
// is on so existing users see no change; when the user disables it the
|
|
832
|
+
// grid axes + bubble tags drop out without affecting dimension/leader
|
|
833
|
+
// annotation rendering.
|
|
834
|
+
const ifcGridVisible = useViewerStore((s) => s.typeVisibility.ifcGrid);
|
|
790
835
|
// For annotations whose storey can't be resolved (or whose authored
|
|
791
836
|
// elevation is 0 because the storey Z lives on the placement instead),
|
|
792
837
|
// lift to the middle of the model's vertical span so they don't end up
|
|
@@ -799,12 +844,37 @@ export function Viewport({
|
|
|
799
844
|
if (!Number.isFinite(min) || !Number.isFinite(max) || max <= min) return 0;
|
|
800
845
|
return (min + max) * 0.5;
|
|
801
846
|
}, [coordinateInfo]);
|
|
847
|
+
|
|
848
|
+
// Issue #862: section-clip grid lines so dense-grid models stay
|
|
849
|
+
// readable when a horizontal cut is active. Use a 1.5 m band on each
|
|
850
|
+
// side of the cut so the cut storey's grids are visible but storeys
|
|
851
|
+
// 1.5 m+ away are hidden (matches typical residential floor heights).
|
|
852
|
+
// Only applies to the floor-plan axis (`'down'`) — vertical cuts
|
|
853
|
+
// don't clip grids since grid lines are inherently vertical.
|
|
854
|
+
const gridSectionClip = useMemo<SectionClipForGrid | undefined>(() => {
|
|
855
|
+
if (!sectionPlane.enabled || sectionPlane.axis !== 'down' || !sectionRange) {
|
|
856
|
+
return undefined;
|
|
857
|
+
}
|
|
858
|
+
const posWorld = sectionRange.min + (sectionPlane.position / 100) * (sectionRange.max - sectionRange.min);
|
|
859
|
+
const GRID_CLIP_HALF_BAND_M = 1.5;
|
|
860
|
+
return {
|
|
861
|
+
enabled: true,
|
|
862
|
+
posWorld,
|
|
863
|
+
viewDepth: GRID_CLIP_HALF_BAND_M,
|
|
864
|
+
axis: sectionPlane.axis,
|
|
865
|
+
};
|
|
866
|
+
}, [sectionPlane.enabled, sectionPlane.axis, sectionPlane.position, sectionRange]);
|
|
867
|
+
|
|
802
868
|
const annotationVertices3D = useSymbolicAnnotations({
|
|
803
869
|
enabled: ifcAnnotationsVisible,
|
|
870
|
+
gridEnabled: ifcGridVisible,
|
|
871
|
+
gridSectionClip,
|
|
804
872
|
fallbackY: annotationFallbackY,
|
|
805
873
|
});
|
|
806
874
|
const { texts: annotationTexts3D, fills: annotationFills3D } = useSymbolicAnnotationsRichData({
|
|
807
875
|
enabled: ifcAnnotationsVisible,
|
|
876
|
+
gridEnabled: ifcGridVisible,
|
|
877
|
+
gridSectionClip,
|
|
808
878
|
fallbackY: annotationFallbackY,
|
|
809
879
|
});
|
|
810
880
|
useEffect(() => {
|
|
@@ -817,6 +887,20 @@ export function Viewport({
|
|
|
817
887
|
}
|
|
818
888
|
}, [annotationVertices3D, isInitialized]);
|
|
819
889
|
|
|
890
|
+
// IfcAlignment centerlines render as thin lines (not a ribbon mesh), always
|
|
891
|
+
// on — see useAlignmentLines3D. Upload/clear mirrors the annotation overlay;
|
|
892
|
+
// a separate renderer buffer keeps alignment visibility independent.
|
|
893
|
+
const alignmentVertices3D = useAlignmentLines3D();
|
|
894
|
+
useEffect(() => {
|
|
895
|
+
const renderer = rendererRef.current;
|
|
896
|
+
if (!renderer || !isInitialized) return;
|
|
897
|
+
if (alignmentVertices3D.length === 0) {
|
|
898
|
+
renderer.clearAlignmentLines3D();
|
|
899
|
+
} else {
|
|
900
|
+
renderer.uploadAlignmentLines3D(alignmentVertices3D);
|
|
901
|
+
}
|
|
902
|
+
}, [alignmentVertices3D, isInitialized]);
|
|
903
|
+
|
|
820
904
|
// Upload IfcAnnotation text + fill data for the WebGPU symbolic overlay
|
|
821
905
|
// pipelines. Map the hook's per-annotation records into the SymbolicFillInput
|
|
822
906
|
// / SymbolicTextInput shape the renderer expects. Empty arrays clear cleanly.
|
|
@@ -1011,6 +1095,7 @@ export function Viewport({
|
|
|
1011
1095
|
geometryContentVersion,
|
|
1012
1096
|
coordinateInfo,
|
|
1013
1097
|
isStreaming,
|
|
1098
|
+
modelCount: modelIdToIndex?.size ?? 0,
|
|
1014
1099
|
geometryBoundsRef,
|
|
1015
1100
|
pendingColorUpdates,
|
|
1016
1101
|
pendingMeshColorUpdates,
|