@emasoft/svg-matrix 1.0.18 → 1.0.20

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.
@@ -137,9 +137,9 @@ export function ellipseToPathDataHP(cx, cy, rx, ry, arcs = 8, precision = 6) {
137
137
  const y2 = y3.minus(kappa.mul(ryD).mul(ty3));
138
138
 
139
139
  if (i === 0) {
140
- commands.push(`M ${f(x0)} ${f(y0)}`);
140
+ commands.push(`M${f(x0)} ${f(y0)}`);
141
141
  }
142
- commands.push(`C ${f(x1)} ${f(y1)} ${f(x2)} ${f(y2)} ${f(x3)} ${f(y3)}`);
142
+ commands.push(`C${f(x1)} ${f(y1)} ${f(x2)} ${f(y2)} ${f(x3)} ${f(y3)}`);
143
143
  }
144
144
 
145
145
  commands.push('Z');
@@ -147,8 +147,13 @@ export function ellipseToPathDataHP(cx, cy, rx, ry, arcs = 8, precision = 6) {
147
147
  }
148
148
 
149
149
  function formatNumber(value, precision = 6) {
150
- // Use toFixed to preserve trailing zeros for consistent output formatting
151
- return value.toFixed(precision);
150
+ // Format with precision then remove trailing zeros for smaller output
151
+ let str = value.toFixed(precision);
152
+ // Remove trailing zeros after decimal point
153
+ if (str.includes('.')) {
154
+ str = str.replace(/\.?0+$/, '');
155
+ }
156
+ return str;
152
157
  }
153
158
 
