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:
parent
f7acb9bfb8
commit
2dd1858026
8 changed files with 2522 additions and 452 deletions
|
@ -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)))))
|
||||
|
|
943
common/src/app/common/svg/path/Parser.java
Normal file
943
common/src/app/common/svg/path/Parser.java
Normal 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;
|
||||
}
|
||||
}
|
480
common/src/app/common/svg/path/legacy.cljc
Normal file
480
common/src/app/common/svg/path/legacy.cljc
Normal 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))))))
|
||||
|
||||
|
||||
|
||||
|
||||
|
910
common/src/app/common/svg/path/parser.js
Normal file
910
common/src/app/common/svg/path/parser.js
Normal 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.
BIN
common/target/classes/app/common/svg/path/Parser$Segment.class
Normal file
BIN
common/target/classes/app/common/svg/path/Parser$Segment.class
Normal file
Binary file not shown.
BIN
common/target/classes/app/common/svg/path/Parser.class
Normal file
BIN
common/target/classes/app/common/svg/path/Parser.class
Normal file
Binary file not shown.
|
@ -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)))
|
||||
|
|
Loading…
Add table
Reference in a new issue