@logomarks/core 0.1.7 → 0.1.9
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/CHANGELOG.md +23 -0
- package/README.md +1 -0
- package/package.json +10 -3
- package/src/analyse/index.d.ts +3 -1
- package/src/analyse/index.js +4 -2
- package/src/analyse/shape/circle.d.ts +15 -0
- package/src/analyse/shape/circle.js +77 -0
- package/src/analyse/shape/hull.d.ts +15 -0
- package/src/analyse/shape/hull.js +113 -0
- package/src/analyse/shape/ink.d.ts +28 -0
- package/src/analyse/shape/ink.js +95 -0
- package/src/analyse/shape/measures.d.ts +52 -0
- package/src/analyse/shape/measures.js +131 -0
- package/src/analyse/shape/symmetry.d.ts +13 -0
- package/src/analyse/shape/symmetry.js +84 -0
- package/src/analyse/shape/thickness.d.ts +6 -0
- package/src/analyse/shape/thickness.js +62 -0
- package/src/analyse/tags.d.ts +17 -1
- package/src/analyse/tags.js +68 -33
- package/src/index.d.ts +1 -0
- package/src/index.js +3 -2
- package/src/stitch/border.d.ts +2 -0
- package/src/stitch/border.js +40 -0
- package/src/stitch/field.d.ts +14 -0
- package/src/stitch/field.js +207 -0
- package/src/stitch/fill.d.ts +2 -0
- package/src/stitch/fill.js +67 -0
- package/src/stitch/index.d.ts +6 -0
- package/src/stitch/index.js +14 -0
- package/src/stitch/plan.d.ts +46 -0
- package/src/stitch/plan.js +127 -0
- package/src/stitch/regions.d.ts +4 -0
- package/src/stitch/regions.js +54 -0
- package/src/stitch/satin.d.ts +2 -0
- package/src/stitch/satin.js +86 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module @logomarks/core/stitch
|
|
3
|
+
*
|
|
4
|
+
* Machine embroidery planned from a mark's outlines: the ink split by width
|
|
5
|
+
* into fill and satin, tatami rows over the fills, satin threads across the
|
|
6
|
+
* strokes, a satin border round the fills. Pure geometry, seeded; a
|
|
7
|
+
* renderer (`@logomarks/mockups`' embroidery finish) draws the stitches as
|
|
8
|
+
* thread.
|
|
9
|
+
*/
|
|
10
|
+
export { STITCH, SATIN_DENSITY, stitchPlan, stitchCount } from './plan.js';
|
|
11
|
+
export { splitByWidth } from './regions.js';
|
|
12
|
+
export { fillStitches } from './fill.js';
|
|
13
|
+
export { satinStitches } from './satin.js';
|
|
14
|
+
export { borderStitches } from './border.js';
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/** The planner's settings. Sizes are in millimetres. */
|
|
2
|
+
export interface StitchSettings {
|
|
3
|
+
/** How far the mark's canvas (its viewBox's longer side) spans, in mm (55). */
|
|
4
|
+
size: number;
|
|
5
|
+
/** The distance between rows of fill, in mm (0.42); satin threads lie `SATIN_DENSITY` times as far apart. */
|
|
6
|
+
density: number;
|
|
7
|
+
/** The widest part stitched in satin, in mm (3.6); wider parts are filled. */
|
|
8
|
+
satin: number;
|
|
9
|
+
/** The fill's stitch length, in mm (3.5). */
|
|
10
|
+
stitch: number;
|
|
11
|
+
/** The fill rows' angle, in degrees: 0 runs across, 90 down (45). */
|
|
12
|
+
angle: number;
|
|
13
|
+
/** The satin border round each fill, in mm; 0 for none (0.9). */
|
|
14
|
+
border: number;
|
|
15
|
+
/** The raster's pitch, in pixels a mm (10). */
|
|
16
|
+
resolution: number;
|
|
17
|
+
/** Seeds the small irregularities ('1'). */
|
|
18
|
+
seed: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** A mark's stitches. Each list is flat, four numbers a stitch (`x0, y0, x1, y1`), in the mark's own units, in the order they're sewn. */
|
|
22
|
+
export interface StitchPlan {
|
|
23
|
+
/** The mark's viewBox: x, y, width, height. */
|
|
24
|
+
viewBox: [number, number, number, number];
|
|
25
|
+
/** Millimetres a unit of the mark. */
|
|
26
|
+
mm: number;
|
|
27
|
+
/** The settings it was planned with. */
|
|
28
|
+
settings: StitchSettings;
|
|
29
|
+
/** Tatami rows over the wide parts. */
|
|
30
|
+
fill: number[];
|
|
31
|
+
/** Satin threads across the narrow parts. */
|
|
32
|
+
satin: number[];
|
|
33
|
+
/** The satin border round each fill, from the edge inwards. */
|
|
34
|
+
border: number[];
|
|
35
|
+
/** How much of the mark is filled and how much is satin, in mm². */
|
|
36
|
+
area: { fill: number; satin: number };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Satin's thread spacing as a share of `density` (0.82). */
|
|
40
|
+
export const SATIN_DENSITY: number;
|
|
41
|
+
/** The defaults. */
|
|
42
|
+
export const STITCH: Readonly<StitchSettings>;
|
|
43
|
+
/** The stitches for a mark's SVG text. Throws a TypeError for a bad option or a file with nothing filled. */
|
|
44
|
+
export function stitchPlan(svg: string, options?: Partial<Omit<StitchSettings, 'seed'>> & { seed?: string | number }): StitchPlan;
|
|
45
|
+
/** How many stitches a plan has, of each kind and in all. */
|
|
46
|
+
export function stitchCount(plan: Pick<StitchPlan, 'fill' | 'satin' | 'border'>): { fill: number; satin: number; border: number; total: number };
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module @logomarks/core/stitch/plan
|
|
3
|
+
*
|
|
4
|
+
* A mark planned as machine embroidery, from its outlines: where each
|
|
5
|
+
* stitch lies. The ink is rasterised at a fine pitch (a tenth of a
|
|
6
|
+
* millimetre by default) at the size it will be stitched, split by width
|
|
7
|
+
* into fill (wide) and satin (narrow) parts, and each part covered as an
|
|
8
|
+
* embroiderer would: tatami rows over the fills, satin threads across the
|
|
9
|
+
* strokes, and a satin border round each fill. The stitches come back in the
|
|
10
|
+
* mark's own units, for a renderer to draw as thread. Seeded: the same mark,
|
|
11
|
+
* options and seed always give the same stitches.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { rings } from '../svg/rings.js';
|
|
15
|
+
import { shapes, viewBox } from '../svg/shapes.js';
|
|
16
|
+
import { frameOf, fill } from '../raster/scanline.js';
|
|
17
|
+
import { distance } from '../raster/distance.js';
|
|
18
|
+
import { seeded } from '../geometry/random.js';
|
|
19
|
+
import { splitByWidth } from './regions.js';
|
|
20
|
+
import { fillStitches } from './fill.js';
|
|
21
|
+
import { satinStitches } from './satin.js';
|
|
22
|
+
import { borderStitches } from './border.js';
|
|
23
|
+
|
|
24
|
+
/** The defaults, in millimetres and degrees: the planner's settings for a mark stitched about 55 mm across. */
|
|
25
|
+
export const STITCH = Object.freeze({
|
|
26
|
+
/** How far the mark's canvas (its viewBox's longer side) spans, in mm. */
|
|
27
|
+
size: 55,
|
|
28
|
+
/** The distance between rows of fill, in mm; satin threads lie a little closer (`SATIN_DENSITY` of it). */
|
|
29
|
+
density: 0.42,
|
|
30
|
+
/** The widest part stitched in satin, in mm; wider parts are filled. */
|
|
31
|
+
satin: 3.6,
|
|
32
|
+
/** The fill's stitch length, in mm. */
|
|
33
|
+
stitch: 3.5,
|
|
34
|
+
/** The fill rows' angle, in degrees (0 runs across, 90 down). */
|
|
35
|
+
angle: 45,
|
|
36
|
+
/** The satin border round each fill, in mm (0 for none). */
|
|
37
|
+
border: 0.9,
|
|
38
|
+
/** The raster's pitch, in pixels a mm. */
|
|
39
|
+
resolution: 10,
|
|
40
|
+
/** Seeds the small irregularities. */
|
|
41
|
+
seed: '1',
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
/** Satin's thread spacing as a share of the fill's row spacing: satin is sewn denser, so its threads close up. */
|
|
45
|
+
export const SATIN_DENSITY = 0.82;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* A positive number option, or its default.
|
|
49
|
+
*
|
|
50
|
+
* @param {Record<string, unknown>} o The options.
|
|
51
|
+
* @param {'size' | 'density' | 'satin' | 'stitch' | 'border' | 'resolution'} k The key.
|
|
52
|
+
* @param {boolean} [zero=false] Whether 0 is allowed too.
|
|
53
|
+
* @returns {number} The value.
|
|
54
|
+
* @throws {TypeError} When it isn't one.
|
|
55
|
+
*/
|
|
56
|
+
function positive(o, k, zero = false) {
|
|
57
|
+
const v = o[k] ?? STITCH[k];
|
|
58
|
+
if (typeof v !== 'number' || !Number.isFinite(v) || v < 0 || (v === 0 && !zero)) throw new TypeError(`stitchPlan: ${k} is a number over 0${zero ? ', or 0' : ''} (got ${JSON.stringify(o[k])})`);
|
|
59
|
+
return v;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The stitches for a mark.
|
|
64
|
+
*
|
|
65
|
+
* @param {string} svg The mark's SVG text (its filled paths, circles, ellipses, rects and polygons, each with its fill rule).
|
|
66
|
+
* @param {Partial<typeof STITCH>} [options] The settings (see `STITCH`).
|
|
67
|
+
* @returns {import('./plan.js').StitchPlan} The plan.
|
|
68
|
+
* @throws {TypeError} For a bad option or a file with nothing filled.
|
|
69
|
+
*/
|
|
70
|
+
export function stitchPlan(svg, options = {}) {
|
|
71
|
+
if (typeof svg !== 'string') throw new TypeError('stitchPlan: pass the mark\'s SVG text');
|
|
72
|
+
const o = /** @type {Record<string, unknown>} */ (options ?? {});
|
|
73
|
+
const size = positive(o, 'size');
|
|
74
|
+
const density = positive(o, 'density');
|
|
75
|
+
const satin = positive(o, 'satin');
|
|
76
|
+
const stitch = positive(o, 'stitch');
|
|
77
|
+
const border = positive(o, 'border', true);
|
|
78
|
+
const resolution = positive(o, 'resolution');
|
|
79
|
+
const angle = o.angle ?? STITCH.angle;
|
|
80
|
+
if (typeof angle !== 'number' || !Number.isFinite(angle)) throw new TypeError(`stitchPlan: angle is a number of degrees (got ${JSON.stringify(o.angle)})`);
|
|
81
|
+
const seed = String(o.seed ?? STITCH.seed);
|
|
82
|
+
|
|
83
|
+
const paths = shapes(svg).map(({ segs, evenodd }) => ({ rings: rings(segs, 0.5), evenodd }));
|
|
84
|
+
if (!paths.length) throw new TypeError('stitchPlan: the SVG has nothing filled');
|
|
85
|
+
const vb = viewBox(svg) ?? [0, 0, 512, 512];
|
|
86
|
+
const mm = size / Math.max(vb[2], vb[3]);
|
|
87
|
+
const K = resolution * mm;
|
|
88
|
+
const box = frameOf(paths, K, 2);
|
|
89
|
+
const frame = { ...box, width: Math.ceil(box.width), height: Math.ceil(box.height) };
|
|
90
|
+
const bm = fill(paths, frame);
|
|
91
|
+
const { width: W, height: H } = bm;
|
|
92
|
+
const dist = distance(bm, 1);
|
|
93
|
+
const half = (satin / 2) * resolution;
|
|
94
|
+
const { wide, narrow } = splitByWidth(bm, dist, half);
|
|
95
|
+
|
|
96
|
+
const random = seeded(`stitch:${seed}`);
|
|
97
|
+
const toUnits = (px) => px.map((v, i) => Math.round(((i % 2 ? frame.y0 : frame.x0) + (v + 0.5) / K) * 1000) / 1000);
|
|
98
|
+
const rows = fillStitches(wide, W, H, { spacing: density * resolution, length: stitch * resolution, angle, random });
|
|
99
|
+
const threads = satinStitches(narrow, dist, W, H, { spacing: density * SATIN_DENSITY * resolution, half, smooth: 0.6 * resolution });
|
|
100
|
+
const edge = borderStitches(wide, dist, W, H, { width: border * resolution, spacing: density * resolution });
|
|
101
|
+
let [wideArea, narrowArea] = [0, 0];
|
|
102
|
+
for (let i = 0; i < W * H; i++) {
|
|
103
|
+
wideArea += wide[i];
|
|
104
|
+
narrowArea += narrow[i];
|
|
105
|
+
}
|
|
106
|
+
const px2 = resolution * resolution;
|
|
107
|
+
return {
|
|
108
|
+
viewBox: /** @type {[number, number, number, number]} */ (vb.slice(0, 4)),
|
|
109
|
+
mm,
|
|
110
|
+
settings: { size, density, satin, stitch, angle, border, resolution, seed },
|
|
111
|
+
fill: toUnits(rows),
|
|
112
|
+
satin: toUnits(threads),
|
|
113
|
+
border: toUnits(edge),
|
|
114
|
+
area: { fill: Math.round((wideArea / px2) * 10) / 10, satin: Math.round((narrowArea / px2) * 10) / 10 },
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* How many stitches a plan has, of each kind and in all.
|
|
120
|
+
*
|
|
121
|
+
* @param {Pick<import('./plan.js').StitchPlan, 'fill' | 'satin' | 'border'>} plan The plan.
|
|
122
|
+
* @returns {{ fill: number, satin: number, border: number, total: number }} The counts.
|
|
123
|
+
*/
|
|
124
|
+
export function stitchCount(plan) {
|
|
125
|
+
const [f, s, b] = [plan.fill.length / 4, plan.satin.length / 4, plan.border.length / 4];
|
|
126
|
+
return { fill: f, satin: s, border: b, total: f + s + b };
|
|
127
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { Bitmap } from '../raster/scanline.js';
|
|
2
|
+
|
|
3
|
+
/** The ink split by width: `wide` where a disc of radius `half` (pixels) fits (the shape opened by it), `narrow` elsewhere; small corner slivers go to `wide`. */
|
|
4
|
+
export function splitByWidth(bm: Bitmap, dist: Float32Array, half: number): { wide: Uint8Array; narrow: Uint8Array };
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module @logomarks/core/stitch/regions
|
|
3
|
+
*
|
|
4
|
+
* A mark's ink split by width, as an embroiderer splits it: the parts a
|
|
5
|
+
* disc wider than the longest satin stitch fits inside (the shape opened by
|
|
6
|
+
* that disc) are filled with rows of short stitches; the rest, the strokes
|
|
7
|
+
* and thin parts, are satin. The opening is two distance transforms. The
|
|
8
|
+
* slivers it leaves in a wide part's corners are small, and go back to the
|
|
9
|
+
* fill they belong to.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { distance } from '../raster/distance.js';
|
|
13
|
+
import { regions } from './field.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The ink's wide and narrow parts.
|
|
17
|
+
*
|
|
18
|
+
* @param {import('../raster/scanline.js').Bitmap} bm The ink.
|
|
19
|
+
* @param {Float32Array} dist Each ink pixel's distance to the edge.
|
|
20
|
+
* @param {number} half Half the widest satin, in pixels.
|
|
21
|
+
* @returns {{ wide: Uint8Array, narrow: Uint8Array }} 1 where each is.
|
|
22
|
+
*/
|
|
23
|
+
export function splitByWidth(bm, dist, half) {
|
|
24
|
+
const { width: W, height: H, data } = bm;
|
|
25
|
+
const n = W * H;
|
|
26
|
+
// the centres of discs wider than a satin stitch, then everything those discs cover
|
|
27
|
+
const core = new Uint8Array(n);
|
|
28
|
+
for (let i = 0; i < n; i++) core[i] = dist[i] > half ? 1 : 0;
|
|
29
|
+
const toCore = distance({ ...bm, data: core }, 0);
|
|
30
|
+
const wide = new Uint8Array(n);
|
|
31
|
+
const narrow = new Uint8Array(n);
|
|
32
|
+
for (let i = 0; i < n; i++) {
|
|
33
|
+
if (!data[i]) continue;
|
|
34
|
+
if (core[i] || toCore[i] <= half) wide[i] = 1;
|
|
35
|
+
else narrow[i] = 1;
|
|
36
|
+
}
|
|
37
|
+
// small narrow pieces against a wide part are the corners the disc couldn't reach: they're fill
|
|
38
|
+
const { label, sizes } = regions(narrow, W, H);
|
|
39
|
+
const touches = new Uint8Array(sizes.length);
|
|
40
|
+
for (let i = 0; i < n; i++) {
|
|
41
|
+
if (label[i] < 0) continue;
|
|
42
|
+
const x = i % W;
|
|
43
|
+
if ((x > 0 && wide[i - 1]) || (x < W - 1 && wide[i + 1]) || (i >= W && wide[i - W]) || (i < n - W && wide[i + W])) touches[label[i]] = 1;
|
|
44
|
+
}
|
|
45
|
+
const small = 2 * half * half;
|
|
46
|
+
for (let i = 0; i < n; i++) {
|
|
47
|
+
const l = label[i];
|
|
48
|
+
if (l >= 0 && touches[l] && sizes[l] < small) {
|
|
49
|
+
narrow[i] = 0;
|
|
50
|
+
wide[i] = 1;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return { wide, narrow };
|
|
54
|
+
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
/** Satin threads across a mask, edge to edge, along the stroke's local direction (pixel coordinates, flat `x0, y0, x1, y1` each). */
|
|
2
|
+
export function satinStitches(mask: Uint8Array, dist: Float32Array, W: number, H: number, o: { spacing: number; half: number; smooth: number }): number[];
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module @logomarks/core/stitch/satin
|
|
3
|
+
*
|
|
4
|
+
* Satin stitch: the narrow parts covered by threads laid side by side
|
|
5
|
+
* straight across them, edge to edge, each following the stroke's local
|
|
6
|
+
* direction. The threads start on the stroke's middle line (the ridge of
|
|
7
|
+
* its distance field), spaced evenly along it, and run both ways along the
|
|
8
|
+
* direction across until the edge. Where the picks along the middle line
|
|
9
|
+
* leave a gap, and where threads fan apart (the outside of a bend, a
|
|
10
|
+
* stroke's end), more threads are laid through what's left bare.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { acrossField, sample, spaced, reach } from './field.js';
|
|
14
|
+
|
|
15
|
+
/** How many times the threads are topped up through what's still bare. */
|
|
16
|
+
const PASSES = 2;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Marks the pixels near a stitch as covered.
|
|
20
|
+
*
|
|
21
|
+
* @param {Uint8Array} covered The coverage, changed in place.
|
|
22
|
+
* @param {number} W The width.
|
|
23
|
+
* @param {number} H The height.
|
|
24
|
+
* @param {number[]} s The stitch: `x0, y0, x1, y1`.
|
|
25
|
+
* @param {number} r How near, in pixels.
|
|
26
|
+
*/
|
|
27
|
+
function cover(covered, W, H, [x0, y0, x1, y1], r) {
|
|
28
|
+
const [dx, dy] = [x1 - x0, y1 - y0];
|
|
29
|
+
const L2 = Math.max(1e-9, dx * dx + dy * dy);
|
|
30
|
+
for (let y = Math.max(0, Math.floor(Math.min(y0, y1) - r)); y <= Math.min(H - 1, Math.ceil(Math.max(y0, y1) + r)); y++) {
|
|
31
|
+
for (let x = Math.max(0, Math.floor(Math.min(x0, x1) - r)); x <= Math.min(W - 1, Math.ceil(Math.max(x0, x1) + r)); x++) {
|
|
32
|
+
const t = Math.max(0, Math.min(1, ((x - x0) * dx + (y - y0) * dy) / L2));
|
|
33
|
+
if ((x - x0 - t * dx) ** 2 + (y - y0 - t * dy) ** 2 <= r * r) covered[y * W + x] = 1;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The satin stitches over a mask.
|
|
40
|
+
*
|
|
41
|
+
* @param {Uint8Array} mask Where (1 inside: the narrow parts).
|
|
42
|
+
* @param {Float32Array} dist Each ink pixel's distance to the edge of the ink.
|
|
43
|
+
* @param {number} W The raster's width.
|
|
44
|
+
* @param {number} H Its height.
|
|
45
|
+
* @param {object} o How.
|
|
46
|
+
* @param {number} o.spacing The distance between threads, in pixels.
|
|
47
|
+
* @param {number} o.half Half the widest satin, in pixels (a thread reaches no farther from where it starts than about this).
|
|
48
|
+
* @param {number} o.smooth How far the direction is smoothed, in pixels.
|
|
49
|
+
* @returns {number[]} The stitches, flat: `x0, y0, x1, y1` each, in pixel coordinates.
|
|
50
|
+
*/
|
|
51
|
+
export function satinStitches(mask, dist, W, H, { spacing, half, smooth }) {
|
|
52
|
+
const field = acrossField(dist, mask, W, H, Math.max(1, Math.round(smooth)));
|
|
53
|
+
const out = [];
|
|
54
|
+
const covered = new Uint8Array(W * H);
|
|
55
|
+
const lay = (i) => {
|
|
56
|
+
const x = i % W, y = (i - x) / W;
|
|
57
|
+
const [ax, ay] = [Math.cos(field[i]), Math.sin(field[i])];
|
|
58
|
+
const max = Math.min(half * 1.6, dist[i] * 1.8) + 1.5;
|
|
59
|
+
const tp = reach(dist, W, H, x, y, ax, ay, max);
|
|
60
|
+
const tm = reach(dist, W, H, x, y, -ax, -ay, max);
|
|
61
|
+
if (tp + tm < 0.8) return;
|
|
62
|
+
const s = [x - ax * tm, y - ay * tm, x + ax * tp, y + ay * tp];
|
|
63
|
+
out.push(...s);
|
|
64
|
+
cover(covered, W, H, s, spacing * 1.1);
|
|
65
|
+
};
|
|
66
|
+
// the middle line: pixels at least as far from the edge as their neighbours across
|
|
67
|
+
const ridge = [];
|
|
68
|
+
for (let i = 0; i < W * H; i++) {
|
|
69
|
+
if (!mask[i] || dist[i] < 1) continue;
|
|
70
|
+
const x = i % W, y = (i - x) / W;
|
|
71
|
+
const [ax, ay] = [Math.cos(field[i]), Math.sin(field[i])];
|
|
72
|
+
if (dist[i] >= sample(dist, W, H, x + ax, y + ay) - 1e-3 && dist[i] >= sample(dist, W, H, x - ax, y - ay) - 1e-3) ridge.push(i);
|
|
73
|
+
}
|
|
74
|
+
for (const i of spaced(ridge, W, spacing)) lay(i);
|
|
75
|
+
// where the picks along the middle line met from two sides and left a gap, one more thread
|
|
76
|
+
const near = new Uint8Array(W * H);
|
|
77
|
+
for (let k = 0; k < out.length; k += 4) cover(near, W, H, out.slice(k, k + 4), spacing * 0.6);
|
|
78
|
+
for (const i of spaced(ridge.filter((j) => !near[j]), W, spacing * 0.8)) lay(i);
|
|
79
|
+
for (let p = 0; p < PASSES; p++) {
|
|
80
|
+
const bare = [];
|
|
81
|
+
for (let i = 0; i < W * H; i++) if (mask[i] && !covered[i]) bare.push(i);
|
|
82
|
+
if (!bare.length) break;
|
|
83
|
+
for (const i of spaced(bare, W, spacing, (j) => covered[j] === 1)) lay(i);
|
|
84
|
+
}
|
|
85
|
+
return out;
|
|
86
|
+
}
|