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.
- package/README.md +71 -13
- package/dist/bloom.js +4 -4
- package/dist/bloom.js.map +1 -1
- package/dist/charts.js +358 -345
- package/dist/charts.js.map +1 -1
- package/dist/core.js +3989 -3600
- package/dist/core.js.map +1 -1
- package/dist/edit.js +43 -43
- package/dist/edit.js.map +1 -1
- package/dist/explain.js +5 -1
- package/dist/explain.js.map +1 -1
- package/dist/gltf.js +96 -96
- package/dist/gltf.js.map +1 -1
- package/dist/motion.js +3 -3
- package/dist/motion.js.map +1 -1
- package/dist/particles.js +16 -16
- package/dist/particles.js.map +1 -1
- package/dist/passes.js +26 -26
- package/dist/passes.js.map +1 -1
- package/dist/phoria2.js +1 -1
- package/dist/picking.js +16 -16
- package/dist/picking.js.map +1 -1
- package/dist/types/OrbitControl.d.ts +6 -2
- package/dist/types/Raycaster.d.ts +10 -0
- package/dist/types/View.d.ts +59 -1
- package/dist/types/charts/Chart.d.ts +7 -0
- package/dist/types/explain.d.ts +3 -0
- package/dist/types/frame.d.ts +7 -0
- package/dist/types/geometry/Geometry.d.ts +8 -2
- package/dist/types/keyboard.d.ts +48 -0
- package/dist/types/labels.d.ts +4 -0
- package/dist/types/scene/Node.d.ts +19 -14
- package/dist/types/scene/drawables.d.ts +32 -14
- package/dist/types/scene/lights.d.ts +16 -2
- package/dist/types/shadow.d.ts +2 -1
- package/dist/types/types.d.ts +14 -0
- package/dist/types/value/Color.d.ts +0 -4
- package/dist/types/value/Quat.d.ts +4 -2
- package/llms-full.txt +58 -9
- package/package.json +4 -1
- package/schema/scene.schema.json +19 -1
- package/src/OrbitControl.js +7 -3
- package/src/Raycaster.js +58 -0
- package/src/View.js +204 -41
- package/src/charts/Chart.js +20 -2
- package/src/explain.js +6 -2
- package/src/frame.js +29 -9
- package/src/geometry/Geometry.js +16 -3
- package/src/keyboard.js +298 -0
- package/src/labels.js +4 -1
- package/src/scene/Node.js +20 -9
- package/src/scene/Scene.js +2 -0
- package/src/scene/cameras.js +4 -1
- package/src/scene/drawables.js +48 -12
- package/src/scene/lights.js +40 -6
- package/src/shadow.js +27 -4
- package/src/types.js +6 -0
- package/src/value/Color.js +21 -0
- package/src/value/Quat.js +15 -6
- package/src/watch.js +6 -1
package/src/scene/drawables.js
CHANGED
|
@@ -12,7 +12,7 @@ import { NODE_OPTIONS, Node } from './Node.js'
|
|
|
12
12
|
/** @import { Material, MaterialOptions } from '../material/Material.js' */
|
|
13
13
|
/** @import { GeometryOptions } from '../geometry/Geometry.js' */
|
|
14
14
|
/** @import { NodeOptions } from './Node.js' */
|
|
15
|
-
/** @import { ColorInput, Vec3Input } from '../types.js' */
|
|
15
|
+
/** @import { ColorInput, QuatInput, Vec3Input } from '../types.js' */
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
18
|
* @typedef {NodeOptions & {
|
|
@@ -45,11 +45,14 @@ import { NODE_OPTIONS, Node } from './Node.js'
|
|
|
45
45
|
* @property {'left' | 'right' | null} [edge] set the label at the view's left or right edge, level with its
|
|
46
46
|
* point - callouts in a column down the side of a diagram, with leader: true. offset then moves it in from
|
|
47
47
|
* the edge and up or down (default null: by its point)
|
|
48
|
+
* @property {number} [minScale] show the label only when zoomed in this far: at least this many CSS pixels to
|
|
49
|
+
* one world unit, at the label's point - details that appear as you come closer (default 0: always)
|
|
48
50
|
*/
|
|
49
51
|
/** @typedef {'center' | 'above' | 'below' | 'left' | 'right'} LabelPlace */
|
|
50
52
|
|
|
51
53
|
/**
|
|
52
|
-
* One label per instance or point, from its row of data
|
|
54
|
+
* One label per instance or point, from its row of data - or a list of these, for several each (a symbol
|
|
55
|
+
* always, its details when zoomed in).
|
|
53
56
|
* @typedef {Omit<LabelPlacement, 'priority'> & {
|
|
54
57
|
* text: (datum: any, index: number) => string | number | null | undefined,
|
|
55
58
|
* priority?: number | ((datum: any, index: number) => number),
|
|
@@ -61,7 +64,7 @@ import { NODE_OPTIONS, Node } from './Node.js'
|
|
|
61
64
|
*/
|
|
62
65
|
|
|
63
66
|
export const LABEL_PLACES = /** @type {LabelPlace[]} */ (['center', 'above', 'below', 'left', 'right'])
|
|
64
|
-
const PLACEMENT_OPTIONS = ['place', 'offset', 'className', 'priority', 'overlap', 'occlude', 'leader', 'edge']
|
|
67
|
+
const PLACEMENT_OPTIONS = ['place', 'offset', 'className', 'priority', 'overlap', 'occlude', 'leader', 'edge', 'minScale']
|
|
65
68
|
const LABEL_HINTS = {
|
|
66
69
|
line: 'use leader: true - a line from the point to the label',
|
|
67
70
|
callout: "use leader: true, with edge: 'left' | 'right' to line the labels up down the side",
|
|
@@ -70,6 +73,8 @@ const LABEL_HINTS = {
|
|
|
70
73
|
pointer: 'use leader: true - a line from the point to the label',
|
|
71
74
|
anchor: "use place: 'center' | 'above' | 'below' | 'left' | 'right' (where the label goes)",
|
|
72
75
|
zIndex: "use 'priority': higher stays when labels overlap",
|
|
76
|
+
minZoom: "use 'minScale': CSS pixels to one world unit at the label's point",
|
|
77
|
+
lod: "use 'minScale': the label shows when zoomed in at least this far (CSS pixels to one world unit)",
|
|
73
78
|
collision: "use overlap: 'hide' | 'dim' | 'show'",
|
|
74
79
|
}
|
|
75
80
|
// labels per instance or point take one more: they can't be moved anywhere but by offset
|
|
@@ -94,12 +99,26 @@ function placement(owner, o) {
|
|
|
94
99
|
occlude: !!o.occlude,
|
|
95
100
|
leader: !!o.leader,
|
|
96
101
|
edge: o.edge == null ? null : /** @type {'left' | 'right'} */ (checkOneOf(owner, 'edge', o.edge, ['left', 'right'])),
|
|
102
|
+
minScale: o.minScale === undefined ? 0 : minScale(owner, o.minScale),
|
|
97
103
|
}
|
|
98
104
|
}
|
|
99
105
|
|
|
100
|
-
/** @param {string} owner @param {unknown} v @returns {
|
|
106
|
+
/** @param {string} owner @param {unknown} v @returns {number} */
|
|
107
|
+
function minScale(owner, v) {
|
|
108
|
+
const n = checkNumber(owner, 'minScale', v)
|
|
109
|
+
if (!(n >= 0)) fail(owner, `'minScale' is CSS pixels to one world unit, 0 or more, got ${n}`)
|
|
110
|
+
return n
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** @typedef {InstanceLabels & Required<Omit<LabelPlacement, 'priority'>> & { priority: number | ((datum: any, index: number) => number) }} CheckedLabels */
|
|
114
|
+
|
|
115
|
+
/** @param {string} owner @param {unknown} v @returns {CheckedLabels | CheckedLabels[] | null} */
|
|
101
116
|
function instanceLabels(owner, v) {
|
|
102
117
|
if (v == null) return null
|
|
118
|
+
if (Array.isArray(v)) {
|
|
119
|
+
if (!v.length) fail(owner, "'labels' as a list needs at least one { text: (datum, index) => string, ... }")
|
|
120
|
+
return v.map((one) => /** @type {CheckedLabels} */ (instanceLabels(owner, one)))
|
|
121
|
+
}
|
|
103
122
|
if (typeof v !== 'object' || typeof (/** @type {any} */ (v)).text !== 'function') {
|
|
104
123
|
fail(owner, `'labels' must be { text: (datum, index) => string, ... }, got ${describe(v)}`)
|
|
105
124
|
}
|
|
@@ -253,7 +272,7 @@ export class Mesh extends Drawable {
|
|
|
253
272
|
* @typedef {object} InstanceOptions
|
|
254
273
|
* @property {Vec3Input} [position]
|
|
255
274
|
* @property {Vec3Input | Euler} [rotation] Euler angles in radians, XYZ order
|
|
256
|
-
* @property {
|
|
275
|
+
* @property {QuatInput} [quaternion] [x, y, z, w] or { x, y, z, w } - as physics engines give them
|
|
257
276
|
* @property {Vec3Input | number} [scale]
|
|
258
277
|
* @property {ColorInput} [color] multiplies the material colour
|
|
259
278
|
*/
|
|
@@ -261,11 +280,23 @@ export class Mesh extends Drawable {
|
|
|
261
280
|
const INSTANCE_OPTIONS = new Set(['position', 'rotation', 'quaternion', 'scale', 'color'])
|
|
262
281
|
// reused by setInstance, so updating every instance of a large mesh each frame allocates nothing
|
|
263
282
|
const scratch = { position: new Vec3(), scale: new Vec3(), quaternion: new Quat(), rotation: new Euler() }
|
|
283
|
+
const SCRATCH_COLOR = new Color()
|
|
264
284
|
const scratchMatrix = new Float32Array(16)
|
|
265
285
|
|
|
286
|
+
/**
|
|
287
|
+
* True when an instance is scaled to nothing - hidden: it draws nothing, takes no room in its mesh's
|
|
288
|
+
* bounds and shows no label.
|
|
289
|
+
* @internal @param {ArrayLike<number>} m instance matrices @param {number} i
|
|
290
|
+
*/
|
|
291
|
+
export function hiddenInstance(m, i) {
|
|
292
|
+
const o = i * 16
|
|
293
|
+
return !m[o] && !m[o + 1] && !m[o + 2] && !m[o + 4] && !m[o + 5] && !m[o + 6] && !m[o + 8] && !m[o + 9] && !m[o + 10]
|
|
294
|
+
}
|
|
295
|
+
|
|
266
296
|
/**
|
|
267
297
|
* Many copies of one geometry and material, drawn together. Set each copy with setInstance().
|
|
268
|
-
* Picking reports which instance was hit.
|
|
298
|
+
* Picking reports which instance was hit. An instance scaled to 0 is hidden - it draws nothing, and
|
|
299
|
+
* framing, culling, shadows and labels leave it out - so a pool of instances can keep spares.
|
|
269
300
|
*/
|
|
270
301
|
export class InstancedMesh extends Mesh {
|
|
271
302
|
static type = 'instanced-mesh'
|
|
@@ -279,12 +310,12 @@ export class InstancedMesh extends Mesh {
|
|
|
279
310
|
instanceColors = null
|
|
280
311
|
/** @internal Bumped when instance data changes, so renderers re-upload. */
|
|
281
312
|
version = 0
|
|
282
|
-
/** A label per instance
|
|
313
|
+
/** A label per instance from its row of data - or a list of them, several per instance. @type {ReturnType<typeof instanceLabels>} */
|
|
283
314
|
labels = null
|
|
284
315
|
|
|
285
316
|
/**
|
|
286
317
|
* @param {DrawableOptions & { count?: number, instances?: InstanceOptions[], instanceMatrices?: ArrayLike<number>,
|
|
287
|
-
* instanceColors?: ArrayLike<number>, labels?: InstanceLabels | null }} options count defaults to the number
|
|
318
|
+
* instanceColors?: ArrayLike<number>, labels?: InstanceLabels | InstanceLabels[] | null }} options count defaults to the number
|
|
288
319
|
* of instances or rows of data; instanceMatrices/instanceColors are raw data, as toJSON() writes
|
|
289
320
|
*/
|
|
290
321
|
constructor(options) {
|
|
@@ -375,7 +406,7 @@ export class InstancedMesh extends Mesh {
|
|
|
375
406
|
/** @param {number} i @param {ColorInput} color */
|
|
376
407
|
#setColor(i, color) {
|
|
377
408
|
if (!this.instanceColors) this.instanceColors = new Float32Array(this.instanceMatrices.length / 16 * 3).fill(1)
|
|
378
|
-
this.instanceColors.set(
|
|
409
|
+
this.instanceColors.set(SCRATCH_COLOR.set(color, 'InstancedMesh'), i * 3)
|
|
379
410
|
}
|
|
380
411
|
|
|
381
412
|
/** @internal @param {Box3} box */
|
|
@@ -401,6 +432,7 @@ export class InstancedMesh extends Mesh {
|
|
|
401
432
|
const ex = (local.max[0] - local.min[0]) / 2, ey = (local.max[1] - local.min[1]) / 2, ez = (local.max[2] - local.min[2]) / 2
|
|
402
433
|
const w = this.worldMatrix, im = this.instanceMatrices, m = scratchMatrix
|
|
403
434
|
for (let i = 0; i < this.count; i++) {
|
|
435
|
+
if (hiddenInstance(im, i)) continue
|
|
404
436
|
mat4.multiply(m, w, im.subarray(i * 16, i * 16 + 16))
|
|
405
437
|
const x = m[0] * cx + m[4] * cy + m[8] * cz + m[12]
|
|
406
438
|
const y = m[1] * cx + m[5] * cy + m[9] * cz + m[13]
|
|
@@ -436,10 +468,10 @@ export class Points extends Drawable {
|
|
|
436
468
|
static type = 'points'
|
|
437
469
|
static get OPTIONS() { return [...DRAWABLE_OPTIONS, 'labels'] }
|
|
438
470
|
|
|
439
|
-
/** A label per point
|
|
471
|
+
/** A label per point from its row of data - or a list of them, several per point. @type {ReturnType<typeof instanceLabels>} */
|
|
440
472
|
labels = null
|
|
441
473
|
|
|
442
|
-
/** @param {DrawableOptions & { labels?: InstanceLabels | null }} options */
|
|
474
|
+
/** @param {DrawableOptions & { labels?: InstanceLabels | InstanceLabels[] | null }} options */
|
|
443
475
|
constructor(options) {
|
|
444
476
|
super(options)
|
|
445
477
|
this.labels = instanceLabels(this.constructor.name, options?.labels)
|
|
@@ -505,6 +537,8 @@ export class Label extends Node {
|
|
|
505
537
|
leader = false
|
|
506
538
|
/** Set at the view's left or right edge, level with its point, or null for by its point. @type {'left' | 'right' | null} */
|
|
507
539
|
edge = null
|
|
540
|
+
/** Shown only when zoomed in this far: CSS pixels to one world unit at its point; 0 for always. */
|
|
541
|
+
minScale = 0
|
|
508
542
|
|
|
509
543
|
/** @param {LabelOptions} options */
|
|
510
544
|
constructor(options) {
|
|
@@ -529,7 +563,8 @@ export class Label extends Node {
|
|
|
529
563
|
/** @internal @returns {Record<string, unknown>} */
|
|
530
564
|
cloneOptions() {
|
|
531
565
|
return { ...super.cloneOptions(), text: this.text, className: this.className, place: this.place,
|
|
532
|
-
offset: [...this.offset], priority: this.priority, overlap: this.overlap, occlude: this.occlude, leader: this.leader, edge: this.edge
|
|
566
|
+
offset: [...this.offset], priority: this.priority, overlap: this.overlap, occlude: this.occlude, leader: this.leader, edge: this.edge,
|
|
567
|
+
minScale: this.minScale }
|
|
533
568
|
}
|
|
534
569
|
|
|
535
570
|
/** @internal @returns {Record<string, unknown>} */
|
|
@@ -544,6 +579,7 @@ export class Label extends Node {
|
|
|
544
579
|
if (this.occlude) json.occlude = true
|
|
545
580
|
if (this.leader) json.leader = true
|
|
546
581
|
if (this.edge) json.edge = this.edge
|
|
582
|
+
if (this.minScale) json.minScale = this.minScale
|
|
547
583
|
return json
|
|
548
584
|
}
|
|
549
585
|
}
|
package/src/scene/lights.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { Color } from '../value/Color.js'
|
|
2
2
|
import { Vec3 } from '../value/Vec3.js'
|
|
3
|
-
import {
|
|
3
|
+
import { Box3 } from '../value/Box3.js'
|
|
4
|
+
import { checkNumber, describe, fail } from '../validate.js'
|
|
4
5
|
import { NODE_OPTIONS, Node, inParentSpace, setPoint } from './Node.js'
|
|
5
6
|
|
|
6
7
|
/** @import { NodeOptions } from './Node.js' */
|
|
7
|
-
/** @import { ColorInput, Vec3Input } from '../types.js' */
|
|
8
|
+
/** @import { Box3Input, ColorInput, Vec3Input } from '../types.js' */
|
|
8
9
|
|
|
9
10
|
/** @typedef {NodeOptions & { color?: ColorInput, intensity?: number }} LightOptions */
|
|
10
11
|
/** @typedef {'ambient' | 'hemisphere' | 'directional' | 'point' | 'spot'} LightKind */
|
|
@@ -189,24 +190,57 @@ export class DirectionalLight extends AimedLight {
|
|
|
189
190
|
static type = 'directional-light'
|
|
190
191
|
/** @internal @type {LightKind} */
|
|
191
192
|
static kind = 'directional'
|
|
192
|
-
static get OPTIONS() { return [...LIGHT_OPTIONS, 'direction', 'target', 'castShadow'] }
|
|
193
|
+
static get OPTIONS() { return [...LIGHT_OPTIONS, 'direction', 'target', 'castShadow', 'shadowBounds'] }
|
|
193
194
|
|
|
194
195
|
castShadow = false
|
|
196
|
+
/** @type {Box3 | null} */
|
|
197
|
+
#shadowBounds = null
|
|
195
198
|
|
|
196
|
-
/** @param {AimedLightOptions & { castShadow?: boolean }} [options] */
|
|
199
|
+
/** @param {AimedLightOptions & { castShadow?: boolean, shadowBounds?: Box3Input | null }} [options] */
|
|
197
200
|
constructor(options = {}) {
|
|
198
201
|
super(options)
|
|
199
202
|
if (options.castShadow !== undefined) this.castShadow = !!options.castShadow
|
|
203
|
+
if (options.shadowBounds !== undefined) this.#setShadowBounds(options.shadowBounds)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* The part of the world, in world units, that has shadows - or null (the default) to follow
|
|
208
|
+
* whatever casts them. The shadow map is fitted to what casts every frame, so one caster flung far
|
|
209
|
+
* off spreads its texels over everything and every shadow blurs; set the area where things happen
|
|
210
|
+
* and its shadows stay sharp. Casters outside it still shadow it, from wherever they stand.
|
|
211
|
+
* Assigning copies.
|
|
212
|
+
* @returns {Box3 | null}
|
|
213
|
+
*/
|
|
214
|
+
get shadowBounds() { return this.#shadowBounds }
|
|
215
|
+
|
|
216
|
+
/** @param {Box3Input | null} v */
|
|
217
|
+
set shadowBounds(v) { this.#setShadowBounds(v) }
|
|
218
|
+
|
|
219
|
+
/** @param {Box3Input | null} v */
|
|
220
|
+
#setShadowBounds(v) {
|
|
221
|
+
if (v === null) {
|
|
222
|
+
this.#shadowBounds = null
|
|
223
|
+
return
|
|
224
|
+
}
|
|
225
|
+
const b = /** @type {any} */ (v)
|
|
226
|
+
if (!b || typeof b !== 'object' || !b.min || !b.max) {
|
|
227
|
+
fail(this.constructor.name, `'shadowBounds' is a box - { min: [x, y, z], max: [x, y, z] } or a Box3 - or null, got ${describe(v)}`)
|
|
228
|
+
}
|
|
229
|
+
const box = new Box3(Vec3.fromInput(b.min, this.constructor.name), Vec3.fromInput(b.max, this.constructor.name))
|
|
230
|
+
if (box.isEmpty()) fail(this.constructor.name, `'shadowBounds' min must not be above max on any axis, got ${describe(v)}`)
|
|
231
|
+
this.#shadowBounds = box
|
|
200
232
|
}
|
|
201
233
|
|
|
202
234
|
/** @internal @returns {Record<string, unknown>} */
|
|
203
235
|
cloneOptions() {
|
|
204
|
-
return { ...super.cloneOptions(), castShadow: this.castShadow }
|
|
236
|
+
return { ...super.cloneOptions(), castShadow: this.castShadow, shadowBounds: this.#shadowBounds }
|
|
205
237
|
}
|
|
206
238
|
|
|
207
239
|
/** @internal @returns {Record<string, unknown>} */
|
|
208
240
|
jsonOptions() {
|
|
209
|
-
|
|
241
|
+
const b = this.#shadowBounds
|
|
242
|
+
return { ...super.jsonOptions(), ...(this.castShadow ? { castShadow: true } : {}),
|
|
243
|
+
...(b ? { shadowBounds: { min: Array.from(b.min), max: Array.from(b.max) } } : {}) }
|
|
210
244
|
}
|
|
211
245
|
}
|
|
212
246
|
|
package/src/shadow.js
CHANGED
|
@@ -23,9 +23,10 @@ export const SHADOW_MAP_SIZE = 2048
|
|
|
23
23
|
* @param {Box3[]} casters world bounds
|
|
24
24
|
* @param {Box3[]} receivers world bounds
|
|
25
25
|
* @param {number} [size] map size in texels
|
|
26
|
+
* @param {Box3 | null} [area] the light's shadowBounds: the map covers this across, whatever casts
|
|
26
27
|
* @returns {ShadowFit | null}
|
|
27
28
|
*/
|
|
28
|
-
export function fitShadow(direction, casters, receivers, size = SHADOW_MAP_SIZE) {
|
|
29
|
+
export function fitShadow(direction, casters, receivers, size = SHADOW_MAP_SIZE, area = null) {
|
|
29
30
|
const boxes = casters.filter((b) => !b.isEmpty())
|
|
30
31
|
if (!boxes.length) return null
|
|
31
32
|
const d = normalized(direction)
|
|
@@ -50,7 +51,10 @@ export function fitShadow(direction, casters, receivers, size = SHADOW_MAP_SIZE)
|
|
|
50
51
|
if (z > z1) z1 = z
|
|
51
52
|
}
|
|
52
53
|
}
|
|
53
|
-
|
|
54
|
+
// across the light's view: the area it was given, or else the casters; in depth, everything that
|
|
55
|
+
// casts - so a caster outside the area, between it and the light, still shadows it
|
|
56
|
+
if (area) corners(area, true)
|
|
57
|
+
for (const b of boxes) corners(b, !area)
|
|
54
58
|
// receivers only stretch the depth (view z is negative in front, so the far end is the minimum)
|
|
55
59
|
for (const b of receivers) if (!b.isEmpty()) corners(b, false)
|
|
56
60
|
|
|
@@ -58,10 +62,29 @@ export function fitShadow(direction, casters, receivers, size = SHADOW_MAP_SIZE)
|
|
|
58
62
|
const margin = Math.max(x1 - x0, y1 - y0, 1e-3) * 0.02
|
|
59
63
|
x0 -= margin; x1 += margin; y0 -= margin; y1 += margin
|
|
60
64
|
const depthMargin = Math.max(z1 - z0, 1e-3) * 0.01
|
|
61
|
-
|
|
65
|
+
z0 -= depthMargin; z1 += depthMargin
|
|
66
|
+
|
|
67
|
+
// steady while things move: the map grows and shrinks in small steps rather than with every change
|
|
68
|
+
// in the casters' extent, and its edges sit on the texel grid, so a shadow that isn't moving falls
|
|
69
|
+
// on the same texels every frame, instead of shimmering as the fit slides about under it
|
|
70
|
+
const w = steps(x1 - x0), h = steps(y1 - y0)
|
|
71
|
+
const tx = w / size, ty = h / size
|
|
72
|
+
x0 = Math.floor((x0 + x1 - w) / 2 / tx) * tx
|
|
73
|
+
y0 = Math.floor((y0 + y1 - h) / 2 / ty) * ty
|
|
74
|
+
// the depth range too, which sets what each stored depth means
|
|
75
|
+
const dz = steps(z1 - z0) / 64
|
|
76
|
+
z0 = Math.floor(z0 / dz) * dz
|
|
77
|
+
z1 = Math.ceil(z1 / dz) * dz
|
|
78
|
+
|
|
79
|
+
const projection = mat4.ortho(mat4.create(), x0, x0 + w, y0, y0 + h, -z1, -z0)
|
|
62
80
|
const viewProjection = new Mat4()
|
|
63
81
|
mat4.multiply(viewProjection, projection, view)
|
|
64
|
-
return { viewProjection, texelSize: Math.max(
|
|
82
|
+
return { viewProjection, texelSize: Math.max(w, h) / size }
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** A length rounded up to the next of a ladder of sizes, sixteen to each doubling. @param {number} v */
|
|
86
|
+
function steps(v) {
|
|
87
|
+
return 2 ** (Math.ceil(Math.log2(Math.max(v, 1e-3)) * 16) / 16)
|
|
65
88
|
}
|
|
66
89
|
|
|
67
90
|
/** Spot shadows' map: four tiles of half its size, one a light. */
|
package/src/types.js
CHANGED
|
@@ -9,6 +9,12 @@
|
|
|
9
9
|
|
|
10
10
|
/** A vector as accepted on input: [x, y, z], { x, y, z } or a Vec3. @typedef {Vec3Like | XYZ} Vec3Input */
|
|
11
11
|
|
|
12
|
+
/** A quaternion as an object literal - as physics engines such as Rapier return them. @typedef {{ x: number, y: number, z: number, w: number }} XYZW */
|
|
13
|
+
|
|
14
|
+
/** A rotation quaternion as accepted on input: [x, y, z, w], { x, y, z, w } or a Quat. @typedef {ArrayLike<number> | XYZW} QuatInput */
|
|
15
|
+
|
|
16
|
+
/** 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 */
|
|
17
|
+
|
|
12
18
|
/**
|
|
13
19
|
* A colour as accepted on input, in sRGB: '#ff8800', '#f80', 0xff8800, 'rgb(255 136 0)',
|
|
14
20
|
* 'hsl(32 100% 50%)', a CSS colour name, or [r, g, b] with components 0-1.
|
package/src/value/Color.js
CHANGED
|
@@ -16,6 +16,11 @@ const HELP = "use '#rrggbb', a CSS name, 'rgb(...)', 'hsl(...)', 0xrrggbb or [r,
|
|
|
16
16
|
* An RGB colour. Input is sRGB, as everywhere in phoria2; the components are stored **linear**,
|
|
17
17
|
* ready for lighting maths. `toJSON()` gives sRGB '#rrggbb' back.
|
|
18
18
|
*/
|
|
19
|
+
// colour strings already worked out, as linear rgb - emptied when full, as a palette never fills it
|
|
20
|
+
/** @type {Map<string, number[]>} */
|
|
21
|
+
const PARSED = new Map()
|
|
22
|
+
const PARSED_MAX = 1024
|
|
23
|
+
|
|
19
24
|
export class Color extends Float32Array {
|
|
20
25
|
static get [Symbol.species]() { return Float32Array }
|
|
21
26
|
|
|
@@ -154,6 +159,22 @@ export class Color extends Float32Array {
|
|
|
154
159
|
|
|
155
160
|
/** @param {string} s @param {string} owner */
|
|
156
161
|
#setString(s, owner) {
|
|
162
|
+
// the same few strings come again and again - an instance's colour set every frame - so each is
|
|
163
|
+
// worked out once
|
|
164
|
+
const known = PARSED.get(s)
|
|
165
|
+
if (known) {
|
|
166
|
+
this[0] = known[0]
|
|
167
|
+
this[1] = known[1]
|
|
168
|
+
this[2] = known[2]
|
|
169
|
+
return
|
|
170
|
+
}
|
|
171
|
+
this.#parse(s, owner)
|
|
172
|
+
if (PARSED.size >= PARSED_MAX) PARSED.clear()
|
|
173
|
+
PARSED.set(s, [this[0], this[1], this[2]])
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** @param {string} s @param {string} owner */
|
|
177
|
+
#parse(s, owner) {
|
|
157
178
|
const lower = s.toLowerCase()
|
|
158
179
|
if (lower.startsWith('#')) {
|
|
159
180
|
let hex = lower.slice(1)
|
package/src/value/Quat.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import * as quat from '../math/quat.js'
|
|
2
2
|
import { describe, fail } from '../validate.js'
|
|
3
3
|
|
|
4
|
+
/** @import { QuatInput } from '../types.js' */
|
|
5
|
+
|
|
4
6
|
/**
|
|
5
7
|
* A rotation quaternion [x, y, z, w]: a Float32Array(4) with accessors and chainable methods.
|
|
6
8
|
* A node's quaternion and its Euler `rotation` keep each other in sync through these setters;
|
|
@@ -61,13 +63,20 @@ export class Quat extends Float32Array {
|
|
|
61
63
|
return this.changed()
|
|
62
64
|
}
|
|
63
65
|
|
|
64
|
-
/** @param {
|
|
66
|
+
/** @param {QuatInput} q [x, y, z, w], or { x, y, z, w } as physics engines give them @param {string} [owner] @returns {this} */
|
|
65
67
|
copy(q, owner = 'Quat') {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
68
|
+
const o = /** @type {any} */ (q)
|
|
69
|
+
if (o && typeof o === 'object' && o.length >= 4) {
|
|
70
|
+
this[0] = o[0]
|
|
71
|
+
this[1] = o[1]
|
|
72
|
+
this[2] = o[2]
|
|
73
|
+
this[3] = o[3]
|
|
74
|
+
} else if (o && typeof o === 'object' && 'x' in o && 'y' in o && 'z' in o && 'w' in o) {
|
|
75
|
+
this[0] = o.x
|
|
76
|
+
this[1] = o.y
|
|
77
|
+
this[2] = o.z
|
|
78
|
+
this[3] = o.w
|
|
79
|
+
} else fail(owner, `expected a quaternion [x, y, z, w] or { x, y, z, w }, got ${describe(q)}`)
|
|
71
80
|
return this.changed()
|
|
72
81
|
}
|
|
73
82
|
|
package/src/watch.js
CHANGED
|
@@ -123,7 +123,7 @@ export class ChangeWatch {
|
|
|
123
123
|
this.#push((node.handlersFor('click').length ? 1 : 0) | (node.handlersFor('hover').length ? 2 : 0) | (node.tooltip ? 4 : 0) | (node.onDrag ? 8 : 0) | (node.highlightWhole ? 16 : 0))
|
|
124
124
|
// what hovering tints it with - a different colour shows straight away on what is hovered
|
|
125
125
|
if (node.highlight) this.#pushAll(node.highlight)
|
|
126
|
-
else this.#push(-4)
|
|
126
|
+
else this.#push(node.highlight === false ? -5 : -4)
|
|
127
127
|
const outline = node.outline
|
|
128
128
|
if (outline) {
|
|
129
129
|
this.#pushAll(outline.color)
|
|
@@ -208,6 +208,7 @@ export class ChangeWatch {
|
|
|
208
208
|
this.#push(label.priority)
|
|
209
209
|
this.#push(label.occlude ? 1 : 0)
|
|
210
210
|
this.#push(label.leader ? 1 : 0)
|
|
211
|
+
this.#push(label.minScale)
|
|
211
212
|
}
|
|
212
213
|
|
|
213
214
|
/** @param {any} light */
|
|
@@ -218,6 +219,10 @@ export class ChangeWatch {
|
|
|
218
219
|
if (light.direction) this.#pushAll(light.direction)
|
|
219
220
|
if (light.target) this.#pushAll(light.target)
|
|
220
221
|
if (light.castShadow) this.#push(1)
|
|
222
|
+
if (light.shadowBounds) {
|
|
223
|
+
this.#pushAll(light.shadowBounds.min)
|
|
224
|
+
this.#pushAll(light.shadowBounds.max)
|
|
225
|
+
}
|
|
221
226
|
if ('range' in light) this.#push(light.range)
|
|
222
227
|
if ('angle' in light) {
|
|
223
228
|
this.#push(light.angle)
|