react-native-nano-icons 0.0.0 → 0.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.
Files changed (159) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +174 -0
  3. package/app.plugin.js +1 -0
  4. package/lib/commonjs/cli/build.d.ts +28 -0
  5. package/lib/commonjs/cli/build.js +83 -0
  6. package/lib/commonjs/cli/config.d.ts +9 -0
  7. package/lib/commonjs/cli/config.js +25 -0
  8. package/lib/commonjs/cli/index.d.ts +4 -0
  9. package/lib/commonjs/cli/index.js +13 -0
  10. package/lib/commonjs/cli/link.d.ts +11 -0
  11. package/lib/commonjs/cli/link.js +135 -0
  12. package/lib/commonjs/cli/logger.d.ts +27 -0
  13. package/lib/commonjs/cli/logger.js +83 -0
  14. package/lib/commonjs/index.node.js +8 -0
  15. package/lib/commonjs/plugin/src/buildFonts.d.ts +7 -0
  16. package/lib/commonjs/plugin/src/buildFonts.js +23 -0
  17. package/lib/commonjs/plugin/src/index.d.ts +5 -0
  18. package/lib/commonjs/plugin/src/index.js +43 -0
  19. package/lib/commonjs/plugin/src/types.d.ts +31 -0
  20. package/lib/commonjs/plugin/src/types.js +2 -0
  21. package/lib/commonjs/plugin/src/withNanoIconsFontLinking.d.ts +14 -0
  22. package/lib/commonjs/plugin/src/withNanoIconsFontLinking.js +92 -0
  23. package/lib/commonjs/scripts/cli.js +28 -0
  24. package/lib/commonjs/src/const/colors.d.ts +1 -0
  25. package/lib/commonjs/src/const/colors.js +153 -0
  26. package/lib/commonjs/src/core/font/compile.d.ts +9 -0
  27. package/lib/commonjs/src/core/font/compile.js +68 -0
  28. package/lib/commonjs/src/core/font/metrics.d.ts +1 -0
  29. package/lib/commonjs/src/core/font/metrics.js +33 -0
  30. package/lib/commonjs/src/core/pipeline/config.d.ts +14 -0
  31. package/lib/commonjs/src/core/pipeline/config.js +17 -0
  32. package/lib/commonjs/src/core/pipeline/index.d.ts +3 -0
  33. package/lib/commonjs/src/core/pipeline/index.js +7 -0
  34. package/lib/commonjs/src/core/pipeline/managers.d.ts +11 -0
  35. package/lib/commonjs/src/core/pipeline/managers.js +89 -0
  36. package/lib/commonjs/src/core/pipeline/run.d.ts +14 -0
  37. package/lib/commonjs/src/core/pipeline/run.js +99 -0
  38. package/lib/commonjs/src/core/svg/layers.d.ts +24 -0
  39. package/lib/commonjs/src/core/svg/layers.js +42 -0
  40. package/lib/commonjs/src/core/svg/svg_dom.d.ts +22 -0
  41. package/lib/commonjs/src/core/svg/svg_dom.js +84 -0
  42. package/lib/commonjs/src/core/svg/svg_pathops.d.ts +21 -0
  43. package/lib/commonjs/src/core/svg/svg_pathops.js +611 -0
  44. package/lib/commonjs/src/core/types.d.ts +120 -0
  45. package/lib/commonjs/src/core/types.js +2 -0
  46. package/lib/commonjs/src/utils/fingerPrint.d.ts +1 -0
  47. package/lib/commonjs/src/utils/fingerPrint.js +20 -0
  48. package/lib/commonjs/src/utils/parse.d.ts +2 -0
  49. package/lib/commonjs/src/utils/parse.js +64 -0
  50. package/lib/module/const/colors.js +153 -0
  51. package/lib/module/const/colors.js.map +1 -0
  52. package/lib/module/core/font/compile.js +70 -0
  53. package/lib/module/core/font/compile.js.map +1 -0
  54. package/lib/module/core/font/metrics.js +37 -0
  55. package/lib/module/core/font/metrics.js.map +1 -0
  56. package/lib/module/core/pipeline/config.js +20 -0
  57. package/lib/module/core/pipeline/config.js.map +1 -0
  58. package/lib/module/core/pipeline/index.js +5 -0
  59. package/lib/module/core/pipeline/index.js.map +1 -0
  60. package/lib/module/core/pipeline/managers.js +80 -0
  61. package/lib/module/core/pipeline/managers.js.map +1 -0
  62. package/lib/module/core/pipeline/run.js +114 -0
  63. package/lib/module/core/pipeline/run.js.map +1 -0
  64. package/lib/module/core/shims/pathops.py +181 -0
  65. package/lib/module/core/svg/layers.js +51 -0
  66. package/lib/module/core/svg/layers.js.map +1 -0
  67. package/lib/module/core/svg/svg_dom.js +83 -0
  68. package/lib/module/core/svg/svg_dom.js.map +1 -0
  69. package/lib/module/core/svg/svg_pathops.js +566 -0
  70. package/lib/module/core/svg/svg_pathops.js.map +1 -0
  71. package/lib/module/core/tsconfig.json +32 -0
  72. package/lib/module/core/types.js +2 -0
  73. package/lib/module/core/types.js.map +1 -0
  74. package/lib/module/createNanoIconsSet.js +84 -0
  75. package/lib/module/createNanoIconsSet.js.map +1 -0
  76. package/lib/module/index.js +5 -0
  77. package/lib/module/index.js.map +1 -0
  78. package/lib/module/index.node.js +13 -0
  79. package/lib/module/index.node.js.map +1 -0
  80. package/lib/module/package.json +1 -0
  81. package/lib/module/utils/fingerPrint.js +17 -0
  82. package/lib/module/utils/fingerPrint.js.map +1 -0
  83. package/lib/module/utils/parse.js +53 -0
  84. package/lib/module/utils/parse.js.map +1 -0
  85. package/lib/typescript/__tests__/build.unit.test.d.ts +3 -0
  86. package/lib/typescript/__tests__/build.unit.test.d.ts.map +1 -0
  87. package/lib/typescript/__tests__/clippath.e2e.test.d.ts +3 -0
  88. package/lib/typescript/__tests__/clippath.e2e.test.d.ts.map +1 -0
  89. package/lib/typescript/__tests__/fingerprint.unit.test.d.ts +3 -0
  90. package/lib/typescript/__tests__/fingerprint.unit.test.d.ts.map +1 -0
  91. package/lib/typescript/__tests__/pipeline.e2e.test.d.ts +3 -0
  92. package/lib/typescript/__tests__/pipeline.e2e.test.d.ts.map +1 -0
  93. package/lib/typescript/__tests__/placement.unit.test.d.ts +3 -0
  94. package/lib/typescript/__tests__/placement.unit.test.d.ts.map +1 -0
  95. package/lib/typescript/__tests__/svg_dom.unit.test.d.ts +3 -0
  96. package/lib/typescript/__tests__/svg_dom.unit.test.d.ts.map +1 -0
  97. package/lib/typescript/cli/build.d.ts +29 -0
  98. package/lib/typescript/cli/build.d.ts.map +1 -0
  99. package/lib/typescript/cli/logger.d.ts +28 -0
  100. package/lib/typescript/cli/logger.d.ts.map +1 -0
  101. package/lib/typescript/package.json +1 -0
  102. package/lib/typescript/src/const/colors.d.ts +2 -0
  103. package/lib/typescript/src/const/colors.d.ts.map +1 -0
  104. package/lib/typescript/src/core/font/compile.d.ts +10 -0
  105. package/lib/typescript/src/core/font/compile.d.ts.map +1 -0
  106. package/lib/typescript/src/core/font/metrics.d.ts +2 -0
  107. package/lib/typescript/src/core/font/metrics.d.ts.map +1 -0
  108. package/lib/typescript/src/core/pipeline/config.d.ts +15 -0
  109. package/lib/typescript/src/core/pipeline/config.d.ts.map +1 -0
  110. package/lib/typescript/src/core/pipeline/index.d.ts +4 -0
  111. package/lib/typescript/src/core/pipeline/index.d.ts.map +1 -0
  112. package/lib/typescript/src/core/pipeline/managers.d.ts +12 -0
  113. package/lib/typescript/src/core/pipeline/managers.d.ts.map +1 -0
  114. package/lib/typescript/src/core/pipeline/run.d.ts +15 -0
  115. package/lib/typescript/src/core/pipeline/run.d.ts.map +1 -0
  116. package/lib/typescript/src/core/svg/layers.d.ts +25 -0
  117. package/lib/typescript/src/core/svg/layers.d.ts.map +1 -0
  118. package/lib/typescript/src/core/svg/svg_dom.d.ts +23 -0
  119. package/lib/typescript/src/core/svg/svg_dom.d.ts.map +1 -0
  120. package/lib/typescript/src/core/svg/svg_pathops.d.ts +22 -0
  121. package/lib/typescript/src/core/svg/svg_pathops.d.ts.map +1 -0
  122. package/lib/typescript/src/core/types.d.ts +121 -0
  123. package/lib/typescript/src/core/types.d.ts.map +1 -0
  124. package/lib/typescript/src/createNanoIconsSet.d.ts +19 -0
  125. package/lib/typescript/src/createNanoIconsSet.d.ts.map +1 -0
  126. package/lib/typescript/src/index.d.ts +3 -0
  127. package/lib/typescript/src/index.d.ts.map +1 -0
  128. package/lib/typescript/src/index.node.d.ts +2 -0
  129. package/lib/typescript/src/index.node.d.ts.map +1 -0
  130. package/lib/typescript/src/utils/fingerPrint.d.ts +2 -0
  131. package/lib/typescript/src/utils/fingerPrint.d.ts.map +1 -0
  132. package/lib/typescript/src/utils/parse.d.ts +3 -0
  133. package/lib/typescript/src/utils/parse.d.ts.map +1 -0
  134. package/package.json +160 -1
  135. package/plugin/src/buildFonts.ts +29 -0
  136. package/plugin/src/index.ts +68 -0
  137. package/plugin/src/types.ts +33 -0
  138. package/plugin/src/withNanoIconsFontLinking.ts +119 -0
  139. package/plugin/tsconfig.json +9 -0
  140. package/scripts/cli.ts +34 -0
  141. package/scripts/tsconfig.json +25 -0
  142. package/src/const/colors.ts +150 -0
  143. package/src/core/font/compile.ts +96 -0
  144. package/src/core/font/metrics.ts +44 -0
  145. package/src/core/pipeline/config.ts +24 -0
  146. package/src/core/pipeline/index.ts +3 -0
  147. package/src/core/pipeline/managers.ts +114 -0
  148. package/src/core/pipeline/run.ts +151 -0
  149. package/src/core/shims/pathops.py +181 -0
  150. package/src/core/svg/layers.ts +66 -0
  151. package/src/core/svg/svg_dom.ts +99 -0
  152. package/src/core/svg/svg_pathops.ts +796 -0
  153. package/src/core/tsconfig.json +32 -0
  154. package/src/core/types.ts +138 -0
  155. package/src/createNanoIconsSet.tsx +131 -0
  156. package/src/index.node.ts +14 -0
  157. package/src/index.ts +7 -0
  158. package/src/utils/fingerPrint.ts +20 -0
  159. package/src/utils/parse.ts +67 -0
