phoria2 1.1.0 → 1.3.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 (81) hide show
  1. package/README.md +70 -13
  2. package/dist/bloom.js +4 -4
  3. package/dist/bloom.js.map +1 -1
  4. package/dist/charts.js +538 -508
  5. package/dist/charts.js.map +1 -1
  6. package/dist/core.js +4270 -3652
  7. package/dist/core.js.map +1 -1
  8. package/dist/edit.js +65 -65
  9. package/dist/edit.js.map +1 -1
  10. package/dist/explain.js +6 -2
  11. package/dist/explain.js.map +1 -1
  12. package/dist/gltf.js +116 -116
  13. package/dist/gltf.js.map +1 -1
  14. package/dist/keyboard.js +188 -0
  15. package/dist/keyboard.js.map +1 -0
  16. package/dist/motion.js +35 -35
  17. package/dist/motion.js.map +1 -1
  18. package/dist/particles.js +188 -115
  19. package/dist/particles.js.map +1 -1
  20. package/dist/passes.js +29 -29
  21. package/dist/passes.js.map +1 -1
  22. package/dist/phoria2.js +1 -1
  23. package/dist/picking.js +32 -16
  24. package/dist/picking.js.map +1 -1
  25. package/dist/snapshot.js +1 -1
  26. package/dist/tooltip.js +1 -1
  27. package/dist/types/OrbitControl.d.ts +6 -2
  28. package/dist/types/Raycaster.d.ts +10 -0
  29. package/dist/types/View.d.ts +70 -1
  30. package/dist/types/area.d.ts +18 -0
  31. package/dist/types/charts/Chart.d.ts +7 -0
  32. package/dist/types/explain.d.ts +3 -0
  33. package/dist/types/frame.d.ts +7 -0
  34. package/dist/types/geometry/Geometry.d.ts +24 -2
  35. package/dist/types/index.d.ts +2 -0
  36. package/dist/types/keyboard.d.ts +48 -0
  37. package/dist/types/labels.d.ts +4 -0
  38. package/dist/types/material/Material.d.ts +15 -0
  39. package/dist/types/material/Texture.d.ts +18 -2
  40. package/dist/types/particles/Emitter.d.ts +39 -2
  41. package/dist/types/scene/Node.d.ts +19 -14
  42. package/dist/types/scene/drawables.d.ts +54 -15
  43. package/dist/types/scene/lights.d.ts +16 -2
  44. package/dist/types/shadow.d.ts +2 -1
  45. package/dist/types/types.d.ts +14 -0
  46. package/dist/types/value/Color.d.ts +0 -4
  47. package/dist/types/value/Quat.d.ts +4 -2
  48. package/dist/types/webgl/resources.d.ts +12 -2
  49. package/dist/types/webgl/shaders.d.ts +7 -2
  50. package/llms-full.txt +106 -10
  51. package/package.json +4 -1
  52. package/schema/scene.schema.json +67 -1
  53. package/src/OrbitControl.js +7 -3
  54. package/src/Raycaster.js +58 -0
  55. package/src/View.js +220 -41
  56. package/src/area.js +131 -0
  57. package/src/charts/Chart.js +20 -2
  58. package/src/charts/marks.js +29 -2
  59. package/src/explain.js +6 -2
  60. package/src/frame.js +29 -9
  61. package/src/geometry/Geometry.js +32 -4
  62. package/src/index.js +1 -0
  63. package/src/keyboard.js +298 -0
  64. package/src/labels.js +4 -1
  65. package/src/material/Material.js +9 -2
  66. package/src/material/Texture.js +55 -2
  67. package/src/particles/Emitter.js +121 -10
  68. package/src/scene/Node.js +20 -9
  69. package/src/scene/Scene.js +2 -0
  70. package/src/scene/cameras.js +4 -1
  71. package/src/scene/drawables.js +115 -15
  72. package/src/scene/lights.js +40 -6
  73. package/src/shadow.js +27 -4
  74. package/src/types.js +6 -0
  75. package/src/value/Color.js +21 -0
  76. package/src/value/Quat.js +15 -6
  77. package/src/watch.js +14 -2
  78. package/src/webgl/WebGLRenderer.js +36 -5
  79. package/src/webgl/picking.js +22 -1
  80. package/src/webgl/resources.js +168 -14
  81. package/src/webgl/shaders.js +110 -11
@@ -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,21 +236,42 @@ 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
+ /**
242
+ * Draw the points back to front, furthest first, each frame - for soft points that cover what is
243
+ * behind them (smoke, with blending 'normal'), which then layer the right way round. Drawn as squares.
244
+ */
245
+ sort: boolean;
246
+ /** @param {DrawableOptions & { labels?: InstanceLabels | InstanceLabels[] | null, sort?: boolean }} options */
228
247
  constructor(options: DrawableOptions & {
229
- labels?: InstanceLabels | null;
248
+ labels?: InstanceLabels | InstanceLabels[] | null;
249
+ sort?: boolean;
230
250
  });
231
251
  /** @returns {'points'} */
232
252
  get drawMode(): 'points';
233
253
  }
254
+ export type LinesOptions = DrawableOptions & {
255
+ arrows?: 'none' | 'start' | 'end' | 'both';
256
+ arrowSize?: number;
257
+ };
258
+ /**
259
+ * @typedef {DrawableOptions & { arrows?: 'none' | 'start' | 'end' | 'both', arrowSize?: number }} LinesOptions
260
+ */
234
261
  /**
235
262
  * Line segments: indices (or consecutive vertices) in pairs. Width is in pixels and works on
236
- * every platform. edges(geometry) makes a wireframe of any geometry.
263
+ * every platform. edges(geometry) makes a wireframe of any geometry. Segments each starting where
264
+ * the one before ended make a chain - a polyline - which dashes run on along and arrowheads finish.
237
265
  */
