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/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
+ }