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/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/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..43b069eb3 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] @@ -133,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) @@ -145,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) @@ -156,7 +157,6 @@ (d/export gtr/calc-child-modifiers) ;; PATHS -(d/export gsp/content->points) (d/export gsp/content->selrect) (d/export gsp/transform-content) @@ -165,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..b0b47c057 --- /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.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 [content (->> children + (map #(stp/convert-to-path % objects)) + (mapv :content) + (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..00eec4386 100644 --- a/common/src/app/common/geom/shapes/common.cljc +++ b/common/src/app/common/geom/shapes/common.cljc @@ -6,6 +6,7 @@ (ns app.common.geom.shapes.common (:require + [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] [app.common.math :as mth])) @@ -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/intersect.cljc b/common/src/app/common/geom/shapes/intersect.cljc index 4b0593dc1..796daf099 100644 --- a/common/src/app/common/geom/shapes/intersect.cljc +++ b/common/src/app/common/geom/shapes/intersect.cljc @@ -308,3 +308,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..4569a2cb9 100644 --- a/common/src/app/common/geom/shapes/path.cljc +++ b/common/src/app/common/geom/shapes/path.cljc @@ -7,97 +7,275 @@ (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.common :as gsc] [app.common.geom.shapes.rect :as gpr] - [app.common.math :as mth])) + [app.common.math :as mth] + [app.common.path.commands :as upc])) -(defn content->points [content] +(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] + (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)))) + (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) + (gpt/scale t))] + (gpt/add from-p move-v))) + +(defn line-windup + [[from-p to-p :as l] t] + (let [p (line-values l t) + cy (:y p) + ay (:y to-p) + by (:y from-p)] + (cond + (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))) + ;; 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-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" - [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]) + + (mth/almost-zero? discriminant) + (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) (<= % 1))))))) (defn command->point ([command] (command->point command nil)) @@ -109,6 +287,48 @@ 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 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] @@ -123,10 +343,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 +524,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 +554,349 @@ (map point+distance) (reduce find-min-point) (first)))) + +(defn- get-line-tval + [[{x1 :x y1 :y} {x2 :x y2 :y}] {:keys [x y]}] + (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 + [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-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 (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)) + (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))))) + +(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]] + + (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] + + ;; 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) + (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 + [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 + (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 + [c1 c2] + + (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)] + + (when (gpr/overlaps-rects? r1 r2) + (let [p1 (curve-values c1 c1-from) + p2 (curve-values c2 c2-from)] + + (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)}] + + (let [c1-half (+ c1-from (/ (- c1-to c1-from) 2)) + c2-half (+ c2-from (/ (- c2-to c2-from) 2)) + + 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)] + + (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]] + (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 []))) + + (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 + 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 [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] + + (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] + (-> 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/rect.cljc b/common/src/app/common/geom/shapes/rect.cljc index 91e7d18a9..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)) @@ -70,3 +71,27 @@ :y (- (:y center) (/ height 2)) :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" + [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 (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/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/common/src/app/common/math.cljc b/common/src/app/common/math.cljc index 22ebd97af..f41328317 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 @@ -143,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) @@ -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/common/src/app/common/pages.cljc b/common/src/app/common/pages.cljc index fdf02cfa3..accb623ad 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) @@ -72,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/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/changes_builder.cljc b/common/src/app/common/pages/changes_builder.cljc new file mode 100644 index 000000000..d9567242c --- /dev/null +++ b/common/src/app/common/pages/changes_builder.cljc @@ -0,0 +1,155 @@ +;; 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 + (: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) + +(defn empty-changes [origin page-id] + (let [changes {:redo-changes [] + :undo-changes [] + :origin origin}] + (with-meta changes + {::page-id page-id}))) + +(defn with-objects [changes objects] + (vary-meta changes assoc ::objects objects)) + +(defn add-obj + ([changes obj index] + (add-obj changes (assoc obj ::index index))) + + ([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 d/preconj del-change))))) + +(defn change-parent + [changes parent-id shapes] + (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)) + :shapes (->> shapes (mapv :id))} + + 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 (cp/position-on-parent (:id shape) objects)}))] + + (-> changes + (update :redo-changes conj set-parent-change) + (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/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/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 new file mode 100644 index 000000000..37aef3402 --- /dev/null +++ b/common/src/app/common/path/bool.cljc @@ -0,0 +1,264 @@ +;; 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.path.bool + (:require + [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])) + +(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 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 (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 + [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])) + + (contains? @cache [seg-2 seg-1]) + (second (get @cache [seg-2 seg-1])) + + :else + (let [value (split seg-1 seg-2)] + (swap! cache assoc [seg-1 seg-2] value) + (first value)))) + + (split-segment-on-content + [segment content] + + (loop [current (first content) + content (rest content) + result [segment]] + + (if (nil? current) + result + (let [result (->> result (into [] (mapcat #(split-cache % current))))] + (recur (first content) + (rest content) + result))))) + + (split-content + [content-a content-b] + (into [] + (mapcat #(split-segment-on-content % content-b)) + content-a))] + + [(split-content content-a content-b) + (split-content content-b content-a)]))) + +(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 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 + (d/concat + [] + (->> content-a-split (filter #(not (contains-segment? % content-b)))) + (->> 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 + ;; Pick all segments in content b that are inside content-a + ;; removing overlapping + (d/concat + [] + (->> content-a-split (filter #(not (contains-segment? % content-b)))) + + ;; Reverse second content so we can have holes inside other shapes + (->> content-b-split + (reverse) + (mapv reverse-command) + (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 + ;; Pick all segments in content-b that are inside content-a + (d/concat + [] + (->> content-a-split (filter #(contains-segment? % content-b))) + (->> content-b-split (filter #(contains-segment? % content-a))))) + + +(defn create-exclusion [content-a 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-pair + [bool-type 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)))) + +(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 84a7725ef..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])) @@ -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/shapes_to_path.cljs b/common/src/app/common/path/shapes_to_path.cljc similarity index 53% rename from frontend/src/app/util/path/shapes_to_path.cljs rename to common/src/app/common/path/shapes_to_path.cljc index 8d7c86cbd..24cbd1892 100644 --- a/frontend/src/app/util/path/shapes_to_path.cljs +++ b/common/src/app/common/path/shapes_to_path.cljc @@ -4,22 +4,52 @@ ;; ;; 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.bool :as pb] + [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 dissoc-attrs + [:x :y :width :height + :rx :ry :r1 :r2 :r3 :r4 + :metadata :shapes]) + +(def allowed-transform-types + #{:rect + :circle + :image + :group + :bool}) + +(def style-group-properties + [:shadow + :blur]) + +(def style-properties + (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" @@ -86,8 +116,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 +144,71 @@ (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 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}] + ([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) - (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) + (= (:type shape) :bool) + (bool-to-path shape objects) - new-content - (case type - :circle - (circle->path x y width height) - (rect->path x y width height r1 r2 r3 r4)) + (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 (gsh/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))))) - ;; Do nothing if the shape is not of a correct type - shape)) + ;; 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))) diff --git a/frontend/src/app/util/path/subpaths.cljs b/common/src/app/common/path/subpaths.cljc similarity index 80% rename from frontend/src/app/util/path/subpaths.cljs rename to common/src/app/common/path/subpaths.cljc index 010f1343a..3c4d90c68 100644 --- a/frontend/src/app/util/path/subpaths.cljs +++ b/common/src/app/common/path/subpaths.cljc @@ -4,10 +4,16 @@ ;; ;; Copyright (c) UXBOX Labs SL -(ns app.util.path.subpaths +(ns app.common.path.subpaths (:require [app.common.data :as d] - [app.util.path.commands :as upc])) + [app.common.geom.point :as gpt] + [app.common.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" @@ -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))] @@ -134,3 +147,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 []))) 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-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/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..f07882f23 100644 --- a/frontend/resources/styles/main/partials/sidebar-align-options.scss +++ b/frontend/resources/styles/main/partials/sidebar-align-options.scss @@ -9,12 +9,12 @@ 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; - width: 100%; + justify-content: flex-start; + width: 50%; &:not(:last-child) { border-right: solid 1px $color-gray-60; @@ -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; @@ -46,5 +51,13 @@ fill: $color-gray-40; } } + + &.selected svg { + fill: $color-primary; + } + + &.selected:hover svg { + fill: $color-white; + } } } 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 a53fdcee3..7b5028fef 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] @@ -30,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] @@ -1097,7 +1099,7 @@ :text (rx/of (dwc/start-edition-mode id)) - :group + (:group :bool) (rx/of (dwc/select-shapes (into (d/ordered-set) [(last shapes)]))) :svg-raw @@ -1987,3 +1989,12 @@ (d/export dwg/unmask-group) (d/export dwg/group-selected) (d/export dwg/ungroup-selected) + +;; Boolean +(d/export dwb/create-bool) +(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/booleans.cljs b/frontend/src/app/main/data/workspace/booleans.cljs new file mode 100644 index 000000000..25a03e57e --- /dev/null +++ b/frontend/src/app/main/data/workspace/booleans.cljs @@ -0,0 +1,124 @@ +;; 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.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] + [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 + [bool-type name shapes objects] + (let [shapes (mapv #(stp/convert-to-path % objects) 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 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] + + (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) + 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 index] (create-bool-data bool-type name shapes objects) + shape-id (:id boolean-data) + changes (-> (cb/empty-changes it page-id) + (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))))))))) + +(defn group-to-bool + [shape-id bool-type] + (ptk/reify ::group-to-bool + ptk/WatchEvent + (watch [_ state _] + (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] + (ptk/reify ::bool-to-group + ptk/WatchEvent + (watch [_ state _] + (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 + [shape-id bool-type] + (ptk/reify ::change-bool-type + ptk/WatchEvent + (watch [_ _ _] + (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/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/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..eae4dfb91 --- /dev/null +++ b/frontend/src/app/main/data/workspace/path/shapes_to_path.cljs @@ -0,0 +1,36 @@ +;; 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.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] + [beicon.core :as rx] + [potok.core :as ptk])) + +(defn convert-selected-to-path [] + (ptk/reify ::convert-selected-to-path + ptk/WatchEvent + (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)))))) 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/data/workspace/shortcuts.cljs b/frontend/src/app/main/data/workspace/shortcuts.cljs index d589df2e8..34c5779ef 100644 --- a/frontend/src/app/main/data/workspace/shortcuts.cljs +++ b/frontend/src/app/main/data/workspace/shortcuts.cljs @@ -260,6 +260,23 @@ :command ["alt" "."] :type "keyup" :fn #(st/emit! (dw/toggle-distances-display false))} + + :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))} + }) (defn get-tooltip [shortcut] 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/refs.cljs b/frontend/src/app/main/refs.cljs index 4dcbac803..cc0197bfa 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)) @@ -239,16 +243,36 @@ ([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 + (d/deep-merge + modifiers + (into {} (map #(vector % {:modifiers 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/icons.cljs b/frontend/src/app/main/ui/icons.cljs index abf60ef6e..a0de63839 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,11 @@ (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-flatten (icon-xref :boolean-flatten)) +(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 +159,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/shapes/bool.cljs b/frontend/src/app/main/ui/shapes/bool.cljs new file mode 100644 index 000000000..ebbe4753f --- /dev/null +++ b/frontend/src/app/main/ui/shapes/bool.cljs @@ -0,0 +1,113 @@ +;; 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.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 1) + (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") + + childs (use-equal-memo childs) + + include-metadata? (mf/use-ctx use/include-metadata-ctx) + + 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))))))] + + [:* + [:& 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)}])]) + + #_[:& 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/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 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 29180d9a1..3ed95846f 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,52 @@ (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 on-click children] :as props}] + (let [submenu-ref (mf/use-ref nil) + hovering? (mf/use-ref false) + + on-pointer-enter + (mf/use-callback + (fn [] + (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 [] + (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 [] @@ -49,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) @@ -98,7 +156,10 @@ :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-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) @@ -147,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) @@ -165,6 +226,30 @@ :shortcut (sc/get-tooltip :start-editing) :on-click do-start-editing}]) + [:& menu-entry {:title (tr "workspace.shape.menu.transform-to-path") + :on-click do-transform-to-path}] + + (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)}] + + (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") :on-click do-show-shape}] @@ -240,7 +325,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/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..2cc8f6e00 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/shapes/bool.cljs @@ -0,0 +1,47 @@ +;; 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 (:id shape)) + #(refs/select-children (:id shape))) + + childs (mf/deref childs-ref)] + + [:> shape-container {:shape shape} + [:& shape-component + {:frame frame + :shape shape + :childs childs}]])))) + 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/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..e0a62cbff 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options.cljs @@ -11,10 +11,12 @@ [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] + [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] @@ -43,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 @@ -60,6 +63,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..64b287417 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/booleans.cljs @@ -0,0 +1,84 @@ +;; 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.dom :as dom] + [app.util.i18n :as i18n :refer [tr]] + [rumext.alpha :as mf])) + +(mf/defc booleans-options + [] + (let [selected (mf/deref refs/selected-objects) + + 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))) + 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 (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-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-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-bool-btns + :selected (= head-bool-type :exclude)) + :on-click (set-bool :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/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/main/ui/workspace/viewport.cljs b/frontend/src/app/main/ui/workspace/viewport.cljs index e5d2cecec..e99495961 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}] @@ -196,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" @@ -229,7 +235,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/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/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/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 4b0640f4e..cc9f52e83 100644 --- a/frontend/src/app/util/path/format.cljs +++ b/frontend/src/app/util/path/format.cljs @@ -6,7 +6,8 @@ (ns app.util.path.format (:require - [app.util.path.commands :as upc] + [app.common.path.commands :as upc] + [app.common.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)) + (print (command->string (set-point current last-move))) + (print (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 deleted file mode 100644 index 0478fff8c..000000000 --- a/frontend/src/app/util/path/geom.cljs +++ /dev/null @@ -1,55 +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.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 [from-p cmd val] - (let [to-p (upc/command->point cmd) - sp (gpt/line-val from-p to-p val)] - [(upc/make-line-to sp) cmd])) - -(defn split-curve-to [from-p cmd 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)] - [(upc/make-curve-to to1 h11 h21) - (upc/make-curve-to to2 h12 h22)])) - -(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 [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 f6409f221..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 @@ -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 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) diff --git a/frontend/src/app/worker/selection.cljs b/frontend/src/app/worker/selection.cljs index d93fcfaf4..db60b7f7f 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 clip-children? 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]}] diff --git a/frontend/translations/en.po b/frontend/translations/en.po index e5211a76d..f39b799f8 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -3114,4 +3114,25 @@ 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" + +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 6b716a299..a0139d9e2 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -3000,3 +3000,24 @@ 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" + +msgid "workspace.shape.menu.flatten" +msgstr "Aplanar" + +msgid "workspace.shape.menu.transform-to-path" +msgstr "Convertir en vector"