trid3d 0.4.0 → 0.6.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 +29 -12
- package/package.json +1 -1
- package/trid/index.d.ts +423 -8
- package/trid/index.js +13 -0
- package/trid/math/projection.js +8 -2
- package/trid/math/transform.js +4 -5
- package/trid/modeling/bevel.js +596 -0
- package/trid/modeling/blobs.js +465 -0
- package/trid/modeling/cleanup.js +471 -0
- package/trid/modeling/curves.js +156 -0
- package/trid/modeling/deform.js +191 -0
- package/trid/modeling/frames.js +97 -0
- package/trid/modeling/loft.js +363 -0
- package/trid/modeling/query.js +706 -0
- package/trid/modeling/subdivide.js +215 -0
- package/trid/modeling/sweep.js +169 -0
- package/trid/modeling/vec.js +35 -0
- package/trid/render/canvasRenderer.js +1 -1
- package/trid/render/draw.js +12 -2
- package/trid/render/post.js +1 -1
- package/trid/render/shapes.js +133 -26
- package/trid/render/webglRenderer.js +1 -1
- package/trid/scene.js +1 -1
package/trid/render/shapes.js
CHANGED
|
@@ -13,7 +13,8 @@ function cubeWire(size = 0.5) {
|
|
|
13
13
|
* @param {number} h - height (y)
|
|
14
14
|
* @param {number} d - depth (z)
|
|
15
15
|
*/
|
|
16
|
-
function rectPrismWire(w = 0.6, h = 0.3, d = 0.4) {
|
|
16
|
+
function rectPrismWire(w = 0.6, h = 0.3, d = 0.4, radius = 0, segments = 4) {
|
|
17
|
+
if (radius > 0) return roundedBox(w, h, d, radius, segments);
|
|
17
18
|
const x = w / 2, y = h / 2, z = d / 2;
|
|
18
19
|
|
|
19
20
|
const mesh = {
|
|
@@ -51,6 +52,91 @@ function rectPrismWire(w = 0.6, h = 0.3, d = 0.4) {
|
|
|
51
52
|
return withFaceUVs(mesh);
|
|
52
53
|
}
|
|
53
54
|
|
|
55
|
+
/**
|
|
56
|
+
* A box with rounded edges and corners, centered on the origin: each side is
|
|
57
|
+
* a grid whose outer rows bend around the edges (`segments` rows per
|
|
58
|
+
* rounded edge, evenly spread over the quarter circle). Vertices along the
|
|
59
|
+
* edges are shared, so smooth shading has no seams. Like the plain box,
|
|
60
|
+
* each side has its own material number (front, back, top, bottom, right,
|
|
61
|
+
* left) and the whole texture, upright as seen from outside.
|
|
62
|
+
*/
|
|
63
|
+
function roundedBox(w, h, d, radius, segments) {
|
|
64
|
+
const half = { x: Math.abs(w) / 2, y: Math.abs(h) / 2, z: Math.abs(d) / 2 };
|
|
65
|
+
const r = Math.min(radius, half.x, half.y, half.z);
|
|
66
|
+
const s = Math.max(1, Math.floor(segments));
|
|
67
|
+
const inner = { x: half.x - r, y: half.y - r, z: half.z - r };
|
|
68
|
+
// Grid lines across a side: `s` rows around each rounded edge, one flat span between
|
|
69
|
+
// (where the edges meet, as on a cube of radius = half its size, the flat span is skipped)
|
|
70
|
+
const ticks = (a) => {
|
|
71
|
+
const arc = Array.from({ length: s }, (_, i) => Math.tan((Math.PI / 4) * (1 - i / s)) * r);
|
|
72
|
+
const lo = arc.map((t) => -inner[a] - t), hi = [...arc].reverse().map((t) => inner[a] + t);
|
|
73
|
+
return inner[a] > 1e-12 ? [...lo, -inner[a], inner[a], ...hi] : [...lo, 0, ...hi];
|
|
74
|
+
};
|
|
75
|
+
// The point on the flat side pulled onto the rounded surface
|
|
76
|
+
const round = (p) => {
|
|
77
|
+
const c = { x: Math.max(-inner.x, Math.min(inner.x, p.x)), y: Math.max(-inner.y, Math.min(inner.y, p.y)), z: Math.max(-inner.z, Math.min(inner.z, p.z)) };
|
|
78
|
+
const dx = p.x - c.x, dy = p.y - c.y, dz = p.z - c.z, l = Math.hypot(dx, dy, dz);
|
|
79
|
+
return l > 1e-12 ? { x: c.x + (dx / l) * r, y: c.y + (dy / l) * r, z: c.z + (dz / l) * r } : p;
|
|
80
|
+
};
|
|
81
|
+
// Sides in the plain box's order: front (-z), back (+z), top, bottom, right (+x), left (-x).
|
|
82
|
+
// The texture goes like on the plain box (withFaceUVs): up is +Y (±Z on the top and bottom), right = normal × up
|
|
83
|
+
const axisOf = (v) => (v.x ? ["x", Math.sign(v.x)] : v.y ? ["y", Math.sign(v.y)] : ["z", Math.sign(v.z)]);
|
|
84
|
+
const sides = [["z", -1], ["z", 1], ["y", 1], ["y", -1], ["x", 1], ["x", -1]].map(([n, sign]) => {
|
|
85
|
+
const nv = { x: 0, y: 0, z: 0 };
|
|
86
|
+
nv[n] = sign;
|
|
87
|
+
const up = n === "y" ? { x: 0, y: 0, z: sign } : { x: 0, y: 1, z: 0 };
|
|
88
|
+
const right = { x: nv.y * up.z - nv.z * up.y, y: nv.z * up.x - nv.x * up.z, z: nv.x * up.y - nv.y * up.x };
|
|
89
|
+
return { n, sign, u: axisOf(right), v: axisOf(up) };
|
|
90
|
+
});
|
|
91
|
+
const vertices = [], faces = [], uvs = [], faceUVs = [], faceMaterials = [];
|
|
92
|
+
const shared = new Map();
|
|
93
|
+
const key = (p) => `${p.x.toFixed(9)},${p.y.toFixed(9)},${p.z.toFixed(9)}`;
|
|
94
|
+
sides.forEach((side, m) => {
|
|
95
|
+
const [ua, us] = side.u, [va, vs] = side.v;
|
|
96
|
+
const tu = ticks(ua), tv = ticks(va);
|
|
97
|
+
const ids = [], uvIds = [];
|
|
98
|
+
for (const b of tv) {
|
|
99
|
+
for (const a of tu) {
|
|
100
|
+
const p = { x: 0, y: 0, z: 0 };
|
|
101
|
+
p[side.n] = side.sign * half[side.n];
|
|
102
|
+
p[ua] = a;
|
|
103
|
+
p[va] = b;
|
|
104
|
+
const q = round(p);
|
|
105
|
+
const k = key(q);
|
|
106
|
+
if (!shared.has(k)) shared.set(k, vertices.push(q) - 1);
|
|
107
|
+
ids.push(shared.get(k));
|
|
108
|
+
uvIds.push(uvs.push({ u: (us * a + half[ua]) / (2 * half[ua] || 1), v: (vs * b + half[va]) / (2 * half[va] || 1) }) - 1);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const nu = tu.length;
|
|
112
|
+
for (let j = 0; j + 1 < tv.length; j++) {
|
|
113
|
+
for (let i = 0; i + 1 < nu; i++) {
|
|
114
|
+
const c = [j * nu + i, j * nu + i + 1, (j + 1) * nu + i + 1, (j + 1) * nu + i];
|
|
115
|
+
// Clockwise from outside
|
|
116
|
+
const p = c.map((t) => vertices[ids[t]]);
|
|
117
|
+
const e1 = { x: p[1].x - p[0].x, y: p[1].y - p[0].y, z: p[1].z - p[0].z };
|
|
118
|
+
const e2 = { x: p[3].x - p[0].x, y: p[3].y - p[0].y, z: p[3].z - p[0].z };
|
|
119
|
+
const nz = { x: e1.y * e2.z - e1.z * e2.y, y: e1.z * e2.x - e1.x * e2.z, z: e1.x * e2.y - e1.y * e2.x }[side.n] * side.sign;
|
|
120
|
+
const order = nz < 0 ? [...c].reverse() : c;
|
|
121
|
+
faces.push(order.map((t) => ids[t]));
|
|
122
|
+
faceUVs.push(order.map((t) => uvIds[t]));
|
|
123
|
+
faceMaterials.push(m);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
// Faces that collapsed where grid points met (a cube rounded into a ball) are dropped
|
|
128
|
+
const keep = faces.map((f) => new Set(f).size >= 3);
|
|
129
|
+
const out = {
|
|
130
|
+
vertices,
|
|
131
|
+
faces: faces.filter((_, i) => keep[i]).map((f) => f.filter((v, i) => v !== f[(i + f.length - 1) % f.length])),
|
|
132
|
+
uvs,
|
|
133
|
+
faceUVs: faceUVs.filter((_, i) => keep[i]),
|
|
134
|
+
faceMaterials: faceMaterials.filter((_, i) => keep[i]),
|
|
135
|
+
};
|
|
136
|
+
out.faceUVs = out.faceUVs.map((uv, i) => (uv.length === out.faces[i].length ? uv : uv.slice(0, out.faces[i].length)));
|
|
137
|
+
return out;
|
|
138
|
+
}
|
|
139
|
+
|
|
54
140
|
/**
|
|
55
141
|
* Gives every face the whole texture, upright as seen from outside:
|
|
56
142
|
* side faces have +Y up, the top face has +Z up, the bottom face -Z.
|
|
@@ -163,6 +249,9 @@ function sphereWire(radius = 0.35, rings = 10, segments = 16) {
|
|
|
163
249
|
return { vertices, edges, faces, uvs, faceUVs };
|
|
164
250
|
}
|
|
165
251
|
|
|
252
|
+
/** Profile points closer to the axis than this are poles (one vertex). */
|
|
253
|
+
const POLE = 1e-9;
|
|
254
|
+
|
|
166
255
|
/**
|
|
167
256
|
* Spins a profile around the Y axis: vases, bottles, cups, columns, chess
|
|
168
257
|
* pieces. Cylinders, cones and tori are built with it.
|
|
@@ -170,6 +259,9 @@ function sphereWire(radius = 0.35, rings = 10, segments = 16) {
|
|
|
170
259
|
* Walk the profile so the outside is on its right in the (r, y) plane:
|
|
171
260
|
* bottom to top for a vase. Faces are clockwise as seen from outside.
|
|
172
261
|
*
|
|
262
|
+
* A point on the axis (`r: 0`) is a pole: one vertex, with triangles around
|
|
263
|
+
* it, like a cone's tip or the closed end of a spinning top.
|
|
264
|
+
*
|
|
173
265
|
* Texture coordinates: u goes around, v follows the profile's length.
|
|
174
266
|
* Caps map the texture as a disc. Edges are the profile's rings plus one
|
|
175
267
|
* line along the profile per segment.
|
|
@@ -206,52 +298,67 @@ export function lathe(profile, { segments = 32, capBottom = false, capTop = fals
|
|
|
206
298
|
}
|
|
207
299
|
const total = lengths[lengths.length - 1] || 1;
|
|
208
300
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
301
|
+
// One ring of vertices per profile point, or a single vertex for a pole (on the axis)
|
|
302
|
+
const pole = profile.map((p) => !(Math.abs(p.r) > POLE));
|
|
303
|
+
const start = [];
|
|
304
|
+
for (const [r, p] of profile.entries()) {
|
|
305
|
+
start.push(vertices.length);
|
|
306
|
+
if (pole[r]) vertices.push({ x: 0, y: p.y, z: 0 });
|
|
307
|
+
else {
|
|
308
|
+
for (let s = 0; s < segments; s++) {
|
|
309
|
+
const t = (s / segments) * Math.PI * 2;
|
|
310
|
+
vertices.push({ x: Math.cos(t) * p.r, y: p.y, z: Math.sin(t) * p.r });
|
|
311
|
+
}
|
|
213
312
|
}
|
|
214
313
|
}
|
|
314
|
+
const at = (r, s) => (pole[r] ? start[r] : start[r] + (s % segments));
|
|
215
315
|
|
|
216
316
|
const rows = pts.length - 1;
|
|
217
317
|
for (let r = 0; r < rows; r++) {
|
|
218
|
-
const
|
|
219
|
-
|
|
318
|
+
const next = (r + 1) % profile.length;
|
|
319
|
+
if (pole[r] && pole[next]) continue; // along the axis: nothing to draw
|
|
220
320
|
for (let s = 0; s < segments; s++) {
|
|
221
|
-
const
|
|
222
|
-
faces.push([a + s, b + s, b + s1, a + s1]);
|
|
223
|
-
const u0 = s / segments, u1 = (s + 1) / segments;
|
|
321
|
+
const u0 = s / segments, u1 = (s + 1) / segments, um = (u0 + u1) / 2;
|
|
224
322
|
const v0 = lengths[r] / total, v1 = lengths[r + 1] / total;
|
|
225
|
-
|
|
323
|
+
// Next to a pole, a triangle; its corner there sits in the middle of u
|
|
324
|
+
if (pole[r]) {
|
|
325
|
+
faces.push([at(r, s), at(next, s), at(next, s + 1)]);
|
|
326
|
+
faceUVs.push([uv(um, v0), uv(u0, v1), uv(u1, v1)]);
|
|
327
|
+
} else if (pole[next]) {
|
|
328
|
+
faces.push([at(r, s), at(next, s), at(r, s + 1)]);
|
|
329
|
+
faceUVs.push([uv(u0, v0), uv(um, v1), uv(u1, v0)]);
|
|
330
|
+
} else {
|
|
331
|
+
faces.push([at(r, s), at(next, s), at(next, s + 1), at(r, s + 1)]);
|
|
332
|
+
faceUVs.push([uv(u0, v0), uv(u0, v1), uv(u1, v1), uv(u1, v0)]);
|
|
333
|
+
}
|
|
226
334
|
}
|
|
227
335
|
}
|
|
228
336
|
|
|
229
|
-
// Rings (
|
|
337
|
+
// Rings (none at a pole) and lines along the profile
|
|
230
338
|
profile.forEach((p, r) => {
|
|
231
|
-
if (
|
|
339
|
+
if (!pole[r]) edges.push(Array.from({ length: segments }, (_, s) => at(r, s)));
|
|
232
340
|
});
|
|
233
341
|
for (let s = 0; s < segments; s++) {
|
|
234
|
-
if (closed && profile.length > 2) edges.push(profile.map((_, r) => r
|
|
235
|
-
else for (let r = 0; r < profile.length - 1; r++)
|
|
342
|
+
if (closed && profile.length > 2) edges.push(profile.map((_, r) => at(r, s)));
|
|
343
|
+
else for (let r = 0; r < profile.length - 1; r++) if (!(pole[r] && pole[r + 1])) edges.push([at(r, s), at(r + 1, s)]);
|
|
236
344
|
}
|
|
237
345
|
|
|
238
346
|
const disc = (s) => {
|
|
239
347
|
const t = (s / segments) * Math.PI * 2;
|
|
240
348
|
return uv(0.5 + Math.cos(t) * 0.5, 0.5 + Math.sin(t) * 0.5);
|
|
241
349
|
};
|
|
242
|
-
if (capBottom && !closed &&
|
|
350
|
+
if (capBottom && !closed && !pole[0]) {
|
|
243
351
|
const c = vertices.push({ x: 0, y: profile[0].y, z: 0 }) - 1;
|
|
244
352
|
for (let s = 0; s < segments; s++) {
|
|
245
|
-
faces.push([c, s, (s + 1)
|
|
353
|
+
faces.push([c, at(0, s), at(0, s + 1)]);
|
|
246
354
|
faceUVs.push([uv(0.5, 0.5), disc(s), disc(s + 1)]);
|
|
247
355
|
}
|
|
248
356
|
}
|
|
249
357
|
const last = profile.length - 1;
|
|
250
|
-
if (capTop && !closed &&
|
|
358
|
+
if (capTop && !closed && !pole[last]) {
|
|
251
359
|
const c = vertices.push({ x: 0, y: profile[last].y, z: 0 }) - 1;
|
|
252
|
-
const a = last * segments;
|
|
253
360
|
for (let s = 0; s < segments; s++) {
|
|
254
|
-
faces.push([c,
|
|
361
|
+
faces.push([c, at(last, s + 1), at(last, s)]);
|
|
255
362
|
faceUVs.push([uv(0.5, 0.5), disc(s + 1), disc(s)]);
|
|
256
363
|
}
|
|
257
364
|
}
|
|
@@ -264,11 +371,9 @@ function cylinder(r = 0.3, h = 0.8, segments = 32) {
|
|
|
264
371
|
return lathe([{ r, y: -h / 2 }, { r, y: h / 2 }], { segments, capBottom: true, capTop: true });
|
|
265
372
|
}
|
|
266
373
|
|
|
267
|
-
/** A cone along Y with its tip up, or a tapered cylinder when `rTop` > 0. */
|
|
374
|
+
/** A cone along Y with its tip up (a single point, so it's closed), or a tapered cylinder when `rTop` > 0. */
|
|
268
375
|
function cone(r = 0.35, h = 0.8, segments = 32, rTop = 0) {
|
|
269
|
-
|
|
270
|
-
const profile = [{ r, y: -h / 2 }, { r: rTop > 0 ? rTop : 1e-6, y: h / 2 }];
|
|
271
|
-
return lathe(profile, { segments, capBottom: true, capTop: rTop > 0 });
|
|
376
|
+
return lathe([{ r, y: -h / 2 }, { r: rTop > 0 ? rTop : 0, y: h / 2 }], { segments, capBottom: true, capTop: rTop > 0 });
|
|
272
377
|
}
|
|
273
378
|
|
|
274
379
|
/** A ring in the XZ plane: `R` from the center to the middle of the tube. */
|
|
@@ -548,8 +653,10 @@ export function scaleMesh(mesh, sx = 1, sy = 1, sz = 1) {
|
|
|
548
653
|
*/
|
|
549
654
|
export function makeShape(kind, opts = {}) {
|
|
550
655
|
switch (kind.toLowerCase()) {
|
|
551
|
-
case "cube": return
|
|
552
|
-
|
|
656
|
+
case "cube": return opts.radius > 0
|
|
657
|
+
? rectPrismWire(opts.size ?? 0.5, opts.size ?? 0.5, opts.size ?? 0.5, opts.radius, opts.segments ?? 4)
|
|
658
|
+
: cubeWire(opts.size ?? 0.5);
|
|
659
|
+
case "box": return rectPrismWire(opts.w ?? 0.6, opts.h ?? 0.6, opts.d ?? 0.6, opts.radius ?? 0, opts.segments ?? 4);
|
|
553
660
|
case "sphere":return sphereWire(opts.r ?? 0.45, opts.rings ?? 12, opts.segments ?? 24);
|
|
554
661
|
case "cylinder": return cylinder(opts.r, opts.h, opts.segments);
|
|
555
662
|
case "cone": return cone(opts.r, opts.h, opts.segments, opts.rTop);
|
|
@@ -343,7 +343,7 @@ export class WebGLRenderer {
|
|
|
343
343
|
// Post effects: the ones with GLSL run here, canvas-only ones after the copy
|
|
344
344
|
const post = activePost(scene.post);
|
|
345
345
|
const gpuPost = post.filter((e) => postPasses(e).length);
|
|
346
|
-
const background = this._color(ctx, scene.background);
|
|
346
|
+
const background = this._color(ctx, scene.background ?? "transparent");
|
|
347
347
|
if (gpuPost.length) {
|
|
348
348
|
// Into a texture, on the scene's background so effects see the whole picture
|
|
349
349
|
gl.bindFramebuffer(gl.FRAMEBUFFER, this._sceneTarget(w, h));
|
package/trid/scene.js
CHANGED
|
@@ -37,7 +37,7 @@ export class Scene {
|
|
|
37
37
|
* @param {Object} [opts]
|
|
38
38
|
* @param {number} [opts.width] - Sets canvas.width (keeps the current one if omitted)
|
|
39
39
|
* @param {number} [opts.height] - Sets canvas.height (keeps the current one if omitted)
|
|
40
|
-
* @param {string} [opts.background="#141414"] - Clear color
|
|
40
|
+
* @param {string|null} [opts.background="#141414"] - Clear color; "transparent" or null: see-through, the page shows behind
|
|
41
41
|
* @param {string} [opts.color="#ffffff"] - Default wireframe color
|
|
42
42
|
* @param {number} [opts.lineWidth=3] - Default wireframe width
|
|
43
43
|
* @param {import("./math/transform.js").Camera} [opts.camera={ z: -1 }] - Camera
|