react-native-nano-icons 0.1.1 → 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 (99) hide show
  1. package/README.md +20 -164
  2. package/android/build.gradle +28 -0
  3. package/android/src/main/java/com/nanoicons/NanoIconView.kt +78 -0
  4. package/android/src/main/java/com/nanoicons/NanoIconViewManager.kt +84 -0
  5. package/android/src/main/java/com/nanoicons/NanoIconsPackage.kt +22 -0
  6. package/ios/NanoIconView.h +4 -0
  7. package/ios/NanoIconView.mm +246 -0
  8. package/lib/commonjs/cli/build.js +1 -1
  9. package/lib/commonjs/cli/config.d.ts +2 -2
  10. package/lib/commonjs/cli/config.js +7 -6
  11. package/lib/commonjs/plugin/src/buildFonts.d.ts +1 -0
  12. package/lib/commonjs/plugin/src/buildFonts.js +9 -0
  13. package/lib/commonjs/plugin/src/index.js +1 -34
  14. package/lib/commonjs/plugin/src/withNanoIconsFontLinking.d.ts +6 -6
  15. package/lib/commonjs/plugin/src/withNanoIconsFontLinking.js +11 -15
  16. package/lib/commonjs/scripts/cli.js +15 -5
  17. package/lib/commonjs/src/core/font/compile.d.ts +13 -2
  18. package/lib/commonjs/src/core/font/compile.js +49 -46
  19. package/lib/commonjs/src/core/pipeline/managers.js +19 -3
  20. package/lib/commonjs/src/core/pipeline/run.js +121 -32
  21. package/lib/commonjs/src/core/svg/layers.d.ts +16 -0
  22. package/lib/commonjs/src/core/svg/layers.js +27 -0
  23. package/lib/commonjs/src/core/svg/svg_dom.d.ts +29 -1
  24. package/lib/commonjs/src/core/svg/svg_dom.js +78 -2
  25. package/lib/commonjs/src/core/svg/svg_pathops.d.ts +11 -0
  26. package/lib/commonjs/src/core/svg/svg_pathops.js +209 -19
  27. package/lib/commonjs/src/core/types.d.ts +30 -15
  28. package/lib/module/core/font/compile.js +52 -41
  29. package/lib/module/core/font/compile.js.map +1 -1
  30. package/lib/module/core/pipeline/managers.js +17 -3
  31. package/lib/module/core/pipeline/managers.js.map +1 -1
  32. package/lib/module/core/pipeline/run.js +131 -44
  33. package/lib/module/core/pipeline/run.js.map +1 -1
  34. package/lib/module/core/shims/picosvg-0.22.3-py3-none-any.whl +0 -0
  35. package/lib/module/core/svg/layers.js +34 -0
  36. package/lib/module/core/svg/layers.js.map +1 -1
  37. package/lib/module/core/svg/svg_dom.js +91 -4
  38. package/lib/module/core/svg/svg_dom.js.map +1 -1
  39. package/lib/module/core/svg/svg_pathops.js +203 -19
  40. package/lib/module/core/svg/svg_pathops.js.map +1 -1
  41. package/lib/module/createNanoIconsSet.js +3 -79
  42. package/lib/module/createNanoIconsSet.js.map +1 -1
  43. package/lib/module/createNanoIconsSet.native.js +68 -0
  44. package/lib/module/createNanoIconsSet.native.js.map +1 -0
  45. package/lib/module/createNanoIconsSet.shared.js +91 -0
  46. package/lib/module/createNanoIconsSet.shared.js.map +1 -0
  47. package/lib/module/index.js +1 -2
  48. package/lib/module/index.js.map +1 -1
  49. package/lib/module/specs/NanoIconViewNativeComponent.ts +15 -0
  50. package/lib/module/types.js +4 -0
  51. package/lib/module/types.js.map +1 -0
  52. package/lib/module/utils/shallowEqualColor.js +15 -0
  53. package/lib/module/utils/shallowEqualColor.js.map +1 -0
  54. package/lib/typescript/src/core/font/compile.d.ts +13 -2
  55. package/lib/typescript/src/core/font/compile.d.ts.map +1 -1
  56. package/lib/typescript/src/core/pipeline/managers.d.ts.map +1 -1
  57. package/lib/typescript/src/core/pipeline/run.d.ts.map +1 -1
  58. package/lib/typescript/src/core/svg/layers.d.ts +16 -0
  59. package/lib/typescript/src/core/svg/layers.d.ts.map +1 -1
  60. package/lib/typescript/src/core/svg/svg_dom.d.ts +29 -1
  61. package/lib/typescript/src/core/svg/svg_dom.d.ts.map +1 -1
  62. package/lib/typescript/src/core/svg/svg_pathops.d.ts +11 -0
  63. package/lib/typescript/src/core/svg/svg_pathops.d.ts.map +1 -1
  64. package/lib/typescript/src/core/types.d.ts +30 -15
  65. package/lib/typescript/src/core/types.d.ts.map +1 -1
  66. package/lib/typescript/src/createNanoIconsSet.d.ts +5 -18
  67. package/lib/typescript/src/createNanoIconsSet.d.ts.map +1 -1
  68. package/lib/typescript/src/createNanoIconsSet.native.d.ts +7 -0
  69. package/lib/typescript/src/createNanoIconsSet.native.d.ts.map +1 -0
  70. package/lib/typescript/src/createNanoIconsSet.shared.d.ts +11 -0
  71. package/lib/typescript/src/createNanoIconsSet.shared.d.ts.map +1 -0
  72. package/lib/typescript/src/index.d.ts.map +1 -1
  73. package/lib/typescript/src/specs/NanoIconViewNativeComponent.d.ts +14 -0
  74. package/lib/typescript/src/specs/NanoIconViewNativeComponent.d.ts.map +1 -0
  75. package/lib/typescript/src/types.d.ts +19 -0
  76. package/lib/typescript/src/types.d.ts.map +1 -0
  77. package/lib/typescript/src/utils/shallowEqualColor.d.ts +4 -0
  78. package/lib/typescript/src/utils/shallowEqualColor.d.ts.map +1 -0
  79. package/package.json +22 -5
  80. package/plugin/src/buildFonts.ts +13 -0
  81. package/plugin/src/index.ts +3 -50
  82. package/plugin/src/withNanoIconsFontLinking.ts +22 -24
  83. package/react-native-nano-icons.podspec +18 -0
  84. package/scripts/cli.ts +14 -5
  85. package/src/core/font/compile.ts +65 -61
  86. package/src/core/pipeline/managers.ts +26 -3
  87. package/src/core/pipeline/run.ts +156 -38
  88. package/src/core/shims/picosvg-0.22.3-py3-none-any.whl +0 -0
  89. package/src/core/svg/layers.ts +44 -0
  90. package/src/core/svg/svg_dom.ts +96 -4
  91. package/src/core/svg/svg_pathops.ts +245 -27
  92. package/src/core/types.ts +21 -10
  93. package/src/createNanoIconsSet.native.tsx +108 -0
  94. package/src/createNanoIconsSet.shared.tsx +121 -0
  95. package/src/createNanoIconsSet.tsx +7 -126
  96. package/src/index.ts +1 -2
  97. package/src/specs/NanoIconViewNativeComponent.ts +15 -0
  98. package/src/types.ts +27 -0
  99. package/src/utils/shallowEqualColor.ts +17 -0
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.computePlacement = computePlacement;
7
7
  exports.writeLayerSvg = writeLayerSvg;
