0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-03-18 10:41:29 -05:00

Reimplement path parsing using native lang

This commit is contained in:
Andrey Antukh 2023-12-12 00:15:52 +01:00
parent f7acb9bfb8
commit 2dd1858026
8 changed files with 2522 additions and 452 deletions

View file

@ -5,452 +5,38 @@
;; Copyright (c) KALEIDOS INC
(ns app.common.svg.path
(:require
[app.common.data :as d]
[app.common.geom.point :as gpt]
[app.common.geom.shapes.path :as upg]
[app.common.math :as mth]
[app.common.svg :as csvg]
[app.common.svg.path.command :as upc]
[cuerdas.core :as str]))
(def commands-regex #"(?i)[mzlhvcsqta][^mzlhvcsqta]*")
(def regex #"[+-]?(\d+(\.\d+)?|\.\d+)(e[+-]?\d+)?")
(defn extract-params
[data pattern]
(loop [result []
ptt-idx 0
current {}
entries (re-seq regex data)
match (ffirst entries)]
(if match
(let [[attr-name attr-type] (nth pattern ptt-idx)
ptt-idx (inc ptt-idx)
ptt-cnt (count pattern)
value (if (= attr-type :flag)
(if (= 1 (count match))
(d/parse-integer match)
(d/parse-integer (subs match 0 1)))
(-> match csvg/fix-dot-number d/parse-double))
current (assoc current attr-name value)
result (if (>= ptt-idx ptt-cnt)
(conj result current)
result)
current (if (>= ptt-idx ptt-cnt)
{}
current)
match (if (and (= attr-type :flag)
(> (count match) 1))
(subs match 1)
nil)
entries (if match
entries
(rest entries))
match (if match
match
(ffirst entries))
ptt-idx (if (>= ptt-idx ptt-cnt)
0
ptt-idx)]
(recur result
ptt-idx
current
entries
match))
(if (seq current)
(conj result current)
result))))
;; Path specification
;; https://www.w3.org/TR/SVG11/paths.html
(defmulti parse-command
(fn [cmd]
(str/upper (subs cmd 0 1))))
(defmethod parse-command "M" [cmd]
(let [relative (str/starts-with? cmd "m")
param-list (extract-params cmd [[:x :number]
[:y :number]])]
(into [{:command :move-to
:relative relative
:params (first param-list)}]
(for [params (rest param-list)]
{:command :line-to
:relative relative
:params params}))))
(defmethod parse-command "Z" [_]
[{:command :close-path}])
(defmethod parse-command "L" [cmd]
(let [relative (str/starts-with? cmd "l")
param-list (extract-params cmd [[:x :number]
[:y :number]])]
(for [params param-list]
{:command :line-to
:relative relative
:params params})))
(defmethod parse-command "H" [cmd]
(let [relative (str/starts-with? cmd "h")
param-list (extract-params cmd [[:value :number]])]
(for [params param-list]
{:command :line-to-horizontal
:relative relative
:params params})))
(defmethod parse-command "V" [cmd]
(let [relative (str/starts-with? cmd "v")
param-list (extract-params cmd [[:value :number]])]
(for [params param-list]
{:command :line-to-vertical
:relative relative
:params params})))
(defmethod parse-command "C" [cmd]
(let [relative (str/starts-with? cmd "c")
param-list (extract-params cmd [[:c1x :number]
[:c1y :number]
[:c2x :number]
[:c2y :number]
[:x :number]
[:y :number]])
]
(for [params param-list]
{:command :curve-to
:relative relative
:params params})))
(defmethod parse-command "S" [cmd]
(let [relative (str/starts-with? cmd "s")
param-list (extract-params cmd [[:cx :number]
[:cy :number]
[:x :number]
[:y :number]])]
(for [params param-list]
{:command :smooth-curve-to
:relative relative
:params params})))
(defmethod parse-command "Q" [cmd]
(let [relative (str/starts-with? cmd "q")
param-list (extract-params cmd [[:cx :number]
[:cy :number]
[:x :number]
[:y :number]])]
(for [params param-list]
{:command :quadratic-bezier-curve-to
:relative relative
:params params})))
(defmethod parse-command "T" [cmd]
(let [relative (str/starts-with? cmd "t")
param-list (extract-params cmd [[:x :number]
[:y :number]])]
(for [params param-list]
{:command :smooth-quadratic-bezier-curve-to
:relative relative
:params params})))
(defmethod parse-command "A" [cmd]
(let [relative (str/starts-with? cmd "a")
param-list (extract-params cmd [[:rx :number]
[:ry :number]
[:x-axis-rotation :number]
[:large-arc-flag :flag]
[:sweep-flag :flag]
[:x :number]
[:y :number]])]
(for [params param-list]
{:command :elliptical-arc
:relative relative
:params params})))
(defn smooth->curve
[{:keys [params]} pos handler]
(let [{c1x :x c1y :y} (upg/calculate-opposite-handler pos handler)]
{:c1x c1x
:c1y c1y
:c2x (:cx params)
:c2y (:cy params)}))
(defn quadratic->curve
[sp ep cp]
(let [cp1 (-> (gpt/to-vec sp cp)
(gpt/scale (/ 2 3))
(gpt/add sp))
cp2 (-> (gpt/to-vec ep cp)
(gpt/scale (/ 2 3))
(gpt/add ep))]
{:c1x (:x cp1)
:c1y (:y cp1)
:c2x (:x cp2)
:c2y (:y cp2)}))
(defn- unit-vector-angle
[ux uy vx vy]
(let [sign (if (> 0 (- (* ux vy) (* uy vx))) -1.0 1.0)
dot (+ (* ux vx) (* uy vy))
dot (cond
(> dot 1.0) 1.0
(< dot -1.0) -1.0
:else dot)]
(* sign (mth/acos dot))))
(defn- get-arc-center [x1 y1 x2 y2 fa fs rx ry sin-phi cos-phi]
(let [x1p (+ (* cos-phi (/ (- x1 x2) 2)) (* sin-phi (/ (- y1 y2) 2)))
y1p (+ (* (- sin-phi) (/ (- x1 x2) 2)) (* cos-phi (/ (- y1 y2) 2)))
rx-sq (* rx rx)
ry-sq (* ry ry)
x1p-sq (* x1p x1p)
y1p-sq (* y1p y1p)
radicant (- (* rx-sq ry-sq)
(* rx-sq y1p-sq)
(* ry-sq x1p-sq))
radicant (if (< radicant 0) 0 radicant)
radicant (/ radicant (+ (* rx-sq y1p-sq) (* ry-sq x1p-sq)))
radicant (* (mth/sqrt radicant) (if (= fa fs) -1 1))
cxp (* radicant (/ rx ry) y1p)
cyp (* radicant (/ (- ry) rx) x1p)
cx (+ (- (* cos-phi cxp)
(* sin-phi cyp))
(/ (+ x1 x2) 2))
cy (+ (* sin-phi cxp)
(* cos-phi cyp)
(/ (+ y1 y2) 2))
v1x (/ (- x1p cxp) rx)
v1y (/ (- y1p cyp) ry)
v2x (/ (- (- x1p) cxp) rx)
v2y (/ (- (- y1p) cyp) ry)
theta1 (unit-vector-angle 1 0 v1x v1y)
dtheta (unit-vector-angle v1x v1y v2x v2y)
dtheta (if (and (= fs 0) (> dtheta 0)) (- dtheta (* mth/PI 2)) dtheta)
dtheta (if (and (= fs 1) (< dtheta 0)) (+ dtheta (* mth/PI 2)) dtheta)]
[cx cy theta1 dtheta]))
(defn approximate-unit-arc
[theta1 dtheta]
(let [alpha (* (/ 4 3) (mth/tan (/ dtheta 4)))
x1 (mth/cos theta1)
y1 (mth/sin theta1)
x2 (mth/cos (+ theta1 dtheta))
y2 (mth/sin (+ theta1 dtheta))]
[x1 y1 (- x1 (* y1 alpha)) (+ y1 (* x1 alpha)) (+ x2 (* y2 alpha)) (- y2 (* x2 alpha)) x2 y2]))
(defn- process-curve
[curve cc rx ry sin-phi cos-phi]
(reduce (fn [curve i]
(let [x (nth curve i)
y (nth curve (inc i))
x (* x rx)
y (* y ry)
xp (- (* cos-phi x) (* sin-phi y))
yp (+ (* sin-phi x) (* cos-phi y))]
(-> curve
(assoc i (+ xp (nth cc 0)))
(assoc (inc i) (+ yp (nth cc 1))))))
curve
(range 0 (count curve) 2)))
(defn arc->beziers*
[x1 y1 x2 y2 fa fs rx ry phi]
(let [tau (* mth/PI 2)
phi-tau (/ (* phi tau) 360)
sin-phi (mth/sin phi-tau)
cos-phi (mth/cos phi-tau)
x1p (+ (/ (* cos-phi (- x1 x2)) 2)
(/ (* sin-phi (- y1 y2)) 2))
y1p (+ (/ (* (- sin-phi) (- x1 x2)) 2)
(/ (* cos-phi (- y1 y2)) 2))]
(if (or (zero? x1p)
(zero? y1p)
(zero? rx)
(zero? ry))
[]
(let [
rx (mth/abs rx)
ry (mth/abs ry)
lambda (+ (/ (* x1p x1p) (* rx rx))
(/ (* y1p y1p) (* ry ry)))
rx (if (> lambda 1) (* rx (mth/sqrt lambda)) rx)
ry (if (> lambda 1) (* ry (mth/sqrt lambda)) ry)
cc (get-arc-center x1 y1 x2 y2 fa fs rx ry sin-phi cos-phi)
theta1 (nth cc 2)
dtheta (nth cc 3)
segments (mth/max (mth/ceil (/ (mth/abs dtheta) (/ tau 4))) 1)
dtheta (/ dtheta segments)]
(loop [i 0.0
t (double theta1)
r []]
(if (< i segments)
(let [curve (approximate-unit-arc t dtheta)
curve (process-curve curve cc rx ry sin-phi cos-phi)]
(recur (inc i)
(+ t dtheta)
(conj r curve)))
r))))))
#?(:clj
(:import app.common.svg.path.Parser
app.common.svg.path.Parser$Segment))
#?(:cljs
(:require ["./path/parser.js" :as parser])))
(defn arc->beziers
[from-p {:keys [params] :as command}]
(let [to-command
(fn [[_ _ c1x c1y c2x c2y x y]]
{:command :curve-to
:relative (:relative command)
:params {:c1x c1x :c1y c1y
:c2x c2x :c2y c2y
:x x :y y}})
{from-x :x from-y :y} from-p
x (get params :x 0.0)
y (get params :y 0.0)
rx (get params :rx 0.0)
ry (get params :ry 0.0)
x-axis-rotation (get params :x-axis-rotation 0)
large-arc-flag (get params :large-arc-flag 0)
sweep-flag (get params :sweep-flag 0)
result (arc->beziers* from-x from-y x y large-arc-flag sweep-flag rx ry x-axis-rotation)]
(mapv to-command result)))
(defn simplify-commands
"Removes some commands and convert relative to absolute coordinates"
[commands]
(let [simplify-command
;; prev-pos : previous position for the current path. Necessary for relative commands
;; prev-start : previous move-to necessary for Z commands
;; prev-cc : previous command control point for cubic beziers
;; prev-qc : previous command control point for quadratic curves
(fn [[result prev-pos prev-start prev-cc prev-qc] [command _prev]]
(let [command (assoc command :prev-pos prev-pos)
command
(cond-> command
(:relative command)
(-> (assoc :relative false)
(d/update-in-when [:params :c1x] + (:x prev-pos))
(d/update-in-when [:params :c1y] + (:y prev-pos))
(d/update-in-when [:params :c2x] + (:x prev-pos))
(d/update-in-when [:params :c2y] + (:y prev-pos))
(d/update-in-when [:params :cx] + (:x prev-pos))
(d/update-in-when [:params :cy] + (:y prev-pos))
(d/update-in-when [:params :x] + (:x prev-pos))
(d/update-in-when [:params :y] + (:y prev-pos))
(cond->
(= :line-to-horizontal (:command command))
(d/update-in-when [:params :value] + (:x prev-pos))
(= :line-to-vertical (:command command))
(d/update-in-when [:params :value] + (:y prev-pos)))))
params (:params command)
orig-command command
command
(cond-> command
(= :line-to-horizontal (:command command))
(-> (assoc :command :line-to)
(update :params dissoc :value)
(assoc-in [:params :x] (:value params))
(assoc-in [:params :y] (:y prev-pos)))
(= :line-to-vertical (:command command))
(-> (assoc :command :line-to)
(update :params dissoc :value)
(assoc-in [:params :y] (:value params))
(assoc-in [:params :x] (:x prev-pos)))
(= :smooth-curve-to (:command command))
(-> (assoc :command :curve-to)
(update :params dissoc :cx :cy)
(update :params merge (smooth->curve command prev-pos prev-cc)))
(= :quadratic-bezier-curve-to (:command command))
(-> (assoc :command :curve-to)
(update :params dissoc :cx :cy)
(update :params merge (quadratic->curve prev-pos (gpt/point params) (gpt/point (:cx params) (:cy params)))))
(= :smooth-quadratic-bezier-curve-to (:command command))
(-> (assoc :command :curve-to)
(update :params merge (quadratic->curve prev-pos (gpt/point params) (upg/calculate-opposite-handler prev-pos prev-qc)))))
result (if (= :elliptical-arc (:command command))
(into result (arc->beziers prev-pos command))
(conj result command))
next-cc (case (:command orig-command)
:smooth-curve-to
(gpt/point (get-in orig-command [:params :cx]) (get-in orig-command [:params :cy]))
:curve-to
(gpt/point (get-in orig-command [:params :c2x]) (get-in orig-command [:params :c2y]))
(:line-to-horizontal :line-to-vertical)
(gpt/point (get-in command [:params :x]) (get-in command [:params :y]))
(gpt/point (get-in orig-command [:params :x]) (get-in orig-command [:params :y])))
next-qc (case (:command orig-command)
:quadratic-bezier-curve-to
(gpt/point (get-in orig-command [:params :cx]) (get-in orig-command [:params :cy]))
:smooth-quadratic-bezier-curve-to
(upg/calculate-opposite-handler prev-pos prev-qc)
(gpt/point (get-in orig-command [:params :x]) (get-in orig-command [:params :y])))
next-pos (if (= :close-path (:command command))
prev-start
(upc/command->point prev-pos command))
next-start (if (= :move-to (:command command)) next-pos prev-start)]
[result next-pos next-start next-cc next-qc]))
start (first commands)
start (cond-> start
(:relative start)
(assoc :relative false))
start-pos (gpt/point (:params start))]
(->> (map vector (rest commands) commands)
(reduce simplify-command [[start] start-pos start-pos start-pos start-pos])
(first))))
"A function for convert Arcs to Beziers, used only for testing
purposes."
[x1 y1 x2 y2 fa fs rx ry phi]
#?(:clj (Parser/arcToBeziers (double x1)
(double y1)
(double x2)
(double y2)
(double fa)
(double fs)
(double rx)
(double ry)
(double phi))
:cljs (parser/arcToBeziers x1 y1 x2 y2 fa fs rx ry phi)))
(defn parse
[path-str]
(if (empty? path-str)
path-str
(let [commands (re-seq commands-regex path-str)]
(-> (mapcat parse-command commands)
(simplify-commands)))))
#?(:clj
(into []
(map (fn [segment]
(.toPersistentMap ^Parser$Segment segment)))
(Parser/parse path-str))
:cljs
(into []
(map (fn [segment]
(.toPersistentMap ^js segment)))
(parser/parse path-str)))))

