diff --git a/common/src/app/common/svg/path.cljc b/common/src/app/common/svg/path.cljc index 73ba490ed..5951002a1 100644 --- a/common/src/app/common/svg/path.cljc +++ b/common/src/app/common/svg/path.cljc @@ -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))))) diff --git a/common/src/app/common/svg/path/Parser.java b/common/src/app/common/svg/path/Parser.java new file mode 100644 index 000000000..3be70c220 --- /dev/null +++ b/common/src/app/common/svg/path/Parser.java @@ -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 parsePathData(String string) { + if (string == null || string.length() == 0) { + return new ArrayList<>(); + } + + List pdata = new ArrayList<>(); + Iterator parser = new ParserImpl(string); + parser.forEachRemaining(pdata::add); + + return pdata; + } + + public static List 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 { + 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 absolutizePathData(List 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 simplifyPathData(List pdata) { + var result = new ArrayList(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 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 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(); + 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; + } +} diff --git a/common/src/app/common/svg/path/legacy.cljc b/common/src/app/common/svg/path/legacy.cljc new file mode 100644 index 000000000..594905906 --- /dev/null +++ b/common/src/app/common/svg/path/legacy.cljc @@ -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)))))) + + + + + diff --git a/common/src/app/common/svg/path/parser.js b/common/src/app/common/svg/path/parser.js new file mode 100644 index 000000000..9369ef3ad --- /dev/null +++ b/common/src/app/common/svg/path/parser.js @@ -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 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> (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)))