trid3d 0.1.1 → 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/trid/index.d.ts CHANGED
@@ -56,10 +56,11 @@ export interface Camera {
56
56
  *
57
57
  * An edge with 2 indices is a single segment; with 3 or more it's a closed loop.
58
58
  * Faces are clockwise as seen from their front.
59
+ * Without `edges`, an object with no material draws the outline of each face.
59
60
  */
60
61
  export interface Mesh {
61
62
  vertices: Vec3[];
62
- edges: number[][];
63
+ edges?: number[][];
63
64
  faces?: number[][];
64
65
  /** Per-corner normals, used by smooth shading */
65
66
  normals?: Vec3[];
@@ -69,6 +70,11 @@ export interface Mesh {
69
70
  uvs?: UV[];
70
71
  /** For each face, the index into `uvs` of each corner (null: none for that face) */
71
72
  faceUVs?: (number[] | null)[];
73
+ /**
74
+ * For each face, which entry of the object's material list it uses
75
+ * (when `material` is a list). Built-in cubes and boxes number their sides 0 to 5.
76
+ */
77
+ faceMaterials?: number[];
72
78
  /** Set after changing the mesh in place so renderers rebuild their caches */
73
79
  dirty?: boolean;
74
80
  }
@@ -104,8 +110,25 @@ export interface Material {
104
110
  pixelated?: boolean;
105
111
  /** Effects applied in order, like `[toon(), rim()]` */
106
112
  effects?: Effect[];
113
+ /** Where texture coordinates come from: the mesh's own ("uv", default) or made from its shape */
114
+ mapping?: Mapping;
115
+ /** How many times the texture repeats across (default 1) */
116
+ textureRepeat?: number | UV;
117
+ /** Moves the texture; 1 is its whole width or height */
118
+ textureOffset?: UV;
119
+ /** Turns the texture around its center, in radians */
120
+ textureRotation?: number;
107
121
  }
108
122
 
123
+ /** Ways to make texture coordinates from a mesh's shape. */
124
+ export type MappingType = "uv" | "planar" | "box" | "sphere" | "cylinder";
125
+
126
+ /**
127
+ * A texture mapping: a type, or a type with an axis. `axis` is the projection
128
+ * direction for "planar" (default "z") and the wrap axis for "sphere" and "cylinder" (default "y").
129
+ */
130
+ export type Mapping = MappingType | { type: MappingType; axis?: "x" | "y" | "z" };
131
+
109
132
  /** What you pass to `scene.add`. Missing transform fields get defaults. */
110
133
  export interface SceneObjectInput {
111
134
  mesh: Mesh;
@@ -116,8 +139,8 @@ export interface SceneObjectInput {
116
139
  color?: string;
117
140
  /** Wireframe width (default: the scene's lineWidth) */
118
141
  lineWidth?: number;
119
- /** Draw faces instead of only lines */
120
- material?: Material;
142
+ /** Draw faces instead of only lines. A list gives each group of faces its own (see `Mesh.faceMaterials`) */
143
+ material?: Material | Material[];
121
144
  /** Set to false to make `DragObject` ignore this object */
122
145
  draggable?: boolean;
123
146
  /** Set to false to make `HoverHighlight` ignore this object */
@@ -191,6 +214,10 @@ export class Scene {
191
214
  start(update?: (dt: number, time: number, scene: Scene) => void): this;
192
215
  /** Stops the frame loop. The last frame stays on the canvas. */
193
216
  stop(): void;
217
+ /** Draws a new frame and returns it as an image file. The page's canvas doesn't change. */
218
+ screenshot(opts?: ScreenshotOptions): Promise<Blob>;
219
+ /** Starts recording the canvas as a video. */
220
+ record(opts?: RecordOptions): Recording;
194
221
  /** Stops the loop, removes every tool and frees the renderer's resources. */
195
222
  destroy(): void;
196
223
  /** Whether the frame loop is running. */
@@ -202,26 +229,79 @@ export class Scene {
202
229
  // ---------------------------------------------------------------------------
203
230
 
204
231
  export interface ShapeOptions {
205
- /** Cube size (default 0.5) */
232
+ /** Cube size, or plane width (default 0.5 cube, 1 plane) */
206
233
  size?: number;
207
234
  /** Box width (default 0.6) */
208
235
  w?: number;
209
- /** Box height (default 0.6) */
236
+ /** Box height (default 0.6), or cylinder / cone height (default 0.8) */
210
237
  h?: number;
211
238
  /** Box depth (default 0.6) */
212
239
  d?: number;
213
- /** Sphere radius (default 0.45) */
240
+ /** Radius: sphere (0.45), cylinder (0.3), cone base (0.35) */
214
241
  r?: number;
215
242
  /** Sphere latitude bands (default 12) */
216
243
  rings?: number;
217
- /** Sphere points per ring (default 24) */
244
+ /** Points around: sphere (24), cylinder / cone (32), torus (48) */
218
245
  segments?: number;
246
+ /** Cone top radius: 0 = pointed, > 0 = tapered cylinder (default 0) */
247
+ rTop?: number;
248
+ /** Torus: distance from the center to the middle of the tube (default 0.35) */
249
+ R?: number;
250
+ /** Torus tube radius (default 0.12) */
251
+ tube?: number;
252
+ /** Torus points around the tube (default 20) */
253
+ tubeSegments?: number;
254
+ /** Plane cells per side (default 8) */
255
+ div?: number;
256
+ /** Plane: lifts each vertex, for terrain */
257
+ height?: (x: number, z: number) => number;
258
+ }
259
+
260
+ export type ShapeKind = "cube" | "box" | "sphere" | "cylinder" | "cone" | "torus" | "plane";
261
+
262
+ /**
263
+ * Creates a built-in mesh (with edges, faces and texture coordinates), centered
264
+ * on the origin. Cylinders, cones and tori stand along Y; the plane lies in XZ facing +Y.
265
+ */
266
+ export function makeShape(kind: ShapeKind, opts?: ShapeOptions): Mesh;
267
+
268
+ /** A point of a `lathe` profile: distance from the Y axis and height. */
269
+ export interface ProfilePoint {
270
+ r: number;
271
+ y: number;
219
272
  }
220
273
 
221
- export type ShapeKind = "cube" | "box" | "sphere";
274
+ export interface LatheOptions {
275
+ /** Points around the axis (default 32) */
276
+ segments?: number;
277
+ /** Close the bottom with a flat cap (default false) */
278
+ capBottom?: boolean;
279
+ /** Close the top with a flat cap (default false) */
280
+ capTop?: boolean;
281
+ /** The profile's last point joins its first, for torus-like shapes (default false) */
282
+ closed?: boolean;
283
+ }
222
284
 
223
- /** Creates a built-in mesh (with faces and texture coordinates). */
224
- export function makeShape(kind: ShapeKind, opts?: ShapeOptions): Mesh;
285
+ /**
286
+ * Spins a profile around the Y axis (vases, bottles, columns, chess pieces).
287
+ * Walk the profile so the outside is on its right: bottom to top for a vase.
288
+ */
289
+ export function lathe(profile: ProfilePoint[], opts?: LatheOptions): Mesh;
290
+
291
+ /** A 2D point (x right, y up). */
292
+ export interface Point2 {
293
+ x: number;
294
+ y: number;
295
+ }
296
+
297
+ /**
298
+ * Turns a flat outline in the XY plane into a solid `depth` thick along Z
299
+ * (default 0.2), centered on the origin. Either direction, concave is fine, no holes.
300
+ */
301
+ export function extrude(outline: Point2[], depth?: number): Mesh;
302
+
303
+ /** Cuts a counter-clockwise polygon into counter-clockwise triangles (ear clipping). */
304
+ export function triangulate(pts: Point2[]): number[][];
225
305
 
226
306
  /** Scales a mesh's vertices in place. Returns the same mesh. */
227
307
  export function scaleMesh<M extends { vertices: Vec3[] }>(mesh: M, sx?: number, sy?: number, sz?: number): M;
@@ -237,6 +317,9 @@ export const DEFAULT_AMBIENT: number;
237
317
  /** Default far distance (WebGL renderer). */
238
318
  export const DEFAULT_FAR: number;
239
319
 
320
+ /** An object's materials as a list, or null when it's a wireframe. */
321
+ export function materialsOf(obj: { material?: Material | Material[] }): Material[] | null;
322
+
240
323
  export interface CanvasRendererOptions {
241
324
  /**
242
325
  * Split faces bigger than this fraction of their mesh's size (like 0.05)
@@ -394,6 +477,41 @@ export function uniformType(value: unknown): "float" | "vec2" | "vec3" | "vec4"
394
477
  /** The GLSL declaration of `Surface`, shared by every effect. */
395
478
  export const SURFACE_GLSL: string;
396
479
 
480
+ // ---------------------------------------------------------------------------
481
+ // Texture mapping
482
+ // ---------------------------------------------------------------------------
483
+
484
+ /** Texture coordinates for every face corner. */
485
+ export interface MeshUVs {
486
+ uvs: UV[];
487
+ faceUVs: number[][];
488
+ }
489
+
490
+ /** The mapping types. */
491
+ export const MAPPINGS: readonly MappingType[];
492
+
493
+ /** Texture coordinates made from a mesh's shape. The mesh isn't changed. */
494
+ export function computeUVs(mesh: Mesh, mapping: Mapping): MeshUVs;
495
+
496
+ /** Replaces a mesh's texture coordinates with ones made from its shape, in place. */
497
+ export function mapUVs<M extends Mesh>(mesh: M, mapping: Mapping): M & MeshUVs;
498
+
499
+ /** The texture coordinates a renderer uses for a mesh and mapping (cached). */
500
+ export function uvsFor(mesh: Mesh, mapping?: Mapping): { uvs?: UV[]; faceUVs?: (number[] | null)[] };
501
+
502
+ /** A material's repeat, offset and rotation as [a, b, c, d, e, f], or null when they change nothing. */
503
+ export function uvTransform(material: Material): [number, number, number, number, number, number] | null;
504
+
505
+ /** Applies a `uvTransform` matrix to one texture coordinate. */
506
+ export function applyUVTransform(m: number[] | null, uv: UV): UV;
507
+ export function applyUVTransform(m: number[] | null, uv: UV | null): UV | null;
508
+
509
+ /** A mapping as `{ type, axis }`. Throws for an unknown type or axis. */
510
+ export function mappingOf(mapping?: Mapping): { type: MappingType; axis?: "x" | "y" | "z" };
511
+
512
+ /** A short string naming a mapping, for caches ("" for "uv"). */
513
+ export function mappingKey(mapping?: Mapping): string;
514
+
397
515
  // ---------------------------------------------------------------------------
398
516
  // Normals
399
517
  // ---------------------------------------------------------------------------
@@ -417,6 +535,8 @@ export function newell(pts: Vec3[]): Vec3;
417
535
  /** A mesh loaded from an OBJ file. Always has faces. */
418
536
  export interface ObjMesh extends Mesh {
419
537
  faces: number[][];
538
+ /** Material names from `usemtl` lines, when the file uses more than one */
539
+ materialNames?: string[];
420
540
  }
421
541
 
422
542
  export interface ObjOptions {
@@ -444,6 +564,80 @@ export function textureSize(src: TextureSource): { w: number; h: number };
444
564
  /** Whether a source can change over time (canvas, video). */
445
565
  export function textureIsLive(src: TextureSource): boolean;
446
566
 
567
+ // ---------------------------------------------------------------------------
568
+ // Exporting
569
+ // ---------------------------------------------------------------------------
570
+
571
+ export interface ExportOBJOptions {
572
+ /**
573
+ * Write texture coordinates (default true). For an object whose material has a
574
+ * `mapping` or texture repeat/offset/rotation, the coordinates it's drawn with are written.
575
+ */
576
+ uvs?: boolean;
577
+ /** Write the mesh's normals when it has them (default true) */
578
+ normals?: boolean;
579
+ /** Write edges that aren't the side of a face as lines (default true) */
580
+ lines?: boolean;
581
+ /** Decimals kept in numbers (default 6) */
582
+ precision?: number;
583
+ /** Comment at the top of the file; "" for none (default "Exported by Trid") */
584
+ header?: string;
585
+ }
586
+
587
+ /** Something exportOBJ can write: a mesh, or an object placed in a scene. */
588
+ export type OBJExportable = Mesh | (SceneObjectInput & Partial<Transform> & { name?: string });
589
+
590
+ /**
591
+ * Writes meshes as the text of an OBJ file. Objects are written where they are
592
+ * in the scene; a list or a Scene gives one `o` per object.
593
+ */
594
+ export function exportOBJ(input: OBJExportable | OBJExportable[] | Scene, opts?: ExportOBJOptions): string;
595
+
596
+ export interface ScreenshotOptions {
597
+ /** "image/png" (default), "image/jpeg" or "image/webp" */
598
+ type?: string;
599
+ /** For JPEG and WebP, from 0 to 1 (default 0.92) */
600
+ quality?: number;
601
+ /** Image width in pixels (default: the canvas's) */
602
+ width?: number;
603
+ /** Image height in pixels (default: the canvas's) */
604
+ height?: number;
605
+ /** Include what tools draw on top (default true) */
606
+ tools?: boolean;
607
+ }
608
+
609
+ export interface RecordOptions {
610
+ /** Frames per second (default 60) */
611
+ fps?: number;
612
+ /** Video format, like "video/webm" (default: the best one the browser has) */
613
+ type?: string;
614
+ /** Quality: higher is sharper and bigger (default 8,000,000) */
615
+ bitsPerSecond?: number;
616
+ /** Stop by itself after this many seconds */
617
+ duration?: number;
618
+ }
619
+
620
+ /** A video being recorded. */
621
+ export interface Recording {
622
+ /** The video format being recorded */
623
+ readonly type: string;
624
+ /** Whether it's still recording */
625
+ readonly recording: boolean;
626
+ /** Resolves with the video once recording stops */
627
+ readonly done: Promise<Blob>;
628
+ /** Stops recording; resolves with the video */
629
+ stop(): Promise<Blob>;
630
+ }
631
+
632
+ /** Draws a new frame of a scene and returns it as an image file. */
633
+ export function screenshot(scene: Scene, opts?: ScreenshotOptions): Promise<Blob>;
634
+
635
+ /** Records a scene's canvas as a video. Throws if the browser can't. */
636
+ export function record(scene: Scene, opts?: RecordOptions): Recording;
637
+
638
+ /** Saves a file or text in the browser's downloads. */
639
+ export function download(data: Blob | string | Promise<Blob | string>, filename: string): Promise<void>;
640
+
447
641
  // ---------------------------------------------------------------------------
448
642
  // Tools
449
643
  // ---------------------------------------------------------------------------
@@ -529,6 +723,12 @@ export interface OrbitCameraOptions {
529
723
  pan?: boolean;
530
724
  /** Allow zooming (default true) */
531
725
  zoom?: boolean;
726
+ /** Turn around the target on its own, paused while dragging (default false) */
727
+ autoRotate?: boolean;
728
+ /** Auto-rotate speed in radians per second; negative turns the other way (default 0.5) */
729
+ autoRotateSpeed?: number;
730
+ /** Seconds to wait after a drag before auto-rotating again (default 0) */
731
+ autoRotateDelay?: number;
532
732
  }
533
733
 
534
734
  /** Mouse camera controls that turn around a target point. */
@@ -548,6 +748,9 @@ export class OrbitCamera implements Tool {
548
748
  rotate: boolean;
549
749
  pan: boolean;
550
750
  zoom: boolean;
751
+ autoRotate: boolean;
752
+ autoRotateSpeed: number;
753
+ autoRotateDelay: number;
551
754
  blockContextMenu: boolean;
552
755
  /** Places the camera from the target, distance, yaw and pitch. */
553
756
  apply(camera: Camera): void;
@@ -744,7 +947,9 @@ export function drawInstance(
744
947
  lineWidth?: number,
745
948
  ): void;
746
949
  /** Turns edge definitions into pairs of vertex indices. */
747
- export function edgeSegments(edges: number[][]): Generator<[number, number]>;
950
+ export function edgeSegments(edges?: number[][]): Generator<[number, number]>;
951
+ /** The lines a wireframe draws for a mesh: its `edges`, or its face outlines when it has none. */
952
+ export function wireEdges(mesh: { edges?: number[][]; faces?: number[][] }): number[][];
748
953
 
749
954
  /** Runs `update` every animation frame. Returns a function that stops it. */
750
955
  export function loop(update: (dt: number, time: number) => void, opts?: { maxDt?: number }): () => void;
package/trid/index.js CHANGED
@@ -13,10 +13,10 @@
13
13
  export { Scene } from "./scene.js";
14
14
 
15
15
  // Shapes and meshes
16
- export { makeShape, scaleMesh } from "./render/shapes.js";
16
+ export { makeShape, scaleMesh, lathe, extrude, triangulate } from "./render/shapes.js";
17
17
 
18
18
  // Renderers
19
- export { CanvasRenderer, DEFAULT_LIGHT, DEFAULT_AMBIENT } from "./render/canvasRenderer.js";
19
+ export { CanvasRenderer, DEFAULT_LIGHT, DEFAULT_AMBIENT, materialsOf } from "./render/canvasRenderer.js";
20
20
  export { WebGLRenderer, DEFAULT_FAR } from "./render/webglRenderer.js";
21
21
 
22
22
  // Effects
@@ -25,6 +25,9 @@ export {
25
25
  buildEffects, effectsKey, uniformType, SURFACE_GLSL,
26
26
  } from "./render/effects.js";
27
27
 
28
+ // Texture mapping
29
+ export { computeUVs, mapUVs, uvsFor, uvTransform, applyUVTransform, mappingOf, mappingKey, MAPPINGS } from "./render/uvMapping.js";
30
+
28
31
  // Normals (smooth shading)
29
32
  export { cornerNormals, creaseOf, newell, DEFAULT_CREASE } from "./render/normals.js";
30
33
 
@@ -32,6 +35,10 @@ export { cornerNormals, creaseOf, newell, DEFAULT_CREASE } from "./render/normal
32
35
  export { parseOBJ, loadOBJ, readOBJFile, pickOBJFile, fitMesh } from "./loaders/obj.js";
33
36
  export { loadTexture, textureReady, textureSize, textureIsLive } from "./loaders/texture.js";
34
37
 
38
+ // Exporting
39
+ export { exportOBJ } from "./export/obj.js";
40
+ export { screenshot, record, download } from "./export/capture.js";
41
+
35
42
  // Tools
36
43
  export {
37
44
  OrbitCamera, FlyCamera, DragObject, HoverHighlight, FpsMeter, ObjUploader,
@@ -39,7 +46,7 @@ export {
39
46
  } from "./tools/index.js";
40
47
 
41
48
  // Drawing and the frame loop
42
- export { clear, point, line, drawWireframe, drawInstance, edgeSegments } from "./render/draw.js";
49
+ export { clear, point, line, drawWireframe, drawInstance, edgeSegments, wireEdges } from "./render/draw.js";
43
50
  export { loop } from "./render/loop.js";
44
51
  export { parseColor } from "./render/color.js";
45
52
 
@@ -2,8 +2,8 @@
2
2
  * Wavefront OBJ loading.
3
3
  *
4
4
  * Reads vertex positions (`v`), texture coordinates (`vt`), normals
5
- * (`vn`), faces (`f`) and polylines (`l`). Everything else (materials,
6
- * groups) is ignored for now.
5
+ * (`vn`), faces (`f`), polylines (`l`) and material names (`usemtl`).
6
+ * Everything else (.mtl files, groups) is ignored for now.
7
7
  *
8
8
  * OBJ files are right-handed with +Z toward the viewer. Trid is
9
9
  * left-handed with the camera looking down +Z, so Z is flipped on
@@ -21,6 +21,9 @@
21
21
  * @property {{u:number,v:number}[]} [uvs] - Texture coordinates from the file (`vt`), if any
22
22
  * @property {(number[]|null)[]} [faceUVs] - For each face, the texture coordinate index of
23
23
  * each corner (null when the file gives none for that face). Used by textures.
24
+ * @property {string[]} [materialNames] - Material names from `usemtl` lines, in the order they first appear
25
+ * @property {number[]} [faceMaterials] - For each face, its index in `materialNames`. Give the
26
+ * object a list of materials in the same order to color or texture each part.
24
27
  *
25
28
  * @typedef {Object} ObjOptions
26
29
  * @property {number} [fit] - If set, center the model on the origin and scale it
@@ -43,6 +46,9 @@ export function parseOBJ(text, { fit } = {}) {
43
46
  const faceNormals = []; // per face: normal index of each corner, or null
44
47
  const uvs = [];
45
48
  const faceUVs = []; // per face: texture coordinate index of each corner, or null
49
+ const materialNames = [];
50
+ const faceMaterials = []; // per face: index in materialNames
51
+ let material = 0;
46
52
 
47
53
  const src = text.split(/\r?\n/);
48
54
 
@@ -60,6 +66,10 @@ export function parseOBJ(text, { fit } = {}) {
60
66
  uvs.push({ u: +(parts[1] ?? 0), v: +(parts[2] ?? 0) });
61
67
  } else if (kind === "vn") {
62
68
  normals.push({ x: +(parts[1] ?? 0), y: +(parts[2] ?? 0), z: -(parts[3] ?? 0) });
69
+ } else if (kind === "usemtl") {
70
+ const name = parts.slice(1).join(" ");
71
+ material = materialNames.indexOf(name);
72
+ if (material < 0) material = materialNames.push(name) - 1;
63
73
  } else if (kind === "f" || kind === "l") {
64
74
  // "7", "7/1", "7//3", "7/1/3" all point at vertex 7. Negative = counted from the end.
65
75
  const ids = parts.slice(1).map((p) => {
@@ -72,6 +82,7 @@ export function parseOBJ(text, { fit } = {}) {
72
82
  });
73
83
  if (kind === "f" && ids.length >= 3) {
74
84
  faces.push(ids.reverse()); // flipping Z flips the winding
85
+ faceMaterials.push(material);
75
86
  // "7//3" and "7/1/3": the third number is the corner's normal
76
87
  const ns = parts.slice(1).map((p) => {
77
88
  const s = p.split("/")[2];
@@ -127,6 +138,11 @@ export function parseOBJ(text, { fit } = {}) {
127
138
  mesh.uvs = uvs;
128
139
  mesh.faceUVs = faceUVs;
129
140
  }
141
+ // Parts using different materials
142
+ if (materialNames.length > 1) {
143
+ mesh.materialNames = materialNames;
144
+ mesh.faceMaterials = faceMaterials;
145
+ }
130
146
  if (fit != null) fitMesh(mesh, fit);
131
147
  return mesh;
132
148
  }