lume
Version:
511 lines • 20.5 kB
JavaScript
import { ShapePath } from 'three/src/extras/core/ShapePath.js';
import { Vector2 } from 'three/src/math/Vector2.js';
// Functions adapted from https://github.com/mrdoob/three.js/blob/c7d06c02e302ab9c20fe8b33eade4b61c6712654/examples/jsm/loaders/SVGLoader.js#L207
export function parseSvgPathElement(path) {
return parseSvgPathDAttribute(path.getAttribute('d'));
}
export function parseSvgPathDAttribute(d) {
const path = new ShapePath();
const point = new Vector2();
const control = new Vector2();
const firstPoint = new Vector2();
let isFirstPoint = true;
let doSetFirstPoint = false;
if (!d) {
console.error('Path has not `d` attribute.');
return path;
}
const commands = d.match(/[a-df-z][^a-df-z]*/gi);
if (!commands) {
console.error('Empty or invalid path: ', d);
return path;
}
for (let i = 0, l = commands.length; i < l; i++) {
const command = commands[i];
const type = command.charAt(0);
const data = command.slice(1).trim();
if (isFirstPoint === true) {
doSetFirstPoint = true;
isFirstPoint = false;
}
let numbers;
switch (type) {
case 'M':
numbers = parseFloats(data);
for (let j = 0, jl = numbers.length; j < jl; j += 2) {
point.x = numbers[j + 0];
point.y = numbers[j + 1];
control.x = point.x;
control.y = point.y;
if (j === 0) {
path.moveTo(point.x, point.y);
}
else {
path.lineTo(point.x, point.y);
}
if (j === 0)
firstPoint.copy(point);
}
break;
case 'H':
numbers = parseFloats(data);
for (let j = 0, jl = numbers.length; j < jl; j++) {
point.x = numbers[j];
control.x = point.x;
control.y = point.y;
path.lineTo(point.x, point.y);
if (j === 0 && doSetFirstPoint === true)
firstPoint.copy(point);
}
break;
case 'V':
numbers = parseFloats(data);
for (let j = 0, jl = numbers.length; j < jl; j++) {
point.y = numbers[j];
control.x = point.x;
control.y = point.y;
path.lineTo(point.x, point.y);
if (j === 0 && doSetFirstPoint === true)
firstPoint.copy(point);
}
break;
case 'L':
numbers = parseFloats(data);
for (let j = 0, jl = numbers.length; j < jl; j += 2) {
point.x = numbers[j + 0];
point.y = numbers[j + 1];
control.x = point.x;
control.y = point.y;
path.lineTo(point.x, point.y);
if (j === 0 && doSetFirstPoint === true)
firstPoint.copy(point);
}
break;
case 'C':
numbers = parseFloats(data);
for (let j = 0, jl = numbers.length; j < jl; j += 6) {
path.bezierCurveTo(numbers[j + 0], numbers[j + 1], numbers[j + 2], numbers[j + 3], numbers[j + 4], numbers[j + 5]);
control.x = numbers[j + 2];
control.y = numbers[j + 3];
point.x = numbers[j + 4];
point.y = numbers[j + 5];
if (j === 0 && doSetFirstPoint === true)
firstPoint.copy(point);
}
break;
case 'S':
numbers = parseFloats(data);
for (let j = 0, jl = numbers.length; j < jl; j += 4) {
path.bezierCurveTo(getReflection(point.x, control.x), getReflection(point.y, control.y), numbers[j + 0], numbers[j + 1], numbers[j + 2], numbers[j + 3]);
control.x = numbers[j + 0];
control.y = numbers[j + 1];
point.x = numbers[j + 2];
point.y = numbers[j + 3];
if (j === 0 && doSetFirstPoint === true)
firstPoint.copy(point);
}
break;
case 'Q':
numbers = parseFloats(data);
for (let j = 0, jl = numbers.length; j < jl; j += 4) {
path.quadraticCurveTo(numbers[j + 0], numbers[j + 1], numbers[j + 2], numbers[j + 3]);
control.x = numbers[j + 0];
control.y = numbers[j + 1];
point.x = numbers[j + 2];
point.y = numbers[j + 3];
if (j === 0 && doSetFirstPoint === true)
firstPoint.copy(point);
}
break;
case 'T':
numbers = parseFloats(data);
for (let j = 0, jl = numbers.length; j < jl; j += 2) {
const rx = getReflection(point.x, control.x);
const ry = getReflection(point.y, control.y);
path.quadraticCurveTo(rx, ry, numbers[j + 0], numbers[j + 1]);
control.x = rx;
control.y = ry;
point.x = numbers[j + 0];
point.y = numbers[j + 1];
if (j === 0 && doSetFirstPoint === true)
firstPoint.copy(point);
}
break;
case 'A':
numbers = parseFloats(data, [3, 4], 7);
for (let j = 0, jl = numbers.length; j < jl; j += 7) {
// skip command if start point == end point
if (numbers[j + 5] == point.x && numbers[j + 6] == point.y)
continue;
const start = point.clone();
point.x = numbers[j + 5];
point.y = numbers[j + 6];
control.x = point.x;
control.y = point.y;
parseArcCommand(path, numbers[j], numbers[j + 1], numbers[j + 2], numbers[j + 3], numbers[j + 4], start, point);
if (j === 0 && doSetFirstPoint === true)
firstPoint.copy(point);
}
break;
case 'm':
numbers = parseFloats(data);
for (let j = 0, jl = numbers.length; j < jl; j += 2) {
point.x += numbers[j + 0];
point.y += numbers[j + 1];
control.x = point.x;
control.y = point.y;
if (j === 0) {
path.moveTo(point.x, point.y);
}
else {
path.lineTo(point.x, point.y);
}
if (j === 0)
firstPoint.copy(point);
}
break;
case 'h':
numbers = parseFloats(data);
for (let j = 0, jl = numbers.length; j < jl; j++) {
point.x += numbers[j];
control.x = point.x;
control.y = point.y;
path.lineTo(point.x, point.y);
if (j === 0 && doSetFirstPoint === true)
firstPoint.copy(point);
}
break;
case 'v':
numbers = parseFloats(data);
for (let j = 0, jl = numbers.length; j < jl; j++) {
point.y += numbers[j];
control.x = point.x;
control.y = point.y;
path.lineTo(point.x, point.y);
if (j === 0 && doSetFirstPoint === true)
firstPoint.copy(point);
}
break;
case 'l':
numbers = parseFloats(data);
for (let j = 0, jl = numbers.length; j < jl; j += 2) {
point.x += numbers[j + 0];
point.y += numbers[j + 1];
control.x = point.x;
control.y = point.y;
path.lineTo(point.x, point.y);
if (j === 0 && doSetFirstPoint === true)
firstPoint.copy(point);
}
break;
case 'c':
numbers = parseFloats(data);
for (let j = 0, jl = numbers.length; j < jl; j += 6) {
path.bezierCurveTo(point.x + numbers[j + 0], point.y + numbers[j + 1], point.x + numbers[j + 2], point.y + numbers[j + 3], point.x + numbers[j + 4], point.y + numbers[j + 5]);
control.x = point.x + numbers[j + 2];
control.y = point.y + numbers[j + 3];
point.x += numbers[j + 4];
point.y += numbers[j + 5];
if (j === 0 && doSetFirstPoint === true)
firstPoint.copy(point);
}
break;
case 's':
numbers = parseFloats(data);
for (let j = 0, jl = numbers.length; j < jl; j += 4) {
path.bezierCurveTo(getReflection(point.x, control.x), getReflection(point.y, control.y), point.x + numbers[j + 0], point.y + numbers[j + 1], point.x + numbers[j + 2], point.y + numbers[j + 3]);
control.x = point.x + numbers[j + 0];
control.y = point.y + numbers[j + 1];
point.x += numbers[j + 2];
point.y += numbers[j + 3];
if (j === 0 && doSetFirstPoint === true)
firstPoint.copy(point);
}
break;
case 'q':
numbers = parseFloats(data);
for (let j = 0, jl = numbers.length; j < jl; j += 4) {
path.quadraticCurveTo(point.x + numbers[j + 0], point.y + numbers[j + 1], point.x + numbers[j + 2], point.y + numbers[j + 3]);
control.x = point.x + numbers[j + 0];
control.y = point.y + numbers[j + 1];
point.x += numbers[j + 2];
point.y += numbers[j + 3];
if (j === 0 && doSetFirstPoint === true)
firstPoint.copy(point);
}
break;
case 't':
numbers = parseFloats(data);
for (let j = 0, jl = numbers.length; j < jl; j += 2) {
const rx = getReflection(point.x, control.x);
const ry = getReflection(point.y, control.y);
path.quadraticCurveTo(rx, ry, point.x + numbers[j + 0], point.y + numbers[j + 1]);
control.x = rx;
control.y = ry;
point.x = point.x + numbers[j + 0];
point.y = point.y + numbers[j + 1];
if (j === 0 && doSetFirstPoint === true)
firstPoint.copy(point);
}
break;
case 'a':
numbers = parseFloats(data, [3, 4], 7);
for (let j = 0, jl = numbers.length; j < jl; j += 7) {
// skip command if no displacement
if (numbers[j + 5] == 0 && numbers[j + 6] == 0)
continue;
const start = point.clone();
point.x += numbers[j + 5];
point.y += numbers[j + 6];
control.x = point.x;
control.y = point.y;
parseArcCommand(path, numbers[j], numbers[j + 1], numbers[j + 2], numbers[j + 3], numbers[j + 4], start, point);
if (j === 0 && doSetFirstPoint === true)
firstPoint.copy(point);
}
break;
case 'Z':
case 'z':
// @ts-expect-error FIXME
path.currentPath.autoClose = true;
// @ts-expect-error FIXME
if (path.currentPath.curves.length > 0) {
// Reset point to beginning of Path
point.copy(firstPoint);
// @ts-expect-error FIXME
path.currentPath.currentPoint.copy(point);
isFirstPoint = true;
}
break;
default:
console.warn(command);
}
doSetFirstPoint = false;
}
return path;
}
function parseFloats(input, flags, stride) {
if (typeof input !== 'string') {
throw new TypeError('Invalid input: ' + typeof input);
}
// Character groups
const RE = {
SEPARATOR: /[ \t\r\n\,.\-+]/,
WHITESPACE: /[ \t\r\n]/,
DIGIT: /[\d]/,
SIGN: /[-+]/,
POINT: /\./,
COMMA: /,/,
EXP: /e/i,
FLAGS: /[01]/,
};
// States
const SEP = 0;
const INT = 1;
const FLOAT = 2;
const EXP = 3;
let state = SEP;
let seenComma = true;
let number = '', exponent = '';
const result = [];
class NumberSyntaxError extends SyntaxError {
partial;
}
function throwSyntaxError(current, i, partial) {
const error = new NumberSyntaxError('Unexpected character "' + current + '" at index ' + i + '.');
error.partial = partial;
throw error;
}
function newNumber() {
if (number !== '') {
if (exponent === '')
result.push(Number(number));
else
result.push(Number(number) * Math.pow(10, Number(exponent)));
}
number = '';
exponent = '';
}
let current;
const length = input.length;
for (let i = 0; i < length; i++) {
current = input[i];
// check for flags
if (Array.isArray(flags) && flags.includes(result.length % stride) && RE.FLAGS.test(current)) {
state = INT;
number = current;
newNumber();
continue;
}
// parse until next number
if (state === SEP) {
// eat whitespace
if (RE.WHITESPACE.test(current)) {
continue;
}
// start new number
if (RE.DIGIT.test(current) || RE.SIGN.test(current)) {
state = INT;
number = current;
continue;
}
if (RE.POINT.test(current)) {
state = FLOAT;
number = current;
continue;
}
// throw on double commas (e.g. "1, , 2")
if (RE.COMMA.test(current)) {
if (seenComma) {
throwSyntaxError(current, i, result);
}
seenComma = true;
}
}
// parse integer part
if (state === INT) {
if (RE.DIGIT.test(current)) {
number += current;
continue;
}
if (RE.POINT.test(current)) {
number += current;
state = FLOAT;
continue;
}
if (RE.EXP.test(current)) {
state = EXP;
continue;
}
// throw on double signs ("-+1"), but not on sign as separator ("-1-2")
if (RE.SIGN.test(current) && number.length === 1 && RE.SIGN.test(number[0])) {
throwSyntaxError(current, i, result);
}
}
// parse decimal part
if (state === FLOAT) {
if (RE.DIGIT.test(current)) {
number += current;
continue;
}
if (RE.EXP.test(current)) {
state = EXP;
continue;
}
// throw on double decimal points (e.g. "1..2")
if (RE.POINT.test(current) && number[number.length - 1] === '.') {
throwSyntaxError(current, i, result);
}
}
// parse exponent part
if (state === EXP) {
if (RE.DIGIT.test(current)) {
exponent += current;
continue;
}
if (RE.SIGN.test(current)) {
if (exponent === '') {
exponent += current;
continue;
}
if (exponent.length === 1 && RE.SIGN.test(exponent)) {
throwSyntaxError(current, i, result);
}
}
}
// end of number
if (RE.WHITESPACE.test(current)) {
newNumber();
state = SEP;
seenComma = false;
}
else if (RE.COMMA.test(current)) {
newNumber();
state = SEP;
seenComma = true;
}
else if (RE.SIGN.test(current)) {
newNumber();
state = INT;
number = current;
}
else if (RE.POINT.test(current)) {
newNumber();
state = FLOAT;
number = current;
}
else {
throwSyntaxError(current, i, result);
}
}
// add the last number found (if any)
newNumber();
return result;
}
/**
* https://www.w3.org/TR/SVG/implnote.html#ArcImplementationNotes
* https://mortoray.com/2017/02/16/rendering-an-svg-elliptical-arc-as-bezier-curves/ Appendix: Endpoint to center arc conversion
* From
* rx ry x-axis-rotation large-arc-flag sweep-flag x y
* To
* aX, aY, xRadius, yRadius, aStartAngle, aEndAngle, aClockwise, aRotation
*/
function parseArcCommand(path, rx, ry, x_axis_rotation, large_arc_flag, sweep_flag, start, end) {
if (rx == 0 || ry == 0) {
// draw a line if either of the radii == 0
path.lineTo(end.x, end.y);
return;
}
x_axis_rotation = (x_axis_rotation * Math.PI) / 180;
// Ensure radii are positive
rx = Math.abs(rx);
ry = Math.abs(ry);
// Compute (x1', y1')
const dx2 = (start.x - end.x) / 2.0;
const dy2 = (start.y - end.y) / 2.0;
const x1p = Math.cos(x_axis_rotation) * dx2 + Math.sin(x_axis_rotation) * dy2;
const y1p = -Math.sin(x_axis_rotation) * dx2 + Math.cos(x_axis_rotation) * dy2;
// Compute (cx', cy')
let rxs = rx * rx;
let rys = ry * ry;
const x1ps = x1p * x1p;
const y1ps = y1p * y1p;
// Ensure radii are large enough
const cr = x1ps / rxs + y1ps / rys;
if (cr > 1) {
// scale up rx,ry equally so cr == 1
const s = Math.sqrt(cr);
rx = s * rx;
ry = s * ry;
rxs = rx * rx;
rys = ry * ry;
}
const dq = rxs * y1ps + rys * x1ps;
const pq = (rxs * rys - dq) / dq;
let q = Math.sqrt(Math.max(0, pq));
if (large_arc_flag === sweep_flag)
q = -q;
const cxp = (q * rx * y1p) / ry;
const cyp = (-q * ry * x1p) / rx;
// Step 3: Compute (cx, cy) from (cx', cy')
const cx = Math.cos(x_axis_rotation) * cxp - Math.sin(x_axis_rotation) * cyp + (start.x + end.x) / 2;
const cy = Math.sin(x_axis_rotation) * cxp + Math.cos(x_axis_rotation) * cyp + (start.y + end.y) / 2;
// Step 4: Compute θ1 and Δθ
const theta = svgAngle(1, 0, (x1p - cxp) / rx, (y1p - cyp) / ry);
const delta = svgAngle((x1p - cxp) / rx, (y1p - cyp) / ry, (-x1p - cxp) / rx, (-y1p - cyp) / ry) % (Math.PI * 2);
// @ts-expect-error FIXME
path.currentPath.absellipse(cx, cy, rx, ry, theta, theta + delta, sweep_flag === 0, x_axis_rotation);
}
// http://www.w3.org/TR/SVG11/implnote.html#PathElementImplementationNotes
function getReflection(a, b) {
return a - (b - a);
}
function svgAngle(ux, uy, vx, vy) {
const dot = ux * vx + uy * vy;
const len = Math.sqrt(ux * ux + uy * uy) * Math.sqrt(vx * vx + vy * vy);
let ang = Math.acos(Math.max(-1, Math.min(1, dot / len))); // floating point precision, slightly over values appear
if (ux * vy - uy * vx < 0)
ang = -ang;
return ang;
}
//# sourceMappingURL=svg.js.map