@rig-c/parts 1.24.0 → 1.26.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/src/squeeze.ts CHANGED
@@ -37,7 +37,7 @@
37
37
  * its own figure too. A remedy whose figure is past the bar is not offered.
38
38
  */
39
39
  import { TEXTURE_STRETCH_CEILING } from './check.ts';
40
- import type { BoneEntry, MeshSpec, Motion, Point } from './config.ts';
40
+ import type { BoneEntry, ConfigConstraint, MeshSpec, Motion, Point } from './config.ts';
41
41
  import { stretchFigures, stretchSeverity, type StretchAt } from './instruments.ts';
42
42
  import { edgeLength } from './length.ts';
43
43
  import { ART_ALPHA, latticeMesh, latticeStray } from './mesh.ts';
@@ -77,6 +77,27 @@ export interface SqueezeFinding {
77
77
  turned: number | null;
78
78
  }
79
79
 
80
+ /**
81
+ * Issue #403: two sibling branch tips forked from the same trunk link on a lattice mesh whose cell-boundary weights
82
+ * pin adjacent vertices to different branch links (or whose fork gives the two branches unequal spring strength), so
83
+ * the triangles where the two branches' cells meet are squeezed under the environment or a hip sway.
84
+ */
85
+ export interface BranchSqueezeFinding {
86
+ mesh: string;
87
+ parent: string;
88
+ a: string;
89
+ b: string;
90
+ tipGap: number;
91
+ triangle: number;
92
+ edge: [number, number];
93
+ weightA: number;
94
+ weightB: number;
95
+ strengthA: number | null;
96
+ strengthB: number | null;
97
+ r: number;
98
+ grid: number;
99
+ }
100
+
80
101
  export interface SqueezeResult {
81
102
  findings: SqueezeFinding[];
82
103
  /** Pairs on one mesh this did not read, and why. */
@@ -272,3 +293,142 @@ export function chainSqueezes(
272
293
  }
273
294
  return { findings, unread, read };
274
295
  }