238
266
  export declare class Lines extends Drawable {
239
267
  static type: string;
268
+ static get OPTIONS(): string[];
269
+ /** Arrowheads at the ends of each chain: at its 'start', its 'end', 'both' or 'none'. @type {'none' | 'start' | 'end' | 'both'} */
270
+ arrows: 'none' | 'start' | 'end' | 'both';
271
+ /** Arrowhead length in pixels; 0 sizes them from the material's width. */
272
+ arrowSize: number;
273
+ /** @param {LinesOptions} options */
274
+ constructor(options: LinesOptions);
240
275
  /** @returns {'lines'} */
241
276
  get drawMode(): 'lines';
242
277
  }
@@ -261,6 +296,8 @@ export declare class Label extends Node {
261
296
  pointer: string;
262
297
  anchor: string;
263
298
  zIndex: string;
299
+ minZoom: string;
300
+ lod: string;
264
301
  collision: string;
265
302
  };
266
303
  text: string;
@@ -280,6 +317,8 @@ export declare class Label extends Node {
280
317
  leader: boolean;
281
318
  /** Set at the view's left or right edge, level with its point, or null for by its point. @type {'left' | 'right' | null} */
282
319
  edge: 'left' | 'right' | null;
320
+ /** Shown only when zoomed in this far: CSS pixels to one world unit at its point; 0 for always. */
321
+ minScale: number;
283
322
  /** @param {LabelOptions} options */
284
323
  constructor(options: LabelOptions);
285
324
  }
@@ -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} */
@@ -101,8 +101,8 @@ export declare class Resources {
101
101
  count: number;
102
102
  } | null, withTangents?: boolean): number;
103
103
  /**
104
- * Vertex array for Lines: one instance per segment with its two endpoints (and colours),
105
- * expanded to a screen-space quad in the vertex shader.
104
+ * Vertex array for Lines: one instance per segment with its two endpoints (and colours) and where
105
+ * it is along its chain, expanded to a screen-space quad - and arrowheads - in the vertex shader.
106
106
  * @param {Geometry} geometry
107
107
  * @returns {MeshBuffers}
108
108
  */
@@ -119,6 +119,16 @@ export declare class Resources {
119
119
  * @param {object} owner @param {Scene} scene
120
120
  */
121
121
  release(owner: object, scene: Scene): void;
122
+ /**
123
+ * Vertex array for points drawn as squares: one instance a point - its position, colour, size,
124
+ * opacity, angle and index - over the corner strip lines use. Given a model-view matrix, the points
125
+ * go back to front (furthest first) - re-sorted every call, as the camera may have moved; without
126
+ * one, in their own order; with undefined, in whatever order they were last sent (picking, which
127
+ * reads each point's index and doesn't mind).
128
+ * @param {Geometry} geometry @param {ArrayLike<number> | null} [modelView]
129
+ * @returns {MeshBuffers}
130
+ */
131
+ sprites(geometry: Geometry, modelView?: ArrayLike<number> | null): MeshBuffers;
122
132
  /** Forget everything - after a lost context, when every GL object is already gone. */
123
133
  reset(): void;
124
134
  }
@@ -8,11 +8,16 @@ export declare const ATTRIBUTES: {
8
8
  instanceColor: number;
9
9
  tangent: number;
10
10
  uv2: number;
11
+ pointSize: number;
12
+ pointOpacity: number;
13
+ pointAngle: number;
14
+ pointIndex: number;
11
15
  segmentStart: number;
12
16
  segmentEnd: number;
13
17
  segmentColorStart: number;
14
18
  segmentColorEnd: number;
15
19
  corner: number;
20
+ segmentInfo: number;
16
21
  };
17
22
  export declare const CLIP = "\n#ifdef CLIP\nuniform vec4 uClipPlane; // world space, unit normal: cut away where dot(n, p) + d > 0\nvoid clipAway(vec3 p) {\n if (dot(uClipPlane.xyz, p) + uClipPlane.w > 0.0) discard;\n}\n#else\nvoid clipAway(vec3 p) {}\n#endif\n";
18
23
  export declare const FOG = "\n#ifdef FOG\nuniform vec3 uFogColor; // linear\nuniform vec2 uFogRange; // linear fog: near, far\nuniform float uFogDensity; // exponential fog\nin float vFogDepth;\nvec3 fogged(vec3 c) {\n#ifdef FOG_LINEAR\n float f = clamp((vFogDepth - uFogRange.x) / max(uFogRange.y - uFogRange.x, 1e-4), 0.0, 1.0);\n#else\n float d = uFogDensity * vFogDepth;\n float f = 1.0 - exp(-d * d);\n#endif\n#ifdef FOG_ADD\n return c * (1.0 - f);\n#elif defined(FOG_MULTIPLY)\n // a filter in the fog fades to white, which leaves what is behind it as it was\n return mix(c, vec3(1.0), f);\n#else\n return mix(c, uFogColor, f);\n#endif\n}\n#else\nvec3 fogged(vec3 c) { return c; }\n#endif\n";
@@ -22,8 +27,8 @@ export declare const EQUIRECT = "\nvec2 equirectUv(vec3 d) {\n return vec2(atan
22
27
  export declare const FULLSCREEN_VERTEX = "\nout vec2 vUv;\nvoid main() {\n vec2 p = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2));\n vUv = p;\n gl_Position = vec4(p * 2.0 - 1.0, 0.0, 1.0);\n}\n";
23
28
  export declare const TONE_MAP = "\nuniform float uExposure;\nuniform int uToneMapping; // 0 none, 1 aces, 2 neutral\n\nvec3 toneMap(vec3 c) {\n c *= uExposure;\n if (uToneMapping == 1) {\n // ACES filmic, Narkowicz's fit\n c = clamp((c * (2.51 * c + 0.03)) / (c * (2.43 * c + 0.59) + 0.14), 0.0, 1.0);\n } else if (uToneMapping == 2) {\n // Khronos PBR Neutral\n float x = min(c.r, min(c.g, c.b));\n float offset = x < 0.08 ? x - 6.25 * x * x : 0.04;\n c -= offset;\n float peak = max(c.r, max(c.g, c.b));\n if (peak > 0.8) {\n float d = 0.2;\n float newPeak = 1.0 - d * d / (peak + d - 0.8);\n c *= newPeak / peak;\n float g = 1.0 - 1.0 / (0.15 * (peak - newPeak) + 1.0);\n c = mix(c, vec3(newPeak), g);\n }\n }\n return c;\n}\n";
