@rig-c/parts 1.9.0 → 1.10.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/headframe.ts CHANGED
@@ -34,6 +34,7 @@
34
34
  import type { Point } from './config.ts';
35
35
  import { cropToSpineY } from './coords.ts';
36
36
  import type { Mask } from './raster/types.ts';
37
+ import { edgeLength } from './length.ts';
37
38
 
38
39
  /**
39
40
  * The eye line's angle in Spine degrees (counter-clockwise, y up) through two
@@ -167,7 +168,7 @@ export function headFrame(
167
168
  const [l, r] = eyes[0][0] < eyes[1][0] ? [eyes[0], eyes[1]] : [eyes[1], eyes[0]];
168
169
  const dx = r[0] - l[0];
169
170
  const dy = r[1] - l[1];
170
- const bar = Math.hypot(dx, dy);
171
+ const bar = edgeLength(dx, dy);
171
172
  const u: [number, number] = [dx / bar, dy / bar];
172
173
  // The bar's perpendicular toward the chin: u turned a quarter clockwise as drawn (y down).
173
174
  const n: [number, number] = [-u[1], u[0]];
@@ -23,6 +23,7 @@
23
23
  */
24
24
  import type { StageBox, Viewport } from './check.ts';
25
25
  import { cropToSpineY } from './coords.ts';
26
+ import { edgeLength } from './length.ts';
26
27
  import { OPAQUE_ALPHA_ABOVE } from './layers.ts';
27
28
  import { type PartRecord, type PartsFile, tagOf } from './parts.ts';
28
29
  import type { Raster } from './raster/types.ts';
@@ -446,7 +447,7 @@ export function halfTravels(art: ArtMask, box: { x: number; y: number; w: number
446
447
  for (const f of frames) {
447
448
  const c = f === null ? null : centroidIn(f, ...band);
448
449
  if (c === null) continue;
449
- max = Math.max(max, Math.hypot(c[0] - c0[0], c[1] - c0[1]));
450
+ max = Math.max(max, edgeLength(c[0] - c0[0], c[1] - c0[1]));
450
451
  }
451
452
  return max;
452
453
  };
@@ -508,7 +509,7 @@ export function readSine(keys: readonly Key[], duration: number): SineReading |
508
509
  }
509
510
  a *= 2 / n;
510
511
  b *= 2 / n;
511
- const mag = Math.hypot(a, b);
512
+ const mag = edgeLength(a, b);
512
513
  if (mag > best.mag) best = { j, a, b, mag };
513
514
  }
514
515
  if (best.j === 0) return null;
@@ -780,6 +781,9 @@ export function stretchSeverity(ratio: number): number {
780
781
  return ratio >= 1 ? ratio : 1 / ratio;
781
782
  }
782
783
 
784
+ /** An edge's length: {@link edgeLength} of `src/length.ts` (issues #270, #279), re-exported where the stretch reader first used it. */
785
+ export { edgeLength } from './length.ts';
786
+
783
787
  /**
784
788
  * For every mesh in `rest`, every triangle's three edges in every frame that
785
789
  * shows the attachment: deformed length over rest length, the largest and the
@@ -792,7 +796,7 @@ export function stretchFigures(rest: readonly MeshRest[], frames: readonly Geome
792
796
  return rest.map((m) => {
793
797
  const t = m.triangles;
794
798
  const v0 = m.vertices;
795
- const len = (v: readonly number[], a: number, b: number): number => Math.hypot(v[2 * a] - v[2 * b], v[2 * a + 1] - v[2 * b + 1]);
799
+ const len = (v: readonly number[], a: number, b: number): number => edgeLength(v[2 * a] - v[2 * b], v[2 * a + 1] - v[2 * b + 1]);
796
800
  const out: MeshStretch = { slot: m.slot, attachment: m.attachment, triangles: t.length / 3, frames: 0, max: null, min: null, degenerate: [] };
797
801
  const restLen: number[] = [];
798
802
  for (let i = 0; i < t.length; i += 3) {
@@ -1032,7 +1036,7 @@ export function stillReading(art: ArtMask, stage: StageBox, frames: ReadonlyArra
1032
1036
  const e2x = d[2 * l] - x0;
1033
1037
  const e2y = d[2 * l + 1] - y0;
1034
1038
  for (const bary of pc.pieces[t]) {
1035
- for (let q = 0; q < bary.length; q += 2) fmax = Math.max(fmax, Math.hypot(x0 + bary[q] * e1x + bary[q + 1] * e2x, y0 + bary[q] * e1y + bary[q + 1] * e2y));
1039
+ for (let q = 0; q < bary.length; q += 2) fmax = Math.max(fmax, edgeLength(x0 + bary[q] * e1x + bary[q + 1] * e2x, y0 + bary[q] * e1y + bary[q + 1] * e2y));
1036
1040
  }
1037
1041
  const cx = x0 + pc.centroid[2 * t] * e1x + pc.centroid[2 * t + 1] * e2x;
1038
1042
  const cy = y0 + pc.centroid[2 * t] * e1y + pc.centroid[2 * t + 1] * e2y;
@@ -1101,7 +1105,7 @@ export function crossingOf(art: ArtMask, stage: StageBox, frames: ReadonlyArray<
1101
1105
  if (Math.max(Math.abs(u - u0), Math.abs(v - v0)) !== r || inR(u, v)) continue;
1102
1106
  const sx0 = stage.x + u;
1103
1107
  const sy1 = stage.y + cropToSpineY(v, H);
1104
- best = Math.min(best, Math.hypot(Math.max(sx0 - x, 0, x - sx0 - 1), Math.max(sy1 - 1 - y, 0, y - sy1)));
1108
+ best = Math.min(best, edgeLength(Math.max(sx0 - x, 0, x - sx0 - 1), Math.max(sy1 - 1 - y, 0, y - sy1)));
1105
1109
  }
1106
1110
  }
1107
1111
  }
package/src/layers.ts CHANGED
@@ -25,6 +25,11 @@
25
25
  * and for a PSD it is the stacking order itself, with `depth` null because a
26
26
  * PSD carries none. Every refusal is collected and thrown once, so one run
27
27
  * names every missing file.
28
+ *
29
+ * A layer is named by a See-through v3 tag or, for a layer the author made
30
+ * by hand and added to the run (issue #256), by `author:<label>`
31
+ * (`src/tags.ts`, `AUTHOR_PREFIX`): read with `group` "author", it has no
32
+ * role, and its place in the draw order is read like any other layer's.
28
33
  */
