@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kupcad/ui",
3
- "version": "0.1.3",
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.3",
39
+ "@kupcad/protocol": "0.1.5",
40
40
  "three": "^0.186.1"
41
41
  },
42
42
  "devDependencies": {
43
- "@kupcad/wasm": "0.1.3",
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",
@@ -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`, which a host
6
- sends to the core; a value refused by the run is shown on its control. A bar at the top, held
7
- there as the panel scrolls, says how many values are set; while any is, its Reset all reverts
8
- them all at once, the panel's refusals with them. The button keeps its place when there is
9
- nothing to reset, so nothing under the bar moves as a first value is set or the last reverted.
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
- * @param {Rule} rule
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
- return range >= 1 ? 1 : 10 ** (Math.floor(Math.log10(range)) - 1);
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
  /**
@@ -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. The edges take
20
- * `theme`'s colour for where they are drawn (edgeColorOf).
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.edgeMaterial;
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).
@@ -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} edgeMaterial
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(new EdgesGeometry(mesh.geometry, EDGE_ANGLE_DEG), edgeMaterial);
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'. A
271
- * selected shape's edges are drawn with the viewer's material, which isn't the model's to free.
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)] : [],
@@ -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
  }