296
+
297
+ /**
298
+ * Issue #403: sibling branch chains forked from the same trunk link on a lattice mesh under the distance rule.
299
+ * When `m.r < m.grid` concentrates a cell-boundary edge's weights on each branch's tip link (above 0.85), or when
300
+ * physics constraints give two sibling branches of one fork unequal `strength` (such as `edge` 50 beside `branch` 100),
301
+ * the triangles where the two branches' cells meet squeeze under the environment or a hip sway. Returns one finding per
302
+ * unheld sibling pair on a fork; quiet on an unbranched mesh or when `split.hold_tip: true` holds the fork.
303
+ */
304
+ export function branchSqueezes(
305
+ spec: { bones: readonly BoneEntry[]; meshes: Readonly<Record<string, MeshSpec>>; constraints?: readonly ConfigConstraint[] },
306
+ parts: ReadonlyMap<string, { x: number; y: number; image: Raster }>,
307
+ ): BranchSqueezeFinding[] {
308
+ const out: BranchSqueezeFinding[] = [];
309
+ const chains = new Map<string, { points: Point[]; tip: Point; parent: string }>();
310
+ for (const e of spec.bones) if ('chain' in e) chains.set(e.chain, { points: e.points, tip: e.tip, parent: e.parent });
311
+ const linkOf = new Set<string>();
312
+ for (const [c, e] of chains) e.points.forEach((_, i) => linkOf.add(`${c}${i}`));
313
+ const strengthOf = new Map<string, number>();
314
+ for (const c of spec.constraints ?? []) {
315
+ if (c.type === 'physics' && typeof c.bone === 'string' && c.rotate !== undefined && typeof c.strength === 'number') {
316
+ strengthOf.set(c.bone, c.strength);
317
+ }
318
+ }
319
+
320
+ for (const [mname, m] of Object.entries(spec.meshes)) {
321
+ if ('contour' in m || 'auto' in m || m.rule === 'heat' || m.r >= m.grid) continue;
322
+ const byFork = new Map<string, string[]>();
323
+ for (const sp of m.segments) {
324
+ if (typeof sp !== 'string') continue;
325
+ const c = chains.get(sp);
326
+ if (c === undefined || !linkOf.has(c.parent)) continue;
327
+ const list = byFork.get(c.parent) ?? [];
328
+ list.push(sp);
329
+ byFork.set(c.parent, list);
330
+ }
331
+ const forks = [...byFork.entries()].filter(([, list]) => list.length >= 2);
332
+ if (forks.length === 0) continue;
333
+ const part = parts.get(mname);
334
+ if (part === undefined) continue;
335
+ const segs: Segment[] = [];
336
+ let unresolved = false;
337
+ for (const sp of m.segments) {
338
+ if (typeof sp !== 'string') {
339
+ segs.push({ bone: sp[0], a: [sp[1][0], sp[1][1]], b: [sp[2][0], sp[2][1]] });
340
+ continue;
341
+ }
342
+ const c = chains.get(sp);
343
+ if (c !== undefined) {
344
+ const line = [...c.points, c.tip];
345
+ c.points.forEach((q, i) => segs.push({ bone: `${sp}${i}`, a: [q[0], q[1]], b: [line[i + 1][0], line[i + 1][1]] }));
346
+ continue;
347
+ }
348
+ const single = spec.bones.find((e) => 'name' in e && e.name === sp);
349
+ if (single === undefined || !('name' in single) || single.tip === undefined) {
350
+ unresolved = true;
351
+ break;
352
+ }
353
+ segs.push({ bone: sp, a: [single.at[0], single.at[1]], b: [single.tip[0], single.tip[1]] });
354
+ }
355
+ if (unresolved) continue;
356
+ const img = pad(part.image, PAD, PAD, PAD, PAD, [0, 0, 0, 0]);
357
+ const allArt = alphaAbove(img, ART_ALPHA);
358
+ const art = m.stray === undefined ? allArt : latticeStray(allArt, m.stray).art;
359
+ const lm = latticeMesh(mname, art, m.grid);
360
+ if ('code' in lm) continue;
361
+ const ox = part.x - PAD;
362
+ const oy = part.y - PAD;
363
+ const limits = meshLimits(m.influences);
364
+ const rest: Array<[number, number]> = lm.vertices.map(([x, y]) => [x + ox, y + oy]);
365
+ const weights = rest.map((p) => {
366
+ const shares = influences(p, segs, m.r, limits, m.exponent ?? DISTANCE_EXPONENT);
367
+ return shares.length === 0 ? [] : limits.floor === 'continuous' ? roundShares(shares) : latticeShares(shares);
368
+ });
369
+ const topOf = (vi: number): { bone: string; weight: number } | null => {
370
+ const w = weights[vi];
371
+ if (w.length === 0) return null;
372
+ return [...w].sort((p, q) => q.weight - p.weight)[0];
373
+ };
374
+ for (const [parent, siblings] of forks) {
375
+ for (let i = 0; i + 1 < siblings.length; i++) {
376
+ const aChain = siblings[i];
377
+ const bChain = siblings[i + 1];
378
+ const ca = chains.get(aChain) as { points: Point[]; tip: Point };
379
+ const cb = chains.get(bChain) as { points: Point[]; tip: Point };
380
+ const aLast = `${aChain}${ca.points.length - 1}`;
381
+ const bLast = `${bChain}${cb.points.length - 1}`;
382
+ let best: { triangle: number; edge: [number, number]; weightA: number; weightB: number; y: number } | null = null;
383
+ for (let t = 0; t + 2 < lm.triangles.length; t += 3) {
384
+ const tri = [lm.triangles[t], lm.triangles[t + 1], lm.triangles[t + 2]];
385
+ const triIdx = t / 3;
386
+ const meanY = (rest[tri[0]][1] + rest[tri[1]][1] + rest[tri[2]][1]) / 3;
387
+ for (const [u, v] of [
388
+ [tri[0], tri[1]],
389
+ [tri[1], tri[2]],
390
+ [tri[2], tri[0]],
391
+ ] as const) {
392
+ const tu = topOf(u);
393
+ const tv = topOf(v);
394
+ if (tu === null || tv === null) continue;
395
+ const match =
396
+ tu.bone === aLast && tv.bone === bLast
397
+ ? { edge: [u, v] as [number, number], weightA: tu.weight, weightB: tv.weight }
398
+ : tu.bone === bLast && tv.bone === aLast
399
+ ? { edge: [v, u] as [number, number], weightA: tv.weight, weightB: tu.weight }
400
+ : null;
401
+ if (match === null) continue;
402
+ const peak = Math.max(match.weightA, match.weightB);
403
+ const bestPeak = best === null ? -1 : Math.max(best.weightA, best.weightB);
404
+ if (best === null || peak > bestPeak || (peak === bestPeak && meanY > best.y)) {
405
+ best = { triangle: triIdx, edge: match.edge, weightA: match.weightA, weightB: match.weightB, y: meanY };
406
+ }
407
+ }
408
+ }
409
+ if (best === null) continue;
410
+ const strengthA = strengthOf.get(aLast) ?? null;
411
+ const strengthB = strengthOf.get(bLast) ?? null;
412
+ const unequalStrength = strengthA !== null && strengthB !== null && strengthA !== strengthB;
413
+ const narrowReach = m.r < m.grid && Math.max(best.weightA, best.weightB) > 0.85;
414
+ if (!unequalStrength && !narrowReach) continue;
415
+ out.push({
416
+ mesh: mname,
417
+ parent,
418
+ a: aLast,
419
+ b: bLast,
420
+ tipGap: edgeLength(ca.tip[0] - cb.tip[0], ca.tip[1] - cb.tip[1]),
421
+ triangle: best.triangle,
422
+ edge: best.edge,
423
+ weightA: best.weightA,
424
+ weightB: best.weightB,
425
+ strengthA,
426
+ strengthB,
427
+ r: m.r,
428
+ grid: m.grid,
429
+ });
430
+ }
431
+ }
432
+ }
433
+ return out;
434
+ }