@rig-c/parts 1.9.1 → 1.11.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 +2 -1
- package/cli.ts +128 -15
- package/docs/AUTHORING.md +193 -20
- package/docs/STABILITY.md +18 -8
- package/package.json +1 -1
- package/src/assemble.ts +100 -22
- package/src/build.ts +62 -9
- package/src/check.ts +150 -32
- package/src/config.ts +82 -7
- package/src/contour.ts +40 -2
- package/src/layers.ts +12 -7
- package/src/meshdraft.ts +888 -0
- package/src/parts.ts +41 -10
- package/src/propose.ts +137 -25
- package/src/requirements.ts +21 -6
- package/src/rig.ts +86 -18
- package/src/tags.ts +86 -0
- package/src/turn.ts +31 -0
- package/src/weights.ts +69 -11
package/src/contour.ts
CHANGED
|
@@ -35,7 +35,9 @@
|
|
|
35
35
|
* pixels, so its trace cannot cross itself; a notch narrower than about
|
|
36
36
|
* twice the margin grows shut. A margin above 0 and below 1 px adds no
|
|
37
37
|
* pixel and is refused by name. This module does not raise a margin by
|
|
38
|
-
* itself: a margin that clips art is refused (`CONTOUR_COVERAGE`).
|
|
38
|
+
* itself: a margin that clips art is refused (`CONTOUR_COVERAGE`). With
|
|
39
|
+
* `maxEdge` declared (issue #265), every simplified edge longer than it is
|
|
40
|
+
* then cut into equal pieces ({@link capOutlineEdges}); undeclared, nothing is.
|
|
39
41
|
* 4. **Interior vertices are declared**, never inferred: see
|
|
40
42
|
* {@link interiorCandidates} for the candidates and {@link KEEP_FRACTION}
|
|
41
43
|
* for the one rule that keeps or drops each. Where the outline passes
|
|
@@ -141,6 +143,7 @@
|
|
|
141
143
|
* automatic mode hands these triangles to rig-c as they are.
|
|
142
144
|
*/
|
|
143
145
|
import { type Problem } from './errors.ts';
|
|
146
|
+
import { edgeLength } from './length.ts';
|
|
144
147
|
import { counterClockwiseInSpineWorld } from './mesh.ts';
|
|
145
148
|
import { connectedComponents, fillHoles, type Mask } from './raster/index.ts';
|
|
146
149
|
import {
|
|
@@ -224,6 +227,12 @@ export interface ContourParams {
|
|
|
224
227
|
margin: number;
|
|
225
228
|
/** The background interior spacing, px, above 0. */
|
|
226
229
|
spacing: number;
|
|
230
|
+
/**
|
|
231
|
+
* The longest outline edge, px (issue #265): every edge of the simplified outline longer than this is cut into
|
|
232
|
+
* `ceil(length / maxEdge)` equal pieces ({@link capOutlineEdges}), before any region or rib touches the outline.
|
|
233
|
+
* Absent, nothing is cut and the mesh is byte for byte the one it was before the field existed; there is no default.
|
|
234
|
+
*/
|
|
235
|
+
maxEdge?: number;
|
|
227
236
|
/** Refuse a mesh with more vertices than this; absent is no budget. */
|
|
228
237
|
budget?: number;
|
|
229
238
|
/**
|
|
@@ -330,6 +339,8 @@ export interface ContourReport {
|
|
|
330
339
|
grownHolePixels: number;
|
|
331
340
|
/** Only when ribs were declared (issue #188): every rib, in placement order, with its vertices. Absent otherwise, so a mesh without ribs reports what it always reported. */
|
|
332
341
|
ribs?: ContourRib[];
|
|
342
|
+
/** Only when `maxEdge` was declared (issue #265): the outline vertices the cap added. Absent otherwise, so a mesh without it reports what it always reported. */
|
|
343
|
+
outlineCut?: number;
|
|
333
344
|
}
|
|
334
345
|
|
|
335
346
|
export interface ContourMesh {
|
|
@@ -382,6 +393,10 @@ function parameterProblems(part: string, mask: AlphaMask, p: ContourParams): Pro
|
|
|
382
393
|
else if (keepRadius(s) > MAX_RADIUS) bad(field, `is ${s}; its keep radius is ${keepRadius(s)} grid units, past the ${MAX_RADIUS} (${MAX_RADIUS / GRID} px) whose square is exact in a double`);
|
|
383
394
|
};
|
|
384
395
|
spacingOk('spacing', p.spacing);
|
|
396
|
+
// issue #265: a cut piece must be longer than one grid unit, so two cut points never snap onto one another.
|
|
397
|
+
if (p.maxEdge !== undefined && (!finite(p.maxEdge) || p.maxEdge * GRID < 2)) {
|
|
398
|
+
bad('maxEdge', `is ${p.maxEdge}; a number of px of at least 2/${GRID} is required — each edge is cut into pieces longer than half of it, and a piece must be longer than one 1/${GRID} px grid unit for its two ends to stay apart once snapped`);
|
|
399
|
+
}
|
|
385
400
|
if (p.budget !== undefined && (!Number.isInteger(p.budget) || p.budget < 3)) bad('budget', `is ${p.budget}; a whole number of vertices, 3 or more, is required`);
|
|
386
401
|
if (p.fitConnectivity !== undefined && p.fitConnectivity !== 4 && p.fitConnectivity !== 8) bad('fitConnectivity', `is ${JSON.stringify(p.fitConnectivity)}; 4 or 8 is required (how the background is flooded when overshoot is measured)`);
|
|
387
402
|
if (p.stray !== undefined && (!Number.isInteger(p.stray) || p.stray < 0)) bad('stray', `is ${p.stray}; a whole number of art pixels, 0 or more, is required (the largest island that may be left out)`);
|
|
@@ -736,6 +751,26 @@ export function outlineInRegions(outline: ReadonlyArray<readonly [number, number
|
|
|
736
751
|
return out;
|
|
737
752
|
}
|
|
738
753
|
|
|
754
|
+
/**
|
|
755
|
+
* An outline with every edge longer than `maxEdge` px cut into `n = ceil(length / maxEdge)` equal pieces (issue
|
|
756
|
+
* #265): between each edge's two ends, the points `a + (b − a)·k/n` for k = 1..n−1, each snapped to the {@link GRID}.
|
|
757
|
+
* Every vertex of the outline is kept, in the same cyclic order and position from index 0, and an edge at or under
|
|
758
|
+
* `maxEdge` gains nothing. The length is {@link edgeLength}'s. The snap moves a cut point off its edge by at most
|
|
759
|
+
* half a grid unit (1/512 px); the mesh's topology and coverage checks read the cut outline as they read any other.
|
|
760
|
+
* The measured reference is `capOutline` in `tools/outline_survey.ts` (issue #265, (G′)), which this states in `src/`.
|
|
761
|
+
*/
|
|
762
|
+
export function capOutlineEdges(outline: ReadonlyArray<readonly [number, number]>, maxEdge: number): Array<[number, number]> {
|
|
763
|
+
const out: Array<[number, number]> = [];
|
|
764
|
+
for (let i = 0; i < outline.length; i++) {
|
|
765
|
+
const [ax, ay] = outline[i];
|
|
766
|
+
const [bx, by] = outline[(i + 1) % outline.length];
|
|
767
|
+
out.push([ax, ay]);
|
|
768
|
+
const n = Math.ceil(edgeLength(bx - ax, by - ay) / maxEdge);
|
|
769
|
+
for (let k = 1; k < n; k++) out.push([snap(ax + ((bx - ax) * k) / n) / GRID, snap(ay + ((by - ay) * k) / n) / GRID]);
|
|
770
|
+
}
|
|
771
|
+
return out;
|
|
772
|
+
}
|
|
773
|
+
|
|
739
774
|
/**
|
|
740
775
|
* The keep rule ({@link KEEP_FRACTION}) over the candidates in order, against
|
|
741
776
|
* an outline in grid units. Exact: inside by the crossing rule on integers,
|
|
@@ -1767,7 +1802,9 @@ export function contourMesh(part: string, mask: AlphaMask, params: ContourParams
|
|
|
1767
1802
|
const stage = contourOutline(part, filled, threshold, tolerance, margin);
|
|
1768
1803
|
if (Array.isArray(stage)) return stage;
|
|
1769
1804
|
const { grown } = stage;
|
|
1770
|
-
|
|
1805
|
+
// issue #265: the cap cuts the simplified outline's long edges first; undeclared, the outline is the stage's own.
|
|
1806
|
+
const capped = params.maxEdge === undefined ? stage.outline : capOutlineEdges(stage.outline, params.maxEdge);
|
|
1807
|
+
let outline = outlineInRegions(capped, params.regions);
|
|
1771
1808
|
// issue #188: the ribs' ends join the outline, and their interior points come first among the interior vertices.
|
|
1772
1809
|
// None declared, nothing here runs and every list below is the one it always was.
|
|
1773
1810
|
let ribs: RibPlacement | null = null;
|
|
@@ -1854,6 +1891,7 @@ export function contourMesh(part: string, mask: AlphaMask, params: ContourParams
|
|
|
1854
1891
|
pinchFilledPixels: grown.pinchFilledPixels,
|
|
1855
1892
|
grownHolePixels: stage.grownHolePixels,
|
|
1856
1893
|
...(ribs === null ? {} : { ribs: ribs.ribs }),
|
|
1894
|
+
...(params.maxEdge === undefined ? {} : { outlineCut: capped.length - stage.outline.length }),
|
|
1857
1895
|
},
|
|
1858
1896
|
};
|
|
1859
1897
|
}
|
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 {
|
|
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
|
|
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:
|
|
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 =
|
|
205
|
+
const tag = readLayerTag(name);
|
|
201
206
|
if (tag === null) {
|
|
202
|
-
refuse('LAYERS_TAG_KNOWN', `layer "${name}"`,
|
|
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 =
|
|
377
|
+
const tag = readLayerTag(name);
|
|
373
378
|
if (tag === null) {
|
|
374
|
-
refuse('LAYERS_TAG_KNOWN', `PSD layer "${name}"`,
|
|
379
|
+
refuse('LAYERS_TAG_KNOWN', `PSD layer "${name}"`, layerTagRequirement(name));
|
|
375
380
|
return;
|
|
376
381
|
}
|
|
377
382
|
if (seen.has(name)) {
|