Merge pull request #1247 from penpot/feat/bool-shapes

Bool shapes
This commit is contained in:
Andrey Antukh 2021-09-28 11:45:49 +02:00 committed by GitHub
commit 9ad43e13da
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
69 changed files with 2308 additions and 348 deletions

View file

@ -69,6 +69,11 @@
(next colls)) (next colls))
(persistent! result)))) (persistent! result))))
(defn preconj
[coll elem]
(assert (vector? coll))
(concat [elem] coll))
(defn enumerate (defn enumerate
([items] (enumerate items 0)) ([items] (enumerate items 0))
([items start] ([items start]

View file

@ -278,6 +278,48 @@
(-> file (-> file
(update :parent-stack pop)))) (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] (defn create-shape [file type data]
(let [frame-id (:current-frame-id file) (let [frame-id (:current-frame-id file)
frame (when-not (= frame-id root-frame) frame (when-not (= frame-id root-frame)

View file

@ -22,7 +22,8 @@
(defn ^boolean point? (defn ^boolean point?
"Return true if `v` is Point instance." "Return true if `v` is Point instance."
[v] [v]
(instance? Point v)) (or (instance? Point v)
(and (map? v) (contains? v :x) (contains? v :y))))
(defn ^boolean point-like? (defn ^boolean point-like?
[{:keys [x y] :as v}] [{:keys [x y] :as v}]
@ -257,15 +258,12 @@
(and (mth/almost-zero? x) (and (mth/almost-zero? x)
(mth/almost-zero? y))) (mth/almost-zero? y)))
(defn line-val (defn lerp
"Given a line with two points p1-p2 and a 'percent'. Returns the point in the vector "Calculates a linear interpolation between two points given a tvalue"
generated by these two points. For example: for p1=(0,0) p2=(1,1) and v=0.25 will return [p1 p2 t]
the point (0.25, 0.25)" (let [x (mth/lerp (:x p1) (:x p2) t)
[p1 p2 v] y (mth/lerp (:y p1) (:y p2) t)]
(let [v (-> (to-vec p1 p2) (point x y)))
(scale v))]
(add p1 v)))
(defn rotate (defn rotate
"Rotates the point around center with an angle" "Rotates the point around center with an angle"

View file

@ -8,6 +8,7 @@
(:require (:require
[app.common.data :as d] [app.common.data :as d]
[app.common.geom.point :as gpt] [app.common.geom.point :as gpt]
[app.common.geom.shapes.bool :as gsb]
[app.common.geom.shapes.common :as gco] [app.common.geom.shapes.common :as gco]
[app.common.geom.shapes.intersect :as gin] [app.common.geom.shapes.intersect :as gin]
[app.common.geom.shapes.path :as gsp] [app.common.geom.shapes.path :as gsp]
@ -133,6 +134,7 @@
(d/export gco/center-rect) (d/export gco/center-rect)
(d/export gco/center-points) (d/export gco/center-points)
(d/export gco/make-centered-rect) (d/export gco/make-centered-rect)
(d/export gco/transform-points)
(d/export gpr/rect->selrect) (d/export gpr/rect->selrect)
(d/export gpr/rect->points) (d/export gpr/rect->points)
@ -145,7 +147,6 @@
(d/export gtr/transform-matrix) (d/export gtr/transform-matrix)
(d/export gtr/inverse-transform-matrix) (d/export gtr/inverse-transform-matrix)
(d/export gtr/transform-point-center) (d/export gtr/transform-point-center)
(d/export gtr/transform-points)
(d/export gtr/transform-rect) (d/export gtr/transform-rect)
(d/export gtr/calculate-adjust-matrix) (d/export gtr/calculate-adjust-matrix)
(d/export gtr/update-group-selrect) (d/export gtr/update-group-selrect)
@ -156,7 +157,6 @@
(d/export gtr/calc-child-modifiers) (d/export gtr/calc-child-modifiers)
;; PATHS ;; PATHS
(d/export gsp/content->points)
(d/export gsp/content->selrect) (d/export gsp/content->selrect)
(d/export gsp/transform-content) (d/export gsp/transform-content)
@ -165,3 +165,6 @@
(d/export gin/has-point?) (d/export gin/has-point?)
(d/export gin/has-point-rect?) (d/export gin/has-point-rect?)
(d/export gin/rect-contains-shape?) (d/export gin/rect-contains-shape?)
;; Bool
(d/export gsb/update-bool-selrect)

View file

@ -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))))

View file

@ -6,6 +6,7 @@
(ns app.common.geom.shapes.common (ns app.common.geom.shapes.common
(:require (:require
[app.common.geom.matrix :as gmt]
[app.common.geom.point :as gpt] [app.common.geom.point :as gpt]
[app.common.math :as mth])) [app.common.math :as mth]))
@ -48,3 +49,14 @@
:y (- (:y center) (/ height 2.0)) :y (- (:y center) (/ height 2.0))
:width width :width width
:height height}) :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))))

View file

@ -308,3 +308,4 @@
(->> shape (->> shape
:points :points
(every? (partial has-point-rect? rect)))) (every? (partial has-point-rect? rect))))

View file

