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"