@@ -0,0 +1,796 @@
1
+ import type {
2
+ Cmd,
3
+ PathKitModule,
4
+ VerbMap,
5
+ WrappedPath,
6
+ Point,
7
+ } from '../types.js';
8
+
9
+ //** */
10
+
11
+ // ----- numeric helpers -----
12
+ const EPS = 1e-2;
13
+
14
+ // ✅ round-half-to-even (banker's rounding) at ndigits
15
+ function roundN(x: number, ndigits = 3): number {
16
+ const m = 10 ** ndigits;
17
+ const s = x * m;
18
+
19
+ // Handle very large values safely
20
+ if (!Number.isFinite(s)) return x;
21
+
22
+ const floor = Math.floor(s);
23
+ const frac = s - floor;
24
+
25
+ // floating tolerance for "exactly .5"
26
+ const TIE_EPS = 1e-12;
27
+
28
+ let roundedInt: number;
29
+ if (Math.abs(frac - 0.5) < TIE_EPS) {
30
+ // tie -> choose even
31
+ roundedInt = floor % 2 === 0 ? floor : floor + 1;
32
+ } else if (Math.abs(frac + 0.5) < TIE_EPS) {
33
+ // negative tie case (rare due to floor behavior, but keep for completeness)
34
+ const ceil = Math.ceil(s);
35
+ roundedInt = ceil % 2 === 0 ? ceil : ceil - 1;
36
+ } else {
37
+ roundedInt = Math.round(s);
38
+ }
39
+
40
+ return roundedInt / m;
41
+ }
42
+
43
+ function normPt(p: Point): [number, number] {
44
+ return [Math.round(p[0] * 10000) / 10000, Math.round(p[1] * 10000) / 10000];
45
+ }
46
+
47
+ function eqPt(a: Point, b: Point): boolean {
48
+ const aa = normPt(a);
49
+ const bb = normPt(b);
50
+ return Math.abs(aa[0] - bb[0]) < EPS && Math.abs(aa[1] - bb[1]) < EPS;
51
+ }
52
+
53
+ function signedAreaPolyline(points: readonly Point[]): number {
54
+ if (points.length < 3) return 0;
55
+ let a = 0;
56
+ for (let i = 0; i < points.length; i++) {
57
+ const [x1, y1] = points[i]!;
58
+ const [x2, y2] = points[(i + 1) % points.length]!;
59
+ a += x1 * y2 - x2 * y1;
60
+ }
61
+ return a / 2;
62
+ }
63
+
64
+ function approxSignedAreaFromContourCmds(
65
+ contourCmds: readonly Cmd[],
66
+ VERB: VerbMap,
67
+ steps = 24
68
+ ): number {
69
+ let cx = 0,
70
+ cy = 0;
71
+ let sx = 0,
72
+ sy = 0;
73
+ const pts: Point[] = [];
74
+ const add = (x: number, y: number) => pts.push([x, y]);
75
+
76
+ for (const cmd of contourCmds) {
77
+ const v = cmd[0]!;
78
+ if (v === VERB.MOVE) {
79
+ cx = cmd[1]!;
80
+ cy = cmd[2]!;
81
+ sx = cx;
82
+ sy = cy;
83
+ add(cx, cy);
84
+ } else if (v === VERB.LINE) {
85
+ cx = cmd[1]!;
86
+ cy = cmd[2]!;
87
+ add(cx, cy);
88
+ } else if (v === VERB.QUAD) {
89
+ const x0 = cx,
90
+ y0 = cy;
91
+ const x1 = cmd[1]!,
92
+ y1 = cmd[2]!;
93
+ const x2 = cmd[3]!,
94
+ y2 = cmd[4]!;
95
+ for (let i = 1; i <= steps; i++) {
96
+ const t = i / steps;
97
+ const mt = 1 - t;
98
+ const x = mt * mt * x0 + 2 * mt * t * x1 + t * t * x2;
99
+ const y = mt * mt * y0 + 2 * mt * t * y1 + t * t * y2;
100
+ add(x, y);
101
+ }
102
+ cx = x2;
103
+ cy = y2;
104
+ } else if (v === VERB.CUBIC) {
105
+ const x0 = cx,
106
+ y0 = cy;
107
+ const x1 = cmd[1]!,
108
+ y1 = cmd[2]!;
109
+ const x2 = cmd[3]!,
110
+ y2 = cmd[4]!;
111
+ const x3 = cmd[5]!,
112
+ y3 = cmd[6]!;
113
+ for (let i = 1; i <= steps; i++) {
114
+ const t = i / steps;
115
+ const mt = 1 - t;
116
+ const x =
117
+ mt * mt * mt * x0 +
118
+ 3 * mt * mt * t * x1 +
119
+ 3 * mt * t * t * x2 +
120
+ t * t * t * x3;
121
+ const y =
122
+ mt * mt * mt * y0 +
123
+ 3 * mt * mt * t * y1 +
124
+ 3 * mt * t * t * y2 +
125
+ t * t * t * y3;
126
+ add(x, y);
127
+ }
128
+ cx = x3;
129
+ cy = y3;
130
+ } else if (v === VERB.CLOSE) {
131
+ add(sx, sy);
132
+ }
133
+ }
134
+
135
+ if (
136
+ pts.length &&
137
+ (pts[0]![0] !== pts[pts.length - 1]![0] ||
138
+ pts[0]![1] !== pts[pts.length - 1]![1])
139
+ ) {
140
+ pts.push([pts[0]![0], pts[0]![1]]);
141
+ }
142
+ return signedAreaPolyline(pts);
143
+ }
144
+
145
+ function splitContours(cmds: readonly Cmd[], VERB: VerbMap): Cmd[][] {
146
+ const contours: Cmd[][] = [];
147
+ let cur: Cmd[] | null = null;
148
+ for (const cmd of cmds) {
149
+ const v = cmd[0]!;
150
+ if (v === VERB.MOVE) {
151
+ if (cur && cur.length) contours.push(cur);
152
+ cur = [cmd];
153
+ } else if (cur) {
154
+ cur.push(cmd);
155
+ }
156
+ }
157
+ if (cur && cur.length) contours.push(cur);
158
+ return contours;
159
+ }
160
+
161
+ function ensureClosed(contourCmds: readonly Cmd[], VERB: VerbMap): Cmd[] {
162
+ return contourCmds.some((c) => c[0] === VERB.CLOSE)
163
+ ? [...contourCmds]
164
+ : [...contourCmds, [VERB.CLOSE]];
165
+ }
166
+
167
+ function explicitCloseWantedFromCmds(
168
+ contourCmds: readonly Cmd[] | undefined,
169
+ VERB: VerbMap
170
+ ): boolean {
171
+ if (!contourCmds?.length) return false;
172
+ const m = contourCmds[0]!;
173
+ if (m[0] !== VERB.MOVE) return false;
174
+ const start: Point = [m[1]!, m[2]!];
175
+
176
+ for (let i = contourCmds.length - 1; i >= 1; i--) {
177
+ const cmd = contourCmds[i]!;
178
+ const v = cmd[0]!;
179
+ if (v === VERB.CLOSE) continue;
180
+
181
+ let end: Point | null = null;
182
+ if (v === VERB.LINE) end = [cmd[1]!, cmd[2]!];
183
+ else if (v === VERB.QUAD) end = [cmd[3]!, cmd[4]!];
184
+ else if (v === VERB.CUBIC) end = [cmd[5]!, cmd[6]!];
185
+ else continue;
186
+
187
+ return eqPt(end, start);
188
+ }
189
+ return false;
190
+ }
191
+
192
+ type SegmentL = { type: 'L'; start: Point; end: Point; synthetic: boolean };
193
+ type SegmentQ = {
194
+ type: 'Q';
195
+ start: Point;
196
+ ctrl: Point;
197
+ end: Point;
198
+ synthetic: boolean;
199
+ };
200
+ type SegmentC = {
201
+ type: 'C';
202
+ start: Point;
203
+ c1: Point;
204
+ c2: Point;
205
+ end: Point;
206
+ synthetic: boolean;
207
+ };
208
+ type Segment = SegmentL | SegmentQ | SegmentC;
209
+
210
+ function contourToSegments(
211
+ contourCmds: readonly Cmd[],
212
+ VERB: VerbMap
213
+ ): { start: Point; segs: Segment[] } {
214
+ const c = ensureClosed(contourCmds, VERB);
215
+ const move = c[0]!;
216
+ const sx = move[1]!,
217
+ sy = move[2]!;
218
+
219
+ let last: Point = [sx, sy];
220
+ const segs: Segment[] = [];
221
+
222
+ for (let i = 1; i < c.length; i++) {
223
+ const cmd = c[i]!;
224
+ const v = cmd[0]!;
225
+
226
+ if (v === VERB.LINE) {
227
+ const end: Point = [cmd[1]!, cmd[2]!];
228
+ segs.push({ type: 'L', start: last, end, synthetic: false });
229
+ last = end;
230
+ } else if (v === VERB.QUAD) {
231
+ const ctrl: Point = [cmd[1]!, cmd[2]!];
232
+ const end: Point = [cmd[3]!, cmd[4]!];
233
+ segs.push({ type: 'Q', start: last, ctrl, end, synthetic: false });
234
+ last = end;
235
+ } else if (v === VERB.CUBIC) {
236
+ const c1: Point = [cmd[1]!, cmd[2]!];
237
+ const c2: Point = [cmd[3]!, cmd[4]!];
238
+ const end: Point = [cmd[5]!, cmd[6]!];
239
+ segs.push({ type: 'C', start: last, c1, c2, end, synthetic: false });
240
+ last = end;
241
+ } else if (v === VERB.CLOSE) {
242
+ const end: Point = [sx, sy];
243
+ if (!eqPt(last, end)) {
244
+ segs.push({ type: 'L', start: last, end, synthetic: true });
245
+ }
246
+ last = end;
247
+ }
248
+ }
249
+
250
+ return { start: [sx, sy], segs };
251
+ }
252
+
253
+ function applyClosePolicy(
254
+ segs: Segment[],
255
+ startPt: Point,
256
+ explicitCloseWanted: boolean
257
+ ): Segment[] {
258
+ if (!segs.length) return segs;
259
+
260
+ for (const s of segs) s.synthetic = false;
261
+
262
+ const last = segs[segs.length - 1]!;
263
+ const lastEnd = last.end;
264
+ if (!eqPt(lastEnd, startPt)) {
265
+ segs.push({ type: 'L', start: lastEnd, end: startPt, synthetic: false });
266
+ }
267
+
268
+ if (!explicitCloseWanted) {
269
+ segs[segs.length - 1]!.synthetic = true;
270
+ }
271
+ return segs;
272
+ }
273
+
274
+ function segmentsToContourCmds(
275
+ startPt: Point,
276
+ segs: readonly Segment[],
277
+ VERB: VerbMap
278
+ ): Cmd[] {
279
+ const out: Cmd[] = [[VERB.MOVE, roundN(startPt[0]), roundN(startPt[1])]];
280
+ for (const s of segs) {
281
+ if (s.synthetic) continue;
282
+ if (s.type === 'L') {
283
+ out.push([VERB.LINE, roundN(s.end[0]), roundN(s.end[1])]);
284
+ } else if (s.type === 'Q') {
285
+ out.push([
286
+ VERB.QUAD,
287
+ roundN(s.ctrl[0]),
288
+ roundN(s.ctrl[1]),
289
+ roundN(s.end[0]),
290
+ roundN(s.end[1]),
291
+ ]);
292
+ } else {
293
+ out.push([
294
+ VERB.CUBIC,
295
+ roundN(s.c1[0]),
296
+ roundN(s.c1[1]),
297
+ roundN(s.c2[0]),
298
+ roundN(s.c2[1]),
299
+ roundN(s.end[0]),
300
+ roundN(s.end[1]),
301
+ ]);
302
+ }
303
+ }
304
+ out.push([VERB.CLOSE]);
305
+ return out;
306
+ }
307
+
308
+ function reverseClosedContourKeepStart(
309
+ contourCmds: readonly Cmd[],
310
+ explicitCloseWanted: boolean,
311
+ VERB: VerbMap
312
+ ): Cmd[] {
313
+ const { start, segs } = contourToSegments(contourCmds, VERB);
314
+
315
+ const reversed: Segment[] = segs
316
+ .slice()
317
+ .reverse()
318
+ .map((s) => {
319
+ if (s.type === 'L') {
320
+ return { type: 'L', start: s.end, end: s.start, synthetic: false };
321
+ }
322
+ if (s.type === 'Q') {
323
+ return {
324
+ type: 'Q',
325
+ start: s.end,
326
+ ctrl: s.ctrl,
327
+ end: s.start,
328
+ synthetic: false,
329
+ };
330
+ }
331
+ return {
332
+ type: 'C',
333
+ start: s.end,
334
+ c1: s.c2,
335
+ c2: s.c1,
336
+ end: s.start,
337
+ synthetic: false,
338
+ };
339
+ });
340
+
341
+ applyClosePolicy(reversed, start, explicitCloseWanted);
342
+ return segmentsToContourCmds(start, reversed, VERB);
343
+ }
344
+
345
+ function rotateClosedContourToStart(
346
+ contourCmds: readonly Cmd[],
347
+ desiredStart: Point,
348
+ explicitCloseWanted: boolean,
349
+ VERB: VerbMap
350
+ ): Cmd[] {
351
+ const { segs } = contourToSegments(contourCmds, VERB);
352
+
353
+ let idx = -1;
354
+ for (let i = 0; i < segs.length; i++) {
355
+ if (eqPt(segs[i]!.start, desiredStart)) {
356
+ idx = i;
357
+ break;
358
+ }
359
+ }
360
+ if (idx === -1) {
361
+ for (let i = 0; i < segs.length; i++) {
362
+ if (eqPt(segs[i]!.end, desiredStart)) {
363
+ idx = (i + 1) % segs.length;
364
+ break;
365
+ }
366
+ }
367
+ }
368
+ if (idx === -1) return [...contourCmds];
369
+
370
+ const rotated = segs.slice(idx).concat(segs.slice(0, idx));
371
+ applyClosePolicy(rotated, desiredStart, explicitCloseWanted);
372
+ return segmentsToContourCmds(desiredStart, rotated, VERB);
373
+ }
374
+
375
+ function cmdsToVerbPoints(
376
+ cmds: readonly Cmd[],
377
+ VERB: VerbMap
378
+ ): Array<[number, Point[]]> {
379
+ const out: Array<[number, Point[]]> = [];
380
+ for (const cmd of cmds) {
381
+ const v = cmd[0]!;
382
+ if (v === VERB.MOVE) out.push([0, [[cmd[1]!, cmd[2]!]]]);
383
+ else if (v === VERB.LINE) out.push([1, [[cmd[1]!, cmd[2]!]]]);
384
+ else if (v === VERB.QUAD)
385
+ out.push([
386
+ 2,
387
+ [
388
+ [cmd[1]!, cmd[2]!],
389
+ [cmd[3]!, cmd[4]!],
390
+ ],
391
+ ]);
392
+ else if (v === VERB.CUBIC)
393
+ out.push([
394
+ 3,
395
+ [
396
+ [cmd[1]!, cmd[2]!],
397
+ [cmd[3]!, cmd[4]!],
398
+ [cmd[5]!, cmd[6]!],
399
+ ],
400
+ ]);
401
+ else if (v === VERB.CLOSE) out.push([4, []]);
402
+ else throw new Error(`Unexpected verb in cmds: ${v}`);
403
+ }
404
+ return out;
405
+ }
406
+
407
+ function wrapPath(pathkitPath: WrappedPath['p']): WrappedPath {
408
+ return { p: pathkitPath, meta: { moves: [] } };
409
+ }
410
+
411
+ function cloneWrap(h: WrappedPath, PathKit: PathKitModule): WrappedPath {
412
+ const p2 = PathKit.NewPath(h.p);
413
+ const out = wrapPath(p2);
414
+ out.meta.moves = h.meta?.moves ? h.meta.moves.map((m) => [m[0], m[1]]) : [];
415
+ return out;
416
+ }
417
+
418
+ function mergeMoves(
419
+ aMoves: readonly Point[] | undefined,
420
+ bMoves: readonly Point[] | undefined
421
+ ): Point[] {
422
+ const out: Point[] = [];
423
+ const pushUnique = (pt: Point) => {
424
+ for (const existing of out) {
425
+ if (eqPt(existing, pt)) return;
426
+ }
427
+ out.push(pt);
428
+ };
429
+ for (const m of aMoves || []) pushUnique(normPt(m));
430
+ for (const m of bMoves || []) pushUnique(normPt(m));
431
+ return out;
432
+ }
433
+
434
+ function bestStartMinYMinX(
435
+ contourCmds: readonly Cmd[],
436
+ VERB: VerbMap
437
+ ): Point | null {
438
+ let best: Point | null = null;
439
+ for (const cmd of contourCmds) {
440
+ const v = cmd[0]!;
441
+ const add = (x: number, y: number) => {
442
+ const p: Point = [x, y];
443
+ if (!best) {
444
+ best = p;
445
+ return;
446
+ }
447
+ if (p[1] < best[1] - 1e-9) best = p;
448
+ else if (Math.abs(p[1] - best[1]) < 1e-9 && p[0] < best[0] - 1e-9)
449
+ best = p;
450
+ };
451
+
452
+ if (v === VERB.MOVE) add(cmd[1]!, cmd[2]!);
453
+ else if (v === VERB.LINE) add(cmd[1]!, cmd[2]!);
454
+ else if (v === VERB.QUAD) add(cmd[3]!, cmd[4]!);
455
+ else if (v === VERB.CUBIC) add(cmd[5]!, cmd[6]!);
456
+ }
457
+ return best;
458
+ }
459
+
460
+ // proxy to for picosvg to interatc with pathkit
461
+ export function buildPathopsBackend(PathKit: PathKitModule) {
462
+ const VERB: VerbMap = {
463
+ MOVE: PathKit.MOVE_VERB ?? 0,
464
+ LINE: PathKit.LINE_VERB ?? 1,
465
+ QUAD: PathKit.QUAD_VERB ?? 2,
466
+ CONIC: PathKit.CONIC_VERB ?? 3,
467
+ CUBIC: PathKit.CUBIC_VERB ?? 4,
468
+ CLOSE: PathKit.CLOSE_VERB ?? 5,
469
+ };
470
+
471
+ const FILL_EVENODD =
472
+ PathKit?.FillType?.EVENODD ?? PathKit?.FillType?.EVEN_ODD ?? 1;
473
+ const FILL_WINDING =
474
+ PathKit?.FillType?.WINDING ?? PathKit?.FillType?.NONZERO ?? 0;
475
+
476
+ function cmdsViaSvgRoundtrip(h: WrappedPath): Cmd[] {
477
+ const svg = h.p.toSVGString();
478
+ const p2 = PathKit.FromSVGString(svg);
479
+ const cmds = p2 ? p2.toCmds() : [];
480
+ if (p2) p2.delete?.();
481
+ return cmds;
482
+ }
483
+
484
+ function normalizeSortRotateContours(
485
+ cmds: Cmd[],
486
+ h: WrappedPath,
487
+ preferStrokeCanonical = false
488
+ ): Cmd[] {
489
+ const moves = (h.meta?.moves || []).map((m) => normPt([m[0], m[1]]));
490
+ const used = new Array(moves.length).fill(false);
491
+
492
+ // Build contour objects, but DO NOT normalize orientation yet.
493
+ const contourObjs = splitContours(cmds, VERB).map((c) => {
494
+ const explicitCloseWanted = explicitCloseWantedFromCmds(c, VERB);
495
+ const cc = ensureClosed(c, VERB);
496
+ const a = approxSignedAreaFromContourCmds(cc, VERB); // signed
497
+ return { cmds: cc, absA: Math.abs(a), explicitCloseWanted };
498
+ });
499
+
500
+ // Big-to-small ordering gives deterministic outer→inner ordering.
501
+ contourObjs.sort((x, y) => y.absA - x.absA);
502
+
503
+ // Enforce winding convention:
504
+ // - largest contour CCW (positive area)
505
+ // - all subsequent contours CW (negative area)
506
+ const ensureOrient = (
507
+ obj: { cmds: Cmd[]; explicitCloseWanted: boolean },
508
+ wantCCW: boolean
509
+ ) => {
510
+ const a = approxSignedAreaFromContourCmds(obj.cmds, VERB);
511
+ const isCCW = a > 0;
512
+ if (wantCCW !== isCCW) {
513
+ obj.cmds = reverseClosedContourKeepStart(
514
+ obj.cmds,
515
+ obj.explicitCloseWanted,
516
+ VERB
517
+ );
518
+ }
519
+ };
520
+
521
+ if (contourObjs.length) {
522
+ ensureOrient(contourObjs[0]!, true);
523
+ for (let i = 1; i < contourObjs.length; i++) {
524
+ ensureOrient(contourObjs[i]!, false);
525
+ }
526
+ }
527
+
528
+ // Rotate starts deterministically:
529
+ // 1) If we have recorded MOVE points, try to rotate contour to a move point that lies on it.
530
+ // 2) Otherwise, rotate to minY/minX point (helps stroke-ish paths)
531
+ for (const obj of contourObjs) {
532
+ let cc = obj.cmds;
533
+
534
+ for (let i = 0; i < moves.length; i++) {
535
+ if (used[i]) continue;
536
+ const target = moves[i]!;
537
+ const { segs } = contourToSegments(cc, VERB);
538
+ const found = segs.some(
539
+ (s) => eqPt(s.start, target) || eqPt(s.end, target)
540
+ );
541
+ if (found) {
542
+ used[i] = true;
543
+ cc = rotateClosedContourToStart(
544
+ cc,
545
+ target,
546
+ obj.explicitCloseWanted,
547
+ VERB
548
+ );
549
+ break;
550
+ }
551
+ }
552
+
553
+ // If no move point was used, apply the deterministic start heuristic
554
+ if (preferStrokeCanonical) {
555
+ const best = bestStartMinYMinX(cc, VERB);
556
+ if (best) {
557
+ cc = rotateClosedContourToStart(
558
+ cc,
559
+ best,
560
+ obj.explicitCloseWanted,
561
+ VERB
562
+ );
563
+ }
564
+ }
565
+
566
+ obj.cmds = cc;
567
+ }
568
+
569
+ return contourObjs.flatMap((x) => x.cmds);
570
+ }
571
+
572
+ return {
573
+ create_path(fillTypeInt: number): WrappedPath {
574
+ const p = PathKit.NewPath();
575
+ p.setFillType(fillTypeInt === 1 ? FILL_EVENODD : FILL_WINDING);
576
+ return wrapPath(p);
577
+ },
578
+
579
+ clone_path(h: WrappedPath): WrappedPath {
580
+ return cloneWrap(h, PathKit);
581
+ },
582
+
583
+ delete_path(h: WrappedPath): void {
584
+ h?.p?.delete?.();
585
+ },
586
+
587
+ get_fill_type(h: WrappedPath): number {
588
+ const ft = h.p.getFillType?.();
589
+ return ft === FILL_EVENODD ? 1 : 0;
590
+ },
591
+
592
+ set_fill_type(h: WrappedPath, fillTypeInt: number): true {
593
+ h.p.setFillType(fillTypeInt === 1 ? FILL_EVENODD : FILL_WINDING);
594
+ return true;
595
+ },
596
+
597
+ move_to(h: WrappedPath, x: number, y: number): void {
598
+ h.p.moveTo(x, y);
599
+ h.meta.moves.push(normPt([x, y]));
600
+ },
601
+
602
+ line_to(h: WrappedPath, x: number, y: number): void {
603
+ h.p.lineTo(x, y);
604
+ },
605
+
606
+ quad_to(
607
+ h: WrappedPath,
608
+ x1: number,
609
+ y1: number,
610
+ x2: number,
611
+ y2: number
612
+ ): void {
613
+ h.p.quadTo(x1, y1, x2, y2);
614
+ },
615
+
616
+ cubic_to(
617
+ h: WrappedPath,
618
+ x1: number,
619
+ y1: number,
620
+ x2: number,
621
+ y2: number,
622
+ x3: number,
623
+ y3: number
624
+ ): void {
625
+ h.p.cubicTo(x1, y1, x2, y2, x3, y3);
626
+ },
627
+
628
+ close(h: WrappedPath): void {
629
+ h.p.close();
630
+ },
631
+
632
+ simplify(h: WrappedPath, fixWinding = false): boolean {
633
+ try {
634
+ // 1) simplify in place
635
+ h.p.simplify();
636
+
637
+ // 2) normalize representation via SVG roundtrip
638
+ const svg = h.p.toSVGString();
639
+ const p2 = PathKit.FromSVGString(svg);
640
+ if (p2) {
641
+ // replace underlying path handle
642
+ h.p.delete?.();
643
+ h.p = p2;
644
+ }
645
+
646
+ if (fixWinding) h.p.setFillType(FILL_WINDING);
647
+ return true;
648
+ } catch {
649
+ return false;
650
+ }
651
+ },
652
+
653
+ stroke(
654
+ h: WrappedPath,
655
+ width: number,
656
+ capInt: number,
657
+ joinInt: number,
658
+ miterLimit: number,
659
+ dashArray: unknown,
660
+ dashOffset: number
661
+ ): WrappedPath {
662
+ const Caps = PathKit.StrokeCap ?? {};
663
+ const Joins = PathKit.StrokeJoin ?? {};
664
+
665
+ const cap =
666
+ capInt === 1
667
+ ? Caps.ROUND ?? 1
668
+ : capInt === 2
669
+ ? Caps.SQUARE ?? 2
670
+ : Caps.BUTT ?? 0;
671
+
672
+ const join =
673
+ joinInt === 1
674
+ ? Joins.ROUND ?? 1
675
+ : joinInt === 2
676
+ ? Joins.BEVEL ?? 2
677
+ : Joins.MITER ?? 0;
678
+
679
+ const work = PathKit.NewPath(h.p);
680
+
681
+ try {
682
+ if (
683
+ Array.isArray(dashArray) &&
684
+ dashArray.length === 2 &&
685
+ typeof work.dash === 'function'
686
+ ) {
687
+ work.dash(
688
+ Number(dashArray[0]),
689
+ Number(dashArray[1]),
690
+ dashOffset || 0
691
+ );
692
+ }
693
+
694
+ let stroked = work.stroke({
695
+ width,
696
+ cap,
697
+ join,
698
+ miter_limit: miterLimit,
699
+ });
700
+ if (!stroked || typeof stroked.toCmds !== 'function') stroked = work;
701
+
702
+ if (stroked !== work) work.delete?.();
703
+
704
+ const wrapped = wrapPath(stroked);
705
+ wrapped.meta.moves = [];
706
+ return wrapped;
707
+ } catch {
708
+ const wrapped = wrapPath(work);
709
+ wrapped.meta.moves = [];
710
+ return wrapped;
711
+ }
712
+ },
713
+
714
+ convert_conics_to_quads(_h: WrappedPath, _tol: number): void {
715
+ // intentionally no-op
716
+ },
717
+
718
+ transform(
719
+ h: WrappedPath,
720
+ a: number,
721
+ b: number,
722
+ c: number,
723
+ d: number,
724
+ e: number,
725
+ f: number
726
+ ): WrappedPath {
727
+ const p = PathKit.NewPath(h.p);
728
+ p.transform(a, c, e, b, d, f, 0, 0, 1);
729
+
730
+ const wrapped = wrapPath(p);
731
+ wrapped.meta.moves = (h.meta?.moves || []).map(([x, y]) =>
732
+ normPt([a * x + c * y + e, b * x + d * y + f])
733
+ );
734
+ return wrapped;
735
+ },
736
+
737
+ op(
738
+ aHandle: WrappedPath,
739
+ bHandle: WrappedPath,
740
+ opInt: number
741
+ ): WrappedPath | null {
742
+ try {
743
+ const Ops = PathKit.PathOp ?? {};
744
+ const op =
745
+ opInt === 1
746
+ ? Ops.INTERSECT ?? 1
747
+ : opInt === 2
748
+ ? Ops.DIFFERENCE ?? 2
749
+ : Ops.UNION ?? 0;
750
+
751
+ const out = PathKit.MakeFromOp(aHandle.p, bHandle.p, op);
752
+ if (!out) return null;
753
+
754
+ const wrapped = wrapPath(out);
755
+ wrapped.meta.moves = mergeMoves(
756
+ aHandle.meta?.moves,
757
+ bHandle.meta?.moves
758
+ );
759
+ return wrapped;
760
+ } catch {
761
+ return null;
762
+ }
763
+ },
764
+
765
+ bounds(h: WrappedPath): [number, number, number, number] {
766
+ const r = h.p.getBounds();
767
+ return [r.fLeft, r.fTop, r.fRight, r.fBottom];
768
+ },
769
+
770
+ area(h: WrappedPath): number {
771
+ try {
772
+ const cmds = cmdsViaSvgRoundtrip(h);
773
+ const contours = splitContours(cmds, VERB);
774
+ let total = 0;
775
+ for (const c of contours) {
776
+ const cc = ensureClosed(c, VERB);
777
+ total += Math.abs(approxSignedAreaFromContourCmds(cc, VERB));
778
+ }
779
+ return total;
780
+ } catch {
781
+ return 0.0;
782
+ }
783
+ },
784
+
785
+ iter_segments(h: WrappedPath): Array<[number, Point[]]> {
786
+ const cmds = cmdsViaSvgRoundtrip(h);
787
+ const preferStrokeCanonical = (h.meta?.moves?.length || 0) === 0;
788
+ const normalized = normalizeSortRotateContours(
789
+ cmds,
790
+ h,
791
+ preferStrokeCanonical
792
+ );
793
+ return cmdsToVerbPoints(normalized, VERB);
794
+ },
795
+ };
796
+ }