Skill Tracer
Skills › Anatomy › anatomy › lathe.mjs

lathe.mjs

1184 lines · kit/lathe.mjs at 1482e84

1import { cameraOf, iso, pathOf } from "./iso-kit.mjs";
2
3const TAU = Math.PI * 2;
4const LIGHT_UP = 1.2;
5const BANDS = [0, 0.2, 0.42, 0.62];
6
7export const dot3 = (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
8export const add3 = (a, b) => [a[0] + b[0], a[1] + b[1], a[2] + b[2]];
9export const sub3 = (a, b) => [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
10export const mul3 = (a, k) => [a[0] * k, a[1] * k, a[2] * k];
11export const len3 = (a) => Math.hypot(a[0], a[1], a[2]);
12export const unit3 = (a) => {
13 const length = len3(a) || 1;
14 return [a[0] / length, a[1] / length, a[2] / length];
15};
16export const cross3 = (a, b) => [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]];
17export const lerp3 = (a, b, t) => [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t, a[2] + (b[2] - a[2]) * t];
19export function viewOf(P) {
20 const { sinA, cosA, sinE, cosE } = cameraOf(P);
21 return [cosA * cosE, sinA * cosE, sinE];
24export function lightOf(P) {
25 const { sinA, cosA } = cameraOf(P);
26 return unit3([-sinA, cosA, LIGHT_UP]);
29export const scaleOf = (P) => cameraOf(P).k;
31export function bandOf(score) {
32 let tone = 0;
33 while (tone < BANDS.length && score >= BANDS[tone]) tone++;
34 return tone;
37export const toneOf = (normal, P) => bandOf(dot3(unit3(normal), lightOf(P)));
39export function frameAlong(o, a) {
40 const axis = unit3(a);
41 const helper = Math.abs(axis[2]) > 0.9 ? [1, 0, 0] : [0, 0, 1];
42 const u = unit3(cross3(helper, axis));
43 const v = cross3(axis, u);
44 return { o, a: axis, u, v };
47export const frameOf = (o, a, u, v) => ({ o, a, u, v });
49export function pointOf(F, s, r, angle) {
50 const c = Math.cos(angle);
51 const n = Math.sin(angle);
52 return [
53 F.o[0] + F.a[0] * s + r * (c * F.u[0] + n * F.v[0]),
54 F.o[1] + F.a[1] * s + r * (c * F.u[1] + n * F.v[1]),
55 F.o[2] + F.a[2] * s + r * (c * F.u[2] + n * F.v[2]),
56 ];
59export const radialOf = (F, angle) => {
60 const c = Math.cos(angle);
61 const n = Math.sin(angle);
62 return [c * F.u[0] + n * F.v[0], c * F.u[1] + n * F.v[1], c * F.u[2] + n * F.v[2]];
63};
65export const tangentOf = (F, angle) => {
66 const c = Math.cos(angle);
67 const n = Math.sin(angle);
68 return [-n * F.u[0] + c * F.v[0], -n * F.u[1] + c * F.v[1], -n * F.u[2] + c * F.v[2]];
69};
71export const angleToward = (F, direction) => Math.atan2(dot3(direction, F.v), dot3(direction, F.u));
73function hullTagged(points) {
74 const sorted = [...points].sort((a, b) => a.p[0] - b.p[0] || a.p[1] - b.p[1]);
75 if (sorted.length < 3) return sorted;
76 const cross = (o, a, b) => (a.p[0] - o.p[0]) * (b.p[1] - o.p[1]) - (a.p[1] - o.p[1]) * (b.p[0] - o.p[0]);
77 const lower = [];
78 for (const point of sorted) {
79 while (lower.length >= 2 && cross(lower[lower.length - 2], lower[lower.length - 1], point) <= 1e-9) lower.pop();
80 lower.push(point);
81 }
82 const upper = [];
83 for (let index = sorted.length - 1; index >= 0; index--) {
84 const point = sorted[index];
85 while (upper.length >= 2 && cross(upper[upper.length - 2], upper[upper.length - 1], point) <= 1e-9) upper.pop();
86 upper.push(point);
87 }
88 return [...lower.slice(0, -1), ...upper.slice(0, -1)];
91export const hullOf = (points) => hullTagged(points.map((p) => ({ p }))).map(({ p }) => p);
93export function runsOf(flags) {
94 const count = flags.length;
95 const start = flags.findIndex((flag, index) => flag !== flags[(index + count - 1) % count]);
96 if (start < 0) return flags[0] ? [Array.from({ length: count + 1 }, (_, index) => index % count)] : [];
97 const runs = [];
98 let run = null;
99 for (let step = 0; step < count; step++) {
100 const index = (start + step) % count;
101 if (flags[index]) {
102 if (!run) run = [index];
103 run.push((index + 1) % count);
104 } else if (run) {
105 runs.push(run);
106 run = null;
107 }
108 }
109 if (run) runs.push(run);
110 return runs;
113function groupsOf(keys) {
114 const count = keys.length;
115 const out = [];
116 const start = keys.findIndex((key, index) => key !== keys[(index + count - 1) % count]);
117 if (start < 0) {
118 if (keys[0] !== null) out.push({ key: keys[0], run: Array.from({ length: count + 1 }, (_, index) => index % count) });
119 return out;
120 }
121 let current = null;
122 for (let step = 0; step < count; step++) {
123 const index = (start + step) % count;
124 const key = keys[index];
125 if (current && current.key === key) {
126 current.run.push((index + 1) % count);
127 continue;
128 }
129 if (current && current.key !== null) out.push(current);
130 current = { key, run: [index, (index + 1) % count] };
131 }
132 if (current && current.key !== null) out.push(current);
133 return out;
136export const stepsFor = (radius, least = 16, most = 112, scale = 0) => Math.max(least, Math.min(most, scale ? Math.ceil((TAU * radius * scale) / 3) : Math.round((radius * 1.3) / 4) * 4));
138function chains(edges) {
139 const out = [];
140 let current = null;
141 for (const [a, b, keep] of edges) {
142 if (!keep) {
143 if (current) out.push(current);
144 current = null;
145 continue;
146 }
147 if (!current) current = [a];
148 current.push(b);
149 }
150 if (current) out.push(current);
151 if (out.length > 1 && edges[0][2] && edges[edges.length - 1][2]) {
152 const last = out.pop();
153 out[0] = [...last, ...out[0].slice(1)];
154 }
155 return out;
158function labelledArcs(nr, na, view, light, gr = nr, ga = na) {
159 const cuts = [];
160 if (Math.abs(gr) > 1e-9) {
161 const visibleCut = -(ga * view.a) / (gr * view.flat);
162 if (visibleCut > -1 && visibleCut < 1) {
163 const d = Math.acos(visibleCut);
164 cuts.push(view.phi - d, view.phi + d);
165 }
166 }
167 if (Math.abs(nr) > 1e-9) {
168 for (const threshold of BANDS) {
169 const c = (threshold - na * light.a) / (nr * light.flat);
170 if (c > -1 && c < 1) {
171 const d = Math.acos(c);
172 cuts.push(light.phi - d, light.phi + d);
173 }
174 }
175 }
176 const wrap = (angle) => ((angle % TAU) + TAU) % TAU;
177 const sorted = [...new Set(cuts.map((angle) => Math.round(wrap(angle) * 1e9) / 1e9))].sort((a, b) => a - b);
178 const label = (angle) => {
179 const facing = gr * view.flat * Math.cos(angle - view.phi) + ga * view.a;
180 if (facing <= 1e-9) return -1;
181 return bandOf(nr * light.flat * Math.cos(angle - light.phi) + na * light.a);
182 };
183 if (!sorted.length) {
184 const tone = label(0);
185 return tone < 0 ? [] : [{ tone, from: 0, to: TAU, full: true }];
186 }
187 const arcs = sorted.map((from, index) => {
188 const to = index + 1 < sorted.length ? sorted[index + 1] : sorted[0] + TAU;
189 return { tone: label((from + to) / 2), from, to };
190 });
191 const merged = [];
192 for (const arc of arcs) {
193 const last = merged[merged.length - 1];
194 if (last && last.tone === arc.tone && Math.abs(last.to - arc.from) < 1e-9) last.to = arc.to;
195 else merged.push({ ...arc });
196 }
197 if (merged.length > 1 && merged[0].tone === merged[merged.length - 1].tone && Math.abs(merged[merged.length - 1].to - TAU - merged[0].from) < 1e-9) {
198 const last = merged.pop();
199 merged[0].from = last.from - TAU;
200 }
201 return merged.filter((arc) => arc.tone >= 0);
204function hermiteSlopes(run) {
205 const n = run.length - 1;
206 const h = Array.from({ length: n }, (_, j) => run[j + 1][0] - run[j][0]);
207 const delta = h.map((value, j) => (run[j + 1][1] - run[j][1]) / value);
208 const d = new Array(n + 1).fill(0);
209 const end = (h0, h1, d0, d1) => {
210 let value = ((2 * h0 + h1) * d0 - h0 * d1) / (h0 + h1);
211 if (Math.sign(value) !== Math.sign(d0)) value = 0;
212 else if (Math.sign(d0) !== Math.sign(d1) && Math.abs(value) > Math.abs(3 * d0)) value = 3 * d0;
213 return value;
214 };
215 if (n === 1) d[0] = d[1] = delta[0];
216 else {
217 d[0] = end(h[0], h[1], delta[0], delta[1]);
218 d[n] = end(h[n - 1], h[n - 2], delta[n - 1], delta[n - 2]);
219 }
220 for (let k = 1; k < n; k++) {
221 if (delta[k - 1] * delta[k] <= 0) continue;
222 const w1 = 2 * h[k] + h[k - 1];
223 const w2 = h[k] + 2 * h[k - 1];
224 d[k] = (w1 + w2) / (w1 / delta[k - 1] + w2 / delta[k]);
225 }
226 return d;
229export function curveRuns(meridian) {
230 const m = meridian.length;
231 const segments = new Array(m - 1).fill(null);
232 let start = 0;
233 while (start < m - 1) {
234 let end = start + 1;
235 while (end < m - 1 && meridian[end][2] && Math.abs(meridian[end + 1][0] - meridian[end][0]) > 1e-9 && Math.abs(meridian[end][0] - meridian[end - 1][0]) > 1e-9) end++;
236 const run = meridian.slice(start, end + 1);
237 const n = run.length - 1;
238 if (n >= 2 && run.every((point, j) => j === 0 || Math.abs(point[0] - run[j - 1][0]) > 1e-9)) {
239 const d = hermiteSlopes(run);
240 for (let j = 0; j < n; j++) {
241 const [s0, r0] = run[j];
242 const [s1, r1] = run[j + 1];
243 const hj = s1 - s0;
244 const d0 = d[j];
245 const d1 = d[j + 1];
246 const radius = (s) => {
247 const t = (s - s0) / hj;
248 return (2 * t ** 3 - 3 * t ** 2 + 1) * r0 + (t ** 3 - 2 * t ** 2 + t) * hj * d0 + (-2 * t ** 3 + 3 * t ** 2) * r1 + (t ** 3 - t ** 2) * hj * d1;
249 };
250 const slope = (s) => {
251 const t = (s - s0) / hj;
252 return ((6 * t ** 2 - 6 * t) * r0 + (6 * t - 6 * t ** 2) * r1) / hj + (3 * t ** 2 - 4 * t + 1) * d0 + (3 * t ** 2 - 2 * t) * d1;
253 };
254 segments[start + j] = { radius, slope };
255 }
256 }
257 start = end;
258 }
259 return segments;
262function curved(meridian, P, segments, { error = 0.2, depth = 6 } = {}) {
263 const k = scaleOf(P);
264 const out = [meridian[0]];
265 for (let i = 0; i < meridian.length - 1; i++) {
266 const curve = segments[i];
267 const [s0] = meridian[i];
268 const [s1] = meridian[i + 1];
269 if (curve) {
270 const split = (a, b, level) => {
271 const mid = (a + b) / 2;
272 const chord = (curve.radius(a) + curve.radius(b)) / 2;
273 if (level < depth && Math.abs(curve.radius(mid) - chord) * k > error) {
274 split(a, mid, level + 1);
275 out.push([mid, curve.radius(mid), 1]);
276 split(mid, b, level + 1);
277 }
278 };
279 split(s0, s1, 0);
280 }
281 out.push(meridian[i + 1]);
282 }
283 return out;
286function smoothRows(meridian, F, P, { rows = 6, slope = null, radius = null, steps = 48, error = 0.35, depth: deepest = 8, fineness = 0.1 } = {}) {
287 const V = viewOf(P);
288 const L = lightOf(P);
289 const view = { a: dot3(F.a, V), flat: Math.hypot(dot3(F.u, V), dot3(F.v, V)), phi: Math.atan2(dot3(F.v, V), dot3(F.u, V)) };
290 const light = { a: dot3(F.a, L), flat: Math.hypot(dot3(F.u, L), dot3(F.v, L)), phi: Math.atan2(dot3(F.v, L), dot3(F.u, L)) };
291 const m = meridian.length;
292 const faces = [];
293 for (let i = 0; i < m - 1; i++) {
294 const ds = meridian[i + 1][0] - meridian[i][0];
295 const dr = meridian[i + 1][1] - meridian[i][1];
296 const length = Math.hypot(ds, dr) || 1;
297 faces.push([ds / length, -dr / length]);
298 }
299 const splines = slope ? null : curveRuns(meridian);
300 const vertexNormal = (k, own) => {
301 if (k <= 0 || k >= m - 1 || !meridian[k][2]) return own;
302 const a = faces[k - 1];
303 const b = faces[k];
304 const x = a[0] + b[0];
305 const y = a[1] + b[1];
306 const length = Math.hypot(x, y) || 1;
307 return [x / length, y / length];
308 };
309 const normalAt = (i, u) => {
310 const [s0] = meridian[i];
311 const [s1] = meridian[i + 1];
312 const s = s0 + (s1 - s0) * u;
313 const sign = Math.sign(s1 - s0) || 1;
314 const g = slope ? slope(s) : splines[i] ? splines[i].slope(s) : null;
315 if (g !== null) {
316 const length = Math.hypot(1, g);
317 return [sign / length, (-sign * g) / length];
318 }
319 const a = vertexNormal(i, faces[i]);
320 const b = vertexNormal(i + 1, faces[i]);
321 const x = a[0] + (b[0] - a[0]) * u;
322 const y = a[1] + (b[1] - a[1]) * u;
323 const length = Math.hypot(x, y) || 1;
324 return [x / length, y / length];
325 };
326 const pointAt = (i, u, angle) => {
327 const [s0, r0] = meridian[i];
328 const [s1, r1] = meridian[i + 1];
329 const s = s0 + (s1 - s0) * u;
330 return iso(pointOf(F, s, radius ? radius(s) : splines && splines[i] ? splines[i].radius(s) : r0 + (r1 - r0) * u, angle), P);
331 };
332 const buckets = [[], [], [], [], []];
333 const fine = TAU / Math.max(24, Math.min(192, steps * 1.5));
334 const arcPoints = (i, u, from, to) => {
335 const count = Math.max(1, Math.ceil((to - from) / fine));
336 return Array.from({ length: count + 1 }, (_, index) => pointAt(i, u, from + ((to - from) * index) / count));
337 };
338 const centreOf = (arc, reference) => {
339 let mid = (arc.from + arc.to) / 2 - reference;
340 mid = ((mid % TAU) + TAU) % TAU;
341 return mid;
342 };
343 const levelOf = (i, u) => {
344 const [nr, na] = normalAt(i, u);
345 const groups = new Map();
346 for (const arc of labelledArcs(nr, na, view, light, faces[i][0], faces[i][1])) {
347 if (!groups.has(arc.tone)) groups.set(arc.tone, []);
348 groups.get(arc.tone).push(arc);
349 }
350 for (const list of groups.values()) list.sort((x, y) => centreOf(x, light.phi) - centreOf(y, light.phi));
351 return { i, u, groups };
352 };
353 const same = (x, y) => {
354 if (x.groups.size !== y.groups.size) return false;
355 for (const [tone, list] of x.groups) if ((y.groups.get(tone) ?? []).length !== list.length) return false;
356 return true;
357 };
358 const align = (reference, arc) => {
359 const out = { ...arc };
360 const centre = (reference.from + reference.to) / 2;
361 while ((out.from + out.to) / 2 - centre > Math.PI) {
362 out.from -= TAU;
363 out.to -= TAU;
364 }
365 while (centre - (out.from + out.to) / 2 > Math.PI) {
366 out.from += TAU;
367 out.to += TAU;
368 }
369 return out;
370 };
371 const strayOf = (i, a, mid, b) => {
372 let worst = 0;
373 for (const [tone, list] of mid.groups) {
374 list.forEach((arc, index) => {
375 const lo = align(arc, a.groups.get(tone)[index]);
376 const hi = align(arc, b.groups.get(tone)[index]);
377 for (const key of ["from", "to"]) {
378 const p = pointAt(i, mid.u, arc[key]);
379 const p0 = pointAt(i, a.u, lo[key]);
380 const p1 = pointAt(i, b.u, hi[key]);
381 worst = Math.max(worst, Math.hypot(p[0] - (p0[0] + p1[0]) / 2, p[1] - (p0[1] + p1[1]) / 2));
382 }
383 });
384 }
385 return worst;
386 };
387 const sheet = (tone, levels) => {
388 levels[0].groups.get(tone).forEach((arc, index) => {
389 const chain = [arc];
390 for (let k = 1; k < levels.length; k++) chain.push(align(chain[k - 1], levels[k].groups.get(tone)[index]));
391 const last = levels.length - 1;
392 const points = [...arcPoints(levels[0].i, levels[0].u, chain[0].from, chain[0].to)];
393 for (let k = 1; k < last; k++) points.push(pointAt(levels[k].i, levels[k].u, chain[k].to));
394 points.push(...arcPoints(levels[last].i, levels[last].u, chain[last].from, chain[last].to).reverse());
395 for (let k = last - 1; k >= 1; k--) points.push(pointAt(levels[k].i, levels[k].u, chain[k].from));
396 buckets[tone].push(pathOf(oriented(points), true));
397 });
398 };
399 const overlap = (x, y) => Math.min(x.to, y.to) - Math.max(x.from, y.from);
400 const strip = (tone, a, b) => {
401 const below = a.groups.get(tone) ?? [];
402 const above = (b.groups.get(tone) ?? []).map((arc) => ({ arc, used: false }));
403 const quad = (lo, hi) => {
404 const points = [...arcPoints(a.i, a.u, lo.from, lo.to), ...arcPoints(b.i, b.u, hi.from, hi.to).reverse()];
405 buckets[tone].push(pathOf(oriented(points), true));
406 };
407 for (const arc of below) {
408 let best = null;
409 let score = -Infinity;
410 for (const entry of above) {
411 if (entry.used) continue;
412 const aligned = align(arc, entry.arc);
413 const value = overlap(arc, aligned);
414 if (value > score) (score = value), (best = entry);
415 }
416 if (best && score > -0.05) {
417 best.used = true;
418 quad(arc, align(arc, best.arc));
419 } else {
420 const centre = (arc.from + arc.to) / 2;
421 quad(arc, { from: centre, to: centre });
422 }
423 }
424 for (const entry of above) {
425 if (entry.used) continue;
426 const centre = (entry.arc.from + entry.arc.to) / 2;
427 quad({ from: centre, to: centre }, entry.arc);
428 }
429 };
430 const k = scaleOf(P);
431 let run = [];
432 const flush = () => {
433 const tones = [...new Set(run.flatMap((level) => [...level.groups.keys()]))].sort((x, y) => x - y);
434 for (const tone of tones) {
435 const countOf = (level) => (level.groups.get(tone) ?? []).length;
436 let start = 0;
437 for (let at = 1; at <= run.length; at++) {
438 if (at < run.length && countOf(run[at - 1]) === countOf(run[at])) continue;
439 if (at - 1 > start && countOf(run[start])) sheet(tone, run.slice(start, at));
440 if (at < run.length) strip(tone, run[at - 1], run[at]);
441 start = at;
442 }
443 }
444 run = [];
445 };
446 for (let i = 0; i < m - 1; i++) {
447 const ds = meridian[i + 1][0] - meridian[i][0];
448 if (Math.abs(ds) < 1e-9) {
449 flush();
450 continue;
451 }
452 const count = Math.max(1, rows);
453 const reach = (Math.hypot(ds, meridian[i + 1][1] - meridian[i][1]) * k) / count;
454 const limit = Math.max(1, Math.min(deepest, Math.ceil(Math.log2(Math.max(2, reach / fineness)))));
455 const coarse = Array.from({ length: count + 1 }, (_, index) => levelOf(i, index / count));
456 run.push(coarse[0]);
457 const refine = (a, b, depth) => {
458 const mid = levelOf(i, (a.u + b.u) / 2);
459 const split = depth < limit && (!same(a, b) || !same(a, mid) || (depth < 6 && strayOf(i, a, mid, b) > error));
460 if (!split) {
461 run.push(b);
462 return;
463 }
464 refine(a, mid, depth + 1);
465 refine(mid, b, depth + 1);
466 };
467 for (let index = 1; index <= count; index++) refine(coarse[index - 1], coarse[index], 0);
468 }
469 flush();
470 return buckets;
473function sideOf(meridian) {
474 const m = meridian.length;
475 let first = 0;
476 let last = m - 1;
477 if (meridian[0][1] <= 1e-9) first = 1;
478 if (meridian[m - 1][1] <= 1e-9) last = m - 2;
479 const side = meridian.slice(first, last + 1);
480 for (let index = 1; index < side.length; index++) if (side[index][0] - side[index - 1][0] <= 1e-9) return null;
481 return side.length >= 3 ? side : null;
484function surfaceOf(side, F, P, rims, { samples = 96, slope = null } = {}) {
485 const V = viewOf(P);
486 const aV = dot3(F.a, V);
487 const facing = Math.atan2(dot3(F.v, V), dot3(F.u, V));
488 const [left, right] = silhouettePoints(side, F, P, { samples, slope });
489 if (left.length <= samples || right.length <= samples) return null;
490 const s0 = side[0][0];
491 const s1 = side[side.length - 1][0];
492 const angleOf = (point) => angleToward(F, sub3(point, add3(F.o, mul3(F.a, dot3(sub3(point, F.o), F.a)))));
493 const wrap = (angle) => ((angle % TAU) + TAU) % TAU;
494 const arc = (s, r, from, to, through) => {
495 let sweep = wrap(to - from);
496 if (wrap(through - from) > sweep) sweep -= TAU;
497 const count = Math.max(4, Math.ceil(Math.abs(sweep) / (TAU / 96)), Math.ceil((Math.abs(sweep) * r * scaleOf(P)) / 1.5));
498 return Array.from({ length: count + 1 }, (_, index) => iso(pointOf(F, s, r, from + (sweep * index) / count), P));
499 };
500 const r0 = side[0][1];
501 const r1 = side[side.length - 1][1];
502 const bottom = -aV > 0 ? facing + Math.PI : facing;
503 const top = aV > 0 ? facing + Math.PI : facing;
504 const L = left.map((point) => iso(point, P));
505 const R = right.map((point) => iso(point, P));
506 const topArc = arc(s1, r1, angleOf(left[left.length - 1]), angleOf(right[right.length - 1]), top);
507 const bottomArc = arc(s0, r0, angleOf(right[0]), angleOf(left[0]), bottom);
508 const keepFirst = rims === "all" || rims === "first" || rims === "ends";
509 const keepLast = rims === "all" || rims === "last" || rims === "ends";
510 const fill = pathOf([...L, ...topArc, ...[...R].reverse(), ...bottomArc], true);
511 const outline = [pathOf(L), pathOf(R), keepLast ? pathOf(topArc) : "", keepFirst ? pathOf(bottomArc) : ""].join("");
512 const hull = hullOf([...arc(s0, r0, 0, TAU - 1e-6, Math.PI), ...arc(s1, r1, 0, TAU - 1e-6, Math.PI), ...L, ...R]);
513 let gap = 0;
514 for (const p of [...L, ...R]) {
515 let best = Infinity;
516 for (let index = 0; index < hull.length; index++) {
517 const a = hull[index];
518 const b = hull[(index + 1) % hull.length];
519 const dx = b[0] - a[0];
520 const dy = b[1] - a[1];
521 const t = Math.max(0, Math.min(1, ((p[0] - a[0]) * dx + (p[1] - a[1]) * dy) / (dx * dx + dy * dy || 1)));
522 best = Math.min(best, Math.hypot(p[0] - a[0] - dx * t, p[1] - a[1] - dy * t));
523 }
524 gap = Math.max(gap, best);
525 }
526 return { fill, outline, gap };
529export const softOf = (meridian) => meridian.some((point) => Boolean(point[2]));
531export function lathe(meridian, F, P, { steps = stepsFor(Math.max(...meridian.map(([, r]) => r)), 32, 160, scaleOf(P)), fill = "auto", hole = null, bevel = 0, rims = "all", smooth: asked = "auto", waist = 0.5 } = {}) {
532 const smooth = asked === "auto" ? softOf(meridian) : asked;
533 const V = viewOf(P);
534 const L = lightOf(P);
535 const aV = dot3(F.a, V);
536 const uV = dot3(F.u, V);
537 const vV = dot3(F.v, V);
538 const aL = dot3(F.a, L);
539 const uL = dot3(F.u, L);
540 const vL = dot3(F.v, L);
541 const n = steps;
542 const m = meridian.length;
543 const rings = meridian.map(([s, r]) => Array.from({ length: n }, (_, j) => iso(pointOf(F, s, r, (j / n) * TAU), P)));
544 const midC = [];
545 const midS = [];
546 for (let j = 0; j < n; j++) {
547 const t = ((j + 0.5) / n) * TAU;
548 midC.push(Math.cos(t));
549 midS.push(Math.sin(t));
550 }
551 const seen = [];
552 const tones = [];
553 for (let i = 0; i < m - 1; i++) {
554 const ds = meridian[i + 1][0] - meridian[i][0];
555 const dr = meridian[i + 1][1] - meridian[i][1];
556 const length = Math.hypot(ds, dr);
557 const row = [];
558 const toneRow = [];
559 for (let j = 0; j < n; j++) {
560 if (length < 1e-9) {
561 row.push(false);
562 toneRow.push(0);
563 continue;
564 }
565 const facing = ds * (midC[j] * uV + midS[j] * vV) - dr * aV;
566 const lit = (ds * (midC[j] * uL + midS[j] * vL) - dr * aL) / length;
567 row.push(facing > 1e-6 * length);
568 toneRow.push(bandOf(lit));
569 }
570 seen.push(row);
571 tones.push(toneRow);
572 }
573 const buckets = [[], [], [], [], []];
574 const all = [];
575 const smoothed = smooth ? smoothRows(meridian, F, P, { steps, ...(smooth === true ? {} : smooth) }) : null;
576 if (smoothed) smoothed.forEach((list, tone) => list.forEach((d) => (buckets[tone].push(d), all.push(d))));
577 for (let i = 0; i < m - 1; i++) {
578 if (smoothed && Math.abs(meridian[i + 1][0] - meridian[i][0]) > 1e-9) continue;
579 const keys = seen[i].map((visible, j) => (visible ? tones[i][j] : null));
580 for (const { key, run } of groupsOf(keys)) {
581 const a = run.map((j) => rings[i][j]);
582 const b = run.map((j) => rings[i + 1][j]).reverse();
583 const d = pathOf(oriented([...a, ...b]), true);
584 buckets[key].push(d);
585 all.push(d);
586 }
587 }
588 const phi = Math.atan2(vV, uV);
589 const flatV = Math.hypot(uV, vV);
590 const rangeOf = (i) => {
591 const ds = meridian[i + 1][0] - meridian[i][0];
592 const dr = meridian[i + 1][1] - meridian[i][1];
593 if (Math.hypot(ds, dr) < 1e-9) return null;
594 if (Math.abs(ds) < 1e-9 || flatV < 1e-9) return -dr * aV > 1e-9 ? [-1, 1] : null;
595 const c = (dr * aV) / (ds * flatV);
596 return ds > 0 ? [Math.max(-1, c), 1] : [-1, Math.min(1, c)];
597 };
598 const arcsOf = (ranges) => {
599 if (ranges.some((range) => !range)) return [];
600 const lo = Math.max(...ranges.map(([x]) => x));
601 const hi = Math.min(...ranges.map(([, x]) => x));
602 if (lo >= hi - 1e-9) return [];
603 const near = Math.acos(Math.min(1, hi));
604 const far = Math.acos(Math.max(-1, lo));
605 if (hi >= 1 && lo <= -1) return [[phi, phi + TAU]];
606 if (hi >= 1) return [[phi - far, phi + far]];
607 if (lo <= -1) return [[phi + near, phi + TAU - near]];
608 return [[phi + near, phi + far], [phi - far, phi - near]];
609 };
610 const along = (s, r, [from, to]) => {
611 const count = Math.max(2, Math.ceil(((to - from) * Math.max(r, 1e-6) * scaleOf(P)) / 1.5));
612 return Array.from({ length: count + 1 }, (_, j) => iso(pointOf(F, s, r, from + ((to - from) * j) / count), P));
613 };
614 const creases = [];
615 for (let i = 1; i < m - 1; i++) {
616 if (meridian[i][2] || meridian[i][1] <= 1e-9) continue;
617 for (const arc of arcsOf([rangeOf(i - 1), rangeOf(i)])) creases.push(pathOf(along(meridian[i][0], meridian[i][1], arc)));
618 }
619 let bevelPath = "";
620 if (bevel > 0 && m > 2) {
621 const ends = [];
622 if (meridian[0][1] <= 1e-9) ends.push([0, 1, 1]);
623 if (meridian[m - 1][1] <= 1e-9) ends.push([m - 2, m - 2, m - 3]);
624 for (const [cap, rim, side] of ends) {
625 const [s, r] = meridian[rim];
626 for (const arc of arcsOf([rangeOf(cap), rangeOf(side)])) {
627 const inset = along(s, Math.max(0, r - bevel), arc);
628 const edge = along(s, r, arc);
629 bevelPath += pathOf([edge[0], ...inset.slice(1, -1), edge[edge.length - 1]]);
630 }
631 }
632 }
633 const { fill: fillPath, outline } = outlineOf(meridian, F, P, { steps: n, fill, hole, rims, smooth, waist, all });
634 return {
635 fill: fillPath,
636 outline,
637 crease: creases.join(""),
638 top: buckets[4].join(""),
639 shades: buckets.slice(0, 4).map((list) => list.join("")),
640 bevel: bevelPath || undefined,
641 seamless: Boolean(smoothed),
642 };
645function shapeOf(meridian, P, smooth) {
646 if (smooth && smooth.radius) {
647 const segments = meridian.slice(0, -1).map(([s0], index) => (Math.abs(meridian[index + 1][0] - s0) > 1e-9 ? { radius: smooth.radius, slope: smooth.slope } : null));
648 return curved(meridian, P, segments);
649 }
650 const curves = smooth && !smooth.slope ? curveRuns(meridian) : null;
651 return curves && curves.some(Boolean) ? curved(meridian, P, curves) : meridian;
654export function outlineOf(meridian, F, P, { steps = stepsFor(Math.max(...meridian.map(([, r]) => r)), 32, 160, scaleOf(P)), fill = "auto", hole = null, rims = "all", smooth: asked = "auto", waist = 0.5, all = null } = {}) {
655 const smooth = asked === "auto" ? softOf(meridian) : asked;
656 const n = steps;
657 let fillPath;
658 let outline;
659 const shape = shapeOf(meridian, P, smooth);
660 const side = fill === "auto" || fill === "surface" ? sideOf(shape) : null;
661 const surface = side && !hole ? surfaceOf(side, F, P, rims, { slope: smooth && smooth.slope ? smooth.slope : null }) : null;
662 if (surface && (fill === "surface" || surface.gap > waist)) {
663 fillPath = surface.fill;
664 outline = surface.outline;
665 } else if (fill === "hull" || fill === "auto" || fill === "surface") {
666 const most = Math.max(...shape.map(([, r]) => r));
667 const around = Math.min(320, Math.max(n, Math.ceil((TAU * most * scaleOf(P)) / 1.5)));
668 const tagged = [];
669 shape.forEach(([s, r], index) => {
670 for (let j = 0; j < around; j++) tagged.push({ p: iso(pointOf(F, s, r, (j / around) * TAU), P), ring: index });
671 });
672 const hull = hullTagged(tagged);
673 const holePath = hole ? pathOf(Array.from({ length: around }, (_, j) => iso(pointOf(F, hole[0], hole[1], (j / around) * TAU), P)), true) : "";
674 fillPath = pathOf(hull.map(({ p }) => p), true) + holePath;
675 if (rims === "all") {
676 outline = pathOf(hull.map(({ p }) => p), true) + holePath;
677 } else {
678 const keepFirst = rims === "first" || rims === "ends";
679 const keepLast = rims === "last" || rims === "ends";
680 const edges = hull.map((a, index) => {
681 const b = hull[(index + 1) % hull.length];
682 const same = a.ring === b.ring;
683 const keep = !same || (a.ring !== 0 && a.ring !== shape.length - 1) || (keepFirst && a.ring === 0) || (keepLast && a.ring === shape.length - 1);
684 return [a.p, b.p, keep];
685 });
686 outline = chains(edges).map((points) => pathOf(points)).join("") + holePath;
687 }
688 } else {
689 fillPath = all ? all.join("") : (({ shades, top }) => [...shades, top].join(""))(lathe(meridian, F, P, { steps: n, fill: "auto", hole, rims, smooth, waist }));
690 outline = "";
691 }
692 return { fill: fillPath, outline };
695const signedArea = (points) => points.reduce((sum, [x, y], index) => {
696 const [nx, ny] = points[(index + 1) % points.length];
697 return sum + x * ny - nx * y;
698}, 0);
700const oriented = (points) => (signedArea(points) < 0 ? [...points].reverse() : points);
702export const saddleStart = (R, a, angle) => Math.sqrt(Math.max(0, R * R - a * a * Math.cos(angle) ** 2));
704export function saddleOf(F, a, start, s1, P, { steps = stepsFor(a, 24) } = {}) {
705 const V = viewOf(P);
706 const L = lightOf(P);
707 const n = steps;
708 const angle = (j) => (j / n) * TAU;
709 const from = typeof start === "function" ? start : (t) => saddleStart(start, a, t);
710 const low = Array.from({ length: n }, (_, j) => iso(pointOf(F, from(angle(j)), a, angle(j)), P));
711 const high = Array.from({ length: n }, (_, j) => iso(pointOf(F, s1, a, angle(j)), P));
712 const seen = [];
713 const tones = [];
714 for (let j = 0; j < n; j++) {
715 const normal = radialOf(F, angle(j + 0.5));
716 seen.push(dot3(normal, V) > 1e-6);
717 tones.push(bandOf(dot3(normal, L)));
718 }
719 const buckets = [[], [], [], [], []];
720 const fills = [];
721 const keys = seen.map((visible, j) => (visible ? tones[j] : null));
722 for (const { key, run } of groupsOf(keys)) {
723 const d = pathOf(oriented([...run.map((j) => low[j]), ...run.map((j) => high[j]).reverse()]), true);
724 buckets[key].push(d);
725 fills.push(d);
726 }
727 const capSeen = dot3(F.a, V) > 1e-6;
728 if (capSeen) {
729 const d = pathOf(oriented(high), true);
730 buckets[bandOf(dot3(F.a, L))].push(d);
731 fills.push(d);
732 }
733 const front = runsOf(seen);
734 const back = runsOf(seen.map((visible) => !visible));
735 const edges = [];
736 for (const run of front) {
737 edges.push(pathOf(run.map((j) => low[j])));
738 edges.push(pathOf([low[run[0]], high[run[0]]]), pathOf([low[run[run.length - 1]], high[run[run.length - 1]]]));
739 }
740 for (const run of capSeen ? back : front) edges.push(pathOf(run.map((j) => high[j])));
741 return {
742 fill: fills.join(""),
743 outline: edges.join(""),
744 crease: capSeen ? front.map((run) => pathOf(run.map((j) => high[j]))).join("") : "",
745 top: buckets[4].join(""),
746 shades: buckets.slice(0, 4).map((list) => list.join("")),
747 seamless: true,
748 };
751export function prismOf(F, polygon, s0, s1, P) {
752 const V = viewOf(P);
753 const L = lightOf(P);
754 const ring = oriented(polygon.map(([x, y]) => [x, y]));
755 const n = ring.length;
756 const at = (s, [x, y]) => [F.o[0] + F.a[0] * s + F.u[0] * x + F.v[0] * y, F.o[1] + F.a[1] * s + F.u[1] * x + F.v[1] * y, F.o[2] + F.a[2] * s + F.u[2] * x + F.v[2] * y];
757 const low = ring.map((point) => iso(at(s0, point), P));
758 const high = ring.map((point) => iso(at(s1, point), P));
759 const buckets = [[], [], [], [], []];
760 const seen = [];
761 for (let j = 0; j < n; j++) {
762 const [x0, y0] = ring[j];
763 const [x1, y1] = ring[(j + 1) % n];
764 const normal = unit3(add3(mul3(F.u, y1 - y0), mul3(F.v, -(x1 - x0))));
765 const visible = dot3(normal, V) > 1e-6;
766 seen.push(visible);
767 if (visible) buckets[bandOf(dot3(normal, L))].push(pathOf([low[j], low[(j + 1) % n], high[(j + 1) % n], high[j]], true));
768 }
769 const ends = [[s0, low, mul3(F.a, -1)], [s1, high, F.a]];
770 const capSeen = ends.map(([, , normal]) => dot3(normal, V) > 1e-6);
771 ends.forEach(([, points, normal], index) => capSeen[index] && buckets[bandOf(dot3(normal, L))].push(pathOf(points, true)));
772 const hull = hullOf([...low, ...high]);
773 const creases = [];
774 for (let j = 0; j < n; j++) {
775 const prev = (j - 1 + n) % n;
776 const a = [ring[j][0] - ring[prev][0], ring[j][1] - ring[prev][1]];
777 const b = [ring[(j + 1) % n][0] - ring[j][0], ring[(j + 1) % n][1] - ring[j][1]];
778 const sharp = (a[0] * b[0] + a[1] * b[1]) / (Math.hypot(a[0], a[1]) * Math.hypot(b[0], b[1]) || 1) < Math.cos(0.35);
779 if (sharp && seen[j] && seen[prev]) creases.push(pathOf([low[j], high[j]]));
780 ends.forEach(([, points], index) => capSeen[index] && seen[j] && creases.push(pathOf([points[j], points[(j + 1) % n]])));
781 }
782 return { fill: pathOf(hull, true), outline: pathOf(hull, true), crease: creases.join(""), top: buckets[4].join(""), shades: buckets.slice(0, 4).map((list) => list.join("")) };
785export function stadium(r0, r1, offset, steps = 32) {
786 const circle = (cx, r) => Array.from({ length: steps }, (_, index) => [cx + r * Math.cos((index / steps) * TAU), r * Math.sin((index / steps) * TAU)]);
787 return hullOf([...circle(0, r0), ...circle(offset, r1)]);
790export function sphereOf(centre, r, P, { steps = 48 } = {}) {
791 const V = viewOf(P);
792 const L = lightOf(P);
793 const F = frameAlong(centre, V);
794 const flat = (p) => iso(p, P);
795 const rim = Array.from({ length: steps }, (_, index) => pointOf(F, 0, r, (index / steps) * TAU));
796 const outline = pathOf(rim.map(flat), true);
797 const capOf = (threshold) => {
798 const points = rim.filter((p) => dot3(sub3(p, centre), L) / r >= threshold).map(flat);
799 const c = dot3(L, V);
800 const LF = frameAlong(add3(centre, mul3(L, r * threshold)), L);
801 const radius = r * Math.sqrt(Math.max(0, 1 - threshold * threshold));
802 for (let index = 0; index < steps; index++) {
803 const p = pointOf(LF, 0, radius, (index / steps) * TAU);
804 if (dot3(sub3(p, centre), V) >= -1e-9) points.push(flat(p));
805 }
806 if (c > threshold) points.push(flat(add3(centre, mul3(V, r))));
807 return points.length > 2 ? pathOf(hullOf(points), true) : "";
808 };
809 const shades = [outline, capOf(BANDS[1]), capOf(BANDS[2]), capOf(BANDS[3])];
810 return { fill: outline, outline, crease: "", top: "", shades, seamless: true };
813export const disc = (s0, s1, r, F, P, options = {}) =>
814 lathe(
815 [
816 [s0, 0],
817 [s0, r],
818 [s1, r],
819 [s1, 0],
820 ],
821 F,
822 P,
823 { steps: stepsFor(r, 32, 160, scaleOf(P)), ...options },
824 );
826export const ringBand = (s0, s1, rIn, rOut, F, P, options = {}) =>
827 lathe(
828 [
829 [s0, rIn],
830 [s0, rOut],
831 [s1, rOut],
832 [s1, rIn],
833 ],
834 F,
835 P,
836 { steps: stepsFor(rOut, 32, 160, scaleOf(P)), hole: null, ...options },
837 );
839export function solidOf(profile, F, P, options = {}) {
840 const first = profile[0];
841 const last = profile[profile.length - 1];
842 const meridian = [[first[0], 0], ...profile, [last[0], 0]];
843 const most = Math.max(...profile.map(([, r]) => r));
844 return lathe(meridian, F, P, { steps: stepsFor(most, 32, 160, scaleOf(P)), ...options });
847export function bandOfProfile(profile, F, P, options = {}) {
848 const most = Math.max(...profile.map(([, r]) => r));
849 return lathe(profile, F, P, { steps: stepsFor(most, 32, 160, scaleOf(P)), ...options });
852export function silhouettePoints(profile, F, P, { samples = 72, slope: given = null, radius = null } = {}) {
853 const V = viewOf(P);
854 const aV = dot3(F.a, V);
855 const uV = dot3(F.u, V);
856 const vV = dot3(F.v, V);
857 const facing = Math.atan2(vV, uV);
858 const flat = Math.hypot(uV, vV) || 1e-9;
859 const s0 = profile[0][0];
860 const s1 = profile[profile.length - 1][0];
861 const radiusAt = radius ?? ((s) => {
862 let index = 1;
863 while (index < profile.length - 1 && profile[index][0] < s) index++;
864 const [sa, ra] = profile[index - 1];
865 const [sb, rb] = profile[index];
866 return ra + ((rb - ra) * (s - sa)) / (sb - sa || 1);
867 });
868 const side = (sign) => {
869 const points = [];
870 for (let step = 0; step <= samples; step++) {
871 const s = s0 + ((s1 - s0) * step) / samples;
872 const h = (s1 - s0) / samples / 2 || 0.01;
873 const slope = given ? given(s) : (radiusAt(Math.min(s1, s + h)) - radiusAt(Math.max(s0, s - h))) / (Math.min(s1, s + h) - Math.max(s0, s - h) || 1);
874 const c = (slope * aV) / flat;
875 if (Math.abs(c) > 1) continue;
876 points.push(pointOf(F, s, radiusAt(s), facing + sign * Math.acos(c)));
877 }
878 return points;
879 };
880 return [side(1), side(-1)];
883export const silhouetteOf = (profile, F, P, options = {}) =>
884 silhouettePoints(profile, F, P, options)
885 .map((points) => pathOf(points.map((point) => iso(point, P))))
886 .join("");
888export function profileCurve(profile, { slope = null, radius = null } = {}) {
889 for (let index = 1; index < profile.length; index++) if (profile[index][0] - profile[index - 1][0] <= 1e-9) throw new Error(`profileCurve: s must rise along the profile (point ${index} at s = ${profile[index][0]})`);
890 const segments = curveRuns(profile);
891 const s0 = profile[0][0];
892 const s1 = profile[profile.length - 1][0];
893 const find = (s) => {
894 let lo = 0;
895 let hi = profile.length - 2;
896 while (lo < hi) {
897 const mid = (lo + hi + 1) >> 1;
898 if (profile[mid][0] <= s) lo = mid;
899 else hi = mid - 1;
900 }
901 return lo;
902 };
903 const linear = (index, s) => {
904 const [sa, ra] = profile[index];
905 const [sb, rb] = profile[index + 1];
906 return [ra + ((rb - ra) * (s - sa)) / (sb - sa), (rb - ra) / (sb - sa)];
907 };
908 const radiusAt = radius ?? ((s) => {
909 const at = Math.max(s0, Math.min(s1, s));
910 const index = find(at);
911 return segments[index] ? segments[index].radius(at) : linear(index, at)[0];
912 });
913 const slopeAt = slope ?? ((s) => {
914 const at = Math.max(s0, Math.min(s1, s));
915 const index = find(at);
916 return segments[index] ? segments[index].slope(at) : linear(index, at)[1];
917 });
918 return { s0, s1, radius: radiusAt, slope: slopeAt, analytic: Boolean(slope && radius) };
921export function bandsOf(profile, cuts, F, P, { slope = null, radius = null, steps, rows = 6, rims = "none", fill = "auto", samples = 72 } = {}) {
922 const curve = profileCurve(profile, { slope, radius });
923 const { s0, s1 } = curve;
924 const stops = [s0, ...[...new Set(cuts)].filter((s) => s > s0 + 1e-6 && s < s1 - 1e-6).sort((a, b) => a - b), s1];
925 const most = Math.max(...profile.map(([, r]) => r), ...stops.map((s) => curve.radius(s)));
926 const n = steps ?? stepsFor(most, 32, 160, scaleOf(P));
927 const knot = (s, soft) => (soft ? [s, curve.radius(s), 1] : [s, curve.radius(s)]);
928 const meridianOf = (a, b) => [knot(a), ...profile.filter(([s]) => s > a + 1e-9 && s < b - 1e-9).map(([s, , soft]) => knot(s, soft)), knot(b)];
929 const shading = { slope: curve.slope, radius: curve.radius, rows };
930 const bands = stops.slice(0, -1).map((a, index) => {
931 const b = stops[index + 1];
932 const meridian = meridianOf(a, b);
933 const paths = lathe(meridian, F, P, { steps: n, rims, fill, smooth: shading });
934 const seam = index < stops.length - 2 ? arcOf(F, b, curve.radius(b), P, { slope: curve.slope(b) }) : "";
935 return { s0: a, s1: b, profile: meridian, paths: { ...paths, outline: "" }, outline: paths.outline, seam };
936 });
937 const sidesOf = (from = s0, to = s1, { ends = false, count = samples } = {}) => {
938 const span = [Math.max(s0, from), Math.min(s1, to)];
939 const dense = Array.from({ length: count + 1 }, (_, index) => knot(span[0] + ((span[1] - span[0]) * index) / count, true));
940 const points = silhouettePoints(dense, F, P, { samples: count, slope: curve.slope, radius: curve.radius });
941 const rim = (s) => arcOf(F, s, curve.radius(s), P, { slope: curve.slope(s) });
942 const rimsAt = ends === true ? span : ends === "first" ? [span[0]] : ends === "last" ? [span[1]] : [];
943 return { d: points.map((list) => pathOf(list.map((point) => iso(point, P)))).join("") + rimsAt.map(rim).join(""), points };
944 };
945 return { bands, sidesOf, radius: curve.radius, slope: curve.slope, steps: n, s0, s1 };
948export function arcOf(F, s, r, P, { slope = 0, steps = 0, least = 0.02, inward = false, from = 0, to = TAU, trim = 0.3 } = {}) {
949 const V = viewOf(P);
950 const aV = dot3(F.a, V);
951 const uV = dot3(F.u, V);
952 const vV = dot3(F.v, V);
953 const flat = Math.hypot(uV, vV);
954 const phi = Math.atan2(vV, uV);
955 const norm = Math.hypot(1, slope);
956 const full = Math.abs(to - from - TAU) < 1e-9;
957 let visible;
958 if (flat < 1e-9) visible = (inward ? slope * aV / norm > least : -slope * aV / norm > least) ? [[phi, phi + TAU]] : [];
959 else if (inward) {
960 const c = (slope * aV - least * norm) / flat;
961 visible = c <= -1 ? [] : c >= 1 ? [[phi, phi + TAU]] : [[phi + Math.acos(c), phi + TAU - Math.acos(c)]];
962 } else {
963 const c = (least * norm + slope * aV) / flat;
964 visible = c >= 1 ? [] : c <= -1 ? [[phi, phi + TAU]] : [[phi - Math.acos(c), phi + Math.acos(c)]];
965 }
966 const at = (t) => iso(pointOf(F, s, r, t), P);
967 const speed = (t) => {
968 const a = at(t - 1e-3);
969 const b = at(t + 1e-3);
970 return Math.hypot(b[0] - a[0], b[1] - a[1]) / 2e-3;
971 };
972 const count = (span) => Math.max(2, Math.ceil(((steps || stepsFor(r, 24, 112)) * span) / TAU), Math.ceil((span * r * scaleOf(P)) / 1.5));
973 const out = [];
974 for (const [a, b] of visible) {
975 if (b - a >= TAU - 1e-9 && full) {
976 const n = count(TAU);
977 out.push(pathOf(Array.from({ length: n }, (_, j) => at(from + (j / n) * TAU)), true));
978 continue;
979 }
980 const pieces = full ? [[a, b, true, true]] : [];
981 if (!full)
982 for (let turn = -2; turn <= 2; turn++) {
983 const lo = Math.max(a + turn * TAU, from);
984 const hi = Math.min(b + turn * TAU, to);
985 if (hi - lo > 1e-6) pieces.push([lo, hi, lo > from + 1e-9, hi < to - 1e-9]);
986 }
987 for (const [lo, hi, limb0, limb1] of pieces) {
988 const span = hi - lo;
989 const cut0 = limb0 && trim > 0 ? Math.min(span * 0.25, trim / (speed(lo) || 1)) : 0;
990 const cut1 = limb1 && trim > 0 ? Math.min(span * 0.25, trim / (speed(hi) || 1)) : 0;
991 const t0 = lo + cut0;
992 const t1 = hi - cut1;
993 const n = count(t1 - t0);
994 out.push(pathOf(Array.from({ length: n + 1 }, (_, j) => at(t0 + ((t1 - t0) * j) / n))));
995 }
996 }
997 return out.join("");
1000export function circleOf(F, s, r, P, steps = 0) {
1001 const count = steps || stepsFor(r, 24, 112);
1002 return pathOf(
1003 Array.from({ length: count }, (_, j) => iso(pointOf(F, s, r, (j / count) * TAU), P)),
1004 true,
1005 );
1008export function spokesOf(F, s, r0, r1, count, P, { phase = 0, sweep = 0, from = 0, to = TAU } = {}) {
1009 const lines = [];
1010 for (let index = 0; index < count; index++) {
1011 const t = phase + from + (index / count) * (to - from);
1012 lines.push(pathOf([iso(pointOf(F, s, r0, t), P), iso(pointOf(F, s, r1, t + sweep), P)]));
1014 return lines.join("");
1017export const fadeOf = (value, [lo, hi]) => {
1018 const t = Math.max(0, Math.min(1, (value - lo) / (hi - lo || 1e-9)));
1019 return t * t * (3 - 2 * t);
1022export function ribsOf(F, s0, s1, r, count, P, { phase = 0, twist = 0, least = 0.12, r1 = r, fade = null, seams = false } = {}) {
1023 const V = viewOf(P);
1024 const lines = [];
1025 const slope = (r1 - r) / (s1 - s0 || 1);
1026 for (let index = 0; index < count; index++) {
1027 const t = phase + (index / count) * TAU;
1028 const radial = radialOf(F, t + twist / 2);
1029 const normal = unit3([radial[0] - slope * F.a[0], radial[1] - slope * F.a[1], radial[2] - slope * F.a[2]]);
1030 const facing = dot3(normal, V);
1031 const d = pathOf([iso(pointOf(F, s0, r, t), P), iso(pointOf(F, s1, r1, t + twist), P)]);
1032 if (fade) {
1033 const alpha = fadeOf(facing, fade);
1034 if (alpha > 0.02) lines.push({ d, alpha });
1035 continue;
1037 if (facing < least) continue;
1038 lines.push(d);
1040 const ends = seams ? [seams === "last" ? "" : arcOf(F, s0, r, P, { slope }), seams === "first" ? "" : arcOf(F, s1, r1, P, { slope })].filter(Boolean) : [];
1041 if (fade) return [...lines, ...ends.map((d) => ({ d, alpha: 1 }))];
1042 return [...lines, ...ends].join("");
1045export function rodOut(F, from, to, r, P, { stroke = 0, ...options } = {}) {
1046 const pad = 0.35 / scaleOf(P);
1047 const ring = (s) => Array.from({ length: 48 }, (_, index) => iso(pointOf(F, s, r + pad, (index / 48) * TAU), P));
1048 return {
1049 paths: disc(from - stroke, to, r, F, P, options),
1050 clip: pathOf(hullOf([...ring(from), ...ring(to + stroke)]), true),
1051 from,
1052 to,
1053 stroke,
1054 reach: to + stroke,
1055 };
1058export function dotsOf(F, s, r, count, P, { phase = 0, slope = 0, least = 0.08, all = false, fade = null } = {}) {
1059 const V = viewOf(P);
1060 const out = [];
1061 for (let index = 0; index < count; index++) {
1062 const t = phase + (index / count) * TAU;
1063 let alpha = 1;
1064 if (!all) {
1065 const radial = radialOf(F, t);
1066 const normal = unit3([radial[0] - slope * F.a[0], radial[1] - slope * F.a[1], radial[2] - slope * F.a[2]]);
1067 const facing = dot3(normal, V);
1068 if (fade) alpha = fadeOf(facing, fade);
1069 else if (facing < least) continue;
1070 if (alpha <= 0.02) continue;
1072 const [x, y] = iso(pointOf(F, s, r, t), P);
1073 out.push(alpha < 1 ? [x, y, alpha] : [x, y]);
1075 return out;
1078export function facingOf(F, angle, P, slope = 0) {
1079 const radial = radialOf(F, angle);
1080 return dot3(unit3([radial[0] - slope * F.a[0], radial[1] - slope * F.a[1], radial[2] - slope * F.a[2]]), viewOf(P));
1083export function fillet(points, radius, perArc = 7, { P = null, tube = 0, margin = 1.3 } = {}) {
1084 if (points.length < 3) return points.map((p) => [...p]);
1085 const V = P && tube ? viewOf(P) : null;
1086 const out = [[...points[0]]];
1087 for (let index = 1; index < points.length - 1; index++) {
1088 const prev = points[index - 1];
1089 const here = points[index];
1090 const next = points[index + 1];
1091 const into = unit3(sub3(here, prev));
1092 const outOf = unit3(sub3(next, here));
1093 const cosTurn = Math.max(-1, Math.min(1, dot3(into, outOf)));
1094 const turn = Math.acos(cosTurn);
1095 if (turn < 1e-3) {
1096 out.push([...here]);
1097 continue;
1099 const room = Math.min(len3(sub3(here, prev)), len3(sub3(next, here))) * 0.48;
1100 const facing = V ? Math.abs(dot3(unit3(cross3(into, outOf)), V)) : 1;
1101 const bend = V ? Math.max(radius, (margin * tube) / (0.7 * Math.max(0.05, facing) ** 2)) : radius;
1102 const cut = Math.min(bend * Math.tan(turn / 2), room);
1103 const a = sub3(here, mul3(into, cut));
1104 const b = add3(here, mul3(outOf, cut));
1105 const count = Math.max(2, Math.ceil((perArc * turn) / (Math.PI / 2)));
1106 for (let step = 0; step <= count; step++) {
1107 const t = step / count;
1108 const p0 = lerp3(a, here, t);
1109 const p1 = lerp3(here, b, t);
1110 out.push(lerp3(p0, p1, t));
1113 out.push([...points[points.length - 1]]);
1114 return out;
1117export function resample(points, spacing) {
1118 const lengths = [0];
1119 for (let index = 1; index < points.length; index++) lengths.push(lengths[index - 1] + len3(sub3(points[index], points[index - 1])));
1120 const total = lengths[lengths.length - 1];
1121 const count = Math.max(1, Math.round(total / spacing));
1122 const out = [];
1123 let segment = 1;
1124 for (let step = 0; step <= count; step++) {
1125 const at = (total * step) / count;
1126 while (segment < points.length - 1 && lengths[segment] < at) segment++;
1127 const a = points[segment - 1];
1128 const b = points[segment];
1129 const span = lengths[segment] - lengths[segment - 1] || 1;
1130 out.push(lerp3(a, b, Math.min(1, Math.max(0, (at - lengths[segment - 1]) / span))));
1132 return out;
1135export function pathLength(points) {
1136 let total = 0;
1137 for (let index = 1; index < points.length; index++) total += len3(sub3(points[index], points[index - 1]));
1138 return total;
1141export function cut(points, from, to) {
1142 const out = [];
1143 let walked = 0;
1144 for (let index = 1; index < points.length; index++) {
1145 const a = points[index - 1];
1146 const b = points[index];
1147 const length = len3(sub3(b, a));
1148 const start = walked;
1149 const end = walked + length;
1150 if (end >= from && start <= to) {
1151 const t0 = Math.max(0, (from - start) / (length || 1));
1152 const t1 = Math.min(1, (to - start) / (length || 1));
1153 if (!out.length) out.push(lerp3(a, b, t0));
1154 out.push(lerp3(a, b, t1));
1156 walked = end;
1158 return out;
1161export const pointAlong = (points, at) => {
1162 const piece = cut(points, at, at + 0.01);
1163 return piece[0] ?? points[points.length - 1];
1166export function tangentAlong(points, at) {
1167 const total = pathLength(points);
1168 const a = pointAlong(points, Math.max(0, at - 0.6));
1169 const b = pointAlong(points, Math.min(total, at + 0.6));
1170 return unit3(sub3(b, a));
1173export function spiral(F, s, r0, r1, a0, a1, steps = 28) {
1174 const out = [];
1175 for (let index = 0; index <= steps; index++) {
1176 const t = index / steps;
1177 out.push(pointOf(F, s, r0 + (r1 - r0) * t, a0 + (a1 - a0) * t));
1179 return out;
1182export function arcPoints(F, s, r, a0, a1, steps = 24) {
1183 return Array.from({ length: steps + 1 }, (_, index) => pointOf(F, s, r, a0 + ((a1 - a0) * index) / steps));