From ad185c421558bef178af9dccfdd5a68fd21a6ecd Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 27 Dec 2023 20:25:20 +0100 Subject: [PATCH 1/5] :bug: Assign correct fill to match standard svg behavior when no fils found On parsing svg --- common/src/app/common/svg/shapes_builder.cljc | 16 ++++++++-------- .../src/app/main/ui/shapes/custom_stroke.cljs | 10 +++++----- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/common/src/app/common/svg/shapes_builder.cljc b/common/src/app/common/svg/shapes_builder.cljc index 632b1483d..6cb5429aa 100644 --- a/common/src/app/common/svg/shapes_builder.cljc +++ b/common/src/app/common/svg/shapes_builder.cljc @@ -229,6 +229,7 @@ :svg-viewbox selrect :svg-attrs attrs :svg-transform transform + :strokes [] :fills []}) (gsh/translate-to-frame origin))))) @@ -355,9 +356,9 @@ (assoc :svg-attrs props)))))) (defn setup-fill - [shape] - (let [color-attr (str/trim (dm/get-in shape [:svg-attrs :fill])) - color-attr (if (= color-attr "currentColor") clr/black color-attr) + [shape] + (let [color-attr (str/trim (dm/get-in shape [:svg-attrs :fill])) + color-attr (if (= color-attr "currentColor") clr/black color-attr) color-style (str/trim (dm/get-in shape [:svg-attrs :style :fill])) color-style (if (= color-style "currentColor") clr/black color-style)] (cond-> shape @@ -384,6 +385,7 @@ (update :svg-attrs dissoc :fillOpacity) (assoc-in [:fills 0 :fill-opacity] (-> (dm/get-in shape [:svg-attrs :style :fillOpacity]) (d/parse-double 1))))))) + (defn- setup-stroke [shape] (let [attrs (get shape :svg-attrs) @@ -422,7 +424,8 @@ (dissoc :stroke) (dissoc :strokeLinecap) (dissoc :strokeWidth) - (dissoc :strokeOpacity)))))] + (dissoc :strokeOpacity)))) + (d/without-nils))] (cond-> (assoc shape :svg-attrs attrs) (some? color) @@ -434,7 +437,7 @@ (and (some? color) (some? width)) (assoc-in [:strokes 0 :stroke-width] width) - (and (some? linecap) (= (:type shape) :path) + (and (some? linecap) (cfh/path-shape? shape) (or (= linecap :round) (= linecap :square))) (assoc :stroke-cap-start linecap :stroke-cap-end linecap) @@ -464,9 +467,6 @@ (-> (update-in [:svg-attrs :style] dissoc :mixBlendMode) (assoc :blend-mode (-> (dm/get-in shape [:svg-attrs :style :mixBlendMode]) assert-valid-blend-mode))))) - - - (defn tag->name "Given a tag returns its layer name" [tag] diff --git a/frontend/src/app/main/ui/shapes/custom_stroke.cljs b/frontend/src/app/main/ui/shapes/custom_stroke.cljs index 205f83a6e..d8607f6b5 100644 --- a/frontend/src/app/main/ui/shapes/custom_stroke.cljs +++ b/frontend/src/app/main/ui/shapes/custom_stroke.cljs @@ -21,6 +21,9 @@ [cuerdas.core :as str] [rumext.v2 :as mf])) +;; FIXME: this clearly should be renamed to something different, this +;; namespace has also fill related code + (mf/defc inner-stroke-clip-path {::mf/wrap-props false} [{:keys [shape render-id index]}] @@ -449,10 +452,8 @@ (obj/unset! style "fillOpacity") (obj/set! props "fill" (dm/fmt "url(#fill-%-%)" position render-id))) - (and ^boolean (or (obj/contains? svg-styles "fill") - (obj/contains? svg-styles "fillOpacity")) + (and ^boolean (some? svg-styles) ^boolean (obj/contains? svg-styles "fill")) - (let [fill (obj/get svg-styles "fill") opacity (obj/get svg-styles "fillOpacity")] (when (some? fill) @@ -460,8 +461,7 @@ (when (some? opacity) (obj/set! style "fillOpacity" opacity))) - (and ^boolean (or (obj/contains? svg-attrs "fill") - (obj/contains? svg-attrs "fillOpacity")) + (and ^boolean (some? svg-attrs) ^boolean (empty? shape-fills)) (let [fill (obj/get svg-attrs "fill") opacity (obj/get svg-attrs "fillOpacity")] From ae4f14ece2eaaebcf9bd9a09326f6469721f5731 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 27 Dec 2023 20:26:01 +0100 Subject: [PATCH 2/5] :zap: Reduce allocation on custom-shape-strokes react component --- frontend/src/app/main/ui/shapes/custom_stroke.cljs | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/frontend/src/app/main/ui/shapes/custom_stroke.cljs b/frontend/src/app/main/ui/shapes/custom_stroke.cljs index d8607f6b5..8bd912047 100644 --- a/frontend/src/app/main/ui/shapes/custom_stroke.cljs +++ b/frontend/src/app/main/ui/shapes/custom_stroke.cljs @@ -562,13 +562,6 @@ (mf/defc shape-custom-strokes {::mf/wrap-props false} [props] - (let [children (unchecked-get props "children") - shape (unchecked-get props "shape") - position (unchecked-get props "position") - render-id (unchecked-get props "render-id") - props #js {:shape shape - :position position - :render-id render-id}] - [:* - [:> shape-fills props children] - [:> shape-strokes props children]])) + [:* + [:> shape-fills props] + [:> shape-strokes props]]) From 88779dd50b0efc4e3e2b635d1eb69f4bd437950e Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 27 Dec 2023 20:26:36 +0100 Subject: [PATCH 3/5] :paperclip: Fix naming of fills react component --- frontend/src/app/main/ui/shapes/fills.cljs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/frontend/src/app/main/ui/shapes/fills.cljs b/frontend/src/app/main/ui/shapes/fills.cljs index dca1f78a9..cea64f7ca 100644 --- a/frontend/src/app/main/ui/shapes/fills.cljs +++ b/frontend/src/app/main/ui/shapes/fills.cljs @@ -17,7 +17,7 @@ (def no-repeat-padding 1.05) -(mf/defc fills* +(mf/defc internal-fills {::mf/wrap-props false} [props] (let [shape (unchecked-get props "shape") @@ -120,7 +120,6 @@ (mf/defc fills {::mf/wrap-props false} [props] - (let [shape (unchecked-get props "shape") type (dm/get-prop shape :type) image (:fill-image shape) @@ -132,4 +131,4 @@ (> (count fills) 1) (some :fill-color-gradient fills) (some :fill-image fills)) - [:> fills* props]))) + [:> internal-fills props]))) From 62b1dc2a4b7bd90a15b8fc8a2e8da451f8565786 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Thu, 28 Dec 2023 00:26:10 +0100 Subject: [PATCH 4/5] :bug: Fix incorrect arc to curve conversion in some cases --- common/src/app/common/svg/path/Parser.java | 29 ++++++++++-------- common/src/app/common/svg/path/parser.js | 17 +++++++++- .../common/svg/path/Parser$ParserImpl.class | Bin 4529 -> 4529 bytes .../app/common/svg/path/Parser$Segment.class | Bin 1926 -> 1926 bytes .../classes/app/common/svg/path/Parser.class | Bin 8991 -> 9008 bytes 5 files changed, 33 insertions(+), 13 deletions(-) diff --git a/common/src/app/common/svg/path/Parser.java b/common/src/app/common/svg/path/Parser.java index cfceaf944..dec9ff00f 100644 --- a/common/src/app/common/svg/path/Parser.java +++ b/common/src/app/common/svg/path/Parser.java @@ -1,3 +1,11 @@ +/** + * Performance focused pure java implementation of the + * SVG path parser. + * + * @author KALEIDOS INC + * @license MPL-2.0 + */ + package app.common.svg.path; import java.util.Arrays; @@ -61,9 +69,11 @@ public class Parser { 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]}; @@ -781,16 +791,6 @@ public class Parser { 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; @@ -871,7 +871,6 @@ public class Parser { } 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; @@ -911,7 +910,13 @@ public class Parser { 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) { + if (x1p == 0 && y1p == 0) { + // we're asked to draw line to itself + return new ArrayList<>(); + } + + if (rx == 0 || ry == 0) { + // one of the radii is zero return new ArrayList<>(); } diff --git a/common/src/app/common/svg/path/parser.js b/common/src/app/common/svg/path/parser.js index d156d6ca2..804112f07 100644 --- a/common/src/app/common/svg/path/parser.js +++ b/common/src/app/common/svg/path/parser.js @@ -1,3 +1,11 @@ +/** + * Performance focused pure javascript implementation of the + * SVG path parser. + * + * @author KALEIDOS INC + * @license MPL-2.0 + */ + import cljs from "goog:cljs.core"; const MOVE_TO = cljs.keyword("move-to"); @@ -674,7 +682,13 @@ export function arcToBeziers(x1, y1, x2, y2, fa, fs, rx, ry, phi) { 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) { + if (x1p === 0 && y1p === 0) { + // we're asked to draw line to itself + return []; + } + + if (rx === 0 || ry === 0) { + // one of the radii is zero return []; } @@ -877,6 +891,7 @@ function simplifyPathData(pdata) { 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); diff --git a/common/target/classes/app/common/svg/path/Parser$ParserImpl.class b/common/target/classes/app/common/svg/path/Parser$ParserImpl.class index eb42edaca5c54398efe994f6477a873c3df02aa7..e146fff62fb749455be92a4fb6fab9feba33a194 100644 GIT binary patch delta 626 zcmWO2drZ!86vy%R=UY$hdCH=qLPVJ4GBv5#GD9|2D_g5&ELt=fBbN;ola=PSpPA5@ z<{rw^+{%*cFxjwWjb+msvbDKJ3*Z0F+1cmpoL571Lv;OfSHuQ<*+?M83}q9;+01CR zFpjNGw`=*JZTPd@PInMPnK#pKgefh@!ydw^U?i19vzPJgBc1)^a)3MzvX(=3r9%lv zD5IL)9J7fUJ2}NsPTQ{5R%f`vSsFP<3m19KB|501i_7$Kl>zGILxTo!O<{O7hDJ^1 zy5iHgp=54q9=DXnZDrG>6*Q}eJK92vO1P_XT6KVXs^-3Gd7z6tR0EIH%ws*~iQ0Io zS3FZYZBDYg(Dl;3^vXH!Ugc`H)f*?&lka{@Fz>8Ir?b;@IP!xv=pu@4#`4Kpd}b~^ zEanRt^m^k0!hA}j^;yMlSI5I!oD9u4EG!ekUKmm(CTNKMoz#VJZTidL>BXqRGCp-DQCrC43iH1#Xq zxHQ9rD8WQ&rkScllc*#!OS8=irI>upHC38#4r+lpsZ>*^MaHYerb%h0Md{{+GR+$; zF&)Y>oyvAb1+`9DN`RIzRC$D1>oLkFRs~GgDw4FC6cv)HHDqWVSz7N692_rqF@37! QkE)!7LlRWvln#me4~kfrJ>_ZqG;4) znv|Hyc_q`VG%hHU7A>V!1zc1qUTx#D%DAEmuBwu2s^+@txS>Yc)JnVBxv9H!sEb>A z%x!gZ$4Pb0@PyzwUZh5C5jWB6cU_A{7|xcOu)p9!>SLilRIekYM1%;hJ!{9-k~DdMlCFhH%K zPKF0^(XFBM$;~_Y(l39$DL_WUGhU^dHl>*k%{LE~Zk}nO z=~0I1Ri-mO@b;8!{Ir-5Eg{tMj?yyXw47;L!Az}Wj#jZi`DAGgIV$q{4^Nc4gg^4| PSLIH6&#Sp>pk0Fv#h#`tm foFSf3njwKvhas8KfFXs^jUkQEpCNs+DSIFQ%RLwo delta 128 zcmZqUZ{y!!%*t!Tz`|h6z{z03z{g;^*@1NmW1F1V_%gg=@MHML;Lj+;5WpzT e5Xvac5XPv(5Xor35XI=m5X0!t5IfnFJrDqn6&D2n diff --git a/common/target/classes/app/common/svg/path/Parser.class b/common/target/classes/app/common/svg/path/Parser.class index 13d8402cf57ee7529679c676319c27f42e1df2f6..351b04b7725d058b04ee5e675aed472c50c79063 100644 GIT binary patch delta 1566 zcmYk+X>gX+6$kMD@}v;5k~d_Bga9$dLW5u_pq&U~43;P;8XKutQ5X;e6%Y`3WpgP7 z#n%Ex!X5?!VrZZ^P@xq!#I~qVpasDa3dKaDC~d?-pO3xg%bj!2+&MGnH)nNuLwUo> z1V&=D2_rjoM*?~vf@0+2T69MVZom&?$%z$7rHDgsbinmUL7&*@#D07!_G;qY@ypPW z%TdS`jctk5L3Aa=S_E8;c3guFT#F>GLn_xJ!VSn^9kRI*dEA8VtVb_y!9Z@sAijnn ze4S<7&T{^iL)pk-+`$pt$twPi)!fBja5o?39!}(&oWi}F&3$}^f8?+D7ME~8m-21C z9OXM)&i7f%160!gI>yd3ECjmGE=*VY6;zi*Df;x}9IDimi%1z^^r$7d3|8 zXdM5oN&JtdvQ5+Zou1-l&EXY2!>d{%Xql*0vg#CQo8q)fKhPi4P6rgP57j|`QGz~J zvW_W5r_@pZP@2vvqVvj7t1{K5Y(qJYSFR(f>LkugB{KPt=b+S^Ygt13XIu9o3DVr<=S`WnQeC z{TmH(tp>YJw|J|{y;HY(uPXeWhWL<%|ID9gsQ;=;AJ;IS(r};Eoj$L-{JBQ>3sw1& z?)4Q_2XVSDNY=<8Q}+is8WnWb13`fv3cBgxph#nao~#f0XhP$f$mt+D8OeGa9rXkv znt}{XMYg6PPfsFW(^04y=%JY?)>F7nv(QVk(OYxSS5frS(-@$+xJmO+rui7G1t`}- z4AHZw(sOu7&*Kp-LX#GwMN4o=FW|p=kz%DVY$}wHmzf&>UfVfa+Ef4j5c$;>iMX)@G))UOf_(>wlk(i)@TP;X(!j} z4c2Qnw`&jg>rH;3z5KKG@gu#(ziB_u=xs)u^$x$)dm)(jLl_6b;U83Ph$KHmQlvww z455tBVO8mf?$c3?3ek+#agEUlP0~qC4*@OEX)V@gs?nK{U6a=7oOXnm-qpXt>I-2s z>npW5OJ6!)Uxk9cc9FhuiN1}xmoB-t+B`ttg@i7Le7^VHy5h0A>KTR?nXa|H!GRAt z&L6v-kGQ>$yMs?U!Jj$NO-^yEQ~kX=260Xck{t;$ogNgpQ&8f};AUq970wBUIyV^R zF2P;y8dSS5xZkl^Iqmsu1i0zZmkPeQ3DqrV@+AU}aSJOv{? z72`Y&)BGgndpch94Agoi>OBkFJsWR$4)!^Ucl|W}Iu%AI!&I-pY`=;HUWr9sg$-Vf-Cl!3UWapDj}~vhMc1Lt8*$Z} z*vXsO*;`oTtt|Cx?C)*7&9C!8H*j2-o8(4(ig0HSLHt#WMM^k{|35%dthwvnp6O_h nPDn)tvXF@aWQU7B2c^ivPtX}HAe#j1fPoRE1(y*O1X~a)%BChZWizx2gNXF~xA)I`=A1cqp5NS^MH`BCu1{b> zv?5`6vn(W_71EG{mS}@^$i*+wE}EQJoRo(c+=+PPqcJ*0MZ!Fm1W4~ z(wZBIB|&5b#3%x;LL;t5Jj;>9myyCXNaI>G=Q?C^J+fGdcC12YZbVPMfBe#&sJAQbhqcLn-}R`m#M(3 zbe}8K-CNYtuc?>!=zi~4Z-1x<{C5q0(8pEilj`eJD)Ko!WQF(h6kNk6?D^6HC1WngUBc(>lvhKG}1H% z%{3O8dKOtKMYhHvSL4xM6OgBg=%`8PtUscwCgUDWL4l^Cho<3vO-CQiKwr&7k!GQv z=3toS;`fSRtmdIs3vgKraZQVGD~yPGjxlYF26oH-_0) zYbSSU7vEP64{0}#Y7hURy&=B0LSSz*a!LC_G=B?0>{q-FC|&QWrQTDvK2Qf8)Bt^? zAv&aCI;KkXM&e{4`D5%~Ybiw($7;$G^a#wxl9%={)T@CsC$3ykKpVkkap={Eu%nEuLgtz+EaJN%Oy1u>2dk{uVMJ3h#9LXhvI;6Wz`#cmPwcgtXq zTL-^(o1nzG!EYU%-6EEAY8JJ)6hy`%*3TlvrO5C&(;{O4LFn1OJ From 74447442b8fa5a6a0a67b1efc618026ee9de2622 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Thu, 28 Dec 2023 00:28:49 +0100 Subject: [PATCH 5/5] :sparkles: Add several improvements to svg path parser tests And properly reorganize legacy implementations --- .../app/common/svg/path}/arc_to_bezier.js | 13 +- .../app/common/svg/path/legacy_parser1.cljs | 325 ++++++++++++++++++ .../path/{legacy.cljc => legacy_parser2.cljc} | 28 +- common/test/common_tests/svg_path_test.cljc | 190 +++++++++- 4 files changed, 519 insertions(+), 37 deletions(-) rename common/{test/common_tests => src/app/common/svg/path}/arc_to_bezier.js (93%) create mode 100644 common/src/app/common/svg/path/legacy_parser1.cljs rename common/src/app/common/svg/path/{legacy.cljc => legacy_parser2.cljc} (97%) diff --git a/common/test/common_tests/arc_to_bezier.js b/common/src/app/common/svg/path/arc_to_bezier.js similarity index 93% rename from common/test/common_tests/arc_to_bezier.js rename to common/src/app/common/svg/path/arc_to_bezier.js index bc2c1a843..39dc8d447 100644 --- a/common/test/common_tests/arc_to_bezier.js +++ b/common/src/app/common/svg/path/arc_to_bezier.js @@ -2,7 +2,8 @@ * Arc to Bezier curves transformer * * Is a modified and google closure compatible version of the a2c - * functions by https://github.com/fontello/svgpath + * functions by https://github.com/fontello/svgpath used as reference + * implementation for tests * * @author KALEIDOS INC * @license MIT License @@ -10,11 +11,11 @@ "use strict"; -goog.provide("common_tests.arc_to_bezier"); +goog.provide("app.common.svg.path.arc_to_bezier"); // https://raw.githubusercontent.com/fontello/svgpath/master/lib/a2c.js goog.scope(function() { - const self = common_tests.arc_to_bezier; + const self = app.common.svg.path.arc_to_bezier; var TAU = Math.PI * 2; @@ -123,7 +124,7 @@ goog.scope(function() { return [ x1, y1, x1 - y1*alpha, y1 + x1*alpha, x2 + y2*alpha, y2 - x2*alpha, x2, y2 ]; } - function a2c(x1, y1, x2, y2, fa, fs, rx, ry, phi) { + function calculate_beziers(x1, y1, x2, y2, fa, fs, rx, ry, phi) { var sin_phi = Math.sin(phi * TAU / 360); var cos_phi = Math.cos(phi * TAU / 360); @@ -132,6 +133,8 @@ goog.scope(function() { var x1p = cos_phi*(x1-x2)/2 + sin_phi*(y1-y2)/2; var y1p = -sin_phi*(x1-x2)/2 + cos_phi*(y1-y2)/2; + // console.log("L", x1p, y1p) + if (x1p === 0 && y1p === 0) { // we're asked to draw line to itself return []; @@ -204,5 +207,5 @@ goog.scope(function() { }); } - self.a2c = a2c; + self.calculateBeziers = calculate_beziers; }); diff --git a/common/src/app/common/svg/path/legacy_parser1.cljs b/common/src/app/common/svg/path/legacy_parser1.cljs new file mode 100644 index 000000000..70acc5824 --- /dev/null +++ b/common/src/app/common/svg/path/legacy_parser1.cljs @@ -0,0 +1,325 @@ +;; 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-parser1 + "The first SVG Path parser implementation. + + Written in a mix of CLJS and JS code and used in production until + 1.19, used mainly for tests." + (:require + [app.common.data :as d] + [app.common.geom.point :as gpt] + [app.common.geom.shapes.path :as upg] + [app.common.svg :as csvg] + [app.common.svg.path.arc-to-bezier :as a2b] + [app.common.svg.path.command :as upc] + [cuerdas.core :as str])) + +(def commands-regex #"(?i)[mzlhvcsqta][^mzlhvcsqta]*") + +;; Matches numbers for path values allows values like... -.01, 10, +12.22 +;; 0 and 1 are special because can refer to flags +(def num-regex #"[+-]?(\d+(\.\d+)?|\.\d+)(e[+-]?\d+)?") + +(def flag-regex #"[01]") + +(defn extract-params [cmd-str extract-commands] + (loop [result [] + extract-idx 0 + current {} + remain (-> cmd-str (subs 1) (str/trim))] + + (let [[param type] (nth extract-commands extract-idx) + regex (case type + :flag flag-regex + #_:number num-regex) + match (re-find regex remain)] + + (if match + (let [value (-> match first csvg/fix-dot-number d/read-string) + remain (str/replace-first remain regex "") + current (assoc current param value) + extract-idx (inc extract-idx) + [result current extract-idx] + (if (>= extract-idx (count extract-commands)) + [(conj result current) {} 0] + [result current extract-idx])] + (recur result + extract-idx + current + remain)) + (cond-> result + (seq current) (conj current)))))) + +;; Path specification +;; https://www.w3.org/TR/SVG11/paths.html +(defmulti parse-command (comp str/upper first)) + +(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 arc->beziers* + [from-x from-y x y large-arc-flag sweep-flag rx ry x-axis-rotation] + (a2b/calculateBeziers from-x from-y x y large-arc-flag sweep-flag rx ry x-axis-rotation)) + +(defn arc->beziers [from-p 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 + {:keys [rx ry x-axis-rotation large-arc-flag sweep-flag x y]} (:params command) + 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 [clean-path-str + (-> path-str + (str/trim) + ;; Change "commas" for spaces + (str/replace #"," " ") + ;; Remove all consecutive spaces + (str/replace #"\s+" " ")) + commands (re-seq commands-regex clean-path-str)] + (-> (mapcat parse-command commands) + (simplify-commands))))) + diff --git a/common/src/app/common/svg/path/legacy.cljc b/common/src/app/common/svg/path/legacy_parser2.cljc similarity index 97% rename from common/src/app/common/svg/path/legacy.cljc rename to common/src/app/common/svg/path/legacy_parser2.cljc index 594905906..fab3f8102 100644 --- a/common/src/app/common/svg/path/legacy.cljc +++ b/common/src/app/common/svg/path/legacy_parser2.cljc @@ -4,9 +4,11 @@ ;; ;; 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." +(ns app.common.svg.path.legacy-parser2 + "The second SVG Path parser implementation. + + Written in crossplatform CLJC code. Used meanwhile a hight + performance parser is developed in the 1.20 version." (:require [app.common.data :as d] [app.common.geom.point :as gpt] @@ -16,7 +18,6 @@ [app.common.svg.path.command :as upc] [cuerdas.core :as str])) - (def commands-regex #"(?i)[mzlhvcsqta][^mzlhvcsqta]*") (def regex #"[+-]?(\d+(\.\d+)?|\.\d+)(e[+-]?\d+)?") @@ -296,10 +297,10 @@ y1p (+ (/ (* (- sin-phi) (- x1 x2)) 2) (/ (* cos-phi (- y1 y2)) 2))] - (if (or (zero? x1p) - (zero? y1p) - (zero? rx) - (zero? ry)) + (if (or (and (zero? x1p) + (zero? y1p)) + (and (zero? rx) + (zero? ry))) [] (let [ rx (mth/abs rx) @@ -462,19 +463,10 @@ (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)))))) - - - - - + (simplify-commands))))) diff --git a/common/test/common_tests/svg_path_test.cljc b/common/test/common_tests/svg_path_test.cljc index 3fb89c432..19140f91e 100644 --- a/common/test/common_tests/svg_path_test.cljc +++ b/common/test/common_tests/svg_path_test.cljc @@ -10,9 +10,9 @@ [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] + [app.common.svg.path.legacy-parser2 :as svg.path.legacy2] [clojure.test :as t] - #?(:cljs [common-tests.arc-to-bezier :as impl]))) + #?(:cljs [app.common.svg.path.legacy-parser2 :as svg.path.legacy1]))) (t/deftest parse-test-1 (let [data (str "m -994.563 4564.1423 149.3086 -52.8821 30.1828 " @@ -23,14 +23,25 @@ result1 (->> (svg.path/parse data) (mapv (fn [entry] (update entry :params #(into (sorted-map) %))))) - result2 (->> (svg.path.legacy/parse data) + result2 (->> (svg.path.legacy2/parse data) (mapv (fn [entry] - (update entry :params #(into (sorted-map) %)))))] + (update entry :params #(into (sorted-map) %))))) + + result3 #?(:cljs (->> (svg.path.legacy1/parse data) + (mapv (fn [entry] + (update entry :params #(into (sorted-map) %))))) + :clj nil)] (t/is (= 15 (count result1) (count result2))) + + #?(:cljs + (t/is (= 15 + (count result1) + (count result3)))) + (dotimes [i (count result1)] (let [item1 (nth result1 i) item2 (nth result2 i)] @@ -40,6 +51,14 @@ (t/is (= (:params item1) (:params item2))) + #?(:cljs + (let [item3 (nth result3 i)] + (t/is (= (:command item1) + (:command item3))) + (t/is (= (:params item1) + (:params item3))))) + + #_(println "------------------------") #_(pp/pprint (dissoc item1 :relative)) #_(pp/pprint (dissoc item2 :prev-pos :relative)))))) @@ -92,7 +111,7 @@ result1 (->> (svg.path/parse data) (mapv (fn [entry] (update entry :params #(into (sorted-map) %))))) - result2 (->> (svg.path.legacy/parse data) + result2 (->> (svg.path.legacy2/parse data) (mapv (fn [entry] (update entry :params #(into (sorted-map) %)))))] @@ -108,7 +127,6 @@ (t/is (= (:command item1) (:command item2))) - ;; (println "================" (:command item1)) ;; (pp/pprint (:params item1)) ;; (println "---------") @@ -124,7 +142,7 @@ result1 (->> (svg.path/parse data) (mapv (fn [entry] (update entry :params #(into (sorted-map) %))))) - result2 (->> (svg.path.legacy/parse data) + result2 (->> (svg.path.legacy2/parse data) (mapv (fn [entry] (update entry :params #(into (sorted-map) %)))))] @@ -203,7 +221,7 @@ result1 (->> (svg.path/parse data) (mapv (fn [entry] (update entry :params #(into (sorted-map) %))))) - result2 (->> (svg.path.legacy/parse data) + result2 (->> (svg.path.legacy2/parse data) (mapv (fn [entry] (update entry :params #(into (sorted-map) %)))))] @@ -256,7 +274,7 @@ result1 (->> (svg.path/parse data) (mapv (fn [entry] (update entry :params #(into (sorted-map) %))))) - result2 (->> (svg.path.legacy/parse data) + result2 (->> (svg.path.legacy2/parse data) (mapv (fn [entry] (update entry :params #(into (sorted-map) %)))))] @@ -279,6 +297,61 @@ (t/is (mth/close? v (get-in item2 [:params k]) 0.000000001)) ))))) +(t/deftest parse-test-6 + (let [data (str "M3.078 3.548v16.9a.5.5 0 0 0 1 0v-16.9a.5.5 0 0 0-1 0ZM18.422 11.5" + "H7.582a2.5 2.5 0 0 1-2.5-2.5V6.565a2.5 2.5 0 0 1 2.5-2.5" + "h10.84a2.5 2.5 0 0 1 2.5 2.5V9a2.5 2.5 0 0 1-2.5 2.5Z" + "M7.582 5.065a1.5 1.5 0 0 0-1.5 1.5V9a1.5 1.5 0 0 0 1.5 1.5" + "h10.84a1.5 1.5 0 0 0 1.5-1.5V6.565a1.5 1.5 0 0 0-1.5-1.5Z" + "M13.451 19.938H7.582a2.5 2.5 0 0 1-2.5-2.5V15" + "a2.5 2.5 0 0 1 2.5-2.5h5.869a2.5 2.5 0 0 1 2.5 2.5v2.436" + "a2.5 2.5 0 0 1-2.5 2.502ZM7.582 13.5a1.5 1.5 0 0 0-1.5 1.5v2.436" + "a1.5 1.5 0 0 0 1.5 1.5h5.869a1.5 1.5 0 0 0 1.5-1.5V15" + "a1.5 1.5 0 0 0-1.5-1.5Z") + + result1 (->> (svg.path/parse data) + (mapv (fn [entry] + (update entry :params #(into (sorted-map) %))))) + result2 (->> (svg.path.legacy2/parse data) + (mapv (fn [entry] + (update entry :params #(into (sorted-map) %)))))] + + (t/is (= 47 + (count result1) + (count result2))) + + ;; (pp/pprint result1 {:length 100}) + ;; (pp/pprint result2 {:length 50}) + + (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)) + ))) + + #?(:cljs + (let [result3 (svg.path.legacy1/parse data)] + (t/is (= 47 + (count result1) + (count result3))) + + (dotimes [i (count result1)] + (let [item1 (nth result1 i) + item3 (nth result2 i)] + + (t/is (= (:command item1) + (:command item3))) + + (t/is (= (:params item1) + (:params item3))))))))) + + (t/deftest arc-to-bezier-1 (let [expected1 [-1.6697754290362354e-13 -5.258016244624741e-13 @@ -316,7 +389,7 @@ (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)] + (let [[result1 result2 :as total] (svg.path.legacy2/arc->beziers* 0 0 30 50 0 0 1 162.55 162.45)] (t/is (= (count total) 2)) (dotimes [i (count result1)] @@ -327,7 +400,96 @@ (dotimes [i (count result2)] (t/is (mth/close? (nth result2 i) (nth expected2 i) - 0.000000000001)))))) + 0.000000000001)))) + + #?(:cljs + (let [[result1 result2 :as total] (svg.path.legacy1/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) + 0.000000000001))) + + (dotimes [i (count result2)] + (t/is (mth/close? (nth result2 i) + (nth expected2 i) + 0.000000000001))))) + + )) + +(t/deftest arc-to-bezier-2 + (let [expected1 [3.0779999999999994, + 20.448, + 3.0780000082296834, + 20.724142369096132, + 3.3018576309038683, + 20.94799998509884, + 3.5779999999999994, + 20.94799998509884] + + expected2 [3.5779999999999994, + 20.94799998509884, + 3.854142369096131, + 20.94799998509884, + 4.077999991770315, + 20.724142369096132, + 4.077999999999999, + 20.448]] + + (let [[result1 result2 :as total] (->> (svg.path/arc->beziers 3.078 20.448 4.077999999999999 20.448 0 0 0.5 0.5 0) + (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.legacy2/arc->beziers* 3.078 20.448 4.077999999999999 20.448 0 0 0.5 0.5 0)] + (t/is (= (count total) 2)) + + ;; (println "================" 11111111) + ;; (pp/pprint expected1 {:width 50}) + ;; (println "------------") + ;; (pp/pprint (vec result1) {:width 50}) + + (dotimes [i (count result1)] + (t/is (mth/close? (nth result1 i) + (nth expected1 i) + 0.000000000001))) + + (dotimes [i (count result2)] + (t/is (mth/close? (nth result2 i) + (nth expected2 i) + 0.000000000001)))) + + #?(:cljs + (let [[result1 result2 :as total] (svg.path.legacy1/arc->beziers* 3.078 20.448 4.077999999999999 20.448 0 0 0.5 0.5 0)] + (t/is (= (count total) 2)) + + (dotimes [i (count result1)] + (t/is (mth/close? (nth result1 i) + (nth expected1 i) + 0.000000000001))) + + (dotimes [i (count result2)] + (t/is (mth/close? (nth result2 i) + (nth expected2 i) + 0.000000000001))))) + + )) + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -357,14 +519,14 @@ "59.9137 -301.293 -1.0595 -51.375 25.7186 -261.0492 -7.706 ") pattern [[:x :number] [:y :number]]] - (t/is (= expected (svg.path.legacy/extract-params cmdstr pattern))))) + (t/is (= expected (svg.path.legacy2/extract-params cmdstr pattern))))) (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.legacy/extract-params cmdstr pattern))))) + (t/is (= expected (svg.path.legacy2/extract-params cmdstr pattern))))) (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 " @@ -382,7 +544,7 @@ [:sweep-flag :flag] [:x :number] [:y :number]] - result (svg.path.legacy/extract-params cmdstr pattern)] + result (svg.path.legacy2/extract-params cmdstr pattern)] (t/is (= (nth result 0) (nth expected 0)))