@logomarks/core 0.0.0-stage → 0.1.3

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.
Files changed (110) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/LICENSE +21 -0
  3. package/README.md +46 -2
  4. package/bin/skill.js +14 -0
  5. package/package.json +118 -4
  6. package/src/analyse/index.d.ts +6 -0
  7. package/src/analyse/index.js +6 -0
  8. package/src/analyse/specks.d.ts +49 -0
  9. package/src/analyse/specks.js +99 -0
  10. package/src/analyse/tags.d.ts +5 -0
  11. package/src/analyse/tags.js +48 -0
  12. package/src/analyse/widths.d.ts +33 -0
  13. package/src/analyse/widths.js +48 -0
  14. package/src/edit/adjust.d.ts +46 -0
  15. package/src/edit/adjust.js +108 -0
  16. package/src/edit/index.d.ts +6 -0
  17. package/src/edit/index.js +7 -0
  18. package/src/edit/pixelate.d.ts +15 -0
  19. package/src/edit/pixelate.js +105 -0
  20. package/src/edit/reshape.d.ts +28 -0
  21. package/src/edit/reshape.js +463 -0
  22. package/src/fit/index.d.ts +29 -0
  23. package/src/fit/index.js +111 -0
  24. package/src/geometry/badges.d.ts +25 -0
  25. package/src/geometry/badges.js +104 -0
  26. package/src/geometry/booleans.d.ts +27 -0
  27. package/src/geometry/booleans.js +59 -0
  28. package/src/geometry/core.d.ts +30 -0
  29. package/src/geometry/core.js +20 -0
  30. package/src/geometry/glitch.d.ts +14 -0
  31. package/src/geometry/glitch.js +23 -0
  32. package/src/geometry/index.d.ts +27 -0
  33. package/src/geometry/index.js +20 -0
  34. package/src/geometry/iso.d.ts +20 -0
  35. package/src/geometry/iso.js +98 -0
  36. package/src/geometry/letters.d.ts +24 -0
  37. package/src/geometry/letters.js +66 -0
  38. package/src/geometry/modular.d.ts +14 -0
  39. package/src/geometry/modular.js +72 -0
  40. package/src/geometry/needle.d.ts +4 -0
  41. package/src/geometry/needle.js +20 -0
  42. package/src/geometry/offset.d.ts +12 -0
  43. package/src/geometry/offset.js +40 -0
  44. package/src/geometry/path.d.ts +10 -0
  45. package/src/geometry/path.js +31 -0
  46. package/src/geometry/polygon.d.ts +18 -0
  47. package/src/geometry/polygon.js +93 -0
  48. package/src/geometry/random.d.ts +2 -0
  49. package/src/geometry/random.js +13 -0
  50. package/src/geometry/shapes.d.ts +36 -0
  51. package/src/geometry/shapes.js +63 -0
  52. package/src/geometry/stroke.d.ts +37 -0
  53. package/src/geometry/stroke.js +152 -0
  54. package/src/geometry/types.d.ts +16 -0
  55. package/src/index.d.ts +8 -0
  56. package/src/index.js +12 -0
  57. package/src/parts/badges.d.ts +11 -0
  58. package/src/parts/badges.js +20 -0
  59. package/src/parts/curves.d.ts +20 -0
  60. package/src/parts/curves.js +39 -0
  61. package/src/parts/faces.d.ts +15 -0
  62. package/src/parts/faces.js +38 -0
  63. package/src/parts/frame.d.ts +28 -0
  64. package/src/parts/frame.js +39 -0
  65. package/src/parts/index.d.ts +16 -0
  66. package/src/parts/index.js +15 -0
  67. package/src/parts/iso.d.ts +49 -0
  68. package/src/parts/iso.js +95 -0
  69. package/src/parts/letters.d.ts +27 -0
  70. package/src/parts/letters.js +115 -0
  71. package/src/parts/lines.d.ts +31 -0
  72. package/src/parts/lines.js +42 -0
  73. package/src/parts/objects.d.ts +55 -0
  74. package/src/parts/objects.js +103 -0
  75. package/src/parts/run.d.ts +11 -0
  76. package/src/parts/run.js +23 -0
  77. package/src/parts/tiles.d.ts +27 -0
  78. package/src/parts/tiles.js +65 -0
  79. package/src/raster/channels.d.ts +31 -0
  80. package/src/raster/channels.js +96 -0
  81. package/src/raster/components.d.ts +19 -0
  82. package/src/raster/components.js +53 -0
  83. package/src/raster/distance.d.ts +4 -0
  84. package/src/raster/distance.js +64 -0
  85. package/src/raster/index.d.ts +7 -0
  86. package/src/raster/index.js +8 -0
  87. package/src/raster/scanline.d.ts +29 -0
  88. package/src/raster/scanline.js +64 -0
  89. package/src/similar/index.d.ts +3 -0
  90. package/src/similar/index.js +6 -0
  91. package/src/similar/nearest.d.ts +17 -0
  92. package/src/similar/nearest.js +32 -0
  93. package/src/similar/signature.d.ts +16 -0
  94. package/src/similar/signature.js +160 -0
  95. package/src/skill/bundle.d.ts +6 -0
  96. package/src/skill/bundle.js +164 -0
  97. package/src/svg/index.d.ts +8 -0
  98. package/src/svg/index.js +9 -0
  99. package/src/svg/outline.d.ts +15 -0
  100. package/src/svg/outline.js +54 -0
  101. package/src/svg/path.d.ts +33 -0
  102. package/src/svg/path.js +132 -0
  103. package/src/svg/rings.d.ts +5 -0
  104. package/src/svg/rings.js +54 -0
  105. package/src/svg/shape.d.ts +10 -0
  106. package/src/svg/shape.js +75 -0
  107. package/src/svg/shapes.d.ts +20 -0
  108. package/src/svg/shapes.js +83 -0
  109. package/src/util/number.d.ts +2 -0
  110. package/src/util/number.js +14 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,38 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@logomarks/core` are documented here.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.1.3] — 2026-11-06
9
+
10
+ ### Fixed
11
+
12
+ - `letterSignatures()` counts a pixel as ink only above half coverage (8-bit alpha 128 and up, as `holecheck.mjs` did): with Float32 coverage, an alpha of 127 rounded past `127 / 255` and counted, which moved letter scores by about a hundredth and could reorder near matches.
13
+
14
+ ## [0.1.2] — 2026-11-06
15
+
16
+ ### Changed
17
+
18
+ - Renamed from `@logomarks/marks-kit` to `@logomarks/core`, and its bin from `marks-kit-skill` to `logomarks-skill` (the generated scripts' headers name both); no code changes. The repository is now github.com/Logomarks/core.
19
+
20
+ ## [0.1.1] — 2026-11-05
21
+
22
+ ### Changed
23
+
24
+ - Renamed to `@logomarks/marks-kit`; no code changes.
25
+
26
+ ## [0.1.0] — 2026-11-05
27
+
28
+ ### Added
29
+
30
+ - **Geometry** (`/geometry`): the Logomarks kit, split into modules: shapes, booleans with the opt-in `robust()` retry (and `robustOps(rand)`, which leaves polygon-clipping and `Math.random` alone), strokes, weaves, tracks, fillets, offsets, iso solids, modular systems, the house badges and PIX7, letter skeletons, glitch editions and `deneedle()`. Every generator of the Logomarks set redraws byte-identical with it.
31
+ - **SVG** (`/svg`): one path parser for everything that reads a mark, the filled elements of a file, outline bounds, rings, and a whole mark read back as a shape.
32
+ - **Fit** (`/fit`): `fitMark()` and `checkFit()`, byte-identical to the design skill's `fit.mjs`.
33
+ - **Analysis** (`/analyse`): pieces and holes (vector, or a scanline raster), small-part classification, gap and stroke widths, shape tags.
34
+ - **Similarity** (`/similar`): outline and letter signatures from coverage, IoU over all 8 rotations and flips, and `nearestAll()` for "More like this".
35
+ - **Raster** (`/raster`): scanline fill, connected components, distance transform, even-width runs.
36
+ - **Edits** (`/edit`): the studio's reshape (weight, soften, gap, detail with clipper-lib, an optional peer), adjustments and frames, and the pixelate grid.
37
+ - **Parts** (`/parts`): the industry-symbol parts merged into one set: placing and sizing, curves, house lines, badges, tiles, iso scenes, letters, trade objects, faceted heads, and the batch writer.
38
+ - **`marks-kit-skill`**: writes the design skill's single-file `kit.mjs` and `fit.mjs`.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ArrayPress Limited
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,47 @@
1
- # Temporary Holding Version
1
+ # @logomarks/core
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Geometry, SVG reading, fitting, analysis and similarity for logomarks on a 512 grid: the kit the [Logomarks](https://logomarks.ai) set is drawn with.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install @logomarks/core
9
+ npm install clipper-lib # only for the shape edits (reshape)
10
+ ```
11
+
12
+ ## Quick start
13
+
14
+ ```js
15
+ import { U, D, rect, circle, svg } from '@logomarks/core/geometry';
16
+ import { fitMark, checkFit } from '@logomarks/core/fit';
17
+
18
+ // Moonrise: a block with a half-moon bitten out of the top, a sun left floating in the bite.
19
+ const raw = svg(U(D(rect(0, 0, 300, 300), circle(150, 0, 120)), circle(150, 30, 46)));
20
+ const { svg: mark } = fitMark(raw); // 512 canvas, longest side 448, centred, 0.1 grid
21
+ checkFit(mark).problems; // []
22
+ ```
23
+
24
+ ## What's in it
25
+
26
+ - `/geometry`: shapes and real booleans, strokes, tracks, iso solids, badges and letter skeletons
27
+ - `/svg`: one SVG path parser, filled shapes, bounds and rings
28
+ - `/fit`: the 448 fit and its check
29
+ - `/analyse`: specks, slivers and pin-holes, gap and stroke widths, shape tags
30
+ - `/similar`: outline and letter signatures, IoU and nearest marks
31
+ - `/raster`: scanline fill, components, distance transform
32
+ - `/edit`: the studio's shape edits (weight, soften, gap, detail) and pixelate
33
+ - `/parts`: the parts the industry symbols are built from (Node)
34
+ - `logomarks-skill <dir>`: writes the design skill's `kit.mjs` and `fit.mjs`
35
+
36
+ Every export, repeatable drawings and the skill scripts: [API](https://github.com/Logomarks/core/blob/main/docs/api.md).
37
+
38
+ ## Links
39
+
40
+ - [Docs](https://github.com/Logomarks/core/tree/main/docs)
41
+ - [Changelog](https://github.com/Logomarks/core/blob/main/CHANGELOG.md)
42
+ - [Licence](https://github.com/Logomarks/core/blob/main/LICENSE)
43
+ - [GitHub](https://github.com/Logomarks/core)
44
+
45
+ ## Licence
46
+
47
+ MIT © ArrayPress Limited; author David Sherlock.
package/bin/skill.js ADDED
@@ -0,0 +1,14 @@
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/package.json CHANGED
@@ -1,6 +1,120 @@
1
1
  {
2
2
  "name": "@logomarks/core",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.3",
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, and the industry-symbol parts.",
5
+ "type": "module",
6
+ "main": "src/index.js",
7
+ "types": "src/index.d.ts",
8
+ "sideEffects": false,
9
+ "exports": {
10
+ ".": {
11
+ "types": "./src/index.d.ts",
12
+ "import": "./src/index.js",
13
+ "default": "./src/index.js"
14
+ },
15
+ "./geometry": {
16
+ "types": "./src/geometry/index.d.ts",
17
+ "import": "./src/geometry/index.js",
18
+ "default": "./src/geometry/index.js"
19
+ },
20
+ "./parts": {
21
+ "types": "./src/parts/index.d.ts",
22
+ "import": "./src/parts/index.js",
23
+ "default": "./src/parts/index.js"
24
+ },
25
+ "./svg": {
26
+ "types": "./src/svg/index.d.ts",
27
+ "import": "./src/svg/index.js",
28
+ "default": "./src/svg/index.js"
29
+ },
30
+ "./fit": {
31
+ "types": "./src/fit/index.d.ts",
32
+ "import": "./src/fit/index.js",
33
+ "default": "./src/fit/index.js"
34
+ },
35
+ "./raster": {
36
+ "types": "./src/raster/index.d.ts",
37
+ "import": "./src/raster/index.js",
38
+ "default": "./src/raster/index.js"
39
+ },
40
+ "./analyse": {
41
+ "types": "./src/analyse/index.d.ts",
42
+ "import": "./src/analyse/index.js",
43
+ "default": "./src/analyse/index.js"
44
+ },
45
+ "./similar": {
46
+ "types": "./src/similar/index.d.ts",
47
+ "import": "./src/similar/index.js",
48
+ "default": "./src/similar/index.js"
49
+ },
50
+ "./edit": {
51
+ "types": "./src/edit/index.d.ts",
52
+ "import": "./src/edit/index.js",
53
+ "default": "./src/edit/index.js"
54
+ },
55
+ "./edit/reshape": {
56
+ "types": "./src/edit/reshape.d.ts",
57
+ "import": "./src/edit/reshape.js",
58
+ "default": "./src/edit/reshape.js"
59
+ },
60
+ "./package.json": "./package.json"
61
+ },
62
+ "bin": {
63
+ "logomarks-skill": "bin/skill.js"
64
+ },
65
+ "files": [
66
+ "bin",
67
+ "src",
68
+ "README.md",
69
+ "CHANGELOG.md",
70
+ "LICENSE"
71
+ ],
72
+ "scripts": {
73
+ "test": "node --test tests/*.test.js",
74
+ "typecheck": "tsc --noEmit"
75
+ },
76
+ "keywords": [
77
+ "logo",
78
+ "logomark",
79
+ "logomarks",
80
+ "geometry",
81
+ "svg",
82
+ "polygon",
83
+ "boolean",
84
+ "isometric",
85
+ "path",
86
+ "fit",
87
+ "similarity"
88
+ ],
89
+ "author": "David Sherlock",
90
+ "license": "MIT",
91
+ "repository": {
92
+ "type": "git",
93
+ "url": "git+https://github.com/Logomarks/core.git"
94
+ },
95
+ "homepage": "https://www.npmjs.com/package/@logomarks/core",
96
+ "bugs": "https://github.com/Logomarks/core/issues",
97
+ "engines": {
98
+ "node": ">=20.0.0"
99
+ },
100
+ "publishConfig": {
101
+ "access": "public",
102
+ "registry": "https://registry.npmjs.org/"
103
+ },
104
+ "dependencies": {
105
+ "polygon-clipping": "^0.15.7"
106
+ },
107
+ "peerDependencies": {
108
+ "clipper-lib": "^6.4.2"
109
+ },
110
+ "peerDependenciesMeta": {
111
+ "clipper-lib": {
112
+ "optional": true
113
+ }
114
+ },
115
+ "devDependencies": {
116
+ "@types/node": "^26.6.6",
117
+ "clipper-lib": "^6.4.2",
118
+ "typescript": "^6.0.3"
119
+ }
120
+ }
@@ -0,0 +1,6 @@
1
+ export type { Part, PartsRead, SmallOptions, SkipReason } from './specks.js';
2
+ export type { ShapeTag } from './tags.js';
3
+ export type { RunGroup, WidthSettings } from './widths.js';
4
+ export { SPECKS, vectorParts, rasterParts, markParts, smallParts } from './specks.js';
5
+ export { shapeTags } from './tags.js';
6
+ export { WIDTHS, groupRuns, widths } from './widths.js';
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Analysis of a mark: its pieces and holes (specks, slivers, pin-holes), its stroke and gap widths, and its shape tags.
3
+ */
4
+ export { SPECKS, vectorParts, rasterParts, markParts, smallParts } from './specks.js';
5
+ export { shapeTags } from './tags.js';
6
+ export { WIDTHS, groupRuns, widths } from './widths.js';
@@ -0,0 +1,49 @@
1
+ import type { Box } from '../geometry/types.js';
2
+ import type { FilledPath } from '../svg/shapes.js';
3
+
4
+ /** One piece or hole of a mark. */
5
+ export interface Part {
6
+ kind: 'piece' | 'hole';
7
+ /** Square units of the 512 canvas. */
8
+ area: number;
9
+ /** Mean width: 2 · area / perimeter. */
10
+ width: number;
11
+ box: Box;
12
+ /** Read from the raster. */
13
+ raster?: boolean;
14
+ }
15
+ /** A mark's parts and how they were read. */
16
+ export interface PartsRead {
17
+ parts: Part[];
18
+ raster: boolean;
19
+ /** Why the vector read failed, when it did. */
20
+ error?: string;
21
+ }
22
+ /** The limits for smallParts(). */
23
+ export interface SmallOptions {
24
+ piece?: number;
25
+ hole?: number;
26
+ raster?: boolean;
27
+ }
28
+ /** Why a small part is left out: '' when it is a real speck. */
29
+ export type SkipReason = '' | 'tiny' | 'hairline' | 'repeated';
30
+
31
+ /** The house limits for specks. */
32
+ export const SPECKS: {
33
+ piece: number;
34
+ hole: number;
35
+ tiny: number;
36
+ hairline: number;
37
+ sliver: number;
38
+ repeat: number;
39
+ tolerance: { vector: number; raster: number };
40
+ rasterScale: number;
41
+ };
42
+ /** Every piece and hole, read as vectors; `seed` (the slug) seeds the booleans' retry jitter. */
43
+ export function vectorParts(paths: readonly FilledPath[], seed?: string): Part[];
44
+ /** Every piece and hole, read from a scanline raster at `scale` px a unit (8). */
45
+ export function rasterParts(paths: readonly FilledPath[], scale?: number): Part[];
46
+ /** The vector read, or the raster read when the booleans throw (or `raster` is set). */
47
+ export function markParts(paths: readonly FilledPath[], o?: { seed?: string; raster?: boolean }): PartsRead;
48
+ /** The small parts, smallest first, each with why it is left out. */
49
+ export function smallParts(parts: readonly Part[], o?: SmallOptions): (Part & { skip: SkipReason })[];
@@ -0,0 +1,99 @@
1
+ /**
2
+ * @module @logomarks/core/analyse/specks
3
+ *
4
+ * A mark's pieces and holes, to find the stray specks, slivers and pin-holes a boolean leaves behind. Read as vectors
5
+ * (every outline unioned with its fill rule, booleans retried with a jitter seeded by the mark's name, so a read
6
+ * repeats), or, when the booleans can't read it, from a scanline raster (connected components: 8-connected pieces,
7
+ * 4-connected holes). Areas are square units of the 512 canvas: run it on fitted marks.
8
+ */
9
+
10
+ import { robustOps } from '../geometry/booleans.js';
11
+ import { seeded } from '../geometry/random.js';
12
+ import { area, perimeter, ringBox } from '../geometry/polygon.js';
13
+ import { filledShape } from '../svg/shape.js';
14
+ import { rings } from '../svg/rings.js';
15
+ import { frameOf, fill } from '../raster/scanline.js';
16
+ import { components } from '../raster/components.js';
17
+
18
+ /** The house limits: a piece under `piece` is a speck, a hole under `hole` a pin-hole; parts `tiny`, `hairline` (mean
19
+ * width) or repeated `repeat` times (within `tolerance` of each other's area) are left out. */
20
+ export const SPECKS = { piece: 60, hole: 30, tiny: 0.5, hairline: 0.12, sliver: 3, repeat: 6, tolerance: { vector: 0.02, raster: 0.05 }, rasterScale: 8 };
21
+
22
+ /**
23
+ * Every piece and hole of a mark, read as vectors.
24
+ *
25
+ * @param {readonly import('../svg/shapes.js').Shape[]} paths The mark's outlines.
26
+ * @param {string} [seed] Seeds the booleans' retry jitter (the mark's slug), so a read repeats.
27
+ * @returns {import('./specks.js').Part[]} Its parts.
28
+ */
29
+ export function vectorParts(paths, seed = '') {
30
+ /** @type {import('./specks.js').Part[]} */
31
+ const out = [];
32
+ for (const [outer, ...holes] of filledShape(paths, robustOps(seeded(seed)))) {
33
+ const a = area(outer) - holes.reduce((s, h) => s + area(h), 0);
34
+ const p = perimeter(outer) + holes.reduce((s, h) => s + perimeter(h), 0);
35
+ out.push({ kind: 'piece', area: a, width: (2 * a) / p, box: ringBox(outer) });
36
+ for (const h of holes) out.push({ kind: 'hole', area: area(h), width: (2 * area(h)) / perimeter(h), box: ringBox(h) });
37
+ }
38
+ return out;
39
+ }
40
+
41
+ /**
42
+ * The same, read from a scanline raster at `scale` pixels a unit (for marks the booleans can't read).
43
+ *
44
+ * @param {readonly import('../svg/shapes.js').Shape[]} paths The mark's outlines.
45
+ * @param {number} [scale] Pixels a unit (8).
46
+ * @returns {import('./specks.js').Part[]} Its parts, each marked `raster`.
47
+ */
48
+ export function rasterParts(paths, scale = SPECKS.rasterScale) {
49
+ const fp = paths.map((s) => ({ rings: rings(s.segs, 0.5), evenodd: s.evenodd }));
50
+ const bm = fill(fp, frameOf(fp, scale));
51
+ const K = bm.scale;
52
+ return components(bm)
53
+ .filter((c) => c.ink || !c.border)
54
+ .map((c) => {
55
+ const a = c.pixels / (K * K);
56
+ return {
57
+ kind: c.ink ? 'piece' : 'hole',
58
+ area: a,
59
+ width: (2 * a) / Math.max(c.rim / K, 1e-9),
60
+ box: /** @type {import('../geometry/types.js').Box} */ ([bm.x0 + c.box[0] / K, bm.y0 + c.box[1] / K, bm.x0 + c.box[2] / K, bm.y0 + c.box[3] / K]),
61
+ raster: true,
62
+ };
63
+ });
64
+ }
65
+
66
+ /**
67
+ * A mark's parts: the vector read, or the raster read when the booleans throw (`error` says why).
68
+ *
69
+ * @param {readonly import('../svg/shapes.js').Shape[]} paths The mark's outlines.
70
+ * @param {{ seed?: string, raster?: boolean }} [o] The seed, and `raster` to skip the vector read.
71
+ * @returns {import('./specks.js').PartsRead} The parts and how they were read.
72
+ */
73
+ export function markParts(paths, { seed = '', raster = false } = {}) {
74
+ if (!raster) {
75
+ try {
76
+ return { parts: vectorParts(paths, seed), raster: false };
77
+ } catch (e) {
78
+ return { parts: rasterParts(paths), raster: true, error: /** @type {Error} */ (e).message };
79
+ }
80
+ }
81
+ return { parts: rasterParts(paths), raster: true };
82
+ }
83
+
84
+ /**
85
+ * The small parts (pieces under `piece`, holes under `hole`), smallest first, each with why it is left out: '' when it
86
+ * is a real speck, else 'tiny' (two outlines touching), 'hairline' (a zero-width sliver) or 'repeated' (a pattern).
87
+ *
88
+ * @param {readonly import('./specks.js').Part[]} parts A mark's parts.
89
+ * @param {import('./specks.js').SmallOptions} [o] The limits, and whether the parts came from the raster read.
90
+ * @returns {(import('./specks.js').Part & { skip: '' | 'tiny' | 'hairline' | 'repeated' })[]} The small parts.
91
+ */
92
+ export function smallParts(parts, { piece = SPECKS.piece, hole = SPECKS.hole, raster = false } = {}) {
93
+ const tol = raster ? SPECKS.tolerance.raster : SPECKS.tolerance.vector;
94
+ const same = (/** @type {import('./specks.js').Part} */ p) => parts.filter((q) => q.kind === p.kind && Math.abs(q.area - p.area) <= tol * Math.max(p.area, q.area)).length;
95
+ return parts
96
+ .filter((p) => p.area < (p.kind === 'piece' ? piece : hole))
97
+ .sort((a, b) => a.area - b.area)
98
+ .map((p) => ({ ...p, skip: p.area < SPECKS.tiny ? 'tiny' : p.width < SPECKS.hairline ? 'hairline' : same(p) >= SPECKS.repeat ? 'repeated' : '' }));
99
+ }
@@ -0,0 +1,5 @@
1
+ /** A shape tag measured from a mark's raster. */
2
+ export type ShapeTag = 'wide' | 'tall' | 'round' | 'square' | 'solid' | 'light' | 'symmetric' | 'radial';
3
+
4
+ /** A fitted mark's shape tags, from its coverage at `size` px (128) and its outline's width and height. */
5
+ export function shapeTags(a: Float32Array, m: { w: number; h: number; size?: number }): ShapeTag[];
@@ -0,0 +1,48 @@
1
+ /**
2
+ * @module @logomarks/core/analyse/tags
3
+ *
4
+ * Shape tags measured from a mark's coverage: wide or tall, round or square, solid or light, symmetric, radial.
5
+ */
6
+
7
+ import { remap, iou } from '../similar/signature.js';
8
+
9
+ /**
10
+ * A fitted mark's shape tags, from its coverage at `size` px (128) and its outline's size on the 512 canvas.
11
+ * - wide / tall: wider than 1.25 or narrower than 0.8;
12
+ * - round: about square, every corner of its box empty and the silhouette reaching the edge midpoints and diagonals;
13
+ * - square: about square with three corners or more filled;
14
+ * - solid / light: over 62% or under 30% of its box inked;
15
+ * - symmetric: mirrors onto itself (IoU over 0.94); radial: a quarter turn (0.92) or a half turn (0.94) does.
16
+ *
17
+ * @param {Float32Array} a Coverage 0–1, `size` × `size`, row by row.
18
+ * @param {{ w: number, h: number, size?: number }} m The outline's width and height (fitted), and the raster's side.
19
+ * @returns {import('./tags.js').ShapeTag[]} The tags, in a fixed order.
20
+ */
21
+ export function shapeTags(a, { w: mw, h: mh, size: N = 128 }) {
22
+ const tags = new Set();
23
+ const tf = (/** @type {(x: number, y: number) => number[]} */ f) => remap(a, N, f);
24
+ // bbox in raster px
25
+ const s = N / 512, x0 = Math.round((256 - mw / 2) * s), y0 = Math.round((256 - mh / 2) * s), w = Math.round(mw * s), h = Math.round(mh * s);
26
+ let fill = 0; for (let y = y0; y < y0 + h; y++) for (let x = x0; x < x0 + w; x++) fill += a[y * N + x] || 0;
27
+ const density = fill / (w * h);
28
+ const ratio = mw / mh;
29
+ if (ratio > 1.25) tags.add('wide'); else if (ratio < 0.8) tags.add('tall');
30
+ // corners of the bbox: empty everywhere → round silhouette; mostly filled → square
31
+ const c = Math.max(2, Math.round(Math.min(w, h) * 0.1));
32
+ const corner = (/** @type {number} */ cx, /** @type {number} */ cy) => { let f = 0; for (let y = cy; y < cy + c; y++) for (let x = cx; x < cx + c; x++) f += a[y * N + x] || 0; return f / (c * c); };
33
+ const cs = [corner(x0, y0), corner(x0 + w - c, y0), corner(x0, y0 + h - c), corner(x0 + w - c, y0 + h - c)];
34
+ const sq = ratio > 0.92 && ratio < 1.08;
35
+ if (sq && cs.every((v) => v < 0.04)) {
36
+ // also require the silhouette to reach the edge midpoints (a disc, not a diamond or a small blob)
37
+ const mid = (/** @type {number} */ x, /** @type {number} */ y) => a[y * N + x] > 0.5;
38
+ const r = Math.round(w / 2), cxp = x0 + r, cyp = y0 + r, d = Math.round(w * 0.06);
39
+ const diag = Math.round(r * 0.70);
40
+ if ([mid(cxp, y0 + d), mid(cxp, y0 + h - 1 - d), mid(x0 + d, cyp), mid(x0 + w - 1 - d, cyp)].filter(Boolean).length >= 3 &&
41
+ [mid(cxp - diag + d, cyp - diag + d), mid(cxp + diag - d, cyp - diag + d), mid(cxp - diag + d, cyp + diag - d), mid(cxp + diag - d, cyp + diag - d)].filter(Boolean).length >= 3) tags.add('round');
42
+ }
43
+ if (sq && cs.filter((v) => v > 0.6).length >= 3) tags.add('square');
44
+ if (density > 0.62) tags.add('solid'); else if (density < 0.3) tags.add('light');
45
+ if (iou(a, tf((x, y) => [N - 1 - x, y])) > 0.94 || iou(a, tf((x, y) => [x, N - 1 - y])) > 0.94) tags.add('symmetric');
46
+ if (iou(a, tf((x, y) => [y, N - 1 - x])) > 0.92 || iou(a, tf((x, y) => [N - 1 - x, N - 1 - y])) > 0.94) tags.add('radial');
47
+ return /** @type {import('./tags.js').ShapeTag[]} */ ([...tags]);
48
+ }
@@ -0,0 +1,33 @@
1
+ import type { Box } from '../geometry/types.js';
2
+ import type { Channel } from '../raster/channels.js';
3
+ import type { FilledPath } from '../svg/shapes.js';
4
+
5
+ /** Runs of about the same width, taken together. */
6
+ export interface RunGroup {
7
+ /** Their mean width, weighted by length. */
8
+ width: number;
9
+ /** Their total length. */
10
+ length: number;
11
+ count: number;
12
+ /** The box round them all. */
13
+ box: Box;
14
+ }
15
+ /** The raster's scale and padding, and what counts as a run (units of the 512 canvas). */
16
+ export interface WidthSettings {
17
+ scale: number;
18
+ pad: number;
19
+ same: number;
20
+ tolerance: number;
21
+ spread: number;
22
+ run: number;
23
+ shortest: number;
24
+ widest: number;
25
+ narrowest: number;
26
+ }
27
+
28
+ /** The Logomarks settings. */
29
+ export const WIDTHS: WidthSettings;
30
+ /** Runs grouped by width: runs within `same` of each other are one group. */
31
+ export function groupRuns(runs: readonly Channel[], same: number): RunGroup[];
32
+ /** A mark's gaps and strokes, each grouped by width. */
33
+ export function widths(paths: readonly FilledPath[], o?: Partial<WidthSettings>): { gaps: RunGroup[]; strokes: RunGroup[] };
@@ -0,0 +1,48 @@
1
+ /**
2
+ * @module @logomarks/core/analyse/widths
3
+ *
4
+ * A mark's gaps and strokes, measured on a raster as runs of even width (raster/channels.js) and grouped by width:
5
+ * what tells a mark that keeps one gap from one that doesn't, and a feature too thin to survive 32 px.
6
+ */
7
+
8
+ import { rings } from '../svg/rings.js';
9
+ import { frameOf, fill } from '../raster/scanline.js';
10
+ import { channels } from '../raster/channels.js';
11
+
12
+ /** What counts as a run (units of the 512 canvas), and the raster's scale: the Logomarks settings. */
13
+ export const WIDTHS = { scale: 2, pad: 4, same: 3, tolerance: 2, spread: 1, run: 3, shortest: 16, widest: 32, narrowest: 1 };
14
+
15
+ /**
16
+ * Runs grouped by width: runs within `same` of each other are one group.
17
+ *
18
+ * @param {readonly import('../raster/channels.js').Channel[]} runs The runs, narrowest first.
19
+ * @param {number} same How far apart two widths can be and still be one group.
20
+ * @returns {import('./widths.js').RunGroup[]} The groups, each with its mean width, total length, count and box.
21
+ */
22
+ export function groupRuns(runs, same) {
23
+ /** @type {import('./widths.js').RunGroup[]} */
24
+ const out = [];
25
+ for (const c of runs) {
26
+ const g = out.find((x) => Math.abs(x.width - c.width) <= same);
27
+ if (!g) { out.push({ width: c.width, length: c.length, count: 1, box: [...c.box] }); continue; }
28
+ g.width = (g.width * g.length + c.width * c.length) / (g.length + c.length);
29
+ g.length += c.length;
30
+ g.count++;
31
+ g.box = [Math.min(g.box[0], c.box[0]), Math.min(g.box[1], c.box[1]), Math.max(g.box[2], c.box[2]), Math.max(g.box[3], c.box[3])];
32
+ }
33
+ return out;
34
+ }
35
+
36
+ /**
37
+ * A mark's gaps and strokes, each grouped by width.
38
+ *
39
+ * @param {readonly import('../svg/shapes.js').Shape[]} paths The mark's outlines.
40
+ * @param {Partial<typeof WIDTHS>} [o] The settings (WIDTHS by default).
41
+ * @returns {{ gaps: import('./widths.js').RunGroup[], strokes: import('./widths.js').RunGroup[] }} The groups.
42
+ */
43
+ export function widths(paths, o = {}) {
44
+ const s = { ...WIDTHS, ...o };
45
+ const fp = paths.map((p) => ({ rings: rings(p.segs, 0.5), evenodd: p.evenodd }));
46
+ const bm = fill(fp, frameOf(fp, s.scale, s.pad));
47
+ return { gaps: groupRuns(channels(bm, 0, s), s.same), strokes: groupRuns(channels(bm, 1, s), s.same) };
48
+ }
@@ -0,0 +1,46 @@
1
+ /** A frame round the mark. */
2
+ export type Frame = 'none' | 'circle' | 'tile' | 'ring';
3
+ /** An adjustment: quarter turns, a fine tilt (degrees), a flip, a frame, and the shape edits. */
4
+ export interface Adjust {
5
+ turns: number;
6
+ flip: boolean;
7
+ frame: Frame;
8
+ angle: number;
9
+ weight: number;
10
+ soften: number;
11
+ gap?: number;
12
+ detail?: number;
13
+ pixel?: number;
14
+ }
15
+ /** A similarity transform: scale k and rotation deg about (cx, cy), an optional mirror, then to (tx, ty). */
16
+ export interface Similarity {
17
+ k: number;
18
+ deg: number;
19
+ flip: boolean;
20
+ cx: number;
21
+ cy: number;
22
+ tx: number;
23
+ ty: number;
24
+ }
25
+ /** What adjustPaths() needs from a renderer. */
26
+ export interface AdjustEnv {
27
+ /** The exact bounds of some path data. */
28
+ measure?: (ds: string[]) => { x: number; y: number; width: number; height: number };
29
+ /** Whether the mark can be cut out of a frame with one even-odd path. */
30
+ canCut?: (paths: string) => boolean;
31
+ /** The pixelated mark as one path's d (null when nothing is drawn). */
32
+ pixelate?: (els: { d: string; rule?: string }[], amount: number, box: { x: number; y: number; width: number; height: number }) => string | null;
33
+ /** reshape(), once loaded. */
34
+ shaper?: (paths: string, weight: number, soften: number, gap: number, detail: number) => string | null;
35
+ }
36
+
37
+ /** No adjustment. */
38
+ export const NO_ADJUST: Adjust;
39
+ /** Whether an adjustment changes anything. */
40
+ export function isAdjusted(a: Adjust): boolean;
41
+ /** A short filename suffix for an adjustment, e.g. "-r90-flip-circle". */
42
+ export function adjustSuffix(a: Adjust): string;
43
+ /** Path data through a similarity transform, written absolute on the 0.1 grid (arcs stay arcs). */
44
+ export function transformPath(d: string, f: Similarity): string;
45
+ /** The adjusted mark, as <path> elements for a 512 canvas. */
46
+ export function adjustPaths(paths: string, a: Adjust, env?: AdjustEnv): string;