@ -7,97 +7,275 @@
(ns app.common.geom.shapes.path (ns app.common.geom.shapes.path
(:require (:require
[app.common.data :as d] [app.common.data :as d]
[app.common.geom.matrix :as gmt]
[app.common.geom.point :as gpt] [app.common.geom.point :as gpt]
[app.common.geom.shapes.common :as gsc]
[app.common.geom.shapes.rect :as gpr] [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 (->> content
(map #(when (-> % :params :x) (gpt/point (-> % :params :x) (-> % :params :y)))) (map #(when (-> % :params :x)
(gpt/point (-> % :params :x) (-> % :params :y))))
(remove nil?) (remove nil?)
(into []))) (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://medium.com/@Acegikmo/the-ever-so-lovely-b%C3%A9zier-curve-eb27514da3bf
;; https://en.wikipedia.org/wiki/Bernstein_polynomial ;; https://en.wikipedia.org/wiki/Bernstein_polynomial
(defn curve-values (defn curve-values
"Parametric equation for cubic beziers. Given a start and end and "Parametric equation for cubic beziers. Given a start and end and
two intermediate points returns points for values of t. two intermediate points returns points for values of t.
If you draw t on a plane you got the bezier cube" 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 ([start end h1 h2 t]
t3 (* t2 t) ;; t cube (let [t2 (* t t) ;; t square
t3 (* t2 t) ;; t cube
start-v (+ (- t3) (* 3 t2) (* -3 t) 1) start-v (+ (- t3) (* 3 t2) (* -3 t) 1)
h1-v (+ (* 3 t3) (* -6 t2) (* 3 t)) h1-v (+ (* 3 t3) (* -6 t2) (* 3 t))
h2-v (+ (* -3 t3) (* 3 t2)) h2-v (+ (* -3 t3) (* 3 t2))
end-v t3 end-v t3
coord-v (fn [coord] coord-v (fn [coord]
(+ (* (coord start) start-v) (+ (* (coord start) start-v)
(* (coord h1) h1-v) (* (coord h1) h1-v)
(* (coord h2) h2-v) (* (coord h2) h2-v)
(* (coord end) end-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 (defn curve-split
"Splits a curve into two at the given parametric value `t`. "Splits a curve into two at the given parametric value `t`.
Calculates the Casteljau's algorithm intermediate points" 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) ([start end h1 h2 t]
p2 (gpt/line-val h1 h2 t) (let [p1 (gpt/lerp start h1 t)
p3 (gpt/line-val h2 end t) p2 (gpt/lerp h1 h2 t)
p4 (gpt/line-val p1 p2 t) p3 (gpt/lerp h2 end t)
p5 (gpt/line-val p2 p3 t) p4 (gpt/lerp p1 p2 t)
sp (gpt/line-val p4 p5 t)] p5 (gpt/lerp p2 p3 t)
[[start sp p1 p4] sp (gpt/lerp p4 p5 t)]
[sp end p5 p3]])) [[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 ;; https://pomax.github.io/bezierinfo/#extremities
(defn curve-extremities (defn curve-extremities
"Given a cubic bezier cube finds its roots in t. This are the extremities "Calculates the extremities by solving the first derivative for a cubic
if we calculate its values for x, y we can find a bounding box for the curve." bezier and then solving the quadratic formula"
[start end h1 h2] ([[start end h1 h2]]
(curve-extremities start end h1 h2))
(let [coords [[(:x start) (:x h1) (:x h2) (:x end)] ([start end h1 h2]
[(:y start) (:y h1) (:y h2) (:y end)]]
coord->tvalue (let [coords [[(:x start) (:x h1) (:x h2) (:x end)]
(fn [[c0 c1 c2 c3]] [(:y start) (:y h1) (:y h2) (:y end)]]
(let [a (+ (* -3 c0) (* 9 c1) (* -9 c2) (* 3 c3)) coord->tvalue
b (+ (* 6 c0) (* -12 c1) (* 6 c2)) (fn [[c0 c1 c2 c3]]
c (+ (* 3 c1) (* -3 c0)) (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 ;; Only values in the range [0, 1] are valid
(and (mth/almost-zero? a) (filterv #(and (> % 0.01) (< % 0.99)))))))
(not (mth/almost-zero? b)))
;; When the term a is close to zero we have a linear equation
[(/ (- c) b)]
;; If a is not close to zero return the two roots for a cuadratic (defn curve-roots
(not (mth/almost-zero? a)) "Uses cardano algorithm to find the roots for a cubic bezier"
[(/ (+ (- b) sqrt-b2-4ac) ([[start end h1 h2] coord]
(* 2 a)) (curve-roots start end h1 h2 coord))
(/ (- (- b) sqrt-b2-4ac)
(* 2 a))]
;; If a and b close to zero we can't find a root for a constant term ([start end h1 h2 coord]
:else
[])))]
(->> coords
(mapcat coord->tvalue)
;; Only values in the range [0, 1] are valid (let [coords [[(get start coord) (get h1 coord) (get h2 coord) (get end coord)]]
(filter #(and (>= % 0) (<= % 1)))
;; Pass t-values to actual points coord->tvalue
(map #(curve-values start end h1 h2 %))) (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 (defn command->point
([command] (command->point command nil)) ([command] (command->point command nil))
@ -109,6 +287,48 @@
y (get params ykey)] y (get params ykey)]
(gpt/point x y)))) (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] (defn content->selrect [content]
(let [calc-extremities (let [calc-extremities
(fn [command prev] (fn [command prev]
@ -123,10 +343,12 @@
:curve-to (d/concat :curve-to (d/concat
[(command->point prev) [(command->point prev)
(command->point command)] (command->point command)]
(curve-extremities (command->point prev) (let [curve [(command->point prev)
(command->point command) (command->point command)
(command->point command :c1) (command->point command :c1)
(command->point command :c2))) (command->point command :c2)]]
(->> (curve-extremities curve)
(mapv #(curve-values curve %)))))
[])) []))
extremities (mapcat calc-extremities extremities (mapcat calc-extremities
@ -302,24 +524,25 @@
"Given a path and a position" "Given a path and a position"
[shape position] [shape position]
(let [point+distance (fn [[cur-cmd prev-cmd]] (let [point+distance
(let [from-p (command->point prev-cmd) (fn [[cur-cmd prev-cmd]]
to-p (command->point cur-cmd) (let [from-p (command->point prev-cmd)
h1 (gpt/point (get-in cur-cmd [:params :c1x]) to-p (command->point cur-cmd)
(get-in cur-cmd [:params :c1y])) h1 (gpt/point (get-in cur-cmd [:params :c1x])
h2 (gpt/point (get-in cur-cmd [:params :c2x]) (get-in cur-cmd [:params :c1y]))
(get-in cur-cmd [:params :c2y])) h2 (gpt/point (get-in cur-cmd [:params :c2x])
point (get-in cur-cmd [:params :c2y]))
(case (:command cur-cmd) point
:line-to (case (:command cur-cmd)
(line-closest-point position from-p to-p) :line-to
(line-closest-point position from-p to-p)
:curve-to :curve-to
(curve-closest-point position from-p to-p h1 h2) (curve-closest-point position from-p to-p h1 h2)
nil)] nil)]
(when point (when point
[point (gpt/distance point position)]))) [point (gpt/distance point position)])))
find-min-point (fn [[min-p min-dist :as acc] [cur-p cur-dist :as cur]] 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))) (if (and (some? acc) (or (not cur) (<= min-dist cur-dist)))
@ -331,3 +554,349 @@
(map point+distance) (map point+distance)
(reduce find-min-point) (reduce find-min-point)
(first)))) (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]))

View file

@ -7,7 +7,8 @@
(ns app.common.geom.shapes.rect (ns app.common.geom.shapes.rect
(:require (:require
[app.common.geom.point :as gpt] [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]}] (defn rect->points [{:keys [x y width height]}]
;; (assert (number? x)) ;; (assert (number? x))
@ -70,3 +71,27 @@
:y (- (:y center) (/ height 2)) :y (- (:y center) (/ height 2))
:width width :width width
:height height}) :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)))))

View file

@ -161,23 +161,12 @@
matrix matrix
(gmt/translate-matrix (gpt/negate center))))) (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 (defn transform-rect
"Transform a rectangles and changes its attributes" "Transform a rectangles and changes its attributes"
[rect matrix] [rect matrix]
(let [points (-> (gpr/rect->points rect) (let [points (-> (gpr/rect->points rect)
(transform-points matrix))] (gco/transform-points matrix))]
(gpr/points->rect points))) (gpr/points->rect points)))
(defn calculate-adjust-matrix (defn calculate-adjust-matrix
@ -201,12 +190,12 @@
stretch-matrix (gmt/multiply stretch-matrix (gmt/skew-matrix skew-angle 0)) stretch-matrix (gmt/multiply stretch-matrix (gmt/skew-matrix skew-angle 0))
h1 (max 1 (calculate-height points-temp)) 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-not (mth/almost-zero? h2) (/ h1 h2) 1)
h3 (if (mth/nan? h3) 1 h3) h3 (if (mth/nan? h3) 1 h3)
w1 (max 1 (calculate-width points-temp)) 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-not (mth/almost-zero? w2) (/ w1 w2) 1)
w3 (if (mth/nan? w3) 1 w3) w3 (if (mth/nan? w3) 1 w3)
@ -214,7 +203,7 @@
rotation-angle (calculate-rotation rotation-angle (calculate-rotation
center 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 points-temp
flip-x flip-x
flip-y) flip-y)
@ -233,13 +222,13 @@
its properties. We adjust de x,y,width,height and create a custom transform" its properties. We adjust de x,y,width,height and create a custom transform"
[shape transform round-coords?] [shape transform round-coords?]
;; ;;
(let [points (-> shape :points (transform-points transform)) (let [points (-> shape :points (gco/transform-points transform))
center (gco/center-points points) center (gco/center-points points)
;; Reverse the current transformation stack to get the base rectangle ;; Reverse the current transformation stack to get the base rectangle
tr-inverse (:transform-inverse shape (gmt/matrix)) 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) points-temp-dim (calculate-dimensions points-temp)
;; This rectangle is the new data for the current rectangle. We want to change our rectangle ;; This rectangle is the new data for the current rectangle. We want to change our rectangle
@ -305,12 +294,12 @@
points (->> children (mapcat :points)) points (->> children (mapcat :points))
;; Invert to get the points minus the transforms applied to the group ;; 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 ;; Defines the new selection rect with its transformations
new-points (-> (gpr/points->selrect base-points) new-points (-> (gpr/points->selrect base-points)
(gpr/rect->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 ;; Calculte the new selrect
new-selrect (gpr/points->selrect base-points)] new-selrect (gpr/points->selrect base-points)]
@ -544,9 +533,9 @@
transformed-parent-rect (-> parent-rect transformed-parent-rect (-> parent-rect
(gpr/rect->points) (gpr/rect->points)
(transform-points parent-displacement) (gco/transform-points parent-displacement)
(transform-points parent-origin (gmt/scale-matrix parent-vector)) (gco/transform-points parent-origin (gmt/scale-matrix parent-vector))
(transform-points parent-origin-2 (gmt/scale-matrix parent-vector-2)) (gco/transform-points parent-origin-2 (gmt/scale-matrix parent-vector-2))
(gpr/points->selrect)) (gpr/points->selrect))
;; Calculate the modifiers in the horizontal and vertical directions ;; Calculate the modifiers in the horizontal and vertical directions

View file

@ -72,17 +72,24 @@
[v] [v]
(* v 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 (defn sqrt
"Returns the square root of a number." "Returns the square root of a number."
[v] [v]
#?(:cljs (js/Math.sqrt v) #?(:cljs (js/Math.sqrt v)
:clj (Math/sqrt v))) :clj (Math/sqrt v)))
(defn pow (defn cubicroot
"Returns the base to the exponent power." "Returns the cubic root of a number"
[b e] [v]
#?(:cljs (js/Math.pow b e) (if (pos? v)
:clj (Math/pow b e))) (pow v (/ 1 3))
(- (pow (- v) (/ 1 3)))))
(defn floor (defn floor
"Returns the largest integer less than or "Returns the largest integer less than or
@ -143,7 +150,7 @@
(if (> num to) to num))) (if (> num to) to num)))
(defn almost-zero? [num] (defn almost-zero? [num]
(< (abs num) 1e-8)) (< (abs num) 1e-5))
(defonce float-equal-precision 0.001) (defonce float-equal-precision 0.001)
@ -151,3 +158,9 @@
"Equality for float numbers. Check if the difference is within a range" "Equality for float numbers. Check if the difference is within a range"
[num1 num2] [num1 num2]
(<= (abs (- num1 num2)) float-equal-precision)) (<= (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)))

View file

@ -40,6 +40,7 @@
(d/export helpers/get-children) (d/export helpers/get-children)
(d/export helpers/get-children-objects) (d/export helpers/get-children-objects)
(d/export helpers/get-object-with-children) (d/export helpers/get-object-with-children)
(d/export helpers/select-children)
(d/export helpers/is-shape-grouped) (d/export helpers/is-shape-grouped)
(d/export helpers/get-parent) (d/export helpers/get-parent)
(d/export helpers/get-parents) (d/export helpers/get-parents)
@ -72,7 +73,7 @@
(d/export indices/update-z-index) (d/export indices/update-z-index)
(d/export indices/generate-child-all-parents-index) (d/export indices/generate-child-all-parents-index)
(d/export indices/generate-child-parent-index) (d/export indices/generate-child-parent-index)
(d/export indices/create-mask-index) (d/export indices/create-clip-index)
;; Process changes ;; Process changes
(d/export changes/process-changes) (d/export changes/process-changes)

View file

@ -9,6 +9,7 @@
[app.common.data :as d] [app.common.data :as d]
[app.common.exceptions :as ex] [app.common.exceptions :as ex]
[app.common.geom.shapes :as gsh] [app.common.geom.shapes :as gsh]
[app.common.geom.shapes.bool :as gshb]
[app.common.pages.common :refer [component-sync-attrs]] [app.common.pages.common :refer [component-sync-attrs]]
[app.common.pages.helpers :as cph] [app.common.pages.helpers :as cph]
[app.common.pages.init :as init] [app.common.pages.init :as init]
@ -156,7 +157,7 @@
(sequence (comp (sequence (comp
(mapcat #(cons % (cph/get-parents % objects))) (mapcat #(cons % (cph/get-parents % objects)))
(map #(get objects %)) (map #(get objects %))
(filter #(= (:type %) :group)) (filter #(contains? #{:group :bool} (:type %)))
(map :id) (map :id)
(distinct)) (distinct))
shapes))) shapes)))
@ -177,6 +178,9 @@
(empty? children) (empty? children)
group group
(= :bool (:type group))
(gshb/update-bool-selrect group children objects)
(:masked-group? group) (:masked-group? group)
(set-mask-selrect group children) (set-mask-selrect group children)

View file

@ -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)))))

View file

@ -138,6 +138,10 @@
[id objects] [id objects]
(mapv #(get objects %) (cons id (get-children 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 (defn is-shape-grouped
"Checks if a shape is inside a group" "Checks if a shape is inside a group"
[shape-id objects] [shape-id objects]

View file

@ -95,16 +95,24 @@
(map #(vector (:id %) (shape->parents %))) (map #(vector (:id %) (shape->parents %)))
(into {}))))) (into {})))))
(defn create-mask-index (defn create-clip-index
"Retrieves the mask information for an object" "Retrieves the mask information for an object"
[objects parents-index] [objects parents-index]
(let [retrieve-masks (let [retrieve-clips
(fn [_ parents] (fn [_ parents]
;; TODO: use transducers? (let [lookup-object (fn [id] (get objects id))
(->> parents get-clip-parents
(map #(get objects %)) (fn [shape]
(filter #(:masked-group? %)) (cond-> []
;; Retrieve the masking element (:masked-group? shape)
(mapv #(get objects (->> % :shapes first)))))] (conj (get objects (->> shape :shapes first)))
(= :bool (:type shape))
(conj shape)))]
(into []
(comp (map lookup-object)
(mapcat get-clip-parents))
parents)))]
(->> parents-index (->> parents-index
(d/mapm retrieve-masks)))) (d/mapm retrieve-clips))))

View file

@ -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 [])))

View file

@ -4,7 +4,7 @@
;; ;;
;; Copyright (c) UXBOX Labs SL ;; Copyright (c) UXBOX Labs SL
(ns app.util.path.commands (ns app.common.path.commands
(:require (:require
[app.common.data :as d] [app.common.data :as d]
[app.common.geom.point :as gpt])) [app.common.geom.point :as gpt]))
@ -199,3 +199,4 @@
(if (= prefix :c1) (if (= prefix :c1)
(command->point (get content (dec index))) (command->point (get content (dec index)))
(command->point (get content index)))) (command->point (get content index))))

View file

@ -4,22 +4,52 @@
;; ;;
;; Copyright (c) UXBOX Labs SL ;; Copyright (c) UXBOX Labs SL
(ns app.util.path.shapes-to-path (ns app.common.path.shapes-to-path
(:require (:require
[app.common.data :as d] [app.common.data :as d]
[app.common.geom.matrix :as gmt] [app.common.geom.matrix :as gmt]
[app.common.geom.point :as gpt] [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.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 ^:const bezier-circle-c 0.551915024494)
(def dissoc-attrs [:x :y :width :height
:rx :ry :r1 :r2 :r3 :r4 (def dissoc-attrs
:medata]) [:x :y :width :height
(def allowed-transform-types #{:rect :rx :ry :r1 :r2 :r3 :r4
:circle :metadata :shapes])
:image})
(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 (defn make-corner-arc
"Creates a curvle corner for border radius" "Creates a curvle corner for border radius"
@ -86,8 +116,9 @@
(defn rect->path (defn rect->path
"Creates a bezier curve that approximates a rounded corner rectangle" "Creates a bezier curve that approximates a rounded corner rectangle"
[x y width height r1 r2 r3 r4] [x y width height r1 r2 r3 r4 rx]
(let [p1 (gpt/point x (+ y r1)) (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) p2 (gpt/point (+ x r1) y)
p3 (gpt/point (+ width x (- r2)) y) p3 (gpt/point (+ width x (- r2)) y)
@ -113,34 +144,71 @@
(conj (make-corner-arc p7 p8 :bottom-left r4))) (conj (make-corner-arc p7 p8 :bottom-left r4)))
(conj (pc/make-line-to p1))))) (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 (defn convert-to-path
"Transforms the given shape to a 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) (= (:type shape) :bool)
(let [r1 (or r1 rx 0) (bool-to-path shape objects)
r2 (or r2 rx 0)
r3 (or r3 rx 0)
r4 (or r4 rx 0)
new-content (contains? allowed-transform-types type)
(case type (let [new-content
:circle (case type
(circle->path x y width height) :circle (circle->path x y width height)
(rect->path x y width height r1 r2 r3 r4)) #_:else (rect->path x y width height r1 r2 r3 r4 rx))
;; Apply the transforms that had the shape ;; Apply the transforms that had the shape
transform (:transform shape) transform (:transform shape)
new-content (cond-> new-content new-content (cond-> new-content
(some? transform) (some? transform)
(gsp/transform-content (gmt/transform-in (gsh/center-shape shape) transform)))] (gsp/transform-content (gmt/transform-in (gsc/center-shape shape) transform)))]
(-> shape
(d/without-keys dissoc-attrs)
(assoc :type :path)
(assoc :content new-content)
(cond-> (= :image type) (-> (assoc :fill-image metadata)
(dissoc :metadata)))))
;; Do nothing if the shape is not of a correct type
shape))
(-> shape
(assoc :type :path)
(assoc :content new-content)
(cond-> (= :image type)
(assoc :fill-image metadata))
(d/without-keys dissoc-attrs)))
:else
;; Do nothing if the shape is not of a correct type
shape)))

View file

@ -4,10 +4,16 @@
;; ;;
;; Copyright (c) UXBOX Labs SL ;; Copyright (c) UXBOX Labs SL
(ns app.util.path.subpaths (ns app.common.path.subpaths
(:require (:require
[app.common.data :as d] [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 (defn make-subpath
"Creates a subpath either from a single command or with all the data" "Creates a subpath either from a single command or with all the data"
@ -76,7 +82,7 @@
(defn subpaths-join (defn subpaths-join
"Join two subpaths together when the first finish where the second starts" "Join two subpaths together when the first finish where the second starts"
[subpath other] [subpath other]
(assert (= (:to subpath) (:from other))) (assert (pt= (:to subpath) (:from other)))
(-> subpath (-> subpath
(update :data d/concat (rest (:data other))) (update :data d/concat (rest (:data other)))
(assoc :to (:to other)))) (assoc :to (:to other))))
@ -88,15 +94,22 @@
(let [merge-with-candidate (let [merge-with-candidate
(fn [[candidate result] current] (fn [[candidate result] current]
(cond (cond
(= (:to current) (:from current)) (pt= (:to current) (:from current))
;; Subpath is already a closed path
[candidate (conj result current)] [candidate (conj result current)]
(= (:to candidate) (:from current)) (pt= (:to candidate) (:from current))
[(subpaths-join candidate current) result] [(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] [(subpaths-join candidate (reverse-subpath current)) result]
(pt= (:from candidate) (:from current))
[(subpaths-join (reverse-subpath current) candidate) result]
:else :else
[candidate (conj result current)]))] [candidate (conj result current)]))]
@ -114,7 +127,7 @@
(if (some? current) (if (some? current)
(let [[new-current new-subpaths] (let [[new-current new-subpaths]
(if (= (:from current) (:to current)) (if (pt= (:from current) (:to current))
[current subpaths] [current subpaths]
(merge-paths current subpaths))] (merge-paths current subpaths))]
@ -134,3 +147,14 @@
(->> closed-subpaths (->> closed-subpaths
(mapcat :data) (mapcat :data)
(into [])))) (into []))))
(defn reverse-content
"Given a content reverse the order of the commands"
[content]
(->> content
(get-subpaths)
(mapv reverse-subpath)
(reverse)
(mapcat :data)
(into [])))

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 500">
<path d="M0 0v337.797h162.201V500H500V162.203H337.797V0Zm34.031 34.033h269.735v269.733H34.03Z"/>
</svg>

After

Width:  |  Height:  |  Size: 169 B

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 500">
<path d="M0 0v337.797h162.203V500H500V162.203H337.799V0Zm196.234 196.234h107.532v107.532H196.234Z"/>
</svg>

After

Width:  |  Height:  |  Size: 173 B

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 500">
<path d="M0 0v337.059h141.904C150.271 428.269 227.21 500 320.557 500 419.47 500 500 419.47 500 320.557c0-93.18-71.47-170.015-162.445-178.614V0H0zm32 32h273.555v109.791c-43.314 3.609-82.301 22.678-111.43 51.65L32 32.8V32zm273.555 141.867V303.85l-88.866-88.051c23.298-23.102 54.286-38.46 88.866-41.932zm32 .246C411.119 182.51 468 244.691 468 320.557c0 40.092-15.942 76.315-41.777 102.855l-88.668-87.855V174.113z"/>
</svg>

After

Width:  |  Height:  |  Size: 485 B

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 500">
<path d="M0 0v337.797h162.201V500H500V162.203H337.797V0Zm34.031 34.033h269.735v128.17H162.2v141.563H34.031Zm303.766 162.201h128.17v269.733H196.234v-128.17h141.563z"/>
</svg>

After

Width:  |  Height:  |  Size: 239 B

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 500">
<path d="M0 0v337.797h162.203V500H500V162.203H337.797V0H0z"/>
</svg>

After

Width:  |  Height:  |  Size: 134 B

View file

@ -9,12 +9,12 @@
display: flex; display: flex;
border-bottom: solid 1px $color-gray-60; border-bottom: solid 1px $color-gray-60;
height: 40px; height: 40px;
padding: 0 $x-small;
.align-group { .align-group {
padding: 0 $x-small;
display: flex; display: flex;
justify-content: space-evenly; justify-content: flex-start;
width: 100%; width: 50%;
&:not(:last-child) { &:not(:last-child) {
border-right: solid 1px $color-gray-60; border-right: solid 1px $color-gray-60;
@ -25,7 +25,12 @@
align-items: center; align-items: center;
cursor: pointer; cursor: pointer;
display: flex; display: flex;
height: 30px;
justify-content: center;
margin: 5px 0;
padding: $small $x-small; padding: $small $x-small;
width: 25%;
svg { svg {
height: 16px; height: 16px;
width: 16px; width: 16px;
@ -46,5 +51,13 @@
fill: $color-gray-40; fill: $color-gray-40;
} }
} }
&.selected svg {
fill: $color-primary;
}
&.selected:hover svg {
fill: $color-white;
}
} }
} }

View file

@ -47,6 +47,17 @@
&:hover { &:hover {
background-color: $color-primary-lighter; background-color: $color-primary-lighter;
} }
.submenu-icon {
position: absolute;
right: 1rem;
svg {
width: 10px;
height: 10px;
}
}
} }
} }

View file

@ -23,6 +23,7 @@
[app.config :as cfg] [app.config :as cfg]
[app.main.data.events :as ev] [app.main.data.events :as ev]
[app.main.data.messages :as dm] [app.main.data.messages :as dm]
[app.main.data.workspace.booleans :as dwb]
[app.main.data.workspace.changes :as dch] [app.main.data.workspace.changes :as dch]
[app.main.data.workspace.common :as dwc] [app.main.data.workspace.common :as dwc]
[app.main.data.workspace.drawing :as dwd] [app.main.data.workspace.drawing :as dwd]
@ -30,6 +31,7 @@
[app.main.data.workspace.libraries :as dwl] [app.main.data.workspace.libraries :as dwl]
[app.main.data.workspace.notifications :as dwn] [app.main.data.workspace.notifications :as dwn]
[app.main.data.workspace.path :as dwdp] [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.persistence :as dwp]
[app.main.data.workspace.selection :as dws] [app.main.data.workspace.selection :as dws]
[app.main.data.workspace.state-helpers :as wsh] [app.main.data.workspace.state-helpers :as wsh]
@ -1097,7 +1099,7 @@
:text :text
(rx/of (dwc/start-edition-mode id)) (rx/of (dwc/start-edition-mode id))
:group (:group :bool)
(rx/of (dwc/select-shapes (into (d/ordered-set) [(last shapes)]))) (rx/of (dwc/select-shapes (into (d/ordered-set) [(last shapes)])))
:svg-raw :svg-raw
@ -1987,3 +1989,12 @@
(d/export dwg/unmask-group) (d/export dwg/unmask-group)
(d/export dwg/group-selected) (d/export dwg/group-selected)
(d/export dwg/ungroup-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)

View file

@ -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}))))))

View file

@ -198,7 +198,7 @@
group-id (first selected) group-id (first selected)
group (get objects group-id)] group (get objects group-id)]
(when (and (= 1 (count selected)) (when (and (= 1 (count selected))
(= (:type group) :group)) (contains? #{:group :bool} (:type group)))
(let [[rchanges uchanges] (let [[rchanges uchanges]
(prepare-remove-group page-id group objects)] (prepare-remove-group page-id group objects)]
(rx/of (dch/commit-changes {:redo-changes rchanges (rx/of (dch/commit-changes {:redo-changes rchanges

View file

@ -7,7 +7,10 @@
(ns app.main.data.workspace.path.drawing (ns app.main.data.workspace.path.drawing
(:require (:require
[app.common.geom.point :as gpt] [app.common.geom.point :as gpt]
[app.common.geom.shapes.path :as upg]
[app.common.pages :as cp] [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.common.spec :as us]
[app.main.data.workspace.changes :as dch] [app.main.data.workspace.changes :as dch]
[app.main.data.workspace.common :as dwc] [app.main.data.workspace.common :as dwc]
@ -21,9 +24,6 @@
[app.main.data.workspace.path.undo :as undo] [app.main.data.workspace.path.undo :as undo]
[app.main.data.workspace.state-helpers :as wsh] [app.main.data.workspace.state-helpers :as wsh]
[app.main.streams :as ms] [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] [beicon.core :as rx]
[potok.core :as ptk])) [potok.core :as ptk]))

View file

@ -8,6 +8,10 @@
(:require (:require
[app.common.data :as d] [app.common.data :as d]
[app.common.geom.point :as gpt] [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.changes :as dch]
[app.main.data.workspace.common :as dwc] [app.main.data.workspace.common :as dwc]
[app.main.data.workspace.path.changes :as changes] [app.main.data.workspace.path.changes :as changes]
@ -19,10 +23,6 @@
[app.main.data.workspace.path.undo :as undo] [app.main.data.workspace.path.undo :as undo]
[app.main.data.workspace.state-helpers :as wsh] [app.main.data.workspace.state-helpers :as wsh]
[app.main.streams :as ms] [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] [app.util.path.tools :as upt]
[beicon.core :as rx] [beicon.core :as rx]
[potok.core :as ptk])) [potok.core :as ptk]))

View file

@ -10,10 +10,10 @@
[app.common.geom.point :as gpt] [app.common.geom.point :as gpt]
[app.common.geom.shapes :as gsh] [app.common.geom.shapes :as gsh]
[app.common.math :as mth] [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.data.workspace.path.common :as common]
[app.main.streams :as ms] [app.main.streams :as ms]
[app.util.path.commands :as upc]
[app.util.path.subpaths :as ups]
[potok.core :as ptk])) [potok.core :as ptk]))
(defn end-path-event? [event] (defn end-path-event? [event]

View file

@ -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))))))

View file

@ -7,7 +7,7 @@
(ns app.main.data.workspace.path.state (ns app.main.data.workspace.path.state
(:require (:require
[app.common.data :as d] [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 (defn get-path-id
"Retrieves the currently editing path id" "Retrieves the currently editing path id"
@ -31,7 +31,8 @@
[state & ks] [state & ks]
(let [path-loc (get-path-location state) (let [path-loc (get-path-location state)
shape (-> (get-in state path-loc) 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) (if (empty? ks)
shape shape

View file

@ -7,12 +7,12 @@
(ns app.main.data.workspace.path.streams (ns app.main.data.workspace.path.streams
(:require (:require
[app.common.geom.point :as gpt] [app.common.geom.point :as gpt]
[app.common.geom.shapes.path :as upg]
[app.common.math :as mth] [app.common.math :as mth]
[app.main.data.workspace.path.state :as state] [app.main.data.workspace.path.state :as state]
[app.main.snap :as snap] [app.main.snap :as snap]
[app.main.store :as st] [app.main.store :as st]
[app.main.streams :as ms] [app.main.streams :as ms]
[app.util.path.geom :as upg]
[beicon.core :as rx] [beicon.core :as rx]
[okulary.core :as l] [okulary.core :as l]
[potok.core :as ptk])) [potok.core :as ptk]))

View file

@ -6,13 +6,13 @@
(ns app.main.data.workspace.path.tools (ns app.main.data.workspace.path.tools
(:require (: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.changes :as dch]
[app.main.data.workspace.common :as dwc] [app.main.data.workspace.common :as dwc]
[app.main.data.workspace.path.changes :as changes] [app.main.data.workspace.path.changes :as changes]
[app.main.data.workspace.path.state :as st] [app.main.data.workspace.path.state :as st]
[app.main.data.workspace.state-helpers :as wsh] [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] [app.util.path.tools :as upt]
[beicon.core :as rx] [beicon.core :as rx]
[potok.core :as ptk])) [potok.core :as ptk]))

View file

@ -260,6 +260,23 @@
:command ["alt" "."] :command ["alt" "."]
:type "keyup" :type "keyup"
:fn #(st/emit! (dw/toggle-distances-display false))} :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] (defn get-tooltip [shortcut]

View file

@ -14,6 +14,7 @@
[app.common.math :as mth] [app.common.math :as mth]
[app.common.pages :as cp] [app.common.pages :as cp]
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
[app.main.ui.shapes.bool :as bool]
[app.main.ui.shapes.circle :as circle] [app.main.ui.shapes.circle :as circle]
[app.main.ui.shapes.embed :as embed] [app.main.ui.shapes.embed :as embed]
[app.main.ui.shapes.export :as use] [app.main.ui.shapes.export :as use]
@ -81,6 +82,18 @@
:is-child-selected? true :is-child-selected? true
:childs childs}])))) :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 (defn svg-raw-wrapper-factory
[objects] [objects]
(let [shape-wrapper (shape-wrapper-factory objects) (let [shape-wrapper (shape-wrapper-factory objects)
@ -104,9 +117,10 @@
[objects] [objects]
(mf/fnc shape-wrapper (mf/fnc shape-wrapper
[{:keys [frame shape] :as props}] [{: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)) 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))) (when (and shape (not (:hidden shape)))
(let [shape (-> (gsh/transform-shape shape) (let [shape (-> (gsh/transform-shape shape)
(gsh/translate-to-frame frame)) (gsh/translate-to-frame frame))
@ -122,6 +136,7 @@
:circle [:> circle/circle-shape opts] :circle [:> circle/circle-shape opts]
:frame [:> frame-wrapper {:shape shape}] :frame [:> frame-wrapper {:shape shape}]
:group [:> group-wrapper {:shape shape :frame frame}] :group [:> group-wrapper {:shape shape :frame frame}]
:bool [:> bool-wrapper {:shape shape :frame frame}]
nil)] nil)]
;; Don't wrap svg elements inside a <g> otherwise some can break ;; Don't wrap svg elements inside a <g> otherwise some can break

View file

@ -122,6 +122,10 @@
:show-distances?]) :show-distances?])
workspace-local =)) workspace-local =))
(def local-displacement
(l/derived #(select-keys % [:modifiers :selected])
workspace-local =))
(def selected-zoom (def selected-zoom
(l/derived :zoom workspace-local)) (l/derived :zoom workspace-local))
@ -239,16 +243,36 @@
([ids {:keys [with-modifiers?] ([ids {:keys [with-modifiers?]
:or { with-modifiers? false }}] :or { with-modifiers? false }}]
(l/derived (fn [state] (let [selector
(let [objects (wsh/lookup-page-objects state) (fn [state]
modifiers (:workspace-modifiers state) (let [objects (wsh/lookup-page-objects state)
objects (cond-> objects modifiers (:workspace-modifiers state)
with-modifiers? objects (cond-> objects
(gsh/merge-modifiers modifiers)) with-modifiers?
xform (comp (map #(get objects %)) (gsh/merge-modifiers modifiers))
(remove nil?))] xform (comp (map #(get objects %))
(into [] xform ids))) (remove nil?))]
st/state =))) (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 (def selected-data
(l/derived #(let [selected (wsh/lookup-selected %) (l/derived #(let [selected (wsh/lookup-selected %)

View file

@ -9,6 +9,8 @@
(:require-macros [app.main.ui.icons :refer [icon-xref]]) (:require-macros [app.main.ui.icons :refer [icon-xref]])
(:require [rumext.alpha :as mf])) (:require [rumext.alpha :as mf]))
;; Keep the list of icons sorted
(def action (icon-xref :action)) (def action (icon-xref :action))
(def actions (icon-xref :actions)) (def actions (icon-xref :actions))
(def align-bottom (icon-xref :align-bottom)) (def align-bottom (icon-xref :align-bottom))
@ -23,6 +25,11 @@
(def auto-fix (icon-xref :auto-fix)) (def auto-fix (icon-xref :auto-fix))
(def auto-height (icon-xref :auto-height)) (def auto-height (icon-xref :auto-height))
(def auto-width (icon-xref :auto-width)) (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 box (icon-xref :box))
(def chain (icon-xref :chain)) (def chain (icon-xref :chain))
(def chat (icon-xref :chat)) (def chat (icon-xref :chat))
@ -152,6 +159,7 @@
(def uppercase (icon-xref :uppercase)) (def uppercase (icon-xref :uppercase))
(def user (icon-xref :user)) (def user (icon-xref :user))
(def loader-pencil (def loader-pencil
(mf/html (mf/html
[:svg [:svg

View file

@ -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}]])))

View file

@ -64,6 +64,7 @@
text? (= :text (:type shape)) text? (= :text (:type shape))
path? (= :path (:type shape)) path? (= :path (:type shape))
mask? (and group? (:masked-group? shape)) mask? (and group? (:masked-group? shape))
bool? (= :bool (:type shape))
center (gsh/center-shape shape)] center (gsh/center-shape shape)]
(-> props (-> props
(add! :name) (add! :name)
@ -102,7 +103,10 @@
(add! :content (comp json/encode uuid->string)))) (add! :content (comp json/encode uuid->string))))
(cond-> mask? (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] (defn add-library-refs [props shape]

View file

@ -72,6 +72,7 @@
[:> wrapper-tag wrapper-props [:> wrapper-tag wrapper-props
(when include-metadata? (when include-metadata?
[:& ed/export-data {:shape shape}]) [:& ed/export-data {:shape shape}])
[:defs [:defs
[:& defs/svg-defs {:shape shape :render-id render-id}] [:& defs/svg-defs {:shape shape :render-id render-id}]
[:& filters/filters {:shape shape :filter-id filter-id}] [:& filters/filters {:shape shape :filter-id filter-id}]

View file

@ -8,8 +8,10 @@
"The main container for a frame in handoff mode" "The main container for a frame in handoff mode"
(:require (:require
[app.common.geom.shapes :as geom] [app.common.geom.shapes :as geom]
[app.common.pages :as cp]
[app.main.data.viewer :as dv] [app.main.data.viewer :as dv]
[app.main.store :as st] [app.main.store :as st]
[app.main.ui.shapes.bool :as bool]
[app.main.ui.shapes.circle :as circle] [app.main.ui.shapes.circle :as circle]
[app.main.ui.shapes.frame :as frame] [app.main.ui.shapes.frame :as frame]
[app.main.ui.shapes.group :as group] [app.main.ui.shapes.group :as group]
@ -106,6 +108,22 @@
(obj/merge! #js {:childs childs}))] (obj/merge! #js {:childs childs}))]
[:> group-wrapper props])))) [:> 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 (defn svg-raw-container-factory
[objects] [objects]
(let [shape-container (shape-container-factory objects) (let [shape-container (shape-container-factory objects)
@ -133,12 +151,18 @@
[props] [props]
(let [shape (unchecked-get props "shape") (let [shape (unchecked-get props "shape")
frame (unchecked-get props "frame") frame (unchecked-get props "frame")
group-container (mf/use-memo
(mf/deps objects) group-container
#(group-container-factory objects)) (mf/use-memo (mf/deps objects)
svg-raw-container (mf/use-memo #(group-container-factory objects))
(mf/deps objects)
#(svg-raw-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))) (when (and shape (not (:hidden shape)))
(let [shape (-> (geom/transform-shape shape) (let [shape (-> (geom/transform-shape shape)
(geom/translate-to-frame frame)) (geom/translate-to-frame frame))
@ -151,6 +175,7 @@
:image [:> image-wrapper opts] :image [:> image-wrapper opts]
:circle [:> circle-wrapper opts] :circle [:> circle-wrapper opts]
:group [:> group-container opts] :group [:> group-container opts]
:bool [:> bool-container opts]
:svg-raw [:> svg-raw-container opts]))))))) :svg-raw [:> svg-raw-container opts])))))))
(mf/defc render-frame-svg (mf/defc render-frame-svg

View file

@ -15,6 +15,7 @@
[app.common.types.interactions :as cti] [app.common.types.interactions :as cti]
[app.main.data.viewer :as dv] [app.main.data.viewer :as dv]
[app.main.store :as st] [app.main.store :as st]
[app.main.ui.shapes.bool :as bool]
[app.main.ui.shapes.circle :as circle] [app.main.ui.shapes.circle :as circle]
[app.main.ui.shapes.frame :as frame] [app.main.ui.shapes.frame :as frame]
[app.main.ui.shapes.group :as group] [app.main.ui.shapes.group :as group]
@ -229,6 +230,10 @@
[shape-container show-interactions?] [shape-container show-interactions?]
(generic-wrapper-factory (group/group-shape 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 (defn svg-raw-wrapper
[shape-container show-interactions?] [shape-container show-interactions?]
(generic-wrapper-factory (svg-raw/svg-raw-shape shape-container) show-interactions?)) (generic-wrapper-factory (svg-raw/svg-raw-shape shape-container) show-interactions?))
@ -287,6 +292,21 @@
:show-interactions? show-interactions?})] :show-interactions? show-interactions?})]
[:> group-wrapper props])))) [:> 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 (defn svg-raw-container-factory
[objects show-interactions?] [objects show-interactions?]
(let [shape-container (shape-container-factory objects show-interactions?) (let [shape-container (shape-container-factory objects show-interactions?)
@ -312,12 +332,17 @@
(mf/fnc shape-container (mf/fnc shape-container
{::mf/wrap-props false} {::mf/wrap-props false}
[props] [props]
(let [group-container (mf/use-memo (let [group-container
(mf/deps objects) (mf/use-memo (mf/deps objects)
#(group-container-factory objects show-interactions?)) #(group-container-factory objects show-interactions?))
svg-raw-container (mf/use-memo
(mf/deps objects) bool-container
#(svg-raw-container-factory objects show-interactions?)) (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") shape (unchecked-get props "shape")
frame (unchecked-get props "frame")] frame (unchecked-get props "frame")]
(when (and shape (not (:hidden shape))) (when (and shape (not (:hidden shape)))
@ -333,6 +358,7 @@
:image [:> image-wrapper opts] :image [:> image-wrapper opts]
:circle [:> circle-wrapper opts] :circle [:> circle-wrapper opts]
:group [:> group-container {:shape shape :frame frame :objects objects}] :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}]))))))) :svg-raw [:> svg-raw-container {:shape shape :frame frame :objects objects}])))))))
(mf/defc frame-svg (mf/defc frame-svg

View file

@ -202,10 +202,10 @@
h h
(str (* s 100) "%") (str (* s 100) "%")
(str (* l 100) "%")))] (str (* l 100) "%")))]
(dom/set-css-property node "--color" (str/join ", " rgb)) (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 "--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-from" (format-hsl hsl-from))
(dom/set-css-property node "--saturation-grad-to" (format-hsl hsl-to))))) (dom/set-css-property! node "--saturation-grad-to" (format-hsl hsl-to)))))
;; When closing the modal we update the recent-color list ;; When closing the modal we update the recent-color list
(mf/use-effect (mf/use-effect

View file

@ -16,6 +16,7 @@
[app.main.store :as st] [app.main.store :as st]
[app.main.ui.components.dropdown :refer [dropdown]] [app.main.ui.components.dropdown :refer [dropdown]]
[app.main.ui.context :as ctx] [app.main.ui.context :as ctx]
[app.main.ui.icons :as i]
[app.util.dom :as dom] [app.util.dom :as dom]
[app.util.i18n :refer [tr] :as i18n] [app.util.i18n :refer [tr] :as i18n]
[app.util.timers :as timers] [app.util.timers :as timers]
@ -31,10 +32,52 @@
(dom/stop-propagation event)) (dom/stop-propagation event))
(mf/defc menu-entry (mf/defc menu-entry
[{:keys [title shortcut on-click] :as props}] [{:keys [title shortcut on-click children] :as props}]
[:li {:on-click on-click} (let [submenu-ref (mf/use-ref nil)
[:span.title title] hovering? (mf/use-ref false)
[:span.shortcut (or shortcut "")]])
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 (mf/defc menu-separator
[] []
@ -49,6 +92,21 @@
multiple? (> (count selected) 1) multiple? (> (count selected) 1)
editable-shape? (#{:group :text :path} (:type shape)) 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) current-file-id (mf/use-ctx ctx/current-file-id)
do-duplicate (st/emitf dw/duplicate-selected) do-duplicate (st/emitf dw/duplicate-selected)
@ -98,7 +156,10 @@
:on-accept confirm-update-remote-component})) :on-accept confirm-update-remote-component}))
do-show-component (st/emitf (dw/go-to-layout :assets)) do-show-component (st/emitf (dw/go-to-layout :assets))
do-navigate-component-file (st/emitf (dwl/nav-to-component-file 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") [:& menu-entry {:title (tr "workspace.shape.menu.copy")
:shortcut (sc/get-tooltip :copy) :shortcut (sc/get-tooltip :copy)
@ -147,7 +208,7 @@
:on-click do-flip-horizontal}] :on-click do-flip-horizontal}]
[:& menu-separator]]) [:& menu-separator]])
(when (and single? (= (:type shape) :group)) (when (and single? (or is-bool? is-group?))
[:* [:*
[:& menu-entry {:title (tr "workspace.shape.menu.ungroup") [:& menu-entry {:title (tr "workspace.shape.menu.ungroup")
:shortcut (sc/get-tooltip :ungroup) :shortcut (sc/get-tooltip :ungroup)
@ -165,6 +226,30 @@
:shortcut (sc/get-tooltip :start-editing) :shortcut (sc/get-tooltip :start-editing)
:on-click do-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) (if (:hidden shape)
[:& menu-entry {:title (tr "workspace.shape.menu.show") [:& menu-entry {:title (tr "workspace.shape.menu.show")
:on-click do-show-shape}] :on-click do-show-shape}]
@ -240,7 +325,7 @@
(when dropdown (when dropdown
(let [bounding-rect (dom/get-bounding-rect dropdown) (let [bounding-rect (dom/get-bounding-rect dropdown)
window-size (dom/get-window-size) 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) delta-y (max (- (:bottom bounding-rect) (:height window-size)) 0)
new-style (str "top: " (- top delta-y) "px; " new-style (str "top: " (- top delta-y) "px; "
"left: " (- left delta-x) "px;")] "left: " (- left delta-x) "px;")]

View file

@ -20,6 +20,7 @@
[app.main.ui.shapes.image :as image] [app.main.ui.shapes.image :as image]
[app.main.ui.shapes.rect :as rect] [app.main.ui.shapes.rect :as rect]
[app.main.ui.shapes.text.fontfaces :as ff] [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.bounding-box :refer [bounding-box]]
[app.main.ui.workspace.shapes.common :as common] [app.main.ui.workspace.shapes.common :as common]
[app.main.ui.workspace.shapes.frame :as frame] [app.main.ui.workspace.shapes.frame :as frame]
@ -35,6 +36,7 @@
(declare shape-wrapper) (declare shape-wrapper)
(declare group-wrapper) (declare group-wrapper)
(declare svg-raw-wrapper) (declare svg-raw-wrapper)
(declare bool-wrapper)
(declare frame-wrapper) (declare frame-wrapper)
(def circle-wrapper (common/generic-wrapper-factory circle/circle-shape)) (def circle-wrapper (common/generic-wrapper-factory circle/circle-shape))
@ -92,13 +94,14 @@
[:* [:*
(if-not svg-element? (if-not svg-element?
(case (:type shape) (case (:type shape)
:path [:> path/path-wrapper opts] :path [:> path/path-wrapper opts]
:text [:> text/text-wrapper opts] :text [:> text/text-wrapper opts]
:group [:> group-wrapper opts] :group [:> group-wrapper opts]
:rect [:> rect-wrapper opts] :rect [:> rect-wrapper opts]
:image [:> image-wrapper opts] :image [:> image-wrapper opts]
:circle [:> circle-wrapper opts] :circle [:> circle-wrapper opts]
:svg-raw [:> svg-raw-wrapper opts] :svg-raw [:> svg-raw-wrapper opts]
:bool [:> bool-wrapper opts]
;; Only used when drawing a new frame. ;; Only used when drawing a new frame.
:frame [:> frame-wrapper {:shape shape}] :frame [:> frame-wrapper {:shape shape}]
@ -113,5 +116,6 @@
(def group-wrapper (group/group-wrapper-factory shape-wrapper)) (def group-wrapper (group/group-wrapper-factory shape-wrapper))
(def svg-raw-wrapper (svg-raw/svg-raw-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)) (def frame-wrapper (frame/frame-wrapper-factory shape-wrapper))

View file

@ -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}]]))))

View file

@ -6,11 +6,11 @@
(ns app.main.ui.workspace.shapes.path (ns app.main.ui.workspace.shapes.path
(:require (:require
[app.common.path.commands :as upc]
[app.main.refs :as refs] [app.main.refs :as refs]
[app.main.ui.shapes.path :as path] [app.main.ui.shapes.path :as path]
[app.main.ui.shapes.shape :refer [shape-container]] [app.main.ui.shapes.shape :refer [shape-container]]
[app.main.ui.workspace.shapes.path.common :as pc] [app.main.ui.workspace.shapes.path.common :as pc]
[app.util.path.commands :as upc]
[rumext.alpha :as mf])) [rumext.alpha :as mf]))
(mf/defc path-wrapper (mf/defc path-wrapper

View file

@ -8,7 +8,9 @@
(:require (:require
[app.common.data :as d] [app.common.data :as d]
[app.common.geom.point :as gpt] [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.data.workspace.path :as drp]
[app.main.snap :as snap] [app.main.snap :as snap]
[app.main.store :as st] [app.main.store :as st]
@ -18,10 +20,7 @@
[app.main.ui.workspace.shapes.path.common :as pc] [app.main.ui.workspace.shapes.path.common :as pc]
[app.util.dom :as dom] [app.util.dom :as dom]
[app.util.keyboard :as kbd] [app.util.keyboard :as kbd]
[app.util.path.commands :as upc]
[app.util.path.format :as upf] [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]] [clojure.set :refer [map-invert]]
[goog.events :as events] [goog.events :as events]
[rumext.alpha :as mf]) [rumext.alpha :as mf])
@ -217,16 +216,16 @@
shape (cond-> shape shape (cond-> shape
(not= :path (:type shape)) (not= :path (:type shape))
ups/convert-to-path (ups/convert-to-path {})
:always :always
hooks/use-equal-memo) hooks/use-equal-memo)
base-content (:content shape) 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 (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)) point->base (->> (map hash-map content-points base-points) (reduce merge))
base->point (map-invert point->base) base->point (map-invert point->base)
@ -269,7 +268,7 @@
ms/mouse-position ms/mouse-position
(mf/deps shape zoom) (mf/deps shape zoom)
(fn [position] (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))))) (reset! hover-point (when (< (gpt/distance position point) (/ 10 zoom)) point)))))
[:g.path-editor {:ref editor-ref} [:g.path-editor {:ref editor-ref}

View file

@ -39,6 +39,11 @@
(if (:masked-group? shape) (if (:masked-group? shape)
i/mask i/mask
i/folder)) 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 :svg-raw i/file-svg
nil)) nil))
@ -292,7 +297,8 @@
:shape-ref :shape-ref
:touched :touched
:metadata :metadata
:masked-group?])) :masked-group?
:bool-type]))
(defn- strip-objects (defn- strip-objects
[objects] [objects]

View file

@ -11,10 +11,12 @@
[app.main.store :as st] [app.main.store :as st]
[app.main.ui.components.tab-container :refer [tab-container tab-element]] [app.main.ui.components.tab-container :refer [tab-container tab-element]]
[app.main.ui.context :as ctx] [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.exports :refer [exports-menu]]
[app.main.ui.workspace.sidebar.options.menus.interactions :refer [interactions-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.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.circle :as circle]
[app.main.ui.workspace.sidebar.options.shapes.frame :as frame] [app.main.ui.workspace.sidebar.options.shapes.frame :as frame]
[app.main.ui.workspace.sidebar.options.shapes.group :as group] [app.main.ui.workspace.sidebar.options.shapes.group :as group]
@ -43,6 +45,7 @@
:path [:& path/options {:shape shape}] :path [:& path/options {:shape shape}]
:image [:& image/options {:shape shape}] :image [:& image/options {:shape shape}]
:svg-raw [:& svg-raw/options {:shape shape}] :svg-raw [:& svg-raw/options {:shape shape}]
:bool [:& bool/options {:shape shape}]
nil) nil)
[:& exports-menu [:& exports-menu
{:shape shape {:shape shape
@ -60,6 +63,7 @@
:title (tr "workspace.options.design")} :title (tr "workspace.options.design")}
[:div.element-options [:div.element-options
[:& align-options] [:& align-options]
[:& booleans-options]
(case (count selected) (case (count selected)
0 [:& page/options] 0 [:& page/options]
1 [:& shape-options {:shape (first shapes) 1 [:& shape-options {:shape (first shapes)

View file

@ -4,7 +4,7 @@
;; ;;
;; Copyright (c) UXBOX Labs SL ;; Copyright (c) UXBOX Labs SL
(ns app.main.ui.workspace.sidebar.align (ns app.main.ui.workspace.sidebar.options.menus.align
(:require (:require
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
[app.main.data.workspace :as dw] [app.main.data.workspace :as dw]

View file

@ -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]]]))

View file

@ -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])}]]))

View file

@ -159,8 +159,13 @@
(hooks/setup-shortcuts node-editing? drawing-path?) (hooks/setup-shortcuts node-editing? drawing-path?)
(hooks/setup-active-frames objects vbox hover active-frames) (hooks/setup-active-frames objects vbox hover active-frames)
[:div.viewport [:div.viewport
[:div.viewport-overlays [:div.viewport-overlays
[:& wtr/frame-renderer {:objects objects [:& wtr/frame-renderer {:objects objects
:background background}] :background background}]
@ -196,11 +201,12 @@
[:& use/export-page {:options options}] [:& use/export-page {:options options}]
[:& (mf/provider embed/context) {:value true} [:& (mf/provider use/include-metadata-ctx) {:value true}
;; Render root shape [:& (mf/provider embed/context) {:value true}
[:& shapes/root-shape {:key page-id ;; Render root shape
:objects objects [:& shapes/root-shape {:key page-id
:active-frames @active-frames}]]] :objects objects
:active-frames @active-frames}]]]]
[:svg.viewport-controls [:svg.viewport-controls
{:xmlns "http://www.w3.org/2000/svg" {:xmlns "http://www.w3.org/2000/svg"
@ -229,7 +235,6 @@
:on-pointer-up on-pointer-up} :on-pointer-up on-pointer-up}
[:g {:style {:pointer-events (if disable-events? "none" "auto")}} [:g {:style {:pointer-events (if disable-events? "none" "auto")}}
(when show-outlines? (when show-outlines?
[:& outline/shape-outlines [:& outline/shape-outlines
{:objects objects {:objects objects

View file

@ -92,6 +92,7 @@
(defn setup-hover-shapes [page-id move-stream objects transform selected ctrl? hover hover-ids hover-disabled? zoom] (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 (let [;; We use ref so we don't recreate the stream on a change
zoom-ref (mf/use-ref zoom) zoom-ref (mf/use-ref zoom)
ctrl-ref (mf/use-ref @ctrl?)
transform-ref (mf/use-ref nil) transform-ref (mf/use-ref nil)
selected-ref (mf/use-ref selected) selected-ref (mf/use-ref selected)
hover-disabled-ref (mf/use-ref hover-disabled?) hover-disabled-ref (mf/use-ref hover-disabled?)
@ -101,6 +102,7 @@
(mf/deps page-id) (mf/deps page-id)
(fn [point] (fn [point]
(let [zoom (mf/ref-val zoom-ref) (let [zoom (mf/ref-val zoom-ref)
ctrl? (mf/ref-val ctrl-ref)
rect (gsh/center->rect point (/ 5 zoom) (/ 5 zoom))] rect (gsh/center->rect point (/ 5 zoom) (/ 5 zoom))]
(if (mf/ref-val hover-disabled-ref) (if (mf/ref-val hover-disabled-ref)
(rx/of nil) (rx/of nil)
@ -109,6 +111,7 @@
:page-id page-id :page-id page-id
:rect rect :rect rect
:include-frames? true :include-frames? true
:clip-children? (not ctrl?)
:reverse? true}))))) ;; we want the topmost shape to be selected first :reverse? true}))))) ;; we want the topmost shape to be selected first
over-shapes-stream over-shapes-stream
@ -120,7 +123,6 @@
(rx/switch-map query-point))))] (rx/switch-map query-point))))]
;; Refresh the refs on a value change ;; Refresh the refs on a value change
(mf/use-effect (mf/use-effect
(mf/deps transform) (mf/deps transform)
#(mf/set-ref-val! transform-ref transform)) #(mf/set-ref-val! transform-ref transform))
@ -129,6 +131,10 @@
(mf/deps zoom) (mf/deps zoom)
#(mf/set-ref-val! zoom-ref 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/use-effect
(mf/deps selected) (mf/deps selected)
#(mf/set-ref-val! selected-ref selected)) #(mf/set-ref-val! selected-ref selected))
@ -143,11 +149,15 @@
(fn [ids] (fn [ids]
(let [selected (mf/ref-val selected-ref) (let [selected (mf/ref-val selected-ref)
remove-id? (into #{} (mapcat #(cp/get-parents % objects)) selected) remove-id? (into #{} (mapcat #(cp/get-parents % objects)) selected)
remove-id? (if @ctrl?
(d/concat remove-id? is-group?
(->> ids (fn [id]
(filterv #(= :group (get-in objects [% :type]))))) (contains? #{:group :bool} (get-in objects [id :type])))
remove-id?)
remove-id?
(if @ctrl?
(d/concat remove-id? (filterv is-group? ids))
remove-id?)
ids (->> ids (filterv (comp not remove-id?)))] ids (->> ids (filterv (comp not remove-id?)))]
(reset! hover (get objects (first ids))) (reset! hover (get objects (first ids)))
(reset! hover-ids ids)))))) (reset! hover-ids ids))))))

View file

@ -281,7 +281,7 @@
(defn set-text! [node text] (defn set-text! [node text]
(set! (.-textContent 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)) (.setProperty (.-style ^js node) property value))
(defn capture-pointer [event] (defn capture-pointer [event]

View file

@ -209,6 +209,13 @@
(->> node :content last))] (->> node :content last))]
(merge (add-attrs {} (:attrs svg-node)) node-attrs)) (merge (add-attrs {} (:attrs svg-node)) node-attrs))
(= type :bool)
(->> node
(:content)
(filter #(= :path (:tag %)))
(map #(:attrs %))
(reduce add-attrs node-attrs))
:else :else
node-attrs))) node-attrs)))
@ -443,6 +450,11 @@
mask? mask?
(assoc :masked-group? true)))) (assoc :masked-group? true))))
(defn add-bool-data
[props node]
(-> props
(assoc :bool-type (get-meta node :bool-type keyword))))
(defn parse-shadow [node] (defn parse-shadow [node]
{:id (uuid/next) {:id (uuid/next)
:style (get-meta node :shadow-type keyword) :style (get-meta node :shadow-type keyword)
@ -706,7 +718,10 @@
(add-image-data type node)) (add-image-data type node))
(cond-> (= :text type) (cond-> (= :text type)
(add-text-data node)))))) (add-text-data node))
(cond-> (= :bool type)
(add-bool-data node))))))
(defn parse-page-data (defn parse-page-data
[node] [node]

View file

@ -6,7 +6,8 @@
(ns app.util.path.format (ns app.util.path.format
(:require (:require
[app.util.path.commands :as upc] [app.common.path.commands :as upc]
[app.common.path.subpaths :refer [pt=]]
[cuerdas.core :as str])) [cuerdas.core :as str]))
(defn command->param-list [command] (defn command->param-list [command]
@ -62,6 +63,12 @@
(str command-str param-list))) (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] (defn format-path [content]
(with-out-str (with-out-str
(loop [last-move nil (loop [last-move nil
@ -72,9 +79,12 @@
(let [point (upc/command->point current) (let [point (upc/command->point current)
current-move? (= :move-to (:command current)) current-move? (= :move-to (:command current))
last-move (if current-move? point last-move)] 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")) (print "Z"))
(recur last-move (recur last-move

View file

@ -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 [])))

View file

@ -8,9 +8,9 @@
(:require (:require
[app.common.data :as d] [app.common.data :as d]
[app.common.geom.point :as gpt] [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.arc-to-curve :refer [a2c]]
[app.util.path.commands :as upc]
[app.util.path.geom :as upg]
[app.util.svg :as usvg] [app.util.svg :as usvg]
[cuerdas.core :as str])) [cuerdas.core :as str]))

View file

@ -8,9 +8,9 @@
(:require (:require
[app.common.data :as d] [app.common.data :as d]
[app.common.geom.point :as gpt] [app.common.geom.point :as gpt]
[app.common.geom.shapes.path :as upg]
[app.common.math :as mth] [app.common.math :as mth]
[app.util.path.commands :as upc] [app.common.path.commands :as upc]
[app.util.path.geom :as upg]
[clojure.set :as set])) [clojure.set :as set]))
(defn remove-line-curves (defn remove-line-curves
@ -210,7 +210,7 @@
(case (:command cmd) (case (:command cmd)
:line-to [index (upg/split-line-to start cmd value)] :line-to [index (upg/split-line-to start cmd value)]
:curve-to [index (upg/split-curve-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)) nil))
cmd-changes cmd-changes

View file

@ -202,6 +202,7 @@
(case type (case type
:frame (fb/close-artboard file) :frame (fb/close-artboard file)
:group (fb/close-group file) :group (fb/close-group file)
:bool (fb/close-bool file)
:svg-raw (fb/close-svg-raw file) :svg-raw (fb/close-svg-raw file)
#_default file) #_default file)
@ -218,6 +219,7 @@
file (case type file (case type
:frame (fb/add-artboard file data) :frame (fb/add-artboard file data)
:group (fb/add-group file data) :group (fb/add-group file data)
:bool (fb/add-bool file data)
:rect (fb/create-rect file data) :rect (fb/create-rect file data)
:circle (fb/create-circle file data) :circle (fb/create-circle file data)
:path (fb/create-path file data) :path (fb/create-path file data)

View file

@ -18,13 +18,13 @@
(defonce state (l/atom {})) (defonce state (l/atom {}))
(defn index-shape (defn index-shape
[objects parents-index masks-index] [objects parents-index clip-parents-index]
(fn [index shape] (fn [index shape]
(let [{:keys [x y width height]} (gsh/points->selrect (:points shape)) (let [{:keys [x y width height]} (gsh/points->selrect (:points shape))
shape-bound #js {:x x :y y :width width :height height} shape-bound #js {:x x :y y :width width :height height}
parents (get parents-index (:id shape)) parents (get parents-index (:id shape))
masks (get masks-index (:id shape)) clip-parents (get clip-parents-index (:id shape))
frame (when (and (not= :frame (:type shape)) frame (when (and (not= :frame (:type shape))
(not= (:frame-id shape) uuid/zero)) (not= (:frame-id shape) uuid/zero))
@ -32,19 +32,22 @@
(qdt/insert index (qdt/insert index
(:id shape) (:id shape)
shape-bound shape-bound
(assoc shape :frame frame :masks masks :parents parents))))) (assoc shape
:frame frame
:clip-parents clip-parents
:parents parents)))))
(defn- create-index (defn- create-index
[objects] [objects]
(let [shapes (-> objects (dissoc uuid/zero) (vals)) (let [shapes (-> objects (dissoc uuid/zero) (vals))
parents-index (cp/generate-child-all-parents-index objects) parents-index (cp/generate-child-all-parents-index objects)
masks-index (cp/create-mask-index objects parents-index) clip-parents-index (cp/create-clip-index objects parents-index)
bounds #js {:x (int -0.5e7) bounds #js {:x (int -0.5e7)
:y (int -0.5e7) :y (int -0.5e7)
:width (int 1e7) :width (int 1e7)
:height (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) (qdt/create bounds)
shapes) shapes)
@ -66,13 +69,13 @@
(set/union (set (keys old-objects)) (set/union (set (keys old-objects))
(set (keys new-objects)))) (set (keys new-objects))))
shapes (->> changed-ids (mapv #(get new-objects %)) (filterv (comp not nil?))) shapes (->> changed-ids (mapv #(get new-objects %)) (filterv (comp not nil?)))
parents-index (cp/generate-child-all-parents-index new-objects shapes) parents-index (cp/generate-child-all-parents-index new-objects shapes)
masks-index (cp/create-mask-index new-objects parents-index) clip-parents-index (cp/create-clip-index new-objects parents-index)
new-index (qdt/remove-all index changed-ids) 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 new-index
shapes) shapes)
@ -84,7 +87,7 @@
(create-index new-objects))) (create-index new-objects)))
(defn- query-index (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)) (let [result (-> (qdt/search index (clj->js rect))
(es6-iterator-seq)) (es6-iterator-seq))
@ -96,7 +99,6 @@
(or (not frame-id) (= frame-id (:frame-id shape))) (or (not frame-id) (= frame-id (:frame-id shape)))
(case (:type shape) (case (:type shape)
:frame include-frames? :frame include-frames?
:group include-groups?
true) true)
(or (not full-frame?) (or (not full-frame?)
@ -107,11 +109,9 @@
(fn [shape] (fn [shape]
(gsh/overlaps? shape rect)) (gsh/overlaps? shape rect))
overlaps-masks? overlaps-parent?
(fn [masks] (fn [clip-parents]
(->> masks (->> clip-parents (some (comp not overlaps?)) not))
(some (comp not overlaps?))
not))
add-z-index add-z-index
(fn [{:keys [id frame-id] :as shape}] (fn [{:keys [id frame-id] :as shape}]
@ -125,7 +125,9 @@
(filter match-criteria?) (filter match-criteria?)
(filter overlaps?) (filter overlaps?)
(filter (comp overlaps? :frame)) (filter (comp overlaps? :frame))
(filter (comp overlaps-masks? :masks)) (filter (if clip-children?
(comp overlaps-parent? :clip-parents)
(constantly true)))
(map add-z-index)) (map add-z-index))
result) result)
@ -155,10 +157,10 @@
nil) nil)
(defmethod impl/handler :selection/query (defmethod impl/handler :selection/query
[{:keys [page-id rect frame-id include-frames? full-frame? include-groups? reverse?] [{:keys [page-id rect frame-id reverse? full-frame? include-frames? clip-children?]
:or {include-groups? true reverse? false include-frames? false full-frame? false} :as message}] :or {reverse? false full-frame? false include-frames? false clip-children? true} :as message}]
(when-let [index (get @state page-id)] (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 (defmethod impl/handler :selection/query-z-index
[{:keys [page-id objects ids]}] [{:keys [page-id objects ids]}]

View file

@ -3114,4 +3114,25 @@ msgid "viewer.breaking-change.message"
msgstr "Sorry!" msgstr "Sorry!"
msgid "viewer.breaking-change.description" 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"

View file

@ -3000,3 +3000,24 @@ msgstr "¡Lo sentimos!"
msgid "viewer.breaking-change.description" msgid "viewer.breaking-change.description"
msgstr "Este link compartido ya no funciona. Crea uno nuevo o pídelo a la persona que lo creó." 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"