aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorbobzel <zzzman@gmail.com>2025-03-04 13:46:48 -0500
committerbobzel <zzzman@gmail.com>2025-03-04 13:46:48 -0500
commit5a9b1a453426df073b0ca43b54331d795ffea365 (patch)
tree752fb42f07d69cb28b84f888c7f3448632623a3a /src
parent93d4c71fe523a4b1f473ee6ab5e8a9b8ca715625 (diff)
fixed svg conversion to bezier to handle relative points and dataformats without spaces.
Diffstat (limited to 'src')
-rw-r--r--src/client/util/bezierFit.ts214
1 files changed, 76 insertions, 138 deletions
diff --git a/src/client/util/bezierFit.ts b/src/client/util/bezierFit.ts
index 65bd44bf9..0399fe1d5 100644
--- a/src/client/util/bezierFit.ts
+++ b/src/client/util/bezierFit.ts
@@ -1,6 +1,7 @@
/* eslint-disable no-use-before-define */
/* eslint-disable no-param-reassign */
import { Point } from '../../pen-gestures/ndollar';
+import { numberRange } from '../../Utils';
export enum SVGType {
Rect = 'rect',
@@ -623,24 +624,20 @@ export function GenerateControlPoints(coordinates: Point[], alpha = 0.1) {
return [...firstEnd, ...points, ...lastEnd];
}
-function convertToAbsolute(pathData: string): string {
+function convertRelativePathCmdsToAbsolute(pathData: string): string {
const commands = pathData.match(/[a-zA-Z][^a-zA-Z]*/g);
- if (!commands) return pathData;
-
let currentX = 0;
let currentY = 0;
let startX = 0;
let startY = 0;
-
- const absoluteCommands = commands.map(command => {
- const type = command[0];
+ const absoluteCommands = commands?.map(command => {
const values = command
.slice(1)
.trim()
.split(/[\s,]+/)
.map(v => +v);
- switch (type) {
+ switch (command[0]) {
case 'M':
currentX = values[0];
currentY = values[1];
@@ -654,13 +651,19 @@ function convertToAbsolute(pathData: string): string {
startY = currentY;
return `M${currentX},${currentY}`;
case 'L':
- currentX = values[0];
- currentY = values[1];
- return `L${currentX},${currentY}`;
- case 'l':
- currentX += values[0];
- currentY += values[1];
- return `L${currentX},${currentY}`;
+ currentX = values[values.length - 2];
+ currentY = values[values.length - 1];
+ return `L${values.join(',')}`;
+ case 'l': {
+ let str = '';
+ for (let i = 0; i < values.length; i += 2) {
+ str += (i === 0 ? 'L':',') + (values[i] + currentX) +
+ ',' + (values[i + 1] + currentY); // prettier-ignore
+ currentX += values[i];
+ currentY += values[i + 1];
+ }
+ return str;
+ }
case 'H':
currentX = values[0];
return `H${currentX}`;
@@ -674,8 +677,8 @@ function convertToAbsolute(pathData: string): string {
currentY += values[0];
return `V${currentY}`;
case 'C':
- currentX = values[4];
- currentY = values[5];
+ currentX = values[values.length - 2];
+ currentY = values[values.length - 1];
return `C${values.join(',')}`;
case 'c': {
let str = '';
@@ -698,8 +701,8 @@ function convertToAbsolute(pathData: string): string {
case 's':
return `S${values.map((v, i) => (i % 2 === 0 ? (currentX += v) : (currentY += v))).join(',')}`;
case 'Q':
- currentX = values[2];
- currentY = values[3];
+ currentX = values[values.length - 2];
+ currentY = values[values.length - 1];
return `Q${values.join(',')}`;
case 'q': {
let str = '';
@@ -737,7 +740,7 @@ function convertToAbsolute(pathData: string): string {
}
});
- return absoluteCommands.join(' ');
+ return absoluteCommands?.join(' ') ?? pathData;
}
export function SVGToBezier(name: SVGType, attributes: Record<string, string>, last: { X: number; Y: number }): Point[] {
@@ -805,138 +808,73 @@ export function SVGToBezier(name: SVGType, attributes: Record<string, string>, l
];
}
case 'path': {
+ const cmds = new Map<string, number>([
+ ['A', 7],
+ ['C', 6],
+ ['Q', 4],
+ ['L', 2],
+ ['V', 1],
+ ['H', 1],
+ ['Z', 0],
+ ['M', 2],
+ ]);
+ const cmdReg = (letter: string) => `${letter}?${numberRange(cmds.get(letter)??0).map(() => '[, ]?(-?\\d*\\.?\\d*)').join('')}`; // prettier-ignore
+ const pathdata = convertRelativePathCmdsToAbsolute(
+ attributes.d
+ .replace(/([0-9])-/g, '$1,-') // numbers are smooshed together - put a ',' between number-number => number,-number
+ .replace(/([.][0-9]+)(?=\.)/g, '$1,') // numbers are smooshed together - put a ',' between .number.number => .number,.number
+ .trim()
+ );
+ const move = pathdata.match(cmdReg('M'));
+ const start = move?.slice(1).map(v => +v) ?? [last.X, last.Y];
const coordList: Point[] = [];
- let fixedattrs = attributes.d.trim().replace(/([0-9])-/g, '$1,-');
- for (let i = 0; i < 100; i++) {
- const test = fixedattrs.replace(/([0-9]?\.[0-9]+)(\.[0-9]+)/g, '$1,$2');
- if (test === fixedattrs) break;
- fixedattrs = test;
- }
- const attrdata = convertToAbsolute(fixedattrs);
- const move = attrdata.match(/M(-?\d*\.?\d*)[, ]?(-?\d*\.?\d*)/);
- const [startX, startY] = move?.slice(1) ?? [last.X + '', last.Y + ''];
- const startPt = { X: +startX, Y: +startY };
- let first = true;
- let lastCmd = '';
- for (let attr = attrdata.slice(move?.[0].length ?? 0).trim(); attr; ) {
- lastCmd = 'AQCLVHZ'.includes(attr[0]) ? attr[0] : lastCmd;
- switch (lastCmd) {
- case 'Q': {
- const match = attr.match(/Q?[, ]?(-?\d*\.?\d*)[, ]?(-?\d*\.?\d*)[, ]?(-?\d*\.?\d*)[, ]?(-?\d*\.?\d*)/);
- if (match) {
- const prev = first ? startPt : coordList.lastElement();
- const Q = [+match[1], +match[2], +match[3], +match[4]];
-
- coordList.push(prev);
- coordList.push({ X: prev.X + (2 / 3) * (Q[0] - prev.X), Y: prev.Y + (2 / 3) * (Q[1] - prev.Y) });
- coordList.push({ X: Q[2] + (2 / 3) * (Q[0] - Q[2]), Y: Q[3] + (2 / 3) * (Q[1] - Q[3]) });
- coordList.push({ X: Q[2], Y: Q[3] });
- attr = attr.slice(match[0].length).trim();
- } else {
- attr = attr.slice(1).trim();
- alert('error' + attr);
- }
- break;
- }
- case 'C': {
- const match = attr.match(/C?[, ]?(-?\d*\.?\d*)[, ]?(-?\d*\.?\d*)[, ]?(-?\d*\.?\d*)[, ]?(-?\d*\.?\d*)[, ]?(-?\d*\.?\d*)[, ]?(-?\d*\.?\d*)/);
- if (match) {
- coordList.push(first ? startPt : coordList.lastElement());
- coordList.push({ X: +match[1], Y: +match[2] });
- coordList.push({ X: +match[3], Y: +match[4] });
- coordList.push({ X: +match[5], Y: +match[6] });
- attr = attr.slice(match[0].length).trim();
- } else {
- attr = attr.slice(1).trim();
- alert('error' + attr);
- }
- break;
- }
- case 'A': {
- const match = attr.match(/A?[, ]?(-?\d*\.?\d*)[, ]?(-?\d*\.?\d*)[, ]?(-?\d*\.?\d*)[, ]?(-?\d*\.?\d*)[, ]?(-?\d*\.?\d*)[, ]?(-?\d*\.?\d*)[, ]?(-?\d*\.?\d*)/);
- if (match) {
- console.log('SKIPPING arc - not implemented');
- // coordList.push(first ? startPt : coordList.lastElement());
- // coordList.push({ X: +match[1], Y: +match[2] });
- // coordList.push({ X: +match[3], Y: +match[4] });
- // coordList.push({ X: +match[5], Y: +match[6] });
- attr = attr.slice(match[0].length).trim();
- } else {
- attr = attr.slice(1).trim();
- alert('error' + attr);
- }
- break;
- }
- case 'L': {
- const match = attr.match(/L?[, ]?(-?\d*\.?\d*)[, ]?(-?\d*\.?\d*)/);
- if (match) {
- coordList.push(first ? startPt : coordList.lastElement());
- coordList.push(coordList.lastElement());
- coordList.push({ X: +match[1], Y: +match[2] });
- coordList.push({ X: +match[1], Y: +match[2] });
- attr = attr.slice(match[0].length).trim();
- } else {
- attr = attr.slice(1).trim();
- alert('error' + attr);
- }
+ for (let prev = coordList.lastElement() ?? { X: start[0], Y: start[1] },
+ pathcmd = pathdata.slice(move?.[0].length ?? 0).trim(),
+ m = move,
+ lastCmd = '';
+ pathcmd;
+ pathcmd = pathcmd.slice(m?.[0].length ?? 1).trim(),
+ prev = coordList.lastElement()
+ ) {
+ lastCmd = Array.from(cmds.keys()).includes(pathcmd[0]) ? pathcmd[0] : lastCmd; // command character is first, otherwise we're continuing coordinates for the last command
+ m = pathcmd.match(new RegExp(cmdReg(lastCmd)))!; // matches command + number parameters specific to command
+ switch (m ? lastCmd : 'error') {
+ case 'Q': // convert quadratic to Bezier
+ ((Q) => coordList.push(
+ prev,
+ { X: prev.X + (2 / 3) * (Q[0] - prev.X), Y: prev.Y + (2 / 3) * (Q[1] - prev.Y) },
+ { X: Q[2] + (2 / 3) * (Q[0] - Q[2]), Y: Q[3] + (2 / 3) * (Q[1] - Q[3]) },
+ { X: Q[2], Y: Q[3] }
+ ))([+m[1], +m[2], +m[3], +m[4]]);
+ break; case 'C': // bezier curve
+ coordList.push(prev, { X: +m[1], Y: +m[2] }, { X: +m[3], Y: +m[4] }, { X: +m[5], Y: +m[6] });
+ break; case 'L': // convert line to bezier
+ coordList.push(prev, prev, { X: +m[1], Y: +m[2] }, { X: +m[1], Y: +m[2] });
+ break; case 'H': // convert horiz line to bezier
+ coordList.push(prev, prev, { X: +m[1], Y: prev.Y }, { X: +m[1], Y: prev.Y });
+ break; case 'V': // convert vert line to bezier
+ coordList.push(prev, prev, { X: prev.X, Y: +m[1] }, { X: prev.X, Y: +m[1] });
+ break; case 'A': // convert arc to bezier
+ console.log('SKIPPING arc - conversion to bezier not implemented');
+ break; case 'Z':
break;
- }
- case 'H': {
- const match = attr.match(/H?[, ]?(-?\d*\.?\d*)/);
- if (match) {
- coordList.push(first ? startPt : coordList.lastElement());
- coordList.push(coordList.lastElement());
- coordList.push({ X: +match[1], Y: coordList.lastElement().Y });
- coordList.push({ X: +match[1], Y: coordList.lastElement().Y });
- attr = attr.slice(match[0].length).trim();
- } else {
- attr = attr.slice(1).trim();
- alert('error' + attr);
- }
- break;
- }
- case 'V': {
- const match = attr.match(/V?[, ]?(-?\d*\.?\d*)/);
- if (match) {
- coordList.push(first ? startPt : coordList.lastElement());
- coordList.push(coordList.lastElement());
- coordList.push({ X: coordList.lastElement().X, Y: +match[1] });
- coordList.push({ X: coordList.lastElement().X, Y: +match[1] });
- attr = attr.slice(match[0].length).trim();
- } else {
- attr = attr.slice(1).trim();
- alert('error' + attr);
- }
- break;
- }
- case 'Z': {
- coordList.push(first ? startPt : coordList.lastElement());
- coordList.push(first ? startPt : coordList.lastElement());
- coordList.push(startPt);
- coordList.push(startPt);
- attr = attr.slice(1).trim();
- break;
- }
default:
- attr = attr.slice(1).trim();
+ // eslint-disable-next-line no-debugger
debugger;
- }
- first = false;
- }
+ } // prettier-ignore
+ } // prettier-ignore
return coordList;
}
case 'polygon': {
- const coords: RegExpMatchArray[] = Array.from(attributes.points.matchAll(/(-?\d+\.?\d*),(-?\d+\.?\d*)/g));
- let list: Point[] = [];
+ const coords = Array.from(attributes.points.matchAll(/(-?\d+\.?\d*),(-?\d+\.?\d*)/g));
+ const list: Point[] = [];
coords.forEach(coord => {
list.push({ X: +coord[1], Y: +coord[2] });
list.push({ X: +coord[1], Y: +coord[2] });
list.push({ X: +coord[1], Y: +coord[2] });
list.push({ X: +coord[1], Y: +coord[2] });
});
- const firstPts = list.splice(0, 2);
- list = list.concat(firstPts);
- return list;
+ return list.concat(list.splice(0, 2)); // repeat start point to close
}
default:
return [];