@ifc-lite/viewer 1.17.6 → 1.19.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 +20 -15
- package/.turbo/turbo-typecheck.log +1 -1
- package/CHANGELOG.md +949 -0
- package/dist/assets/{basketViewActivator-86rgogji.js → basketViewActivator-RZy5c3Td.js} +1 -1
- package/dist/assets/decode-worker-Collf_X_.js +1320 -0
- package/dist/assets/{exporters-CcPS9MK5.js → exporters-BraHBeoi.js} +4194 -3025
- package/dist/assets/{geometry.worker-BFUYA08u.js → geometry.worker-DQEZB2rB.js} +1 -1
- package/dist/assets/ifc-lite_bg-4yUkDRD8.wasm +0 -0
- package/dist/assets/index-0XpVr_S5.css +1 -0
- package/dist/assets/{index-Bfms9I4A.js → index-BOi3BuUI.js} +46423 -31181
- package/dist/assets/index-XwKzDuw6.js +22 -0
- package/dist/assets/{native-bridge-DUyLCMZS.js → native-bridge-CpBeOPQa.js} +1 -1
- package/dist/assets/sandbox-Baez7n-t.js +9682 -0
- package/dist/assets/{server-client-BuZK7OST.js → server-client-BB6cMAXE.js} +1 -1
- package/dist/assets/{wasm-bridge-JsqEGDV8.js → wasm-bridge-CAYCUHbE.js} +1 -1
- package/dist/index.html +6 -6
- package/package.json +11 -10
- package/src/apache-arrow.d.ts +30 -0
- package/src/components/viewer/AddElementPanel.tsx +758 -0
- package/src/components/viewer/BulkPropertyEditor.tsx +7 -0
- package/src/components/viewer/ChatPanel.tsx +64 -2
- package/src/components/viewer/CommandPalette.tsx +56 -7
- package/src/components/viewer/EntityContextMenu.tsx +168 -4
- package/src/components/viewer/ExportChangesButton.tsx +25 -5
- package/src/components/viewer/ExportDialog.tsx +19 -1
- package/src/components/viewer/MainToolbar.tsx +73 -12
- package/src/components/viewer/PointCloudPanel.tsx +174 -0
- package/src/components/viewer/PropertiesPanel.tsx +222 -22
- package/src/components/viewer/SearchInline.tsx +669 -0
- package/src/components/viewer/SearchModal.filter.builder.tsx +766 -0
- package/src/components/viewer/SearchModal.filter.tsx +514 -0
- package/src/components/viewer/SearchModal.text.tsx +388 -0
- package/src/components/viewer/SearchModal.tsx +235 -0
- package/src/components/viewer/ToolOverlays.tsx +5 -0
- package/src/components/viewer/ViewerLayout.tsx +24 -4
- package/src/components/viewer/Viewport.tsx +29 -2
- package/src/components/viewer/ViewportContainer.tsx +45 -5
- package/src/components/viewer/ViewportOverlays.tsx +13 -2
- package/src/components/viewer/annotations/AnnotationDropInput.tsx +203 -0
- package/src/components/viewer/annotations/AnnotationLayer.tsx +287 -0
- package/src/components/viewer/annotations/AnnotationPin.tsx +90 -0
- package/src/components/viewer/annotations/AnnotationPopover.tsx +296 -0
- package/src/components/viewer/bcf/BCFTopicDetail.tsx +1 -1
- package/src/components/viewer/lists/ListPanel.tsx +14 -21
- package/src/components/viewer/properties/RawStepCard.tsx +332 -0
- package/src/components/viewer/properties/RawStepRow.tsx +261 -0
- package/src/components/viewer/properties/ScheduleCard.tsx +224 -0
- package/src/components/viewer/properties/TaskEditCard.tsx +510 -0
- package/src/components/viewer/properties/raw-step-format.ts +193 -0
- package/src/components/viewer/schedule/AnimationSettingsPopover.tsx +542 -0
- package/src/components/viewer/schedule/GanttDependencyArrows.tsx +89 -0
- package/src/components/viewer/schedule/GanttDragTooltip.tsx +48 -0
- package/src/components/viewer/schedule/GanttEmptyState.tsx +97 -0
- package/src/components/viewer/schedule/GanttPanel.tsx +295 -0
- package/src/components/viewer/schedule/GanttTaskBar.tsx +199 -0
- package/src/components/viewer/schedule/GanttTaskTree.tsx +250 -0
- package/src/components/viewer/schedule/GanttTimeline.tsx +305 -0
- package/src/components/viewer/schedule/GanttToolbar.tsx +406 -0
- package/src/components/viewer/schedule/GenerateAdvancedPanel.tsx +147 -0
- package/src/components/viewer/schedule/GenerateScheduleDialog.tsx +392 -0
- package/src/components/viewer/schedule/HeightStrategyPanel.tsx +120 -0
- package/src/components/viewer/schedule/generate-schedule.test.ts +439 -0
- package/src/components/viewer/schedule/generate-schedule.ts +648 -0
- package/src/components/viewer/schedule/schedule-animator.test.ts +452 -0
- package/src/components/viewer/schedule/schedule-animator.ts +488 -0
- package/src/components/viewer/schedule/schedule-selection.test.ts +148 -0
- package/src/components/viewer/schedule/schedule-selection.ts +163 -0
- package/src/components/viewer/schedule/schedule-utils.ts +223 -0
- package/src/components/viewer/schedule/useConstructionSequence.ts +156 -0
- package/src/components/viewer/schedule/useGanttBarDrag.test.ts +90 -0
- package/src/components/viewer/schedule/useGanttBarDrag.ts +305 -0
- package/src/components/viewer/schedule/useGanttSelection3DHighlight.ts +152 -0
- package/src/components/viewer/schedule/useOverlayCompositor.ts +108 -0
- package/src/components/viewer/selectionHandlers.ts +446 -0
- package/src/components/viewer/tools/AddElementOverlay.tsx +581 -0
- package/src/components/viewer/useDuplicateShortcut.ts +77 -0
- package/src/components/viewer/useMouseControls.ts +9 -1
- package/src/components/viewer/usePointCloudLifecycle.ts +64 -0
- package/src/components/viewer/usePointCloudSync.ts +98 -0
- package/src/hooks/ingest/pointCloudIngest.ts +391 -0
- package/src/hooks/ingest/viewerModelIngest.ts +32 -3
- package/src/hooks/useIfcFederation.ts +72 -3
- package/src/hooks/useIfcLoader.ts +89 -13
- package/src/hooks/useKeyboardShortcuts.ts +25 -0
- package/src/hooks/useSandbox.ts +1 -1
- package/src/hooks/useSearchIndex.ts +125 -0
- package/src/index.css +66 -0
- package/src/lib/llm/system-prompt.test.ts +14 -0
- package/src/lib/llm/system-prompt.ts +102 -1
- package/src/lib/llm/types.ts +6 -0
- package/src/lib/recent-files.ts +38 -4
- package/src/lib/scripts/templates/bim-globals.d.ts +136 -114
- package/src/lib/scripts/templates/construction-schedule.ts +223 -0
- package/src/lib/scripts/templates.ts +7 -0
- package/src/lib/search/common-ifc-types.ts +36 -0
- package/src/lib/search/filter-evaluate.test.ts +537 -0
- package/src/lib/search/filter-evaluate.ts +610 -0
- package/src/lib/search/filter-rules.test.ts +119 -0
- package/src/lib/search/filter-rules.ts +198 -0
- package/src/lib/search/filter-schema.test.ts +233 -0
- package/src/lib/search/filter-schema.ts +146 -0
- package/src/lib/search/recent-searches.test.ts +116 -0
- package/src/lib/search/recent-searches.ts +93 -0
- package/src/lib/search/result-export.test.ts +101 -0
- package/src/lib/search/result-export.ts +104 -0
- package/src/lib/search/saved-filters.test.ts +118 -0
- package/src/lib/search/saved-filters.ts +154 -0
- package/src/lib/search/tier0-scan.test.ts +196 -0
- package/src/lib/search/tier0-scan.ts +237 -0
- package/src/lib/search/tier1-index.test.ts +242 -0
- package/src/lib/search/tier1-index.ts +448 -0
- package/src/sdk/adapters/export-adapter.test.ts +434 -1
- package/src/sdk/adapters/export-adapter.ts +404 -1
- package/src/sdk/adapters/export-schedule-splice.test.ts +127 -0
- package/src/sdk/adapters/export-schedule-splice.ts +87 -0
- package/src/sdk/adapters/model-compat.ts +8 -2
- package/src/sdk/adapters/schedule-adapter.ts +73 -0
- package/src/sdk/adapters/store-adapter.ts +201 -0
- package/src/sdk/adapters/visibility-adapter.ts +3 -0
- package/src/sdk/local-backend.ts +16 -8
- package/src/services/desktop-export.ts +3 -1
- package/src/services/desktop-native-metadata.ts +41 -18
- package/src/services/file-dialog.ts +8 -3
- package/src/services/tauri-modules.d.ts +25 -0
- package/src/store/basketVisibleSet.ts +3 -0
- package/src/store/globalId.ts +4 -1
- package/src/store/index.ts +79 -1
- package/src/store/slices/addElementMeshes.ts +365 -0
- package/src/store/slices/addElementSlice.ts +275 -0
- package/src/store/slices/annotationsSlice.test.ts +133 -0
- package/src/store/slices/annotationsSlice.ts +251 -0
- package/src/store/slices/dataSlice.test.ts +23 -4
- package/src/store/slices/dataSlice.ts +1 -1
- package/src/store/slices/modelSlice.test.ts +67 -9
- package/src/store/slices/modelSlice.ts +39 -7
- package/src/store/slices/mutationSlice.ts +964 -3
- package/src/store/slices/overlayCompositor.test.ts +164 -0
- package/src/store/slices/overlaySlice.test.ts +93 -0
- package/src/store/slices/overlaySlice.ts +151 -0
- package/src/store/slices/pinboardSlice.test.ts +6 -1
- package/src/store/slices/playbackSlice.ts +128 -0
- package/src/store/slices/pointCloudSlice.ts +102 -0
- package/src/store/slices/schedule-edit-helpers.test.ts +97 -0
- package/src/store/slices/schedule-edit-helpers.ts +179 -0
- package/src/store/slices/scheduleSlice.test.ts +694 -0
- package/src/store/slices/scheduleSlice.ts +1330 -0
- package/src/store/slices/searchSlice.test.ts +342 -0
- package/src/store/slices/searchSlice.ts +341 -0
- package/src/store/slices/selectionSlice.test.ts +46 -0
- package/src/store/slices/selectionSlice.ts +20 -0
- package/src/store/types.ts +7 -0
- package/src/store.ts +14 -0
- package/vite.config.ts +1 -0
- package/dist/assets/ifc-lite_bg-BINvzoCP.wasm +0 -0
- package/dist/assets/index-_bfZsDCC.css +0 -1
- package/dist/assets/sandbox-C8575tul.js +0 -5951
|
@@ -0,0 +1,64 @@
|
|
|
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
|
+
* Tear down streamed point cloud GPU resources when a model is removed.
|
|
7
|
+
*
|
|
8
|
+
* Streamed assets (LAS/LAZ) live in a separate ownership bucket on the
|
|
9
|
+
* renderer (see `PointCloudRenderer`'s `'streamed'` owner tag), so they
|
|
10
|
+
* survive `setPointClouds` calls. That isolation cuts both ways: nothing
|
|
11
|
+
* else clears them, so when a model is removed we have to do it here or
|
|
12
|
+
* the GPU buffers leak for the rest of the session.
|
|
13
|
+
*
|
|
14
|
+
* The hook tracks the previous set of `(modelId → handleId)` pairs and,
|
|
15
|
+
* on every store change, frees the handles for models that disappeared.
|
|
16
|
+
* Pure cleanup — no state mutation.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { useEffect, useRef, type MutableRefObject } from 'react';
|
|
20
|
+
import type { Renderer } from '@ifc-lite/renderer';
|
|
21
|
+
import { useViewerStore } from '@/store';
|
|
22
|
+
|
|
23
|
+
export interface UsePointCloudLifecycleParams {
|
|
24
|
+
rendererRef: MutableRefObject<Renderer | null>;
|
|
25
|
+
isInitialized: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function usePointCloudLifecycle(params: UsePointCloudLifecycleParams): void {
|
|
29
|
+
const { rendererRef, isInitialized } = params;
|
|
30
|
+
const models = useViewerStore((s) => s.models);
|
|
31
|
+
const decCount = useViewerStore((s) => s.incrementPointCloudAssetCount);
|
|
32
|
+
const previousRef = useRef<Map<string, number>>(new Map());
|
|
33
|
+
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
if (!isInitialized) return;
|
|
36
|
+
const renderer = rendererRef.current;
|
|
37
|
+
if (!renderer) return;
|
|
38
|
+
|
|
39
|
+
const current = new Map<string, number>();
|
|
40
|
+
for (const [modelId, model] of models) {
|
|
41
|
+
if (typeof model.pointCloudHandleId === 'number') {
|
|
42
|
+
current.set(modelId, model.pointCloudHandleId);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Dispose handles whose model disappeared OR whose model still
|
|
47
|
+
// exists but was rebound to a new handle (e.g. the user reloaded
|
|
48
|
+
// the same file and got a fresh streaming session). Without the
|
|
49
|
+
// rebind branch the old GPU buffers stay allocated for the rest
|
|
50
|
+
// of the session.
|
|
51
|
+
for (const [modelId, handleId] of previousRef.current) {
|
|
52
|
+
const nextHandle = current.get(modelId);
|
|
53
|
+
if (nextHandle !== handleId) {
|
|
54
|
+
renderer.removePointCloudAsset({ id: handleId });
|
|
55
|
+
decCount(-1);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
previousRef.current = current;
|
|
60
|
+
renderer.requestRender();
|
|
61
|
+
}, [models, isInitialized, rendererRef, decCount]);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export default usePointCloudLifecycle;
|
|
@@ -0,0 +1,98 @@
|
|
|
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
|
+
* Sync IFCx-derived point cloud assets to the renderer.
|
|
7
|
+
*
|
|
8
|
+
* On every change of the `pointClouds` array we replace the renderer's
|
|
9
|
+
* asset list and request a fresh frame. When the active scene has no
|
|
10
|
+
* triangle meshes (the buildingSMART point-cloud-only samples), we
|
|
11
|
+
* additionally trigger a one-shot camera fit — the geometry streaming
|
|
12
|
+
* hook bails out early in that case and would otherwise leave points
|
|
13
|
+
* stranded outside the camera frustum.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { useEffect, useRef, type MutableRefObject } from 'react';
|
|
17
|
+
import type { PointColorMode, PointSizeMode, Renderer } from '@ifc-lite/renderer';
|
|
18
|
+
import type { PointCloudAsset } from '@ifc-lite/geometry';
|
|
19
|
+
import { useViewerStore } from '@/store';
|
|
20
|
+
|
|
21
|
+
export interface UsePointCloudSyncParams {
|
|
22
|
+
rendererRef: MutableRefObject<Renderer | null>;
|
|
23
|
+
isInitialized: boolean;
|
|
24
|
+
pointClouds: ReadonlyArray<PointCloudAsset> | null | undefined;
|
|
25
|
+
/** True when the scene has triangle meshes — the geometry streaming
|
|
26
|
+
* hook owns fit-to-view in that case and we shouldn't fight it. */
|
|
27
|
+
hasMeshes: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function usePointCloudSync(params: UsePointCloudSyncParams): void {
|
|
31
|
+
const { rendererRef, isInitialized, pointClouds, hasMeshes } = params;
|
|
32
|
+
const colorMode = useViewerStore((s) => s.pointCloudColorMode) as PointColorMode;
|
|
33
|
+
const fixedColor = useViewerStore((s) => s.pointCloudFixedColor);
|
|
34
|
+
const sizeMode = useViewerStore((s) => s.pointCloudSizeMode) as PointSizeMode;
|
|
35
|
+
const pointSize = useViewerStore((s) => s.pointCloudPointSize);
|
|
36
|
+
const worldRadius = useViewerStore((s) => s.pointCloudWorldRadius);
|
|
37
|
+
const roundShape = useViewerStore((s) => s.pointCloudRoundShape);
|
|
38
|
+
const edlEnabled = useViewerStore((s) => s.pointCloudEdlEnabled);
|
|
39
|
+
const edlStrength = useViewerStore((s) => s.pointCloudEdlStrength);
|
|
40
|
+
const setAssetCount = useViewerStore((s) => s.setPointCloudAssetCount);
|
|
41
|
+
const fittedRef = useRef(false);
|
|
42
|
+
|
|
43
|
+
// Reset the one-shot fit flag whenever the asset list identity changes.
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
fittedRef.current = false;
|
|
46
|
+
}, [pointClouds]);
|
|
47
|
+
|
|
48
|
+
// Replace IFCx-owned assets when the merged list changes
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
const renderer = rendererRef.current;
|
|
51
|
+
if (!renderer || !isInitialized) return;
|
|
52
|
+
|
|
53
|
+
const assets = pointClouds ?? [];
|
|
54
|
+
renderer.setPointClouds(assets);
|
|
55
|
+
const count = renderer.getPointCloudAssetCount();
|
|
56
|
+
setAssetCount(count);
|
|
57
|
+
|
|
58
|
+
// Camera fit for points-only scenes — useGeometryStreaming skips its
|
|
59
|
+
// own fit branch when meshes is empty, so points stay off-screen
|
|
60
|
+
// unless we step in. Run once per fresh asset list.
|
|
61
|
+
if (count > 0 && !hasMeshes && !fittedRef.current) {
|
|
62
|
+
const bounds = renderer.getModelBounds();
|
|
63
|
+
if (bounds && Number.isFinite(bounds.min.x) && Number.isFinite(bounds.max.x)) {
|
|
64
|
+
renderer.getCamera().fitToBounds(bounds.min, bounds.max);
|
|
65
|
+
fittedRef.current = true;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
renderer.requestRender();
|
|
70
|
+
}, [pointClouds, isInitialized, rendererRef, setAssetCount, hasMeshes]);
|
|
71
|
+
|
|
72
|
+
// Push color + sizing + shape preferences to the renderer whenever the
|
|
73
|
+
// user changes them. The slice already clamps numeric ranges so the
|
|
74
|
+
// shader only ever sees sane values.
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
const renderer = rendererRef.current;
|
|
77
|
+
if (!renderer || !isInitialized) return;
|
|
78
|
+
renderer.setPointCloudOptions({
|
|
79
|
+
colorMode,
|
|
80
|
+
fixedColor,
|
|
81
|
+
sizeMode,
|
|
82
|
+
pointSize,
|
|
83
|
+
worldRadius,
|
|
84
|
+
roundShape,
|
|
85
|
+
});
|
|
86
|
+
renderer.requestRender();
|
|
87
|
+
}, [colorMode, fixedColor, sizeMode, pointSize, worldRadius, roundShape, isInitialized, rendererRef]);
|
|
88
|
+
|
|
89
|
+
// Push EDL toggle + strength to the renderer.
|
|
90
|
+
useEffect(() => {
|
|
91
|
+
const renderer = rendererRef.current;
|
|
92
|
+
if (!renderer || !isInitialized) return;
|
|
93
|
+
renderer.setEdlOptions({ enabled: edlEnabled, strength: edlStrength });
|
|
94
|
+
renderer.requestRender();
|
|
95
|
+
}, [edlEnabled, edlStrength, isInitialized, rendererRef]);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export default usePointCloudSync;
|
|
@@ -0,0 +1,391 @@
|
|
|
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
|
+
* LAS / LAZ ingest path for the viewer.
|
|
7
|
+
*
|
|
8
|
+
* Streams a Blob through `@ifc-lite/pointcloud`'s decode worker and
|
|
9
|
+
* pushes chunks directly into the renderer via the streaming API. The
|
|
10
|
+
* federated model entry carries no per-chunk data — it only holds the
|
|
11
|
+
* renderer handle, summary metadata, and bbox so removeModel can free
|
|
12
|
+
* the GPU resources cleanly.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { Renderer } from '@ifc-lite/renderer';
|
|
16
|
+
import {
|
|
17
|
+
streamPointCloud,
|
|
18
|
+
type DecodedPointChunk,
|
|
19
|
+
type StreamHandle,
|
|
20
|
+
} from '@ifc-lite/pointcloud';
|
|
21
|
+
import type { CoordinateInfo, GeometryResult, PointCloudAsset } from '@ifc-lite/geometry';
|
|
22
|
+
import type { IfcDataStore } from '@ifc-lite/parser';
|
|
23
|
+
import type { SchemaVersion } from '../../store/types.js';
|
|
24
|
+
import { createCoordinateInfo } from '../../utils/localParsingUtils.js';
|
|
25
|
+
|
|
26
|
+
export type PointCloudFormat = 'las' | 'laz' | 'ply' | 'pcd' | 'e57';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* IfcTypeEnum.IfcGeographicElement — the closest IFC4 entity for a scan
|
|
30
|
+
* is `IfcGeographicElement`. We hard-code the enum value (58) here so
|
|
31
|
+
* we don't pull `@ifc-lite/data` into the viewer ingest path.
|
|
32
|
+
*/
|
|
33
|
+
const IFC_GEOGRAPHIC_ELEMENT_ENUM = 58;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Synthetic IfcDataStore for a point-cloud-only model. Picking a point
|
|
37
|
+
* sets the synthetic expressId as the selected entity, which then runs
|
|
38
|
+
* through the regular property/hover/properties-panel pipeline. That
|
|
39
|
+
* pipeline calls `entities.getTypeName / getName / getGlobalId` and
|
|
40
|
+
* `properties.getForEntity` — without those methods, picking crashes
|
|
41
|
+
* with "getTypeName is not a function". We give it just enough shape
|
|
42
|
+
* to round-trip the single synthetic entity.
|
|
43
|
+
*/
|
|
44
|
+
function emptyDataStore(
|
|
45
|
+
buffer: ArrayBuffer,
|
|
46
|
+
expressId: number,
|
|
47
|
+
fileName: string,
|
|
48
|
+
): IfcDataStore {
|
|
49
|
+
const expressIds = new Uint32Array([expressId]);
|
|
50
|
+
const empty32 = new Uint32Array(0);
|
|
51
|
+
const empty8 = new Uint8Array(0);
|
|
52
|
+
const emptyI32 = new Int32Array(0);
|
|
53
|
+
const indexOf = (id: number) => (id === expressId ? 0 : -1);
|
|
54
|
+
const entities = {
|
|
55
|
+
count: 1,
|
|
56
|
+
expressId: expressIds,
|
|
57
|
+
typeEnum: new Uint16Array([IFC_GEOGRAPHIC_ELEMENT_ENUM]),
|
|
58
|
+
globalId: empty32,
|
|
59
|
+
name: empty32,
|
|
60
|
+
description: empty32,
|
|
61
|
+
objectType: empty32,
|
|
62
|
+
flags: new Uint8Array([0]),
|
|
63
|
+
containedInStorey: new Int32Array([-1]),
|
|
64
|
+
definedByType: new Int32Array([-1]),
|
|
65
|
+
geometryIndex: new Int32Array([-1]),
|
|
66
|
+
typeRanges: new Map(),
|
|
67
|
+
getGlobalId: (id: number) => (indexOf(id) >= 0 ? `pointcloud-${expressId}` : ''),
|
|
68
|
+
getName: (id: number) => (indexOf(id) >= 0 ? fileName : ''),
|
|
69
|
+
getDescription: () => '',
|
|
70
|
+
getObjectType: () => '',
|
|
71
|
+
getTypeName: (id: number) => (indexOf(id) >= 0 ? 'IfcGeographicElement' : 'Unknown'),
|
|
72
|
+
hasGeometry: (id: number) => indexOf(id) >= 0,
|
|
73
|
+
getByType: () => [expressId],
|
|
74
|
+
getTypeEnum: (id: number) =>
|
|
75
|
+
indexOf(id) >= 0 ? IFC_GEOGRAPHIC_ELEMENT_ENUM : 9999, // 9999 = Unknown
|
|
76
|
+
getExpressIdByGlobalId: (gid: string) =>
|
|
77
|
+
gid === `pointcloud-${expressId}` ? expressId : -1,
|
|
78
|
+
getGlobalIdMap: () => new Map([[`pointcloud-${expressId}`, expressId]]),
|
|
79
|
+
};
|
|
80
|
+
const properties = {
|
|
81
|
+
count: 0,
|
|
82
|
+
entityId: empty32, psetName: empty32, psetGlobalId: empty32,
|
|
83
|
+
propName: empty32, propType: empty8,
|
|
84
|
+
valueString: empty32, valueReal: new Float64Array(0),
|
|
85
|
+
valueInt: emptyI32, valueBool: empty8, unitId: emptyI32,
|
|
86
|
+
entityIndex: new Map<number, number[]>(),
|
|
87
|
+
psetIndex: new Map<number, number[]>(),
|
|
88
|
+
propIndex: new Map<number, number[]>(),
|
|
89
|
+
getForEntity: () => [],
|
|
90
|
+
getPropertyValue: () => null,
|
|
91
|
+
findByProperty: () => [],
|
|
92
|
+
};
|
|
93
|
+
const quantities = {
|
|
94
|
+
count: 0,
|
|
95
|
+
entityId: empty32, qsetName: empty32, qsetGlobalId: empty32,
|
|
96
|
+
quantityName: empty32, quantityType: empty8,
|
|
97
|
+
valueReal: new Float64Array(0), unitId: emptyI32,
|
|
98
|
+
entityIndex: new Map<number, number[]>(),
|
|
99
|
+
qsetIndex: new Map<number, number[]>(),
|
|
100
|
+
getForEntity: () => [],
|
|
101
|
+
};
|
|
102
|
+
const relationships = {
|
|
103
|
+
count: 0,
|
|
104
|
+
relType: empty8, relatingId: empty32, relatedId: empty32,
|
|
105
|
+
byRelating: new Map<number, number[]>(),
|
|
106
|
+
byRelated: new Map<number, number[]>(),
|
|
107
|
+
getOutgoing: () => [],
|
|
108
|
+
getIncoming: () => [],
|
|
109
|
+
getRelated: () => [],
|
|
110
|
+
getRelating: () => [],
|
|
111
|
+
};
|
|
112
|
+
const byId = new Map<number, unknown>([[expressId, { expressId }]]);
|
|
113
|
+
return {
|
|
114
|
+
fileSize: buffer.byteLength,
|
|
115
|
+
schemaVersion: 'IFC4' as const,
|
|
116
|
+
entityCount: 1,
|
|
117
|
+
parseTime: 0,
|
|
118
|
+
source: new Uint8Array(0),
|
|
119
|
+
entityIndex: {
|
|
120
|
+
byId: byId as unknown as IfcDataStore['entityIndex']['byId'],
|
|
121
|
+
byType: new Map([['IFCGEOGRAPHICELEMENT', [expressId]]]),
|
|
122
|
+
},
|
|
123
|
+
strings: {
|
|
124
|
+
get: () => '',
|
|
125
|
+
getId: () => 0,
|
|
126
|
+
count: 0,
|
|
127
|
+
} as unknown as IfcDataStore['strings'],
|
|
128
|
+
entities: entities as unknown as IfcDataStore['entities'],
|
|
129
|
+
properties: properties as unknown as IfcDataStore['properties'],
|
|
130
|
+
quantities: quantities as unknown as IfcDataStore['quantities'],
|
|
131
|
+
relationships: relationships as unknown as IfcDataStore['relationships'],
|
|
132
|
+
spatialHierarchy: undefined,
|
|
133
|
+
} as unknown as IfcDataStore;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export interface PointCloudIngestResult {
|
|
137
|
+
dataStore: IfcDataStore;
|
|
138
|
+
geometryResult: GeometryResult;
|
|
139
|
+
schemaVersion: SchemaVersion;
|
|
140
|
+
/** Renderer handle so the model removal path can free GPU resources. */
|
|
141
|
+
rendererHandle: { id: number };
|
|
142
|
+
/** Stream handle so the caller can `cancel()` mid-flight. */
|
|
143
|
+
streamHandle: StreamHandle;
|
|
144
|
+
/** Resolves once decoding finishes (or rejects on error / cancel). */
|
|
145
|
+
done: Promise<void>;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export interface PointCloudIngestOptions {
|
|
149
|
+
format: PointCloudFormat;
|
|
150
|
+
blob: Blob;
|
|
151
|
+
fileName: string;
|
|
152
|
+
buffer: ArrayBuffer;
|
|
153
|
+
/** Renderer to push chunks into. Streaming starts immediately. */
|
|
154
|
+
renderer: Renderer;
|
|
155
|
+
/** Express ID assigned to this asset (for picking + federation). */
|
|
156
|
+
expressId?: number;
|
|
157
|
+
/** Federation index (set when the model registry is multi-model). */
|
|
158
|
+
modelIndex?: number;
|
|
159
|
+
/** Soft cap on points held on the GPU. Default: 25M. */
|
|
160
|
+
maxPointsInMemory?: number;
|
|
161
|
+
/** Hard cap on file size in bytes. Default: 4 GB. */
|
|
162
|
+
maxFileSize?: number;
|
|
163
|
+
/** Progress callback shared with the existing UI. */
|
|
164
|
+
onProgress?: (progress: { phase: string; percent: number }) => void;
|
|
165
|
+
/** Notified with +1 when streaming starts and -1 if it errors. */
|
|
166
|
+
onAssetCountDelta?: (delta: number) => void;
|
|
167
|
+
/** Abort signal to cancel ingest. */
|
|
168
|
+
signal?: AbortSignal;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Detect a supported point-cloud format from filename or magic bytes.
|
|
173
|
+
* Returns null when the buffer isn't a recognised format.
|
|
174
|
+
*
|
|
175
|
+
* Magic-byte sniffing covers files renamed by users:
|
|
176
|
+
* - LAS: "LASF" (0x4653414c)
|
|
177
|
+
* - PLY: "ply\n" or "ply\r\n" at offset 0
|
|
178
|
+
* - PCD: "# .PCD" or any `.PCD` token in first 32 bytes
|
|
179
|
+
* - LAZ: shares LAS magic; we trust the extension here
|
|
180
|
+
*/
|
|
181
|
+
export function detectPointCloudFormat(
|
|
182
|
+
fileName: string,
|
|
183
|
+
buffer: ArrayBuffer | null,
|
|
184
|
+
): PointCloudFormat | null {
|
|
185
|
+
const lower = fileName.toLowerCase();
|
|
186
|
+
if (lower.endsWith('.las')) return 'las';
|
|
187
|
+
if (lower.endsWith('.laz')) return 'laz';
|
|
188
|
+
if (lower.endsWith('.ply')) return 'ply';
|
|
189
|
+
if (lower.endsWith('.pcd')) return 'pcd';
|
|
190
|
+
if (lower.endsWith('.e57')) return 'e57';
|
|
191
|
+
if (buffer && buffer.byteLength >= 8) {
|
|
192
|
+
const view = new DataView(buffer, 0, Math.min(buffer.byteLength, 32));
|
|
193
|
+
if (view.getUint32(0, true) === 0x4653414c) return 'las';
|
|
194
|
+
// ASCII probe — first three bytes "ply" → PLY; "# .P" or ".PCD" → PCD.
|
|
195
|
+
const b0 = view.getUint8(0), b1 = view.getUint8(1), b2 = view.getUint8(2);
|
|
196
|
+
if (b0 === 0x70 /* p */ && b1 === 0x6c /* l */ && b2 === 0x79 /* y */) return 'ply';
|
|
197
|
+
if (b0 === 0x23 /* # */ && view.byteLength > 4 && view.getUint8(2) === 0x2e /* . */) return 'pcd';
|
|
198
|
+
// E57 magic = "ASTM-E57" (8 bytes)
|
|
199
|
+
if (
|
|
200
|
+
view.getUint8(0) === 0x41 && view.getUint8(1) === 0x53
|
|
201
|
+
&& view.getUint8(2) === 0x54 && view.getUint8(3) === 0x4d
|
|
202
|
+
&& view.getUint8(4) === 0x2d && view.getUint8(5) === 0x45
|
|
203
|
+
&& view.getUint8(6) === 0x35 && view.getUint8(7) === 0x37
|
|
204
|
+
) return 'e57';
|
|
205
|
+
}
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Map common unsupported formats to a user-facing explanation. Drop
|
|
211
|
+
* handlers call this when nothing else recognises a dropped file so the
|
|
212
|
+
* user sees "this is a Recap project, export to E57" instead of nothing
|
|
213
|
+
* happening.
|
|
214
|
+
*/
|
|
215
|
+
export function describeUnsupportedFormat(fileName: string): string | null {
|
|
216
|
+
const lower = fileName.toLowerCase();
|
|
217
|
+
if (lower.endsWith('.zip')) {
|
|
218
|
+
return 'ZIP archive — please extract first. .ply / .las / .laz / .e57 files inside will load.';
|
|
219
|
+
}
|
|
220
|
+
if (
|
|
221
|
+
lower.endsWith('.rwp') || lower.endsWith('.rwi')
|
|
222
|
+
|| lower.endsWith('.rwcx') || lower.endsWith('.dmt')
|
|
223
|
+
|| lower.endsWith('.lay') || lower.endsWith('.db1')
|
|
224
|
+
) {
|
|
225
|
+
return 'Autodesk ReCap (.rwp/.rwi/.rwcx) is a proprietary format we cannot decode. Export to E57 or LAS from ReCap.';
|
|
226
|
+
}
|
|
227
|
+
if (lower.endsWith('.skp')) return 'SketchUp model — not a point cloud.';
|
|
228
|
+
if (lower.endsWith('.fls') || lower.endsWith('.lsproj')) {
|
|
229
|
+
return 'Faro Scene project — export to E57 from Scene to load it here.';
|
|
230
|
+
}
|
|
231
|
+
if (lower.endsWith('.pts') || lower.endsWith('.xyz')) {
|
|
232
|
+
return 'PTS / XYZ ASCII points — not yet supported (export to PLY or LAS).';
|
|
233
|
+
}
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Counter for synthetic expressIds when callers don't supply one.
|
|
239
|
+
* Multiple inline-LAS/LAZ/E57 ingests in the same session would
|
|
240
|
+
* otherwise collide on `1`, breaking federation lookup, picking, and
|
|
241
|
+
* BCF hooks. Bumping a process-local counter is enough — the
|
|
242
|
+
* FederationRegistry then layers in the per-model offset on top.
|
|
243
|
+
*/
|
|
244
|
+
let nextSyntheticExpressId = 1;
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Stream a point cloud into the renderer. Returns immediately; await
|
|
248
|
+
* `result.done` for completion.
|
|
249
|
+
*/
|
|
250
|
+
export function ingestPointCloud(opts: PointCloudIngestOptions): PointCloudIngestResult {
|
|
251
|
+
const expressId = opts.expressId ?? nextSyntheticExpressId++;
|
|
252
|
+
// Use 'IfcGeographicElement' for PLY/PCD/LAS/LAZ — IFC4 doesn't define
|
|
253
|
+
// an IfcPointCloud entity, and IfcGeographicElement is the closest
|
|
254
|
+
// semantic fit (a real-world geographic feature backed by a scan).
|
|
255
|
+
const handle = opts.renderer.beginPointCloudStream({
|
|
256
|
+
expressId,
|
|
257
|
+
ifcType: 'IfcGeographicElement',
|
|
258
|
+
modelIndex: opts.modelIndex,
|
|
259
|
+
});
|
|
260
|
+
const onCountChange = opts.onAssetCountDelta ?? (() => {});
|
|
261
|
+
onCountChange(+1);
|
|
262
|
+
|
|
263
|
+
// `streamPointCloud()` can throw synchronously during validation /
|
|
264
|
+
// worker setup (e.g. invalid `chunkSize`, oversized blob). The
|
|
265
|
+
// renderer asset + counter increment have already happened above, so
|
|
266
|
+
// a sync throw must clean those up before propagating — otherwise
|
|
267
|
+
// we leak an empty GPU asset and the `pointCloudAssetCount` stays
|
|
268
|
+
// permanently inflated.
|
|
269
|
+
let stream: StreamHandle;
|
|
270
|
+
try {
|
|
271
|
+
stream = streamPointCloud({
|
|
272
|
+
format: opts.format,
|
|
273
|
+
blob: opts.blob,
|
|
274
|
+
label: opts.fileName,
|
|
275
|
+
maxPointsInMemory: opts.maxPointsInMemory,
|
|
276
|
+
maxFileSize: opts.maxFileSize,
|
|
277
|
+
signal: opts.signal,
|
|
278
|
+
onOpen: (info) => {
|
|
279
|
+
opts.onProgress?.({
|
|
280
|
+
phase: info.stride > 1
|
|
281
|
+
? `Streaming (${info.stride}× downsampled, ${info.totalPointCount.toLocaleString()} pts)`
|
|
282
|
+
: `Streaming (${info.totalPointCount.toLocaleString()} pts)`,
|
|
283
|
+
percent: 10,
|
|
284
|
+
});
|
|
285
|
+
},
|
|
286
|
+
onChunk: (chunk) => {
|
|
287
|
+
// LAS / LAZ / E57 / typical scan-style PLY + PCD all store data
|
|
288
|
+
// Z-up by convention (LIDAR / surveying tradition). The renderer
|
|
289
|
+
// is Y-up internally — the IFCx ingest path applies the same
|
|
290
|
+
// swap inside `pointcloud-extractor.ts`. Without this, the scan
|
|
291
|
+
// shows up rotated 90° onto its side.
|
|
292
|
+
const yUp = swapZupChunkToYup(chunk);
|
|
293
|
+
opts.renderer.appendPointCloudChunk(handle, yUp);
|
|
294
|
+
opts.renderer.requestRender();
|
|
295
|
+
},
|
|
296
|
+
onProgress: (loaded, total) => {
|
|
297
|
+
const pct = total > 0 ? Math.min(99, 10 + Math.floor((loaded / total) * 89)) : 50;
|
|
298
|
+
opts.onProgress?.({
|
|
299
|
+
phase: `Streaming (${loaded.toLocaleString()} / ${total.toLocaleString()})`,
|
|
300
|
+
percent: pct,
|
|
301
|
+
});
|
|
302
|
+
},
|
|
303
|
+
onComplete: () => {
|
|
304
|
+
opts.renderer.endPointCloudStream(handle);
|
|
305
|
+
opts.onProgress?.({ phase: 'Streaming complete', percent: 100 });
|
|
306
|
+
},
|
|
307
|
+
onError: () => {
|
|
308
|
+
opts.renderer.removePointCloudAsset(handle);
|
|
309
|
+
onCountChange(-1);
|
|
310
|
+
},
|
|
311
|
+
});
|
|
312
|
+
} catch (err) {
|
|
313
|
+
opts.renderer.removePointCloudAsset(handle);
|
|
314
|
+
onCountChange(-1);
|
|
315
|
+
throw err;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Build a minimal GeometryResult that satisfies the model registry.
|
|
319
|
+
// The actual point data is on the GPU, not in memory.
|
|
320
|
+
const coordinateInfo: CoordinateInfo = createCoordinateInfo({
|
|
321
|
+
min: { x: 0, y: 0, z: 0 },
|
|
322
|
+
max: { x: 0, y: 0, z: 0 },
|
|
323
|
+
});
|
|
324
|
+
// Synthetic pointcloud descriptor. Federation (`useIfcFederation`)
|
|
325
|
+
// folds `idOffset` into every entry's `expressId` and then calls
|
|
326
|
+
// `relabelPointCloudAsset` on the renderer; without an entry here
|
|
327
|
+
// streamed assets keep their local synthetic id and pick collisions
|
|
328
|
+
// appear once a second model is added.
|
|
329
|
+
const pointClouds: PointCloudAsset[] = [{
|
|
330
|
+
expressId,
|
|
331
|
+
ifcType: 'IfcGeographicElement',
|
|
332
|
+
modelIndex: opts.modelIndex,
|
|
333
|
+
chunk: {
|
|
334
|
+
// Empty placeholder — actual point data is GPU-resident, never
|
|
335
|
+
// re-uploaded from JS.
|
|
336
|
+
positions: new Float32Array(0),
|
|
337
|
+
pointCount: 0,
|
|
338
|
+
bbox: { min: [0, 0, 0], max: [0, 0, 0] },
|
|
339
|
+
},
|
|
340
|
+
}];
|
|
341
|
+
const geometryResult: GeometryResult = {
|
|
342
|
+
meshes: [],
|
|
343
|
+
pointClouds,
|
|
344
|
+
totalVertices: 0,
|
|
345
|
+
totalTriangles: 0,
|
|
346
|
+
coordinateInfo,
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
return {
|
|
350
|
+
dataStore: emptyDataStore(opts.buffer, expressId, opts.fileName),
|
|
351
|
+
geometryResult,
|
|
352
|
+
schemaVersion: 'IFC4',
|
|
353
|
+
rendererHandle: handle,
|
|
354
|
+
streamHandle: stream,
|
|
355
|
+
done: stream.done,
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Re-orient a Z-up chunk into the renderer's Y-up convention.
|
|
361
|
+
* Z-up: X=right, Y=forward, Z=up
|
|
362
|
+
* Y-up: X=right, Y=up, Z=back (negate Y to keep right-hand rule)
|
|
363
|
+
*
|
|
364
|
+
* Mirrors the geometry / pointcloud extractors' Z↔Y handling for IFCx.
|
|
365
|
+
* Allocates a fresh positions buffer so the source chunk's typed array
|
|
366
|
+
* (often a transferable from the worker) stays untouched.
|
|
367
|
+
*/
|
|
368
|
+
function swapZupChunkToYup(chunk: DecodedPointChunk): DecodedPointChunk {
|
|
369
|
+
const src = chunk.positions;
|
|
370
|
+
const positions = new Float32Array(src.length);
|
|
371
|
+
for (let i = 0; i < src.length; i += 3) {
|
|
372
|
+
const x = src[i];
|
|
373
|
+
const y = src[i + 1];
|
|
374
|
+
const z = src[i + 2];
|
|
375
|
+
positions[i] = x;
|
|
376
|
+
positions[i + 1] = z; // new Y = old Z
|
|
377
|
+
positions[i + 2] = -y; // new Z = -old Y
|
|
378
|
+
}
|
|
379
|
+
// BBox transforms the same way. New min/max derive from the swapped
|
|
380
|
+
// axes; note the negation flips min and max on the Z-back axis.
|
|
381
|
+
const oldMin = chunk.bbox.min;
|
|
382
|
+
const oldMax = chunk.bbox.max;
|
|
383
|
+
return {
|
|
384
|
+
...chunk,
|
|
385
|
+
positions,
|
|
386
|
+
bbox: {
|
|
387
|
+
min: [oldMin[0], oldMin[2], -oldMax[1]],
|
|
388
|
+
max: [oldMax[0], oldMax[2], -oldMin[1]],
|
|
389
|
+
},
|
|
390
|
+
};
|
|
391
|
+
}
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
3
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
4
4
|
|
|
5
|
-
import { IfcParser, parseIfcx, type IfcDataStore } from '@ifc-lite/parser';
|
|
6
|
-
import { GeometryProcessor, GeometryQuality, type CoordinateInfo, type DynamicBatchConfig, type GeometryResult, type MeshData } from '@ifc-lite/geometry';
|
|
5
|
+
import { IfcParser, parseIfcx, type IfcDataStore, type PointCloudExtraction } from '@ifc-lite/parser';
|
|
6
|
+
import { GeometryProcessor, GeometryQuality, type CoordinateInfo, type DynamicBatchConfig, type GeometryResult, type MeshData, type PointCloudAsset } from '@ifc-lite/geometry';
|
|
7
7
|
import { loadGLBToMeshData } from '@ifc-lite/cache';
|
|
8
8
|
import type { SchemaVersion } from '../../store/types.js';
|
|
9
9
|
import { calculateMeshBounds, calculateStoreyHeights, createCoordinateInfo, normalizeColor } from '../../utils/localParsingUtils.js';
|
|
@@ -133,11 +133,26 @@ export async function parseIfcxViewerModel(
|
|
|
133
133
|
});
|
|
134
134
|
|
|
135
135
|
const meshes = convertIfcxMeshes(ifcxResult.meshes);
|
|
136
|
-
|
|
136
|
+
const pointClouds = convertIfcxPointClouds(ifcxResult.pointClouds ?? []);
|
|
137
|
+
// Treat as overlay-only ONLY when neither meshes nor pointclouds were extracted.
|
|
138
|
+
// Files that carry just point cloud assets (the buildingSMART Point_Cloud
|
|
139
|
+
// samples) still represent a renderable model on their own.
|
|
140
|
+
if (meshes.length === 0 && pointClouds.length === 0 && ifcxResult.entityCount > 0) {
|
|
137
141
|
throw new Error('overlay-only-ifcx');
|
|
138
142
|
}
|
|
139
143
|
|
|
140
144
|
const { bounds, stats } = calculateMeshBounds(meshes);
|
|
145
|
+
// Expand bounds to include point cloud asset extents so fit-to-view, the
|
|
146
|
+
// section-plane slider, and camera near/far all see the points too.
|
|
147
|
+
for (const pc of pointClouds) {
|
|
148
|
+
const { min, max } = pc.chunk.bbox;
|
|
149
|
+
bounds.min.x = Math.min(bounds.min.x, min[0]);
|
|
150
|
+
bounds.min.y = Math.min(bounds.min.y, min[1]);
|
|
151
|
+
bounds.min.z = Math.min(bounds.min.z, min[2]);
|
|
152
|
+
bounds.max.x = Math.max(bounds.max.x, max[0]);
|
|
153
|
+
bounds.max.y = Math.max(bounds.max.y, max[1]);
|
|
154
|
+
bounds.max.z = Math.max(bounds.max.z, max[2]);
|
|
155
|
+
}
|
|
141
156
|
return {
|
|
142
157
|
dataStore: {
|
|
143
158
|
fileSize: ifcxResult.fileSize,
|
|
@@ -155,6 +170,7 @@ export async function parseIfcxViewerModel(
|
|
|
155
170
|
} as unknown as IfcDataStore,
|
|
156
171
|
geometryResult: {
|
|
157
172
|
meshes,
|
|
173
|
+
pointClouds,
|
|
158
174
|
totalVertices: stats.totalVertices,
|
|
159
175
|
totalTriangles: stats.totalTriangles,
|
|
160
176
|
coordinateInfo: createCoordinateInfo(bounds),
|
|
@@ -163,6 +179,19 @@ export async function parseIfcxViewerModel(
|
|
|
163
179
|
};
|
|
164
180
|
}
|
|
165
181
|
|
|
182
|
+
export function convertIfcxPointClouds(extractions: PointCloudExtraction[]): PointCloudAsset[] {
|
|
183
|
+
return extractions.map((pc) => ({
|
|
184
|
+
expressId: pc.expressId,
|
|
185
|
+
ifcType: pc.ifcType,
|
|
186
|
+
chunk: {
|
|
187
|
+
positions: pc.positions,
|
|
188
|
+
colors: pc.colors,
|
|
189
|
+
pointCount: pc.pointCount,
|
|
190
|
+
bbox: pc.bbox,
|
|
191
|
+
},
|
|
192
|
+
}));
|
|
193
|
+
}
|
|
194
|
+
|
|
166
195
|
export async function parseGlbViewerModel(buffer: ArrayBuffer): Promise<ViewerModelPayload> {
|
|
167
196
|
const meshes = loadGLBToMeshData(new Uint8Array(buffer));
|
|
168
197
|
if (meshes.length === 0) {
|