trid3d 0.1.0 → 0.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 +16 -9
- package/package.json +8 -3
- package/trid/export/capture.js +150 -0
- package/trid/export/obj.js +141 -0
- package/trid/index.d.ts +988 -0
- package/trid/index.js +10 -3
- package/trid/loaders/obj.js +18 -2
- package/trid/render/canvasRenderer.js +94 -61
- package/trid/render/draw.js +15 -4
- package/trid/render/shapes.js +285 -23
- package/trid/render/uvMapping.js +225 -0
- package/trid/render/webglRenderer.js +148 -54
- package/trid/scene.js +23 -0
- package/trid/tools/hoverHighlight.js +2 -1
- package/trid/tools/orbitCamera.js +18 -0
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { DEFAULT_FOV, DEFAULT_NEAR } from "../math/transform.js";
|
|
2
|
-
import { clear, edgeSegments } from "./draw.js";
|
|
2
|
+
import { clear, edgeSegments, wireEdges } from "./draw.js";
|
|
3
3
|
import { parseColor } from "./color.js";
|
|
4
|
-
import { DEFAULT_LIGHT, DEFAULT_AMBIENT } from "./canvasRenderer.js";
|
|
4
|
+
import { DEFAULT_LIGHT, DEFAULT_AMBIENT, materialsOf } from "./canvasRenderer.js";
|
|
5
|
+
import { uvsFor, uvTransform, mappingKey } from "./uvMapping.js";
|
|
5
6
|
import { newell, cornerNormals, creaseOf } from "./normals.js";
|
|
6
7
|
import { SURFACE_GLSL, buildEffects, effectsKey } from "./effects.js";
|
|
7
8
|
import { textureReady, textureSize, textureIsLive } from "../loaders/texture.js";
|
|
@@ -38,11 +39,12 @@ varying vec3 vNormal;
|
|
|
38
39
|
varying vec3 vWorld;
|
|
39
40
|
varying vec3 vLocal;
|
|
40
41
|
varying vec2 vUV;
|
|
42
|
+
uniform mat3 uUVTransform; // textureRepeat, textureOffset, textureRotation
|
|
41
43
|
${COMMON}
|
|
42
44
|
void main() {
|
|
43
45
|
vNormal = rotXYZ(aNormal / uScale); // inverse scale keeps normals right on stretched objects
|
|
44
46
|
vLocal = aPos;
|
|
45
|
-
vUV = aUV;
|
|
47
|
+
vUV = (uUVTransform * vec3(aUV, 1.0)).xy;
|
|
46
48
|
vWorld = toWorld(aPos);
|
|
47
49
|
gl_Position = toClip(toCamera(vWorld));
|
|
48
50
|
}
|
|
@@ -106,7 +108,7 @@ ${fx.calls}
|
|
|
106
108
|
return { source, uniforms: fx.uniforms };
|
|
107
109
|
}
|
|
108
110
|
|
|
109
|
-
const FACE_UNIFORMS = ["uLight", "uAmbient", "uShade", "uTime", "uBackground", "uCamWorld", "uTexture", "uUseTexture"];
|
|
111
|
+
const FACE_UNIFORMS = ["uUVTransform", "uLight", "uAmbient", "uShade", "uTime", "uBackground", "uCamWorld", "uTexture", "uUseTexture"];
|
|
110
112
|
|
|
111
113
|
// Each line segment is a thin rectangle facing the screen, `uWidth` pixels wide
|
|
112
114
|
const LINE_VS = `
|
|
@@ -268,16 +270,26 @@ export class WebGLRenderer {
|
|
|
268
270
|
const geo = this._geometry(obj.mesh);
|
|
269
271
|
const lineColor = this._color(ctx, obj.color ?? scene.color);
|
|
270
272
|
const lineWidth = obj.lineWidth ?? scene.lineWidth;
|
|
271
|
-
const
|
|
272
|
-
|
|
273
|
-
if (
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
273
|
+
const mats = materialsOf(obj);
|
|
274
|
+
|
|
275
|
+
if (mats) {
|
|
276
|
+
// One draw per group of faces sharing a material (mesh.faceMaterials)
|
|
277
|
+
const groups = geo.groups.map((g) => {
|
|
278
|
+
const k = g.index >= 0 && g.index < mats.length && mats[g.index] ? g.index : 0;
|
|
279
|
+
return { ...g, mat: mats[k] };
|
|
280
|
+
});
|
|
281
|
+
for (const g of groups) {
|
|
282
|
+
const fill = this._color(ctx, g.mat.fill ?? (g.mat.texture ? "#ffffff" : scene.color));
|
|
283
|
+
(fill[3] < 1 ? clear2 : opaque).push({ kind: "face", obj, geo, color: fill, mat: g.mat, first: g.first, count: g.count });
|
|
278
284
|
}
|
|
285
|
+
const lines = (buf) => (lineColor[3] < 1 ? clear2 : opaque).push({ kind: "line", obj, buf, color: lineColor, width: lineWidth });
|
|
286
|
+
if (groups.every((g) => g.mat.wireframe)) lines(this._lineBuffer(geo, "outlines", () => outlinePairs(geo.faceList ?? [])));
|
|
287
|
+
else for (const g of groups) if (g.mat.wireframe) lines(this._outlineBuffer(geo, obj.mesh, g));
|
|
279
288
|
} else {
|
|
280
|
-
(lineColor[3] < 1 ? clear2 : opaque).push({
|
|
289
|
+
(lineColor[3] < 1 ? clear2 : opaque).push({
|
|
290
|
+
kind: "line", obj, color: lineColor, width: lineWidth,
|
|
291
|
+
buf: this._lineBuffer(geo, "edges", () => edgeSegments(wireEdges(obj.mesh))),
|
|
292
|
+
});
|
|
281
293
|
}
|
|
282
294
|
}
|
|
283
295
|
|
|
@@ -310,8 +322,11 @@ export class WebGLRenderer {
|
|
|
310
322
|
const isFace = d.kind === "face";
|
|
311
323
|
const effects = isFace ? d.mat.effects ?? [] : null;
|
|
312
324
|
const prog = isFace ? this._faceProgram(effects) : this._line;
|
|
313
|
-
const buf = isFace
|
|
314
|
-
|
|
325
|
+
const buf = isFace
|
|
326
|
+
? this._faceBuffer(d.geo, d.obj.mesh, d.mat.shade === false ? null : creaseOf(d.mat.smooth), d.mat.mapping)
|
|
327
|
+
: d.buf;
|
|
328
|
+
const first = d.first ?? 0, count = d.count ?? buf.count;
|
|
329
|
+
if (!count) return;
|
|
315
330
|
|
|
316
331
|
gl.useProgram(prog.program);
|
|
317
332
|
const u = prog.uniforms;
|
|
@@ -331,6 +346,8 @@ export class WebGLRenderer {
|
|
|
331
346
|
const a = prog.attributes;
|
|
332
347
|
|
|
333
348
|
if (isFace) {
|
|
349
|
+
const m = uvTransform(d.mat) ?? [1, 0, 0, 1, 0, 0];
|
|
350
|
+
gl.uniformMatrix3fv(u.uUVTransform, false, [m[0], m[1], 0, m[2], m[3], 0, m[4], m[5], 1]);
|
|
334
351
|
gl.uniform3fv(u.uLight, light);
|
|
335
352
|
gl.uniform1f(u.uAmbient, ambient);
|
|
336
353
|
gl.uniform1f(u.uShade, d.mat.shade === false ? 0 : 1);
|
|
@@ -346,7 +363,9 @@ export class WebGLRenderer {
|
|
|
346
363
|
gl.vertexAttribPointer(a.aPos, 3, gl.FLOAT, false, 32, 0);
|
|
347
364
|
if (a.aNormal >= 0) gl.vertexAttribPointer(a.aNormal, 3, gl.FLOAT, false, 32, 12);
|
|
348
365
|
if (a.aUV >= 0) gl.vertexAttribPointer(a.aUV, 2, gl.FLOAT, false, 32, 24);
|
|
349
|
-
const tex = d.mat.texture
|
|
366
|
+
const tex = d.mat.texture
|
|
367
|
+
? this._texture(d.mat.texture, !!d.mat.pixelated, d.obj.mesh, !!uvsFor(d.obj.mesh, d.mat.mapping).faceUVs)
|
|
368
|
+
: null;
|
|
350
369
|
gl.activeTexture(gl.TEXTURE0);
|
|
351
370
|
gl.bindTexture(gl.TEXTURE_2D, tex);
|
|
352
371
|
gl.uniform1i(u.uTexture, 0);
|
|
@@ -363,7 +382,7 @@ export class WebGLRenderer {
|
|
|
363
382
|
}
|
|
364
383
|
const locs = Object.values(a).filter((loc) => loc >= 0);
|
|
365
384
|
for (const loc of locs) gl.enableVertexAttribArray(loc);
|
|
366
|
-
gl.drawArrays(gl.TRIANGLES,
|
|
385
|
+
gl.drawArrays(gl.TRIANGLES, first, count);
|
|
367
386
|
for (const loc of locs) gl.disableVertexAttribArray(loc);
|
|
368
387
|
}
|
|
369
388
|
|
|
@@ -398,13 +417,13 @@ export class WebGLRenderer {
|
|
|
398
417
|
* (canvases and videos: every frame). Null while it isn't ready,
|
|
399
418
|
* or if the mesh has no texture coordinates.
|
|
400
419
|
*/
|
|
401
|
-
_texture(src, pixelated, mesh) {
|
|
420
|
+
_texture(src, pixelated, mesh, hasUVs) {
|
|
402
421
|
const gl = this.gl;
|
|
403
422
|
if (!textureReady(src)) return null;
|
|
404
|
-
if (!
|
|
423
|
+
if (!hasUVs) {
|
|
405
424
|
if (!this._warned.has(mesh)) {
|
|
406
425
|
this._warned.add(mesh);
|
|
407
|
-
console.warn("Trid: this mesh has no texture coordinates (uvs / faceUVs), so its texture isn't drawn");
|
|
426
|
+
console.warn("Trid: this mesh has no texture coordinates (uvs / faceUVs), so its texture isn't drawn. Set material.mapping to make some.");
|
|
408
427
|
}
|
|
409
428
|
return null;
|
|
410
429
|
}
|
|
@@ -413,7 +432,7 @@ export class WebGLRenderer {
|
|
|
413
432
|
const live = textureIsLive(src);
|
|
414
433
|
if (!t || live || t.failed) {
|
|
415
434
|
if (t?.failed) return null;
|
|
416
|
-
|
|
435
|
+
let { w, h } = textureSize(src);
|
|
417
436
|
if (!t) {
|
|
418
437
|
t = { tex: gl.createTexture(), frame: -1 };
|
|
419
438
|
this._textures.set(src, t);
|
|
@@ -422,9 +441,23 @@ export class WebGLRenderer {
|
|
|
422
441
|
if (t.frame !== this._frame) {
|
|
423
442
|
t.frame = this._frame;
|
|
424
443
|
gl.bindTexture(gl.TEXTURE_2D, t.tex);
|
|
444
|
+
// Bigger than the graphics card allows: upload a smaller copy
|
|
445
|
+
let pixels = src;
|
|
446
|
+
const max = gl.getParameter(gl.MAX_TEXTURE_SIZE);
|
|
447
|
+
if (w > max || h > max) {
|
|
448
|
+
const k = max / Math.max(w, h);
|
|
449
|
+
if (!t.small || live) t.small = shrink(src, Math.floor(w * k), Math.floor(h * k), t.small);
|
|
450
|
+
if (!t.warned) {
|
|
451
|
+
t.warned = true;
|
|
452
|
+
console.warn(`Trid: a ${w} × ${h} texture is bigger than this graphics card allows (${max}), so it's drawn at ${t.small.width} × ${t.small.height}`);
|
|
453
|
+
}
|
|
454
|
+
pixels = t.small;
|
|
455
|
+
w = t.small.width;
|
|
456
|
+
h = t.small.height;
|
|
457
|
+
}
|
|
425
458
|
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true); // images go top-down, v goes up
|
|
426
459
|
try {
|
|
427
|
-
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE,
|
|
460
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
|
|
428
461
|
} catch (err) {
|
|
429
462
|
// Usually an image from another site without permission (CORS)
|
|
430
463
|
console.warn("Trid: can't use this image as a texture: " + err.message);
|
|
@@ -465,59 +498,87 @@ export class WebGLRenderer {
|
|
|
465
498
|
_geometry(mesh) {
|
|
466
499
|
let geo = this._meshes.get(mesh);
|
|
467
500
|
const stale = !geo || mesh.dirty || geo.vertices !== mesh.vertices || geo.faceList !== mesh.faces
|
|
468
|
-
|| geo.edgeList !== mesh.edges || geo.counts !== countsOf(mesh);
|
|
501
|
+
|| geo.edgeList !== mesh.edges || geo.faceMats !== mesh.faceMaterials || geo.counts !== countsOf(mesh);
|
|
469
502
|
if (!stale) return geo;
|
|
470
503
|
|
|
471
504
|
if (geo) {
|
|
472
|
-
for (const b of [geo.
|
|
505
|
+
for (const b of [...geo.lines.values(), ...geo.faceBuffers.values(), ...geo.groupOutlines.values()]) this._free(b);
|
|
473
506
|
}
|
|
474
|
-
const v = mesh.vertices;
|
|
475
507
|
const faces = mesh.faces ?? [];
|
|
476
508
|
|
|
477
|
-
//
|
|
478
|
-
const
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
509
|
+
// Faces grouped by material index, in the order the face buffers store them
|
|
510
|
+
const byIndex = new Map();
|
|
511
|
+
faces.forEach((f, fi) => {
|
|
512
|
+
if (f.length < 3) return;
|
|
513
|
+
const k = mesh.faceMaterials?.[fi] ?? 0;
|
|
514
|
+
if (!byIndex.has(k)) byIndex.set(k, []);
|
|
515
|
+
byIndex.get(k).push(fi);
|
|
516
|
+
});
|
|
517
|
+
const groups = [];
|
|
518
|
+
let first = 0;
|
|
519
|
+
for (const k of [...byIndex.keys()].sort((a, b) => a - b)) {
|
|
520
|
+
const list = byIndex.get(k);
|
|
521
|
+
const count = list.reduce((n, fi) => n + (faces[fi].length - 2) * 3, 0);
|
|
522
|
+
groups.push({ index: k, faces: list, first, count });
|
|
523
|
+
first += count;
|
|
489
524
|
}
|
|
490
525
|
|
|
491
526
|
geo = {
|
|
492
527
|
vertices: mesh.vertices,
|
|
493
528
|
faceList: mesh.faces,
|
|
494
529
|
edgeList: mesh.edges,
|
|
530
|
+
faceMats: mesh.faceMaterials,
|
|
495
531
|
counts: countsOf(mesh),
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
532
|
+
groups,
|
|
533
|
+
groupOutlines: new Map(), // outlines of one group, when only some materials have `wireframe`
|
|
534
|
+
faceBuffers: new Map(), // one per shading mode and mapping, built when first needed
|
|
535
|
+
lines: new Map(), // "outlines" and "edges", built when first drawn (they're big on big models)
|
|
499
536
|
};
|
|
500
537
|
this._meshes.set(mesh, geo);
|
|
501
538
|
return geo;
|
|
502
539
|
}
|
|
503
540
|
|
|
541
|
+
/** A line buffer of the mesh, built the first time it's drawn. */
|
|
542
|
+
_lineBuffer(geo, name, pairs) {
|
|
543
|
+
let buf = geo.lines.get(name);
|
|
544
|
+
if (!buf) {
|
|
545
|
+
buf = this._upload(lineData(geo.vertices, pairs()), 8);
|
|
546
|
+
geo.lines.set(name, buf);
|
|
547
|
+
}
|
|
548
|
+
return buf;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/** Outlines of one group of faces (see `_geometry`), built when first needed. */
|
|
552
|
+
_outlineBuffer(geo, mesh, group) {
|
|
553
|
+
let buf = geo.groupOutlines.get(group.index);
|
|
554
|
+
if (!buf) {
|
|
555
|
+
const faces = mesh.faces ?? [];
|
|
556
|
+
buf = this._upload(lineData(mesh.vertices, outlinePairs(group.faces.map((fi) => faces[fi]))), 8);
|
|
557
|
+
geo.groupOutlines.set(group.index, buf);
|
|
558
|
+
}
|
|
559
|
+
return buf;
|
|
560
|
+
}
|
|
561
|
+
|
|
504
562
|
/**
|
|
505
|
-
* Triangles of a mesh's faces, each corner with its own position
|
|
506
|
-
* normal
|
|
507
|
-
* shading uses the corner
|
|
563
|
+
* Triangles of a mesh's faces, each corner with its own position,
|
|
564
|
+
* normal and texture coordinate, grouped by material (`geo.groups`).
|
|
565
|
+
* Flat shading uses the face's normal at every corner; smooth shading
|
|
566
|
+
* uses the corner normals. `mapping` picks the texture coordinates.
|
|
508
567
|
*/
|
|
509
|
-
_faceBuffer(geo, mesh, crease) {
|
|
510
|
-
const key = crease ?? "flat";
|
|
568
|
+
_faceBuffer(geo, mesh, crease, mapping) {
|
|
569
|
+
const key = (crease ?? "flat") + "|" + mappingKey(mapping);
|
|
511
570
|
let buf = geo.faceBuffers.get(key);
|
|
512
571
|
if (buf) return buf;
|
|
513
572
|
|
|
514
573
|
const v = mesh.vertices;
|
|
515
574
|
const faces = mesh.faces ?? [];
|
|
516
575
|
const corners = crease ? cornerNormals(mesh, crease) : null;
|
|
517
|
-
const
|
|
518
|
-
const
|
|
519
|
-
|
|
520
|
-
|
|
576
|
+
const { uvs, faceUVs } = uvsFor(mesh, mapping);
|
|
577
|
+
const total = geo.groups.reduce((n, g) => n + g.count, 0);
|
|
578
|
+
const tri = new Float32Array(total * 8);
|
|
579
|
+
let o = 0;
|
|
580
|
+
for (const fi of geo.groups.flatMap((g) => g.faces)) {
|
|
581
|
+
const f = faces[fi];
|
|
521
582
|
let ns;
|
|
522
583
|
if (corners) {
|
|
523
584
|
ns = corners[fi];
|
|
@@ -532,10 +593,12 @@ export class WebGLRenderer {
|
|
|
532
593
|
for (const c of [0, k, k + 1]) {
|
|
533
594
|
const p = v[f[c]], n = ns[c];
|
|
534
595
|
const t = ts ? uvs[ts[c]] : null;
|
|
535
|
-
tri
|
|
596
|
+
tri[o++] = p.x; tri[o++] = p.y; tri[o++] = p.z;
|
|
597
|
+
tri[o++] = n.x; tri[o++] = n.y; tri[o++] = n.z;
|
|
598
|
+
tri[o++] = t ? t.u : 0; tri[o++] = t ? t.v : 0;
|
|
536
599
|
}
|
|
537
600
|
}
|
|
538
|
-
}
|
|
601
|
+
}
|
|
539
602
|
|
|
540
603
|
buf = this._upload(tri, 8);
|
|
541
604
|
geo.faceBuffers.set(key, buf);
|
|
@@ -546,7 +609,7 @@ export class WebGLRenderer {
|
|
|
546
609
|
const gl = this.gl;
|
|
547
610
|
const buffer = gl.createBuffer();
|
|
548
611
|
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
|
|
549
|
-
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(data), gl.STATIC_DRAW);
|
|
612
|
+
gl.bufferData(gl.ARRAY_BUFFER, data instanceof Float32Array ? data : new Float32Array(data), gl.STATIC_DRAW);
|
|
550
613
|
this._buffers.add(buffer);
|
|
551
614
|
return { buffer, count: data.length / floatsPerVertex };
|
|
552
615
|
}
|
|
@@ -573,13 +636,44 @@ function countsOf(mesh) {
|
|
|
573
636
|
return `${mesh.vertices.length}/${mesh.edges?.length ?? 0}/${mesh.faces?.length ?? 0}`;
|
|
574
637
|
}
|
|
575
638
|
|
|
639
|
+
/** A smaller copy of an image, drawn on a canvas (reused when given one). */
|
|
640
|
+
function shrink(src, w, h, canvas = document.createElement("canvas")) {
|
|
641
|
+
canvas.width = w;
|
|
642
|
+
canvas.height = h;
|
|
643
|
+
canvas.getContext("2d").drawImage(src, 0, 0, w, h);
|
|
644
|
+
return canvas;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
/** Each edge of the faces once, as [a, b] pairs. */
|
|
648
|
+
function outlinePairs(faces) {
|
|
649
|
+
const seen = new Set();
|
|
650
|
+
const pairs = [];
|
|
651
|
+
for (const f of faces) {
|
|
652
|
+
for (let k = 0; k < f.length; k++) {
|
|
653
|
+
const a = f[k], b = f[(k + 1) % f.length];
|
|
654
|
+
const key = a < b ? a * 4294967296 + b : b * 4294967296 + a; // one number per edge
|
|
655
|
+
if (!seen.has(key)) {
|
|
656
|
+
seen.add(key);
|
|
657
|
+
pairs.push([a, b]);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
return pairs;
|
|
662
|
+
}
|
|
663
|
+
|
|
576
664
|
/** Six corners (two triangles) per segment: A, B, then which corner. */
|
|
577
|
-
function lineData(v,
|
|
578
|
-
const
|
|
665
|
+
function lineData(v, segments) {
|
|
666
|
+
const pairs = Array.isArray(segments) ? segments : [...segments]; // edgeSegments is a generator
|
|
667
|
+
const out = new Float32Array(pairs.length * 48);
|
|
579
668
|
const corners = [[0, -1], [1, -1], [1, 1], [0, -1], [1, 1], [0, 1]];
|
|
669
|
+
let o = 0;
|
|
580
670
|
for (const [i, j] of pairs) {
|
|
581
671
|
const a = v[i], b = v[j];
|
|
582
|
-
for (const [cx, cy] of corners)
|
|
672
|
+
for (const [cx, cy] of corners) {
|
|
673
|
+
out[o++] = a.x; out[o++] = a.y; out[o++] = a.z;
|
|
674
|
+
out[o++] = b.x; out[o++] = b.y; out[o++] = b.z;
|
|
675
|
+
out[o++] = cx; out[o++] = cy;
|
|
676
|
+
}
|
|
583
677
|
}
|
|
584
678
|
return out;
|
|
585
679
|
}
|
package/trid/scene.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { CanvasRenderer, DEFAULT_LIGHT, DEFAULT_AMBIENT } from "./render/canvasRenderer.js";
|
|
2
2
|
import { loop } from "./render/loop.js";
|
|
3
3
|
import { ToolManager } from "./tools/manager.js";
|
|
4
|
+
import { screenshot, record } from "./export/capture.js";
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Holds a canvas, a camera and a list of objects, and draws them.
|
|
@@ -150,6 +151,28 @@ export class Scene {
|
|
|
150
151
|
this._stop = null;
|
|
151
152
|
}
|
|
152
153
|
|
|
154
|
+
/**
|
|
155
|
+
* Draws a new frame and returns it as an image file (PNG by default).
|
|
156
|
+
* The page's canvas doesn't change. Save it with `download`.
|
|
157
|
+
*
|
|
158
|
+
* @param {import("./export/capture.js").ScreenshotOptions} [opts]
|
|
159
|
+
* @returns {Promise<Blob>}
|
|
160
|
+
*/
|
|
161
|
+
screenshot(opts) {
|
|
162
|
+
return screenshot(this, opts);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Starts recording the canvas as a video. Call `stop()` on the result,
|
|
167
|
+
* or pass `duration`, then save `await recording.done` with `download`.
|
|
168
|
+
*
|
|
169
|
+
* @param {import("./export/capture.js").RecordOptions} [opts]
|
|
170
|
+
* @returns {import("./export/capture.js").Recording}
|
|
171
|
+
*/
|
|
172
|
+
record(opts) {
|
|
173
|
+
return record(this, opts);
|
|
174
|
+
}
|
|
175
|
+
|
|
153
176
|
/**
|
|
154
177
|
* Stops the loop, removes every tool (and its event listeners) and
|
|
155
178
|
* frees the renderer's resources. Use it when the scene is no longer needed.
|
|
@@ -54,7 +54,8 @@ export class HoverHighlight {
|
|
|
54
54
|
obj.color = this.color;
|
|
55
55
|
// Swap in a copy, so other objects sharing the material don't change
|
|
56
56
|
this._savedMaterial = obj.material;
|
|
57
|
-
if (obj.material) obj.material = { ...
|
|
57
|
+
if (Array.isArray(obj.material)) obj.material = obj.material.map((m) => m && { ...m, fill: this.color });
|
|
58
|
+
else if (obj.material) obj.material = { ...obj.material, fill: this.color };
|
|
58
59
|
}
|
|
59
60
|
|
|
60
61
|
this.onChange?.(obj);
|
|
@@ -13,6 +13,9 @@ const MAX_PITCH = Math.PI / 2 - 0.01;
|
|
|
13
13
|
* The tool drives `scene.camera` (x, y, z, yaw, pitch). Its own
|
|
14
14
|
* `target`, `distance`, `yaw` and `pitch` can be changed at any time.
|
|
15
15
|
* Don't use it together with another tool that moves the camera.
|
|
16
|
+
*
|
|
17
|
+
* With `autoRotate: true` the camera also turns around the target on its
|
|
18
|
+
* own, pausing while the user drags.
|
|
16
19
|
*/
|
|
17
20
|
export class OrbitCamera {
|
|
18
21
|
/**
|
|
@@ -28,6 +31,9 @@ export class OrbitCamera {
|
|
|
28
31
|
* @param {boolean} [opts.rotate=true] - Allow orbiting
|
|
29
32
|
* @param {boolean} [opts.pan=true] - Allow panning
|
|
30
33
|
* @param {boolean} [opts.zoom=true] - Allow zooming
|
|
34
|
+
* @param {boolean} [opts.autoRotate=false] - Turn around the target on its own (paused while dragging)
|
|
35
|
+
* @param {number} [opts.autoRotateSpeed=0.5] - Auto-rotate speed in radians per second (negative: other way)
|
|
36
|
+
* @param {number} [opts.autoRotateDelay=0] - Seconds to wait after a drag before auto-rotating again
|
|
31
37
|
*/
|
|
32
38
|
constructor({
|
|
33
39
|
target = { x: 0, y: 0, z: 0 },
|
|
@@ -41,6 +47,9 @@ export class OrbitCamera {
|
|
|
41
47
|
rotate = true,
|
|
42
48
|
pan = true,
|
|
43
49
|
zoom = true,
|
|
50
|
+
autoRotate = false,
|
|
51
|
+
autoRotateSpeed = 0.5,
|
|
52
|
+
autoRotateDelay = 0,
|
|
44
53
|
} = {}) {
|
|
45
54
|
this.target = target;
|
|
46
55
|
this.distance = distance;
|
|
@@ -53,9 +62,13 @@ export class OrbitCamera {
|
|
|
53
62
|
this.rotate = rotate;
|
|
54
63
|
this.pan = pan;
|
|
55
64
|
this.zoom = zoom;
|
|
65
|
+
this.autoRotate = autoRotate;
|
|
66
|
+
this.autoRotateSpeed = autoRotateSpeed;
|
|
67
|
+
this.autoRotateDelay = autoRotateDelay;
|
|
56
68
|
|
|
57
69
|
this.blockContextMenu = true;
|
|
58
70
|
this._mode = null;
|
|
71
|
+
this._idle = 0; // seconds since the last drag ended
|
|
59
72
|
}
|
|
60
73
|
|
|
61
74
|
attach(scene) {
|
|
@@ -117,6 +130,7 @@ export class OrbitCamera {
|
|
|
117
130
|
|
|
118
131
|
pointerUp() {
|
|
119
132
|
this._mode = null;
|
|
133
|
+
this._idle = 0;
|
|
120
134
|
}
|
|
121
135
|
|
|
122
136
|
wheel(e, scene) {
|
|
@@ -127,6 +141,10 @@ export class OrbitCamera {
|
|
|
127
141
|
}
|
|
128
142
|
|
|
129
143
|
update(dt, scene) {
|
|
144
|
+
if (this.autoRotate && !this._mode) {
|
|
145
|
+
this._idle += dt;
|
|
146
|
+
if (this._idle >= this.autoRotateDelay) this.yaw += this.autoRotateSpeed * dt;
|
|
147
|
+
}
|
|
130
148
|
this.apply(scene.camera);
|
|
131
149
|
}
|
|
132
150
|
}
|