From 5031700af65bcaebbce686ae6028f4d32dd546fd Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Thu, 9 Sep 2021 14:42:05 +0200 Subject: [PATCH 01/16] :sparkles: Add utilities to calculate boolean shapes --- common/src/app/common/geom/point.cljc | 18 +- common/src/app/common/geom/shapes.cljc | 1 - .../src/app/common/geom/shapes/intersect.cljc | 21 ++ common/src/app/common/geom/shapes/path.cljc | 251 ++++++++++------ common/src/app/common/math.cljc | 23 +- frontend/src/app/util/path/bool.cljs | 270 ++++++++++++++++++ frontend/src/app/util/path/geom.cljs | 49 +++- frontend/src/app/util/path/tools.cljs | 2 +- 8 files changed, 532 insertions(+), 103 deletions(-) create mode 100644 frontend/src/app/util/path/bool.cljs diff --git a/common/src/app/common/geom/point.cljc b/common/src/app/common/geom/point.cljc index 0d3feeb06..b73a050e6 100644 --- a/common/src/app/common/geom/point.cljc +++ b/common/src/app/common/geom/point.cljc @@ -22,7 +22,8 @@ (defn ^boolean point? "Return true if `v` is Point instance." [v] - (instance? Point v)) + (or (instance? Point v) + (and (map? v) (contains? v :x) (contains? v :y)))) (defn ^boolean point-like? [{:keys [x y] :as v}] @@ -257,15 +258,12 @@ (and (mth/almost-zero? x) (mth/almost-zero? y))) -(defn line-val - "Given a line with two points p1-p2 and a 'percent'. Returns the point in the vector - generated by these two points. For example: for p1=(0,0) p2=(1,1) and v=0.25 will return - the point (0.25, 0.25)" - [p1 p2 v] - (let [v (-> (to-vec p1 p2) - (scale v))] - (add p1 v))) - +(defn lerp + "Calculates a linear interpolation between two points given a tvalue" + [p1 p2 t] + (let [x (mth/lerp (:x p1) (:x p2) t) + y (mth/lerp (:y p1) (:y p2) t)] + (point x y))) (defn rotate "Rotates the point around center with an angle" diff --git a/common/src/app/common/geom/shapes.cljc b/common/src/app/common/geom/shapes.cljc index 8771ff2e3..7ef49ea81 100644 --- a/common/src/app/common/geom/shapes.cljc +++ b/common/src/app/common/geom/shapes.cljc @@ -156,7 +156,6 @@ (d/export gtr/calc-child-modifiers) ;; PATHS -(d/export gsp/content->points) (d/export gsp/content->selrect) (d/export gsp/transform-content) diff --git a/common/src/app/common/geom/shapes/intersect.cljc b/common/src/app/common/geom/shapes/intersect.cljc index 4b0593dc1..633a74e8b 100644 --- a/common/src/app/common/geom/shapes/intersect.cljc +++ b/common/src/app/common/geom/shapes/intersect.cljc @@ -168,6 +168,26 @@ (is-point-inside-evenodd? (first points) rect-lines) (intersects-lines? rect-lines points-lines)))) +(defn overlaps-rects? + "Check for two rects to overlap. Rects won't overlap only if + one of them is fully to the left or the top" + [rect-a rect-b] + + (let [x1a (:x rect-a) + y1a (:y rect-a) + x2a (+ (:x rect-a) (:width rect-a)) + y2a (+ (:y rect-a) (:height rect-a)) + + x1b (:x rect-b) + y1b (:y rect-b) + x2b (+ (:x rect-b) (:width rect-b)) + y2b (+ (:y rect-b) (:height rect-b))] + + (and (> x2a x1b) + (> x2b x1a) + (> y2a y1b) + (> y2b y1a)))) + (defn overlaps-path? "Checks if the given rect overlaps with the path in any point" [shape rect] @@ -308,3 +328,4 @@ (->> shape :points (every? (partial has-point-rect? rect)))) + diff --git a/common/src/app/common/geom/shapes/path.cljc b/common/src/app/common/geom/shapes/path.cljc index ff39fa7db..46ceb9927 100644 --- a/common/src/app/common/geom/shapes/path.cljc +++ b/common/src/app/common/geom/shapes/path.cljc @@ -11,93 +11,180 @@ [app.common.geom.shapes.rect :as gpr] [app.common.math :as mth])) -(defn content->points [content] - (->> content - (map #(when (-> % :params :x) (gpt/point (-> % :params :x) (-> % :params :y)))) - (remove nil?) - (into []))) - ;; https://medium.com/@Acegikmo/the-ever-so-lovely-b%C3%A9zier-curve-eb27514da3bf ;; https://en.wikipedia.org/wiki/Bernstein_polynomial (defn curve-values "Parametric equation for cubic beziers. Given a start and end and two intermediate points returns points for values of t. If you draw t on a plane you got the bezier cube" - [start end h1 h2 t] + ([[start end h1 h2] t] + (curve-values start end h1 h2 t)) - (let [t2 (* t t) ;; t square - t3 (* t2 t) ;; t cube + ([start end h1 h2 t] + (let [t2 (* t t) ;; t square + t3 (* t2 t) ;; t cube - start-v (+ (- t3) (* 3 t2) (* -3 t) 1) - h1-v (+ (* 3 t3) (* -6 t2) (* 3 t)) - h2-v (+ (* -3 t3) (* 3 t2)) - end-v t3 + start-v (+ (- t3) (* 3 t2) (* -3 t) 1) + h1-v (+ (* 3 t3) (* -6 t2) (* 3 t)) + h2-v (+ (* -3 t3) (* 3 t2)) + end-v t3 - coord-v (fn [coord] - (+ (* (coord start) start-v) - (* (coord h1) h1-v) - (* (coord h2) h2-v) - (* (coord end) end-v)))] + coord-v (fn [coord] + (+ (* (coord start) start-v) + (* (coord h1) h1-v) + (* (coord h2) h2-v) + (* (coord end) end-v)))] - (gpt/point (coord-v :x) (coord-v :y)))) + (gpt/point (coord-v :x) (coord-v :y))))) (defn curve-split "Splits a curve into two at the given parametric value `t`. Calculates the Casteljau's algorithm intermediate points" - [start end h1 h2 t] + ([[start end h1 h2] t] + (curve-split start end h1 h2 t)) - (let [p1 (gpt/line-val start h1 t) - p2 (gpt/line-val h1 h2 t) - p3 (gpt/line-val h2 end t) - p4 (gpt/line-val p1 p2 t) - p5 (gpt/line-val p2 p3 t) - sp (gpt/line-val p4 p5 t)] - [[start sp p1 p4] - [sp end p5 p3]])) + ([start end h1 h2 t] + (let [p1 (gpt/lerp start h1 t) + p2 (gpt/lerp h1 h2 t) + p3 (gpt/lerp h2 end t) + p4 (gpt/lerp p1 p2 t) + p5 (gpt/lerp p2 p3 t) + sp (gpt/lerp p4 p5 t)] + [[start sp p1 p4] + [sp end p5 p3]]))) + +(defn subcurve-range + "Given a curve returns a new curve between the values t1-t2" + ([[start end h1 h2] [t1 t2]] + (subcurve-range start end h1 h2 t1 t2)) + + ([[start end h1 h2] t1 t2] + (subcurve-range start end h1 h2 t1 t2)) + + ([start end h1 h2 t1 t2] + ;; Make sure that t2 is greater than t1 + (let [[t1 t2] (if (< t1 t2) [t1 t2] [t2 t1]) + t2' (/ (- t2 t1) (- 1 t1)) + [_ curve'] (curve-split start end h1 h2 t1)] + (first (curve-split curve' t2'))))) + + +;; https://trans4mind.com/personal_development/mathematics/polynomials/cubicAlgebra.htm +(defn- solve-roots + "Solvers a quadratic or cubic equation given by the parameters a b c d" + ([a b c] + (solve-roots a b c 0)) + + ([a b c d] + (let [sqrt-b2-4ac (mth/sqrt (- (* b b) (* 4 a c)))] + (cond + ;; No solutions + (and (mth/almost-zero? d) (mth/almost-zero? a) (mth/almost-zero? b)) + [] + + ;; Linear solution + (and (mth/almost-zero? d) (mth/almost-zero? a)) + [(/ (- c) b)] + + ;; Cuadratic + (mth/almost-zero? d) + [(/ (+ (- b) sqrt-b2-4ac) + (* 2 a)) + (/ (- (- b) sqrt-b2-4ac) + (* 2 a))] + + ;; Cubic + :else + (let [a (/ a d) + b (/ b d) + c (/ c d) + + p (/ (- (* 3 b) (* a a)) 3) + q (/ (+ (* 2 a a a) (* -9 a b) (* 27 c)) 27) + + p3 (/ p 3) + q2 (/ q 2) + discriminant (+ (* q2 q2) (* p3 p3 p3))] + + (cond + (< discriminant 0) + (let [mp3 (/ (- p) 3) + mp33 (* mp3 mp3 mp3) + r (mth/sqrt mp33) + t (/ (- q) (* 2 r)) + cosphi (cond (< t -1) -1 + (> t 1) 1 + :else t) + phi (mth/acos cosphi) + crtr (mth/cubicroot r) + t1 (* 2 crtr) + root1 (- (* t1 (mth/cos (/ phi 3))) (/ a 3)) + root2 (- (* t1 (mth/cos (/ (+ phi (* 2 mth/PI)) 3))) (/ a 3)) + root3 (- (* t1 (mth/cos (/ (+ phi (* 4 mth/PI)) 3))) (/ a 3))] + + [root1 root2 root3]) + + (= discriminant 0) + (let [u1 (if (< q2 0) (mth/cubicroot (- q2)) (- (mth/cubicroot q2))) + root1 (- (* 2 u1) (/ a 3)) + root2 (- (- u1) (/ a 3))] + [root1 root2]) + + :else + (let [sd (mth/sqrt discriminant) + u1 (mth/cubicroot (- sd q2)) + v1 (mth/cubicroot (+ sd q2)) + root (- u1 v1 (/ a 3))] + [root]))))))) ;; https://pomax.github.io/bezierinfo/#extremities (defn curve-extremities - "Given a cubic bezier cube finds its roots in t. This are the extremities - if we calculate its values for x, y we can find a bounding box for the curve." - [start end h1 h2] + "Calculates the extremities by solving the first derivative for a cubic + bezier and then solving the quadratic formula" + ([[start end h1 h2]] + (curve-extremities start end h1 h2)) - (let [coords [[(:x start) (:x h1) (:x h2) (:x end)] - [(:y start) (:y h1) (:y h2) (:y end)]] + ([start end h1 h2] - coord->tvalue - (fn [[c0 c1 c2 c3]] + (let [coords [[(:x start) (:x h1) (:x h2) (:x end)] + [(:y start) (:y h1) (:y h2) (:y end)]] - (let [a (+ (* -3 c0) (* 9 c1) (* -9 c2) (* 3 c3)) - b (+ (* 6 c0) (* -12 c1) (* 6 c2)) - c (+ (* 3 c1) (* -3 c0)) + coord->tvalue + (fn [[c0 c1 c2 c3]] + (let [a (+ (* -3 c0) (* 9 c1) (* -9 c2) (* 3 c3)) + b (+ (* 6 c0) (* -12 c1) (* 6 c2)) + c (+ (* 3 c1) (* -3 c0))] - sqrt-b2-4ac (mth/sqrt (- (* b b) (* 4 a c)))] + (solve-roots a b c)))] + (->> coords + (mapcat coord->tvalue) - (cond - (and (mth/almost-zero? a) - (not (mth/almost-zero? b))) - ;; When the term a is close to zero we have a linear equation - [(/ (- c) b)] + ;; Only values in the range [0, 1] are valid + (filterv #(and (> % 0.01) (< % 0.99))))))) - ;; If a is not close to zero return the two roots for a cuadratic - (not (mth/almost-zero? a)) - [(/ (+ (- b) sqrt-b2-4ac) - (* 2 a)) - (/ (- (- b) sqrt-b2-4ac) - (* 2 a))] +(defn curve-roots + "Uses cardano algorithm to find the roots for a cubic bezier" + ([[start end h1 h2] coord] + (curve-roots start end h1 h2 coord)) - ;; If a and b close to zero we can't find a root for a constant term - :else - [])))] - (->> coords - (mapcat coord->tvalue) + ([start end h1 h2 coord] - ;; Only values in the range [0, 1] are valid - (filter #(and (>= % 0) (<= % 1))) + (let [coords [[(get start coord) (get h1 coord) (get h2 coord) (get end coord)]] - ;; Pass t-values to actual points - (map #(curve-values start end h1 h2 %))) - )) + coord->tvalue + (fn [[pa pb pc pd]] + + (let [a (+ (* 3 pa) (* -6 pb) (* 3 pc)) + b (+ (* -3 pa) (* 3 pb)) + c pa + d (+ (- pa) (* 3 pb) (* -3 pc) pd)] + + (solve-roots a b c d)))] + (->> coords + (mapcat coord->tvalue) + + ;; Only values in the range [0, 1] are valid + (filterv #(and (> % 0.01) (< % 0.99))))))) (defn command->point ([command] (command->point command nil)) @@ -123,10 +210,12 @@ :curve-to (d/concat [(command->point prev) (command->point command)] - (curve-extremities (command->point prev) - (command->point command) - (command->point command :c1) - (command->point command :c2))) + (let [curve [(command->point prev) + (command->point command) + (command->point command :c1) + (command->point command :c2)]] + (->> (curve-extremities curve) + (mapv #(curve-values curve %))))) [])) extremities (mapcat calc-extremities @@ -302,24 +391,25 @@ "Given a path and a position" [shape position] - (let [point+distance (fn [[cur-cmd prev-cmd]] - (let [from-p (command->point prev-cmd) - to-p (command->point cur-cmd) - h1 (gpt/point (get-in cur-cmd [:params :c1x]) - (get-in cur-cmd [:params :c1y])) - h2 (gpt/point (get-in cur-cmd [:params :c2x]) - (get-in cur-cmd [:params :c2y])) - point - (case (:command cur-cmd) - :line-to - (line-closest-point position from-p to-p) + (let [point+distance + (fn [[cur-cmd prev-cmd]] + (let [from-p (command->point prev-cmd) + to-p (command->point cur-cmd) + h1 (gpt/point (get-in cur-cmd [:params :c1x]) + (get-in cur-cmd [:params :c1y])) + h2 (gpt/point (get-in cur-cmd [:params :c2x]) + (get-in cur-cmd [:params :c2y])) + point + (case (:command cur-cmd) + :line-to + (line-closest-point position from-p to-p) - :curve-to - (curve-closest-point position from-p to-p h1 h2) + :curve-to + (curve-closest-point position from-p to-p h1 h2) - nil)] - (when point - [point (gpt/distance point position)]))) + nil)] + (when point + [point (gpt/distance point position)]))) find-min-point (fn [[min-p min-dist :as acc] [cur-p cur-dist :as cur]] (if (and (some? acc) (or (not cur) (<= min-dist cur-dist))) @@ -331,3 +421,4 @@ (map point+distance) (reduce find-min-point) (first)))) + diff --git a/common/src/app/common/math.cljc b/common/src/app/common/math.cljc index 22ebd97af..145bbf65c 100644 --- a/common/src/app/common/math.cljc +++ b/common/src/app/common/math.cljc @@ -72,17 +72,24 @@ [v] (* v v)) +(defn pow + "Returns the base to the exponent power." + [b e] + #?(:cljs (js/Math.pow b e) + :clj (Math/pow b e))) + (defn sqrt "Returns the square root of a number." [v] #?(:cljs (js/Math.sqrt v) :clj (Math/sqrt v))) -(defn pow - "Returns the base to the exponent power." - [b e] - #?(:cljs (js/Math.pow b e) - :clj (Math/pow b e))) +(defn cubicroot + "Returns the cubic root of a number" + [v] + (if (pos? v) + (pow v (/ 1 3)) + (- (pow (- v) (/ 1 3))))) (defn floor "Returns the largest integer less than or @@ -151,3 +158,9 @@ "Equality for float numbers. Check if the difference is within a range" [num1 num2] (<= (abs (- num1 num2)) float-equal-precision)) + +(defn lerp + "Calculates a the linear interpolation between two values and a given percent" + [v0 v1 t] + (+ (* (- 1 t) v0) + (* t v1))) diff --git a/frontend/src/app/util/path/bool.cljs b/frontend/src/app/util/path/bool.cljs new file mode 100644 index 000000000..24e840840 --- /dev/null +++ b/frontend/src/app/util/path/bool.cljs @@ -0,0 +1,270 @@ +;; 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) UXBOX Labs SL + +(ns app.util.path.bool + (:require + [app.common.data :as d] + [app.common.geom.matrix :as gmt] + [app.common.geom.point :as gpt] + [app.common.geom.shapes.intersect :as gsi] + [app.common.geom.shapes.path :as gpp] + [app.common.geom.shapes.rect :as gpr] + [app.common.math :as mth] + [app.util.path.geom :as upg] + [cuerdas.core :as str])) + +(def ^:const curve-curve-precision 0.001) + +(defn curve->rect + [[from-p to-p :as curve]] + (let [extremes (->> (gpp/curve-extremities curve) + (mapv #(gpp/curve-values curve %)))] + (gpr/points->rect (into [from-p to-p] extremes)))) + +(defn curve-range->rect + [curve from-t to-t] + + (let [[from-p to-p :as curve] (gpp/subcurve-range curve from-t to-t) + extremes (->> (gpp/curve-extremities curve) + (mapv #(gpp/curve-values curve %)))] + (gpr/points->rect (into [from-p to-p] extremes)))) + +(defn line+point->tvalue + [[{x1 :x y1 :y} {x2 :x y2 :y}] {:keys [x y]}] + (if (mth/almost-zero? (- x2 x1)) + (/ (- y y1) (- y2 y1)) + (/ (- x x1) (- x2 x1)))) + +(defn line-line-intersect + [[from-p1 to-p1] [from-p2 to-p2]] + + (let [{x1 :x y1 :y} from-p1 + {x2 :x y2 :y} to-p1 + + {x3 :x y3 :y} from-p2 + {x4 :x y4 :y} to-p2 + + nx (- (* (- x3 x4) (- (* x1 y2) (* y1 x2))) + (* (- x1 x2) (- (* x3 y4) (* y3 x4)))) + + ny (- (* (- y3 y4) (- (* x1 y2) (* y1 x2))) + (* (- y1 y2) (- (* x3 y4) (* y3 x4)))) + + d (- (* (- x1 x2) (- y3 y4)) + (* (- y1 y2) (- x3 x4)))] + + (when-not (mth/almost-zero? d) + ;; ix,iy are the coordinates in the line. We calculate the + ;; tvalue that will return 0-1 as a percentage in the segment + + (let [ix (/ nx d) + iy (/ ny d) + t1 (if (mth/almost-zero? (- x2 x1)) + (/ (- iy y1) (- y2 y1)) + (/ (- ix x1) (- x2 x1))) + t2 (if (mth/almost-zero? (- x4 x3)) + (/ (- iy y3) (- y4 y3)) + (/ (- ix x3) (- x4 x3)))] + + (when (and (> t1 0) (< t1 1) + (> t2 0) (< t2 1)) + [[t1] [t2]]))))) + +(defn line-curve-intersect + [[from-p1 to-p1 :as l1] + [from-p2 to-p2 h1-p2 h2-p2 :as c2]] + + + (let [theta (-> (mth/atan2 (- (:y to-p1) (:y from-p1)) + (- (:x to-p1) (:x from-p1))) + (mth/degrees)) + + transform (-> (gmt/matrix) + (gmt/rotate (- theta)) + (gmt/translate (gpt/negate from-p1))) + + c2' [(gpt/transform from-p2 transform) + (gpt/transform to-p2 transform) + (gpt/transform h1-p2 transform) + (gpt/transform h2-p2 transform)] + + ;; Curve intersections as t-values + curve-ts (->> (gpp/curve-roots c2' :y) + (filterv #(let [curve-v (gpp/curve-values c2 %) + line-t (line+point->tvalue l1 curve-v)] + (and (> line-t 0.001) (< line-t 0.999))))) + + ;; Intersection line-curve points + intersect-ps (->> curve-ts + (mapv #(gpp/curve-values c2 %))) + + line-ts (->> intersect-ps + (mapv #(line+point->tvalue l1 %)))] + + [line-ts curve-ts])) + +(defn curve-curve-intersect + [c1 c2] + + (letfn [(remove-close-ts [ts] + (loop [current (first ts) + pending (rest ts) + acc nil + result []] + (if (nil? current) + result + (if (and (some? acc) + (< (mth/abs (- current acc)) 0.01)) + (recur (first pending) + (rest pending) + acc + result) + + (recur (first pending) + (rest pending) + current + (conj result current)))))) + + (check-range [c1-from c1-to c2-from c2-to] + (let [r1 (curve-range->rect c1 c1-from c1-to) + r2 (curve-range->rect c2 c2-from c2-to)] + + (when (gsi/overlaps-rects? r1 r2) + + (if (and (< (mth/abs (- c1-from c1-to)) curve-curve-precision) + (< (mth/abs (- c2-from c2-to)) curve-curve-precision)) + + [(sorted-set (mth/precision c1-from 4)) + (sorted-set (mth/precision c2-from 4))] + + (let [c1-half (+ c1-from (/ (- c1-to c1-from) 2)) + c2-half (+ c2-from (/ (- c2-to c2-from) 2)) + + [c1-ts-1 c2-ts-1] (check-range c1-from c1-half c2-from c2-half) + [c1-ts-2 c2-ts-2] (check-range c1-from c1-half c2-half c2-to) + [c1-ts-3 c2-ts-3] (check-range c1-half c1-to c2-from c2-half) + [c1-ts-4 c2-ts-4] (check-range c1-half c1-to c2-half c2-to)] + + [(into (sorted-set) (d/concat [] c1-ts-1 c1-ts-2 c1-ts-3 c1-ts-4)) + (into (sorted-set) (d/concat [] c2-ts-1 c2-ts-2 c2-ts-3 c2-ts-4))])))))] + + (let [[c1-ts c2-ts] (check-range 0.005 0.995 0.005 0.995) + c1-ts (remove-close-ts c1-ts) + c2-ts (remove-close-ts c2-ts)] + [c1-ts c2-ts]))) + +(defn- line-to->line + [cmd] + [(:prev cmd) (gpp/command->point cmd)]) + +(defn- curve-to->bezier + [cmd] + [(:prev cmd) + (gpp/command->point cmd) + (gpt/point (-> cmd :params :c1x) (-> cmd :params :c1y)) + (gpt/point (-> cmd :params :c2x) (-> cmd :params :c2y))]) + +(defn- split-command + [cmd values] + (case (:command cmd) + :line-to (upg/split-line-to-ranges (:prev cmd) cmd values) + :curve-to (upg/split-curve-to-ranges (:prev cmd) cmd values) + [cmd])) + +(defn split [seg-1 seg-2] + (let [[ts-seg-1 ts-seg-2] + (cond + (and (= :line-to (:command seg-1)) + (= :line-to (:command seg-2))) + (line-line-intersect (line-to->line seg-1) (line-to->line seg-2)) + + (and (= :line-to (:command seg-1)) + (= :curve-to (:command seg-2))) + (line-curve-intersect (line-to->line seg-1) (curve-to->bezier seg-2)) + + (and (= :curve-to (:command seg-1)) + (= :line-to (:command seg-2))) + (let [[seg-2' seg-1'] + (line-curve-intersect (line-to->line seg-2) (curve-to->bezier seg-1))] + ;; Need to reverse because we send the arguments reversed + [seg-1' seg-2']) + + (and (= :curve-to (:command seg-1)) + (= :curve-to (:command seg-2))) + (curve-curve-intersect (curve-to->bezier seg-1) (curve-to->bezier seg-2)) + + :else + [[] []])] + + [(split-command seg-1 ts-seg-1) + (split-command seg-2 ts-seg-2)])) + +(defn add-previous + ([content] + (add-previous content nil)) + ([content first] + (->> (d/with-prev content) + (mapv (fn [[cmd prev]] + (cond-> cmd + (and (nil? prev) (some? first)) + (assoc :prev first) + + (some? prev) + (assoc :prev (gpp/command->point prev)))))))) + +(defn content-intersect-split + "Given two path contents will return the intersect between them" + [content-a content-b] + + (let [content-a (add-previous content-a) + content-b (add-previous content-b)] + (if (or (empty? content-a) (empty? content-b)) + [content-a content-b] + + (loop [current (first content-a) + pending (rest content-a) + content-b content-b + new-content-a []] + + (if (not (some? current)) + [new-content-a content-b] + + (let [[new-current new-pending new-content-b] + + (loop [current current + pending pending + other (first content-b) + head-content [] + tail-content (rest content-b)] + + (if (not (some? other)) + ;; Finished recorring second content + [current pending head-content] + + ;; We split the current + (let [[new-as new-bs] (split current other) + new-as (add-previous new-as (:prev current)) + new-bs (add-previous new-bs (:prev other))] + + (if (> (count new-as) 1) + ;; We add the new-a's to the stack and change the b then we iterate to the top + (recur (first new-as) + (d/concat [] (rest new-as) pending) + (first tail-content) + (d/concat [] head-content new-bs) + (rest tail-content)) + + ;; No current segment-segment split we continue searching + (recur current + pending + (first tail-content) + (conj head-content other) + (rest tail-content))))))] + + (recur (first new-pending) + (rest new-pending) + new-content-b + (conj new-content-a new-current)))))))) diff --git a/frontend/src/app/util/path/geom.cljs b/frontend/src/app/util/path/geom.cljs index 0478fff8c..08432f41a 100644 --- a/frontend/src/app/util/path/geom.cljs +++ b/frontend/src/app/util/path/geom.cljs @@ -6,6 +6,7 @@ (ns app.util.path.geom (:require + [app.common.data :as d] [app.common.geom.point :as gpt] [app.common.geom.shapes.path :as gshp] [app.util.path.commands :as upc])) @@ -16,21 +17,54 @@ (let [handler-vector (gpt/to-vec point handler)] (gpt/add point (gpt/negate handler-vector)))) -(defn split-line-to [from-p cmd val] +(defn split-line-to + "Given a point and a line-to command will create a two new line-to commands + that will split the original line into two given a value between 0-1" + [from-p cmd t-val] (let [to-p (upc/command->point cmd) - sp (gpt/line-val from-p to-p val)] + sp (gpt/lerp from-p to-p t-val)] [(upc/make-line-to sp) cmd])) -(defn split-curve-to [from-p cmd val] +(defn split-curve-to + "Given the point and a curve-to command will split the curve into two new + curve-to commands given a value between 0-1" + [from-p cmd t-val] (let [params (:params cmd) end (gpt/point (:x params) (:y params)) h1 (gpt/point (:c1x params) (:c1y params)) h2 (gpt/point (:c2x params) (:c2y params)) [[_ to1 h11 h21] - [_ to2 h12 h22]] (gshp/curve-split from-p end h1 h2 val)] + [_ to2 h12 h22]] (gshp/curve-split from-p end h1 h2 t-val)] [(upc/make-curve-to to1 h11 h21) (upc/make-curve-to to2 h12 h22)])) +(defn split-line-to-ranges + "Splits a line into several lines given the points in `values` + for example (split-line-to-ranges p c [0 0.25 0.5 0.75 1] will split + the line into 4 lines" + [from-p cmd values] + (let [to-p (upc/command->point cmd)] + (->> (conj values 1) + (mapv (fn [val] + (upc/make-line-to (gpt/lerp from-p to-p val))))))) + +(defn split-curve-to-ranges + "Splits a curve into several curves given the points in `values` + for example (split-curve-to-ranges p c [0 0.25 0.5 0.75 1] will split + the curve into 4 curves that draw the same curve" + [from-p cmd values] + (let [to-p (upc/command->point cmd) + params (:params cmd) + h1 (gpt/point (:c1x params) (:c1y params)) + h2 (gpt/point (:c2x params) (:c2y params))] + + (->> (d/with-prev (conj values 1)) + (mapv + (fn [[t1 t0]] + (let [t0 (if (nil? t0) 0 t0) + [_ to-p h1' h2'] (gshp/subcurve-range from-p to-p h1 h2 t0 t1)] + (upc/make-curve-to to-p h1' h2'))))))) + (defn opposite-handler "Calculates the coordinates of the opposite handler" [point handler] @@ -47,9 +81,12 @@ (gpt/point old-distance))] (gpt/add point phv2))) -(defn content->points [content] +(defn content->points + "Returns the points in the given content" + [content] (->> content - (map #(when (-> % :params :x) (gpt/point (-> % :params :x) (-> % :params :y)))) + (map #(when (-> % :params :x) + (gpt/point (-> % :params :x) (-> % :params :y)))) (remove nil?) (into []))) diff --git a/frontend/src/app/util/path/tools.cljs b/frontend/src/app/util/path/tools.cljs index f6409f221..3a05c2e1d 100644 --- a/frontend/src/app/util/path/tools.cljs +++ b/frontend/src/app/util/path/tools.cljs @@ -210,7 +210,7 @@ (case (:command cmd) :line-to [index (upg/split-line-to start cmd value)] :curve-to [index (upg/split-curve-to start cmd value)] - :close-path [index [(upc/make-line-to (gpt/line-val start end value)) cmd]] + :close-path [index [(upc/make-line-to (gpt/lerp start end value)) cmd]] nil)) cmd-changes From 9f08153a85c5ea003ae908e9f4c17b73738fe7c0 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Thu, 9 Sep 2021 14:48:32 +0200 Subject: [PATCH 02/16] :sparkles: Created bool shapes --- .../src/app/common/pages/changes_builder.cljc | 59 +++++++++++++++ frontend/src/app/main/data/workspace.cljs | 11 ++- .../src/app/main/data/workspace/booleans.cljs | 66 ++++++++++++++++ .../app/main/data/workspace/shortcuts.cljs | 5 ++ frontend/src/app/main/ui/shapes/bool.cljs | 75 +++++++++++++++++++ .../app/main/ui/workspace/context_menu.cljs | 8 +- .../src/app/main/ui/workspace/shapes.cljs | 16 ++-- .../app/main/ui/workspace/shapes/bool.cljs | 44 +++++++++++ 8 files changed, 276 insertions(+), 8 deletions(-) create mode 100644 common/src/app/common/pages/changes_builder.cljc create mode 100644 frontend/src/app/main/data/workspace/booleans.cljs create mode 100644 frontend/src/app/main/ui/shapes/bool.cljs create mode 100644 frontend/src/app/main/ui/workspace/shapes/bool.cljs diff --git a/common/src/app/common/pages/changes_builder.cljc b/common/src/app/common/pages/changes_builder.cljc new file mode 100644 index 000000000..b16e984fb --- /dev/null +++ b/common/src/app/common/pages/changes_builder.cljc @@ -0,0 +1,59 @@ +;; 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) UXBOX Labs SL + +(ns app.common.pages.changes-builder) + +;; Auxiliary functions to help create a set of changes (undo + redo) + +(defn empty-changes [origin page-id] + (with-meta + {:redo-changes [] + :undo-changes [] + :origin origin} + {::page-id page-id})) + +(defn add-obj + [changes obj] + (let [add-change + {:type :add-obj + :id (:id obj) + :page-id (::page-id (meta changes)) + :parent-id (:parent-id obj) + :frame-id (:frame-id obj) + :index (::index obj) + :obj (dissoc obj ::index :parent-id)} + + del-change + {:type :del-obj + :id (:id obj) + :page-id (::page-id (meta changes))}] + + (-> changes + (update :redo-changes conj add-change) + (update :undo-changes #(into [del-change] %))))) + +(defn change-parent + [changes parent-id shapes] + (let [set-parent-change + {:type :mov-objects + :parent-id parent-id + :page-id (::page-id (meta changes)) + :shapes (->> shapes (mapv :id))} + + mk-undo-change + (fn [shape] + {:type :mov-objects + :page-id (::page-id (meta changes)) + :parent-id (:parent-id shape) + :shapes [(:id shape)] + :index (::index shape)}) + + undo-moves + (->> shapes (mapv mk-undo-change))] + + (-> changes + (update :redo-changes conj set-parent-change) + (update :undo-changes #(into undo-moves %))))) diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index a53fdcee3..476046781 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -23,6 +23,7 @@ [app.config :as cfg] [app.main.data.events :as ev] [app.main.data.messages :as dm] + [app.main.data.workspace.booleans :as dwb] [app.main.data.workspace.changes :as dch] [app.main.data.workspace.common :as dwc] [app.main.data.workspace.drawing :as dwd] @@ -48,7 +49,8 @@ [cljs.spec.alpha :as s] [clojure.set :as set] [cuerdas.core :as str] - [potok.core :as ptk])) + [potok.core :as ptk] + )) ;; (log/set-level! :trace) @@ -1100,6 +1102,10 @@ :group (rx/of (dwc/select-shapes (into (d/ordered-set) [(last shapes)]))) + :bool + ;; TODO + (js/alert "TODO") + :svg-raw nil @@ -1987,3 +1993,6 @@ (d/export dwg/unmask-group) (d/export dwg/group-selected) (d/export dwg/ungroup-selected) + +;; Boolean +(d/export dwb/create-bool) diff --git a/frontend/src/app/main/data/workspace/booleans.cljs b/frontend/src/app/main/data/workspace/booleans.cljs new file mode 100644 index 000000000..6702af71d --- /dev/null +++ b/frontend/src/app/main/data/workspace/booleans.cljs @@ -0,0 +1,66 @@ +;; 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) UXBOX Labs SL + +(ns app.main.data.workspace.booleans + (:require + [app.common.data :as d] + [app.common.geom.shapes :as gsh] + [app.common.pages :as cp] + [app.common.pages.changes-builder :as cb] + [app.common.uuid :as uuid] + [app.main.data.workspace.changes :as dch] + [app.main.data.workspace.common :as dwc] + [app.main.data.workspace.state-helpers :as wsh] + [beicon.core :as rx] + [cuerdas.core :as str] + [potok.core :as ptk])) + +(defn selected-shapes + [state] + (let [objects (wsh/lookup-page-objects state)] + (->> (wsh/lookup-selected state) + (cp/clean-loops objects) + (map #(get objects %)) + (filter #(not= :frame (:type %))) + (map #(assoc % ::index (cp/position-on-parent (:id %) objects))) + (sort-by ::index)))) + +(defn create-bool-data + [type name shapes] + (let [head (first shapes) + selrect (gsh/selection-rect shapes)] + (-> {:id (uuid/next) + :type :bool + :bool-type type + :frame-id (:frame-id head) + :parent-id (:parent-id head) + :name name + ::index (::index head) + :shapes []} + (gsh/setup selrect)))) + + +(defn create-bool + [bool-type] + (ptk/reify ::create-bool-union + ptk/WatchEvent + + (watch [it state _] + (let [page-id (:current-page-id state) + objects (wsh/lookup-page-objects state page-id) + base-name (-> bool-type d/name str/capital (str "-1")) + name (-> (dwc/retrieve-used-names objects) + (dwc/generate-unique-name base-name)) + shapes (selected-shapes state)] + + (when-not (empty? shapes) + (let [boolean-data (create-bool-data bool-type name shapes) + shape-id (:id boolean-data) + changes (-> (cb/empty-changes it page-id) + (cb/add-obj boolean-data) + (cb/change-parent shape-id shapes))] + (rx/of (dch/commit-changes changes) + (dwc/select-shapes (d/ordered-set shape-id))))))))) diff --git a/frontend/src/app/main/data/workspace/shortcuts.cljs b/frontend/src/app/main/data/workspace/shortcuts.cljs index d589df2e8..7373d6e76 100644 --- a/frontend/src/app/main/data/workspace/shortcuts.cljs +++ b/frontend/src/app/main/data/workspace/shortcuts.cljs @@ -260,6 +260,11 @@ :command ["alt" "."] :type "keyup" :fn #(st/emit! (dw/toggle-distances-display false))} + + :create-union {:tooltip (ds/alt "U") + :command ["alt" "u"] + :fn #(st/emit! (dw/create-bool :union))} + }) (defn get-tooltip [shortcut] diff --git a/frontend/src/app/main/ui/shapes/bool.cljs b/frontend/src/app/main/ui/shapes/bool.cljs new file mode 100644 index 000000000..be7337084 --- /dev/null +++ b/frontend/src/app/main/ui/shapes/bool.cljs @@ -0,0 +1,75 @@ +;; 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) UXBOX Labs SL + +(ns app.main.ui.shapes.bool + (:require + [app.common.data :as d] + [app.common.geom.shapes :as gsh] + [app.common.math :as mth] + [app.util.object :as obj] + [app.util.path.bool :as pb] + [app.util.path.geom :as upg] + [app.util.path.shapes-to-path :as stp] + [clojure.set :as set] + [rumext.alpha :as mf])) + +(mf/defc path-points + [{:keys [points color]}] + + [:* + (for [[idx {:keys [x y]}] (d/enumerate points)] + [:circle {:key (str "circle-" idx) + :cx x + :cy y + :r 5 + :style {:fill color + ;;:fillOpacity 0.5 + }}])]) + +(defn bool-shape + [shape-wrapper] + (mf/fnc bool-shape + {::mf/wrap-props false} + [props] + (let [frame (obj/get props "frame") + childs (obj/get props "childs") + shape-1 (stp/convert-to-path (nth childs 0)) + shape-2 (stp/convert-to-path (nth childs 1)) + + content-1 (-> shape-1 gsh/transform-shape (gsh/translate-to-frame frame) :content) + content-2 (-> shape-2 gsh/transform-shape (gsh/translate-to-frame frame) :content) + + + [content-1' content-2'] (pb/content-intersect-split content-1 content-2) + + points-1 (->> (upg/content->points content-1') + (map #(hash-map :x (mth/round (:x %)) + :y (mth/round (:y %)))) + (into #{})) + + points-2 (->> (upg/content->points content-2') + (map #(hash-map :x (mth/round (:x %)) + :y (mth/round (:y %)))) + (into #{})) + + points-3 (set/intersection points-1 points-2)] + + [:* + [:& shape-wrapper {:shape (-> shape-1 #_(assoc :content content-1')) + :frame frame}] + + [:& shape-wrapper {:shape (-> shape-2 #_(assoc :content content-2')) + :frame frame}] + + [:& path-points {:points points-1 :color "#FF0000"}] + [:& path-points {:points points-2 :color "#0000FF"}] + [:& path-points {:points points-3 :color "#FF00FF"}] + + + ]))) + + + diff --git a/frontend/src/app/main/ui/workspace/context_menu.cljs b/frontend/src/app/main/ui/workspace/context_menu.cljs index 29180d9a1..ed9933297 100644 --- a/frontend/src/app/main/ui/workspace/context_menu.cljs +++ b/frontend/src/app/main/ui/workspace/context_menu.cljs @@ -98,8 +98,14 @@ :on-accept confirm-update-remote-component})) do-show-component (st/emitf (dw/go-to-layout :assets)) do-navigate-component-file (st/emitf (dwl/nav-to-component-file - (:component-file shape)))] + (:component-file shape))) + + do-create-bool-shape (st/emitf (dw/create-bool :union))] [:* + ;; + [:& menu-entry {:title ">BOOL" + :on-click do-create-bool-shape}] + ;; [:& menu-entry {:title (tr "workspace.shape.menu.copy") :shortcut (sc/get-tooltip :copy) :on-click do-copy}] diff --git a/frontend/src/app/main/ui/workspace/shapes.cljs b/frontend/src/app/main/ui/workspace/shapes.cljs index b310d2118..70efd1f49 100644 --- a/frontend/src/app/main/ui/workspace/shapes.cljs +++ b/frontend/src/app/main/ui/workspace/shapes.cljs @@ -20,6 +20,7 @@ [app.main.ui.shapes.image :as image] [app.main.ui.shapes.rect :as rect] [app.main.ui.shapes.text.fontfaces :as ff] + [app.main.ui.workspace.shapes.bool :as bool] [app.main.ui.workspace.shapes.bounding-box :refer [bounding-box]] [app.main.ui.workspace.shapes.common :as common] [app.main.ui.workspace.shapes.frame :as frame] @@ -35,6 +36,7 @@ (declare shape-wrapper) (declare group-wrapper) (declare svg-raw-wrapper) +(declare bool-wrapper) (declare frame-wrapper) (def circle-wrapper (common/generic-wrapper-factory circle/circle-shape)) @@ -92,13 +94,14 @@ [:* (if-not svg-element? (case (:type shape) - :path [:> path/path-wrapper opts] - :text [:> text/text-wrapper opts] - :group [:> group-wrapper opts] - :rect [:> rect-wrapper opts] - :image [:> image-wrapper opts] - :circle [:> circle-wrapper opts] + :path [:> path/path-wrapper opts] + :text [:> text/text-wrapper opts] + :group [:> group-wrapper opts] + :rect [:> rect-wrapper opts] + :image [:> image-wrapper opts] + :circle [:> circle-wrapper opts] :svg-raw [:> svg-raw-wrapper opts] + :bool [:> bool-wrapper opts] ;; Only used when drawing a new frame. :frame [:> frame-wrapper {:shape shape}] @@ -113,5 +116,6 @@ (def group-wrapper (group/group-wrapper-factory shape-wrapper)) (def svg-raw-wrapper (svg-raw/svg-raw-wrapper-factory shape-wrapper)) +(def bool-wrapper (bool/bool-wrapper-factory shape-wrapper)) (def frame-wrapper (frame/frame-wrapper-factory shape-wrapper)) diff --git a/frontend/src/app/main/ui/workspace/shapes/bool.cljs b/frontend/src/app/main/ui/workspace/shapes/bool.cljs new file mode 100644 index 000000000..a226eff57 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/shapes/bool.cljs @@ -0,0 +1,44 @@ +;; 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) UXBOX Labs SL + +(ns app.main.ui.workspace.shapes.bool + (:require + [app.main.data.workspace :as dw] + [app.main.refs :as refs] + [app.main.store :as st] + [app.main.streams :as ms] + [app.main.ui.shapes.bool :as bool] + [app.main.ui.shapes.shape :refer [shape-container]] + [app.util.dom :as dom] + [rumext.alpha :as mf])) + +(defn use-double-click [{:keys [id]}] + (mf/use-callback + (mf/deps id) + (fn [event] + (dom/stop-propagation event) + (dom/prevent-default event) + (st/emit! (dw/select-inside-group id @ms/mouse-position))))) + +(defn bool-wrapper-factory + [shape-wrapper] + (let [shape-component (bool/bool-shape shape-wrapper)] + (mf/fnc bool-wrapper + {::mf/wrap [#(mf/memo' % (mf/check-props ["shape" "frame"]))] + ::mf/wrap-props false} + [props] + (let [shape (unchecked-get props "shape") + frame (unchecked-get props "frame") + + childs-ref (mf/use-memo (mf/deps shape) #(refs/objects-by-id (:shapes shape) {:with-modifiers? true})) + childs (mf/deref childs-ref)] + + [:> shape-container {:shape shape} + [:& shape-component + {:frame frame + :shape shape + :childs childs}]])))) + From 0b4b2d3814114cf0715f24395c83bbcad0ca644a Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Fri, 10 Sep 2021 10:48:55 +0200 Subject: [PATCH 03/16] :sparkles: Add UI for boolean operations --- .../images/icons/boolean-difference.svg | 3 + .../images/icons/boolean-exclude.svg | 3 + .../images/icons/boolean-intersection.svg | 3 + .../resources/images/icons/boolean-union.svg | 3 + .../main/partials/sidebar-align-options.scss | 2 +- .../styles/main/partials/workspace.scss | 11 +++ frontend/src/app/main/data/workspace.cljs | 3 +- .../src/app/main/data/workspace/booleans.cljs | 1 - .../app/main/data/workspace/shortcuts.cljs | 18 ++++- frontend/src/app/main/ui/icons.cljs | 7 ++ .../app/main/ui/workspace/colorpicker.cljs | 8 +- .../app/main/ui/workspace/context_menu.cljs | 78 ++++++++++++++++--- .../app/main/ui/workspace/sidebar/layers.cljs | 8 +- .../main/ui/workspace/sidebar/options.cljs | 4 +- .../sidebar/{ => options/menus}/align.cljs | 2 +- .../sidebar/options/menus/booleans.cljs | 54 +++++++++++++ frontend/src/app/util/dom.cljs | 2 +- frontend/src/app/util/path/bool.cljs | 9 +-- frontend/translations/en.po | 17 +++- frontend/translations/es.po | 15 ++++ 20 files changed, 220 insertions(+), 31 deletions(-) create mode 100644 frontend/resources/images/icons/boolean-difference.svg create mode 100644 frontend/resources/images/icons/boolean-exclude.svg create mode 100644 frontend/resources/images/icons/boolean-intersection.svg create mode 100644 frontend/resources/images/icons/boolean-union.svg rename frontend/src/app/main/ui/workspace/sidebar/{ => options/menus}/align.cljs (98%) create mode 100644 frontend/src/app/main/ui/workspace/sidebar/options/menus/booleans.cljs diff --git a/frontend/resources/images/icons/boolean-difference.svg b/frontend/resources/images/icons/boolean-difference.svg new file mode 100644 index 000000000..4d5c7f6a8 --- /dev/null +++ b/frontend/resources/images/icons/boolean-difference.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/images/icons/boolean-exclude.svg b/frontend/resources/images/icons/boolean-exclude.svg new file mode 100644 index 000000000..6a3865703 --- /dev/null +++ b/frontend/resources/images/icons/boolean-exclude.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/images/icons/boolean-intersection.svg b/frontend/resources/images/icons/boolean-intersection.svg new file mode 100644 index 000000000..3480e6366 --- /dev/null +++ b/frontend/resources/images/icons/boolean-intersection.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/images/icons/boolean-union.svg b/frontend/resources/images/icons/boolean-union.svg new file mode 100644 index 000000000..fdeb117b7 --- /dev/null +++ b/frontend/resources/images/icons/boolean-union.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/styles/main/partials/sidebar-align-options.scss b/frontend/resources/styles/main/partials/sidebar-align-options.scss index 7b9750426..634c40861 100644 --- a/frontend/resources/styles/main/partials/sidebar-align-options.scss +++ b/frontend/resources/styles/main/partials/sidebar-align-options.scss @@ -14,7 +14,7 @@ .align-group { display: flex; justify-content: space-evenly; - width: 100%; + width: 50%; &:not(:last-child) { border-right: solid 1px $color-gray-60; diff --git a/frontend/resources/styles/main/partials/workspace.scss b/frontend/resources/styles/main/partials/workspace.scss index f167e9814..a6ad2df1e 100644 --- a/frontend/resources/styles/main/partials/workspace.scss +++ b/frontend/resources/styles/main/partials/workspace.scss @@ -47,6 +47,17 @@ &:hover { background-color: $color-primary-lighter; } + + .submenu-icon { + position: absolute; + right: 1rem; + + svg { + width: 10px; + height: 10px; + } + } + } } diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index 476046781..cb055c688 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -49,8 +49,7 @@ [cljs.spec.alpha :as s] [clojure.set :as set] [cuerdas.core :as str] - [potok.core :as ptk] - )) + [potok.core :as ptk])) ;; (log/set-level! :trace) diff --git a/frontend/src/app/main/data/workspace/booleans.cljs b/frontend/src/app/main/data/workspace/booleans.cljs index 6702af71d..d4aa7455a 100644 --- a/frontend/src/app/main/data/workspace/booleans.cljs +++ b/frontend/src/app/main/data/workspace/booleans.cljs @@ -42,7 +42,6 @@ :shapes []} (gsh/setup selrect)))) - (defn create-bool [bool-type] (ptk/reify ::create-bool-union diff --git a/frontend/src/app/main/data/workspace/shortcuts.cljs b/frontend/src/app/main/data/workspace/shortcuts.cljs index 7373d6e76..15dd66f66 100644 --- a/frontend/src/app/main/data/workspace/shortcuts.cljs +++ b/frontend/src/app/main/data/workspace/shortcuts.cljs @@ -261,9 +261,21 @@ :type "keyup" :fn #(st/emit! (dw/toggle-distances-display false))} - :create-union {:tooltip (ds/alt "U") - :command ["alt" "u"] - :fn #(st/emit! (dw/create-bool :union))} + :boolean-union {:tooltip (ds/alt "U") + :command ["alt" "u"] + :fn #(st/emit! (dw/create-bool :union))} + + :boolean-difference {:tooltip (ds/alt "D") + :command ["alt" "d"] + :fn #(st/emit! (dw/create-bool :difference))} + + :boolean-intersection {:tooltip (ds/alt "I") + :command ["alt" "i"] + :fn #(st/emit! (dw/create-bool :intersection))} + + :boolean-exclude {:tooltip (ds/alt "E") + :command ["alt" "e"] + :fn #(st/emit! (dw/create-bool :exclude))} }) diff --git a/frontend/src/app/main/ui/icons.cljs b/frontend/src/app/main/ui/icons.cljs index abf60ef6e..35c5285d2 100644 --- a/frontend/src/app/main/ui/icons.cljs +++ b/frontend/src/app/main/ui/icons.cljs @@ -9,6 +9,8 @@ (:require-macros [app.main.ui.icons :refer [icon-xref]]) (:require [rumext.alpha :as mf])) +;; Keep the list of icons sorted + (def action (icon-xref :action)) (def actions (icon-xref :actions)) (def align-bottom (icon-xref :align-bottom)) @@ -23,6 +25,10 @@ (def auto-fix (icon-xref :auto-fix)) (def auto-height (icon-xref :auto-height)) (def auto-width (icon-xref :auto-width)) +(def boolean-difference (icon-xref :boolean-difference)) +(def boolean-exclude (icon-xref :boolean-exclude)) +(def boolean-intersection (icon-xref :boolean-intersection)) +(def boolean-union (icon-xref :boolean-union)) (def box (icon-xref :box)) (def chain (icon-xref :chain)) (def chat (icon-xref :chat)) @@ -152,6 +158,7 @@ (def uppercase (icon-xref :uppercase)) (def user (icon-xref :user)) + (def loader-pencil (mf/html [:svg diff --git a/frontend/src/app/main/ui/workspace/colorpicker.cljs b/frontend/src/app/main/ui/workspace/colorpicker.cljs index 7e71d5c4a..b239a9185 100644 --- a/frontend/src/app/main/ui/workspace/colorpicker.cljs +++ b/frontend/src/app/main/ui/workspace/colorpicker.cljs @@ -202,10 +202,10 @@ h (str (* s 100) "%") (str (* l 100) "%")))] - (dom/set-css-property node "--color" (str/join ", " rgb)) - (dom/set-css-property node "--hue-rgb" (str/join ", " hue-rgb)) - (dom/set-css-property node "--saturation-grad-from" (format-hsl hsl-from)) - (dom/set-css-property node "--saturation-grad-to" (format-hsl hsl-to))))) + (dom/set-css-property! node "--color" (str/join ", " rgb)) + (dom/set-css-property! node "--hue-rgb" (str/join ", " hue-rgb)) + (dom/set-css-property! node "--saturation-grad-from" (format-hsl hsl-from)) + (dom/set-css-property! node "--saturation-grad-to" (format-hsl hsl-to))))) ;; When closing the modal we update the recent-color list (mf/use-effect diff --git a/frontend/src/app/main/ui/workspace/context_menu.cljs b/frontend/src/app/main/ui/workspace/context_menu.cljs index ed9933297..327864592 100644 --- a/frontend/src/app/main/ui/workspace/context_menu.cljs +++ b/frontend/src/app/main/ui/workspace/context_menu.cljs @@ -16,6 +16,7 @@ [app.main.store :as st] [app.main.ui.components.dropdown :refer [dropdown]] [app.main.ui.context :as ctx] + [app.main.ui.icons :as i] [app.util.dom :as dom] [app.util.i18n :refer [tr] :as i18n] [app.util.timers :as timers] @@ -31,10 +32,53 @@ (dom/stop-propagation event)) (mf/defc menu-entry - [{:keys [title shortcut on-click] :as props}] - [:li {:on-click on-click} - [:span.title title] - [:span.shortcut (or shortcut "")]]) + [{:keys [title shortcut submenu-ref on-click children] :as props}] + (let [entry-ref (mf/use-ref nil) + submenu-ref (mf/use-ref nil) + hovering? (mf/use-ref false) + + on-pointer-enter + (mf/use-callback + (fn [event] + (mf/set-ref-val! hovering? true) + (let [submenu-node (mf/ref-val submenu-ref)] + (when (some? submenu-node) + (dom/set-css-property! submenu-node "display" "block"))))) + + on-pointer-leave + (mf/use-callback + (fn [event] + (mf/set-ref-val! hovering? false) + (let [submenu-node (mf/ref-val submenu-ref)] + (when (some? submenu-node) + (timers/schedule + 200 + #(when-not (mf/ref-val hovering?) + (dom/set-css-property! submenu-node "display" "none"))))))) + + set-dom-node + (mf/use-callback + (fn [dom] + (let [submenu-node (mf/ref-val submenu-ref)] + (when (and (some? dom) (some? submenu-node)) + (dom/set-css-property! submenu-node "top" (str (.-offsetTop dom) "px"))))))] + + [:li {:ref set-dom-node + :on-click on-click + :on-pointer-enter on-pointer-enter + :on-pointer-leave on-pointer-leave} + [:span.title title] + [:span.shortcut (or shortcut "")] + + (when (> (count children) 1) + [:span.submenu-icon i/arrow-slide]) + + (when (> (count children) 1) + [:ul.workspace-context-menu + {:ref submenu-ref + :style {:display "none" :left 250} + :on-context-menu prevent-default} + children])])) (mf/defc menu-separator [] @@ -100,12 +144,12 @@ do-navigate-component-file (st/emitf (dwl/nav-to-component-file (:component-file shape))) - do-create-bool-shape (st/emitf (dw/create-bool :union))] + do-boolean-union (st/emitf (dw/create-bool :union)) + do-boolean-difference (st/emitf (dw/create-bool :difference)) + do-boolean-intersection (st/emitf (dw/create-bool :intersection)) + do-boolean-exclude (st/emitf (dw/create-bool :exclude)) + ] [:* - ;; - [:& menu-entry {:title ">BOOL" - :on-click do-create-bool-shape}] - ;; [:& menu-entry {:title (tr "workspace.shape.menu.copy") :shortcut (sc/get-tooltip :copy) :on-click do-copy}] @@ -171,6 +215,20 @@ :shortcut (sc/get-tooltip :start-editing) :on-click do-start-editing}]) + [:& menu-entry {:title (tr "workspace.shape.menu.path")} + [:& menu-entry {:title (tr "workspace.shape.menu.union") + :shortcut (sc/get-tooltip :boolean-union) + :on-click do-boolean-union}] + [:& menu-entry {:title (tr "workspace.shape.menu.difference") + :shortcut (sc/get-tooltip :boolean-difference) + :on-click do-boolean-difference}] + [:& menu-entry {:title (tr "workspace.shape.menu.intersection") + :shortcut (sc/get-tooltip :boolean-intersection) + :on-click do-boolean-intersection}] + [:& menu-entry {:title (tr "workspace.shape.menu.exclude") + :shortcut (sc/get-tooltip :boolean-exclude) + :on-click do-boolean-exclude}]] + (if (:hidden shape) [:& menu-entry {:title (tr "workspace.shape.menu.show") :on-click do-show-shape}] @@ -246,7 +304,7 @@ (when dropdown (let [bounding-rect (dom/get-bounding-rect dropdown) window-size (dom/get-window-size) - delta-x (max (- (:right bounding-rect) (:width window-size)) 0) + delta-x (max (- (+ (:right bounding-rect) 250) (:width window-size)) 0) delta-y (max (- (:bottom bounding-rect) (:height window-size)) 0) new-style (str "top: " (- top delta-y) "px; " "left: " (- left delta-x) "px;")] diff --git a/frontend/src/app/main/ui/workspace/sidebar/layers.cljs b/frontend/src/app/main/ui/workspace/sidebar/layers.cljs index 0f147b1f2..f3c51c963 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/layers.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/layers.cljs @@ -39,6 +39,11 @@ (if (:masked-group? shape) i/mask i/folder)) + :bool (case (:bool-type shape) + :difference i/boolean-difference + :exclude i/boolean-exclude + :intersection i/boolean-intersection + #_:default i/boolean-union) :svg-raw i/file-svg nil)) @@ -292,7 +297,8 @@ :shape-ref :touched :metadata - :masked-group?])) + :masked-group? + :bool-type])) (defn- strip-objects [objects] diff --git a/frontend/src/app/main/ui/workspace/sidebar/options.cljs b/frontend/src/app/main/ui/workspace/sidebar/options.cljs index 89248b58b..e99d25802 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options.cljs @@ -11,7 +11,8 @@ [app.main.store :as st] [app.main.ui.components.tab-container :refer [tab-container tab-element]] [app.main.ui.context :as ctx] - [app.main.ui.workspace.sidebar.align :refer [align-options]] + [app.main.ui.workspace.sidebar.options.menus.align :refer [align-options]] + [app.main.ui.workspace.sidebar.options.menus.booleans :refer [booleans-options]] [app.main.ui.workspace.sidebar.options.menus.exports :refer [exports-menu]] [app.main.ui.workspace.sidebar.options.menus.interactions :refer [interactions-menu]] [app.main.ui.workspace.sidebar.options.page :as page] @@ -60,6 +61,7 @@ :title (tr "workspace.options.design")} [:div.element-options [:& align-options] + [:& booleans-options] (case (count selected) 0 [:& page/options] 1 [:& shape-options {:shape (first shapes) diff --git a/frontend/src/app/main/ui/workspace/sidebar/align.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/align.cljs similarity index 98% rename from frontend/src/app/main/ui/workspace/sidebar/align.cljs rename to frontend/src/app/main/ui/workspace/sidebar/options/menus/align.cljs index acb3c6a42..7b32c969a 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/align.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/align.cljs @@ -4,7 +4,7 @@ ;; ;; Copyright (c) UXBOX Labs SL -(ns app.main.ui.workspace.sidebar.align +(ns app.main.ui.workspace.sidebar.options.menus.align (:require [app.common.uuid :as uuid] [app.main.data.workspace :as dw] diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/booleans.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/booleans.cljs new file mode 100644 index 000000000..0ad6b3ddd --- /dev/null +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/booleans.cljs @@ -0,0 +1,54 @@ +;; 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) UXBOX Labs SL + +(ns app.main.ui.workspace.sidebar.options.menus.booleans + (:require + + [app.main.data.workspace :as dw] + [app.main.refs :as refs] + [app.main.store :as st] + [app.main.ui.icons :as i] + [app.util.i18n :as i18n :refer [tr]] + [rumext.alpha :as mf] + )) + +(mf/defc booleans-options + [] + (let [selected (mf/deref refs/selected-shapes) + disabled (and (some? selected) + (<= (count selected) 1)) + + do-boolean-union (st/emitf (dw/create-bool :union)) + do-boolean-difference (st/emitf (dw/create-bool :difference)) + do-boolean-intersection (st/emitf (dw/create-bool :intersection)) + do-boolean-exclude (st/emitf (dw/create-bool :exclude))] + + [:div.align-options + [:div.align-group + [:div.align-button.tooltip.tooltip-bottom + {:alt (tr "workspace.shape.menu.union") + :class (when disabled "disabled") + :on-click do-boolean-union} + i/boolean-union] + + [:div.align-button.tooltip.tooltip-bottom + {:alt (tr "workspace.shape.menu.difference") + :class (when disabled "disabled") + :on-click do-boolean-difference} + i/boolean-difference] + + [:div.align-button.tooltip.tooltip-bottom + {:alt (tr "workspace.shape.menu.intersection") + :class (when disabled "disabled") + :on-click do-boolean-intersection} + i/boolean-intersection] + + [:div.align-button.tooltip.tooltip-bottom + {:alt (tr "workspace.shape.menu.exclude") + :class (when disabled "disabled") + :on-click do-boolean-exclude} + i/boolean-exclude]]])) + diff --git a/frontend/src/app/util/dom.cljs b/frontend/src/app/util/dom.cljs index 1f952fb35..a170481fc 100644 --- a/frontend/src/app/util/dom.cljs +++ b/frontend/src/app/util/dom.cljs @@ -281,7 +281,7 @@ (defn set-text! [node text] (set! (.-textContent node) text)) -(defn set-css-property [node property value] +(defn set-css-property! [node property value] (.setProperty (.-style ^js node) property value)) (defn capture-pointer [event] diff --git a/frontend/src/app/util/path/bool.cljs b/frontend/src/app/util/path/bool.cljs index 24e840840..e6a41c884 100644 --- a/frontend/src/app/util/path/bool.cljs +++ b/frontend/src/app/util/path/bool.cljs @@ -16,7 +16,7 @@ [app.util.path.geom :as upg] [cuerdas.core :as str])) -(def ^:const curve-curve-precision 0.001) +(def ^:const curve-curve-precision 0.1) (defn curve->rect [[from-p to-p :as curve]] @@ -133,10 +133,9 @@ r2 (curve-range->rect c2 c2-from c2-to)] (when (gsi/overlaps-rects? r1 r2) - - (if (and (< (mth/abs (- c1-from c1-to)) curve-curve-precision) - (< (mth/abs (- c2-from c2-to)) curve-curve-precision)) - + (if (< (gpt/distance (gpp/curve-values c1 c1-from) + (gpp/curve-values c2 c2-from)) + curve-curve-precision) [(sorted-set (mth/precision c1-from 4)) (sorted-set (mth/precision c2-from 4))] diff --git a/frontend/translations/en.po b/frontend/translations/en.po index e5211a76d..27c40c21c 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -3114,4 +3114,19 @@ msgid "viewer.breaking-change.message" msgstr "Sorry!" msgid "viewer.breaking-change.description" -msgstr "This shareable link is no longer valid. Create a new one or ask the owner for a new one. +msgstr "This shareable link is no longer valid. Create a new one or ask the owner for a new one." + +msgid "workspace.shape.menu.path" +msgstr "Path" + +msgid "workspace.shape.menu.union" +msgstr "Union" + +msgid "workspace.shape.menu.difference" +msgstr "Difference" + +msgid "workspace.shape.menu.intersection" +msgstr "Intersection" + +msgid "workspace.shape.menu.exclude" +msgstr "Exclude" diff --git a/frontend/translations/es.po b/frontend/translations/es.po index 6b716a299..a1020f9c3 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -3000,3 +3000,18 @@ msgstr "¡Lo sentimos!" msgid "viewer.breaking-change.description" msgstr "Este link compartido ya no funciona. Crea uno nuevo o pídelo a la persona que lo creó." + +msgid "workspace.shape.menu.path" +msgstr "Path" + +msgid "workspace.shape.menu.union" +msgstr "Unión" + +msgid "workspace.shape.menu.difference" +msgstr "Diferencia" + +msgid "workspace.shape.menu.intersection" +msgstr "Intersección" + +msgid "workspace.shape.menu.exclude" +msgstr "Exclusión" From df60ee06a1a49883fbfe2ad9363f51b7213d420e Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Fri, 10 Sep 2021 15:54:11 +0200 Subject: [PATCH 04/16] :sparkles: Add exclusion boolean operation --- .../src/app/main/data/workspace/booleans.cljs | 20 ++++++ frontend/src/app/main/ui/shapes/bool.cljs | 64 +++++-------------- .../app/main/ui/workspace/context_menu.cljs | 16 +++-- .../main/ui/workspace/sidebar/options.cljs | 2 + .../sidebar/options/shapes/bool.cljs | 45 +++++++++++++ frontend/src/app/util/path/bool.cljs | 38 ++++++++++- frontend/src/app/util/path/commands.cljs | 1 + frontend/src/app/util/path/subpaths.cljs | 11 ++++ 8 files changed, 142 insertions(+), 55 deletions(-) create mode 100644 frontend/src/app/main/ui/workspace/sidebar/options/shapes/bool.cljs diff --git a/frontend/src/app/main/data/workspace/booleans.cljs b/frontend/src/app/main/data/workspace/booleans.cljs index d4aa7455a..b2be46791 100644 --- a/frontend/src/app/main/data/workspace/booleans.cljs +++ b/frontend/src/app/main/data/workspace/booleans.cljs @@ -18,6 +18,24 @@ [cuerdas.core :as str] [potok.core :as ptk])) +(def ^:const style-properties + [:fill-color + :fill-opacity + :fill-color-gradient + :fill-color-ref-file + :fill-color-ref-id + :stroke-color + :stroke-color-ref-file + :stroke-color-ref-id + :stroke-opacity + :stroke-style + :stroke-width + :stroke-alignment + :stroke-cap-start + :stroke-cap-end + :shadow + :blur]) + (defn selected-shapes [state] (let [objects (wsh/lookup-page-objects state)] @@ -31,6 +49,7 @@ (defn create-bool-data [type name shapes] (let [head (first shapes) + head-data (select-keys head style-properties) selrect (gsh/selection-rect shapes)] (-> {:id (uuid/next) :type :bool @@ -40,6 +59,7 @@ :name name ::index (::index head) :shapes []} + (merge head-data) (gsh/setup selrect)))) (defn create-bool diff --git a/frontend/src/app/main/ui/shapes/bool.cljs b/frontend/src/app/main/ui/shapes/bool.cljs index be7337084..9d8117401 100644 --- a/frontend/src/app/main/ui/shapes/bool.cljs +++ b/frontend/src/app/main/ui/shapes/bool.cljs @@ -6,70 +6,38 @@ (ns app.main.ui.shapes.bool (:require - [app.common.data :as d] [app.common.geom.shapes :as gsh] - [app.common.math :as mth] + [app.main.ui.hooks :refer [use-equal-memo]] [app.util.object :as obj] [app.util.path.bool :as pb] - [app.util.path.geom :as upg] [app.util.path.shapes-to-path :as stp] - [clojure.set :as set] [rumext.alpha :as mf])) -(mf/defc path-points - [{:keys [points color]}] - - [:* - (for [[idx {:keys [x y]}] (d/enumerate points)] - [:circle {:key (str "circle-" idx) - :cx x - :cy y - :r 5 - :style {:fill color - ;;:fillOpacity 0.5 - }}])]) - (defn bool-shape [shape-wrapper] (mf/fnc bool-shape {::mf/wrap-props false} [props] (let [frame (obj/get props "frame") - childs (obj/get props "childs") - shape-1 (stp/convert-to-path (nth childs 0)) - shape-2 (stp/convert-to-path (nth childs 1)) + shape (obj/get props "shape") + childs (obj/get props "childs")] - content-1 (-> shape-1 gsh/transform-shape (gsh/translate-to-frame frame) :content) - content-2 (-> shape-2 gsh/transform-shape (gsh/translate-to-frame frame) :content) - + (when (> (count childs) 1) + (let [shape-1 (stp/convert-to-path (nth childs 0)) + shape-2 (stp/convert-to-path (nth childs 1)) - [content-1' content-2'] (pb/content-intersect-split content-1 content-2) - - points-1 (->> (upg/content->points content-1') - (map #(hash-map :x (mth/round (:x %)) - :y (mth/round (:y %)))) - (into #{})) - - points-2 (->> (upg/content->points content-2') - (map #(hash-map :x (mth/round (:x %)) - :y (mth/round (:y %)))) - (into #{})) + content-1 (use-equal-memo (-> shape-1 :content gsh/transform-shape)) + content-2 (use-equal-memo (-> shape-2 :content gsh/transform-shape)) - points-3 (set/intersection points-1 points-2)] + content + (mf/use-memo + (mf/deps content-1 content-2) + #(pb/content-bool (:bool-type shape) content-1 content-2))] - [:* - [:& shape-wrapper {:shape (-> shape-1 #_(assoc :content content-1')) - :frame frame}] - - [:& shape-wrapper {:shape (-> shape-2 #_(assoc :content content-2')) - :frame frame}] - - [:& path-points {:points points-1 :color "#FF0000"}] - [:& path-points {:points points-2 :color "#0000FF"}] - [:& path-points {:points points-3 :color "#FF00FF"}] - - - ]))) + [:& shape-wrapper {:shape (-> shape + (assoc :type :path) + (assoc :content content)) + :frame frame}]))))) diff --git a/frontend/src/app/main/ui/workspace/context_menu.cljs b/frontend/src/app/main/ui/workspace/context_menu.cljs index 327864592..8e2b43b78 100644 --- a/frontend/src/app/main/ui/workspace/context_menu.cljs +++ b/frontend/src/app/main/ui/workspace/context_menu.cljs @@ -32,14 +32,13 @@ (dom/stop-propagation event)) (mf/defc menu-entry - [{:keys [title shortcut submenu-ref on-click children] :as props}] - (let [entry-ref (mf/use-ref nil) - submenu-ref (mf/use-ref nil) + [{:keys [title shortcut on-click children] :as props}] + (let [submenu-ref (mf/use-ref nil) hovering? (mf/use-ref false) on-pointer-enter (mf/use-callback - (fn [event] + (fn [] (mf/set-ref-val! hovering? true) (let [submenu-node (mf/ref-val submenu-ref)] (when (some? submenu-node) @@ -47,7 +46,7 @@ on-pointer-leave (mf/use-callback - (fn [event] + (fn [] (mf/set-ref-val! hovering? false) (let [submenu-node (mf/ref-val submenu-ref)] (when (some? submenu-node) @@ -227,7 +226,12 @@ :on-click do-boolean-intersection}] [:& menu-entry {:title (tr "workspace.shape.menu.exclude") :shortcut (sc/get-tooltip :boolean-exclude) - :on-click do-boolean-exclude}]] + :on-click do-boolean-exclude}] + + [:& menu-separator] + ;; TODO + [:& menu-entry {:title "Flatten"}] + [:& menu-entry {:title "Transform to path"}]] (if (:hidden shape) [:& menu-entry {:title (tr "workspace.shape.menu.show") diff --git a/frontend/src/app/main/ui/workspace/sidebar/options.cljs b/frontend/src/app/main/ui/workspace/sidebar/options.cljs index e99d25802..e0a62cbff 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options.cljs @@ -16,6 +16,7 @@ [app.main.ui.workspace.sidebar.options.menus.exports :refer [exports-menu]] [app.main.ui.workspace.sidebar.options.menus.interactions :refer [interactions-menu]] [app.main.ui.workspace.sidebar.options.page :as page] + [app.main.ui.workspace.sidebar.options.shapes.bool :as bool] [app.main.ui.workspace.sidebar.options.shapes.circle :as circle] [app.main.ui.workspace.sidebar.options.shapes.frame :as frame] [app.main.ui.workspace.sidebar.options.shapes.group :as group] @@ -44,6 +45,7 @@ :path [:& path/options {:shape shape}] :image [:& image/options {:shape shape}] :svg-raw [:& svg-raw/options {:shape shape}] + :bool [:& bool/options {:shape shape}] nil) [:& exports-menu {:shape shape diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/bool.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/bool.cljs new file mode 100644 index 000000000..dc5a8fa8c --- /dev/null +++ b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/bool.cljs @@ -0,0 +1,45 @@ +;; 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) UXBOX Labs SL + +(ns app.main.ui.workspace.sidebar.options.shapes.bool + (:require + [app.main.ui.workspace.sidebar.options.menus.blur :refer [blur-menu]] + [app.main.ui.workspace.sidebar.options.menus.constraints :refer [constraint-attrs constraints-menu]] + [app.main.ui.workspace.sidebar.options.menus.fill :refer [fill-attrs fill-menu]] + [app.main.ui.workspace.sidebar.options.menus.layer :refer [layer-attrs layer-menu]] + [app.main.ui.workspace.sidebar.options.menus.measures :refer [measure-attrs measures-menu]] + [app.main.ui.workspace.sidebar.options.menus.shadow :refer [shadow-menu]] + [app.main.ui.workspace.sidebar.options.menus.stroke :refer [stroke-attrs stroke-menu]] + [rumext.alpha :as mf])) + +(mf/defc options + [{:keys [shape] :as props}] + (let [ids [(:id shape)] + type (:type shape) + measure-values (select-keys shape measure-attrs) + stroke-values (select-keys shape stroke-attrs) + layer-values (select-keys shape layer-attrs) + constraint-values (select-keys shape constraint-attrs)] + [:* + [:& measures-menu {:ids ids + :type type + :values measure-values}] + [:& constraints-menu {:ids ids + :values constraint-values}] + [:& layer-menu {:ids ids + :type type + :values layer-values}] + [:& fill-menu {:ids ids + :type type + :values (select-keys shape fill-attrs)}] + [:& stroke-menu {:ids ids + :type type + :show-caps true + :values stroke-values}] + [:& shadow-menu {:ids ids + :values (select-keys shape [:shadow])}] + [:& blur-menu {:ids ids + :values (select-keys shape [:blur])}]])) diff --git a/frontend/src/app/util/path/bool.cljs b/frontend/src/app/util/path/bool.cljs index e6a41c884..d9c190286 100644 --- a/frontend/src/app/util/path/bool.cljs +++ b/frontend/src/app/util/path/bool.cljs @@ -14,7 +14,7 @@ [app.common.geom.shapes.rect :as gpr] [app.common.math :as mth] [app.util.path.geom :as upg] - [cuerdas.core :as str])) + [app.util.path.subpaths :as ups])) (def ^:const curve-curve-precision 0.1) @@ -267,3 +267,39 @@ (rest new-pending) new-content-b (conj new-content-a new-current)))))))) + + +(defn create-union [content-a content-b] + (d/concat + [] + content-a + (ups/reverse-content content-b))) + +(defn create-difference [content-a content-b] + (d/concat + [] + content-a + (ups/reverse-content content-b))) + +(defn create-intersection [content-a content-b] + (d/concat + [] + content-a + (ups/reverse-content content-b))) + + +(defn create-exclusion [content-a content-b] + (d/concat + [] + content-a + (ups/reverse-content content-b))) + +(defn content-bool + [bool-type content-a content-b] + + (let [[content-a' content-b'] (content-intersect-split content-a content-b)] + (case bool-type + :union (create-union content-a' content-b') + :difference (create-difference content-a' content-b') + :intersection (create-intersection content-a' content-b') + :exclusion (create-exclusion content-a' content-b')))) diff --git a/frontend/src/app/util/path/commands.cljs b/frontend/src/app/util/path/commands.cljs index 84a7725ef..fd1df9da9 100644 --- a/frontend/src/app/util/path/commands.cljs +++ b/frontend/src/app/util/path/commands.cljs @@ -199,3 +199,4 @@ (if (= prefix :c1) (command->point (get content (dec index))) (command->point (get content index)))) + diff --git a/frontend/src/app/util/path/subpaths.cljs b/frontend/src/app/util/path/subpaths.cljs index 010f1343a..5f0e2bf34 100644 --- a/frontend/src/app/util/path/subpaths.cljs +++ b/frontend/src/app/util/path/subpaths.cljs @@ -134,3 +134,14 @@ (->> closed-subpaths (mapcat :data) (into [])))) + +(defn reverse-content + "Given a content reverse the order of the commands" + [content] + + (->> content + (get-subpaths) + (mapv reverse-subpath) + (reverse) + (mapcat :data) + (into []))) From 1db289560647e83b704dc234e6bf8a9a85dd35a1 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Mon, 13 Sep 2021 21:42:56 +0200 Subject: [PATCH 05/16] :sparkles: Union,intersection,difference --- .../src/app/common/geom/shapes/intersect.cljc | 20 - common/src/app/common/geom/shapes/path.cljc | 250 +++++++++++- common/src/app/common/geom/shapes/rect.cljc | 20 + .../app/main/data/workspace/shortcuts.cljs | 8 +- frontend/src/app/main/refs.cljs | 4 + frontend/src/app/main/ui/shapes/bool.cljs | 20 +- .../app/main/ui/workspace/shapes/bool.cljs | 11 +- .../src/app/main/ui/workspace/viewport.cljs | 6 +- frontend/src/app/util/path/bool.cljs | 357 +++++++----------- frontend/src/app/util/path/format.cljs | 14 +- frontend/src/app/util/path/geom.cljs | 27 +- frontend/src/app/util/path/subpaths.cljs | 23 +- 12 files changed, 488 insertions(+), 272 deletions(-) diff --git a/common/src/app/common/geom/shapes/intersect.cljc b/common/src/app/common/geom/shapes/intersect.cljc index 633a74e8b..796daf099 100644 --- a/common/src/app/common/geom/shapes/intersect.cljc +++ b/common/src/app/common/geom/shapes/intersect.cljc @@ -168,26 +168,6 @@ (is-point-inside-evenodd? (first points) rect-lines) (intersects-lines? rect-lines points-lines)))) -(defn overlaps-rects? - "Check for two rects to overlap. Rects won't overlap only if - one of them is fully to the left or the top" - [rect-a rect-b] - - (let [x1a (:x rect-a) - y1a (:y rect-a) - x2a (+ (:x rect-a) (:width rect-a)) - y2a (+ (:y rect-a) (:height rect-a)) - - x1b (:x rect-b) - y1b (:y rect-b) - x2b (+ (:x rect-b) (:width rect-b)) - y2b (+ (:y rect-b) (:height rect-b))] - - (and (> x2a x1b) - (> x2b x1a) - (> y2a y1b) - (> y2b y1a)))) - (defn overlaps-path? "Checks if the given rect overlaps with the path in any point" [shape rect] diff --git a/common/src/app/common/geom/shapes/path.cljc b/common/src/app/common/geom/shapes/path.cljc index 46ceb9927..6a7c4c6d8 100644 --- a/common/src/app/common/geom/shapes/path.cljc +++ b/common/src/app/common/geom/shapes/path.cljc @@ -7,10 +7,28 @@ (ns app.common.geom.shapes.path (:require [app.common.data :as d] + [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] [app.common.geom.shapes.rect :as gpr] [app.common.math :as mth])) +(def ^:const curve-curve-precision 0.1) + +(defn line-values + [[from-p to-p] t] + (let [move-v (-> (gpt/to-vec from-p to-p) + (gpt/scale t))] + (gpt/add from-p move-v))) + +(defn line-windup + [[_ to-p :as l] t] + (let [p (line-values l t) + v (gpt/to-vec p to-p)] + (cond + (> (:y v) 0) 1 + (< (:y v) 0) -1 + :else 0))) + ;; https://medium.com/@Acegikmo/the-ever-so-lovely-b%C3%A9zier-curve-eb27514da3bf ;; https://en.wikipedia.org/wiki/Bernstein_polynomial (defn curve-values @@ -37,6 +55,39 @@ (gpt/point (coord-v :x) (coord-v :y))))) +(defn curve-tangent + "Retrieve the tangent vector to the curve in the point `t`" + [[start end h1 h2] t] + + (let [coords [[(:x start) (:x h1) (:x h2) (:x end)] + [(:y start) (:y h1) (:y h2) (:y end)]] + + solve-derivative + (fn [[c0 c1 c2 c3]] + ;; Solve B'(t) given t to retrieve the value for the + ;; first derivative + (let [t2 (* t t)] + (+ (* c0 (+ (* -3 t2) (* 6 t) -3)) + (* c1 (+ (* 9 t2) (* -12 t) 3)) + (* c2 (+ (* -9 t2) (* 6 t))) + (* c3 (* 3 t2))))) + + [x y] (->> coords (mapv solve-derivative)) + + ;; normalize value + d (mth/sqrt (+ (* x x) (* y y)))] + + (gpt/point (/ x d) (/ y d)))) + +(defn curve-windup + [curve t] + + (let [tangent (curve-tangent curve t)] + (cond + (> (:y tangent) 0) 1 + (< (:y tangent) 0) -1 + :else 0))) + (defn curve-split "Splits a curve into two at the given parametric value `t`. Calculates the Casteljau's algorithm intermediate points" @@ -184,7 +235,8 @@ (mapcat coord->tvalue) ;; Only values in the range [0, 1] are valid - (filterv #(and (> % 0.01) (< % 0.99))))))) + #_(filterv #(and (> % 0.01) (< % 0.99))) + (filterv #(and (>= % 0) (<= % 1))))))) (defn command->point ([command] (command->point command nil)) @@ -196,6 +248,21 @@ y (get params ykey)] (gpt/point x y)))) +(defn command->line + ([cmd] + (command->line cmd (:prev cmd))) + ([cmd prev] + [prev (command->point cmd)])) + +(defn command->bezier + ([cmd] + (command->bezier cmd (:prev cmd))) + ([cmd prev] + [prev + (command->point cmd) + (gpt/point (-> cmd :params :c1x) (-> cmd :params :c1y)) + (gpt/point (-> cmd :params :c2x) (-> cmd :params :c2y))])) + (defn content->selrect [content] (let [calc-extremities (fn [command prev] @@ -422,3 +489,184 @@ (reduce find-min-point) (first)))) +(defn- get-line-tval + [[{x1 :x y1 :y} {x2 :x y2 :y}] {:keys [x y]}] + (if (mth/almost-zero? (- x2 x1)) + (/ (- y y1) (- y2 y1)) + (/ (- x x1) (- x2 x1)))) + +(defn- curve-range->rect + [curve from-t to-t] + + (let [[from-p to-p :as curve] (subcurve-range curve from-t to-t) + extremes (->> (curve-extremities curve) + (mapv #(curve-values curve %)))] + (gpr/points->rect (into [from-p to-p] extremes)))) + + +(defn line-line-crossing + [[from-p1 to-p1 :as l1] [from-p2 to-p2 :as l2]] + + (let [{x1 :x y1 :y} from-p1 + {x2 :x y2 :y} to-p1 + + {x3 :x y3 :y} from-p2 + {x4 :x y4 :y} to-p2 + + nx (- (* (- x3 x4) (- (* x1 y2) (* y1 x2))) + (* (- x1 x2) (- (* x3 y4) (* y3 x4)))) + + ny (- (* (- y3 y4) (- (* x1 y2) (* y1 x2))) + (* (- y1 y2) (- (* x3 y4) (* y3 x4)))) + + d (- (* (- x1 x2) (- y3 y4)) + (* (- y1 y2) (- x3 x4)))] + + (when-not (mth/almost-zero? d) + ;; Coordinates in the line. We calculate the tvalue that will + ;; return 0-1 as a percentage in the segment + (let [cross-p (gpt/point (/ nx d) (/ ny d)) + t1 (get-line-tval l1 cross-p) + t2 (get-line-tval l2 cross-p)] + [t1 t2])))) + +(defn line-curve-crossing + [[from-p1 to-p1] + [from-p2 to-p2 h1-p2 h2-p2]] + + (let [theta (-> (mth/atan2 (- (:y to-p1) (:y from-p1)) + (- (:x to-p1) (:x from-p1))) + (mth/degrees)) + + transform (-> (gmt/matrix) + (gmt/rotate (- theta)) + (gmt/translate (gpt/negate from-p1))) + + c2' [(gpt/transform from-p2 transform) + (gpt/transform to-p2 transform) + (gpt/transform h1-p2 transform) + (gpt/transform h2-p2 transform)]] + + (curve-roots c2' :y))) + +(defn ray-line-intersect + [point line] + + (let [ray-line [point (gpt/point (inc (:x point)) (:y point))] + [ray-t line-t] (line-line-crossing ray-line line)] + + (when (and (some? line-t) (> ray-t 0) (>= line-t 0) (< line-t 1)) + [[(line-values line line-t) + (line-windup line line-t)]]))) + +(defn line-line-intersect + [l1 l2] + + (let [[l1-t l2-t] (line-line-crossing l1 l2)] + (when (and (some? l1-t) (some? l2-t) + (> l1-t 0.01) (< l1-t 0.99) + (> l2-t 0.01) (< l2-t 0.99)) + [[l1-t] [l2-t]]))) + +(defn ray-curve-intersect + [ray-line c2] + + (let [;; ray-line [point (gpt/point (inc (:x point)) (:y point))] + curve-ts (->> (line-curve-crossing ray-line c2) + (filterv #(let [curve-v (curve-values c2 %) + curve-tg (curve-tangent c2 %) + curve-tg-angle (gpt/angle curve-tg) + ray-t (get-line-tval ray-line curve-v)] + (and (> ray-t 0) + (> (mth/abs (- curve-tg-angle 180)) 0.01) + (> (mth/abs (- curve-tg-angle 0)) 0.01)) )))] + (->> curve-ts + (mapv #(vector (curve-values c2 %) + (curve-windup c2 %)))))) + +(defn line-curve-intersect + [l1 c2] + (let [curve-ts (->> (line-curve-crossing l1 c2) + (filterv #(let [curve-v (curve-values c2 %) + line-t (get-line-tval l1 curve-v)] + (and (> line-t 0.001) (< line-t 0.999))))) + ;; Intersection line-curve points + intersect-ps (->> curve-ts + (mapv #(curve-values c2 %))) + + line-ts (->> intersect-ps + (mapv #(get-line-tval l1 %)))] + [line-ts curve-ts])) + +(defn curve-curve-intersect + [c1 c2] + + (letfn [(remove-close-ts [ts] + (loop [current (first ts) + pending (rest ts) + acc nil + result []] + (if (nil? current) + result + (if (and (some? acc) + (< (mth/abs (- current acc)) 0.01)) + (recur (first pending) + (rest pending) + acc + result) + + (recur (first pending) + (rest pending) + current + (conj result current)))))) + + (check-range [c1-from c1-to c2-from c2-to] + (let [r1 (curve-range->rect c1 c1-from c1-to) + r2 (curve-range->rect c2 c2-from c2-to)] + + (when (gpr/overlaps-rects? r1 r2) + (if (< (gpt/distance (curve-values c1 c1-from) + (curve-values c2 c2-from)) + curve-curve-precision) + [(sorted-set (mth/precision c1-from 4)) + (sorted-set (mth/precision c2-from 4))] + + (let [c1-half (+ c1-from (/ (- c1-to c1-from) 2)) + c2-half (+ c2-from (/ (- c2-to c2-from) 2)) + + [c1-ts-1 c2-ts-1] (check-range c1-from c1-half c2-from c2-half) + [c1-ts-2 c2-ts-2] (check-range c1-from c1-half c2-half c2-to) + [c1-ts-3 c2-ts-3] (check-range c1-half c1-to c2-from c2-half) + [c1-ts-4 c2-ts-4] (check-range c1-half c1-to c2-half c2-to)] + + [(into (sorted-set) (d/concat [] c1-ts-1 c1-ts-2 c1-ts-3 c1-ts-4)) + (into (sorted-set) (d/concat [] c2-ts-1 c2-ts-2 c2-ts-3 c2-ts-4))])))))] + + (let [[c1-ts c2-ts] (check-range 0.005 0.995 0.005 0.995) + c1-ts (remove-close-ts c1-ts) + c2-ts (remove-close-ts c2-ts)] + [c1-ts c2-ts]))) + +(defn curve->rect + [[from-p to-p :as curve]] + (let [extremes (->> (curve-extremities curve) + (mapv #(curve-values curve %)))] + (gpr/points->rect (into [from-p to-p] extremes)))) + + +(defn is-point-in-content? + [point content] + + (letfn [(cast-ray [[cmd prev]] + (let [ray-line [point (gpt/point (inc (:x point)) (:y point))]] + (case (:command cmd) + :line-to (ray-line-intersect point (command->line cmd (command->point prev))) + :curve-to (ray-curve-intersect ray-line (command->bezier cmd (command->point prev))) + #_:else [])))] + + ;; non-zero windup rule + (->> (d/with-prev content) + (mapcat cast-ray) + (map second) + (reduce +) + (not= 0)))) diff --git a/common/src/app/common/geom/shapes/rect.cljc b/common/src/app/common/geom/shapes/rect.cljc index 91e7d18a9..205fcca2b 100644 --- a/common/src/app/common/geom/shapes/rect.cljc +++ b/common/src/app/common/geom/shapes/rect.cljc @@ -70,3 +70,23 @@ :y (- (:y center) (/ height 2)) :width width :height height}) + +(defn overlaps-rects? + "Check for two rects to overlap. Rects won't overlap only if + one of them is fully to the left or the top" + [rect-a rect-b] + + (let [x1a (:x rect-a) + y1a (:y rect-a) + x2a (+ (:x rect-a) (:width rect-a)) + y2a (+ (:y rect-a) (:height rect-a)) + + x1b (:x rect-b) + y1b (:y rect-b) + x2b (+ (:x rect-b) (:width rect-b)) + y2b (+ (:y rect-b) (:height rect-b))] + + (and (> x2a x1b) + (> x2b x1a) + (> y2a y1b) + (> y2b y1a)))) diff --git a/frontend/src/app/main/data/workspace/shortcuts.cljs b/frontend/src/app/main/data/workspace/shortcuts.cljs index 15dd66f66..34c5779ef 100644 --- a/frontend/src/app/main/data/workspace/shortcuts.cljs +++ b/frontend/src/app/main/data/workspace/shortcuts.cljs @@ -262,19 +262,19 @@ :fn #(st/emit! (dw/toggle-distances-display false))} :boolean-union {:tooltip (ds/alt "U") - :command ["alt" "u"] + :command "alt+u" :fn #(st/emit! (dw/create-bool :union))} :boolean-difference {:tooltip (ds/alt "D") - :command ["alt" "d"] + :command "alt+d" :fn #(st/emit! (dw/create-bool :difference))} :boolean-intersection {:tooltip (ds/alt "I") - :command ["alt" "i"] + :command "alt+i" :fn #(st/emit! (dw/create-bool :intersection))} :boolean-exclude {:tooltip (ds/alt "E") - :command ["alt" "e"] + :command "alt+e" :fn #(st/emit! (dw/create-bool :exclude))} }) diff --git a/frontend/src/app/main/refs.cljs b/frontend/src/app/main/refs.cljs index 4dcbac803..9224abce2 100644 --- a/frontend/src/app/main/refs.cljs +++ b/frontend/src/app/main/refs.cljs @@ -122,6 +122,10 @@ :show-distances?]) workspace-local =)) +(def local-displacement + (l/derived #(select-keys % [:modifiers :selected]) + workspace-local =)) + (def selected-zoom (l/derived :zoom workspace-local)) diff --git a/frontend/src/app/main/ui/shapes/bool.cljs b/frontend/src/app/main/ui/shapes/bool.cljs index 9d8117401..dd77780e9 100644 --- a/frontend/src/app/main/ui/shapes/bool.cljs +++ b/frontend/src/app/main/ui/shapes/bool.cljs @@ -26,18 +26,26 @@ (let [shape-1 (stp/convert-to-path (nth childs 0)) shape-2 (stp/convert-to-path (nth childs 1)) - content-1 (use-equal-memo (-> shape-1 :content gsh/transform-shape)) - content-2 (use-equal-memo (-> shape-2 :content gsh/transform-shape)) + content-1 (use-equal-memo (-> shape-1 gsh/transform-shape :content)) + content-2 (use-equal-memo (-> shape-2 gsh/transform-shape :content)) content (mf/use-memo (mf/deps content-1 content-2) #(pb/content-bool (:bool-type shape) content-1 content-2))] - [:& shape-wrapper {:shape (-> shape - (assoc :type :path) - (assoc :content content)) - :frame frame}]))))) + [:* + [:& shape-wrapper {:shape (-> shape + (assoc :type :path) + (assoc :content content)) + :frame frame}] + + #_[:g + (for [point (app.util.path.geom/content->points content)] + [:circle {:cx (:x point) + :cy (:y point) + :r 1 + :style {:fill "blue"}}])]]))))) diff --git a/frontend/src/app/main/ui/workspace/shapes/bool.cljs b/frontend/src/app/main/ui/workspace/shapes/bool.cljs index a226eff57..e53fc3b8f 100644 --- a/frontend/src/app/main/ui/workspace/shapes/bool.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/bool.cljs @@ -34,7 +34,16 @@ frame (unchecked-get props "frame") childs-ref (mf/use-memo (mf/deps shape) #(refs/objects-by-id (:shapes shape) {:with-modifiers? true})) - childs (mf/deref childs-ref)] + {:keys [selected modifiers]} (mf/deref refs/local-displacement) + + add-modifiers + (fn [{:keys [id] :as shape}] + (cond-> shape + (contains? selected id) + (update :modifiers merge modifiers))) + + childs (->> (mf/deref childs-ref) + (mapv add-modifiers))] [:> shape-container {:shape shape} [:& shape-component diff --git a/frontend/src/app/main/ui/workspace/viewport.cljs b/frontend/src/app/main/ui/workspace/viewport.cljs index e5d2cecec..3b377a857 100644 --- a/frontend/src/app/main/ui/workspace/viewport.cljs +++ b/frontend/src/app/main/ui/workspace/viewport.cljs @@ -159,8 +159,13 @@ (hooks/setup-shortcuts node-editing? drawing-path?) (hooks/setup-active-frames objects vbox hover active-frames) + + [:div.viewport [:div.viewport-overlays + + + [:& wtr/frame-renderer {:objects objects :background background}] @@ -229,7 +234,6 @@ :on-pointer-up on-pointer-up} [:g {:style {:pointer-events (if disable-events? "none" "auto")}} - (when show-outlines? [:& outline/shape-outlines {:objects objects diff --git a/frontend/src/app/util/path/bool.cljs b/frontend/src/app/util/path/bool.cljs index d9c190286..5e0eb3068 100644 --- a/frontend/src/app/util/path/bool.cljs +++ b/frontend/src/app/util/path/bool.cljs @@ -7,165 +7,12 @@ (ns app.util.path.bool (:require [app.common.data :as d] - [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] - [app.common.geom.shapes.intersect :as gsi] - [app.common.geom.shapes.path :as gpp] - [app.common.geom.shapes.rect :as gpr] - [app.common.math :as mth] + [app.common.geom.shapes.path :as gsp] + [app.util.path.commands :as upc] [app.util.path.geom :as upg] [app.util.path.subpaths :as ups])) -(def ^:const curve-curve-precision 0.1) - -(defn curve->rect - [[from-p to-p :as curve]] - (let [extremes (->> (gpp/curve-extremities curve) - (mapv #(gpp/curve-values curve %)))] - (gpr/points->rect (into [from-p to-p] extremes)))) - -(defn curve-range->rect - [curve from-t to-t] - - (let [[from-p to-p :as curve] (gpp/subcurve-range curve from-t to-t) - extremes (->> (gpp/curve-extremities curve) - (mapv #(gpp/curve-values curve %)))] - (gpr/points->rect (into [from-p to-p] extremes)))) - -(defn line+point->tvalue - [[{x1 :x y1 :y} {x2 :x y2 :y}] {:keys [x y]}] - (if (mth/almost-zero? (- x2 x1)) - (/ (- y y1) (- y2 y1)) - (/ (- x x1) (- x2 x1)))) - -(defn line-line-intersect - [[from-p1 to-p1] [from-p2 to-p2]] - - (let [{x1 :x y1 :y} from-p1 - {x2 :x y2 :y} to-p1 - - {x3 :x y3 :y} from-p2 - {x4 :x y4 :y} to-p2 - - nx (- (* (- x3 x4) (- (* x1 y2) (* y1 x2))) - (* (- x1 x2) (- (* x3 y4) (* y3 x4)))) - - ny (- (* (- y3 y4) (- (* x1 y2) (* y1 x2))) - (* (- y1 y2) (- (* x3 y4) (* y3 x4)))) - - d (- (* (- x1 x2) (- y3 y4)) - (* (- y1 y2) (- x3 x4)))] - - (when-not (mth/almost-zero? d) - ;; ix,iy are the coordinates in the line. We calculate the - ;; tvalue that will return 0-1 as a percentage in the segment - - (let [ix (/ nx d) - iy (/ ny d) - t1 (if (mth/almost-zero? (- x2 x1)) - (/ (- iy y1) (- y2 y1)) - (/ (- ix x1) (- x2 x1))) - t2 (if (mth/almost-zero? (- x4 x3)) - (/ (- iy y3) (- y4 y3)) - (/ (- ix x3) (- x4 x3)))] - - (when (and (> t1 0) (< t1 1) - (> t2 0) (< t2 1)) - [[t1] [t2]]))))) - -(defn line-curve-intersect - [[from-p1 to-p1 :as l1] - [from-p2 to-p2 h1-p2 h2-p2 :as c2]] - - - (let [theta (-> (mth/atan2 (- (:y to-p1) (:y from-p1)) - (- (:x to-p1) (:x from-p1))) - (mth/degrees)) - - transform (-> (gmt/matrix) - (gmt/rotate (- theta)) - (gmt/translate (gpt/negate from-p1))) - - c2' [(gpt/transform from-p2 transform) - (gpt/transform to-p2 transform) - (gpt/transform h1-p2 transform) - (gpt/transform h2-p2 transform)] - - ;; Curve intersections as t-values - curve-ts (->> (gpp/curve-roots c2' :y) - (filterv #(let [curve-v (gpp/curve-values c2 %) - line-t (line+point->tvalue l1 curve-v)] - (and (> line-t 0.001) (< line-t 0.999))))) - - ;; Intersection line-curve points - intersect-ps (->> curve-ts - (mapv #(gpp/curve-values c2 %))) - - line-ts (->> intersect-ps - (mapv #(line+point->tvalue l1 %)))] - - [line-ts curve-ts])) - -(defn curve-curve-intersect - [c1 c2] - - (letfn [(remove-close-ts [ts] - (loop [current (first ts) - pending (rest ts) - acc nil - result []] - (if (nil? current) - result - (if (and (some? acc) - (< (mth/abs (- current acc)) 0.01)) - (recur (first pending) - (rest pending) - acc - result) - - (recur (first pending) - (rest pending) - current - (conj result current)))))) - - (check-range [c1-from c1-to c2-from c2-to] - (let [r1 (curve-range->rect c1 c1-from c1-to) - r2 (curve-range->rect c2 c2-from c2-to)] - - (when (gsi/overlaps-rects? r1 r2) - (if (< (gpt/distance (gpp/curve-values c1 c1-from) - (gpp/curve-values c2 c2-from)) - curve-curve-precision) - [(sorted-set (mth/precision c1-from 4)) - (sorted-set (mth/precision c2-from 4))] - - (let [c1-half (+ c1-from (/ (- c1-to c1-from) 2)) - c2-half (+ c2-from (/ (- c2-to c2-from) 2)) - - [c1-ts-1 c2-ts-1] (check-range c1-from c1-half c2-from c2-half) - [c1-ts-2 c2-ts-2] (check-range c1-from c1-half c2-half c2-to) - [c1-ts-3 c2-ts-3] (check-range c1-half c1-to c2-from c2-half) - [c1-ts-4 c2-ts-4] (check-range c1-half c1-to c2-half c2-to)] - - [(into (sorted-set) (d/concat [] c1-ts-1 c1-ts-2 c1-ts-3 c1-ts-4)) - (into (sorted-set) (d/concat [] c2-ts-1 c2-ts-2 c2-ts-3 c2-ts-4))])))))] - - (let [[c1-ts c2-ts] (check-range 0.005 0.995 0.005 0.995) - c1-ts (remove-close-ts c1-ts) - c2-ts (remove-close-ts c2-ts)] - [c1-ts c2-ts]))) - -(defn- line-to->line - [cmd] - [(:prev cmd) (gpp/command->point cmd)]) - -(defn- curve-to->bezier - [cmd] - [(:prev cmd) - (gpp/command->point cmd) - (gpt/point (-> cmd :params :c1x) (-> cmd :params :c1y)) - (gpt/point (-> cmd :params :c2x) (-> cmd :params :c2y))]) - (defn- split-command [cmd values] (case (:command cmd) @@ -178,26 +25,26 @@ (cond (and (= :line-to (:command seg-1)) (= :line-to (:command seg-2))) - (line-line-intersect (line-to->line seg-1) (line-to->line seg-2)) + (gsp/line-line-intersect (gsp/command->line seg-1) (gsp/command->line seg-2)) (and (= :line-to (:command seg-1)) (= :curve-to (:command seg-2))) - (line-curve-intersect (line-to->line seg-1) (curve-to->bezier seg-2)) + (gsp/line-curve-intersect (gsp/command->line seg-1) (gsp/command->bezier seg-2)) (and (= :curve-to (:command seg-1)) (= :line-to (:command seg-2))) (let [[seg-2' seg-1'] - (line-curve-intersect (line-to->line seg-2) (curve-to->bezier seg-1))] + (gsp/line-curve-intersect (gsp/command->line seg-2) (gsp/command->bezier seg-1))] ;; Need to reverse because we send the arguments reversed [seg-1' seg-2']) (and (= :curve-to (:command seg-1)) (= :curve-to (:command seg-2))) - (curve-curve-intersect (curve-to->bezier seg-1) (curve-to->bezier seg-2)) + (gsp/curve-curve-intersect (gsp/command->bezier seg-1) (gsp/command->bezier seg-2)) :else [[] []])] - + [(split-command seg-1 ts-seg-1) (split-command seg-2 ts-seg-2)])) @@ -212,94 +59,162 @@ (assoc :prev first) (some? prev) - (assoc :prev (gpp/command->point prev)))))))) + (assoc :prev (gsp/command->point prev)))))))) (defn content-intersect-split "Given two path contents will return the intersect between them" [content-a content-b] - (let [content-a (add-previous content-a) - content-b (add-previous content-b)] - (if (or (empty? content-a) (empty? content-b)) - [content-a content-b] + (if (or (empty? content-a) (empty? content-b)) + [content-a content-b] - (loop [current (first content-a) - pending (rest content-a) - content-b content-b - new-content-a []] + (loop [current (first content-a) + pending (rest content-a) + content-b content-b + new-content-a []] - (if (not (some? current)) - [new-content-a content-b] + (if (not (some? current)) + [new-content-a content-b] - (let [[new-current new-pending new-content-b] + (let [[new-current new-pending new-content-b] - (loop [current current - pending pending - other (first content-b) - head-content [] - tail-content (rest content-b)] + (loop [current current + pending pending + other (first content-b) + head-content [] + tail-content (rest content-b)] - (if (not (some? other)) - ;; Finished recorring second content - [current pending head-content] + (if (not (some? other)) + ;; Finished recorring second content + [current pending head-content] - ;; We split the current - (let [[new-as new-bs] (split current other) - new-as (add-previous new-as (:prev current)) - new-bs (add-previous new-bs (:prev other))] - - (if (> (count new-as) 1) - ;; We add the new-a's to the stack and change the b then we iterate to the top - (recur (first new-as) - (d/concat [] (rest new-as) pending) - (first tail-content) - (d/concat [] head-content new-bs) - (rest tail-content)) + ;; We split the current + (let [[new-as new-bs] (split current other) + new-as (add-previous new-as (:prev current)) + new-bs (add-previous new-bs (:prev other))] - ;; No current segment-segment split we continue searching - (recur current - pending - (first tail-content) - (conj head-content other) - (rest tail-content))))))] + (if (> (count new-as) 1) + ;; We add the new-a's to the stack and change the b then we iterate to the top + (recur (first new-as) + (d/concat [] (rest new-as) pending) + (first tail-content) + (d/concat [] head-content new-bs) + (rest tail-content)) - (recur (first new-pending) - (rest new-pending) - new-content-b - (conj new-content-a new-current)))))))) + ;; No current segment-segment split we continue searching + (recur current + pending + (first tail-content) + (conj head-content other) + (rest tail-content))))))] + (recur (first new-pending) + (rest new-pending) + new-content-b + (conj new-content-a new-current))))))) -(defn create-union [content-a content-b] +(defn is-segment? + [cmd] + (and (contains? cmd :prev) + (contains? #{:line-to :curve-to} (:command cmd)))) + +(defn contains-segment? + [segment content] + + (let [point (case (:command segment) + :line-to (-> (gsp/command->line segment) + (gsp/line-values 0.5)) + + :curve-to (-> (gsp/command->bezier segment) + (gsp/curve-values 0.5)))] + (gsp/is-point-in-content? point content))) + +(defn create-union [content-a content-a-split content-b content-b-split] + ;; Pick all segments in content-a that are not inside content-b + ;; Pick all segments in content-b that are not inside content-a (d/concat [] - content-a - (ups/reverse-content content-b))) + (->> content-a-split (filter #(not (contains-segment? % content-b)))) + (->> content-b-split (filter #(not (contains-segment? % content-a)))))) -(defn create-difference [content-a content-b] +(defn create-difference [content-a content-a-split content-b content-b-split] + ;; Pick all segments in content-a that are not inside content-b + ;; Pick all segments in content b that are inside content-a (d/concat [] - content-a - (ups/reverse-content content-b))) + (->> content-a-split (filter #(not (contains-segment? % content-b)))) + (->> content-b-split (filter #(contains-segment? % content-a))))) -(defn create-intersection [content-a content-b] +(defn create-intersection [content-a content-a-split content-b content-b-split] + ;; Pick all segments in content-a that are inside content-b + ;; Pick all segments in content-b that are inside content-a (d/concat [] - content-a - (ups/reverse-content content-b))) + (->> content-a-split (filter #(contains-segment? % content-b))) + (->> content-b-split (filter #(contains-segment? % content-a))))) +(defn reverse-command + "Reverses a single command" + [command] + + (let [{old-x :x old-y :y} (:params command) + {:keys [x y]} (:prev command) + {:keys [c1x c1y c2x c2y]} (:params command)] + + (-> command + (assoc :prev (gpt/point old-x old-y)) + (update :params assoc :x x :y y) + + (cond-> (= :curve-to (:command command)) + (update :params assoc + :c1x c2x :c1y c2y + :c2x c1x :c2y c1y))))) (defn create-exclusion [content-a content-b] - (d/concat - [] - content-a - (ups/reverse-content content-b))) + ;; Pick all segments but reverse content-b (so it makes an exclusion) + (let [content-b' (->> (reverse content-b) + (mapv reverse-command))] + (d/concat [] content-a content-b'))) + + +(defn fix-move-to + [content] + ;; Remove the field `:prev` and makes the necesaries `move-to` + ;; then clean the subpaths + + (loop [current (first content) + content (rest content) + prev nil + result []] + + (if (nil? current) + result + + (let [result (if (not= (:prev current) prev) + (conj result (upc/make-move-to (:prev current))) + result)] + (recur (first content) + (rest content) + (gsp/command->point current) + (conj result (dissoc current :prev))))))) (defn content-bool [bool-type content-a content-b] - (let [[content-a' content-b'] (content-intersect-split content-a content-b)] - (case bool-type - :union (create-union content-a' content-b') - :difference (create-difference content-a' content-b') - :intersection (create-intersection content-a' content-b') - :exclusion (create-exclusion content-a' content-b')))) + (let [content-a (add-previous content-a) + content-b (add-previous content-b) + + ;; Split content in new segments in the intersection with the other path + [content-a-split content-b-split] (content-intersect-split content-a content-b) + content-a-split (->> content-a-split add-previous (filter is-segment?)) + content-b-split (->> content-b-split add-previous (filter is-segment?)) + + bool-content + (case bool-type + :union (create-union content-a content-a-split content-b content-b-split) + :difference (create-difference content-a content-a-split content-b content-b-split) + :intersection (create-intersection content-a content-a-split content-b content-b-split) + :exclude (create-exclusion content-a-split content-b-split))] + + (->> (fix-move-to bool-content) + (ups/close-subpaths)))) diff --git a/frontend/src/app/util/path/format.cljs b/frontend/src/app/util/path/format.cljs index 4b0640f4e..312746f90 100644 --- a/frontend/src/app/util/path/format.cljs +++ b/frontend/src/app/util/path/format.cljs @@ -7,6 +7,7 @@ (ns app.util.path.format (:require [app.util.path.commands :as upc] + [app.util.path.subpaths :refer [pt=]] [cuerdas.core :as str])) (defn command->param-list [command] @@ -62,6 +63,12 @@ (str command-str param-list))) +(defn set-point + [command point] + (-> command + (assoc-in [:params :x] (:x point)) + (assoc-in [:params :y] (:y point)))) + (defn format-path [content] (with-out-str (loop [last-move nil @@ -72,9 +79,12 @@ (let [point (upc/command->point current) current-move? (= :move-to (:command current)) last-move (if current-move? point last-move)] - (print (command->string current)) - (when (and (not current-move?) (= last-move point)) + (if (and (not current-move?) (pt= last-move point)) + (println (command->string (set-point current last-move))) + (println (command->string current))) + + (when (and (not current-move?) (pt= last-move point)) (print "Z")) (recur last-move diff --git a/frontend/src/app/util/path/geom.cljs b/frontend/src/app/util/path/geom.cljs index 08432f41a..afb8787a1 100644 --- a/frontend/src/app/util/path/geom.cljs +++ b/frontend/src/app/util/path/geom.cljs @@ -46,24 +46,29 @@ (let [to-p (upc/command->point cmd)] (->> (conj values 1) (mapv (fn [val] - (upc/make-line-to (gpt/lerp from-p to-p val))))))) + (-> (gpt/lerp from-p to-p val) + #_(gpt/round 2) + (upc/make-line-to))))))) (defn split-curve-to-ranges "Splits a curve into several curves given the points in `values` for example (split-curve-to-ranges p c [0 0.25 0.5 0.75 1] will split the curve into 4 curves that draw the same curve" [from-p cmd values] - (let [to-p (upc/command->point cmd) - params (:params cmd) - h1 (gpt/point (:c1x params) (:c1y params)) - h2 (gpt/point (:c2x params) (:c2y params))] + (if (empty? values) + [cmd] + (let [to-p (upc/command->point cmd) + params (:params cmd) + h1 (gpt/point (:c1x params) (:c1y params)) + h2 (gpt/point (:c2x params) (:c2y params)) - (->> (d/with-prev (conj values 1)) - (mapv - (fn [[t1 t0]] - (let [t0 (if (nil? t0) 0 t0) - [_ to-p h1' h2'] (gshp/subcurve-range from-p to-p h1 h2 t0 t1)] - (upc/make-curve-to to-p h1' h2'))))))) + values-set (->> (conj values 1) (into (sorted-set)))] + (->> (d/with-prev values-set) + (mapv + (fn [[t1 t0]] + (let [t0 (if (nil? t0) 0 t0) + [_ to-p h1' h2'] (gshp/subcurve-range from-p to-p h1 h2 t0 t1)] + (upc/make-curve-to (-> to-p #_(gpt/round 2)) h1' h2')))))))) (defn opposite-handler "Calculates the coordinates of the opposite handler" diff --git a/frontend/src/app/util/path/subpaths.cljs b/frontend/src/app/util/path/subpaths.cljs index 5f0e2bf34..d4ddf10b3 100644 --- a/frontend/src/app/util/path/subpaths.cljs +++ b/frontend/src/app/util/path/subpaths.cljs @@ -7,8 +7,14 @@ (ns app.util.path.subpaths (:require [app.common.data :as d] + [app.common.geom.point :as gpt] [app.util.path.commands :as upc])) +(defn pt= + "Check if two points are close" + [p1 p2] + (< (gpt/distance p1 p2) 0.1)) + (defn make-subpath "Creates a subpath either from a single command or with all the data" ([command] @@ -76,7 +82,7 @@ (defn subpaths-join "Join two subpaths together when the first finish where the second starts" [subpath other] - (assert (= (:to subpath) (:from other))) + (assert (pt= (:to subpath) (:from other))) (-> subpath (update :data d/concat (rest (:data other))) (assoc :to (:to other)))) @@ -88,15 +94,22 @@ (let [merge-with-candidate (fn [[candidate result] current] (cond - (= (:to current) (:from current)) + (pt= (:to current) (:from current)) + ;; Subpath is already a closed path [candidate (conj result current)] - (= (:to candidate) (:from current)) + (pt= (:to candidate) (:from current)) [(subpaths-join candidate current) result] - (= (:to candidate) (:to current)) + (pt= (:from candidate) (:to current)) + [(subpaths-join current candidate) result] + + (pt= (:to candidate) (:to current)) [(subpaths-join candidate (reverse-subpath current)) result] + (pt= (:from candidate) (:from current)) + [(subpaths-join (reverse-subpath current) candidate) result] + :else [candidate (conj result current)]))] @@ -114,7 +127,7 @@ (if (some? current) (let [[new-current new-subpaths] - (if (= (:from current) (:to current)) + (if (pt= (:from current) (:to current)) [current subpaths] (merge-paths current subpaths))] From 6fd35ae5d926bc6bd305c6e1fc805f220394fdf8 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Fri, 17 Sep 2021 10:50:35 +0200 Subject: [PATCH 06/16] :sparkles: Updates selrects, groups to path --- common/src/app/common/geom/shapes.cljc | 4 + common/src/app/common/geom/shapes/bool.cljc | 25 +++++ common/src/app/common/geom/shapes/path.cljc | 88 ++++++++++++++++- common/src/app/common/pages.cljc | 1 + common/src/app/common/pages/changes.cljc | 6 +- common/src/app/common/pages/helpers.cljc | 4 + .../src/app/common/path/bool.cljc | 21 ++-- .../src/app/common/path/commands.cljc | 2 +- .../src/app/common/path/shapes_to_path.cljc | 98 +++++++++++++------ .../src/app/common/path/subpaths.cljc | 4 +- .../src/app/main/data/workspace/booleans.cljs | 33 ++----- .../app/main/data/workspace/path/drawing.cljs | 6 +- .../app/main/data/workspace/path/edition.cljs | 8 +- .../app/main/data/workspace/path/helpers.cljs | 4 +- .../data/workspace/path/shapes_to_path.cljs | 21 ++++ .../app/main/data/workspace/path/state.cljs | 5 +- .../app/main/data/workspace/path/streams.cljs | 2 +- .../app/main/data/workspace/path/tools.cljs | 4 +- frontend/src/app/main/refs.cljs | 38 +++++-- frontend/src/app/main/ui/shapes/bool.cljs | 44 ++++----- .../app/main/ui/workspace/context_menu.cljs | 8 +- .../app/main/ui/workspace/shapes/bool.cljs | 14 +-- .../app/main/ui/workspace/shapes/path.cljs | 2 +- .../main/ui/workspace/shapes/path/editor.cljs | 15 ++- frontend/src/app/util/path/format.cljs | 4 +- frontend/src/app/util/path/geom.cljs | 97 ------------------ frontend/src/app/util/path/parser.cljs | 4 +- frontend/src/app/util/path/tools.cljs | 4 +- 28 files changed, 327 insertions(+), 239 deletions(-) create mode 100644 common/src/app/common/geom/shapes/bool.cljc rename frontend/src/app/util/path/bool.cljs => common/src/app/common/path/bool.cljc (94%) rename frontend/src/app/util/path/commands.cljs => common/src/app/common/path/commands.cljc (99%) rename frontend/src/app/util/path/shapes_to_path.cljs => common/src/app/common/path/shapes_to_path.cljc (68%) rename frontend/src/app/util/path/subpaths.cljs => common/src/app/common/path/subpaths.cljc (98%) create mode 100644 frontend/src/app/main/data/workspace/path/shapes_to_path.cljs delete mode 100644 frontend/src/app/util/path/geom.cljs diff --git a/common/src/app/common/geom/shapes.cljc b/common/src/app/common/geom/shapes.cljc index 7ef49ea81..6a21f6c58 100644 --- a/common/src/app/common/geom/shapes.cljc +++ b/common/src/app/common/geom/shapes.cljc @@ -8,6 +8,7 @@ (:require [app.common.data :as d] [app.common.geom.point :as gpt] + [app.common.geom.shapes.bool :as gsb] [app.common.geom.shapes.common :as gco] [app.common.geom.shapes.intersect :as gin] [app.common.geom.shapes.path :as gsp] @@ -164,3 +165,6 @@ (d/export gin/has-point?) (d/export gin/has-point-rect?) (d/export gin/rect-contains-shape?) + +;; Bool +(d/export gsb/update-bool-selrect) diff --git a/common/src/app/common/geom/shapes/bool.cljc b/common/src/app/common/geom/shapes/bool.cljc new file mode 100644 index 000000000..35c91b2dc --- /dev/null +++ b/common/src/app/common/geom/shapes/bool.cljc @@ -0,0 +1,25 @@ +;; 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) UXBOX Labs SL + +(ns app.common.geom.shapes.bool + (:require + [app.common.geom.shapes.path :as gsp] + [app.common.geom.shapes.rect :as gpr] + [app.common.path.bool :as pb] + [app.common.path.shapes-to-path :as stp])) + +(defn update-bool-selrect + [shape children objects] + + (let [selrect (->> children + (map #(stp/convert-to-path % objects)) + (mapv :content) + (pb/content-bool (:bool-type shape)) + (gsp/content->selrect)) + points (gpr/rect->points selrect)] + (-> shape + (assoc :selrect selrect) + (assoc :points points)))) diff --git a/common/src/app/common/geom/shapes/path.cljc b/common/src/app/common/geom/shapes/path.cljc index 6a7c4c6d8..b2d5643b3 100644 --- a/common/src/app/common/geom/shapes/path.cljc +++ b/common/src/app/common/geom/shapes/path.cljc @@ -10,10 +10,42 @@ [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] [app.common.geom.shapes.rect :as gpr] - [app.common.math :as mth])) + [app.common.math :as mth] + [app.common.path.commands :as upc])) (def ^:const curve-curve-precision 0.1) +(defn calculate-opposite-handler + "Given a point and its handler, gives the symetric handler" + [point handler] + (let [handler-vector (gpt/to-vec point handler)] + (gpt/add point (gpt/negate handler-vector)))) + +(defn opposite-handler + "Calculates the coordinates of the opposite handler" + [point handler] + (let [phv (gpt/to-vec point handler)] + (gpt/add point (gpt/negate phv)))) + +(defn opposite-handler-keep-distance + "Calculates the coordinates of the opposite handler but keeping the old distance" + [point handler old-opposite] + (let [old-distance (gpt/distance point old-opposite) + phv (gpt/to-vec point handler) + phv2 (gpt/multiply + (gpt/unit (gpt/negate phv)) + (gpt/point old-distance))] + (gpt/add point phv2))) + +(defn content->points + "Returns the points in the given content" + [content] + (->> content + (map #(when (-> % :params :x) + (gpt/point (-> % :params :x) (-> % :params :y)))) + (remove nil?) + (into []))) + (defn line-values [[from-p to-p] t] (let [move-v (-> (gpt/to-vec from-p to-p) @@ -670,3 +702,57 @@ (map second) (reduce +) (not= 0)))) + +(defn split-line-to + "Given a point and a line-to command will create a two new line-to commands + that will split the original line into two given a value between 0-1" + [from-p cmd t-val] + (let [to-p (upc/command->point cmd) + sp (gpt/lerp from-p to-p t-val)] + [(upc/make-line-to sp) cmd])) + +(defn split-curve-to + "Given the point and a curve-to command will split the curve into two new + curve-to commands given a value between 0-1" + [from-p cmd t-val] + (let [params (:params cmd) + end (gpt/point (:x params) (:y params)) + h1 (gpt/point (:c1x params) (:c1y params)) + h2 (gpt/point (:c2x params) (:c2y params)) + [[_ to1 h11 h21] + [_ to2 h12 h22]] (curve-split from-p end h1 h2 t-val)] + [(upc/make-curve-to to1 h11 h21) + (upc/make-curve-to to2 h12 h22)])) + +(defn split-line-to-ranges + "Splits a line into several lines given the points in `values` + for example (split-line-to-ranges p c [0 0.25 0.5 0.75 1] will split + the line into 4 lines" + [from-p cmd values] + (let [to-p (upc/command->point cmd)] + (->> (conj values 1) + (mapv (fn [val] + (-> (gpt/lerp from-p to-p val) + #_(gpt/round 2) + (upc/make-line-to))))))) + +(defn split-curve-to-ranges + "Splits a curve into several curves given the points in `values` + for example (split-curve-to-ranges p c [0 0.25 0.5 0.75 1] will split + the curve into 4 curves that draw the same curve" + [from-p cmd values] + (if (empty? values) + [cmd] + (let [to-p (upc/command->point cmd) + params (:params cmd) + h1 (gpt/point (:c1x params) (:c1y params)) + h2 (gpt/point (:c2x params) (:c2y params)) + + values-set (->> (conj values 1) (into (sorted-set)))] + (->> (d/with-prev values-set) + (mapv + (fn [[t1 t0]] + (let [t0 (if (nil? t0) 0 t0) + [_ to-p h1' h2'] (subcurve-range from-p to-p h1 h2 t0 t1)] + (upc/make-curve-to (-> to-p #_(gpt/round 2)) h1' h2')))))))) + diff --git a/common/src/app/common/pages.cljc b/common/src/app/common/pages.cljc index fdf02cfa3..640a6858e 100644 --- a/common/src/app/common/pages.cljc +++ b/common/src/app/common/pages.cljc @@ -40,6 +40,7 @@ (d/export helpers/get-children) (d/export helpers/get-children-objects) (d/export helpers/get-object-with-children) +(d/export helpers/select-children) (d/export helpers/is-shape-grouped) (d/export helpers/get-parent) (d/export helpers/get-parents) diff --git a/common/src/app/common/pages/changes.cljc b/common/src/app/common/pages/changes.cljc index 997edc170..a2211b238 100644 --- a/common/src/app/common/pages/changes.cljc +++ b/common/src/app/common/pages/changes.cljc @@ -9,6 +9,7 @@ [app.common.data :as d] [app.common.exceptions :as ex] [app.common.geom.shapes :as gsh] + [app.common.geom.shapes.bool :as gshb] [app.common.pages.common :refer [component-sync-attrs]] [app.common.pages.helpers :as cph] [app.common.pages.init :as init] @@ -156,7 +157,7 @@ (sequence (comp (mapcat #(cons % (cph/get-parents % objects))) (map #(get objects %)) - (filter #(= (:type %) :group)) + (filter #(contains? #{:group :bool} (:type %))) (map :id) (distinct)) shapes))) @@ -177,6 +178,9 @@ (empty? children) group + (= :bool (:type group)) + (gshb/update-bool-selrect group children objects) + (:masked-group? group) (set-mask-selrect group children) diff --git a/common/src/app/common/pages/helpers.cljc b/common/src/app/common/pages/helpers.cljc index 0d729275b..d170b45f1 100644 --- a/common/src/app/common/pages/helpers.cljc +++ b/common/src/app/common/pages/helpers.cljc @@ -138,6 +138,10 @@ [id objects] (mapv #(get objects %) (cons id (get-children id objects)))) +(defn select-children [id objects] + (->> (get-children id objects) + (select-keys objects))) + (defn is-shape-grouped "Checks if a shape is inside a group" [shape-id objects] diff --git a/frontend/src/app/util/path/bool.cljs b/common/src/app/common/path/bool.cljc similarity index 94% rename from frontend/src/app/util/path/bool.cljs rename to common/src/app/common/path/bool.cljc index 5e0eb3068..3d61ff98c 100644 --- a/frontend/src/app/util/path/bool.cljs +++ b/common/src/app/common/path/bool.cljc @@ -4,20 +4,19 @@ ;; ;; Copyright (c) UXBOX Labs SL -(ns app.util.path.bool +(ns app.common.path.bool (:require [app.common.data :as d] [app.common.geom.point :as gpt] [app.common.geom.shapes.path :as gsp] - [app.util.path.commands :as upc] - [app.util.path.geom :as upg] - [app.util.path.subpaths :as ups])) + [app.common.path.commands :as upc] + [app.common.path.subpaths :as ups])) (defn- split-command [cmd values] (case (:command cmd) - :line-to (upg/split-line-to-ranges (:prev cmd) cmd values) - :curve-to (upg/split-curve-to-ranges (:prev cmd) cmd values) + :line-to (gsp/split-line-to-ranges (:prev cmd) cmd values) + :curve-to (gsp/split-curve-to-ranges (:prev cmd) cmd values) [cmd])) (defn split [seg-1 seg-2] @@ -198,7 +197,7 @@ (gsp/command->point current) (conj result (dissoc current :prev))))))) -(defn content-bool +(defn content-bool-pair [bool-type content-a content-b] (let [content-a (add-previous content-a) @@ -218,3 +217,11 @@ (->> (fix-move-to bool-content) (ups/close-subpaths)))) + +(defn content-bool + [bool-type contents] + ;; We apply the boolean operation in to each pair and the result to the next + ;; element + (->> contents + (reduce (partial content-bool-pair bool-type)) + (into []))) diff --git a/frontend/src/app/util/path/commands.cljs b/common/src/app/common/path/commands.cljc similarity index 99% rename from frontend/src/app/util/path/commands.cljs rename to common/src/app/common/path/commands.cljc index fd1df9da9..80737db8c 100644 --- a/frontend/src/app/util/path/commands.cljs +++ b/common/src/app/common/path/commands.cljc @@ -4,7 +4,7 @@ ;; ;; Copyright (c) UXBOX Labs SL -(ns app.util.path.commands +(ns app.common.path.commands (:require [app.common.data :as d] [app.common.geom.point :as gpt])) diff --git a/frontend/src/app/util/path/shapes_to_path.cljs b/common/src/app/common/path/shapes_to_path.cljc similarity index 68% rename from frontend/src/app/util/path/shapes_to_path.cljs rename to common/src/app/common/path/shapes_to_path.cljc index 8d7c86cbd..ec8294fd0 100644 --- a/frontend/src/app/util/path/shapes_to_path.cljs +++ b/common/src/app/common/path/shapes_to_path.cljc @@ -4,22 +4,46 @@ ;; ;; Copyright (c) UXBOX Labs SL -(ns app.util.path.shapes-to-path +(ns app.common.path.shapes-to-path (:require [app.common.data :as d] [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] - [app.common.geom.shapes :as gsh] + [app.common.geom.shapes.common :as gsc] [app.common.geom.shapes.path :as gsp] - [app.util.path.commands :as pc])) + [app.common.path.commands :as pc])) -(def bezier-circle-c 0.551915024494) -(def dissoc-attrs [:x :y :width :height - :rx :ry :r1 :r2 :r3 :r4 - :medata]) -(def allowed-transform-types #{:rect - :circle - :image}) +(def ^:const bezier-circle-c 0.551915024494) + +(def ^:const dissoc-attrs + [:x :y :width :height + :rx :ry :r1 :r2 :r3 :r4 + :metadata :shapes]) + +(def ^:const allowed-transform-types + #{:rect + :circle + :image + :group}) + +(def ^:const style-properties + [:fill-color + :fill-opacity + :fill-color-gradient + :fill-color-ref-file + :fill-color-ref-id + :fill-image + :stroke-color + :stroke-color-ref-file + :stroke-color-ref-id + :stroke-opacity + :stroke-style + :stroke-width + :stroke-alignment + :stroke-cap-start + :stroke-cap-end + :shadow + :blur]) (defn make-corner-arc "Creates a curvle corner for border radius" @@ -86,8 +110,9 @@ (defn rect->path "Creates a bezier curve that approximates a rounded corner rectangle" - [x y width height r1 r2 r3 r4] - (let [p1 (gpt/point x (+ y r1)) + [x y width height r1 r2 r3 r4 rx] + (let [[r1 r2 r3 r4] (->> [r1 r2 r3 r4] (mapv #(or % rx 0))) + p1 (gpt/point x (+ y r1)) p2 (gpt/point (+ x r1) y) p3 (gpt/point (+ width x (- r2)) y) @@ -113,34 +138,51 @@ (conj (make-corner-arc p7 p8 :bottom-left r4))) (conj (pc/make-line-to p1))))) +(declare convert-to-path) + +(defn group-to-path + [group objects] + + (let [xform (comp (map #(get objects %)) + (map #(-> (convert-to-path % objects)))) + + child-as-paths (into [] xform (:shapes group)) + head (first child-as-paths) + head-data (select-keys head style-properties) + content (into [] (mapcat :content) child-as-paths)] + + (-> group + (assoc :type :path) + (assoc :content content) + (merge head-data) + (d/without-keys dissoc-attrs)))) + (defn convert-to-path "Transforms the given shape to a path" - [{:keys [type x y width height r1 r2 r3 r4 rx metadata] :as shape}] + [{:keys [type x y width height r1 r2 r3 r4 rx metadata] :as shape} objects] + (assert (map? objects)) + (cond + (= (:type shape) :group) + (group-to-path shape objects) - (if (contains? allowed-transform-types type) - (let [r1 (or r1 rx 0) - r2 (or r2 rx 0) - r3 (or r3 rx 0) - r4 (or r4 rx 0) - - new-content + (contains? allowed-transform-types type) + (let [new-content (case type - :circle - (circle->path x y width height) - (rect->path x y width height r1 r2 r3 r4)) + :circle (circle->path x y width height) + #_:else (rect->path x y width height r1 r2 r3 r4 rx)) ;; Apply the transforms that had the shape transform (:transform shape) new-content (cond-> new-content (some? transform) - (gsp/transform-content (gmt/transform-in (gsh/center-shape shape) transform)))] + (gsp/transform-content (gmt/transform-in (gsc/center-shape shape) transform)))] (-> shape - (d/without-keys dissoc-attrs) (assoc :type :path) (assoc :content new-content) - (cond-> (= :image type) (-> (assoc :fill-image metadata) - (dissoc :metadata))))) + (cond-> (= :image type) + (assoc :fill-image metadata)) + (d/without-keys dissoc-attrs))) + :else ;; Do nothing if the shape is not of a correct type shape)) - diff --git a/frontend/src/app/util/path/subpaths.cljs b/common/src/app/common/path/subpaths.cljc similarity index 98% rename from frontend/src/app/util/path/subpaths.cljs rename to common/src/app/common/path/subpaths.cljc index d4ddf10b3..3c4d90c68 100644 --- a/frontend/src/app/util/path/subpaths.cljs +++ b/common/src/app/common/path/subpaths.cljc @@ -4,11 +4,11 @@ ;; ;; Copyright (c) UXBOX Labs SL -(ns app.util.path.subpaths +(ns app.common.path.subpaths (:require [app.common.data :as d] [app.common.geom.point :as gpt] - [app.util.path.commands :as upc])) + [app.common.path.commands :as upc])) (defn pt= "Check if two points are close" diff --git a/frontend/src/app/main/data/workspace/booleans.cljs b/frontend/src/app/main/data/workspace/booleans.cljs index b2be46791..301fa9c2f 100644 --- a/frontend/src/app/main/data/workspace/booleans.cljs +++ b/frontend/src/app/main/data/workspace/booleans.cljs @@ -10,6 +10,7 @@ [app.common.geom.shapes :as gsh] [app.common.pages :as cp] [app.common.pages.changes-builder :as cb] + [app.common.path.shapes-to-path :as stp] [app.common.uuid :as uuid] [app.main.data.workspace.changes :as dch] [app.main.data.workspace.common :as dwc] @@ -18,24 +19,6 @@ [cuerdas.core :as str] [potok.core :as ptk])) -(def ^:const style-properties - [:fill-color - :fill-opacity - :fill-color-gradient - :fill-color-ref-file - :fill-color-ref-id - :stroke-color - :stroke-color-ref-file - :stroke-color-ref-id - :stroke-opacity - :stroke-style - :stroke-width - :stroke-alignment - :stroke-cap-start - :stroke-cap-end - :shadow - :blur]) - (defn selected-shapes [state] (let [objects (wsh/lookup-page-objects state)] @@ -47,10 +30,10 @@ (sort-by ::index)))) (defn create-bool-data - [type name shapes] - (let [head (first shapes) - head-data (select-keys head style-properties) - selrect (gsh/selection-rect shapes)] + [type name shapes objects] + (let [shapes (mapv #(stp/convert-to-path % objects) shapes) + head (first shapes) + head-data (select-keys head stp/style-properties)] (-> {:id (uuid/next) :type :bool :bool-type type @@ -60,7 +43,7 @@ ::index (::index head) :shapes []} (merge head-data) - (gsh/setup selrect)))) + (gsh/update-bool-selrect shapes objects)))) (defn create-bool [bool-type] @@ -69,14 +52,14 @@ (watch [it state _] (let [page-id (:current-page-id state) - objects (wsh/lookup-page-objects state page-id) + objects (wsh/lookup-page-objects state) base-name (-> bool-type d/name str/capital (str "-1")) name (-> (dwc/retrieve-used-names objects) (dwc/generate-unique-name base-name)) shapes (selected-shapes state)] (when-not (empty? shapes) - (let [boolean-data (create-bool-data bool-type name shapes) + (let [boolean-data (create-bool-data bool-type name shapes objects) shape-id (:id boolean-data) changes (-> (cb/empty-changes it page-id) (cb/add-obj boolean-data) diff --git a/frontend/src/app/main/data/workspace/path/drawing.cljs b/frontend/src/app/main/data/workspace/path/drawing.cljs index 202266e79..8e7de7cae 100644 --- a/frontend/src/app/main/data/workspace/path/drawing.cljs +++ b/frontend/src/app/main/data/workspace/path/drawing.cljs @@ -7,7 +7,10 @@ (ns app.main.data.workspace.path.drawing (:require [app.common.geom.point :as gpt] + [app.common.geom.shapes.path :as upg] [app.common.pages :as cp] + [app.common.path.commands :as upc] + [app.common.path.shapes-to-path :as upsp] [app.common.spec :as us] [app.main.data.workspace.changes :as dch] [app.main.data.workspace.common :as dwc] @@ -21,9 +24,6 @@ [app.main.data.workspace.path.undo :as undo] [app.main.data.workspace.state-helpers :as wsh] [app.main.streams :as ms] - [app.util.path.commands :as upc] - [app.util.path.geom :as upg] - [app.util.path.shapes-to-path :as upsp] [beicon.core :as rx] [potok.core :as ptk])) diff --git a/frontend/src/app/main/data/workspace/path/edition.cljs b/frontend/src/app/main/data/workspace/path/edition.cljs index 89331ebd2..9df8b6e9a 100644 --- a/frontend/src/app/main/data/workspace/path/edition.cljs +++ b/frontend/src/app/main/data/workspace/path/edition.cljs @@ -8,6 +8,10 @@ (:require [app.common.data :as d] [app.common.geom.point :as gpt] + [app.common.geom.shapes.path :as upg] + [app.common.path.commands :as upc] + [app.common.path.shapes-to-path :as upsp] + [app.common.path.subpaths :as ups] [app.main.data.workspace.changes :as dch] [app.main.data.workspace.common :as dwc] [app.main.data.workspace.path.changes :as changes] @@ -19,10 +23,6 @@ [app.main.data.workspace.path.undo :as undo] [app.main.data.workspace.state-helpers :as wsh] [app.main.streams :as ms] - [app.util.path.commands :as upc] - [app.util.path.geom :as upg] - [app.util.path.shapes-to-path :as upsp] - [app.util.path.subpaths :as ups] [app.util.path.tools :as upt] [beicon.core :as rx] [potok.core :as ptk])) diff --git a/frontend/src/app/main/data/workspace/path/helpers.cljs b/frontend/src/app/main/data/workspace/path/helpers.cljs index 9b36e4099..a7d47d238 100644 --- a/frontend/src/app/main/data/workspace/path/helpers.cljs +++ b/frontend/src/app/main/data/workspace/path/helpers.cljs @@ -10,10 +10,10 @@ [app.common.geom.point :as gpt] [app.common.geom.shapes :as gsh] [app.common.math :as mth] + [app.common.path.commands :as upc] + [app.common.path.subpaths :as ups] [app.main.data.workspace.path.common :as common] [app.main.streams :as ms] - [app.util.path.commands :as upc] - [app.util.path.subpaths :as ups] [potok.core :as ptk])) (defn end-path-event? [event] diff --git a/frontend/src/app/main/data/workspace/path/shapes_to_path.cljs b/frontend/src/app/main/data/workspace/path/shapes_to_path.cljs new file mode 100644 index 000000000..6f68123de --- /dev/null +++ b/frontend/src/app/main/data/workspace/path/shapes_to_path.cljs @@ -0,0 +1,21 @@ +;; 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) UXBOX Labs SL + +(ns app.main.data.workspace.path.shapes-to-path + (:require + [app.common.path.shapes-to-path :as upsp] + [app.main.data.workspace.changes :as dch] + [app.main.data.workspace.state-helpers :as wsh] + [beicon.core :as rx] + [potok.core :as ptk])) + +(defn convert-selected-to-path [] + (ptk/reify ::convert-selected-to-path + ptk/WatchEvent + (watch [_ state _] + (let [objects (wsh/lookup-page-objects state) + selected (wsh/lookup-selected state)] + (rx/of (dch/update-shapes selected #(upsp/convert-to-path % objects))))))) diff --git a/frontend/src/app/main/data/workspace/path/state.cljs b/frontend/src/app/main/data/workspace/path/state.cljs index 229e46256..86ac1731e 100644 --- a/frontend/src/app/main/data/workspace/path/state.cljs +++ b/frontend/src/app/main/data/workspace/path/state.cljs @@ -7,7 +7,7 @@ (ns app.main.data.workspace.path.state (:require [app.common.data :as d] - [app.util.path.shapes-to-path :as upsp])) + [app.common.path.shapes-to-path :as upsp])) (defn get-path-id "Retrieves the currently editing path id" @@ -31,7 +31,8 @@ [state & ks] (let [path-loc (get-path-location state) shape (-> (get-in state path-loc) - (upsp/convert-to-path))] + ;; Empty map because we know the current shape will not have children + (upsp/convert-to-path {}))] (if (empty? ks) shape diff --git a/frontend/src/app/main/data/workspace/path/streams.cljs b/frontend/src/app/main/data/workspace/path/streams.cljs index b67607ae0..8a8f8a59b 100644 --- a/frontend/src/app/main/data/workspace/path/streams.cljs +++ b/frontend/src/app/main/data/workspace/path/streams.cljs @@ -7,12 +7,12 @@ (ns app.main.data.workspace.path.streams (:require [app.common.geom.point :as gpt] + [app.common.geom.shapes.path :as upg] [app.common.math :as mth] [app.main.data.workspace.path.state :as state] [app.main.snap :as snap] [app.main.store :as st] [app.main.streams :as ms] - [app.util.path.geom :as upg] [beicon.core :as rx] [okulary.core :as l] [potok.core :as ptk])) diff --git a/frontend/src/app/main/data/workspace/path/tools.cljs b/frontend/src/app/main/data/workspace/path/tools.cljs index 18f262743..fce88f9db 100644 --- a/frontend/src/app/main/data/workspace/path/tools.cljs +++ b/frontend/src/app/main/data/workspace/path/tools.cljs @@ -6,13 +6,13 @@ (ns app.main.data.workspace.path.tools (:require + [app.common.path.shapes-to-path :as upsp] + [app.common.path.subpaths :as ups] [app.main.data.workspace.changes :as dch] [app.main.data.workspace.common :as dwc] [app.main.data.workspace.path.changes :as changes] [app.main.data.workspace.path.state :as st] [app.main.data.workspace.state-helpers :as wsh] - [app.util.path.shapes-to-path :as upsp] - [app.util.path.subpaths :as ups] [app.util.path.tools :as upt] [beicon.core :as rx] [potok.core :as ptk])) diff --git a/frontend/src/app/main/refs.cljs b/frontend/src/app/main/refs.cljs index 9224abce2..15e90c422 100644 --- a/frontend/src/app/main/refs.cljs +++ b/frontend/src/app/main/refs.cljs @@ -243,16 +243,34 @@ ([ids {:keys [with-modifiers?] :or { with-modifiers? false }}] - (l/derived (fn [state] - (let [objects (wsh/lookup-page-objects state) - modifiers (:workspace-modifiers state) - objects (cond-> objects - with-modifiers? - (gsh/merge-modifiers modifiers)) - xform (comp (map #(get objects %)) - (remove nil?))] - (into [] xform ids))) - st/state =))) + (let [selector + (fn [state] + (let [objects (wsh/lookup-page-objects state) + modifiers (:workspace-modifiers state) + objects (cond-> objects + with-modifiers? + (gsh/merge-modifiers modifiers)) + xform (comp (map #(get objects %)) + (remove nil?))] + (into [] xform ids)))] + (l/derived selector st/state =)))) + +(defn select-children [id] + (let [selector + (fn [state] + (let [objects (wsh/lookup-page-objects state) + children (cp/select-children id objects) + modifiers (-> (:workspace-modifiers state)) + + {selected :selected disp-modifiers :modifiers} + (-> (:workspace-local state) + (select-keys [:modifiers :selected])) + + modifiers (merge modifiers + (into #{} (map #(vector % disp-modifiers)) selected))] + + (gsh/merge-modifiers children modifiers)))] + (l/derived selector st/state =))) (def selected-data (l/derived #(let [selected (wsh/lookup-selected %) diff --git a/frontend/src/app/main/ui/shapes/bool.cljs b/frontend/src/app/main/ui/shapes/bool.cljs index dd77780e9..7fdbfbcc3 100644 --- a/frontend/src/app/main/ui/shapes/bool.cljs +++ b/frontend/src/app/main/ui/shapes/bool.cljs @@ -6,11 +6,10 @@ (ns app.main.ui.shapes.bool (:require - [app.common.geom.shapes :as gsh] + [app.common.path.bool :as pb] + [app.common.path.shapes-to-path :as stp] [app.main.ui.hooks :refer [use-equal-memo]] [app.util.object :as obj] - [app.util.path.bool :as pb] - [app.util.path.shapes-to-path :as stp] [rumext.alpha :as mf])) (defn bool-shape @@ -20,32 +19,25 @@ [props] (let [frame (obj/get props "frame") shape (obj/get props "shape") - childs (obj/get props "childs")] + childs (obj/get props "childs") - (when (> (count childs) 1) - (let [shape-1 (stp/convert-to-path (nth childs 0)) - shape-2 (stp/convert-to-path (nth childs 1)) + childs (use-equal-memo childs) - content-1 (use-equal-memo (-> shape-1 gsh/transform-shape :content)) - content-2 (use-equal-memo (-> shape-2 gsh/transform-shape :content)) + bool-content + (mf/use-memo + (mf/deps childs) + (fn [] + (->> shape + :shapes + (map #(get childs %)) + (map #(stp/convert-to-path % childs)) + (mapv :content) + (pb/content-bool (:bool-type shape)))))] - content - (mf/use-memo - (mf/deps content-1 content-2) - #(pb/content-bool (:bool-type shape) content-1 content-2))] - - [:* - [:& shape-wrapper {:shape (-> shape - (assoc :type :path) - (assoc :content content)) - :frame frame}] - - #_[:g - (for [point (app.util.path.geom/content->points content)] - [:circle {:cx (:x point) - :cy (:y point) - :r 1 - :style {:fill "blue"}}])]]))))) + [:& shape-wrapper {:shape (-> shape + (assoc :type :path) + (assoc :content bool-content)) + :frame frame}]))) diff --git a/frontend/src/app/main/ui/workspace/context_menu.cljs b/frontend/src/app/main/ui/workspace/context_menu.cljs index 8e2b43b78..b65f9d062 100644 --- a/frontend/src/app/main/ui/workspace/context_menu.cljs +++ b/frontend/src/app/main/ui/workspace/context_menu.cljs @@ -10,6 +10,7 @@ [app.main.data.modal :as modal] [app.main.data.workspace :as dw] [app.main.data.workspace.libraries :as dwl] + [app.main.data.workspace.path.shapes-to-path :as dwpe] [app.main.data.workspace.shortcuts :as sc] [app.main.data.workspace.undo :as dwu] [app.main.refs :as refs] @@ -147,6 +148,7 @@ do-boolean-difference (st/emitf (dw/create-bool :difference)) do-boolean-intersection (st/emitf (dw/create-bool :intersection)) do-boolean-exclude (st/emitf (dw/create-bool :exclude)) + do-transform-to-path (st/emitf (dwpe/convert-selected-to-path)) ] [:* [:& menu-entry {:title (tr "workspace.shape.menu.copy") @@ -214,6 +216,9 @@ :shortcut (sc/get-tooltip :start-editing) :on-click do-start-editing}]) + [:& menu-entry {:title "Transform to path" + :on-click do-transform-to-path}] + [:& menu-entry {:title (tr "workspace.shape.menu.path")} [:& menu-entry {:title (tr "workspace.shape.menu.union") :shortcut (sc/get-tooltip :boolean-union) @@ -230,8 +235,7 @@ [:& menu-separator] ;; TODO - [:& menu-entry {:title "Flatten"}] - [:& menu-entry {:title "Transform to path"}]] + [:& menu-entry {:title "Flatten"}]] (if (:hidden shape) [:& menu-entry {:title (tr "workspace.shape.menu.show") diff --git a/frontend/src/app/main/ui/workspace/shapes/bool.cljs b/frontend/src/app/main/ui/workspace/shapes/bool.cljs index e53fc3b8f..2cc8f6e00 100644 --- a/frontend/src/app/main/ui/workspace/shapes/bool.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/bool.cljs @@ -33,17 +33,11 @@ (let [shape (unchecked-get props "shape") frame (unchecked-get props "frame") - childs-ref (mf/use-memo (mf/deps shape) #(refs/objects-by-id (:shapes shape) {:with-modifiers? true})) - {:keys [selected modifiers]} (mf/deref refs/local-displacement) + childs-ref (mf/use-memo + (mf/deps (:id shape)) + #(refs/select-children (:id shape))) - add-modifiers - (fn [{:keys [id] :as shape}] - (cond-> shape - (contains? selected id) - (update :modifiers merge modifiers))) - - childs (->> (mf/deref childs-ref) - (mapv add-modifiers))] + childs (mf/deref childs-ref)] [:> shape-container {:shape shape} [:& shape-component diff --git a/frontend/src/app/main/ui/workspace/shapes/path.cljs b/frontend/src/app/main/ui/workspace/shapes/path.cljs index bae6a5d99..a245d5bb2 100644 --- a/frontend/src/app/main/ui/workspace/shapes/path.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/path.cljs @@ -6,11 +6,11 @@ (ns app.main.ui.workspace.shapes.path (:require + [app.common.path.commands :as upc] [app.main.refs :as refs] [app.main.ui.shapes.path :as path] [app.main.ui.shapes.shape :refer [shape-container]] [app.main.ui.workspace.shapes.path.common :as pc] - [app.util.path.commands :as upc] [rumext.alpha :as mf])) (mf/defc path-wrapper diff --git a/frontend/src/app/main/ui/workspace/shapes/path/editor.cljs b/frontend/src/app/main/ui/workspace/shapes/path/editor.cljs index aa683f708..ed653f588 100644 --- a/frontend/src/app/main/ui/workspace/shapes/path/editor.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/path/editor.cljs @@ -8,7 +8,9 @@ (:require [app.common.data :as d] [app.common.geom.point :as gpt] - [app.common.geom.shapes.path :as gshp] + [app.common.geom.shapes.path :as gsp] + [app.common.path.commands :as upc] + [app.common.path.shapes-to-path :as ups] [app.main.data.workspace.path :as drp] [app.main.snap :as snap] [app.main.store :as st] @@ -18,10 +20,7 @@ [app.main.ui.workspace.shapes.path.common :as pc] [app.util.dom :as dom] [app.util.keyboard :as kbd] - [app.util.path.commands :as upc] [app.util.path.format :as upf] - [app.util.path.geom :as upg] - [app.util.path.shapes-to-path :as ups] [clojure.set :refer [map-invert]] [goog.events :as events] [rumext.alpha :as mf]) @@ -217,16 +216,16 @@ shape (cond-> shape (not= :path (:type shape)) - ups/convert-to-path + (ups/convert-to-path {}) :always hooks/use-equal-memo) base-content (:content shape) - base-points (mf/use-memo (mf/deps base-content) #(->> base-content upg/content->points)) + base-points (mf/use-memo (mf/deps base-content) #(->> base-content gsp/content->points)) content (upc/apply-content-modifiers base-content content-modifiers) - content-points (mf/use-memo (mf/deps content) #(->> content upg/content->points)) + content-points (mf/use-memo (mf/deps content) #(->> content gsp/content->points)) point->base (->> (map hash-map content-points base-points) (reduce merge)) base->point (map-invert point->base) @@ -269,7 +268,7 @@ ms/mouse-position (mf/deps shape zoom) (fn [position] - (when-let [point (gshp/path-closest-point shape position)] + (when-let [point (gsp/path-closest-point shape position)] (reset! hover-point (when (< (gpt/distance position point) (/ 10 zoom)) point))))) [:g.path-editor {:ref editor-ref} diff --git a/frontend/src/app/util/path/format.cljs b/frontend/src/app/util/path/format.cljs index 312746f90..2cdf6f900 100644 --- a/frontend/src/app/util/path/format.cljs +++ b/frontend/src/app/util/path/format.cljs @@ -6,8 +6,8 @@ (ns app.util.path.format (:require - [app.util.path.commands :as upc] - [app.util.path.subpaths :refer [pt=]] + [app.common.path.commands :as upc] + [app.common.path.subpaths :refer [pt=]] [cuerdas.core :as str])) (defn command->param-list [command] diff --git a/frontend/src/app/util/path/geom.cljs b/frontend/src/app/util/path/geom.cljs deleted file mode 100644 index afb8787a1..000000000 --- a/frontend/src/app/util/path/geom.cljs +++ /dev/null @@ -1,97 +0,0 @@ -;; 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) UXBOX Labs SL - -(ns app.util.path.geom - (:require - [app.common.data :as d] - [app.common.geom.point :as gpt] - [app.common.geom.shapes.path :as gshp] - [app.util.path.commands :as upc])) - -(defn calculate-opposite-handler - "Given a point and its handler, gives the symetric handler" - [point handler] - (let [handler-vector (gpt/to-vec point handler)] - (gpt/add point (gpt/negate handler-vector)))) - -(defn split-line-to - "Given a point and a line-to command will create a two new line-to commands - that will split the original line into two given a value between 0-1" - [from-p cmd t-val] - (let [to-p (upc/command->point cmd) - sp (gpt/lerp from-p to-p t-val)] - [(upc/make-line-to sp) cmd])) - -(defn split-curve-to - "Given the point and a curve-to command will split the curve into two new - curve-to commands given a value between 0-1" - [from-p cmd t-val] - (let [params (:params cmd) - end (gpt/point (:x params) (:y params)) - h1 (gpt/point (:c1x params) (:c1y params)) - h2 (gpt/point (:c2x params) (:c2y params)) - [[_ to1 h11 h21] - [_ to2 h12 h22]] (gshp/curve-split from-p end h1 h2 t-val)] - [(upc/make-curve-to to1 h11 h21) - (upc/make-curve-to to2 h12 h22)])) - -(defn split-line-to-ranges - "Splits a line into several lines given the points in `values` - for example (split-line-to-ranges p c [0 0.25 0.5 0.75 1] will split - the line into 4 lines" - [from-p cmd values] - (let [to-p (upc/command->point cmd)] - (->> (conj values 1) - (mapv (fn [val] - (-> (gpt/lerp from-p to-p val) - #_(gpt/round 2) - (upc/make-line-to))))))) - -(defn split-curve-to-ranges - "Splits a curve into several curves given the points in `values` - for example (split-curve-to-ranges p c [0 0.25 0.5 0.75 1] will split - the curve into 4 curves that draw the same curve" - [from-p cmd values] - (if (empty? values) - [cmd] - (let [to-p (upc/command->point cmd) - params (:params cmd) - h1 (gpt/point (:c1x params) (:c1y params)) - h2 (gpt/point (:c2x params) (:c2y params)) - - values-set (->> (conj values 1) (into (sorted-set)))] - (->> (d/with-prev values-set) - (mapv - (fn [[t1 t0]] - (let [t0 (if (nil? t0) 0 t0) - [_ to-p h1' h2'] (gshp/subcurve-range from-p to-p h1 h2 t0 t1)] - (upc/make-curve-to (-> to-p #_(gpt/round 2)) h1' h2')))))))) - -(defn opposite-handler - "Calculates the coordinates of the opposite handler" - [point handler] - (let [phv (gpt/to-vec point handler)] - (gpt/add point (gpt/negate phv)))) - -(defn opposite-handler-keep-distance - "Calculates the coordinates of the opposite handler but keeping the old distance" - [point handler old-opposite] - (let [old-distance (gpt/distance point old-opposite) - phv (gpt/to-vec point handler) - phv2 (gpt/multiply - (gpt/unit (gpt/negate phv)) - (gpt/point old-distance))] - (gpt/add point phv2))) - -(defn content->points - "Returns the points in the given content" - [content] - (->> content - (map #(when (-> % :params :x) - (gpt/point (-> % :params :x) (-> % :params :y)))) - (remove nil?) - (into []))) - diff --git a/frontend/src/app/util/path/parser.cljs b/frontend/src/app/util/path/parser.cljs index 7b68caf64..9e6023600 100644 --- a/frontend/src/app/util/path/parser.cljs +++ b/frontend/src/app/util/path/parser.cljs @@ -8,9 +8,9 @@ (:require [app.common.data :as d] [app.common.geom.point :as gpt] + [app.common.geom.shapes.path :as upg] + [app.common.path.commands :as upc] [app.util.path.arc-to-curve :refer [a2c]] - [app.util.path.commands :as upc] - [app.util.path.geom :as upg] [app.util.svg :as usvg] [cuerdas.core :as str])) diff --git a/frontend/src/app/util/path/tools.cljs b/frontend/src/app/util/path/tools.cljs index 3a05c2e1d..9f97ab666 100644 --- a/frontend/src/app/util/path/tools.cljs +++ b/frontend/src/app/util/path/tools.cljs @@ -8,9 +8,9 @@ (: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.util.path.commands :as upc] - [app.util.path.geom :as upg] + [app.common.path.commands :as upc] [clojure.set :as set])) (defn remove-line-curves From c56f024a86c2eb6b0ad054dad3e8ae6657dacd50 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Fri, 17 Sep 2021 14:38:44 +0200 Subject: [PATCH 07/16] :sparkles: Handling groups inside bool shapes --- .../src/app/common/path/shapes_to_path.cljc | 62 +++++++++++++------ .../main/partials/sidebar-align-options.scss | 8 +++ frontend/src/app/main/data/workspace.cljs | 9 ++- .../src/app/main/data/workspace/booleans.cljs | 53 +++++++++++++++- frontend/src/app/main/refs.cljs | 6 +- frontend/src/app/main/ui/shapes/bool.cljs | 14 +++-- .../sidebar/options/menus/booleans.cljs | 54 ++++++++++------ 7 files changed, 155 insertions(+), 51 deletions(-) diff --git a/common/src/app/common/path/shapes_to_path.cljc b/common/src/app/common/path/shapes_to_path.cljc index ec8294fd0..8bd1f24f6 100644 --- a/common/src/app/common/path/shapes_to_path.cljc +++ b/common/src/app/common/path/shapes_to_path.cljc @@ -11,6 +11,7 @@ [app.common.geom.point :as gpt] [app.common.geom.shapes.common :as gsc] [app.common.geom.shapes.path :as gsp] + [app.common.path.bool :as pb] [app.common.path.commands :as pc])) (def ^:const bezier-circle-c 0.551915024494) @@ -24,26 +25,31 @@ #{:rect :circle :image - :group}) + :group + :bool}) + +(def ^:const style-group-properties + [:shadow + :blur]) (def ^:const style-properties - [:fill-color - :fill-opacity - :fill-color-gradient - :fill-color-ref-file - :fill-color-ref-id - :fill-image - :stroke-color - :stroke-color-ref-file - :stroke-color-ref-id - :stroke-opacity - :stroke-style - :stroke-width - :stroke-alignment - :stroke-cap-start - :stroke-cap-end - :shadow - :blur]) + (d/concat + style-group-properties + [:fill-color + :fill-opacity + :fill-color-gradient + :fill-color-ref-file + :fill-color-ref-id + :fill-image + :stroke-color + :stroke-color-ref-file + :stroke-color-ref-id + :stroke-opacity + :stroke-style + :stroke-width + :stroke-alignment + :stroke-cap-start + :stroke-cap-end])) (defn make-corner-arc "Creates a curvle corner for border radius" @@ -142,7 +148,6 @@ (defn group-to-path [group objects] - (let [xform (comp (map #(get objects %)) (map #(-> (convert-to-path % objects)))) @@ -157,6 +162,22 @@ (merge head-data) (d/without-keys dissoc-attrs)))) +(defn bool-to-path + [shape objects] + + (let [children (->> (:shapes shape) + (map #(get objects %)) + (map #(convert-to-path % objects))) + head (first children) + head-data (select-keys head style-properties) + content (pb/content-bool (:bool-type shape) (mapv :content children))] + + (-> shape + (assoc :type :path) + (assoc :content content) + (merge head-data) + (d/without-keys dissoc-attrs)))) + (defn convert-to-path "Transforms the given shape to a path" [{:keys [type x y width height r1 r2 r3 r4 rx metadata] :as shape} objects] @@ -165,6 +186,9 @@ (= (:type shape) :group) (group-to-path shape objects) + (= (:type shape) :bool) + (bool-to-path shape objects) + (contains? allowed-transform-types type) (let [new-content (case type diff --git a/frontend/resources/styles/main/partials/sidebar-align-options.scss b/frontend/resources/styles/main/partials/sidebar-align-options.scss index 634c40861..2d3565c4a 100644 --- a/frontend/resources/styles/main/partials/sidebar-align-options.scss +++ b/frontend/resources/styles/main/partials/sidebar-align-options.scss @@ -46,5 +46,13 @@ fill: $color-gray-40; } } + + &.selected svg { + fill: $color-primary; + } + + &.selected:hover svg { + fill: $color-white; + } } } diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index cb055c688..8f4e30599 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -1098,13 +1098,9 @@ :text (rx/of (dwc/start-edition-mode id)) - :group + (:group :bool) (rx/of (dwc/select-shapes (into (d/ordered-set) [(last shapes)]))) - :bool - ;; TODO - (js/alert "TODO") - :svg-raw nil @@ -1995,3 +1991,6 @@ ;; Boolean (d/export dwb/create-bool) +(d/export dwb/group-to-bool) +(d/export dwb/bool-to-group) +(d/export dwb/change-bool-type) diff --git a/frontend/src/app/main/data/workspace/booleans.cljs b/frontend/src/app/main/data/workspace/booleans.cljs index 301fa9c2f..f1aa65474 100644 --- a/frontend/src/app/main/data/workspace/booleans.cljs +++ b/frontend/src/app/main/data/workspace/booleans.cljs @@ -45,11 +45,36 @@ (merge head-data) (gsh/update-bool-selrect shapes objects)))) +(defn group->bool + [group bool-type objects] + + (let [shapes (->> (:shapes group) + (map #(get objects %)) + (mapv #(stp/convert-to-path % objects))) + head (first shapes) + head-data (select-keys head stp/style-properties)] + + (-> group + (assoc :type :bool) + (assoc :bool-type bool-type) + (merge head-data) + (gsh/update-bool-selrect shapes objects)))) + +(defn bool->group + [shape objects] + + (let [children (->> (:shapes shape) + (mapv #(get objects %)))] + (-> shape + (assoc :type :group) + (dissoc :bool-type) + (d/without-keys stp/style-group-properties) + (gsh/update-group-selrect children)))) + (defn create-bool [bool-type] (ptk/reify ::create-bool-union ptk/WatchEvent - (watch [it state _] (let [page-id (:current-page-id state) objects (wsh/lookup-page-objects state) @@ -66,3 +91,29 @@ (cb/change-parent shape-id shapes))] (rx/of (dch/commit-changes changes) (dwc/select-shapes (d/ordered-set shape-id))))))))) + +(defn group-to-bool + [shape-id bool-type] + (ptk/reify ::group-to-bool + ptk/WatchEvent + (watch [_ state _] + (let [objects (wsh/lookup-page-objects state)] + (rx/of (dch/update-shapes [shape-id] #(group->bool % bool-type objects))))))) + +(defn bool-to-group + [shape-id] + (ptk/reify ::bool-to-group + ptk/WatchEvent + (watch [_ state _] + (let [objects (wsh/lookup-page-objects state)] + (rx/of (dch/update-shapes [shape-id] #(bool->group % objects))))))) + + +(defn change-bool-type + [shape-id bool-type] + (ptk/reify ::change-bool-type + ptk/WatchEvent + (watch [_ _ _] + (rx/of (dch/update-shapes + [shape-id] + #(assoc % :bool-type bool-type)))))) diff --git a/frontend/src/app/main/refs.cljs b/frontend/src/app/main/refs.cljs index 15e90c422..cc0197bfa 100644 --- a/frontend/src/app/main/refs.cljs +++ b/frontend/src/app/main/refs.cljs @@ -266,8 +266,10 @@ (-> (:workspace-local state) (select-keys [:modifiers :selected])) - modifiers (merge modifiers - (into #{} (map #(vector % disp-modifiers)) selected))] + modifiers + (d/deep-merge + modifiers + (into {} (map #(vector % {:modifiers disp-modifiers})) selected))] (gsh/merge-modifiers children modifiers)))] (l/derived selector st/state =))) diff --git a/frontend/src/app/main/ui/shapes/bool.cljs b/frontend/src/app/main/ui/shapes/bool.cljs index 7fdbfbcc3..c4c0fd994 100644 --- a/frontend/src/app/main/ui/shapes/bool.cljs +++ b/frontend/src/app/main/ui/shapes/bool.cljs @@ -6,6 +6,8 @@ (ns app.main.ui.shapes.bool (:require + [app.common.data :as d] + [app.common.geom.shapes :as gsh] [app.common.path.bool :as pb] [app.common.path.shapes-to-path :as stp] [app.main.ui.hooks :refer [use-equal-memo]] @@ -27,12 +29,12 @@ (mf/use-memo (mf/deps childs) (fn [] - (->> shape - :shapes - (map #(get childs %)) - (map #(stp/convert-to-path % childs)) - (mapv :content) - (pb/content-bool (:bool-type shape)))))] + (let [childs (d/mapm #(gsh/transform-shape %2) childs)] + (->> (:shapes shape) + (map #(get childs %)) + (map #(stp/convert-to-path % childs)) + (mapv :content) + (pb/content-bool (:bool-type shape))))))] [:& shape-wrapper {:shape (-> shape (assoc :type :path) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/booleans.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/booleans.cljs index 0ad6b3ddd..4f1137a60 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/booleans.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/booleans.cljs @@ -6,49 +6,67 @@ (ns app.main.ui.workspace.sidebar.options.menus.booleans (:require - [app.main.data.workspace :as dw] [app.main.refs :as refs] [app.main.store :as st] [app.main.ui.icons :as i] + [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] - [rumext.alpha :as mf] - )) + [rumext.alpha :as mf])) (mf/defc booleans-options [] - (let [selected (mf/deref refs/selected-shapes) - disabled (and (some? selected) - (<= (count selected) 1)) + (let [selected (mf/deref refs/selected-objects) + disabled (or (empty? selected) + (and (<= (count selected) 1) + (not (contains? #{:group :bool} (:type (first selected)))))) - do-boolean-union (st/emitf (dw/create-bool :union)) - do-boolean-difference (st/emitf (dw/create-bool :difference)) - do-boolean-intersection (st/emitf (dw/create-bool :intersection)) - do-boolean-exclude (st/emitf (dw/create-bool :exclude))] + head (first selected) + is-group? (and (some? head) (= :group (:type head))) + is-bool? (and (some? head) (= :bool (:type head))) + head-bool-type (and (some? head) (:bool-type head)) + + set-bool + (fn [bool-type] + #(cond + (> (count selected) 1) + (st/emit! (dw/create-bool bool-type)) + + (and (= (count selected) 1) is-group?) + (st/emit! (dw/group-to-bool (:id head) bool-type)) + + (and (= (count selected) 1) is-bool?) + (if (= head-bool-type bool-type) + (st/emit! (dw/bool-to-group (:id head))) + (st/emit! (dw/change-bool-type (:id head) bool-type)))))] [:div.align-options [:div.align-group [:div.align-button.tooltip.tooltip-bottom {:alt (tr "workspace.shape.menu.union") - :class (when disabled "disabled") - :on-click do-boolean-union} + :class (dom/classnames :disabled disabled + :selected (= head-bool-type :union)) + :on-click (set-bool :union)} i/boolean-union] [:div.align-button.tooltip.tooltip-bottom {:alt (tr "workspace.shape.menu.difference") - :class (when disabled "disabled") - :on-click do-boolean-difference} + :class (dom/classnames :disabled disabled + :selected (= head-bool-type :difference)) + :on-click (set-bool :difference)} i/boolean-difference] [:div.align-button.tooltip.tooltip-bottom {:alt (tr "workspace.shape.menu.intersection") - :class (when disabled "disabled") - :on-click do-boolean-intersection} + :class (dom/classnames :disabled disabled + :selected (= head-bool-type :intersection)) + :on-click (set-bool :intersection)} i/boolean-intersection] [:div.align-button.tooltip.tooltip-bottom {:alt (tr "workspace.shape.menu.exclude") - :class (when disabled "disabled") - :on-click do-boolean-exclude} + :class (dom/classnames :disabled disabled + :selected (= head-bool-type :exclude)) + :on-click (set-bool :exclude)} i/boolean-exclude]]])) From 56e2db22eb06db608804dc9653c40050878fd1ad Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Mon, 20 Sep 2021 11:47:02 +0200 Subject: [PATCH 08/16] :sparkles: Clip boolean selrects --- common/src/app/common/pages.cljc | 2 +- common/src/app/common/pages/indices.cljc | 26 +++++---- common/src/app/common/path/bool.cljc | 40 ++++++++------ .../src/app/common/path/shapes_to_path.cljc | 54 ++++++++++--------- .../src/app/main/data/workspace/booleans.cljs | 14 +++-- frontend/src/app/main/ui/shapes/bool.cljs | 5 +- .../app/main/ui/workspace/viewport/hooks.cljs | 22 +++++--- frontend/src/app/worker/selection.cljs | 48 +++++++++-------- 8 files changed, 120 insertions(+), 91 deletions(-) diff --git a/common/src/app/common/pages.cljc b/common/src/app/common/pages.cljc index 640a6858e..accb623ad 100644 --- a/common/src/app/common/pages.cljc +++ b/common/src/app/common/pages.cljc @@ -73,7 +73,7 @@ (d/export indices/update-z-index) (d/export indices/generate-child-all-parents-index) (d/export indices/generate-child-parent-index) -(d/export indices/create-mask-index) +(d/export indices/create-clip-index) ;; Process changes (d/export changes/process-changes) diff --git a/common/src/app/common/pages/indices.cljc b/common/src/app/common/pages/indices.cljc index c1681fef1..5a5f40595 100644 --- a/common/src/app/common/pages/indices.cljc +++ b/common/src/app/common/pages/indices.cljc @@ -95,16 +95,24 @@ (map #(vector (:id %) (shape->parents %))) (into {}))))) -(defn create-mask-index +(defn create-clip-index "Retrieves the mask information for an object" [objects parents-index] - (let [retrieve-masks + (let [retrieve-clips (fn [_ parents] - ;; TODO: use transducers? - (->> parents - (map #(get objects %)) - (filter #(:masked-group? %)) - ;; Retrieve the masking element - (mapv #(get objects (->> % :shapes first)))))] + (let [lookup-object (fn [id] (get objects id)) + get-clip-parents + (fn [shape] + (cond-> [] + (:masked-group? shape) + (conj (get objects (->> shape :shapes first))) + + (= :bool (:type shape)) + (conj shape)))] + + (into [] + (comp (map lookup-object) + (mapcat get-clip-parents)) + parents)))] (->> parents-index - (d/mapm retrieve-masks)))) + (d/mapm retrieve-clips)))) diff --git a/common/src/app/common/path/bool.cljc b/common/src/app/common/path/bool.cljc index 3d61ff98c..d12763c5d 100644 --- a/common/src/app/common/path/bool.cljc +++ b/common/src/app/common/path/bool.cljc @@ -12,6 +12,23 @@ [app.common.path.commands :as upc] [app.common.path.subpaths :as ups])) +(defn- reverse-command + "Reverses a single command" + [command] + + (let [{old-x :x old-y :y} (:params command) + {:keys [x y]} (:prev command) + {:keys [c1x c1y c2x c2y]} (:params command)] + + (-> command + (assoc :prev (gpt/point old-x old-y)) + (update :params assoc :x x :y y) + + (cond-> (= :curve-to (:command command)) + (update :params assoc + :c1x c2x :c1y c2y + :c2x c1x :c2y c1y))))) + (defn- split-command [cmd values] (case (:command cmd) @@ -142,7 +159,12 @@ (d/concat [] (->> content-a-split (filter #(not (contains-segment? % content-b)))) - (->> content-b-split (filter #(contains-segment? % content-a))))) + + ;; Reverse second content so we can have holes inside other shapes + (->> content-b-split + (reverse) + (mapv reverse-command) + (filter #(contains-segment? % content-a))))) (defn create-intersection [content-a content-a-split content-b content-b-split] ;; Pick all segments in content-a that are inside content-b @@ -152,22 +174,6 @@ (->> content-a-split (filter #(contains-segment? % content-b))) (->> content-b-split (filter #(contains-segment? % content-a))))) -(defn reverse-command - "Reverses a single command" - [command] - - (let [{old-x :x old-y :y} (:params command) - {:keys [x y]} (:prev command) - {:keys [c1x c1y c2x c2y]} (:params command)] - - (-> command - (assoc :prev (gpt/point old-x old-y)) - (update :params assoc :x x :y y) - - (cond-> (= :curve-to (:command command)) - (update :params assoc - :c1x c2x :c1y c2y - :c2x c1x :c2y c1y))))) (defn create-exclusion [content-a content-b] ;; Pick all segments but reverse content-b (so it makes an exclusion) diff --git a/common/src/app/common/path/shapes_to_path.cljc b/common/src/app/common/path/shapes_to_path.cljc index 8bd1f24f6..83a9358c8 100644 --- a/common/src/app/common/path/shapes_to_path.cljc +++ b/common/src/app/common/path/shapes_to_path.cljc @@ -180,33 +180,35 @@ (defn convert-to-path "Transforms the given shape to a path" - [{:keys [type x y width height r1 r2 r3 r4 rx metadata] :as shape} objects] - (assert (map? objects)) - (cond - (= (:type shape) :group) - (group-to-path shape objects) + ([shape] + (convert-to-path shape {})) + ([{:keys [type x y width height r1 r2 r3 r4 rx metadata] :as shape} objects] + (assert (map? objects)) + (cond + (= (:type shape) :group) + (group-to-path shape objects) - (= (:type shape) :bool) - (bool-to-path shape objects) + (= (:type shape) :bool) + (bool-to-path shape objects) - (contains? allowed-transform-types type) - (let [new-content - (case type - :circle (circle->path x y width height) - #_:else (rect->path x y width height r1 r2 r3 r4 rx)) + (contains? allowed-transform-types type) + (let [new-content + (case type + :circle (circle->path x y width height) + #_:else (rect->path x y width height r1 r2 r3 r4 rx)) - ;; Apply the transforms that had the shape - transform (:transform shape) - new-content (cond-> new-content - (some? transform) - (gsp/transform-content (gmt/transform-in (gsc/center-shape shape) transform)))] + ;; Apply the transforms that had the shape + transform (:transform shape) + new-content (cond-> new-content + (some? transform) + (gsp/transform-content (gmt/transform-in (gsc/center-shape shape) transform)))] - (-> shape - (assoc :type :path) - (assoc :content new-content) - (cond-> (= :image type) - (assoc :fill-image metadata)) - (d/without-keys dissoc-attrs))) - :else - ;; Do nothing if the shape is not of a correct type - shape)) + (-> shape + (assoc :type :path) + (assoc :content new-content) + (cond-> (= :image type) + (assoc :fill-image metadata)) + (d/without-keys dissoc-attrs))) + :else + ;; Do nothing if the shape is not of a correct type + shape))) diff --git a/frontend/src/app/main/data/workspace/booleans.cljs b/frontend/src/app/main/data/workspace/booleans.cljs index f1aa65474..4482d1da6 100644 --- a/frontend/src/app/main/data/workspace/booleans.cljs +++ b/frontend/src/app/main/data/workspace/booleans.cljs @@ -98,7 +98,9 @@ ptk/WatchEvent (watch [_ state _] (let [objects (wsh/lookup-page-objects state)] - (rx/of (dch/update-shapes [shape-id] #(group->bool % bool-type objects))))))) + (let [change-to-bool + (fn [shape] (group->bool shape bool-type objects))] + (rx/of (dch/update-shapes [shape-id] change-to-bool {:reg-objects? true}))))))) (defn bool-to-group [shape-id] @@ -106,7 +108,9 @@ ptk/WatchEvent (watch [_ state _] (let [objects (wsh/lookup-page-objects state)] - (rx/of (dch/update-shapes [shape-id] #(bool->group % objects))))))) + (let [change-to-group + (fn [shape] (bool->group shape objects))] + (rx/of (dch/update-shapes [shape-id] change-to-group {:reg-objects? true}))))))) (defn change-bool-type @@ -114,6 +118,6 @@ (ptk/reify ::change-bool-type ptk/WatchEvent (watch [_ _ _] - (rx/of (dch/update-shapes - [shape-id] - #(assoc % :bool-type bool-type)))))) + (let [change-type + (fn [shape] (assoc shape :bool-type bool-type))] + (rx/of (dch/update-shapes [shape-id] change-type {:reg-objects? true})))))) diff --git a/frontend/src/app/main/ui/shapes/bool.cljs b/frontend/src/app/main/ui/shapes/bool.cljs index c4c0fd994..8a3db6d19 100644 --- a/frontend/src/app/main/ui/shapes/bool.cljs +++ b/frontend/src/app/main/ui/shapes/bool.cljs @@ -27,7 +27,7 @@ bool-content (mf/use-memo - (mf/deps childs) + (mf/deps shape childs) (fn [] (let [childs (d/mapm #(gsh/transform-shape %2) childs)] (->> (:shapes shape) @@ -40,6 +40,3 @@ (assoc :type :path) (assoc :content bool-content)) :frame frame}]))) - - - diff --git a/frontend/src/app/main/ui/workspace/viewport/hooks.cljs b/frontend/src/app/main/ui/workspace/viewport/hooks.cljs index 754523c73..aab5f7f08 100644 --- a/frontend/src/app/main/ui/workspace/viewport/hooks.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/hooks.cljs @@ -92,6 +92,7 @@ (defn setup-hover-shapes [page-id move-stream objects transform selected ctrl? hover hover-ids hover-disabled? zoom] (let [;; We use ref so we don't recreate the stream on a change zoom-ref (mf/use-ref zoom) + ctrl-ref (mf/use-ref @ctrl?) transform-ref (mf/use-ref nil) selected-ref (mf/use-ref selected) hover-disabled-ref (mf/use-ref hover-disabled?) @@ -101,6 +102,7 @@ (mf/deps page-id) (fn [point] (let [zoom (mf/ref-val zoom-ref) + ctrl? (mf/ref-val ctrl-ref) rect (gsh/center->rect point (/ 5 zoom) (/ 5 zoom))] (if (mf/ref-val hover-disabled-ref) (rx/of nil) @@ -109,6 +111,7 @@ :page-id page-id :rect rect :include-frames? true + :clip-children? (not ctrl?) :reverse? true}))))) ;; we want the topmost shape to be selected first over-shapes-stream @@ -120,7 +123,6 @@ (rx/switch-map query-point))))] ;; Refresh the refs on a value change - (mf/use-effect (mf/deps transform) #(mf/set-ref-val! transform-ref transform)) @@ -129,6 +131,10 @@ (mf/deps zoom) #(mf/set-ref-val! zoom-ref zoom)) + (mf/use-effect + (mf/deps @ctrl?) + #(mf/set-ref-val! ctrl-ref @ctrl?)) + (mf/use-effect (mf/deps selected) #(mf/set-ref-val! selected-ref selected)) @@ -143,11 +149,15 @@ (fn [ids] (let [selected (mf/ref-val selected-ref) remove-id? (into #{} (mapcat #(cp/get-parents % objects)) selected) - remove-id? (if @ctrl? - (d/concat remove-id? - (->> ids - (filterv #(= :group (get-in objects [% :type]))))) - remove-id?) + + is-group? + (fn [id] + (contains? #{:group :bool} (get-in objects [id :type]))) + + remove-id? + (if @ctrl? + (d/concat remove-id? (filterv is-group? ids)) + remove-id?) ids (->> ids (filterv (comp not remove-id?)))] (reset! hover (get objects (first ids))) (reset! hover-ids ids)))))) diff --git a/frontend/src/app/worker/selection.cljs b/frontend/src/app/worker/selection.cljs index d93fcfaf4..47cb834b1 100644 --- a/frontend/src/app/worker/selection.cljs +++ b/frontend/src/app/worker/selection.cljs @@ -18,13 +18,13 @@ (defonce state (l/atom {})) (defn index-shape - [objects parents-index masks-index] + [objects parents-index clip-parents-index] (fn [index shape] (let [{:keys [x y width height]} (gsh/points->selrect (:points shape)) shape-bound #js {:x x :y y :width width :height height} - parents (get parents-index (:id shape)) - masks (get masks-index (:id shape)) + parents (get parents-index (:id shape)) + clip-parents (get clip-parents-index (:id shape)) frame (when (and (not= :frame (:type shape)) (not= (:frame-id shape) uuid/zero)) @@ -32,19 +32,22 @@ (qdt/insert index (:id shape) shape-bound - (assoc shape :frame frame :masks masks :parents parents))))) + (assoc shape + :frame frame + :clip-parents clip-parents + :parents parents))))) (defn- create-index [objects] - (let [shapes (-> objects (dissoc uuid/zero) (vals)) - parents-index (cp/generate-child-all-parents-index objects) - masks-index (cp/create-mask-index objects parents-index) + (let [shapes (-> objects (dissoc uuid/zero) (vals)) + parents-index (cp/generate-child-all-parents-index objects) + clip-parents-index (cp/create-clip-index objects parents-index) bounds #js {:x (int -0.5e7) :y (int -0.5e7) :width (int 1e7) :height (int 1e7)} - index (reduce (index-shape objects parents-index masks-index) + index (reduce (index-shape objects parents-index clip-parents-index) (qdt/create bounds) shapes) @@ -66,13 +69,13 @@ (set/union (set (keys old-objects)) (set (keys new-objects)))) - shapes (->> changed-ids (mapv #(get new-objects %)) (filterv (comp not nil?))) - parents-index (cp/generate-child-all-parents-index new-objects shapes) - masks-index (cp/create-mask-index new-objects parents-index) + shapes (->> changed-ids (mapv #(get new-objects %)) (filterv (comp not nil?))) + parents-index (cp/generate-child-all-parents-index new-objects shapes) + clip-parents-index (cp/create-clip-index new-objects parents-index) new-index (qdt/remove-all index changed-ids) - index (reduce (index-shape new-objects parents-index masks-index) + index (reduce (index-shape new-objects parents-index clip-parents-index) new-index shapes) @@ -84,7 +87,7 @@ (create-index new-objects))) (defn- query-index - [{index :index z-index :z-index} rect frame-id include-frames? full-frame? include-groups? reverse?] + [{index :index z-index :z-index} rect frame-id full-frame? include-frames? clip-children? reverse?] (let [result (-> (qdt/search index (clj->js rect)) (es6-iterator-seq)) @@ -96,7 +99,6 @@ (or (not frame-id) (= frame-id (:frame-id shape))) (case (:type shape) :frame include-frames? - :group include-groups? true) (or (not full-frame?) @@ -107,11 +109,9 @@ (fn [shape] (gsh/overlaps? shape rect)) - overlaps-masks? - (fn [masks] - (->> masks - (some (comp not overlaps?)) - not)) + overlaps-parent? + (fn [clip-parents] + (->> clip-parents (some (comp not overlaps?)) not)) add-z-index (fn [{:keys [id frame-id] :as shape}] @@ -125,7 +125,9 @@ (filter match-criteria?) (filter overlaps?) (filter (comp overlaps? :frame)) - (filter (comp overlaps-masks? :masks)) + (filter (if clip-children? + (comp overlaps-parent? :clip-parents) + (constantly true))) (map add-z-index)) result) @@ -155,10 +157,10 @@ nil) (defmethod impl/handler :selection/query - [{:keys [page-id rect frame-id include-frames? full-frame? include-groups? reverse?] - :or {include-groups? true reverse? false include-frames? false full-frame? false} :as message}] + [{:keys [page-id rect frame-id reverse? full-frame? include-frames? clip-children?] + :or {reverse? false full-frame? false include-frames? false include-booleans? true include-groups? true} :as message}] (when-let [index (get @state page-id)] - (query-index index rect frame-id include-frames? full-frame? include-groups? reverse?))) + (query-index index rect frame-id full-frame? include-frames? clip-children? reverse?))) (defmethod impl/handler :selection/query-z-index [{:keys [page-id objects ids]}] From fcc7b6791e87773e14e75f072d34478bbb1d3eb1 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Mon, 20 Sep 2021 12:55:51 +0200 Subject: [PATCH 09/16] :sparkles: Correct selrect calculation --- common/src/app/common/geom/shapes.cljc | 2 +- common/src/app/common/geom/shapes/bool.cljc | 10 ++--- common/src/app/common/geom/shapes/common.cljc | 12 ++++++ common/src/app/common/geom/shapes/path.cljc | 43 +++++++++++++++++++ .../app/common/geom/shapes/transforms.cljc | 33 +++++--------- .../src/app/main/data/workspace/booleans.cljs | 8 ++-- frontend/src/app/worker/selection.cljs | 2 +- 7 files changed, 77 insertions(+), 33 deletions(-) diff --git a/common/src/app/common/geom/shapes.cljc b/common/src/app/common/geom/shapes.cljc index 6a21f6c58..43b069eb3 100644 --- a/common/src/app/common/geom/shapes.cljc +++ b/common/src/app/common/geom/shapes.cljc @@ -134,6 +134,7 @@ (d/export gco/center-rect) (d/export gco/center-points) (d/export gco/make-centered-rect) +(d/export gco/transform-points) (d/export gpr/rect->selrect) (d/export gpr/rect->points) @@ -146,7 +147,6 @@ (d/export gtr/transform-matrix) (d/export gtr/inverse-transform-matrix) (d/export gtr/transform-point-center) -(d/export gtr/transform-points) (d/export gtr/transform-rect) (d/export gtr/calculate-adjust-matrix) (d/export gtr/update-group-selrect) diff --git a/common/src/app/common/geom/shapes/bool.cljc b/common/src/app/common/geom/shapes/bool.cljc index 35c91b2dc..b0b47c057 100644 --- a/common/src/app/common/geom/shapes/bool.cljc +++ b/common/src/app/common/geom/shapes/bool.cljc @@ -7,19 +7,19 @@ (ns app.common.geom.shapes.bool (:require [app.common.geom.shapes.path :as gsp] - [app.common.geom.shapes.rect :as gpr] [app.common.path.bool :as pb] [app.common.path.shapes-to-path :as stp])) (defn update-bool-selrect + "Calculates the selrect+points for the boolean shape" [shape children objects] - (let [selrect (->> children + (let [content (->> children (map #(stp/convert-to-path % objects)) (mapv :content) - (pb/content-bool (:bool-type shape)) - (gsp/content->selrect)) - points (gpr/rect->points selrect)] + (pb/content-bool (:bool-type shape))) + + [points selrect] (gsp/content->points+selrect shape content)] (-> shape (assoc :selrect selrect) (assoc :points points)))) diff --git a/common/src/app/common/geom/shapes/common.cljc b/common/src/app/common/geom/shapes/common.cljc index 9b5c6d1b3..4ebd6a98e 100644 --- a/common/src/app/common/geom/shapes/common.cljc +++ b/common/src/app/common/geom/shapes/common.cljc @@ -7,6 +7,7 @@ (ns app.common.geom.shapes.common (:require [app.common.geom.point :as gpt] + [app.common.geom.matrix :as gmt] [app.common.math :as mth])) (defn center-rect @@ -48,3 +49,14 @@ :y (- (:y center) (/ height 2.0)) :width width :height height}) + +(defn transform-points + ([points matrix] + (transform-points points nil matrix)) + ([points center matrix] + (let [prev (if center (gmt/translate-matrix center) (gmt/matrix)) + post (if center (gmt/translate-matrix (gpt/negate center)) (gmt/matrix)) + + tr-point (fn [point] + (gpt/transform point (gmt/multiply prev matrix post)))] + (mapv tr-point points)))) diff --git a/common/src/app/common/geom/shapes/path.cljc b/common/src/app/common/geom/shapes/path.cljc index b2d5643b3..27c4eea02 100644 --- a/common/src/app/common/geom/shapes/path.cljc +++ b/common/src/app/common/geom/shapes/path.cljc @@ -9,6 +9,7 @@ [app.common.data :as d] [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] + [app.common.geom.shapes.common :as gsc] [app.common.geom.shapes.rect :as gpr] [app.common.math :as mth] [app.common.path.commands :as upc])) @@ -756,3 +757,45 @@ [_ to-p h1' h2'] (subcurve-range from-p to-p h1 h2 t0 t1)] (upc/make-curve-to (-> to-p #_(gpt/round 2)) h1' h2')))))))) + +(defn content-center + [content] + (-> content + content->selrect + gsc/center-selrect)) + +(defn content->points+selrect + "Given the content of a shape, calculate its points and selrect" + [shape content] + (let [{:keys [flip-x flip-y]} shape + transform + (cond-> (:transform shape (gmt/matrix)) + flip-x (gmt/scale (gpt/point -1 1)) + flip-y (gmt/scale (gpt/point 1 -1))) + + transform-inverse + (cond-> (gmt/matrix) + flip-x (gmt/scale (gpt/point -1 1)) + flip-y (gmt/scale (gpt/point 1 -1)) + :always (gmt/multiply (:transform-inverse shape (gmt/matrix)))) + + center (or (gsc/center-shape shape) + (content-center content)) + + base-content (transform-content + content + (gmt/transform-in center transform-inverse)) + + ;; Calculates the new selrect with points given the old center + points (-> (content->selrect base-content) + (gpr/rect->points) + (gsc/transform-points center transform)) + + points-center (gsc/center-points points) + + ;; Points is now the selrect but the center is different so we can create the selrect + ;; through points + selrect (-> points + (gsc/transform-points points-center transform-inverse) + (gpr/points->selrect))] + [points selrect])) diff --git a/common/src/app/common/geom/shapes/transforms.cljc b/common/src/app/common/geom/shapes/transforms.cljc index 97250b361..7f9bfa7a5 100644 --- a/common/src/app/common/geom/shapes/transforms.cljc +++ b/common/src/app/common/geom/shapes/transforms.cljc @@ -161,23 +161,12 @@ matrix (gmt/translate-matrix (gpt/negate center))))) -(defn transform-points - ([points matrix] - (transform-points points nil matrix)) - ([points center matrix] - (let [prev (if center (gmt/translate-matrix center) (gmt/matrix)) - post (if center (gmt/translate-matrix (gpt/negate center)) (gmt/matrix)) - - tr-point (fn [point] - (gpt/transform point (gmt/multiply prev matrix post)))] - (mapv tr-point points)))) - (defn transform-rect "Transform a rectangles and changes its attributes" [rect matrix] (let [points (-> (gpr/rect->points rect) - (transform-points matrix))] + (gco/transform-points matrix))] (gpr/points->rect points))) (defn calculate-adjust-matrix @@ -201,12 +190,12 @@ stretch-matrix (gmt/multiply stretch-matrix (gmt/skew-matrix skew-angle 0)) h1 (max 1 (calculate-height points-temp)) - h2 (max 1 (calculate-height (transform-points points-rec center stretch-matrix))) + h2 (max 1 (calculate-height (gco/transform-points points-rec center stretch-matrix))) h3 (if-not (mth/almost-zero? h2) (/ h1 h2) 1) h3 (if (mth/nan? h3) 1 h3) w1 (max 1 (calculate-width points-temp)) - w2 (max 1 (calculate-width (transform-points points-rec center stretch-matrix))) + w2 (max 1 (calculate-width (gco/transform-points points-rec center stretch-matrix))) w3 (if-not (mth/almost-zero? w2) (/ w1 w2) 1) w3 (if (mth/nan? w3) 1 w3) @@ -214,7 +203,7 @@ rotation-angle (calculate-rotation center - (transform-points points-rec (gco/center-points points-rec) stretch-matrix) + (gco/transform-points points-rec (gco/center-points points-rec) stretch-matrix) points-temp flip-x flip-y) @@ -233,13 +222,13 @@ its properties. We adjust de x,y,width,height and create a custom transform" [shape transform round-coords?] ;; - (let [points (-> shape :points (transform-points transform)) + (let [points (-> shape :points (gco/transform-points transform)) center (gco/center-points points) ;; Reverse the current transformation stack to get the base rectangle tr-inverse (:transform-inverse shape (gmt/matrix)) - points-temp (transform-points points center tr-inverse) + points-temp (gco/transform-points points center tr-inverse) points-temp-dim (calculate-dimensions points-temp) ;; This rectangle is the new data for the current rectangle. We want to change our rectangle @@ -305,12 +294,12 @@ points (->> children (mapcat :points)) ;; Invert to get the points minus the transforms applied to the group - base-points (transform-points points shape-center (:transform-inverse group (gmt/matrix))) + base-points (gco/transform-points points shape-center (:transform-inverse group (gmt/matrix))) ;; Defines the new selection rect with its transformations new-points (-> (gpr/points->selrect base-points) (gpr/rect->points) - (transform-points shape-center (:transform group (gmt/matrix)))) + (gco/transform-points shape-center (:transform group (gmt/matrix)))) ;; Calculte the new selrect new-selrect (gpr/points->selrect base-points)] @@ -544,9 +533,9 @@ transformed-parent-rect (-> parent-rect (gpr/rect->points) - (transform-points parent-displacement) - (transform-points parent-origin (gmt/scale-matrix parent-vector)) - (transform-points parent-origin-2 (gmt/scale-matrix parent-vector-2)) + (gco/transform-points parent-displacement) + (gco/transform-points parent-origin (gmt/scale-matrix parent-vector)) + (gco/transform-points parent-origin-2 (gmt/scale-matrix parent-vector-2)) (gpr/points->selrect)) ;; Calculate the modifiers in the horizontal and vertical directions diff --git a/frontend/src/app/main/data/workspace/booleans.cljs b/frontend/src/app/main/data/workspace/booleans.cljs index 4482d1da6..d5d7b1d36 100644 --- a/frontend/src/app/main/data/workspace/booleans.cljs +++ b/frontend/src/app/main/data/workspace/booleans.cljs @@ -107,10 +107,10 @@ (ptk/reify ::bool-to-group ptk/WatchEvent (watch [_ state _] - (let [objects (wsh/lookup-page-objects state)] - (let [change-to-group - (fn [shape] (bool->group shape objects))] - (rx/of (dch/update-shapes [shape-id] change-to-group {:reg-objects? true}))))))) + (let [objects (wsh/lookup-page-objects state) + change-to-group + (fn [shape] (bool->group shape objects))] + (rx/of (dch/update-shapes [shape-id] change-to-group {:reg-objects? true})))))) (defn change-bool-type diff --git a/frontend/src/app/worker/selection.cljs b/frontend/src/app/worker/selection.cljs index 47cb834b1..db60b7f7f 100644 --- a/frontend/src/app/worker/selection.cljs +++ b/frontend/src/app/worker/selection.cljs @@ -158,7 +158,7 @@ (defmethod impl/handler :selection/query [{:keys [page-id rect frame-id reverse? full-frame? include-frames? clip-children?] - :or {reverse? false full-frame? false include-frames? false include-booleans? true include-groups? true} :as message}] + :or {reverse? false full-frame? false include-frames? false clip-children? true} :as message}] (when-let [index (get @state page-id)] (query-index index rect frame-id full-frame? include-frames? clip-children? reverse?))) From 74f3d551f2ae364b9eb98c19a32d6c21e9673564 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Mon, 20 Sep 2021 14:29:18 +0200 Subject: [PATCH 10/16] :sparkles: Context menu for booleans --- .../images/icons/boolean-flatten.svg | 3 + .../main/partials/sidebar-align-options.scss | 9 ++- frontend/src/app/main/data/workspace.cljs | 4 ++ .../src/app/main/data/workspace/groups.cljs | 2 +- frontend/src/app/main/ui/icons.cljs | 1 + .../app/main/ui/workspace/context_menu.cljs | 63 +++++++++++-------- .../sidebar/options/menus/booleans.cljs | 28 ++++++--- frontend/translations/en.po | 6 ++ frontend/translations/es.po | 6 ++ 9 files changed, 86 insertions(+), 36 deletions(-) create mode 100644 frontend/resources/images/icons/boolean-flatten.svg diff --git a/frontend/resources/images/icons/boolean-flatten.svg b/frontend/resources/images/icons/boolean-flatten.svg new file mode 100644 index 000000000..f7816f8b5 --- /dev/null +++ b/frontend/resources/images/icons/boolean-flatten.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/styles/main/partials/sidebar-align-options.scss b/frontend/resources/styles/main/partials/sidebar-align-options.scss index 2d3565c4a..c9ee61451 100644 --- a/frontend/resources/styles/main/partials/sidebar-align-options.scss +++ b/frontend/resources/styles/main/partials/sidebar-align-options.scss @@ -9,11 +9,11 @@ display: flex; border-bottom: solid 1px $color-gray-60; height: 40px; - padding: 0 $x-small; .align-group { + padding: 0 $x-small; display: flex; - justify-content: space-evenly; + justify-content: start; width: 50%; &:not(:last-child) { @@ -25,7 +25,12 @@ align-items: center; cursor: pointer; display: flex; + height: 30px; + justify-content: center; + margin: 5px 0; padding: $small $x-small; + width: 25%; + svg { height: 16px; width: 16px; diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index 8f4e30599..7b5028fef 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -31,6 +31,7 @@ [app.main.data.workspace.libraries :as dwl] [app.main.data.workspace.notifications :as dwn] [app.main.data.workspace.path :as dwdp] + [app.main.data.workspace.path.shapes-to-path :as dwps] [app.main.data.workspace.persistence :as dwp] [app.main.data.workspace.selection :as dws] [app.main.data.workspace.state-helpers :as wsh] @@ -1994,3 +1995,6 @@ (d/export dwb/group-to-bool) (d/export dwb/bool-to-group) (d/export dwb/change-bool-type) + +;; Shapes to path +(d/export dwps/convert-selected-to-path) diff --git a/frontend/src/app/main/data/workspace/groups.cljs b/frontend/src/app/main/data/workspace/groups.cljs index f3f92c2b3..11605a13b 100644 --- a/frontend/src/app/main/data/workspace/groups.cljs +++ b/frontend/src/app/main/data/workspace/groups.cljs @@ -198,7 +198,7 @@ group-id (first selected) group (get objects group-id)] (when (and (= 1 (count selected)) - (= (:type group) :group)) + (contains? #{:group :bool} (:type group))) (let [[rchanges uchanges] (prepare-remove-group page-id group objects)] (rx/of (dch/commit-changes {:redo-changes rchanges diff --git a/frontend/src/app/main/ui/icons.cljs b/frontend/src/app/main/ui/icons.cljs index 35c5285d2..a0de63839 100644 --- a/frontend/src/app/main/ui/icons.cljs +++ b/frontend/src/app/main/ui/icons.cljs @@ -27,6 +27,7 @@ (def auto-width (icon-xref :auto-width)) (def boolean-difference (icon-xref :boolean-difference)) (def boolean-exclude (icon-xref :boolean-exclude)) +(def boolean-flatten (icon-xref :boolean-flatten)) (def boolean-intersection (icon-xref :boolean-intersection)) (def boolean-union (icon-xref :boolean-union)) (def box (icon-xref :box)) diff --git a/frontend/src/app/main/ui/workspace/context_menu.cljs b/frontend/src/app/main/ui/workspace/context_menu.cljs index b65f9d062..3ed95846f 100644 --- a/frontend/src/app/main/ui/workspace/context_menu.cljs +++ b/frontend/src/app/main/ui/workspace/context_menu.cljs @@ -10,7 +10,6 @@ [app.main.data.modal :as modal] [app.main.data.workspace :as dw] [app.main.data.workspace.libraries :as dwl] - [app.main.data.workspace.path.shapes-to-path :as dwpe] [app.main.data.workspace.shortcuts :as sc] [app.main.data.workspace.undo :as dwu] [app.main.refs :as refs] @@ -93,6 +92,21 @@ multiple? (> (count selected) 1) editable-shape? (#{:group :text :path} (:type shape)) + is-group? (and (some? shape) (= :group (:type shape))) + is-bool? (and (some? shape) (= :bool (:type shape))) + + set-bool + (fn [bool-type] + #(cond + (> (count selected) 1) + (st/emit! (dw/create-bool bool-type)) + + (and (= (count selected) 1) is-group?) + (st/emit! (dw/group-to-bool (:id shape) bool-type)) + + (and (= (count selected) 1) is-bool?) + (st/emit! (dw/change-bool-type (:id shape) bool-type)))) + current-file-id (mf/use-ctx ctx/current-file-id) do-duplicate (st/emitf dw/duplicate-selected) @@ -144,12 +158,8 @@ do-navigate-component-file (st/emitf (dwl/nav-to-component-file (:component-file shape))) - do-boolean-union (st/emitf (dw/create-bool :union)) - do-boolean-difference (st/emitf (dw/create-bool :difference)) - do-boolean-intersection (st/emitf (dw/create-bool :intersection)) - do-boolean-exclude (st/emitf (dw/create-bool :exclude)) - do-transform-to-path (st/emitf (dwpe/convert-selected-to-path)) - ] + do-transform-to-path (st/emitf (dw/convert-selected-to-path)) + do-flatten (st/emitf (dw/convert-selected-to-path))] [:* [:& menu-entry {:title (tr "workspace.shape.menu.copy") :shortcut (sc/get-tooltip :copy) @@ -198,7 +208,7 @@ :on-click do-flip-horizontal}] [:& menu-separator]]) - (when (and single? (= (:type shape) :group)) + (when (and single? (or is-bool? is-group?)) [:* [:& menu-entry {:title (tr "workspace.shape.menu.ungroup") :shortcut (sc/get-tooltip :ungroup) @@ -216,26 +226,29 @@ :shortcut (sc/get-tooltip :start-editing) :on-click do-start-editing}]) - [:& menu-entry {:title "Transform to path" + [:& menu-entry {:title (tr "workspace.shape.menu.transform-to-path") :on-click do-transform-to-path}] - [:& menu-entry {:title (tr "workspace.shape.menu.path")} - [:& menu-entry {:title (tr "workspace.shape.menu.union") - :shortcut (sc/get-tooltip :boolean-union) - :on-click do-boolean-union}] - [:& menu-entry {:title (tr "workspace.shape.menu.difference") - :shortcut (sc/get-tooltip :boolean-difference) - :on-click do-boolean-difference}] - [:& menu-entry {:title (tr "workspace.shape.menu.intersection") - :shortcut (sc/get-tooltip :boolean-intersection) - :on-click do-boolean-intersection}] - [:& menu-entry {:title (tr "workspace.shape.menu.exclude") - :shortcut (sc/get-tooltip :boolean-exclude) - :on-click do-boolean-exclude}] + (when (or multiple? (and single? (or is-group? is-bool?))) + [:& menu-entry {:title (tr "workspace.shape.menu.path")} + [:& menu-entry {:title (tr "workspace.shape.menu.union") + :shortcut (sc/get-tooltip :boolean-union) + :on-click (set-bool :union)}] + [:& menu-entry {:title (tr "workspace.shape.menu.difference") + :shortcut (sc/get-tooltip :boolean-difference) + :on-click (set-bool :difference)}] + [:& menu-entry {:title (tr "workspace.shape.menu.intersection") + :shortcut (sc/get-tooltip :boolean-intersection) + :on-click (set-bool :intersection)}] + [:& menu-entry {:title (tr "workspace.shape.menu.exclude") + :shortcut (sc/get-tooltip :boolean-exclude) + :on-click (set-bool :exclude)}] - [:& menu-separator] - ;; TODO - [:& menu-entry {:title "Flatten"}]] + (when (and single? is-bool?) + [:* + [:& menu-separator] + [:& menu-entry {:title (tr "workspace.shape.menu.flatten") + :on-click do-flatten}]])]) (if (:hidden shape) [:& menu-entry {:title (tr "workspace.shape.menu.show") diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/booleans.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/booleans.cljs index 4f1137a60..64b287417 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/booleans.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/booleans.cljs @@ -17,9 +17,14 @@ (mf/defc booleans-options [] (let [selected (mf/deref refs/selected-objects) - disabled (or (empty? selected) - (and (<= (count selected) 1) - (not (contains? #{:group :bool} (:type (first selected)))))) + + disabled-bool-btns + (or (empty? selected) + (and (<= (count selected) 1) + (not (contains? #{:group :bool} (:type (first selected)))))) + + disabled-flatten + (empty? selected) head (first selected) is-group? (and (some? head) (= :group (:type head))) @@ -44,29 +49,36 @@ [:div.align-group [:div.align-button.tooltip.tooltip-bottom {:alt (tr "workspace.shape.menu.union") - :class (dom/classnames :disabled disabled + :class (dom/classnames :disabled disabled-bool-btns :selected (= head-bool-type :union)) :on-click (set-bool :union)} i/boolean-union] [:div.align-button.tooltip.tooltip-bottom {:alt (tr "workspace.shape.menu.difference") - :class (dom/classnames :disabled disabled + :class (dom/classnames :disabled disabled-bool-btns :selected (= head-bool-type :difference)) :on-click (set-bool :difference)} i/boolean-difference] [:div.align-button.tooltip.tooltip-bottom {:alt (tr "workspace.shape.menu.intersection") - :class (dom/classnames :disabled disabled + :class (dom/classnames :disabled disabled-bool-btns :selected (= head-bool-type :intersection)) :on-click (set-bool :intersection)} i/boolean-intersection] [:div.align-button.tooltip.tooltip-bottom {:alt (tr "workspace.shape.menu.exclude") - :class (dom/classnames :disabled disabled + :class (dom/classnames :disabled disabled-bool-btns :selected (= head-bool-type :exclude)) :on-click (set-bool :exclude)} - i/boolean-exclude]]])) + i/boolean-exclude]] + + [:div.align-group + [:div.align-button.tooltip.tooltip-bottom + {:alt (tr "workspace.shape.menu.flatten") + :class (dom/classnames :disabled disabled-flatten) + :on-click (st/emitf (dw/convert-selected-to-path))} + i/boolean-flatten]]])) diff --git a/frontend/translations/en.po b/frontend/translations/en.po index 27c40c21c..f39b799f8 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -3130,3 +3130,9 @@ msgstr "Intersection" msgid "workspace.shape.menu.exclude" msgstr "Exclude" + +msgid "workspace.shape.menu.flatten" +msgstr "Flatten" + +msgid "workspace.shape.menu.transform-to-path" +msgstr "Transform to path" diff --git a/frontend/translations/es.po b/frontend/translations/es.po index a1020f9c3..a0139d9e2 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -3015,3 +3015,9 @@ msgstr "Intersección" msgid "workspace.shape.menu.exclude" msgstr "Exclusión" + +msgid "workspace.shape.menu.flatten" +msgstr "Aplanar" + +msgid "workspace.shape.menu.transform-to-path" +msgstr "Convertir en vector" From 778a542e1c78bdc27e088cea44ae5c04220b5e4c Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Mon, 20 Sep 2021 16:30:26 +0200 Subject: [PATCH 11/16] :sparkles: Removes children when flattening a group or bool shape --- common/src/app/common/data.cljc | 5 + .../src/app/common/pages/changes_builder.cljc | 116 ++++++++++++++++-- .../main/partials/sidebar-align-options.scss | 2 +- .../data/workspace/path/shapes_to_path.cljs | 23 +++- 4 files changed, 129 insertions(+), 17 deletions(-) diff --git a/common/src/app/common/data.cljc b/common/src/app/common/data.cljc index 3d227cb38..09dd24310 100644 --- a/common/src/app/common/data.cljc +++ b/common/src/app/common/data.cljc @@ -69,6 +69,11 @@ (next colls)) (persistent! result)))) +(defn preconj + [coll elem] + (assert (vector? coll)) + (concat [elem] coll)) + (defn enumerate ([items] (enumerate items 0)) ([items start] diff --git a/common/src/app/common/pages/changes_builder.cljc b/common/src/app/common/pages/changes_builder.cljc index b16e984fb..982285ad7 100644 --- a/common/src/app/common/pages/changes_builder.cljc +++ b/common/src/app/common/pages/changes_builder.cljc @@ -4,7 +4,10 @@ ;; ;; Copyright (c) UXBOX Labs SL -(ns app.common.pages.changes-builder) +(ns app.common.pages.changes-builder + (:require + [app.common.data :as d] + [app.common.pages.helpers :as h])) ;; Auxiliary functions to help create a set of changes (undo + redo) @@ -15,6 +18,12 @@ :origin origin} {::page-id page-id})) +(defn with-objects [changes objects] + (with-meta + changes + (-> (meta changes) + (assoc ::objects objects)))) + (defn add-obj [changes obj] (let [add-change @@ -33,7 +42,7 @@ (-> changes (update :redo-changes conj add-change) - (update :undo-changes #(into [del-change] %))))) + (update :undo-changes d/preconj del-change)))) (defn change-parent [changes parent-id shapes] @@ -44,16 +53,99 @@ :shapes (->> shapes (mapv :id))} mk-undo-change - (fn [shape] - {:type :mov-objects - :page-id (::page-id (meta changes)) - :parent-id (:parent-id shape) - :shapes [(:id shape)] - :index (::index shape)}) - - undo-moves - (->> shapes (mapv mk-undo-change))] + (fn [change-set shape] + (d/preconj + change-set + {:type :mov-objects + :page-id (::page-id (meta changes)) + :parent-id (:parent-id shape) + :shapes [(:id shape)] + :index (::index shape)}))] (-> changes (update :redo-changes conj set-parent-change) - (update :undo-changes #(into undo-moves %))))) + (update :undo-changes #(reduce mk-undo-change % shapes))))) + +(defn- generate-operation + "Given an object old and new versions and an attribute will append into changes + the set and undo operations" + [changes attr old new ignore-geometry?] + (let [old-val (get old attr) + new-val (get new attr)] + (if (= old-val new-val) + changes + (-> changes + (update :rops conj {:type :set :attr attr :val new-val :ignore-geometry ignore-geometry?}) + (update :uops conj {:type :set :attr attr :val old-val :ignore-touched true}))))) + +(defn update-shapes + "Calculate the changes and undos to be done when a function is applied to a + single object" + ([changes ids update-fn] + (update-shapes changes ids update-fn nil)) + + ([changes ids update-fn {:keys [attrs ignore-geometry?] :or {attrs nil ignore-geometry? false}}] + (assert (contains? (meta changes) ::objects) "Call (with-objects) first to use this function") + (let [objects (::objects (meta changes)) + + update-shape + (fn [changes id] + (let [old-obj (get objects id) + new-obj (update-fn old-obj) + + attrs (or attrs (d/concat #{} (keys old-obj) (keys new-obj))) + + {rops :rops uops :uops} + (reduce #(generate-operation %1 %2 old-obj new-obj ignore-geometry?) + {:rops [] :uops []} + attrs) + + uops (cond-> uops + (seq uops) + (conj {:type :set-touched :touched (:touched old-obj)})) + + change {:type :mod-obj + :page-id (::page-id (meta changes)) + :id id}] + + (cond-> changes + (seq rops) + (update :redo-changes conj (assoc change :operations rops)) + + (seq uops) + (update :undo-changes d/preconj (assoc change :operations uops)))))] + + (reduce update-shape changes ids)))) + +(defn remove-objects + [changes ids] + (assert (contains? (meta changes) ::objects) "Call (with-objects) first to use this function") + (let [page-id (::page-id (meta changes)) + objects (::objects (meta changes)) + + add-redo-change + (fn [change-set id] + (conj change-set + {:type :del-obj + :page-id page-id + :id id})) + + add-undo-change + (fn [change-set id] + (let [shape (get objects id)] + (d/preconj + change-set + {:type :add-obj + :page-id page-id + :parent-id (:parent-id shape) + :frame-id (:frame-id shape) + :id id + :obj (cond-> shape + (contains? shape :shapes) + (assoc :shapes [])) + :index (h/position-on-parent id objects)})))] + + + (-> changes + (update :redo-changes #(reduce add-redo-change % ids)) + (update :undo-changes #(reduce add-undo-change % ids))))) diff --git a/frontend/resources/styles/main/partials/sidebar-align-options.scss b/frontend/resources/styles/main/partials/sidebar-align-options.scss index c9ee61451..f07882f23 100644 --- a/frontend/resources/styles/main/partials/sidebar-align-options.scss +++ b/frontend/resources/styles/main/partials/sidebar-align-options.scss @@ -13,7 +13,7 @@ .align-group { padding: 0 $x-small; display: flex; - justify-content: start; + justify-content: flex-start; width: 50%; &:not(:last-child) { diff --git a/frontend/src/app/main/data/workspace/path/shapes_to_path.cljs b/frontend/src/app/main/data/workspace/path/shapes_to_path.cljs index 6f68123de..eae4dfb91 100644 --- a/frontend/src/app/main/data/workspace/path/shapes_to_path.cljs +++ b/frontend/src/app/main/data/workspace/path/shapes_to_path.cljs @@ -6,6 +6,8 @@ (ns app.main.data.workspace.path.shapes-to-path (:require + [app.common.pages :as cp] + [app.common.pages.changes-builder :as cb] [app.common.path.shapes-to-path :as upsp] [app.main.data.workspace.changes :as dch] [app.main.data.workspace.state-helpers :as wsh] @@ -15,7 +17,20 @@ (defn convert-selected-to-path [] (ptk/reify ::convert-selected-to-path ptk/WatchEvent - (watch [_ state _] - (let [objects (wsh/lookup-page-objects state) - selected (wsh/lookup-selected state)] - (rx/of (dch/update-shapes selected #(upsp/convert-to-path % objects))))))) + (watch [it state _] + (let [page-id (:current-page-id state) + objects (wsh/lookup-page-objects state) + selected (wsh/lookup-selected state) + + children-ids + (into #{} + (mapcat #(cp/get-children % objects)) + selected) + + changes + (-> (cb/empty-changes it page-id) + (cb/with-objects objects) + (cb/remove-objects children-ids) + (cb/update-shapes selected #(upsp/convert-to-path % objects)))] + + (rx/of (dch/commit-changes changes)))))) From 75d2d97d8eb71a189f36f4194a5f2c40f72c2101 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Tue, 21 Sep 2021 12:11:54 +0200 Subject: [PATCH 12/16] :sparkles: Renderers for booleans --- common/src/app/common/geom/shapes/common.cljc | 2 +- .../src/app/main/data/workspace/booleans.cljs | 8 ++-- frontend/src/app/main/exports.cljs | 19 +++++++++- frontend/src/app/main/ui/shapes/bool.cljs | 1 + .../app/main/ui/viewer/handoff/render.cljs | 37 +++++++++++++++--- frontend/src/app/main/ui/viewer/shapes.cljs | 38 ++++++++++++++++--- 6 files changed, 86 insertions(+), 19 deletions(-) diff --git a/common/src/app/common/geom/shapes/common.cljc b/common/src/app/common/geom/shapes/common.cljc index 4ebd6a98e..00eec4386 100644 --- a/common/src/app/common/geom/shapes/common.cljc +++ b/common/src/app/common/geom/shapes/common.cljc @@ -6,8 +6,8 @@ (ns app.common.geom.shapes.common (:require - [app.common.geom.point :as gpt] [app.common.geom.matrix :as gmt] + [app.common.geom.point :as gpt] [app.common.math :as mth])) (defn center-rect diff --git a/frontend/src/app/main/data/workspace/booleans.cljs b/frontend/src/app/main/data/workspace/booleans.cljs index d5d7b1d36..845bce61a 100644 --- a/frontend/src/app/main/data/workspace/booleans.cljs +++ b/frontend/src/app/main/data/workspace/booleans.cljs @@ -97,10 +97,10 @@ (ptk/reify ::group-to-bool ptk/WatchEvent (watch [_ state _] - (let [objects (wsh/lookup-page-objects state)] - (let [change-to-bool - (fn [shape] (group->bool shape bool-type objects))] - (rx/of (dch/update-shapes [shape-id] change-to-bool {:reg-objects? true}))))))) + (let [objects (wsh/lookup-page-objects state) + change-to-bool + (fn [shape] (group->bool shape bool-type objects))] + (rx/of (dch/update-shapes [shape-id] change-to-bool {:reg-objects? true})))))) (defn bool-to-group [shape-id] diff --git a/frontend/src/app/main/exports.cljs b/frontend/src/app/main/exports.cljs index 5c0217152..726562f4c 100644 --- a/frontend/src/app/main/exports.cljs +++ b/frontend/src/app/main/exports.cljs @@ -14,6 +14,7 @@ [app.common.math :as mth] [app.common.pages :as cp] [app.common.uuid :as uuid] + [app.main.ui.shapes.bool :as bool] [app.main.ui.shapes.circle :as circle] [app.main.ui.shapes.embed :as embed] [app.main.ui.shapes.export :as use] @@ -81,6 +82,18 @@ :is-child-selected? true :childs childs}])))) +(defn bool-wrapper-factory + [objects] + (let [shape-wrapper (shape-wrapper-factory objects) + bool-shape (bool/bool-shape shape-wrapper)] + (mf/fnc bool-wrapper + [{:keys [shape frame] :as props}] + (let [childs (->> (cp/get-children (:id shape) objects) + (select-keys objects))] + [:& bool-shape {:frame frame + :shape shape + :childs childs}])))) + (defn svg-raw-wrapper-factory [objects] (let [shape-wrapper (shape-wrapper-factory objects) @@ -104,9 +117,10 @@ [objects] (mf/fnc shape-wrapper [{:keys [frame shape] :as props}] - (let [group-wrapper (mf/use-memo (mf/deps objects) #(group-wrapper-factory objects)) + (let [group-wrapper (mf/use-memo (mf/deps objects) #(group-wrapper-factory objects)) svg-raw-wrapper (mf/use-memo (mf/deps objects) #(svg-raw-wrapper-factory objects)) - frame-wrapper (mf/use-memo (mf/deps objects) #(frame-wrapper-factory objects))] + bool-wrapper (mf/use-memo (mf/deps objects) #(bool-wrapper-factory objects)) + frame-wrapper (mf/use-memo (mf/deps objects) #(frame-wrapper-factory objects))] (when (and shape (not (:hidden shape))) (let [shape (-> (gsh/transform-shape shape) (gsh/translate-to-frame frame)) @@ -122,6 +136,7 @@ :circle [:> circle/circle-shape opts] :frame [:> frame-wrapper {:shape shape}] :group [:> group-wrapper {:shape shape :frame frame}] + :bool [:> bool-wrapper {:shape shape :frame frame}] nil)] ;; Don't wrap svg elements inside a otherwise some can break diff --git a/frontend/src/app/main/ui/shapes/bool.cljs b/frontend/src/app/main/ui/shapes/bool.cljs index 8a3db6d19..037479581 100644 --- a/frontend/src/app/main/ui/shapes/bool.cljs +++ b/frontend/src/app/main/ui/shapes/bool.cljs @@ -32,6 +32,7 @@ (let [childs (d/mapm #(gsh/transform-shape %2) childs)] (->> (:shapes shape) (map #(get childs %)) + (filter #(not (:hidden %))) (map #(stp/convert-to-path % childs)) (mapv :content) (pb/content-bool (:bool-type shape))))))] diff --git a/frontend/src/app/main/ui/viewer/handoff/render.cljs b/frontend/src/app/main/ui/viewer/handoff/render.cljs index 093cfc846..0f1854fc1 100644 --- a/frontend/src/app/main/ui/viewer/handoff/render.cljs +++ b/frontend/src/app/main/ui/viewer/handoff/render.cljs @@ -8,8 +8,10 @@ "The main container for a frame in handoff mode" (:require [app.common.geom.shapes :as geom] + [app.common.pages :as cp] [app.main.data.viewer :as dv] [app.main.store :as st] + [app.main.ui.shapes.bool :as bool] [app.main.ui.shapes.circle :as circle] [app.main.ui.shapes.frame :as frame] [app.main.ui.shapes.group :as group] @@ -106,6 +108,22 @@ (obj/merge! #js {:childs childs}))] [:> group-wrapper props])))) +(defn bool-container-factory + [objects] + (let [shape-container (shape-container-factory objects) + bool-shape (bool/bool-shape shape-container) + bool-wrapper (shape-wrapper-factory bool-shape)] + (mf/fnc bool-container + {::mf/wrap-props false} + [props] + (let [shape (unchecked-get props "shape") + children-ids (cp/get-children (:id shape) objects) + childs (select-keys objects children-ids) + props (-> (obj/new) + (obj/merge! props) + (obj/merge! #js {:childs childs}))] + [:> bool-wrapper props])))) + (defn svg-raw-container-factory [objects] (let [shape-container (shape-container-factory objects) @@ -133,12 +151,18 @@ [props] (let [shape (unchecked-get props "shape") frame (unchecked-get props "frame") - group-container (mf/use-memo - (mf/deps objects) - #(group-container-factory objects)) - svg-raw-container (mf/use-memo - (mf/deps objects) - #(svg-raw-container-factory objects))] + + group-container + (mf/use-memo (mf/deps objects) + #(group-container-factory objects)) + + bool-container + (mf/use-memo (mf/deps objects) + #(bool-container-factory objects)) + + svg-raw-container + (mf/use-memo (mf/deps objects) + #(svg-raw-container-factory objects))] (when (and shape (not (:hidden shape))) (let [shape (-> (geom/transform-shape shape) (geom/translate-to-frame frame)) @@ -151,6 +175,7 @@ :image [:> image-wrapper opts] :circle [:> circle-wrapper opts] :group [:> group-container opts] + :bool [:> bool-container opts] :svg-raw [:> svg-raw-container opts]))))))) (mf/defc render-frame-svg diff --git a/frontend/src/app/main/ui/viewer/shapes.cljs b/frontend/src/app/main/ui/viewer/shapes.cljs index 225c04a1d..5a2b88d73 100644 --- a/frontend/src/app/main/ui/viewer/shapes.cljs +++ b/frontend/src/app/main/ui/viewer/shapes.cljs @@ -15,6 +15,7 @@ [app.common.types.interactions :as cti] [app.main.data.viewer :as dv] [app.main.store :as st] + [app.main.ui.shapes.bool :as bool] [app.main.ui.shapes.circle :as circle] [app.main.ui.shapes.frame :as frame] [app.main.ui.shapes.group :as group] @@ -229,6 +230,10 @@ [shape-container show-interactions?] (generic-wrapper-factory (group/group-shape shape-container) show-interactions?)) +(defn bool-wrapper + [shape-container show-interactions?] + (generic-wrapper-factory (bool/bool-shape shape-container) show-interactions?)) + (defn svg-raw-wrapper [shape-container show-interactions?] (generic-wrapper-factory (svg-raw/svg-raw-shape shape-container) show-interactions?)) @@ -287,6 +292,21 @@ :show-interactions? show-interactions?})] [:> group-wrapper props])))) +(defn bool-container-factory + [objects show-interactions?] + (let [shape-container (shape-container-factory objects show-interactions?) + bool-wrapper (bool-wrapper shape-container show-interactions?)] + (mf/fnc bool-container + {::mf/wrap-props false} + [props] + (let [shape (unchecked-get props "shape") + childs (select-keys objects (cp/get-children (:id shape) objects)) + props (obj/merge! #js {} props + #js {:childs childs + :objects objects + :show-interactions? show-interactions?})] + [:> bool-wrapper props])))) + (defn svg-raw-container-factory [objects show-interactions?] (let [shape-container (shape-container-factory objects show-interactions?) @@ -312,12 +332,17 @@ (mf/fnc shape-container {::mf/wrap-props false} [props] - (let [group-container (mf/use-memo - (mf/deps objects) - #(group-container-factory objects show-interactions?)) - svg-raw-container (mf/use-memo - (mf/deps objects) - #(svg-raw-container-factory objects show-interactions?)) + (let [group-container + (mf/use-memo (mf/deps objects) + #(group-container-factory objects show-interactions?)) + + bool-container + (mf/use-memo (mf/deps objects) + #(bool-container-factory objects show-interactions?)) + + svg-raw-container + (mf/use-memo (mf/deps objects) + #(svg-raw-container-factory objects show-interactions?)) shape (unchecked-get props "shape") frame (unchecked-get props "frame")] (when (and shape (not (:hidden shape))) @@ -333,6 +358,7 @@ :image [:> image-wrapper opts] :circle [:> circle-wrapper opts] :group [:> group-container {:shape shape :frame frame :objects objects}] + :bool [:> bool-container {:shape shape :frame frame :objects objects}] :svg-raw [:> svg-raw-container {:shape shape :frame frame :objects objects}]))))))) (mf/defc frame-svg From c3520cf60612522506fb7c0a1448af10261a5296 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Wed, 22 Sep 2021 16:42:47 +0200 Subject: [PATCH 13/16] :sparkles: Improved intersection calculation --- common/src/app/common/geom/shapes/path.cljc | 185 ++++++++++++++------ common/src/app/common/math.cljc | 2 +- common/src/app/common/path/bool.cljc | 185 ++++++++++++-------- 3 files changed, 237 insertions(+), 135 deletions(-) diff --git a/common/src/app/common/geom/shapes/path.cljc b/common/src/app/common/geom/shapes/path.cljc index 27c4eea02..d45db2793 100644 --- a/common/src/app/common/geom/shapes/path.cljc +++ b/common/src/app/common/geom/shapes/path.cljc @@ -15,6 +15,7 @@ [app.common.path.commands :as upc])) (def ^:const curve-curve-precision 0.1) +(def ^:const curve-range-precision 2) (defn calculate-opposite-handler "Given a point and its handler, gives the symetric handler" @@ -54,13 +55,18 @@ (gpt/add from-p move-v))) (defn line-windup - [[_ to-p :as l] t] + [[from-p to-p :as l] t] (let [p (line-values l t) - v (gpt/to-vec p to-p)] + cy (:y p) + ay (:y to-p) + by (:y from-p)] + (cond - (> (:y v) 0) 1 - (< (:y v) 0) -1 - :else 0))) + (> (- cy ay) 0) 1 + (< (- cy ay) 0) -1 + (< (- cy by) 0) 1 + (> (- cy by) 0) -1 + :else 0))) ;; https://medium.com/@Acegikmo/the-ever-so-lovely-b%C3%A9zier-curve-eb27514da3bf ;; https://en.wikipedia.org/wiki/Bernstein_polynomial @@ -208,7 +214,7 @@ [root1 root2 root3]) - (= discriminant 0) + (mth/almost-zero? discriminant) (let [u1 (if (< q2 0) (mth/cubicroot (- q2)) (- (mth/cubicroot q2))) root1 (- (* 2 u1) (/ a 3)) root2 (- (- u1) (/ a 3))] @@ -266,9 +272,7 @@ (solve-roots a b c d)))] (->> coords (mapcat coord->tvalue) - ;; Only values in the range [0, 1] are valid - #_(filterv #(and (> % 0.01) (< % 0.99))) (filterv #(and (>= % 0) (<= % 1))))))) (defn command->point @@ -296,6 +300,33 @@ (gpt/point (-> cmd :params :c1x) (-> cmd :params :c1y)) (gpt/point (-> cmd :params :c2x) (-> cmd :params :c2y))])) +(defn command->selrect + ([command] + (command->selrect command (:prev command))) + + ([command prev-point] + (let [points (case (:command command) + :move-to [(command->point command)] + + ;; If it's a line we add the beginning point and endpoint + :line-to [prev-point (command->point command)] + + ;; We return the bezier extremities + :curve-to (d/concat + [prev-point + (command->point command)] + (let [curve [prev-point + (command->point command) + (command->point command :c1) + (command->point command :c2)]] + (->> (curve-extremities curve) + (mapv #(curve-values curve %))))) + []) + selrect (gpr/points->selrect points)] + (-> selrect + (update :width #(if (mth/almost-zero? %) 1 %)) + (update :height #(if (mth/almost-zero? %) 1 %)))))) + (defn content->selrect [content] (let [calc-extremities (fn [command prev] @@ -583,22 +614,25 @@ (curve-roots c2' :y))) (defn ray-line-intersect - [point line] + [point [from-p to-p :as line]] - (let [ray-line [point (gpt/point (inc (:x point)) (:y point))] - [ray-t line-t] (line-line-crossing ray-line line)] - - (when (and (some? line-t) (> ray-t 0) (>= line-t 0) (< line-t 1)) - [[(line-values line line-t) - (line-windup line line-t)]]))) + (let [ray-line-angle (gpt/angle (gpt/to-vec from-p to-p) (gpt/point 1 0))] + ;; If the ray is paralell to the line there will be no crossings + (when (and (> (mth/abs (- ray-line-angle 180)) 0.01) + (> (mth/abs (- ray-line-angle 0)) 0.01)) + (let [ray-line [point (gpt/point (inc (:x point)) (:y point))] + [ray-t line-t] (line-line-crossing ray-line line)] + (when (and (some? line-t) (> ray-t 0) (>= line-t 0) (<= line-t 1)) + [[(line-values line line-t) + (line-windup line line-t)]]))))) (defn line-line-intersect [l1 l2] (let [[l1-t l2-t] (line-line-crossing l1 l2)] (when (and (some? l1-t) (some? l2-t) - (> l1-t 0.01) (< l1-t 0.99) - (> l2-t 0.01) (< l2-t 0.99)) + (>= l1-t 0) (<= l1-t 1) + (>= l2-t 0) (<= l2-t 1)) [[l1-t] [l2-t]]))) (defn ray-curve-intersect @@ -619,16 +653,23 @@ (defn line-curve-intersect [l1 c2] + (let [curve-ts (->> (line-curve-crossing l1 c2) - (filterv #(let [curve-v (curve-values c2 %) - line-t (get-line-tval l1 curve-v)] - (and (> line-t 0.001) (< line-t 0.999))))) + (filterv + (fn [curve-t] + (let [curve-t (if (mth/almost-zero? curve-t) 0 curve-t) + curve-v (curve-values c2 curve-t) + line-t (get-line-tval l1 curve-v)] + (and (>= curve-t 0) (<= curve-t 1) + (>= line-t 0) (<= line-t 1)))))) + ;; Intersection line-curve points intersect-ps (->> curve-ts (mapv #(curve-values c2 %))) - + line-ts (->> intersect-ps (mapv #(get-line-tval l1 %)))] + [line-ts curve-ts])) (defn curve-curve-intersect @@ -658,27 +699,51 @@ r2 (curve-range->rect c2 c2-from c2-to)] (when (gpr/overlaps-rects? r1 r2) - (if (< (gpt/distance (curve-values c1 c1-from) - (curve-values c2 c2-from)) - curve-curve-precision) - [(sorted-set (mth/precision c1-from 4)) - (sorted-set (mth/precision c2-from 4))] + (let [p1 (curve-values c1 c1-from) + p2 (curve-values c2 c2-from)] - (let [c1-half (+ c1-from (/ (- c1-to c1-from) 2)) - c2-half (+ c2-from (/ (- c2-to c2-from) 2)) + (if (< (gpt/distance p1 p2) curve-curve-precision) + [{:p1 p1 + :p2 p2 + :d (gpt/distance p1 p2) + :t1 (mth/precision c1-from 4) + :t2 (mth/precision c2-from 4)}] - [c1-ts-1 c2-ts-1] (check-range c1-from c1-half c2-from c2-half) - [c1-ts-2 c2-ts-2] (check-range c1-from c1-half c2-half c2-to) - [c1-ts-3 c2-ts-3] (check-range c1-half c1-to c2-from c2-half) - [c1-ts-4 c2-ts-4] (check-range c1-half c1-to c2-half c2-to)] + (let [c1-half (+ c1-from (/ (- c1-to c1-from) 2)) + c2-half (+ c2-from (/ (- c2-to c2-from) 2)) - [(into (sorted-set) (d/concat [] c1-ts-1 c1-ts-2 c1-ts-3 c1-ts-4)) - (into (sorted-set) (d/concat [] c2-ts-1 c2-ts-2 c2-ts-3 c2-ts-4))])))))] + ts-1 (check-range c1-from c1-half c2-from c2-half) + ts-2 (check-range c1-from c1-half c2-half c2-to) + ts-3 (check-range c1-half c1-to c2-from c2-half) + ts-4 (check-range c1-half c1-to c2-half c2-to)] - (let [[c1-ts c2-ts] (check-range 0.005 0.995 0.005 0.995) - c1-ts (remove-close-ts c1-ts) - c2-ts (remove-close-ts c2-ts)] - [c1-ts c2-ts]))) + (d/concat [] ts-1 ts-2 ts-3 ts-4))))))) + + (remove-close-ts [{cp1 :p1 cp2 :p2}] + (fn [{:keys [p1 p2]}] + (and (>= (gpt/distance p1 cp1) curve-range-precision) + (>= (gpt/distance p2 cp2) curve-range-precision)))) + + (process-ts [ts] + (loop [current (first ts) + pending (rest ts) + c1-ts [] + c2-ts []] + + (if (nil? current) + [c1-ts c2-ts] + + (let [pending (->> pending (filter (remove-close-ts current))) + c1-ts (conj c1-ts (:t1 current)) + c2-ts (conj c2-ts (:t2 current))] + (recur (first pending) + (rest pending) + c1-ts + c2-ts)))))] + + (->> (check-range 0 1 0 1) + (sort-by :d) + (process-ts)))) (defn curve->rect [[from-p to-p :as curve]] @@ -730,33 +795,39 @@ for example (split-line-to-ranges p c [0 0.25 0.5 0.75 1] will split the line into 4 lines" [from-p cmd values] - (let [to-p (upc/command->point cmd)] - (->> (conj values 1) - (mapv (fn [val] - (-> (gpt/lerp from-p to-p val) - #_(gpt/round 2) - (upc/make-line-to))))))) + (let [values (->> values (filter #(and (> % 0) (< % 1))))] + (if (empty? values) + [cmd] + (let [to-p (upc/command->point cmd) + values-set (->> (conj values 1) (into (sorted-set)))] + (->> values-set + (mapv (fn [val] + (-> (gpt/lerp from-p to-p val) + #_(gpt/round 2) + (upc/make-line-to))))))))) (defn split-curve-to-ranges "Splits a curve into several curves given the points in `values` for example (split-curve-to-ranges p c [0 0.25 0.5 0.75 1] will split the curve into 4 curves that draw the same curve" [from-p cmd values] - (if (empty? values) - [cmd] - (let [to-p (upc/command->point cmd) - params (:params cmd) - h1 (gpt/point (:c1x params) (:c1y params)) - h2 (gpt/point (:c2x params) (:c2y params)) - values-set (->> (conj values 1) (into (sorted-set)))] - (->> (d/with-prev values-set) - (mapv - (fn [[t1 t0]] - (let [t0 (if (nil? t0) 0 t0) - [_ to-p h1' h2'] (subcurve-range from-p to-p h1 h2 t0 t1)] - (upc/make-curve-to (-> to-p #_(gpt/round 2)) h1' h2')))))))) + (let [values (->> values (filter #(and (> % 0) (< % 1))))] + (if (empty? values) + [cmd] + (let [to-p (upc/command->point cmd) + params (:params cmd) + h1 (gpt/point (:c1x params) (:c1y params)) + h2 (gpt/point (:c2x params) (:c2y params)) + values-set (->> (conj values 0 1) (into (sorted-set)))] + + (->> (d/with-prev values-set) + (rest) + (mapv + (fn [[t1 t0]] + (let [[_ to-p h1' h2'] (subcurve-range from-p to-p h1 h2 t0 t1)] + (upc/make-curve-to (-> to-p #_(gpt/round 2)) h1' h2'))))))))) (defn content-center [content] diff --git a/common/src/app/common/math.cljc b/common/src/app/common/math.cljc index 145bbf65c..f41328317 100644 --- a/common/src/app/common/math.cljc +++ b/common/src/app/common/math.cljc @@ -150,7 +150,7 @@ (if (> num to) to num))) (defn almost-zero? [num] - (< (abs num) 1e-8)) + (< (abs num) 1e-5)) (defonce float-equal-precision 0.001) diff --git a/common/src/app/common/path/bool.cljc b/common/src/app/common/path/bool.cljc index d12763c5d..b0c6ab406 100644 --- a/common/src/app/common/path/bool.cljc +++ b/common/src/app/common/path/bool.cljc @@ -9,6 +9,7 @@ [app.common.data :as d] [app.common.geom.point :as gpt] [app.common.geom.shapes.path :as gsp] + [app.common.geom.shapes.rect :as gpr] [app.common.path.commands :as upc] [app.common.path.subpaths :as ups])) @@ -29,41 +30,6 @@ :c1x c2x :c1y c2y :c2x c1x :c2y c1y))))) -(defn- split-command - [cmd values] - (case (:command cmd) - :line-to (gsp/split-line-to-ranges (:prev cmd) cmd values) - :curve-to (gsp/split-curve-to-ranges (:prev cmd) cmd values) - [cmd])) - -(defn split [seg-1 seg-2] - (let [[ts-seg-1 ts-seg-2] - (cond - (and (= :line-to (:command seg-1)) - (= :line-to (:command seg-2))) - (gsp/line-line-intersect (gsp/command->line seg-1) (gsp/command->line seg-2)) - - (and (= :line-to (:command seg-1)) - (= :curve-to (:command seg-2))) - (gsp/line-curve-intersect (gsp/command->line seg-1) (gsp/command->bezier seg-2)) - - (and (= :curve-to (:command seg-1)) - (= :line-to (:command seg-2))) - (let [[seg-2' seg-1'] - (gsp/line-curve-intersect (gsp/command->line seg-2) (gsp/command->bezier seg-1))] - ;; Need to reverse because we send the arguments reversed - [seg-1' seg-2']) - - (and (= :curve-to (:command seg-1)) - (= :curve-to (:command seg-2))) - (gsp/curve-curve-intersect (gsp/command->bezier seg-1) (gsp/command->bezier seg-2)) - - :else - [[] []])] - - [(split-command seg-1 ts-seg-1) - (split-command seg-2 ts-seg-2)])) - (defn add-previous ([content] (add-previous content nil)) @@ -77,57 +43,86 @@ (some? prev) (assoc :prev (gsp/command->point prev)))))))) +(defn- split-command + [cmd values] + (case (:command cmd) + :line-to (gsp/split-line-to-ranges (:prev cmd) cmd values) + :curve-to (gsp/split-curve-to-ranges (:prev cmd) cmd values) + [cmd])) + +(defn split-ts [seg-1 seg-2] + (cond + (and (= :line-to (:command seg-1)) + (= :line-to (:command seg-2))) + (gsp/line-line-intersect (gsp/command->line seg-1) (gsp/command->line seg-2)) + + (and (= :line-to (:command seg-1)) + (= :curve-to (:command seg-2))) + (gsp/line-curve-intersect (gsp/command->line seg-1) (gsp/command->bezier seg-2)) + + (and (= :curve-to (:command seg-1)) + (= :line-to (:command seg-2))) + (let [[seg-2' seg-1'] + (gsp/line-curve-intersect (gsp/command->line seg-2) (gsp/command->bezier seg-1))] + ;; Need to reverse because we send the arguments reversed + [seg-1' seg-2']) + + (and (= :curve-to (:command seg-1)) + (= :curve-to (:command seg-2))) + (gsp/curve-curve-intersect (gsp/command->bezier seg-1) (gsp/command->bezier seg-2)) + + :else + [[] []])) + +(defn split + [seg-1 seg-2] + (let [r1 (gsp/command->selrect seg-1) + r2 (gsp/command->selrect seg-2)] + (if (not (gpr/overlaps-rects? r1 r2)) + [[seg-1] [seg-2]] + (let [[ts-seg-1 ts-seg-2] (split-ts seg-1 seg-2)] + [(-> (split-command seg-1 ts-seg-1) (add-previous (:prev seg-1))) + (-> (split-command seg-2 ts-seg-2) (add-previous (:prev seg-2)))])))) + (defn content-intersect-split - "Given two path contents will return the intersect between them" [content-a content-b] - (if (or (empty? content-a) (empty? content-b)) - [content-a content-b] + (let [cache (atom {})] + (letfn [(split-cache [seg-1 seg-2] + (cond + (contains? @cache [seg-1 seg-2]) + (first (get @cache [seg-1 seg-2])) - (loop [current (first content-a) - pending (rest content-a) - content-b content-b - new-content-a []] + (contains? @cache [seg-2 seg-1]) + (second (get @cache [seg-2 seg-1])) - (if (not (some? current)) - [new-content-a content-b] + :else + (let [value (split seg-1 seg-2)] + (swap! cache assoc [seg-1 seg-2] value) + (first value)))) - (let [[new-current new-pending new-content-b] + (split-segment-on-content + [segment content] - (loop [current current - pending pending - other (first content-b) - head-content [] - tail-content (rest content-b)] + (loop [current (first content) + content (rest content) + result [segment]] - (if (not (some? other)) - ;; Finished recorring second content - [current pending head-content] + (if (nil? current) + result + (let [result (->> result (into [] (mapcat #(split-cache % current))))] + (recur (first content) + (rest content) + result))))) - ;; We split the current - (let [[new-as new-bs] (split current other) - new-as (add-previous new-as (:prev current)) - new-bs (add-previous new-bs (:prev other))] + (split-content + [content-a content-b] + (into [] + (mapcat #(split-segment-on-content % content-b)) + content-a))] - (if (> (count new-as) 1) - ;; We add the new-a's to the stack and change the b then we iterate to the top - (recur (first new-as) - (d/concat [] (rest new-as) pending) - (first tail-content) - (d/concat [] head-content new-bs) - (rest tail-content)) - - ;; No current segment-segment split we continue searching - (recur current - pending - (first tail-content) - (conj head-content other) - (rest tail-content))))))] - - (recur (first new-pending) - (rest new-pending) - new-content-b - (conj new-content-a new-current))))))) + [(split-content content-a content-b) + (split-content content-b content-a)]))) (defn is-segment? [cmd] @@ -145,6 +140,40 @@ (gsp/curve-values 0.5)))] (gsp/is-point-in-content? point content))) +(defn overlap-segment? + "Finds if the current segment is overlapping against other + segment meaning they have the same coordinates" + [segment content] + + (letfn [(overlap-single? + [other] + (when (and (= (:command segment) (:command other)) + (contains? #{:line-to :curve-to} (:command segment))) + + (case (:command segment) + + :line-to (let [[p1 q1] (gsp/command->line segment) + [p2 q2] (gsp/command->line other)] + + (or (and (< (gpt/distance p1 p2) 0.1) + (< (gpt/distance q1 q2) 0.1)) + (and (< (gpt/distance p1 q2) 0.1) + (< (gpt/distance q1 p2) 0.1)))) + + :curve-to (let [[p1 q1 h11 h21] (gsp/command->bezier segment) + [p2 q2 h12 h22] (gsp/command->bezier other)] + + (or (and (< (gpt/distance p1 p2) 0.1) + (< (gpt/distance q1 q2) 0.1) + (< (gpt/distance h11 h12) 0.1) + (< (gpt/distance h21 h22) 0.1)) + + (and (< (gpt/distance p1 q2) 0.1) + (< (gpt/distance q1 p2) 0.1) + (< (gpt/distance h11 h22) 0.1) + (< (gpt/distance h21 h12) 0.1)))))))] + (some? (d/seek overlap-single? content)))) + (defn create-union [content-a content-a-split content-b content-b-split] ;; Pick all segments in content-a that are not inside content-b ;; Pick all segments in content-b that are not inside content-a @@ -156,6 +185,7 @@ (defn create-difference [content-a content-a-split content-b content-b-split] ;; Pick all segments in content-a that are not inside content-b ;; Pick all segments in content b that are inside content-a + ;; removing overlapping (d/concat [] (->> content-a-split (filter #(not (contains-segment? % content-b)))) @@ -164,7 +194,8 @@ (->> content-b-split (reverse) (mapv reverse-command) - (filter #(contains-segment? % content-a))))) + (filter #(contains-segment? % content-a)) + (filter #(not (overlap-segment? % content-a-split)))))) (defn create-intersection [content-a content-a-split content-b content-b-split] ;; Pick all segments in content-a that are inside content-b From 8c25ee779644f1dc2332910b141638de80ff3731 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Fri, 24 Sep 2021 16:28:12 +0200 Subject: [PATCH 14/16] :sparkles: Fix style for bool shapes --- .../src/app/common/pages/changes_builder.cljc | 43 +++++++------ .../src/app/main/data/workspace/booleans.cljs | 29 ++++----- frontend/src/app/main/ui/shapes/bool.cljs | 63 +++++++++++++++++-- 3 files changed, 98 insertions(+), 37 deletions(-) diff --git a/common/src/app/common/pages/changes_builder.cljc b/common/src/app/common/pages/changes_builder.cljc index 982285ad7..a591913ff 100644 --- a/common/src/app/common/pages/changes_builder.cljc +++ b/common/src/app/common/pages/changes_builder.cljc @@ -7,6 +7,7 @@ (ns app.common.pages.changes-builder (:require [app.common.data :as d] + [app.common.pages :as cp] [app.common.pages.helpers :as h])) ;; Auxiliary functions to help create a set of changes (undo + redo) @@ -25,28 +26,34 @@ (assoc ::objects objects)))) (defn add-obj - [changes obj] - (let [add-change - {:type :add-obj - :id (:id obj) - :page-id (::page-id (meta changes)) - :parent-id (:parent-id obj) - :frame-id (:frame-id obj) - :index (::index obj) - :obj (dissoc obj ::index :parent-id)} + ([changes obj index] + (add-obj changes (assoc obj ::index index))) - del-change - {:type :del-obj - :id (:id obj) - :page-id (::page-id (meta changes))}] + ([changes obj] + (let [add-change + {:type :add-obj + :id (:id obj) + :page-id (::page-id (meta changes)) + :parent-id (:parent-id obj) + :frame-id (:frame-id obj) + :index (::index obj) + :obj (dissoc obj ::index :parent-id)} - (-> changes - (update :redo-changes conj add-change) - (update :undo-changes d/preconj del-change)))) + del-change + {:type :del-obj + :id (:id obj) + :page-id (::page-id (meta changes))}] + + (-> changes + (update :redo-changes conj add-change) + (update :undo-changes d/preconj del-change))))) (defn change-parent [changes parent-id shapes] - (let [set-parent-change + (assert (contains? (meta changes) ::objects) "Call (with-objects) first to use this function") + + (let [objects (::objects (meta changes)) + set-parent-change {:type :mov-objects :parent-id parent-id :page-id (::page-id (meta changes)) @@ -60,7 +67,7 @@ :page-id (::page-id (meta changes)) :parent-id (:parent-id shape) :shapes [(:id shape)] - :index (::index shape)}))] + :index (cp/position-on-parent (:id shape) objects)}))] (-> changes (update :redo-changes conj set-parent-change) diff --git a/frontend/src/app/main/data/workspace/booleans.cljs b/frontend/src/app/main/data/workspace/booleans.cljs index 845bce61a..25a03e57e 100644 --- a/frontend/src/app/main/data/workspace/booleans.cljs +++ b/frontend/src/app/main/data/workspace/booleans.cljs @@ -30,20 +30,20 @@ (sort-by ::index)))) (defn create-bool-data - [type name shapes objects] + [bool-type name shapes objects] (let [shapes (mapv #(stp/convert-to-path % objects) shapes) - head (first shapes) + head (if (= bool-type :difference) (first shapes) (last shapes)) head-data (select-keys head stp/style-properties)] - (-> {:id (uuid/next) - :type :bool - :bool-type type - :frame-id (:frame-id head) - :parent-id (:parent-id head) - :name name - ::index (::index head) - :shapes []} - (merge head-data) - (gsh/update-bool-selrect shapes objects)))) + [(-> {:id (uuid/next) + :type :bool + :bool-type bool-type + :frame-id (:frame-id head) + :parent-id (:parent-id head) + :name name + :shapes []} + (merge head-data) + (gsh/update-bool-selrect shapes objects)) + (cp/position-on-parent (:id head) objects)])) (defn group->bool [group bool-type objects] @@ -84,10 +84,11 @@ shapes (selected-shapes state)] (when-not (empty? shapes) - (let [boolean-data (create-bool-data bool-type name shapes objects) + (let [[boolean-data index] (create-bool-data bool-type name shapes objects) shape-id (:id boolean-data) changes (-> (cb/empty-changes it page-id) - (cb/add-obj boolean-data) + (cb/with-objects objects) + (cb/add-obj boolean-data index) (cb/change-parent shape-id shapes))] (rx/of (dch/commit-changes changes) (dwc/select-shapes (d/ordered-set shape-id))))))))) diff --git a/frontend/src/app/main/ui/shapes/bool.cljs b/frontend/src/app/main/ui/shapes/bool.cljs index 037479581..e671dacab 100644 --- a/frontend/src/app/main/ui/shapes/bool.cljs +++ b/frontend/src/app/main/ui/shapes/bool.cljs @@ -25,6 +25,23 @@ childs (use-equal-memo childs) + ;;[content-a content-b] + ;;(mf/use-memo + ;; (mf/deps shape childs) + ;; (fn [] + ;; (let [childs (d/mapm #(gsh/transform-shape %2) childs) + ;; [content-a content-b] + ;; (->> (:shapes shape) + ;; (map #(get childs %)) + ;; (filter #(not (:hidden %))) + ;; (map #(stp/convert-to-path % childs)) + ;; (mapv :content) + ;; (mapv pb/add-previous))] + ;; (pb/content-intersect-split content-a content-b)))) + + ;;_ (.log js/console "content-a" (clj->js content-a)) + ;;_ (.log js/console "content-b" (clj->js content-b)) + bool-content (mf/use-memo (mf/deps shape childs) @@ -35,9 +52,45 @@ (filter #(not (:hidden %))) (map #(stp/convert-to-path % childs)) (mapv :content) - (pb/content-bool (:bool-type shape))))))] + (pb/content-bool (:bool-type shape)))))) + ] - [:& shape-wrapper {:shape (-> shape - (assoc :type :path) - (assoc :content bool-content)) - :frame frame}]))) + [:* + [:& shape-wrapper {:shape (-> shape + (assoc :type :path) + (assoc :content bool-content)) + :frame frame}] + + + #_[:* + [:g + [:& shape-wrapper {:shape (-> shape + (assoc :type :path) + (assoc :stroke-color "blue") + (assoc :stroke-opacity 1) + (assoc :stroke-width 0.5) + (assoc :stroke-style :solid) + (dissoc :fill-color :fill-opacity) + (assoc :content content-b)) + :frame frame}] + (for [{:keys [x y]} (app.common.geom.shapes.path/content->points content-b)] + [:circle {:cx x + :cy y + :r 2.5 + :style {:fill "blue"}}])] + + [:g + [:& shape-wrapper {:shape (-> shape + (assoc :type :path) + (assoc :stroke-color "red") + (assoc :stroke-opacity 1) + (assoc :stroke-width 0.5) + (assoc :stroke-style :solid) + (dissoc :fill-color :fill-opacity) + (assoc :content content-a)) + :frame frame}] + (for [{:keys [x y]} (app.common.geom.shapes.path/content->points content-a)] + [:circle {:cx x + :cy y + :r 1.25 + :style {:fill "red"}}])]]]))) From 75f8e473a5fb591e6cb4e8d346ef75a9d60af1c1 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Mon, 27 Sep 2021 21:54:47 +0200 Subject: [PATCH 15/16] :sparkles: Export/Import and edgecases fixing --- common/src/app/common/file_builder.cljc | 42 +++++ common/src/app/common/geom/shapes/path.cljc | 104 +++++++----- common/src/app/common/geom/shapes/rect.cljc | 15 +- common/src/app/common/path/bool.cljc | 8 +- frontend/src/app/main/ui/shapes/bool.cljs | 159 ++++++++++-------- frontend/src/app/main/ui/shapes/export.cljs | 6 +- frontend/src/app/main/ui/shapes/shape.cljs | 1 + .../src/app/main/ui/workspace/viewport.cljs | 11 +- frontend/src/app/util/import/parser.cljs | 17 +- frontend/src/app/util/path/format.cljs | 4 +- frontend/src/app/worker/import.cljs | 2 + 11 files changed, 240 insertions(+), 129 deletions(-) diff --git a/common/src/app/common/file_builder.cljc b/common/src/app/common/file_builder.cljc index 4a497cda5..8b84d8c11 100644 --- a/common/src/app/common/file_builder.cljc +++ b/common/src/app/common/file_builder.cljc @@ -278,6 +278,48 @@ (-> file (update :parent-stack pop)))) +(defn add-bool [file data] + (let [frame-id (:current-frame-id file) + name (:name data) + obj (-> {:id (uuid/next) + :type :bool + :name name + :shapes [] + :frame-id frame-id} + (merge data) + (check-name file :bool) + (d/without-nils))] + (-> file + (commit-shape obj) + (assoc :last-id (:id obj)) + (add-name (:name obj)) + (update :parent-stack conjv (:id obj))))) + +(defn close-bool [file] + (let [bool-id (-> file :parent-stack peek) + bool (lookup-shape file bool-id) + children (->> bool :shapes (mapv #(lookup-shape file %))) + + file + (let [objects (lookup-objects file) + bool' (gsh/update-bool-selrect bool children objects)] + (commit-change + file + {:type :mod-obj + :id bool-id + :operations + [{:type :set :attr :selrect :val (:selrect bool')} + {:type :set :attr :points :val (:points bool')} + {:type :set :attr :x :val (-> bool' :selrect :x)} + {:type :set :attr :y :val (-> bool' :selrect :y)} + {:type :set :attr :width :val (-> bool' :selrect :width)} + {:type :set :attr :height :val (-> bool' :selrect :height)}]} + + {:add-container? true}))] + + (-> file + (update :parent-stack pop)))) + (defn create-shape [file type data] (let [frame-id (:current-frame-id file) frame (when-not (= frame-id root-frame) diff --git a/common/src/app/common/geom/shapes/path.cljc b/common/src/app/common/geom/shapes/path.cljc index d45db2793..dea5a3e50 100644 --- a/common/src/app/common/geom/shapes/path.cljc +++ b/common/src/app/common/geom/shapes/path.cljc @@ -17,6 +17,9 @@ (def ^:const curve-curve-precision 0.1) (def ^:const curve-range-precision 2) +(defn s= [a b] + (mth/almost-zero? (- (mth/abs a) b))) + (defn calculate-opposite-handler "Given a point and its handler, gives the symetric handler" [point handler] @@ -567,6 +570,34 @@ (mapv #(curve-values curve %)))] (gpr/points->rect (into [from-p to-p] extremes)))) +(defn line-has-point? + "Using the line equation we put the x value and check if matches with + the given Y. If it does the point is inside the line" + [point [from-p to-p :as line]] + (let [{x1 :x y1 :y} from-p + {x2 :x y2 :y} to-p + {px :x py :y} point + + m (/ (- y2 y1) (- x2 x1)) + vy (+ (* m px) (* (- m) x1) y1) + + t (get-line-tval line point)] + + ;; If x1 = x2 there is no slope, to see if the point is in the line + ;; only needs to check the x is the same + (and (or (and (s= x1 x2) (s= px x1)) + (s= py vy)) + ;; This will check if is between both segments + (or (> t 0) (s= t 0)) + (or (< t 1) (s= t 1))))) + +(defn curve-has-point? + [_point _curve] + ;; TODO + #_(or (< (gpt/distance point from-p) 0.01) + (< (gpt/distance point to-p) 0.01)) + false + ) (defn line-line-crossing [[from-p1 to-p1 :as l1] [from-p2 to-p2 :as l2]] @@ -613,26 +644,30 @@ (curve-roots c2' :y))) -(defn ray-line-intersect - [point [from-p to-p :as line]] - (let [ray-line-angle (gpt/angle (gpt/to-vec from-p to-p) (gpt/point 1 0))] - ;; If the ray is paralell to the line there will be no crossings - (when (and (> (mth/abs (- ray-line-angle 180)) 0.01) - (> (mth/abs (- ray-line-angle 0)) 0.01)) - (let [ray-line [point (gpt/point (inc (:x point)) (:y point))] - [ray-t line-t] (line-line-crossing ray-line line)] - (when (and (some? line-t) (> ray-t 0) (>= line-t 0) (<= line-t 1)) - [[(line-values line line-t) - (line-windup line line-t)]]))))) + +(defn ray-line-intersect + [point line] + + ;; If the ray is paralell to the line there will be no crossings + (let [ray-line [point (gpt/point (inc (:x point)) (:y point))] + [ray-t line-t] (line-line-crossing ray-line line)] + (when (and (some? line-t) + (> ray-t 0) + (or (> line-t 0) (s= line-t 0)) + (or (< line-t 1) (s= line-t 1))) + [[(line-values line line-t) + (line-windup line line-t)]]))) (defn line-line-intersect [l1 l2] (let [[l1-t l2-t] (line-line-crossing l1 l2)] (when (and (some? l1-t) (some? l2-t) - (>= l1-t 0) (<= l1-t 1) - (>= l2-t 0) (<= l2-t 1)) + (or (> l1-t 0) (s= l1-t 0)) + (or (< l1-t 1) (s= l1-t 1)) + (or (> l2-t 0) (s= l2-t 0)) + (or (< l2-t 1) (s= l2-t 1))) [[l1-t] [l2-t]]))) (defn ray-curve-intersect @@ -675,26 +710,7 @@ (defn curve-curve-intersect [c1 c2] - (letfn [(remove-close-ts [ts] - (loop [current (first ts) - pending (rest ts) - acc nil - result []] - (if (nil? current) - result - (if (and (some? acc) - (< (mth/abs (- current acc)) 0.01)) - (recur (first pending) - (rest pending) - acc - result) - - (recur (first pending) - (rest pending) - current - (conj result current)))))) - - (check-range [c1-from c1-to c2-from c2-to] + (letfn [(check-range [c1-from c1-to c2-from c2-to] (let [r1 (curve-range->rect c1 c1-from c1-to) r2 (curve-range->rect c2 c2-from c2-to)] @@ -760,14 +776,22 @@ (case (:command cmd) :line-to (ray-line-intersect point (command->line cmd (command->point prev))) :curve-to (ray-curve-intersect ray-line (command->bezier cmd (command->point prev))) - #_:else [])))] + #_:else []))) - ;; non-zero windup rule - (->> (d/with-prev content) - (mapcat cast-ray) - (map second) - (reduce +) - (not= 0)))) + (inside-border? [[cmd prev]] + (case (:command cmd) + :line-to (line-has-point? point (command->line cmd (command->point prev))) + :curve-to (curve-has-point? point (command->bezier cmd (command->point prev))) + #_:else false) + )] + (let [content-with-prev (d/with-prev content)] + (or (->> content-with-prev + (some inside-border?)) + (->> content-with-prev + (mapcat cast-ray) + (map second) + (reduce +) + (not= 0)))))) (defn split-line-to "Given a point and a line-to command will create a two new line-to commands diff --git a/common/src/app/common/geom/shapes/rect.cljc b/common/src/app/common/geom/shapes/rect.cljc index 205fcca2b..fe1541f23 100644 --- a/common/src/app/common/geom/shapes/rect.cljc +++ b/common/src/app/common/geom/shapes/rect.cljc @@ -7,7 +7,8 @@ (ns app.common.geom.shapes.rect (:require [app.common.geom.point :as gpt] - [app.common.geom.shapes.common :as gco])) + [app.common.geom.shapes.common :as gco] + [app.common.math :as mth])) (defn rect->points [{:keys [x y width height]}] ;; (assert (number? x)) @@ -71,6 +72,10 @@ :width width :height height}) +(defn s= + [a b] + (mth/almost-zero? (- a b))) + (defn overlaps-rects? "Check for two rects to overlap. Rects won't overlap only if one of them is fully to the left or the top" @@ -86,7 +91,7 @@ x2b (+ (:x rect-b) (:width rect-b)) y2b (+ (:y rect-b) (:height rect-b))] - (and (> x2a x1b) - (> x2b x1a) - (> y2a y1b) - (> y2b y1a)))) + (and (or (> x2a x1b) (s= x2a x1b)) + (or (>= x2b x1a) (s= x2b x1a)) + (or (<= y1b y2a) (s= y1b y2a)) + (or (<= y1a y2b) (s= y1a y2b))))) diff --git a/common/src/app/common/path/bool.cljc b/common/src/app/common/path/bool.cljc index b0c6ab406..37aef3402 100644 --- a/common/src/app/common/path/bool.cljc +++ b/common/src/app/common/path/bool.cljc @@ -151,7 +151,6 @@ (contains? #{:line-to :curve-to} (:command segment))) (case (:command segment) - :line-to (let [[p1 q1] (gsp/command->line segment) [p2 q2] (gsp/command->line other)] @@ -180,7 +179,8 @@ (d/concat [] (->> content-a-split (filter #(not (contains-segment? % content-b)))) - (->> content-b-split (filter #(not (contains-segment? % content-a)))))) + (->> content-b-split (filter #(or (not (contains-segment? % content-a)) + (overlap-segment? % content-a-split)))))) (defn create-difference [content-a content-a-split content-b content-b-split] ;; Pick all segments in content-a that are not inside content-b @@ -194,8 +194,8 @@ (->> content-b-split (reverse) (mapv reverse-command) - (filter #(contains-segment? % content-a)) - (filter #(not (overlap-segment? % content-a-split)))))) + (filter #(and (contains-segment? % content-a) + (not (overlap-segment? % content-a-split))))))) (defn create-intersection [content-a content-a-split content-b content-b-split] ;; Pick all segments in content-a that are inside content-b diff --git a/frontend/src/app/main/ui/shapes/bool.cljs b/frontend/src/app/main/ui/shapes/bool.cljs index e671dacab..e4f8fc3dc 100644 --- a/frontend/src/app/main/ui/shapes/bool.cljs +++ b/frontend/src/app/main/ui/shapes/bool.cljs @@ -8,89 +8,106 @@ (:require [app.common.data :as d] [app.common.geom.shapes :as gsh] + [app.common.geom.shapes.path :as gsp] [app.common.path.bool :as pb] [app.common.path.shapes-to-path :as stp] [app.main.ui.hooks :refer [use-equal-memo]] + [app.main.ui.shapes.export :as use] + [app.main.ui.shapes.path :refer [path-shape]] [app.util.object :as obj] [rumext.alpha :as mf])) +(mf/defc debug-bool + {::mf/wrap-props false} + [props] + + (let [frame (obj/get props "frame") + shape (obj/get props "shape") + childs (obj/get props "childs") + + [content-a content-b] + (mf/use-memo + (mf/deps shape childs) + (fn [] + (let [childs (d/mapm #(-> %2 (gsh/translate-to-frame frame) gsh/transform-shape) childs) + [content-a content-b] + (->> (:shapes shape) + (map #(get childs %)) + (filter #(not (:hidden %))) + (map #(stp/convert-to-path % childs)) + (mapv :content) + (mapv pb/add-previous))] + (pb/content-intersect-split content-a content-b))))] + [:g.debug-bool + [:g.shape-a + [:& path-shape {:shape (-> shape + (assoc :type :path) + (assoc :stroke-color "blue") + (assoc :stroke-opacity 1) + (assoc :stroke-width 0.5) + (assoc :stroke-style :solid) + (dissoc :fill-color :fill-opacity) + (assoc :content content-b)) + :frame frame}] + (for [{:keys [x y]} (gsp/content->points content-b)] + [:circle {:cx x + :cy y + :r 2.5 + :style {:fill "blue"}}])] + + [:g.shape-b + [:& path-shape {:shape (-> shape + (assoc :type :path) + (assoc :stroke-color "red") + (assoc :stroke-opacity 1) + (assoc :stroke-width 0.5) + (assoc :stroke-style :solid) + (dissoc :fill-color :fill-opacity) + (assoc :content content-a)) + :frame frame}] + (for [{:keys [x y]} (gsp/content->points content-a)] + [:circle {:cx x + :cy y + :r 1.25 + :style {:fill "red"}}])]]) + ) + + (defn bool-shape [shape-wrapper] (mf/fnc bool-shape - {::mf/wrap-props false} - [props] - (let [frame (obj/get props "frame") - shape (obj/get props "shape") - childs (obj/get props "childs") + {::mf/wrap-props false} + [props] + (let [frame (obj/get props "frame") + shape (obj/get props "shape") + childs (obj/get props "childs") - childs (use-equal-memo childs) + childs (use-equal-memo childs) - ;;[content-a content-b] - ;;(mf/use-memo - ;; (mf/deps shape childs) - ;; (fn [] - ;; (let [childs (d/mapm #(gsh/transform-shape %2) childs) - ;; [content-a content-b] - ;; (->> (:shapes shape) - ;; (map #(get childs %)) - ;; (filter #(not (:hidden %))) - ;; (map #(stp/convert-to-path % childs)) - ;; (mapv :content) - ;; (mapv pb/add-previous))] - ;; (pb/content-intersect-split content-a content-b)))) + include-metadata? (mf/use-ctx use/include-metadata-ctx) - ;;_ (.log js/console "content-a" (clj->js content-a)) - ;;_ (.log js/console "content-b" (clj->js content-b)) - - bool-content - (mf/use-memo - (mf/deps shape childs) - (fn [] - (let [childs (d/mapm #(gsh/transform-shape %2) childs)] - (->> (:shapes shape) - (map #(get childs %)) - (filter #(not (:hidden %))) - (map #(stp/convert-to-path % childs)) - (mapv :content) - (pb/content-bool (:bool-type shape)))))) - ] + bool-content + (mf/use-memo + (mf/deps shape childs) + (fn [] + (let [childs (d/mapm #(-> %2 (gsh/translate-to-frame frame) gsh/transform-shape) childs)] + (->> (:shapes shape) + (map #(get childs %)) + (filter #(not (:hidden %))) + (map #(stp/convert-to-path % childs)) + (mapv :content) + (pb/content-bool (:bool-type shape))))))] - [:* - [:& shape-wrapper {:shape (-> shape - (assoc :type :path) - (assoc :content bool-content)) - :frame frame}] + [:* + [:& path-shape {:shape (assoc shape :content bool-content)}] + (when include-metadata? + [:> "penpot:bool" {} + (for [item (->> (:shapes shape) (mapv #(get childs %)))] + [:& shape-wrapper {:frame frame + :shape item + :key (:id item)}])]) - #_[:* - [:g - [:& shape-wrapper {:shape (-> shape - (assoc :type :path) - (assoc :stroke-color "blue") - (assoc :stroke-opacity 1) - (assoc :stroke-width 0.5) - (assoc :stroke-style :solid) - (dissoc :fill-color :fill-opacity) - (assoc :content content-b)) - :frame frame}] - (for [{:keys [x y]} (app.common.geom.shapes.path/content->points content-b)] - [:circle {:cx x - :cy y - :r 2.5 - :style {:fill "blue"}}])] - - [:g - [:& shape-wrapper {:shape (-> shape - (assoc :type :path) - (assoc :stroke-color "red") - (assoc :stroke-opacity 1) - (assoc :stroke-width 0.5) - (assoc :stroke-style :solid) - (dissoc :fill-color :fill-opacity) - (assoc :content content-a)) - :frame frame}] - (for [{:keys [x y]} (app.common.geom.shapes.path/content->points content-a)] - [:circle {:cx x - :cy y - :r 1.25 - :style {:fill "red"}}])]]]))) + #_[:& debug-bool {:frame frame + :shape shape + :childs childs}]]))) diff --git a/frontend/src/app/main/ui/shapes/export.cljs b/frontend/src/app/main/ui/shapes/export.cljs index 7f1d3bcb9..97a9caf88 100644 --- a/frontend/src/app/main/ui/shapes/export.cljs +++ b/frontend/src/app/main/ui/shapes/export.cljs @@ -64,6 +64,7 @@ text? (= :text (:type shape)) path? (= :path (:type shape)) mask? (and group? (:masked-group? shape)) + bool? (= :bool (:type shape)) center (gsh/center-shape shape)] (-> props (add! :name) @@ -102,7 +103,10 @@ (add! :content (comp json/encode uuid->string)))) (cond-> mask? - (obj/set! "penpot:masked-group" "true"))))) + (obj/set! "penpot:masked-group" "true")) + + (cond-> bool? + (add! :bool-type))))) (defn add-library-refs [props shape] diff --git a/frontend/src/app/main/ui/shapes/shape.cljs b/frontend/src/app/main/ui/shapes/shape.cljs index eb26eb5a4..5b68b7ee9 100644 --- a/frontend/src/app/main/ui/shapes/shape.cljs +++ b/frontend/src/app/main/ui/shapes/shape.cljs @@ -72,6 +72,7 @@ [:> wrapper-tag wrapper-props (when include-metadata? [:& ed/export-data {:shape shape}]) + [:defs [:& defs/svg-defs {:shape shape :render-id render-id}] [:& filters/filters {:shape shape :filter-id filter-id}] diff --git a/frontend/src/app/main/ui/workspace/viewport.cljs b/frontend/src/app/main/ui/workspace/viewport.cljs index 3b377a857..e99495961 100644 --- a/frontend/src/app/main/ui/workspace/viewport.cljs +++ b/frontend/src/app/main/ui/workspace/viewport.cljs @@ -201,11 +201,12 @@ [:& use/export-page {:options options}] - [:& (mf/provider embed/context) {:value true} - ;; Render root shape - [:& shapes/root-shape {:key page-id - :objects objects - :active-frames @active-frames}]]] + [:& (mf/provider use/include-metadata-ctx) {:value true} + [:& (mf/provider embed/context) {:value true} + ;; Render root shape + [:& shapes/root-shape {:key page-id + :objects objects + :active-frames @active-frames}]]]] [:svg.viewport-controls {:xmlns "http://www.w3.org/2000/svg" diff --git a/frontend/src/app/util/import/parser.cljs b/frontend/src/app/util/import/parser.cljs index f4628f478..257fc169b 100644 --- a/frontend/src/app/util/import/parser.cljs +++ b/frontend/src/app/util/import/parser.cljs @@ -209,6 +209,13 @@ (->> node :content last))] (merge (add-attrs {} (:attrs svg-node)) node-attrs)) + (= type :bool) + (->> node + (:content) + (filter #(= :path (:tag %))) + (map #(:attrs %)) + (reduce add-attrs node-attrs)) + :else node-attrs))) @@ -443,6 +450,11 @@ mask? (assoc :masked-group? true)))) +(defn add-bool-data + [props node] + (-> props + (assoc :bool-type (get-meta node :bool-type keyword)))) + (defn parse-shadow [node] {:id (uuid/next) :style (get-meta node :shadow-type keyword) @@ -706,7 +718,10 @@ (add-image-data type node)) (cond-> (= :text type) - (add-text-data node)))))) + (add-text-data node)) + + (cond-> (= :bool type) + (add-bool-data node)))))) (defn parse-page-data [node] diff --git a/frontend/src/app/util/path/format.cljs b/frontend/src/app/util/path/format.cljs index 2cdf6f900..cc9f52e83 100644 --- a/frontend/src/app/util/path/format.cljs +++ b/frontend/src/app/util/path/format.cljs @@ -81,8 +81,8 @@ last-move (if current-move? point last-move)] (if (and (not current-move?) (pt= last-move point)) - (println (command->string (set-point current last-move))) - (println (command->string current))) + (print (command->string (set-point current last-move))) + (print (command->string current))) (when (and (not current-move?) (pt= last-move point)) (print "Z")) diff --git a/frontend/src/app/worker/import.cljs b/frontend/src/app/worker/import.cljs index fc6c8a4c1..a3bb981b3 100644 --- a/frontend/src/app/worker/import.cljs +++ b/frontend/src/app/worker/import.cljs @@ -202,6 +202,7 @@ (case type :frame (fb/close-artboard file) :group (fb/close-group file) + :bool (fb/close-bool file) :svg-raw (fb/close-svg-raw file) #_default file) @@ -218,6 +219,7 @@ file (case type :frame (fb/add-artboard file data) :group (fb/add-group file data) + :bool (fb/add-bool file data) :rect (fb/create-rect file data) :circle (fb/create-circle file data) :path (fb/create-path file data) From 1bd3a792dad3a9b9176be75e18e8309cadce6eb3 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Tue, 28 Sep 2021 11:30:06 +0200 Subject: [PATCH 16/16] :sparkles: Improved intersection edge cases --- common/src/app/common/geom/shapes/path.cljc | 20 ++++++++++++------- .../src/app/common/pages/changes_builder.cljc | 15 ++++++-------- .../src/app/common/path/shapes_to_path.cljc | 8 ++++---- frontend/src/app/main/ui/shapes/bool.cljs | 2 +- 4 files changed, 24 insertions(+), 21 deletions(-) diff --git a/common/src/app/common/geom/shapes/path.cljc b/common/src/app/common/geom/shapes/path.cljc index dea5a3e50..4569a2cb9 100644 --- a/common/src/app/common/geom/shapes/path.cljc +++ b/common/src/app/common/geom/shapes/path.cljc @@ -63,10 +63,9 @@ cy (:y p) ay (:y to-p) by (:y from-p)] - (cond - (> (- cy ay) 0) 1 - (< (- cy ay) 0) -1 + (and (> (- cy ay) 0) (not (s= cy ay))) 1 + (and (< (- cy ay) 0) (not (s= cy ay))) -1 (< (- cy by) 0) 1 (> (- cy by) 0) -1 :else 0))) @@ -558,8 +557,14 @@ (defn- get-line-tval [[{x1 :x y1 :y} {x2 :x y2 :y}] {:keys [x y]}] - (if (mth/almost-zero? (- x2 x1)) + (cond + (and (s= x1 x2) (s= y1 y2)) + ##Inf + + (s= x1 x2) (/ (- y y1) (- y2 y1)) + + :else (/ (- x x1) (- x2 x1)))) (defn- curve-range->rect @@ -578,15 +583,16 @@ {x2 :x y2 :y} to-p {px :x py :y} point - m (/ (- y2 y1) (- x2 x1)) - vy (+ (* m px) (* (- m) x1) y1) + m (when-not (s= x1 x2) (/ (- y2 y1) (- x2 x1))) + vy (when (some? m) (+ (* m px) (* (- m) x1) y1)) t (get-line-tval line point)] + ;; If x1 = x2 there is no slope, to see if the point is in the line ;; only needs to check the x is the same (and (or (and (s= x1 x2) (s= px x1)) - (s= py vy)) + (and (some? vy) (s= py vy))) ;; This will check if is between both segments (or (> t 0) (s= t 0)) (or (< t 1) (s= t 1))))) diff --git a/common/src/app/common/pages/changes_builder.cljc b/common/src/app/common/pages/changes_builder.cljc index a591913ff..d9567242c 100644 --- a/common/src/app/common/pages/changes_builder.cljc +++ b/common/src/app/common/pages/changes_builder.cljc @@ -13,17 +13,14 @@ ;; Auxiliary functions to help create a set of changes (undo + redo) (defn empty-changes [origin page-id] - (with-meta - {:redo-changes [] - :undo-changes [] - :origin origin} - {::page-id page-id})) + (let [changes {:redo-changes [] + :undo-changes [] + :origin origin}] + (with-meta changes + {::page-id page-id}))) (defn with-objects [changes objects] - (with-meta - changes - (-> (meta changes) - (assoc ::objects objects)))) + (vary-meta changes assoc ::objects objects)) (defn add-obj ([changes obj index] diff --git a/common/src/app/common/path/shapes_to_path.cljc b/common/src/app/common/path/shapes_to_path.cljc index 83a9358c8..24cbd1892 100644 --- a/common/src/app/common/path/shapes_to_path.cljc +++ b/common/src/app/common/path/shapes_to_path.cljc @@ -16,23 +16,23 @@ (def ^:const bezier-circle-c 0.551915024494) -(def ^:const dissoc-attrs +(def dissoc-attrs [:x :y :width :height :rx :ry :r1 :r2 :r3 :r4 :metadata :shapes]) -(def ^:const allowed-transform-types +(def allowed-transform-types #{:rect :circle :image :group :bool}) -(def ^:const style-group-properties +(def style-group-properties [:shadow :blur]) -(def ^:const style-properties +(def style-properties (d/concat style-group-properties [:fill-color diff --git a/frontend/src/app/main/ui/shapes/bool.cljs b/frontend/src/app/main/ui/shapes/bool.cljs index e4f8fc3dc..ebbe4753f 100644 --- a/frontend/src/app/main/ui/shapes/bool.cljs +++ b/frontend/src/app/main/ui/shapes/bool.cljs @@ -44,7 +44,7 @@ (assoc :type :path) (assoc :stroke-color "blue") (assoc :stroke-opacity 1) - (assoc :stroke-width 0.5) + (assoc :stroke-width 1) (assoc :stroke-style :solid) (dissoc :fill-color :fill-opacity) (assoc :content content-b))