@ifc-lite/viewer 1.0.0 → 1.1.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/dist/assets/{geometry.worker-DpnHtNr3.ts → geometry.worker-CiROVpKV.ts} +6 -9
- package/dist/assets/ifc-lite_bg-BQVS1Fh7.wasm +0 -0
- package/dist/assets/index-CvYpbxNF.js +728 -0
- package/dist/ifc-lite_bg.wasm +0 -0
- package/dist/index.html +1 -1
- package/package.json +9 -9
- package/public/ifc-lite_bg.wasm +0 -0
- package/src/components/viewer/Viewport.tsx +198 -66
- package/src/components/viewer/ViewportContainer.tsx +1 -1
- package/src/hooks/useIfc.ts +235 -20
- package/src/services/ifc-cache.ts +251 -0
- package/src/store.ts +55 -4
- package/tsconfig.json +5 -1
- package/vite.config.ts +15 -1
- package/dist/assets/ifc_lite_wasm_bg-Cd3m3f2h.wasm +0 -0
- package/dist/assets/index-Dzz3WVwq.js +0 -637
- package/dist/ifc_lite_wasm_bg.wasm +0 -0
- package/public/ifc_lite_wasm_bg.wasm +0 -0
|
Binary file
|
package/dist/index.html
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
6
|
<title>IFC-Lite Viewer</title>
|
|
7
|
-
<script type="module" crossorigin src="/assets/index-
|
|
7
|
+
<script type="module" crossorigin src="/assets/index-CvYpbxNF.js"></script>
|
|
8
8
|
<link rel="stylesheet" crossorigin href="/assets/index-DKe9Oy-s.css">
|
|
9
9
|
</head>
|
|
10
10
|
<body>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ifc-lite/viewer",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "IFC-Lite viewer application",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"dependencies": {
|
|
@@ -27,13 +27,14 @@
|
|
|
27
27
|
"tailwind-merge": "^3.4.0",
|
|
28
28
|
"tailwindcss": "^4.1.18",
|
|
29
29
|
"zustand": "^4.4.0",
|
|
30
|
-
"@ifc-lite/
|
|
31
|
-
"@ifc-lite/
|
|
32
|
-
"@ifc-lite/
|
|
33
|
-
"@ifc-lite/
|
|
34
|
-
"@ifc-lite/
|
|
35
|
-
"@ifc-lite/
|
|
36
|
-
"@ifc-lite/
|
|
30
|
+
"@ifc-lite/cache": "1.1.0",
|
|
31
|
+
"@ifc-lite/export": "1.1.0",
|
|
32
|
+
"@ifc-lite/geometry": "1.1.0",
|
|
33
|
+
"@ifc-lite/parser": "1.1.0",
|
|
34
|
+
"@ifc-lite/renderer": "1.1.0",
|
|
35
|
+
"@ifc-lite/spatial": "1.1.0",
|
|
36
|
+
"@ifc-lite/data": "1.1.0",
|
|
37
|
+
"@ifc-lite/query": "1.1.0"
|
|
37
38
|
},
|
|
38
39
|
"devDependencies": {
|
|
39
40
|
"@tailwindcss/postcss": "^4.1.18",
|
|
@@ -45,7 +46,6 @@
|
|
|
45
46
|
},
|
|
46
47
|
"scripts": {
|
|
47
48
|
"dev": "vite",
|
|
48
|
-
"prebuild": "cp ../../packages/wasm/ifc_lite_wasm_bg.wasm public/ifc_lite_wasm_bg.wasm",
|
|
49
49
|
"build": "(tsc || true) && vite build",
|
|
50
50
|
"preview": "vite preview"
|
|
51
51
|
}
|
|
Binary file
|
|
@@ -389,7 +389,9 @@ export function Viewport({ geometry, coordinateInfo }: ViewportProps) {
|
|
|
389
389
|
const y = e.clientY - rect.top;
|
|
390
390
|
|
|
391
391
|
// Pick at cursor position - orbit around what user is clicking on
|
|
392
|
-
const
|
|
392
|
+
const currentProgress = useViewerStore.getState().progress;
|
|
393
|
+
const currentIsStreaming = currentProgress !== null && currentProgress.percent < 100;
|
|
394
|
+
const pickedId = await renderer.pick(x, y, { isStreaming: currentIsStreaming });
|
|
393
395
|
if (pickedId !== null) {
|
|
394
396
|
const center = getEntityCenter(geometryRef.current, pickedId);
|
|
395
397
|
if (center) {
|
|
@@ -477,7 +479,9 @@ export function Viewport({ geometry, coordinateInfo }: ViewportProps) {
|
|
|
477
479
|
const now = Date.now();
|
|
478
480
|
if (now - lastHoverCheckRef.current > hoverThrottleMs) {
|
|
479
481
|
lastHoverCheckRef.current = now;
|
|
480
|
-
const
|
|
482
|
+
const currentProgress = useViewerStore.getState().progress;
|
|
483
|
+
const currentIsStreaming = currentProgress !== null && currentProgress.percent < 100;
|
|
484
|
+
const pickedId = await renderer.pick(x, y, { isStreaming: currentIsStreaming });
|
|
481
485
|
if (pickedId) {
|
|
482
486
|
setHoverState({ entityId: pickedId, screenX: e.clientX, screenY: e.clientY });
|
|
483
487
|
} else {
|
|
@@ -548,7 +552,9 @@ export function Viewport({ geometry, coordinateInfo }: ViewportProps) {
|
|
|
548
552
|
|
|
549
553
|
// Handle measure tool clicks
|
|
550
554
|
if (tool === 'measure') {
|
|
551
|
-
const
|
|
555
|
+
const currentProgress = useViewerStore.getState().progress;
|
|
556
|
+
const currentIsStreaming = currentProgress !== null && currentProgress.percent < 100;
|
|
557
|
+
const pickedId = await renderer.pick(x, y, { isStreaming: currentIsStreaming });
|
|
552
558
|
if (pickedId) {
|
|
553
559
|
// Get 3D position from mesh vertices (simplified - uses center of clicked entity)
|
|
554
560
|
// In a full implementation, you'd use ray-triangle intersection
|
|
@@ -656,7 +662,9 @@ export function Viewport({ geometry, coordinateInfo }: ViewportProps) {
|
|
|
656
662
|
Math.abs(clickPos.x - lastClickPosRef.current.x) < 5 &&
|
|
657
663
|
Math.abs(clickPos.y - lastClickPosRef.current.y) < 5) {
|
|
658
664
|
// Double-click - isolate element
|
|
659
|
-
const
|
|
665
|
+
const currentProgress = useViewerStore.getState().progress;
|
|
666
|
+
const currentIsStreaming = currentProgress !== null && currentProgress.percent < 100;
|
|
667
|
+
const pickedId = await renderer.pick(x, y, { isStreaming: currentIsStreaming });
|
|
660
668
|
if (pickedId) {
|
|
661
669
|
setSelectedEntityId(pickedId);
|
|
662
670
|
}
|
|
@@ -664,7 +672,10 @@ export function Viewport({ geometry, coordinateInfo }: ViewportProps) {
|
|
|
664
672
|
lastClickPosRef.current = null;
|
|
665
673
|
} else {
|
|
666
674
|
// Single click
|
|
667
|
-
|
|
675
|
+
// Get current progress state (not from closure)
|
|
676
|
+
const currentProgress = useViewerStore.getState().progress;
|
|
677
|
+
const currentIsStreaming = currentProgress !== null && currentProgress.percent < 100;
|
|
678
|
+
const pickedId = await renderer.pick(x, y, { isStreaming: currentIsStreaming });
|
|
668
679
|
|
|
669
680
|
// Multi-selection with Ctrl/Cmd
|
|
670
681
|
if (e.ctrlKey || e.metaKey) {
|
|
@@ -709,7 +720,9 @@ export function Viewport({ geometry, coordinateInfo }: ViewportProps) {
|
|
|
709
720
|
const x = touchState.touches[0].clientX - rect.left;
|
|
710
721
|
const y = touchState.touches[0].clientY - rect.top;
|
|
711
722
|
|
|
712
|
-
const
|
|
723
|
+
const currentProgress = useViewerStore.getState().progress;
|
|
724
|
+
const currentIsStreaming = currentProgress !== null && currentProgress.percent < 100;
|
|
725
|
+
const pickedId = await renderer.pick(x, y, { isStreaming: currentIsStreaming });
|
|
713
726
|
if (pickedId !== null) {
|
|
714
727
|
const center = getEntityCenter(geometryRef.current, pickedId);
|
|
715
728
|
if (center) {
|
|
@@ -947,6 +960,13 @@ export function Viewport({ geometry, coordinateInfo }: ViewportProps) {
|
|
|
947
960
|
const lastGeometryLengthRef = useRef<number>(0);
|
|
948
961
|
const lastGeometryRef = useRef<MeshData[] | null>(null);
|
|
949
962
|
const cameraFittedRef = useRef<boolean>(false);
|
|
963
|
+
const finalBoundsRefittedRef = useRef<boolean>(false); // Track if we've refitted after streaming
|
|
964
|
+
|
|
965
|
+
// Render throttling during streaming
|
|
966
|
+
const lastRenderTimeRef = useRef<number>(0);
|
|
967
|
+
const RENDER_THROTTLE_MS = 200; // Render at most every 200ms during streaming
|
|
968
|
+
const progress = useViewerStore((state) => state.progress);
|
|
969
|
+
const isStreaming = progress !== null && progress.percent < 100;
|
|
950
970
|
|
|
951
971
|
useEffect(() => {
|
|
952
972
|
const renderer = rendererRef.current;
|
|
@@ -958,46 +978,52 @@ export function Viewport({ geometry, coordinateInfo }: ViewportProps) {
|
|
|
958
978
|
|
|
959
979
|
const scene = renderer.getScene();
|
|
960
980
|
const currentLength = geometry.length;
|
|
961
|
-
const
|
|
981
|
+
const lastLength = lastGeometryLengthRef.current;
|
|
962
982
|
|
|
963
|
-
|
|
964
|
-
|
|
983
|
+
// Use length-based detection instead of reference comparison
|
|
984
|
+
// React creates new array references on every appendGeometryBatch call,
|
|
985
|
+
// so reference comparison would always trigger scene.clear()
|
|
986
|
+
const isIncremental = currentLength > lastLength && lastLength > 0;
|
|
987
|
+
const isNewFile = currentLength > 0 && lastLength === 0 && lastGeometryRef.current !== null;
|
|
988
|
+
const isCleared = currentLength === 0;
|
|
989
|
+
|
|
990
|
+
if (isCleared) {
|
|
991
|
+
// Geometry cleared - reset camera and bounds
|
|
965
992
|
scene.clear();
|
|
966
993
|
processedMeshIdsRef.current.clear();
|
|
967
994
|
cameraFittedRef.current = false;
|
|
995
|
+
finalBoundsRefittedRef.current = false;
|
|
968
996
|
lastGeometryLengthRef.current = 0;
|
|
969
|
-
lastGeometryRef.current =
|
|
970
|
-
// Reset camera state
|
|
997
|
+
lastGeometryRef.current = null;
|
|
998
|
+
// Reset camera state
|
|
971
999
|
renderer.getCamera().reset();
|
|
972
1000
|
// Reset geometry bounds to default
|
|
973
1001
|
geometryBoundsRef.current = {
|
|
974
1002
|
min: { x: -100, y: -100, z: -100 },
|
|
975
1003
|
max: { x: 100, y: 100, z: 100 },
|
|
976
1004
|
};
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
// Geometry cleared - reset camera and bounds
|
|
1005
|
+
return;
|
|
1006
|
+
} else if (isNewFile) {
|
|
1007
|
+
// New file loaded - reset camera and bounds
|
|
981
1008
|
scene.clear();
|
|
982
1009
|
processedMeshIdsRef.current.clear();
|
|
983
1010
|
cameraFittedRef.current = false;
|
|
1011
|
+
finalBoundsRefittedRef.current = false;
|
|
984
1012
|
lastGeometryLengthRef.current = 0;
|
|
985
|
-
lastGeometryRef.current =
|
|
986
|
-
// Reset camera state
|
|
1013
|
+
lastGeometryRef.current = geometry;
|
|
1014
|
+
// Reset camera state (clear orbit pivot, stop inertia, cancel animations)
|
|
987
1015
|
renderer.getCamera().reset();
|
|
988
1016
|
// Reset geometry bounds to default
|
|
989
1017
|
geometryBoundsRef.current = {
|
|
990
1018
|
min: { x: -100, y: -100, z: -100 },
|
|
991
1019
|
max: { x: 100, y: 100, z: 100 },
|
|
992
1020
|
};
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
return;
|
|
996
|
-
} else {
|
|
997
|
-
// Length changed or other scenario - reset camera and bounds
|
|
1021
|
+
} else if (!isIncremental && currentLength !== lastLength) {
|
|
1022
|
+
// Length decreased (shouldn't happen during streaming) - reset
|
|
998
1023
|
scene.clear();
|
|
999
1024
|
processedMeshIdsRef.current.clear();
|
|
1000
1025
|
cameraFittedRef.current = false;
|
|
1026
|
+
finalBoundsRefittedRef.current = false;
|
|
1001
1027
|
lastGeometryLengthRef.current = 0;
|
|
1002
1028
|
lastGeometryRef.current = geometry;
|
|
1003
1029
|
// Reset camera state
|
|
@@ -1007,58 +1033,130 @@ export function Viewport({ geometry, coordinateInfo }: ViewportProps) {
|
|
|
1007
1033
|
min: { x: -100, y: -100, z: -100 },
|
|
1008
1034
|
max: { x: 100, y: 100, z: 100 },
|
|
1009
1035
|
};
|
|
1036
|
+
} else if (currentLength === lastLength) {
|
|
1037
|
+
// No geometry change - but check if we need to update bounds when streaming completes
|
|
1038
|
+
if (cameraFittedRef.current && !isStreaming && !finalBoundsRefittedRef.current && coordinateInfo?.shiftedBounds) {
|
|
1039
|
+
const shiftedBounds = coordinateInfo.shiftedBounds;
|
|
1040
|
+
const newMaxSize = Math.max(
|
|
1041
|
+
shiftedBounds.max.x - shiftedBounds.min.x,
|
|
1042
|
+
shiftedBounds.max.y - shiftedBounds.min.y,
|
|
1043
|
+
shiftedBounds.max.z - shiftedBounds.min.z
|
|
1044
|
+
);
|
|
1045
|
+
|
|
1046
|
+
if (newMaxSize > 0 && Number.isFinite(newMaxSize)) {
|
|
1047
|
+
// Only refit camera for LARGE models (>1000 meshes) where geometry streamed in multiple batches
|
|
1048
|
+
// Small models complete in one batch, so their initial camera fit is already correct
|
|
1049
|
+
const isLargeModel = geometry.length > 1000;
|
|
1050
|
+
|
|
1051
|
+
if (isLargeModel) {
|
|
1052
|
+
const oldBounds = geometryBoundsRef.current;
|
|
1053
|
+
const oldMaxSize = Math.max(
|
|
1054
|
+
oldBounds.max.x - oldBounds.min.x,
|
|
1055
|
+
oldBounds.max.y - oldBounds.min.y,
|
|
1056
|
+
oldBounds.max.z - oldBounds.min.z
|
|
1057
|
+
);
|
|
1058
|
+
|
|
1059
|
+
// Refit camera if bounds expanded significantly (>10% larger)
|
|
1060
|
+
// This handles skyscrapers where upper floors arrive in later batches
|
|
1061
|
+
const boundsExpanded = newMaxSize > oldMaxSize * 1.1;
|
|
1062
|
+
|
|
1063
|
+
if (boundsExpanded) {
|
|
1064
|
+
console.log('[Viewport] Refitting camera after streaming complete - bounds expanded:', {
|
|
1065
|
+
oldMaxSize: oldMaxSize.toFixed(1),
|
|
1066
|
+
newMaxSize: newMaxSize.toFixed(1),
|
|
1067
|
+
expansion: ((newMaxSize / oldMaxSize - 1) * 100).toFixed(0) + '%'
|
|
1068
|
+
});
|
|
1069
|
+
renderer.getCamera().fitToBounds(shiftedBounds.min, shiftedBounds.max);
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
// Always update bounds for accurate zoom-to-fit, home view, etc.
|
|
1074
|
+
geometryBoundsRef.current = { min: { ...shiftedBounds.min }, max: { ...shiftedBounds.max } };
|
|
1075
|
+
finalBoundsRefittedRef.current = true;
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
return;
|
|
1010
1079
|
}
|
|
1011
1080
|
|
|
1012
|
-
|
|
1081
|
+
// For incremental batches: update reference and continue to add new meshes
|
|
1082
|
+
if (isIncremental) {
|
|
1083
|
+
lastGeometryRef.current = geometry;
|
|
1084
|
+
} else if (lastGeometryRef.current === null) {
|
|
1013
1085
|
lastGeometryRef.current = geometry;
|
|
1014
1086
|
}
|
|
1015
1087
|
|
|
1016
1088
|
const startIndex = lastGeometryLengthRef.current;
|
|
1017
1089
|
const meshesToAdd = geometry.slice(startIndex);
|
|
1018
1090
|
|
|
1091
|
+
// Filter out already processed meshes
|
|
1092
|
+
const newMeshes: MeshData[] = [];
|
|
1019
1093
|
for (const meshData of meshesToAdd) {
|
|
1020
|
-
if (processedMeshIdsRef.current.has(meshData.expressId))
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
const interleaved = new Float32Array(vertexCount * 6);
|
|
1024
|
-
for (let i = 0; i < vertexCount; i++) {
|
|
1025
|
-
const base = i * 6;
|
|
1026
|
-
const posBase = i * 3;
|
|
1027
|
-
interleaved[base] = meshData.positions[posBase];
|
|
1028
|
-
interleaved[base + 1] = meshData.positions[posBase + 1];
|
|
1029
|
-
interleaved[base + 2] = meshData.positions[posBase + 2];
|
|
1030
|
-
interleaved[base + 3] = meshData.normals[posBase];
|
|
1031
|
-
interleaved[base + 4] = meshData.normals[posBase + 1];
|
|
1032
|
-
interleaved[base + 5] = meshData.normals[posBase + 2];
|
|
1094
|
+
if (!processedMeshIdsRef.current.has(meshData.expressId)) {
|
|
1095
|
+
newMeshes.push(meshData);
|
|
1096
|
+
processedMeshIdsRef.current.add(meshData.expressId);
|
|
1033
1097
|
}
|
|
1098
|
+
}
|
|
1034
1099
|
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1100
|
+
if (newMeshes.length > 0) {
|
|
1101
|
+
// Batch meshes by color for efficient rendering (reduces draw calls from N to ~100-500)
|
|
1102
|
+
// This dramatically improves performance for large models (50K+ meshes)
|
|
1103
|
+
const pipeline = renderer.getPipeline();
|
|
1104
|
+
if (pipeline) {
|
|
1105
|
+
// Use batched rendering - groups meshes by color into single draw calls
|
|
1106
|
+
(scene as any).appendToBatches(newMeshes, device, pipeline);
|
|
1107
|
+
|
|
1108
|
+
// Store mesh data for on-demand selection rendering
|
|
1109
|
+
// We DON'T create GPU buffers here during streaming - that's 2x the overhead!
|
|
1110
|
+
// Instead, store MeshData references and create buffers lazily when selected
|
|
1111
|
+
for (const meshData of newMeshes) {
|
|
1112
|
+
// Store minimal mesh data for picker and lazy selection buffer creation
|
|
1113
|
+
scene.addMeshData(meshData);
|
|
1114
|
+
}
|
|
1115
|
+
} else {
|
|
1116
|
+
// Fallback: add individual meshes if pipeline not ready
|
|
1117
|
+
for (const meshData of newMeshes) {
|
|
1118
|
+
const vertexCount = meshData.positions.length / 3;
|
|
1119
|
+
const interleaved = new Float32Array(vertexCount * 6);
|
|
1120
|
+
for (let i = 0; i < vertexCount; i++) {
|
|
1121
|
+
const base = i * 6;
|
|
1122
|
+
const posBase = i * 3;
|
|
1123
|
+
interleaved[base] = meshData.positions[posBase];
|
|
1124
|
+
interleaved[base + 1] = meshData.positions[posBase + 1];
|
|
1125
|
+
interleaved[base + 2] = meshData.positions[posBase + 2];
|
|
1126
|
+
interleaved[base + 3] = meshData.normals[posBase];
|
|
1127
|
+
interleaved[base + 4] = meshData.normals[posBase + 1];
|
|
1128
|
+
interleaved[base + 5] = meshData.normals[posBase + 2];
|
|
1129
|
+
}
|
|
1040
1130
|
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
scene.addMesh({
|
|
1048
|
-
expressId: meshData.expressId,
|
|
1049
|
-
vertexBuffer,
|
|
1050
|
-
indexBuffer,
|
|
1051
|
-
indexCount: meshData.indices.length,
|
|
1052
|
-
transform: MathUtils.identity(),
|
|
1053
|
-
color: meshData.color,
|
|
1054
|
-
});
|
|
1131
|
+
const vertexBuffer = device.createBuffer({
|
|
1132
|
+
size: interleaved.byteLength,
|
|
1133
|
+
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
|
|
1134
|
+
});
|
|
1135
|
+
device.queue.writeBuffer(vertexBuffer, 0, interleaved);
|
|
1055
1136
|
|
|
1056
|
-
|
|
1137
|
+
const indexBuffer = device.createBuffer({
|
|
1138
|
+
size: meshData.indices.byteLength,
|
|
1139
|
+
usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST,
|
|
1140
|
+
});
|
|
1141
|
+
device.queue.writeBuffer(indexBuffer, 0, meshData.indices);
|
|
1142
|
+
|
|
1143
|
+
scene.addMesh({
|
|
1144
|
+
expressId: meshData.expressId,
|
|
1145
|
+
vertexBuffer,
|
|
1146
|
+
indexBuffer,
|
|
1147
|
+
indexCount: meshData.indices.length,
|
|
1148
|
+
transform: MathUtils.identity(),
|
|
1149
|
+
color: meshData.color,
|
|
1150
|
+
});
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1057
1153
|
}
|
|
1058
1154
|
|
|
1059
1155
|
lastGeometryLengthRef.current = currentLength;
|
|
1060
1156
|
|
|
1061
1157
|
// Fit camera and store bounds
|
|
1158
|
+
// IMPORTANT: Fit camera immediately when we have valid bounds to avoid starting inside model
|
|
1159
|
+
// The default camera position (50, 50, 100) is inside most models that are shifted to origin
|
|
1062
1160
|
if (!cameraFittedRef.current && coordinateInfo?.shiftedBounds) {
|
|
1063
1161
|
const shiftedBounds = coordinateInfo.shiftedBounds;
|
|
1064
1162
|
const maxSize = Math.max(
|
|
@@ -1066,12 +1164,18 @@ export function Viewport({ geometry, coordinateInfo }: ViewportProps) {
|
|
|
1066
1164
|
shiftedBounds.max.y - shiftedBounds.min.y,
|
|
1067
1165
|
shiftedBounds.max.z - shiftedBounds.min.z
|
|
1068
1166
|
);
|
|
1167
|
+
// Fit camera immediately when we have valid bounds
|
|
1168
|
+
// For streaming: the first batch already has complete bounds from coordinate handler
|
|
1169
|
+
// (bounds are calculated from ALL geometry before streaming starts)
|
|
1170
|
+
// Waiting for streaming to complete causes the camera to start inside the model
|
|
1069
1171
|
if (maxSize > 0 && Number.isFinite(maxSize)) {
|
|
1070
1172
|
renderer.getCamera().fitToBounds(shiftedBounds.min, shiftedBounds.max);
|
|
1071
1173
|
geometryBoundsRef.current = { min: { ...shiftedBounds.min }, max: { ...shiftedBounds.max } };
|
|
1072
1174
|
cameraFittedRef.current = true;
|
|
1073
1175
|
}
|
|
1074
|
-
} else if (!cameraFittedRef.current && geometry.length > 0) {
|
|
1176
|
+
} else if (!cameraFittedRef.current && geometry.length > 0 && !isStreaming) {
|
|
1177
|
+
// Fallback: calculate bounds from geometry array (only when streaming is complete)
|
|
1178
|
+
// This ensures we have complete bounds before fitting camera
|
|
1075
1179
|
const fallbackBounds = {
|
|
1076
1180
|
min: { x: Infinity, y: Infinity, z: Infinity },
|
|
1077
1181
|
max: { x: -Infinity, y: -Infinity, z: -Infinity },
|
|
@@ -1093,21 +1197,49 @@ export function Viewport({ geometry, coordinateInfo }: ViewportProps) {
|
|
|
1093
1197
|
}
|
|
1094
1198
|
}
|
|
1095
1199
|
|
|
1096
|
-
|
|
1200
|
+
const maxSize = Math.max(
|
|
1201
|
+
fallbackBounds.max.x - fallbackBounds.min.x,
|
|
1202
|
+
fallbackBounds.max.y - fallbackBounds.min.y,
|
|
1203
|
+
fallbackBounds.max.z - fallbackBounds.min.z
|
|
1204
|
+
);
|
|
1205
|
+
|
|
1206
|
+
if (fallbackBounds.min.x !== Infinity && maxSize > 0 && Number.isFinite(maxSize)) {
|
|
1097
1207
|
renderer.getCamera().fitToBounds(fallbackBounds.min, fallbackBounds.max);
|
|
1098
1208
|
geometryBoundsRef.current = fallbackBounds;
|
|
1099
1209
|
cameraFittedRef.current = true;
|
|
1100
1210
|
}
|
|
1101
1211
|
}
|
|
1102
1212
|
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
//
|
|
1110
|
-
|
|
1213
|
+
// Note: Background instancing conversion removed
|
|
1214
|
+
// Regular MeshData meshes are rendered directly with their correct positions
|
|
1215
|
+
// Instancing conversion would require preserving actual mesh transforms, which is complex
|
|
1216
|
+
// For now, we render regular meshes directly (fast enough for most cases)
|
|
1217
|
+
|
|
1218
|
+
// Render throttling: During streaming, only render every RENDER_THROTTLE_MS
|
|
1219
|
+
// This prevents rendering 28K+ meshes from blocking WASM batch processing
|
|
1220
|
+
const now = Date.now();
|
|
1221
|
+
const timeSinceLastRender = now - lastRenderTimeRef.current;
|
|
1222
|
+
const shouldRender = !isStreaming || timeSinceLastRender >= RENDER_THROTTLE_MS;
|
|
1223
|
+
|
|
1224
|
+
if (shouldRender) {
|
|
1225
|
+
renderer.render();
|
|
1226
|
+
lastRenderTimeRef.current = now;
|
|
1227
|
+
}
|
|
1228
|
+
}, [geometry, coordinateInfo, isInitialized, isStreaming]);
|
|
1229
|
+
|
|
1230
|
+
// Force render when streaming completes (progress goes from <100% to 100% or null)
|
|
1231
|
+
const prevIsStreamingRef = useRef(isStreaming);
|
|
1232
|
+
useEffect(() => {
|
|
1233
|
+
const renderer = rendererRef.current;
|
|
1234
|
+
if (!renderer || !isInitialized) return;
|
|
1235
|
+
|
|
1236
|
+
// If streaming just completed (was streaming, now not), force immediate render
|
|
1237
|
+
if (prevIsStreamingRef.current && !isStreaming) {
|
|
1238
|
+
renderer.render();
|
|
1239
|
+
lastRenderTimeRef.current = Date.now();
|
|
1240
|
+
}
|
|
1241
|
+
prevIsStreamingRef.current = isStreaming;
|
|
1242
|
+
}, [isStreaming, isInitialized]);
|
|
1111
1243
|
|
|
1112
1244
|
// Get selectedEntityIds from store for multi-selection
|
|
1113
1245
|
const selectedEntityIds = useViewerStore((state) => state.selectedEntityIds);
|
|
@@ -13,7 +13,7 @@ export function ViewportContainer() {
|
|
|
13
13
|
const { geometryResult, ifcDataStore } = useIfc();
|
|
14
14
|
const selectedStorey = useViewerStore((s) => s.selectedStorey);
|
|
15
15
|
|
|
16
|
-
// Filter geometry based on selected storey
|
|
16
|
+
// Filter geometry based on selected storey (for non-instanced fallback)
|
|
17
17
|
const filteredGeometry = useMemo(() => {
|
|
18
18
|
if (!geometryResult?.meshes || !ifcDataStore?.spatialHierarchy) {
|
|
19
19
|
return geometryResult?.meshes || null;
|