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/notify.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reporting viewer changes back to Python.
|
|
3
|
+
*
|
|
4
|
+
* three-cad-viewer calls a notification callback with `{key: {old, new}}` for
|
|
5
|
+
* whatever changed. This turns that into the message a host sends, and keeps a
|
|
6
|
+
* running picture of the viewer's state so a host can answer `status()` out of
|
|
7
|
+
* memory instead of asking the browser.
|
|
8
|
+
*
|
|
9
|
+
* The names here are the renderer's own notification names, which are
|
|
10
|
+
* snake_case - `clip_intersection`, `zebra_count`, `relative_time`. So nothing
|
|
11
|
+
* is translated on the way back: the renderer already emits what Python
|
|
12
|
+
* speaks, and the camelCase conversion is one-directional by the renderer's own
|
|
13
|
+
* choice rather than by ours.
|
|
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
|
+
/**
|
|
33
|
+
* Keys that report that something happened, as opposed to what something is.
|
|
34
|
+
*
|
|
35
|
+
* These must never be accumulated into a status snapshot. An accumulated
|
|
36
|
+
* `selectedShapeIDs` replays a selection the user made minutes ago into the
|
|
37
|
+
* next measurement, against a model that may not even contain those ids.
|
|
38
|
+
*
|
|
39
|
+
* The distinction is not "does it change often". It is whether re-applying the
|
|
40
|
+
* last value to a different model still means anything.
|
|
41
|
+
*/
|
|
42
|
+
export const EVENT_KEYS = new Set([
|
|
43
|
+
"selectedShapeIDs",
|
|
44
|
+
"selected",
|
|
45
|
+
"lastPick",
|
|
46
|
+
"activeTool",
|
|
47
|
+
"relative_time",
|
|
48
|
+
]);
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Create the notification callback.
|
|
52
|
+
*
|
|
53
|
+
* The message sent is the *delta* - only what this change carried - rather than
|
|
54
|
+
* a snapshot accumulated across every change so far. `viewer.html` accumulates
|
|
55
|
+
* into one long-lived object, which is what lets a stale event key ride along
|
|
56
|
+
* with an unrelated update. The accumulated picture still exists here, as
|
|
57
|
+
* `status`, but it holds state keys only and is never what goes on the wire.
|
|
58
|
+
*
|
|
59
|
+
* @param viewer the viewer, read for tree states
|
|
60
|
+
* @param send (message) => void, the host's transport
|
|
61
|
+
* @param debug optional (label, value) => void
|
|
62
|
+
* @returns `{ notify, status }` - `notify` is the callback to hand the viewer,
|
|
63
|
+
* `status` is the live picture to answer a status request from
|
|
64
|
+
*/
|
|
65
|
+
export function createNotifier({ viewer, send, debug }) {
|
|
66
|
+
const status = {};
|
|
67
|
+
let lastStatesJson = null;
|
|
68
|
+
|
|
69
|
+
function notify(change) {
|
|
70
|
+
if (debug) {
|
|
71
|
+
debug("notify", change);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const message = {};
|
|
75
|
+
let changed = false;
|
|
76
|
+
|
|
77
|
+
for (const key of Object.keys(change)) {
|
|
78
|
+
const value = change[key] == null ? undefined : change[key].new;
|
|
79
|
+
if (value === undefined) {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
message[key] = value;
|
|
83
|
+
changed = true;
|
|
84
|
+
if (!EVENT_KEYS.has(key)) {
|
|
85
|
+
status[key] = value;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Tree state is not part of the change set - the viewer reports it
|
|
90
|
+
// separately - so it is read and compared. Serialising is also how it gets
|
|
91
|
+
// cloned: `getStates()` hands back the tree's own live arrays, so keeping
|
|
92
|
+
// the reference would compare an object against itself and never report a
|
|
93
|
+
// change.
|
|
94
|
+
if (viewer != null && viewer.treeview != null) {
|
|
95
|
+
const json = JSON.stringify(viewer.treeview.getStates());
|
|
96
|
+
if (lastStatesJson == null || lastStatesJson !== json) {
|
|
97
|
+
const states = JSON.parse(json);
|
|
98
|
+
message.states = states;
|
|
99
|
+
status.states = states;
|
|
100
|
+
lastStatesJson = json;
|
|
101
|
+
changed = true;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (changed) {
|
|
106
|
+
send(message);
|
|
107
|
+
}
|
|
108
|
+
return changed ? message : null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return { notify, status };
|
|
112
|
+
}
|
package/src/options.js
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Assembling the three option objects `Viewer.render` and `new Display` take.
|
|
3
|
+
*
|
|
4
|
+
* Which option belongs to which object is three-cad-viewer's fact rather than
|
|
5
|
+
* Python's, so the lists live here in renderer names. The config arrives in
|
|
6
|
+
* those names, Python having converted once at the boundary; no name is
|
|
7
|
+
* translated here. A key still in snake_case is a host that has not converted,
|
|
8
|
+
* and it will miss its entry and fall back to a default rather than be renamed
|
|
9
|
+
* into something plausible.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/*
|
|
13
|
+
Copyright 2026 Bernhard Walter
|
|
14
|
+
|
|
15
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
16
|
+
you may not use this file except in compliance with the License.
|
|
17
|
+
You may obtain a copy of the License at
|
|
18
|
+
|
|
19
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
20
|
+
|
|
21
|
+
Unless required by applicable law or agreed to in writing, software
|
|
22
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
23
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
24
|
+
See the License for the specific language governing permissions and
|
|
25
|
+
limitations under the License.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
// The defaults themselves, not only the key lists. They describe
|
|
29
|
+
// three-cad-viewer, so every client should start from the same numbers and
|
|
30
|
+
// override only what is genuinely its own - a viewer whose ambient light or
|
|
31
|
+
// whose metalness differs from another's for no stated reason is a difference
|
|
32
|
+
// nobody chose.
|
|
33
|
+
|
|
34
|
+
/** Defaults for `Viewer.render`'s second argument. */
|
|
35
|
+
export const RENDER_DEFAULTS = {
|
|
36
|
+
ambientIntensity: 1.0,
|
|
37
|
+
directIntensity: 1.1,
|
|
38
|
+
metalness: 0.3,
|
|
39
|
+
roughness: 0.65,
|
|
40
|
+
// A CSS colour rather than 0x707070. The renderer takes either, but Python
|
|
41
|
+
// always sends this form - `Color(...).web_color` - and a host that mirrors
|
|
42
|
+
// what the renderer reports back into a string-typed setting cannot take the
|
|
43
|
+
// number. One representation across the ecosystem, and it is this one.
|
|
44
|
+
edgeColor: "#707070",
|
|
45
|
+
defaultOpacity: 0.5,
|
|
46
|
+
normalLen: 0,
|
|
47
|
+
angularTolerance: 0.2,
|
|
48
|
+
deviation: 0.1,
|
|
49
|
+
defaultColor: "#e8b024",
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/** Defaults for `Viewer.render`'s third argument. */
|
|
53
|
+
export const VIEWER_DEFAULTS = {
|
|
54
|
+
timeit: false,
|
|
55
|
+
zoom: 1.0,
|
|
56
|
+
position: null,
|
|
57
|
+
quaternion: null,
|
|
58
|
+
target: null,
|
|
59
|
+
centerGrid: false,
|
|
60
|
+
gridFontSize: 12,
|
|
61
|
+
newTreeBehavior: true,
|
|
62
|
+
studioEnvironment: "studio",
|
|
63
|
+
studioEnvIntensity: 1.0,
|
|
64
|
+
studioEnvRotation: 0,
|
|
65
|
+
studioBackground: "environment",
|
|
66
|
+
studioToneMapping: "neutral",
|
|
67
|
+
studioExposure: 1.0,
|
|
68
|
+
studioShadowIntensity: 0.5,
|
|
69
|
+
studioShadowSoftness: 0.2,
|
|
70
|
+
studioAOIntensity: 0.5,
|
|
71
|
+
studioTextureMapping: "parametric",
|
|
72
|
+
studio4kEnvMaps: false,
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Defaults for `new Display(container, options)`.
|
|
77
|
+
*
|
|
78
|
+
* `cadWidth`, `height` and `treeWidth` are here for completeness and are always
|
|
79
|
+
* replaced by the geometry the host measures. The tool flags say which tools
|
|
80
|
+
* exist in this surface at all, which is a host's decision - these are the
|
|
81
|
+
* common answer, not the only one.
|
|
82
|
+
*/
|
|
83
|
+
export const DISPLAY_DEFAULTS = {
|
|
84
|
+
cadWidth: 730,
|
|
85
|
+
height: 525,
|
|
86
|
+
treeWidth: 240,
|
|
87
|
+
glass: false,
|
|
88
|
+
tools: true,
|
|
89
|
+
theme: "browser",
|
|
90
|
+
pinning: false,
|
|
91
|
+
newTreeBehavior: true,
|
|
92
|
+
keymap: {
|
|
93
|
+
shift: "shiftKey",
|
|
94
|
+
ctrl: "ctrlKey",
|
|
95
|
+
meta: "metaKey",
|
|
96
|
+
alt: "altKey",
|
|
97
|
+
},
|
|
98
|
+
measureTools: true,
|
|
99
|
+
selectTool: true,
|
|
100
|
+
explodeTool: true,
|
|
101
|
+
// Stated here rather than left to the renderer's own default, which is
|
|
102
|
+
// `true`: a host passing `studioTool: false` to hide the Studio tab was
|
|
103
|
+
// silently keeping it, because `display.ts` tests `=== false` and the key
|
|
104
|
+
// never arrived to be tested.
|
|
105
|
+
studioTool: true,
|
|
106
|
+
zebraTool: true,
|
|
107
|
+
zscaleTool: false,
|
|
108
|
+
externalMeasurementBackend: true,
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
/** Options `Viewer.render` takes as its second argument. */
|
|
112
|
+
export const RENDER_OPTION_KEYS = [
|
|
113
|
+
"ambientIntensity",
|
|
114
|
+
"directIntensity",
|
|
115
|
+
"metalness",
|
|
116
|
+
"roughness",
|
|
117
|
+
"edgeColor",
|
|
118
|
+
"defaultOpacity",
|
|
119
|
+
"normalLen",
|
|
120
|
+
];
|
|
121
|
+
|
|
122
|
+
/** Options `Viewer.render` takes as its third argument. */
|
|
123
|
+
export const VIEWER_OPTION_KEYS = [
|
|
124
|
+
"axes",
|
|
125
|
+
"axes0",
|
|
126
|
+
// A state key `ViewerState._update` accepts and the tree builder reads on
|
|
127
|
+
// every render (`viewer.ts:1343`, `:3446`). Without it here the setting had
|
|
128
|
+
// no route at all in the two page hosts: it is not in either one's splash
|
|
129
|
+
// config, so the Display and Viewer were built with the logo config's
|
|
130
|
+
// `true`, later shows computed it and `page.js` dropped it because the
|
|
131
|
+
// Display already existed, and `apply.js` has no setter for it. Jupyter
|
|
132
|
+
// CadQuery is the contrast that proves it - there `viewer_args` carries it
|
|
133
|
+
// and it works.
|
|
134
|
+
"newTreeBehavior",
|
|
135
|
+
"blackEdges",
|
|
136
|
+
"grid",
|
|
137
|
+
"collapse",
|
|
138
|
+
"ortho",
|
|
139
|
+
"ticks",
|
|
140
|
+
"centerGrid",
|
|
141
|
+
"gridFontSize",
|
|
142
|
+
"timeit",
|
|
143
|
+
"tools",
|
|
144
|
+
"glass",
|
|
145
|
+
"up",
|
|
146
|
+
"transparent",
|
|
147
|
+
"control",
|
|
148
|
+
"panSpeed",
|
|
149
|
+
"zoomSpeed",
|
|
150
|
+
"rotateSpeed",
|
|
151
|
+
"clipSlider0",
|
|
152
|
+
"clipSlider1",
|
|
153
|
+
"clipSlider2",
|
|
154
|
+
"clipNormal0",
|
|
155
|
+
"clipNormal1",
|
|
156
|
+
"clipNormal2",
|
|
157
|
+
"clipIntersection",
|
|
158
|
+
"clipPlaneHelpers",
|
|
159
|
+
"clipObjectColors",
|
|
160
|
+
"zebraCount",
|
|
161
|
+
"zebraOpacity",
|
|
162
|
+
"zebraDirection",
|
|
163
|
+
"zebraColorScheme",
|
|
164
|
+
"zebraMappingMode",
|
|
165
|
+
"studioEnvironment",
|
|
166
|
+
"studioEnvIntensity",
|
|
167
|
+
"studioEnvRotation",
|
|
168
|
+
"studioBackground",
|
|
169
|
+
"studioToneMapping",
|
|
170
|
+
"studioExposure",
|
|
171
|
+
"studioShadowIntensity",
|
|
172
|
+
"studioShadowSoftness",
|
|
173
|
+
"studioAOIntensity",
|
|
174
|
+
"studioTextureMapping",
|
|
175
|
+
"studio4kEnvMaps",
|
|
176
|
+
];
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* The config's value for a key, or the fallback.
|
|
180
|
+
*
|
|
181
|
+
* `null` and `undefined` both mean "not given": a host that sends an explicit
|
|
182
|
+
* null is asking for the default, not for null. Every other falsy value -
|
|
183
|
+
* `false`, `0`, `""` - is a value the caller meant, so the test is against null
|
|
184
|
+
* rather than truthiness. Getting that wrong turns `axes: false` into `axes:
|
|
185
|
+
* true` wherever the default is on.
|
|
186
|
+
*/
|
|
187
|
+
export function preset(config, key, fallback) {
|
|
188
|
+
return config == null || config[key] == null ? fallback : config[key];
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** Pick a list of keys out of the config, defaulting each. */
|
|
192
|
+
function pick(keys, config, defaults) {
|
|
193
|
+
const options = {};
|
|
194
|
+
for (const key of keys) {
|
|
195
|
+
const value = preset(config, key, defaults == null ? undefined : defaults[key]);
|
|
196
|
+
// A key nobody has a value for is left out rather than passed as
|
|
197
|
+
// undefined. The two are not the same to the renderer: an option that is
|
|
198
|
+
// present is applied, and applying undefined resets whatever it names -
|
|
199
|
+
// which for a camera key means the viewer reports null back, and a host
|
|
200
|
+
// whose settings are typed refuses it.
|
|
201
|
+
//
|
|
202
|
+
// It matters most for a host whose config comes from what a user has set
|
|
203
|
+
// rather than from a stored workspace, where most keys are legitimately
|
|
204
|
+
// absent.
|
|
205
|
+
if (value !== undefined) {
|
|
206
|
+
options[key] = value;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return options;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Each builder starts from the core's defaults and lets a host override the
|
|
213
|
+
// ones that are genuinely its own. A host that passes nothing gets the shared
|
|
214
|
+
// answer, which is the point: the numbers describe three-cad-viewer, so a
|
|
215
|
+
// client differing from another for no stated reason is a difference nobody
|
|
216
|
+
// chose.
|
|
217
|
+
|
|
218
|
+
export function buildRenderOptions(config, overrides) {
|
|
219
|
+
return pick(RENDER_OPTION_KEYS, config, { ...RENDER_DEFAULTS, ...overrides });
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function buildViewerOptions(config, overrides) {
|
|
223
|
+
return pick(VIEWER_OPTION_KEYS, config, { ...VIEWER_DEFAULTS, ...overrides });
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* The display options, which are what `new Display(container, options)` reads
|
|
228
|
+
* and what seeds the viewer's state.
|
|
229
|
+
*
|
|
230
|
+
* `geometry` is the host's: only it knows the size of the surface it is drawing
|
|
231
|
+
* on and what to subtract for its own chrome. It supplies `cadWidth`, `height`
|
|
232
|
+
* and `treeWidth` already normalised.
|
|
233
|
+
*
|
|
234
|
+
* `theme` arrives as "light", "dark" or "browser" and is passed through: it is
|
|
235
|
+
* one word in every language here, and the renderer resolves "browser" itself.
|
|
236
|
+
* The boolean `dark` it replaced is gone from the vocabulary - it had not been
|
|
237
|
+
* on the wire since 2025, because each host converted it to `theme` before
|
|
238
|
+
* answering a config request.
|
|
239
|
+
*/
|
|
240
|
+
export function buildDisplayOptions(config, overrides, geometry) {
|
|
241
|
+
const defaults = { ...DISPLAY_DEFAULTS, ...overrides };
|
|
242
|
+
const fromConfig = pick(["glass", "tools", "keymap", "newTreeBehavior"], config, defaults);
|
|
243
|
+
|
|
244
|
+
const theme = preset(config, "theme", defaults.theme);
|
|
245
|
+
|
|
246
|
+
return {
|
|
247
|
+
...fromConfig,
|
|
248
|
+
theme,
|
|
249
|
+
cadWidth: geometry.cadWidth,
|
|
250
|
+
height: geometry.height,
|
|
251
|
+
treeWidth: geometry.treeWidth,
|
|
252
|
+
|
|
253
|
+
// Capability flags the host owns: whether a tool exists in this surface at
|
|
254
|
+
// all is not something a document can ask for.
|
|
255
|
+
//
|
|
256
|
+
// `pinning` and `studioTool` were merged into `defaults` above and then
|
|
257
|
+
// left out of this hand-written list, so they never reached the renderer.
|
|
258
|
+
// `display.ts` reads `options.pinning` in its constructor, which is why
|
|
259
|
+
// cad-viewer-widget's pin-as-PNG button could not appear in any cell
|
|
260
|
+
// viewer, `pinning=True` asked for or defaulted. Nothing was said: an
|
|
261
|
+
// option the renderer does not receive is indistinguishable from one it
|
|
262
|
+
// was never given.
|
|
263
|
+
pinning: defaults.pinning,
|
|
264
|
+
studioTool: defaults.studioTool,
|
|
265
|
+
measureTools: defaults.measureTools,
|
|
266
|
+
selectTool: defaults.selectTool,
|
|
267
|
+
explodeTool: defaults.explodeTool,
|
|
268
|
+
zscaleTool: defaults.zscaleTool,
|
|
269
|
+
zebraTool: defaults.zebraTool,
|
|
270
|
+
externalMeasurementBackend: defaults.externalMeasurementBackend,
|
|
271
|
+
};
|
|
272
|
+
}
|