phoria2 1.1.0 → 1.2.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.
Files changed (60) hide show
  1. package/README.md +71 -13
  2. package/dist/bloom.js +4 -4
  3. package/dist/bloom.js.map +1 -1
  4. package/dist/charts.js +358 -345
  5. package/dist/charts.js.map +1 -1
  6. package/dist/core.js +3989 -3600
  7. package/dist/core.js.map +1 -1
  8. package/dist/edit.js +43 -43
  9. package/dist/edit.js.map +1 -1
  10. package/dist/explain.js +5 -1
  11. package/dist/explain.js.map +1 -1
  12. package/dist/gltf.js +96 -96
  13. package/dist/gltf.js.map +1 -1
  14. package/dist/motion.js +3 -3
  15. package/dist/motion.js.map +1 -1
  16. package/dist/particles.js +16 -16
  17. package/dist/particles.js.map +1 -1
  18. package/dist/passes.js +26 -26
  19. package/dist/passes.js.map +1 -1
  20. package/dist/phoria2.js +1 -1
  21. package/dist/picking.js +16 -16
  22. package/dist/picking.js.map +1 -1
  23. package/dist/types/OrbitControl.d.ts +6 -2
  24. package/dist/types/Raycaster.d.ts +10 -0
  25. package/dist/types/View.d.ts +59 -1
  26. package/dist/types/charts/Chart.d.ts +7 -0
  27. package/dist/types/explain.d.ts +3 -0
  28. package/dist/types/frame.d.ts +7 -0
  29. package/dist/types/geometry/Geometry.d.ts +8 -2
  30. package/dist/types/keyboard.d.ts +48 -0
  31. package/dist/types/labels.d.ts +4 -0
  32. package/dist/types/scene/Node.d.ts +19 -14
  33. package/dist/types/scene/drawables.d.ts +32 -14
  34. package/dist/types/scene/lights.d.ts +16 -2
  35. package/dist/types/shadow.d.ts +2 -1
  36. package/dist/types/types.d.ts +14 -0
  37. package/dist/types/value/Color.d.ts +0 -4
  38. package/dist/types/value/Quat.d.ts +4 -2
  39. package/llms-full.txt +58 -9
  40. package/package.json +4 -1
  41. package/schema/scene.schema.json +19 -1
  42. package/src/OrbitControl.js +7 -3
  43. package/src/Raycaster.js +58 -0
  44. package/src/View.js +204 -41
  45. package/src/charts/Chart.js +20 -2
  46. package/src/explain.js +6 -2
  47. package/src/frame.js +29 -9
  48. package/src/geometry/Geometry.js +16 -3
  49. package/src/keyboard.js +298 -0
  50. package/src/labels.js +4 -1
  51. package/src/scene/Node.js +20 -9
  52. package/src/scene/Scene.js +2 -0
  53. package/src/scene/cameras.js +4 -1
  54. package/src/scene/drawables.js +48 -12
  55. package/src/scene/lights.js +40 -6
  56. package/src/shadow.js +27 -4
  57. package/src/types.js +6 -0
  58. package/src/value/Color.js +21 -0
  59. package/src/value/Quat.js +15 -6
  60. package/src/watch.js +6 -1
@@ -0,0 +1,48 @@
1
+ import type { Hit } from './Raycaster.js';
2
+ import type { TooltipContent } from './tooltip.js';
3
+ import type { Node } from './scene/Node.js';
4
+ import type { View } from './View.js';
5
+ export type KeyboardOptions = {
6
+ /**
7
+ * what a screen reader says for each thing stepped to (default: the
8
+ * tooltip's text, then its data, then the node's name)
9
+ */
10
+ label?: (hit: Hit) => string;
11
+ };
12
+ export type KeyboardHooks = {
13
+ /**
14
+ * show this as hovered, or nothing
15
+ */
16
+ focus: (hit: Hit | null) => void;
17
+ /**
18
+ * click it
19
+ */
20
+ press: (hit: Hit) => void;
21
+ /**
22
+ * the tooltip's content for it, if there is a tooltip
23
+ */
24
+ tooltip: (hit: Hit) => TooltipContent;
25
+ /**
26
+ * does the View itself answer hovers or clicks (a tooltip, handlers)?
27
+ */
28
+ viewWide: () => boolean;
29
+ };
30
+ export type Item = {
31
+ node: Node;
32
+ index: number | null;
33
+ whole: Node | null;
34
+ };
35
+ export declare class Keyboard {
36
+ #private;
37
+ /** @param {View} view @param {KeyboardHooks} hooks @param {KeyboardOptions} options */
38
+ constructor(view: View, hooks: KeyboardHooks, options: KeyboardOptions);
39
+ /**
40
+ * The canvas's label for screen readers: the View's description, or - when it has none - one made
41
+ * from the scene: what its charts show, and how many things there are to step through.
42
+ * @param {string | null} text
43
+ */
44
+ setDescription(text: string | null): void;
45
+ /** Step off whatever was stepped to: the pointer has taken over, or focus has gone. */
46
+ clear(): void;
47
+ dispose(): void;
48
+ }
@@ -18,6 +18,10 @@ export type LabelStats = {
18
18
  * behind the camera or outside the view
19
19
  */
20
20
  offscreen: number;
21
+ /**
22
+ * hidden because the view isn't zoomed in to their minScale
23
+ */
24
+ zoomedOut: number;
21
25
  };
