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/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
  }
@@ -1,11 +1,26 @@
1
1
  import { localToWorld, worldToCamera, cameraToScreen, clipNear, rotateXYZ, DEFAULT_NEAR } from "../math/transform.js";
2
- import { clear, drawInstance, edgeSegments } from "./draw.js";
2
+ import { clear, drawInstance, edgeSegments, wireEdges } from "./draw.js";
3
3
  import { parseColor } from "./color.js";
4
4
  import { newell, cornerNormals, creaseOf } from "./normals.js";
5
5
  import { textureReady, textureSize, textureIsLive } from "../loaders/texture.js";
6
+ import { uvsFor, uvTransform, applyUVTransform } from "./uvMapping.js";
6
7
 
7
8
  export { newell };
8
9
 
10
+ /**
11
+ * An object's materials as a list (`material` can be one material or a
12
+ * list of them, picked per face by `mesh.faceMaterials`). Null when the
13
+ * object is a wireframe.
14
+ *
15
+ * @returns {Material[]|null}
16
+ */
17
+ export function materialsOf(obj) {
18
+ const m = obj.material;
19
+ if (!m) return null;
20
+ if (!Array.isArray(m)) return [m];
21
+ return m.length && m[0] ? m : null;
22
+ }
23
+
9
24
  /** Direction light comes from when the scene doesn't set one: above, left, in front. */
10
25
  export const DEFAULT_LIGHT = { x: -0.5, y: 1, z: -0.8 };
11
26
 
@@ -16,6 +31,10 @@ export const DEFAULT_AMBIENT = 0.25;
16
31
  * Material: how an object's faces look. Objects without one are drawn
17
32
  * as wireframes using their `color` and `lineWidth`.
18
33
  *
34
+ * An object's `material` can also be a list: each face then uses the
35
+ * entry `mesh.faceMaterials[face]` points at (the first one by default).
36
+ * Built-in cubes and boxes number their sides 0 to 5.
37
+ *
19
38
  * @typedef {Object} Material
