@ifc-lite/mcp 0.2.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/LICENSE +373 -0
- package/README.md +186 -0
- package/dist/auth/index.d.ts +3 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +5 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/auth/scope.d.ts +24 -0
- package/dist/auth/scope.d.ts.map +1 -0
- package/dist/auth/scope.js +26 -0
- package/dist/auth/scope.js.map +1 -0
- package/dist/browser.d.ts +37 -0
- package/dist/browser.d.ts.map +1 -0
- package/dist/browser.js +37 -0
- package/dist/browser.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +233 -0
- package/dist/cli.js.map +1 -0
- package/dist/context.d.ts +87 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +41 -0
- package/dist/context.js.map +1 -0
- package/dist/errors.d.ts +37 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +49 -0
- package/dist/errors.js.map +1 -0
- package/dist/headless-backend.d.ts +61 -0
- package/dist/headless-backend.d.ts.map +1 -0
- package/dist/headless-backend.js +472 -0
- package/dist/headless-backend.js.map +1 -0
- package/dist/index.d.ts +39 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +49 -0
- package/dist/index.js.map +1 -0
- package/dist/loader.d.ts +9 -0
- package/dist/loader.d.ts.map +1 -0
- package/dist/loader.js +76 -0
- package/dist/loader.js.map +1 -0
- package/dist/prompts/index.d.ts +6 -0
- package/dist/prompts/index.d.ts.map +1 -0
- package/dist/prompts/index.js +13 -0
- package/dist/prompts/index.js.map +1 -0
- package/dist/prompts/templates.d.ts +21 -0
- package/dist/prompts/templates.d.ts.map +1 -0
- package/dist/prompts/templates.js +238 -0
- package/dist/prompts/templates.js.map +1 -0
- package/dist/prompts/types.d.ts +17 -0
- package/dist/prompts/types.d.ts.map +1 -0
- package/dist/prompts/types.js +27 -0
- package/dist/prompts/types.js.map +1 -0
- package/dist/protocol/index.d.ts +3 -0
- package/dist/protocol/index.d.ts.map +1 -0
- package/dist/protocol/index.js +6 -0
- package/dist/protocol/index.js.map +1 -0
- package/dist/protocol/jsonrpc.d.ts +12 -0
- package/dist/protocol/jsonrpc.d.ts.map +1 -0
- package/dist/protocol/jsonrpc.js +62 -0
- package/dist/protocol/jsonrpc.js.map +1 -0
- package/dist/protocol/types.d.ts +223 -0
- package/dist/protocol/types.d.ts.map +1 -0
- package/dist/protocol/types.js +37 -0
- package/dist/protocol/types.js.map +1 -0
- package/dist/resources/index.d.ts +6 -0
- package/dist/resources/index.d.ts.map +1 -0
- package/dist/resources/index.js +13 -0
- package/dist/resources/index.js.map +1 -0
- package/dist/resources/providers.d.ts +3 -0
- package/dist/resources/providers.d.ts.map +1 -0
- package/dist/resources/providers.js +270 -0
- package/dist/resources/providers.js.map +1 -0
- package/dist/resources/types.d.ts +29 -0
- package/dist/resources/types.d.ts.map +1 -0
- package/dist/resources/types.js +30 -0
- package/dist/resources/types.js.map +1 -0
- package/dist/server.d.ts +93 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +396 -0
- package/dist/server.js.map +1 -0
- package/dist/tools/bcf.d.ts +3 -0
- package/dist/tools/bcf.d.ts.map +1 -0
- package/dist/tools/bcf.js +208 -0
- package/dist/tools/bcf.js.map +1 -0
- package/dist/tools/bsdd.d.ts +3 -0
- package/dist/tools/bsdd.d.ts.map +1 -0
- package/dist/tools/bsdd.js +267 -0
- package/dist/tools/bsdd.js.map +1 -0
- package/dist/tools/diff.d.ts +3 -0
- package/dist/tools/diff.d.ts.map +1 -0
- package/dist/tools/diff.js +149 -0
- package/dist/tools/diff.js.map +1 -0
- package/dist/tools/discovery.d.ts +8 -0
- package/dist/tools/discovery.d.ts.map +1 -0
- package/dist/tools/discovery.js +181 -0
- package/dist/tools/discovery.js.map +1 -0
- package/dist/tools/export.d.ts +3 -0
- package/dist/tools/export.d.ts.map +1 -0
- package/dist/tools/export.js +157 -0
- package/dist/tools/export.js.map +1 -0
- package/dist/tools/geometry.d.ts +3 -0
- package/dist/tools/geometry.d.ts.map +1 -0
- package/dist/tools/geometry.js +252 -0
- package/dist/tools/geometry.js.map +1 -0
- package/dist/tools/ids-accessor.d.ts +9 -0
- package/dist/tools/ids-accessor.d.ts.map +1 -0
- package/dist/tools/ids-accessor.js +131 -0
- package/dist/tools/ids-accessor.js.map +1 -0
- package/dist/tools/index.d.ts +21 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +36 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/mutate.d.ts +3 -0
- package/dist/tools/mutate.d.ts.map +1 -0
- package/dist/tools/mutate.js +350 -0
- package/dist/tools/mutate.js.map +1 -0
- package/dist/tools/query.d.ts +3 -0
- package/dist/tools/query.d.ts.map +1 -0
- package/dist/tools/query.js +549 -0
- package/dist/tools/query.js.map +1 -0
- package/dist/tools/types.d.ts +18 -0
- package/dist/tools/types.d.ts.map +1 -0
- package/dist/tools/types.js +26 -0
- package/dist/tools/types.js.map +1 -0
- package/dist/tools/util.d.ts +19 -0
- package/dist/tools/util.d.ts.map +1 -0
- package/dist/tools/util.js +53 -0
- package/dist/tools/util.js.map +1 -0
- package/dist/tools/validation.d.ts +3 -0
- package/dist/tools/validation.d.ts.map +1 -0
- package/dist/tools/validation.js +244 -0
- package/dist/tools/validation.js.map +1 -0
- package/dist/tools/viewer.d.ts +3 -0
- package/dist/tools/viewer.d.ts.map +1 -0
- package/dist/tools/viewer.js +651 -0
- package/dist/tools/viewer.js.map +1 -0
- package/dist/transport/http.d.ts +64 -0
- package/dist/transport/http.d.ts.map +1 -0
- package/dist/transport/http.js +268 -0
- package/dist/transport/http.js.map +1 -0
- package/dist/transport/in-process.d.ts +25 -0
- package/dist/transport/in-process.d.ts.map +1 -0
- package/dist/transport/in-process.js +48 -0
- package/dist/transport/in-process.js.map +1 -0
- package/dist/transport/stdio.d.ts +33 -0
- package/dist/transport/stdio.d.ts.map +1 -0
- package/dist/transport/stdio.js +86 -0
- package/dist/transport/stdio.js.map +1 -0
- package/dist/validate.d.ts +23 -0
- package/dist/validate.d.ts.map +1 -0
- package/dist/validate.js +88 -0
- package/dist/validate.js.map +1 -0
- package/dist/viewer-manager.d.ts +51 -0
- package/dist/viewer-manager.d.ts.map +1 -0
- package/dist/viewer-manager.js +183 -0
- package/dist/viewer-manager.js.map +1 -0
- package/package.json +86 -0
|
@@ -0,0 +1,651 @@
|
|
|
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
|
+
* Viewer tools — open the WebGL viewer, drive it from the agent, and
|
|
6
|
+
* surface live user selection back into MCP.
|
|
7
|
+
*
|
|
8
|
+
* Design:
|
|
9
|
+
* • `viewer_open` boots an HTTP server that serves /index.html (the
|
|
10
|
+
* WebGL viewer) and /events (an SSE stream of selection picks).
|
|
11
|
+
* It also swaps in streaming adapters on the headless backend so
|
|
12
|
+
* SDK calls (`bim.viewer.colorize`, `bim.visibility.isolate`, …)
|
|
13
|
+
* fire commands at the running viewer.
|
|
14
|
+
* • Every other tool here is a thin wrapper around the SDK so an
|
|
15
|
+
* agent can call `viewer_colorize` instead of orchestrating
|
|
16
|
+
* query → resolve refs → adapter call by hand.
|
|
17
|
+
* • `viewer_get_selection` reports what the user has clicked. The
|
|
18
|
+
* resource `ifc-lite://viewer/selection` mirrors the same data and
|
|
19
|
+
* supports `resources/subscribe` so a subscribing agent gets a
|
|
20
|
+
* `notifications/resources/updated` push every time the user picks.
|
|
21
|
+
*/
|
|
22
|
+
import { EntityNode } from '@ifc-lite/query';
|
|
23
|
+
import { okResult, resolveModel } from './util.js';
|
|
24
|
+
import { ToolErrorCode, ToolExecutionError } from '../errors.js';
|
|
25
|
+
function requireViewer(ctx) {
|
|
26
|
+
const viewer = ctx.viewer;
|
|
27
|
+
if (!viewer)
|
|
28
|
+
throw new ToolExecutionError({ code: ToolErrorCode.UNSUPPORTED_OPERATION, message: 'No viewer manager attached.' });
|
|
29
|
+
return viewer;
|
|
30
|
+
}
|
|
31
|
+
function refsForGlobalIds(m, gids) {
|
|
32
|
+
const wanted = new Set(gids);
|
|
33
|
+
const refs = [];
|
|
34
|
+
for (const [, list] of m.store.entityIndex.byType) {
|
|
35
|
+
for (const id of list) {
|
|
36
|
+
if (refs.length >= wanted.size)
|
|
37
|
+
break;
|
|
38
|
+
const node = new EntityNode(m.store, id);
|
|
39
|
+
if (wanted.has(node.globalId))
|
|
40
|
+
refs.push({ modelId: m.id, expressId: id });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return refs;
|
|
44
|
+
}
|
|
45
|
+
function refsForExpressIds(m, eids) {
|
|
46
|
+
return eids.map((expressId) => ({ modelId: m.id, expressId }));
|
|
47
|
+
}
|
|
48
|
+
function resolveTargetRefs(m, input) {
|
|
49
|
+
const refs = [];
|
|
50
|
+
if (Array.isArray(input.global_ids))
|
|
51
|
+
refs.push(...refsForGlobalIds(m, input.global_ids));
|
|
52
|
+
if (Array.isArray(input.express_ids))
|
|
53
|
+
refs.push(...refsForExpressIds(m, input.express_ids));
|
|
54
|
+
if (typeof input.global_id === 'string')
|
|
55
|
+
refs.push(...refsForGlobalIds(m, [input.global_id]));
|
|
56
|
+
if (typeof input.express_id === 'number')
|
|
57
|
+
refs.push({ modelId: m.id, expressId: input.express_id });
|
|
58
|
+
if (typeof input.type === 'string') {
|
|
59
|
+
for (const e of m.bim.query().byType(input.type).toArray())
|
|
60
|
+
refs.push(e.ref);
|
|
61
|
+
}
|
|
62
|
+
return refs;
|
|
63
|
+
}
|
|
64
|
+
function parseColor(input) {
|
|
65
|
+
if (Array.isArray(input)) {
|
|
66
|
+
const arr = input.map(Number);
|
|
67
|
+
if (arr.length === 3)
|
|
68
|
+
return [arr[0], arr[1], arr[2], 1];
|
|
69
|
+
if (arr.length === 4)
|
|
70
|
+
return [arr[0], arr[1], arr[2], arr[3]];
|
|
71
|
+
}
|
|
72
|
+
if (typeof input === 'string') {
|
|
73
|
+
const named = {
|
|
74
|
+
red: [1, 0.2, 0.2, 1],
|
|
75
|
+
orange: [1, 0.6, 0.1, 1],
|
|
76
|
+
yellow: [1, 0.9, 0.1, 1],
|
|
77
|
+
green: [0.2, 0.8, 0.2, 1],
|
|
78
|
+
blue: [0.2, 0.4, 1, 1],
|
|
79
|
+
purple: [0.6, 0.2, 0.8, 1],
|
|
80
|
+
gray: [0.6, 0.6, 0.6, 1],
|
|
81
|
+
white: [1, 1, 1, 1],
|
|
82
|
+
black: [0, 0, 0, 1],
|
|
83
|
+
};
|
|
84
|
+
if (named[input.toLowerCase()])
|
|
85
|
+
return named[input.toLowerCase()];
|
|
86
|
+
// #RRGGBB / #RGB hex
|
|
87
|
+
const hex = input.replace('#', '');
|
|
88
|
+
if (hex.length === 6) {
|
|
89
|
+
return [parseInt(hex.slice(0, 2), 16) / 255, parseInt(hex.slice(2, 4), 16) / 255, parseInt(hex.slice(4, 6), 16) / 255, 1];
|
|
90
|
+
}
|
|
91
|
+
if (hex.length === 3) {
|
|
92
|
+
return [parseInt(hex[0] + hex[0], 16) / 255, parseInt(hex[1] + hex[1], 16) / 255, parseInt(hex[2] + hex[2], 16) / 255, 1];
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
throw new ToolExecutionError({
|
|
96
|
+
code: ToolErrorCode.INVALID_INPUT,
|
|
97
|
+
message: 'color must be [r,g,b] / [r,g,b,a] (0–1), a hex string (#ff8800), or a name (red, orange, …).',
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
// ── open / close / status ─────────────────────────────────────────────────
|
|
101
|
+
const viewerOpen = {
|
|
102
|
+
name: 'viewer_open',
|
|
103
|
+
description: 'Boot the in-process WebGL viewer for a model. Returns the URL to open in a browser. Idempotent for the same model.',
|
|
104
|
+
scope: 'read',
|
|
105
|
+
inputSchema: {
|
|
106
|
+
type: 'object',
|
|
107
|
+
properties: {
|
|
108
|
+
model_id: { type: 'string', description: 'Model to load. Defaults to the active model.' },
|
|
109
|
+
port: { type: 'integer', description: 'Preferred HTTP port (0 / omit = auto).', default: 0, minimum: 0, maximum: 65535 },
|
|
110
|
+
},
|
|
111
|
+
additionalProperties: false,
|
|
112
|
+
},
|
|
113
|
+
async handler(input, ctx) {
|
|
114
|
+
const viewer = requireViewer(ctx);
|
|
115
|
+
const m = resolveModel(ctx, input.model_id);
|
|
116
|
+
const port = input.port ?? 0;
|
|
117
|
+
const state = await viewer.open(m, port);
|
|
118
|
+
// Swap streaming adapters into the headless backend so subsequent
|
|
119
|
+
// bim.viewer.* and bim.visibility.* calls hit this viewer instance.
|
|
120
|
+
const adapters = viewer.adapters();
|
|
121
|
+
if (adapters)
|
|
122
|
+
m.backend.attachStreamingAdapters(adapters.viewer, adapters.visibility);
|
|
123
|
+
ctx.log.log('info', 'viewer_open', { url: state.url, model: m.id });
|
|
124
|
+
return okResult(`Viewer ready at ${state.url}. Open it in a browser to see '${m.name}'. Pick interactions sync back via 'ifc-lite://viewer/selection'.`, { ...state, instructions: `Open ${state.url} in a browser to interact with the model.` });
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
const viewerClose = {
|
|
128
|
+
name: 'viewer_close',
|
|
129
|
+
description: 'Stop the in-process viewer and clear its selection state.',
|
|
130
|
+
scope: 'read',
|
|
131
|
+
inputSchema: { type: 'object', additionalProperties: false },
|
|
132
|
+
handler(_input, ctx) {
|
|
133
|
+
const viewer = requireViewer(ctx);
|
|
134
|
+
if (!viewer.isOpen())
|
|
135
|
+
return okResult('Viewer was already closed.', { wasOpen: false });
|
|
136
|
+
// Restore no-op adapters before tearing down the manager.
|
|
137
|
+
for (const m of ctx.registry.list())
|
|
138
|
+
m.backend.detachStreamingAdapters();
|
|
139
|
+
viewer.close();
|
|
140
|
+
return okResult('Viewer closed.', { wasOpen: true });
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
const viewerStatus = {
|
|
144
|
+
name: 'viewer_status',
|
|
145
|
+
description: 'Report whether the viewer is open, on what port, and the current selection.',
|
|
146
|
+
scope: 'read',
|
|
147
|
+
inputSchema: { type: 'object', additionalProperties: false },
|
|
148
|
+
handler(_input, ctx) {
|
|
149
|
+
const viewer = requireViewer(ctx);
|
|
150
|
+
const state = viewer.state();
|
|
151
|
+
if (!state)
|
|
152
|
+
return okResult('Viewer is closed.', { open: false });
|
|
153
|
+
return okResult(`Viewer open at ${state.url} (${state.clientCount} client${state.clientCount === 1 ? '' : 's'} connected).`, { open: true, ...state });
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
// ── visibility / paint ────────────────────────────────────────────────────
|
|
157
|
+
const viewerColorize = {
|
|
158
|
+
name: 'viewer_colorize',
|
|
159
|
+
description: 'Paint a set of entities with a color. Pass `type`, `global_ids`, or `express_ids` to pick the set; pass `color` as [r,g,b]/[r,g,b,a] (0–1), a #hex, or a named color.',
|
|
160
|
+
scope: 'read',
|
|
161
|
+
inputSchema: {
|
|
162
|
+
type: 'object',
|
|
163
|
+
properties: {
|
|
164
|
+
model_id: { type: 'string' },
|
|
165
|
+
type: { type: 'string' },
|
|
166
|
+
global_ids: { type: 'array', items: { type: 'string' } },
|
|
167
|
+
express_ids: { type: 'array', items: { type: 'integer' } },
|
|
168
|
+
global_id: { type: 'string' },
|
|
169
|
+
express_id: { type: 'integer' },
|
|
170
|
+
color: { description: '[r,g,b], [r,g,b,a], hex, or named color.' },
|
|
171
|
+
reset_others: { type: 'boolean', default: false, description: 'When true, reset all other element colors first.' },
|
|
172
|
+
},
|
|
173
|
+
required: ['color'],
|
|
174
|
+
additionalProperties: false,
|
|
175
|
+
},
|
|
176
|
+
handler(input, ctx) {
|
|
177
|
+
const viewer = requireViewer(ctx);
|
|
178
|
+
if (!viewer.isOpen())
|
|
179
|
+
throw new ToolExecutionError({ code: ToolErrorCode.UNSUPPORTED_OPERATION, message: 'Viewer is not open. Call viewer_open first.' });
|
|
180
|
+
const m = resolveModel(ctx, input.model_id);
|
|
181
|
+
const refs = resolveTargetRefs(m, input);
|
|
182
|
+
if (refs.length === 0)
|
|
183
|
+
throw new ToolExecutionError({ code: ToolErrorCode.INVALID_INPUT, message: 'No entities matched the selector.' });
|
|
184
|
+
const color = parseColor(input.color);
|
|
185
|
+
if (input.reset_others)
|
|
186
|
+
m.bim.viewer.resetColors();
|
|
187
|
+
m.bim.viewer.colorizeRgba(refs, color);
|
|
188
|
+
return okResult(`Painted ${refs.length} entit${refs.length === 1 ? 'y' : 'ies'}.`, { count: refs.length, color });
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
const viewerIsolate = {
|
|
192
|
+
name: 'viewer_isolate',
|
|
193
|
+
description: 'Hide everything except the listed entities. Great for "show me only the load-bearing walls".',
|
|
194
|
+
scope: 'read',
|
|
195
|
+
inputSchema: {
|
|
196
|
+
type: 'object',
|
|
197
|
+
properties: {
|
|
198
|
+
model_id: { type: 'string' },
|
|
199
|
+
type: { type: 'string' },
|
|
200
|
+
global_ids: { type: 'array', items: { type: 'string' } },
|
|
201
|
+
express_ids: { type: 'array', items: { type: 'integer' } },
|
|
202
|
+
},
|
|
203
|
+
additionalProperties: false,
|
|
204
|
+
},
|
|
205
|
+
handler(input, ctx) {
|
|
206
|
+
const viewer = requireViewer(ctx);
|
|
207
|
+
if (!viewer.isOpen())
|
|
208
|
+
throw new ToolExecutionError({ code: ToolErrorCode.UNSUPPORTED_OPERATION, message: 'Viewer is not open.' });
|
|
209
|
+
const m = resolveModel(ctx, input.model_id);
|
|
210
|
+
const refs = resolveTargetRefs(m, input);
|
|
211
|
+
if (refs.length === 0)
|
|
212
|
+
throw new ToolExecutionError({ code: ToolErrorCode.INVALID_INPUT, message: 'No entities matched.' });
|
|
213
|
+
m.bim.viewer.isolate(refs);
|
|
214
|
+
return okResult(`Isolated ${refs.length} entit${refs.length === 1 ? 'y' : 'ies'}.`, { count: refs.length });
|
|
215
|
+
},
|
|
216
|
+
};
|
|
217
|
+
const viewerHide = {
|
|
218
|
+
name: 'viewer_hide',
|
|
219
|
+
description: 'Hide a set of entities in the viewer.',
|
|
220
|
+
scope: 'read',
|
|
221
|
+
inputSchema: {
|
|
222
|
+
type: 'object',
|
|
223
|
+
properties: {
|
|
224
|
+
model_id: { type: 'string' },
|
|
225
|
+
type: { type: 'string' },
|
|
226
|
+
global_ids: { type: 'array', items: { type: 'string' } },
|
|
227
|
+
express_ids: { type: 'array', items: { type: 'integer' } },
|
|
228
|
+
},
|
|
229
|
+
additionalProperties: false,
|
|
230
|
+
},
|
|
231
|
+
handler(input, ctx) {
|
|
232
|
+
const viewer = requireViewer(ctx);
|
|
233
|
+
if (!viewer.isOpen())
|
|
234
|
+
throw new ToolExecutionError({ code: ToolErrorCode.UNSUPPORTED_OPERATION, message: 'Viewer is not open.' });
|
|
235
|
+
const m = resolveModel(ctx, input.model_id);
|
|
236
|
+
const refs = resolveTargetRefs(m, input);
|
|
237
|
+
m.bim.viewer.hide(refs);
|
|
238
|
+
return okResult(`Hid ${refs.length} entit${refs.length === 1 ? 'y' : 'ies'}.`, { count: refs.length });
|
|
239
|
+
},
|
|
240
|
+
};
|
|
241
|
+
const viewerShow = {
|
|
242
|
+
name: 'viewer_show',
|
|
243
|
+
description: 'Make a set of entities visible (un-hide).',
|
|
244
|
+
scope: 'read',
|
|
245
|
+
inputSchema: {
|
|
246
|
+
type: 'object',
|
|
247
|
+
properties: {
|
|
248
|
+
model_id: { type: 'string' },
|
|
249
|
+
type: { type: 'string' },
|
|
250
|
+
global_ids: { type: 'array', items: { type: 'string' } },
|
|
251
|
+
express_ids: { type: 'array', items: { type: 'integer' } },
|
|
252
|
+
},
|
|
253
|
+
additionalProperties: false,
|
|
254
|
+
},
|
|
255
|
+
handler(input, ctx) {
|
|
256
|
+
const viewer = requireViewer(ctx);
|
|
257
|
+
if (!viewer.isOpen())
|
|
258
|
+
throw new ToolExecutionError({ code: ToolErrorCode.UNSUPPORTED_OPERATION, message: 'Viewer is not open.' });
|
|
259
|
+
const m = resolveModel(ctx, input.model_id);
|
|
260
|
+
const refs = resolveTargetRefs(m, input);
|
|
261
|
+
m.bim.viewer.show(refs);
|
|
262
|
+
return okResult(`Showed ${refs.length} entit${refs.length === 1 ? 'y' : 'ies'}.`, { count: refs.length });
|
|
263
|
+
},
|
|
264
|
+
};
|
|
265
|
+
const viewerReset = {
|
|
266
|
+
name: 'viewer_reset',
|
|
267
|
+
description: 'Reset visibility (show all) and clear all per-element color overrides.',
|
|
268
|
+
scope: 'read',
|
|
269
|
+
inputSchema: { type: 'object', properties: { model_id: { type: 'string' } }, additionalProperties: false },
|
|
270
|
+
handler(input, ctx) {
|
|
271
|
+
const viewer = requireViewer(ctx);
|
|
272
|
+
if (!viewer.isOpen())
|
|
273
|
+
throw new ToolExecutionError({ code: ToolErrorCode.UNSUPPORTED_OPERATION, message: 'Viewer is not open.' });
|
|
274
|
+
const m = resolveModel(ctx, input.model_id);
|
|
275
|
+
m.bim.viewer.resetVisibility();
|
|
276
|
+
m.bim.viewer.resetColors();
|
|
277
|
+
return okResult('Reset visibility + colors.', {});
|
|
278
|
+
},
|
|
279
|
+
};
|
|
280
|
+
const viewerFlyTo = {
|
|
281
|
+
name: 'viewer_fly_to',
|
|
282
|
+
description: 'Animate the camera to frame the listed entities.',
|
|
283
|
+
scope: 'read',
|
|
284
|
+
inputSchema: {
|
|
285
|
+
type: 'object',
|
|
286
|
+
properties: {
|
|
287
|
+
model_id: { type: 'string' },
|
|
288
|
+
type: { type: 'string' },
|
|
289
|
+
global_ids: { type: 'array', items: { type: 'string' } },
|
|
290
|
+
express_ids: { type: 'array', items: { type: 'integer' } },
|
|
291
|
+
global_id: { type: 'string' },
|
|
292
|
+
express_id: { type: 'integer' },
|
|
293
|
+
},
|
|
294
|
+
additionalProperties: false,
|
|
295
|
+
},
|
|
296
|
+
handler(input, ctx) {
|
|
297
|
+
const viewer = requireViewer(ctx);
|
|
298
|
+
if (!viewer.isOpen())
|
|
299
|
+
throw new ToolExecutionError({ code: ToolErrorCode.UNSUPPORTED_OPERATION, message: 'Viewer is not open.' });
|
|
300
|
+
const m = resolveModel(ctx, input.model_id);
|
|
301
|
+
const refs = resolveTargetRefs(m, input);
|
|
302
|
+
if (refs.length === 0)
|
|
303
|
+
throw new ToolExecutionError({ code: ToolErrorCode.INVALID_INPUT, message: 'No entities matched.' });
|
|
304
|
+
m.bim.viewer.flyTo(refs);
|
|
305
|
+
return okResult(`Flying to ${refs.length} entit${refs.length === 1 ? 'y' : 'ies'}.`, { count: refs.length });
|
|
306
|
+
},
|
|
307
|
+
};
|
|
308
|
+
// ── section ───────────────────────────────────────────────────────────────
|
|
309
|
+
const viewerSetSection = {
|
|
310
|
+
name: 'viewer_set_section',
|
|
311
|
+
description: 'Apply a section plane to the viewer.',
|
|
312
|
+
scope: 'read',
|
|
313
|
+
inputSchema: {
|
|
314
|
+
type: 'object',
|
|
315
|
+
properties: {
|
|
316
|
+
axis: { type: 'string', enum: ['x', 'y', 'z'] },
|
|
317
|
+
position: { type: 'number' },
|
|
318
|
+
flipped: { type: 'boolean', default: false },
|
|
319
|
+
enabled: { type: 'boolean', default: true },
|
|
320
|
+
},
|
|
321
|
+
required: ['axis', 'position'],
|
|
322
|
+
additionalProperties: false,
|
|
323
|
+
},
|
|
324
|
+
async handler(input, ctx) {
|
|
325
|
+
const viewer = requireViewer(ctx);
|
|
326
|
+
if (!viewer.isOpen())
|
|
327
|
+
throw new ToolExecutionError({ code: ToolErrorCode.UNSUPPORTED_OPERATION, message: 'Viewer is not open.' });
|
|
328
|
+
await viewer.sendCommand('section', {
|
|
329
|
+
section: { axis: input.axis, position: input.position, flipped: input.flipped ?? false, enabled: input.enabled ?? true },
|
|
330
|
+
});
|
|
331
|
+
return okResult(`Section ${input.axis} = ${input.position.toFixed(2)}.`, {});
|
|
332
|
+
},
|
|
333
|
+
};
|
|
334
|
+
const viewerClearSection = {
|
|
335
|
+
name: 'viewer_clear_section',
|
|
336
|
+
description: 'Remove the active section plane.',
|
|
337
|
+
scope: 'read',
|
|
338
|
+
inputSchema: { type: 'object', additionalProperties: false },
|
|
339
|
+
async handler(_input, ctx) {
|
|
340
|
+
const viewer = requireViewer(ctx);
|
|
341
|
+
if (!viewer.isOpen())
|
|
342
|
+
throw new ToolExecutionError({ code: ToolErrorCode.UNSUPPORTED_OPERATION, message: 'Viewer is not open.' });
|
|
343
|
+
await viewer.sendCommand('clearSection');
|
|
344
|
+
return okResult('Section cleared.', {});
|
|
345
|
+
},
|
|
346
|
+
};
|
|
347
|
+
// ── color helpers ─────────────────────────────────────────────────────────
|
|
348
|
+
const viewerColorByStorey = {
|
|
349
|
+
name: 'viewer_color_by_storey',
|
|
350
|
+
description: 'Apply a default per-storey color overlay (built-in viewer preset).',
|
|
351
|
+
scope: 'read',
|
|
352
|
+
inputSchema: { type: 'object', additionalProperties: false },
|
|
353
|
+
async handler(_input, ctx) {
|
|
354
|
+
const viewer = requireViewer(ctx);
|
|
355
|
+
if (!viewer.isOpen())
|
|
356
|
+
throw new ToolExecutionError({ code: ToolErrorCode.UNSUPPORTED_OPERATION, message: 'Viewer is not open.' });
|
|
357
|
+
await viewer.sendCommand('colorByStorey');
|
|
358
|
+
return okResult('Colored by storey.', {});
|
|
359
|
+
},
|
|
360
|
+
};
|
|
361
|
+
const PALETTE = [
|
|
362
|
+
[0.20, 0.40, 1.00, 1], [1.00, 0.60, 0.10, 1], [0.20, 0.80, 0.20, 1],
|
|
363
|
+
[0.95, 0.20, 0.30, 1], [0.60, 0.20, 0.80, 1], [0.10, 0.70, 0.70, 1],
|
|
364
|
+
[0.95, 0.85, 0.10, 1], [0.50, 0.50, 0.50, 1], [0.85, 0.40, 0.65, 1],
|
|
365
|
+
];
|
|
366
|
+
const viewerColorByProperty = {
|
|
367
|
+
name: 'viewer_color_by_property',
|
|
368
|
+
description: 'Color a type set by the value of a property — distinct color per unique value, plus a "missing" group. Returns the legend so the agent can describe what colors mean.',
|
|
369
|
+
scope: 'read',
|
|
370
|
+
inputSchema: {
|
|
371
|
+
type: 'object',
|
|
372
|
+
properties: {
|
|
373
|
+
model_id: { type: 'string' },
|
|
374
|
+
type: { type: 'string' },
|
|
375
|
+
pset: { type: 'string' },
|
|
376
|
+
property: { type: 'string' },
|
|
377
|
+
missing_color: { description: 'Color for entities that lack the property.', default: 'gray' },
|
|
378
|
+
},
|
|
379
|
+
required: ['type', 'pset', 'property'],
|
|
380
|
+
additionalProperties: false,
|
|
381
|
+
},
|
|
382
|
+
handler(input, ctx) {
|
|
383
|
+
const viewer = requireViewer(ctx);
|
|
384
|
+
if (!viewer.isOpen())
|
|
385
|
+
throw new ToolExecutionError({ code: ToolErrorCode.UNSUPPORTED_OPERATION, message: 'Viewer is not open.' });
|
|
386
|
+
const m = resolveModel(ctx, input.model_id);
|
|
387
|
+
const buckets = new Map();
|
|
388
|
+
for (const e of m.bim.query().byType(input.type).toArray()) {
|
|
389
|
+
const v = m.bim.property(e.ref, input.pset, input.property);
|
|
390
|
+
const key = v == null ? '__missing__' : String(v);
|
|
391
|
+
const list = buckets.get(key) ?? [];
|
|
392
|
+
list.push(e.ref);
|
|
393
|
+
buckets.set(key, list);
|
|
394
|
+
}
|
|
395
|
+
const legend = [];
|
|
396
|
+
let i = 0;
|
|
397
|
+
m.bim.viewer.resetColors();
|
|
398
|
+
for (const [value, refs] of buckets) {
|
|
399
|
+
const color = value === '__missing__' ? parseColor(input.missing_color ?? 'gray') : PALETTE[i++ % PALETTE.length];
|
|
400
|
+
m.bim.viewer.colorizeRgba(refs, color);
|
|
401
|
+
legend.push({ value, count: refs.length, color });
|
|
402
|
+
}
|
|
403
|
+
return okResult(`Colored ${input.type} by ${input.pset}.${input.property} — ${legend.length} bucket(s).`, { legend });
|
|
404
|
+
},
|
|
405
|
+
};
|
|
406
|
+
const ALL_INCLUDES = ['attributes', 'properties', 'quantities', 'classifications', 'materials'];
|
|
407
|
+
const DEFAULT_INCLUDES = ['attributes', 'classifications', 'materials'];
|
|
408
|
+
/** Map the SDK's camelCase entity shape to IFC EXPRESS PascalCase. */
|
|
409
|
+
function projectEntity(data) {
|
|
410
|
+
if (!data)
|
|
411
|
+
return null;
|
|
412
|
+
const out = {};
|
|
413
|
+
if (data.type)
|
|
414
|
+
out.IfcType = data.type;
|
|
415
|
+
if (data.globalId)
|
|
416
|
+
out.GlobalId = data.globalId;
|
|
417
|
+
if (data.name)
|
|
418
|
+
out.Name = data.name;
|
|
419
|
+
if (data.description)
|
|
420
|
+
out.Description = data.description;
|
|
421
|
+
if (data.objectType)
|
|
422
|
+
out.ObjectType = data.objectType;
|
|
423
|
+
return out;
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* Build the rich payload an agent needs to actually answer "what did the
|
|
427
|
+
* user just click?". The structured payload always carries the full entity
|
|
428
|
+
* (ref, type, name, GlobalId, description, objectType) and any sections the
|
|
429
|
+
* caller asked to include. The text content mirrors that as a human-readable
|
|
430
|
+
* summary so MCP clients that only forward `content[].text` to the model
|
|
431
|
+
* (Claude Desktop is one) still get the substance — that's the bug-fix here:
|
|
432
|
+
* before, text was just "1 selected." and the data "collapsed" out of view.
|
|
433
|
+
*/
|
|
434
|
+
function buildSelectionPayload(ctx, include) {
|
|
435
|
+
const viewer = requireViewer(ctx);
|
|
436
|
+
const state = viewer.state();
|
|
437
|
+
const raw = state?.selection ?? [];
|
|
438
|
+
if (raw.length === 0) {
|
|
439
|
+
return { selection: [], modelId: state?.modelId ?? null, text: 'No selection in viewer.' };
|
|
440
|
+
}
|
|
441
|
+
const modelId = state?.modelId ?? null;
|
|
442
|
+
const m = modelId ? ctx.registry.get(modelId) : null;
|
|
443
|
+
if (!m) {
|
|
444
|
+
// We still know the basics from the SSE pick — return them so the
|
|
445
|
+
// agent can at least name the entity even if the model unloaded.
|
|
446
|
+
const lines = raw.map((s) => `• ${s.ifcType ?? '?'} #${s.expressId}` + (s.globalId ? ` (${s.globalId})` : ''));
|
|
447
|
+
return {
|
|
448
|
+
selection: raw.map((s) => ({ expressId: s.expressId, IfcType: s.ifcType, GlobalId: s.globalId, entity: null })),
|
|
449
|
+
modelId,
|
|
450
|
+
text: `${raw.length} selected (model '${modelId}' not resolvable):\n${lines.join('\n')}`,
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
const includeSet = new Set(include);
|
|
454
|
+
const enriched = raw.map((s) => {
|
|
455
|
+
const ref = { modelId: m.id, expressId: s.expressId };
|
|
456
|
+
const data = m.bim.entity(ref);
|
|
457
|
+
const out = {
|
|
458
|
+
expressId: s.expressId,
|
|
459
|
+
IfcType: s.ifcType ?? data?.type,
|
|
460
|
+
GlobalId: s.globalId ?? data?.globalId,
|
|
461
|
+
entity: projectEntity(data),
|
|
462
|
+
};
|
|
463
|
+
if (includeSet.has('attributes'))
|
|
464
|
+
out.attributes = m.bim.attributes(ref);
|
|
465
|
+
if (includeSet.has('properties'))
|
|
466
|
+
out.properties = m.bim.properties(ref);
|
|
467
|
+
if (includeSet.has('quantities'))
|
|
468
|
+
out.quantities = m.bim.quantities(ref);
|
|
469
|
+
if (includeSet.has('classifications'))
|
|
470
|
+
out.classifications = m.bim.classifications(ref);
|
|
471
|
+
if (includeSet.has('materials'))
|
|
472
|
+
out.materials = m.bim.materials(ref);
|
|
473
|
+
return out;
|
|
474
|
+
});
|
|
475
|
+
// Build a rich, multi-line text summary so agents whose clients only
|
|
476
|
+
// surface text content still see the substance of the pick.
|
|
477
|
+
const blocks = [`${enriched.length} entity selected in viewer:`];
|
|
478
|
+
for (const e of enriched) {
|
|
479
|
+
const parts = [];
|
|
480
|
+
parts.push(`• ${e.entity?.IfcType ?? e.IfcType ?? '?'} #${e.expressId}`);
|
|
481
|
+
const name = e.entity?.Name;
|
|
482
|
+
if (name)
|
|
483
|
+
parts.push(`'${name}'`);
|
|
484
|
+
if (e.GlobalId)
|
|
485
|
+
parts.push(`GlobalId=${e.GlobalId}`);
|
|
486
|
+
blocks.push(parts.join(' '));
|
|
487
|
+
if (e.entity?.Description)
|
|
488
|
+
blocks.push(` Description: ${e.entity.Description}`);
|
|
489
|
+
if (e.entity?.ObjectType)
|
|
490
|
+
blocks.push(` ObjectType: ${e.entity.ObjectType}`);
|
|
491
|
+
if (e.attributes && e.attributes.length > 0) {
|
|
492
|
+
const summary = e.attributes
|
|
493
|
+
.filter((a) => a.value != null && a.value !== '')
|
|
494
|
+
.map((a) => `${a.name}=${JSON.stringify(a.value)}`)
|
|
495
|
+
.join(', ');
|
|
496
|
+
if (summary)
|
|
497
|
+
blocks.push(` Attributes: ${summary}`);
|
|
498
|
+
}
|
|
499
|
+
if (e.properties && e.properties.length > 0) {
|
|
500
|
+
const psetSummaries = e.properties.map((p) => `${p.name} (${p.properties.length})`);
|
|
501
|
+
blocks.push(` Property sets: ${psetSummaries.join(', ')}`);
|
|
502
|
+
}
|
|
503
|
+
if (e.quantities && e.quantities.length > 0) {
|
|
504
|
+
const qtoSummaries = e.quantities.map((q) => `${q.name} (${q.quantities.length})`);
|
|
505
|
+
blocks.push(` Quantity sets: ${qtoSummaries.join(', ')}`);
|
|
506
|
+
}
|
|
507
|
+
if (e.classifications && e.classifications.length > 0) {
|
|
508
|
+
const c = e.classifications
|
|
509
|
+
.map((cls) => `${cls.system ?? '?'}:${cls.identification ?? cls.name ?? '?'}`)
|
|
510
|
+
.join(', ');
|
|
511
|
+
blocks.push(` Classifications: ${c}`);
|
|
512
|
+
}
|
|
513
|
+
if (e.materials) {
|
|
514
|
+
const mat = e.materials;
|
|
515
|
+
if (Array.isArray(mat.layers) && mat.layers.length > 0) {
|
|
516
|
+
blocks.push(` Materials: ${mat.layers.map((l) => l.materialName ?? l.name ?? '?').join(', ')}`);
|
|
517
|
+
}
|
|
518
|
+
else if (Array.isArray(mat.materials) && mat.materials.length > 0) {
|
|
519
|
+
blocks.push(` Materials: ${mat.materials.map((l) => l.name ?? '?').join(', ')}`);
|
|
520
|
+
}
|
|
521
|
+
else if (mat.name ?? mat.materialName) {
|
|
522
|
+
blocks.push(` Material: ${mat.name ?? mat.materialName}`);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
return { selection: enriched, modelId, text: blocks.join('\n') };
|
|
527
|
+
}
|
|
528
|
+
const viewerGetSelection = {
|
|
529
|
+
name: 'viewer_get_selection',
|
|
530
|
+
description: 'Return what the user has clicked in the viewer. Both the human-readable text content and the structured payload include type, expressId, globalId, name, description, and any of the optional sections in `include` (default: attributes + classifications + materials).',
|
|
531
|
+
scope: 'read',
|
|
532
|
+
inputSchema: {
|
|
533
|
+
type: 'object',
|
|
534
|
+
properties: {
|
|
535
|
+
include: {
|
|
536
|
+
type: 'array',
|
|
537
|
+
items: { type: 'string', enum: [...ALL_INCLUDES] },
|
|
538
|
+
description: 'Sections to enrich the response with. Defaults to ["attributes","classifications","materials"]. Pass [] to skip enrichment.',
|
|
539
|
+
},
|
|
540
|
+
},
|
|
541
|
+
additionalProperties: false,
|
|
542
|
+
},
|
|
543
|
+
handler(input, ctx) {
|
|
544
|
+
requireViewer(ctx);
|
|
545
|
+
const include = Array.isArray(input.include)
|
|
546
|
+
? input.include.filter((k) => ALL_INCLUDES.includes(k))
|
|
547
|
+
: DEFAULT_INCLUDES;
|
|
548
|
+
const { selection, modelId, text } = buildSelectionPayload(ctx, include);
|
|
549
|
+
return okResult(text, { selection, modelId, includes: include });
|
|
550
|
+
},
|
|
551
|
+
};
|
|
552
|
+
const viewerDescribeSelection = {
|
|
553
|
+
name: 'viewer_describe_selection',
|
|
554
|
+
description: 'Like viewer_get_selection but always pulls the full kitchen sink — attributes, properties, quantities, classifications, materials. Use this when the user asks "tell me everything about what I just clicked".',
|
|
555
|
+
scope: 'read',
|
|
556
|
+
inputSchema: { type: 'object', additionalProperties: false },
|
|
557
|
+
handler(_input, ctx) {
|
|
558
|
+
requireViewer(ctx);
|
|
559
|
+
const { selection, modelId, text } = buildSelectionPayload(ctx, ALL_INCLUDES);
|
|
560
|
+
return okResult(text, { selection, modelId, includes: ALL_INCLUDES });
|
|
561
|
+
},
|
|
562
|
+
};
|
|
563
|
+
const viewerWaitForSelection = {
|
|
564
|
+
name: 'viewer_wait_for_selection',
|
|
565
|
+
description: 'Block until the user picks an entity in the viewer (or `timeout_ms` elapses). Useful for "click on the wall you want me to inspect" workflows. Returns the same rich payload as viewer_get_selection.',
|
|
566
|
+
scope: 'read',
|
|
567
|
+
inputSchema: {
|
|
568
|
+
type: 'object',
|
|
569
|
+
properties: {
|
|
570
|
+
timeout_ms: { type: 'integer', default: 60000, minimum: 100, maximum: 600000 },
|
|
571
|
+
include: {
|
|
572
|
+
type: 'array',
|
|
573
|
+
items: { type: 'string', enum: [...ALL_INCLUDES] },
|
|
574
|
+
description: 'Sections to enrich the response with. Defaults to ["attributes","classifications","materials"].',
|
|
575
|
+
},
|
|
576
|
+
},
|
|
577
|
+
additionalProperties: false,
|
|
578
|
+
},
|
|
579
|
+
async handler(input, ctx) {
|
|
580
|
+
const viewer = requireViewer(ctx);
|
|
581
|
+
if (!viewer.isOpen())
|
|
582
|
+
throw new ToolExecutionError({ code: ToolErrorCode.UNSUPPORTED_OPERATION, message: 'Viewer is not open.' });
|
|
583
|
+
const timeout = input.timeout_ms ?? 60000;
|
|
584
|
+
const include = Array.isArray(input.include)
|
|
585
|
+
? input.include.filter((k) => ALL_INCLUDES.includes(k))
|
|
586
|
+
: DEFAULT_INCLUDES;
|
|
587
|
+
return new Promise((resolve) => {
|
|
588
|
+
let resolved = false;
|
|
589
|
+
const finish = (result) => {
|
|
590
|
+
if (resolved)
|
|
591
|
+
return;
|
|
592
|
+
resolved = true;
|
|
593
|
+
unsub();
|
|
594
|
+
clearTimeout(timer);
|
|
595
|
+
ctx.signal.removeEventListener('abort', onAbort);
|
|
596
|
+
resolve(result);
|
|
597
|
+
};
|
|
598
|
+
const unsub = viewer.onSelection((sel) => {
|
|
599
|
+
if (sel.length > 0) {
|
|
600
|
+
const { selection, modelId, text } = buildSelectionPayload(ctx, include);
|
|
601
|
+
finish(okResult(text, { selection, modelId, includes: include }));
|
|
602
|
+
}
|
|
603
|
+
});
|
|
604
|
+
const onAbort = () => finish(okResult('Wait cancelled.', { selection: [], cancelled: true }));
|
|
605
|
+
ctx.signal.addEventListener('abort', onAbort);
|
|
606
|
+
const timer = setTimeout(() => finish(okResult('Timed out waiting for selection.', { selection: [], timedOut: true })), timeout);
|
|
607
|
+
});
|
|
608
|
+
},
|
|
609
|
+
};
|
|
610
|
+
// ── elicitation-style ask ─────────────────────────────────────────────────
|
|
611
|
+
const viewerAsk = {
|
|
612
|
+
name: 'viewer_ask',
|
|
613
|
+
description: 'Inform the user that the agent would like to open the viewer for visual context. Returns guidance for the agent. The agent is expected to relay this to the user, then call `viewer_open` once they confirm.',
|
|
614
|
+
scope: 'read',
|
|
615
|
+
inputSchema: {
|
|
616
|
+
type: 'object',
|
|
617
|
+
properties: {
|
|
618
|
+
reason: { type: 'string', description: 'Short explanation, e.g. "to highlight non-compliant doors".' },
|
|
619
|
+
model_id: { type: 'string' },
|
|
620
|
+
},
|
|
621
|
+
additionalProperties: false,
|
|
622
|
+
},
|
|
623
|
+
handler(input, ctx) {
|
|
624
|
+
const m = resolveModel(ctx, input.model_id);
|
|
625
|
+
const reason = input.reason ?? 'visualize the result';
|
|
626
|
+
return okResult([
|
|
627
|
+
`Ask the user: "I'd like to open the 3D viewer for ${m.name} ${reason}. May I?"`,
|
|
628
|
+
`If they agree, call \`viewer_open\` with model_id="${m.id}". After it returns, share the URL with the user (\`http://localhost:<port>/\`) and tell them clicks in the viewer will sync back automatically.`,
|
|
629
|
+
].join(' '), { modelId: m.id, suggestedTool: 'viewer_open', suggestedArgs: { model_id: m.id } });
|
|
630
|
+
},
|
|
631
|
+
};
|
|
632
|
+
export const viewerTools = [
|
|
633
|
+
viewerOpen,
|
|
634
|
+
viewerClose,
|
|
635
|
+
viewerStatus,
|
|
636
|
+
viewerColorize,
|
|
637
|
+
viewerIsolate,
|
|
638
|
+
viewerHide,
|
|
639
|
+
viewerShow,
|
|
640
|
+
viewerReset,
|
|
641
|
+
viewerFlyTo,
|
|
642
|
+
viewerSetSection,
|
|
643
|
+
viewerClearSection,
|
|
644
|
+
viewerColorByStorey,
|
|
645
|
+
viewerColorByProperty,
|
|
646
|
+
viewerGetSelection,
|
|
647
|
+
viewerDescribeSelection,
|
|
648
|
+
viewerWaitForSelection,
|
|
649
|
+
viewerAsk,
|
|
650
|
+
];
|
|
651
|
+
//# sourceMappingURL=viewer.js.map
|