View file

@ -0,0 +1,943 @@
package app.common.svg.path;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.List;
import java.util.Iterator;
import clojure.lang.Keyword;
import clojure.lang.AMapEntry;
import clojure.lang.PersistentArrayMap;
public class Parser {
static final Keyword MOVE_TO = Keyword.intern("move-to");
static final Keyword CLOSE_PATH = Keyword.intern("close-path");
static final Keyword LINE_TO = Keyword.intern("line-to");
static final Keyword CURVE_TO = Keyword.intern("curve-to");
static final Keyword K_COMMAND = Keyword.intern("command");
static final Keyword K_PARAMS = Keyword.intern("params");
static final Keyword K_X = Keyword.intern("x");
static final Keyword K_Y = Keyword.intern("y");
static final Keyword K_C1X = Keyword.intern("c1x");
static final Keyword K_C1Y = Keyword.intern("c1y");
static final Keyword K_C2X = Keyword.intern("c2x");
static final Keyword K_C2Y = Keyword.intern("c2y");
public static List<Segment> parsePathData(String string) {
if (string == null || string.length() == 0) {
return new ArrayList<>();
}
List<Segment> pdata = new ArrayList<>();
Iterator<Segment> parser = new ParserImpl(string);
parser.forEachRemaining(pdata::add);
return pdata;
}
public static List<Segment> parse(final String data) {
var result = parsePathData(data);
result = absolutizePathData(result);
result = simplifyPathData(result);
return result;
}
public static class Segment {
public char command;
public double[] params;
public Segment(final char cmd, final double[] vals) {
this.command = cmd;
this.params = vals;
}
public Object toPersistentMap() {
Keyword command = null;
Object[] params = null;
switch(this.command) {
case 'M':
command = MOVE_TO;
params = new Object[] {K_X, this.params[0], K_Y, this.params[1]};
break;
case 'Z':
command = CLOSE_PATH;
break;
case 'L':
command = LINE_TO;
params = new Object[] {K_X, this.params[0], K_Y, this.params[1]};
break;
case 'C':
command = CURVE_TO;
params = new Object[] {
K_C1X, this.params[0],
K_C1Y, this.params[1],
K_C2X, this.params[2],
K_C2Y, this.params[3],
K_X, this.params[4],
K_Y, this.params[5]
};
break;
}
if (command == null) {
throw new IllegalArgumentException("invalid segment:" + this.command);
}
if (params == null) {
return new PersistentArrayMap(new Object[] {K_COMMAND, command});
} else {
var _params = new PersistentArrayMap(params);
return new PersistentArrayMap(new Object[] {K_COMMAND, command, K_PARAMS, _params});
}
}
}
public static class ParserImpl implements Iterator<Segment> {
private final char[] string;
private int currentIndex;
private final int endIndex;
private Character prevCommand;
public ParserImpl(String string) {
this.string = string.toCharArray();
this.endIndex = this.string.length;
this.currentIndex = 0;
this.prevCommand = null;
this.skipOptionalSpaces();
}
public boolean hasNext() {
if (this.currentIndex == 0) {
char command = peekSegmentCommand();
return ((this.currentIndex < this.endIndex) &&
(command == 'M' || command == 'm'));
} else {
return this.currentIndex < this.endIndex;
}
}
public Segment next() {
char currentChar = this.string[this.currentIndex];
char command = (validCommand(currentChar)) ? currentChar : '\u0000';
if (command == '\u0000') {
if (this.prevCommand == null) {
return null;
}
if ((currentChar == '+' || currentChar == '-' || currentChar == '.' || (currentChar >= '0' && currentChar <= '9')) && this.prevCommand != 'Z') {
if (this.prevCommand == 'M') {
command = 'L';
} else if (this.prevCommand == 'm') {
command = 'l';
} else {
command = this.prevCommand;
}
} else {
command = '\u0000';
}
if (command == '\u0000') {
return null;
}
} else {
this.currentIndex++;
}
this.prevCommand = command;
double[] params = null;
char cmd = Character.toUpperCase(command);
if (cmd == 'H' || cmd == 'V') {
params = new double[] {
parseNumber()
};
} else if (cmd == 'M' || cmd == 'L' || cmd == 'T') {
params = new double[] {
parseNumber(),
parseNumber()
};
} else if (cmd == 'S' || cmd == 'Q') {
params = new double[] {
parseNumber(),
parseNumber(),
parseNumber(),
parseNumber()
};
} else if (cmd == 'C') {
params = new double[] {
parseNumber(),
parseNumber(),
parseNumber(),
parseNumber(),
parseNumber(),
parseNumber()
};
} else if (cmd == 'A') {
params = new double[] {
parseNumber(),
parseNumber(),
parseNumber(),
parseArcFlag(),
parseArcFlag(),
parseNumber(),
parseNumber()
};
} else if (cmd == 'Z') {
skipOptionalSpaces();
params = new double[] {};
}
return new Segment(command, params);
}
private char peekSegmentCommand() {
char currentChar = this.string[this.currentIndex];
return validCommand(currentChar) ? currentChar : '\u0000';
}
private boolean isCurrentSpace() {
char currentChar = this.string[this.currentIndex];
return currentChar <= ' ' && (currentChar == ' ' || currentChar == '\n' || currentChar == '\t' || currentChar == '\r' || currentChar == '\f');
}
private boolean skipOptionalSpaces() {
while (this.currentIndex < this.endIndex && isCurrentSpace()) {
this.currentIndex++;
}
return this.currentIndex < this.endIndex;
}
private boolean skipOptionalSpacesOrDelimiter() {
if (this.currentIndex < this.endIndex &&
!isCurrentSpace() &&
this.string[this.currentIndex] != ',') {
return false;
}
if (skipOptionalSpaces()) {
if (this.currentIndex < this.endIndex && this.string[this.currentIndex] == ',') {
this.currentIndex++;
skipOptionalSpaces();
}
}
return this.currentIndex < this.endIndex;
}
private Double parseNumber() {
int exponent = 0;
int integer = 0;
double frac = 1;
double decimal = 0;
int sign = 1;
int expsign = 1;
int startIndex = this.currentIndex;
skipOptionalSpaces();
if (this.currentIndex < this.endIndex && this.string[this.currentIndex] == '+') {
this.currentIndex++;
} else if (this.currentIndex < this.endIndex && this.string[this.currentIndex] == '-') {
this.currentIndex++;
sign = -1;
}
if (this.currentIndex == this.endIndex ||
((this.string[this.currentIndex] < '0' || this.string[this.currentIndex] > '9') &&
this.string[this.currentIndex] != '.')) {
return null;
}
int startIntPartIndex = this.currentIndex;
while (this.currentIndex < this.endIndex &&
this.string[this.currentIndex] >= '0' &&
this.string[this.currentIndex] <= '9') {
this.currentIndex++;
}
if (this.currentIndex != startIntPartIndex) {
int scanIntPartIndex = this.currentIndex - 1;
int multiplier = 1;
while (scanIntPartIndex >= startIntPartIndex) {
integer += multiplier * (this.string[scanIntPartIndex] - '0');
scanIntPartIndex--;
multiplier *= 10;
}
}
if (this.currentIndex < this.endIndex && this.string[this.currentIndex] == '.') {
this.currentIndex++;
if (this.currentIndex >= this.endIndex ||
this.string[this.currentIndex] < '0' ||
this.string[this.currentIndex] > '9') {
return null;
}
while (this.currentIndex < this.endIndex &&
this.string[this.currentIndex] >= '0' &&
this.string[this.currentIndex] <= '9') {
frac *= 10;
decimal += (this.string[this.currentIndex] - '0') / frac;
this.currentIndex++;
}
}
if (this.currentIndex != startIndex &&
this.currentIndex + 1 < this.endIndex &&
(this.string[this.currentIndex] == 'e' || this.string[this.currentIndex] == 'E') &&
(this.string[this.currentIndex + 1] != 'x' && this.string[this.currentIndex + 1] != 'm')) {
this.currentIndex++;
if (this.string[this.currentIndex] == '+') {
this.currentIndex++;
} else if (this.string[this.currentIndex] == '-') {
this.currentIndex++;
expsign = -1;
}
if (this.currentIndex >= this.endIndex ||
this.string[this.currentIndex] < '0' ||
this.string[this.currentIndex] > '9') {
return null;
}
while (this.currentIndex < this.endIndex &&
this.string[this.currentIndex] >= '0' &&
this.string[this.currentIndex] <= '9') {
exponent *= 10;
exponent += (this.string[this.currentIndex] - '0');
this.currentIndex++;
}
}
double number = integer + decimal;
number *= sign;
if (exponent != 0) {
number *= Math.pow(10, expsign * exponent);
}
if (startIndex == this.currentIndex) {
return null;
}
skipOptionalSpacesOrDelimiter();
return number;
}
private double parseArcFlag() {
if (this.currentIndex >= this.endIndex) {
// return null;
throw new RuntimeException("");
}
Integer flag = null;
char flagChar = this.string[this.currentIndex];
this.currentIndex++;
if (flagChar == '0') {
flag = 0;
} else if (flagChar == '1') {
flag = 1;
} else {
throw new RuntimeException("");
// return null;
}
skipOptionalSpacesOrDelimiter();
return (double) flag;
}
private boolean validCommand(char c) {
switch (c) {
case 'Z':
case 'M':
case 'L':
case 'C':
case 'Q':
case 'A':
case 'H':
case 'V':
case 'S':
case 'T':
case 'z':
case 'm':
case 'l':
case 'c':
case 'q':
case 'a':
case 'h':
case 'v':
case 's':
case 't':
return true;
default:
return false;
}
}
}
public static double degToRad(double degrees) {
return (Math.PI * degrees) / 180;
}
public static double[] rotate(double x, double y, double angleRad) {
double X = x * Math.cos(angleRad) - y * Math.sin(angleRad);
double Y = x * Math.sin(angleRad) + y * Math.cos(angleRad);
return new double[]{X, Y};
}
public static List<Segment> absolutizePathData(List<Segment> pdata) {
double currentX = 0;
double currentY = 0;
double subpathX = 0;
double subpathY = 0;
double x = 0;
double y = 0;
double x1 = 0;
double y1 = 0;
double x2 = 0;
double y2 = 0;
var length = pdata.size();
for (int index=0; index < length; index++) {
Segment segment = pdata.get(index);
char command = segment.command;
double[] params = segment.params;
switch (command) {
case 'M':
x = params[0];
y = params[1];
subpathX = x;
subpathY = y;
currentX = x;
currentY = y;
break;
case 'm':
x = currentX + params[0];
y = currentY + params[1];
segment.command = 'M';
segment.params[0] = x;
segment.params[1] = y;
subpathX = x;
subpathY = y;
currentX = x;
currentY = y;
break;
case 'L':
x = params[0];
y = params[1];
currentX = x;
currentY = y;
break;
case 'l':
x = currentX + params[0];
y = currentY + params[1];
segment.command = 'L';
segment.params[0] = x;
segment.params[1] = y;
currentX = x;
currentY = y;
break;
case 'C':
x = params[4];
y = params[5];
currentX = x;
currentY = y;
break;
case 'c':
x1 = currentX + params[0];
y1 = currentY + params[1];
x2 = currentX + params[2];
y2 = currentY + params[3];
x = currentX + params[4];
y = currentY + params[5];
segment.command = 'C';
segment.params[0] = x1;
segment.params[1] = y1;
segment.params[2] = x2;
segment.params[3] = y2;
segment.params[4] = x;
segment.params[5] = y;
currentX = x;
currentY = y;
break;
case 'Q':
x = params[2];
y = params[3];
currentX = x;
currentY = y;
break;
case 'q':
x1 = currentX + params[0];
y1 = currentY + params[1];
x = currentX + params[2];
y = currentY + params[3];
segment.command = 'Q';
segment.params[0] = x1;
segment.params[1] = y1;
segment.params[2] = x;
segment.params[3] = y;
currentX = x;
currentY = y;
break;
case 'A':
x = params[5];
y = params[6];
currentX = x;
currentY = y;
break;
case 'a':
x = currentX + params[5];
y = currentY + params[6];
segment.command = 'A';
segment.params[5] = x;
segment.params[6] = y;
currentX = x;
currentY = y;
break;
case 'H':
x = params[0];
currentX = x;
break;
case 'h':
x = currentX + params[0];
segment.command = 'H';
segment.params[0] = x;
currentX = x;
break;
case 'V':
y = params[0];
currentY = y;
break;
case 'v':
y = currentY + params[0];
segment.command = 'V';
segment.params[0] = y;
currentY = y;
break;
case 'S':
x = params[2];
y = params[3];
currentX = x;
currentY = y;
break;
case 's':
x2 = currentX + params[0];
y2 = currentY + params[1];
x = currentX + params[2];
y = currentY + params[3];
segment.command = 'S';
segment.params[0] = x2;
segment.params[1] = y2;
segment.params[2] = x;
segment.params[3] = y;
currentX = x;
currentY = y;
break;
case 'T':
x = params[0];
y = params[1];
currentX = x;
currentY = y;
break;
case 't':
x = currentX + params[0];
y = currentY + params[1];
segment.command = 'T';
segment.params[0] = x;
segment.params[1] = y;
currentX = x;
currentY = y;
break;
case 'Z':
case 'z':
currentX = subpathX;
currentY = subpathY;
segment.command = 'Z';
break;
}
}
return pdata;
}
public static List<Segment> simplifyPathData(List<Segment> pdata) {
var result = new ArrayList<Segment>(pdata.size());
char lastCommand = ' ';
double lastControlX = 0;
double lastControlY = 0;
double currentX = 0;
double currentY = 0;
double subpathX = 0;
double subpathY = 0;
double x = 0;
double y = 0;
double x1 = 0;
double y1 = 0;
double x2 = 0;
double y2 = 0;
double cx1 = 0;
double cy1 = 0;
double cx2 = 0;
double cy2 = 0;
double fa = 0;
double fs = 0;
double phi = 0;
for (int i=0; i<pdata.size(); i++) {
Segment segment = pdata.get(i);
switch(segment.command) {
case 'M':
x = segment.params[0];
y = segment.params[1];
result.add(segment);
subpathX = x;
subpathY = y;
currentX = x;
currentY = y;
break;
case 'C':
x2 = segment.params[2];
y2 = segment.params[3];
x = segment.params[4];
y = segment.params[5];
result.add(segment);
lastControlX = x2;
lastControlY = y2;
currentX = x;
currentY = y;
break;
case 'L':
x = segment.params[0];
y = segment.params[1];
result.add(segment);
currentX = x;
currentY = y;
break;
case 'H':
x = segment.params[0];
segment.command = 'L';
segment.params = new double[] {x, currentY};
result.add(segment);
currentX = x;
break;
case 'V':
y = segment.params[0];
segment.command = 'L';
segment.params = new double[] {currentX, y};
result.add(segment);
currentY = y;
break;
case 'S':
x2 = segment.params[0];
y2 = segment.params[1];
x = segment.params[2];
y = segment.params[3];
if (lastCommand == 'C' || lastCommand == 'S') {
cx1 = currentX + (currentX - lastControlX);
cy1 = currentY + (currentY - lastControlY);
} else {
cx1 = currentX;
cy1 = currentY;
}
segment.command = 'C';
segment.params = new double[] {cx1, cy1, x2, y2, x, y};
result.add(segment);
lastControlX = x2;
lastControlY = y2;
currentX = x;
currentY = y;
break;
case 'T':
x = segment.params[0];
y = segment.params[1];
if (lastCommand == 'Q' || lastCommand == 'T') {
x1 = currentX + (currentX - lastControlX);
y1 = currentY + (currentY - lastControlY);
} else {
x1 = currentX;
y1 = currentY;
}
cx1 = currentX + 2 * (x1 - currentX) / 3;
cy1 = currentY + 2 * (y1 - currentY) / 3;
cx2 = x + 2 * (x1 - x) / 3;
cy2 = y + 2 * (y1 - y) / 3;
segment.command = 'C';
segment.params = new double[] {cx1, cy1, cx2, cy2, x, y};
result.add(segment);
lastControlX = x1;
lastControlY = y1;
currentX = x;
currentY = y;
break;
case 'Q':
x1 = segment.params[0];
y1 = segment.params[1];
x = segment.params[2];
y = segment.params[3];
cx1 = currentX + 2 * (x1 - currentX) / 3;
cy1 = currentY + 2 * (y1 - currentY) / 3;
cx2 = x + 2 * (x1 - x) / 3;
cy2 = y + 2 * (y1 - y) / 3;
segment.command = 'C';
segment.params = new double[] {cx1, cy1, cx2, cy2, x, y};
result.add(segment);
lastControlX = x1;
lastControlY = y1;
currentX = x;
currentY = y;
break;
case 'A':
double rx = Math.abs(segment.params[0]);
double ry = Math.abs(segment.params[1]);
phi = segment.params[2];
fa = segment.params[3];
fs = segment.params[4];
x = segment.params[5];
y = segment.params[6];
var segments = arcToBeziers(currentX, currentY, x, y, fa, fs, rx, ry, phi);
result.addAll(segments);
// if (rx == 0 || ry == 0) {
// segment.command = 'C';
// segment.params = new double[] {currentX, currentY, x, y, x, y};
// result.add(segment);
// } else if (currentX != x || currentY != y) {
// var segments = arcToBeziers(currentX, currentY, x, y, fa, fs, rx, ry, phi);
// result.addAll(segments);
// }
currentX = x;
currentY = y;
break;
case 'Z':
result.add(segment);
currentX = subpathX;
currentY = subpathY;
break;
}
lastCommand = segment.command;
}
return result;
}
public static double unitVectorAngle(double ux, double uy, double vx, double vy) {
double sign = (ux * vy - uy * vx) < 0 ? -1.0 : 1.0;
double dot = ux * vx + uy * vy;
dot = (dot > 1.0) ? 1.0 : (dot < -1.0) ? -1.0 : dot;
return sign * Math.acos(dot);
}
public static double[] getArcCenter(double x1, double y1, double x2, double y2,
double fa, double fs, double rx, double ry,
double sinPhi, double cosPhi) {
double x1p = (cosPhi * ((x1 - x2) / 2)) + (sinPhi * ((y1 - y2) / 2));
double y1p = (-sinPhi * ((x1 - x2) / 2)) + (cosPhi * ((y1 - y2) / 2));
double rxSq = rx * rx;
double rySq = ry * ry;
double x1pSq = x1p * x1p;
double y1pSq = y1p * y1p;
double radicant = rxSq * rySq - rxSq * y1pSq - rySq * x1pSq;
radicant = (radicant < 0) ? 0 : radicant;
radicant /= (rxSq * y1pSq + rySq * x1pSq);
radicant = (Math.sqrt(radicant) * ((fa == fs) ? -1 : 1));
double cxp = radicant * (rx / ry) * y1p;
double cyp = radicant * (-ry / rx) * x1p;
double cx = cosPhi * cxp - sinPhi * cyp + (x1 + x2) / 2;
double cy = sinPhi * cxp + cosPhi * cyp + (y1 + y2) / 2;
double v1x = (x1p - cxp) / rx;
double v1y = (y1p - cyp) / ry;
double v2x = (-x1p - cxp) / rx;
double v2y = (-y1p - cyp) / ry;
double theta1 = unitVectorAngle(1, 0, v1x, v1y);
double dtheta = unitVectorAngle(v1x, v1y, v2x, v2y);
dtheta = (fs == 0 && dtheta > 0) ? dtheta - Math.PI * 2 : dtheta;
dtheta = (fs == 1 && dtheta < 0) ? dtheta + Math.PI * 2 : dtheta;
return new double[] {cx, cy, theta1, dtheta};
}
public static double[] approximateUnitArc(double theta1, double dtheta) {
double alpha = (4.0 / 3.0) * Math.tan(dtheta / 4);
double x1 = Math.cos(theta1);
double y1 = Math.sin(theta1);
double x2 = Math.cos(theta1 + dtheta);
double y2 = Math.sin(theta1 + dtheta);
return new double[] {
x1,
y1,
x1 - y1 * alpha,
y1 + x1 * alpha,
x2 + y2 * alpha,
y2 - x2 * alpha,
x2,
y2
};
}
private static void processCurve(double[] curve, double cx, double cy, double rx, double ry, double sinPhi, double cosPhi) {
double x0 = curve[0] * rx;
double y0 = curve[1] * ry;
double x1 = curve[2] * rx;
double y1 = curve[3] * ry;
double x2 = curve[4] * rx;
double y2 = curve[5] * ry;
double x3 = curve[6] * rx;
double y3 = curve[7] * ry;
double xp0 = cosPhi * x0 - sinPhi * y0;
double yp0 = sinPhi * x0 + cosPhi * y0;
double xp1 = cosPhi * x1 - sinPhi * y1;
double yp1 = sinPhi * x1 + cosPhi * y1;
double xp2 = cosPhi * x2 - sinPhi * y2;
double yp2 = sinPhi * x2 + cosPhi * y2;
double xp3 = cosPhi * x3 - sinPhi * y3;
double yp3 = sinPhi * x3 + cosPhi * y3;
curve[0] = cx + xp0;
curve[1] = cy + yp0;
curve[2] = cx + xp1;
curve[3] = cy + yp1;
curve[4] = cx + xp2;
curve[5] = cy + yp2;
curve[6] = cx + xp3;
curve[7] = cy + yp3;
}
public static List<Segment> arcToBeziers(double x1, double y1, double x2, double y2,
double fa, double fs, double rx, double ry, double phi) {
double tau = Math.PI * 2;
double phiTau = phi * tau / 360;
double sinPhi = Math.sin(phiTau);
double cosPhi = Math.cos(phiTau);
double x1p = ((cosPhi * (x1 - x2)) / 2) + ((sinPhi * (y1 - y2)) / 2);
double y1p = ((-sinPhi * (x1 - x2)) / 2) + ((cosPhi * (y1 - y2)) / 2);
if (x1p == 0 || y1p == 0 || rx == 0 || ry == 0) {
return new ArrayList<>();
}
rx = Math.abs(rx);
ry = Math.abs(ry);
double lambda = (x1p * x1p) / (rx * rx) + (y1p * y1p) / (ry * ry);
rx = (lambda > 1) ? rx * Math.sqrt(lambda) : rx;
ry = (lambda > 1) ? ry * Math.sqrt(lambda) : ry;
var cc = getArcCenter(x1, y1, x2, y2, fa, fs, rx, ry, sinPhi, cosPhi);
var cx = cc[0];
var cy = cc[1];
var theta1 = cc[2];
var dtheta = cc[3];
int segments = Math.max((int) Math.ceil(Math.abs(dtheta) / (tau / 4)), 1);
dtheta /= segments;
var result = new ArrayList<Segment>();
for (int i = 0; i < segments; i++) {
var curve = approximateUnitArc(theta1, dtheta);
processCurve(curve, cx, cy, rx, ry, sinPhi, cosPhi);
result.add(new Segment('C', Arrays.copyOfRange(curve, 2, curve.length)));
theta1 += dtheta;
}
return result;
}
}