24
29
  export declare const MESH_VERTEX = "\nlayout(location = 0) in vec3 position;\nlayout(location = 1) in vec3 normal;\nlayout(location = 2) in vec2 uv;\nlayout(location = 3) in vec3 color;\n#ifdef INSTANCED\nlayout(location = 4) in mat4 instanceMatrix;\nlayout(location = 8) in vec3 instanceColor;\n#endif\n#ifdef ANISOTROPY\nlayout(location = 9) in vec4 tangent;\nout vec4 vTangent;\n#endif\n#ifdef UV2\nlayout(location = 10) in vec2 uv2;\nout vec2 vUv2;\n#endif\n\nuniform mat4 uModel;\nuniform mat3 uNormalMatrix;\nuniform mat4 uViewProjection;\nuniform int uHighlightIndex; // -2 none, -1 the whole node, otherwise the instance\n#ifdef FOG\nuniform mat4 uView;\nout float vFogDepth;\n#endif\n\nout vec3 vWorldPosition;\nout vec3 vNormal;\nout vec2 vUv;\nout vec3 vColor;\nflat out float vHighlight;\n#ifdef TRIPLANAR\n// the mesh's own space, so projected maps stay on it as it moves; and how its normals turn into the world\nout vec3 vObjectPosition;\nout vec3 vObjectNormal;\nout mat3 vObjectToWorld;\n#endif\n#ifdef PICK\nflat out uint vPickElement; // which instance, point or segment, for GPU picking\n#endif\n\n\nvec3 srgbToLinear(vec3 c) {\n return mix(c / 12.92, pow((c + 0.055) / 1.055, vec3(2.4)), step(0.04045, c));\n}\nvec3 linearToSrgb(vec3 c) {\n c = clamp(c, 0.0, 1.0);\n return mix(c * 12.92, 1.055 * pow(c, vec3(1.0 / 2.4)) - 0.055, step(0.0031308, c));\n}\n\n\n// Matrices times vectors only - a matrix times a matrix per vertex is four times the work, and\n// with 100,000 instances the vertex stage is most of the frame.\nvoid main() {\n vColor = vec3(1.0);\n vHighlight = uHighlightIndex == -1 ? 1.0 : 0.0;\n#ifdef INSTANCED\n vec4 local = instanceMatrix * vec4(position, 1.0);\n // the instance's normal matrix as its cofactor: the inverse transpose up to scale, from three\n // cross products, flipped back for mirroring transforms\n mat3 m = mat3(instanceMatrix);\n mat3 instanceNormal = mat3(cross(m[1], m[2]), cross(m[2], m[0]), cross(m[0], m[1])) * sign(dot(m[0], cross(m[1], m[2])));\n vec3 n = instanceNormal * normal;\n vColor *= instanceColor;\n if (gl_InstanceID == uHighlightIndex) vHighlight = 1.0;\n#else\n vec4 local = vec4(position, 1.0);\n vec3 n = normal;\n#endif\n#ifdef VERTEX_COLORS\n vColor *= srgbToLinear(color);\n#endif\n vec4 world = uModel * local;\n vWorldPosition = world.xyz;\n vNormal = uNormalMatrix * n;\n vUv = uv;\n#ifdef UV2\n vUv2 = uv2;\n#endif\n#ifdef ANISOTROPY\n // a direction along the surface, so it turns as the model does\n#ifdef INSTANCED\n vTangent = vec4(mat3(uModel) * (m * tangent.xyz), tangent.w);\n#else\n vTangent = vec4(mat3(uModel) * tangent.xyz, tangent.w);\n#endif\n#endif\n#ifdef TRIPLANAR\n vObjectPosition = position;\n vObjectNormal = normal;\n#ifdef INSTANCED\n vObjectToWorld = uNormalMatrix * instanceNormal;\n#else\n vObjectToWorld = uNormalMatrix;\n#endif\n#endif\n gl_Position = uViewProjection * world;\n#ifdef FOG\n vFogDepth = -(uView * world).z;\n#endif\n#ifdef PICK\n vPickElement = uint(gl_InstanceID);\n#endif\n}\n";