22
26
  export type Entry = {
23
27
  el: HTMLDivElement;
@@ -5,7 +5,7 @@ import { Mat4 } from '../value/Mat4.js';
5
5
  import { Quat } from '../value/Quat.js';
6
6
  import { Vec3 } from '../value/Vec3.js';
7
7
  import { Clip } from '../clip.js';
8
- import type { ColorInput, Vec3Input } from '../types.js';
8
+ import type { ColorInput, QuatInput, Vec3Input } from '../types.js';
9
9
  import type { Hit } from '../Raycaster.js';
10
10
  import type { Camera } from './cameras.js';
11
11
  import type { ClipOptions } from '../clip.js';
@@ -59,9 +59,9 @@ export type NodeOptions = {
59
59
  */
60
60
  rotation?: Vec3Input | Euler;
61
61
  /**
62
- * [x, y, z, w] - instead of rotation
62
+ * [x, y, z, w] or { x, y, z, w } - instead of rotation
63
63
  */
64
- quaternion?: ArrayLike<number>;
64
+ quaternion?: QuatInput;
65
65
  /**
66
66
  * a number scales uniformly
67
67
  */
@@ -87,9 +87,10 @@ export type NodeOptions = {
87
87
  */
88
88
  tooltip?: ((hit: Hit) => any) | null;
89
89
  /**
90
- * hover tint for this subtree, over the View's
90
+ * hover tint for this subtree, over the View's; false for none - a
91
+ * floor or wall left plain under a View that tints everything else (null: the View's)
91
92
  */
92
- highlight?: ColorInput | null;
93
+ highlight?: ColorInput | null | false;
93
94
  /**
94
95
  * hovering any part of this subtree tints all of it, as one thing - a
95
96
  * piece of furniture, a machine - rather than just the part under the pointer (default false)
@@ -127,7 +128,7 @@ export type NodeOptions = {
127
128
  */
128
129
  clip?: ClipOptions | Clip | null;
129
130
  };
130
- /** @import { ColorInput, Vec3Input } from '../types.js' */
131
+ /** @import { ColorInput, QuatInput, Vec3Input } from '../types.js' */
131
132
  /** @import { Hit } from '../Raycaster.js' */
132
133
  /** @import { Camera } from './cameras.js' */
133
134
  /** @import { ClipOptions } from '../clip.js' */
@@ -157,7 +158,7 @@ export type NodeOptions = {
157
158
  * @property {string} [name]
158
159
  * @property {Vec3Input} [position]
159
160
  * @property {Vec3Input | Euler} [rotation] Euler angles in radians, XYZ order
160
- * @property {ArrayLike<number>} [quaternion] [x, y, z, w] - instead of rotation
161
+ * @property {QuatInput} [quaternion] [x, y, z, w] or { x, y, z, w } - instead of rotation
161
162
  * @property {Vec3Input | number} [scale] a number scales uniformly
162
163
  * @property {boolean} [visible]
163
164
  * @property {boolean} [pickable] include in view.pick() (default true)
@@ -167,7 +168,8 @@ export type NodeOptions = {
167
168
  * @property {UpdateHandler | UpdateHandler[]} [onUpdate]
168
169
  * @property {((hit: Hit) => any) | null} [tooltip] tooltip for anything picked in this subtree; nearer nodes win, and
169
170
  * all win over the View's
170
- * @property {ColorInput | null} [highlight] hover tint for this subtree, over the View's
171
+ * @property {ColorInput | null | false} [highlight] hover tint for this subtree, over the View's; false for none - a
172
+ * floor or wall left plain under a View that tints everything else (null: the View's)
171
173
  * @property {boolean} [highlightWhole] hovering any part of this subtree tints all of it, as one thing - a
172
174
  * piece of furniture, a machine - rather than just the part under the pointer (default false)
173
175
  * @property {((hit: Hit | null) => void) | null} [onHover] called as the thing under the pointer in this subtree changes
@@ -244,14 +246,17 @@ export declare class Node {
244
246
  set rotation(v: Vec3Input);
245
247
  /** Kept in sync with rotation. Assigning copies, normalised. @returns {Quat} */
246
248
  get quaternion(): Quat;
247
- /** @param {ArrayLike<number>} v */
248
- set quaternion(v: ArrayLike<number>);
249
+ /** @param {QuatInput} v */
250
+ set quaternion(v: QuatInput);
249
251
  /** @param {NodeOptions} [options] */
250
252
  constructor(options?: NodeOptions);
251
- /** Hover tint for this subtree, over the View's; set any colour, or null. @returns {Color | null} */
252
- get highlight(): Color | null;
253
- /** @param {ColorInput | null} value */
254
- set highlight(value: ColorInput | null);
253
+ /**
254
+ * Hover tint for this subtree, over the View's: a colour, false for none, or null to use the View's.
255
+ * @returns {Color | null | false}
256
+ */
257
+ get highlight(): Color | null | false;
258
+ /** @param {ColorInput | null | false} value */
259
+ set highlight(value: ColorInput | null | false);
255
260
  /**
256
261
  * The section plane cutting this subtree, or null. Set { normal, offset, cap }, a Clip or null;
257
262
  * move the cut by changing its offset or normal in place.
@@ -4,7 +4,7 @@ import type { Geometry } from '../geometry/Geometry.js';
4
4
  import type { Material, MaterialOptions } from '../material/Material.js';
5
5
  import type { GeometryOptions } from '../geometry/Geometry.js';
6
6
  import type { NodeOptions } from './Node.js';
7
- import type { ColorInput, Vec3Input } from '../types.js';
7
+ import type { ColorInput, QuatInput, Vec3Input } from '../types.js';
8
8
  export type DrawableOptions = NodeOptions & {
9
9
  geometry: Geometry | GeometryOptions;
10
10
  material?: Material | MaterialOptions;
@@ -52,6 +52,11 @@ export type LabelPlacement = {
52
52
  * the edge and up or down (default null: by its point)
53
53
  */
54
54
  edge?: 'left' | 'right' | null;
55
+ /**
56
+ * show the label only when zoomed in this far: at least this many CSS pixels to
57
+ * one world unit, at the label's point - details that appear as you come closer (default 0: always)
58
+ */
59
+ minScale?: number;
55
60
  };
56
61
  export type LabelPlace = 'center' | 'above' | 'below' | 'left' | 'right';
57
62
  export type InstanceLabels = Omit<LabelPlacement, 'priority'> & {
@@ -64,7 +69,7 @@ export type InstanceLabels = Omit<LabelPlacement, 'priority'> & {
64
69
  /** @import { Material, MaterialOptions } from '../material/Material.js' */
65
70
  /** @import { GeometryOptions } from '../geometry/Geometry.js' */
66
71
  /** @import { NodeOptions } from './Node.js' */
67
- /** @import { ColorInput, Vec3Input } from '../types.js' */
72
+ /** @import { ColorInput, QuatInput, Vec3Input } from '../types.js' */
68
73
  /**
69
74
  * @typedef {NodeOptions & {
70
75
  * geometry: Geometry | GeometryOptions,
@@ -95,10 +100,13 @@ export type InstanceLabels = Omit<LabelPlacement, 'priority'> & {
95
100
  * @property {'left' | 'right' | null} [edge] set the label at the view's left or right edge, level with its
96
101
  * point - callouts in a column down the side of a diagram, with leader: true. offset then moves it in from
97
102
  * the edge and up or down (default null: by its point)
103
+ * @property {number} [minScale] show the label only when zoomed in this far: at least this many CSS pixels to
104
+ * one world unit, at the label's point - details that appear as you come closer (default 0: always)
98
105
  */
99
106
  /** @typedef {'center' | 'above' | 'below' | 'left' | 'right'} LabelPlace */
100
107
  /**
101
- * One label per instance or point, from its row of data.
108
+ * One label per instance or point, from its row of data - or a list of these, for several each (a symbol
109
+ * always, its details when zoomed in).
102
110
  * @typedef {Omit<LabelPlacement, 'priority'> & {
103
111
  * text: (datum: any, index: number) => string | number | null | undefined,
104
112
  * priority?: number | ((datum: any, index: number) => number),
@@ -109,10 +117,12 @@ export type InstanceLabels = Omit<LabelPlacement, 'priority'> & {
109
117
  * label the point itself.
110
118
  */
111
119
  export declare const LABEL_PLACES: LabelPlace[];
112
- /** @param {string} owner @param {unknown} v @returns {(InstanceLabels & Required<Omit<LabelPlacement, 'priority'>> & { priority: number | ((datum: any, index: number) => number) }) | null} */
113
- declare function instanceLabels(owner: string, v: unknown): (InstanceLabels & Required<Omit<LabelPlacement, 'priority'>> & {
120
+ export type CheckedLabels = InstanceLabels & Required<Omit<LabelPlacement, 'priority'>> & {
114
121
  priority: number | ((datum: any, index: number) => number);
115
- }) | null;
122
+ };
123
+ /** @typedef {InstanceLabels & Required<Omit<LabelPlacement, 'priority'>> & { priority: number | ((datum: any, index: number) => number) }} CheckedLabels */
124
+ /** @param {string} owner @param {unknown} v @returns {CheckedLabels | CheckedLabels[] | null} */
125
+ declare function instanceLabels(owner: string, v: unknown): CheckedLabels | CheckedLabels[] | null;
116
126
  /** Base for nodes with geometry and a material: Mesh, InstancedMesh, Points and Lines. */
117
127
  export declare class Drawable extends Node {
118
128
  #private;
@@ -157,7 +167,10 @@ export type InstanceOptions = {
157
167
  * Euler angles in radians, XYZ order
158
168
  */
159
169
  rotation?: Vec3Input | Euler;
160
- quaternion?: ArrayLike<number>;
170
+ /**
171
+ * [x, y, z, w] or { x, y, z, w } - as physics engines give them
172
+ */
173
+ quaternion?: QuatInput;
161
174
  scale?: Vec3Input | number;
162
175
  /**
163
176
  * multiplies the material colour
@@ -166,7 +179,8 @@ export type InstanceOptions = {
166
179
  };
167
180
  /**
168
181
  * Many copies of one geometry and material, drawn together. Set each copy with setInstance().
169
- * Picking reports which instance was hit.
182
+ * Picking reports which instance was hit. An instance scaled to 0 is hidden - it draws nothing, and
183
+ * framing, culling, shadows and labels leave it out - so a pool of instances can keep spares.
170
184
  */
171
185
  export declare class InstancedMesh extends Mesh {
172
186
  #private;
@@ -178,11 +192,11 @@ export declare class InstancedMesh extends Mesh {
178
192
  instanceMatrices: Float32Array;
179
193
  /** Linear rgb per instance, or null until a colour is set. @type {Float32Array | null} */
180
194
  instanceColors: Float32Array | null;
181
- /** A label per instance, from its row of data. @type {ReturnType<typeof instanceLabels>} */
195
+ /** A label per instance from its row of data - or a list of them, several per instance. @type {ReturnType<typeof instanceLabels>} */
182
196
  labels: ReturnType<typeof instanceLabels>;
183
197
  /**
184
198
  * @param {DrawableOptions & { count?: number, instances?: InstanceOptions[], instanceMatrices?: ArrayLike<number>,
185
- * instanceColors?: ArrayLike<number>, labels?: InstanceLabels | null }} options count defaults to the number
199
+ * instanceColors?: ArrayLike<number>, labels?: InstanceLabels | InstanceLabels[] | null }} options count defaults to the number
186
200
  * of instances or rows of data; instanceMatrices/instanceColors are raw data, as toJSON() writes
187
201
  */
188
202
  constructor(options: DrawableOptions & {
@@ -190,7 +204,7 @@ export declare class InstancedMesh extends Mesh {
190
204
  instances?: InstanceOptions[];
191
205
  instanceMatrices?: ArrayLike<number>;
192
206
  instanceColors?: ArrayLike<number>;
193
- labels?: InstanceLabels | null;
207
+ labels?: InstanceLabels | InstanceLabels[] | null;
194
208
  });
195
209
  /**
196
210
  * Set an instance's transform and optional colour. Unset transform parts default to identity.
@@ -222,11 +236,11 @@ export declare class InstancedMesh extends Mesh {
222
236
  export declare class Points extends Drawable {
223
237
  static type: string;
224
238
  static get OPTIONS(): string[];
225
- /** A label per point, from its row of data. @type {ReturnType<typeof instanceLabels>} */
239
+ /** A label per point from its row of data - or a list of them, several per point. @type {ReturnType<typeof instanceLabels>} */
226
240
  labels: ReturnType<typeof instanceLabels>;
227
- /** @param {DrawableOptions & { labels?: InstanceLabels | null }} options */
241
+ /** @param {DrawableOptions & { labels?: InstanceLabels | InstanceLabels[] | null }} options */
228
242
  constructor(options: DrawableOptions & {
229
- labels?: InstanceLabels | null;
243
+ labels?: InstanceLabels | InstanceLabels[] | null;
230
244
  });
231
245
  /** @returns {'points'} */
232
246
  get drawMode(): 'points';
@@ -261,6 +275,8 @@ export declare class Label extends Node {
261
275
  pointer: string;
262
276
  anchor: string;
263
277
  zIndex: string;
278
+ minZoom: string;
279
+ lod: string;
264
280
  collision: string;
265
281
  };
266
282
  text: string;
@@ -280,6 +296,8 @@ export declare class Label extends Node {
280
296
  leader: boolean;
281
297
  /** Set at the view's left or right edge, level with its point, or null for by its point. @type {'left' | 'right' | null} */
282
298
  edge: 'left' | 'right' | null;
299
+ /** Shown only when zoomed in this far: CSS pixels to one world unit at its point; 0 for always. */
300
+ minScale: number;
283
301
  /** @param {LabelOptions} options */
284
302
  constructor(options: LabelOptions);
285
303
  }
@@ -1,8 +1,9 @@
1
1
  import { Color } from '../value/Color.js';
2
2
  import { Vec3 } from '../value/Vec3.js';
3
+ import { Box3 } from '../value/Box3.js';
3
4
  import { Node } from './Node.js';
4
5
  import type { NodeOptions } from './Node.js';
5
- import type { ColorInput, Vec3Input } from '../types.js';
6
+ import type { Box3Input, ColorInput, Vec3Input } from '../types.js';
6
7
  export type LightOptions = NodeOptions & {
7
8
  color?: ColorInput;
8
9
  intensity?: number;
@@ -87,13 +88,26 @@ declare class AimedLight extends Light {
87
88
  }
88
89
  /** Parallel light from far away, like the sun. Position only matters for its shadow. */
89
90
  export declare class DirectionalLight extends AimedLight {
91
+ #private;
90
92
  static type: string;
91
93
  static get OPTIONS(): string[];
92
94
  castShadow: boolean;
93
- /** @param {AimedLightOptions & { castShadow?: boolean }} [options] */
95
+ /** @param {AimedLightOptions & { castShadow?: boolean, shadowBounds?: Box3Input | null }} [options] */
94
96
  constructor(options?: AimedLightOptions & {
95
97
  castShadow?: boolean;
98
+ shadowBounds?: Box3Input | null;
96
99
  });
100
+ /**
101
+ * The part of the world, in world units, that has shadows - or null (the default) to follow
102
+ * whatever casts them. The shadow map is fitted to what casts every frame, so one caster flung far
103
+ * off spreads its texels over everything and every shadow blurs; set the area where things happen
104
+ * and its shadows stay sharp. Casters outside it still shadow it, from wherever they stand.
105
+ * Assigning copies.
106
+ * @returns {Box3 | null}
107
+ */
108
+ get shadowBounds(): Box3 | null;
109
+ /** @param {Box3Input | null} v */
110
+ set shadowBounds(v: Box3Input | null);
97
111
  }
98
112
  /** Light from a point in all directions, falling off with the square of the distance. */
99
113
  export declare class PointLight extends Light {
@@ -25,9 +25,10 @@ export type ShadowFit = {
25
25
  * @param {Box3[]} casters world bounds
26
26
  * @param {Box3[]} receivers world bounds
27
27
  * @param {number} [size] map size in texels
28
+ * @param {Box3 | null} [area] the light's shadowBounds: the map covers this across, whatever casts
28
29
  * @returns {ShadowFit | null}
29
30
  */
30
- export declare function fitShadow(direction: ArrayLike<number>, casters: Box3[], receivers: Box3[], size?: number): ShadowFit | null;
31
+ export declare function fitShadow(direction: ArrayLike<number>, casters: Box3[], receivers: Box3[], size?: number, area?: Box3 | null): ShadowFit | null;
31
32
  /** Spot shadows' map: four tiles of half its size, one a light. */
32
33
  export declare const SPOT_ATLAS_SIZE = 2048;
33
34
  export declare const SPOT_TILES = 4;
@@ -7,12 +7,26 @@ export type XYZ = {
7
7
  z: number;
8
8
  };
9
9
  export type Vec3Input = Vec3Like | XYZ;
10
+ export type XYZW = {
11
+ x: number;
12
+ y: number;
13
+ z: number;
14
+ w: number;
15
+ };
16
+ export type QuatInput = ArrayLike<number> | XYZW;
17
+ export type Box3Input = import('./value/Box3.js').Box3 | {
18
+ min: Vec3Input;
19
+ max: Vec3Input;
20
+ };
10
21
  export type ColorInput = string | number | number[] | Float32Array | import('./value/Color.js').Color;
11
22
  /** @typedef {Float32Array | number[]} Vec2Like */
12
23
  /** @typedef {Float32Array | number[]} Vec3Like */
13
24
  /** @typedef {Float32Array | number[]} Vec4Like */
14
25
  /** A position or direction as an object literal. @typedef {{ x: number, y: number, z: number }} XYZ */
15
26
  /** A vector as accepted on input: [x, y, z], { x, y, z } or a Vec3. @typedef {Vec3Like | XYZ} Vec3Input */
27
+ /** A quaternion as an object literal - as physics engines such as Rapier return them. @typedef {{ x: number, y: number, z: number, w: number }} XYZW */
28
+ /** A rotation quaternion as accepted on input: [x, y, z, w], { x, y, z, w } or a Quat. @typedef {ArrayLike<number> | XYZW} QuatInput */
29
+ /** A box as accepted on input: a Box3, or its lowest and highest corners. @typedef {import('./value/Box3.js').Box3 | { min: Vec3Input, max: Vec3Input }} Box3Input */
16
30
  /**
17
31
  * A colour as accepted on input, in sRGB: '#ff8800', '#f80', 0xff8800, 'rgb(255 136 0)',
18
32
  * 'hsl(32 100% 50%)', a CSS colour name, or [r, g, b] with components 0-1.
@@ -1,8 +1,4 @@
1
1
  import type { ColorInput } from '../types.js';
2
- /**
3
- * An RGB colour. Input is sRGB, as everywhere in phoria2; the components are stored **linear**,
4
- * ready for lighting maths. `toJSON()` gives sRGB '#rrggbb' back.
5
- */
6
2
  export declare class Color extends Float32Array {
7
3
  #private;
8
4
  0: any;
@@ -1,3 +1,5 @@
1
+ import type { QuatInput } from '../types.js';
2
+ /** @import { QuatInput } from '../types.js' */
1
3
  /**
2
4
  * A rotation quaternion [x, y, z, w]: a Float32Array(4) with accessors and chainable methods.
3
5
  * A node's quaternion and its Euler `rotation` keep each other in sync through these setters;
@@ -44,8 +46,8 @@ export declare class Quat extends Float32Array {
44
46
  */
45
47
  /** @param {number | ArrayLike<number>} x @param {number} [y] @param {number} [z] @param {number} [w] */
46
48
  set(array: ArrayLike<number>, offset?: number): void;
47
- /** @param {ArrayLike<number>} q [x, y, z, w] @param {string} [owner] @returns {this} */
48
- copy(q: ArrayLike<number>, owner?: string): this;
49
+ /** @param {QuatInput} q [x, y, z, w], or { x, y, z, w } as physics engines give them @param {string} [owner] @returns {this} */
50
+ copy(q: QuatInput, owner?: string): this;
49
51
  /** @returns {Quat} */
50
52
  clone(): Quat;
51
53
  /** @returns {this} */
package/llms-full.txt CHANGED
@@ -114,9 +114,13 @@ const view = new View({ canvas: '#c', orbit: true })
114
114
  | `autoLight` | `true` | light a scene that has no lights (a chart needs none of its own); `false` to leave it dark |
115
115
  | `pixelRatio` | `min(devicePixelRatio, 2)` | |
116
116
  | `context` | `'auto'` | WebGL context: own for the first four views on a page, then one shared context - so a page can hold any number of views |
117
+ | `keyboard` | `false` | `true` or `{ label: (hit) => text }`: use without a pointer, with a screen reader - see Interaction |
118
+ | `description` | none | what the view shows, for screen readers (the canvas's label); made from the scene when `keyboard` is on |
119
+ | `reducedMotion` | `'auto'` | as the system's reduced-motion setting: `frame()` jumps rather than glides, auto-rotation holds, charts change at once. `true` or `false` to choose |
117
120
 
118
121
  Members: `view.scene`, `view.camera` (assign a camera or a preset name to switch), `view.controls`
119
- (the orbit control), `view.renderer`, `view.width`, `view.height`.
122
+ (the orbit control), `view.renderer`, `view.width`, `view.height`, `view.description`, `view.reducedMotion`
123
+ (true or false as it stands - check it for motion of your own, such as a timeline; set `'auto'`, true or false).
120
124
 
121
125
  Methods:
122
126
 
@@ -164,7 +168,7 @@ something changes):
164
168
  ## Scene graph
165
169
 
166
170
  Everything in a scene is a node. Node options (all optional): `name`, `position`, `rotation`
167
- (Euler XYZ radians, `[x, y, z]`), `quaternion`, `scale` (number or `[x, y, z]`), `visible`,
171
+ (Euler XYZ radians, `[x, y, z]`), `quaternion` (`[x, y, z, w]` or `{ x, y, z, w }`), `scale` (number or `[x, y, z]`), `visible`,
168
172
  `pickable` (default true), `userData`, `children`, `onUpdate`, `clip` (a section plane - see
169
173
  Cutaways), plus the interaction options below.
170
174
 
@@ -229,7 +233,11 @@ bars.setColor(3, 'red')
229
233
  `setInstance(i, { position, rotation, quaternion, scale, color })`. An instance's `color` multiplies the
230
234
  material's colour, which is white unless you set one - so the instance colour shows as given. `count` defaults to the number
231
235
  of rows in `data`. For very large data, write `bars.instanceMatrices` (16 numbers per instance,
232
- column-major) directly and call `bars.needsUpdate()`.
236
+ column-major) directly and call `bars.needsUpdate()`. An instance scaled to 0 is hidden: it draws
237
+ nothing, and bounds, framing, shadows and labels leave it out - so a pool can keep spare instances.
238
+ Positions and quaternions also take `{ x, y, z }` and `{ x, y, z, w }`, as physics engines return
239
+ them, so a physics bridge is one line a body:
240
+ `bricks.setInstance(i, { position: body.translation(), quaternion: body.rotation() })`.
233
241
 
234
242
  **Labels** are HTML elements pinned to 3D points (style them with CSS). Options: `text`, `position`,
235
243
  `className`, `place` (`'center'` | `'above'` | `'below'` | `'left'` | `'right'`), `offset` ([x, y]
@@ -237,7 +245,8 @@ pixels), `priority` (higher wins when labels overlap), `overlap` (`'hide'` | `'d
237
245
  a label does when covered), `occlude` (true: counts as covered when geometry is in front), `leader`
238
246
  (true: a line from the point to the label, with a dot on the point - for callouts), `edge` (`'left'` |
239
247
  `'right'`: set the label at that edge of the view, level with its point - callouts in a column, with
240
- `leader: true`; `offset` then moves it in and up or down).
248
+ `leader: true`; `offset` then moves it in and up or down), `minScale` (show it only when zoomed in this
249
+ far - below).
241
250
 
242
251
  A label answers the pointer when it has handlers: a Label with its own `onClick` or `onHover` is
243
252
  clicked and hovered like a mesh (`new Label({ text: 'Mantle', onClick: () => show('mantle') })` - a
@@ -250,6 +259,21 @@ clicking that bar, with the same hit. Other labels let the pointer through to th
250
259
  new InstancedMesh({ geometry: box(1), data: rows, labels: { text: (row) => row.name, place: 'above', priority: (row) => row.alert ? 2 : 0 } })
251
260
  ```
252
261
 
262
+ **Labels by zoom level**: `minScale` on any label shows it only when the view is zoomed in at least that
263
+ far - that many CSS pixels to one world unit, at the label's point (perspective or orthographic alike).
264
+ Until then it takes no part in the layout, so it can't push other labels out. `labels` can also be a
265
+ list, several labels per instance - the short one always, the details as you come closer:
266
+
267
+ ```js
268
+ new InstancedMesh({ geometry: box(1), data: elements, labels: [
269
+ { text: (e) => e.symbol, place: 'center', className: 'symbol' },
270
+ { text: (e) => e.name, place: 'below', className: 'name', minScale: 45 },
271
+ ] })
272
+ ```
273
+
274
+ Pick `minScale` by looking: at the zoom where the details should appear, a world unit spans that many
275
+ pixels (a 1-unit tile drawn 60 px wide is 60). `view.explain()` counts the labels still waiting.
276
+
253
277
  ## Geometry
254
278
 
255
279
  Generators return a `Geometry`; each takes one options object (a number is the main size):
@@ -287,7 +311,9 @@ extrude({
287
311
 
288
312
  Your own: `new Geometry({ positions, normals?, uvs?, uvs2?, colors?, indices? })` - flat arrays, xyz per
289
313
  vertex; `colors` are rgb 0-1 per vertex (use `material: { vertexColors: true }`); `uvs2` is a second UV
290
- set, for baked occlusion and light maps (below).
314
+ set, for baked occlusion and light maps (below). To animate one - cloth, a wave, a morph - write
315
+ `geometry.positions` in place and call `geometry.needsUpdate()` each frame; normals you didn't give are
316
+ worked out again to follow the shape.
291
317
 
292
318
  ## Materials
293
319
 
@@ -425,7 +451,7 @@ on top, as with UVs (animate `offset` to scroll a pattern). 0 (the default) uses
425
451
  |---|---|
426
452
  | `AmbientLight` | `color`, `intensity` |
427
453
  | `HemisphereLight` | `sky`, `ground`, `intensity` |
428
- | `DirectionalLight` | `color`, `intensity`, `direction` (the way it travels, e.g. `[-1, -2, -1]`), `castShadow` |
454
+ | `DirectionalLight` | `color`, `intensity`, `direction` (the way it travels, e.g. `[-1, -2, -1]`), `castShadow`, `shadowBounds` |
429
455
  | `PointLight` | `color`, `intensity`, `position`, `range` - inverse-square falloff, so intensity ~ distance squared (30 lights things ~5 m away); `castShadow` |
430
456
  | `SpotLight` | point light options plus `direction`, `angle`, `penumbra`, `castShadow` |
431
457
 
@@ -443,7 +469,9 @@ meshes opt in with `castShadow` and `receiveShadow`. No shadow camera to set up.
443
469
  lamp - anything whose bounds hold the light - doesn't block it, so a lamp's shade can cast shadows of
444
470
  its own. A point light casts all round, and everything that casts blocks it, around it too: a candle in
445
471
  a lantern or a carved pumpkin lights the room only through the openings. Give a bulb or flame mesh
446
- drawn at the light `castShadow: false`.
472
+ drawn at the light `castShadow: false`. The directional shadow is fitted to whatever casts, every
473
+ frame; when things fly far off, give the light `shadowBounds: { min: [x, y, z], max: [x, y, z] }` -
474
+ the world area that gets shadows - so the map stays sharp there.
447
475
 
448
476
  ```js
449
477
  scene.add(new PointLight({ position: [0, 0.1, 0], intensity: 0.5, range: 4, castShadow: true })) // a candle in a jack-o'-lantern
@@ -469,6 +497,9 @@ and `target` yourself; the orbit always turns about wherever the target is.
469
497
  press or the wheel takes over.
470
498
  - **Back to the start**: `view.frame(undefined, { direction: 'perspective', duration: 0.6 })`
471
499
  (or the preset the view started with).
500
+ - **Where framing would go**: `view.framing(target, { padding, direction })` returns
501
+ `{ position, target, height? }` without moving the camera - for a camera that eases after
502
+ something that keeps changing size (`camera.position.lerp(aim.position, 0.1)`, the same for target).
472
503
  - **Follow something that moves** - move the camera and its target by as much as it moved, so the
473
504
  view keeps its angle and the user can still orbit round it:
474
505
 
@@ -524,7 +555,8 @@ view.on('hover', (hit) => show(hit?.datum)) // fires when the th
524
555
  children. A node's `highlight` is the colour for its subtree; it tints the mesh, instance or point
525
556
  under the pointer. Add `highlightWhole: true` to tint the whole subtree as one thing (a piece of
526
557
  furniture, a machine) whichever part is hovered. Without the View's `highlight`, only nodes with
527
- their own highlight light up - leave it off to keep floors and walls plain.
558
+ their own highlight light up - leave it off to keep floors and walls plain, or keep it and give
559
+ those `highlight: false` (`null` means "the View's").
528
560
  - `onHover` and `onClick` are the app's own handler for a node; to add more without replacing it
529
561
  (an add-on, a second concern), `node.on('hover' | 'click', fn)` - it returns a function that stops
530
562
  listening. Both run. A drag has one handler, `onDrag`: one thing moves a node at a time.
@@ -549,6 +581,22 @@ chair.onDrag = (drag) => {
549
581
  corners - as the canvas is; `outlineAntialias: false` on the View (or `view.renderer.outlineAntialias =
550
582
  false`, any time) draws them in hard pixel steps instead.
551
583
  - `view.controls.cancel()` lets go of the orbit gesture in progress, for apps that take over a press.
584
+ - **Keyboard and screen readers**: `keyboard: true` on the View makes the canvas a stop in the page's
585
+ tab order. The arrow keys (and Home, End, PageUp, PageDown) step through what can be hovered or
586
+ clicked, in scene order and each mesh's instances in order: each instance, point, surface grid
587
+ point (a mesh with a row per vertex) or row of data under a node with its own `tooltip`, `onClick`,
588
+ `onHover`, `onDrag` or `highlight` - and, when the View has a tooltip or handlers itself, each one
589
+ with data. A mesh with no rows is a stop when it has a name or a handler of its own. A
590
+ `highlightWhole` subtree is one stop. Lines with a row per segment (a network's links) are a stop
591
+ per segment, after everything else. The one stepped to is shown as hovering would show it - tint (`#ffcc00` when no highlight
592
+ is set), tooltip, hover handlers - and Enter or Space clicks it (the handler's event is null);
593
+ Escape leaves it, and moving the pointer takes over. A screen reader hears each one: the tooltip's
594
+ text, else its data, else its name, with "3 of 12" - or `keyboard: { label: (hit) => text }`. The
595
+ canvas is labelled with `description`, or from the scene: each chart's `summary()` ("Sales: bar
596
+ chart of 12 rows by month, sales"), else "3D view of 12 items".
597
+ - **Reduced motion**: `reducedMotion` follows the system's setting by default. When on, `frame()`
598
+ with a `duration` jumps, `autoRotate` holds, and chart transitions and flowing links settle at
599
+ once. Motion of your own (a `Timeline`, `onUpdate` spin) can check `view.reducedMotion`.
552
600
 
553
601
  ## Charts: `phoria2/charts`
554
602
 
@@ -600,7 +648,8 @@ A chart is a node with axes, ticks, grid walls, a legend and tooltips built in.
600
648
  in it as a wrapping row, so give it no fixed height). Categories get swatches that click to hide
601
649
  and show; a number colour gets a key - a ramp, highest at the top, with values marked up its side,
602
650
  `ticks` as on an axis),
603
- `theme: 'dark' | 'light'`, `title`, `transition: false | { duration, ease }`,
651
+ `theme: 'dark' | 'light'`, `title`, `description` (what a screen reader hears for it; else `chart.summary()`
652
+ makes one from the title and channels), `transition: false | { duration, ease }`,
604
653
  `overlap: 'dim' | 'hide' | 'show'` (labels), `highlight` (a colour, false, or for networks
605
654
  `'neighbours'` - the default, lighting the node's links and linked nodes - or `'node'`, the node alone), and node options (`position`, ...).
606
655
  - **Surfaces**: one row per grid point - `x` and `z` are the grid (numbers or categories), `y` the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "phoria2",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "Small, friendly 3D for the web - a WebGL 2 renderer with charts, labels, picking, particles and glTF",
5
5
  "keywords": [
6
6
  "3d",
@@ -85,10 +85,13 @@
85
85
  "plugin": "node scripts/plugin.mjs"
86
86
  },
87
87
  "devDependencies": {
88
+ "@dimforge/rapier3d-compat": "^0.21.0",
88
89
  "@eslint/js": "^10.0.1",
89
90
  "eslint": "^10.3.0",
90
91
  "gl-matrix": "^3.4.4",
91
92
  "globals": "^17.6.0",
93
+ "manifold-3d": "^3.5.4",
94
+ "recast-navigation": "^0.44.0",
92
95
  "three": "0.186.1",
93
96
  "typescript": "^7.0.2",
94
97
  "vite": "^8.0.12",
@@ -202,6 +202,11 @@
202
202
  "type": "null"
203
203
  }
204
204
  ]
205
+ },
206
+ "minScale": {
207
+ "type": "number",
208
+ "description": "Show the label only when zoomed in this far: CSS pixels to one world unit at its point (0, always).",
209
+ "minimum": 0
205
210
  }
206
211
  },
207
212
  "unevaluatedProperties": false
@@ -327,6 +332,16 @@
327
332
  },
328
333
  "castShadow": {
329
334
  "type": "boolean"
335
+ },
336
+ "shadowBounds": {
337
+ "type": ["object", "null"],
338
+ "description": "The part of the world, in world units, that has shadows; null (the default) follows what casts.",
339
+ "required": ["min", "max"],
340
+ "properties": {
341
+ "min": { "$ref": "#/$defs/vec3" },
342
+ "max": { "$ref": "#/$defs/vec3" }
343
+ },
344
+ "additionalProperties": false
330
345
  }
331
346
  },
332
347
  "unevaluatedProperties": false
@@ -462,13 +477,16 @@
462
477
  "description": "Anything of your own."
463
478
  },
464
479
  "highlight": {
465
- "description": "Hover tint for this subtree, or null for none.",
480
+ "description": "Hover tint for this subtree; false for none, null to use the View's.",
466
481
  "anyOf": [
467
482
  {
468
483
  "$ref": "#/$defs/color"
469
484
  },
470
485
  {
471
486
  "type": "null"
487
+ },
488
+ {
489
+ "const": false
472
490
  }
473
491
  ]
474
492
  },
@@ -255,8 +255,12 @@ export class OrbitControl {
255
255
  this.target.add(move)
256
256
  }
257
257
 
258
- /** Apply inertia and auto-rotate; the View calls this each frame. @param {number} dt seconds */
259
- update(dt) {
258
+ /**
259
+ * Apply inertia and auto-rotate; the View calls this each frame.
260
+ * @param {number} dt seconds
261
+ * @param {boolean} [still] hold auto-rotation - the View passes its reducedMotion
262
+ */
263
+ update(dt, still = false) {
260
264
  if (!this.enabled) return
261
265
  if (this.#pointers.size === 0) {
262
266
  const v = this.#velocity
@@ -267,7 +271,7 @@ export class OrbitControl {
267
271
  v.yaw *= keep
268
272
  v.pitch *= keep
269
273
  }
270
- if (this.autoRotate) this.rotate(-(this.autoRotate === true ? 0.5 : this.autoRotate) * dt, 0)
274
+ if (this.autoRotate && !still) this.rotate(-(this.autoRotate === true ? 0.5 : this.autoRotate) * dt, 0)
271
275
  }
272
276
  }
273
277