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.
- package/README.md +70 -13
- package/dist/bloom.js +4 -4
- package/dist/bloom.js.map +1 -1
- package/dist/charts.js +538 -508
- package/dist/charts.js.map +1 -1
- package/dist/core.js +4270 -3652
- package/dist/core.js.map +1 -1
- package/dist/edit.js +65 -65
- package/dist/edit.js.map +1 -1
- package/dist/explain.js +6 -2
- package/dist/explain.js.map +1 -1
- package/dist/gltf.js +116 -116
- package/dist/gltf.js.map +1 -1
- package/dist/keyboard.js +188 -0
- package/dist/keyboard.js.map +1 -0
- package/dist/motion.js +35 -35
- package/dist/motion.js.map +1 -1
- package/dist/particles.js +188 -115
- package/dist/particles.js.map +1 -1
- package/dist/passes.js +29 -29
- package/dist/passes.js.map +1 -1
- package/dist/phoria2.js +1 -1
- package/dist/picking.js +32 -16
- package/dist/picking.js.map +1 -1
- package/dist/snapshot.js +1 -1
- package/dist/tooltip.js +1 -1
- package/dist/types/OrbitControl.d.ts +6 -2
- package/dist/types/Raycaster.d.ts +10 -0
- package/dist/types/View.d.ts +70 -1
- package/dist/types/area.d.ts +18 -0
- 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 +24 -2
- package/dist/types/index.d.ts +2 -0
- package/dist/types/keyboard.d.ts +48 -0
- package/dist/types/labels.d.ts +4 -0
- package/dist/types/material/Material.d.ts +15 -0
- package/dist/types/material/Texture.d.ts +18 -2
- package/dist/types/particles/Emitter.d.ts +39 -2
- package/dist/types/scene/Node.d.ts +19 -14
- package/dist/types/scene/drawables.d.ts +54 -15
- 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/dist/types/webgl/resources.d.ts +12 -2
- package/dist/types/webgl/shaders.d.ts +7 -2
- package/llms-full.txt +106 -10
- package/package.json +4 -1
- package/schema/scene.schema.json +67 -1
- package/src/OrbitControl.js +7 -3
- package/src/Raycaster.js +58 -0
- package/src/View.js +220 -41
- package/src/area.js +131 -0
- package/src/charts/Chart.js +20 -2
- package/src/charts/marks.js +29 -2
- package/src/explain.js +6 -2
- package/src/frame.js +29 -9
- package/src/geometry/Geometry.js +32 -4
- package/src/index.js +1 -0
- package/src/keyboard.js +298 -0
- package/src/labels.js +4 -1
- package/src/material/Material.js +9 -2
- package/src/material/Texture.js +55 -2
- package/src/particles/Emitter.js +121 -10
- 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 +115 -15
- 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 +14 -2
- package/src/webgl/WebGLRenderer.js +36 -5
- package/src/webgl/picking.js +22 -1
- package/src/webgl/resources.js +168 -14
- package/src/webgl/shaders.js +110 -11
package/src/particles/Emitter.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// A particle system as one Points node: particles are born at the emitter's origin, move under
|
|
2
2
|
// velocity, gravity and drag, fade through a list of colours and die. The simulation runs in the
|
|
3
3
|
// emitter's own update handler, so it follows the scene clock (frame-rate independent) and its
|
|
4
|
-
// arrays go to the GPU as one buffer refill per frame.
|
|
4
|
+
// arrays go to the GPU as one buffer refill per frame. Particles that grow, fade or turn give the
|
|
5
|
+
// geometry per-point sizes, opacities or angles, and are drawn as squares.
|
|
5
6
|
import * as mat4 from '../math/mat4.js'
|
|
6
7
|
import { Geometry } from '../geometry/Geometry.js'
|
|
7
8
|
import { Material } from '../material/Material.js'
|
|
@@ -48,17 +49,32 @@ import { checkNumber, describe, fail } from '../validate.js'
|
|
|
48
49
|
* frustumCulled?: boolean,
|
|
49
50
|
* renderOrder?: number,
|
|
50
51
|
* onParticle?: ((p: Particle) => boolean | void) | null,
|
|
51
|
-
*
|
|
52
|
+
* sort?: boolean,
|
|
53
|
+
* } & LookOptions} EmitterOptions
|
|
52
54
|
* max: most live particles at once (default 1000). rate: particles born per second (default 100).
|
|
53
55
|
* gravity: acceleration, e.g. [0, -9.8, 0] (default none). drag: share of velocity lost per second
|
|
54
56
|
* (default 0). follow: a node whose position becomes the origin each frame - a trail that stays
|
|
55
57
|
* behind it. onParticle: called for every live particle each frame after it moves; return false to
|
|
56
|
-
* remove it (a particle hitting something).
|
|
58
|
+
* remove it (a particle hitting something). sort: draw furthest first, for soft particles with
|
|
59
|
+
* blending 'normal' (smoke) that would otherwise layer wrongly.
|
|
60
|
+
*/
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* How particles look over their lives, beyond colour.
|
|
64
|
+
* @typedef {object} LookOptions
|
|
65
|
+
* @property {number | number[]} [size] a multiple of the material's size, or several each particle goes
|
|
66
|
+
* through over its life - [0.5, 3] for smoke that billows out (default 1)
|
|
67
|
+
* @property {number} [sizeSpread] each particle's size varies by up to this share either way, 0-1 (default 0)
|
|
68
|
+
* @property {number | number[]} [opacity] 0-1, or several each particle goes through - [0, 1, 0] fades in
|
|
69
|
+
* and out (default 1)
|
|
70
|
+
* @property {number | [number, number]} [spin] radians per second: a number turns each particle up to that
|
|
71
|
+
* fast either way, [min, max] a range. Spinning particles start at a random angle (default 0)
|
|
57
72
|
*/
|
|
58
73
|
|
|
59
74
|
const SPAWN_OPTIONS = ['origin', 'originSpread', 'velocity', 'velocitySpread', 'lifetime', 'color']
|
|
60
|
-
const
|
|
61
|
-
|
|
75
|
+
const LOOK_OPTIONS = ['size', 'sizeSpread', 'opacity', 'spin']
|
|
76
|
+
const BEHAVIOUR_OPTIONS = ['rate', 'gravity', 'drag', 'follow', 'onParticle', ...LOOK_OPTIONS]
|
|
77
|
+
const OPTIONS = [...Points.OPTIONS, ...SPAWN_OPTIONS, 'max', ...BEHAVIOUR_OPTIONS]
|
|
62
78
|
const HINTS = {
|
|
63
79
|
count: "use 'max' for the most particles alive at once, 'rate' for how many are born per second",
|
|
64
80
|
lifetimeRnd: "give 'lifetime' a [min, max] range",
|
|
@@ -111,6 +127,13 @@ export class Emitter extends Points {
|
|
|
111
127
|
/** @type {Map<string, number>} */ #paletteIds = new Map()
|
|
112
128
|
/** @type {Uint8Array} */ #palette
|
|
113
129
|
#spawnPalette = 0
|
|
130
|
+
// the looks: stops over each particle's life, and each one's own size factor and turn rate
|
|
131
|
+
/** @type {number[]} */ #size = [1]
|
|
132
|
+
#sizeSpread = 0
|
|
133
|
+
/** @type {number[]} */ #opacity = [1]
|
|
134
|
+
/** @type {[number, number]} */ #spin = [0, 0]
|
|
135
|
+
/** @type {Float32Array | null} */ #sizeFactors = null
|
|
136
|
+
/** @type {Float32Array | null} */ #spins = null
|
|
114
137
|
/** Last frame's origin, so a moving source leaves an even trail rather than clumps. */
|
|
115
138
|
#lastOrigin = /** @type {Vec3 | null} */ (null)
|
|
116
139
|
#particle = /** @type {Particle} */ ({ position: new Vec3(), velocity: new Vec3(), age: 0, lifetime: 0 })
|
|
@@ -144,12 +167,12 @@ export class Emitter extends Points {
|
|
|
144
167
|
/**
|
|
145
168
|
* Change several settings at once - a preset, say. Vectors are copied into the emitter's own,
|
|
146
169
|
* so anything bound to `emitter.gravity` and friends stays bound. Live particles carry on.
|
|
147
|
-
* @param {SpawnOptions & { rate?: number, gravity?: Vec3Input, drag?: number, follow?: Node | null,
|
|
170
|
+
* @param {SpawnOptions & LookOptions & { rate?: number, gravity?: Vec3Input, drag?: number, follow?: Node | null,
|
|
148
171
|
* onParticle?: ((p: Particle) => boolean | void) | null }} options
|
|
149
172
|
* @returns {this}
|
|
150
173
|
*/
|
|
151
174
|
set(options) {
|
|
152
|
-
const known = [...SPAWN_OPTIONS,
|
|
175
|
+
const known = [...SPAWN_OPTIONS, ...BEHAVIOUR_OPTIONS]
|
|
153
176
|
for (const key of Object.keys(options ?? {})) {
|
|
154
177
|
if (!known.includes(key)) fail('Emitter', `set() doesn't take '${key}' - it takes ${known.join(', ')}`)
|
|
155
178
|
}
|
|
@@ -169,6 +192,22 @@ export class Emitter extends Points {
|
|
|
169
192
|
/** @returns {[number, number, number][]} */
|
|
170
193
|
get color() { return /** @type {[number, number, number][]} */ (this.#palettes[0].map((s) => [...s])) }
|
|
171
194
|
|
|
195
|
+
/** Each particle's size over its life, as multiples of the material's. @param {number | number[]} v */
|
|
196
|
+
set size(v) { this.#setLooks({ size: v }) }
|
|
197
|
+
/** @returns {number[]} */
|
|
198
|
+
get size() { return [...this.#size] }
|
|
199
|
+
/** How much each particle's size varies, 0-1 either way. @param {number} v */
|
|
200
|
+
set sizeSpread(v) { this.#setLooks({ sizeSpread: v }) }
|
|
201
|
+
get sizeSpread() { return this.#sizeSpread }
|
|
202
|
+
/** Each particle's opacity over its life. @param {number | number[]} v */
|
|
203
|
+
set opacity(v) { this.#setLooks({ opacity: v }) }
|
|
204
|
+
/** @returns {number[]} */
|
|
205
|
+
get opacity() { return [...this.#opacity] }
|
|
206
|
+
/** How fast particles turn, radians per second, as a [min, max] range. @param {number | [number, number]} v */
|
|
207
|
+
set spin(v) { this.#setLooks({ spin: v }) }
|
|
208
|
+
/** @returns {[number, number]} */
|
|
209
|
+
get spin() { return [...this.#spin] }
|
|
210
|
+
|
|
172
211
|
/**
|
|
173
212
|
* Birth `count` particles at once - explosions, splashes. Options override the emitter's own
|
|
174
213
|
* spawn settings for this burst only.
|
|
@@ -211,6 +250,7 @@ export class Emitter extends Points {
|
|
|
211
250
|
const v = this.#velocities, ages = this.#ages, lives = this.#lifetimes
|
|
212
251
|
const g = this.gravity, keep = Math.exp(-this.drag * dt)
|
|
213
252
|
const hook = this.onParticle, particle = this.#particle
|
|
253
|
+
const angles = this.geometry.angles, spins = this.#spins
|
|
214
254
|
|
|
215
255
|
for (let i = 0; i < this.#alive;) {
|
|
216
256
|
const age = ages[i] += dt
|
|
@@ -223,6 +263,7 @@ export class Emitter extends Points {
|
|
|
223
263
|
v[i3 + k] = (v[i3 + k] + g[k] * dt) * keep
|
|
224
264
|
p[i3 + k] += v[i3 + k] * dt
|
|
225
265
|
}
|
|
266
|
+
if (angles && spins) angles[i] += spins[i] * dt
|
|
226
267
|
this.#paint(i, age / lives[i])
|
|
227
268
|
if (hook) {
|
|
228
269
|
particle.position.set(p[i3], p[i3 + 1], p[i3 + 2])
|
|
@@ -256,7 +297,7 @@ export class Emitter extends Points {
|
|
|
256
297
|
const rest = super.cloneOptions()
|
|
257
298
|
delete rest.geometry
|
|
258
299
|
return { ...rest, ...this.#saveSpawn(), max: this.#max, rate: this.rate, gravity: this.gravity.clone(),
|
|
259
|
-
drag: this.drag, follow: this.follow, onParticle: this.onParticle, color: this.color }
|
|
300
|
+
drag: this.drag, follow: this.follow, onParticle: this.onParticle, color: this.color, ...this.#looks() }
|
|
260
301
|
}
|
|
261
302
|
|
|
262
303
|
/** @internal @returns {Record<string, unknown>} */
|
|
@@ -266,7 +307,8 @@ export class Emitter extends Points {
|
|
|
266
307
|
return { ...rest, max: this.#max, rate: this.rate, gravity: this.gravity.toArray(), drag: this.drag,
|
|
267
308
|
origin: this.origin.toArray(), originSpread: this.originSpread, velocity: this.velocity.toArray(),
|
|
268
309
|
velocitySpread: typeof this.velocitySpread === 'number' ? this.velocitySpread : this.velocitySpread.toArray(),
|
|
269
|
-
lifetime: [...this.lifetime], color: this.#palettes[0].map((s) => new Color().setSRGB(s[0], s[1], s[2]).toJSON())
|
|
310
|
+
lifetime: [...this.lifetime], color: this.#palettes[0].map((s) => new Color().setSRGB(s[0], s[1], s[2]).toJSON()),
|
|
311
|
+
...this.#looks() }
|
|
270
312
|
}
|
|
271
313
|
|
|
272
314
|
// birth one particle somewhere between a and b, already `head` seconds into its life
|
|
@@ -287,6 +329,12 @@ export class Emitter extends Points {
|
|
|
287
329
|
this.#lifetimes[i] = min + Math.random() * (max - min)
|
|
288
330
|
this.#ages[i] = head
|
|
289
331
|
this.#palette[i] = this.#spawnPalette
|
|
332
|
+
if (this.#sizeFactors) this.#sizeFactors[i] = 1 + (Math.random() * 2 - 1) * this.#sizeSpread
|
|
333
|
+
const angles = this.geometry.angles, spins = this.#spins
|
|
334
|
+
if (angles && spins) {
|
|
335
|
+
spins[i] = this.#spin[0] + Math.random() * (this.#spin[1] - this.#spin[0])
|
|
336
|
+
angles[i] = Math.random() * Math.PI * 2 + spins[i] * head
|
|
337
|
+
}
|
|
290
338
|
this.#paint(i, head / this.#lifetimes[i])
|
|
291
339
|
}
|
|
292
340
|
|
|
@@ -302,10 +350,16 @@ export class Emitter extends Points {
|
|
|
302
350
|
this.#ages[i] = this.#ages[last]
|
|
303
351
|
this.#lifetimes[i] = this.#lifetimes[last]
|
|
304
352
|
this.#palette[i] = this.#palette[last]
|
|
353
|
+
for (const a of [this.geometry.sizes, this.geometry.opacities, this.geometry.angles, this.#sizeFactors, this.#spins]) {
|
|
354
|
+
if (a) a[i] = a[last]
|
|
355
|
+
}
|
|
305
356
|
}
|
|
306
357
|
|
|
307
358
|
/** @param {number} i @param {number} t 0-1 through its life */
|
|
308
359
|
#paint(i, t) {
|
|
360
|
+
const { sizes, opacities } = this.geometry
|
|
361
|
+
if (sizes) sizes[i] = along(this.#size, t) * (this.#sizeFactors ? this.#sizeFactors[i] : 1)
|
|
362
|
+
if (opacities) opacities[i] = along(this.#opacity, t)
|
|
309
363
|
const c = /** @type {Float32Array} */ (this.geometry.colors), stops = this.#palettes[this.#palette[i]]
|
|
310
364
|
if (stops.length === 1) {
|
|
311
365
|
c.set(stops[0], i * 3)
|
|
@@ -349,8 +403,9 @@ export class Emitter extends Points {
|
|
|
349
403
|
if (o.color !== undefined) this.#palettes[0] = colorStops(o.color)
|
|
350
404
|
}
|
|
351
405
|
|
|
352
|
-
/** @param {{ rate?: number, gravity?: Vec3Input, drag?: number, follow?: Node | null, onParticle?: ((p: Particle) => boolean | void) | null }} o */
|
|
406
|
+
/** @param {LookOptions & { rate?: number, gravity?: Vec3Input, drag?: number, follow?: Node | null, onParticle?: ((p: Particle) => boolean | void) | null }} o */
|
|
353
407
|
#setBehaviour(o) {
|
|
408
|
+
this.#setLooks(o)
|
|
354
409
|
const owner = 'Emitter'
|
|
355
410
|
if (o.rate !== undefined) this.rate = Math.max(0, checkNumber(owner, 'rate', o.rate))
|
|
356
411
|
if (o.gravity !== undefined) this.gravity.copy(o.gravity, owner)
|
|
@@ -359,6 +414,31 @@ export class Emitter extends Points {
|
|
|
359
414
|
if (o.onParticle !== undefined) this.onParticle = o.onParticle
|
|
360
415
|
}
|
|
361
416
|
|
|
417
|
+
// Sizes, opacities and angles go on the geometry when first wanted (the points are then drawn as
|
|
418
|
+
// squares) and stay; live particles get the plain values until they are next painted.
|
|
419
|
+
/** @param {LookOptions} o */
|
|
420
|
+
#setLooks(o) {
|
|
421
|
+
const owner = 'Emitter', g = this.geometry, max = this.#max
|
|
422
|
+
if (o.size !== undefined) this.#size = lookStops('size', o.size, Infinity)
|
|
423
|
+
if (o.sizeSpread !== undefined) this.#sizeSpread = Math.min(Math.max(checkNumber(owner, 'sizeSpread', o.sizeSpread), 0), 1)
|
|
424
|
+
if (o.opacity !== undefined) this.#opacity = lookStops('opacity', o.opacity, 1)
|
|
425
|
+
if (o.spin !== undefined) this.#spin = spinRange(o.spin)
|
|
426
|
+
const plain = (/** @type {number[]} */ stops) => stops.length === 1 && stops[0] === 1
|
|
427
|
+
if (!g.sizes && (!plain(this.#size) || this.#sizeSpread)) g.sizes = new Float32Array(max).fill(1)
|
|
428
|
+
if (!this.#sizeFactors && this.#sizeSpread) this.#sizeFactors = new Float32Array(max).fill(1)
|
|
429
|
+
if (!g.opacities && !plain(this.#opacity)) g.opacities = new Float32Array(max).fill(1)
|
|
430
|
+
if (!g.angles && (this.#spin[0] || this.#spin[1])) {
|
|
431
|
+
g.angles = new Float32Array(max)
|
|
432
|
+
this.#spins = new Float32Array(max)
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/** The looks, as options. @returns {LookOptions} */
|
|
437
|
+
#looks() {
|
|
438
|
+
return { size: unstop(this.#size), sizeSpread: this.#sizeSpread, opacity: unstop(this.#opacity),
|
|
439
|
+
spin: /** @type {[number, number]} */ ([...this.#spin]) }
|
|
440
|
+
}
|
|
441
|
+
|
|
362
442
|
/** The spawn settings other than colour, as options. @returns {SpawnOptions} */
|
|
363
443
|
#saveSpawn() {
|
|
364
444
|
return { origin: this.origin.clone(), originSpread: this.originSpread, velocity: this.velocity.clone(),
|
|
@@ -397,6 +477,37 @@ function lifetimeRange(v) {
|
|
|
397
477
|
return [Math.min(range[0], range[1]), Math.max(range[0], range[1])]
|
|
398
478
|
}
|
|
399
479
|
|
|
480
|
+
/** @param {string} name @param {number | number[]} v @param {number} most @returns {number[]} */
|
|
481
|
+
function lookStops(name, v, most) {
|
|
482
|
+
const list = Array.isArray(v) ? v : [v]
|
|
483
|
+
if (!list.length || !list.every((n) => typeof n === 'number' && Number.isFinite(n) && n >= 0)) {
|
|
484
|
+
fail('Emitter', `'${name}' must be a number of 0 or more, or a list of them a particle goes through, got ${describe(v)}`)
|
|
485
|
+
}
|
|
486
|
+
return list.map((n) => Math.min(n, most))
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/** One stop as a number, several as a list. @param {number[]} stops @returns {number | number[]} */
|
|
490
|
+
function unstop(stops) {
|
|
491
|
+
return stops.length === 1 ? stops[0] : [...stops]
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/** The value t (0-1) of the way through evenly spaced stops. @param {number[]} stops @param {number} t */
|
|
495
|
+
function along(stops, t) {
|
|
496
|
+
if (stops.length === 1) return stops[0]
|
|
497
|
+
const x = Math.min(Math.max(t, 0), 1) * (stops.length - 1)
|
|
498
|
+
const s = Math.min(Math.floor(x), stops.length - 2)
|
|
499
|
+
return stops[s] + (stops[s + 1] - stops[s]) * (x - s)
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/** @param {number | [number, number]} v @returns {[number, number]} */
|
|
503
|
+
function spinRange(v) {
|
|
504
|
+
const range = typeof v === 'number' ? [-Math.abs(v), Math.abs(v)] : v
|
|
505
|
+
if (!Array.isArray(range) || range.length !== 2 || !range.every((n) => typeof n === 'number' && Number.isFinite(n))) {
|
|
506
|
+
fail('Emitter', `'spin' must be radians per second, or a [min, max] range, got ${describe(v)}`)
|
|
507
|
+
}
|
|
508
|
+
return [Math.min(range[0], range[1]), Math.max(range[0], range[1])]
|
|
509
|
+
}
|
|
510
|
+
|
|
400
511
|
/** @param {ColorInput | ColorInput[]} v @returns {number[][]} */
|
|
401
512
|
function colorStops(v) {
|
|
402
513
|
// a single [r, g, b] array is one colour, not three
|
package/src/scene/Node.js
CHANGED
|
@@ -10,7 +10,7 @@ import { Vec3 } from '../value/Vec3.js'
|
|
|
10
10
|
import { checkOptions, describe, fail } from '../validate.js'
|
|
11
11
|
import { Clip } from '../clip.js'
|
|
12
12
|
|
|
13
|
-
/** @import { ColorInput, Vec3Input } from '../types.js' */
|
|
13
|
+
/** @import { ColorInput, QuatInput, Vec3Input } from '../types.js' */
|
|
14
14
|
/** @import { Hit } from '../Raycaster.js' */
|
|
15
15
|
/** @import { Camera } from './cameras.js' */
|
|
16
16
|
/** @import { ClipOptions } from '../clip.js' */
|
|
@@ -44,7 +44,7 @@ import { Clip } from '../clip.js'
|
|
|
44
44
|
* @property {string} [name]
|
|
45
45
|
* @property {Vec3Input} [position]
|
|
46
46
|
* @property {Vec3Input | Euler} [rotation] Euler angles in radians, XYZ order
|
|
47
|
-
* @property {
|
|
47
|
+
* @property {QuatInput} [quaternion] [x, y, z, w] or { x, y, z, w } - instead of rotation
|
|
48
48
|
* @property {Vec3Input | number} [scale] a number scales uniformly
|
|
49
49
|
* @property {boolean} [visible]
|
|
50
50
|
* @property {boolean} [pickable] include in view.pick() (default true)
|
|
@@ -54,7 +54,8 @@ import { Clip } from '../clip.js'
|
|
|
54
54
|
* @property {UpdateHandler | UpdateHandler[]} [onUpdate]
|
|
55
55
|
* @property {((hit: Hit) => any) | null} [tooltip] tooltip for anything picked in this subtree; nearer nodes win, and
|
|
56
56
|
* all win over the View's
|
|
57
|
-
* @property {ColorInput | null} [highlight] hover tint for this subtree, over the View's
|
|
57
|
+
* @property {ColorInput | null | false} [highlight] hover tint for this subtree, over the View's; false for none - a
|
|
58
|
+
* floor or wall left plain under a View that tints everything else (null: the View's)
|
|
58
59
|
* @property {boolean} [highlightWhole] hovering any part of this subtree tints all of it, as one thing - a
|
|
59
60
|
* piece of furniture, a machine - rather than just the part under the pointer (default false)
|
|
60
61
|
* @property {((hit: Hit | null) => void) | null} [onHover] called as the thing under the pointer in this subtree changes
|
|
@@ -136,7 +137,7 @@ export class Node {
|
|
|
136
137
|
onDrag = null
|
|
137
138
|
/** @type {Outline | null} */
|
|
138
139
|
#outline = null
|
|
139
|
-
/** @type {Color | null} */
|
|
140
|
+
/** @type {Color | null | false} */
|
|
140
141
|
#highlight = null
|
|
141
142
|
/** Hovering any part of this subtree tints all of it, not just the part under the pointer. */
|
|
142
143
|
highlightWhole = false
|
|
@@ -164,7 +165,7 @@ export class Node {
|
|
|
164
165
|
set rotation(v) { this.#rotation.copy(/** @type {any} */ (v), this.constructor.name) }
|
|
165
166
|
/** Kept in sync with rotation. Assigning copies, normalised. @returns {Quat} */
|
|
166
167
|
get quaternion() { return this.#quaternion }
|
|
167
|
-
/** @param {
|
|
168
|
+
/** @param {QuatInput} v */
|
|
168
169
|
set quaternion(v) { this.#quaternion.copy(v, this.constructor.name).normalize() }
|
|
169
170
|
|
|
170
171
|
/** @param {NodeOptions} [options] */
|
|
@@ -191,7 +192,7 @@ export class Node {
|
|
|
191
192
|
if (typeof fn !== 'function') fail(owner, `'${key}' must be a function, got ${describe(fn)}`)
|
|
192
193
|
this[key] = fn
|
|
193
194
|
}
|
|
194
|
-
if (options.highlight != null) this.#
|
|
195
|
+
if (options.highlight != null) this.#setHighlight(options.highlight, owner)
|
|
195
196
|
if (options.highlightWhole !== undefined) {
|
|
196
197
|
if (typeof options.highlightWhole !== 'boolean') fail(owner, `'highlightWhole' must be true or false, got ${describe(options.highlightWhole)}`)
|
|
197
198
|
this.highlightWhole = options.highlightWhole
|
|
@@ -201,14 +202,23 @@ export class Node {
|
|
|
201
202
|
if (options.children) for (const child of options.children) this.add(child)
|
|
202
203
|
}
|
|
203
204
|
|
|
204
|
-
/**
|
|
205
|
+
/**
|
|
206
|
+
* Hover tint for this subtree, over the View's: a colour, false for none, or null to use the View's.
|
|
207
|
+
* @returns {Color | null | false}
|
|
208
|
+
*/
|
|
205
209
|
get highlight() {
|
|
206
210
|
return this.#highlight
|
|
207
211
|
}
|
|
208
212
|
|
|
209
|
-
/** @param {ColorInput | null} value */
|
|
213
|
+
/** @param {ColorInput | null | false} value */
|
|
210
214
|
set highlight(value) {
|
|
211
|
-
this.#
|
|
215
|
+
this.#setHighlight(value, this.constructor.name)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** @param {ColorInput | null | false | true} value @param {string} owner */
|
|
219
|
+
#setHighlight(value, owner) {
|
|
220
|
+
if (value === true) fail(owner, "'highlight' is a colour, false for no tint, or null to use the View's - got true")
|
|
221
|
+
this.#highlight = value == null ? null : value === false ? false : Color.fromInput(value, owner)
|
|
212
222
|
}
|
|
213
223
|
|
|
214
224
|
/**
|
|
@@ -612,6 +622,7 @@ export class Node {
|
|
|
612
622
|
if (!this.pickable) json.pickable = false
|
|
613
623
|
if (!this.matrixAutoUpdate) json.matrixAutoUpdate = false
|
|
614
624
|
if (this.highlight) json.highlight = this.highlight.getStyle()
|
|
625
|
+
else if (this.highlight === false) json.highlight = false
|
|
615
626
|
if (this.highlightWhole) json.highlightWhole = true
|
|
616
627
|
if (this.#clip) json.clip = this.#clip.toJSON()
|
|
617
628
|
if (this.#outline) json.outline = { color: this.#outline.color.getStyle(), width: this.#outline.width }
|
package/src/scene/Scene.js
CHANGED
|
@@ -44,6 +44,8 @@ export class Scene extends Node {
|
|
|
44
44
|
environmentIntensity = 1
|
|
45
45
|
/** 0-1: how blurred a panorama background is drawn - a soft backdrop that doesn't compete. */
|
|
46
46
|
backgroundBlur = 0
|
|
47
|
+
/** @internal Set by the View drawing it, from its reducedMotion: charts' transitions and flows stand still. */
|
|
48
|
+
motionReduced = false
|
|
47
49
|
/** @type {Fog | null} */
|
|
48
50
|
#fog = null
|
|
49
51
|
|
package/src/scene/cameras.js
CHANGED
|
@@ -20,7 +20,10 @@ const tmp3 = new Float32Array(9)
|
|
|
20
20
|
export function aimAt(node, target) {
|
|
21
21
|
if (node.position.equals(target, 1e-9)) return
|
|
22
22
|
const dir = target.clone().sub(node.position).normalize()
|
|
23
|
-
|
|
23
|
+
// world up keeps the picture upright, and turns it smoothly with the view right up to straight up
|
|
24
|
+
// or down - the orbit stops a thousandth of a radian short. Only within a hair of exactly vertical
|
|
25
|
+
// (the 'top' preset's view), where up would no longer say which way the screen faces, is north up
|
|
26
|
+
const up = Math.hypot(dir[0], dir[2]) < 5e-4 ? [0, 0, -1] : UP
|
|
24
27
|
mat4.targetTo(tmp, node.position, target, up)
|
|
25
28
|
const q = quat.normalize(new Float32Array(4), quat.fromMat3(new Float32Array(4), mat3.fromMat4(tmp3, tmp)))
|
|
26
29
|
node.quaternion.copy(q)
|
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]
|
|
@@ -434,20 +466,44 @@ export class InstancedMesh extends Mesh {
|
|
|
434
466
|
/** Each vertex drawn as a square point sprite: star fields, particles, scatter plots. */
|
|
435
467
|
export class Points extends Drawable {
|
|
436
468
|
static type = 'points'
|
|
437
|
-
static get OPTIONS() { return [...DRAWABLE_OPTIONS, 'labels'] }
|
|
469
|
+
static get OPTIONS() { return [...DRAWABLE_OPTIONS, 'labels', 'sort'] }
|
|
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
|
|
473
|
+
/**
|
|
474
|
+
* Draw the points back to front, furthest first, each frame - for soft points that cover what is
|
|
475
|
+
* behind them (smoke, with blending 'normal'), which then layer the right way round. Drawn as squares.
|
|
476
|
+
*/
|
|
477
|
+
sort = false
|
|
441
478
|
|
|
442
|
-
/** @param {DrawableOptions & { labels?: InstanceLabels | null }} options */
|
|
479
|
+
/** @param {DrawableOptions & { labels?: InstanceLabels | InstanceLabels[] | null, sort?: boolean }} options */
|
|
443
480
|
constructor(options) {
|
|
444
481
|
super(options)
|
|
445
482
|
this.labels = instanceLabels(this.constructor.name, options?.labels)
|
|
483
|
+
if (options?.sort !== undefined) {
|
|
484
|
+
if (typeof options.sort !== 'boolean') fail(this.constructor.name, `'sort' is true or false, got ${describe(options.sort)}`)
|
|
485
|
+
this.sort = options.sort
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* @internal Drawn as squares, one a point, rather than as GL points: when the geometry gives each
|
|
491
|
+
* point its own size, opacity or angle, or the points are sorted. Squares can be any size and stay
|
|
492
|
+
* whole at the edge of the view.
|
|
493
|
+
*/
|
|
494
|
+
get drawsSprites() {
|
|
495
|
+
const g = this.geometry
|
|
496
|
+
return this.sort || !!(g.sizes || g.opacities || g.angles)
|
|
446
497
|
}
|
|
447
498
|
|
|
448
499
|
/** @internal @returns {Record<string, unknown>} */
|
|
449
500
|
cloneOptions() {
|
|
450
|
-
return { ...super.cloneOptions(), labels: this.labels }
|
|
501
|
+
return { ...super.cloneOptions(), labels: this.labels, sort: this.sort }
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/** @internal @returns {Record<string, unknown>} */
|
|
505
|
+
jsonOptions() {
|
|
506
|
+
return { ...super.jsonOptions(), ...(this.sort ? { sort: true } : {}) }
|
|
451
507
|
}
|
|
452
508
|
|
|
453
509
|
/** @returns {'points'} */
|
|
@@ -456,12 +512,52 @@ export class Points extends Drawable {
|
|
|
456
512
|
}
|
|
457
513
|
}
|
|
458
514
|
|
|
515
|
+
const ARROWS = ['none', 'start', 'end', 'both']
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* @typedef {DrawableOptions & { arrows?: 'none' | 'start' | 'end' | 'both', arrowSize?: number }} LinesOptions
|
|
519
|
+
*/
|
|
520
|
+
|
|
459
521
|
/**
|
|
460
522
|
* Line segments: indices (or consecutive vertices) in pairs. Width is in pixels and works on
|
|
461
|
-
* every platform. edges(geometry) makes a wireframe of any geometry.
|
|
523
|
+
* every platform. edges(geometry) makes a wireframe of any geometry. Segments each starting where
|
|
524
|
+
* the one before ended make a chain - a polyline - which dashes run on along and arrowheads finish.
|
|
462
525
|
*/
|
|
463
526
|
export class Lines extends Drawable {
|
|
464
527
|
static type = 'lines'
|
|
528
|
+
static get OPTIONS() { return [...DRAWABLE_OPTIONS, 'arrows', 'arrowSize'] }
|
|
529
|
+
|
|
530
|
+
/** Arrowheads at the ends of each chain: at its 'start', its 'end', 'both' or 'none'. @type {'none' | 'start' | 'end' | 'both'} */
|
|
531
|
+
arrows = 'none'
|
|
532
|
+
/** Arrowhead length in pixels; 0 sizes them from the material's width. */
|
|
533
|
+
arrowSize = 0
|
|
534
|
+
|
|
535
|
+
/** @param {LinesOptions} options */
|
|
536
|
+
constructor(options) {
|
|
537
|
+
super(options)
|
|
538
|
+
if (options?.arrows !== undefined) this.arrows = /** @type {any} */ (checkOneOf('Lines', 'arrows', options.arrows, ARROWS))
|
|
539
|
+
if (options?.arrowSize !== undefined) this.arrowSize = Math.max(0, checkNumber('Lines', 'arrowSize', options.arrowSize))
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/** @internal Which ends get arrowheads, for the shader: 1 the start, 2 the end. */
|
|
543
|
+
get arrowBits() {
|
|
544
|
+
return ARROWS.indexOf(this.arrows)
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/** @internal An arrowhead's length in pixels, before the pixel ratio. @param {number} width */
|
|
548
|
+
arrowPixels(width) {
|
|
549
|
+
return this.arrowSize || 6 + width * 3
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/** @internal @returns {Record<string, unknown>} */
|
|
553
|
+
cloneOptions() {
|
|
554
|
+
return { ...super.cloneOptions(), arrows: this.arrows, arrowSize: this.arrowSize }
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/** @internal @returns {Record<string, unknown>} */
|
|
558
|
+
jsonOptions() {
|
|
559
|
+
return { ...super.jsonOptions(), ...(this.arrows !== 'none' ? { arrows: this.arrows } : {}), ...(this.arrowSize ? { arrowSize: this.arrowSize } : {}) }
|
|
560
|
+
}
|
|
465
561
|
|
|
466
562
|
/** @returns {'lines'} */
|
|
467
563
|
get drawMode() {
|
|
@@ -505,6 +601,8 @@ export class Label extends Node {
|
|
|
505
601
|
leader = false
|
|
506
602
|
/** Set at the view's left or right edge, level with its point, or null for by its point. @type {'left' | 'right' | null} */
|
|
507
603
|
edge = null
|
|
604
|
+
/** Shown only when zoomed in this far: CSS pixels to one world unit at its point; 0 for always. */
|
|
605
|
+
minScale = 0
|
|
508
606
|
|
|
509
607
|
/** @param {LabelOptions} options */
|
|
510
608
|
constructor(options) {
|
|
@@ -529,7 +627,8 @@ export class Label extends Node {
|
|
|
529
627
|
/** @internal @returns {Record<string, unknown>} */
|
|
530
628
|
cloneOptions() {
|
|
531
629
|
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
|
|
630
|
+
offset: [...this.offset], priority: this.priority, overlap: this.overlap, occlude: this.occlude, leader: this.leader, edge: this.edge,
|
|
631
|
+
minScale: this.minScale }
|
|
533
632
|
}
|
|
534
633
|
|
|
535
634
|
/** @internal @returns {Record<string, unknown>} */
|
|
@@ -544,6 +643,7 @@ export class Label extends Node {
|
|
|
544
643
|
if (this.occlude) json.occlude = true
|
|
545
644
|
if (this.leader) json.leader = true
|
|
546
645
|
if (this.edge) json.edge = this.edge
|
|
646
|
+
if (this.minScale) json.minScale = this.minScale
|
|
547
647
|
return json
|
|
548
648
|
}
|
|
549
649
|
}
|