diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/client/util/bezierFit.ts | 214 |
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 []; |