@kupcad/ui 0.1.3 → 0.1.5
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 +3 -3
- package/src/ParamsPanel.svelte +9 -6
- package/src/params.js +30 -3
- package/src/viewer/appearance.js +19 -4
- package/src/viewer/model.js +16 -9
- package/src/viewer/types.d.ts +6 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kupcad/ui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "KupCAD's UI: a three.js viewer of a model the core made, for any host (@kupcad/ui/viewer), and the Svelte panels around it (the viewer's pane, parameters, console, a language server's preview), styled by --kc-* tokens.",
|
|
5
5
|
"license": "AGPL-3.0-only",
|
|
6
6
|
"repository": {
|
|
@@ -36,11 +36,11 @@
|
|
|
36
36
|
"access": "public"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@kupcad/protocol": "0.1.
|
|
39
|
+
"@kupcad/protocol": "0.1.5",
|
|
40
40
|
"three": "^0.186.1"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
|
-
"@kupcad/wasm": "0.1.
|
|
43
|
+
"@kupcad/wasm": "0.1.5",
|
|
44
44
|
"@playwright/test": "^1.64.1",
|
|
45
45
|
"@sveltejs/vite-plugin-svelte": "^7.3.1",
|
|
46
46
|
"@testing-library/svelte": "^5.4.2",
|
package/src/ParamsPanel.svelte
CHANGED
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
@component
|
|
3
3
|
The parameters' controls, from the schema a frame (or `kupcad doc`) gives: grouped by their
|
|
4
4
|
`group:`, each showing the value set in the panel, else the one the run took. `overrides` is
|
|
5
|
-
what the panel sets (bindable), each change (and revert) given to `onchange
|
|
6
|
-
sends to the core
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
what the panel sets (bindable), each change (and revert) given to `onchange` as plain values,
|
|
6
|
+
which a host sends to the core (to a worker, a webview's host: a list kept in state is a proxy
|
|
7
|
+
that a message can't copy); a value refused by the run is shown on its control. A bar at the
|
|
8
|
+
top, held there as the panel scrolls, says how many values are set; while any is, its Reset
|
|
9
|
+
all reverts them all at once, the panel's refusals with them. The button keeps its place when
|
|
10
|
+
there is nothing to reset, so nothing under the bar moves as a first value is set or the last
|
|
11
|
+
reverted.
|
|
10
12
|
A value set back to the parameter's own (the one it showed before the panel set it: the run's,
|
|
11
13
|
the script's or a host's preset's) is a revert, not a value set.
|
|
12
14
|
-->
|
|
@@ -20,6 +22,7 @@
|
|
|
20
22
|
withOverride,
|
|
21
23
|
withoutOverride,
|
|
22
24
|
} from './params.js';
|
|
25
|
+
import { plainValues } from './values.svelte.js';
|
|
23
26
|
|
|
24
27
|
/** @import { Failure, ParamSpec, ParamValue, ParamValues, UiSchema } from '@kupcad/protocol/types' */
|
|
25
28
|
|
|
@@ -52,7 +55,7 @@
|
|
|
52
55
|
/** @param {ParamValues} next */
|
|
53
56
|
function change(next) {
|
|
54
57
|
overrides = next;
|
|
55
|
-
onchange?.(next);
|
|
58
|
+
onchange?.(plainValues(next));
|
|
56
59
|
}
|
|
57
60
|
|
|
58
61
|
/**
|
package/src/params.js
CHANGED
|
@@ -60,8 +60,10 @@ export function partsOf(rule) {
|
|
|
60
60
|
* A number control's increment: the script's `step:`, else 1 for one between a `min` and a
|
|
61
61
|
* `max` (a slider moved by a hand lands on whole numbers, not on 27.41112), or, for a range
|
|
62
62
|
* narrower than 1, which a step of 1 couldn't move in, the power of ten that gives it at least
|
|
63
|
-
* ten positions; any value for a number with no range.
|
|
64
|
-
*
|
|
63
|
+
* ten positions; any value for a number with no range. Finer still where the range's ends or
|
|
64
|
+
* the parameter's default have more decimals: a control's steps count from its `min`, so 1.2 to
|
|
65
|
+
* 4 by 1 reached neither 4 nor a default of 2, which the slider showed as 2.2.
|
|
66
|
+
* @param {Rule | ParamSpec} rule
|
|
65
67
|
* @returns {number | 'any'}
|
|
66
68
|
*/
|
|
67
69
|
export function stepOf(rule) {
|
|
@@ -69,7 +71,32 @@ export function stepOf(rule) {
|
|
|
69
71
|
const { min, max } = rule;
|
|
70
72
|
if (min === null || max === null || !(max > min)) return 'any';
|
|
71
73
|
const range = max - min;
|
|
72
|
-
|
|
74
|
+
const coarsest = range >= 1 ? 1 : 10 ** (Math.floor(Math.log10(range)) - 1);
|
|
75
|
+
const marks = [min, max];
|
|
76
|
+
if ('default_value' in rule && typeof rule.default_value === 'number') {
|
|
77
|
+
marks.push(rule.default_value);
|
|
78
|
+
}
|
|
79
|
+
const steps = marks.map(decimalStepOf);
|
|
80
|
+
return steps.includes(null) ? 'any' : Math.min(coarsest, .../** @type {number[]} */ (steps));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** The decimal places a step may have: past them, a number is taken as having no grid. */
|
|
84
|
+
const MAX_PLACES = 9;
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* The coarsest power of ten, 1 at most, that `n` is a whole multiple of (0.1 for 1.2, 1 for 40),
|
|
88
|
+
* or null for a number of more than MAX_PLACES decimals.
|
|
89
|
+
* @param {number} n
|
|
90
|
+
* @returns {number | null}
|
|
91
|
+
*/
|
|
92
|
+
function decimalStepOf(n) {
|
|
93
|
+
for (let places = 0; places <= MAX_PLACES; places += 1) {
|
|
94
|
+
const scaled = n * 10 ** places;
|
|
95
|
+
if (Math.abs(scaled - Math.round(scaled)) < 1e-9 * Math.max(1, Math.abs(scaled))) {
|
|
96
|
+
return 10 ** -places;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return null;
|
|
73
100
|
}
|
|
74
101
|
|
|
75
102
|
/**
|
package/src/viewer/appearance.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { Color } from 'three';
|
|
6
6
|
|
|
7
7
|
/** @import { LineBasicMaterial, MeshBasicMaterial, MeshStandardMaterial } from 'three' */
|
|
8
|
-
/** @import { RenderMode, SceneModel, Theme } from './types.js' */
|
|
8
|
+
/** @import { RenderMode, SceneModel, Shape, Theme } from './types.js' */
|
|
9
9
|
|
|
10
10
|
/** How see-through a solid is in x-ray. */
|
|
11
11
|
export const XRAY_OPACITY = 0.25;
|
|
@@ -16,8 +16,9 @@ export const SELECTION_TINT = 0.45;
|
|
|
16
16
|
* Draws the model's shapes as `mode` says: as they are, see-through, or not at all but for their
|
|
17
17
|
* edges (the mesh stays, for picking, and its edges with it); their edges with them when
|
|
18
18
|
* `edges`, and always in a wireframe, which is nothing else. A highlight is seen through what
|
|
19
|
-
* hides it only as they are: in x-ray everything is, and a wireframe has no faces
|
|
20
|
-
* `theme`'s colour for where they are
|
|
19
|
+
* hides it only as they are: in x-ray everything is, and a wireframe has no faces, its edges
|
|
20
|
+
* drawn in the highlight's colour instead. The edges take `theme`'s colour for where they are
|
|
21
|
+
* drawn (edgeColorOf).
|
|
21
22
|
* @param {SceneModel | null} model
|
|
22
23
|
* @param {RenderMode} mode
|
|
23
24
|
* @param {boolean} edges
|
|
@@ -26,6 +27,9 @@ export const SELECTION_TINT = 0.45;
|
|
|
26
27
|
export function applyRenderMode(model, mode, edges, theme) {
|
|
27
28
|
if (!model) return;
|
|
28
29
|
model.edgeMaterial.color.set(edgeColorOf(model.kind, mode, theme));
|
|
30
|
+
model.highlightEdges?.color.set(
|
|
31
|
+
mode === 'wireframe' ? theme.highlight : edgeColorOf(model.kind, mode, theme),
|
|
32
|
+
);
|
|
29
33
|
for (const shape of model.shapes) {
|
|
30
34
|
if (shape.edges) shape.edges.visible = edges || mode === 'wireframe';
|
|
31
35
|
if (shape.seenThrough) shape.seenThrough.visible = mode === 'solid';
|
|
@@ -68,10 +72,21 @@ export function applySelection(model, part, selection, selectionEdges) {
|
|
|
68
72
|
const chosen = part !== null && shape.part === part;
|
|
69
73
|
material.color.copy(shape.color);
|
|
70
74
|
if (chosen) material.color.lerp(tint, SELECTION_TINT);
|
|
71
|
-
if (shape.edges) shape.edges.material = chosen ? selectionEdges : model
|
|
75
|
+
if (shape.edges) shape.edges.material = chosen ? selectionEdges : edgesOf(model, shape);
|
|
72
76
|
}
|
|
73
77
|
}
|
|
74
78
|
|
|
79
|
+
/**
|
|
80
|
+
* The material a shape's edges are drawn with unless it is selected: a highlight's own, else
|
|
81
|
+
* the model's.
|
|
82
|
+
* @param {SceneModel} model
|
|
83
|
+
* @param {Shape} shape
|
|
84
|
+
* @returns {LineBasicMaterial}
|
|
85
|
+
*/
|
|
86
|
+
function edgesOf(model, shape) {
|
|
87
|
+
return shape.seenThrough && model.highlightEdges ? model.highlightEdges : model.edgeMaterial;
|
|
88
|
+
}
|
|
89
|
+
|
|
75
90
|
/**
|
|
76
91
|
* Gives the model the colours of `theme` it takes besides its edges' (applyRenderMode's): a
|
|
77
92
|
* ghost's and a highlight's (the selection, applied after, tints from them).
|
package/src/viewer/model.js
CHANGED
|
@@ -96,30 +96,32 @@ async function solids(glb, parts, loader, theme) {
|
|
|
96
96
|
return index === undefined ? undefined : nodes[index]?.name;
|
|
97
97
|
};
|
|
98
98
|
const edgeMaterial = new LineBasicMaterial({ color: theme.edges });
|
|
99
|
+
const highlightEdges = new LineBasicMaterial({ color: theme.edges });
|
|
99
100
|
/** @type {Shape[]} */
|
|
100
101
|
const shapes = [];
|
|
101
102
|
scene.traverse((object) => {
|
|
102
103
|
if (object instanceof Mesh) {
|
|
103
|
-
shapes.push(solidShape(object, partOf, nodeName, edgeMaterial, theme));
|
|
104
|
+
shapes.push(solidShape(object, partOf, nodeName, { edgeMaterial, highlightEdges }, theme));
|
|
104
105
|
}
|
|
105
106
|
});
|
|
106
107
|
const root = new Group();
|
|
107
108
|
root.add(scene);
|
|
108
|
-
return { kind: '3d', root, shapes, edgeMaterial };
|
|
109
|
+
return { kind: '3d', root, shapes, edgeMaterial, highlightEdges };
|
|
109
110
|
}
|
|
110
111
|
|
|
111
112
|
/**
|
|
112
113
|
* A solid's mesh as a shape: its own material (so tinting one part tints no other), its part,
|
|
113
114
|
* and its edges; a ghost seen through, in the theme's colour; a highlighted one (the core tags
|
|
114
|
-
* its material) in the highlight's, seen through what hides it.
|
|
115
|
+
* its material) in the highlight's, seen through what hides it, its edges its own.
|
|
115
116
|
* @param {Mesh} mesh
|
|
116
117
|
* @param {Map<string, string>} partOf
|
|
117
118
|
* @param {(object: Object3D) => string | undefined} nodeName
|
|
118
|
-
* @param {LineBasicMaterial}
|
|
119
|
+
* @param {{ edgeMaterial: LineBasicMaterial, highlightEdges: LineBasicMaterial }} edges the
|
|
120
|
+
* model's edges' materials
|
|
119
121
|
* @param {Theme} theme
|
|
120
122
|
* @returns {Shape}
|
|
121
123
|
*/
|
|
122
|
-
function solidShape(mesh, partOf, nodeName, edgeMaterial, theme) {
|
|
124
|
+
function solidShape(mesh, partOf, nodeName, { edgeMaterial, highlightEdges }, theme) {
|
|
123
125
|
const material = /** @type {import('three').MeshStandardMaterial} */ (
|
|
124
126
|
/** @type {Material} */ (mesh.material).clone()
|
|
125
127
|
);
|
|
@@ -144,7 +146,10 @@ function solidShape(mesh, partOf, nodeName, edgeMaterial, theme) {
|
|
|
144
146
|
const highlighted = role === 'highlight';
|
|
145
147
|
if (highlighted) material.color.set(theme.highlight);
|
|
146
148
|
Object.assign(material, FACES_BEHIND_EDGES);
|
|
147
|
-
const edges = new LineSegments(
|
|
149
|
+
const edges = new LineSegments(
|
|
150
|
+
new EdgesGeometry(mesh.geometry, EDGE_ANGLE_DEG),
|
|
151
|
+
highlighted ? highlightEdges : edgeMaterial,
|
|
152
|
+
);
|
|
148
153
|
mesh.add(edges);
|
|
149
154
|
return {
|
|
150
155
|
mesh,
|
|
@@ -242,7 +247,7 @@ function drawing(svg, theme) {
|
|
|
242
247
|
opacity,
|
|
243
248
|
});
|
|
244
249
|
}
|
|
245
|
-
return { kind: '2d', root, shapes, edgeMaterial };
|
|
250
|
+
return { kind: '2d', root, shapes, edgeMaterial, highlightEdges: null };
|
|
246
251
|
}
|
|
247
252
|
|
|
248
253
|
/**
|
|
@@ -267,14 +272,16 @@ function outlineOf(path, closed) {
|
|
|
267
272
|
}
|
|
268
273
|
|
|
269
274
|
/**
|
|
270
|
-
* The materials the model's scene owns: each shape's, a highlight's copy's, and its edges'
|
|
271
|
-
* selected shape's edges are drawn with the viewer's material,
|
|
275
|
+
* The materials the model's scene owns: each shape's, a highlight's copy's, and its edges' (a
|
|
276
|
+
* highlight's of their own). A selected shape's edges are drawn with the viewer's material,
|
|
277
|
+
* which isn't the model's to free.
|
|
272
278
|
* @param {SceneModel} model
|
|
273
279
|
* @returns {Set<Material>}
|
|
274
280
|
*/
|
|
275
281
|
export function ownMaterials(model) {
|
|
276
282
|
return new Set([
|
|
277
283
|
model.edgeMaterial,
|
|
284
|
+
...(model.highlightEdges ? [model.highlightEdges] : []),
|
|
278
285
|
...model.shapes.flatMap((shape) =>
|
|
279
286
|
[shape.mesh, shape.seenThrough].flatMap((mesh) =>
|
|
280
287
|
mesh ? [/** @type {Material} */ (mesh.material)] : [],
|
package/src/viewer/types.d.ts
CHANGED
|
@@ -131,6 +131,11 @@ export interface SceneModel {
|
|
|
131
131
|
kind: '3d' | '2d';
|
|
132
132
|
root: Group;
|
|
133
133
|
shapes: Shape[];
|
|
134
|
-
/** The edges' material, shared by every shape but the selected ones. */
|
|
134
|
+
/** The edges' material, shared by every shape but the selected and the highlighted ones. */
|
|
135
135
|
edgeMaterial: LineBasicMaterial;
|
|
136
|
+
/**
|
|
137
|
+
* A highlighted solid's edges' material: the highlight's colour in a wireframe, which has no
|
|
138
|
+
* faces to show it by, the edges' elsewhere; null for a drawing, which has no highlights.
|
|
139
|
+
*/
|
|
140
|
+
highlightEdges: LineBasicMaterial | null;
|
|
136
141
|
}
|