20
39
  * @property {string} [fill] - Face color, any CSS color (default: the scene's color)
21
40
  * @property {boolean} [shade=true] - Darken faces that turn away from the light
@@ -26,6 +45,11 @@ export const DEFAULT_AMBIENT = 0.25;
26
45
  * @property {CanvasImageSource} [texture] - Image drawn on the faces (needs texture coordinates:
27
46
  * built-in shapes and OBJ files with `vt` have them). `fill` tints it (default white).
28
47
  * @property {boolean} [pixelated=false] - Keep texture pixels sharp instead of smoothing them
48
+ * @property {import("./uvMapping.js").Mapping} [mapping="uv"] - Where texture coordinates come
49
+ * from: the mesh's own ("uv"), or made from its shape ("planar", "box", "sphere", "cylinder")
50
+ * @property {number|{u:number,v:number}} [textureRepeat=1] - How many times the image repeats across
51
+ * @property {{u:number,v:number}} [textureOffset] - Moves the image (1 = its whole width or height)
52
+ * @property {number} [textureRotation=0] - Turns the image around its center, in radians
29
53
  * @property {Object[]} [effects] - Effects applied in order, like `[toon(), rim()]`.
30
54
  * See trid/render/effects.js. The canvas renderer runs them once per face.
31
55
  */
@@ -79,7 +103,7 @@ export class CanvasRenderer {
79
103
  this._frame++;
80
104
 
81
105
  // Wireframes only: nothing to sort
82
- if (!scene.objects.some((o) => o.material)) {
106
+ if (!scene.objects.some((o) => materialsOf(o))) {
83
107
  for (const obj of scene.objects) {
84
108
  drawInstance(ctx, obj, camera, w, h, obj.color ?? scene.color, obj.lineWidth ?? scene.lineWidth);
85
109
  }
@@ -97,17 +121,17 @@ export class CanvasRenderer {
97
121
  const background = this._rgba(ctx, scene.background);
98
122
 
99
123
  for (const obj of scene.objects) {
124
+ const mats = materialsOf(obj);
100
125
  // Split big faces (cached per mesh); extra vertices are midpoints of existing ones
101
- const split = obj.material && this.maxFaceSize ? this._split(obj.mesh) : null;
126
+ const split = mats && this.maxFaceSize ? this._split(obj.mesh) : null;
102
127
  const verts = split ? withMidpoints(obj.mesh.vertices, split.mids) : obj.mesh.vertices;
103
128
  const world = verts.map((v) => localToWorld(v, obj));
104
129
  const cam = world.map((p) => worldToCamera(p, camera));
105
130
  const color = obj.color ?? scene.color;
106
131
  const lineWidth = obj.lineWidth ?? scene.lineWidth;
107
- const mat = obj.material;
108
132
 
109
- if (!mat) {
110
- for (const [i, j] of edgeSegments(obj.mesh.edges)) {
133
+ if (!mats) {
134
+ for (const [i, j] of edgeSegments(wireEdges(obj.mesh))) {
111
135
  const seg = clipNear(cam[i], cam[j], near);
112
136
  if (!seg) continue;
113
137
  items.push({
@@ -119,19 +143,14 @@ export class CanvasRenderer {
119
143
  continue;
120
144
  }
121
145
 
122
- const fill = mat.fill ?? (mat.texture ? "#ffffff" : scene.color);
123
- const shade = mat.shade !== false;
124
- const outline = mat.wireframe ? color : null;
125
-
126
- // Smooth shading: a normal per face corner (split faces get midpoints' normals by averaging)
127
- const crease = shade ? creaseOf(mat.smooth) : null;
128
- const corners = crease ? cornerNormals(obj.mesh, crease) : null;
129
146
  const sourceFaces = obj.mesh.faces ?? [];
130
147
  const base = obj.mesh.vertices.length;
131
- const normalAt = (F, i) => {
148
+
149
+ // Smooth shading: a normal per face corner (split faces get midpoints' normals by averaging)
150
+ const normalAt = (corners, F, i) => {
132
151
  if (i < base) return corners[F][sourceFaces[F].indexOf(i)];
133
152
  const [a, b] = split.mids[i - base];
134
- const na = normalAt(F, a), nb = normalAt(F, b);
153
+ const na = normalAt(corners, F, a), nb = normalAt(corners, F, b);
135
154
  return normalize({ x: na.x + nb.x, y: na.y + nb.y, z: na.z + nb.z });
136
155
  };
137
156
  const lightAt = (n, back) => {
@@ -140,47 +159,69 @@ export class CanvasRenderer {
140
159
  const d = (back ? -1 : 1) * (r.x * light.x + r.y * light.y + r.z * light.z);
141
160
  return ambient + (1 - ambient) * Math.max(0, d);
142
161
  };
143
-
144
- // Textures: each triangle is filled with the image, mapped by its corners' texture coordinates
145
- const meshUVs = obj.mesh.uvs, faceUVs = obj.mesh.faceUVs;
146
- let tex = null;
147
- if (mat.texture && textureReady(mat.texture)) {
148
- if (faceUVs && meshUVs) tex = mat.texture;
149
- else if (!this._warned.has(obj.mesh)) {
150
- this._warned.add(obj.mesh);
151
- console.warn("Trid: this mesh has no texture coordinates (uvs / faceUVs), so its texture isn't drawn");
152
- }
153
- }
154
- const uvAt = (F, i) => {
155
- const t = faceUVs?.[F];
156
- if (!t || !meshUVs) return null;
157
- if (i < base) return meshUVs[t[sourceFaces[F].indexOf(i)]];
158
- const [a, b] = split.mids[i - base];
159
- const ta = uvAt(F, a), tb = uvAt(F, b);
160
- return ta && tb ? { u: (ta.u + tb.u) / 2, v: (ta.v + tb.v) / 2 } : null;
161
- };
162
162
  const flatLight = (face, back) => {
163
163
  const nw = normalize(newell(face.map((i) => world[i])));
164
164
  const d = (back ? -1 : 1) * (nw.x * light.x + nw.y * light.y + nw.z * light.z);
165
165
  return ambient + (1 - ambient) * Math.max(0, d);
166
166
  };
167
-
168
- // Effects run once per face, at its center (see trid/render/effects.js)
169
- const effects = mat.effects?.length ? mat.effects : null;
170
167
  const toWorldNormal = (n) =>
171
168
  normalize(rotateXYZ({ x: n.x / obj.scale.x, y: n.y / obj.scale.y, z: n.z / obj.scale.z }, obj.rot));
172
169
 
170
+ // Everything a material needs, worked out once per object and frame
171
+ const prepare = (mat) => {
172
+ const shade = mat.shade !== false;
173
+ const crease = shade ? creaseOf(mat.smooth) : null;
174
+ // Texture coordinates: the mesh's own, or made from its shape (mat.mapping)
175
+ const { uvs: meshUVs, faceUVs } = uvsFor(obj.mesh, mat.mapping);
176
+ const xf = uvTransform(mat);
177
+ const rawUV = (F, i) => {
178
+ const t = faceUVs?.[F];
179
+ if (!t || !meshUVs) return null;
180
+ if (i < base) return meshUVs[t[sourceFaces[F].indexOf(i)]];
181
+ const [a, b] = split.mids[i - base];
182
+ const ta = rawUV(F, a), tb = rawUV(F, b);
183
+ return ta && tb ? { u: (ta.u + tb.u) / 2, v: (ta.v + tb.v) / 2 } : null;
184
+ };
185
+ let tex = null;
186
+ if (mat.texture && textureReady(mat.texture)) {
187
+ if (faceUVs && meshUVs) tex = mat.texture;
188
+ else if (!this._warned.has(obj.mesh)) {
189
+ this._warned.add(obj.mesh);
190
+ console.warn("Trid: this mesh has no texture coordinates (uvs / faceUVs), so its texture isn't drawn. Set material.mapping to make some.");
191
+ }
192
+ }
193
+ return {
194
+ mat, shade, tex,
195
+ fill: mat.fill ?? (mat.texture ? "#ffffff" : scene.color),
196
+ outline: mat.wireframe ? color : null,
197
+ corners: crease ? cornerNormals(obj.mesh, crease) : null,
198
+ effects: mat.effects?.length ? mat.effects : null,
199
+ uvAt: (F, i) => applyUVTransform(xf, rawUV(F, i)),
200
+ };
201
+ };
202
+ // Which material each face uses (mesh.faceMaterials), prepared when first needed
203
+ const looks = new Array(mats.length);
204
+ const faceMats = obj.mesh.faceMaterials;
205
+ const lookOf = (F) => {
206
+ let k = faceMats?.[F] ?? 0;
207
+ if (!(k >= 0 && k < mats.length && mats[k])) k = 0;
208
+ return (looks[k] ??= prepare(mats[k]));
209
+ };
210
+
173
211
  const faces = split ? split.faces : sourceFaces;
174
212
  for (let f = 0; f < faces.length; f++) {
175
213
  const face = faces[f];
176
214
  if (face.length < 3) continue;
215
+ const F = split ? split.origins[f] : f; // the mesh face this one comes from
216
+ const L = lookOf(F);
217
+ const { fill, shade, outline, corners } = L;
177
218
  const pts = face.map((i) => cam[i]);
178
219
 
179
220
  // Seen from behind? (camera sits at the origin of camera space)
180
221
  const n = newell(pts);
181
222
  const c = centroid(pts);
182
223
  const back = n.x * c.x + n.y * c.y + n.z * c.z >= 0;
183
- if (back && !mat.doubleSided) continue;
224
+ if (back && !L.mat.doubleSided) continue;
184
225
 
185
226
  const clipped = clipPolygon(pts, near);
186
227
  if (clipped.length < 3) continue;
@@ -188,13 +229,13 @@ export class CanvasRenderer {
188
229
  // Which edges are real outlines (split faces have extra inner edges); null = all
189
230
  const edges = split && clipped === pts ? split.masks[f] : null;
190
231
 
191
- if (tex && clipped === pts) {
192
- const F = split ? split.origins[f] : f;
193
- const ts = face.map((i) => uvAt(F, i));
232
+ // Textures: each triangle is filled with the image, mapped by its corners' texture coordinates
233
+ if (L.tex && clipped === pts) {
234
+ const ts = face.map((i) => L.uvAt(F, i));
194
235
  if (ts.every(Boolean)) {
195
236
  let lit;
196
237
  if (!shade) lit = face.map(() => 1);
197
- else if (corners) lit = face.map((i) => lightAt(normalAt(F, i), back));
238
+ else if (corners) lit = face.map((i) => lightAt(normalAt(corners, F, i), back));
198
239
  else lit = face.map(() => flatLight(face, back));
199
240
  const screen = pts.map(toScreen);
200
241
  const n = face.length;
@@ -203,7 +244,7 @@ export class CanvasRenderer {
203
244
  items.push({
204
245
  depth: c.z,
205
246
  poly: [screen[0], screen[k], screen[k + 1]],
206
- texture: { src: tex, uvs: [ts[0], ts[k], ts[k + 1]], pixelated: !!mat.pixelated, tint: fill },
247
+ texture: { src: L.tex, uvs: [ts[0], ts[k], ts[k + 1]], pixelated: !!L.mat.pixelated, tint: fill },
207
248
  lit: [lit[0], lit[k], lit[k + 1]],
208
249
  outline, lineWidth,
209
250
  edges: n === 3 ? edges : fan,
@@ -213,12 +254,12 @@ export class CanvasRenderer {
213
254
  }
214
255
  }
215
256
 
216
- if (effects) {
257
+ // Effects run once per face, at its center (see trid/render/effects.js)
258
+ if (L.effects) {
217
259
  // Normal: the face's own, or the average of its corners when smooth
218
260
  let nw;
219
261
  if (corners) {
220
- const F = split ? split.origins[f] : f;
221
- const sum = face.map((i) => toWorldNormal(normalAt(F, i)))
262
+ const sum = face.map((i) => toWorldNormal(normalAt(corners, F, i)))
222
263
  .reduce((a, b) => ({ x: a.x + b.x, y: a.y + b.y, z: a.z + b.z }), { x: 0, y: 0, z: 0 });
223
264
  nw = normalize(sum);
224
265
  } else {
@@ -230,16 +271,16 @@ export class CanvasRenderer {
230
271
  const toCam = { x: camPos.x - position.x, y: camPos.y - position.y, z: camPos.z - position.z };
231
272
  const diffuse = Math.max(0, nw.x * light.x + nw.y * light.y + nw.z * light.z);
232
273
  const lit = shade ? ambient + (1 - ambient) * diffuse : 1;
233
- const base = this._rgba(ctx, fill);
274
+ const rgba = this._rgba(ctx, fill);
234
275
  const s = {
235
- base, normal: nw, position, local: centroid(face.map((i) => verts[i])),
276
+ base: rgba, normal: nw, position, local: centroid(face.map((i) => verts[i])),
236
277
  view: normalize(toCam), lightDir: light, diffuse, light: lit, ambient,
237
- uv: centerUV(face.map((i) => uvAt(split ? split.origins[f] : f, i))),
278
+ uv: centerUV(face.map((i) => L.uvAt(F, i))),
238
279
  depth: Math.hypot(toCam.x, toCam.y, toCam.z), time, background,
239
280
  rgba: (v) => (typeof v === "string" ? this._rgba(ctx, v) : Array.isArray(v) ? v : [v.x, v.y, v.z, 1]),
240
281
  };
241
- let col = [base[0] * lit, base[1] * lit, base[2] * lit, base[3]];
242
- for (const e of effects) if (e.canvas) col = e.canvas(col, s, e.uniforms ?? {});
282
+ let col = [rgba[0] * lit, rgba[1] * lit, rgba[2] * lit, rgba[3]];
283
+ for (const e of L.effects) if (e.canvas) col = e.canvas(col, s, e.uniforms ?? {});
243
284
  const k = col.map((x) => Math.min(1, Math.max(0, x)));
244
285
  items.push({
245
286
  depth: c.z,
@@ -251,8 +292,7 @@ export class CanvasRenderer {
251
292
  }
252
293
 
253
294
  if (corners) {
254
- const F = split ? split.origins[f] : f;
255
- const lit = face.map((i) => lightAt(normalAt(F, i), back));
295
+ const lit = face.map((i) => lightAt(normalAt(corners, F, i), back));
256
296
  if (clipped === pts) {
257
297
  // One gradient per triangle: brightness changes linearly across a triangle
258
298
  const screen = pts.map(toScreen);
@@ -276,14 +316,7 @@ export class CanvasRenderer {
276
316
  continue;
277
317
  }
278
318
 
279
- let brightness = 1;
280
- if (shade) {
281
- const nw = normalize(newell(face.map((i) => world[i])));
282
- const s = back ? -1 : 1;
283
- const d = s * (nw.x * light.x + nw.y * light.y + nw.z * light.z);
284
- brightness = ambient + (1 - ambient) * Math.max(0, d);
285
- }
286
-
319
+ const brightness = shade ? flatLight(face, back) : 1;
287
320
  items.push({
288
321
  depth: c.z,
289
322
  poly: clipped.map(toScreen),
@@ -59,11 +59,11 @@ export function line(ctx, p1, p2, color, width = 4) {
59
59
  * - An edge with 3+ indices is a closed loop: the last index
60
60
  * connects back to the first
61
61
  *
62
- * @param {number[][]} edges - Edge definitions using vertex indices
62
+ * @param {number[][]} [edges] - Edge definitions using vertex indices (none: no segments)
63
63
  * @returns {Generator<[number, number]>} Index pairs to connect
64
64
  */
65
65
  export function* edgeSegments(edges) {
66
- for (const e of edges) {
66
+ for (const e of edges ?? []) {
67
67
  const closed = e.length > 2;
68
68
  const count = closed ? e.length : e.length - 1;
69
69
  for (let i = 0; i < count; i++) {
@@ -72,6 +72,17 @@ export function* edgeSegments(edges) {
72
72
  }
73
73
  }
74
74
 
75
+ /**
76
+ * The lines to draw for a mesh shown as a wireframe: its `edges`, or the
77
+ * outline of each face when it has no `edges` (a face-only mesh).
78
+ *
79
+ * @param {{ edges?: number[][], faces?: number[][] }} mesh
80
+ * @returns {number[][]} Edge definitions (faces are closed loops)
81
+ */
82
+ export function wireEdges(mesh) {
83
+ return mesh.edges ?? mesh.faces ?? [];
84
+ }
85
+
75
86
  /**
76
87
  * Draws a wireframe object by connecting projected vertices
77
88
  * using index-based edge definitions.
@@ -115,7 +126,7 @@ export function drawWireframe(ctx, points2, edges, color, width = 4) {
115
126
  *
116
127
  * @param {CanvasRenderingContext2D} ctx - Canvas 2D context
117
128
  * @param {{
118
- * mesh:{ vertices:{x:number,y:number,z:number}[], edges:number[][] },
129
+ * mesh:{ vertices:{x:number,y:number,z:number}[], edges?:number[][], faces?:number[][] },
119
130
  * pos:{x:number,y:number,z:number},
120
131
  * rot:{x:number,y:number,z:number},
121
132
  * scale:{x:number,y:number,z:number}
@@ -134,7 +145,7 @@ export function drawInstance(ctx, inst, camera, w, h, color, lineWidth = 3) {
134
145
  ctx.strokeStyle = color;
135
146
 
136
147
  ctx.beginPath();
137
- for (const [i, j] of edgeSegments(inst.mesh.edges)) {
148
+ for (const [i, j] of edgeSegments(wireEdges(inst.mesh))) {
138
149
  const seg = clipNear(ptsCam[i], ptsCam[j], near);
139
150
  if (!seg) continue;
140
151
  const a = cameraToScreen(seg[0], camera, w, h);