25
- export declare const POINTS_VERTEX = "\nlayout(location = 0) in vec3 position;\nlayout(location = 3) in vec3 color;\n\nuniform mat4 uModel;\nuniform mat4 uView;\nuniform mat4 uProjection;\nuniform float uSize; // pixels (with the pixel ratio applied), or world units when attenuated\nuniform float uScale; // projection focal term x half the buffer height: world size -> pixels at depth 1\nuniform int uHighlightIndex; // -2 none, -1 every point, otherwise the point\n\nout vec3 vColor;\nflat out float vHighlight;\n#ifdef PICK\nflat out uint vPickElement; // which instance, point or segment, for GPU picking\n#endif\n#ifdef FOG\nout float vFogDepth;\n#endif\n\n\nvec3 srgbToLinear(vec3 c) {\n return mix(c / 12.92, pow((c + 0.055) / 1.055, vec3(2.4)), step(0.04045, c));\n}\nvec3 linearToSrgb(vec3 c) {\n c = clamp(c, 0.0, 1.0);\n return mix(c * 12.92, 1.055 * pow(c, vec3(1.0 / 2.4)) - 0.055, step(0.0031308, c));\n}\n\n\nvoid main() {\n vec4 viewPosition = uView * (uModel * vec4(position, 1.0));\n gl_Position = uProjection * viewPosition;\n#ifdef FOG\n vFogDepth = -viewPosition.z;\n#endif\n#ifdef SIZE_ATTENUATION\n // perspective shrinks with distance; an orthographic projection (w row 0, 0, 0, 1) doesn't\n float depth = uProjection[3][3] == 1.0 ? 1.0 : max(-viewPosition.z, 1e-4);\n gl_PointSize = uSize * uScale / depth;\n#else\n gl_PointSize = uSize;\n#endif\n vColor = vec3(1.0);\n#ifdef VERTEX_COLORS\n vColor = srgbToLinear(color);\n#endif\n vHighlight = uHighlightIndex == -1 || gl_VertexID == uHighlightIndex ? 1.0 : 0.0;\n#ifdef PICK\n vPickElement = uint(gl_VertexID);\n#endif\n}\n";
26
- export declare const LINES_VERTEX = "\nlayout(location = 0) in vec3 segmentStart;\nlayout(location = 1) in vec3 segmentEnd;\nlayout(location = 2) in vec3 segmentColorStart;\nlayout(location = 3) in vec3 segmentColorEnd;\nlayout(location = 4) in vec2 corner; // x: 0 at start, 1 at end; y: -1 or 1 across\n\nuniform mat4 uModel;\nuniform mat4 uView;\nuniform mat4 uProjection;\nuniform vec2 uResolution; // drawing buffer size in pixels\nuniform float uWidth; // pixels, already scaled by the pixel ratio\nuniform float uNear;\n\nout vec3 vColor;\n#ifdef PICK\nflat out uint vPickElement; // which instance, point or segment, for GPU picking\n#endif\n#ifdef FOG\nout float vFogDepth;\n#endif\n\n\nvec3 srgbToLinear(vec3 c) {\n return mix(c / 12.92, pow((c + 0.055) / 1.055, vec3(2.4)), step(0.04045, c));\n}\nvec3 linearToSrgb(vec3 c) {\n c = clamp(c, 0.0, 1.0);\n return mix(c * 12.92, 1.055 * pow(c, vec3(1.0 / 2.4)) - 0.055, step(0.0031308, c));\n}\n\n\nvoid main() {\n vec4 a = uView * (uModel * vec4(segmentStart, 1.0));\n vec4 b = uView * (uModel * vec4(segmentEnd, 1.0));\n // clip the segment to the near plane so lines passing behind the camera stay correct\n float clipZ = -uNear;\n if (a.z > clipZ && b.z > clipZ) {\n gl_Position = vec4(2.0, 2.0, 2.0, 1.0); // wholly behind: off screen\n return;\n }\n if (a.z > clipZ) a = mix(a, b, (clipZ - a.z) / (b.z - a.z));\n if (b.z > clipZ) b = mix(b, a, (clipZ - b.z) / (a.z - b.z));\n\n vec4 ca = uProjection * a;\n vec4 cb = uProjection * b;\n vec2 sa = ca.xy / ca.w * uResolution * 0.5;\n vec2 sb = cb.xy / cb.w * uResolution * 0.5;\n vec2 dir = sb - sa;\n float len = length(dir);\n dir = len > 0.0 ? dir / len : vec2(1.0, 0.0);\n vec2 normal = vec2(-dir.y, dir.x);\n\n vec4 clip = mix(ca, cb, corner.x);\n#ifdef FOG\n vFogDepth = -mix(a.z, b.z, corner.x);\n#endif\n // extend past each end by half the width so joins look continuous\n vec2 offset = normal * corner.y * uWidth * 0.5 + dir * (corner.x * 2.0 - 1.0) * uWidth * 0.5;\n clip.xy += offset / (uResolution * 0.5) * clip.w;\n gl_Position = clip;\n#ifdef PICK\n vPickElement = uint(gl_InstanceID);\n#endif\n\n vColor = vec3(1.0);\n#ifdef VERTEX_COLORS\n vColor = srgbToLinear(mix(segmentColorStart, segmentColorEnd, corner.x));\n#endif\n}\n";
30
+ export declare const POINTS_VERTEX = "\nlayout(location = 0) in vec3 position;\nlayout(location = 3) in vec3 color;\n#ifdef SPRITES\n// each point a square: one instance a point, drawn as a strip over the corners (along 0-1, across -1-1)\nlayout(location = 4) in vec2 corner;\nlayout(location = 11) in float pointSize;\nlayout(location = 12) in float pointOpacity;\nlayout(location = 13) in float pointAngle;\nlayout(location = 14) in float pointIndex;\nuniform vec2 uResolution; // drawing buffer size in pixels\nout vec2 vUv;\nout float vOpacity;\n#endif\n\nuniform mat4 uModel;\nuniform mat4 uView;\nuniform mat4 uProjection;\nuniform float uSize; // pixels (with the pixel ratio applied), or world units when attenuated\nuniform float uScale; // projection focal term x half the buffer height: world size -> pixels at depth 1\nuniform int uHighlightIndex; // -2 none, -1 every point, otherwise the point\n\nout vec3 vColor;\nflat out float vHighlight;\n#ifdef PICK\nflat out uint vPickElement; // which instance, point or segment, for GPU picking\n#endif\n#ifdef FOG\nout float vFogDepth;\n#endif\n\n\nvec3 srgbToLinear(vec3 c) {\n return mix(c / 12.92, pow((c + 0.055) / 1.055, vec3(2.4)), step(0.04045, c));\n}\nvec3 linearToSrgb(vec3 c) {\n c = clamp(c, 0.0, 1.0);\n return mix(c * 12.92, 1.055 * pow(c, vec3(1.0 / 2.4)) - 0.055, step(0.0031308, c));\n}\n\n\nvoid main() {\n vec4 viewPosition = uView * (uModel * vec4(position, 1.0));\n#ifdef SPRITES\n // the square's corner, turned by the point's angle: the texture turns with it\n vec2 c = vec2(corner.x * 2.0 - 1.0, corner.y);\n float s = sin(pointAngle), k = cos(pointAngle);\n vec2 turned = vec2(c.x * k - c.y * s, c.x * s + c.y * k);\n float size = uSize * pointSize;\n#ifdef SIZE_ATTENUATION\n // world units: the square sits in the view, facing the camera\n viewPosition.xy += turned * size * 0.5;\n gl_Position = uProjection * viewPosition;\n#else\n // pixels: the square is laid out on the screen\n gl_Position = uProjection * viewPosition;\n gl_Position.xy += turned * size / uResolution * gl_Position.w;\n#endif\n // the image upright on the square, its top at the top, as a point's is\n vUv = vec2(c.x * 0.5 + 0.5, 0.5 - c.y * 0.5);\n vOpacity = pointOpacity;\n int self = int(pointIndex + 0.5);\n#else\n gl_Position = uProjection * viewPosition;\n int self = gl_VertexID;\n#endif\n#ifdef FOG\n vFogDepth = -viewPosition.z;\n#endif\n#ifdef SPRITES\n#elif defined(SIZE_ATTENUATION)\n // perspective shrinks with distance; an orthographic projection (w row 0, 0, 0, 1) doesn't\n float depth = uProjection[3][3] == 1.0 ? 1.0 : max(-viewPosition.z, 1e-4);\n gl_PointSize = uSize * uScale / depth;\n#else\n gl_PointSize = uSize;\n#endif\n vColor = vec3(1.0);\n#ifdef VERTEX_COLORS\n vColor = srgbToLinear(color);\n#endif\n vHighlight = uHighlightIndex == -1 || self == uHighlightIndex ? 1.0 : 0.0;\n#ifdef PICK\n vPickElement = uint(self);\n#endif\n}\n";
31
+ export declare const LINES_VERTEX = "\nlayout(location = 0) in vec3 segmentStart;\nlayout(location = 1) in vec3 segmentEnd;\nlayout(location = 2) in vec3 segmentColorStart;\nlayout(location = 3) in vec3 segmentColorEnd;\n// the distance along its chain (segments joined end to start) where it starts, in the model's\n// units; 1 if it starts a chain; 1 if it ends one\nlayout(location = 5) in vec3 segmentInfo;\n\nuniform mat4 uModel;\nuniform mat4 uView;\nuniform mat4 uProjection;\nuniform vec2 uResolution; // drawing buffer size in pixels\nuniform float uWidth; // pixels, already scaled by the pixel ratio\nuniform float uNear;\nuniform int uArrows; // arrowheads: 1 at the start of each chain, 2 at its end, 3 at both\nuniform float uArrowSize; // their length in pixels, scaled by the pixel ratio\n\n// six vertices an instance make the segment's quad, as two triangles - x: 0 at its start, 1 at its\n// end; y: -1 or 1 across - and six more its two arrowheads, drawn only when asked for\nconst vec2 QUAD[6] = vec2[6](vec2(0.0, -1.0), vec2(0.0, 1.0), vec2(1.0, -1.0), vec2(1.0, -1.0), vec2(0.0, 1.0), vec2(1.0, 1.0));\n\nout vec3 vColor;\n#ifdef DASHED\nout float vDistance; // along the chain, in the model's units\nflat out float vArrow; // 1 on an arrowhead, which is never dashed\n#endif\n#ifdef PICK\nflat out uint vPickElement; // which instance, point or segment, for GPU picking\n#endif\n#ifdef FOG\nout float vFogDepth;\n#endif\n\n\nvec3 srgbToLinear(vec3 c) {\n return mix(c / 12.92, pow((c + 0.055) / 1.055, vec3(2.4)), step(0.04045, c));\n}\nvec3 linearToSrgb(vec3 c) {\n c = clamp(c, 0.0, 1.0);\n return mix(c * 12.92, 1.055 * pow(c, vec3(1.0 / 2.4)) - 0.055, step(0.0031308, c));\n}\n\n\nvoid main() {\n vec4 a = uView * (uModel * vec4(segmentStart, 1.0));\n vec4 b = uView * (uModel * vec4(segmentEnd, 1.0));\n // clip the segment to the near plane so lines passing behind the camera stay correct\n float clipZ = -uNear;\n if (a.z > clipZ && b.z > clipZ) {\n gl_Position = vec4(2.0, 2.0, 2.0, 1.0); // wholly behind: off screen\n return;\n }\n if (a.z > clipZ) a = mix(a, b, (clipZ - a.z) / (b.z - a.z));\n if (b.z > clipZ) b = mix(b, a, (clipZ - b.z) / (a.z - b.z));\n\n vec4 ca = uProjection * a;\n vec4 cb = uProjection * b;\n vec2 sa = ca.xy / ca.w * uResolution * 0.5;\n vec2 sb = cb.xy / cb.w * uResolution * 0.5;\n vec2 dir = sb - sa;\n float len = length(dir);\n dir = len > 0.0 ? dir / len : vec2(1.0, 0.0);\n vec2 normal = vec2(-dir.y, dir.x);\n\n bool arrowStart = (uArrows & 1) != 0 && segmentInfo.y > 0.5;\n bool arrowEnd = (uArrows & 2) != 0 && segmentInfo.z > 0.5;\n float along, arrow = 0.0;\n vec2 offset;\n if (gl_VertexID < 6) {\n vec2 c = QUAD[gl_VertexID];\n along = c.x;\n // past each end by half the width so joins look continuous - or short of it, under an arrowhead\n float startOut = arrowStart ? -uArrowSize * 0.8 : uWidth * 0.5;\n float endOut = arrowEnd ? -uArrowSize * 0.8 : uWidth * 0.5;\n offset = normal * c.y * uWidth * 0.5 + dir * (along > 0.5 ? endOut : -startOut);\n } else {\n // an arrowhead: its tip on the chain's end, pointing out along the segment\n int which = (gl_VertexID - 6) / 3, k = gl_VertexID - 6 - which * 3;\n if (which == 0 ? !arrowStart : !arrowEnd) {\n gl_Position = vec4(2.0, 2.0, 2.0, 1.0);\n return;\n }\n along = float(which);\n vec2 d = which == 1 ? dir : -dir;\n float wing = uArrowSize * 0.45;\n offset = k == 0 ? vec2(0.0) : -d * uArrowSize + normal * (k == 1 ? wing : -wing);\n arrow = 1.0;\n }\n\n vec4 clip = mix(ca, cb, along);\n#ifdef FOG\n vFogDepth = -mix(a.z, b.z, along);\n#endif\n clip.xy += offset / (uResolution * 0.5) * clip.w;\n gl_Position = clip;\n#ifdef DASHED\n vDistance = segmentInfo.x + length(segmentEnd - segmentStart) * along;\n vArrow = arrow;\n#endif\n#ifdef PICK\n vPickElement = uint(gl_InstanceID);\n#endif\n\n vColor = vec3(1.0);\n#ifdef VERTEX_COLORS\n vColor = srgbToLinear(mix(segmentColorStart, segmentColorEnd, along));\n#endif\n}\n";
27
32
  export type ShaderKind = 'mesh' | 'points' | 'lines';