8
+ exports.transformPathForFont = transformPathForFont;
8
9
  const promises_1 = __importDefault(require("node:fs/promises"));
9
10
  const node_path_1 = __importDefault(require("node:path"));
10
11
  function roundInt(n) {
@@ -40,3 +41,29 @@ async function writeLayerSvg(opts) {
40
41
  </svg>`.trim();
41
42
  await promises_1.default.writeFile(node_path_1.default.join(opts.tempDir, `u${hex}.svg`), layerSvg, 'utf8');
42
43
  }
44
+ /**
45
+ * Transform an SVG path `d` string from source SVG coordinates into font
46
+ * glyph coordinates (Y-up, with placement scaling and centering applied).
47
+ *
48
+ * Combines placement transform (translate + scale + translate) with the
49
+ * SVG→font Y-axis flip into a single affine transform applied via PathKit.
50
+ */
51
+ function transformPathForFont(PathKit, d, opts) {
52
+ const { vx, vy, scale, xOff, yOff, upm } = opts;
53
+ const p = PathKit.FromSVGString(d);
54
+ if (!p)
55
+ return d;
56
+ // Combined affine: placement + Y-flip for font coordinates.
57
+ // x' = scale * (x - vx) + xOff
58
+ // y' = upm - (scale * (y - vy) + yOff)
59
+ //
60
+ // SkMatrix row-major: [scaleX, skewX, transX, skewY, scaleY, transY, 0,0,1]
61
+ const scaleX = scale;
62
+ const scaleY = -scale;
63
+ const transX = xOff - vx * scale;
64
+ const transY = upm - yOff + vy * scale;
65
+ p.transform(scaleX, 0, transX, 0, scaleY, transY, 0, 0, 1);
66
+ const result = p.toSVGString();
67
+ p.delete?.();
68
+ return result;
69
+ }
@@ -3,14 +3,27 @@ export type ParsedFlatSvg = {
3
3
  paths: Array<{
4
4
  d: string;
5
5
  fill: string | null;
6
+ fillRule?: 'evenodd';
6
7
  }>;
7
8
  };
8
9
  export declare function calculateOpColor(fill: string | null, opacity: number, el: Element): `rgba(${number},${number},${number},${number})`;
10
+ /**
11
+ * If a flattened path lost its initial moveto (e.g. picosvg dropped an empty
12
+ * `Mx y z` subpath), prepend `M` using the path's last coordinate pair.
13
+ * For closed icon shapes the endpoint equals the start point.
14
+ */
15
+ export declare function sanitizePathData(d: string): {
16
+ d: string;
17
+ sanitized: boolean;
18
+ };
9
19
  export declare const parsePath: (p: Element) => {
10
20
  d: string;
11
21
  fill: string | null;
22
+ fillRule?: "evenodd";
12
23
  };
13
- export declare function parseFlattenedSvg(flattenedSvg: string): ParsedFlatSvg;
24
+ export declare function parseFlattenedSvg(flattenedSvg: string, options?: {
25
+ onSanitize?: (original: string) => void;
26
+ }): ParsedFlatSvg;
14
27
  export declare function shouldSkipPath(d: string, fill: string | null): boolean;
15
28
  export type SvgValidation = {
16
29
  valid: true;
@@ -19,4 +32,19 @@ export type SvgValidation = {
19
32
  reason: string;
20
33
  };
21
34
  export declare function validateSvg(content: string): SvgValidation;
35
+ /**
36
+ * Extract the original `d` strings of evenodd paths from the raw SVG
37
+ * BEFORE picosvg processes it. Picosvg's simplify (via our PathKit shim)
38
+ * can drop contours from multi-subpath evenodd paths, so we preserve
39
+ * the originals and apply our own winding conversion later.
40
+ *
41
+ * Returns one `d` string per evenodd path, in document order.
42
+ */
43
+ export declare function extractOriginalEvenoddDs(svgContent: string): string[];
44
+ /**
45
+ * Replace picosvg's (potentially damaged) evenodd path data with the
46
+ * preserved originals. Matches by position: the Nth evenodd path in
47
+ * the parsed output gets the Nth original `d` string.
48
+ */
49
+ export declare function restoreOriginalEvenoddDs(paths: ParsedFlatSvg['paths'], originalDs: string[]): void;
22
50
  export declare function preprocessSvg(content: string): string;
@@ -2,9 +2,12 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.parsePath = void 0;
4
4
  exports.calculateOpColor = calculateOpColor;
5
+ exports.sanitizePathData = sanitizePathData;
5
6
  exports.parseFlattenedSvg = parseFlattenedSvg;
6
7
  exports.shouldSkipPath = shouldSkipPath;
7
8
  exports.validateSvg = validateSvg;
9
+ exports.extractOriginalEvenoddDs = extractOriginalEvenoddDs;
10
+ exports.restoreOriginalEvenoddDs = restoreOriginalEvenoddDs;
8
11
  exports.preprocessSvg = preprocessSvg;
9
12
  const jsdom_1 = require("jsdom");
10
13
  const parse_1 = require("../../utils/parse");
@@ -26,11 +29,36 @@ function calculateOpColor(fill, opacity, el) {
26
29
  const finalAlpha = +(a * opacity).toFixed(4);
27
30
  return `rgba(${r},${g},${b},${finalAlpha})`;
28
31
  }
32
+ /**
33
+ * If a flattened path lost its initial moveto (e.g. picosvg dropped an empty
34
+ * `Mx y z` subpath), prepend `M` using the path's last coordinate pair.
35
+ * For closed icon shapes the endpoint equals the start point.
36
+ */
37
+ function sanitizePathData(d) {
38
+ const trimmed = d.trim();
39
+ if (!trimmed || /^[Mm]/.test(trimmed)) {
40
+ return { d: trimmed, sanitized: false };
41
+ }
42
+ // Strip trailing close commands, then grab the last two numbers as x,y
43
+ const withoutClose = trimmed.replace(/[Zz]\s*$/, '');
44
+ const nums = withoutClose.match(/-?\d+(?:\.\d+)?/g);
45
+ if (!nums || nums.length < 2) {
46
+ return { d: trimmed, sanitized: false };
47
+ }
48
+ const x = nums[nums.length - 2];
49
+ const y = nums[nums.length - 1];
50
+ return { d: `M${x},${y} ${trimmed}`, sanitized: true };
51
+ }
29
52
  const parsePath = (p) => {
30
53
  const d = p.getAttribute('d') ?? '';
31
54
  const op = p.getAttribute('opacity');
32
55
  const fillOp = p.getAttribute('fill-opacity');
33
56
  const fill = p.getAttribute('fill');
57
+ // picosvg may drop fill-rule but preserve clip-rule; treat either as evenodd
58
+ const fillRule = p.getAttribute('fill-rule') === 'evenodd' ||
59
+ p.getAttribute('clip-rule') === 'evenodd'
60
+ ? 'evenodd'
61
+ : undefined;
34
62
  if (op !== null || fillOp !== null) {
35
63
  const opVal = op !== null ? parseFloat(op) : 1;
36
64
  const fillOpVal = fillOp !== null ? parseFloat(fillOp) : 1;
@@ -38,15 +66,17 @@ const parsePath = (p) => {
38
66
  return {
39
67
  d,
40
68
  fill: calculateOpColor(fill, combinedOpacity, p),
69
+ fillRule,
41
70
  };
42
71
  }
43
72
  return {
44
73
  d,
45
74
  fill,
75
+ fillRule,
46
76
  };
47
77
  };
48
78
  exports.parsePath = parsePath;
49
- function parseFlattenedSvg(flattenedSvg) {
79
+ function parseFlattenedSvg(flattenedSvg, options) {
50
80
  const dom = new jsdom_1.JSDOM(flattenedSvg);
51
81
  const doc = dom.window.document;
52
82
  const svgEl = doc.querySelector('svg');
@@ -58,7 +88,16 @@ function parseFlattenedSvg(flattenedSvg) {
58
88
  ? [viewBoxRaw[0], viewBoxRaw[1], viewBoxRaw[2], viewBoxRaw[3]]
59
89
  : [0, 0, 100, 100];
60
90
  const pathEls = Array.from(doc.querySelectorAll('path'));
61
- const paths = pathEls.map(exports.parsePath).filter((p) => p.d.trim() !== '');
91
+ const paths = pathEls
92
+ .map(exports.parsePath)
93
+ .filter((p) => p.d.trim() !== '')
94
+ .map((p) => {
95
+ const { d, sanitized } = sanitizePathData(p.d);
96
+ if (sanitized) {
97
+ options?.onSanitize?.(p.d);
98
+ }
99
+ return { ...p, d };
100
+ });
62
101
  return { viewBox, paths };
63
102
  }
64
103
  function shouldSkipPath(d, fill) {
@@ -76,6 +115,43 @@ function validateSvg(content) {
76
115
  }
77
116
  return { valid: true };
78
117
  }
118
+ /**
119
+ * Extract the original `d` strings of evenodd paths from the raw SVG
120
+ * BEFORE picosvg processes it. Picosvg's simplify (via our PathKit shim)
121
+ * can drop contours from multi-subpath evenodd paths, so we preserve
122
+ * the originals and apply our own winding conversion later.
123
+ *
124
+ * Returns one `d` string per evenodd path, in document order.
125
+ */
126
+ function extractOriginalEvenoddDs(svgContent) {
127
+ if (!/<[^>]*fill-rule\s*=\s*["']evenodd/i.test(svgContent)) {
128
+ return [];
129
+ }
130
+ const dom = new jsdom_1.JSDOM(svgContent, { contentType: 'image/svg+xml' });
131
+ const doc = dom.window.document;
132
+ const results = [];
133
+ const pathEls = doc.querySelectorAll('path[fill-rule="evenodd"], path[clip-rule="evenodd"]');
134
+ for (const el of pathEls) {
135
+ const d = el.getAttribute('d');
136
+ if (d)
137
+ results.push(d);
138
+ }
139
+ return results;
140
+ }
141
+ /**
142
+ * Replace picosvg's (potentially damaged) evenodd path data with the
143
+ * preserved originals. Matches by position: the Nth evenodd path in
144
+ * the parsed output gets the Nth original `d` string.
145
+ */
146
+ function restoreOriginalEvenoddDs(paths, originalDs) {
147
+ let oi = 0;
148
+ for (const p of paths) {
149
+ if (p.fillRule === 'evenodd' && oi < originalDs.length) {
150
+ p.d = originalDs[oi];
151
+ oi++;
152
+ }
153
+ }
154
+ }
79
155
  // ensure the svg has a xmlns attribute
80
156
  function preprocessSvg(content) {
81
157
  if (/xmlns\s*=/.test(content))
@@ -1,4 +1,15 @@
1
1
  import type { PathKitModule, WrappedPath, Point } from '../types.js';
2
+ /**
3
+ * Convert a path `d` string with evenodd fill semantics to an equivalent
4
+ * path that renders identically under nonzero winding.
5
+ *
6
+ * Steps:
7
+ * 1. Parse via PathKit, set fill type to EVENODD, simplify (resolve topology)
8
+ * 2. Split into contours, compute containment depths
9
+ * 3. Fix winding: even depth = CCW (outer), odd depth = CW (hole)
10
+ * 4. Reconstruct d string
11
+ */
12
+ export declare function convertEvenoddToWinding(PathKit: PathKitModule, d: string): string;
2
13
  export declare function buildPathopsBackend(PathKit: PathKitModule): {
3
14
  create_path(fillTypeInt: number): WrappedPath;
4
15
  clone_path(h: WrappedPath): WrappedPath;
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.convertEvenoddToWinding = convertEvenoddToWinding;
3
4
  exports.buildPathopsBackend = buildPathopsBackend;
4
5
  //** */
5
6
  // ----- numeric helpers -----
@@ -381,9 +382,11 @@ function bestStartMinYMinX(contourCmds, VERB) {
381
382
  }
382
383
  return best;
383
384
  }
384
- // proxy to for picosvg to interatc with pathkit
385
- function buildPathopsBackend(PathKit) {
386
- const VERB = {
385
+ // ---------------------------------------------------------------------------
386
+ // Containment helpers (module-level for reuse by fixPathWinding)
387
+ // ---------------------------------------------------------------------------
388
+ function buildVerbMap(PathKit) {
389
+ return {
387
390
  MOVE: PathKit.MOVE_VERB ?? 0,
388
391
  LINE: PathKit.LINE_VERB ?? 1,
389
392
  QUAD: PathKit.QUAD_VERB ?? 2,
@@ -391,6 +394,206 @@ function buildPathopsBackend(PathKit) {
391
394
  CUBIC: PathKit.CUBIC_VERB ?? 4,
392
395
  CLOSE: PathKit.CLOSE_VERB ?? 5,
393
396
  };
397
+ }
398
+ /**
399
+ * Convert contour commands to a polyline by sampling curves.
400
+ * Used for ray-casting containment tests.
401
+ */
402
+ function contourToPolyline(contourCmds, V, steps = 8) {
403
+ let cx = 0, cy = 0;
404
+ let sx = 0, sy = 0;
405
+ const pts = [];
406
+ for (const cmd of contourCmds) {
407
+ const v = cmd[0];
408
+ if (v === V.MOVE) {
409
+ cx = cmd[1];
410
+ cy = cmd[2];
411
+ sx = cx;
412
+ sy = cy;
413
+ pts.push([cx, cy]);
414
+ }
415
+ else if (v === V.LINE) {
416
+ cx = cmd[1];
417
+ cy = cmd[2];
418
+ pts.push([cx, cy]);
419
+ }
420
+ else if (v === V.QUAD) {
421
+ const x0 = cx, y0 = cy;
422
+ const x1 = cmd[1], y1 = cmd[2];
423
+ const x2 = cmd[3], y2 = cmd[4];
424
+ for (let i = 1; i <= steps; i++) {
425
+ const t = i / steps;
426
+ const mt = 1 - t;
427
+ pts.push([mt * mt * x0 + 2 * mt * t * x1 + t * t * x2, mt * mt * y0 + 2 * mt * t * y1 + t * t * y2]);
428
+ }
429
+ cx = x2;
430
+ cy = y2;
431
+ }
432
+ else if (v === V.CUBIC) {
433
+ const x0 = cx, y0 = cy;
434
+ const x1 = cmd[1], y1 = cmd[2];
435
+ const x2 = cmd[3], y2 = cmd[4];
436
+ const x3 = cmd[5], y3 = cmd[6];
437
+ for (let i = 1; i <= steps; i++) {
438
+ const t = i / steps;
439
+ const mt = 1 - t;
440
+ pts.push([
441
+ mt * mt * mt * x0 + 3 * mt * mt * t * x1 + 3 * mt * t * t * x2 + t * t * t * x3,
442
+ mt * mt * mt * y0 + 3 * mt * mt * t * y1 + 3 * mt * t * t * y2 + t * t * t * y3,
443
+ ]);
444
+ }
445
+ cx = x3;
446
+ cy = y3;
447
+ }
448
+ else if (v === V.CLOSE) {
449
+ if (cx !== sx || cy !== sy)
450
+ pts.push([sx, sy]);
451
+ cx = sx;
452
+ cy = sy;
453
+ }
454
+ }
455
+ return pts;
456
+ }
457
+ /**
458
+ * Ray-casting point-in-polygon test.
459
+ */
460
+ function pointInPolygon(px, py, poly) {
461
+ let inside = false;
462
+ for (let i = 0, j = poly.length - 1; i < poly.length; j = i++) {
463
+ const [xi, yi] = poly[i];
464
+ const [xj, yj] = poly[j];
465
+ if (yi > py !== yj > py &&
466
+ px < ((xj - xi) * (py - yi)) / (yj - yi) + xi) {
467
+ inside = !inside;
468
+ }
469
+ }
470
+ return inside;
471
+ }
472
+ /**
473
+ * Get a representative point on the contour boundary (midpoint of first segment).
474
+ */
475
+ function getContourSamplePoint(contourCmds, V) {
476
+ let cx = 0, cy = 0;
477
+ for (const cmd of contourCmds) {
478
+ const v = cmd[0];
479
+ if (v === V.MOVE) {
480
+ cx = cmd[1];
481
+ cy = cmd[2];
482
+ }
483
+ else if (v === V.LINE) {
484
+ return [(cx + cmd[1]) / 2, (cy + cmd[2]) / 2];
485
+ }
486
+ else if (v === V.QUAD) {
487
+ const t = 0.5, mt = 0.5;
488
+ return [
489
+ mt * mt * cx + 2 * mt * t * cmd[1] + t * t * cmd[3],
490
+ mt * mt * cy + 2 * mt * t * cmd[2] + t * t * cmd[4],
491
+ ];
492
+ }
493
+ else if (v === V.CUBIC) {
494
+ const t = 0.5, mt = 0.5;
495
+ return [
496
+ mt ** 3 * cx + 3 * mt ** 2 * t * cmd[1] + 3 * mt * t ** 2 * cmd[3] + t ** 3 * cmd[5],
497
+ mt ** 3 * cy + 3 * mt ** 2 * t * cmd[2] + 3 * mt * t ** 2 * cmd[4] + t ** 3 * cmd[6],
498
+ ];
499
+ }
500
+ }
501
+ return null;
502
+ }
503
+ /**
504
+ * Apply containment-based winding fix to contour objects.
505
+ * Even nesting depth = CCW (outer), odd = CW (hole).
506
+ */
507
+ function applyContainmentWinding(contourObjs, V) {
508
+ const n = contourObjs.length;
509
+ if (n === 0)
510
+ return;
511
+ const polylines = contourObjs.map((obj) => contourToPolyline(obj.cmds, V));
512
+ const samplePts = contourObjs.map((obj) => getContourSamplePoint(obj.cmds, V));
513
+ const depths = new Array(n).fill(0);
514
+ for (let i = 0; i < n; i++) {
515
+ const pt = samplePts[i];
516
+ if (!pt)
517
+ continue;
518
+ for (let j = 0; j < n; j++) {
519
+ if (i === j)
520
+ continue;
521
+ if (pointInPolygon(pt[0], pt[1], polylines[j])) {
522
+ depths[i] = (depths[i] ?? 0) + 1;
523
+ }
524
+ }
525
+ }
526
+ const ensureOrient = (obj, wantCCW) => {
527
+ const a = approxSignedAreaFromContourCmds(obj.cmds, V);
528
+ const isCCW = a > 0;
529
+ if (wantCCW !== isCCW) {
530
+ obj.cmds = reverseClosedContourKeepStart(obj.cmds, obj.explicitCloseWanted, V);
531
+ }
532
+ };
533
+ for (let i = 0; i < n; i++) {
534
+ ensureOrient(contourObjs[i], depths[i] % 2 === 0);
535
+ }
536
+ }
537
+ /**
538
+ * Convert a path `d` string with evenodd fill semantics to an equivalent
539
+ * path that renders identically under nonzero winding.
540
+ *
541
+ * Steps:
542
+ * 1. Parse via PathKit, set fill type to EVENODD, simplify (resolve topology)
543
+ * 2. Split into contours, compute containment depths
544
+ * 3. Fix winding: even depth = CCW (outer), odd depth = CW (hole)
545
+ * 4. Reconstruct d string
546
+ */
547
+ function convertEvenoddToWinding(PathKit, d) {
548
+ const V = buildVerbMap(PathKit);
549
+ // 1. Parse and simplify with EVENODD fill type
550
+ const p = PathKit.FromSVGString(d);
551
+ if (!p)
552
+ return d;
553
+ const FILL_EVENODD = PathKit?.FillType?.EVENODD ?? PathKit?.FillType?.EVEN_ODD ?? 1;
554
+ p.setFillType(FILL_EVENODD);
555
+ p.simplify();
556
+ // Get the simplified SVG string and re-parse for command access
557
+ const simplified = p.toSVGString();
558
+ p.delete?.();
559
+ const p2 = PathKit.FromSVGString(simplified);
560
+ if (!p2)
561
+ return simplified;
562
+ const cmds = p2.toCmds();
563
+ p2.delete?.();
564
+ if (cmds.length === 0)
565
+ return simplified;
566
+ // 2. Split into contours
567
+ const contourObjs = splitContours(cmds, V).map((c) => {
568
+ const explicitCloseWanted = explicitCloseWantedFromCmds(c, V);
569
+ const cc = ensureClosed(c, V);
570
+ const a = approxSignedAreaFromContourCmds(cc, V);
571
+ return { cmds: cc, absA: Math.abs(a), explicitCloseWanted };
572
+ });
573
+ contourObjs.sort((x, y) => y.absA - x.absA);
574
+ // 3. Fix winding via containment analysis
575
+ applyContainmentWinding(contourObjs, V);
576
+ // 4. Reconstruct d string from fixed commands
577
+ const allCmds = contourObjs.flatMap((x) => x.cmds);
578
+ const parts = [];
579
+ for (const cmd of allCmds) {
580
+ const v = cmd[0];
581
+ if (v === V.MOVE)
582
+ parts.push(`M${roundN(cmd[1])} ${roundN(cmd[2])}`);
583
+ else if (v === V.LINE)
584
+ parts.push(`L${roundN(cmd[1])} ${roundN(cmd[2])}`);
585
+ else if (v === V.QUAD)
586
+ parts.push(`Q${roundN(cmd[1])} ${roundN(cmd[2])} ${roundN(cmd[3])} ${roundN(cmd[4])}`);
587
+ else if (v === V.CUBIC)
588
+ parts.push(`C${roundN(cmd[1])} ${roundN(cmd[2])} ${roundN(cmd[3])} ${roundN(cmd[4])} ${roundN(cmd[5])} ${roundN(cmd[6])}`);
589
+ else if (v === V.CLOSE)
590
+ parts.push('Z');
591
+ }
592
+ return parts.join(' ');
593
+ }
594
+ // proxy to for picosvg to interatc with pathkit
595
+ function buildPathopsBackend(PathKit) {
596
+ const VERB = buildVerbMap(PathKit);
394
597
  const FILL_EVENODD = PathKit?.FillType?.EVENODD ?? PathKit?.FillType?.EVEN_ODD ?? 1;
395
598
  const FILL_WINDING = PathKit?.FillType?.WINDING ?? PathKit?.FillType?.NONZERO ?? 0;
396
599
  function cmdsViaSvgRoundtrip(h) {
@@ -413,22 +616,9 @@ function buildPathopsBackend(PathKit) {
413
616
  });
414
617
  // Big-to-small ordering gives deterministic outer→inner ordering.
415
618
  contourObjs.sort((x, y) => y.absA - x.absA);
416
- // Enforce winding convention:
417
- // - largest contour CCW (positive area)
418
- // - all subsequent contours CW (negative area)
419
- const ensureOrient = (obj, wantCCW) => {
420
- const a = approxSignedAreaFromContourCmds(obj.cmds, VERB);
421
- const isCCW = a > 0;
422
- if (wantCCW !== isCCW) {
423
- obj.cmds = reverseClosedContourKeepStart(obj.cmds, obj.explicitCloseWanted, VERB);
424
- }
425
- };
426
- if (contourObjs.length) {
427
- ensureOrient(contourObjs[0], true);
428
- for (let i = 1; i < contourObjs.length; i++) {
429
- ensureOrient(contourObjs[i], false);
430
- }
431
- }
619
+ // Containment-based winding: compute nesting depth per contour via
620
+ // ray-casting point-in-polygon. Even depth = outer (CCW), odd = hole (CW).
621
+ applyContainmentWinding(contourObjs, VERB);
432
622
  // Rotate starts deterministically:
433
623
  // 1) If we have recorded MOVE points, try to rotate contour to a move point that lies on it.
434
624
  // 2) Otherwise, rotate to minY/minX point (helps stroke-ish paths)
@@ -99,22 +99,37 @@ export type PyodideModule = {
99
99
  get: (key: string) => unknown;
100
100
  };
101
101
  };
102
- export type GlyphLayer = {
103
- codepoint: number;
104
- color: string;
105
- };
106
- export type GlyphEntry = {
107
- adv: number;
108
- layers: GlyphLayer[];
109
- };
102
+ export type GlyphLayer = [codepoint: number, color: string];
103
+ export type GlyphEntry = [adv: number, layers: GlyphLayer[]];
110
104
  export type IconsMap = Record<string, GlyphEntry>;
105
+ /**
106
+ * m - metadata,
107
+ * f - font family,
108
+ * u - units per em,
109
+ * z - safe zone,
110
+ * s - start unicode,
111
+ * h - hash,
112
+ * i - icons,
113
+ * adv - advance width,
114
+ */
111
115
  export type NanoGlyphMap = {
112
- meta: {
113
- fontFamily: string;
114
- upm: number;
115
- safeZone: number;
116
- startUnicode: number;
117
- hash?: string;
116
+ m: {
117
+ f: string;
118
+ u: number;
119
+ z: number;
120
+ s: number;
121
+ h?: string;
122
+ };
123
+ i: IconsMap;
124
+ };
125
+ /** Accepts JSON-inferred types where arrays aren't tuples. */
126
+ export type NanoGlyphMapInput = {
127
+ m: {
128
+ f: string;
129
+ u: number;
130
+ z: number;
131
+ s: number;
132
+ h?: string;
118
133
  };
119
- icons: IconsMap;
134
+ i: Record<string, readonly unknown[]>;
120
135
  };
@@ -2,32 +2,59 @@
2
2
 
3
3
  import fs from 'node:fs';
4
4
  import path from 'node:path';
5
- import { once } from 'node:events';
6
5
  import { forceTtfMetrics } from './metrics.js';
7
6
  import svg2ttf from 'svg2ttf';
8
- import { parseCodepointFromFilename } from '../../utils/parse.js';
9
- async function writeGlyphStreamToFont(fontStream, svgPath, filename) {
10
- const codepoint = parseCodepointFromFilename(filename);
11
- const name = path.basename(filename, '.svg');
12
- return new Promise((resolve, reject) => {
13
- const glyphStream = fs.createReadStream(svgPath);
14
- glyphStream.metadata = {
15
- name,
16
- unicode: [String.fromCodePoint(codepoint)]
17
- };
18
- glyphStream.on('error', reject);
19
- // Do not add fontStream.on("error", reject) here — one per glyph would exceed
20
- // Node's default MaxListeners (10). Font stream errors are handled once below.
21
- fontStream.write(glyphStream);
22
- glyphStream.on('end', resolve);
23
- });
7
+ function escapeXml(s) {
8
+ return s.replace(/&/g, '&amp;').replace(/"/g, '&quot;');
24
9
  }
25
- export async function compileTtfFromGlyphSVGs(opts) {
10
+
11
+ /**
12
+ * Build an SVG font XML string from pre-transformed glyph data.
13
+ */
14
+ function buildSvgFontXml(opts) {
26
15
  const {
27
- SVGIcons2SVGFontStream
28
- } = await import('svgicons2svgfont');
16
+ fontName,
17
+ glyphs,
18
+ upm,
19
+ ascent,
20
+ descent
21
+ } = opts;
22
+ const glyphLines = glyphs.map(g => {
23
+ const hex = g.codepoint.toString(16);
24
+ const name = `u${hex.padStart(4, '0')}`;
25
+ return `<glyph glyph-name="${name}" unicode="&#x${hex};" horiz-adv-x="${g.advanceWidth}" d="${escapeXml(g.d)}"/>`;
26
+ });
27
+ return `<?xml version="1.0" standalone="no"?>
28
+ <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
29
+ <svg xmlns="http://www.w3.org/2000/svg">
30
+ <defs>
31
+ <font id="${escapeXml(fontName)}" horiz-adv-x="${upm}">
32
+ <font-face font-family="${escapeXml(fontName)}" units-per-em="${upm}" ascent="${ascent}" descent="${-Math.abs(descent)}"/>
33
+ <missing-glyph horiz-adv-x="0"/>
34
+ ${glyphLines.join('\n')}
35
+ </font>
36
+ </defs>
37
+ </svg>`;
38
+ }
39
+ export function parseCompileTtfFromGlyphsError(err, codepointToIcon) {
40
+ const msg = err instanceof Error ? err.message : String(err);
41
+ const cpMatch = msg.match(/glyph\s+"u([0-9a-fA-F]+)"/);
42
+ if (cpMatch) {
43
+ const cp = parseInt(cpMatch[1], 16);
44
+ const iconName = codepointToIcon.get(cp);
45
+ const detail = iconName ? `icon "${iconName}" (codepoint u${cpMatch[1]})` : `codepoint u${cpMatch[1]}`;
46
+ throw new Error(`Font compilation failed for ${detail}: ${msg}`);
47
+ }
48
+ throw err;
49
+ }
50
+
51
+ /**
52
+ * Compile a TTF font from pre-transformed glyph data.
53
+ * Builds SVG font XML directly (no intermediate files), then converts via svg2ttf.
54
+ */
55
+ export async function compileTtfFromGlyphs(opts) {
29
56
  const {
30
- glyphDir,
57
+ glyphs,
31
58
  outTtfPath,
32
59
  fontName,
33
60
  upm,
@@ -35,30 +62,14 @@ export async function compileTtfFromGlyphSVGs(opts) {
35
62
  descent
36
63
  } = opts;
37
64
  const lineGap = opts.lineGap ?? 0;
38
- const files = fs.readdirSync(glyphDir).filter(f => /^u[0-9a-fA-F]+\.svg$/.test(f)).sort((a, b) => parseCodepointFromFilename(a) - parseCodepointFromFilename(b));
39
- if (files.length === 0) throw new Error(`No glyph SVGs found in: ${glyphDir}`);
40
- const fontStream = new SVGIcons2SVGFontStream({
65
+ if (glyphs.length === 0) throw new Error('No glyphs to compile');
66
+ const svgFontString = buildSvgFontXml({
41
67
  fontName,
42
- fontHeight: upm,
43
- normalize: false,
68
+ glyphs,
69
+ upm,
44
70
  ascent,
45
71
  descent
46
72
  });
47
- const svgFontChunks = [];
48
- fontStream.on('data', c => svgFontChunks.push(Buffer.isBuffer(c) ? c : Buffer.from(c)));
49
-
50
- // Single error listener for the font stream; per-glyph errors are handled in writeGlyphStreamToFont.
51
- let fontStreamReject;
52
- const fontStreamErrorPromise = new Promise((_, rej) => {
53
- fontStreamReject = rej;
54
- });
55
- fontStream.on('error', err => fontStreamReject(err));
56
- for (const f of files) {
57
- await Promise.race([writeGlyphStreamToFont(fontStream, path.join(glyphDir, f), f), fontStreamErrorPromise]);
58
- }
59
- fontStream.end();
60
- await once(fontStream, 'end');
61
- const svgFontString = Buffer.concat(svgFontChunks).toString('utf8');
62
73
  const ttfRaw = svg2ttf(svgFontString);
63
74
  const rawBuf = Buffer.from(ttfRaw.buffer);
64
75
  const fixedBuf = forceTtfMetrics(rawBuf, upm, ascent, descent, lineGap);