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/package.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ocp-viewer-core",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Shared viewer policy for ocp_vscode, ocp_viewer, Jupyter CadQuery and build123d Studio",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"src"
|
|
12
|
+
],
|
|
13
|
+
"peerDependencies": {
|
|
14
|
+
"three-cad-viewer": ">=5.0.3 <5.1.0"
|
|
15
|
+
},
|
|
16
|
+
"license": "Apache-2.0",
|
|
17
|
+
"author": "Bernhard Walter <b_walter@arcor.de>",
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "https://github.com/bernhard-42/ocp-viewer-core.git"
|
|
21
|
+
}
|
|
22
|
+
}
|
package/src/animation.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Animation tracks.
|
|
3
|
+
*
|
|
4
|
+
* A track is `[selector, action, times, values]`, and the action decides which
|
|
5
|
+
* of the viewer's five track methods it becomes. `viewer.html` has this as a
|
|
6
|
+
* switch in its message handler; cad-viewer-widget has the same mapping spread
|
|
7
|
+
* across `addTrack`, `addTracks`, `animate` and `clearAnimation`.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/*
|
|
11
|
+
Copyright 2026 Bernhard Walter
|
|
12
|
+
|
|
13
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
14
|
+
you may not use this file except in compliance with the License.
|
|
15
|
+
You may obtain a copy of the License at
|
|
16
|
+
|
|
17
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
18
|
+
|
|
19
|
+
Unless required by applicable law or agreed to in writing, software
|
|
20
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
21
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
22
|
+
See the License for the specific language governing permissions and
|
|
23
|
+
limitations under the License.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/** The two-letter action codes, and the call each becomes. */
|
|
27
|
+
const TRACKS = {
|
|
28
|
+
t: (v, sel, times, values) => v.addPositionTrack(sel, times, values),
|
|
29
|
+
q: (v, sel, times, values) => v.addQuaternionTrack(sel, times, values),
|
|
30
|
+
tx: (v, sel, times, values) => v.addTranslationTrack(sel, "x", times, values),
|
|
31
|
+
ty: (v, sel, times, values) => v.addTranslationTrack(sel, "y", times, values),
|
|
32
|
+
tz: (v, sel, times, values) => v.addTranslationTrack(sel, "z", times, values),
|
|
33
|
+
rx: (v, sel, times, values) => v.addRotationTrack(sel, "x", times, values),
|
|
34
|
+
ry: (v, sel, times, values) => v.addRotationTrack(sel, "y", times, values),
|
|
35
|
+
rz: (v, sel, times, values) => v.addRotationTrack(sel, "z", times, values),
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Add one track. An unknown action is reported rather than ignored - it means a
|
|
40
|
+
* producer and this table disagree, and silently dropping a track produces an
|
|
41
|
+
* animation that is subtly wrong instead of one that fails.
|
|
42
|
+
*/
|
|
43
|
+
export function addAnimationTrack(viewer, track, onUnknown) {
|
|
44
|
+
const [selector, action, times, values] = track;
|
|
45
|
+
const add = TRACKS[action];
|
|
46
|
+
if (add == null) {
|
|
47
|
+
if (onUnknown) {
|
|
48
|
+
onUnknown(action, track);
|
|
49
|
+
}
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
add(viewer, selector, times, values);
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** The longest time in any track, which is how long the animation runs. */
|
|
57
|
+
export function animationDuration(tracks) {
|
|
58
|
+
let duration = 0;
|
|
59
|
+
for (const track of tracks) {
|
|
60
|
+
for (const time of track[2]) {
|
|
61
|
+
if (time > duration) {
|
|
62
|
+
duration = time;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return duration;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Load a set of tracks and start the animation.
|
|
71
|
+
*
|
|
72
|
+
* Explode is turned off first: both are transforms on the same objects, and
|
|
73
|
+
* leaving explode on animates an already-displaced model. A speed of zero loads
|
|
74
|
+
* the tracks without starting - that is how a caller scrubs by hand rather than
|
|
75
|
+
* playing.
|
|
76
|
+
*/
|
|
77
|
+
export function animate(viewer, tracks, speed, onUnknown) {
|
|
78
|
+
viewer.setExplode(false);
|
|
79
|
+
for (const track of tracks) {
|
|
80
|
+
addAnimationTrack(viewer, track, onUnknown);
|
|
81
|
+
}
|
|
82
|
+
const duration = animationDuration(tracks);
|
|
83
|
+
if (speed > 0) {
|
|
84
|
+
viewer.initAnimation(duration, speed);
|
|
85
|
+
}
|
|
86
|
+
return duration;
|
|
87
|
+
}
|
package/src/apply.js
ADDED
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Applying a configuration to a running three-cad-viewer instance.
|
|
3
|
+
*
|
|
4
|
+
* One dispatch for every host: a set of changed keys in, a call on the viewer
|
|
5
|
+
* for each. A host asked to apply a configuration - from Python, or from a
|
|
6
|
+
* widget's own change observer - routes it through here.
|
|
7
|
+
*
|
|
8
|
+
* The keys are camelCase and the values are plain JSON, because that is what
|
|
9
|
+
* this side of the wire speaks. Python owns snake_case and the enums, and
|
|
10
|
+
* converts once, at the boundary, in `Config.to_javascript`. Nothing here
|
|
11
|
+
* translates a name or unwraps an enum: a key that arrives in snake_case is a
|
|
12
|
+
* host that has not converted, and it should surface as an unknown key rather
|
|
13
|
+
* than be quietly accepted in both spellings.
|
|
14
|
+
*
|
|
15
|
+
* A host whose wire format shares one name across both halves - an ipywidgets
|
|
16
|
+
* traitlet, say - converts before calling in. The rule is the same; only the
|
|
17
|
+
* place it is applied differs.
|
|
18
|
+
*
|
|
19
|
+
* The method is not derivable from the name: `grid` is `setGrids`, `glass` is
|
|
20
|
+
* `glassMode`, `tools` is `showTools`, `collapse` is `collapseNodes`. Deriving a
|
|
21
|
+
* mechanism from a name is how the option path silently lost keys, so the table
|
|
22
|
+
* is written out.
|
|
23
|
+
*
|
|
24
|
+
* Nothing here touches the DOM, a transport, or a host, so it can be tested
|
|
25
|
+
* headless against a stub viewer.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/*
|
|
29
|
+
Copyright 2026 Bernhard Walter
|
|
30
|
+
|
|
31
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
32
|
+
you may not use this file except in compliance with the License.
|
|
33
|
+
You may obtain a copy of the License at
|
|
34
|
+
|
|
35
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
36
|
+
|
|
37
|
+
Unless required by applicable law or agreed to in writing, software
|
|
38
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
39
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
40
|
+
See the License for the specific language governing permissions and
|
|
41
|
+
limitations under the License.
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
// Keys whose value is a viewport dimension. Only the host knows the other two,
|
|
45
|
+
// so applying one means asking the host to resize rather than calling the
|
|
46
|
+
// viewer directly. Both hosts end in `resizeCadView`, reading the other
|
|
47
|
+
// dimensions from their own state.
|
|
48
|
+
export const GEOMETRY_KEYS = ["cadWidth", "treeWidth", "height"];
|
|
49
|
+
|
|
50
|
+
// The camera views `resetCamera` accepts beyond "reset", which is "iso" plus a
|
|
51
|
+
// resize. Anything else - including "keep" - deliberately does nothing, because
|
|
52
|
+
// `setView` on an unknown name leaves the camera where it was with no error.
|
|
53
|
+
export const VIEWS = ["iso", "left", "right", "top", "bottom", "rear", "front"];
|
|
54
|
+
|
|
55
|
+
const TOOLS = ["distance", "properties", "select"];
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Call a viewer setter, appending the notify flag only when the host asked for
|
|
59
|
+
* one. cad-viewer-widget passes `true` to keep its traitlets in step;
|
|
60
|
+
* ocp_vscode omits it and takes the setter's own default. Passing `undefined`
|
|
61
|
+
* is not the same as not passing it, so the argument list is built rather than
|
|
62
|
+
* padded.
|
|
63
|
+
*/
|
|
64
|
+
function call(viewer, method, args, notify) {
|
|
65
|
+
if (notify === undefined) {
|
|
66
|
+
viewer[method](...args);
|
|
67
|
+
} else {
|
|
68
|
+
viewer[method](...args, notify);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const SETTERS = {
|
|
73
|
+
axes: (v, value, ctx) => call(v, "setAxes", [value], ctx.notify),
|
|
74
|
+
axes0: (v, value, ctx) => call(v, "setAxes0", [value], ctx.notify),
|
|
75
|
+
grid: (v, value, ctx) => call(v, "setGrids", [value], ctx.notify),
|
|
76
|
+
centerGrid: (v, value, ctx) => call(v, "setGridCenter", [value], ctx.notify),
|
|
77
|
+
ortho: (v, value, ctx) => call(v, "setOrtho", [value], ctx.notify),
|
|
78
|
+
transparent: (v, value, ctx) => call(v, "setTransparent", [value], ctx.notify),
|
|
79
|
+
blackEdges: (v, value, ctx) => call(v, "setBlackEdges", [value], ctx.notify),
|
|
80
|
+
|
|
81
|
+
zoom: (v, value, ctx) => call(v, "setCameraZoom", [value], ctx.notify),
|
|
82
|
+
// `setCameraPosition(position, relative, notify)` takes a flag between the
|
|
83
|
+
// value and the notify flag that the other three camera setters do not, so
|
|
84
|
+
// `relative` is passed explicitly. Without it a host that asks for
|
|
85
|
+
// notification lands `true` in that slot and the camera moves *by* the
|
|
86
|
+
// vector instead of *to* it.
|
|
87
|
+
position: (v, value, ctx) => call(v, "setCameraPosition", [value, false], ctx.notify),
|
|
88
|
+
quaternion: (v, value, ctx) => call(v, "setCameraQuaternion", [value], ctx.notify),
|
|
89
|
+
target: (v, value, ctx) => call(v, "setCameraTarget", [value], ctx.notify),
|
|
90
|
+
|
|
91
|
+
// No `up` here, and it is not an omission. It cannot be applied to a live
|
|
92
|
+
// viewer: `Camera` reads `cameraUp[this.up]` once, in its constructor, so
|
|
93
|
+
// assigning `camera.up` afterwards changes nothing about the cameras - it only
|
|
94
|
+
// corrupts the lookup `presetCamera` makes, and the next click on ISO or TOP
|
|
95
|
+
// dies on `defaultDirections[undefined]`. Worse, the value written was the
|
|
96
|
+
// config's "Z", where that lookup is keyed by "z_up".
|
|
97
|
+
//
|
|
98
|
+
// `up` is a render option instead, and `Viewer.render` honours it: it builds a
|
|
99
|
+
// new `Camera` with `viewerOptions.up` every time. So `set_defaults(up=...)`
|
|
100
|
+
// takes effect on the next show, which is the only moment it can.
|
|
101
|
+
|
|
102
|
+
edgeColor: (v, value, ctx) => call(v, "setEdgeColor", [value], ctx.notify),
|
|
103
|
+
defaultOpacity: (v, value, ctx) => call(v, "setOpacity", [value], ctx.notify),
|
|
104
|
+
ambientIntensity: (v, value, ctx) => call(v, "setAmbientLight", [value], ctx.notify),
|
|
105
|
+
directIntensity: (v, value, ctx) => call(v, "setDirectLight", [value], ctx.notify),
|
|
106
|
+
metalness: (v, value, ctx) => call(v, "setMetalness", [value], ctx.notify),
|
|
107
|
+
roughness: (v, value, ctx) => call(v, "setRoughness", [value], ctx.notify),
|
|
108
|
+
|
|
109
|
+
// three-cad-viewer has had `setKeyMap(config)` all along (`viewer.ts:4538`)
|
|
110
|
+
// and nothing called it: the keymap reached only `new Display(...)`, which
|
|
111
|
+
// runs once per page, so the modifier keys a user configured applied in
|
|
112
|
+
// whichever host happened to pass them at splash and nowhere else, and could
|
|
113
|
+
// never be changed on a live viewer.
|
|
114
|
+
keymap: (v, value) => v.setKeyMap(value),
|
|
115
|
+
|
|
116
|
+
zoomSpeed: (v, value, ctx) => call(v, "setZoomSpeed", [value], ctx.notify),
|
|
117
|
+
panSpeed: (v, value, ctx) => call(v, "setPanSpeed", [value], ctx.notify),
|
|
118
|
+
rotateSpeed: (v, value, ctx) => call(v, "setRotateSpeed", [value], ctx.notify),
|
|
119
|
+
|
|
120
|
+
// "light", "dark", or "browser" to follow the surface. Applied on a live
|
|
121
|
+
// viewer as well as at construction, which is what lets a host whose theme
|
|
122
|
+
// changes under it - a VS Code colour theme - say so without rebuilding.
|
|
123
|
+
theme: (v, value) => v.setTheme(value),
|
|
124
|
+
|
|
125
|
+
glass: (v, value, ctx) => call(v, "glassMode", [value], ctx.notify),
|
|
126
|
+
tools: (v, value, ctx) => call(v, "showTools", [value], ctx.notify),
|
|
127
|
+
tab: (v, value, ctx) => call(v, "setActiveTab", [value], ctx.notify),
|
|
128
|
+
explode: (v, value, ctx) => call(v, "setExplode", [value], ctx.notify),
|
|
129
|
+
|
|
130
|
+
// The value is the CollapseState the renderer uses. Python's Collapse enum
|
|
131
|
+
// carries those same numbers, so unwrapping the enum is the whole of the
|
|
132
|
+
// translation and nothing is mapped here.
|
|
133
|
+
collapse: (v, value, ctx) => call(v, "collapseNodes", [value], ctx.notify),
|
|
134
|
+
|
|
135
|
+
clipIntersection: (v, value, ctx) => call(v, "setClipIntersection", [value], ctx.notify),
|
|
136
|
+
clipPlaneHelpers: (v, value, ctx) => call(v, "setClipPlaneHelpers", [value], ctx.notify),
|
|
137
|
+
clipObjectColors: (v, value, ctx) => call(v, "setClipObjectColorCaps", [value], ctx.notify),
|
|
138
|
+
|
|
139
|
+
zebraCount: (v, value, ctx) => call(v, "setZebraCount", [value], ctx.notify),
|
|
140
|
+
zebraOpacity: (v, value, ctx) => call(v, "setZebraOpacity", [value], ctx.notify),
|
|
141
|
+
zebraDirection: (v, value, ctx) => call(v, "setZebraDirection", [value], ctx.notify),
|
|
142
|
+
zebraColorScheme: (v, value, ctx) => call(v, "setZebraColorScheme", [value], ctx.notify),
|
|
143
|
+
zebraMappingMode: (v, value, ctx) => call(v, "setZebraMappingMode", [value], ctx.notify),
|
|
144
|
+
|
|
145
|
+
// The studio family. cad-viewer-widget has had these as a setter table;
|
|
146
|
+
// ocp_vscode accepts all eleven through set_viewer_config and has no branch
|
|
147
|
+
// for any of them, so sending one posts a message the viewer drops without a
|
|
148
|
+
// word. Unifying the dispatch closes that by construction, and it is a gain in
|
|
149
|
+
// capability for ocp_vscode rather than a like-for-like move.
|
|
150
|
+
studioEnvironment: (v, value, ctx) => call(v, "setStudioEnvironment", [value], ctx.notify),
|
|
151
|
+
studioEnvIntensity: (v, value, ctx) => call(v, "setStudioEnvIntensity", [value], ctx.notify),
|
|
152
|
+
studioEnvRotation: (v, value, ctx) => call(v, "setStudioEnvRotation", [value], ctx.notify),
|
|
153
|
+
studioBackground: (v, value, ctx) => call(v, "setStudioBackground", [value], ctx.notify),
|
|
154
|
+
studioToneMapping: (v, value, ctx) => call(v, "setStudioToneMapping", [value], ctx.notify),
|
|
155
|
+
studioExposure: (v, value, ctx) => call(v, "setStudioExposure", [value], ctx.notify),
|
|
156
|
+
studioShadowIntensity: (v, value, ctx) =>
|
|
157
|
+
call(v, "setStudioShadowIntensity", [value], ctx.notify),
|
|
158
|
+
studioShadowSoftness: (v, value, ctx) => call(v, "setStudioShadowSoftness", [value], ctx.notify),
|
|
159
|
+
studioAOIntensity: (v, value, ctx) => call(v, "setStudioAOIntensity", [value], ctx.notify),
|
|
160
|
+
studioTextureMapping: (v, value, ctx) => call(v, "setStudioTextureMapping", [value], ctx.notify),
|
|
161
|
+
studio4kEnvMaps: (v, value, ctx) => call(v, "setStudio4kEnvMaps", [value], ctx.notify),
|
|
162
|
+
|
|
163
|
+
// "reset" means iso plus a resize; the other view names pass through.
|
|
164
|
+
resetCamera: (v, value) => {
|
|
165
|
+
if (value === "reset") {
|
|
166
|
+
v.setView("iso");
|
|
167
|
+
v.resize();
|
|
168
|
+
} else if (VIEWS.includes(value)) {
|
|
169
|
+
v.setView(value);
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
|
|
173
|
+
// Deactivate whatever measurement tool is on before activating the new one.
|
|
174
|
+
// `setTool(name, false)` is a no-op when that tool was not on, so this is safe
|
|
175
|
+
// to run unconditionally. "off" leaves everything off.
|
|
176
|
+
analysisTool: (v, value) => {
|
|
177
|
+
const active = v.state.get("activeTool");
|
|
178
|
+
if (typeof active === "string" && TOOLS.includes(active)) {
|
|
179
|
+
v.display.setTool(active, false);
|
|
180
|
+
}
|
|
181
|
+
if (TOOLS.includes(value)) {
|
|
182
|
+
v.display.setTool(value, true);
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
|
|
186
|
+
// Batched on purpose: a per-key `setState` loop over a large model is one
|
|
187
|
+
// repaint per key and freezes the host. Paths absent from the current model
|
|
188
|
+
// are dropped - a state map outlives the model it was taken from.
|
|
189
|
+
states: (v, value, ctx) => {
|
|
190
|
+
const valid = Object.keys(v.treeview.getStates());
|
|
191
|
+
const next = {};
|
|
192
|
+
for (const path of Object.keys(value)) {
|
|
193
|
+
if (valid.includes(path)) {
|
|
194
|
+
next[path] = value[path];
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
if (Object.keys(next).length > 0) {
|
|
198
|
+
call(v, "setStates", [next], ctx.notify);
|
|
199
|
+
}
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
/** Clip sliders and normals are indexed by the digit their key ends with. */
|
|
204
|
+
function clipSetter(key) {
|
|
205
|
+
if (key.startsWith("clipSlider")) {
|
|
206
|
+
return (v, value, ctx) => call(v, "setClipSlider", [Number(key.slice(-1)), value], ctx.notify);
|
|
207
|
+
}
|
|
208
|
+
if (key.startsWith("clipNormal")) {
|
|
209
|
+
// The slider has to be handed back in, or setting a normal moves the plane.
|
|
210
|
+
return (v, value, ctx) => {
|
|
211
|
+
const index = Number(key.slice(-1));
|
|
212
|
+
call(v, "setClipNormal", [index, value, v.getClipSlider(index)], ctx.notify);
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* What the viewer currently holds for a key, or `undefined` if it cannot be
|
|
220
|
+
* read back.
|
|
221
|
+
*
|
|
222
|
+
* The other half of `SETTERS`, and here for the same reason: which getter
|
|
223
|
+
* answers for which option is three-cad-viewer's fact, not a host's, and a
|
|
224
|
+
* wrong pairing is silent. It exists for `applyConfig`'s `accept` hook - a host
|
|
225
|
+
* driven by change notifications needs to know whether a value is already in
|
|
226
|
+
* place, or it re-applies what the viewer just told it.
|
|
227
|
+
*
|
|
228
|
+
* `undefined` means "no way to ask", not "unset", and a caller should read that
|
|
229
|
+
* as "apply it". Several settings are genuinely write-only from here - explode
|
|
230
|
+
* and the tab are actions rather than state, and the studio family has no
|
|
231
|
+
* getters at all.
|
|
232
|
+
*/
|
|
233
|
+
export function currentValue(viewer, key) {
|
|
234
|
+
const getter = GETTERS[key];
|
|
235
|
+
return getter === undefined ? undefined : getter(viewer);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const GETTERS = {
|
|
239
|
+
axes: (v) => v.getAxes(),
|
|
240
|
+
axes0: (v) => v.getAxes0(),
|
|
241
|
+
grid: (v) => v.getGrids(),
|
|
242
|
+
ortho: (v) => v.getOrtho(),
|
|
243
|
+
transparent: (v) => v.getTransparent(),
|
|
244
|
+
blackEdges: (v) => v.getBlackEdges(),
|
|
245
|
+
tools: (v) => v.getTools(),
|
|
246
|
+
|
|
247
|
+
zoom: (v) => v.getCameraZoom(),
|
|
248
|
+
position: (v) => v.getCameraPosition(),
|
|
249
|
+
quaternion: (v) => v.getCameraQuaternion(),
|
|
250
|
+
target: (v) => v.getCameraTarget(),
|
|
251
|
+
|
|
252
|
+
edgeColor: (v) => v.getEdgeColor(),
|
|
253
|
+
defaultOpacity: (v) => v.getOpacity(),
|
|
254
|
+
ambientIntensity: (v) => v.getAmbientLight(),
|
|
255
|
+
directIntensity: (v) => v.getDirectLight(),
|
|
256
|
+
metalness: (v) => v.getMetalness(),
|
|
257
|
+
roughness: (v) => v.getRoughness(),
|
|
258
|
+
|
|
259
|
+
zoomSpeed: (v) => v.getZoomSpeed(),
|
|
260
|
+
panSpeed: (v) => v.getPanSpeed(),
|
|
261
|
+
rotateSpeed: (v) => v.getRotateSpeed(),
|
|
262
|
+
|
|
263
|
+
clipIntersection: (v) => v.getClipIntersection(),
|
|
264
|
+
clipPlaneHelpers: (v) => v.getClipPlaneHelpers(),
|
|
265
|
+
clipObjectColors: (v) => v.getObjectColorCaps(),
|
|
266
|
+
|
|
267
|
+
clipSlider0: (v) => v.getClipSlider(0),
|
|
268
|
+
clipSlider1: (v) => v.getClipSlider(1),
|
|
269
|
+
clipSlider2: (v) => v.getClipSlider(2),
|
|
270
|
+
clipNormal0: (v) => v.getClipNormal(0),
|
|
271
|
+
clipNormal1: (v) => v.getClipNormal(1),
|
|
272
|
+
clipNormal2: (v) => v.getClipNormal(2),
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
/** Whether this key is one `applyConfig` knows how to apply. */
|
|
276
|
+
export function isApplicable(key) {
|
|
277
|
+
return Boolean(SETTERS[key]) || Boolean(clipSetter(key)) || GEOMETRY_KEYS.includes(key);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Apply a configuration to a live viewer.
|
|
282
|
+
*
|
|
283
|
+
* @param viewer a three-cad-viewer instance
|
|
284
|
+
* @param config {key: value}, camelCase keys and plain JSON values
|
|
285
|
+
* @param ctx optional hooks:
|
|
286
|
+
* notify - passed as the trailing flag to every setter
|
|
287
|
+
* that takes one; omitted entirely when undefined
|
|
288
|
+
* resize - (key, value) => void, for a viewport dimension,
|
|
289
|
+
* which only the host can resolve
|
|
290
|
+
* accept - (key, value) => boolean, a chance to skip a key
|
|
291
|
+
* whose value the viewer already holds. The widget
|
|
292
|
+
* needs it: driven by traitlet changes, it would
|
|
293
|
+
* otherwise re-apply what it just reported
|
|
294
|
+
* onUnknown - (key, value) => void, for a key nothing here
|
|
295
|
+
* handles. Left unset the key is dropped, which is
|
|
296
|
+
* the default, but silently
|
|
297
|
+
* @returns the keys that were applied
|
|
298
|
+
*/
|
|
299
|
+
export function applyConfig(viewer, config, ctx = {}) {
|
|
300
|
+
if (viewer == null || config == null) {
|
|
301
|
+
return [];
|
|
302
|
+
}
|
|
303
|
+
const applied = [];
|
|
304
|
+
for (const key of Object.keys(config)) {
|
|
305
|
+
const value = config[key];
|
|
306
|
+
|
|
307
|
+
if (ctx.accept && !ctx.accept(key, value)) {
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (GEOMETRY_KEYS.includes(key)) {
|
|
312
|
+
if (ctx.resize) {
|
|
313
|
+
ctx.resize(key, value);
|
|
314
|
+
applied.push(key);
|
|
315
|
+
}
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const setter = SETTERS[key] || clipSetter(key);
|
|
320
|
+
if (setter) {
|
|
321
|
+
setter(viewer, value, ctx);
|
|
322
|
+
applied.push(key);
|
|
323
|
+
} else if (ctx.onUnknown) {
|
|
324
|
+
ctx.onUnknown(key, value);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return applied;
|
|
328
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shared viewer policy, as one entry point.
|
|
3
|
+
*
|
|
4
|
+
* Everything here is host-neutral. All of it but `page.js` takes a
|
|
5
|
+
* three-cad-viewer instance and plain data and touches no DOM, no transport
|
|
6
|
+
* and no host; `page.js` is the page, so it does. What stays with a host is
|
|
7
|
+
* how it starts - where it loaded these modules from, where its settings come
|
|
8
|
+
* from - and the sending half of its Comms.
|
|
9
|
+
*
|
|
10
|
+
* Names are camelCase and values are plain JSON, because that is what this side
|
|
11
|
+
* of the wire speaks. Python converts once, at the boundary, on its way out.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/*
|
|
15
|
+
Copyright 2026 Bernhard Walter
|
|
16
|
+
|
|
17
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
18
|
+
you may not use this file except in compliance with the License.
|
|
19
|
+
You may obtain a copy of the License at
|
|
20
|
+
|
|
21
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
22
|
+
|
|
23
|
+
Unless required by applicable law or agreed to in writing, software
|
|
24
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
25
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
26
|
+
See the License for the specific language governing permissions and
|
|
27
|
+
limitations under the License.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
export { applyConfig, currentValue, isApplicable, GEOMETRY_KEYS, VIEWS } from "./apply.js";
|
|
31
|
+
|
|
32
|
+
export {
|
|
33
|
+
buildDisplayOptions,
|
|
34
|
+
buildRenderOptions,
|
|
35
|
+
buildViewerOptions,
|
|
36
|
+
preset,
|
|
37
|
+
DISPLAY_DEFAULTS,
|
|
38
|
+
RENDER_DEFAULTS,
|
|
39
|
+
RENDER_OPTION_KEYS,
|
|
40
|
+
VIEWER_DEFAULTS,
|
|
41
|
+
VIEWER_OPTION_KEYS,
|
|
42
|
+
} from "./options.js";
|
|
43
|
+
|
|
44
|
+
// Drawing a model and deciding where the camera ends up - the policy every
|
|
45
|
+
// client has to agree on, and the one that was hardest to get right.
|
|
46
|
+
export { createRenderer } from "./render.js";
|
|
47
|
+
|
|
48
|
+
// The page itself: the viewer, the message handling and the resizing. The one
|
|
49
|
+
// module here that touches the DOM, because it is the page.
|
|
50
|
+
export { createPage } from "./page.js";
|
|
51
|
+
|
|
52
|
+
export {
|
|
53
|
+
collectStates,
|
|
54
|
+
currentStates,
|
|
55
|
+
restoreStates,
|
|
56
|
+
statesToRestore,
|
|
57
|
+
} from "./states.js";
|
|
58
|
+
|
|
59
|
+
export { addAnimationTrack, animate, animationDuration } from "./animation.js";
|
|
60
|
+
|
|
61
|
+
export { createNotifier, EVENT_KEYS } from "./notify.js";
|
|
62
|
+
|
|
63
|
+
// The splash. Data rather than policy, and here for the same reason the policy
|
|
64
|
+
// is: every client shows it, and four of them had their own copy.
|
|
65
|
+
export { logo } from "./logo.js";
|