@logomarks/core 0.1.8 → 0.1.11
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 +33 -0
- package/README.md +4 -1
- package/package.json +22 -3
- package/src/batch/args.d.ts +18 -0
- package/src/batch/args.js +44 -0
- package/src/batch/index.d.ts +7 -0
- package/src/batch/index.js +10 -0
- package/src/batch/run.d.ts +53 -0
- package/src/batch/run.js +92 -0
- package/src/batch/slug.d.ts +2 -0
- package/src/batch/slug.js +12 -0
- package/src/batch/source.d.ts +12 -0
- package/src/batch/source.js +23 -0
- package/src/fit/index.d.ts +4 -0
- package/src/fit/index.js +29 -5
- package/src/geometry/index.d.ts +1 -0
- package/src/geometry/index.js +1 -0
- package/src/geometry/mitre.d.ts +8 -0
- package/src/geometry/mitre.js +64 -0
- package/src/geometry/modular.d.ts +1 -1
- package/src/index.d.ts +1 -0
- package/src/index.js +3 -2
- package/src/parts/index.d.ts +2 -0
- package/src/parts/index.js +2 -0
- package/src/parts/pixels.d.ts +11 -0
- package/src/parts/pixels.js +74 -0
- package/src/parts/run.js +4 -2
- package/src/parts/span.d.ts +13 -0
- package/src/parts/span.js +100 -0
- package/src/sdf/fields.js +135 -0
- package/src/sdf/gap.js +42 -0
- package/src/sdf/index.d.ts +56 -0
- package/src/sdf/index.js +12 -0
- package/src/sdf/ops.js +102 -0
- package/src/sdf/trace.js +90 -0
- package/src/skill/bundle.js +1 -1
- 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
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,39 @@ All notable changes to `@logomarks/core` are documented here.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.1.11] — 2026-11-15
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **Exact mitred offsets in `/geometry`:** `mitreOffset(pts, d)` (an open polyline's parallel, corners mitred), `mitreBand(pts, w)` (a solid band along it, ends square) and `offsetPolygon(pts, d)` (a closed polygon grown or shrunk, either winding). The Logomarks tools had them five ways; these are the same arithmetic as the ones there.
|
|
13
|
+
- **Span tiles in `/parts`:** `spanGrid`, `spanTile`, `evenGrid`, `evenCell` and `longSide`: a tile grid where one token may span a block of cells (`tok^2`, `tok>2`, `tok_2`, `tok@2x3`), from the tools' span parts.
|
|
14
|
+
- **Pixel editions in `/parts`:** `rasterRows`, `rasterMark` (a drawn mark sampled into rows of text, `#` a pixel, on spans or square pixels with a phase and hand fixes) and `insideShape` (even-odd point in shape), from the tools' dither parts.
|
|
15
|
+
- **Golden tests:** a hash of a drawing by every geometry and parts export the other tests don't reach (`tests/golden.json`; refresh with `GOLDEN=update npm test`).
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- `nest()`'s type: `cut` is a function `(w, h, side) => number`, as the code has always read it (the type said a number would do).
|
|
20
|
+
|
|
21
|
+
Proved on the Logomarks generators: the nineteen drawn with span tiles or pixel rasters, the ten drawn with the tools' mitred offsets and the twelve drawn with distance fields all redraw byte-identical with these in place of their local copies.
|
|
22
|
+
|
|
23
|
+
## [0.1.10] — 2026-11-15
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
27
|
+
- **`fitMark()` no longer moves a mark that's already fitted.** A finished mark's points sit on the 0.1 grid, so it measured a hair off 448 and was scaled by that hair: points tipped over a rounding edge and arc radii, rounded down, lost 0.1 on every refit. Of the 5,255 Logomarks marks, 24 changed on a refit and 16 never settled (Trio 52.9 → 52.8 → 52.7 …). A mark on the 512 canvas whose longest side and centre are within `REFIT_TOLERANCE` (0.25) keeps its scale and position, and an arc radius within `ARC_SLACK` (0.005) of the next 0.1 step is taken as it. Every live mark now refits to the same numbers (four older files only have their spacing normalised, once). Semicircles still round down.
|
|
28
|
+
- `runIndustry()`'s `fam:` also takes the family as passed, so `fam:Letters · Latch` builds a batch that passes whole families as its trades (it built none).
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
|
|
32
|
+
- **`/batch`: running a generator (Node).** `runBatch(batch, marks, options)` draws each mark seeded by its name (and puts `Math.random` back), with `exact()` so the gap is 22 once fitted, into `<workdir>/newraw<N>/`, and writes `_names.json`, `_families.json`, `_styles.json`, and `_variants.json` / `_pairs.json` when used; held marks are left out, and re-running one mark keeps every sidecar whole. Also `batchArgs` and `selected` (`"Name,Name"`, `fam:`, `pair:` and the generator's own modes), `sidecars`, `rawSvg`, `withSeed`, `slug` (the one slug: no hyphen at either end) and `sourceMark` (an edition's source: live first, then the queue; `LOGOMARKS_SVG`, `LOGOMARKS_QUEUE`). See [docs/api.md](docs/api.md#batches).
|
|
33
|
+
- **`/sdf`: signed distance fields**, from the Logomarks tools' Y2K parts: the fields (`sdCircle`, `sdCapsule`, `sdPath`, `sdBox`, `sdEllipse`, `sdRing`, and the new exact `sdPoly`), smooth and sharp combining (`smin`, `smax`, `ssub`, `fmin`, and the new `fmax` and `fsub`), the gap (`gapCut`, `fover`, `flink`) and `shapeOf`, which traces a field into a kit shape. The twelve generators drawn with them redraw byte-identical on it. See [docs/api.md](docs/api.md#distance-fields).
|
|
34
|
+
|
|
35
|
+
## [0.1.9] — 2026-11-08
|
|
36
|
+
|
|
37
|
+
### Added
|
|
38
|
+
|
|
39
|
+
- **`/stitch`: a mark planned as machine embroidery.** `stitchPlan(svg, { size, density, satin, stitch, angle, border, resolution, seed })` splits the ink by width (two distance transforms: the shape opened by a disc as wide as the widest satin) into fill and satin parts, then lays tatami rows over the fills (staggered row by row into a brick), satin threads across the strokes (from the stroke's middle line, along the direction across it from the distance field's structure tensor, topped up where they fan apart), and a satin border round each fill. Stitches come back in the mark's units with `mm`, the millimetres a unit; seeded, so repeatable. Also `stitchCount`, `STITCH`, `SATIN_DENSITY` and the steps (`splitByWidth`, `fillStitches`, `satinStitches`, `borderStitches`). It draws nothing: `@logomarks/mockups` 0.6.0's embroidery finish renders the stitches as thread. See [docs/api.md](docs/api.md#stitch-plans).
|
|
40
|
+
|
|
8
41
|
## [0.1.8] — 2026-11-07
|
|
9
42
|
|
|
10
43
|
### Fixed
|
package/README.md
CHANGED
|
@@ -25,12 +25,15 @@ checkFit(mark).problems; // []
|
|
|
25
25
|
|
|
26
26
|
- `/geometry`: shapes and real booleans, strokes, tracks, iso solids, badges and letter skeletons
|
|
27
27
|
- `/svg`: one SVG path parser, filled shapes, bounds and rings
|
|
28
|
-
- `/fit`: the 448 fit and its check
|
|
28
|
+
- `/fit`: the 448 fit and its check (refitting a fitted mark gives the same file)
|
|
29
29
|
- `/analyse`: specks, slivers and pin-holes, gap and stroke widths, shape tags
|
|
30
30
|
- `/similar`: outline and letter signatures, IoU and nearest marks
|
|
31
31
|
- `/raster`: scanline fill, components, distance transform
|
|
32
32
|
- `/edit`: the studio's shape edits (weight, soften, gap, detail) and pixelate
|
|
33
|
+
- `/sdf`: shapes as signed distance fields, smooth unions and exact gap cuts, traced to kit shapes
|
|
34
|
+
- `/stitch`: a mark planned as machine embroidery (fill, satin and border stitches), for a renderer to draw as thread
|
|
33
35
|
- `/parts`: the parts the industry symbols are built from (Node)
|
|
36
|
+
- `/batch`: a generator's runner: seeded marks, the sidecars, the command line, `slug` (Node)
|
|
34
37
|
- `logomarks-skill <dir>`: writes the design skill's `kit.mjs` and `fit.mjs`
|
|
35
38
|
|
|
36
39
|
Every export, repeatable drawings and the skill scripts: [API](https://github.com/Logomarks/core/blob/main/docs/api.md).
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@logomarks/core",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Geometry, SVG reading, fitting, analysis and similarity for logomarks on a 512 grid: the kit the Logomarks set is drawn with (shapes, real booleans, strokes, tracks, iso solids, badges, letter skeletons), one SVG path parser, the 448 fit, speck and shape-tag analysis, outline signatures,
|
|
3
|
+
"version": "0.1.11",
|
|
4
|
+
"description": "Geometry, SVG reading, fitting, analysis and similarity for logomarks on a 512 grid: the kit the Logomarks set is drawn with (shapes, real booleans, strokes, tracks, iso solids, badges, letter skeletons), one SVG path parser, the 448 fit, speck and shape-tag analysis, outline signatures, signed distance fields traced to shapes, stitch plans for embroidery, the industry-symbol parts and the batch runner generators use.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
7
7
|
"types": "src/index.d.ts",
|
|
@@ -22,6 +22,11 @@
|
|
|
22
22
|
"import": "./src/parts/index.js",
|
|
23
23
|
"default": "./src/parts/index.js"
|
|
24
24
|
},
|
|
25
|
+
"./batch": {
|
|
26
|
+
"types": "./src/batch/index.d.ts",
|
|
27
|
+
"import": "./src/batch/index.js",
|
|
28
|
+
"default": "./src/batch/index.js"
|
|
29
|
+
},
|
|
25
30
|
"./svg": {
|
|
26
31
|
"types": "./src/svg/index.d.ts",
|
|
27
32
|
"import": "./src/svg/index.js",
|
|
@@ -47,6 +52,11 @@
|
|
|
47
52
|
"import": "./src/similar/index.js",
|
|
48
53
|
"default": "./src/similar/index.js"
|
|
49
54
|
},
|
|
55
|
+
"./sdf": {
|
|
56
|
+
"types": "./src/sdf/index.d.ts",
|
|
57
|
+
"import": "./src/sdf/index.js",
|
|
58
|
+
"default": "./src/sdf/index.js"
|
|
59
|
+
},
|
|
50
60
|
"./edit": {
|
|
51
61
|
"types": "./src/edit/index.d.ts",
|
|
52
62
|
"import": "./src/edit/index.js",
|
|
@@ -57,6 +67,11 @@
|
|
|
57
67
|
"import": "./src/edit/reshape.js",
|
|
58
68
|
"default": "./src/edit/reshape.js"
|
|
59
69
|
},
|
|
70
|
+
"./stitch": {
|
|
71
|
+
"types": "./src/stitch/index.d.ts",
|
|
72
|
+
"import": "./src/stitch/index.js",
|
|
73
|
+
"default": "./src/stitch/index.js"
|
|
74
|
+
},
|
|
60
75
|
"./package.json": "./package.json"
|
|
61
76
|
},
|
|
62
77
|
"bin": {
|
|
@@ -84,7 +99,11 @@
|
|
|
84
99
|
"isometric",
|
|
85
100
|
"path",
|
|
86
101
|
"fit",
|
|
87
|
-
"similarity"
|
|
102
|
+
"similarity",
|
|
103
|
+
"embroidery",
|
|
104
|
+
"satin-stitch",
|
|
105
|
+
"signed-distance-field",
|
|
106
|
+
"sdf"
|
|
88
107
|
],
|
|
89
108
|
"author": "David Sherlock",
|
|
90
109
|
"license": "MIT",
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** What a generator was asked to build. */
|
|
2
|
+
export interface BatchArgs {
|
|
3
|
+
/** Where the batch's `newraw<N>/` folder goes. */
|
|
4
|
+
workdir: string;
|
|
5
|
+
/** Only these marks, by name; null for no name list. */
|
|
6
|
+
names: string[] | null;
|
|
7
|
+
/** Only this family's marks; null for any. */
|
|
8
|
+
family: string | null;
|
|
9
|
+
/** Only the monograms of these letter pairs; null for any. */
|
|
10
|
+
pairs: string[] | null;
|
|
11
|
+
/** A mode the generator handles itself (one of its `modes`); null for none. */
|
|
12
|
+
mode: string | null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** The work folder and the selection from `node genbatchN.mjs <workdir> ["Name,Name" | "fam:…" | "pair:DS,JS" | mode]`. */
|
|
16
|
+
export function batchArgs(argv?: readonly string[], options?: { modes?: readonly string[] }): BatchArgs;
|
|
17
|
+
/** Whether a mark is in the selection (in no mark when a mode was asked for). */
|
|
18
|
+
export function selected(args: BatchArgs, name: string, mark: { family?: string; pair?: string }): boolean;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A generator's command line: `node genbatchN.mjs <workdir> [what]`.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* What a generator was asked to build. `what` is one of:
|
|
7
|
+
* - nothing: every mark;
|
|
8
|
+
* - `"Name,Name"`: those marks;
|
|
9
|
+
* - `"fam:<family>"`: one family's marks;
|
|
10
|
+
* - `"pair:DS,JS"`: the monograms of those letter pairs;
|
|
11
|
+
* - one of `modes` (e.g. `"matrix"`): a mode the generator handles itself.
|
|
12
|
+
*
|
|
13
|
+
* @param {readonly string[]} [argv] The arguments after the script (default `process.argv.slice(2)`).
|
|
14
|
+
* @param {{ modes?: readonly string[] }} [options] The generator's own modes.
|
|
15
|
+
* @returns {import('./args.js').BatchArgs} The work folder and the selection.
|
|
16
|
+
* @throws {Error} When there's no work folder.
|
|
17
|
+
*/
|
|
18
|
+
export function batchArgs(argv = process.argv.slice(2), { modes = [] } = {}) {
|
|
19
|
+
const [workdir, what = ''] = argv;
|
|
20
|
+
if (!workdir) throw new Error('usage: node genbatchN.mjs <workdir> ["Name,Name" | "fam:<family>" | "pair:DS,JS"]');
|
|
21
|
+
const list = (/** @type {string} */ s) => s.split(',').map((x) => x.trim()).filter(Boolean);
|
|
22
|
+
return {
|
|
23
|
+
workdir,
|
|
24
|
+
names: what && !/^(fam|pair):/.test(what) && !modes.includes(what) ? list(what) : null,
|
|
25
|
+
family: what.startsWith('fam:') ? what.slice(4).trim() : null,
|
|
26
|
+
pairs: what.startsWith('pair:') ? list(what.slice(5)) : null,
|
|
27
|
+
mode: modes.includes(what) ? what : null,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Whether a mark is in the selection.
|
|
33
|
+
*
|
|
34
|
+
* @param {import('./args.js').BatchArgs} args The selection.
|
|
35
|
+
* @param {string} name The mark's name.
|
|
36
|
+
* @param {{ family?: string, pair?: string }} mark Its family and letter pair.
|
|
37
|
+
* @returns {boolean} True when it should be built.
|
|
38
|
+
*/
|
|
39
|
+
export function selected(args, name, mark) {
|
|
40
|
+
if (args.names) return args.names.includes(name);
|
|
41
|
+
if (args.family) return mark.family === args.family;
|
|
42
|
+
if (args.pairs) return !!mark.pair && args.pairs.includes(mark.pair);
|
|
43
|
+
return !args.mode;
|
|
44
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { slug } from './slug.js';
|
|
2
|
+
export { batchArgs, selected } from './args.js';
|
|
3
|
+
export type { BatchArgs } from './args.js';
|
|
4
|
+
export { sourceMark } from './source.js';
|
|
5
|
+
export type { SourceMark } from './source.js';
|
|
6
|
+
export { runBatch, sidecars, rawSvg, withSeed } from './run.js';
|
|
7
|
+
export type { BatchMark, BatchOptions, BatchResult } from './run.js';
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module @logomarks/core/batch
|
|
3
|
+
*
|
|
4
|
+
* Running a generator (Node only): the command line, a mark's slug, reading an existing mark for an edition, and the
|
|
5
|
+
* runner that draws a batch seeded per mark and writes the sidecars the export and the review page read.
|
|
6
|
+
*/
|
|
7
|
+
export { slug } from './slug.js';
|
|
8
|
+
export { batchArgs, selected } from './args.js';
|
|
9
|
+
export { sourceMark } from './source.js';
|
|
10
|
+
export { runBatch, sidecars, rawSvg, withSeed } from './run.js';
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { Shape } from '../geometry/types.js';
|
|
2
|
+
import type { BatchArgs } from './args.js';
|
|
3
|
+
|
|
4
|
+
/** One mark in a batch. */
|
|
5
|
+
export interface BatchMark {
|
|
6
|
+
/** Its family, e.g. "Letters · Latch" (`_families.json`; `fam:` selects by it). */
|
|
7
|
+
family: string;
|
|
8
|
+
/** Its style, e.g. "Bold" (`_styles.json`). */
|
|
9
|
+
style?: string;
|
|
10
|
+
/** The live mark it is an edition or variation of, by slug (`_variants.json`). */
|
|
11
|
+
variantOf?: string;
|
|
12
|
+
/** Its letter pair, for monograms (`_pairs.json`; `pair:` selects by it). */
|
|
13
|
+
pair?: string;
|
|
14
|
+
/** The drawing: takes the gap (or nothing with `gap: false`) and returns the mark. */
|
|
15
|
+
build: (g: number) => Shape;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** How a batch is drawn. */
|
|
19
|
+
export interface BatchOptions {
|
|
20
|
+
/** Draw each build with the gap and exact(), so the gap is 22 once fitted (default true). */
|
|
21
|
+
gap?: boolean;
|
|
22
|
+
/** Drop the pieces under `piece` and the holes under `hole` square units after drawing. */
|
|
23
|
+
despeck?: { piece?: number; hole?: number };
|
|
24
|
+
/** Names held back for a later drop: built by nobody, left out of the sidecars. */
|
|
25
|
+
held?: readonly string[];
|
|
26
|
+
/** The command line (default `process.argv.slice(2)`). */
|
|
27
|
+
argv?: readonly string[];
|
|
28
|
+
/** The generator's own modes (e.g. "matrix"): asking for one builds no mark here. */
|
|
29
|
+
modes?: readonly string[];
|
|
30
|
+
/** Where progress goes (default console.log). */
|
|
31
|
+
log?: (line: string) => void;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** What a batch run did. */
|
|
35
|
+
export interface BatchResult {
|
|
36
|
+
/** The `newraw<N>` folder. */
|
|
37
|
+
out: string;
|
|
38
|
+
/** The command line it read. */
|
|
39
|
+
args: BatchArgs;
|
|
40
|
+
/** The names it drew. */
|
|
41
|
+
built: string[];
|
|
42
|
+
/** The names that threw, and why. */
|
|
43
|
+
failed: { name: string; error: string }[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** A raw mark's file: one even-odd path on the 512 canvas. */
|
|
47
|
+
export function rawSvg(shape: Shape): string;
|
|
48
|
+
/** Runs a drawing with Math.random seeded by the name, then puts Math.random back. */
|
|
49
|
+
export function withSeed<T>(name: string, draw: () => T): T;
|
|
50
|
+
/** The batch's sidecar files: file name → contents (`_names`, `_families`, `_styles`, and `_variants` / `_pairs` when used). */
|
|
51
|
+
export function sidecars(marks: Readonly<Record<string, BatchMark>>): Record<string, unknown>;
|
|
52
|
+
/** Draws a batch into `<workdir>/newraw<batch>/`, seeded per mark, and writes its sidecars. */
|
|
53
|
+
export function runBatch(batch: number, marks: Readonly<Record<string, BatchMark>>, options?: BatchOptions): BatchResult;
|
package/src/batch/run.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The batch runner: draws a generator's marks into `<workdir>/newraw<N>/` with the sidecars the export and the review
|
|
3
|
+
* page read (Node only: it writes files).
|
|
4
|
+
*/
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { seeded, toPath } from '../geometry/index.js';
|
|
8
|
+
import { exact, despeckShape } from '../parts/frame.js';
|
|
9
|
+
import { batchArgs, selected } from './args.js';
|
|
10
|
+
import { slug } from './slug.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A finished raw mark: one even-odd path on the 512 canvas (normalize fits it).
|
|
14
|
+
*
|
|
15
|
+
* @param {import('../geometry/types.js').Shape} shape The mark.
|
|
16
|
+
* @returns {string} The SVG file's text.
|
|
17
|
+
*/
|
|
18
|
+
export const rawSvg = (shape) => `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="currentColor"><path fill-rule="evenodd" d="${toPath(shape)}"/></svg>\n`;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Runs a drawing with Math.random seeded by the mark's name, putting Math.random back afterwards, so the mark is the
|
|
22
|
+
* same on every run whatever else ran first.
|
|
23
|
+
*
|
|
24
|
+
* @template T
|
|
25
|
+
* @param {string} name The mark's name (the seed).
|
|
26
|
+
* @param {() => T} draw The drawing.
|
|
27
|
+
* @returns {T} What it drew.
|
|
28
|
+
*/
|
|
29
|
+
export function withSeed(name, draw) {
|
|
30
|
+
const random = Math.random;
|
|
31
|
+
Math.random = seeded(name);
|
|
32
|
+
try { return draw(); } finally { Math.random = random; }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The sidecars for a batch's marks: `_names.json` (how normalize finds the batch), `_families.json` and `_styles.json`,
|
|
37
|
+
* and `_variants.json` and `_pairs.json` when any mark has a `variantOf` or a `pair`. Each maps slug → value.
|
|
38
|
+
*
|
|
39
|
+
* @param {Readonly<Record<string, import('./run.js').BatchMark>>} marks The batch's marks, by name.
|
|
40
|
+
* @returns {Record<string, unknown>} File name → contents.
|
|
41
|
+
*/
|
|
42
|
+
export function sidecars(marks) {
|
|
43
|
+
const entries = Object.entries(marks);
|
|
44
|
+
const map = (/** @type {keyof import('./run.js').BatchMark} */ key) =>
|
|
45
|
+
Object.fromEntries(entries.filter(([, m]) => m[key] !== undefined).map(([name, m]) => [slug(name), m[key]]));
|
|
46
|
+
/** @type {Record<string, unknown>} */
|
|
47
|
+
const files = { '_names.json': Object.keys(marks), '_families.json': map('family'), '_styles.json': map('style') };
|
|
48
|
+
for (const [key, file] of /** @type {const} */ ([['variantOf', '_variants.json'], ['pair', '_pairs.json']])) {
|
|
49
|
+
const m = map(key);
|
|
50
|
+
if (Object.keys(m).length) files[file] = m;
|
|
51
|
+
}
|
|
52
|
+
return files;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Draws a batch: every mark (or the ones the command line names: `"Name,Name"`, `"fam:<family>"`, `"pair:DS,JS"`)
|
|
57
|
+
* into `<workdir>/newraw<batch>/<slug>.svg`, seeded per mark so a re-run is byte-identical, then writes the sidecars
|
|
58
|
+
* for every mark that isn't held (so re-running one mark never trims them). A mark that throws is reported and the
|
|
59
|
+
* rest still drawn.
|
|
60
|
+
*
|
|
61
|
+
* With `gap: true` (the default) each build takes the gap and is drawn with exact(), so its gap is 22 once fitted;
|
|
62
|
+
* with `gap: false` it takes nothing. `despeck` drops the pieces and holes a boolean left under those areas.
|
|
63
|
+
*
|
|
64
|
+
* @param {number} batch The batch number.
|
|
65
|
+
* @param {Readonly<Record<string, import('./run.js').BatchMark>>} marks The marks, by name.
|
|
66
|
+
* @param {import('./run.js').BatchOptions} [options] The drawing and the command line.
|
|
67
|
+
* @returns {import('./run.js').BatchResult} What was built and what failed.
|
|
68
|
+
*/
|
|
69
|
+
export function runBatch(batch, marks, { gap = true, despeck, held = [], argv, modes, log = console.log } = {}) {
|
|
70
|
+
const args = batchArgs(argv, { modes });
|
|
71
|
+
const out = path.join(args.workdir, `newraw${batch}`);
|
|
72
|
+
fs.mkdirSync(out, { recursive: true });
|
|
73
|
+
const live = Object.fromEntries(Object.entries(marks).filter(([name]) => !held.includes(name)));
|
|
74
|
+
/** @type {string[]} */ const built = [];
|
|
75
|
+
/** @type {{ name: string, error: string }[]} */ const failed = [];
|
|
76
|
+
for (const [name, mark] of Object.entries(live)) {
|
|
77
|
+
if (!selected(args, name, mark)) continue;
|
|
78
|
+
try {
|
|
79
|
+
// exact() redraws up to three times to land the gap: each attempt starts from the same seed
|
|
80
|
+
let shape = withSeed(name, () => (gap ? exact(mark.build, () => { Math.random = seeded(name); }) : mark.build(0)));
|
|
81
|
+
if (despeck) shape = despeckShape(shape, despeck.piece, despeck.hole);
|
|
82
|
+
fs.writeFileSync(path.join(out, `${slug(name)}.svg`), rawSvg(shape));
|
|
83
|
+
built.push(name);
|
|
84
|
+
} catch (e) {
|
|
85
|
+
failed.push({ name, error: e instanceof Error ? e.message : String(e) });
|
|
86
|
+
log(`FAIL ${name}: ${failed[failed.length - 1].error.slice(0, 160)}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
for (const [file, data] of Object.entries(sidecars(live))) fs.writeFileSync(path.join(out, file), JSON.stringify(data, null, 1));
|
|
90
|
+
log(`built ${built.length} of ${Object.keys(live).length}${held.length ? ` (${held.length} held)` : ''} into ${out}`);
|
|
91
|
+
return { out, args, built, failed };
|
|
92
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A mark's slug: the name its file, its page and every sidecar are keyed by.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A mark's slug from its name: lower case, each run of anything but a–z and 0–9 one hyphen, no hyphen at either end
|
|
7
|
+
* ("Ship's Wheel" → "ship-s-wheel", "Tree (r)" → "tree-r"). The site, normalize and the review tools all use this one.
|
|
8
|
+
*
|
|
9
|
+
* @param {string} name The mark's name.
|
|
10
|
+
* @returns {string} Its slug.
|
|
11
|
+
*/
|
|
12
|
+
export const slug = (name) => name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** An existing mark read for an edition or a variation. */
|
|
2
|
+
export interface SourceMark {
|
|
3
|
+
/** The file's text. */
|
|
4
|
+
svg: string;
|
|
5
|
+
/** Where it was read from. */
|
|
6
|
+
file: string;
|
|
7
|
+
/** The live set, or the review queue. */
|
|
8
|
+
from: 'live' | 'queue';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** An existing mark: the live file first, then the queue's `<prefix>NNN-<slug>.svg` (`LOGOMARKS_SVG` / `LOGOMARKS_QUEUE` replace the folders); null when it's in neither. */
|
|
12
|
+
export function sourceMark(slug: string, folders: { live: string; queue?: string }): SourceMark | null;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading a mark that already exists, for editions and variations drawn from it (Node only).
|
|
3
|
+
*/
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* An existing mark's SVG: the live file first, then the review queue, where a mark approved for the next drop waits
|
|
9
|
+
* as `<prefix>NNN-<slug>.svg`. `LOGOMARKS_SVG` and `LOGOMARKS_QUEUE` replace the two folders, so a proof can read a
|
|
10
|
+
* snapshot while an export rewrites them.
|
|
11
|
+
*
|
|
12
|
+
* @param {string} slug The mark's slug.
|
|
13
|
+
* @param {{ live: string, queue?: string }} folders The live svg folder and the queue folder.
|
|
14
|
+
* @returns {import('./source.js').SourceMark | null} The file, or null when it's in neither.
|
|
15
|
+
*/
|
|
16
|
+
export function sourceMark(slug, { live, queue }) {
|
|
17
|
+
const liveDir = process.env.LOGOMARKS_SVG || live, queueDir = process.env.LOGOMARKS_QUEUE || queue;
|
|
18
|
+
const file = path.join(liveDir, `${slug}.svg`);
|
|
19
|
+
if (fs.existsSync(file)) return { svg: fs.readFileSync(file, 'utf8'), file, from: 'live' };
|
|
20
|
+
if (!queueDir || !fs.existsSync(queueDir)) return null;
|
|
21
|
+
const queued = fs.readdirSync(queueDir).find((f) => f.replace(/^[a-z]+\d+-/, '') === `${slug}.svg`);
|
|
22
|
+
return queued ? { svg: fs.readFileSync(path.join(queueDir, queued), 'utf8'), file: path.join(queueDir, queued), from: 'queue' } : null;
|
|
23
|
+
}
|
package/src/fit/index.d.ts
CHANGED
|
@@ -21,6 +21,10 @@ export const CANVAS: 512;
|
|
|
21
21
|
export const FIT: 448;
|
|
22
22
|
/** How far size, centre and edges may drift: 0.5. */
|
|
23
23
|
export const FIT_TOLERANCE: 0.5;
|
|
24
|
+
/** How far an already-fitted mark may sit from 448 and the centre and keep its scale on a refit: 0.25. */
|
|
25
|
+
export const REFIT_TOLERANCE: 0.25;
|
|
26
|
+
/** How close an arc radius may sit below the next 0.1 step and still be taken as it: 0.005. */
|
|
27
|
+
export const ARC_SLACK: 0.005;
|
|
24
28
|
/** Markup a finished mark must not contain. */
|
|
25
29
|
export const FORBIDDEN_MARKUP: RegExp;
|
|
26
30
|
/** A mark fitted to the standard; throws when the file has no filled shapes. */
|
package/src/fit/index.js
CHANGED
|
@@ -16,6 +16,10 @@ export const CANVAS = 512;
|
|
|
16
16
|
export const FIT = 448;
|
|
17
17
|
/** How far size, centre and canvas edges may drift before a check fails. */
|
|
18
18
|
export const FIT_TOLERANCE = 0.5;
|
|
19
|
+
/** How far a mark already on the 512 canvas may sit from 448 and the centre and still keep its scale and position on a refit. */
|
|
20
|
+
export const REFIT_TOLERANCE = 0.25;
|
|
21
|
+
/** How close (in units) an arc radius may sit below the next 0.1 step and still be taken as it. */
|
|
22
|
+
export const ARC_SLACK = 0.005;
|
|
19
23
|
/** Markup a finished mark must not contain: flatten to filled paths. */
|
|
20
24
|
export const FORBIDDEN_MARKUP = /<(g|style|mask|clipPath|use|text|image)\b|transform=|stroke=/;
|
|
21
25
|
|
|
@@ -31,7 +35,9 @@ const snap = (v) => Math.round(v * 10) / 10;
|
|
|
31
35
|
* Segments as compact relative path data. Points are snapped to the 0.1 grid
|
|
32
36
|
* first and the relative steps taken between snapped points, so rounding never
|
|
33
37
|
* drifts. Arc radii are rounded down: a semicircle stays exact (a radius a
|
|
34
|
-
* hair too big would bulge the arc).
|
|
38
|
+
* hair too big would bulge the arc). A radius within ARC_SLACK of the next
|
|
39
|
+
* step up is taken as that step, so refitting a fitted mark (its scale a hair
|
|
40
|
+
* under 1) keeps its radii instead of shaving 0.1 off each time.
|
|
35
41
|
*
|
|
36
42
|
* @param {readonly import('./path.js').Segment[]} segs The segments.
|
|
37
43
|
* @param {(x: number, y: number) => number[]} f Where each point goes.
|
|
@@ -60,7 +66,7 @@ function serialise(segs, f, k) {
|
|
|
60
66
|
cx = sx = x; cy = sy = y;
|
|
61
67
|
} else if (s[0] === 'A') {
|
|
62
68
|
const [x, y] = f(s[6], s[7]).map(snap);
|
|
63
|
-
const r = (/** @type {number} */ v) => r1(Math.floor(v * k * 10) / 10);
|
|
69
|
+
const r = (/** @type {number} */ v) => r1(Math.floor(v * k * 10 + ARC_SLACK * 10) / 10);
|
|
64
70
|
d += cmd('a', `${r(s[1])} ${r(s[2])} ${r1(s[3])} ${s[4] ? 1 : 0} ${s[5] ? 1 : 0} ${rel(x, y)}`);
|
|
65
71
|
cx = x; cy = y;
|
|
66
72
|
} else {
|
|
@@ -74,7 +80,24 @@ function serialise(segs, f, k) {
|
|
|
74
80
|
}
|
|
75
81
|
|
|
76
82
|
/**
|
|
77
|
-
*
|
|
83
|
+
* Whether a mark is already fitted: on the 512 canvas, its longest side and centre within REFIT_TOLERANCE. Its
|
|
84
|
+
* points sit on the 0.1 grid, so it measures a hair off 448; scaling it by that hair would tip points and arc radii
|
|
85
|
+
* over a rounding edge, so a refit keeps it as it is.
|
|
86
|
+
*
|
|
87
|
+
* @param {string} svg The file's text.
|
|
88
|
+
* @param {{ x: number, y: number, w: number, h: number }} b Its outline's box.
|
|
89
|
+
* @returns {boolean} True when it is.
|
|
90
|
+
*/
|
|
91
|
+
function isFitted(svg, b) {
|
|
92
|
+
const vb = viewBox(svg);
|
|
93
|
+
if (!vb || vb.join(' ') !== `0 0 ${CANVAS} ${CANVAS}`) return false;
|
|
94
|
+
const near = (/** @type {number} */ a, /** @type {number} */ e) => Math.abs(a - e) <= REFIT_TOLERANCE;
|
|
95
|
+
return near(Math.max(b.w, b.h), FIT) && near(b.x + b.w / 2, CANVAS / 2) && near(b.y + b.h / 2, CANVAS / 2);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* A mark fitted to the standard. A mark already fitted keeps its scale and position (see isFitted), so refitting a
|
|
100
|
+
* finished file gives the same file.
|
|
78
101
|
*
|
|
79
102
|
* @param {string} svg The file's text.
|
|
80
103
|
* @returns {import('./index.js').Fitted} The fitted file and the box it had.
|
|
@@ -84,8 +107,9 @@ export function fitMark(svg) {
|
|
|
84
107
|
const all = shapes(svg);
|
|
85
108
|
if (!all.length) throw new Error('no filled shapes found');
|
|
86
109
|
const before = bounds(all);
|
|
87
|
-
const
|
|
88
|
-
const
|
|
110
|
+
const fitted = isFitted(svg, before);
|
|
111
|
+
const k = fitted ? 1 : FIT / Math.max(before.w, before.h);
|
|
112
|
+
const tx = fitted ? 0 : CANVAS / 2 - (before.x + before.w / 2) * k, ty = fitted ? 0 : CANVAS / 2 - (before.y + before.h / 2) * k;
|
|
89
113
|
const f = (/** @type {number} */ x, /** @type {number} */ y) => [x * k + tx, y * k + ty];
|
|
90
114
|
const paths = all.map((s) => `<path${s.evenodd ? ' fill-rule="evenodd"' : ''} d="${serialise(s.segs, f, k)}"/>`).join('');
|
|
91
115
|
return { svg: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${CANVAS} ${CANVAS}" fill="currentColor">${paths}</svg>\n`, before };
|
package/src/geometry/index.d.ts
CHANGED
|
@@ -16,6 +16,7 @@ export { signedArea, area, perimeter, ringBox, bbox, winding, dedupe, openRing,
|
|
|
16
16
|
export { seeded } from './random.js';
|
|
17
17
|
export { circle, ellipse, rect, regular, annulus, sector, arcBand, rotN, bar, ring, roundPoly, rr, lens, drop, squircle, pill, egg } from './shapes.js';
|
|
18
18
|
export { stroke, weave, offsetPts, fillet, tracks, filletClosed, trackLoop, walk, balloon, linePts, arcPts, joinPts } from './stroke.js';
|
|
19
|
+
export { mitreOffset, mitreBand, offsetPolygon } from './mitre.js';
|
|
19
20
|
export { toPath, svg, write } from './path.js';
|
|
20
21
|
export { grow, soften, grown, over, slices } from './offset.js';
|
|
21
22
|
export { iso, cubes, isoPrism, isoBlock, isoRing, isoPile } from './iso.js';
|
package/src/geometry/index.js
CHANGED
|
@@ -9,6 +9,7 @@ export { signedArea, area, perimeter, ringBox, bbox, winding, dedupe, openRing,
|
|
|
9
9
|
export { seeded } from './random.js';
|
|
10
10
|
export { circle, ellipse, rect, regular, annulus, sector, arcBand, rotN, bar, ring, roundPoly, rr, lens, drop, squircle, pill, egg } from './shapes.js';
|
|
11
11
|
export { stroke, weave, offsetPts, fillet, tracks, filletClosed, trackLoop, walk, balloon, linePts, arcPts, joinPts } from './stroke.js';
|
|
12
|
+
export { mitreOffset, mitreBand, offsetPolygon } from './mitre.js';
|
|
12
13
|
export { toPath, svg, write } from './path.js';
|
|
13
14
|
export { grow, soften, grown, over, slices } from './offset.js';
|
|
14
15
|
export { iso, cubes, isoPrism, isoBlock, isoRing, isoPile } from './iso.js';
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Point, Shape } from './types.js';
|
|
2
|
+
|
|
3
|
+
/** An open polyline offset d to its left (y down), corners mitred so it runs exactly parallel; d < 0 goes right. */
|
|
4
|
+
export function mitreOffset(pts: readonly (readonly number[])[], d: number): Point[];
|
|
5
|
+
/** A solid band w wide along an open polyline, corners mitred, ends square. */
|
|
6
|
+
export function mitreBand(pts: readonly (readonly number[])[], w: number): Shape;
|
|
7
|
+
/** A closed polygon grown (d > 0) or shrunk (d < 0), corners mitred; either winding. */
|
|
8
|
+
export function offsetPolygon(pts: readonly (readonly number[])[], d: number): Point[];
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Exact offsets with mitred corners: a polyline's parallel, a band along it, and a polygon grown or shrunk.
|
|
3
|
+
*/
|
|
4
|
+
import { poly } from './core.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The unit left normal (y down) of the step a → b.
|
|
8
|
+
*
|
|
9
|
+
* @param {readonly number[]} a From.
|
|
10
|
+
* @param {readonly number[]} b To.
|
|
11
|
+
* @returns {number[]} The normal.
|
|
12
|
+
*/
|
|
13
|
+
const leftNormal = (a, b) => {
|
|
14
|
+
const v = [b[0] - a[0], b[1] - a[1]], l = Math.hypot(...v) || 1;
|
|
15
|
+
return [v[1] / l, -(v[0] / l)];
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* An open polyline offset to its left (y down) by d, every corner mitred, so the offset runs exactly parallel to it
|
|
20
|
+
* (d < 0 offsets to the right). The ends move square to their segments.
|
|
21
|
+
*
|
|
22
|
+
* @param {readonly (readonly number[])[]} pts The polyline.
|
|
23
|
+
* @param {number} d How far.
|
|
24
|
+
* @returns {number[][]} The offset polyline, a point for each point.
|
|
25
|
+
*/
|
|
26
|
+
export const mitreOffset = (pts, d) => pts.map((p, i) => {
|
|
27
|
+
const n1 = i > 0 ? leftNormal(pts[i - 1], p) : null, n2 = i < pts.length - 1 ? leftNormal(p, pts[i + 1]) : null;
|
|
28
|
+
if (!n1) return [p[0] + /** @type {number[]} */ (n2)[0] * d, p[1] + /** @type {number[]} */ (n2)[1] * d];
|
|
29
|
+
if (!n2) return [p[0] + n1[0] * d, p[1] + n1[1] * d];
|
|
30
|
+
const s = [n1[0] + n2[0], n1[1] + n2[1]], l = Math.hypot(...s) || 1, m = [s[0] / l, s[1] / l], k = d / (m[0] * n1[0] + m[1] * n1[1]);
|
|
31
|
+
return [p[0] + m[0] * k, p[1] + m[1] * k];
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* A solid band w wide along an open polyline, every corner mitred sharp and both ends cut square.
|
|
36
|
+
*
|
|
37
|
+
* @param {readonly (readonly number[])[]} pts The polyline (its centre line).
|
|
38
|
+
* @param {number} w The width.
|
|
39
|
+
* @returns {import('./types.js').Shape} The band.
|
|
40
|
+
*/
|
|
41
|
+
export const mitreBand = (pts, w) => poly([...mitreOffset(pts, w / 2), ...mitreOffset(pts, -w / 2).reverse()]);
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* A closed polygon grown (d > 0) or shrunk (d < 0) by d, every corner mitred: each edge moves out along its normal and
|
|
45
|
+
* neighbouring edges meet where their lines cross. Either winding. Shrinking past a corner's reach turns it inside out,
|
|
46
|
+
* so keep |d| under the shortest edge's reach.
|
|
47
|
+
*
|
|
48
|
+
* @param {readonly (readonly number[])[]} pts The polygon's corners, in order (open: no repeated first point).
|
|
49
|
+
* @param {number} d How far.
|
|
50
|
+
* @returns {number[][]} The offset polygon, a corner for each corner.
|
|
51
|
+
*/
|
|
52
|
+
export const offsetPolygon = (pts, d) => {
|
|
53
|
+
const n = pts.length;
|
|
54
|
+
let a = 0;
|
|
55
|
+
for (let i = 0; i < n; i++) { const p = pts[i], q = pts[(i + 1) % n]; a += p[0] * q[1] - q[0] * p[1]; }
|
|
56
|
+
const sg = a > 0 ? 1 : -1, L = pts.map((p, i) => {
|
|
57
|
+
const q = pts[(i + 1) % n], dx = q[0] - p[0], dy = q[1] - p[1], l = Math.hypot(dx, dy), nx = sg * dy / l, ny = -sg * dx / l;
|
|
58
|
+
return [p[0] + nx * d, p[1] + ny * d, dx, dy];
|
|
59
|
+
});
|
|
60
|
+
return L.map((b, i) => {
|
|
61
|
+
const a2 = L[(i + n - 1) % n], den = a2[2] * b[3] - a2[3] * b[2], t = ((b[0] - a2[0]) * b[3] - (b[1] - a2[1]) * b[2]) / den;
|
|
62
|
+
return [a2[0] + a2[2] * t, a2[1] + a2[3] * t];
|
|
63
|
+
});
|
|
64
|
+
};
|
|
@@ -9,6 +9,6 @@ export function isoPieces(pieces: [Point3, Point3][], u?: number, g?: number, ox
|
|
|
9
9
|
/** Blocks [x, y, w, h] on a W × H grid, scaled to 448 and parted by g. */
|
|
10
10
|
export function blocks(list: [number, number, number, number][], W: number, H: number, g?: number): Shape;
|
|
11
11
|
/** A nested subdivision for blocks(): n − 1 blocks peeled off the sides named in turn (l t r b), each `cut` of what's left. */
|
|
12
|
-
export function nest(W: number, H: number, n: number, sides: string, cut: number
|
|
12
|
+
export function nest(W: number, H: number, n: number, sides: string, cut: (w: number, h: number, side: string) => number): [number, number, number, number][];
|
|
13
13
|
/** Capsules w wide pinned together, back to front, each cut free by g; pin holes at `pins`. */
|
|
14
14
|
export function linkage(bars: [Point, Point][], pins?: Point[], o?: { w?: number; g?: number; pin?: number }): Shape;
|
package/src/index.d.ts
CHANGED
package/src/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @logomarks/core: geometry, SVG reading, fitting, analysis, similarity
|
|
3
|
-
* grid. Each area is also its own subpath (`/geometry`, `/svg`, `/fit`, `/raster`, `/analyse`, `/similar`, `/edit`,
|
|
2
|
+
* @logomarks/core: geometry, SVG reading, fitting, analysis, similarity, shape edits and stitch plans for logomarks
|
|
3
|
+
* on a 512 grid. Each area is also its own subpath (`/geometry`, `/svg`, `/fit`, `/raster`, `/analyse`, `/similar`, `/edit`, `/stitch`,
|
|
4
4
|
* and `/parts` for the industry-symbol parts, which needs Node for its batch writer).
|
|
5
5
|
*/
|
|
6
6
|
export * from './geometry/index.js';
|
|
@@ -10,3 +10,4 @@ export * from './raster/index.js';
|
|
|
10
10
|
export * from './analyse/index.js';
|
|
11
11
|
export * from './similar/index.js';
|
|
12
12
|
export * from './edit/index.js';
|
|
13
|
+
export * from './stitch/index.js';
|
package/src/parts/index.d.ts
CHANGED