@logomarks/core 0.2.0 → 0.2.2
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 +25 -0
- package/README.md +9 -4
- package/bin/fit.js +76 -0
- package/package.json +2 -2
- package/src/analyse/index.d.ts +1 -0
- package/src/analyse/index.js +3 -2
- package/src/analyse/optical.d.ts +20 -0
- package/src/analyse/optical.js +76 -0
- package/src/edit/cells.d.ts +13 -0
- package/src/edit/cells.js +26 -0
- package/src/edit/index.d.ts +3 -1
- package/src/edit/index.js +4 -2
- package/src/edit/pixelate.d.ts +2 -0
- package/src/edit/pixelate.js +15 -0
- package/src/fit/index.js +1 -1
- package/src/geometry/glitch.js +1 -1
- package/src/geometry/path.js +1 -1
- package/src/parts/frame.js +1 -1
- package/bin/skill.js +0 -14
- package/src/skill/bundle.d.ts +0 -6
- package/src/skill/bundle.js +0 -181
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,31 @@ 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.2.2] — 2026-11-15
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **`logomarks-fit`, the fit as a command:** `fitMark()` and `checkFit()` on files. It fits a file in place, several files, or every `.svg` in a folder; `-o` (or a second path that doesn't exist yet) writes one file's fit to another; `--check` only reports and exits 1 when anything is off; a bad call exits 2 with the usage. It is the design skill's `fit.mjs` command line, on the library instead of a pasted copy: on the 5,300 Logomarks marks and the skill's six examples it writes byte-identical files and prints the same lines. See [docs/api.md](docs/api.md#the-fit-command).
|
|
13
|
+
|
|
14
|
+
### Removed
|
|
15
|
+
|
|
16
|
+
- **The `logomarks-skill` bin and `src/skill/`** (`bundleKit`, `bundleFit`, `writeSkillScripts`). They pasted `/geometry` into one `kit.mjs` and the fit into one `fit.mjs` for the design skill, which left the skill a second copy of the kit and none of the parts (`/parts`, `/sdf`, `/iso`, `/letters`, `/forms`, `/objects`, `/pixels`: their names clash, so they can't be pasted into one file). The skill now installs `@logomarks/core` and imports its subpaths, and fits with `npx logomarks-fit`. A patch: nothing outside the skill used the bin, and every export is unchanged, so the libraries on `^0.2` keep one copy of core.
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- `checkFit()`'s "runs outside the 512 canvas" problem says "run logomarks-fit on it" (it named `fit.mjs`).
|
|
21
|
+
|
|
22
|
+
## [0.2.1] — 2026-11-15
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
|
|
26
|
+
The Lookbook's mark measures, moved here from `@logomarks/lookbook/marks` (the code unchanged, documented and typed), so the cards and anything else that sizes a mark share one copy. Browser-safe, like the rest of `/analyse` and `/edit`.
|
|
27
|
+
|
|
28
|
+
- **The optical size in `/analyse`:** `markBox(paths)` (the real box round a mark's outlines, the 448 fit's box when there is nothing to read), `inkShare(paths)` (how much of that box is ink, by scanline coverage 16 cells across), `opticalScale(box, ink)` (how much bigger to draw a wide, slim or light mark: 1 for a square solid, 448 × 200 about 1.22, never above 1.3) and `OPTICAL`, its limits.
|
|
29
|
+
- **Cells by count in `/edit`:** `cellsAcross(n, box)` beside `pixelGrid` (a grid `n` cells across the box's longest side, any `n`, where `pixelGrid` takes an amount and stops at 14 across), and `markCells(paths, n)` (shapes → rings → coverage → filled cells and their path in one call).
|
|
30
|
+
|
|
31
|
+
Proved on `@logomarks/lookbook` 0.1.0: its own functions give the same values as these on the golden cases, and its equivalence tests pass unchanged on them. The golden tests gain the five cases (`optical.*`, `cells.*`), on six live marks.
|
|
32
|
+
|
|
8
33
|
## [0.2.0] — 2026-11-15
|
|
9
34
|
|
|
10
35
|
### Added
|
package/README.md
CHANGED
|
@@ -22,15 +22,20 @@ const { svg: mark } = fitMark(raw); // 512 canvas, longest side 448, centred,
|
|
|
22
22
|
checkFit(mark).problems; // []
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
+
```bash
|
|
26
|
+
npx logomarks-fit marks/ # fit every .svg in marks/ in place
|
|
27
|
+
npx logomarks-fit --check marks/ # only report; exits 1 if anything is off
|
|
28
|
+
```
|
|
29
|
+
|
|
25
30
|
## What's in it
|
|
26
31
|
|
|
27
32
|
- `/geometry`: shapes and real booleans, strokes, tracks, iso solids, badges and letter skeletons
|
|
28
33
|
- `/svg`: one SVG path parser, filled shapes, bounds and rings
|
|
29
34
|
- `/fit`: the 448 fit and its check (refitting a fitted mark gives the same file)
|
|
30
|
-
- `/analyse`: specks, slivers and pin-holes, gap and stroke widths, shape tags
|
|
35
|
+
- `/analyse`: specks, slivers and pin-holes, gap and stroke widths, shape tags, the optical size (a mark's box, its ink, how much bigger to draw it)
|
|
31
36
|
- `/similar`: outline and letter signatures, IoU and nearest marks
|
|
32
37
|
- `/raster`: scanline fill, components, distance transform
|
|
33
|
-
- `/edit`: the studio's shape edits (weight, soften, gap, detail) and pixelate
|
|
38
|
+
- `/edit`: the studio's shape edits (weight, soften, gap, detail) and pixelate, by amount or by a count of cells
|
|
34
39
|
- `/sdf`: shapes as signed distance fields, smooth unions and exact gap cuts, traced to kit shapes
|
|
35
40
|
- `/stitch`: a mark planned as machine embroidery (fill, satin and border stitches), for a renderer to draw as thread
|
|
36
41
|
- `/parts`: the parts the industry symbols are built from (Node)
|
|
@@ -41,9 +46,9 @@ checkFit(mark).problems; // []
|
|
|
41
46
|
- `/objects`: devices and code glyphs, blades, dishes, sport, cells, faces, animals and rosettes
|
|
42
47
|
- `/forms`: the Bold, Line and Track forms: discs, hooks, jogs, emblems, shatter, slats, flow grids, folds, swashes, stripes, rails, locks and track editions
|
|
43
48
|
- `/batch`: a generator's runner: seeded marks, the sidecars, the command line, `slug` (Node)
|
|
44
|
-
- `logomarks-
|
|
49
|
+
- `logomarks-fit`: the fit as a command: fits marks in place (a file, several, or every `.svg` in a folder), `-o` to another file, `--check` only reports and exits 1 when anything is off
|
|
45
50
|
|
|
46
|
-
Every export, repeatable drawings and the
|
|
51
|
+
Every export, repeatable drawings and the fit command: [API](https://github.com/Logomarks/core/blob/main/docs/api.md).
|
|
47
52
|
|
|
48
53
|
## Links
|
|
49
54
|
|
package/bin/fit.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* logomarks-fit: fits SVG marks to the Logomarks standard (a 512 canvas, the mark's longest side exactly 448, centred,
|
|
4
|
+
* filled paths in currentColor) with `fitMark()`, and reports with `checkFit()`.
|
|
5
|
+
*
|
|
6
|
+
* logomarks-fit mark.svg fit in place
|
|
7
|
+
* logomarks-fit in.svg -o out.svg fit to another file (in.svg out.svg too, while out.svg doesn't exist)
|
|
8
|
+
* logomarks-fit a.svg b.svg marks/ fit each file, and every .svg in a folder, in place
|
|
9
|
+
* logomarks-fit --check mark.svg marks/ only report; exits 1 if anything is off
|
|
10
|
+
*
|
|
11
|
+
* Reads <path d> (with fill-rule), <circle>, <ellipse>, <rect> and <polygon>; nested <g> transforms are NOT applied,
|
|
12
|
+
* so flatten those first. Measures the real outline, curves and arcs included (sampled), never the viewBox.
|
|
13
|
+
*/
|
|
14
|
+
import fs from 'node:fs';
|
|
15
|
+
import path from 'node:path';
|
|
16
|
+
import { fitMark, checkFit } from '../src/fit/index.js';
|
|
17
|
+
|
|
18
|
+
const USAGE = `Usage: logomarks-fit [--check] <file.svg | folder>... [-o out.svg]
|
|
19
|
+
Fits each file in place (a folder: every .svg in it). -o writes one file's fit to another file;
|
|
20
|
+
so does a second path that doesn't exist yet (in.svg out.svg). --check only reports.`;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Stops with a message and the usage.
|
|
24
|
+
*
|
|
25
|
+
* @param {string} msg What is wrong.
|
|
26
|
+
* @returns {never}
|
|
27
|
+
*/
|
|
28
|
+
const fail = (msg) => { console.error(`logomarks-fit: ${msg}\n${USAGE}`); process.exit(2); };
|
|
29
|
+
|
|
30
|
+
const args = process.argv.slice(2);
|
|
31
|
+
if (args.some((a) => a === '--help' || a === '-h')) { console.log(USAGE); process.exit(0); }
|
|
32
|
+
const checking = args.includes('--check');
|
|
33
|
+
/** @type {string | null} */
|
|
34
|
+
let out = null;
|
|
35
|
+
/** @type {string[]} */
|
|
36
|
+
const paths = [];
|
|
37
|
+
for (let i = 0; i < args.length; i++) {
|
|
38
|
+
if (args[i] === '--check') continue;
|
|
39
|
+
if (args[i] === '-o' || args[i] === '--out') { out = args[++i] ?? fail(`${args[i - 1]} needs a file`); continue; }
|
|
40
|
+
if (args[i].startsWith('-')) fail(`unknown option ${args[i]}`);
|
|
41
|
+
paths.push(args[i]);
|
|
42
|
+
}
|
|
43
|
+
if (!paths.length) fail('no file or folder given');
|
|
44
|
+
for (const p of paths) if (!fs.existsSync(p)) {
|
|
45
|
+
// in.svg out.svg: the second path names the file to write, as -o does
|
|
46
|
+
if (!checking && !out && paths.length === 2 && p === paths[1] && p.endsWith('.svg')) out = /** @type {string} */ (paths.pop());
|
|
47
|
+
else fail(`${p} doesn't exist`);
|
|
48
|
+
}
|
|
49
|
+
if (out && (checking || paths.length !== 1 || fs.statSync(paths[0]).isDirectory())) fail('-o takes one .svg file to fit, without --check');
|
|
50
|
+
/** @type {string[]} */
|
|
51
|
+
const files = [];
|
|
52
|
+
for (const a of paths) {
|
|
53
|
+
if (fs.statSync(a).isDirectory()) for (const f of fs.readdirSync(a)) { if (f.endsWith('.svg')) files.push(path.join(a, f)); }
|
|
54
|
+
else files.push(a);
|
|
55
|
+
}
|
|
56
|
+
let bad = 0;
|
|
57
|
+
for (const file of files) {
|
|
58
|
+
const svg = fs.readFileSync(file, 'utf8');
|
|
59
|
+
try {
|
|
60
|
+
if (checking) {
|
|
61
|
+
const { problems, size } = checkFit(svg);
|
|
62
|
+
if (problems.length) bad++;
|
|
63
|
+
console.log(`${problems.length ? '✗' : '✓'} ${file} ${size}${problems.length ? ' — ' + problems.join('; ') : ''}`);
|
|
64
|
+
} else {
|
|
65
|
+
const r = fitMark(svg);
|
|
66
|
+
fs.writeFileSync(out ?? file, r.svg);
|
|
67
|
+
const c = checkFit(r.svg);
|
|
68
|
+
console.log(`${c.problems.length ? '✗' : '✓'} ${out ?? file} (was ${r.before.w.toFixed(1)}×${r.before.h.toFixed(1)}, now ${c.size})`);
|
|
69
|
+
if (c.problems.length) bad++;
|
|
70
|
+
}
|
|
71
|
+
} catch (e) {
|
|
72
|
+
bad++;
|
|
73
|
+
console.log(`✗ ${file} — ${/** @type {Error} */ (e).message}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
process.exit(bad ? 1 : 0);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@logomarks/core",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
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, and the parts the generators draw with: iso objects, letters and type, font glyphs, pixel dither, objects and the Bold, Line and Track forms, plus the batch runner.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -105,7 +105,7 @@
|
|
|
105
105
|
"./package.json": "./package.json"
|
|
106
106
|
},
|
|
107
107
|
"bin": {
|
|
108
|
-
"logomarks-
|
|
108
|
+
"logomarks-fit": "bin/fit.js"
|
|
109
109
|
},
|
|
110
110
|
"files": [
|
|
111
111
|
"bin",
|
package/src/analyse/index.d.ts
CHANGED
|
@@ -6,3 +6,4 @@ export { SPECKS, vectorParts, rasterParts, markParts, smallParts } from './speck
|
|
|
6
6
|
export { SHAPE, shapeTags, tagsOf } from './tags.js';
|
|
7
7
|
export { MEASURE, shapeMeasures } from './shape/measures.js';
|
|
8
8
|
export { WIDTHS, groupRuns, widths } from './widths.js';
|
|
9
|
+
export { OPTICAL, markBox, inkShare, opticalScale } from './optical.js';
|
package/src/analyse/index.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Analysis of a mark: its pieces and holes (specks, slivers, pin-holes), its stroke and gap widths,
|
|
3
|
-
* (with the measures they are decided on).
|
|
2
|
+
* Analysis of a mark: its pieces and holes (specks, slivers, pin-holes), its stroke and gap widths, its shape tags
|
|
3
|
+
* (with the measures they are decided on), and its optical size (its real box and how much of it is ink).
|
|
4
4
|
*/
|
|
5
5
|
export { SPECKS, vectorParts, rasterParts, markParts, smallParts } from './specks.js';
|
|
6
6
|
export { SHAPE, shapeTags, tagsOf } from './tags.js';
|
|
7
7
|
export { MEASURE, shapeMeasures } from './shape/measures.js';
|
|
8
8
|
export { WIDTHS, groupRuns, widths } from './widths.js';
|
|
9
|
+
export { OPTICAL, markBox, inkShare, opticalScale } from './optical.js';
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Bounds } from '../svg/outline.js';
|
|
2
|
+
|
|
3
|
+
/** The optical size's limits: the most a mark grows, and how much its box and its ink count. */
|
|
4
|
+
export const OPTICAL: {
|
|
5
|
+
/** The most a mark grows (× its 448 fit). */
|
|
6
|
+
readonly max: 1.3;
|
|
7
|
+
/** The box's term: (448² / its area) to this power, so a square is 1, 448 × 200 about 1.22. */
|
|
8
|
+
readonly power: 0.25;
|
|
9
|
+
/** The ink's term: up to this much more for a light mark … */
|
|
10
|
+
readonly light: 0.1;
|
|
11
|
+
/** … in full at or below this share of its box inked, none at or above `solid`. */
|
|
12
|
+
readonly thin: 0.2;
|
|
13
|
+
readonly solid: 0.45;
|
|
14
|
+
};
|
|
15
|
+
/** The real box round a mark's outlines (its paths, or the whole file), on the 512 canvas; the 448 fit's box when there is nothing to read. */
|
|
16
|
+
export function markBox(paths: string): Bounds;
|
|
17
|
+
/** How much of a mark's own box is ink, 0–1 (scanline coverage, 16 cells across its longest side); 1 when there is nothing to read. */
|
|
18
|
+
export function inkShare(paths: string): number;
|
|
19
|
+
/** The mark's optical size: how much bigger to draw a wide, slim or light mark, 1 to `OPTICAL.max`; `ink` 1 leaves the ink out of it. */
|
|
20
|
+
export function opticalScale(box: Pick<Bounds, 'w' | 'h'>, ink?: number): number;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module @logomarks/core/analyse/optical
|
|
3
|
+
*
|
|
4
|
+
* A mark's optical size: its real box, how much of that box is ink, and how much bigger to draw it so a wide, slim or
|
|
5
|
+
* light mark holds as much of the eye as a square, solid one at the 448 fit. Browser-safe (core's SVG reader and
|
|
6
|
+
* scanline coverage, no renderer). From the Lookbook's mark measures (`@logomarks/lookbook/marks`).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { shapes } from '../svg/shapes.js';
|
|
10
|
+
import { bounds } from '../svg/outline.js';
|
|
11
|
+
import { rings } from '../svg/rings.js';
|
|
12
|
+
import { cellsAcross, pixelCoverage } from '../edit/pixelate.js';
|
|
13
|
+
import { CANVAS, FIT } from '../fit/index.js';
|
|
14
|
+
|
|
15
|
+
/** The 448 fit's box on the 512 canvas: what `markBox` gives when there is nothing to read. */
|
|
16
|
+
const FITTED = { x: (CANVAS - FIT) / 2, y: (CANVAS - FIT) / 2, w: FIT, h: FIT };
|
|
17
|
+
|
|
18
|
+
/** The optical size's limits: the most a mark grows, and how much its box and its ink count. */
|
|
19
|
+
export const OPTICAL = {
|
|
20
|
+
/** The most a mark grows (× its 448 fit). */
|
|
21
|
+
max: 1.3,
|
|
22
|
+
/** The box's term: (448² / its area) to this power, so a square is 1, 448 × 200 about 1.22. */
|
|
23
|
+
power: 0.25,
|
|
24
|
+
/** The ink's term: up to this much more for a light mark … */
|
|
25
|
+
light: 0.1,
|
|
26
|
+
/** … in full at or below this share of its box inked, none at or above `solid`. */
|
|
27
|
+
thin: 0.2,
|
|
28
|
+
solid: 0.45,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The real box round a mark's outlines, on the 512 canvas (curves and arcs sampled, as the fit measures them); the
|
|
33
|
+
* 448 fit's box when there is nothing to read.
|
|
34
|
+
*
|
|
35
|
+
* @param {string} paths The mark's markup: its paths, or the whole file.
|
|
36
|
+
* @returns {import('../svg/outline.js').Bounds} The box.
|
|
37
|
+
*/
|
|
38
|
+
export function markBox(paths) {
|
|
39
|
+
const list = shapes(paths);
|
|
40
|
+
if (!list.length) return { ...FITTED };
|
|
41
|
+
const b = bounds(list);
|
|
42
|
+
return Number.isFinite(b.w) && b.w > 0 ? b : { ...FITTED };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* How much of a mark's own box is ink, 0–1: the scanline coverage on a grid 16 cells across its longest side.
|
|
47
|
+
*
|
|
48
|
+
* @param {string} paths The mark's markup: its paths, or the whole file.
|
|
49
|
+
* @returns {number} The share inked; 1 when there is nothing to read.
|
|
50
|
+
*/
|
|
51
|
+
export function inkShare(paths) {
|
|
52
|
+
const list = shapes(paths);
|
|
53
|
+
if (!list.length) return 1;
|
|
54
|
+
const grid = cellsAcross(16, markBox(paths));
|
|
55
|
+
const cov = pixelCoverage(list.map((s) => ({ rings: rings(s.segs, 3), evenodd: s.evenodd })), grid);
|
|
56
|
+
let sum = 0;
|
|
57
|
+
for (const v of cov) sum += v;
|
|
58
|
+
return cov.length ? sum / cov.length : 1;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The mark's optical size: how much bigger to draw it so it holds as much of the eye as a square, solid mark at the
|
|
63
|
+
* 448 fit. Every mark is fitted to 448 on its longest side, so a wide, short one (448 × 200) or a slim one (266 × 448)
|
|
64
|
+
* carries far less area, and a light, line-drawn one less weight. 1 for a square solid; 448 × 200 about 1.22; never
|
|
65
|
+
* below 1, never above `OPTICAL.max`.
|
|
66
|
+
*
|
|
67
|
+
* @param {{ w: number, h: number }} box The mark's box (`markBox`).
|
|
68
|
+
* @param {number} [ink=1] The share of its box inked (`inkShare`); 1 leaves the ink out of it.
|
|
69
|
+
* @returns {number} The scale, 1 to `OPTICAL.max`.
|
|
70
|
+
*/
|
|
71
|
+
export function opticalScale(box, ink = 1) {
|
|
72
|
+
const { max, power, light, thin, solid } = OPTICAL;
|
|
73
|
+
const area = (FIT * FIT) / Math.max(1, box.w * box.h);
|
|
74
|
+
const weight = 1 + light * Math.min(1, Math.max(0, (solid - ink) / (solid - thin)));
|
|
75
|
+
return Math.min(max, Math.max(1, area ** power * weight));
|
|
76
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { PixelGrid } from './pixelate.js';
|
|
2
|
+
|
|
3
|
+
/** A mark as square cells: the grid, which cells are filled (row by row), and the filled cells as one path. */
|
|
4
|
+
export interface MarkCells {
|
|
5
|
+
grid: PixelGrid;
|
|
6
|
+
/** 1 per filled cell, row by row. */
|
|
7
|
+
on: Uint8Array;
|
|
8
|
+
/** The filled cells merged into rectangles, as one path's d; empty when none is filled. */
|
|
9
|
+
d: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** The mark (its paths, or the whole file) as square cells, `n` across its longest side, by scanline coverage. */
|
|
13
|
+
export function markCells(paths: string, n: number): MarkCells;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module @logomarks/core/edit/cells
|
|
3
|
+
*
|
|
4
|
+
* A mark as square cells by count: shapes → rings → scanline coverage → filled cells in one call, on a grid `n` cells
|
|
5
|
+
* across the mark's real box (`cellsAcross`). Browser-safe. From the Lookbook's mark measures (its pixel and LED looks).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { shapes } from '../svg/shapes.js';
|
|
9
|
+
import { rings } from '../svg/rings.js';
|
|
10
|
+
import { markBox } from '../analyse/optical.js';
|
|
11
|
+
import { cellsAcross, pixelCoverage, pixelCells, pixelPath } from './pixelate.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The mark as square cells, `n` across its longest side, measured by scanline coverage (half a cell covered, or a
|
|
15
|
+
* fifth where a cell would otherwise break a thin line: `pixelCells`).
|
|
16
|
+
*
|
|
17
|
+
* @param {string} paths The mark's markup: its paths, or the whole file.
|
|
18
|
+
* @param {number} n Cells across the longest side.
|
|
19
|
+
* @returns {import('./cells.js').MarkCells} The grid, the filled cells and the filled cells as one path (empty when none).
|
|
20
|
+
*/
|
|
21
|
+
export function markCells(paths, n) {
|
|
22
|
+
const list = shapes(paths);
|
|
23
|
+
const grid = cellsAcross(n, markBox(paths));
|
|
24
|
+
const cov = pixelCoverage(list.map((s) => ({ rings: rings(s.segs), evenodd: s.evenodd })), grid);
|
|
25
|
+
return { grid, on: pixelCells(cov, grid), d: pixelPath(cov, grid) ?? '' };
|
|
26
|
+
}
|
package/src/edit/index.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export type { ShapeInfo, SmallSizeInfo } from './reshape.js';
|
|
2
2
|
export type { Adjust, Frame, Similarity, AdjustEnv } from './adjust.js';
|
|
3
3
|
export type { PixelGrid } from './pixelate.js';
|
|
4
|
+
export type { MarkCells } from './cells.js';
|
|
4
5
|
export { loadShaper, shaperReady, hasCorners, reshape, pieceCount, shapeInfo, smallSizeInfo } from './reshape.js';
|
|
5
6
|
export { NO_ADJUST, isAdjusted, adjustSuffix, transformPath, adjustPaths } from './adjust.js';
|
|
6
|
-
export { SS, pixelGrid, pixelPath, pixelCells, pixelCoverage } from './pixelate.js';
|
|
7
|
+
export { SS, pixelGrid, cellsAcross, pixelPath, pixelCells, pixelCoverage } from './pixelate.js';
|
|
8
|
+
export { markCells } from './cells.js';
|
package/src/edit/index.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shape edits for a studio: weight, soften, gap and detail (Clipper, loaded on first use), turns, tilts, flips and
|
|
3
|
-
* frames, and pixel-art redraws. Browser-safe: what needs a renderer (exact
|
|
3
|
+
* frames, and pixel-art redraws (by amount, or by a count of cells across). Browser-safe: what needs a renderer (exact
|
|
4
|
+
* bounds, a canvas) is passed in.
|
|
4
5
|
*/
|
|
5
6
|
export { loadShaper, shaperReady, hasCorners, reshape, pieceCount, shapeInfo, smallSizeInfo } from './reshape.js';
|
|
6
7
|
export { NO_ADJUST, isAdjusted, adjustSuffix, transformPath, adjustPaths } from './adjust.js';
|
|
7
|
-
export { SS, pixelGrid, pixelPath, pixelCells, pixelCoverage } from './pixelate.js';
|
|
8
|
+
export { SS, pixelGrid, cellsAcross, pixelPath, pixelCells, pixelCoverage } from './pixelate.js';
|
|
9
|
+
export { markCells } from './cells.js';
|
package/src/edit/pixelate.d.ts
CHANGED
|
@@ -13,6 +13,8 @@ export interface PixelGrid {
|
|
|
13
13
|
export const SS: 6;
|
|
14
14
|
/** The grid for a box: 48 cells across its longest side at amount 0, 14 at amount 1, centred. */
|
|
15
15
|
export function pixelGrid(amount: number, box: { x: number; y: number; width: number; height: number }): PixelGrid;
|
|
16
|
+
/** The grid for a box by count: `n` cells across its longest side (any `n`), centred. */
|
|
17
|
+
export function cellsAcross(n: number, box: { x: number; y: number; w: number; h: number }): PixelGrid;
|
|
16
18
|
/** How much of each cell the mark's rings cover (0–1, row by row), by scanline at SS × SS samples a cell. */
|
|
17
19
|
export function pixelCoverage(paths: readonly FillPath[], grid: PixelGrid): Float32Array;
|
|
18
20
|
/** The filled cells (1 each, row by row): half covered, or a fifth where a cell stands alone or bridges two. */
|
package/src/edit/pixelate.js
CHANGED
|
@@ -36,6 +36,21 @@ export function pixelGrid(amount, box) {
|
|
|
36
36
|
return { cols, rows, cell, x0, y0 };
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
/**
|
|
40
|
+
* A pixel grid of `n` cells across a box's longest side, centred on it: `pixelGrid` sized by a count, any count
|
|
41
|
+
* (`pixelGrid` takes an amount and stops at 14 across).
|
|
42
|
+
*
|
|
43
|
+
* @param {number} n Cells across the longest side.
|
|
44
|
+
* @param {{ x: number, y: number, w: number, h: number }} box The mark's box (`markBox` in `/analyse`, or `bounds`).
|
|
45
|
+
* @returns {import('./index.js').PixelGrid} The grid: columns, rows, cell size and top-left corner.
|
|
46
|
+
*/
|
|
47
|
+
export function cellsAcross(n, box) {
|
|
48
|
+
const cell = Math.max(box.w, box.h) / n;
|
|
49
|
+
const cols = Math.max(1, Math.round(box.w / cell)), rows = Math.max(1, Math.round(box.h / cell));
|
|
50
|
+
const x0 = box.x + box.w / 2 - (cols * cell) / 2, y0 = box.y + box.h / 2 - (rows * cell) / 2;
|
|
51
|
+
return { cols, rows, cell, x0, y0 };
|
|
52
|
+
}
|
|
53
|
+
|
|
39
54
|
/**
|
|
40
55
|
* How much of each cell the mark covers, measured from its rings by scanline at `SS` × `SS` samples a cell: what a
|
|
41
56
|
* canvas would measure, without one.
|
package/src/fit/index.js
CHANGED
|
@@ -129,7 +129,7 @@ export function checkFit(svg) {
|
|
|
129
129
|
if (!vb || vb.join(' ') !== `0 0 ${CANVAS} ${CANVAS}`) problems.push(`viewBox is "${vb?.join(' ')}", not "0 0 ${CANVAS} ${CANVAS}"`);
|
|
130
130
|
if (Math.abs(long - FIT) > FIT_TOLERANCE) problems.push(`longest side ${long.toFixed(1)}, not ${FIT}`);
|
|
131
131
|
if (Math.abs(cx - CANVAS / 2) > FIT_TOLERANCE || Math.abs(cy - CANVAS / 2) > FIT_TOLERANCE) problems.push(`centre (${cx.toFixed(1)}, ${cy.toFixed(1)}), not (${CANVAS / 2}, ${CANVAS / 2})`);
|
|
132
|
-
if (b.x < -FIT_TOLERANCE || b.y < -FIT_TOLERANCE || b.x + b.w > CANVAS + FIT_TOLERANCE || b.y + b.h > CANVAS + FIT_TOLERANCE) problems.push(`runs outside the ${CANVAS} canvas — it will be cut off; run fit
|
|
132
|
+
if (b.x < -FIT_TOLERANCE || b.y < -FIT_TOLERANCE || b.x + b.w > CANVAS + FIT_TOLERANCE || b.y + b.h > CANVAS + FIT_TOLERANCE) problems.push(`runs outside the ${CANVAS} canvas — it will be cut off; run logomarks-fit on it`);
|
|
133
133
|
if (FORBIDDEN_MARKUP.test(svg)) problems.push('contains groups, transforms, strokes, styles or masks — flatten to filled paths');
|
|
134
134
|
return { problems, size: `${b.w.toFixed(1)}×${b.h.toFixed(1)}`, bounds: b };
|
|
135
135
|
}
|
package/src/geometry/glitch.js
CHANGED
|
@@ -12,7 +12,7 @@ import { slices } from './offset.js';
|
|
|
12
12
|
* { kind: 'steps', cuts: [a, b, c(, d)], shift: 22 } four or five thin bands, slid +shift, −shift alternately
|
|
13
13
|
* { kind: 'cut', cuts: [a], shift: 44 } one cut, the lower (or right) band slid
|
|
14
14
|
* { kind: 'shear', cuts: [a(, b)], deg: 30, shift: 44 } a diagonal cut (deg from horizontal); two or three bands
|
|
15
|
-
* `shifts` (one per band) overrides the pattern.
|
|
15
|
+
* `shifts` (one per band) overrides the pattern. Fit the result with fitMark() or logomarks-fit.
|
|
16
16
|
*/
|
|
17
17
|
export function glitchMark(src, { kind = 'bands', cuts, shift = 44, shifts, deg, g = 22 } = {}) {
|
|
18
18
|
const s = typeof src === 'string' ? svgShape(src) : src, n = cuts.length + 1;
|
package/src/geometry/path.js
CHANGED
|
@@ -24,7 +24,7 @@ export function toPath(mp, tol = 0.12) {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
|
|
27
|
-
/** The SVG file for a shape (any size;
|
|
27
|
+
/** The SVG file for a shape (any size; fitMark(), or logomarks-fit, fits it to the standard). */
|
|
28
28
|
export const svg = (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`;
|
|
29
29
|
|
|
30
30
|
/** Writes a shape to an SVG file. */
|
package/src/parts/frame.js
CHANGED
|
@@ -27,7 +27,7 @@ export const centre = (g) => { const [x0, y0, x1, y1] = bbox(g); return mapPts(g
|
|
|
27
27
|
export const outline = (shape) => (typeof shape[0][0][0] === 'number' ? shape[0] : shape[0][0]);
|
|
28
28
|
/** Stacks parts back to front, each later part in front of everything before it and cut free by the gap g. */
|
|
29
29
|
export const chain = (g, ...parts) => parts.reduce((scene, p) => over(scene, p, g));
|
|
30
|
-
/** Builds a mark whose gap lands on exactly `G` once fit
|
|
30
|
+
/** Builds a mark whose gap lands on exactly `G` once the fit scales it to 448: `build(g)` draws it with gap g; it's drawn,
|
|
31
31
|
* measured, and drawn again with the gap divided by the fit's scale (twice, as the gap changes the size a little).
|
|
32
32
|
* `before()` runs before every draw (seed Math.random there, so robust()'s jitter repeats). */
|
|
33
33
|
export const exact = (build, before = () => {}, target = G) => { let g = target, s;
|
package/bin/skill.js
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* logomarks-skill <dir> [--url <url>]: writes the design skill's scripts/kit.mjs and scripts/fit.mjs into <dir>,
|
|
4
|
-
* generated from @logomarks/core. `{{url}}` stays in their headers unless --url fills it.
|
|
5
|
-
*/
|
|
6
|
-
import { writeSkillScripts } from '../src/skill/bundle.js';
|
|
7
|
-
|
|
8
|
-
const args = process.argv.slice(2), at = args.indexOf('--url');
|
|
9
|
-
const url = at >= 0 ? args.splice(at, 2)[1] : undefined;
|
|
10
|
-
if (!args[0]) {
|
|
11
|
-
console.log('Usage: logomarks-skill <dir> [--url <url>]');
|
|
12
|
-
process.exit(1);
|
|
13
|
-
}
|
|
14
|
-
for (const file of writeSkillScripts(args[0], { url })) console.log('wrote', file);
|
package/src/skill/bundle.d.ts
DELETED
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
/** The skill's kit.mjs: the geometry kit as one file needing only polygon-clipping, `{{url}}` unfilled. */
|
|
2
|
-
export function bundleKit(): string;
|
|
3
|
-
/** The skill's fit.mjs: the fit and check as one script with no dependencies, `{{url}}` unfilled. */
|
|
4
|
-
export function bundleFit(): string;
|
|
5
|
-
/** Writes both scripts into a folder; `url` fills `{{url}}`. Returns the files written. */
|
|
6
|
-
export function writeSkillScripts(dir: string, o?: { url?: string }): string[];
|
package/src/skill/bundle.js
DELETED
|
@@ -1,181 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @module @logomarks/core/skill
|
|
3
|
-
*
|
|
4
|
-
* The design skill's two scripts, generated from the library so they never drift: `kit.mjs` (the geometry kit as one
|
|
5
|
-
* file, needing only polygon-clipping) and `fit.mjs` (the fit and check, no dependencies). Each module's code goes in
|
|
6
|
-
* as it is, its imports dropped and its exports kept only where the script exposes them. `{{url}}` in each header is
|
|
7
|
-
* left for the skill's build to fill.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import fs from 'node:fs';
|
|
11
|
-
import * as geometry from '../geometry/index.js';
|
|
12
|
-
|
|
13
|
-
const SRC = new URL('../', import.meta.url);
|
|
14
|
-
|
|
15
|
-
/** The geometry kit's modules, in dependency order. */
|
|
16
|
-
const KIT = ['geometry/core.js', 'geometry/booleans.js', 'geometry/polygon.js', 'geometry/random.js', 'geometry/shapes.js', 'geometry/stroke.js',
|
|
17
|
-
'geometry/path.js', 'geometry/offset.js', 'geometry/mitre.js', 'geometry/iso.js', 'geometry/modular.js', 'geometry/badges.js', 'geometry/letters.js',
|
|
18
|
-
'svg/path.js', 'svg/shapes.js', 'svg/rings.js', 'svg/nonzero.js', 'svg/shape.js', 'geometry/glitch.js', 'geometry/needle.js'];
|
|
19
|
-
|
|
20
|
-
/** The fit's modules, in dependency order. */
|
|
21
|
-
const FIT = ['svg/path.js', 'svg/shapes.js', 'svg/outline.js', 'util/number.js', 'fit/index.js'];
|
|
22
|
-
|
|
23
|
-
const KIT_HEADER = `// Logomarks geometry kit — the helpers the Logomarks set was built with.
|
|
24
|
-
// {{url}} · CC0
|
|
25
|
-
//
|
|
26
|
-
// Shapes are polygon sets (arrays of rings of [x, y] points, y pointing down) on a
|
|
27
|
-
// 512 canvas with C = 256 as the centre. Combine them with real booleans:
|
|
28
|
-
// U(...shapes) union · D(a, ...b) subtract · I(...) intersect · X(...) exclude
|
|
29
|
-
// then write the result with svg(shape) or write('name.svg', shape).
|
|
30
|
-
// Sizes don't need to be exact: scripts/fit.mjs scales every mark to the set's
|
|
31
|
-
// standard (longest side 448, centred) afterwards.
|
|
32
|
-
//
|
|
33
|
-
// Needs one package: npm install polygon-clipping
|
|
34
|
-
// Generated from @logomarks/core (its geometry) by logomarks-skill: edit the library, not this file.
|
|
35
|
-
import pc from 'polygon-clipping';
|
|
36
|
-
`;
|
|
37
|
-
|
|
38
|
-
const FIT_HEADER = `#!/usr/bin/env node
|
|
39
|
-
// Fits SVG marks to the Logomarks standard: 512×512 canvas, the mark's longest side
|
|
40
|
-
// exactly 448, centred (a 32 margin), filled paths in currentColor, nothing else.
|
|
41
|
-
// {{url}} · CC0 · no dependencies
|
|
42
|
-
//
|
|
43
|
-
// node fit.mjs mark.svg fit in place
|
|
44
|
-
// node fit.mjs in.svg -o out.svg fit to another file (in.svg out.svg too, while out.svg doesn't exist)
|
|
45
|
-
// node fit.mjs a.svg b.svg marks/ fit each file, and every .svg in a folder, in place
|
|
46
|
-
// node fit.mjs --check mark.svg marks/ only report; exits 1 if anything is off
|
|
47
|
-
//
|
|
48
|
-
// Reads <path d> (with fill-rule), <circle>, <ellipse>, <rect> and <polygon>; nested
|
|
49
|
-
// <g> transforms are NOT applied, so flatten those first. Measures the real outline,
|
|
50
|
-
// curves and arcs included (sampled), never the viewBox.
|
|
51
|
-
// Generated from @logomarks/core (its svg and fit) by logomarks-skill: edit the library, not this file.
|
|
52
|
-
|
|
53
|
-
import fs from 'node:fs';
|
|
54
|
-
import path from 'node:path';
|
|
55
|
-
`;
|
|
56
|
-
|
|
57
|
-
/** fit.mjs's command line. */
|
|
58
|
-
const FIT_MAIN = `
|
|
59
|
-
// ---------- command line ----------
|
|
60
|
-
|
|
61
|
-
const USAGE = \`Usage: node fit.mjs [--check] <file.svg | folder>... [-o out.svg]
|
|
62
|
-
Fits each file in place (a folder: every .svg in it). -o writes one file's fit to another file;
|
|
63
|
-
so does a second path that doesn't exist yet (in.svg out.svg). --check only reports.\`;
|
|
64
|
-
|
|
65
|
-
/** Stops with a message and the usage. */
|
|
66
|
-
const fail = (msg) => { console.error(\`fit.mjs: \${msg}\\n\${USAGE}\`); process.exit(2); };
|
|
67
|
-
|
|
68
|
-
const args = process.argv.slice(2);
|
|
69
|
-
if (args.some((a) => a === '--help' || a === '-h')) { console.log(USAGE); process.exit(0); }
|
|
70
|
-
const checking = args.includes('--check');
|
|
71
|
-
let out = null;
|
|
72
|
-
const paths = [];
|
|
73
|
-
for (let i = 0; i < args.length; i++) {
|
|
74
|
-
if (args[i] === '--check') continue;
|
|
75
|
-
if (args[i] === '-o' || args[i] === '--out') { out = args[++i] ?? fail(\`\${args[i - 1]} needs a file\`); continue; }
|
|
76
|
-
if (args[i].startsWith('-')) fail(\`unknown option \${args[i]}\`);
|
|
77
|
-
paths.push(args[i]);
|
|
78
|
-
}
|
|
79
|
-
if (!paths.length) fail('no file or folder given');
|
|
80
|
-
for (const p of paths) if (!fs.existsSync(p)) {
|
|
81
|
-
// in.svg out.svg: the second path names the file to write, as -o does
|
|
82
|
-
if (!checking && !out && paths.length === 2 && p === paths[1] && p.endsWith('.svg')) out = paths.pop();
|
|
83
|
-
else fail(\`\${p} doesn't exist\`);
|
|
84
|
-
}
|
|
85
|
-
if (out && (checking || paths.length !== 1 || fs.statSync(paths[0]).isDirectory())) fail('-o takes one .svg file to fit, without --check');
|
|
86
|
-
const files = [];
|
|
87
|
-
for (const a of paths) {
|
|
88
|
-
if (fs.statSync(a).isDirectory()) for (const f of fs.readdirSync(a)) { if (f.endsWith('.svg')) files.push(path.join(a, f)); }
|
|
89
|
-
else files.push(a);
|
|
90
|
-
}
|
|
91
|
-
let bad = 0;
|
|
92
|
-
for (const file of files) {
|
|
93
|
-
const svg = fs.readFileSync(file, 'utf8');
|
|
94
|
-
try {
|
|
95
|
-
if (checking) {
|
|
96
|
-
const { problems, size } = checkFit(svg);
|
|
97
|
-
if (problems.length) bad++;
|
|
98
|
-
console.log(\`\${problems.length ? '✗' : '✓'} \${file} \${size}\${problems.length ? ' — ' + problems.join('; ') : ''}\`);
|
|
99
|
-
} else {
|
|
100
|
-
const r = fitMark(svg);
|
|
101
|
-
fs.writeFileSync(out ?? file, r.svg);
|
|
102
|
-
const c = checkFit(r.svg);
|
|
103
|
-
console.log(\`\${c.problems.length ? '✗' : '✓'} \${out ?? file} (was \${r.before.w.toFixed(1)}×\${r.before.h.toFixed(1)}, now \${c.size})\`);
|
|
104
|
-
if (c.problems.length) bad++;
|
|
105
|
-
}
|
|
106
|
-
} catch (e) {
|
|
107
|
-
bad++;
|
|
108
|
-
console.log(\`✗ \${file} — \${e.message}\`);
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
process.exit(bad ? 1 : 0);
|
|
112
|
-
`;
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* One module's code for a single-file script: its header docblock as a section line, its imports dropped, and
|
|
116
|
-
* `export` kept only on the names in `keep`.
|
|
117
|
-
*
|
|
118
|
-
* @param {string} file The module, relative to src/.
|
|
119
|
-
* @param {ReadonlySet<string>} keep The names the script exports.
|
|
120
|
-
* @returns {{ code: string, names: string[] }} The code, and the top-level names it declares.
|
|
121
|
-
*/
|
|
122
|
-
function inline(file, keep) {
|
|
123
|
-
let s = fs.readFileSync(new URL(file, SRC), 'utf8');
|
|
124
|
-
const doc = /^\/\*\*([\s\S]*?)\*\/\n/.exec(s);
|
|
125
|
-
const title = doc ? doc[1].split('\n').map((l) => l.replace(/^\s*\*\s?/, '')).filter((l) => l && !l.startsWith('@module')).join(' ').split(/[.:](\s|$)/)[0] : file;
|
|
126
|
-
if (doc) s = s.slice(doc[0].length);
|
|
127
|
-
if (/^import \{[^}]* as /m.test(s)) throw new Error(`skill bundle: ${file} renames an import, which a single file can't keep`);
|
|
128
|
-
s = s.replace(/^import [^\n]*\n/gm, '').replace(/^\n+/, '');
|
|
129
|
-
const names = [];
|
|
130
|
-
s = s.replace(/^export (const|function|async function|let) ([\w$]+)/gm, (m, kind, name) => { names.push(name); return keep.has(name) ? m : `${kind} ${name}`; });
|
|
131
|
-
for (const m of s.matchAll(/^(?:const|function|async function|let) ([\w$]+)/gm)) if (!names.includes(m[1])) names.push(m[1]);
|
|
132
|
-
return { code: `// ---------- ${title.trim()} (${file}) ----------\n\n${s.trimEnd()}\n`, names };
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* Modules joined into one script; throws when two declare the same top-level name.
|
|
137
|
-
*
|
|
138
|
-
* @param {readonly string[]} files The modules.
|
|
139
|
-
* @param {ReadonlySet<string>} keep The names the script exports.
|
|
140
|
-
* @returns {string} The code.
|
|
141
|
-
*/
|
|
142
|
-
function join(files, keep) {
|
|
143
|
-
const seen = new Map();
|
|
144
|
-
return files.map((f) => {
|
|
145
|
-
const { code, names } = inline(f, keep);
|
|
146
|
-
for (const n of names) {
|
|
147
|
-
if (seen.has(n)) throw new Error(`skill bundle: ${n} is declared in both ${seen.get(n)} and ${f}`);
|
|
148
|
-
seen.set(n, f);
|
|
149
|
-
}
|
|
150
|
-
return code;
|
|
151
|
-
}).join('\n');
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* The skill's `kit.mjs`: the geometry kit as one file (every export of `@logomarks/core/geometry`).
|
|
156
|
-
*
|
|
157
|
-
* @returns {string} The script, `{{url}}` unfilled.
|
|
158
|
-
*/
|
|
159
|
-
export const bundleKit = () => KIT_HEADER + '\n' + join(KIT, new Set(Object.keys(geometry)));
|
|
160
|
-
|
|
161
|
-
/**
|
|
162
|
-
* The skill's `fit.mjs`: the fit and the check as one script with no dependencies.
|
|
163
|
-
*
|
|
164
|
-
* @returns {string} The script, `{{url}}` unfilled.
|
|
165
|
-
*/
|
|
166
|
-
export const bundleFit = () => FIT_HEADER + '\n' + join(FIT, new Set()) + FIT_MAIN;
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* Writes both scripts into a folder (the skill's `scripts/`).
|
|
170
|
-
*
|
|
171
|
-
* @param {string} dir The folder.
|
|
172
|
-
* @param {{ url?: string }} [o] A URL to fill `{{url}}` with (left as is by default).
|
|
173
|
-
* @returns {string[]} The files written.
|
|
174
|
-
*/
|
|
175
|
-
export function writeSkillScripts(dir, { url } = {}) {
|
|
176
|
-
const fill = (/** @type {string} */ s) => (url ? s.replaceAll('{{url}}', url) : s);
|
|
177
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
178
|
-
const out = [[`${dir}/kit.mjs`, bundleKit()], [`${dir}/fit.mjs`, bundleFit()]];
|
|
179
|
-
for (const [file, text] of out) fs.writeFileSync(file, fill(text));
|
|
180
|
-
return out.map(([file]) => file);
|
|
181
|
-
}
|