@ifc-lite/viewer 1.18.0 → 1.19.1
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 +19 -16
- package/.turbo/turbo-typecheck.log +1 -1
- package/CHANGELOG.md +444 -0
- package/dist/assets/basketViewActivator-CA2CTcVo.js +71 -0
- package/dist/assets/{bcf-DOG9_WPX.js → bcf-4K724hw0.js} +18 -18
- package/dist/assets/decode-worker-Collf_X_.js +1320 -0
- package/dist/assets/{exporters-B_OBqIyD.js → exporters-xbXqEDlO.js} +2547 -1958
- package/dist/assets/{geometry.worker-xHHy-9DV.js → geometry.worker-DQEZB2rB.js} +1 -1
- package/dist/assets/ids-2WdONLlu.js +2033 -0
- package/dist/assets/ifc-lite_bg-4yUkDRD8.wasm +0 -0
- package/dist/assets/index-BXeEKqJG.css +1 -0
- package/dist/assets/{index-BKq-M3Mk.js → index-D8Epw-e7.js} +51781 -32599
- package/dist/assets/index-XwKzDuw6.js +22 -0
- package/dist/assets/{native-bridge-SHXiQwFW.js → native-bridge-DKmx1z95.js} +2 -2
- package/dist/assets/{sandbox-jez21HtV.js → sandbox-tccwm5Bo.js} +1402 -1329
- package/dist/assets/{server-client-ncOQVNso.js → server-client-LoWPK1N2.js} +1 -1
- package/dist/assets/three-CDRZThFA.js +4057 -0
- package/dist/assets/{wasm-bridge-DyfBSB8z.js → wasm-bridge-BsJGgPMs.js} +1 -1
- package/dist/index.html +8 -7
- package/dist/samples/building-architecture.ifc +453 -0
- package/dist/samples/hello-wall.ifc +1054 -0
- package/dist/samples/infra-bridge.ifc +962 -0
- package/package.json +13 -7
- package/public/samples/building-architecture.ifc +453 -0
- package/public/samples/hello-wall.ifc +1054 -0
- package/public/samples/infra-bridge.ifc +962 -0
- package/src/App.tsx +37 -3
- package/src/components/mcp/HeroScene.tsx +876 -0
- package/src/components/mcp/McpLanding.tsx +1318 -0
- package/src/components/mcp/McpPlayground.tsx +524 -0
- package/src/components/mcp/PlaygroundChat.tsx +1097 -0
- package/src/components/mcp/PlaygroundViewer.tsx +815 -0
- package/src/components/mcp/README.md +171 -0
- package/src/components/mcp/data.ts +659 -0
- package/src/components/mcp/playground-dispatcher.ts +1649 -0
- package/src/components/mcp/playground-files.ts +107 -0
- package/src/components/mcp/playground-uploads.ts +122 -0
- package/src/components/mcp/types.ts +65 -0
- package/src/components/mcp/use-mcp-page.ts +109 -0
- package/src/components/viewer/MainToolbar.tsx +23 -2
- package/src/components/viewer/PointCloudPanel.tsx +174 -0
- package/src/components/viewer/Viewport.tsx +18 -1
- package/src/components/viewer/ViewportContainer.tsx +78 -9
- package/src/components/viewer/ViewportOverlays.tsx +13 -2
- package/src/components/viewer/tools/AddElementOverlay.tsx +43 -2
- package/src/components/viewer/usePointCloudLifecycle.ts +64 -0
- package/src/components/viewer/usePointCloudSync.ts +98 -0
- package/src/generated/mcp-catalog.json +82 -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 +67 -3
- package/src/services/file-dialog.ts +4 -2
- package/src/store/index.ts +10 -1
- package/src/store/slices/pointCloudSlice.ts +102 -0
- package/src/store/types.ts +7 -0
- package/vite.config.ts +7 -0
- package/dist/assets/basketViewActivator-Cm1QEk_R.js +0 -1
- package/dist/assets/ids-DQ5jY0E8.js +0 -1
- package/dist/assets/ifc-lite_bg-ADjKXSms.wasm +0 -0
- package/dist/assets/index-COnQRuqY.css +0 -1
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import { useCallback, useEffect, useRef, useState, useMemo } from 'react';
|
|
10
10
|
import { Renderer, type VisualEnhancementOptions } from '@ifc-lite/renderer';
|
|
11
|
-
import type { MeshData, CoordinateInfo } from '@ifc-lite/geometry';
|
|
11
|
+
import type { MeshData, CoordinateInfo, PointCloudAsset } from '@ifc-lite/geometry';
|
|
12
12
|
import { useViewerStore, resolveEntityRef, type MeasurePoint, type SnapVisualization } from '@/store';
|
|
13
13
|
import {
|
|
14
14
|
useSelectionState,
|
|
@@ -36,6 +36,8 @@ import { useTouchControls, type TouchState } from './useTouchControls.js';
|
|
|
36
36
|
import { useKeyboardControls } from './useKeyboardControls.js';
|
|
37
37
|
import { useAnimationLoop } from './useAnimationLoop.js';
|
|
38
38
|
import { useGeometryStreaming } from './useGeometryStreaming.js';
|
|
39
|
+
import { usePointCloudSync } from './usePointCloudSync.js';
|
|
40
|
+
import { usePointCloudLifecycle } from './usePointCloudLifecycle.js';
|
|
39
41
|
import { useRenderUpdates } from './useRenderUpdates.js';
|
|
40
42
|
|
|
41
43
|
interface ViewportProps {
|
|
@@ -43,6 +45,8 @@ interface ViewportProps {
|
|
|
43
45
|
/** Monotonic counter that increments when geometry changes — used to trigger
|
|
44
46
|
* streaming effects even when the geometry array reference is stable. */
|
|
45
47
|
geometryVersion?: number;
|
|
48
|
+
/** Point cloud assets aggregated across visible federated models. */
|
|
49
|
+
pointClouds?: ReadonlyArray<PointCloudAsset> | null;
|
|
46
50
|
coordinateInfo?: CoordinateInfo;
|
|
47
51
|
computedIsolatedIds?: Set<number> | null;
|
|
48
52
|
modelIdToIndex?: Map<string, number>;
|
|
@@ -56,6 +60,7 @@ interface ViewportProps {
|
|
|
56
60
|
export function Viewport({
|
|
57
61
|
geometry,
|
|
58
62
|
geometryVersion,
|
|
63
|
+
pointClouds,
|
|
59
64
|
coordinateInfo,
|
|
60
65
|
computedIsolatedIds,
|
|
61
66
|
modelIdToIndex,
|
|
@@ -856,6 +861,18 @@ export function Viewport({
|
|
|
856
861
|
onGeometryReleased,
|
|
857
862
|
});
|
|
858
863
|
|
|
864
|
+
usePointCloudSync({
|
|
865
|
+
rendererRef,
|
|
866
|
+
isInitialized,
|
|
867
|
+
pointClouds,
|
|
868
|
+
hasMeshes: (geometry?.length ?? 0) > 0,
|
|
869
|
+
});
|
|
870
|
+
|
|
871
|
+
usePointCloudLifecycle({
|
|
872
|
+
rendererRef,
|
|
873
|
+
isInitialized,
|
|
874
|
+
});
|
|
875
|
+
|
|
859
876
|
useRenderUpdates({
|
|
860
877
|
rendererRef,
|
|
861
878
|
isInitialized,
|
|
@@ -20,8 +20,10 @@ import { openIfcFileDialog } from '@/services/file-dialog';
|
|
|
20
20
|
import { logToDesktopTerminal } from '@/services/desktop-logger';
|
|
21
21
|
import { cacheFileBlobs, formatFileSize, getCachedFile, getRecentFiles, recordRecentFiles, type RecentFileEntry } from '@/lib/recent-files';
|
|
22
22
|
import { isTauri } from '@/lib/platform';
|
|
23
|
-
import {
|
|
24
|
-
import
|
|
23
|
+
import { toast } from '@/components/ui/toast';
|
|
24
|
+
import { describeUnsupportedFormat } from '@/hooks/ingest/pointCloudIngest';
|
|
25
|
+
import { Upload, MousePointer, Layers, Info, Command, AlertTriangle, ChevronDown, ExternalLink, Plus, Clock3, Sparkles, ArrowUpRight } from 'lucide-react';
|
|
26
|
+
import type { MeshData, CoordinateInfo, GeometryResult, PointCloudAsset } from '@ifc-lite/geometry';
|
|
25
27
|
import { type IfcDataStore } from '@ifc-lite/parser';
|
|
26
28
|
import { getEffectiveGeoreference } from '@/lib/geo/effective-georef';
|
|
27
29
|
|
|
@@ -174,6 +176,30 @@ export function ViewportContainer() {
|
|
|
174
176
|
return geometryResult;
|
|
175
177
|
}, [storeModels, geometryResult, modelIdToIndex]);
|
|
176
178
|
|
|
179
|
+
/**
|
|
180
|
+
* Aggregate point clouds across visible models.
|
|
181
|
+
*
|
|
182
|
+
* Phase 0: identity-stamping with modelIndex. Returns the same array
|
|
183
|
+
* reference when nothing has changed so the consumer effect skips work.
|
|
184
|
+
*/
|
|
185
|
+
const mergedPointClouds = useMemo(() => {
|
|
186
|
+
const collected: PointCloudAsset[] = [];
|
|
187
|
+
if (storeModels.size > 0) {
|
|
188
|
+
for (const [modelId, model] of storeModels) {
|
|
189
|
+
if (!model.visible) continue;
|
|
190
|
+
const assets = model.geometryResult?.pointClouds;
|
|
191
|
+
if (!assets || assets.length === 0) continue;
|
|
192
|
+
const modelIndex = modelIdToIndex.get(modelId) ?? 0;
|
|
193
|
+
for (const asset of assets) {
|
|
194
|
+
collected.push(asset.modelIndex === modelIndex ? asset : { ...asset, modelIndex });
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
} else if (geometryResult?.pointClouds) {
|
|
198
|
+
collected.push(...geometryResult.pointClouds);
|
|
199
|
+
}
|
|
200
|
+
return collected;
|
|
201
|
+
}, [storeModels, geometryResult, modelIdToIndex]);
|
|
202
|
+
|
|
177
203
|
// Extract georeferencing info merged with any live mutations (for Cesium overlay).
|
|
178
204
|
// Reacts to: model load, Cesium toggle, and every georef field edit.
|
|
179
205
|
const georef = useMemo(() => {
|
|
@@ -281,12 +307,22 @@ export function ViewportContainer() {
|
|
|
281
307
|
return;
|
|
282
308
|
}
|
|
283
309
|
|
|
284
|
-
// Filter to supported files (IFC, IFCX, GLB)
|
|
285
|
-
const
|
|
310
|
+
// Filter to supported files (IFC, IFCX, GLB, point clouds)
|
|
311
|
+
const allDropped = Array.from(e.dataTransfer.files);
|
|
312
|
+
const supportedFiles = allDropped.filter(
|
|
286
313
|
f => f.name.endsWith('.ifc') || f.name.endsWith('.ifcx') || f.name.endsWith('.glb')
|
|
314
|
+
|| f.name.toLowerCase().endsWith('.las') || f.name.toLowerCase().endsWith('.laz') || f.name.toLowerCase().endsWith('.ply') || f.name.toLowerCase().endsWith('.pcd') || f.name.toLowerCase().endsWith('.e57')
|
|
287
315
|
);
|
|
288
316
|
|
|
289
|
-
if (supportedFiles.length === 0)
|
|
317
|
+
if (supportedFiles.length === 0) {
|
|
318
|
+
// Tell the user *why* — common case is a Recap project / SketchUp
|
|
319
|
+
// file dropped because they assumed our viewer would understand it.
|
|
320
|
+
const explained = allDropped.find((f) => describeUnsupportedFormat(f.name));
|
|
321
|
+
if (explained) {
|
|
322
|
+
toast.error(`${explained.name}: ${describeUnsupportedFormat(explained.name)}`);
|
|
323
|
+
}
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
290
326
|
|
|
291
327
|
recordRecentFiles(supportedFiles.map((file) => ({ name: file.name, size: file.size })));
|
|
292
328
|
void cacheFileBlobs(supportedFiles);
|
|
@@ -318,6 +354,7 @@ export function ViewportContainer() {
|
|
|
318
354
|
// Filter to supported files (IFC, IFCX, GLB)
|
|
319
355
|
const supportedFiles = Array.from(files).filter(
|
|
320
356
|
f => f.name.endsWith('.ifc') || f.name.endsWith('.ifcx') || f.name.endsWith('.glb')
|
|
357
|
+
|| f.name.toLowerCase().endsWith('.las') || f.name.toLowerCase().endsWith('.laz') || f.name.toLowerCase().endsWith('.ply') || f.name.toLowerCase().endsWith('.pcd') || f.name.toLowerCase().endsWith('.e57')
|
|
321
358
|
);
|
|
322
359
|
|
|
323
360
|
if (supportedFiles.length === 0) return;
|
|
@@ -529,7 +566,7 @@ export function ViewportContainer() {
|
|
|
529
566
|
<input
|
|
530
567
|
ref={fileInputRef}
|
|
531
568
|
type="file"
|
|
532
|
-
accept=".ifc,.ifcx,.glb"
|
|
569
|
+
accept=".ifc,.ifcx,.glb,.las,.laz,.ply,.pcd,.e57"
|
|
533
570
|
multiple
|
|
534
571
|
onChange={handleFileSelect}
|
|
535
572
|
className="hidden"
|
|
@@ -698,10 +735,18 @@ export function ViewportContainer() {
|
|
|
698
735
|
IFClite
|
|
699
736
|
</h2>
|
|
700
737
|
<p className="text-zinc-500 dark:text-[#565f89] font-mono text-sm text-center mb-8 border-b border-zinc-200 dark:border-[#3b4261] pb-4 w-full">
|
|
701
|
-
|
|
738
|
+
IFC toolkit for the open web
|
|
702
739
|
</p>
|
|
703
740
|
|
|
704
|
-
{/*
|
|
741
|
+
{/*
|
|
742
|
+
Two-track action area: a primary "open file" track and a
|
|
743
|
+
secondary "drive with LLM" track sit in mirrored slots — same
|
|
744
|
+
width, same vertical rhythm, each followed by its own caption
|
|
745
|
+
line. Reads as one balanced composition instead of a primary
|
|
746
|
+
CTA + a tacked-on link, while keeping the file-open path
|
|
747
|
+
visually dominant via the filled-on-hover treatment.
|
|
748
|
+
*/}
|
|
749
|
+
{/* Track 1 — open / drag */}
|
|
705
750
|
<button
|
|
706
751
|
onClick={async () => {
|
|
707
752
|
if (!webgpu.supported) {
|
|
@@ -737,10 +782,33 @@ export function ViewportContainer() {
|
|
|
737
782
|
<span>{webgpu.checking ? 'Checking WebGPU...' : webgpu.supported ? 'Open .ifc file' : 'WebGPU Required'}</span>
|
|
738
783
|
</button>
|
|
739
784
|
|
|
740
|
-
<p className="mt-
|
|
785
|
+
<p className="mt-2.5 text-[11px] font-mono text-center text-zinc-400 dark:text-[#565f89]">
|
|
741
786
|
{webgpu.supported ? 'or drag & drop anywhere' : 'file upload disabled'}
|
|
742
787
|
</p>
|
|
743
788
|
|
|
789
|
+
{/* Subtle "or" rule — anchors the symmetry between the two tracks */}
|
|
790
|
+
<div className="mt-5 mb-5 w-full flex items-center gap-3 text-[10px] font-mono uppercase tracking-[0.22em] text-zinc-400 dark:text-[#565f89]">
|
|
791
|
+
<span className="h-px flex-1 bg-zinc-200 dark:bg-[#3b4261]" />
|
|
792
|
+
<span>or</span>
|
|
793
|
+
<span className="h-px flex-1 bg-zinc-200 dark:bg-[#3b4261]" />
|
|
794
|
+
</div>
|
|
795
|
+
|
|
796
|
+
{/* Track 2 — agent / MCP. Compact inline pill, self-centred so
|
|
797
|
+
it reads as a meta-link sibling to the primary file-open
|
|
798
|
+
CTA, not a competing full-width button. */}
|
|
799
|
+
<a
|
|
800
|
+
href="/mcp"
|
|
801
|
+
className="group inline-flex self-center items-center gap-1.5 px-3 py-1.5 font-mono text-[11px] border border-dashed border-zinc-300 dark:border-[#3b4261] text-zinc-500 dark:text-[#7a82a5] hover:border-primary hover:text-primary transition-all cursor-pointer"
|
|
802
|
+
>
|
|
803
|
+
<Sparkles className="h-3 w-3 transition-transform group-hover:-translate-y-0.5" />
|
|
804
|
+
<span>Drive with any LLM</span>
|
|
805
|
+
<ArrowUpRight className="h-2.5 w-2.5 opacity-60 transition-transform group-hover:translate-x-0.5 group-hover:-translate-y-0.5" />
|
|
806
|
+
</a>
|
|
807
|
+
|
|
808
|
+
<p className="mt-1.5 text-[10px] font-mono text-center text-zinc-400 dark:text-[#565f89]">
|
|
809
|
+
via MCP · install or try the playground
|
|
810
|
+
</p>
|
|
811
|
+
|
|
744
812
|
{recentFiles.length > 0 && (
|
|
745
813
|
<div className="mt-6 w-full border-t border-zinc-200 dark:border-[#3b4261] pt-4">
|
|
746
814
|
<div className="mb-3 flex items-center gap-2 text-xs font-mono uppercase tracking-[0.2em] text-zinc-400 dark:text-[#565f89]">
|
|
@@ -845,6 +913,7 @@ export function ViewportContainer() {
|
|
|
845
913
|
<Viewport
|
|
846
914
|
geometry={filteredGeometry}
|
|
847
915
|
geometryVersion={geometryVersion}
|
|
916
|
+
pointClouds={mergedPointClouds}
|
|
848
917
|
coordinateInfo={mergedGeometryResult?.coordinateInfo}
|
|
849
918
|
computedIsolatedIds={computedIsolatedIds}
|
|
850
919
|
modelIdToIndex={modelIdToIndex}
|
|
@@ -22,10 +22,11 @@ import { goHomeFromStore } from '@/store/homeView';
|
|
|
22
22
|
import { useIfc } from '@/hooks/useIfc';
|
|
23
23
|
import { cn } from '@/lib/utils';
|
|
24
24
|
import { isTauri } from '@/lib/platform';
|
|
25
|
-
|
|
26
|
-
const isDesktop = isTauri();
|
|
27
25
|
import { ViewCube, type ViewCubeRef } from './ViewCube';
|
|
28
26
|
import { AxisHelper, type AxisHelperRef } from './AxisHelper';
|
|
27
|
+
import { PointCloudPanel } from './PointCloudPanel';
|
|
28
|
+
|
|
29
|
+
const isDesktop = isTauri();
|
|
29
30
|
|
|
30
31
|
export function ViewportOverlays({ hideViewCube = false }: { hideViewCube?: boolean } = {}) {
|
|
31
32
|
const selectedStoreys = useViewerStore((s) => s.selectedStoreys);
|
|
@@ -149,6 +150,7 @@ export function ViewportOverlays({ hideViewCube = false }: { hideViewCube?: bool
|
|
|
149
150
|
|
|
150
151
|
return (
|
|
151
152
|
<>
|
|
153
|
+
<PointCloudPanelMount />
|
|
152
154
|
{/* Bottom-right: Cesium settings overlay OR Navigation controls (Cesium is web-only) */}
|
|
153
155
|
{cesiumEnabled && !isDesktop ? (
|
|
154
156
|
<CesiumSettingsOverlay
|
|
@@ -314,3 +316,12 @@ function CesiumSettingsOverlay({
|
|
|
314
316
|
</div>
|
|
315
317
|
);
|
|
316
318
|
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Tiny indirection so the panel can subscribe to its own slice without
|
|
322
|
+
* pulling extra state into the parent overlay component.
|
|
323
|
+
*/
|
|
324
|
+
function PointCloudPanelMount() {
|
|
325
|
+
const count = useViewerStore((s) => s.pointCloudAssetCount);
|
|
326
|
+
return <PointCloudPanel assetCount={count} />;
|
|
327
|
+
}
|
|
@@ -51,14 +51,54 @@ export function AddElementOverlay() {
|
|
|
51
51
|
// hover points each frame. The tick state is just a number that
|
|
52
52
|
// forces a re-render; the projection itself is read fresh from the
|
|
53
53
|
// store callback.
|
|
54
|
+
//
|
|
55
|
+
// Two perf gates:
|
|
56
|
+
// 1. Skip the loop entirely when there's nothing to project.
|
|
57
|
+
// pendingPoints / hoverPoint / autoSpacePreview already trigger
|
|
58
|
+
// React re-renders via the store, so the only reason we'd need
|
|
59
|
+
// a per-frame tick is to track the camera while content exists.
|
|
60
|
+
// 2. Only re-render when the camera actually moved since last tick.
|
|
61
|
+
// A held tool with a static camera does ~0 work.
|
|
62
|
+
const getViewpoint = useViewerStore((s) => s.cameraCallbacks.getViewpoint);
|
|
63
|
+
const hasOverlayContent =
|
|
64
|
+
pendingPoints.length > 0 ||
|
|
65
|
+
hoverPoint !== null ||
|
|
66
|
+
(autoSpacePreview != null && autoSpacePreview.outlines.length > 0);
|
|
54
67
|
const [frameTick, setFrameTick] = useState(0);
|
|
55
68
|
const rafRef = useRef<number | null>(null);
|
|
69
|
+
const lastViewpointRef = useRef<{
|
|
70
|
+
px: number; py: number; pz: number;
|
|
71
|
+
tx: number; ty: number; tz: number;
|
|
72
|
+
fov: number;
|
|
73
|
+
} | null>(null);
|
|
56
74
|
useEffect(() => {
|
|
57
75
|
if (activeTool !== 'addElement') return;
|
|
76
|
+
if (!hasOverlayContent) return;
|
|
58
77
|
let mounted = true;
|
|
59
78
|
const loop = () => {
|
|
60
79
|
if (!mounted) return;
|
|
61
|
-
|
|
80
|
+
const vp = getViewpoint?.();
|
|
81
|
+
if (vp) {
|
|
82
|
+
const last = lastViewpointRef.current;
|
|
83
|
+
const moved =
|
|
84
|
+
!last ||
|
|
85
|
+
last.px !== vp.position.x || last.py !== vp.position.y || last.pz !== vp.position.z ||
|
|
86
|
+
last.tx !== vp.target.x || last.ty !== vp.target.y || last.tz !== vp.target.z ||
|
|
87
|
+
last.fov !== vp.fov;
|
|
88
|
+
if (moved) {
|
|
89
|
+
lastViewpointRef.current = {
|
|
90
|
+
px: vp.position.x, py: vp.position.y, pz: vp.position.z,
|
|
91
|
+
tx: vp.target.x, ty: vp.target.y, tz: vp.target.z,
|
|
92
|
+
fov: vp.fov,
|
|
93
|
+
};
|
|
94
|
+
setFrameTick((t) => (t + 1) & 0xffff);
|
|
95
|
+
}
|
|
96
|
+
} else {
|
|
97
|
+
// Fallback for environments without getViewpoint — preserves the
|
|
98
|
+
// pre-fix behaviour of an unconditional tick so the projection
|
|
99
|
+
// can't get stuck stale.
|
|
100
|
+
setFrameTick((t) => (t + 1) & 0xffff);
|
|
101
|
+
}
|
|
62
102
|
rafRef.current = requestAnimationFrame(loop);
|
|
63
103
|
};
|
|
64
104
|
rafRef.current = requestAnimationFrame(loop);
|
|
@@ -66,8 +106,9 @@ export function AddElementOverlay() {
|
|
|
66
106
|
mounted = false;
|
|
67
107
|
if (rafRef.current !== null) cancelAnimationFrame(rafRef.current);
|
|
68
108
|
rafRef.current = null;
|
|
109
|
+
lastViewpointRef.current = null;
|
|
69
110
|
};
|
|
70
|
-
}, [activeTool]);
|
|
111
|
+
}, [activeTool, hasOverlayContent, getViewpoint]);
|
|
71
112
|
|
|
72
113
|
const projection = useMemo(
|
|
73
114
|
() => makeProjection(projectToScreen),
|
|
@@ -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,82 @@
|
|
|
1
|
+
{
|
|
2
|
+
"generatedAt": "2026-05-03T09:30:00Z",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"tools": [
|
|
5
|
+
{ "name": "model_info", "category": "Discovery", "scope": "read", "description": "Schema, entity counts, units, georeferencing — the at-a-glance summary of a loaded IFC.", "inputSchema": { "type": "object", "properties": { "model_id": { "type": "string" } } } },
|
|
6
|
+
{ "name": "model_list", "category": "Discovery", "scope": "read", "description": "List every model loaded in the current MCP session.", "inputSchema": { "type": "object" } },
|
|
7
|
+
{ "name": "model_load", "category": "Discovery", "scope": "mutate", "description": "Load an additional .ifc from disk into the federated session.", "inputSchema": { "type": "object", "required": ["file_path"], "properties": { "file_path": { "type": "string" }, "model_id": { "type": "string" } } } },
|
|
8
|
+
{ "name": "model_unload", "category": "Discovery", "scope": "mutate", "description": "Drop a model from the registry; frees memory.", "inputSchema": { "type": "object", "required": ["model_id"], "properties": { "model_id": { "type": "string" } } } },
|
|
9
|
+
{ "name": "schema_describe", "category": "Discovery", "scope": "read", "description": "Attributes, parent class, and inheritance for any IfcType — useful before mutating.", "inputSchema": { "type": "object", "required": ["type"], "properties": { "type": { "type": "string" }, "include_inherited": { "type": "boolean" } } } },
|
|
10
|
+
|
|
11
|
+
{ "name": "query_entities", "category": "Query", "scope": "read", "description": "Type + property filters with pagination. Returns expressId, GlobalId, name, type for each match.", "inputSchema": { "type": "object", "properties": { "type": { "type": "string" }, "limit": { "type": "integer" } } } },
|
|
12
|
+
{ "name": "count_entities", "category": "Query", "scope": "read", "description": "Group counts by type, storey, or material — histogram form, not the full set.", "inputSchema": { "type": "object", "properties": { "group_by": { "type": "string", "enum": ["type", "storey", "material"] } } } },
|
|
13
|
+
{ "name": "get_entity", "category": "Query", "scope": "read", "description": "Full attributes + property sets for one entity by GlobalId or expressId.", "inputSchema": { "type": "object", "properties": { "global_id": { "type": "string" }, "express_id": { "type": "integer" } } } },
|
|
14
|
+
{ "name": "get_entities_bulk", "category": "Query", "scope": "read", "description": "Batched get_entity for up to 200 ids at once.", "inputSchema": { "type": "object", "required": ["global_ids"], "properties": { "global_ids": { "type": "array", "items": { "type": "string" } } } } },
|
|
15
|
+
{ "name": "spatial_hierarchy", "category": "Query", "scope": "read", "description": "Project → site → building → storey → space tree, with element counts at each node.", "inputSchema": { "type": "object" } },
|
|
16
|
+
{ "name": "containment_chain", "category": "Query", "scope": "read", "description": "Walk up the spatial chain for one entity (storey + parent + grandparent…).", "inputSchema": { "type": "object", "required": ["global_id"] } },
|
|
17
|
+
{ "name": "relationships", "category": "Query", "scope": "read", "description": "Voids, fills, groups, connections — every IfcRel touching this entity.", "inputSchema": { "type": "object", "required": ["global_id"] } },
|
|
18
|
+
{ "name": "properties_unique", "category": "Query", "scope": "read", "description": "Unique values + counts for one property across a type set (perfect for filter UIs).", "inputSchema": { "type": "object", "required": ["type", "pset", "property"] } },
|
|
19
|
+
{ "name": "materials_list", "category": "Query", "scope": "read", "description": "Distinct materials across the model with usage counts.", "inputSchema": { "type": "object" } },
|
|
20
|
+
{ "name": "classifications_list","category": "Query", "scope": "read", "description": "Distinct classification references (system + identification) and how often each is used.", "inputSchema": { "type": "object" } },
|
|
21
|
+
{ "name": "georeferencing", "category": "Query", "scope": "read", "description": "MapConversion, projected CRS, project north, true north — the geo handshake.", "inputSchema": { "type": "object" } },
|
|
22
|
+
{ "name": "units", "category": "Query", "scope": "read", "description": "Length unit scale + the unit assignments declared in the file.", "inputSchema": { "type": "object" } },
|
|
23
|
+
|
|
24
|
+
{ "name": "geometry_bbox", "category": "Geometry", "scope": "read", "description": "Per-entity axis-aligned bounding box (read from quantity sets when available).", "inputSchema": { "type": "object", "required": ["global_id"] } },
|
|
25
|
+
{ "name": "geometry_volume", "category": "Geometry", "scope": "read", "description": "Net/gross volume in m³ for a single entity or a type aggregate.", "inputSchema": { "type": "object", "required": ["global_id"] } },
|
|
26
|
+
{ "name": "geometry_area", "category": "Geometry", "scope": "read", "description": "Surface area for an entity (front/side/footprint depending on what the IFC carries).", "inputSchema": { "type": "object", "required": ["global_id"] } },
|
|
27
|
+
|
|
28
|
+
{ "name": "model_audit", "category": "Validation", "scope": "read", "description": "Out-of-the-box health score + a list of issues (missing GlobalIds, broken refs, orphan entities).", "inputSchema": { "type": "object" } },
|
|
29
|
+
{ "name": "ids_validate", "category": "Validation", "scope": "read", "description": "Run a buildingSMART IDS spec against the loaded model. Per-spec pass/fail with offending entities.", "inputSchema": { "type": "object", "required": ["ids_path"], "properties": { "ids_path": { "type": "string" } } } },
|
|
30
|
+
{ "name": "ids_explain", "category": "Validation", "scope": "read", "description": "Parse + summarize an IDS file in plain language — what each spec asks for, in what order.", "inputSchema": { "type": "object", "required": ["ids_path"] } },
|
|
31
|
+
|
|
32
|
+
{ "name": "entity_set_property", "category": "Mutation", "scope": "mutate", "description": "Queue a Pset.property write on one entity. Persist later via export_ifc / model_save.", "inputSchema": { "type": "object", "required": ["pset", "name"] } },
|
|
33
|
+
{ "name": "entity_delete_property", "category": "Mutation","scope": "mutate", "description": "Queue a property removal from a Pset. Reversible via mutation_undo.", "inputSchema": { "type": "object", "required": ["pset", "name"] } },
|
|
34
|
+
{ "name": "entity_set_attribute","category": "Mutation", "scope": "mutate", "description": "Set Name, Description, ObjectType, or Tag on an entity.", "inputSchema": { "type": "object", "required": ["attribute", "value"] } },
|
|
35
|
+
{ "name": "entity_create", "category": "Mutation", "scope": "mutate", "description": "Create a new IFC entity with raw STEP attributes and get back its expressId.", "inputSchema": { "type": "object", "required": ["type"] } },
|
|
36
|
+
{ "name": "entity_delete", "category": "Mutation", "scope": "mutate", "description": "Delete an entity by expressId/GlobalId. Caller is responsible for cascading rels.", "inputSchema": { "type": "object" } },
|
|
37
|
+
{ "name": "mutation_batch", "category": "Mutation", "scope": "mutate", "description": "Apply N mutation ops in order, returning per-step results.", "inputSchema": { "type": "object", "required": ["operations"] } },
|
|
38
|
+
{ "name": "mutation_diff", "category": "Mutation", "scope": "read", "description": "Inspect every queued mutation vs the original parsed state.", "inputSchema": { "type": "object" } },
|
|
39
|
+
{ "name": "mutation_undo", "category": "Mutation", "scope": "mutate", "description": "Pop the last N pending mutations off the queue.", "inputSchema": { "type": "object", "properties": { "n": { "type": "integer" } } } },
|
|
40
|
+
{ "name": "model_save", "category": "Mutation", "scope": "mutate", "description": "Write the current model (with pending mutations) back to .ifc.", "inputSchema": { "type": "object", "required": ["file_path"] } },
|
|
41
|
+
|
|
42
|
+
{ "name": "bcf_topic_list", "category": "BCF", "scope": "read", "description": "List BCF topics in this session, optionally filtered by status.", "inputSchema": { "type": "object" } },
|
|
43
|
+
{ "name": "bcf_topic_create", "category": "BCF", "scope": "mutate", "description": "Create a topic with title/description/priority and get the GUID for follow-ups.", "inputSchema": { "type": "object", "required": ["title"] } },
|
|
44
|
+
{ "name": "bcf_topic_update", "category": "BCF", "scope": "mutate", "description": "Update topic fields or append a comment.", "inputSchema": { "type": "object", "required": ["guid"] } },
|
|
45
|
+
{ "name": "bcf_topic_close", "category": "BCF", "scope": "mutate", "description": "Mark a topic resolved (status=Closed).", "inputSchema": { "type": "object", "required": ["guid"] } },
|
|
46
|
+
{ "name": "bcf_viewpoint_create","category": "BCF", "scope": "mutate", "description": "Attach a selection-based viewpoint (or full viewer state) to a topic.", "inputSchema": { "type": "object", "required": ["guid"] } },
|
|
47
|
+
{ "name": "bcf_export", "category": "BCF", "scope": "export", "description": "Export the in-memory BCF project as a .bcfzip file.", "inputSchema": { "type": "object", "required": ["file_path"] } },
|
|
48
|
+
|
|
49
|
+
{ "name": "bsdd_search", "category": "bSDD", "scope": "read", "description": "Full-text search the buildingSMART Data Dictionary for classes by keyword.", "inputSchema": { "type": "object", "required": ["query"] } },
|
|
50
|
+
{ "name": "bsdd_class", "category": "bSDD", "scope": "read", "description": "Full bSDD class info for an IFC entity name (definition, parent, properties).", "inputSchema": { "type": "object", "required": ["ifc_type"] } },
|
|
51
|
+
{ "name": "bsdd_property_sets", "category": "bSDD", "scope": "read", "description": "Pset_* groups for an IFC type (Pset_WallCommon for IfcWall, etc.).", "inputSchema": { "type": "object", "required": ["ifc_type"] } },
|
|
52
|
+
{ "name": "bsdd_match", "category": "bSDD", "scope": "read", "description": "Suggest matching bSDD classes for an entity in the loaded model.", "inputSchema": { "type": "object" } },
|
|
53
|
+
|
|
54
|
+
{ "name": "model_diff", "category": "Diff", "scope": "read", "description": "Compare two loaded models. Reports added/removed entities by GlobalId and per-type count deltas.", "inputSchema": { "type": "object", "required": ["a", "b"] } },
|
|
55
|
+
{ "name": "quantity_diff", "category": "Diff", "scope": "read", "description": "Per-entity-type quantity comparison between two models, optionally grouped by storey.", "inputSchema": { "type": "object", "required": ["a", "b"] } },
|
|
56
|
+
|
|
57
|
+
{ "name": "export_ifc", "category": "Export", "scope": "export", "description": "Write the model (with pending mutations) to .ifc/.ifczip on disk.", "inputSchema": { "type": "object", "required": ["file_path"] } },
|
|
58
|
+
{ "name": "export_csv", "category": "Export", "scope": "export", "description": "Tabular property/quantity export. Columns may be Pset_X.Property paths.", "inputSchema": { "type": "object" } },
|
|
59
|
+
{ "name": "export_json", "category": "Export", "scope": "export", "description": "Structured JSON dump of attributes/properties/quantities for a type set.", "inputSchema": { "type": "object" } },
|
|
60
|
+
{ "name": "export_glb", "category": "Export", "scope": "export", "description": "(v0.2) Geometry export to glTF binary — needs the WASM mesh pipeline.", "inputSchema": { "type": "object" } },
|
|
61
|
+
{ "name": "export_ifcx", "category": "Export", "scope": "export", "description": "(v0.2) Export to the new IFCx interchange format.", "inputSchema": { "type": "object" } },
|
|
62
|
+
{ "name": "export_pdf_report", "category": "Export", "scope": "export", "description": "(v0.5) Branded PDF audit report with charts.", "inputSchema": { "type": "object" } },
|
|
63
|
+
|
|
64
|
+
{ "name": "viewer_ask", "category": "Viewer", "scope": "read", "description": "Suggest wording the agent can use to ask the user for permission to open the viewer.", "inputSchema": { "type": "object" } },
|
|
65
|
+
{ "name": "viewer_open", "category": "Viewer", "scope": "read", "description": "Boot the in-process WebGL viewer and return its URL for the user to open.", "inputSchema": { "type": "object" } },
|
|
66
|
+
{ "name": "viewer_close", "category": "Viewer", "scope": "read", "description": "Stop the viewer + clear its selection state.", "inputSchema": { "type": "object" } },
|
|
67
|
+
{ "name": "viewer_status", "category": "Viewer", "scope": "read", "description": "Report whether the viewer is open, on what port, and the current selection.", "inputSchema": { "type": "object" } },
|
|
68
|
+
{ "name": "viewer_colorize", "category": "Viewer", "scope": "read", "description": "Paint a set of entities with a color (hex / rgb / named).", "inputSchema": { "type": "object", "required": ["color"] } },
|
|
69
|
+
{ "name": "viewer_isolate", "category": "Viewer", "scope": "read", "description": "Hide everything except the picked set.", "inputSchema": { "type": "object" } },
|
|
70
|
+
{ "name": "viewer_hide", "category": "Viewer", "scope": "read", "description": "Hide the picked set; everything else stays.", "inputSchema": { "type": "object" } },
|
|
71
|
+
{ "name": "viewer_show", "category": "Viewer", "scope": "read", "description": "Show the picked set (un-hide).", "inputSchema": { "type": "object" } },
|
|
72
|
+
{ "name": "viewer_reset", "category": "Viewer", "scope": "read", "description": "Reset visibility + colors to the model defaults.", "inputSchema": { "type": "object" } },
|
|
73
|
+
{ "name": "viewer_fly_to", "category": "Viewer", "scope": "read", "description": "Frame the camera on a set of entities or a bbox.", "inputSchema": { "type": "object" } },
|
|
74
|
+
{ "name": "viewer_set_section", "category": "Viewer", "scope": "read", "description": "Apply an axis-aligned section plane.", "inputSchema": { "type": "object", "required": ["axis", "position"] } },
|
|
75
|
+
{ "name": "viewer_clear_section","category": "Viewer", "scope": "read", "description": "Remove the active section plane.", "inputSchema": { "type": "object" } },
|
|
76
|
+
{ "name": "viewer_color_by_storey","category": "Viewer", "scope": "read", "description": "Apply a per-storey overlay (built-in viewer preset).", "inputSchema": { "type": "object" } },
|
|
77
|
+
{ "name": "viewer_color_by_property","category": "Viewer", "scope": "read", "description": "Color a type set by property value, returns a legend the agent can describe.", "inputSchema": { "type": "object", "required": ["type", "pset", "property"] } },
|
|
78
|
+
{ "name": "viewer_get_selection","category": "Viewer", "scope": "read", "description": "Return the current selection — type, expressId, GlobalId, name, attributes, materials.", "inputSchema": { "type": "object" } },
|
|
79
|
+
{ "name": "viewer_describe_selection","category": "Viewer","scope": "read", "description": "Kitchen-sink: every section (attributes, properties, quantities, classifications, materials) for the picked entity.", "inputSchema": { "type": "object" } },
|
|
80
|
+
{ "name": "viewer_wait_for_selection","category": "Viewer","scope": "read", "description": "Block until the user picks something in the viewer (or timeout).", "inputSchema": { "type": "object" } }
|
|
81
|
+
]
|
|
82
|
+
}
|