react-native-nano-icons 0.1.2 → 0.1.4
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/README.md +20 -164
- package/android/build.gradle +28 -0
- package/android/src/main/java/com/nanoicons/NanoIconView.kt +78 -0
- package/android/src/main/java/com/nanoicons/NanoIconViewManager.kt +84 -0
- package/android/src/main/java/com/nanoicons/NanoIconsPackage.kt +22 -0
- package/ios/NanoIconView.h +4 -0
- package/ios/NanoIconView.mm +286 -0
- package/lib/commonjs/cli/build.js +1 -1
- package/lib/commonjs/cli/config.d.ts +2 -2
- package/lib/commonjs/cli/config.js +7 -6
- package/lib/commonjs/scripts/cli.js +15 -5
- package/lib/commonjs/src/core/font/compile.d.ts +13 -2
- package/lib/commonjs/src/core/font/compile.js +49 -46
- package/lib/commonjs/src/core/pipeline/managers.js +19 -3
- package/lib/commonjs/src/core/pipeline/run.js +121 -32
- package/lib/commonjs/src/core/svg/layers.d.ts +16 -0
- package/lib/commonjs/src/core/svg/layers.js +27 -0
- package/lib/commonjs/src/core/svg/svg_dom.d.ts +29 -1
- package/lib/commonjs/src/core/svg/svg_dom.js +78 -2
- package/lib/commonjs/src/core/svg/svg_pathops.d.ts +11 -0
- package/lib/commonjs/src/core/svg/svg_pathops.js +209 -19
- package/lib/commonjs/src/core/types.d.ts +30 -15
- package/lib/module/core/font/compile.js +52 -41
- package/lib/module/core/font/compile.js.map +1 -1
- package/lib/module/core/pipeline/managers.js +17 -3
- package/lib/module/core/pipeline/managers.js.map +1 -1
- package/lib/module/core/pipeline/run.js +131 -44
- package/lib/module/core/pipeline/run.js.map +1 -1
- package/lib/module/core/shims/picosvg-0.22.3-py3-none-any.whl +0 -0
- package/lib/module/core/svg/layers.js +34 -0
- package/lib/module/core/svg/layers.js.map +1 -1
- package/lib/module/core/svg/svg_dom.js +91 -4
- package/lib/module/core/svg/svg_dom.js.map +1 -1
- package/lib/module/core/svg/svg_pathops.js +203 -19
- package/lib/module/core/svg/svg_pathops.js.map +1 -1
- package/lib/module/createNanoIconsSet.js +3 -79
- package/lib/module/createNanoIconsSet.js.map +1 -1
- package/lib/module/createNanoIconsSet.native.js +108 -0
- package/lib/module/createNanoIconsSet.native.js.map +1 -0
- package/lib/module/createNanoIconsSet.shared.js +91 -0
- package/lib/module/createNanoIconsSet.shared.js.map +1 -0
- package/lib/module/index.js +1 -2
- package/lib/module/index.js.map +1 -1
- package/lib/module/specs/NanoIconViewNativeComponent.ts +15 -0
- package/lib/module/types.js +4 -0
- package/lib/module/types.js.map +1 -0
- package/lib/module/utils/shallowEqualColor.js +15 -0
- package/lib/module/utils/shallowEqualColor.js.map +1 -0
- package/lib/typescript/src/core/font/compile.d.ts +13 -2
- package/lib/typescript/src/core/font/compile.d.ts.map +1 -1
- package/lib/typescript/src/core/pipeline/managers.d.ts.map +1 -1
- package/lib/typescript/src/core/pipeline/run.d.ts.map +1 -1
- package/lib/typescript/src/core/svg/layers.d.ts +16 -0
- package/lib/typescript/src/core/svg/layers.d.ts.map +1 -1
- package/lib/typescript/src/core/svg/svg_dom.d.ts +29 -1
- package/lib/typescript/src/core/svg/svg_dom.d.ts.map +1 -1
- package/lib/typescript/src/core/svg/svg_pathops.d.ts +11 -0
- package/lib/typescript/src/core/svg/svg_pathops.d.ts.map +1 -1
- package/lib/typescript/src/core/types.d.ts +30 -15
- package/lib/typescript/src/core/types.d.ts.map +1 -1
- package/lib/typescript/src/createNanoIconsSet.d.ts +5 -18
- package/lib/typescript/src/createNanoIconsSet.d.ts.map +1 -1
- package/lib/typescript/src/createNanoIconsSet.native.d.ts +7 -0
- package/lib/typescript/src/createNanoIconsSet.native.d.ts.map +1 -0
- package/lib/typescript/src/createNanoIconsSet.shared.d.ts +11 -0
- package/lib/typescript/src/createNanoIconsSet.shared.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/specs/NanoIconViewNativeComponent.d.ts +14 -0
- package/lib/typescript/src/specs/NanoIconViewNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/types.d.ts +19 -0
- package/lib/typescript/src/types.d.ts.map +1 -0
- package/lib/typescript/src/utils/shallowEqualColor.d.ts +4 -0
- package/lib/typescript/src/utils/shallowEqualColor.d.ts.map +1 -0
- package/package.json +22 -5
- package/react-native-nano-icons.podspec +18 -0
- package/scripts/cli.ts +14 -5
- package/src/core/font/compile.ts +65 -61
- package/src/core/pipeline/managers.ts +26 -3
- package/src/core/pipeline/run.ts +156 -38
- package/src/core/shims/picosvg-0.22.3-py3-none-any.whl +0 -0
- package/src/core/svg/layers.ts +44 -0
- package/src/core/svg/svg_dom.ts +96 -4
- package/src/core/svg/svg_pathops.ts +245 -27
- package/src/core/types.ts +21 -10
- package/src/createNanoIconsSet.native.tsx +140 -0
- package/src/createNanoIconsSet.shared.tsx +121 -0
- package/src/createNanoIconsSet.tsx +7 -126
- package/src/index.ts +1 -2
- package/src/specs/NanoIconViewNativeComponent.ts +15 -0
- package/src/types.ts +27 -0
- package/src/utils/shallowEqualColor.ts +17 -0
package/scripts/cli.ts
CHANGED
|
@@ -2,14 +2,16 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Bare React Native workflow: build icon fonts and link them into the native project.
|
|
4
4
|
*
|
|
5
|
-
* Run from your app root: npx react-native-nano-icons [--verbose]
|
|
5
|
+
* Run from your app root: npx react-native-nano-icons [--verbose] [--path <dir>]
|
|
6
6
|
*
|
|
7
7
|
* Reads .nanoicons.json (same shape as Expo plugin options) so Expo and bare apps
|
|
8
8
|
* share one config format.
|
|
9
9
|
*
|
|
10
10
|
* Flags:
|
|
11
|
-
* --verbose
|
|
11
|
+
* --verbose Show per-SVG processing details and pipeline timing
|
|
12
|
+
* --path <dir> Directory containing .nanoicons.json (default: cwd)
|
|
12
13
|
*/
|
|
14
|
+
import path from 'node:path';
|
|
13
15
|
import {
|
|
14
16
|
createOraLogger,
|
|
15
17
|
loadNanoIconsConfig,
|
|
@@ -21,10 +23,17 @@ async function main(): Promise<void> {
|
|
|
21
23
|
const verbose = process.argv.includes('--verbose');
|
|
22
24
|
const level = verbose ? 'verbose' : 'normal';
|
|
23
25
|
|
|
26
|
+
const pathIdx = process.argv.indexOf('--path');
|
|
27
|
+
const projectRoot = process.cwd();
|
|
28
|
+
const configRoot =
|
|
29
|
+
pathIdx !== -1 && process.argv[pathIdx + 1]
|
|
30
|
+
? path.resolve(projectRoot, process.argv[pathIdx + 1]!)
|
|
31
|
+
: projectRoot;
|
|
32
|
+
|
|
24
33
|
const logger = await createOraLogger(level);
|
|
25
|
-
const config = loadNanoIconsConfig(
|
|
26
|
-
const built = await buildAllFonts(config.iconSets,
|
|
27
|
-
await linkBare(
|
|
34
|
+
const config = loadNanoIconsConfig(configRoot);
|
|
35
|
+
const built = await buildAllFonts(config.iconSets, projectRoot, { logger });
|
|
36
|
+
await linkBare(projectRoot, built, logger);
|
|
28
37
|
}
|
|
29
38
|
|
|
30
39
|
main().catch((err: unknown) => {
|
package/src/core/font/compile.ts
CHANGED
|
@@ -1,38 +1,74 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { once } from 'node:events';
|
|
4
|
-
import { type Transform } from 'node:stream';
|
|
5
3
|
|
|
6
4
|
import { forceTtfMetrics } from './metrics.js';
|
|
7
5
|
import svg2ttf from 'svg2ttf';
|
|
8
|
-
import { parseCodepointFromFilename } from '../../utils/parse.js';
|
|
9
6
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const name = path.basename(filename, '.svg');
|
|
7
|
+
export type FontGlyph = {
|
|
8
|
+
codepoint: number;
|
|
9
|
+
advanceWidth: number;
|
|
10
|
+
/** Path data already in font coordinates (Y-up, placement applied). */
|
|
11
|
+
d: string;
|
|
12
|
+
};
|
|
17
13
|
|
|
18
|
-
|
|
19
|
-
|
|
14
|
+
function escapeXml(s: string): string {
|
|
15
|
+
return s.replace(/&/g, '&').replace(/"/g, '"');
|
|
16
|
+
}
|
|
20
17
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
18
|
+
/**
|
|
19
|
+
* Build an SVG font XML string from pre-transformed glyph data.
|
|
20
|
+
*/
|
|
21
|
+
function buildSvgFontXml(opts: {
|
|
22
|
+
fontName: string;
|
|
23
|
+
glyphs: FontGlyph[];
|
|
24
|
+
upm: number;
|
|
25
|
+
ascent: number;
|
|
26
|
+
descent: number;
|
|
27
|
+
}): string {
|
|
28
|
+
const { fontName, glyphs, upm, ascent, descent } = opts;
|
|
25
29
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
glyphStream.on('end', resolve);
|
|
30
|
+
const glyphLines = glyphs.map((g) => {
|
|
31
|
+
const hex = g.codepoint.toString(16);
|
|
32
|
+
const name = `u${hex.padStart(4, '0')}`;
|
|
33
|
+
return `<glyph glyph-name="${name}" unicode="&#x${hex};" horiz-adv-x="${g.advanceWidth}" d="${escapeXml(g.d)}"/>`;
|
|
31
34
|
});
|
|
35
|
+
|
|
36
|
+
return `<?xml version="1.0" standalone="no"?>
|
|
37
|
+
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
|
38
|
+
<svg xmlns="http://www.w3.org/2000/svg">
|
|
39
|
+
<defs>
|
|
40
|
+
<font id="${escapeXml(fontName)}" horiz-adv-x="${upm}">
|
|
41
|
+
<font-face font-family="${escapeXml(fontName)}" units-per-em="${upm}" ascent="${ascent}" descent="${-Math.abs(descent)}"/>
|
|
42
|
+
<missing-glyph horiz-adv-x="0"/>
|
|
43
|
+
${glyphLines.join('\n')}
|
|
44
|
+
</font>
|
|
45
|
+
</defs>
|
|
46
|
+
</svg>`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function parseCompileTtfFromGlyphsError(
|
|
50
|
+
err: unknown,
|
|
51
|
+
codepointToIcon: Map<number, string>
|
|
52
|
+
) {
|
|
53
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
54
|
+
const cpMatch = msg.match(/glyph\s+"u([0-9a-fA-F]+)"/);
|
|
55
|
+
if (cpMatch) {
|
|
56
|
+
const cp = parseInt(cpMatch[1]!, 16);
|
|
57
|
+
const iconName = codepointToIcon.get(cp);
|
|
58
|
+
const detail = iconName
|
|
59
|
+
? `icon "${iconName}" (codepoint u${cpMatch[1]})`
|
|
60
|
+
: `codepoint u${cpMatch[1]}`;
|
|
61
|
+
throw new Error(`Font compilation failed for ${detail}: ${msg}`);
|
|
62
|
+
}
|
|
63
|
+
throw err;
|
|
32
64
|
}
|
|
33
65
|
|
|
34
|
-
|
|
35
|
-
|
|
66
|
+
/**
|
|
67
|
+
* Compile a TTF font from pre-transformed glyph data.
|
|
68
|
+
* Builds SVG font XML directly (no intermediate files), then converts via svg2ttf.
|
|
69
|
+
*/
|
|
70
|
+
export async function compileTtfFromGlyphs(opts: {
|
|
71
|
+
glyphs: FontGlyph[];
|
|
36
72
|
outTtfPath: string;
|
|
37
73
|
fontName: string;
|
|
38
74
|
upm: number;
|
|
@@ -40,52 +76,20 @@ export async function compileTtfFromGlyphSVGs(opts: {
|
|
|
40
76
|
descent: number;
|
|
41
77
|
lineGap?: number;
|
|
42
78
|
}): Promise<void> {
|
|
43
|
-
const {
|
|
44
|
-
|
|
45
|
-
const { glyphDir, outTtfPath, fontName, upm, ascent, descent } = opts;
|
|
79
|
+
const { glyphs, outTtfPath, fontName, upm, ascent, descent } = opts;
|
|
46
80
|
const lineGap = opts.lineGap ?? 0;
|
|
47
81
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
.filter((f) => /^u[0-9a-fA-F]+\.svg$/.test(f))
|
|
51
|
-
.sort(
|
|
52
|
-
(a, b) => parseCodepointFromFilename(a) - parseCodepointFromFilename(b)
|
|
53
|
-
);
|
|
54
|
-
|
|
55
|
-
if (files.length === 0)
|
|
56
|
-
throw new Error(`No glyph SVGs found in: ${glyphDir}`);
|
|
82
|
+
if (glyphs.length === 0)
|
|
83
|
+
throw new Error('No glyphs to compile');
|
|
57
84
|
|
|
58
|
-
const
|
|
85
|
+
const svgFontString = buildSvgFontXml({
|
|
59
86
|
fontName,
|
|
60
|
-
|
|
61
|
-
|
|
87
|
+
glyphs,
|
|
88
|
+
upm,
|
|
62
89
|
ascent,
|
|
63
90
|
descent,
|
|
64
91
|
});
|
|
65
92
|
|
|
66
|
-
const svgFontChunks: Buffer[] = [];
|
|
67
|
-
fontStream.on('data', (c: Buffer | string) =>
|
|
68
|
-
svgFontChunks.push(Buffer.isBuffer(c) ? c : Buffer.from(c))
|
|
69
|
-
);
|
|
70
|
-
|
|
71
|
-
// Single error listener for the font stream; per-glyph errors are handled in writeGlyphStreamToFont.
|
|
72
|
-
let fontStreamReject: (err: Error) => void;
|
|
73
|
-
const fontStreamErrorPromise = new Promise<never>((_, rej) => {
|
|
74
|
-
fontStreamReject = rej;
|
|
75
|
-
});
|
|
76
|
-
fontStream.on('error', (err: Error) => fontStreamReject(err));
|
|
77
|
-
|
|
78
|
-
for (const f of files) {
|
|
79
|
-
await Promise.race([
|
|
80
|
-
writeGlyphStreamToFont(fontStream, path.join(glyphDir, f), f),
|
|
81
|
-
fontStreamErrorPromise,
|
|
82
|
-
]);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
fontStream.end();
|
|
86
|
-
await once(fontStream, 'end');
|
|
87
|
-
|
|
88
|
-
const svgFontString = Buffer.concat(svgFontChunks).toString('utf8');
|
|
89
93
|
const ttfRaw = svg2ttf(svgFontString);
|
|
90
94
|
const rawBuf = Buffer.from(ttfRaw.buffer);
|
|
91
95
|
|
|
@@ -73,14 +73,37 @@ export class PyodideManager {
|
|
|
73
73
|
|
|
74
74
|
await py.loadPackage(['micropip', 'lxml'], { messageCallback: () => {} });
|
|
75
75
|
|
|
76
|
+
// Resolve local picosvg wheel path for offline-first installation.
|
|
77
|
+
const picosvgWhlDir = path.join(
|
|
78
|
+
getPackageRoot(),
|
|
79
|
+
'src',
|
|
80
|
+
'core',
|
|
81
|
+
'shims'
|
|
82
|
+
);
|
|
83
|
+
const picosvgWhl = (await fs.readdir(picosvgWhlDir))
|
|
84
|
+
.find((f) => f.startsWith('picosvg-') && f.endsWith('.whl'));
|
|
85
|
+
const localWhlUrl = picosvgWhl
|
|
86
|
+
? `file://${path.join(picosvgWhlDir, picosvgWhl)}`
|
|
87
|
+
: null;
|
|
88
|
+
|
|
89
|
+
py.globals.set('_picosvg_local_whl', localWhlUrl);
|
|
90
|
+
|
|
76
91
|
await py.runPythonAsync(`
|
|
77
92
|
import sys
|
|
78
93
|
if "/" not in sys.path:
|
|
79
94
|
sys.path.insert(0, "/")
|
|
80
|
-
|
|
95
|
+
|
|
81
96
|
import micropip
|
|
82
|
-
|
|
83
|
-
|
|
97
|
+
|
|
98
|
+
_whl = _picosvg_local_whl
|
|
99
|
+
if _whl:
|
|
100
|
+
try:
|
|
101
|
+
await micropip.install(_whl, deps=False)
|
|
102
|
+
except Exception:
|
|
103
|
+
await micropip.install("picosvg", deps=False)
|
|
104
|
+
else:
|
|
105
|
+
await micropip.install("picosvg", deps=False)
|
|
106
|
+
|
|
84
107
|
import pathops
|
|
85
108
|
import picosvg
|
|
86
109
|
`);
|
package/src/core/pipeline/run.ts
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
1
|
import fsp from 'node:fs/promises';
|
|
3
2
|
import path from 'node:path';
|
|
4
3
|
|
|
5
|
-
import {
|
|
6
|
-
|
|
4
|
+
import {
|
|
5
|
+
compileTtfFromGlyphs,
|
|
6
|
+
parseCompileTtfFromGlyphsError,
|
|
7
|
+
type FontGlyph,
|
|
8
|
+
} from '../font/compile.js';
|
|
9
|
+
import { picoFromFile, PathKitManager } from './managers.js';
|
|
7
10
|
|
|
8
11
|
import {
|
|
9
12
|
ensureDir,
|
|
10
|
-
ensureEmptyDir,
|
|
11
13
|
type PipelineConfig,
|
|
12
14
|
type PipelinePaths,
|
|
13
15
|
} from './config.js';
|
|
@@ -16,9 +18,15 @@ import {
|
|
|
16
18
|
preprocessSvg,
|
|
17
19
|
shouldSkipPath,
|
|
18
20
|
validateSvg,
|
|
21
|
+
extractOriginalEvenoddDs,
|
|
22
|
+
restoreOriginalEvenoddDs,
|
|
19
23
|
} from '../svg/svg_dom.js';
|
|
20
|
-
import { computePlacement,
|
|
21
|
-
import
|
|
24
|
+
import { computePlacement, transformPathForFont } from '../svg/layers.js';
|
|
25
|
+
import { convertEvenoddToWinding } from '../svg/svg_pathops.js';
|
|
26
|
+
import type {
|
|
27
|
+
GlyphLayer,
|
|
28
|
+
NanoGlyphMap,
|
|
29
|
+
} from '../types.js';
|
|
22
30
|
import type { NanoLogger } from '../types.js';
|
|
23
31
|
|
|
24
32
|
export type PipelineResult = {
|
|
@@ -26,6 +34,78 @@ export type PipelineResult = {
|
|
|
26
34
|
glyphmapPath: string;
|
|
27
35
|
};
|
|
28
36
|
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
// Same-color path merging
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
type ParsedPath = { d: string; fill: string | null; fillRule?: 'evenodd'; noMerge?: boolean };
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Concatenate multiple SVG path `d` strings into a single compound path.
|
|
45
|
+
* This preserves the exact geometry of each path (no boolean operations)
|
|
46
|
+
* while combining them into one glyph. Under nonzero winding, this renders
|
|
47
|
+
* identically to drawing each path separately with the same color.
|
|
48
|
+
*/
|
|
49
|
+
function concatPathDs(ds: string[]): string | null {
|
|
50
|
+
if (ds.length === 0) return null;
|
|
51
|
+
if (ds.length === 1) return ds[0]!;
|
|
52
|
+
return ds.join(' ');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Merge consecutive same-color paths into single compound paths via boolean UNION.
|
|
57
|
+
* Preserves z-order: only merges runs of adjacent paths with identical fill color.
|
|
58
|
+
*/
|
|
59
|
+
function mergeSameColorPaths(
|
|
60
|
+
paths: ParsedPath[],
|
|
61
|
+
logger?: NanoLogger
|
|
62
|
+
): ParsedPath[] {
|
|
63
|
+
if (paths.length <= 1) return paths;
|
|
64
|
+
|
|
65
|
+
const result: ParsedPath[] = [];
|
|
66
|
+
let i = 0;
|
|
67
|
+
|
|
68
|
+
while (i < paths.length) {
|
|
69
|
+
const fill = paths[i]!.fill;
|
|
70
|
+
|
|
71
|
+
// Find consecutive run of same fill that are all mergeable.
|
|
72
|
+
// Paths converted from evenodd have compound hole structure and must not
|
|
73
|
+
// be merged — their CW hole contours would cancel CCW contours from
|
|
74
|
+
// adjacent paths, producing incorrect fill.
|
|
75
|
+
let j = i + 1;
|
|
76
|
+
if (!paths[i]!.noMerge) {
|
|
77
|
+
while (
|
|
78
|
+
j < paths.length &&
|
|
79
|
+
paths[j]!.fill === fill &&
|
|
80
|
+
!paths[j]!.noMerge
|
|
81
|
+
) {
|
|
82
|
+
j++;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (j - i === 1) {
|
|
87
|
+
result.push(paths[i]!);
|
|
88
|
+
} else {
|
|
89
|
+
const group = paths.slice(i, j);
|
|
90
|
+
const merged = concatPathDs(group.map((p) => p.d));
|
|
91
|
+
if (merged) {
|
|
92
|
+
logger?.info(
|
|
93
|
+
` ⊕ Merged ${group.length} same-color paths (fill=${fill})`
|
|
94
|
+
);
|
|
95
|
+
result.push({ d: merged, fill });
|
|
96
|
+
} else {
|
|
97
|
+
result.push(...group);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
i = j;
|
|
101
|
+
}
|
|
102
|
+
return result;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
// Pipeline
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
|
|
29
109
|
/**
|
|
30
110
|
* Run the font pipeline with given config and paths.
|
|
31
111
|
* Uses the singleton Pyodide/PathKit instance (initialized on first call).
|
|
@@ -40,7 +120,6 @@ export async function runPipeline(
|
|
|
40
120
|
|
|
41
121
|
logger?.update(`Building "${config.fontFamily}"…`);
|
|
42
122
|
|
|
43
|
-
ensureEmptyDir(paths.tempDir);
|
|
44
123
|
ensureDir(paths.outputDir);
|
|
45
124
|
|
|
46
125
|
const files = (await fsp.readdir(paths.inputDir)).filter((f) =>
|
|
@@ -48,16 +127,20 @@ export async function runPipeline(
|
|
|
48
127
|
);
|
|
49
128
|
|
|
50
129
|
const glyphMap: NanoGlyphMap = {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
130
|
+
m: {
|
|
131
|
+
f: config.fontFamily,
|
|
132
|
+
u: config.upm,
|
|
133
|
+
z: config.safeZone,
|
|
134
|
+
s: config.startUnicode,
|
|
56
135
|
},
|
|
57
|
-
|
|
136
|
+
i: {},
|
|
58
137
|
};
|
|
59
138
|
|
|
60
139
|
let currentUnicode = config.startUnicode;
|
|
140
|
+
const codepointToIcon = new Map<number, string>();
|
|
141
|
+
const allGlyphs: FontGlyph[] = [];
|
|
142
|
+
|
|
143
|
+
const PathKit = await PathKitManager.getInstance();
|
|
61
144
|
|
|
62
145
|
for (const file of files) {
|
|
63
146
|
const iconName = path.parse(file).name;
|
|
@@ -76,8 +159,40 @@ export async function runPipeline(
|
|
|
76
159
|
}
|
|
77
160
|
|
|
78
161
|
const preprocessed = preprocessSvg(rawContent);
|
|
162
|
+
|
|
163
|
+
// Preserve original evenodd `d` strings BEFORE picosvg processes them.
|
|
164
|
+
// Picosvg's simplify (via our PathKit shim) can drop contours from
|
|
165
|
+
// multi-subpath evenodd paths — we restore the originals after.
|
|
166
|
+
const originalEvenoddDs = extractOriginalEvenoddDs(preprocessed);
|
|
167
|
+
|
|
79
168
|
const flattenedSvg = await picoFromFile(filePath, preprocessed);
|
|
80
|
-
const parsed = parseFlattenedSvg(flattenedSvg
|
|
169
|
+
const parsed = parseFlattenedSvg(flattenedSvg, {
|
|
170
|
+
onSanitize: (original) => {
|
|
171
|
+
logger?.info(
|
|
172
|
+
` ⚠ Sanitized path in "${file}": path was missing initial moveto (prepended M from endpoint)`
|
|
173
|
+
);
|
|
174
|
+
logger?.info(` Original: ${original.slice(0, 80)}…`);
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// Restore original evenodd path data (undamaged by picosvg's simplify),
|
|
179
|
+
// then convert to nonzero winding with our containment-based algorithm.
|
|
180
|
+
// Mark as noMerge — compound paths with holes must stay separate so their
|
|
181
|
+
// CW hole contours don't cancel adjacent paths' CCW contours.
|
|
182
|
+
if (originalEvenoddDs.length > 0) {
|
|
183
|
+
restoreOriginalEvenoddDs(parsed.paths, originalEvenoddDs);
|
|
184
|
+
}
|
|
185
|
+
for (const p of parsed.paths) {
|
|
186
|
+
if (p.fillRule === 'evenodd') {
|
|
187
|
+
logger?.info(` ↻ Converting evenodd path to nonzero winding in "${file}"`);
|
|
188
|
+
p.d = convertEvenoddToWinding(PathKit, p.d);
|
|
189
|
+
delete p.fillRule;
|
|
190
|
+
(p as ParsedPath).noMerge = true;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Merge consecutive same-color paths into single compound glyphs
|
|
195
|
+
const mergedPaths = mergeSameColorPaths(parsed.paths, logger);
|
|
81
196
|
|
|
82
197
|
const { vx, vy, scale, xOff, yOff, adv } = computePlacement({
|
|
83
198
|
upm: config.upm,
|
|
@@ -85,31 +200,34 @@ export async function runPipeline(
|
|
|
85
200
|
viewBox: parsed.viewBox,
|
|
86
201
|
});
|
|
87
202
|
|
|
88
|
-
const
|
|
203
|
+
const layers: GlyphLayer[] = [];
|
|
89
204
|
|
|
90
|
-
for (const p of
|
|
205
|
+
for (const p of mergedPaths) {
|
|
91
206
|
if (shouldSkipPath(p.d, p.fill)) continue;
|
|
92
207
|
|
|
93
208
|
const cp = currentUnicode++;
|
|
209
|
+
codepointToIcon.set(cp, iconName);
|
|
94
210
|
|
|
95
|
-
|
|
96
|
-
tempDir: paths.tempDir,
|
|
97
|
-
upm: config.upm,
|
|
98
|
-
adv,
|
|
211
|
+
const fontD = transformPathForFont(PathKit, p.d, {
|
|
99
212
|
vx,
|
|
100
213
|
vy,
|
|
101
214
|
scale,
|
|
102
215
|
xOff,
|
|
103
216
|
yOff,
|
|
104
|
-
|
|
217
|
+
upm: config.upm,
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
allGlyphs.push({
|
|
105
221
|
codepoint: cp,
|
|
222
|
+
advanceWidth: adv,
|
|
223
|
+
d: fontD,
|
|
106
224
|
});
|
|
107
225
|
|
|
108
|
-
|
|
226
|
+
layers.push([cp, p.fill || 'black']);
|
|
109
227
|
}
|
|
110
228
|
|
|
111
|
-
if (
|
|
112
|
-
glyphMap.
|
|
229
|
+
if (layers.length > 0) {
|
|
230
|
+
glyphMap.i[iconName] = [adv, layers];
|
|
113
231
|
}
|
|
114
232
|
}
|
|
115
233
|
|
|
@@ -119,27 +237,27 @@ export async function runPipeline(
|
|
|
119
237
|
);
|
|
120
238
|
|
|
121
239
|
if (options?.inputHash) {
|
|
122
|
-
glyphMap.
|
|
240
|
+
glyphMap.m.h = options.inputHash;
|
|
123
241
|
}
|
|
124
|
-
await fsp.writeFile(glyphmapPath, JSON.stringify(glyphMap
|
|
242
|
+
await fsp.writeFile(glyphmapPath, JSON.stringify(glyphMap), 'utf8');
|
|
125
243
|
|
|
126
244
|
logger?.info(`Compiling TTF…`);
|
|
127
245
|
const ttfPath = path.join(paths.outputDir, `${config.fontFamily}.ttf`);
|
|
128
246
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
247
|
+
try {
|
|
248
|
+
await compileTtfFromGlyphs({
|
|
249
|
+
glyphs: allGlyphs,
|
|
250
|
+
outTtfPath: ttfPath,
|
|
251
|
+
fontName: config.fontFamily,
|
|
252
|
+
upm: config.upm,
|
|
253
|
+
ascent: config.upm,
|
|
254
|
+
descent: 0,
|
|
255
|
+
});
|
|
256
|
+
} catch (err: unknown) {
|
|
257
|
+
parseCompileTtfFromGlyphsError(err, codepointToIcon);
|
|
140
258
|
}
|
|
141
259
|
|
|
142
|
-
const iconCount = Object.keys(glyphMap.
|
|
260
|
+
const iconCount = Object.keys(glyphMap.i).length;
|
|
143
261
|
const elapsed = Date.now() - startTime;
|
|
144
262
|
logger?.succeed(
|
|
145
263
|
`Built ${config.fontFamily}.ttf [${iconCount} icon${
|
|
Binary file
|
package/src/core/svg/layers.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import fsp from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
|
|
4
|
+
import type { PathKitModule } from '../types.js';
|
|
5
|
+
|
|
4
6
|
function roundInt(n: number): number {
|
|
5
7
|
return Math.round(n);
|
|
6
8
|
}
|
|
@@ -64,3 +66,45 @@ export async function writeLayerSvg(opts: {
|
|
|
64
66
|
|
|
65
67
|
await fsp.writeFile(path.join(opts.tempDir, `u${hex}.svg`), layerSvg, 'utf8');
|
|
66
68
|
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Transform an SVG path `d` string from source SVG coordinates into font
|
|
72
|
+
* glyph coordinates (Y-up, with placement scaling and centering applied).
|
|
73
|
+
*
|
|
74
|
+
* Combines placement transform (translate + scale + translate) with the
|
|
75
|
+
* SVG→font Y-axis flip into a single affine transform applied via PathKit.
|
|
76
|
+
*/
|
|
77
|
+
export function transformPathForFont(
|
|
78
|
+
PathKit: PathKitModule,
|
|
79
|
+
d: string,
|
|
80
|
+
opts: {
|
|
81
|
+
vx: number;
|
|
82
|
+
vy: number;
|
|
83
|
+
scale: number;
|
|
84
|
+
xOff: number;
|
|
85
|
+
yOff: number;
|
|
86
|
+
upm: number;
|
|
87
|
+
}
|
|
88
|
+
): string {
|
|
89
|
+
const { vx, vy, scale, xOff, yOff, upm } = opts;
|
|
90
|
+
|
|
91
|
+
const p = PathKit.FromSVGString(d);
|
|
92
|
+
if (!p) return d;
|
|
93
|
+
|
|
94
|
+
// Combined affine: placement + Y-flip for font coordinates.
|
|
95
|
+
// x' = scale * (x - vx) + xOff
|
|
96
|
+
// y' = upm - (scale * (y - vy) + yOff)
|
|
97
|
+
//
|
|
98
|
+
// SkMatrix row-major: [scaleX, skewX, transX, skewY, scaleY, transY, 0,0,1]
|
|
99
|
+
const scaleX = scale;
|
|
100
|
+
const scaleY = -scale;
|
|
101
|
+
const transX = xOff - vx * scale;
|
|
102
|
+
const transY = upm - yOff + vy * scale;
|
|
103
|
+
|
|
104
|
+
p.transform(scaleX, 0, transX, 0, scaleY, transY, 0, 0, 1);
|
|
105
|
+
|
|
106
|
+
const result = p.toSVGString();
|
|
107
|
+
p.delete?.();
|
|
108
|
+
|
|
109
|
+
return result;
|
|
110
|
+
}
|