@ifc-lite/viewer 1.29.0 → 1.30.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 +32 -31
- package/CHANGELOG.md +58 -0
- package/dist/assets/{basketViewActivator-BSRgF1Hw.js → basketViewActivator-BHNb23Vw.js} +6 -6
- package/dist/assets/{bcf-3uE1MvcT.js → bcf-C0XZ2DSl.js} +1 -1
- package/dist/assets/{deflate-BLlUfw9-.js → deflate-DvvFcUtV.js} +1 -1
- package/dist/assets/{exporters-BL6UmxRa.js → exporters-CvORJOLn.js} +1345 -1434
- package/dist/assets/geometry.worker-B7X9DQQY.js +1 -0
- package/dist/assets/{geotiff-BydcIud8.js → geotiff-fSD_sVw_.js} +10 -10
- package/dist/assets/{ids-DFl74rTt.js → ids-BUOe5QQl.js} +951 -713
- package/dist/assets/idsValidation.worker-DEodXb0f.js +190468 -0
- package/dist/assets/ifc-lite_bg-CmMuB1zf.wasm +0 -0
- package/dist/assets/{index-BNTlm2lP.js → index-B6T42T86.js} +35235 -32937
- package/dist/assets/index-D0tqJL0X.css +1 -0
- package/dist/assets/{index.es-Bk4nLsyS.js → index.es-YGMensDM.js} +7 -7
- package/dist/assets/{jpeg-BvMO8-Tc.js → jpeg-0Sla88_N.js} +1 -1
- package/dist/assets/{jspdf.es.min-BZ_ed66E.js → jspdf.es.min-mnbLNj-p.js} +4 -4
- package/dist/assets/{lerc-CNnDpLpV.js → lerc-C7xUDHpL.js} +1 -1
- package/dist/assets/{lzw-DBaPrGGZ.js → lzw-CK480t0_.js} +1 -1
- package/dist/assets/{native-bridge-DFOoBvTg.js → native-bridge-sLWRanza.js} +1 -1
- package/dist/assets/{packbits-C7uyD2Bi.js → packbits-DcL4imYS.js} +1 -1
- package/dist/assets/parser.worker-BsGV6ml7.js +182 -0
- package/dist/assets/{pdf-DlqdjX9e.js → pdf-BARGfLmx.js} +8 -8
- package/dist/assets/raw-BMWh6mDy.js +1 -0
- package/dist/assets/{sandbox-0Z2NzeOJ.js → sandbox-BSiO04m8.js} +2801 -2609
- package/dist/assets/server-client-AlpWMVq9.js +741 -0
- package/dist/assets/{webimage-zN-oCabb.js → webimage-uy5DjZLk.js} +1 -1
- package/dist/assets/{xlsx-N2LbIR1G.js → xlsx-D02ho69_.js} +6 -6
- package/dist/assets/{zstd-Jk3QKIeb.js → zstd-DcR1TBwT.js} +1 -1
- package/dist/index.html +7 -7
- package/package.json +27 -32
- package/src/components/viewer/CesiumOverlay.tsx +195 -8
- package/src/components/viewer/IDSPanel.tsx +23 -7
- package/src/components/viewer/MainToolbar.tsx +31 -0
- package/src/components/viewer/SunSkyPanel.tsx +363 -0
- package/src/components/viewer/Viewport.tsx +49 -1
- package/src/components/viewer/ViewportContainer.tsx +20 -1
- package/src/components/viewer/useAnimationLoop.ts +19 -1
- package/src/disable-react-dev-perf-track.ts +38 -0
- package/src/hooks/has-entity-type.ts +33 -0
- package/src/hooks/ids/idsWorkerClient.ts +136 -0
- package/src/hooks/ingest/federationAlign.ts +1 -1
- package/src/hooks/ingest/pointCloudIngest.ts +22 -98
- package/src/hooks/ingest/viewerModelIngest.ts +8 -13
- package/src/hooks/useAlignmentLines3D.ts +5 -0
- package/src/hooks/useGridLines3D.ts +4 -0
- package/src/hooks/useIDS.ts +77 -13
- package/src/hooks/useIfcCache.ts +1 -1
- package/src/hooks/useIfcFederation.ts +1 -1
- package/src/hooks/useIfcServer.ts +1 -1
- package/src/hooks/useModelSelection.ts +1 -1
- package/src/hooks/useSolarEnvironment.ts +114 -0
- package/src/hooks/useSolarSweep.ts +66 -0
- package/src/hooks/useSymbolicAnnotations.ts +10 -0
- package/src/hooks/useViewerSelectors.ts +1 -1
- package/src/lib/geo/cesium-sun.ts +277 -0
- package/src/lib/geo/solar-direction.test.ts +70 -0
- package/src/lib/geo/solar-direction.ts +94 -0
- package/src/lib/lighting-presets.ts +128 -0
- package/src/lib/recent-files.ts +4 -24
- package/src/lib/solar-time.ts +55 -0
- package/src/main.tsx +5 -0
- package/src/store/index.ts +8 -0
- package/src/store/slices/annotationsSlice.test.ts +0 -16
- package/src/store/slices/cesiumSlice.ts +3 -3
- package/src/store/slices/dataSlice.test.ts +0 -40
- package/src/store/slices/environmentSlice.ts +101 -0
- package/src/store/slices/idsSlice.ts +6 -1
- package/src/store/slices/selectionSlice.test.ts +0 -43
- package/src/store/slices/solarSlice.ts +121 -0
- package/src/store/slices/visibilitySlice.test.ts +15 -45
- package/src/utils/loadingUtils.ts +1 -1
- package/src/workers/idsValidation.worker.ts +98 -0
- package/dist/assets/geometry.worker-DVwFYHTq.js +0 -1
- package/dist/assets/ifc-lite_bg-FPffpFK_.wasm +0 -0
- package/dist/assets/index-DpoJvkdg.css +0 -1
- package/dist/assets/parser.worker-U_PVhLNi.js +0 -182
- package/dist/assets/raw-p_2cfl6T.js +0 -1
- package/dist/assets/server-client-DUMy2mXg.js +0 -719
- package/src/components/ui/context-menu.tsx +0 -174
- package/src/store.ts +0 -80
|
@@ -30,6 +30,8 @@ import {
|
|
|
30
30
|
shouldPreferOrthometricTerrain,
|
|
31
31
|
} from '@/lib/geo/cesium-placement';
|
|
32
32
|
import { getEffectiveHorizontalScale, resolveMapUnitToMetreScale } from '@/lib/geo/geo-scale';
|
|
33
|
+
import { applySolarScene, SunPathDome } from '@/lib/geo/cesium-sun';
|
|
34
|
+
import { sunPosition, sunTimes } from '@ifc-lite/solar';
|
|
33
35
|
|
|
34
36
|
// Lazy-loaded Cesium module and CSS
|
|
35
37
|
let cesiumPromise: Promise<typeof import('cesium')> | null = null;
|
|
@@ -253,9 +255,33 @@ export function CesiumOverlay({
|
|
|
253
255
|
const setCesiumTerrainClipY = useViewerStore((s) => s.setCesiumTerrainClipY);
|
|
254
256
|
const setCesiumGlbLoaded = useViewerStore((s) => s.setCesiumGlbLoaded);
|
|
255
257
|
|
|
258
|
+
// Solar study state — drives the sun-path dome + shadow study.
|
|
259
|
+
const solarEnabled = useViewerStore((s) => s.solarEnabled);
|
|
260
|
+
const solarDateMs = useViewerStore((s) => s.solarDateMs);
|
|
261
|
+
const solarShowSunPath = useViewerStore((s) => s.solarShowSunPath);
|
|
262
|
+
const solarShowShadows = useViewerStore((s) => s.solarShowShadows);
|
|
263
|
+
const setSolarSunInfo = useViewerStore((s) => s.setSolarSunInfo);
|
|
264
|
+
// Environment sky toggle — atmosphere + sun + fog in geo mode.
|
|
265
|
+
const envSkyEnabled = useViewerStore((s) => s.envSkyEnabled);
|
|
266
|
+
// Re-run the solar effect once the deferred GLB load completes, so the IFC
|
|
267
|
+
// model's shadow mode is applied even when the study was enabled before the
|
|
268
|
+
// model finished loading into Cesium.
|
|
269
|
+
const cesiumGlbLoaded = useViewerStore((s) => s.cesiumGlbLoaded);
|
|
270
|
+
|
|
256
271
|
// Track the Cesium model (IFC geometry loaded as glTF for correct world positioning)
|
|
257
|
-
const cesiumModelRef = useRef<{ modelMatrix: any; destroy?: () => void } | null>(null);
|
|
272
|
+
const cesiumModelRef = useRef<{ modelMatrix: any; shadows?: any; destroy?: () => void } | null>(null);
|
|
258
273
|
const glbCacheRef = useRef<{ meshCount: number; glb: Uint8Array } | null>(null);
|
|
274
|
+
// Active 3D context tileset (Google Photorealistic / OSM buildings) — kept so
|
|
275
|
+
// solar mode can toggle its shadow casting/receiving.
|
|
276
|
+
const tilesetRef = useRef<{ shadows?: any } | null>(null);
|
|
277
|
+
// Active sun-path dome entity collection (null when solar study is off).
|
|
278
|
+
const sunPathDomeRef = useRef<SunPathDome | null>(null);
|
|
279
|
+
// UTC calendar day the dome's static geometry (day-arc, analemmas) was built
|
|
280
|
+
// for. Intra-day time scrubs only move the sun marker; a new day rebuilds.
|
|
281
|
+
const sunPathDomeDayRef = useRef<string | null>(null);
|
|
282
|
+
// Whether the solar study has ever touched Cesium scene state. Guards us
|
|
283
|
+
// from mutating the default (non-solar) lighting on plain mount.
|
|
284
|
+
const solarTouchedSceneRef = useRef(false);
|
|
259
285
|
|
|
260
286
|
// Last-known placement altitude (in metres) used to keep the user's WORLD
|
|
261
287
|
// camera position stable across bridge rebuilds. When the user toggles the
|
|
@@ -337,7 +363,9 @@ export function CesiumOverlay({
|
|
|
337
363
|
bottomContainer.style.right = 'auto';
|
|
338
364
|
}
|
|
339
365
|
|
|
340
|
-
// Disable skybox/atmosphere/fog for transparent compositing
|
|
366
|
+
// Disable skybox/atmosphere/fog for transparent compositing.
|
|
367
|
+
// (The Sun & Sky panel's Sky toggle re-enables atmosphere/sun/fog
|
|
368
|
+
// via Effect 4b.)
|
|
341
369
|
if (scene.skyBox) (scene.skyBox as any).show = false;
|
|
342
370
|
if (scene.sun) scene.sun.show = false;
|
|
343
371
|
if (scene.moon) scene.moon.show = false;
|
|
@@ -346,7 +374,22 @@ export function CesiumOverlay({
|
|
|
346
374
|
scene.globe.showGroundAtmosphere = false;
|
|
347
375
|
scene.backgroundColor = Cesium.Color.TRANSPARENT;
|
|
348
376
|
scene.globe.baseColor = Cesium.Color.TRANSPARENT;
|
|
349
|
-
|
|
377
|
+
if (dataSource === 'osm-buildings') {
|
|
378
|
+
// OSM massing context: keep the globe with the satellite base map —
|
|
379
|
+
// the extruded buildings sit ON TOP of the imagery, and the globe
|
|
380
|
+
// is what receives their cast shadows during a sun study.
|
|
381
|
+
scene.globe.show = true;
|
|
382
|
+
scene.globe.shadows = Cesium.ShadowMode.RECEIVE_ONLY;
|
|
383
|
+
try {
|
|
384
|
+
const imagery = await Cesium.createWorldImageryAsync();
|
|
385
|
+
if (!cancelled) viewer.imageryLayers.addImageryProvider(imagery);
|
|
386
|
+
} catch { /* imagery unavailable — buildings still render */ }
|
|
387
|
+
} else {
|
|
388
|
+
// Photorealistic tiles bring their own ground; the globe would
|
|
389
|
+
// z-fight underneath them.
|
|
390
|
+
scene.globe.show = false;
|
|
391
|
+
}
|
|
392
|
+
if (cancelled) { viewer.destroy(); return; }
|
|
350
393
|
|
|
351
394
|
// Add terrain
|
|
352
395
|
if (terrainEnabled && ionToken) {
|
|
@@ -357,7 +400,7 @@ export function CesiumOverlay({
|
|
|
357
400
|
}
|
|
358
401
|
|
|
359
402
|
// Add data source layer
|
|
360
|
-
await addDataSourceLayer(Cesium, viewer, dataSource, ionToken);
|
|
403
|
+
tilesetRef.current = await addDataSourceLayer(Cesium, viewer, dataSource, ionToken);
|
|
361
404
|
|
|
362
405
|
if (cancelled) { viewer.destroy(); return; }
|
|
363
406
|
|
|
@@ -386,6 +429,11 @@ export function CesiumOverlay({
|
|
|
386
429
|
// so Effect 2c must re-load the GLB into the next viewer instance.
|
|
387
430
|
cesiumModelRef.current = null;
|
|
388
431
|
bridgeRef.current = null;
|
|
432
|
+
// The destroyed viewer also took the tileset + sun-path entities.
|
|
433
|
+
tilesetRef.current = null;
|
|
434
|
+
sunPathDomeRef.current = null;
|
|
435
|
+
sunPathDomeDayRef.current = null;
|
|
436
|
+
solarTouchedSceneRef.current = false;
|
|
389
437
|
setStatus('idle');
|
|
390
438
|
};
|
|
391
439
|
}, [cesiumEnabled, ionToken, terrainEnabled, dataSource]);
|
|
@@ -664,6 +712,133 @@ export function CesiumOverlay({
|
|
|
664
712
|
// bridge.modelOrigin.height by Effect 2.
|
|
665
713
|
}, [mapConversion, projectedCRS, coordinateInfo, lengthUnitScale, bridgeVersion]);
|
|
666
714
|
|
|
715
|
+
// ─── Effect 4: Solar study — sun-path dome + shadows ────────────────────
|
|
716
|
+
// Drives Cesium's sun/lighting/shadow map from the studied instant, builds
|
|
717
|
+
// (and live-updates) the 3D sun-path dome anchored at the model origin, and
|
|
718
|
+
// publishes the resolved sun position/times back to the store for the panel.
|
|
719
|
+
useEffect(() => {
|
|
720
|
+
const viewer = viewerRef.current;
|
|
721
|
+
const bridge = bridgeRef.current;
|
|
722
|
+
const Cesium = cesiumModule;
|
|
723
|
+
if (status !== 'ready' || !viewer || !bridge || !Cesium) return;
|
|
724
|
+
|
|
725
|
+
// Never mutate the default Cesium lighting until the study is first
|
|
726
|
+
// enabled — a plain georeferenced model shouldn't have its context
|
|
727
|
+
// re-lit just because this effect mounts with solar off.
|
|
728
|
+
if (!solarEnabled && !solarTouchedSceneRef.current) return;
|
|
729
|
+
solarTouchedSceneRef.current = true;
|
|
730
|
+
|
|
731
|
+
const date = new Date(solarDateMs);
|
|
732
|
+
const { latitude, longitude, height } = bridge.modelOrigin;
|
|
733
|
+
|
|
734
|
+
// Cast/receive shadows on the IFC model and the context tileset.
|
|
735
|
+
const shadowMode = solarEnabled && solarShowShadows
|
|
736
|
+
? Cesium.ShadowMode.ENABLED
|
|
737
|
+
: Cesium.ShadowMode.DISABLED;
|
|
738
|
+
if (cesiumModelRef.current) cesiumModelRef.current.shadows = shadowMode;
|
|
739
|
+
if (tilesetRef.current) tilesetRef.current.shadows = shadowMode;
|
|
740
|
+
|
|
741
|
+
applySolarScene(Cesium, viewer, {
|
|
742
|
+
date,
|
|
743
|
+
enabled: solarEnabled,
|
|
744
|
+
shadows: solarShowShadows,
|
|
745
|
+
showSun: envSkyEnabled,
|
|
746
|
+
});
|
|
747
|
+
|
|
748
|
+
if (solarEnabled) {
|
|
749
|
+
// Publish the readout for the panel.
|
|
750
|
+
const times = sunTimes(date, latitude, longitude);
|
|
751
|
+
const sp = sunPosition(date, latitude, longitude);
|
|
752
|
+
setSolarSunInfo({
|
|
753
|
+
latitude,
|
|
754
|
+
longitude,
|
|
755
|
+
azimuth: sp.azimuth,
|
|
756
|
+
altitude: sp.altitude,
|
|
757
|
+
sunriseMs: times.sunrise ? times.sunrise.getTime() : null,
|
|
758
|
+
sunsetMs: times.sunset ? times.sunset.getTime() : null,
|
|
759
|
+
solarNoonMs: times.solarNoon.getTime(),
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
if (solarShowSunPath) {
|
|
763
|
+
const dayKey = `${date.getUTCFullYear()}-${date.getUTCMonth()}-${date.getUTCDate()}:${latitude.toFixed(4)},${longitude.toFixed(4)}`;
|
|
764
|
+
try {
|
|
765
|
+
if (!sunPathDomeRef.current || sunPathDomeDayRef.current !== dayKey) {
|
|
766
|
+
// New day or site → rebuild static geometry (day arc + analemmas).
|
|
767
|
+
sunPathDomeRef.current?.destroy();
|
|
768
|
+
const bounds = coordinateInfo?.originalBounds;
|
|
769
|
+
// Size the dome to roughly the model footprint, but clamp to an
|
|
770
|
+
// architectural scale: with many federated models the combined
|
|
771
|
+
// bounds can span kilometres, which would push the dome arcs so
|
|
772
|
+
// far out they read as nothing. Half-diagonal ≈ bounding radius.
|
|
773
|
+
const rawRadius = bounds
|
|
774
|
+
? 0.5 * Math.hypot(
|
|
775
|
+
bounds.max.x - bounds.min.x,
|
|
776
|
+
bounds.max.y - bounds.min.y,
|
|
777
|
+
bounds.max.z - bounds.min.z,
|
|
778
|
+
)
|
|
779
|
+
: 80;
|
|
780
|
+
const radius = Math.min(250, Math.max(40, rawRadius));
|
|
781
|
+
sunPathDomeRef.current = new SunPathDome(Cesium, viewer, {
|
|
782
|
+
origin: { latitude, longitude, height },
|
|
783
|
+
radius,
|
|
784
|
+
date,
|
|
785
|
+
showAnalemmas: true,
|
|
786
|
+
});
|
|
787
|
+
sunPathDomeDayRef.current = dayKey;
|
|
788
|
+
} else {
|
|
789
|
+
// Same day, new time → just move the sun marker + beam.
|
|
790
|
+
sunPathDomeRef.current.update(date);
|
|
791
|
+
}
|
|
792
|
+
} catch (err) {
|
|
793
|
+
console.warn('[CesiumOverlay] sun-path dome build/update failed:', err);
|
|
794
|
+
}
|
|
795
|
+
} else if (sunPathDomeRef.current) {
|
|
796
|
+
sunPathDomeRef.current.destroy();
|
|
797
|
+
sunPathDomeRef.current = null;
|
|
798
|
+
sunPathDomeDayRef.current = null;
|
|
799
|
+
}
|
|
800
|
+
} else if (sunPathDomeRef.current) {
|
|
801
|
+
sunPathDomeRef.current.destroy();
|
|
802
|
+
sunPathDomeRef.current = null;
|
|
803
|
+
sunPathDomeDayRef.current = null;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
viewer.scene.requestRender();
|
|
807
|
+
}, [
|
|
808
|
+
status,
|
|
809
|
+
bridgeVersion,
|
|
810
|
+
cesiumGlbLoaded,
|
|
811
|
+
solarEnabled,
|
|
812
|
+
solarDateMs,
|
|
813
|
+
solarShowSunPath,
|
|
814
|
+
solarShowShadows,
|
|
815
|
+
envSkyEnabled,
|
|
816
|
+
coordinateInfo,
|
|
817
|
+
setSolarSunInfo,
|
|
818
|
+
]);
|
|
819
|
+
|
|
820
|
+
// ─── Effect 4b: Sky — atmosphere + sun + fog ────────────────────────────
|
|
821
|
+
// The environment panel's Sky toggle. Init disables all of these for
|
|
822
|
+
// transparent compositing; this effect re-enables them on demand. The
|
|
823
|
+
// area outside the atmosphere stays transparent (skyBox off), so space
|
|
824
|
+
// composites over the app background like the rest of the overlay.
|
|
825
|
+
useEffect(() => {
|
|
826
|
+
const viewer = viewerRef.current;
|
|
827
|
+
const Cesium = cesiumModule;
|
|
828
|
+
if (status !== 'ready' || !viewer || !Cesium) return;
|
|
829
|
+
const scene = viewer.scene;
|
|
830
|
+
if (scene.skyAtmosphere) scene.skyAtmosphere.show = envSkyEnabled;
|
|
831
|
+
scene.fog.enabled = envSkyEnabled;
|
|
832
|
+
// Haze on the satellite base map (no-op while the globe is hidden).
|
|
833
|
+
scene.globe.showGroundAtmosphere = envSkyEnabled && scene.globe.show;
|
|
834
|
+
// Sun billboard only when the solar effect isn't already managing it
|
|
835
|
+
// (applySolarScene runs with showSun and wins on solar state changes).
|
|
836
|
+
if (scene.sun && !solarTouchedSceneRef.current) {
|
|
837
|
+
scene.sun.show = envSkyEnabled;
|
|
838
|
+
}
|
|
839
|
+
scene.requestRender();
|
|
840
|
+
}, [status, envSkyEnabled]);
|
|
841
|
+
|
|
667
842
|
// ─── Effect 3: Camera sync loop ─────────────────────────────────────────
|
|
668
843
|
useEffect(() => {
|
|
669
844
|
if (status !== 'ready') return;
|
|
@@ -745,31 +920,43 @@ export function CesiumOverlay({
|
|
|
745
920
|
}
|
|
746
921
|
|
|
747
922
|
/**
|
|
748
|
-
* Add the
|
|
923
|
+
* Add the selected 3D context layer to the Cesium viewer. Returns the created
|
|
924
|
+
* tileset so callers can toggle its shadow casting/receiving for solar
|
|
925
|
+
* studies (`null` if none could be created).
|
|
749
926
|
*/
|
|
750
927
|
async function addDataSourceLayer(
|
|
751
928
|
Cesium: typeof import('cesium'),
|
|
752
929
|
viewer: InstanceType<typeof import('cesium').Viewer>,
|
|
753
930
|
dataSource: string,
|
|
754
931
|
ionToken: string,
|
|
755
|
-
) {
|
|
932
|
+
): Promise<InstanceType<typeof import('cesium').Cesium3DTileset> | null> {
|
|
756
933
|
try {
|
|
757
934
|
switch (dataSource) {
|
|
935
|
+
case 'osm-buildings': {
|
|
936
|
+
// OpenStreetMap Buildings — flat-shaded extruded footprints, the grey
|
|
937
|
+
// massing context used for sun-path / overshadowing studies.
|
|
938
|
+
const tileset = await Cesium.createOsmBuildingsAsync();
|
|
939
|
+
viewer.scene.primitives.add(tileset);
|
|
940
|
+
return tileset;
|
|
941
|
+
}
|
|
758
942
|
case 'google-photorealistic':
|
|
759
943
|
default: {
|
|
760
944
|
try {
|
|
761
945
|
const tileset = await Cesium.createGooglePhotorealistic3DTileset();
|
|
762
|
-
|
|
946
|
+
viewer.scene.primitives.add(tileset);
|
|
947
|
+
return tileset;
|
|
763
948
|
} catch {
|
|
764
949
|
if (ionToken) {
|
|
765
950
|
const tileset = await Cesium.Cesium3DTileset.fromIonAssetId(2275207);
|
|
766
951
|
viewer.scene.primitives.add(tileset);
|
|
952
|
+
return tileset;
|
|
767
953
|
}
|
|
954
|
+
return null;
|
|
768
955
|
}
|
|
769
|
-
break;
|
|
770
956
|
}
|
|
771
957
|
}
|
|
772
958
|
} catch (err) {
|
|
773
959
|
console.warn('[CesiumOverlay] Failed to add data source:', dataSource, err);
|
|
960
|
+
return null;
|
|
774
961
|
}
|
|
775
962
|
}
|
|
@@ -521,16 +521,32 @@ export function IDSPanel({ onClose }: IDSPanelProps) {
|
|
|
521
521
|
const renderProgress = () => {
|
|
522
522
|
if (!progress) return null;
|
|
523
523
|
|
|
524
|
+
// Validation of large code-list IDS packs runs for many seconds, and
|
|
525
|
+
// a few broad specs dominate the time — so a percentage keyed on spec
|
|
526
|
+
// index sits near 0 for a while. Surface the always-advancing spec
|
|
527
|
+
// counter (and the per-spec entity count) so the panel visibly moves
|
|
528
|
+
// throughout, not just in the back half.
|
|
529
|
+
const specNumber = Math.min(progress.specificationIndex + 1, progress.totalSpecifications);
|
|
530
|
+
const isComplete = progress.phase === 'complete';
|
|
531
|
+
const headline = isComplete
|
|
532
|
+
? 'Validation complete'
|
|
533
|
+
: `Validating specification ${specNumber} of ${progress.totalSpecifications}`;
|
|
534
|
+
const detail =
|
|
535
|
+
progress.phase === 'validating' && progress.totalEntities > 0
|
|
536
|
+
? `Checking ${progress.entitiesProcessed.toLocaleString()} / ${progress.totalEntities.toLocaleString()} entities`
|
|
537
|
+
: progress.phase === 'filtering' && progress.totalEntities > 0
|
|
538
|
+
? `Scanning ${progress.entitiesProcessed.toLocaleString()} / ${progress.totalEntities.toLocaleString()} candidates`
|
|
539
|
+
: progress.phase === 'filtering'
|
|
540
|
+
? 'Finding applicable entities…'
|
|
541
|
+
: null;
|
|
542
|
+
|
|
524
543
|
return (
|
|
525
544
|
<div className="p-3 border-b">
|
|
526
|
-
<div className="flex items-center gap-2 mb-
|
|
527
|
-
<Loader2 className="h-4 w-4 animate-spin" />
|
|
528
|
-
<span className="text-sm">
|
|
529
|
-
{progress.phase === 'filtering' && 'Finding applicable entities...'}
|
|
530
|
-
{progress.phase === 'validating' && `Validating... (${progress.entitiesProcessed}/${progress.totalEntities})`}
|
|
531
|
-
{progress.phase === 'complete' && 'Complete'}
|
|
532
|
-
</span>
|
|
545
|
+
<div className="flex items-center gap-2 mb-1">
|
|
546
|
+
{!isComplete && <Loader2 className="h-4 w-4 animate-spin shrink-0" />}
|
|
547
|
+
<span className="text-sm font-medium tabular-nums">{headline}</span>
|
|
533
548
|
</div>
|
|
549
|
+
{detail && <div className="text-xs text-muted-foreground mb-2 tabular-nums">{detail}</div>}
|
|
534
550
|
<Progress value={progress.percentage} className="h-2" />
|
|
535
551
|
</div>
|
|
536
552
|
);
|
|
@@ -43,6 +43,7 @@ import {
|
|
|
43
43
|
FileCode2,
|
|
44
44
|
CalendarClock,
|
|
45
45
|
Globe2,
|
|
46
|
+
Sun,
|
|
46
47
|
Move,
|
|
47
48
|
PenLine,
|
|
48
49
|
Layers3,
|
|
@@ -515,6 +516,12 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
515
516
|
const toggleCesium = useViewerStore((state) => state.toggleCesium);
|
|
516
517
|
const cesiumPlacementEditMode = useViewerStore((state) => state.cesiumPlacementEditMode);
|
|
517
518
|
const setCesiumPlacementEditMode = useViewerStore((state) => state.setCesiumPlacementEditMode);
|
|
519
|
+
// Sun & Sky panel state (sky, lighting presets, sun-path study)
|
|
520
|
+
const solarEnabled = useViewerStore((state) => state.solarEnabled);
|
|
521
|
+
const envPanelOpen = useViewerStore((state) => state.envPanelOpen);
|
|
522
|
+
const toggleEnvPanel = useViewerStore((state) => state.toggleEnvPanel);
|
|
523
|
+
const envSkyEnabled = useViewerStore((state) => state.envSkyEnabled);
|
|
524
|
+
const envPreset = useViewerStore((state) => state.envPreset);
|
|
518
525
|
const storeModels = useViewerStore((state) => state.models);
|
|
519
526
|
const analysisExtensionState = useSyncExternalStore(
|
|
520
527
|
subscribeAnalysisExtensions,
|
|
@@ -1624,6 +1631,30 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
1624
1631
|
</>
|
|
1625
1632
|
)}
|
|
1626
1633
|
|
|
1634
|
+
{/* Sun & Sky panel — sky, lighting presets and the sun-path study.
|
|
1635
|
+
Available for every model, georeferenced or not. */}
|
|
1636
|
+
<Tooltip>
|
|
1637
|
+
<TooltipTrigger asChild>
|
|
1638
|
+
<Button
|
|
1639
|
+
variant={envPanelOpen ? 'default' : 'ghost'}
|
|
1640
|
+
size="icon-sm"
|
|
1641
|
+
aria-label={envPanelOpen ? 'Close Sun & Sky panel' : 'Open Sun & Sky panel'}
|
|
1642
|
+
aria-pressed={envPanelOpen}
|
|
1643
|
+
onClick={(e) => {
|
|
1644
|
+
(e.currentTarget as HTMLButtonElement).blur();
|
|
1645
|
+
toggleEnvPanel();
|
|
1646
|
+
}}
|
|
1647
|
+
className={cn(
|
|
1648
|
+
(envPanelOpen || solarEnabled || envSkyEnabled || envPreset !== 'default')
|
|
1649
|
+
&& 'bg-amber-500 text-zinc-950 hover:bg-amber-400',
|
|
1650
|
+
)}
|
|
1651
|
+
>
|
|
1652
|
+
<Sun className="h-4 w-4" />
|
|
1653
|
+
</Button>
|
|
1654
|
+
</TooltipTrigger>
|
|
1655
|
+
<TooltipContent>Sun & sky</TooltipContent>
|
|
1656
|
+
</Tooltip>
|
|
1657
|
+
|
|
1627
1658
|
{/*
|
|
1628
1659
|
Consolidated View dropdown — holds projection toggle, preset
|
|
1629
1660
|
views, and hover tooltips. These are "view options" the user
|