29
34
  import { existsSync, readFileSync, statSync } from 'node:fs';
30
35
  import { basename, dirname, join } from 'node:path';
@@ -32,7 +37,7 @@ import { initializeCanvas, type Layer as PsdLayer, readPsd } from 'ag-psd';
32
37
  import { type Problem, refuseIfAny } from './errors.ts';
33
38
  import { decodePngBytes } from './raster/png.ts';
34
39
  import type { Raster } from './raster/types.ts';
35
- import { acceptedTagNames, readTag, type TagReading } from './tags.ts';
40
+ import { type LayerTagReading, layerTagRequirement, readLayerTag } from './tags.ts';
36
41
 
37
42
  /** Alpha strictly above this counts as painted. The reference uses the same cut for its ghost clean-up and its sheets. */
38
43
  export const OPAQUE_ALPHA_ABOVE = 8;
@@ -45,9 +50,9 @@ export const OPAQUE_ALPHA_ABOVE = 8;
45
50
  export const NEAR_WHITE_MIN = 235;
46
51
 
47
52
  export interface Layer {
48
- /** As the input names it; always a v3 tag, possibly with a `-r`/`-l` side. */
53
+ /** As the input names it: a v3 tag, possibly with a `-r`/`-l` side, or an author-made layer's `author:<label>`. */
49
54
  name: string;
50
- tag: TagReading;
55
+ tag: LayerTagReading;
51
56
  /** The file the pixels came from, or null for a PSD layer. */
52
57
  file: string | null;
53
58
  /** The layer's own pixels, `(right-left)x(bottom-top)`, straight alpha. */
@@ -197,9 +202,9 @@ export function readWrapperLayers(path: string): LayerSet {
197
202
  return;
198
203
  }
199
204
  const name = e.name;
200
- const tag = readTag(name);
205
+ const tag = readLayerTag(name);
201
206
  if (tag === null) {
202
- refuse('LAYERS_TAG_KNOWN', `layer "${name}"`, `is not a See-through v3 tag; one of ${acceptedTagNames().join(', ')} is required`);
207
+ refuse('LAYERS_TAG_KNOWN', `layer "${name}"`, layerTagRequirement(name));
203
208
  ok = false;
204
209
  }
205
210
  for (const key of ['left', 'top', 'right', 'bottom'] as const) {
@@ -369,9 +374,9 @@ export function readPsdLayers(path: string): LayerSet {
369
374
  const layers: Layer[] = [];
370
375
  leaves.forEach((layer) => {
371
376
  const name = layer.name ?? '';
372
- const tag = readTag(name);
377
+ const tag = readLayerTag(name);
373
378
  if (tag === null) {
374
- refuse('LAYERS_TAG_KNOWN', `PSD layer "${name}"`, `is not a See-through v3 tag; one of ${acceptedTagNames().join(', ')} is required`);
379
+ refuse('LAYERS_TAG_KNOWN', `PSD layer "${name}"`, layerTagRequirement(name));
375
380
  return;
376
381
  }
377
382
  if (seen.has(name)) {
package/src/length.ts ADDED
@@ -0,0 +1,21 @@
1
+ /**
2
+ * The one length this package writes (issues #270, #279): `sqrt(dx * dx + dy * dy)`.
3
+ *
4
+ * Not `Math.hypot`. The language does not promise how `Math.hypot` rounds, and
5
+ * Bun 1.4.2 rounds it differently on two platforms: on the demo's `earring`,
6
+ * edge 11-10 at idle frame 10 (dx 34.26716181006313, dy 0.5713103861783111),
7
+ * it is 34.27192399143071 on macOS and 34.27192399143072 on Linux, measured,
8
+ * with the posed vertices bit-identical on both. That last bit decided which of
9
+ * two edges 2 ulp apart held a mesh's largest ratio, so `TEXTURE_STRETCH`'s
10
+ * `max_at` named one triangle on one machine and another on the other (#276).
11
+ * A multiply, an add and a square root are each correctly rounded by IEEE 754,
12
+ * so this gives the same bits everywhere (34.27192399143071 on both, measured).
13
+ * At pixel scale `Math.hypot`'s guard against overflow is not needed: a
14
+ * coordinate would have to pass about 1e154 for the squares to overflow.
15
+ *
16
+ * The tree rule `TY15` refuses a call of `Math.hypot` anywhere under `src/` and in
17
+ * `cli.ts`, so every length is this one.
18
+ */
19
+ export function edgeLength(dx: number, dy: number): number {
20
+ return Math.sqrt(dx * dx + dy * dy);
21
+ }