28
33
  /**
29
34
  * Vertex and fragment source for a kind of drawable, or a pass's own pair, with a set of feature defines.
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
 
@@ -135,6 +139,8 @@ Methods:
135
139
  - `on('click' | 'hover', (hit, event) => ...)` - see Interaction. Returns an unsubscribe function.
136
140
  - `tooltip(fn)` - set the tooltip function later.
137
141
  - `pick(x, y)` / `pick(event)` - the hit under a canvas position; `pickAll` for every hit.
142
+ - `pickArea(area)` - every hit inside a box `{ x, y, width, height }` or a lasso of `[x, y]` points
143
+ (CSS pixels), nearest first - see Interaction.
138
144
  - `rayAt(x, y)` / `rayAt(event)` - the world `Ray` through a canvas position, for placing things:
139
145
  `ray.at(ray.intersectPlane([0, 1, 0], [0, 0, 0]))` is the point on the floor under the pointer
140
146
  (`intersectPlane` returns null when the ray runs along the plane or away from it).
@@ -164,7 +170,7 @@ something changes):
164
170
  ## Scene graph
165
171
 
166
172
  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`,
173
+ (Euler XYZ radians, `[x, y, z]`), `quaternion` (`[x, y, z, w]` or `{ x, y, z, w }`), `scale` (number or `[x, y, z]`), `visible`,
168
174
  `pickable` (default true), `userData`, `children`, `onUpdate`, `clip` (a section plane - see
169
175
  Cutaways), plus the interaction options below.
170
176
 
@@ -218,6 +224,24 @@ new Lines({ geometry: edges(box(2)), material: { color: '#333', width: 2 } })
218
224
  new Label({ text: 'Rack A1', position: [0, 2, 0], className: 'my-label', place: 'above' })
219
225
  ```
220
226
 
227
+ **Lines** dashed and with arrowheads - flow and direction in network diagrams. Dashes are material
228
+ options in the model's units: `dash` (0 is solid), `gap` (0 is the dash's length) and `dashOffset`,
229
+ which moves them along - animate it for traffic. Arrowheads are Lines options: `arrows: 'start' |
230
+ 'end' | 'both'` puts them at the ends of each chain (segments each starting where the one before
231
+ ended - a polyline is one chain, separate links are one each), `arrowSize` in pixels (0, the default,
232
+ sizes them from the width). The dashes run on along a chain, and arrowheads are never dashed.
233
+
234
+ ```js
235
+ const flow = view.scene.add(new Lines({ geometry: links, arrows: 'end', material: { color: '#4cc9f0', width: 2, dash: 0.3, gap: 0.15 } }))
236
+ view.start((dt) => { flow.material.dashOffset += dt * 0.8 }) // traffic moving along each link
237
+ ```
238
+
239
+ **Points** each their own size, opacity or angle: give the geometry `sizes` (multiples of the material's
240
+ `size`), `opacities` (0-1) or `angles` (radians, turning the point's `map`), one per vertex. Such points
241
+ are drawn as camera-facing squares, so any size works and they stay whole at the edge of the view.
242
+ `sort: true` draws them furthest first each frame - for soft points with `blending: 'normal'`
243
+ (smoke, cloud) that would otherwise layer wrongly; with additive blending order doesn't matter.
244
+
221
245
  **InstancedMesh** - many copies of one geometry in one draw call, for bars, racks, nodes:
222
246
 
223
247
  ```js
@@ -229,7 +253,11 @@ bars.setColor(3, 'red')
229
253
  `setInstance(i, { position, rotation, quaternion, scale, color })`. An instance's `color` multiplies the
230
254
  material's colour, which is white unless you set one - so the instance colour shows as given. `count` defaults to the number
231
255
  of rows in `data`. For very large data, write `bars.instanceMatrices` (16 numbers per instance,
232
- column-major) directly and call `bars.needsUpdate()`.
256
+ column-major) directly and call `bars.needsUpdate()`. An instance scaled to 0 is hidden: it draws
257
+ nothing, and bounds, framing, shadows and labels leave it out - so a pool can keep spare instances.
258
+ Positions and quaternions also take `{ x, y, z }` and `{ x, y, z, w }`, as physics engines return
259
+ them, so a physics bridge is one line a body:
260
+ `bricks.setInstance(i, { position: body.translation(), quaternion: body.rotation() })`.
233
261
 
234
262
  **Labels** are HTML elements pinned to 3D points (style them with CSS). Options: `text`, `position`,
235
263
  `className`, `place` (`'center'` | `'above'` | `'below'` | `'left'` | `'right'`), `offset` ([x, y]
@@ -237,7 +265,8 @@ pixels), `priority` (higher wins when labels overlap), `overlap` (`'hide'` | `'d
237
265
  a label does when covered), `occlude` (true: counts as covered when geometry is in front), `leader`
238
266
  (true: a line from the point to the label, with a dot on the point - for callouts), `edge` (`'left'` |
239
267
  `'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).
268
+ `leader: true`; `offset` then moves it in and up or down), `minScale` (show it only when zoomed in this
269
+ far - below).
241
270
 
242
271
  A label answers the pointer when it has handlers: a Label with its own `onClick` or `onHover` is
243
272
  clicked and hovered like a mesh (`new Label({ text: 'Mantle', onClick: () => show('mantle') })` - a
@@ -250,6 +279,21 @@ clicking that bar, with the same hit. Other labels let the pointer through to th
250
279
  new InstancedMesh({ geometry: box(1), data: rows, labels: { text: (row) => row.name, place: 'above', priority: (row) => row.alert ? 2 : 0 } })
251
280
  ```
252
281
 
282
+ **Labels by zoom level**: `minScale` on any label shows it only when the view is zoomed in at least that
283
+ far - that many CSS pixels to one world unit, at the label's point (perspective or orthographic alike).
284
+ Until then it takes no part in the layout, so it can't push other labels out. `labels` can also be a
285
+ list, several labels per instance - the short one always, the details as you come closer:
286
+
287
+ ```js
288
+ new InstancedMesh({ geometry: box(1), data: elements, labels: [
289
+ { text: (e) => e.symbol, place: 'center', className: 'symbol' },
290
+ { text: (e) => e.name, place: 'below', className: 'name', minScale: 45 },
291
+ ] })
292
+ ```
293
+
294
+ Pick `minScale` by looking: at the zoom where the details should appear, a world unit spans that many
295
+ pixels (a 1-unit tile drawn 60 px wide is 60). `view.explain()` counts the labels still waiting.
296
+
253
297
  ## Geometry
254
298
 
255
299
  Generators return a `Geometry`; each takes one options object (a number is the main size):
@@ -287,7 +331,9 @@ extrude({
287
331
 
288
332
  Your own: `new Geometry({ positions, normals?, uvs?, uvs2?, colors?, indices? })` - flat arrays, xyz per
289
333
  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).
334
+ set, for baked occlusion and light maps (below). To animate one - cloth, a wave, a morph - write
335
+ `geometry.positions` in place and call `geometry.needsUpdate()` each frame; normals you didn't give are
336
+ worked out again to follow the shape.
291
337
 
292
338
  ## Materials
293
339
 
@@ -335,7 +381,10 @@ unless its `colorSpace` was set on purpose - `normalMap: await loadTexture('n.pn
335
381
 
336
382
  **Canvas and video textures**: a texture's `image` can be a canvas - draw on it, then call
337
383
  `texture.needsUpdate()` - or a `<video>`, whose frames upload by themselves while it plays, with
338
- nothing to call. `new Texture({ url: 'promo.mp4' })` (or .webm, .ogv, .mov, .m4v) makes the video
384
+ nothing to call. For a big canvas drawn on a little at a time (paint, a map being revealed), say what
385
+ changed - `texture.needsUpdate({ x, y, width, height })`, in the canvas's pixels from its top left -
386
+ and only that rectangle is sent, rather than the whole canvas every frame (several calls before a
387
+ frame add up). `new Texture({ url: 'promo.mp4' })` (or .webm, .ogv, .mov, .m4v) makes the video
339
388
  for you, muted and looping so browsers let it start by itself; `texture.image` is that `<video>` -
340
389
  pause, seek or unmute it as any other. A browser that blocks autoplay leaves it on its first frame
341
390
  until `texture.image.play()` from a click. Video textures make no mipmaps (they would be remade
@@ -425,7 +474,7 @@ on top, as with UVs (animate `offset` to scroll a pattern). 0 (the default) uses
425
474
  |---|---|
426
475
  | `AmbientLight` | `color`, `intensity` |
427
476
  | `HemisphereLight` | `sky`, `ground`, `intensity` |
428
- | `DirectionalLight` | `color`, `intensity`, `direction` (the way it travels, e.g. `[-1, -2, -1]`), `castShadow` |
477
+ | `DirectionalLight` | `color`, `intensity`, `direction` (the way it travels, e.g. `[-1, -2, -1]`), `castShadow`, `shadowBounds` |
429
478
  | `PointLight` | `color`, `intensity`, `position`, `range` - inverse-square falloff, so intensity ~ distance squared (30 lights things ~5 m away); `castShadow` |
430
479
  | `SpotLight` | point light options plus `direction`, `angle`, `penumbra`, `castShadow` |
431
480
 
@@ -443,7 +492,9 @@ meshes opt in with `castShadow` and `receiveShadow`. No shadow camera to set up.
443
492
  lamp - anything whose bounds hold the light - doesn't block it, so a lamp's shade can cast shadows of
444
493
  its own. A point light casts all round, and everything that casts blocks it, around it too: a candle in
445
494
  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`.
495
+ drawn at the light `castShadow: false`. The directional shadow is fitted to whatever casts, every
496
+ frame; when things fly far off, give the light `shadowBounds: { min: [x, y, z], max: [x, y, z] }` -
497
+ the world area that gets shadows - so the map stays sharp there.
447
498
 
448
499
  ```js
449
500
  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 +520,9 @@ and `target` yourself; the orbit always turns about wherever the target is.
469
520
  press or the wheel takes over.
470
521
  - **Back to the start**: `view.frame(undefined, { direction: 'perspective', duration: 0.6 })`
471
522
  (or the preset the view started with).
523
+ - **Where framing would go**: `view.framing(target, { padding, direction })` returns
524
+ `{ position, target, height? }` without moving the camera - for a camera that eases after
525
+ something that keeps changing size (`camera.position.lerp(aim.position, 0.1)`, the same for target).
472
526
  - **Follow something that moves** - move the camera and its target by as much as it moved, so the
473
527
  view keeps its angle and the user can still orbit round it:
474
528
 
@@ -506,6 +560,16 @@ view.on('hover', (hit) => show(hit?.datum)) // fires when the th
506
560
  - The **nearest surface** under the pointer is the hit - a link drawn over a site's top wins the
507
561
  hover there. To let one kind of thing through, give it `pickable: false`; to choose among
508
562
  everything under the pointer, `view.pickAll(event)` lists every hit, nearest first.
563
+ - **Selecting an area** - `view.pickArea({ x, y, width, height })` (width and height may be negative,
564
+ as a drag up or left makes them) or `view.pickArea([[x, y], ...])` for a lasso: a hit for each point,
565
+ instance and line segment whose place on screen is inside, and each mesh whose middle is. Places, not
566
+ surfaces: what is hidden behind other things is found too, so a box over a scatter takes that whole
567
+ region. Hidden, unpickable and zero-scale instances are left out. Turn `view.controls.enabled` off
568
+ while the user drags out the area.
569
+
570
+ ```js
571
+ const chosen = view.pickArea({ x: 40, y: 30, width: 200, height: 120 }).map((h) => h.datum)
572
+ ```
509
573
  - **Rays of your own** - line of sight, what's below a dropped object, picking in tests: a
510
574
  `Raycaster` finds what any ray hits, on the CPU. Set `raycaster.ray.origin` and `.direction`, or aim
511
575
  it with `setFromCamera([x, y], camera)` (-1 to 1, y up); `intersect(nodeOrNodes)` returns hits,
@@ -524,7 +588,8 @@ view.on('hover', (hit) => show(hit?.datum)) // fires when the th
524
588
  children. A node's `highlight` is the colour for its subtree; it tints the mesh, instance or point
525
589
  under the pointer. Add `highlightWhole: true` to tint the whole subtree as one thing (a piece of
526
590
  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.
591
+ their own highlight light up - leave it off to keep floors and walls plain, or keep it and give
592
+ those `highlight: false` (`null` means "the View's").
528
593
  - `onHover` and `onClick` are the app's own handler for a node; to add more without replacing it
529
594
  (an add-on, a second concern), `node.on('hover' | 'click', fn)` - it returns a function that stops
530
595
  listening. Both run. A drag has one handler, `onDrag`: one thing moves a node at a time.
@@ -549,6 +614,22 @@ chair.onDrag = (drag) => {
549
614
  corners - as the canvas is; `outlineAntialias: false` on the View (or `view.renderer.outlineAntialias =
550
615
  false`, any time) draws them in hard pixel steps instead.
551
616
  - `view.controls.cancel()` lets go of the orbit gesture in progress, for apps that take over a press.
617
+ - **Keyboard and screen readers**: `keyboard: true` on the View makes the canvas a stop in the page's
618
+ tab order. The arrow keys (and Home, End, PageUp, PageDown) step through what can be hovered or
619
+ clicked, in scene order and each mesh's instances in order: each instance, point, surface grid
620
+ point (a mesh with a row per vertex) or row of data under a node with its own `tooltip`, `onClick`,
621
+ `onHover`, `onDrag` or `highlight` - and, when the View has a tooltip or handlers itself, each one
622
+ with data. A mesh with no rows is a stop when it has a name or a handler of its own. A
623
+ `highlightWhole` subtree is one stop. Lines with a row per segment (a network's links) are a stop
624
+ per segment, after everything else. The one stepped to is shown as hovering would show it - tint (`#ffcc00` when no highlight
625
+ is set), tooltip, hover handlers - and Enter or Space clicks it (the handler's event is null);
626
+ Escape leaves it, and moving the pointer takes over. A screen reader hears each one: the tooltip's
627
+ text, else its data, else its name, with "3 of 12" - or `keyboard: { label: (hit) => text }`. The
628
+ canvas is labelled with `description`, or from the scene: each chart's `summary()` ("Sales: bar
629
+ chart of 12 rows by month, sales"), else "3D view of 12 items".
630
+ - **Reduced motion**: `reducedMotion` follows the system's setting by default. When on, `frame()`
631
+ with a `duration` jumps, `autoRotate` holds, and chart transitions and flowing links settle at
632
+ once. Motion of your own (a `Timeline`, `onUpdate` spin) can check `view.reducedMotion`.
552
633
 
553
634
  ## Charts: `phoria2/charts`
554
635
 
@@ -600,7 +681,8 @@ A chart is a node with axes, ticks, grid walls, a legend and tooltips built in.
600
681
  in it as a wrapping row, so give it no fixed height). Categories get swatches that click to hide
601
682
  and show; a number colour gets a key - a ramp, highest at the top, with values marked up its side,
602
683
  `ticks` as on an axis),
603
- `theme: 'dark' | 'light'`, `title`, `transition: false | { duration, ease }`,
684
+ `theme: 'dark' | 'light'`, `title`, `description` (what a screen reader hears for it; else `chart.summary()`
685
+ makes one from the title and channels), `transition: false | { duration, ease }`,
604
686
  `overlap: 'dim' | 'hide' | 'show'` (labels), `highlight` (a colour, false, or for networks
605
687
  `'neighbours'` - the default, lighting the node's links and linked nodes - or `'node'`, the node alone), and node options (`position`, ...).
606
688
  - **Surfaces**: one row per grid point - `x` and `z` are the grid (numbers or categories), `y` the
@@ -758,6 +840,20 @@ second), `follow` (a node whose position is the origin each frame - a trail), `o
758
840
  default material is small additive points. `burst(count, spawnOptions)`, `clear()`, `set(options)`,
759
841
  `count`.
760
842
 
843
+ How particles look over their lives: `size` (a multiple of the material's size, or a list each particle
844
+ goes through - `[0.5, 3]` billows), `sizeSpread` (each one's size varies by up to this share, 0-1),
845
+ `opacity` (0-1, or a list - `[0, 1, 0]` fades in and out) and `spin` (radians per second: a number
846
+ either way, or `[min, max]`; spinning particles start at a random angle). For smoke, a soft `map`,
847
+ `blending: 'normal'` and `sort: true`:
848
+
849
+ ```js
850
+ const smoke = view.scene.add(new Emitter({
851
+ rate: 40, velocity: [0, 1.5, 0], velocitySpread: 0.4, lifetime: [3, 5], color: ['#777', '#333'],
852
+ size: [0.6, 4], sizeSpread: 0.3, opacity: [0, 0.6, 0], spin: 0.5, sort: true,
853
+ material: { map: puff, size: 1, blending: 'normal' },
854
+ }))
855
+ ```
856
+
761
857
  `Body` moves any node: `node.onUpdate(new Body({ velocity, gravity, drag }).update)`;
762
858
  `body.impulse([x, y, z])` kicks it.
763
859
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "phoria2",
3
- "version": "1.1.0",
3
+ "version": "1.3.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",