View file

@ -0,0 +1,480 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns app.common.svg.path.legacy
"The first svg path parser implementation in pure clojure, used as reference impl
and for tests."
(:require
[app.common.data :as d]
[app.common.geom.point :as gpt]
[app.common.geom.shapes.path :as upg]
[app.common.math :as mth]
[app.common.svg :as csvg]
[app.common.svg.path.command :as upc]
[cuerdas.core :as str]))
(def commands-regex #"(?i)[mzlhvcsqta][^mzlhvcsqta]*")
(def regex #"[+-]?(\d+(\.\d+)?|\.\d+)(e[+-]?\d+)?")
(defn extract-params
[data pattern]
(loop [result []
ptt-idx 0
current {}
entries (re-seq regex data)
match (ffirst entries)]
(if match
(let [[attr-name attr-type] (nth pattern ptt-idx)
ptt-idx (inc ptt-idx)
ptt-cnt (count pattern)
value (if (= attr-type :flag)
(if (= 1 (count match))
(d/parse-integer match)
(d/parse-integer (subs match 0 1)))
(-> match csvg/fix-dot-number d/parse-double))
current (assoc current attr-name value)
result (if (>= ptt-idx ptt-cnt)
(conj result current)
result)
current (if (>= ptt-idx ptt-cnt)
{}
current)
match (if (and (= attr-type :flag)
(> (count match) 1))
(subs match 1)
nil)
entries (if match
entries
(rest entries))
match (if match
match
(ffirst entries))
ptt-idx (if (>= ptt-idx ptt-cnt)
0
ptt-idx)]
(recur result
ptt-idx
current
entries
match))
(if (seq current)
(conj result current)
result))))
;; Path specification
;; https://www.w3.org/TR/SVG11/paths.html
(defmulti parse-command
(fn [cmd]
(str/upper (subs cmd 0 1))))
(defmethod parse-command "M" [cmd]
(let [relative (str/starts-with? cmd "m")
param-list (extract-params cmd [[:x :number]
[:y :number]])]
(into [{:command :move-to
:relative relative
:params (first param-list)}]
(for [params (rest param-list)]
{:command :line-to
:relative relative
:params params}))))
(defmethod parse-command "Z" [_]
[{:command :close-path}])
(defmethod parse-command "L" [cmd]
(let [relative (str/starts-with? cmd "l")
param-list (extract-params cmd [[:x :number]
[:y :number]])]
(for [params param-list]
{:command :line-to
:relative relative
:params params})))
(defmethod parse-command "H" [cmd]
(let [relative (str/starts-with? cmd "h")
param-list (extract-params cmd [[:value :number]])]
(for [params param-list]
{:command :line-to-horizontal
:relative relative
:params params})))
(defmethod parse-command "V" [cmd]
(let [relative (str/starts-with? cmd "v")
param-list (extract-params cmd [[:value :number]])]
(for [params param-list]
{:command :line-to-vertical
:relative relative
:params params})))
(defmethod parse-command "C" [cmd]
(let [relative (str/starts-with? cmd "c")
param-list (extract-params cmd [[:c1x :number]
[:c1y :number]
[:c2x :number]
[:c2y :number]
[:x :number]
[:y :number]])
]
(for [params param-list]
{:command :curve-to
:relative relative
:params params})))
(defmethod parse-command "S" [cmd]
(let [relative (str/starts-with? cmd "s")
param-list (extract-params cmd [[:cx :number]
[:cy :number]
[:x :number]
[:y :number]])]
(for [params param-list]
{:command :smooth-curve-to
:relative relative
:params params})))
(defmethod parse-command "Q" [cmd]
(let [relative (str/starts-with? cmd "q")
param-list (extract-params cmd [[:cx :number]
[:cy :number]
[:x :number]
[:y :number]])]
(for [params param-list]
{:command :quadratic-bezier-curve-to
:relative relative
:params params})))
(defmethod parse-command "T" [cmd]
(let [relative (str/starts-with? cmd "t")
param-list (extract-params cmd [[:x :number]
[:y :number]])]
(for [params param-list]
{:command :smooth-quadratic-bezier-curve-to
:relative relative
:params params})))
(defmethod parse-command "A" [cmd]
(let [relative (str/starts-with? cmd "a")
param-list (extract-params cmd [[:rx :number]
[:ry :number]
[:x-axis-rotation :number]
[:large-arc-flag :flag]
[:sweep-flag :flag]
[:x :number]
[:y :number]])]
(for [params param-list]
{:command :elliptical-arc
:relative relative
:params params})))
(defn smooth->curve
[{:keys [params]} pos handler]
(let [{c1x :x c1y :y} (upg/calculate-opposite-handler pos handler)]
{:c1x c1x
:c1y c1y
:c2x (:cx params)
:c2y (:cy params)}))
(defn quadratic->curve
[sp ep cp]
(let [cp1 (-> (gpt/to-vec sp cp)
(gpt/scale (/ 2 3))
(gpt/add sp))
cp2 (-> (gpt/to-vec ep cp)
(gpt/scale (/ 2 3))
(gpt/add ep))]
{:c1x (:x cp1)
:c1y (:y cp1)
:c2x (:x cp2)
:c2y (:y cp2)}))
(defn unit-vector-angle
[ux uy vx vy]
(let [sign (if (> 0 (- (* ux vy) (* uy vx))) -1.0 1.0)
dot (+ (* ux vx) (* uy vy))
dot (cond
(> dot 1.0) 1.0
(< dot -1.0) -1.0
:else dot)]
(* sign (mth/acos dot))))
(defn- get-arc-center [x1 y1 x2 y2 fa fs rx ry sin-phi cos-phi]
(let [x1p (+ (* cos-phi (/ (- x1 x2) 2)) (* sin-phi (/ (- y1 y2) 2)))
y1p (+ (* (- sin-phi) (/ (- x1 x2) 2)) (* cos-phi (/ (- y1 y2) 2)))
rx-sq (* rx rx)
ry-sq (* ry ry)
x1p-sq (* x1p x1p)
y1p-sq (* y1p y1p)
radicant (- (* rx-sq ry-sq)
(* rx-sq y1p-sq)
(* ry-sq x1p-sq))
radicant (if (< radicant 0) 0 radicant)
radicant (/ radicant (+ (* rx-sq y1p-sq) (* ry-sq x1p-sq)))
radicant (* (mth/sqrt radicant) (if (= fa fs) -1 1))
cxp (* radicant (/ rx ry) y1p)
cyp (* radicant (/ (- ry) rx) x1p)
cx (+ (- (* cos-phi cxp)
(* sin-phi cyp))
(/ (+ x1 x2) 2))
cy (+ (* sin-phi cxp)
(* cos-phi cyp)
(/ (+ y1 y2) 2))
v1x (/ (- x1p cxp) rx)
v1y (/ (- y1p cyp) ry)
v2x (/ (- (- x1p) cxp) rx)
v2y (/ (- (- y1p) cyp) ry)
theta1 (unit-vector-angle 1 0 v1x v1y)
dtheta (unit-vector-angle v1x v1y v2x v2y)
dtheta (if (and (= fs 0) (> dtheta 0)) (- dtheta (* mth/PI 2)) dtheta)
dtheta (if (and (= fs 1) (< dtheta 0)) (+ dtheta (* mth/PI 2)) dtheta)
]
[cx cy theta1 dtheta]))
(defn approximate-unit-arc
[theta1 dtheta]
;; (js/console.log "LEGACY approximate-unit-arc" theta1 dtheta)
(let [alpha (* (/ 4 3) (mth/tan (/ dtheta 4)))
x1 (mth/cos theta1)
y1 (mth/sin theta1)
x2 (mth/cos (+ theta1 dtheta))
y2 (mth/sin (+ theta1 dtheta))]
[x1 y1 (- x1 (* y1 alpha)) (+ y1 (* x1 alpha)) (+ x2 (* y2 alpha)) (- y2 (* x2 alpha)) x2 y2]))
(defn- process-curve
[curve cc rx ry sin-phi cos-phi]
(reduce (fn [curve i]
(let [x (nth curve i)
y (nth curve (inc i))
x (* x rx)
y (* y ry)
xp (- (* cos-phi x) (* sin-phi y))
yp (+ (* sin-phi x) (* cos-phi y))]
(-> curve
(assoc i (+ xp (nth cc 0)))
(assoc (inc i) (+ yp (nth cc 1))))))
curve
(range 0 (count curve) 2)))
(defn arc->beziers*
[x1 y1 x2 y2 fa fs rx ry phi]
(let [tau (* mth/PI 2)
phi-tau (/ (* phi tau) 360)
sin-phi (mth/sin phi-tau)
cos-phi (mth/cos phi-tau)
x1p (+ (/ (* cos-phi (- x1 x2)) 2)
(/ (* sin-phi (- y1 y2)) 2))
y1p (+ (/ (* (- sin-phi) (- x1 x2)) 2)
(/ (* cos-phi (- y1 y2)) 2))]
(if (or (zero? x1p)
(zero? y1p)
(zero? rx)
(zero? ry))
[]
(let [
rx (mth/abs rx)
ry (mth/abs ry)
lambda (+ (/ (* x1p x1p) (* rx rx))
(/ (* y1p y1p) (* ry ry)))
rx (if (> lambda 1) (* rx (mth/sqrt lambda)) rx)
ry (if (> lambda 1) (* ry (mth/sqrt lambda)) ry)
cc (get-arc-center x1 y1 x2 y2 fa fs rx ry sin-phi cos-phi)
theta1 (nth cc 2)
dtheta (nth cc 3)
segments (mth/max (mth/ceil (/ (mth/abs dtheta) (/ tau 4))) 1)
dtheta (/ dtheta segments)]
(loop [i 0.0
t (double theta1)
r []]
(if (< i segments)
(let [curve (approximate-unit-arc t dtheta)
curve (process-curve curve cc rx ry sin-phi cos-phi)]
(recur (inc i)
(+ t dtheta)
(conj r curve)))
r))))))
(defn arc->beziers
[from-p {:keys [params] :as command}]
(let [to-command
(fn [[_ _ c1x c1y c2x c2y x y]]
{:command :curve-to
:relative (:relative command)
:params {:c1x c1x :c1y c1y
:c2x c2x :c2y c2y
:x x :y y}})
{from-x :x from-y :y} from-p
x (get params :x 0.0)
y (get params :y 0.0)
rx (get params :rx 0.0)
ry (get params :ry 0.0)
x-axis-rotation (get params :x-axis-rotation 0)
large-arc-flag (get params :large-arc-flag 0)
sweep-flag (get params :sweep-flag 0)
result (arc->beziers* from-x from-y x y large-arc-flag sweep-flag rx ry x-axis-rotation)]
(mapv to-command result)))
(defn simplify-commands
"Removes some commands and convert relative to absolute coordinates"
[commands]
(let [simplify-command
;; prev-pos : previous position for the current path. Necessary for relative commands
;; prev-start : previous move-to necessary for Z commands
;; prev-cc : previous command control point for cubic beziers
;; prev-qc : previous command control point for quadratic curves
(fn [[result prev-pos prev-start prev-cc prev-qc] [command _prev]]
(let [command (assoc command :prev-pos prev-pos)
command
(cond-> command
(:relative command)
(-> (assoc :relative false)
(d/update-in-when [:params :c1x] + (:x prev-pos))
(d/update-in-when [:params :c1y] + (:y prev-pos))
(d/update-in-when [:params :c2x] + (:x prev-pos))
(d/update-in-when [:params :c2y] + (:y prev-pos))
(d/update-in-when [:params :cx] + (:x prev-pos))
(d/update-in-when [:params :cy] + (:y prev-pos))
(d/update-in-when [:params :x] + (:x prev-pos))
(d/update-in-when [:params :y] + (:y prev-pos))
(cond->
(= :line-to-horizontal (:command command))
(d/update-in-when [:params :value] + (:x prev-pos))
(= :line-to-vertical (:command command))
(d/update-in-when [:params :value] + (:y prev-pos)))))
params (:params command)
orig-command command
command
(cond-> command
(= :line-to-horizontal (:command command))
(-> (assoc :command :line-to)
(update :params dissoc :value)
(assoc-in [:params :x] (:value params))
(assoc-in [:params :y] (:y prev-pos)))
(= :line-to-vertical (:command command))
(-> (assoc :command :line-to)
(update :params dissoc :value)
(assoc-in [:params :y] (:value params))
(assoc-in [:params :x] (:x prev-pos)))
(= :smooth-curve-to (:command command))
(-> (assoc :command :curve-to)
(update :params dissoc :cx :cy)
(update :params merge (smooth->curve command prev-pos prev-cc)))
(= :quadratic-bezier-curve-to (:command command))
(-> (assoc :command :curve-to)
(update :params dissoc :cx :cy)
(update :params merge (quadratic->curve prev-pos (gpt/point params) (gpt/point (:cx params) (:cy params)))))
(= :smooth-quadratic-bezier-curve-to (:command command))
(-> (assoc :command :curve-to)
(update :params merge (quadratic->curve prev-pos (gpt/point params) (upg/calculate-opposite-handler prev-pos prev-qc)))))
result (if (= :elliptical-arc (:command command))
(into result (arc->beziers prev-pos command))
(conj result command))
next-cc (case (:command orig-command)
:smooth-curve-to
(gpt/point (get-in orig-command [:params :cx]) (get-in orig-command [:params :cy]))
:curve-to
(gpt/point (get-in orig-command [:params :c2x]) (get-in orig-command [:params :c2y]))
(:line-to-horizontal :line-to-vertical)
(gpt/point (get-in command [:params :x]) (get-in command [:params :y]))
(gpt/point (get-in orig-command [:params :x]) (get-in orig-command [:params :y])))
next-qc (case (:command orig-command)
:quadratic-bezier-curve-to
(gpt/point (get-in orig-command [:params :cx]) (get-in orig-command [:params :cy]))
:smooth-quadratic-bezier-curve-to
(upg/calculate-opposite-handler prev-pos prev-qc)
(gpt/point (get-in orig-command [:params :x]) (get-in orig-command [:params :y])))
next-pos (if (= :close-path (:command command))
prev-start
(upc/command->point prev-pos command))
next-start (if (= :move-to (:command command)) next-pos prev-start)]
[result next-pos next-start next-cc next-qc]))
start (first commands)
start (cond-> start
(:relative start)
(assoc :relative false))
start-pos (gpt/point (:params start))]
(->> (map vector (rest commands) commands)
(reduce simplify-command [[start] start-pos start-pos start-pos start-pos])
(first))))
(defn parse
[path-str]
(if (empty? path-str)
path-str
(let [commands (re-seq commands-regex path-str)]
(->> (mapcat parse-command commands)
(simplify-commands)
(map (fn [segment]
;; (prn "LEGACY:" segment)
segment))))))

View file

@ -0,0 +1,910 @@
import cljs from "goog:cljs.core";
const MOVE_TO = cljs.keyword("move-to");
const CLOSE_PATH = cljs.keyword("close-path");
const LINE_TO = cljs.keyword("line-to");
const CURVE_TO = cljs.keyword("curve-to");
const K_COMMAND = cljs.keyword("command");
const K_PARAMS = cljs.keyword("params");
const K_X = cljs.keyword("x");
const K_Y = cljs.keyword("y");
const K_C1X = cljs.keyword("c1x");
const K_C1Y = cljs.keyword("c1y");
const K_C2X = cljs.keyword("c2x");
const K_C2Y = cljs.keyword("c2y");
class Segment {
constructor(command, params) {
this.command = command;
this.params = params;
}
toPersistentMap() {
const fromArray = (data) => {
return cljs.PersistentArrayMap.fromArray(data);
}
let command, params;
switch(this.command) {
case "M":
command = MOVE_TO;
params = fromArray([K_X, this.params[0], K_Y, this.params[1]]);
break;
case "Z":
command = CLOSE_PATH;
params = cljs.PersistentArrayMap.EMPTY;
break;
case "L":
command = LINE_TO;
params = fromArray([K_X, this.params[0], K_Y, this.params[1]]);
break;
case "C":
command = CURVE_TO;
params = fromArray([K_C1X, this.params[0],
K_C1Y, this.params[1],
K_C2X, this.params[2],
K_C2Y, this.params[3],
K_X, this.params[4],
K_Y, this.params[5]]);
break;
default:
command = null
params = null;
}
if (command === null || params === null) {
throw new Error("invalid segment");
}
return fromArray([K_COMMAND, command,
K_PARAMS, params])
}
}
function validCommand(c) {
switch (c) {
case "Z":
case "M":
case "L":
case "C":
case "Q":
case "A":
case "H":
case "V":
case "S":
case "T":
case "z":
case "m":
case "l":
case "c":
case "q":
case "a":
case "h":
case "v":
case "s":
case "t":
return true;
default:
return false;
}
}
class Parser {
constructor(string) {
this._string = string;
this._currentIndex = 0;
this._endIndex = this._string.length;
this._prevCommand = null;
this._skipOptionalSpaces();
}
[Symbol.iterator]() {
return this;
}
next() {
const done = !this.hasNext();
if (done) {
return {done: true};
} else {
return {
done: false,
value: this.parseSegment()
};
}
}
hasNext() {
if (this._currentIndex === 0) {
const command = this._peekSegmentCommand();
return ((this._currentIndex < this._endIndex) &&
(command === "M" || command === "m"));
} else {
return this._currentIndex < this._endIndex;
}
}
parseSegment() {
var ch = this._string[this._currentIndex];
var command = validCommand(ch) ? ch : null;
if (command === null) {
// Possibly an implicit command. Not allowed if this is the first command.
if (this._prevCommand === null) {
return null;
}
// Check for remaining coordinates in the current command.
if ((ch === "+" || ch === "-" || ch === "." || (ch >= "0" && ch <= "9")) && this._prevCommand !== "Z") {
if (this._prevCommand === "M") {
command = "L";
} else if (this._prevCommand === "m") {
command = "l";
} else {
command = this._prevCommand;
}
} else {
command = null;
}
if (command === null) {
return null;
}
} else {
this._currentIndex += 1;
}
this._prevCommand = command;
var params = null;
var cmd = command.toUpperCase();
if (cmd === "H" || cmd === "V") {
params = [this._parseNumber()];
} else if (cmd === "M" || cmd === "L" || cmd === "T") {
params = [this._parseNumber(), this._parseNumber()];
} else if (cmd === "S" || cmd === "Q") {
params = [this._parseNumber(), this._parseNumber(), this._parseNumber(), this._parseNumber()];
} else if (cmd === "C") {
params = [
this._parseNumber(),
this._parseNumber(),
this._parseNumber(),
this._parseNumber(),
this._parseNumber(),
this._parseNumber()
];
} else if (cmd === "A") {
params = [
this._parseNumber(),
this._parseNumber(),
this._parseNumber(),
this._parseArcFlag(),
this._parseArcFlag(),
this._parseNumber(),
this._parseNumber()
];
} else if (cmd === "Z") {
this._skipOptionalSpaces();
params = [];
}
if (params === null || params.indexOf(null) >= 0) {
// Unknown command or known command with invalid params
return null;
} else {
return new Segment(command, params);
}
}
_peekSegmentCommand() {
var ch = this._string[this._currentIndex];
return validCommand(ch) ? ch : null;
}
_isCurrentSpace() {
var ch = this._string[this._currentIndex];
return ch <= " " && (ch === " " || ch === "\n" || ch === "\t" || ch === "\r" || ch === "\f");
}
_skipOptionalSpaces() {
while (this._currentIndex < this._endIndex && this._isCurrentSpace()) {
this._currentIndex += 1;
}
return this._currentIndex < this._endIndex;
}
_skipOptionalSpacesOrDelimiter() {
if (this._currentIndex < this._endIndex &&
!this._isCurrentSpace() &&
this._string[this._currentIndex] !== ",") {
return false;
}
if (this._skipOptionalSpaces()) {
if (this._currentIndex < this._endIndex && this._string[this._currentIndex] === ",") {
this._currentIndex += 1;
this._skipOptionalSpaces();
}
}
return this._currentIndex < this._endIndex;
}
// Parse a number from an SVG path. This very closely follows genericParseNumber(...) from
// Source/core/svg/SVGParserUtilities.cpp.
// Spec: http://www.w3.org/TR/SVG11/single-page.html#paths-PathDataBNF
_parseNumber() {
var exponent = 0;
var integer = 0;
var frac = 1;
var decimal = 0;
var sign = 1;
var expsign = 1;
var startIndex = this._currentIndex;
this._skipOptionalSpaces();
// Read the sign.
if (this._currentIndex < this._endIndex && this._string[this._currentIndex] === "+") {
this._currentIndex += 1;
} else if (this._currentIndex < this._endIndex && this._string[this._currentIndex] === "-") {
this._currentIndex += 1;
sign = -1;
}
if (this._currentIndex === this._endIndex ||
((this._string[this._currentIndex] < "0" || this._string[this._currentIndex] > "9") &&
this._string[this._currentIndex] !== ".")) {
// The first chacter of a number must be one of [0-9+-.].
return null;
}
// Read the integer part, build right-to-left.
var startIntPartIndex = this._currentIndex;
while (this._currentIndex < this._endIndex &&
this._string[this._currentIndex] >= "0" &&
this._string[this._currentIndex] <= "9") {
this._currentIndex += 1; // Advance to first non-digit.
}
if (this._currentIndex !== startIntPartIndex) {
var scanIntPartIndex = this._currentIndex - 1;
var multiplier = 1;
while (scanIntPartIndex >= startIntPartIndex) {
integer += multiplier * (this._string[scanIntPartIndex] - "0");
scanIntPartIndex -= 1;
multiplier *= 10;
}
}
// Read the decimals.
if (this._currentIndex < this._endIndex && this._string[this._currentIndex] === ".") {
this._currentIndex += 1;
// There must be a least one digit following the .
if (this._currentIndex >= this._endIndex ||
this._string[this._currentIndex] < "0" ||
this._string[this._currentIndex] > "9") {
return null;
}
while (this._currentIndex < this._endIndex &&
this._string[this._currentIndex] >= "0" &&
this._string[this._currentIndex] <= "9") {
frac *= 10;
decimal += (this._string[this._currentIndex] - "0") / frac;
this._currentIndex += 1;
}
}
// Read the exponent part.
if (this._currentIndex !== startIndex &&
this._currentIndex + 1 < this._endIndex &&
(this._string[this._currentIndex] === "e" || this._string[this._currentIndex] === "E") &&
(this._string[this._currentIndex + 1] !== "x" && this._string[this._currentIndex + 1] !== "m")) {
this._currentIndex += 1;
// Read the sign of the exponent.
if (this._string[this._currentIndex] === "+") {
this._currentIndex += 1;
} else if (this._string[this._currentIndex] === "-") {
this._currentIndex += 1;
expsign = -1;
}
// There must be an exponent.
if (this._currentIndex >= this._endIndex ||
this._string[this._currentIndex] < "0" ||
this._string[this._currentIndex] > "9") {
return null;
}
while (this._currentIndex < this._endIndex &&
this._string[this._currentIndex] >= "0" &&
this._string[this._currentIndex] <= "9") {
exponent *= 10;
exponent += (this._string[this._currentIndex] - "0");
this._currentIndex += 1;
}
}
var number = integer + decimal;
number *= sign;
if (exponent) {
number *= Math.pow(10, expsign * exponent);
}
if (startIndex === this._currentIndex) {
return null;
}
this._skipOptionalSpacesOrDelimiter();
return number;
}
_parseArcFlag() {
if (this._currentIndex >= this._endIndex) {
return null;
}
var flag = null;
var flagChar = this._string[this._currentIndex];
this._currentIndex += 1;
if (flagChar === "0") {
flag = 0;
} else if (flagChar === "1") {
flag = 1;
} else {
return null;
}
this._skipOptionalSpacesOrDelimiter();
return flag;
}
};
function absolutizePathData(pdata) {
var currentX = null;
var currentY = null;
var subpathX = null;
var subpathY = null;
for (let i=0; i<pdata.length; i++) {
let segment = pdata[i];
switch(segment.command) {
case "M":
var x = segment.params[0];
var y = segment.params[1];
subpathX = x;
subpathY = y;
currentX = x;
currentY = y;
break;
case "m":
var x = currentX + segment.params[0];
var y = currentY + segment.params[1];
segment.command = "M";
segment.params[0] = x;
segment.params[1] = y;
subpathX = x;
subpathY = y;
currentX = x;
currentY = y;
break;
case "L":
var x = segment.params[0];
var y = segment.params[1];
currentX = x;
currentY = y;
break;
case "l":
var x = currentX + segment.params[0];
var y = currentY + segment.params[1];
segment.command = "L";
segment.params[0] = x;
segment.params[1] = y;
currentX = x;
currentY = y;
break;
case "C":
var x = segment.params[4];
var y = segment.params[5];
currentX = x;
currentY = y;
break;
case "c":
var x1 = currentX + segment.params[0];
var y1 = currentY + segment.params[1];
var x2 = currentX + segment.params[2];
var y2 = currentY + segment.params[3];
var x = currentX + segment.params[4];
var y = currentY + segment.params[5];
segment.command = "C";
segment.params[0] = x1;
segment.params[1] = y1;
segment.params[2] = x2;
segment.params[3] = y2;
segment.params[4] = x;
segment.params[5] = y;
currentX = x;
currentY = y;
break;
case "Q":
var x = segment.params[2];
var y = segment.params[3];
currentX = x;
currentY = y;
break;
case "q":
var x1 = currentX + segment.params[0];
var y1 = currentY + segment.params[1];
var x = currentX + segment.params[2];
var y = currentY + segment.params[3];
segment.command = "Q";
segment.params[0] = x1;
segment.params[1] = y1;
segment.params[2] = x;
segment.params[3] = y;
currentX = x;
currentY = y;
break;
case "A":
var x = segment.params[5];
var y = segment.params[6];
currentX = x;
currentY = y;
break;
case "a":
var x = currentX + segment.params[5];
var y = currentY + segment.params[6];
segment.command = "A";
segment.params[5] = x;
segment.params[6] = y;
currentX = x;
currentY = y;
break;
case "H":
var x = segment.params[0];
currentX = x;
break;
case "h":
var x = currentX + segment.params[0];
segment.command = "H";
segment.params[0] = x;
currentX = x;
break;
case "V":
var y = segment.params[0];
currentY = y;
break;
case "v":
var y = currentY + segment.params[0];
segment.command = "V";
segment.params[0] = y;
currentY = y;
break;
case "S":
var x = segment.params[2];
var y = segment.params[3];
currentX = x;
currentY = y;
break;
case "s":
var x2 = currentX + segment.params[0];
var y2 = currentY + segment.params[1];
var x = currentX + segment.params[2];
var y = currentY + segment.params[3];
segment.command = "S";
segment.params[0] = x2;
segment.params[1] = y2;
segment.params[2] = x;
segment.params[3] = y;
currentX = x;
currentY = y;
break;
case "T":
var x = segment.params[0];
var y = segment.params[1]
currentX = x;
currentY = y;
break;
case "t":
var x = currentX + segment.params[0];
var y = currentY + segment.params[1]
segment.command = "T";
segment.params[0] = x;
segment.params[1] = y;
currentX = x;
currentY = y;
break;
case "Z":
case "z":
currentX = subpathX;
currentY = subpathY;
segment.command = "Z";
break;
}
}
return pdata;
}
function unitVectorAngle(ux, uy, vx, vy) {
const sign = (ux * vy - uy * vx) < 0 ? -1.0 : 1.0;
let dot = ux * vx + uy * vy;
dot = (dot > 1.0) ? 1.0 : (dot < -1.0) ? -1.0 : dot;
return sign * Math.acos(dot);
}
function getArcCenter(x1, y1, x2, y2, fa, fs, rx, ry, sinPhi, cosPhi) {
let x1p = (cosPhi * ((x1 - x2) / 2)) + (sinPhi * ((y1 - y2) / 2));
let y1p = (-sinPhi * ((x1 - x2) / 2)) + (cosPhi * ((y1 - y2) / 2));
let rxSq = rx * rx;
let rySq = ry * ry;
let x1pSq = x1p * x1p;
let y1pSq = y1p * y1p;
let radicant = rxSq * rySq - rxSq * y1pSq - rySq * x1pSq;
radicant = (radicant < 0) ? 0 : radicant;
radicant /= (rxSq * y1pSq + rySq * x1pSq);
radicant = (Math.sqrt(radicant) * ((fa === fs) ? -1 : 1))
let cxp = radicant * (rx / ry) * y1p;
let cyp = radicant * (-ry / rx) * x1p;
let cx = cosPhi * cxp - sinPhi * cyp + (x1 + x2) / 2;
let cy = sinPhi * cxp + cosPhi * cyp + (y1 + y2) / 2;
let v1x = (x1p - cxp) / rx;
let v1y = (y1p - cyp) / ry;
let v2x = (-x1p - cxp) / rx;
let v2y = (-y1p - cyp) / ry;
let theta1 = unitVectorAngle(1, 0, v1x, v1y);
let dtheta = unitVectorAngle(v1x, v1y, v2x, v2y);
dtheta = (fs === 0 && dtheta > 0) ? dtheta - Math.PI * 2 : dtheta;
dtheta = (fs === 1 && dtheta < 0) ? dtheta + Math.PI * 2 : dtheta;
return [cx, cy, theta1, dtheta];
}
function approximateUnitArc(theta1, dtheta) {
const alpha = (4.0 / 3.0) * Math.tan(dtheta / 4);
const x1 = Math.cos(theta1);
const y1 = Math.sin(theta1);
const x2 = Math.cos(theta1 + dtheta);
const y2 = Math.sin(theta1 + dtheta);
return [
x1,
y1,
x1 - y1 * alpha,
y1 + x1 * alpha,
x2 + y2 * alpha,
y2 - x2 * alpha,
x2,
y2
];
}
function processCurve(curve, cx, cy, rx, ry, sinPhi, cosPhi) {
const x0 = curve[0] * rx;
const y0 = curve[1] * ry;
const x1 = curve[2] * rx;
const y1 = curve[3] * ry;
const x2 = curve[4] * rx;
const y2 = curve[5] * ry;
const x3 = curve[6] * rx;
const y3 = curve[7] * ry;
const xp0 = cosPhi * x0 - sinPhi * y0;
const yp0 = sinPhi * x0 + cosPhi * y0;
const xp1 = cosPhi * x1 - sinPhi * y1;
const yp1 = sinPhi * x1 + cosPhi * y1;
const xp2 = cosPhi * x2 - sinPhi * y2;
const yp2 = sinPhi * x2 + cosPhi * y2;
const xp3 = cosPhi * x3 - sinPhi * y3;
const yp3 = sinPhi * x3 + cosPhi * y3;
curve[0] = cx + xp0;
curve[1] = cy + yp0;
curve[2] = cx + xp1;
curve[3] = cy + yp1;
curve[4] = cx + xp2;
curve[5] = cy + yp2;
curve[6] = cx + xp3;
curve[7] = cy + yp3;
}
export function arcToBeziers(x1, y1, x2, y2, fa, fs, rx, ry, phi) {
const tau = Math.PI * 2;
const phiTau = phi * tau / 360;
const sinPhi = Math.sin(phiTau);
const cosPhi = Math.cos(phiTau);
let x1p = (cosPhi * (x1 - x2)) / 2 + (sinPhi * (y1 - y2)) / 2;
let y1p = (-sinPhi * (x1 - x2)) / 2 + (cosPhi * (y1 - y2)) / 2;
if (x1p === 0 || y1p === 0 || rx === 0 || ry === 0) {
return [];
}
rx = Math.abs(rx);
ry = Math.abs(ry);
let lambda = (x1p * x1p) / (rx * rx) + (y1p * y1p) / (ry * ry);
rx = (lambda > 1) ? rx * Math.sqrt(lambda) : rx;
ry = (lambda > 1) ? ry * Math.sqrt(lambda) : ry;
const cc = getArcCenter(x1, y1, x2, y2, fa, fs, rx, ry, sinPhi, cosPhi);
const cx = cc[0];
const cy = cc[1];
let theta1 = cc[2];
let dtheta = cc[3];
const segments = Math.max(Math.ceil(Math.abs(dtheta) / (tau / 4)), 1);
dtheta /= segments;
const result = [];
for (let i = 0; i < segments; i++) {
const curve = approximateUnitArc(theta1, dtheta);
processCurve(curve, cx, cy, rx, ry, sinPhi, cosPhi);
result.push(new Segment("C", curve.slice(2)));
theta1 += dtheta;
}
return result;
}
// Takes path data that consists only from absolute commands,
// returns path data that consists only from "M", "L", "C" and "Z"
// commands.
function simplifyPathData(pdata) {
var result = [];
var lastType = null;
var lastControlX = null;
var lastControlY = null;
var currentX = null;
var currentY = null;
var subpathX = null;
var subpathY = null;
for (let i=0; i<pdata.length; i++) {
const segment = pdata[i];
switch(segment.command) {
case "M":
var x = segment.params[0];
var y = segment.params[1];
result.push(segment);
subpathX = x;
subpathY = y;
currentX = x;
currentY = y;
break;
case "C":
var x2 = segment.params[2];
var y2 = segment.params[3];
var x = segment.params[4];
var y = segment.params[5];
result.push(segment);
lastControlX = x2;
lastControlY = y2;
currentX = x;
currentY = y;
break;
case "L":
var x = segment.params[0];
var y = segment.params[1];
result.push(segment);
currentX = x;
currentY = y;
break;
case "H":
var x = segment.params[0];
segment.command = "L";
segment.params = [x, currentY];
result.push(segment);
currentX = x;
break;
case "V":
var y = segment.params[0];
segment.command = "L";
segment.params = [currentX, y];
result.push(segment);
currentY = y;
break;
case "S":
var x2 = segment.params[0];
var y2 = segment.params[1];
var x = segment.params[2];
var y = segment.params[3];
var cx1, cy1;
if (lastType === "C" || lastType === "S") {
cx1 = currentX + (currentX - lastControlX);
cy1 = currentY + (currentY - lastControlY);
} else {
cx1 = currentX;
cy1 = currentY;
}
segment.command = "C";
segment.params = [cx1, cy1, x2, y2, x, y];
result.push(segment);
lastControlX = x2;
lastControlY = y2;
currentX = x;
currentY = y;
break;
case "T":
var x = segment.params[0];
var y = segment.params[1];
var x1, y1;
if (lastType === "Q" || lastType === "T") {
x1 = currentX + (currentX - lastControlX);
y1 = currentY + (currentY - lastControlY);
} else {
x1 = currentX;
y1 = currentY;
}
var cx1 = currentX + 2 * (x1 - currentX) / 3;
var cy1 = currentY + 2 * (y1 - currentY) / 3;
var cx2 = x + 2 * (x1 - x) / 3;
var cy2 = y + 2 * (y1 - y) / 3;
segment.command = "C";
segment.params = [cx1, cy1, cx2, cy2, x, y];
result.push(segment);
lastControlX = x1;
lastControlY = y1;
currentX = x;
currentY = y;
break;
case "Q":
var x1 = segment.params[0];
var y1 = segment.params[1];
var x = segment.params[2];
var y = segment.params[3];
var cx1 = currentX + 2 * (x1 - currentX) / 3;
var cy1 = currentY + 2 * (y1 - currentY) / 3;
var cx2 = x + 2 * (x1 - x) / 3;
var cy2 = y + 2 * (y1 - y) / 3;
segment.command = "C";
segment.params = [cx1, cy1, cx2, cy2, x, y];
result.push(segment);
lastControlX = x1;
lastControlY = y1;
currentX = x;
currentY = y;
break;
case "A":
var rx = Math.abs(segment.params[0]);
var ry = Math.abs(segment.params[1]);
var phi = segment.params[2];
var fa = segment.params[3];
var fs = segment.params[4];
var x = segment.params[5];
var y = segment.params[6];
if (rx === 0 || ry === 0) {
segment.command = "C";
segment.params = [currentX, currentY, x, y, x, y];
result.add(segment);
currentX = x;
currentY = y;
} else if (currentX !== x || currentY !== y) {
var segments = arcToBeziers(currentX, currentY, x, y, fa, fs, rx, ry, phi);
result.push(...segments);
currentX = x;
currentY = y;
}
break;
case "Z":
result.push(segment);
currentX = subpathX;
currentY = subpathY;
break;
}
lastCommand = segment.command;
}
return result;
}
export function parse(string) {
if (!string || string.length === 0) return [];
var source = new Parser(string);
var result = Array.from(source);
result = absolutizePathData(result);
result = simplifyPathData(result);
return result;
}

Binary file not shown.

View file

@ -7,11 +7,141 @@
(ns common-tests.svg-path-test
(:require
[app.common.data :as d]
[app.common.pprint :as pp]
[app.common.math :as mth]
[app.common.svg.path :as svg.path]
[app.common.svg.path.legacy :as svg.path.legacy]
[clojure.test :as t]
#?(:cljs [common-tests.arc-to-bezier :as impl])))
(t/deftest parse-test-1
(let [data (str "m -994.563 4564.1423 149.3086 -52.8821 30.1828 "
"-1.9265 5.2446 -117.5157 98.6828 -43.7312 219.9492 "
"9.5361 9.0977 121.0797 115.0586 12.7148 -1.1774 "
"75.7109 134.7524 3.1787 -6.1008 85.0544 -137.3211 "
"59.9137 -301.293 -1.0595 -51.375 25.7186 -261.0492 -7.706 ")
result1 (->> (svg.path/parse data)
(mapv (fn [entry]
(update entry :params #(into (sorted-map) %)))))
result2 (->> (svg.path.legacy/parse data)
(mapv (fn [entry]
(update entry :params #(into (sorted-map) %)))))]
(t/is (= 15
(count result1)
(count result2)))
(dotimes [i (count result1)]
(let [item1 (nth result1 i)
item2 (nth result2 i)]
(t/is (= (:command item1)
(:command item2)))
(t/is (= (:params item1)
(:params item2)))
#_(println "------------------------")
#_(pp/pprint (dissoc item1 :relative))
#_(pp/pprint (dissoc item2 :prev-pos :relative))))))
(t/deftest parse-test-2
(let [data (str "M259.958 89.134c-6.88-.354-10.484-1.241-12.44-3.064-1.871-1.743-6.937-3.098-15.793-4.226-7.171-.913"
"-17.179-2.279-22.24-3.034-5.06-.755-15.252-2.016-22.648-2.8-18.685-1.985-35.63-4.223-38.572-5.096"
"-3.655-1.084-3.016-3.548.708-2.726 1.751.387 13.376 1.701 25.833 2.922 12.456 1.22 29.018 3.114 36.803 "
"4.208 29.94 4.206 29.433 4.204 34.267.136 3.787-3.186 5.669-3.669 14.303-3.669 14.338 0 17.18 1.681 "
"12.182 7.205-2.053 2.268-1.994 2.719.707 5.42 3.828 3.827 3.74 5.846-.238 5.5-1.752-.153-7.544-.502-12.872"
"-.776zm7.563-3.194c0-.778-1.751-1.352-3.892-1.274l-3.893.141 3.539 1.133c1.946.624 3.698 1.197 3.893 1.275"
".194.077.354-.496.354-1.275zm-15.899-8.493c1.43-2.29 1.414-2.83-.084-2.83-2.05 0-5.25 2.76-5.25 4.529 0 "
"2.226 3.599 1.08 5.334-1.699zm8.114 0c2.486-2.746 2.473-2.83-.438-2.83-1.65 0-3.683 1.273-4.516 2.83-1.175 "
"2.196-1.077 2.831.438 2.831 1.075 0 3.107-1.274 4.516-2.83zm7.814.674c2.858-3.444.476-4.085-3.033-.816"
"-2.451 2.284-2.677 2.973-.975 2.973 1.22 0 3.023-.97 4.008-2.157zm-49.571-4.509c-1.168-.43-3.294-1.802-4.725"
"-3.051-2.112-1.843-9.304-2.595-38.219-3.994-46.474-2.25-63-4.077-60.27-6.665.324-.308 9.507.261 20.406 "
"1.264 10.9 1.003 31.16 2.258 45.024 2.789l25.207.964 4.625-3.527c4.313-3.29 5.41-3.474 16.24-2.732 6.389"
".438 11.981 1.388 12.428 2.111.447.723-.517 2.73-2.141 4.46l-2.954 3.144c1.607 1.697 3.308 3.289 5.049 "
"4.845 3.248 2.189-5.438 1.289-8.678 1.284-5.428-.061-10.825-.463-11.992-.892zm12.74-3.242c-1.123-.694-2.36"
"-.943-2.75-.554-.389.39.21 1.275 1.334 1.97 1.122.693 2.36.942 2.749.553.389-.39-.21-1.275-1.334-1.97zm"
"-5.663 0a1.42 1.42 0 00-1.415-1.416 1.42 1.42 0 00-1.416 1.416 1.42 1.42 0 001.416 1.415 1.42 1.42 0 001"
".415-1.415zm-8.464-6.404c.984-1.187 1.35-2.598.813-3.135-1.181-1.18-5.408 1.297-6.184 3.624-.806 2.42 "
"3.265 2.048 5.37-.49zm6.863.258c.867-1.045 1.163-2.313.658-2.819-1.063-1.062-4.719 1.631-4.719 3.476 0 "
"1.864 2.274 1.496 4.061-.657zm8.792-.36c1.637-1.972 1.448-2.197-1.486-1.77-1.848.27-3.622 1.287-3.943 2.26"
"-.838 2.547 3.212 2.181 5.429-.49zm32.443-4.11c-6.156-2.228-67.1-6.138-119.124-7.642-39.208-1.134-72.072"
"-.928-94.618.593-6.617.446-19.681 1.16-29.03 1.587-15.798.72-17.183.573-19.588-2.085-4.498-4.97-2.544-7.857 "
"6.39-9.44 4.394-.778 9.164-2.436 10.6-3.685 5.44-4.729 20.332-14.06 31.14-19.509C65.717 11.88 78.955 7.79 "
"103.837 3.08 121.686-.3 125.552-.642 129.318.82c2.44.948 12.4 1.948 22.132 2.221 15.37.432 20.004 1.18 "
"35.294 5.698 22.36 6.606 39.732 15.1 56.55 27.653 7.307 5.452 14.086 9.913 15.066 9.913.98 0 2.148.956 "
"2.596 2.124.55 1.432 2.798 2.123 6.914 2.123 6.213 0 12.4 3.046 12.38 6.096-.012 1.75-6.502 5.353-9.118 "
"5.063-.818-.09-3.717-.972-6.442-1.958zm-16.986-7.436c0-1.575-33.326-18.118-43.173-21.43-23.008-7.739-54.084"
"-12.922-77.136-12.866-16.863.041-37.877 3.628-52.465 8.956-18.062 6.596-26.563 10.384-29.181 13.002-1.205 "
"1.205-5.306 3.769-9.112 5.698-7.754 3.929-8.841 5.482-3.029 4.325 13.494-2.685 66.794-3.773 110.913-2.264 "
"38.005 1.3 96.812 4.435 102.122 5.443.584.111 1.061-.277 1.061-.864zm-236.39-3.18c0-.78-1.592-1.416-3.539"
"-1.416-1.946 0-3.538.637-3.538 1.415 0 .779 1.592 1.416 3.538 1.416 1.947 0 3.54-.637 3.54-1.416zm7.078"
"-1.416c0-.779-.956-1.416-2.124-1.416-1.167 0-2.123.637-2.123 1.416 0 .778.956 1.415 2.123 1.415 1.168 0 "
"2.124-.637 2.124-1.415zm11.734-4.437c3.278-1.661 6.278-3.483 6.667-4.048 1.366-1.98 20.645-11.231 32.557"
"-15.622 11.862-4.372 36.546-9.865 44.327-9.865 3.485 0 3.867-.404 3.012-3.185-.538-1.752-1.177-3.41-1.42"
"-3.685-.907-1.026-36.72 7.16-45.065 10.302-17.226 6.484-47.566 24.27-47.566 27.886 0 1.786.845 1.585 7.488"
"-1.783zm206.254-5.577c-12.298-10.518-53.842-27.166-70.896-28.41-5.526-.404-6.3-.097-6.695 2.655-.33 2.307"
".402 3.275 2.831 3.742 32.436 6.237 52.205 12.315 66.975 20.594 11.904 6.673 14.477 7.141 7.785 1.419z"
"M150.1 11.04c-1.949-3.64-7.568-4.078-6.886-.538.256 1.329 2.054 2.817 3.997 3.309 4.498 1.137 4.816.832 "
"2.888-2.771zm6.756.94c-.248-1.752-1.026-3.185-1.727-3.185-.7 0-1.493 1.433-1.76 3.185-.328 2.152.232 "
"3.185 1.727 3.185 1.485 0 2.064-1.047 1.76-3.185zm-30.178-2.458c0-2.303-.908-3.694-2.627-4.025-3.6-.694"
"-5.23 1.301-4.22 5.166 1.216 4.647 6.847 3.709 6.847-1.14zm12.544 2.104c-.448-1.168-1.224-2.132-1.725"
"-2.142-.5-.013-2.343-.404-4.095-.873-2.569-.689-3.185-.274-3.185 2.142 0 2.476.854 2.996 4.91 2.996 "
"3.783 0 4.723-.487 4.095-2.123z")
result1 (->> (svg.path/parse data)
(mapv (fn [entry]
(update entry :params #(into (sorted-map) %)))))
result2 (->> (svg.path.legacy/parse data)
(mapv (fn [entry]
(update entry :params #(into (sorted-map) %)))))]
(t/is (= 165
(count result1)
(count result2)))
(dotimes [i (count result1)]
(let [item1 (nth result1 i)
item2 (nth result2 i)]
(t/is (= (:command item1)
(:command item2)))
;; (println "================" (:command item1))
;; (pp/pprint (:params item1))
;; (println "---------")
;; (pp/pprint (:params item2))
(doseq [[k v] (:params item1)]
(t/is (mth/close? v (get-in item2 [:params k]) 0.0000001)))))))
(t/deftest parse-test-3
(let [data (str "m-5.663 0a1.42 1.42 0 00-1.415-1.416 1.42 1.42 0 00-1.416 1.416 "
"1.42 1.42 0 001.416 1.415 1.42 1.42 0 001.415-1.415z")
result1 (->> (svg.path/parse data)
(mapv (fn [entry]
(update entry :params #(into (sorted-map) %)))))
result2 (->> (svg.path.legacy/parse data)
(mapv (fn [entry]
(update entry :params #(into (sorted-map) %)))))]
(t/is (= 6
(count result1)
(count result2)))
(dotimes [i (count result1)]
(let [item1 (nth result1 i)
item2 (nth result2 i)]
(t/is (= (:command item1)
(:command item2)))
(doseq [[k v] (:params item1)]
(t/is (mth/close? v (get-in item2 [:params k]) 0.000000001)))))))
(t/deftest arc-to-bezier-1
(let [expected1 [-1.6697754290362354e-13
-5.258016244624741e-13
@ -30,8 +160,28 @@
30.00000000000016
50.000000000000504]]
(let [[result1 result2 :as total] (svg.path/arc->beziers* 0 0 30 50 0 0 1 162.55 162.45)]
(let [[result1 result2 :as total] (->> (svg.path/arc->beziers 0 0 30 50 0 0 1 162.55 162.45)
(mapv (fn [segment]
(vec (.-params segment)))))]
;; (t/is (= (count total) 2))
;; (println "================" 11111111)
;; (pp/pprint expected1 {:width 50})
;; (println "------------")
;; (pp/pprint result1 {:width 50})
(dotimes [i (count result1)]
(t/is (mth/close? (nth result1 i)
(nth expected1 (+ i 2))
0.0000000001)))
(dotimes [i (count result2)]
(t/is (mth/close? (nth result2 i)
(nth expected2 (+ i 2))
0.0000000001))))
(let [[result1 result2 :as total] (svg.path.legacy/arc->beziers* 0 0 30 50 0 0 1 162.55 162.45)]
(t/is (= (count total) 2))
(dotimes [i (count result1)]
(t/is (mth/close? (nth result1 i)
(nth expected1 i)
@ -40,13 +190,14 @@
(dotimes [i (count result2)]
(t/is (mth/close? (nth result2 i)
(nth expected2 i)
0.000000000001))))
0.000000000001))))))
))
;; "m -994.563 4564.1423 149.3086 -52.8821 30.1828 -1.9265 5.2446 -117.5157 98.6828 -43.7312 219.9492 9.5361 9.0977 121.0797 115.0586 12.7148 -1.1774 75.7109 134.7524 3.1787 -6.1008 85.0544 -137.3211 59.9137 -301.293 -1.0595 -51.375 25.7186 -261.0492 -7.706 " [[:x :number] [:y :number]]
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; LEGACY CODE TESTS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(t/deftest extract-params-1
(t/deftest extract-params-legacy-1
(let [expected [{:x -994.563, :y 4564.1423}
{:x 149.3086, :y -52.8821}
{:x 30.1828, :y -1.9265}
@ -69,16 +220,16 @@
"59.9137 -301.293 -1.0595 -51.375 25.7186 -261.0492 -7.706 ")
pattern [[:x :number] [:y :number]]]
(t/is (= expected (svg.path/extract-params cmdstr pattern)))))
(t/is (= expected (svg.path.legacy/extract-params cmdstr pattern)))))
(t/deftest extract-params-2
(t/deftest extract-params-legacy-2
(let [expected [{:x -994.563, :y 4564.1423 :r 0}]
cmdstr (str "m -994.563 4564.1423 0")
pattern [[:x :number] [:y :number] [:r :flag]]]
(t/is (= expected (svg.path/extract-params cmdstr pattern)))))
(t/is (= expected (svg.path.legacy/extract-params cmdstr pattern)))))
(t/deftest extract-params-3
(t/deftest extract-params-legacy-3
(let [cmdstr (str "a1.42 1.42 0 00-1.415-1.416 1.42 1.42 0 00-1.416 1.416 "
"1.42 1.42 0 001.416 1.415 1.42 1.42 0 001.415-1.415")
@ -94,7 +245,7 @@
[:sweep-flag :flag]
[:x :number]
[:y :number]]
result (svg.path/extract-params cmdstr pattern)]
result (svg.path.legacy/extract-params cmdstr pattern)]
(t/is (= (nth result 0)
(nth expected 0)))