logomarks 0.0.0-stage → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +42 -0
- package/LICENSE +21 -0
- package/README.md +56 -2
- package/bin/logomarks.js +4 -0
- package/data/fonts/BricolageGrotesque-SemiBold.ttf +0 -0
- package/data/fonts/Cagliostro-Regular.ttf +0 -0
- package/data/fonts/Fraunces-SemiBold.ttf +0 -0
- package/data/fonts/IBMPlexMono-Regular.ttf +0 -0
- package/data/fonts/IBMPlexMono-SemiBold.ttf +0 -0
- package/data/fonts/InstrumentSans-SemiBold.ttf +0 -0
- package/data/fonts/InstrumentSerif-Regular.ttf +0 -0
- package/data/fonts/Inter-SemiBold.ttf +0 -0
- package/data/fonts/Limelight-Regular.ttf +0 -0
- package/data/fonts/OFL.txt +103 -0
- package/data/fonts/Shrikhand-Regular.ttf +0 -0
- package/data/fonts/SpaceGrotesk-SemiBold.ttf +0 -0
- package/data/index.bin +0 -0
- package/docs/api.md +31 -0
- package/docs/commands.md +412 -0
- package/docs/editing.md +61 -0
- package/docs/mcp.md +45 -0
- package/package.json +94 -4
- package/src/brand/brand.d.ts +23 -0
- package/src/brand/brand.js +29 -0
- package/src/brand/fonts.d.ts +61 -0
- package/src/brand/fonts.js +93 -0
- package/src/brand/mark.d.ts +53 -0
- package/src/brand/mark.js +151 -0
- package/src/checks/finding.d.ts +56 -0
- package/src/checks/finding.js +57 -0
- package/src/checks/fit.d.ts +5 -0
- package/src/checks/fit.js +46 -0
- package/src/checks/index.d.ts +33 -0
- package/src/checks/index.js +57 -0
- package/src/checks/specks.d.ts +5 -0
- package/src/checks/specks.js +38 -0
- package/src/checks/widths.d.ts +7 -0
- package/src/checks/widths.js +48 -0
- package/src/cli/args.d.ts +20 -0
- package/src/cli/args.js +35 -0
- package/src/cli/commands/check.d.ts +10 -0
- package/src/cli/commands/check.js +66 -0
- package/src/cli/commands/combine.d.ts +10 -0
- package/src/cli/commands/combine.js +54 -0
- package/src/cli/commands/cover.d.ts +10 -0
- package/src/cli/commands/cover.js +53 -0
- package/src/cli/commands/edit.d.ts +10 -0
- package/src/cli/commands/edit.js +70 -0
- package/src/cli/commands/find.d.ts +10 -0
- package/src/cli/commands/find.js +61 -0
- package/src/cli/commands/get.d.ts +10 -0
- package/src/cli/commands/get.js +57 -0
- package/src/cli/commands/glitch.d.ts +10 -0
- package/src/cli/commands/glitch.js +57 -0
- package/src/cli/commands/index.d.ts +15 -0
- package/src/cli/commands/index.js +24 -0
- package/src/cli/commands/kit.d.ts +10 -0
- package/src/cli/commands/kit.js +66 -0
- package/src/cli/commands/mcp.d.ts +9 -0
- package/src/cli/commands/mcp.js +36 -0
- package/src/cli/commands/mockup.d.ts +10 -0
- package/src/cli/commands/mockup.js +61 -0
- package/src/cli/commands/parts.d.ts +10 -0
- package/src/cli/commands/parts.js +44 -0
- package/src/cli/commands/sheet.d.ts +10 -0
- package/src/cli/commands/sheet.js +66 -0
- package/src/cli/commands/similar.d.ts +10 -0
- package/src/cli/commands/similar.js +111 -0
- package/src/cli/commands/social.d.ts +10 -0
- package/src/cli/commands/social.js +65 -0
- package/src/cli/commands/specks.d.ts +20 -0
- package/src/cli/commands/specks.js +103 -0
- package/src/cli/edited.d.ts +6 -0
- package/src/cli/edited.js +32 -0
- package/src/cli/files.d.ts +4 -0
- package/src/cli/files.js +27 -0
- package/src/cli/help.d.ts +20 -0
- package/src/cli/help.js +55 -0
- package/src/cli/index.d.ts +10 -0
- package/src/cli/index.js +104 -0
- package/src/cli/options.d.ts +30 -0
- package/src/cli/options.js +155 -0
- package/src/cli/report.d.ts +4 -0
- package/src/cli/report.js +28 -0
- package/src/cli/style.d.ts +4 -0
- package/src/cli/style.js +34 -0
- package/src/cover/index.d.ts +49 -0
- package/src/cover/index.js +63 -0
- package/src/edit/combine.d.ts +31 -0
- package/src/edit/combine.js +103 -0
- package/src/edit/doc.d.ts +34 -0
- package/src/edit/doc.js +96 -0
- package/src/edit/glitch.d.ts +48 -0
- package/src/edit/glitch.js +43 -0
- package/src/edit/index.d.ts +8 -0
- package/src/edit/index.js +11 -0
- package/src/edit/ops.d.ts +51 -0
- package/src/edit/ops.js +90 -0
- package/src/edit/preview.d.ts +8 -0
- package/src/edit/preview.js +51 -0
- package/src/find/index.d.ts +54 -0
- package/src/find/index.js +88 -0
- package/src/index.d.ts +30 -0
- package/src/index.js +28 -0
- package/src/kit/index.d.ts +26 -0
- package/src/kit/index.js +31 -0
- package/src/mcp/content.d.ts +30 -0
- package/src/mcp/content.js +56 -0
- package/src/mcp/edited.d.ts +5 -0
- package/src/mcp/edited.js +25 -0
- package/src/mcp/index.d.ts +41 -0
- package/src/mcp/index.js +72 -0
- package/src/mcp/inputs.d.ts +46 -0
- package/src/mcp/inputs.js +122 -0
- package/src/mcp/preview.d.ts +4 -0
- package/src/mcp/preview.js +24 -0
- package/src/mcp/tools/check.d.ts +15 -0
- package/src/mcp/tools/check.js +47 -0
- package/src/mcp/tools/combine.d.ts +15 -0
- package/src/mcp/tools/combine.js +42 -0
- package/src/mcp/tools/cover.d.ts +20 -0
- package/src/mcp/tools/cover.js +46 -0
- package/src/mcp/tools/edit.d.ts +21 -0
- package/src/mcp/tools/edit.js +73 -0
- package/src/mcp/tools/find.d.ts +14 -0
- package/src/mcp/tools/find.js +36 -0
- package/src/mcp/tools/get.d.ts +15 -0
- package/src/mcp/tools/get.js +44 -0
- package/src/mcp/tools/glitch.d.ts +12 -0
- package/src/mcp/tools/glitch.js +39 -0
- package/src/mcp/tools/index.d.ts +4 -0
- package/src/mcp/tools/index.js +23 -0
- package/src/mcp/tools/kit.d.ts +28 -0
- package/src/mcp/tools/kit.js +51 -0
- package/src/mcp/tools/mockup.d.ts +22 -0
- package/src/mcp/tools/mockup.js +51 -0
- package/src/mcp/tools/sheet.d.ts +15 -0
- package/src/mcp/tools/sheet.js +43 -0
- package/src/mcp/tools/similar.d.ts +13 -0
- package/src/mcp/tools/similar.js +42 -0
- package/src/mcp/tools/social.d.ts +5 -0
- package/src/mcp/tools/social.js +68 -0
- package/src/mockup/index.d.ts +52 -0
- package/src/mockup/index.js +152 -0
- package/src/raster/render.d.ts +30 -0
- package/src/raster/render.js +100 -0
- package/src/rules.d.ts +73 -0
- package/src/rules.js +139 -0
- package/src/sheet/card.d.ts +20 -0
- package/src/sheet/card.js +190 -0
- package/src/sheet/draw.d.ts +26 -0
- package/src/sheet/draw.js +94 -0
- package/src/sheet/grid.d.ts +2 -0
- package/src/sheet/grid.js +32 -0
- package/src/sheet/index.d.ts +31 -0
- package/src/sheet/index.js +47 -0
- package/src/sheet/theme.d.ts +8 -0
- package/src/sheet/theme.js +45 -0
- package/src/similar/folder.d.ts +20 -0
- package/src/similar/folder.js +66 -0
- package/src/similar/nearest.d.ts +32 -0
- package/src/similar/nearest.js +60 -0
- package/src/similar/signature.d.ts +12 -0
- package/src/similar/signature.js +42 -0
- package/src/similar/store.d.ts +67 -0
- package/src/similar/store.js +88 -0
- package/src/util/color.d.ts +2 -0
- package/src/util/color.js +23 -0
- package/src/util/files.d.ts +8 -0
- package/src/util/files.js +60 -0
- package/src/util/round.d.ts +2 -0
- package/src/util/round.js +14 -0
- package/src/util/version.d.ts +2 -0
- package/src/util/version.js +10 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/** A bundled typeface. */
|
|
2
|
+
export interface Font {
|
|
3
|
+
/** Its name (`Inter`). */
|
|
4
|
+
name: string;
|
|
5
|
+
/** The face's family, as an SVG names it with `weight` (`Inter SemiBold`, `IBM Plex Mono`). */
|
|
6
|
+
family: string;
|
|
7
|
+
/** The one weight bundled. */
|
|
8
|
+
weight: number;
|
|
9
|
+
/** Its file in `data/fonts/`. */
|
|
10
|
+
file: string;
|
|
11
|
+
/** Letter-spacing for a name, in em. */
|
|
12
|
+
tracking: number;
|
|
13
|
+
/** Its name on Google Fonts, where to get the other weights. */
|
|
14
|
+
google: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** The typefaces a brand's name can be set in (all SIL OFL 1.1). */
|
|
18
|
+
export const FONTS: {
|
|
19
|
+
readonly inter: Font;
|
|
20
|
+
readonly 'space-grotesk': Font;
|
|
21
|
+
readonly 'instrument-sans': Font;
|
|
22
|
+
readonly 'bricolage-grotesque': Font;
|
|
23
|
+
readonly 'instrument-serif': Font;
|
|
24
|
+
readonly fraunces: Font;
|
|
25
|
+
readonly cagliostro: Font;
|
|
26
|
+
readonly limelight: Font;
|
|
27
|
+
readonly shrikhand: Font;
|
|
28
|
+
readonly 'plex-mono': Font;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/** A typeface's id. */
|
|
32
|
+
export type FontId = keyof typeof FONTS;
|
|
33
|
+
|
|
34
|
+
/** The name's typeface when none is asked for: `inter`. */
|
|
35
|
+
export const DEFAULT_FONT: FontId;
|
|
36
|
+
/** The labels' family, IBM Plex Mono (regular and semibold, by weight). */
|
|
37
|
+
export const LABEL_FONT: string;
|
|
38
|
+
/** A file bundled whole, as published: where it comes from, and its sha256. */
|
|
39
|
+
export interface Upstream {
|
|
40
|
+
url: string;
|
|
41
|
+
sha256: string;
|
|
42
|
+
}
|
|
43
|
+
/** The files bundled whole (the faces with a Reserved Font Name), by file. */
|
|
44
|
+
export const UPSTREAM: Readonly<Record<string, Upstream>>;
|
|
45
|
+
/** Every bundled font file. */
|
|
46
|
+
export function fontFiles(): URL[];
|
|
47
|
+
/** A typeface by id or name, any case; null when it isn't bundled. */
|
|
48
|
+
export function fontId(name: string): FontId | null;
|
|
49
|
+
|
|
50
|
+
/** A bundled face as `@logomarks/brand-kit` takes it. */
|
|
51
|
+
export interface BrandFont {
|
|
52
|
+
name: string;
|
|
53
|
+
/** The TrueType file's bytes. */
|
|
54
|
+
data: Uint8Array;
|
|
55
|
+
weight: number;
|
|
56
|
+
/** Letter-spacing for a name, in em. */
|
|
57
|
+
tracking: number;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** A bundled face as `@logomarks/brand-kit` takes it: name, bytes (read once), weight and tracking. */
|
|
61
|
+
export function brandFont(id?: FontId): BrandFont;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module logomarks/brand/fonts
|
|
3
|
+
*
|
|
4
|
+
* The bundled typefaces, all SIL Open Font License, as plain TrueType (resvg
|
|
5
|
+
* reads it) in `data/fonts/` (`scripts/fonts.js` writes them): Fontsource's
|
|
6
|
+
* latin subsets, except the faces with a Reserved Font Name, bundled whole
|
|
7
|
+
* (see {@link UPSTREAM}). IBM Plex Mono sets the sheets' labels; the rest set
|
|
8
|
+
* a brand's name. Four are for restaurants: Fraunces (a warm serif: bistros,
|
|
9
|
+
* wine bars), Cagliostro (a flared sans, set wide), Limelight (art deco) and
|
|
10
|
+
* Shrikhand (retro chunky: diners, brunch, ice cream).
|
|
11
|
+
* Each file is one face. An SVG names it by `family` and `weight`: most files
|
|
12
|
+
* are a family of their own (`Inter SemiBold`), but Plex Mono's two share
|
|
13
|
+
* `IBM Plex Mono`, so the weight picks the semibold. `brandFont()` hands a face
|
|
14
|
+
* to `@logomarks/brand-kit` as bytes, with its name and weight.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import fs from 'node:fs';
|
|
18
|
+
|
|
19
|
+
/** The typefaces a brand's name can be set in, by id. */
|
|
20
|
+
export const FONTS = {
|
|
21
|
+
inter: { name: 'Inter', family: 'Inter SemiBold', weight: 600, file: 'Inter-SemiBold.ttf', tracking: -0.02, google: 'Inter' },
|
|
22
|
+
'space-grotesk': { name: 'Space Grotesk', family: 'Space Grotesk SemiBold', weight: 600, file: 'SpaceGrotesk-SemiBold.ttf', tracking: -0.02, google: 'Space Grotesk' },
|
|
23
|
+
'instrument-sans': { name: 'Instrument Sans', family: 'Instrument Sans SemiBold', weight: 600, file: 'InstrumentSans-SemiBold.ttf', tracking: -0.015, google: 'Instrument Sans' },
|
|
24
|
+
'bricolage-grotesque': { name: 'Bricolage Grotesque', family: 'Bricolage Grotesque SemiBold', weight: 600, file: 'BricolageGrotesque-SemiBold.ttf', tracking: -0.02, google: 'Bricolage Grotesque' },
|
|
25
|
+
'instrument-serif': { name: 'Instrument Serif', family: 'Instrument Serif', weight: 400, file: 'InstrumentSerif-Regular.ttf', tracking: -0.01, google: 'Instrument Serif' },
|
|
26
|
+
fraunces: { name: 'Fraunces', family: 'Fraunces SemiBold', weight: 600, file: 'Fraunces-SemiBold.ttf', tracking: -0.02, google: 'Fraunces' },
|
|
27
|
+
cagliostro: { name: 'Cagliostro', family: 'Cagliostro', weight: 400, file: 'Cagliostro-Regular.ttf', tracking: 0.3, google: 'Cagliostro' },
|
|
28
|
+
limelight: { name: 'Limelight', family: 'Limelight', weight: 400, file: 'Limelight-Regular.ttf', tracking: 0.06, google: 'Limelight' },
|
|
29
|
+
shrikhand: { name: 'Shrikhand', family: 'Shrikhand', weight: 400, file: 'Shrikhand-Regular.ttf', tracking: 0, google: 'Shrikhand' },
|
|
30
|
+
'plex-mono': { name: 'IBM Plex Mono', family: 'IBM Plex Mono', weight: 600, file: 'IBMPlexMono-SemiBold.ttf', tracking: -0.02, google: 'IBM Plex Mono' },
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/** The name's typeface when none is asked for. */
|
|
34
|
+
export const DEFAULT_FONT = 'inter';
|
|
35
|
+
|
|
36
|
+
/** The labels' family (the review sheet and the brand sheet), regular and semibold by weight. */
|
|
37
|
+
export const LABEL_FONT = 'IBM Plex Mono';
|
|
38
|
+
|
|
39
|
+
/** google/fonts at one commit, where the whole files come from. */
|
|
40
|
+
const GOOGLE_FONTS = 'https://raw.githubusercontent.com/google/fonts/24ddc1f324cac5d9f5aedca126d298f56e3f1640/ofl';
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The files bundled whole, byte for byte as published, by file: the faces
|
|
44
|
+
* with a Reserved Font Name, which the OFL lets no Modified Version (a subset
|
|
45
|
+
* is one) carry. `scripts/fonts.js` fetches each and checks its sha256, and
|
|
46
|
+
* its licence comes from the `OFL.txt` beside it. (The studio's webfonts are
|
|
47
|
+
* Fontsource's subsets, the usual web-font distribution; this is about the
|
|
48
|
+
* files the package ships.)
|
|
49
|
+
*/
|
|
50
|
+
export const UPSTREAM = {
|
|
51
|
+
'Cagliostro-Regular.ttf': { url: `${GOOGLE_FONTS}/cagliostro/Cagliostro-Regular.ttf`, sha256: '0d205e9a9b34691dca50064ea681404157f1747e39b592fce978b2a32bd5fb61' },
|
|
52
|
+
'Limelight-Regular.ttf': { url: `${GOOGLE_FONTS}/limelight/Limelight-Regular.ttf`, sha256: 'f5bf8a9159adfc5b9c756c85d812a4fd1299b8916fefdf39ad4b24ade6d8d0cf' },
|
|
53
|
+
'IBMPlexMono-SemiBold.ttf': { url: `${GOOGLE_FONTS}/ibmplexmono/IBMPlexMono-SemiBold.ttf`, sha256: 'd3c38e55c78f5b0f28009fddba4834ec503278936a5986032424c9bd2d23aa46' },
|
|
54
|
+
'IBMPlexMono-Regular.ttf': { url: `${GOOGLE_FONTS}/ibmplexmono/IBMPlexMono-Regular.ttf`, sha256: '6a3412f058c7d8dfd9170c41e85ade48e5156ecb89356110ca57a0a27734af46' },
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/** Every bundled file: the brand faces and Plex Mono's regular, for labels. */
|
|
58
|
+
const FILES = [...new Set([...Object.values(FONTS).map((f) => f.file), 'IBMPlexMono-Regular.ttf'])];
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Where every bundled font file is.
|
|
62
|
+
*
|
|
63
|
+
* @returns {URL[]} The files.
|
|
64
|
+
*/
|
|
65
|
+
export const fontFiles = () => FILES.map((f) => new URL(`../../data/fonts/${f}`, import.meta.url));
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* A typeface by id or name, any case (`space-grotesk`, `Space Grotesk`).
|
|
69
|
+
*
|
|
70
|
+
* @param {string} name The id or name.
|
|
71
|
+
* @returns {import('./fonts.js').FontId | null} Its id, or null when it isn't bundled.
|
|
72
|
+
*/
|
|
73
|
+
export function fontId(name) {
|
|
74
|
+
const key = name.trim().toLowerCase().replace(/\s+/g, '-');
|
|
75
|
+
if (key in FONTS) return /** @type {import('./fonts.js').FontId} */ (key);
|
|
76
|
+
const hit = Object.entries(FONTS).find(([, f]) => f.name.toLowerCase().replace(/\s+/g, '-') === key);
|
|
77
|
+
return hit ? /** @type {import('./fonts.js').FontId} */ (hit[0]) : null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** @type {Map<string, Uint8Array>} */
|
|
81
|
+
const bytes = new Map();
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* A bundled face as `@logomarks/brand-kit` takes it: its name, its bytes (read once), its weight and tracking.
|
|
85
|
+
*
|
|
86
|
+
* @param {import('./fonts.js').FontId} [id] The face (inter).
|
|
87
|
+
* @returns {import('./fonts.js').BrandFont} The font.
|
|
88
|
+
*/
|
|
89
|
+
export function brandFont(id = DEFAULT_FONT) {
|
|
90
|
+
const f = FONTS[id];
|
|
91
|
+
if (!bytes.has(f.file)) bytes.set(f.file, new Uint8Array(fs.readFileSync(new URL(`../../data/fonts/${f.file}`, import.meta.url))));
|
|
92
|
+
return { name: f.name, data: /** @type {Uint8Array} */ (bytes.get(f.file)), weight: f.weight, tracking: f.tracking };
|
|
93
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { MarkIndex, IndexedMark } from '../similar/store.js';
|
|
2
|
+
|
|
3
|
+
/** A mark to build on. */
|
|
4
|
+
export interface Mark {
|
|
5
|
+
slug: string;
|
|
6
|
+
name: string;
|
|
7
|
+
/** The SVG: 512 canvas, filled paths in `currentColor`. */
|
|
8
|
+
svg: string;
|
|
9
|
+
/** Whether it is a live Logomark (CC0) rather than a file. */
|
|
10
|
+
live: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** How to draw a mark on a square. */
|
|
14
|
+
export interface TileOptions {
|
|
15
|
+
/** The side in pixels. */
|
|
16
|
+
size: number;
|
|
17
|
+
/** The mark's colour. */
|
|
18
|
+
color: string;
|
|
19
|
+
/** A ground behind it (transparent otherwise). */
|
|
20
|
+
ground?: string;
|
|
21
|
+
/** Space round the 512 canvas, as a fraction of it (0). */
|
|
22
|
+
pad?: number;
|
|
23
|
+
/** The ground's corner radius, as a fraction of its side (0). */
|
|
24
|
+
radius?: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Where a live mark's page is: this and its slug. */
|
|
28
|
+
export const MARK_URL: string;
|
|
29
|
+
|
|
30
|
+
/** A mark that wasn't found; `suggestions` holds the closest slugs. */
|
|
31
|
+
export class MarkNotFound extends Error {
|
|
32
|
+
constructor(ref: string, suggestions: string[]);
|
|
33
|
+
suggestions: string[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Text as a slug. */
|
|
37
|
+
export function slugify(s: string): string;
|
|
38
|
+
/** A slug as a name. */
|
|
39
|
+
export function titleCase(slug: string): string;
|
|
40
|
+
/** A live mark by slug or name, any case. */
|
|
41
|
+
export function findMark(ref: string, index?: MarkIndex): IndexedMark | undefined;
|
|
42
|
+
/** The live slugs closest to a query. */
|
|
43
|
+
export function suggest(ref: string, index?: MarkIndex, n?: number): string[];
|
|
44
|
+
/** A live mark by slug or name, or an SVG file fitted to the standard. Throws `MarkNotFound`. */
|
|
45
|
+
export function loadMark(ref: string, index?: MarkIndex): Mark;
|
|
46
|
+
/** The mark's paths in a colour, in a `<g>`, without the `<svg>`. */
|
|
47
|
+
export function markBody(svg: string, color: string): string;
|
|
48
|
+
/** The mark as a file, the fill on the root (`currentColor` by default). */
|
|
49
|
+
export function paint(svg: string, color?: string): string;
|
|
50
|
+
/** The mark on a square (transparent, or on a ground with rounded corners), as SVG. */
|
|
51
|
+
export function tile(svg: string, o: TileOptions): string;
|
|
52
|
+
/** The mark drawn to a square PNG. */
|
|
53
|
+
export function markPng(svg: string, o: TileOptions): Promise<Uint8Array>;
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module logomarks/brand/mark
|
|
3
|
+
*
|
|
4
|
+
* The mark a brand is built on: a live mark by slug or name (from the
|
|
5
|
+
* bundled index), or your own SVG, fitted to the standard. Painted in a
|
|
6
|
+
* colour and drawn at a size.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import fs from 'node:fs';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import { loadIndex } from '../similar/store.js';
|
|
12
|
+
import { fitMark } from '@logomarks/core/fit';
|
|
13
|
+
import { body } from '@logomarks/core/svg';
|
|
14
|
+
import { render } from '../raster/render.js';
|
|
15
|
+
import { CANVAS } from '../rules.js';
|
|
16
|
+
import { round } from '../util/round.js';
|
|
17
|
+
|
|
18
|
+
/** Where a live mark's page is. */
|
|
19
|
+
export const MARK_URL = 'https://logomarks.ai/marks/';
|
|
20
|
+
|
|
21
|
+
/** A mark that wasn't found, with the closest slugs. */
|
|
22
|
+
export class MarkNotFound extends Error {
|
|
23
|
+
/**
|
|
24
|
+
* @param {string} ref What was asked for.
|
|
25
|
+
* @param {string[]} suggestions The closest slugs.
|
|
26
|
+
*/
|
|
27
|
+
constructor(ref, suggestions) {
|
|
28
|
+
super(`no live mark "${ref}"${suggestions.length ? `: did you mean ${suggestions.join(', ')}?` : ''}`);
|
|
29
|
+
this.suggestions = suggestions;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Text as a slug: lower case, words joined by hyphens.
|
|
35
|
+
*
|
|
36
|
+
* @param {string} s The text.
|
|
37
|
+
* @returns {string} The slug.
|
|
38
|
+
*/
|
|
39
|
+
export const slugify = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* A slug as a name: `hex-gem` → `Hex Gem`.
|
|
43
|
+
*
|
|
44
|
+
* @param {string} slug The slug.
|
|
45
|
+
* @returns {string} The name.
|
|
46
|
+
*/
|
|
47
|
+
export const titleCase = (slug) => slug.split('-').filter(Boolean).map((w) => w[0].toUpperCase() + w.slice(1)).join(' ');
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* A live mark by slug or name, any case.
|
|
51
|
+
*
|
|
52
|
+
* @param {string} ref The slug (`hex-gem`) or name (`Hex Gem`).
|
|
53
|
+
* @param {import('../similar/store.js').MarkIndex} [index] The live set (the bundled index).
|
|
54
|
+
* @returns {import('../similar/store.js').IndexedMark | undefined} The mark.
|
|
55
|
+
*/
|
|
56
|
+
export function findMark(ref, index = loadIndex()) {
|
|
57
|
+
const slug = slugify(ref);
|
|
58
|
+
return index.marks.find((m) => m.slug === ref) ?? index.marks.find((m) => m.slug === slug || slugify(m.name) === slug);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The live slugs closest to a query: the most of its words (all of it, run together, counts
|
|
63
|
+
* double), then the shortest.
|
|
64
|
+
*
|
|
65
|
+
* @param {string} ref The query.
|
|
66
|
+
* @param {import('../similar/store.js').MarkIndex} [index] The live set.
|
|
67
|
+
* @param {number} [n] How many.
|
|
68
|
+
* @returns {string[]} The slugs.
|
|
69
|
+
*/
|
|
70
|
+
export function suggest(ref, index = loadIndex(), n = 3) {
|
|
71
|
+
const words = slugify(ref).split('-').filter(Boolean);
|
|
72
|
+
const joined = words.join('');
|
|
73
|
+
return index.marks
|
|
74
|
+
.map((m) => ({ slug: m.slug, score: words.filter((w) => m.slug.includes(w)).length + (m.slug.replace(/-/g, '').includes(joined) ? 2 : 0) }))
|
|
75
|
+
.filter((m) => m.score > 0)
|
|
76
|
+
.sort((a, b) => b.score - a.score || a.slug.length - b.slug.length || a.slug.localeCompare(b.slug))
|
|
77
|
+
.slice(0, n)
|
|
78
|
+
.map((m) => m.slug);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* A mark from a slug, a name or an SVG file. A file is fitted to the
|
|
83
|
+
* standard (512 canvas, longest side 448, centred, `currentColor`).
|
|
84
|
+
*
|
|
85
|
+
* @param {string} ref A live slug or name, or a path to an SVG.
|
|
86
|
+
* @param {import('../similar/store.js').MarkIndex} [index] The live set.
|
|
87
|
+
* @returns {import('./mark.js').Mark} The mark.
|
|
88
|
+
* @throws {MarkNotFound} When it is neither a file nor a live mark.
|
|
89
|
+
*/
|
|
90
|
+
export function loadMark(ref, index) {
|
|
91
|
+
if (/\.svg$/i.test(ref) || fs.existsSync(ref)) {
|
|
92
|
+
const slug = slugify(path.basename(ref).replace(/\.svg$/i, '')) || 'mark';
|
|
93
|
+
return { slug, name: titleCase(slug), svg: fitMark(fs.readFileSync(ref, 'utf8')).svg, live: false };
|
|
94
|
+
}
|
|
95
|
+
const all = index ?? loadIndex();
|
|
96
|
+
const m = findMark(ref, all);
|
|
97
|
+
if (!m) throw new MarkNotFound(ref, suggest(ref, all));
|
|
98
|
+
return { slug: m.slug, name: m.name, svg: m.svg, live: true };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* The mark's paths, any `currentColor` in them made a colour.
|
|
103
|
+
*
|
|
104
|
+
* @param {string} svg The mark.
|
|
105
|
+
* @param {string} color The colour.
|
|
106
|
+
* @returns {string} The paths.
|
|
107
|
+
*/
|
|
108
|
+
const paths = (svg, color) => body(svg).replace(/fill="currentColor"/g, `fill="${color}"`);
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* The mark's drawing in a colour, in a group, without its `<svg>`: to place in another SVG.
|
|
112
|
+
*
|
|
113
|
+
* @param {string} svg The mark.
|
|
114
|
+
* @param {string} color The fill (`currentColor` keeps it).
|
|
115
|
+
* @returns {string} Its paths.
|
|
116
|
+
*/
|
|
117
|
+
export const markBody = (svg, color) => `<g fill="${color}">${paths(svg, color)}</g>`;
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* The mark as a file in a colour (`currentColor` leaves it to the page).
|
|
121
|
+
*
|
|
122
|
+
* @param {string} svg The mark.
|
|
123
|
+
* @param {string} [color] The fill.
|
|
124
|
+
* @returns {string} The SVG, 512 canvas, the fill on the root.
|
|
125
|
+
*/
|
|
126
|
+
export const paint = (svg, color = 'currentColor') =>
|
|
127
|
+
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${CANVAS} ${CANVAS}" fill="${color}">${paths(svg, color)}</svg>\n`;
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* The mark on a square, transparent or on a ground: the SVG to draw.
|
|
131
|
+
*
|
|
132
|
+
* @param {string} svg The mark.
|
|
133
|
+
* @param {import('./mark.js').TileOptions} o Size and colours.
|
|
134
|
+
* @returns {string} The SVG.
|
|
135
|
+
*/
|
|
136
|
+
export function tile(svg, { size, color, ground, pad = 0, radius = 0 }) {
|
|
137
|
+
const at = round(-pad * CANVAS), side = round(CANVAS * (1 + 2 * pad));
|
|
138
|
+
const bg = ground ? `<rect x="${at}" y="${at}" width="${side}" height="${side}" rx="${round(radius * side)}" fill="${ground}"/>` : '';
|
|
139
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="${at} ${at} ${side} ${side}">${bg}${markBody(svg, color)}</svg>\n`;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* The mark drawn to a square PNG, transparent or on a ground.
|
|
144
|
+
*
|
|
145
|
+
* @param {string} svg The mark.
|
|
146
|
+
* @param {import('./mark.js').TileOptions} o Size and colours.
|
|
147
|
+
* @returns {Promise<Uint8Array>} The PNG.
|
|
148
|
+
*/
|
|
149
|
+
export async function markPng(svg, o) {
|
|
150
|
+
return /** @type {Uint8Array} */ ((await render(tile(svg, o), { png: true })).png);
|
|
151
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { Box, Point } from '@logomarks/core/geometry';
|
|
2
|
+
|
|
3
|
+
/** How much a finding matters: errors fail the check; warnings fail it with `--strict`; notes never do. */
|
|
4
|
+
export type Severity = 'error' | 'warning' | 'info';
|
|
5
|
+
|
|
6
|
+
/** Every finding a check can report. */
|
|
7
|
+
export type FindingId =
|
|
8
|
+
| 'empty'
|
|
9
|
+
| 'viewbox'
|
|
10
|
+
| 'size'
|
|
11
|
+
| 'centre'
|
|
12
|
+
| 'clipped'
|
|
13
|
+
| 'markup'
|
|
14
|
+
| 'fill'
|
|
15
|
+
| 'precision'
|
|
16
|
+
| 'speck'
|
|
17
|
+
| 'sliver'
|
|
18
|
+
| 'pinhole'
|
|
19
|
+
| 'thin'
|
|
20
|
+
| 'narrow-gap'
|
|
21
|
+
| 'uneven-gaps'
|
|
22
|
+
| 'raster-read';
|
|
23
|
+
|
|
24
|
+
/** What a finding id means. */
|
|
25
|
+
export interface FindingKind {
|
|
26
|
+
/** Its usual severity. */
|
|
27
|
+
severity: Severity;
|
|
28
|
+
/** Whether `--fix` repairs it. */
|
|
29
|
+
fixable: boolean;
|
|
30
|
+
/** How to fix it. */
|
|
31
|
+
fix: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** One thing a check found. */
|
|
35
|
+
export interface Finding {
|
|
36
|
+
id: FindingId;
|
|
37
|
+
severity: Severity;
|
|
38
|
+
/** A plain-English sentence. */
|
|
39
|
+
message: string;
|
|
40
|
+
/** Where, as a box on the 512 canvas: `[x0, y0, x1, y1]`. */
|
|
41
|
+
box?: Box;
|
|
42
|
+
/** Where, as a point on the canvas. */
|
|
43
|
+
point?: Point;
|
|
44
|
+
/** The measured number (an area in square units, a width in units). */
|
|
45
|
+
value?: number;
|
|
46
|
+
/** Whether `--fix` repairs it. */
|
|
47
|
+
fixable: boolean;
|
|
48
|
+
/** How to fix it. */
|
|
49
|
+
fix: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Every finding id and what it means. */
|
|
53
|
+
export const FINDINGS: Readonly<Record<FindingId, FindingKind>>;
|
|
54
|
+
|
|
55
|
+
/** A finding, its location rounded to one decimal. */
|
|
56
|
+
export function finding(id: FindingId, message: string, o?: { box?: Box; point?: Point; value?: number; severity?: Severity }): Finding;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module logomarks/checks/finding
|
|
3
|
+
*
|
|
4
|
+
* The findings a check can report, by id, and a constructor that rounds
|
|
5
|
+
* locations for output. Ids are stable: an AI can act on them.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Every finding id: its usual severity, whether `--fix` repairs it, and how
|
|
10
|
+
* to fix it by hand.
|
|
11
|
+
*
|
|
12
|
+
* @type {Record<import('./finding.js').FindingId, import('./finding.js').FindingKind>}
|
|
13
|
+
*/
|
|
14
|
+
export const FINDINGS = {
|
|
15
|
+
empty: { severity: 'error', fixable: false, fix: 'Draw the mark as filled <path> elements.' },
|
|
16
|
+
viewbox: { severity: 'error', fixable: true, fix: 'Use viewBox="0 0 512 512" (--fix refits).' },
|
|
17
|
+
size: { severity: 'error', fixable: true, fix: 'Scale so the longest side is exactly 448 (--fix refits).' },
|
|
18
|
+
centre: { severity: 'error', fixable: true, fix: 'Centre the outline on (256, 256) (--fix refits).' },
|
|
19
|
+
clipped: { severity: 'error', fixable: true, fix: 'Part of the outline is outside 0–512 and is cut off (--fix refits).' },
|
|
20
|
+
markup: { severity: 'error', fixable: false, fix: 'Flatten to filled paths: expand strokes, apply transforms, drop groups, styles, masks, text and images.' },
|
|
21
|
+
fill: { severity: 'warning', fixable: true, fix: 'Fill with currentColor only; holes are real holes, never a painted background (--fix refits).' },
|
|
22
|
+
precision: { severity: 'warning', fixable: true, fix: 'Round coordinates to one decimal place (--fix refits).' },
|
|
23
|
+
speck: { severity: 'error', fixable: false, fix: 'A stray piece a boolean left behind: render it large, and remove it if it is not meant.' },
|
|
24
|
+
sliver: { severity: 'error', fixable: false, fix: 'A thin leftover piece along a cut: remove it, or grow the cut with a true offset.' },
|
|
25
|
+
pinhole: { severity: 'error', fixable: false, fix: 'A tiny hole an overlap left: union the overlapping parts, or remove the hole if it is not meant.' },
|
|
26
|
+
thin: { severity: 'warning', fixable: false, fix: 'Thicken it to at least 10 units, or remove it: it vanishes at 32 px.' },
|
|
27
|
+
'narrow-gap': { severity: 'warning', fixable: false, fix: 'Open the gap to at least 10 units (the house gap is 22): it closes up at 32 px.' },
|
|
28
|
+
'uneven-gaps': { severity: 'warning', fixable: false, fix: 'Use one gap width throughout (22), or a clear 1:2 ratio.' },
|
|
29
|
+
'raster-read': { severity: 'info', fixable: false, fix: 'The vector booleans could not read this mark; small parts were found on a raster, which can miss a speck touching the outline.' },
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* A finding, with its location rounded to one decimal.
|
|
34
|
+
*
|
|
35
|
+
* @param {import('./finding.js').FindingId} id What was found.
|
|
36
|
+
* @param {string} message A plain-English sentence.
|
|
37
|
+
* @param {object} [o] Where and how much.
|
|
38
|
+
* @param {import('@logomarks/core/geometry').Box} [o.box] Where, as a box on the canvas.
|
|
39
|
+
* @param {import('@logomarks/core/geometry').Point} [o.point] Where, as a point.
|
|
40
|
+
* @param {number} [o.value] The measured number (an area, a width).
|
|
41
|
+
* @param {import('./finding.js').Severity} [o.severity] Overrides the usual severity.
|
|
42
|
+
* @returns {import('./finding.js').Finding} The finding.
|
|
43
|
+
*/
|
|
44
|
+
export function finding(id, message, { box, point, value, severity } = {}) {
|
|
45
|
+
const r = (/** @type {number} */ v) => Math.round(v * 10) / 10;
|
|
46
|
+
const kind = FINDINGS[id];
|
|
47
|
+
return {
|
|
48
|
+
id,
|
|
49
|
+
severity: severity ?? kind.severity,
|
|
50
|
+
message,
|
|
51
|
+
...(box && { box: /** @type {import('@logomarks/core/geometry').Box} */ (box.map(r)) }),
|
|
52
|
+
...(point && { point: /** @type {import('@logomarks/core/geometry').Point} */ (point.map(r)) }),
|
|
53
|
+
...(value !== undefined && { value: r(value) }),
|
|
54
|
+
fixable: kind.fixable,
|
|
55
|
+
fix: kind.fix,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { Bounds, FilledPath } from '@logomarks/core/svg';
|
|
2
|
+
import type { Finding } from './finding.js';
|
|
3
|
+
|
|
4
|
+
/** The standard's findings: viewBox, size, centre, clipping, markup, fill colour and precision. */
|
|
5
|
+
export function fitFindings(svg: string, b: Bounds, paths: readonly FilledPath[]): Finding[];
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module logomarks/checks/fit
|
|
3
|
+
*
|
|
4
|
+
* The standard: the canvas, the fit (longest side 448, centred), nothing
|
|
5
|
+
* outside the canvas, filled paths only, `currentColor`, one decimal. From
|
|
6
|
+
* `fit.mjs --check` and `clipcheck.mjs` (measured on the sampled curve, so
|
|
7
|
+
* control points outside the canvas are not false alarms).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { CANVAS, FIT, CENTRE, FIT_TOLERANCE, DECIMALS, FORBIDDEN_MARKUP } from '../rules.js';
|
|
11
|
+
import { viewBox, attr } from '@logomarks/core/svg';
|
|
12
|
+
import { finding } from './finding.js';
|
|
13
|
+
|
|
14
|
+
const f1 = (/** @type {number} */ v) => v.toFixed(1);
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Fit findings for one mark.
|
|
18
|
+
*
|
|
19
|
+
* @param {string} svg The file's text.
|
|
20
|
+
* @param {import('@logomarks/core/svg').Bounds} b Its real outline's box.
|
|
21
|
+
* @param {readonly import('@logomarks/core/svg').FilledPath[]} paths Its shapes.
|
|
22
|
+
* @returns {import('./finding.js').Finding[]} What is off.
|
|
23
|
+
*/
|
|
24
|
+
export function fitFindings(svg, b, paths) {
|
|
25
|
+
const out = [];
|
|
26
|
+
const vb = viewBox(svg);
|
|
27
|
+
const box = /** @type {import('@logomarks/core/geometry').Box} */ ([b.x, b.y, b.x + b.w, b.y + b.h]);
|
|
28
|
+
if (!vb || vb.join(' ') !== `0 0 ${CANVAS} ${CANVAS}`) out.push(finding('viewbox', `The viewBox is "${vb?.join(' ') ?? 'missing'}", not "0 0 ${CANVAS} ${CANVAS}".`));
|
|
29
|
+
const long = Math.max(b.w, b.h);
|
|
30
|
+
if (Math.abs(long - FIT) > FIT_TOLERANCE) out.push(finding('size', `The longest side is ${f1(long)}, not ${FIT}.`, { box, value: long }));
|
|
31
|
+
const cx = b.x + b.w / 2, cy = b.y + b.h / 2;
|
|
32
|
+
if (Math.abs(cx - CENTRE) > FIT_TOLERANCE || Math.abs(cy - CENTRE) > FIT_TOLERANCE) {
|
|
33
|
+
out.push(finding('centre', `The outline is centred on (${f1(cx)}, ${f1(cy)}), not (${CENTRE}, ${CENTRE}).`, { point: [cx, cy] }));
|
|
34
|
+
}
|
|
35
|
+
if (b.x < -FIT_TOLERANCE || b.y < -FIT_TOLERANCE || b.x + b.w > CANVAS + FIT_TOLERANCE || b.y + b.h > CANVAS + FIT_TOLERANCE) {
|
|
36
|
+
out.push(finding('clipped', `The outline runs outside the ${CANVAS} canvas, so the viewBox cuts it off.`, { box }));
|
|
37
|
+
}
|
|
38
|
+
const markup = FORBIDDEN_MARKUP.exec(svg);
|
|
39
|
+
if (markup) out.push(finding('markup', `It contains \`${markup[0]}\`: a finished mark is filled paths only.`));
|
|
40
|
+
const fills = new Set([...svg.matchAll(/<(?:path|circle|ellipse|rect|polygon|svg)\b[^>]*>/g)].map((m) => attr(m[0], 'fill')).filter((v) => v && v !== 'none' && v !== 'currentColor'));
|
|
41
|
+
if (fills.size) out.push(finding('fill', `It is filled with ${[...fills].join(', ')}, not currentColor.`));
|
|
42
|
+
else if (!/fill="currentColor"/.test(svg)) out.push(finding('fill', 'Nothing sets fill="currentColor", so it renders black whatever the text colour.'));
|
|
43
|
+
const long2 = paths.map((p) => p.d).join(' ').match(new RegExp(`\\d\\.\\d{${DECIMALS + 1},}`, 'g'));
|
|
44
|
+
if (long2) out.push(finding('precision', `${long2.length} coordinate${long2.length > 1 ? 's have' : ' has'} more than ${DECIMALS} decimal place (e.g. ${long2[0]}).`, { value: long2.length }));
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { Finding } from './finding.js';
|
|
2
|
+
|
|
3
|
+
export type { Finding, FindingId, Severity } from './finding.js';
|
|
4
|
+
export { FINDINGS } from './finding.js';
|
|
5
|
+
|
|
6
|
+
/** What to check. */
|
|
7
|
+
export interface CheckOptions {
|
|
8
|
+
/** The mark's name, usually its file's slug: seeds the booleans and finds reviewed live marks ('mark'). */
|
|
9
|
+
name?: string;
|
|
10
|
+
/** Finding ids to leave out, e.g. `['speck']` for a mark whose small parts are meant. */
|
|
11
|
+
ignore?: string[];
|
|
12
|
+
/** Warnings fail the check too (false). */
|
|
13
|
+
strict?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** One mark's check. */
|
|
17
|
+
export interface Report {
|
|
18
|
+
name: string;
|
|
19
|
+
/** No errors (and, with `strict`, no warnings). */
|
|
20
|
+
ok: boolean;
|
|
21
|
+
errors: number;
|
|
22
|
+
warnings: number;
|
|
23
|
+
notes: number;
|
|
24
|
+
/** The outline's size, or null when there is nothing drawn. */
|
|
25
|
+
size: { w: number; h: number } | null;
|
|
26
|
+
/** Most serious first. */
|
|
27
|
+
findings: Finding[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Checks one mark against the house rules. */
|
|
31
|
+
export function check(svg: string, o?: CheckOptions): Report;
|
|
32
|
+
/** The mark refitted to the grid, or null when there is nothing safe to fix (no fit findings, or markup a refit would drop). */
|
|
33
|
+
export function fix(svg: string, report?: Report): string | null;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module logomarks/checks
|
|
3
|
+
*
|
|
4
|
+
* `check`: every house rule a mark can be measured against, as findings
|
|
5
|
+
* with ids, severities, plain-English messages and locations. `fix`: the
|
|
6
|
+
* one safe repair, refitting to the grid.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { shapes, bounds } from '@logomarks/core/svg';
|
|
10
|
+
import { fitMark } from '@logomarks/core/fit';
|
|
11
|
+
import { finding } from './finding.js';
|
|
12
|
+
import { fitFindings } from './fit.js';
|
|
13
|
+
import { speckFindings } from './specks.js';
|
|
14
|
+
import { widthFindings } from './widths.js';
|
|
15
|
+
|
|
16
|
+
/** Severities, most serious first, for sorting. */
|
|
17
|
+
const RANK = { error: 0, warning: 1, info: 2 };
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Checks one mark.
|
|
21
|
+
*
|
|
22
|
+
* @param {string} svg The file's text.
|
|
23
|
+
* @param {import('./index.js').CheckOptions} [o] Its name, and what to leave out.
|
|
24
|
+
* @returns {import('./index.js').Report} The findings, most serious first.
|
|
25
|
+
*/
|
|
26
|
+
export function check(svg, { name = 'mark', ignore = [], strict = false } = {}) {
|
|
27
|
+
const paths = shapes(svg);
|
|
28
|
+
/** @type {import('./finding.js').Finding[]} */
|
|
29
|
+
let findings;
|
|
30
|
+
/** @type {import('./index.js').Report['size']} */
|
|
31
|
+
let size = null;
|
|
32
|
+
if (!paths.length) findings = [finding('empty', 'There are no filled shapes to check.')];
|
|
33
|
+
else {
|
|
34
|
+
const b = bounds(paths);
|
|
35
|
+
size = { w: Math.round(b.w * 10) / 10, h: Math.round(b.h * 10) / 10 };
|
|
36
|
+
findings = [...fitFindings(svg, b, paths), ...speckFindings(paths, name), ...widthFindings(paths)];
|
|
37
|
+
}
|
|
38
|
+
findings = findings.filter((f) => !ignore.includes(f.id)).sort((a, b) => RANK[a.severity] - RANK[b.severity]);
|
|
39
|
+
const count = (/** @type {import('./finding.js').Severity} */ s) => findings.filter((f) => f.severity === s).length;
|
|
40
|
+
const errors = count('error'), warnings = count('warning');
|
|
41
|
+
return { name, ok: errors === 0 && (!strict || warnings === 0), errors, warnings, notes: count('info'), size, findings };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Refits a mark when that is all it needs: the fit findings (size, centre,
|
|
46
|
+
* clipping, viewBox, fill, precision) are repaired by `fitMark()`; a mark with
|
|
47
|
+
* groups, transforms or strokes is left alone, since the refit would drop them.
|
|
48
|
+
*
|
|
49
|
+
* @param {string} svg The file's text.
|
|
50
|
+
* @param {import('./index.js').Report} [report] Its report (checked here when not given).
|
|
51
|
+
* @returns {string | null} The fitted file, or null when there is nothing safe to fix.
|
|
52
|
+
*/
|
|
53
|
+
export function fix(svg, report = check(svg)) {
|
|
54
|
+
if (report.findings.some((f) => f.id === 'markup' || f.id === 'empty')) return null;
|
|
55
|
+
if (!report.findings.some((f) => f.fixable)) return null;
|
|
56
|
+
return fitMark(svg).svg;
|
|
57
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { FilledPath } from '@logomarks/core/svg';
|
|
2
|
+
import type { Finding } from './finding.js';
|
|
3
|
+
|
|
4
|
+
/** Specks, slivers and pin-holes, smallest first; `name` (the slug) seeds the jitter and finds reviewed marks. */
|
|
5
|
+
export function speckFindings(paths: readonly FilledPath[], name: string): Finding[];
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module logomarks/checks/specks
|
|
3
|
+
*
|
|
4
|
+
* Stray specks, slivers and pin-holes a boolean left behind (speckcheck.mjs).
|
|
5
|
+
* The mark is read as vectors (its filled area, by booleans); when the
|
|
6
|
+
* booleans can't read it, as a scanline raster at 8 px a unit. Small parts
|
|
7
|
+
* are reported unless they are tiny (two outlines touching), hairline
|
|
8
|
+
* (invisible) or repeated (a dot field). A reviewed live mark's findings come
|
|
9
|
+
* back as notes.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { SPECKS, REVIEWED } from '../rules.js';
|
|
13
|
+
import { markParts, smallParts } from '@logomarks/core/analyse';
|
|
14
|
+
import { finding } from './finding.js';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* A mark's specks, slivers and pin-holes, smallest first.
|
|
18
|
+
*
|
|
19
|
+
* @param {readonly import('@logomarks/core/svg').FilledPath[]} paths The mark's shapes.
|
|
20
|
+
* @param {string} name The mark's name (its slug: seeds the booleans' jitter, and finds a reviewed mark).
|
|
21
|
+
* @returns {import('./finding.js').Finding[]} Specks, slivers and pin-holes, smallest first.
|
|
22
|
+
*/
|
|
23
|
+
export function speckFindings(paths, name) {
|
|
24
|
+
/** @type {import('./finding.js').Finding[]} */
|
|
25
|
+
const out = [];
|
|
26
|
+
const read = markParts(paths, { seed: name });
|
|
27
|
+
if (read.raster) out.push(finding('raster-read', 'The booleans could not read this mark, so small parts were found on a raster (which can miss a speck touching the outline).'));
|
|
28
|
+
const reviewed = REVIEWED[/** @type {keyof typeof REVIEWED} */ (name)];
|
|
29
|
+
for (const p of smallParts(read.parts, { piece: SPECKS.piece, hole: SPECKS.hole, raster: read.raster })) {
|
|
30
|
+
if (p.skip) continue;
|
|
31
|
+
const o = { box: p.box, value: p.area, ...(reviewed && { severity: /** @type {const} */ ('info') }) };
|
|
32
|
+
const note = reviewed ? ` Reviewed and kept: ${reviewed}.` : '';
|
|
33
|
+
if (p.kind === 'hole') out.push(finding('pinhole', `A pin-hole of ${p.area.toFixed(1)} sq units.${note}`, o));
|
|
34
|
+
else if (p.width < SPECKS.sliver) out.push(finding('sliver', `A sliver ${p.width.toFixed(2)} wide (${p.area.toFixed(1)} sq units).${note}`, o));
|
|
35
|
+
else out.push(finding('speck', `A stray piece of ${p.area.toFixed(1)} sq units.${note}`, o));
|
|
36
|
+
}
|
|
37
|
+
return out;
|
|
38
|
+
}
|