154
159
  export function circleToPathData(cx, cy, r, precision = 6) {
@@ -160,7 +165,7 @@ export function circleToPathData(cx, cy, r, precision = 6) {
160
165
  const c3x1 = x2, c3y1 = cyD.plus(k), c3x2 = cxD.minus(k), c3y2 = cyD.plus(rD), x3 = cxD, y3 = cyD.plus(rD);
161
166
  const c4x1 = cxD.plus(k), c4y1 = y3, c4x2 = x0, c4y2 = cyD.plus(k);
162
167
  const f = v => formatNumber(v, precision);
163
- return `M ${f(x0)} ${f(y0)} C ${f(c1x1)} ${f(c1y1)} ${f(c1x2)} ${f(c1y2)} ${f(x1)} ${f(y1)} C ${f(c2x1)} ${f(c2y1)} ${f(c2x2)} ${f(c2y2)} ${f(x2)} ${f(y2)} C ${f(c3x1)} ${f(c3y1)} ${f(c3x2)} ${f(c3y2)} ${f(x3)} ${f(y3)} C ${f(c4x1)} ${f(c4y1)} ${f(c4x2)} ${f(c4y2)} ${f(x0)} ${f(y0)} Z`;
168
+ return `M${f(x0)} ${f(y0)}C${f(c1x1)} ${f(c1y1)} ${f(c1x2)} ${f(c1y2)} ${f(x1)} ${f(y1)}C${f(c2x1)} ${f(c2y1)} ${f(c2x2)} ${f(c2y2)} ${f(x2)} ${f(y2)}C${f(c3x1)} ${f(c3y1)} ${f(c3x2)} ${f(c3y2)} ${f(x3)} ${f(y3)}C${f(c4x1)} ${f(c4y1)} ${f(c4x2)} ${f(c4y2)} ${f(x0)} ${f(y0)}Z`;
164
169
  }
165
170
 
166
171
  export function ellipseToPathData(cx, cy, rx, ry, precision = 6) {
@@ -172,7 +177,7 @@ export function ellipseToPathData(cx, cy, rx, ry, precision = 6) {
172
177
  const c3x1 = x2, c3y1 = cyD.plus(ky), c3x2 = cxD.minus(kx), c3y2 = cyD.plus(ryD), x3 = cxD, y3 = cyD.plus(ryD);
173
178
  const c4x1 = cxD.plus(kx), c4y1 = y3, c4x2 = x0, c4y2 = cyD.plus(ky);
174
179
  const f = v => formatNumber(v, precision);
175
- return `M ${f(x0)} ${f(y0)} C ${f(c1x1)} ${f(c1y1)} ${f(c1x2)} ${f(c1y2)} ${f(x1)} ${f(y1)} C ${f(c2x1)} ${f(c2y1)} ${f(c2x2)} ${f(c2y2)} ${f(x2)} ${f(y2)} C ${f(c3x1)} ${f(c3y1)} ${f(c3x2)} ${f(c3y2)} ${f(x3)} ${f(y3)} C ${f(c4x1)} ${f(c4y1)} ${f(c4x2)} ${f(c4y2)} ${f(x0)} ${f(y0)} Z`;
180
+ return `M${f(x0)} ${f(y0)}C${f(c1x1)} ${f(c1y1)} ${f(c1x2)} ${f(c1y2)} ${f(x1)} ${f(y1)}C${f(c2x1)} ${f(c2y1)} ${f(c2x2)} ${f(c2y2)} ${f(x2)} ${f(y2)}C${f(c3x1)} ${f(c3y1)} ${f(c3x2)} ${f(c3y2)} ${f(x3)} ${f(y3)}C${f(c4x1)} ${f(c4y1)} ${f(c4x2)} ${f(c4y2)} ${f(x0)} ${f(y0)}Z`;
176
181
  }
177
182
 
178
183
  export function rectToPathData(x, y, width, height, rx = 0, ry = null, useArcs = false, precision = 6) {
@@ -184,21 +189,25 @@ export function rectToPathData(x, y, width, height, rx = 0, ry = null, useArcs =
184
189
  const f = v => formatNumber(v, precision);
185
190
  if (rxD.isZero() || ryD.isZero()) {
186
191
  const x1 = xD.plus(wD), y1 = yD.plus(hD);
187
- return `M ${f(xD)} ${f(yD)} L ${f(x1)} ${f(yD)} L ${f(x1)} ${f(y1)} L ${f(xD)} ${f(y1)} Z`;
192
+ // Use H (horizontal) and V (vertical) commands for smaller output
193
+ return `M${f(xD)} ${f(yD)}H${f(x1)}V${f(y1)}H${f(xD)}Z`;
188
194
  }
189
195
  const left = xD, right = xD.plus(wD), top = yD, bottom = yD.plus(hD);
190
196
  const leftInner = left.plus(rxD), rightInner = right.minus(rxD);
191
197
  const topInner = top.plus(ryD), bottomInner = bottom.minus(ryD);
192
198
  if (useArcs) {
193
- return `M ${f(leftInner)} ${f(top)} L ${f(rightInner)} ${f(top)} A ${f(rxD)} ${f(ryD)} 0 0 1 ${f(right)} ${f(topInner)} L ${f(right)} ${f(bottomInner)} A ${f(rxD)} ${f(ryD)} 0 0 1 ${f(rightInner)} ${f(bottom)} L ${f(leftInner)} ${f(bottom)} A ${f(rxD)} ${f(ryD)} 0 0 1 ${f(left)} ${f(bottomInner)} L ${f(left)} ${f(topInner)} A ${f(rxD)} ${f(ryD)} 0 0 1 ${f(leftInner)} ${f(top)} Z`;
199
+ return `M${f(leftInner)} ${f(top)}L${f(rightInner)} ${f(top)}A${f(rxD)} ${f(ryD)} 0 0 1 ${f(right)} ${f(topInner)}L${f(right)} ${f(bottomInner)}A${f(rxD)} ${f(ryD)} 0 0 1 ${f(rightInner)} ${f(bottom)}L${f(leftInner)} ${f(bottom)}A${f(rxD)} ${f(ryD)} 0 0 1 ${f(left)} ${f(bottomInner)}L${f(left)} ${f(topInner)}A${f(rxD)} ${f(ryD)} 0 0 1 ${f(leftInner)} ${f(top)}Z`;
194
200
  }
195
201
  const kappa = getKappa(), kx = kappa.mul(rxD), ky = kappa.mul(ryD);
196
- return `M ${f(leftInner)} ${f(top)} L ${f(rightInner)} ${f(top)} C ${f(rightInner)} ${f(top)} ${f(right)} ${f(topInner.minus(ky))} ${f(right)} ${f(topInner)} L ${f(right)} ${f(bottomInner)} C ${f(right)} ${f(bottomInner)} ${f(rightInner.plus(kx))} ${f(bottom)} ${f(rightInner)} ${f(bottom)} L ${f(leftInner)} ${f(bottom)} C ${f(leftInner)} ${f(bottom)} ${f(left)} ${f(bottomInner.plus(ky))} ${f(left)} ${f(bottomInner)} L ${f(left)} ${f(topInner)} C ${f(left)} ${f(topInner)} ${f(leftInner.minus(kx))} ${f(top)} ${f(leftInner)} ${f(top)} Z`;
202
+ // Each corner has two Bezier control points:
203
+ // First control point: offset from start point along the edge tangent
204
+ // Second control point: offset from end point along the edge tangent
205
+ return `M${f(leftInner)} ${f(top)}L${f(rightInner)} ${f(top)}C${f(rightInner.plus(kx))} ${f(top)} ${f(right)} ${f(topInner.minus(ky))} ${f(right)} ${f(topInner)}L${f(right)} ${f(bottomInner)}C${f(right)} ${f(bottomInner.plus(ky))} ${f(rightInner.plus(kx))} ${f(bottom)} ${f(rightInner)} ${f(bottom)}L${f(leftInner)} ${f(bottom)}C${f(leftInner.minus(kx))} ${f(bottom)} ${f(left)} ${f(bottomInner.plus(ky))} ${f(left)} ${f(bottomInner)}L${f(left)} ${f(topInner)}C${f(left)} ${f(topInner.minus(ky))} ${f(leftInner.minus(kx))} ${f(top)} ${f(leftInner)} ${f(top)}Z`;
197
206
  }
198
207
 
199
208
  export function lineToPathData(x1, y1, x2, y2, precision = 6) {
200
209
  const f = v => formatNumber(D(v), precision);
201
- return `M ${f(x1)} ${f(y1)} L ${f(x2)} ${f(y2)}`;
210
+ return `M${f(x1)} ${f(y1)}L${f(x2)} ${f(y2)}`;
202
211
  }
203
212
 
204
213
  function parsePoints(points) {
@@ -216,10 +225,10 @@ export function polylineToPathData(points, precision = 6) {
216
225
  if (pairs.length === 0) return '';
217
226
  const f = v => formatNumber(v, precision);
218
227
  const [x0, y0] = pairs[0];
219
- let path = `M ${f(x0)} ${f(y0)}`;
228
+ let path = `M${f(x0)} ${f(y0)}`;
220
229
  for (let i = 1; i < pairs.length; i++) {
221
230
  const [x, y] = pairs[i];
222
- path += ` L ${f(x)} ${f(y)}`;
231
+ path += `L${f(x)} ${f(y)}`;
223
232
  }
224
233
  return path;
225
234
  }
@@ -229,6 +238,13 @@ export function polygonToPathData(points, precision = 6) {
229
238
  return path ? path + ' Z' : '';
230
239
  }
231
240
 
241
+ // Parameter count for each SVG path command
242
+ const COMMAND_PARAMS = {
243
+ M: 2, m: 2, L: 2, l: 2, H: 1, h: 1, V: 1, v: 1,
244
+ C: 6, c: 6, S: 4, s: 4, Q: 4, q: 4, T: 2, t: 2,
245
+ A: 7, a: 7, Z: 0, z: 0
246
+ };
247
+
232
248
  export function parsePathData(pathData) {
233
249
  const commands = [];
234
250
  const commandRegex = /([MmLlHhVvCcSsQqTtAaZz])\s*([^MmLlHhVvCcSsQqTtAaZz]*)/g;
@@ -236,8 +252,29 @@ export function parsePathData(pathData) {
236
252
  while ((match = commandRegex.exec(pathData)) !== null) {
237
253
  const command = match[1];
238
254
  const argsStr = match[2].trim();
239
- const args = argsStr.length > 0 ? argsStr.split(/[\s,]+/).filter(s => s.length > 0).map(s => D(s)) : [];
240
- commands.push({ command, args });
255
+ const allArgs = argsStr.length > 0 ? argsStr.split(/[\s,]+/).filter(s => s.length > 0).map(s => D(s)) : [];
256
+
257
+ const paramCount = COMMAND_PARAMS[command];
258
+
259
+ if (paramCount === 0 || allArgs.length === 0) {
260
+ // Z/z command or command with no args
261
+ commands.push({ command, args: [] });
262
+ } else {
263
+ // Split args into groups based on parameter count
264
+ // Handle implicit command repetition per SVG spec
265
+ for (let i = 0; i < allArgs.length; i += paramCount) {
266
+ const args = allArgs.slice(i, i + paramCount);
267
+ if (args.length === paramCount) {
268
+ // For M/m, first group is moveto, subsequent groups become implicit lineto (L/l)
269
+ let effectiveCmd = command;
270
+ if (i > 0 && (command === 'M' || command === 'm')) {
271
+ effectiveCmd = command === 'M' ? 'L' : 'l';
272
+ }
273
+ commands.push({ command: effectiveCmd, args });
274
+ }
275
+ // Incomplete arg groups are silently dropped per SVG error handling
276
+ }
277
+ }
241
278
  }
242
279
  return commands;
243
280
  }
@@ -254,6 +291,9 @@ export function pathToAbsolute(pathData) {
254
291
  const result = [];
255
292
  let currentX = new Decimal(0), currentY = new Decimal(0);
256
293
  let subpathStartX = new Decimal(0), subpathStartY = new Decimal(0);
294
+ let lastControlX = new Decimal(0), lastControlY = new Decimal(0);
295
+ let lastCommand = '';
296
+
257
297
  for (const { command, args } of commands) {
258
298
  const isRelative = command === command.toLowerCase();
259
299
  const upperCmd = command.toUpperCase();
@@ -262,19 +302,23 @@ export function pathToAbsolute(pathData) {
262
302
  const y = isRelative ? currentY.plus(args[1]) : args[1];
263
303
  currentX = x; currentY = y; subpathStartX = x; subpathStartY = y;
264
304
  result.push({ command: 'M', args: [x, y] });
305
+ lastCommand = 'M';
265
306
  } else if (upperCmd === 'L') {
266
307
  const x = isRelative ? currentX.plus(args[0]) : args[0];
267
308
  const y = isRelative ? currentY.plus(args[1]) : args[1];
268
309
  currentX = x; currentY = y;
269
310
  result.push({ command: 'L', args: [x, y] });
311
+ lastCommand = 'L';
270
312
  } else if (upperCmd === 'H') {
271
313
  const x = isRelative ? currentX.plus(args[0]) : args[0];
272
314
  currentX = x;
273
315
  result.push({ command: 'L', args: [x, currentY] });
316
+ lastCommand = 'H';
274
317
  } else if (upperCmd === 'V') {
275
318
  const y = isRelative ? currentY.plus(args[0]) : args[0];
276
319
  currentY = y;
277
320
  result.push({ command: 'L', args: [currentX, y] });
321
+ lastCommand = 'V';
278
322
  } else if (upperCmd === 'C') {
279
323
  const x1 = isRelative ? currentX.plus(args[0]) : args[0];
280
324
  const y1 = isRelative ? currentY.plus(args[1]) : args[1];
@@ -282,18 +326,69 @@ export function pathToAbsolute(pathData) {
282
326
  const y2 = isRelative ? currentY.plus(args[3]) : args[3];
283
327
  const x = isRelative ? currentX.plus(args[4]) : args[4];
284
328
  const y = isRelative ? currentY.plus(args[5]) : args[5];
329
+ lastControlX = x2; lastControlY = y2;
330
+ currentX = x; currentY = y;
331
+ result.push({ command: 'C', args: [x1, y1, x2, y2, x, y] });
332
+ lastCommand = 'C';
333
+ } else if (upperCmd === 'S') {
334
+ // Smooth cubic Bezier: 4 args (x2, y2, x, y)
335
+ // First control point is reflection of previous second control point
336
+ let x1, y1;
337
+ if (lastCommand === 'C' || lastCommand === 'S') {
338
+ x1 = currentX.mul(2).minus(lastControlX);
339
+ y1 = currentY.mul(2).minus(lastControlY);
340
+ } else {
341
+ x1 = currentX;
342
+ y1 = currentY;
343
+ }
344
+ const x2 = isRelative ? currentX.plus(args[0]) : args[0];
345
+ const y2 = isRelative ? currentY.plus(args[1]) : args[1];
346
+ const x = isRelative ? currentX.plus(args[2]) : args[2];
347
+ const y = isRelative ? currentY.plus(args[3]) : args[3];
348
+ lastControlX = x2; lastControlY = y2;
285
349
  currentX = x; currentY = y;
286
350
  result.push({ command: 'C', args: [x1, y1, x2, y2, x, y] });
351
+ lastCommand = 'S';
352
+ } else if (upperCmd === 'Q') {
353
+ // Quadratic Bezier: 4 args (x1, y1, x, y)
354
+ const x1 = isRelative ? currentX.plus(args[0]) : args[0];
355
+ const y1 = isRelative ? currentY.plus(args[1]) : args[1];
356
+ const x = isRelative ? currentX.plus(args[2]) : args[2];
357
+ const y = isRelative ? currentY.plus(args[3]) : args[3];
358
+ lastControlX = x1; lastControlY = y1;
359
+ currentX = x; currentY = y;
360
+ result.push({ command: 'Q', args: [x1, y1, x, y] });
361
+ lastCommand = 'Q';
362
+ } else if (upperCmd === 'T') {
363
+ // Smooth quadratic Bezier: 2 args (x, y)
364
+ // Control point is reflection of previous control point
365
+ let x1, y1;
366
+ if (lastCommand === 'Q' || lastCommand === 'T') {
367
+ x1 = currentX.mul(2).minus(lastControlX);
368
+ y1 = currentY.mul(2).minus(lastControlY);
369
+ } else {
370
+ x1 = currentX;
371
+ y1 = currentY;
372
+ }
373
+ const x = isRelative ? currentX.plus(args[0]) : args[0];
374
+ const y = isRelative ? currentY.plus(args[1]) : args[1];
375
+ lastControlX = x1; lastControlY = y1;
376
+ currentX = x; currentY = y;
377
+ result.push({ command: 'Q', args: [x1, y1, x, y] });
378
+ lastCommand = 'T';
287
379
  } else if (upperCmd === 'A') {
288
380
  const x = isRelative ? currentX.plus(args[5]) : args[5];
289
381
  const y = isRelative ? currentY.plus(args[6]) : args[6];
290
382
  currentX = x; currentY = y;
291
383
  result.push({ command: 'A', args: [args[0], args[1], args[2], args[3], args[4], x, y] });
384
+ lastCommand = 'A';
292
385
  } else if (upperCmd === 'Z') {
293
386
  currentX = subpathStartX; currentY = subpathStartY;
294
387
  result.push({ command: 'Z', args: [] });
388
+ lastCommand = 'Z';
295
389
  } else {
296
390
  result.push({ command, args });
391
+ lastCommand = command;
297
392
  }
298
393
  }
299
394
  return pathArrayToString(result);
@@ -461,10 +556,25 @@ export function pathToCubics(pathData) {
461
556
  return pathArrayToString(result);
462
557
  }
463
558
 
559
+ /**
560
+ * Strip CSS units from a value string (e.g., "100px" -> 100, "50%" -> 50, "2em" -> 2)
561
+ * Returns the numeric value or 0 if parsing fails.
562
+ * @param {string|number|Decimal} val - Value to strip units from
563
+ * @returns {number} Numeric value without units
564
+ */
565
+ function stripUnits(val) {
566
+ if (typeof val === 'string') {
567
+ return parseFloat(val) || 0;
568
+ }
569
+ return val;
570
+ }
571
+
464
572
  export function convertElementToPath(element, precision = 6) {
465
573
  const getAttr = (name, defaultValue = 0) => {
466
- if (element.getAttribute) return element.getAttribute(name) || defaultValue;
467
- return element[name] !== undefined ? element[name] : defaultValue;
574
+ const rawValue = element.getAttribute ? element.getAttribute(name) : element[name];
575
+ const value = rawValue !== undefined && rawValue !== null ? rawValue : defaultValue;
576
+ // Strip CSS units before returning (handles px, em, %, etc.)
577
+ return stripUnits(value);
468
578
  };
469
579
  const tagName = (element.tagName || element.type || '').toLowerCase();
470
580
  if (tagName === 'circle') {