ocp-viewer-core 1.0.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/package.json +22 -0
- package/src/animation.js +87 -0
- package/src/apply.js +328 -0
- package/src/index.js +65 -0
- package/src/logo.js +279 -0
- package/src/notify.js +112 -0
- package/src/options.js +272 -0
- package/src/page.js +476 -0
- package/src/render.js +299 -0
- package/src/states.js +120 -0
package/src/page.js
ADDED
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The viewer page, held once for every host that shows one.
|
|
3
|
+
*
|
|
4
|
+
* A host's HTML supplies only what is genuinely its own: where it loaded these
|
|
5
|
+
* modules from, where its settings come from, and how to send a message. Even
|
|
6
|
+
* measuring the surface is here - a host that owns a whole browsing context
|
|
7
|
+
* gets the window measured for it, and one that shows the viewer in a pane
|
|
8
|
+
* passes its own container and its own size.
|
|
9
|
+
*
|
|
10
|
+
* This is the one module in the package that touches the DOM, and it does so
|
|
11
|
+
* because it *is* the page. Everything else here takes a viewer and plain data
|
|
12
|
+
* and could run headless; that distinction is worth keeping, so new DOM work
|
|
13
|
+
* belongs in this file or in a host, not spread through the others.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/*
|
|
17
|
+
Copyright 2026 Bernhard Walter
|
|
18
|
+
|
|
19
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
20
|
+
you may not use this file except in compliance with the License.
|
|
21
|
+
You may obtain a copy of the License at
|
|
22
|
+
|
|
23
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
24
|
+
|
|
25
|
+
Unless required by applicable law or agreed to in writing, software
|
|
26
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
27
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
28
|
+
See the License for the specific language governing permissions and
|
|
29
|
+
limitations under the License.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import { animate } from "./animation.js";
|
|
33
|
+
import { applyConfig } from "./apply.js";
|
|
34
|
+
import { logo } from "./logo.js";
|
|
35
|
+
import { createNotifier } from "./notify.js";
|
|
36
|
+
import { buildDisplayOptions, preset } from "./options.js";
|
|
37
|
+
import { createRenderer } from "./render.js";
|
|
38
|
+
import { currentStates, restoreStates } from "./states.js";
|
|
39
|
+
|
|
40
|
+
const MIN_WIDTH = 450;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Build the page.
|
|
44
|
+
*
|
|
45
|
+
* @param Viewer, Display, Timer three-cad-viewer's three, passed in because
|
|
46
|
+
* only the host knows where it loaded them from
|
|
47
|
+
* @param send (command, message) => void - the host's channel to Python
|
|
48
|
+
* @param overrides {display, viewer} - the settings this host differs on
|
|
49
|
+
* @param theme the host's resolved theme, for the observer below
|
|
50
|
+
* @param container the element to draw into, or its id. Default "cad_viewer"
|
|
51
|
+
* @param getSize () => {width, height} of the surface. Default the window
|
|
52
|
+
* @param listen attach the window's `message` and `resize` listeners.
|
|
53
|
+
* Default true; a host that has neither passes false and
|
|
54
|
+
* drives `handleMessage` and `resize` itself
|
|
55
|
+
* @returns `{ showSplash, setTheme, handleMessage, resize }` - the two page
|
|
56
|
+
* hosts deliver messages as a `message` event, the extension posting
|
|
57
|
+
* into the webview and the standalone's socket shim posting what came
|
|
58
|
+
* off the wire, and both let this file listen for them. A host whose
|
|
59
|
+
* models arrive some other way calls `handleMessage` with the same
|
|
60
|
+
* objects, so every host runs the one dispatch and no host can end up
|
|
61
|
+
* with a message branch, and so a feature, that another lacks.
|
|
62
|
+
*/
|
|
63
|
+
export function createPage({
|
|
64
|
+
Viewer,
|
|
65
|
+
Display,
|
|
66
|
+
Timer,
|
|
67
|
+
send,
|
|
68
|
+
overrides,
|
|
69
|
+
theme,
|
|
70
|
+
container,
|
|
71
|
+
getSize,
|
|
72
|
+
listen
|
|
73
|
+
}) {
|
|
74
|
+
var viewer = null;
|
|
75
|
+
var display = null;
|
|
76
|
+
var _shapes = null;
|
|
77
|
+
var _meshData = null;
|
|
78
|
+
var _config = null;
|
|
79
|
+
var _camera_distance = null;
|
|
80
|
+
var viewerOptions = {};
|
|
81
|
+
var last_bb_radius = null;
|
|
82
|
+
|
|
83
|
+
// The viewer's own state, as the renderer reports it: one object the
|
|
84
|
+
// notifier keeps, under the names the renderer notifies with - camelCase
|
|
85
|
+
// for the camera, which is sent straight to checkChanges, and snake_case
|
|
86
|
+
// for everything that goes through its notification map. render() writes
|
|
87
|
+
// into it too, after
|
|
88
|
+
// reading the camera back off the viewer.
|
|
89
|
+
//
|
|
90
|
+
// It is also what goes on the wire. The extension answers a status
|
|
91
|
+
// request with the last message the webview sent, replacing rather
|
|
92
|
+
// than merging, so a delta would lose every earlier value - and
|
|
93
|
+
// combined_config merges the viewer's state into the next show. The
|
|
94
|
+
// notifier's delta plus this snapshot is what preserves both.
|
|
95
|
+
var notifier = null;
|
|
96
|
+
var renderer = null;
|
|
97
|
+
var status = {};
|
|
98
|
+
|
|
99
|
+
function debugLog(tag, obj) {
|
|
100
|
+
console.log(tag, obj);
|
|
101
|
+
var msg = tag + (obj ? " " + JSON.stringify(obj) : "");
|
|
102
|
+
send("log", msg);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function nc(change) {
|
|
106
|
+
return notifier == null ? null : notifier.notify(change);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function makeRenderer() {
|
|
110
|
+
renderer = createRenderer({
|
|
111
|
+
viewer: viewer,
|
|
112
|
+
status: status,
|
|
113
|
+
overrides: { viewer: overrides.viewer, theme: overrides.display.theme },
|
|
114
|
+
// A tree width is a resize, and only this host knows the
|
|
115
|
+
// other two dimensions.
|
|
116
|
+
resize: () => {
|
|
117
|
+
const displayOptions = getDisplayOptions(_config.theme);
|
|
118
|
+
viewer.resizeCadView(
|
|
119
|
+
displayOptions.cadWidth,
|
|
120
|
+
displayOptions.treeWidth,
|
|
121
|
+
displayOptions.height,
|
|
122
|
+
displayOptions.glass
|
|
123
|
+
);
|
|
124
|
+
},
|
|
125
|
+
sendStatus: (snapshot) => send("status", snapshot),
|
|
126
|
+
debug: _config != null && _config.debug ? debugLog : null
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function makeNotifier() {
|
|
131
|
+
notifier = createNotifier({
|
|
132
|
+
viewer: viewer,
|
|
133
|
+
// The delta is what changed; the snapshot is what the
|
|
134
|
+
// extension will answer the next status request with.
|
|
135
|
+
// Event keys - a selection, a pick, the active tool - ride
|
|
136
|
+
// on the delta only, so a selection made minutes ago is not
|
|
137
|
+
// replayed into an unrelated update.
|
|
138
|
+
send: (delta) => send("status", { ...status, ...delta }),
|
|
139
|
+
debug: _config != null && _config.debug ? debugLog : null
|
|
140
|
+
});
|
|
141
|
+
status = notifier.status;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function normalizeWidth(width, glass, tools) {
|
|
145
|
+
const treeWidth =
|
|
146
|
+
glass || !tools
|
|
147
|
+
? 0
|
|
148
|
+
: preset(_config, "treeWidth", overrides.display.treeWidth);
|
|
149
|
+
return Math.max(MIN_WIDTH - treeWidth, width - treeWidth - 20);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* The chrome above the canvas, measured rather than assumed.
|
|
154
|
+
*
|
|
155
|
+
* This was the constant 65, which is an approximation of the canvas's own
|
|
156
|
+
* top offset and wrong in both directions: it over-reserves in a pane,
|
|
157
|
+
* showing as a bottom margin visibly larger than the left and top ones,
|
|
158
|
+
* and measuring the toolbar alone instead under-reserves - the toolbar's
|
|
159
|
+
* offset and the gap below it are part of the same distance, and without
|
|
160
|
+
* them the canvas overflows and clips the status line, which is pinned to
|
|
161
|
+
* `bottom: 4px` inside it.
|
|
162
|
+
*
|
|
163
|
+
* Taken from the container so that it means the same thing to a host that
|
|
164
|
+
* owns the window and to one that draws into a pane.
|
|
165
|
+
*/
|
|
166
|
+
const ASSUMED_CHROME = 65;
|
|
167
|
+
|
|
168
|
+
function getContainer() {
|
|
169
|
+
if (container == null) {
|
|
170
|
+
return document.getElementById("cad_viewer");
|
|
171
|
+
}
|
|
172
|
+
return typeof container === "string"
|
|
173
|
+
? document.getElementById(container)
|
|
174
|
+
: container;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function reservedHeight() {
|
|
178
|
+
const element = getContainer();
|
|
179
|
+
if (element == null) {
|
|
180
|
+
return ASSUMED_CHROME;
|
|
181
|
+
}
|
|
182
|
+
const canvas = element.querySelector(
|
|
183
|
+
".tcv_cad_view_glass, .tcv_cad_view"
|
|
184
|
+
);
|
|
185
|
+
if (canvas != null) {
|
|
186
|
+
const offset =
|
|
187
|
+
canvas.getBoundingClientRect().top -
|
|
188
|
+
element.getBoundingClientRect().top;
|
|
189
|
+
if (offset > 0) {
|
|
190
|
+
return Math.round(offset);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
// Before the canvas exists, approximate from the toolbar if it is there.
|
|
194
|
+
const toolbar = element.querySelector(".tcv_cad_toolbar");
|
|
195
|
+
if (toolbar != null && toolbar.offsetHeight > 0) {
|
|
196
|
+
return toolbar.offsetHeight + 10;
|
|
197
|
+
}
|
|
198
|
+
return ASSUMED_CHROME;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function normalizeHeight(height) {
|
|
202
|
+
return height - reservedHeight();
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function measureSize() {
|
|
206
|
+
if (getSize != null) {
|
|
207
|
+
return getSize();
|
|
208
|
+
}
|
|
209
|
+
return {
|
|
210
|
+
width: window.innerWidth,
|
|
211
|
+
height: window.innerHeight
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function getGeometry() {
|
|
216
|
+
const size = measureSize();
|
|
217
|
+
const glass = preset(_config, "glass", overrides.display.glass);
|
|
218
|
+
const tools = preset(_config, "tools", overrides.display.tools);
|
|
219
|
+
return {
|
|
220
|
+
cadWidth: normalizeWidth(size.width, glass, tools),
|
|
221
|
+
height: normalizeHeight(size.height),
|
|
222
|
+
treeWidth: preset(
|
|
223
|
+
_config,
|
|
224
|
+
"treeWidth",
|
|
225
|
+
overrides.display.treeWidth
|
|
226
|
+
)
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function getDisplayOptions(theme) {
|
|
231
|
+
const options = buildDisplayOptions(
|
|
232
|
+
_config,
|
|
233
|
+
overrides.display,
|
|
234
|
+
getGeometry()
|
|
235
|
+
);
|
|
236
|
+
// An explicit theme argument still wins: showViewer is called
|
|
237
|
+
// with config.theme, and the observer below with the host's.
|
|
238
|
+
if (theme) {
|
|
239
|
+
options.theme = theme;
|
|
240
|
+
}
|
|
241
|
+
return options;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function showViewer(meshData, config) {
|
|
245
|
+
debugLog("showViewer called");
|
|
246
|
+
_meshData = meshData;
|
|
247
|
+
_shapes = meshData.shapes || meshData;
|
|
248
|
+
_config = config;
|
|
249
|
+
const displayOptions = getDisplayOptions(config.theme);
|
|
250
|
+
if (display == null) {
|
|
251
|
+
const element = getContainer();
|
|
252
|
+
element.innerHTML = "";
|
|
253
|
+
display = new Display(element, displayOptions);
|
|
254
|
+
}
|
|
255
|
+
if (_config == null) {
|
|
256
|
+
debugLog("OCP CAD Viewer: config is null");
|
|
257
|
+
_config = {};
|
|
258
|
+
}
|
|
259
|
+
if (_config.debug) {
|
|
260
|
+
debugLog("_config", _config);
|
|
261
|
+
debugLog("displayOptions", displayOptions);
|
|
262
|
+
}
|
|
263
|
+
// Reuse the viewer across shows: clear() tears down the scene and
|
|
264
|
+
// resets activeTab to "tree" (which triggers leaveStudioMode if the
|
|
265
|
+
// previous show was in Studio mode), but keeps the WebGL context,
|
|
266
|
+
// ViewerState (preferences), and StudioManager (incl. PMREM env
|
|
267
|
+
// cache) alive — saving a context teardown and HDR re-fetch per show.
|
|
268
|
+
if (viewer != null) {
|
|
269
|
+
viewer.clear();
|
|
270
|
+
} else {
|
|
271
|
+
viewer = new Viewer(display, displayOptions, nc, null);
|
|
272
|
+
// After the viewer, because both read it. `nc` is the
|
|
273
|
+
// trampoline that bridges the notifier to the constructor.
|
|
274
|
+
makeNotifier();
|
|
275
|
+
makeRenderer();
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (_shapes) renderer.render(_meshData, _config);
|
|
279
|
+
|
|
280
|
+
// Three display options are re-applied on every show, for one reason:
|
|
281
|
+
// the Display is built once, so everything it was constructed with is
|
|
282
|
+
// dropped by every show after the first. The theme is the one that was
|
|
283
|
+
// missing - `show(theme=...)` and a changed workspace setting both
|
|
284
|
+
// computed it into displayOptions and then threw it away, because by
|
|
285
|
+
// then `display` was no longer null.
|
|
286
|
+
viewer.setTheme(displayOptions.theme);
|
|
287
|
+
viewer.glassMode(displayOptions.glass);
|
|
288
|
+
viewer.showTools(displayOptions.tools);
|
|
289
|
+
|
|
290
|
+
debugLog("showViewer finished");
|
|
291
|
+
|
|
292
|
+
return viewer;
|
|
293
|
+
|
|
294
|
+
// viewer.trimUI(["axes", "axes0", "grid", "ortho", "more", "help"])
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* The surface changed size.
|
|
299
|
+
*
|
|
300
|
+
* A window resize for the two page hosts, and a splitter drag for a host
|
|
301
|
+
* that draws into a pane - the same work either way, which is why it is a
|
|
302
|
+
* function rather than a listener body.
|
|
303
|
+
*/
|
|
304
|
+
function resize() {
|
|
305
|
+
if (viewer != null) {
|
|
306
|
+
const displayOptions = getDisplayOptions(_config.theme);
|
|
307
|
+
viewer.resizeCadView(
|
|
308
|
+
displayOptions.cadWidth,
|
|
309
|
+
displayOptions.treeWidth,
|
|
310
|
+
displayOptions.height,
|
|
311
|
+
displayOptions.glass
|
|
312
|
+
);
|
|
313
|
+
viewer.gridHelper.clearCache();
|
|
314
|
+
viewer.gridHelper.update(viewer.getCameraZoom(), true);
|
|
315
|
+
viewer.update(true, true);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function handleMessage(message) {
|
|
320
|
+
var data = message;
|
|
321
|
+
|
|
322
|
+
if (data.type === "logo") {
|
|
323
|
+
// The splash lives in the core, so a host asks for it by name and
|
|
324
|
+
// sends only what it alone knows: its theme, its tree width and
|
|
325
|
+
// its modifier keys.
|
|
326
|
+
const splash = logo();
|
|
327
|
+
data = {
|
|
328
|
+
...splash,
|
|
329
|
+
config: { ...splash.config, ...(data.config || {}) }
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (data.type === "data" && data?.data?.shapes?.parts?.length > 0) {
|
|
334
|
+
const timer = new Timer("webView", data.config.timeit);
|
|
335
|
+
|
|
336
|
+
const oldStates = currentStates(viewer);
|
|
337
|
+
|
|
338
|
+
let meshData = data.data;
|
|
339
|
+
let config = data.config;
|
|
340
|
+
|
|
341
|
+
if (config._splash) {
|
|
342
|
+
const displayOptions = getDisplayOptions(config.theme);
|
|
343
|
+
config.zoom = Math.min(
|
|
344
|
+
1.0,
|
|
345
|
+
displayOptions.cadWidth / displayOptions.height
|
|
346
|
+
);
|
|
347
|
+
// debugLog("logo zoom =", config.zoom);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
showViewer(meshData, config);
|
|
351
|
+
|
|
352
|
+
// Explicit states win, otherwise the user's prior visibility
|
|
353
|
+
// choices are restored for whatever survived into the new
|
|
354
|
+
// model - in one batched setStates either way, because a
|
|
355
|
+
// per-key loop is a repaint per key and freezes the host.
|
|
356
|
+
restoreStates(viewer, meshData.shapes, oldStates, config.states);
|
|
357
|
+
timer.split("states updated");
|
|
358
|
+
|
|
359
|
+
timer.stop();
|
|
360
|
+
} else if (data.type === "screenshot") {
|
|
361
|
+
var promise = viewer.getImage(data.filename);
|
|
362
|
+
promise.then((result) => {
|
|
363
|
+
send("screenshot", {
|
|
364
|
+
filename: result.task,
|
|
365
|
+
data: result.dataUrl
|
|
366
|
+
});
|
|
367
|
+
});
|
|
368
|
+
} else if (data.type === "set_relative_time") {
|
|
369
|
+
if (viewer) {
|
|
370
|
+
viewer.setRelativeTime(data.value);
|
|
371
|
+
}
|
|
372
|
+
} else if (data.type === "backend_response") {
|
|
373
|
+
viewer.handleBackendResponse(data);
|
|
374
|
+
} else if (data.type === "clear") {
|
|
375
|
+
// Python's `show_clear()`, and `show_all()` when it finds nothing
|
|
376
|
+
// drawable. clear() is a no-op on a viewer that shows nothing, and
|
|
377
|
+
// the stored model goes with the scene - a cleared model must not
|
|
378
|
+
// be resurrectable by anything that re-renders `_meshData`.
|
|
379
|
+
if (viewer != null) {
|
|
380
|
+
viewer.clear();
|
|
381
|
+
_meshData = null;
|
|
382
|
+
_shapes = null;
|
|
383
|
+
}
|
|
384
|
+
// No `init` branch, which was one of two of the same kind and the
|
|
385
|
+
// worse: it called `init(data.paths, data.settings)`, a name this
|
|
386
|
+
// module never defines. Only the extension sends an `init` message,
|
|
387
|
+
// and `viewer.html`'s own listener answers it - by *calling*
|
|
388
|
+
// `createPage`, so this listener does not exist yet when the first one
|
|
389
|
+
// arrives. A second one would have reached here and thrown a
|
|
390
|
+
// ReferenceError. Deleting it also removes the one branch that could
|
|
391
|
+
// not work from a host driving `handleMessage` directly.
|
|
392
|
+
//
|
|
393
|
+
// No `show` branch: nothing in any host sends it, and it was a
|
|
394
|
+
// landmine rather than merely dead - `showViewer()` with
|
|
395
|
+
// no arguments evaluates `getDisplayOptions(config.theme)` against an
|
|
396
|
+
// undefined config and throws before reaching its own null guard, and
|
|
397
|
+
// it assigns `_meshData = undefined` on the way, destroying the stored
|
|
398
|
+
// model so that even a corrected call could not re-show. If a host
|
|
399
|
+
// ever needs to re-render what it already has, that is `showViewer(
|
|
400
|
+
// _meshData, _config)` guarded on `_meshData` being present.
|
|
401
|
+
} else if (data.type === "ui") {
|
|
402
|
+
if (_config["_splash"]) {
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
if (data.config.debug) {
|
|
406
|
+
debugLog("data.config", data.config);
|
|
407
|
+
}
|
|
408
|
+
// One dispatch for every key, shared with every other host, so
|
|
409
|
+
// that a setter cannot exist in one client and not another.
|
|
410
|
+
applyConfig(viewer, data.config, {
|
|
411
|
+
// Only the host knows the other two dimensions, so a
|
|
412
|
+
// viewport key is a resize rather than a setter.
|
|
413
|
+
resize: (key, value) => {
|
|
414
|
+
const displayOptions = getDisplayOptions(data.config.theme);
|
|
415
|
+
const glass =
|
|
416
|
+
data.config.glass !== undefined
|
|
417
|
+
? data.config.glass
|
|
418
|
+
: displayOptions.glass;
|
|
419
|
+
viewer.resizeCadView(
|
|
420
|
+
key === "cadWidth" ? value : displayOptions.cadWidth,
|
|
421
|
+
key === "treeWidth" ? value : displayOptions.treeWidth,
|
|
422
|
+
key === "height" ? value : displayOptions.height,
|
|
423
|
+
glass
|
|
424
|
+
);
|
|
425
|
+
},
|
|
426
|
+
onUnknown: (key) => {
|
|
427
|
+
debugLog(`ui: no setter for '${key}'`);
|
|
428
|
+
}
|
|
429
|
+
});
|
|
430
|
+
} else if (data.type === "animation") {
|
|
431
|
+
// Explode goes off first: both transform the same objects,
|
|
432
|
+
// and animating an already-displaced model is wrong.
|
|
433
|
+
animate(viewer, data.data, data.config.speed, (action) => {
|
|
434
|
+
console.error(`Unknown animation action: ${action}`);
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// The window is where two of the three hosts' messages and resizes arrive,
|
|
440
|
+
// and it is not where the third's do: a pane host has no window message of
|
|
441
|
+
// its own and resizes when a splitter moves, so it passes `listen: false`
|
|
442
|
+
// and calls `handleMessage` and `resize` itself. The dispatch above is the
|
|
443
|
+
// same one either way, which is the point - a message branch is a feature,
|
|
444
|
+
// and a host that wrote its own dispatch would have its own feature set.
|
|
445
|
+
if (listen !== false) {
|
|
446
|
+
window.addEventListener("resize", () => resize(), true);
|
|
447
|
+
window.addEventListener("message", (event) => {
|
|
448
|
+
handleMessage(
|
|
449
|
+
typeof event.data === "string" || event.data instanceof String
|
|
450
|
+
? JSON.parse(event.data)
|
|
451
|
+
: event.data
|
|
452
|
+
);
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
return {
|
|
457
|
+
/** Draw the splash, with whatever the host knows that the core cannot. */
|
|
458
|
+
showSplash(config) {
|
|
459
|
+
const splash = logo();
|
|
460
|
+
showViewer(splash.data, { ...splash.config, ...config });
|
|
461
|
+
},
|
|
462
|
+
|
|
463
|
+
/** The host's theme changed under it - a VS Code colour theme, say. */
|
|
464
|
+
setTheme(next) {
|
|
465
|
+
if (viewer != null) {
|
|
466
|
+
viewer.setTheme(next);
|
|
467
|
+
}
|
|
468
|
+
},
|
|
469
|
+
|
|
470
|
+
/** One message, already parsed. What the window listener above calls. */
|
|
471
|
+
handleMessage,
|
|
472
|
+
|
|
473
|
+
/** The surface changed size. What the resize listener above calls. */
|
|
474
|
+
resize
|
|
475
|
+
};
|
|
476
|
+
}
|