mirror of
https://github.com/penpot/penpot.git
synced 2025-03-25 14:11:33 -05:00
♻️ Viewport refactor and improvements
This commit is contained in:
parent
5c31830edb
commit
136a48a18f
54 changed files with 2081 additions and 1521 deletions
|
@ -15,6 +15,7 @@
|
|||
- Improve french translations [#731](https://github.com/penpot/penpot/pull/731)
|
||||
- Reimplement workspace presence (remove database state).
|
||||
- Replace Slate-Editor with DraftJS [Taiga #1346](https://tree.taiga.io/project/penpot/us/1346)
|
||||
- Several enhancements in shape selection [Taiga #1195](https://tree.taiga.io/project/penpot/us/1195)
|
||||
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
|
|
@ -407,3 +407,39 @@
|
|||
(or default-value
|
||||
(str maybe-keyword)))))
|
||||
|
||||
(defn with-next
|
||||
"Given a collectin will return a new collection where each element
|
||||
is paried with the next item in the collection
|
||||
(with-next (range 5)) => [[0 1] [1 2] [2 3] [3 4] [4 nil]"
|
||||
[coll]
|
||||
(map vector
|
||||
coll
|
||||
(concat [] (rest coll) [nil])))
|
||||
|
||||
(defn with-prev
|
||||
"Given a collectin will return a new collection where each element
|
||||
is paried with the previous item in the collection
|
||||
(with-prev (range 5)) => [[0 nil] [1 0] [2 1] [3 2] [4 3]"
|
||||
[coll]
|
||||
(map vector
|
||||
coll
|
||||
(concat [nil] coll)))
|
||||
|
||||
(defn with-prev-next
|
||||
"Given a collection will return a new collection where every item is paired
|
||||
with the previous and the next item of a collection
|
||||
(with-prev-next (range 5)) => [[0 nil 1] [1 0 2] [2 1 3] [3 2 4] [4 3 nil]"
|
||||
[coll]
|
||||
(map vector
|
||||
coll
|
||||
(concat [nil] coll)
|
||||
(concat [] (rest coll) [nil])))
|
||||
|
||||
(defn prefix-keyword
|
||||
"Given a keyword and a prefix will return a new keyword with the prefix attached
|
||||
(prefix-keyword \"prefix\" :test) => :prefix-test"
|
||||
[prefix kw]
|
||||
(let [prefix (if (keyword? prefix) (name prefix) prefix)
|
||||
kw (if (keyword? kw) (name kw) kw)]
|
||||
(keyword (str prefix kw))))
|
||||
|
||||
|
|
|
@ -10,12 +10,14 @@
|
|||
(ns app.common.geom.shapes
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.math :as mth]
|
||||
[app.common.geom.matrix :as gmt]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.geom.shapes.common :as gco]
|
||||
[app.common.geom.shapes.path :as gsp]
|
||||
[app.common.geom.shapes.rect :as gpr]
|
||||
[app.common.geom.shapes.transforms :as gtr]
|
||||
[app.common.geom.shapes.intersect :as gin]
|
||||
[app.common.spec :as us]))
|
||||
|
||||
;; --- Relative Movement
|
||||
|
@ -156,29 +158,6 @@
|
|||
|
||||
;; --- Helpers
|
||||
|
||||
(defn contained-in?
|
||||
"Check if a shape is contained in the
|
||||
provided selection rect."
|
||||
[shape selrect]
|
||||
(let [{sx1 :x1 sx2 :x2 sy1 :y1 sy2 :y2} selrect
|
||||
{rx1 :x1 rx2 :x2 ry1 :y1 ry2 :y2} (:selrect shape)]
|
||||
(and (neg? (- sy1 ry1))
|
||||
(neg? (- sx1 rx1))
|
||||
(pos? (- sy2 ry2))
|
||||
(pos? (- sx2 rx2)))))
|
||||
|
||||
;; TODO: This not will work for rotated shapes
|
||||
(defn overlaps?
|
||||
"Check if a shape overlaps with provided selection rect."
|
||||
[shape rect]
|
||||
(let [{sx1 :x1 sx2 :x2 sy1 :y1 sy2 :y2} (gpr/rect->selrect rect)
|
||||
{rx1 :x1 rx2 :x2 ry1 :y1 ry2 :y2} (gpr/points->selrect (:points shape))]
|
||||
|
||||
(and (< rx1 sx2)
|
||||
(> rx2 sx1)
|
||||
(< ry1 sy2)
|
||||
(> ry2 sy1))))
|
||||
|
||||
(defn fully-contained?
|
||||
"Checks if one rect is fully inside the other"
|
||||
[rect other]
|
||||
|
@ -187,20 +166,6 @@
|
|||
(<= (:y1 rect) (:y1 other))
|
||||
(>= (:y2 rect) (:y2 other))))
|
||||
|
||||
(defn has-point?
|
||||
[shape position]
|
||||
(let [{:keys [x y]} position
|
||||
selrect {:x1 (- x 5)
|
||||
:y1 (- y 5)
|
||||
:x2 (+ x 5)
|
||||
:y2 (+ y 5)
|
||||
:x (- x 5)
|
||||
:y (- y 5)
|
||||
:width 10
|
||||
:height 10
|
||||
:type :rect}]
|
||||
(overlaps? shape selrect)))
|
||||
|
||||
(defn pad-selrec
|
||||
([selrect] (pad-selrec selrect 1))
|
||||
([selrect size]
|
||||
|
@ -287,3 +252,7 @@
|
|||
(d/export gsp/content->points)
|
||||
(d/export gsp/content->selrect)
|
||||
(d/export gsp/transform-content)
|
||||
|
||||
;; Intersection
|
||||
(d/export gin/overlaps?)
|
||||
(d/export gin/has-point?)
|
||||
|
|
296
common/app/common/geom/shapes/intersect.cljc
Normal file
296
common/app/common/geom/shapes/intersect.cljc
Normal file
|
@ -0,0 +1,296 @@
|
|||
;; 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/.
|
||||
;;
|
||||
;; This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
;; defined by the Mozilla Public License, v. 2.0.
|
||||
;;
|
||||
;; Copyright (c) 2020 UXBOX Labs SL
|
||||
|
||||
(ns app.common.geom.shapes.intersect
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.geom.matrix :as gmt]
|
||||
[app.common.geom.shapes.path :as gpp]
|
||||
[app.common.geom.shapes.rect :as gpr]
|
||||
[app.common.math :as mth]))
|
||||
|
||||
(defn orientation
|
||||
"Given three ordered points gives the orientation
|
||||
(clockwise, counterclock or coplanar-line)"
|
||||
[p1 p2 p3]
|
||||
(let [{x1 :x y1 :y} p1
|
||||
{x2 :x y2 :y} p2
|
||||
{x3 :x y3 :y} p3
|
||||
v (- (* (- y2 y1) (- x3 x2))
|
||||
(* (- y3 y2) (- x2 x1)))]
|
||||
(cond
|
||||
(pos? v) ::clockwise
|
||||
(neg? v) ::counter-clockwise
|
||||
:else ::coplanar)))
|
||||
|
||||
(defn on-segment?
|
||||
"Given three colinear points p, q, r checks if q lies on segment pr"
|
||||
[{qx :x qy :y} {px :x py :y} {rx :x ry :y}]
|
||||
(and (<= qx (max px rx))
|
||||
(>= qx (min px rx))
|
||||
(<= qy (max py ry))
|
||||
(>= qy (min py ry))))
|
||||
|
||||
;; Based on solution described here
|
||||
;; https://www.geeksforgeeks.org/check-if-two-given-line-segments-intersect/
|
||||
(defn intersect-segments?
|
||||
"Given two segments A<pa1,pa2> and B<pb1,pb2> defined by two points.
|
||||
Checks if they intersects."
|
||||
[[p1 q1] [p2 q2]]
|
||||
(let [o1 (orientation p1 q1 p2)
|
||||
o2 (orientation p1 q1 q2)
|
||||
o3 (orientation p2 q2 p1)
|
||||
o4 (orientation p2 q2 q1)]
|
||||
|
||||
(or
|
||||
;; General case
|
||||
(and (not= o1 o2) (not= o3 o4))
|
||||
|
||||
;; p1, q1 and p2 colinear and p2 lies on p1q1
|
||||
(and (= o1 :coplanar) (on-segment? p2 p1 q1))
|
||||
|
||||
;; p1, q1 and q2 colinear and q2 lies on p1q1
|
||||
(and (= o2 :coplanar) (on-segment? q2 p1 q1))
|
||||
|
||||
;; p2, q2 and p1 colinear and p1 lies on p2q2
|
||||
(and (= o3 :coplanar) (on-segment? p1 p2 q2))
|
||||
|
||||
;; p2, q2 and p1 colinear and q1 lies on p2q2
|
||||
(and (= o4 :coplanar) (on-segment? q1 p2 q2)))))
|
||||
|
||||
(defn points->lines
|
||||
"Given a set of points for a polygon will return
|
||||
the lines that define it"
|
||||
([points]
|
||||
(points->lines points true))
|
||||
|
||||
([points closed?]
|
||||
(map vector
|
||||
points
|
||||
(-> (rest points)
|
||||
(vec)
|
||||
(cond-> closed?
|
||||
(conj (first points)))))))
|
||||
|
||||
(defn intersects-lines?
|
||||
"Checks if two sets of lines intersect in any point"
|
||||
[lines-a lines-b]
|
||||
|
||||
(loop [cur-line (first lines-a)
|
||||
pending (rest lines-a)]
|
||||
(if-not cur-line
|
||||
;; There is no line intersecting polygon b
|
||||
false
|
||||
|
||||
;; Check if any of the segments intersect
|
||||
(if (->> lines-b
|
||||
(some #(intersect-segments? cur-line %)))
|
||||
true
|
||||
(recur (first pending) (rest pending))))))
|
||||
|
||||
(defn intersect-ray?
|
||||
"Checks the intersection between segment qr and a ray
|
||||
starting in point p with an angle of 0 degrees"
|
||||
[{px :x py :y} [{x1 :x y1 :y} {x2 :x y2 :y}]]
|
||||
|
||||
(if (or (and (<= y1 py) (> y2 py))
|
||||
(and (> y1 py) (<= y2 py)))
|
||||
|
||||
;; calculate the edge-ray intersect x-coord
|
||||
(let [vt (/ (- py y1) (- y2 y1))
|
||||
ix (+ x1 (* vt (- x2 x1)))]
|
||||
(< px ix))
|
||||
|
||||
false))
|
||||
|
||||
(defn is-point-inside-evenodd?
|
||||
"Check if the point P is inside the polygon defined by `points`"
|
||||
[p lines]
|
||||
;; Even-odd algorithm
|
||||
;; Cast a ray from the point in any direction and count the intersections
|
||||
;; if it's odd the point is inside the polygon
|
||||
(let []
|
||||
(->> lines
|
||||
(filter #(intersect-ray? p %))
|
||||
(count)
|
||||
(odd?))))
|
||||
|
||||
(defn- next-windup
|
||||
"Calculates the next windup number for the nonzero algorithm"
|
||||
[wn {px :x py :y} [{x1 :x y1 :y} {x2 :x y2 :y}]]
|
||||
|
||||
(let [line-side (- (* (- x2 x1) (- py y1))
|
||||
(* (- px x1) (- y2 y1)))]
|
||||
(if (<= y1 py)
|
||||
;; Upward crossing
|
||||
(if (and (> y2 py) (> line-side 0)) (inc wn) wn)
|
||||
|
||||
;; Downward crossing
|
||||
(if (and (<= y2 py) (< line-side 0)) (dec wn) wn))))
|
||||
|
||||
(defn is-point-inside-nonzero?
|
||||
"Check if the point P is inside the polygon defined by `points`"
|
||||
[p lines]
|
||||
;; Non-zero winding number
|
||||
;; Calculates the winding number
|
||||
|
||||
(loop [wn 0
|
||||
line (first lines)
|
||||
lines (rest lines)]
|
||||
|
||||
(if line
|
||||
(let [wn (next-windup wn p line)]
|
||||
(recur wn (first lines) (rest lines)))
|
||||
(not= wn 0))))
|
||||
|
||||
;; A intersects with B
|
||||
;; Three posible cases:
|
||||
;; 1) A is inside of B
|
||||
;; 2) B is inside of A
|
||||
;; 3) A intersects B
|
||||
;; 4) A is outside of B
|
||||
;;
|
||||
;; . check point in A is inside B => case 1 or 3 otherwise discard 1
|
||||
;; . check point in B is inside A => case 2 or 3 otherwise discard 2
|
||||
;; . check if intersection otherwise case 4
|
||||
;;
|
||||
(defn overlaps-rect-points?
|
||||
"Checks if the given rect intersects with the selrect"
|
||||
[rect points]
|
||||
|
||||
(let [rect-points (gpr/rect->points rect)
|
||||
rect-lines (points->lines rect-points)
|
||||
points-lines (points->lines points)]
|
||||
|
||||
(or (is-point-inside-evenodd? (first rect-points) points-lines)
|
||||
(is-point-inside-evenodd? (first points) rect-lines)
|
||||
(intersects-lines? rect-lines points-lines))))
|
||||
|
||||
(defn overlaps-path?
|
||||
"Checks if the given rect overlaps with the path in any point"
|
||||
[shape rect]
|
||||
|
||||
(let [rect-points (gpr/rect->points rect)
|
||||
rect-lines (points->lines rect-points)
|
||||
path-lines (gpp/path->lines shape)
|
||||
start-point (-> shape :content (first) :params (gpt/point))]
|
||||
|
||||
(or (is-point-inside-nonzero? (first rect-points) path-lines)
|
||||
(is-point-inside-nonzero? start-point rect-lines)
|
||||
(intersects-lines? rect-lines path-lines))))
|
||||
|
||||
(defn is-point-inside-ellipse?
|
||||
"checks if a point is inside an ellipse"
|
||||
[point {:keys [cx cy rx ry transform]}]
|
||||
|
||||
(let [center (gpt/point cx cy)
|
||||
transform (gmt/transform-in center transform)
|
||||
{px :x py :y} (gpt/transform point transform)]
|
||||
;; Ellipse inequality formula
|
||||
;; https://en.wikipedia.org/wiki/Ellipse#Shifted_ellipse
|
||||
(let [v (+ (/ (mth/sq (- px cx))
|
||||
(mth/sq rx))
|
||||
(/ (mth/sq (- py cy))
|
||||
(mth/sq ry)))]
|
||||
(<= v 1))))
|
||||
|
||||
(defn intersects-line-ellipse?
|
||||
"Checks wether a single line intersects with the given ellipse"
|
||||
[[{x1 :x y1 :y} {x2 :x y2 :y}] {:keys [cx cy rx ry]}]
|
||||
|
||||
;; Given the ellipse inequality after inserting the line parametric equations
|
||||
;; we resolve t and gives us a cuadratic formula
|
||||
;; The result of this quadratic will give us a value of T that needs to be
|
||||
;; between 0-1 to be in the segment
|
||||
|
||||
(let [a (+ (/ (mth/sq (- x2 x1))
|
||||
(mth/sq rx))
|
||||
(/ (mth/sq (- y2 y1))
|
||||
(mth/sq ry)))
|
||||
|
||||
b (+ (/ (- (* 2 x1 (- x2 x1))
|
||||
(* 2 cx (- x2 x1)))
|
||||
(mth/sq rx))
|
||||
(/ (- (* 2 y1 (- y2 y1))
|
||||
(* 2 cy (- y2 y1)))
|
||||
(mth/sq ry)))
|
||||
|
||||
c (+ (/ (+ (mth/sq x1)
|
||||
(mth/sq cx)
|
||||
(* -2 x1 cx))
|
||||
(mth/sq rx))
|
||||
(/ (+ (mth/sq y1)
|
||||
(mth/sq cy)
|
||||
(* -2 y1 cy))
|
||||
(mth/sq ry))
|
||||
-1)
|
||||
|
||||
;; B^2 - 4AC
|
||||
determ (- (mth/sq b) (* 4 a c))]
|
||||
|
||||
(if (mth/almost-zero? a)
|
||||
;; If a=0 we need to calculate the linear solution
|
||||
(when-not (mth/almost-zero? b)
|
||||
(let [t (/ (- c) b)]
|
||||
(and (>= t 0) (<= t 1))))
|
||||
|
||||
(when (>= determ 0)
|
||||
(let [t1 (/ (+ (- b) (mth/sqrt determ)) (* 2 a))
|
||||
t2 (/ (- (- b) (mth/sqrt determ)) (* 2 a))]
|
||||
(or (and (>= t1 0) (<= t1 1))
|
||||
(and (>= t2 0) (<= t2 1))))))))
|
||||
|
||||
(defn intersects-lines-ellipse?
|
||||
"Checks if a set of lines intersect with an ellipse in any point"
|
||||
[rect-lines {:keys [cx cy transform] :as ellipse-data}]
|
||||
(let [center (gpt/point cx cy)
|
||||
transform (gmt/transform-in center transform)]
|
||||
(some (fn [[p1 p2]]
|
||||
(let [p1 (gpt/transform p1 transform)
|
||||
p2 (gpt/transform p2 transform)]
|
||||
(intersects-line-ellipse? [p1 p2] ellipse-data))) rect-lines)))
|
||||
|
||||
(defn overlaps-ellipse?
|
||||
"Checks if the given rect overlaps with an ellipse"
|
||||
[shape rect]
|
||||
|
||||
(let [rect-points (gpr/rect->points rect)
|
||||
rect-lines (points->lines rect-points)
|
||||
{:keys [x y width height]} shape
|
||||
|
||||
center (gpt/point (+ x (/ width 2))
|
||||
(+ y (/ height 2)))
|
||||
|
||||
ellipse-data {:cx (:x center)
|
||||
:cy (:y center)
|
||||
:rx (/ width 2)
|
||||
:ry (/ height 2)
|
||||
:transform (:transform-inverse shape)}]
|
||||
|
||||
(or (is-point-inside-evenodd? center rect-lines)
|
||||
(is-point-inside-ellipse? (first rect-points) ellipse-data)
|
||||
(intersects-lines-ellipse? rect-lines ellipse-data))))
|
||||
|
||||
(defn overlaps?
|
||||
"General case to check for overlaping between shapes and a rectangle"
|
||||
[shape rect]
|
||||
(or (not shape)
|
||||
(let [path? (= :path (:type shape))
|
||||
circle? (= :circle (:type shape))]
|
||||
(and (overlaps-rect-points? rect (:points shape))
|
||||
(or (not path?) (overlaps-path? shape rect))
|
||||
(or (not circle?) (overlaps-ellipse? shape rect))))))
|
||||
|
||||
(defn has-point?
|
||||
"Check if the shape contains a point"
|
||||
[shape point]
|
||||
(let [lines (points->lines (:points shape))]
|
||||
;; TODO: Will only work for simple shapes
|
||||
(is-point-inside-evenodd? point lines)))
|
|
@ -161,3 +161,56 @@
|
|||
|
||||
(when closed?
|
||||
[{:command :close-path}])))))
|
||||
|
||||
(defonce num-segments 10)
|
||||
|
||||
(defn curve->lines
|
||||
"Transform the bezier curve given by the parameters into a series of straight lines
|
||||
defined by the constant num-segments"
|
||||
[start end h1 h2]
|
||||
(let [offset (/ 1 num-segments)
|
||||
tp (fn [t] (curve-values start end h1 h2 t))]
|
||||
(loop [from 0
|
||||
result []]
|
||||
|
||||
(let [to (min 1 (+ from offset))
|
||||
line [(tp from) (tp to)]
|
||||
result (conj result line)]
|
||||
|
||||
(if (>= to 1)
|
||||
result
|
||||
(recur to result))))))
|
||||
|
||||
(defn path->lines
|
||||
"Given a path returns a list of lines that approximate the path"
|
||||
[shape]
|
||||
(loop [command (first (:content shape))
|
||||
pending (rest (:content shape))
|
||||
result []
|
||||
last-start nil
|
||||
prev-point nil]
|
||||
|
||||
(if-let [{:keys [command params]} command]
|
||||
(let [point (if (= :close-path command)
|
||||
last-start
|
||||
(gpt/point params))
|
||||
|
||||
result (case command
|
||||
:line-to (conj result [prev-point point])
|
||||
:curve-to (let [h1 (gpt/point (:c1x params) (:c1y params))
|
||||
h2 (gpt/point (:c2x params) (:c2y params))]
|
||||
(into result (curve->lines prev-point point h1 h2)))
|
||||
:move-to (cond-> result
|
||||
last-start (conj [prev-point last-start]))
|
||||
result)
|
||||
last-start (if (= :move-to command)
|
||||
point
|
||||
last-start)
|
||||
]
|
||||
(recur (first pending)
|
||||
(rest pending)
|
||||
result
|
||||
last-start
|
||||
point))
|
||||
|
||||
(conj result [prev-point last-start]))))
|
||||
|
|
|
@ -70,6 +70,11 @@
|
|||
[v]
|
||||
(- v))
|
||||
|
||||
(defn sq
|
||||
"Calculates the square of a number"
|
||||
[v]
|
||||
(* v v))
|
||||
|
||||
(defn sqrt
|
||||
"Returns the square root of a number."
|
||||
[v]
|
||||
|
|
|
@ -63,6 +63,8 @@
|
|||
(d/export helpers/get-base-shape)
|
||||
(d/export helpers/is-parent?)
|
||||
(d/export helpers/get-index-in-parent)
|
||||
(d/export helpers/calculate-z-index)
|
||||
(d/export helpers/generate-child-all-parents-index)
|
||||
|
||||
;; Process changes
|
||||
(d/export changes/process-changes)
|
||||
|
|
|
@ -169,6 +169,21 @@
|
|||
(assoc index id (:parent-id obj)))
|
||||
{} objects))
|
||||
|
||||
(defn generate-child-all-parents-index
|
||||
"Creates an index where the key is the shape id and the value is a set
|
||||
with all the parents"
|
||||
([objects]
|
||||
(generate-child-all-parents-index objects (vals objects)))
|
||||
|
||||
([objects shapes]
|
||||
(let [shape->parents
|
||||
(fn [shape]
|
||||
(->> (get-parents (:id shape) objects)
|
||||
(into [])))]
|
||||
(->> shapes
|
||||
(map #(vector (:id %) (shape->parents %)))
|
||||
(into {})))))
|
||||
|
||||
(defn clean-loops
|
||||
"Clean a list of ids from circular references."
|
||||
[objects ids]
|
||||
|
@ -333,6 +348,41 @@
|
|||
(reduce red-fn cur-idx (reverse (:shapes object)))))]
|
||||
(into {} (rec-index '() uuid/zero))))
|
||||
|
||||
(defn calculate-z-index
|
||||
"Given a collection of shapes calculates their z-index. Greater index
|
||||
means is displayed over other shapes with less index."
|
||||
[objects]
|
||||
|
||||
(let [is-frame? (fn [id] (= :frame (get-in objects [id :type])))
|
||||
root-children (get-in objects [uuid/zero :shapes])
|
||||
num-frames (->> root-children (filter is-frame?) count)]
|
||||
(when (seq root-children)
|
||||
(loop [current (peek root-children)
|
||||
pending (pop root-children)
|
||||
current-idx (+ (count objects) num-frames -1)
|
||||
z-index {}]
|
||||
|
||||
(let [children (->> (get-in objects [current :shapes]))
|
||||
children (cond
|
||||
(and (is-frame? current) (contains? z-index current))
|
||||
[]
|
||||
|
||||
(and (is-frame? current)
|
||||
(not (contains? z-index current)))
|
||||
(into [current] children)
|
||||
|
||||
:else
|
||||
children)
|
||||
pending (into (vec pending) children)]
|
||||
(if (empty? pending)
|
||||
(assoc z-index current current-idx)
|
||||
|
||||
(let []
|
||||
(recur (peek pending)
|
||||
(pop pending)
|
||||
(dec current-idx)
|
||||
(assoc z-index current current-idx)))))))))
|
||||
|
||||
(defn expand-region-selection
|
||||
"Given a selection selects all the shapes between the first and last in
|
||||
an indexed manner (shift selection)"
|
||||
|
|
|
@ -349,6 +349,7 @@
|
|||
z-index: 1000;
|
||||
pointer-events: none;
|
||||
overflow: hidden;
|
||||
user-select: text;
|
||||
|
||||
.threads {
|
||||
position: absolute;
|
||||
|
|
|
@ -104,6 +104,7 @@
|
|||
white-space: nowrap;
|
||||
padding-bottom: 2px;
|
||||
transition: bottom 0.5s;
|
||||
z-index: 2;
|
||||
|
||||
&.color-palette-open {
|
||||
bottom: 5rem;
|
||||
|
@ -135,27 +136,52 @@
|
|||
display: grid;
|
||||
grid-template-rows: 20px 1fr;
|
||||
grid-template-columns: 20px 1fr;
|
||||
}
|
||||
|
||||
.viewport {
|
||||
cursor: none;
|
||||
grid-column: 1 / span 2;
|
||||
grid-row: 1 / span 2;
|
||||
overflow: hidden;
|
||||
.viewport {
|
||||
cursor: none;
|
||||
grid-column: 1 / span 2;
|
||||
grid-row: 1 / span 2;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
|
||||
rect.selection-rect {
|
||||
fill: rgba(235, 215, 92, 0.1);
|
||||
stroke: #000000;
|
||||
stroke-width: 0.1px;
|
||||
}
|
||||
.viewport-overlays {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 10;
|
||||
pointer-events: none;
|
||||
cursor: initial;
|
||||
|
||||
g.controls {
|
||||
rect.main { pointer-events: none; }
|
||||
.pixel-overlay {
|
||||
height: 100%;
|
||||
left: 0;
|
||||
pointer-events: initial;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.page-canvas, .page-layout {
|
||||
overflow: visible;
|
||||
.selection-rect {
|
||||
fill: rgba(235, 215, 92, 0.1);
|
||||
stroke: #000000;
|
||||
stroke-width: 0.1px;
|
||||
}
|
||||
|
||||
.render-shapes {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.viewport-controls {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.page-canvas, .page-layout {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
/* Rules */
|
||||
|
@ -231,14 +257,16 @@
|
|||
}
|
||||
|
||||
.viewport-actions {
|
||||
position: absolute;
|
||||
margin-left: auto;
|
||||
width: 100%;
|
||||
margin-top: 2rem;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-left: auto;
|
||||
margin-top: 2rem;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
z-index: 12;
|
||||
pointer-events: initial;
|
||||
|
||||
.path-actions {
|
||||
display: flex;
|
||||
|
@ -315,3 +343,4 @@
|
|||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -439,13 +439,33 @@
|
|||
(assoc :left-offset left-offset))))))))))))
|
||||
|
||||
|
||||
(defn start-pan [state]
|
||||
(-> state
|
||||
(assoc-in [:workspace-local :panning] true)))
|
||||
(defn start-panning []
|
||||
(ptk/reify ::start-panning
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(-> state
|
||||
(assoc-in [:workspace-local :panning] true)))
|
||||
|
||||
(defn finish-pan [state]
|
||||
(-> state
|
||||
(update :workspace-local dissoc :panning)))
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(let [stopper (->> stream (rx/filter (ptk/type? ::finish-panning)))
|
||||
zoom (-> (get-in state [:workspace-local :zoom]) gpt/point)]
|
||||
(->> stream
|
||||
(rx/filter ms/pointer-event?)
|
||||
(rx/filter #(= :delta (:source %)))
|
||||
(rx/map :pt)
|
||||
(rx/take-until stopper)
|
||||
(rx/map (fn [delta]
|
||||
(let [delta (gpt/divide delta zoom)]
|
||||
(update-viewport-position {:x #(- % (:x delta))
|
||||
:y #(- % (:y delta))})))))))))
|
||||
|
||||
(defn finish-panning []
|
||||
(ptk/reify ::finish-panning
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(-> state
|
||||
(update :workspace-local dissoc :panning)))))
|
||||
|
||||
|
||||
;; --- Toggle layout flag
|
||||
|
@ -981,15 +1001,12 @@
|
|||
{:keys [id type shapes]} (get objects (first selected))]
|
||||
|
||||
(case type
|
||||
:text
|
||||
(:text :path)
|
||||
(rx/of (dwc/start-edition-mode id))
|
||||
|
||||
:group
|
||||
(rx/of (dwc/select-shapes (into (d/ordered-set) [(last shapes)])))
|
||||
|
||||
:path
|
||||
(rx/of (dwc/start-edition-mode id)
|
||||
(dwdp/start-path-edit id))
|
||||
:else (rx/empty))))))))
|
||||
|
||||
|
||||
|
@ -1255,8 +1272,7 @@
|
|||
(let [selected (get-in state [:workspace-local :selected])]
|
||||
(rx/concat
|
||||
(when-not (selected (:id shape))
|
||||
(rx/of (dws/deselect-all)
|
||||
(dws/select-shape (:id shape))))
|
||||
(rx/of (dws/select-shape (:id shape))))
|
||||
(rx/of (show-context-menu params)))))))
|
||||
|
||||
(def hide-context-menu
|
||||
|
@ -1734,6 +1750,7 @@
|
|||
;; Selection
|
||||
|
||||
(d/export dws/select-shape)
|
||||
(d/export dws/deselect-shape)
|
||||
(d/export dws/select-all)
|
||||
(d/export dws/deselect-all)
|
||||
(d/export dwc/select-shapes)
|
||||
|
@ -1741,12 +1758,12 @@
|
|||
(d/export dws/duplicate-selected)
|
||||
(d/export dws/handle-selection)
|
||||
(d/export dws/select-inside-group)
|
||||
(d/export dws/select-last-layer)
|
||||
;;(d/export dws/select-last-layer)
|
||||
(d/export dwd/select-for-drawing)
|
||||
(d/export dwc/clear-edition-mode)
|
||||
(d/export dwc/add-shape)
|
||||
(d/export dwc/start-edition-mode)
|
||||
(d/export dwdp/start-path-edit)
|
||||
#_(d/export dwc/start-path-edit)
|
||||
|
||||
;; Groups
|
||||
|
||||
|
|
|
@ -518,6 +518,31 @@
|
|||
(rx/of (expand-all-parents ids objects))))))
|
||||
|
||||
;; --- Start shape "edition mode"
|
||||
(defn stop-path-edit []
|
||||
(ptk/reify ::stop-path-edit
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(let [id (get-in state [:workspace-local :edition])]
|
||||
(update state :workspace-local dissoc :edit-path id)))))
|
||||
|
||||
(defn start-path-edit
|
||||
[id]
|
||||
(ptk/reify ::start-path-edit
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
;; Only edit if the object has been created
|
||||
(if-let [id (get-in state [:workspace-local :edition])]
|
||||
(assoc-in state [:workspace-local :edit-path id] {:edit-mode :move
|
||||
:selected #{}
|
||||
:snap-toggled true})
|
||||
state))
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(->> stream
|
||||
(rx/filter #(= % :interrupt))
|
||||
(rx/take 1)
|
||||
(rx/map #(stop-path-edit))))))
|
||||
|
||||
(declare clear-edition-mode)
|
||||
|
||||
|
@ -527,8 +552,7 @@
|
|||
(ptk/reify ::start-edition-mode
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(let [page-id (:current-page-id state)
|
||||
objects (get-in state [:workspace-data :pages-index page-id :objects])]
|
||||
(let [objects (lookup-page-objects state)]
|
||||
;; Can only edit objects that exist
|
||||
(if (contains? objects id)
|
||||
(-> state
|
||||
|
@ -538,10 +562,15 @@
|
|||
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(->> stream
|
||||
(rx/filter interrupt?)
|
||||
(rx/take 1)
|
||||
(rx/map (constantly clear-edition-mode))))))
|
||||
(let [objects (lookup-page-objects state)
|
||||
path? (= :path (get-in objects [id :type]))]
|
||||
(rx/merge
|
||||
(when path?
|
||||
(rx/of (start-path-edit id)))
|
||||
(->> stream
|
||||
(rx/filter interrupt?)
|
||||
(rx/take 1)
|
||||
(rx/map (constantly clear-edition-mode))))))))
|
||||
|
||||
(def clear-edition-mode
|
||||
(ptk/reify ::clear-edition-mode
|
||||
|
|
|
@ -9,23 +9,22 @@
|
|||
|
||||
(ns app.main.data.workspace.drawing.path
|
||||
(:require
|
||||
[clojure.spec.alpha :as s]
|
||||
[app.common.spec :as us]
|
||||
[beicon.core :as rx]
|
||||
[potok.core :as ptk]
|
||||
[app.common.math :as mth]
|
||||
[app.common.data :as d]
|
||||
[app.common.geom.matrix :as gmt]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.geom.shapes :as gsh]
|
||||
[app.common.geom.matrix :as gmt]
|
||||
[app.util.data :as ud]
|
||||
[app.common.data :as cd]
|
||||
[app.util.geom.path :as ugp]
|
||||
[app.main.streams :as ms]
|
||||
[app.main.store :as st]
|
||||
[app.common.geom.shapes.path :as gsp]
|
||||
[app.common.math :as mth]
|
||||
[app.common.pages :as cp]
|
||||
[app.common.spec :as us]
|
||||
[app.main.data.workspace.common :as dwc]
|
||||
[app.main.data.workspace.drawing.common :as common]
|
||||
[app.common.geom.shapes.path :as gsp]
|
||||
[app.common.pages :as cp]))
|
||||
[app.main.store :as st]
|
||||
[app.main.streams :as ms]
|
||||
[app.util.geom.path :as ugp]
|
||||
[beicon.core :as rx]
|
||||
[clojure.spec.alpha :as s]
|
||||
[potok.core :as ptk]))
|
||||
|
||||
;; SCHEMAS
|
||||
|
||||
|
@ -83,7 +82,7 @@
|
|||
[state & path]
|
||||
(let [edit-id (get-in state [:workspace-local :edition])
|
||||
page-id (:current-page-id state)]
|
||||
(cd/concat
|
||||
(d/concat
|
||||
(if edit-id
|
||||
[:workspace-data :pages-index page-id :objects edit-id]
|
||||
[:workspace-drawing :object])
|
||||
|
@ -515,31 +514,7 @@
|
|||
mousedown-events)
|
||||
(rx/of (finish-path "after-events")))))))
|
||||
|
||||
(defn stop-path-edit []
|
||||
(ptk/reify ::stop-path-edit
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(let [id (get-in state [:workspace-local :edition])]
|
||||
(update state :workspace-local dissoc :edit-path id)))))
|
||||
|
||||
(defn start-path-edit
|
||||
[id]
|
||||
(ptk/reify ::start-path-edit
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
;; Only edit if the object has been created
|
||||
(if-let [id (get-in state [:workspace-local :edition])]
|
||||
(assoc-in state [:workspace-local :edit-path id] {:edit-mode :move
|
||||
:selected #{}
|
||||
:snap-toggled true})
|
||||
state))
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(->> stream
|
||||
(rx/filter #(= % :interrupt))
|
||||
(rx/take 1)
|
||||
(rx/map #(stop-path-edit))))))
|
||||
|
||||
(defn modify-point [index prefix dx dy]
|
||||
(ptk/reify ::modify-point
|
||||
|
@ -635,7 +610,7 @@
|
|||
(let [point (ugp/command->point command)]
|
||||
(= point start-point)))
|
||||
|
||||
point-indices (->> (cd/enumerate content)
|
||||
point-indices (->> (d/enumerate content)
|
||||
(filter command-for-point)
|
||||
(map first))
|
||||
|
||||
|
@ -646,8 +621,8 @@
|
|||
(assoc-in [index :y] dy)))
|
||||
|
||||
handler-reducer (fn [modifiers [index prefix]]
|
||||
(let [cx (ud/prefix-keyword prefix :x)
|
||||
cy (ud/prefix-keyword prefix :y)]
|
||||
(let [cx (d/prefix-keyword prefix :x)
|
||||
cy (d/prefix-keyword prefix :y)]
|
||||
(-> modifiers
|
||||
(assoc-in [index cx] dx)
|
||||
(assoc-in [index cy] dy))))
|
||||
|
@ -680,8 +655,8 @@
|
|||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(let [id (get-in state [:workspace-local :edition])
|
||||
cx (ud/prefix-keyword prefix :x)
|
||||
cy (ud/prefix-keyword prefix :y)
|
||||
cx (d/prefix-keyword prefix :x)
|
||||
cy (d/prefix-keyword prefix :y)
|
||||
start-point @ms/mouse-position
|
||||
modifiers (get-in state [:workspace-local :edit-path id :content-modifiers])
|
||||
start-delta-x (get-in modifiers [index cx] 0)
|
||||
|
@ -838,7 +813,6 @@
|
|||
(->> (rx/of (setup-frame-path)
|
||||
common/handle-finish-drawing
|
||||
(dwc/start-edition-mode shape-id)
|
||||
(start-path-edit shape-id)
|
||||
(change-edit-mode :draw))))))
|
||||
|
||||
(defn handle-new-shape
|
||||
|
|
|
@ -85,7 +85,9 @@
|
|||
;; --- Toggle shape's selection status (selected or deselected)
|
||||
|
||||
(defn select-shape
|
||||
([id] (select-shape id false))
|
||||
([id]
|
||||
(select-shape id false))
|
||||
|
||||
([id toggle?]
|
||||
(us/verify ::us/uuid id)
|
||||
(ptk/reify ::select-shape
|
||||
|
@ -94,7 +96,7 @@
|
|||
(update-in state [:workspace-local :selected]
|
||||
(fn [selected]
|
||||
(if-not toggle?
|
||||
(conj selected id)
|
||||
(conj (d/ordered-set) id)
|
||||
(if (contains? selected id)
|
||||
(disj selected id)
|
||||
(conj selected id))))))
|
||||
|
@ -137,8 +139,7 @@
|
|||
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(let [page-id (:current-page-id state)
|
||||
objects (dwc/lookup-page-objects state page-id)]
|
||||
(let [objects (dwc/lookup-page-objects state)]
|
||||
(rx/of (dwc/expand-all-parents ids objects))))))
|
||||
|
||||
(defn select-all
|
||||
|
@ -207,22 +208,21 @@
|
|||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(let [page-id (:current-page-id state)
|
||||
objects (dwc/lookup-page-objects state)
|
||||
selected (get-in state [:workspace-local :selected])
|
||||
initial-set (if preserve?
|
||||
selected
|
||||
lks/empty-linked-set)
|
||||
selrect (get-in state [:workspace-local :selrect])
|
||||
is-not-blocked (fn [shape-id] (not (get-in state [:workspace-data
|
||||
:pages-index page-id
|
||||
:objects shape-id
|
||||
:blocked] false)))]
|
||||
blocked? (fn [id] (get-in objects [id :blocked] false))]
|
||||
(rx/merge
|
||||
(rx/of (update-selrect nil))
|
||||
(when selrect
|
||||
(->> (uw/ask! {:cmd :selection/query
|
||||
:page-id page-id
|
||||
:rect selrect})
|
||||
(rx/map #(into initial-set (filter is-not-blocked) %))
|
||||
(rx/map #(cp/clean-loops objects %))
|
||||
(rx/map #(into initial-set (filter (comp not blocked?)) %))
|
||||
(rx/map select-shapes))))))))
|
||||
|
||||
(defn select-inside-group
|
||||
|
@ -243,34 +243,8 @@
|
|||
reverse
|
||||
(d/seek #(geom/has-point? % position)))]
|
||||
(when selected
|
||||
(rx/of (deselect-all) (select-shape (:id selected)))))))))
|
||||
(rx/of (select-shape (:id selected)))))))))
|
||||
|
||||
(defn select-last-layer
|
||||
([position]
|
||||
(ptk/reify ::select-last-layer
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(let [page-id (:current-page-id state)
|
||||
objects (dwc/lookup-page-objects state page-id)
|
||||
find-shape
|
||||
(fn [selection]
|
||||
(let [id (first selection)
|
||||
shape (get objects id)]
|
||||
(let [child-id (->> (cp/get-children id objects)
|
||||
(map #(get objects %))
|
||||
(remove (comp empty :shapes))
|
||||
(filter #(geom/has-point? % position))
|
||||
(first)
|
||||
:id)]
|
||||
(or child-id id))))]
|
||||
(->> (uw/ask! {:cmd :selection/query
|
||||
:page-id page-id
|
||||
:rect (geom/make-centered-rect position 1 1)})
|
||||
(rx/first)
|
||||
(rx/filter (comp not empty?))
|
||||
(rx/map find-shape)
|
||||
(rx/filter #(not (nil? %)))
|
||||
(rx/map #(select-shape % false))))))))
|
||||
|
||||
;; --- Duplicate Shapes
|
||||
(declare prepare-duplicate-change)
|
||||
|
|
|
@ -197,6 +197,8 @@
|
|||
|
||||
(declare start-move)
|
||||
(declare start-move-duplicate)
|
||||
(declare start-local-displacement)
|
||||
(declare clear-local-transform)
|
||||
|
||||
(defn start-move-selected
|
||||
[]
|
||||
|
@ -297,13 +299,11 @@
|
|||
(->> snap-delta
|
||||
(rx/with-latest vector position)
|
||||
(rx/map (fn [[delta pos]] (-> (gpt/add pos delta) (gpt/round 0))))
|
||||
(rx/map gmt/translate-matrix)
|
||||
(rx/map #(fn [state] (assoc-in state [:workspace-local :modifiers] {:displacement %}))))
|
||||
(rx/map start-local-displacement))
|
||||
|
||||
(rx/of (set-modifiers ids)
|
||||
(apply-modifiers ids)
|
||||
(calculate-frame-for-move ids)
|
||||
(fn [state] (update state :workspace-local dissoc :modifiers))
|
||||
finish-transform))))))))
|
||||
|
||||
(defn- get-displacement-with-grid
|
||||
|
@ -368,15 +368,11 @@
|
|||
(->> move-events
|
||||
(rx/take-until stopper)
|
||||
(rx/scan #(gpt/add %1 mov-vec) (gpt/point 0 0))
|
||||
(rx/map gmt/translate-matrix)
|
||||
(rx/map #(fn [state] (assoc-in state [:workspace-local :modifiers] {:displacement %}))))
|
||||
(rx/map start-local-displacement))
|
||||
(rx/of (move-selected direction shift?)))
|
||||
|
||||
(rx/of (set-modifiers selected)
|
||||
(apply-modifiers selected)
|
||||
(fn [state] (-> state
|
||||
(update :workspace-local dissoc :modifiers)
|
||||
(update :workspace-local dissoc :current-move-selected)))
|
||||
finish-transform)))
|
||||
(rx/empty))))))
|
||||
|
||||
|
@ -486,6 +482,7 @@
|
|||
|
||||
(rx/of (dwc/start-undo-transaction)
|
||||
(dwc/commit-changes rchanges uchanges {:commit-local? true})
|
||||
(clear-local-transform)
|
||||
(dwc/commit-undo-transaction))))))
|
||||
|
||||
;; --- Update Dimensions
|
||||
|
@ -564,3 +561,18 @@
|
|||
:displacement (gmt/translate-matrix (gpt/point 0 (- (:height selrect))))}
|
||||
false)
|
||||
(apply-modifiers selected))))))
|
||||
|
||||
(defn start-local-displacement [point]
|
||||
(ptk/reify ::start-local-displacement
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(let [mtx (gmt/translate-matrix point)]
|
||||
(-> state
|
||||
(assoc-in [:workspace-local :modifiers] {:displacement mtx}))))))
|
||||
|
||||
(defn clear-local-transform []
|
||||
(ptk/reify ::clear-local-transform
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(-> state
|
||||
(update :workspace-local dissoc :modifiers :current-move-selected)))))
|
||||
|
|
|
@ -35,6 +35,9 @@
|
|||
(def exception
|
||||
(l/derived :exception st/state))
|
||||
|
||||
(def threads-ref
|
||||
(l/derived :comment-threads st/state))
|
||||
|
||||
;; ---- Dashboard refs
|
||||
|
||||
(def dashboard-local
|
||||
|
|
|
@ -184,6 +184,7 @@
|
|||
(->> (uw/ask! {:cmd :selection/query
|
||||
:page-id page-id
|
||||
:frame-id (->> shapes first :frame-id)
|
||||
:include-frames? true
|
||||
:rect area-selrect})
|
||||
(rx/map #(set/difference % (into #{} (map :id shapes))))
|
||||
(rx/map (fn [ids] (map #(get objects %) ids)))))
|
||||
|
|
|
@ -14,7 +14,6 @@
|
|||
(def embed-ctx (mf/create-context false))
|
||||
(def render-ctx (mf/create-context nil))
|
||||
(def def-ctx (mf/create-context false))
|
||||
(def ghost-ctx (mf/create-context false))
|
||||
|
||||
(def current-route (mf/create-context nil))
|
||||
(def current-team-id (mf/create-context nil))
|
||||
|
|
|
@ -212,10 +212,14 @@
|
|||
|
||||
(defn use-stream
|
||||
"Wraps the subscription to a strem into a `use-effect` call"
|
||||
[stream on-subscribe]
|
||||
(mf/use-effect (fn []
|
||||
(let [sub (->> stream (rx/subs on-subscribe))]
|
||||
#(rx/dispose! sub)))))
|
||||
([stream on-subscribe]
|
||||
(use-stream stream (mf/deps) on-subscribe))
|
||||
([stream deps on-subscribe]
|
||||
(mf/use-effect
|
||||
deps
|
||||
(fn []
|
||||
(let [sub (->> stream (rx/subs on-subscribe))]
|
||||
#(rx/dispose! sub))))))
|
||||
|
||||
;; https://reactjs.org/docs/hooks-faq.html#how-to-get-the-previous-props-or-state
|
||||
(defn use-previous
|
||||
|
|
|
@ -29,7 +29,8 @@
|
|||
[app.main.ui.workspace.libraries]
|
||||
[app.main.ui.workspace.rules :refer [horizontal-rule vertical-rule]]
|
||||
[app.main.ui.workspace.sidebar :refer [left-sidebar right-sidebar]]
|
||||
[app.main.ui.workspace.viewport :refer [viewport viewport-actions coordinates]]
|
||||
[app.main.ui.workspace.viewport :refer [viewport]]
|
||||
[app.main.ui.workspace.coordinates :as coordinates]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.keyboard :as kbd]
|
||||
[app.util.object :as obj]
|
||||
|
@ -57,7 +58,7 @@
|
|||
[:& vertical-rule {:zoom zoom
|
||||
:vbox vbox
|
||||
:vport vport}]
|
||||
[:& coordinates {:colorpalette? colorpalette?}]]))
|
||||
[:& coordinates/coordinates {:colorpalette? colorpalette?}]]))
|
||||
|
||||
(mf/defc workspace-content
|
||||
{::mf/wrap-props false}
|
||||
|
@ -80,7 +81,6 @@
|
|||
:vport vport
|
||||
:colorpalette? (contains? layout :colorpalette)}])
|
||||
|
||||
[:& viewport-actions]
|
||||
[:& viewport {:file file
|
||||
:local local
|
||||
:layout layout}]]]
|
||||
|
|
|
@ -9,88 +9,20 @@
|
|||
|
||||
(ns app.main.ui.workspace.comments
|
||||
(:require
|
||||
[app.config :as cfg]
|
||||
[app.main.data.comments :as dcm]
|
||||
[app.main.data.workspace :as dw]
|
||||
[app.main.data.workspace.comments :as dwcm]
|
||||
[app.main.data.comments :as dcm]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.context :as ctx]
|
||||
[app.main.ui.components.dropdown :refer [dropdown]]
|
||||
[app.main.ui.icons :as i]
|
||||
[app.main.ui.comments :as cmt]
|
||||
[app.util.time :as dt]
|
||||
[app.util.timers :as tm]
|
||||
[app.main.ui.components.dropdown :refer [dropdown]]
|
||||
[app.main.ui.context :as ctx]
|
||||
[app.main.ui.icons :as i]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :as i18n :refer [t tr]]
|
||||
[cuerdas.core :as str]
|
||||
[okulary.core :as l]
|
||||
[app.util.timers :as tm]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
(def threads-ref
|
||||
(l/derived :comment-threads st/state))
|
||||
|
||||
(mf/defc comments-layer
|
||||
[{:keys [vbox vport zoom file-id page-id drawing] :as props}]
|
||||
(let [pos-x (* (- (:x vbox)) zoom)
|
||||
pos-y (* (- (:y vbox)) zoom)
|
||||
|
||||
profile (mf/deref refs/profile)
|
||||
users (mf/deref refs/users)
|
||||
local (mf/deref refs/comments-local)
|
||||
threads-map (mf/deref threads-ref)
|
||||
|
||||
threads (->> (vals threads-map)
|
||||
(filter #(= (:page-id %) page-id))
|
||||
(dcm/apply-filters local profile))
|
||||
|
||||
on-bubble-click
|
||||
(fn [{:keys [id] :as thread}]
|
||||
(if (= (:open local) id)
|
||||
(st/emit! (dcm/close-thread))
|
||||
(st/emit! (dcm/open-thread thread))))
|
||||
|
||||
on-draft-cancel
|
||||
(mf/use-callback
|
||||
(st/emitf :interrupt))
|
||||
|
||||
on-draft-submit
|
||||
(mf/use-callback
|
||||
(fn [draft]
|
||||
(st/emit! (dcm/create-thread draft))))]
|
||||
|
||||
(mf/use-effect
|
||||
(mf/deps file-id)
|
||||
(fn []
|
||||
(st/emit! (dwcm/initialize-comments file-id))
|
||||
(fn []
|
||||
(st/emit! ::dwcm/finalize))))
|
||||
|
||||
[:div.comments-section
|
||||
[:div.workspace-comments-container
|
||||
{:style {:width (str (:width vport) "px")
|
||||
:height (str (:height vport) "px")}}
|
||||
[:div.threads {:style {:transform (str/format "translate(%spx, %spx)" pos-x pos-y)}}
|
||||
(for [item threads]
|
||||
[:& cmt/thread-bubble {:thread item
|
||||
:zoom zoom
|
||||
:on-click on-bubble-click
|
||||
:open? (= (:id item) (:open local))
|
||||
:key (:seqn item)}])
|
||||
|
||||
(when-let [id (:open local)]
|
||||
(when-let [thread (get threads-map id)]
|
||||
[:& cmt/thread-comments {:thread thread
|
||||
:users users
|
||||
:zoom zoom}]))
|
||||
|
||||
(when-let [draft (:comment drawing)]
|
||||
[:& cmt/draft-thread {:draft draft
|
||||
:on-cancel on-draft-cancel
|
||||
:on-submit on-draft-submit
|
||||
:zoom zoom}])]]]))
|
||||
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Sidebar
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
@ -130,7 +62,7 @@
|
|||
|
||||
(mf/defc comments-sidebar
|
||||
[]
|
||||
(let [threads-map (mf/deref threads-ref)
|
||||
(let [threads-map (mf/deref refs/threads-ref)
|
||||
profile (mf/deref refs/profile)
|
||||
users (mf/deref refs/users)
|
||||
local (mf/deref refs/comments-local)
|
||||
|
@ -184,5 +116,3 @@
|
|||
[:div.thread-groups-placeholder
|
||||
i/chat
|
||||
(tr "labels.no-comments-available")])]))
|
||||
|
||||
|
||||
|
|
23
frontend/src/app/main/ui/workspace/coordinates.cljs
Normal file
23
frontend/src/app/main/ui/workspace/coordinates.cljs
Normal file
|
@ -0,0 +1,23 @@
|
|||
; 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/.
|
||||
;;
|
||||
;; This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
;; defined by the Mozilla Public License, v. 2.0.
|
||||
;;
|
||||
;; Copyright (c) 2020-2021 UXBOX Labs SL
|
||||
|
||||
(ns app.main.ui.workspace.coordinates
|
||||
(:require
|
||||
[app.main.ui.hooks :as hooks]
|
||||
[app.main.streams :as ms]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
(mf/defc coordinates
|
||||
[{:keys [colorpalette?]}]
|
||||
(let [coords (hooks/use-rxsub ms/mouse-position)]
|
||||
[:ul.coordinates {:class (when colorpalette? "color-palette-open")}
|
||||
[:span {:alt "x"}
|
||||
(str "X: " (:x coords "-"))]
|
||||
[:span {:alt "y"}
|
||||
(str "Y: " (:y coords "-"))]]))
|
|
@ -13,74 +13,8 @@
|
|||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
[app.util.router :as rt]
|
||||
[app.util.time :as dt]
|
||||
[app.util.timers :as ts]
|
||||
[beicon.core :as rx]
|
||||
[cuerdas.core :as str]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
(def pointer-icon-path
|
||||
(str "M5.292 4.027L1.524.26l-.05-.01L0 0l.258 1.524 3.769 3.768zm-.45 "
|
||||
"0l-.313.314L1.139.95l.314-.314zm-.5.5l-.315.316-3.39-3.39.315-.315 "
|
||||
"3.39 3.39zM1.192.526l-.668.667L.431.646.64.43l.552.094z"))
|
||||
|
||||
(mf/defc session-cursor
|
||||
[{:keys [session profile] :as props}]
|
||||
(let [zoom (mf/deref refs/selected-zoom)
|
||||
point (:point session)
|
||||
color (:color session "#000000")
|
||||
transform (str/fmt "translate(%s, %s) scale(%s)" (:x point) (:y point) (/ 4 zoom))]
|
||||
[:g.multiuser-cursor {:transform transform}
|
||||
[:path {:fill color
|
||||
:d pointer-icon-path
|
||||
}]
|
||||
[:g {:transform "translate(0 -291.708)"}
|
||||
[:rect {:width 25
|
||||
:height 5
|
||||
:x 7
|
||||
:y 291.5
|
||||
:fill color
|
||||
:fill-opacity 0.8
|
||||
:paint-order "stroke fill markers"
|
||||
:rx 1
|
||||
:ry 1}]
|
||||
[:text {:x 8
|
||||
:y 295
|
||||
:width 25
|
||||
:height 5
|
||||
:overflow "hidden"
|
||||
:fill "#fff"
|
||||
:stroke-width 1
|
||||
:font-family "Works Sans"
|
||||
:font-size 3
|
||||
:font-weight 400
|
||||
:letter-spacing 0
|
||||
:style { :line-height 1.25 }
|
||||
:word-spacing 0}
|
||||
(str (str/slice (:fullname profile) 0 14)
|
||||
(when (> (count (:fullname profile)) 14) "..."))]]]))
|
||||
|
||||
(mf/defc active-cursors
|
||||
{::mf/wrap [mf/memo]}
|
||||
[{:keys [page-id] :as props}]
|
||||
(let [counter (mf/use-state 0)
|
||||
users (mf/deref refs/users)
|
||||
sessions (mf/deref refs/workspace-presence)
|
||||
sessions (->> (vals sessions)
|
||||
(filter #(= page-id (:page-id %)))
|
||||
(filter #(>= 5000 (- (inst-ms (dt/now)) (inst-ms (:updated-at %))))))]
|
||||
(mf/use-effect
|
||||
nil
|
||||
(fn []
|
||||
(let [sem (ts/schedule 1000 #(swap! counter inc))]
|
||||
(fn [] (rx/dispose! sem)))))
|
||||
|
||||
(for [session sessions]
|
||||
(when (:point session)
|
||||
[:& session-cursor {:session session
|
||||
:profile (get users (:profile-id session))
|
||||
:key (:id session)}]))))
|
||||
|
||||
;; --- SESSION WIDGET
|
||||
|
||||
(mf/defc session-widget
|
||||
|
|
|
@ -16,12 +16,8 @@
|
|||
common."
|
||||
(:require
|
||||
[app.common.geom.shapes :as geom]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
[app.main.streams :as ms]
|
||||
[app.main.ui.cursors :as cur]
|
||||
[app.main.ui.hooks :as hooks]
|
||||
[app.main.ui.context :as muc]
|
||||
[app.main.ui.shapes.circle :as circle]
|
||||
[app.main.ui.shapes.image :as image]
|
||||
[app.main.ui.shapes.rect :as rect]
|
||||
|
@ -29,15 +25,15 @@
|
|||
[app.main.ui.workspace.shapes.common :as common]
|
||||
[app.main.ui.workspace.shapes.frame :as frame]
|
||||
[app.main.ui.workspace.shapes.group :as group]
|
||||
[app.main.ui.workspace.shapes.svg-raw :as svg-raw]
|
||||
[app.main.ui.workspace.shapes.path :as path]
|
||||
[app.main.ui.workspace.shapes.svg-raw :as svg-raw]
|
||||
[app.main.ui.workspace.shapes.text :as text]
|
||||
[app.util.object :as obj]
|
||||
[app.util.debug :refer [debug?]]
|
||||
[beicon.core :as rx]
|
||||
[app.util.object :as obj]
|
||||
[okulary.core :as l]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
(declare shape-wrapper)
|
||||
(declare group-wrapper)
|
||||
(declare svg-raw-wrapper)
|
||||
(declare frame-wrapper)
|
||||
|
@ -54,28 +50,41 @@
|
|||
(contains? (:selected local) id)))]
|
||||
(l/derived check-moving refs/workspace-local))))
|
||||
|
||||
(mf/defc root-shape
|
||||
"Draws the root shape of the viewport and recursively all the shapes"
|
||||
{::mf/wrap-props false}
|
||||
[props]
|
||||
(let [objects (obj/get props "objects")
|
||||
root-shapes (get-in objects [uuid/zero :shapes])
|
||||
shapes (->> root-shapes (mapv #(get objects %)))]
|
||||
|
||||
(for [item shapes]
|
||||
(if (= (:type item) :frame)
|
||||
[:& frame-wrapper {:shape item
|
||||
:key (:id item)
|
||||
:objects objects}]
|
||||
|
||||
[:& shape-wrapper {:shape item
|
||||
:key (:id item)}]))))
|
||||
|
||||
(mf/defc shape-wrapper
|
||||
{::mf/wrap [#(mf/memo' % (mf/check-props ["shape" "frame"]))]
|
||||
::mf/wrap-props false}
|
||||
[props]
|
||||
(let [shape (obj/get props "shape")
|
||||
frame (obj/get props "frame")
|
||||
ghost? (mf/use-ctx muc/ghost-ctx)
|
||||
shape (-> (geom/transform-shape shape)
|
||||
(geom/translate-to-frame frame))
|
||||
opts #js {:shape shape
|
||||
:frame frame}
|
||||
|
||||
moving-iref (mf/use-memo (mf/deps (:id shape)) (make-is-moving-ref (:id shape)))
|
||||
moving? (mf/deref moving-iref)
|
||||
svg-element? (and (= (:type shape) :svg-raw)
|
||||
(not= :svg (get-in shape [:content :tag])))
|
||||
hide-moving? (and (not ghost?) moving?)]
|
||||
(not= :svg (get-in shape [:content :tag])))]
|
||||
|
||||
(when (and shape (not (:hidden shape)))
|
||||
[:*
|
||||
(if-not svg-element?
|
||||
[:g.shape-wrapper {:style {:display (when hide-moving? "none")}}
|
||||
[:g.shape-wrapper
|
||||
(case (:type shape)
|
||||
:path [:> path/path-wrapper opts]
|
||||
:text [:> text/text-wrapper opts]
|
||||
|
|
|
@ -10,7 +10,6 @@
|
|||
(ns app.main.ui.workspace.shapes.common
|
||||
(:require
|
||||
[app.main.ui.shapes.shape :refer [shape-container]]
|
||||
[app.main.ui.workspace.effects :as we]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
(defn generic-wrapper-factory
|
||||
|
@ -19,10 +18,5 @@
|
|||
{::mf/wrap-props false}
|
||||
[props]
|
||||
(let [shape (unchecked-get props "shape")]
|
||||
[:> shape-container {:shape shape
|
||||
:on-mouse-down (we/use-mouse-down shape)
|
||||
:on-double-click (we/use-double-click shape)
|
||||
:on-context-menu (we/use-context-menu shape)
|
||||
:on-pointer-over (we/use-pointer-enter shape)
|
||||
:on-pointer-out (we/use-pointer-leave shape)}
|
||||
[:> shape-container {:shape shape}
|
||||
[:& component {:shape shape}]])))
|
||||
|
|
|
@ -17,7 +17,6 @@
|
|||
[app.main.ui.context :as muc]
|
||||
[app.main.ui.shapes.frame :as frame]
|
||||
[app.main.ui.shapes.shape :refer [shape-container]]
|
||||
[app.main.ui.workspace.effects :as we]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.keyboard :as kbd]
|
||||
[app.util.timers :as ts]
|
||||
|
@ -25,56 +24,6 @@
|
|||
[okulary.core :as l]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
(defn use-select-shape [{:keys [id]} edition]
|
||||
(mf/use-callback
|
||||
(mf/deps id edition)
|
||||
(fn [event]
|
||||
(when (not edition)
|
||||
(let [selected @refs/selected-shapes
|
||||
selected? (contains? selected id)
|
||||
shift? (kbd/shift? event)]
|
||||
(cond
|
||||
(and selected? shift?)
|
||||
(st/emit! (dw/select-shape id true))
|
||||
|
||||
(and (not (empty? selected)) (not shift?))
|
||||
(st/emit! (dw/deselect-all) (dw/select-shape id))
|
||||
|
||||
(not selected?)
|
||||
(st/emit! (dw/select-shape id))))))))
|
||||
|
||||
;; Ensure that the label has always the same font
|
||||
;; size, regardless of zoom
|
||||
;; https://css-tricks.com/transforms-on-svg-elements/
|
||||
(defn text-transform
|
||||
[{:keys [x y]} zoom]
|
||||
(let [inv-zoom (/ 1 zoom)]
|
||||
(str
|
||||
"scale(" inv-zoom ", " inv-zoom ") "
|
||||
"translate(" (* zoom x) ", " (* zoom y) ")")))
|
||||
|
||||
(mf/defc frame-title
|
||||
[{:keys [frame]}]
|
||||
(let [{:keys [width x y]} frame
|
||||
zoom (mf/deref refs/selected-zoom)
|
||||
edition (mf/deref refs/selected-edition)
|
||||
label-pos (gpt/point x (- y (/ 10 zoom)))
|
||||
handle-click (use-select-shape frame edition)
|
||||
handle-mouse-down (we/use-mouse-down frame)
|
||||
handle-pointer-enter (we/use-pointer-enter frame)
|
||||
handle-pointer-leave (we/use-pointer-leave frame)]
|
||||
[:text {:x 0
|
||||
:y 0
|
||||
:width width
|
||||
:height 20
|
||||
:class "workspace-frame-label"
|
||||
:transform (text-transform label-pos zoom)
|
||||
:on-click handle-click
|
||||
:on-mouse-down handle-mouse-down
|
||||
:on-pointer-over handle-pointer-enter
|
||||
:on-pointer-out handle-pointer-leave}
|
||||
(:name frame)]))
|
||||
|
||||
(defn make-is-moving-ref
|
||||
[id]
|
||||
(let [check-moving (fn [local]
|
||||
|
@ -89,17 +38,14 @@
|
|||
(mf/fnc deferred
|
||||
{::mf/wrap-props false}
|
||||
[props]
|
||||
(let [ghost? (mf/use-ctx muc/ghost-ctx)
|
||||
tmp (mf/useState false)
|
||||
(let [tmp (mf/useState false)
|
||||
^boolean render? (aget tmp 0)
|
||||
^js set-render (aget tmp 1)]
|
||||
(mf/use-layout-effect
|
||||
(fn []
|
||||
(let [sem (ts/schedule-on-idle #(set-render true))]
|
||||
#(rx/dispose! sem))))
|
||||
(if ghost?
|
||||
(mf/create-element component props)
|
||||
(when render? (mf/create-element component props))))))
|
||||
(when render? (mf/create-element component props)))))
|
||||
|
||||
(defn frame-wrapper-factory
|
||||
[shape-wrapper]
|
||||
|
@ -108,38 +54,16 @@
|
|||
{::mf/wrap [#(mf/memo' % (mf/check-props ["shape" "objects"])) custom-deferred]
|
||||
::mf/wrap-props false}
|
||||
[props]
|
||||
(let [shape (unchecked-get props "shape")
|
||||
objects (unchecked-get props "objects")
|
||||
ghost? (mf/use-ctx muc/ghost-ctx)
|
||||
edition (mf/deref refs/selected-edition)
|
||||
(let [shape (unchecked-get props "shape")
|
||||
objects (unchecked-get props "objects")
|
||||
edition (mf/deref refs/selected-edition)
|
||||
|
||||
moving-iref (mf/use-memo (mf/deps (:id shape))
|
||||
#(make-is-moving-ref (:id shape)))
|
||||
moving? (mf/deref moving-iref)
|
||||
|
||||
selected-iref (mf/use-memo (mf/deps (:id shape))
|
||||
#(refs/make-selected-ref (:id shape)))
|
||||
selected? (mf/deref selected-iref)
|
||||
|
||||
shape (gsh/transform-shape shape)
|
||||
children (mapv #(get objects %) (:shapes shape))
|
||||
ds-modifier (get-in shape [:modifiers :displacement])
|
||||
|
||||
handle-context-menu (we/use-context-menu shape)
|
||||
handle-double-click (use-select-shape shape edition)
|
||||
handle-mouse-down (we/use-mouse-down shape)
|
||||
|
||||
hide-moving? (and (not ghost?) moving?)]
|
||||
shape (gsh/transform-shape shape)
|
||||
children (mapv #(get objects %) (:shapes shape))
|
||||
ds-modifier (get-in shape [:modifiers :displacement])]
|
||||
|
||||
(when (and shape (not (:hidden shape)))
|
||||
[:g.frame-wrapper {:class (when selected? "selected")
|
||||
:style {:display (when hide-moving? "none")}
|
||||
:on-context-menu handle-context-menu
|
||||
:on-double-click handle-double-click
|
||||
:on-mouse-down handle-mouse-down}
|
||||
|
||||
[:& frame-title {:frame shape}]
|
||||
|
||||
[:g.frame-wrapper {:display (when (:hidden shape) "none")}
|
||||
[:> shape-container {:shape shape}
|
||||
[:& frame-shape
|
||||
{:shape shape
|
||||
|
|
|
@ -17,7 +17,6 @@
|
|||
[app.main.ui.hooks :as hooks]
|
||||
[app.main.ui.shapes.group :as group]
|
||||
[app.main.ui.shapes.shape :refer [shape-container]]
|
||||
[app.main.ui.workspace.effects :as we]
|
||||
[app.util.debug :refer [debug?]]
|
||||
[app.util.dom :as dom]
|
||||
[rumext.alpha :as mf]))
|
||||
|
@ -42,58 +41,13 @@
|
|||
|
||||
{:keys [id x y width height]} shape
|
||||
|
||||
transform (gsh/transform-matrix shape)
|
||||
|
||||
ctrl? (mf/use-state false)
|
||||
childs-ref (mf/use-memo (mf/deps shape) #(refs/objects-by-id (:shapes shape)))
|
||||
childs (mf/deref childs-ref)
|
||||
|
||||
is-child-selected-ref
|
||||
(mf/use-memo (mf/deps (:id shape)) #(refs/is-child-selected? (:id shape)))
|
||||
|
||||
is-child-selected?
|
||||
(mf/deref is-child-selected-ref)
|
||||
|
||||
mask-id (when (:masked-group? shape) (first (:shapes shape)))
|
||||
|
||||
is-mask-selected-ref
|
||||
(mf/use-memo (mf/deps mask-id) #(refs/make-selected-ref mask-id))
|
||||
|
||||
is-mask-selected?
|
||||
(mf/deref is-mask-selected-ref)
|
||||
|
||||
expand-mask? is-child-selected?
|
||||
group-interactions? (not (or @ctrl? is-child-selected?))
|
||||
|
||||
handle-mouse-down (we/use-mouse-down shape)
|
||||
handle-context-menu (we/use-context-menu shape)
|
||||
handle-pointer-enter (we/use-pointer-enter shape)
|
||||
handle-pointer-leave (we/use-pointer-leave shape)
|
||||
handle-double-click (use-double-click shape)]
|
||||
|
||||
(hooks/use-stream ms/keyboard-ctrl #(reset! ctrl? %))
|
||||
childs (mf/deref childs-ref)]
|
||||
|
||||
[:> shape-container {:shape shape}
|
||||
[:g.group-shape
|
||||
[:& group-shape
|
||||
{:frame frame
|
||||
:shape shape
|
||||
:childs childs
|
||||
:expand-mask expand-mask?
|
||||
:pointer-events (when group-interactions? "none")}]
|
||||
|
||||
[:rect.group-actions
|
||||
{:x x
|
||||
:y y
|
||||
:width width
|
||||
:height height
|
||||
:transform transform
|
||||
:style {:pointer-events (when-not group-interactions? "none")
|
||||
:fill (if (debug? :group) "red" "transparent")
|
||||
:opacity 0.5}
|
||||
:on-mouse-down handle-mouse-down
|
||||
:on-context-menu handle-context-menu
|
||||
:on-pointer-over handle-pointer-enter
|
||||
:on-pointer-out handle-pointer-leave
|
||||
:on-double-click handle-double-click}]]]))))
|
||||
:childs childs}]]]))))
|
||||
|
||||
|
|
|
@ -14,21 +14,11 @@
|
|||
[app.main.store :as st]
|
||||
[app.main.ui.shapes.path :as path]
|
||||
[app.main.ui.shapes.shape :refer [shape-container]]
|
||||
[app.main.ui.workspace.effects :as we]
|
||||
[app.main.ui.workspace.shapes.path.common :as pc]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.geom.path :as ugp]
|
||||
[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/start-edition-mode id)
|
||||
(dw/start-path-edit id)))))
|
||||
|
||||
(mf/defc path-wrapper
|
||||
{::mf/wrap-props false}
|
||||
[props]
|
||||
|
@ -37,19 +27,9 @@
|
|||
content-modifiers (mf/deref content-modifiers-ref)
|
||||
editing-id (mf/deref refs/selected-edition)
|
||||
editing? (= editing-id (:id shape))
|
||||
shape (update shape :content ugp/apply-content-modifiers content-modifiers)
|
||||
handle-mouse-down (we/use-mouse-down shape)
|
||||
handle-context-menu (we/use-context-menu shape)
|
||||
handle-pointer-enter (we/use-pointer-enter shape)
|
||||
handle-pointer-leave (we/use-pointer-leave shape)
|
||||
handle-double-click (use-double-click shape)]
|
||||
shape (update shape :content ugp/apply-content-modifiers content-modifiers)]
|
||||
|
||||
[:> shape-container {:shape shape
|
||||
:pointer-events (when editing? "none")
|
||||
:on-mouse-down handle-mouse-down
|
||||
:on-context-menu handle-context-menu
|
||||
:on-pointer-over handle-pointer-enter
|
||||
:on-pointer-out handle-pointer-leave
|
||||
:on-double-click handle-double-click}
|
||||
:pointer-events (when editing? "none")}
|
||||
[:& path/path-shape {:shape shape
|
||||
:background? true}]]))
|
||||
|
|
|
@ -9,12 +9,12 @@
|
|||
|
||||
(ns app.main.ui.workspace.shapes.path.editor
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.main.data.workspace.drawing.path :as drp]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.cursors :as cur]
|
||||
[app.main.ui.workspace.shapes.path.common :as pc]
|
||||
[app.util.data :as d]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.geom.path :as ugp]
|
||||
[goog.events :as events]
|
||||
|
@ -35,32 +35,32 @@
|
|||
on-click
|
||||
(fn [event]
|
||||
(when-not last-p?
|
||||
(do (dom/stop-propagation event)
|
||||
(dom/prevent-default event)
|
||||
(dom/stop-propagation event)
|
||||
(dom/prevent-default event)
|
||||
|
||||
(cond
|
||||
(and (= edit-mode :move) (not selected?))
|
||||
(st/emit! (drp/select-node position))
|
||||
(cond
|
||||
(and (= edit-mode :move) (not selected?))
|
||||
(st/emit! (drp/select-node position))
|
||||
|
||||
(and (= edit-mode :move) selected?)
|
||||
(st/emit! (drp/deselect-node position))))))
|
||||
(and (= edit-mode :move) selected?)
|
||||
(st/emit! (drp/deselect-node position)))))
|
||||
|
||||
|
||||
on-mouse-down
|
||||
(fn [event]
|
||||
(when-not last-p?
|
||||
(do (dom/stop-propagation event)
|
||||
(dom/prevent-default event)
|
||||
(dom/stop-propagation event)
|
||||
(dom/prevent-default event)
|
||||
|
||||
(cond
|
||||
(= edit-mode :move)
|
||||
(st/emit! (drp/start-move-path-point position))
|
||||
(cond
|
||||
(= edit-mode :move)
|
||||
(st/emit! (drp/start-move-path-point position))
|
||||
|
||||
(and (= edit-mode :draw) start-path?)
|
||||
(st/emit! (drp/start-path-from-point position))
|
||||
(and (= edit-mode :draw) start-path?)
|
||||
(st/emit! (drp/start-path-from-point position))
|
||||
|
||||
(and (= edit-mode :draw) (not start-path?))
|
||||
(st/emit! (drp/close-path-drag-start position))))))]
|
||||
(and (= edit-mode :draw) (not start-path?))
|
||||
(st/emit! (drp/close-path-drag-start position)))))]
|
||||
|
||||
[:g.path-point
|
||||
[:circle.path-point
|
||||
|
@ -170,7 +170,9 @@
|
|||
selected-handlers
|
||||
selected-points
|
||||
hover-handlers
|
||||
hover-points]} (mf/deref edit-path-ref)
|
||||
hover-points]
|
||||
:as edit-path} (mf/deref edit-path-ref)
|
||||
|
||||
{:keys [content]} shape
|
||||
content (ugp/apply-content-modifiers content content-modifiers)
|
||||
points (->> content ugp/content->points (into #{}))
|
||||
|
|
|
@ -12,7 +12,6 @@
|
|||
[app.main.refs :as refs]
|
||||
[app.main.ui.shapes.svg-raw :as svg-raw]
|
||||
[app.main.ui.shapes.shape :refer [shape-container]]
|
||||
[app.main.ui.workspace.effects :as we]
|
||||
[rumext.alpha :as mf]
|
||||
[app.common.geom.shapes :as gsh]
|
||||
[app.main.ui.context :as muc]))
|
||||
|
@ -41,12 +40,6 @@
|
|||
|
||||
tag (get-in shape [:content :tag])
|
||||
|
||||
handle-mouse-down (we/use-mouse-down shape)
|
||||
handle-context-menu (we/use-context-menu shape)
|
||||
handle-pointer-enter (we/use-pointer-enter shape)
|
||||
handle-pointer-leave (we/use-pointer-leave shape)
|
||||
handle-double-click (we/use-double-click shape)
|
||||
|
||||
def-ctx? (mf/use-ctx muc/def-ctx)]
|
||||
|
||||
(cond
|
||||
|
@ -64,12 +57,7 @@
|
|||
:width width
|
||||
:height height
|
||||
:fill "transparent"
|
||||
:stroke "none"
|
||||
:on-mouse-down handle-mouse-down
|
||||
:on-double-click handle-double-click
|
||||
:on-context-menu handle-context-menu
|
||||
:on-pointer-over handle-pointer-enter
|
||||
:on-pointer-out handle-pointer-leave}]]
|
||||
:stroke "none"}]]
|
||||
|
||||
;; We cannot wrap inside groups the shapes that go inside the defs tag
|
||||
;; we use the context so we know when we should not render the container
|
||||
|
|
|
@ -19,9 +19,7 @@
|
|||
[app.main.ui.context :as muc]
|
||||
[app.main.ui.shapes.shape :refer [shape-container]]
|
||||
[app.main.ui.shapes.text :as text]
|
||||
[app.main.ui.workspace.effects :as we]
|
||||
[app.main.ui.workspace.shapes.common :as common]
|
||||
[app.main.ui.workspace.shapes.text.editor :as editor]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.logging :as log]
|
||||
[app.util.object :as obj]
|
||||
|
@ -33,16 +31,6 @@
|
|||
;; Change this to :info :debug or :trace to debug this module
|
||||
(log/set-level! :warn)
|
||||
|
||||
;; --- Events
|
||||
|
||||
(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/start-edition-mode id)))))
|
||||
|
||||
;; --- Text Wrapper for workspace
|
||||
|
||||
(mf/defc text-static-content
|
||||
|
@ -107,15 +95,8 @@
|
|||
{::mf/wrap-props false}
|
||||
[props]
|
||||
(let [{:keys [id x y width height] :as shape} (unchecked-get props "shape")
|
||||
ghost? (mf/use-ctx muc/ghost-ctx)
|
||||
edition (mf/deref refs/selected-edition)
|
||||
edition? (= edition id)
|
||||
|
||||
handle-mouse-down (we/use-mouse-down shape)
|
||||
handle-context-menu (we/use-context-menu shape)
|
||||
handle-pointer-enter (we/use-pointer-enter shape)
|
||||
handle-pointer-leave (we/use-pointer-leave shape)
|
||||
handle-double-click (use-double-click shape)]
|
||||
edition? (= edition id)]
|
||||
|
||||
[:> shape-container {:shape shape}
|
||||
;; We keep hidden the shape when we're editing so it keeps track of the size
|
||||
|
@ -123,24 +104,4 @@
|
|||
[:g.text-shape {:opacity (when edition? 0)
|
||||
:pointer-events "none"}
|
||||
|
||||
(if ghost?
|
||||
[:& text-static-content {:shape shape}]
|
||||
[:& text-resize-content {:shape shape}])]
|
||||
|
||||
(when (and (not ghost?) edition?)
|
||||
[:& editor/text-shape-edit {:key (str "editor" (:id shape))
|
||||
:shape shape}])
|
||||
|
||||
(when-not edition?
|
||||
[:rect.text-actions
|
||||
{:x x
|
||||
:y y
|
||||
:width width
|
||||
:height height
|
||||
:style {:fill "transparent"}
|
||||
:on-mouse-down handle-mouse-down
|
||||
:on-context-menu handle-context-menu
|
||||
:on-pointer-over handle-pointer-enter
|
||||
:on-pointer-out handle-pointer-leave
|
||||
:on-double-click handle-double-click
|
||||
:transform (gsh/transform-matrix shape)}])]))
|
||||
[:& text-resize-content {:shape shape}]]]))
|
||||
|
|
|
@ -31,17 +31,6 @@
|
|||
goog.events.EventType
|
||||
goog.events.KeyCodes))
|
||||
|
||||
;; --- Data functions
|
||||
|
||||
;; TODO: why we need this?
|
||||
;; (defn- fix-gradients
|
||||
;; "Fix for the gradient types that need to be keywords"
|
||||
;; [content]
|
||||
;; (let [fix-node
|
||||
;; (fn [node]
|
||||
;; (d/update-in-when node [:fill-color-gradient :type] keyword))]
|
||||
;; (txt/map-node fix-node content)))
|
||||
|
||||
;; --- Text Editor Rendering
|
||||
|
||||
(mf/defc block-component
|
||||
|
@ -95,22 +84,6 @@
|
|||
|
||||
blured (mf/use-var false)
|
||||
|
||||
on-click-outside
|
||||
(fn [event]
|
||||
(let [target (dom/get-target event)
|
||||
options (dom/get-element-by-class "element-options")
|
||||
assets (dom/get-element-by-class "assets-bar")
|
||||
cpicker (dom/get-element-by-class "colorpicker-tooltip")
|
||||
palette (dom/get-element-by-class "color-palette")
|
||||
self (mf/ref-val self-ref)]
|
||||
(when-not (or (and options (.contains options target))
|
||||
(and assets (.contains assets target))
|
||||
(and self (.contains self target))
|
||||
(and cpicker (.contains cpicker target))
|
||||
(and palette (.contains palette target))
|
||||
(= "foreignObject" (.-tagName ^js target)))
|
||||
(st/emit! dw/clear-edition-mode))))
|
||||
|
||||
on-key-up
|
||||
(fn [event]
|
||||
(dom/stop-propagation event)
|
||||
|
@ -121,9 +94,7 @@
|
|||
|
||||
on-mount
|
||||
(fn []
|
||||
(let [keys [(events/listen js/document EventType.MOUSEDOWN on-click-outside)
|
||||
(events/listen js/document EventType.CLICK on-click-outside)
|
||||
(events/listen js/document EventType.KEYUP on-key-up)]]
|
||||
(let [keys [(events/listen js/document EventType.KEYUP on-key-up)]]
|
||||
(st/emit! (dwt/initialize-editor-state shape default-decorator)
|
||||
(dwt/select-all shape))
|
||||
#(do
|
||||
|
|
|
@ -50,3 +50,4 @@
|
|||
(if (= drawing-tool :comments)
|
||||
[:& comments-sidebar]
|
||||
[:> options-toolbox props])]]))
|
||||
|
||||
|
|
|
@ -122,7 +122,7 @@
|
|||
(if (:blocked item)
|
||||
(st/emit! (dw/update-shape-flags id {:blocked false}))
|
||||
(st/emit! (dw/update-shape-flags id {:blocked true})
|
||||
(dw/select-shape id true))))
|
||||
(dw/deselect-shape id))))
|
||||
|
||||
toggle-visibility
|
||||
(fn [event]
|
||||
|
@ -147,11 +147,9 @@
|
|||
(st/emit! (dw/select-shape id true))
|
||||
|
||||
(> (count selected) 1)
|
||||
(st/emit! (dw/deselect-all)
|
||||
(dw/select-shape id))
|
||||
(st/emit! (dw/select-shape id))
|
||||
:else
|
||||
(st/emit! (dw/deselect-all)
|
||||
(dw/select-shape id)))))
|
||||
(st/emit! (dw/select-shape id)))))
|
||||
|
||||
on-context-menu
|
||||
(fn [event]
|
||||
|
@ -164,8 +162,7 @@
|
|||
on-drag
|
||||
(fn [{:keys [id]}]
|
||||
(when (not (contains? selected id))
|
||||
(st/emit! (dw/deselect-all)
|
||||
(dw/select-shape id))))
|
||||
(st/emit! (dw/select-shape id))))
|
||||
|
||||
on-drop
|
||||
(fn [side {:keys [id] :as data}]
|
||||
|
|
File diff suppressed because it is too large
Load diff
453
frontend/src/app/main/ui/workspace/viewport/actions.cljs
Normal file
453
frontend/src/app/main/ui/workspace/viewport/actions.cljs
Normal file
|
@ -0,0 +1,453 @@
|
|||
; 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/.
|
||||
;;
|
||||
;; This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
;; defined by the Mozilla Public License, v. 2.0.
|
||||
;;
|
||||
;; Copyright (c) 2020-2021 UXBOX Labs SL
|
||||
|
||||
(ns app.main.ui.workspace.viewport.actions
|
||||
(:require
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cfg]
|
||||
[app.main.data.workspace :as dw]
|
||||
[app.main.data.workspace.drawing :as dd]
|
||||
[app.main.data.workspace.libraries :as dwl]
|
||||
[app.main.store :as st]
|
||||
[app.main.streams :as ms]
|
||||
[app.main.ui.workspace.viewport.utils :as utils]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.dom.dnd :as dnd]
|
||||
[app.util.keyboard :as kbd]
|
||||
[app.util.object :as obj]
|
||||
[app.util.timers :as timers]
|
||||
[beicon.core :as rx]
|
||||
[cuerdas.core :as str]
|
||||
[rumext.alpha :as mf])
|
||||
(:import goog.events.WheelEvent
|
||||
goog.events.KeyCodes))
|
||||
|
||||
(defn on-mouse-down
|
||||
[{:keys [id blocked hidden type]} drawing-tool text-editing? edition edit-path selected]
|
||||
(mf/use-callback
|
||||
(mf/deps id blocked hidden type drawing-tool text-editing? edition selected)
|
||||
(fn [bevent]
|
||||
(dom/stop-propagation bevent)
|
||||
|
||||
(let [event (.-nativeEvent bevent)
|
||||
ctrl? (kbd/ctrl? event)
|
||||
shift? (kbd/shift? event)
|
||||
alt? (kbd/alt? event)
|
||||
|
||||
left-click? (= 1 (.-which event))
|
||||
middle-click? (= 2 (.-which event))
|
||||
|
||||
frame? (= :frame type)
|
||||
selected? (contains? selected id)]
|
||||
|
||||
(when middle-click?
|
||||
(dom/prevent-default bevent)
|
||||
(st/emit! (dw/start-panning)))
|
||||
|
||||
(when left-click?
|
||||
(st/emit! (ms/->MouseEvent :down ctrl? shift? alt?))
|
||||
|
||||
(when (and (not= edition id) text-editing?)
|
||||
(st/emit! dw/clear-edition-mode))
|
||||
|
||||
(when (and (or (not edition) (not= edition id)) (not blocked) (not hidden))
|
||||
(cond
|
||||
(and drawing-tool (not (#{:comments :path} drawing-tool)))
|
||||
(st/emit! (dd/start-drawing drawing-tool))
|
||||
|
||||
edit-path
|
||||
;; Handle node select-drawing. NOP at the moment
|
||||
nil
|
||||
|
||||
(or (not id) (and frame? (not selected?)))
|
||||
(st/emit! (dw/handle-selection shift?))
|
||||
|
||||
:else
|
||||
(st/emit! (when (or shift? (not selected?))
|
||||
(dw/select-shape id shift?))
|
||||
(when (not shift?)
|
||||
(dw/start-move-selected))))))))))
|
||||
|
||||
(defn on-move-selected
|
||||
[hover selected]
|
||||
(mf/use-callback
|
||||
(mf/deps @hover selected)
|
||||
(fn [bevent]
|
||||
(let [event (.-nativeEvent bevent)
|
||||
shift? (kbd/shift? event)
|
||||
left-click? (= 1 (.-which event))]
|
||||
(when (and left-click?
|
||||
(not shift?)
|
||||
(or (not @hover)
|
||||
(contains? selected (:id @hover))
|
||||
(contains? selected (:frame-id @hover))))
|
||||
(dom/prevent-default bevent)
|
||||
(dom/stop-propagation bevent)
|
||||
(st/emit! (dw/start-move-selected)))))))
|
||||
|
||||
(defn on-frame-select
|
||||
[selected]
|
||||
(mf/use-callback
|
||||
(mf/deps selected)
|
||||
(fn [event id]
|
||||
(let [shift? (kbd/shift? event)
|
||||
selected? (contains? selected id)]
|
||||
(st/emit! (when (or shift? (not selected?))
|
||||
(dw/select-shape id shift?))
|
||||
(when (not shift?)
|
||||
(dw/start-move-selected)))))))
|
||||
|
||||
(defn on-frame-enter
|
||||
[frame-hover]
|
||||
(mf/use-callback
|
||||
(fn [id]
|
||||
(reset! frame-hover id))))
|
||||
|
||||
(defn on-frame-leave
|
||||
[frame-hover]
|
||||
(mf/use-callback
|
||||
(fn []
|
||||
(reset! frame-hover nil))))
|
||||
|
||||
(defn on-click
|
||||
[]
|
||||
(mf/use-callback
|
||||
(fn [event]
|
||||
(let [ctrl? (kbd/ctrl? event)
|
||||
shift? (kbd/shift? event)
|
||||
alt? (kbd/alt? event)]
|
||||
(st/emit! (ms/->MouseEvent :click ctrl? shift? alt?))))))
|
||||
|
||||
(defn on-double-click
|
||||
[hover hover-ids objects]
|
||||
(mf/use-callback
|
||||
(mf/deps @hover @hover-ids)
|
||||
(fn [event]
|
||||
(dom/stop-propagation event)
|
||||
(let [ctrl? (kbd/ctrl? event)
|
||||
shift? (kbd/shift? event)
|
||||
alt? (kbd/alt? event)
|
||||
|
||||
{:keys [id type] :as shape} @hover
|
||||
|
||||
frame? (= :frame type)
|
||||
group? (= :group type)
|
||||
text? (= :text type)
|
||||
path? (= :path type)]
|
||||
|
||||
(st/emit! (ms/->MouseEvent :double-click ctrl? shift? alt?))
|
||||
|
||||
(when shape
|
||||
(cond frame?
|
||||
(st/emit! (dw/select-shape id shift?))
|
||||
|
||||
(and group? (> (count @hover-ids) 1))
|
||||
(let [selected (get objects (second @hover-ids))]
|
||||
(reset! hover selected)
|
||||
(reset! hover-ids (into [] (rest @hover-ids)))
|
||||
(st/emit! (dw/select-shape (:id selected))))
|
||||
|
||||
(or text? path?)
|
||||
(st/emit! (dw/start-edition-mode id))
|
||||
|
||||
:else
|
||||
;; Do nothing
|
||||
nil))))))
|
||||
|
||||
(defn on-context-menu
|
||||
[hover]
|
||||
(let [{:keys [id]} @hover]
|
||||
(mf/use-callback
|
||||
(mf/deps id)
|
||||
(fn [event]
|
||||
(dom/prevent-default event)
|
||||
(let [position (dom/get-client-position event)]
|
||||
(st/emit! (dw/show-context-menu {:position position
|
||||
:shape @hover})))))))
|
||||
|
||||
(defn on-mouse-up
|
||||
[disable-paste]
|
||||
(mf/use-callback
|
||||
(fn [event]
|
||||
(dom/stop-propagation event)
|
||||
|
||||
(let [event (.-nativeEvent event)
|
||||
ctrl? (kbd/ctrl? event)
|
||||
shift? (kbd/shift? event)
|
||||
alt? (kbd/alt? event)
|
||||
|
||||
left-click? (= 1 (.-which event))
|
||||
middle-click? (= 2 (.-which event))]
|
||||
|
||||
(when left-click?
|
||||
(st/emit! (ms/->MouseEvent :up ctrl? shift? alt?)))
|
||||
|
||||
(when middle-click?
|
||||
(dom/prevent-default event)
|
||||
|
||||
;; We store this so in Firefox the middle button won't do a paste of the content
|
||||
(reset! disable-paste true)
|
||||
(timers/schedule #(reset! disable-paste false))
|
||||
(st/emit! (dw/finish-panning)))))))
|
||||
|
||||
(defn on-pointer-enter [in-viewport?]
|
||||
(mf/use-callback
|
||||
(fn []
|
||||
(reset! in-viewport? true))))
|
||||
|
||||
(defn on-pointer-leave [in-viewport?]
|
||||
(mf/use-callback
|
||||
(fn []
|
||||
(reset! in-viewport? false))))
|
||||
|
||||
(defn on-pointer-down []
|
||||
(mf/use-callback
|
||||
(fn [event]
|
||||
;; We need to handle editor related stuff here because
|
||||
;; handling on editor dom node does not works properly.
|
||||
(let [target (dom/get-target event)
|
||||
editor (.closest ^js target ".public-DraftEditor-content")]
|
||||
;; Capture mouse pointer to detect the movements even if cursor
|
||||
;; leaves the viewport or the browser itself
|
||||
;; https://developer.mozilla.org/en-US/docs/Web/API/Element/setPointerCapture
|
||||
(if editor
|
||||
(.setPointerCapture editor (.-pointerId event))
|
||||
(.setPointerCapture target (.-pointerId event)))))))
|
||||
|
||||
(defn on-pointer-up []
|
||||
(mf/use-callback
|
||||
(fn [event]
|
||||
(let [target (dom/get-target event)]
|
||||
; Release pointer on mouse up
|
||||
(.releasePointerCapture target (.-pointerId event))))))
|
||||
|
||||
(defn on-key-down []
|
||||
(mf/use-callback
|
||||
(fn [event]
|
||||
(let [bevent (.getBrowserEvent ^js event)
|
||||
key (.-keyCode ^js event)
|
||||
key (.normalizeKeyCode KeyCodes key)
|
||||
ctrl? (kbd/ctrl? event)
|
||||
shift? (kbd/shift? event)
|
||||
alt? (kbd/alt? event)
|
||||
meta? (kbd/meta? event)
|
||||
target (dom/get-target event)]
|
||||
|
||||
(when-not (.-repeat bevent)
|
||||
(st/emit! (ms/->KeyboardEvent :down key shift? ctrl? alt? meta?))
|
||||
(when (and (kbd/space? event)
|
||||
(not= "rich-text" (obj/get target "className"))
|
||||
(not= "INPUT" (obj/get target "tagName"))
|
||||
(not= "TEXTAREA" (obj/get target "tagName")))
|
||||
(st/emit! (dw/start-panning))))))))
|
||||
|
||||
(defn on-key-up []
|
||||
(mf/use-callback
|
||||
(fn [event]
|
||||
(let [key (.-keyCode event)
|
||||
key (.normalizeKeyCode KeyCodes key)
|
||||
ctrl? (kbd/ctrl? event)
|
||||
shift? (kbd/shift? event)
|
||||
alt? (kbd/alt? event)
|
||||
meta? (kbd/meta? event)]
|
||||
(when (kbd/space? event)
|
||||
(st/emit! (dw/finish-panning)))
|
||||
(st/emit! (ms/->KeyboardEvent :up key shift? ctrl? alt? meta?))))))
|
||||
|
||||
(defn on-mouse-move [viewport-ref zoom]
|
||||
(let [last-position (mf/use-var nil)
|
||||
viewport (mf/ref-val viewport-ref)]
|
||||
(mf/use-callback
|
||||
(mf/deps zoom)
|
||||
(fn [event]
|
||||
(let [event (.getBrowserEvent ^js event)
|
||||
raw-pt (dom/get-client-position event)
|
||||
viewport (mf/ref-val viewport-ref)
|
||||
pt (utils/translate-point-to-viewport viewport zoom raw-pt)
|
||||
|
||||
;; We calculate the delta because Safari's MouseEvent.movementX/Y drop
|
||||
;; events
|
||||
delta (if @last-position
|
||||
(gpt/subtract raw-pt @last-position)
|
||||
(gpt/point 0 0))]
|
||||
|
||||
(reset! last-position raw-pt)
|
||||
(st/emit! (ms/->PointerEvent :delta delta
|
||||
(kbd/ctrl? event)
|
||||
(kbd/shift? event)
|
||||
(kbd/alt? event)))
|
||||
(st/emit! (ms/->PointerEvent :viewport pt
|
||||
(kbd/ctrl? event)
|
||||
(kbd/shift? event)
|
||||
(kbd/alt? event))))))))
|
||||
|
||||
(defn on-pointer-move [viewport-ref zoom move-stream]
|
||||
(mf/use-callback
|
||||
(mf/deps zoom move-stream)
|
||||
(fn [event]
|
||||
(let [raw-pt (dom/get-client-position event)
|
||||
viewport (mf/ref-val viewport-ref)
|
||||
pt (utils/translate-point-to-viewport viewport zoom raw-pt)]
|
||||
(rx/push! move-stream pt)))))
|
||||
|
||||
(defn on-mouse-wheel [viewport-ref zoom]
|
||||
(mf/use-callback
|
||||
(mf/deps zoom)
|
||||
(fn [event]
|
||||
(let [event (.getBrowserEvent ^js event)
|
||||
raw-pt (dom/get-client-position event)
|
||||
viewport (mf/ref-val viewport-ref)
|
||||
pt (utils/translate-point-to-viewport viewport zoom raw-pt)
|
||||
|
||||
ctrl? (kbd/ctrl? event)
|
||||
meta? (kbd/meta? event)
|
||||
target (dom/get-target event)]
|
||||
(cond
|
||||
(or ctrl? meta?)
|
||||
(do
|
||||
(dom/prevent-default event)
|
||||
(dom/stop-propagation event)
|
||||
(let [delta (+ (.-deltaY ^js event)
|
||||
(.-deltaX ^js event))]
|
||||
(if (pos? delta)
|
||||
(st/emit! (dw/decrease-zoom pt))
|
||||
(st/emit! (dw/increase-zoom pt)))))
|
||||
|
||||
(.contains ^js viewport target)
|
||||
(let [delta-mode (.-deltaMode ^js event)
|
||||
|
||||
unit (cond
|
||||
(= delta-mode WheelEvent.DeltaMode.PIXEL) 1
|
||||
(= delta-mode WheelEvent.DeltaMode.LINE) 16
|
||||
(= delta-mode WheelEvent.DeltaMode.PAGE) 100)
|
||||
|
||||
delta-y (-> (.-deltaY ^js event)
|
||||
(* unit)
|
||||
(/ zoom))
|
||||
delta-x (-> (.-deltaX ^js event)
|
||||
(* unit)
|
||||
(/ zoom))]
|
||||
(dom/prevent-default event)
|
||||
(dom/stop-propagation event)
|
||||
(if (kbd/shift? event)
|
||||
(st/emit! (dw/update-viewport-position {:x #(+ % delta-y)}))
|
||||
(st/emit! (dw/update-viewport-position {:x #(+ % delta-x)
|
||||
:y #(+ % delta-y)})))))))))
|
||||
|
||||
(defn on-drag-enter []
|
||||
(mf/use-callback
|
||||
(fn [e]
|
||||
(when (or (dnd/has-type? e "penpot/shape")
|
||||
(dnd/has-type? e "penpot/component")
|
||||
(dnd/has-type? e "Files")
|
||||
(dnd/has-type? e "text/uri-list")
|
||||
(dnd/has-type? e "text/asset-id"))
|
||||
(dom/prevent-default e)))))
|
||||
|
||||
(defn on-drag-over []
|
||||
(mf/use-callback
|
||||
(fn [e]
|
||||
(when (or (dnd/has-type? e "penpot/shape")
|
||||
(dnd/has-type? e "penpot/component")
|
||||
(dnd/has-type? e "Files")
|
||||
(dnd/has-type? e "text/uri-list")
|
||||
(dnd/has-type? e "text/asset-id"))
|
||||
(dom/prevent-default e)))))
|
||||
|
||||
(defn on-image-uploaded []
|
||||
(mf/use-callback
|
||||
(fn [image {:keys [x y]}]
|
||||
(st/emit! (dw/image-uploaded image x y)))))
|
||||
|
||||
(defn on-drop [file viewport-ref zoom]
|
||||
(let [on-image-uploaded (on-image-uploaded)]
|
||||
(mf/use-callback
|
||||
(fn [event]
|
||||
(dom/prevent-default event)
|
||||
(let [point (gpt/point (.-clientX event) (.-clientY event))
|
||||
viewport (mf/ref-val viewport-ref)
|
||||
viewport-coord (utils/translate-point-to-viewport viewport zoom point)
|
||||
asset-id (-> (dnd/get-data event "text/asset-id") uuid/uuid)
|
||||
asset-name (dnd/get-data event "text/asset-name")
|
||||
asset-type (dnd/get-data event "text/asset-type")]
|
||||
(cond
|
||||
(dnd/has-type? event "penpot/shape")
|
||||
(let [shape (dnd/get-data event "penpot/shape")
|
||||
final-x (- (:x viewport-coord) (/ (:width shape) 2))
|
||||
final-y (- (:y viewport-coord) (/ (:height shape) 2))]
|
||||
(st/emit! (dw/add-shape (-> shape
|
||||
(assoc :id (uuid/next))
|
||||
(assoc :x final-x)
|
||||
(assoc :y final-y)))))
|
||||
|
||||
(dnd/has-type? event "penpot/component")
|
||||
(let [{:keys [component file-id]} (dnd/get-data event "penpot/component")
|
||||
shape (get-in component [:objects (:id component)])
|
||||
final-x (- (:x viewport-coord) (/ (:width shape) 2))
|
||||
final-y (- (:y viewport-coord) (/ (:height shape) 2))]
|
||||
(st/emit! (dwl/instantiate-component file-id
|
||||
(:id component)
|
||||
(gpt/point final-x final-y))))
|
||||
|
||||
;; Will trigger when the user drags an image from a browser to the viewport
|
||||
(dnd/has-type? event "text/uri-list")
|
||||
(let [data (dnd/get-data event "text/uri-list")
|
||||
lines (str/lines data)
|
||||
urls (filter #(and (not (str/blank? %))
|
||||
(not (str/starts-with? % "#")))
|
||||
lines)
|
||||
params {:file-id (:id file)
|
||||
:uris urls}]
|
||||
(st/emit! (dw/upload-media-workspace params viewport-coord)))
|
||||
|
||||
;; Will trigger when the user drags an SVG asset from the assets panel
|
||||
(and (dnd/has-type? event "text/asset-id") (= asset-type "image/svg+xml"))
|
||||
(let [path (cfg/resolve-file-media {:id asset-id})
|
||||
params {:file-id (:id file)
|
||||
:uris [path]
|
||||
:name asset-name
|
||||
:mtype asset-type}]
|
||||
(st/emit! (dw/upload-media-workspace params viewport-coord)))
|
||||
|
||||
;; Will trigger when the user drags an image from the assets SVG
|
||||
(dnd/has-type? event "text/asset-id")
|
||||
(let [params {:file-id (:id file)
|
||||
:object-id asset-id
|
||||
:name asset-name}]
|
||||
(st/emit! (dw/clone-media-object
|
||||
(with-meta params
|
||||
{:on-success #(on-image-uploaded % viewport-coord)}))))
|
||||
|
||||
;; Will trigger when the user drags a file from their file explorer into the viewport
|
||||
;; Or the user pastes an image
|
||||
;; Or the user uploads an image using the image tool
|
||||
:else
|
||||
(let [files (dnd/get-files event)
|
||||
params {:file-id (:id file)
|
||||
:data (seq files)}]
|
||||
(st/emit! (dw/upload-media-workspace params viewport-coord)))))))))
|
||||
|
||||
(defn on-paste [disable-paste in-viewport?]
|
||||
(mf/use-callback
|
||||
(fn [event]
|
||||
;; We disable the paste just after mouse-up of a middle button so when panning won't
|
||||
;; paste the content into the workspace
|
||||
(let [tag-name (-> event dom/get-target dom/get-tag-name)]
|
||||
(when (and (not (#{"INPUT" "TEXTAREA"} tag-name)) (not @disable-paste))
|
||||
(st/emit! (dw/paste-from-event event @in-viewport?)))))))
|
||||
|
||||
(defn on-resize [viewport-ref]
|
||||
(mf/use-callback
|
||||
(fn [event]
|
||||
(let [node (mf/ref-val viewport-ref)
|
||||
prnt (dom/get-parent node)
|
||||
size (dom/get-client-size prnt)]
|
||||
;; We schedule the event so it fires after `initialize-page` event
|
||||
(timers/schedule #(st/emit! (dw/update-viewport-size size)))))))
|
80
frontend/src/app/main/ui/workspace/viewport/comments.cljs
Normal file
80
frontend/src/app/main/ui/workspace/viewport/comments.cljs
Normal file
|
@ -0,0 +1,80 @@
|
|||
;; 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/.
|
||||
;;
|
||||
;; This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
;; defined by the Mozilla Public License, v. 2.0.
|
||||
;;
|
||||
;; Copyright (c) 2020 UXBOX Labs SL
|
||||
|
||||
(ns app.main.ui.workspace.viewport.comments
|
||||
(:require
|
||||
[app.main.data.comments :as dcm]
|
||||
[app.main.data.workspace.comments :as dwcm]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.comments :as cmt]
|
||||
[cuerdas.core :as str]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
(mf/defc comments-layer
|
||||
[{:keys [vbox vport zoom file-id page-id drawing] :as props}]
|
||||
(let [pos-x (* (- (:x vbox)) zoom)
|
||||
pos-y (* (- (:y vbox)) zoom)
|
||||
|
||||
profile (mf/deref refs/profile)
|
||||
users (mf/deref refs/users)
|
||||
local (mf/deref refs/comments-local)
|
||||
threads-map (mf/deref refs/threads-ref)
|
||||
|
||||
threads (->> (vals threads-map)
|
||||
(filter #(= (:page-id %) page-id))
|
||||
(dcm/apply-filters local profile))
|
||||
|
||||
on-bubble-click
|
||||
(fn [{:keys [id] :as thread}]
|
||||
(if (= (:open local) id)
|
||||
(st/emit! (dcm/close-thread))
|
||||
(st/emit! (dcm/open-thread thread))))
|
||||
|
||||
on-draft-cancel
|
||||
(mf/use-callback
|
||||
(st/emitf :interrupt))
|
||||
|
||||
on-draft-submit
|
||||
(mf/use-callback
|
||||
(fn [draft]
|
||||
(st/emit! (dcm/create-thread draft))))]
|
||||
|
||||
(mf/use-effect
|
||||
(mf/deps file-id)
|
||||
(fn []
|
||||
(st/emit! (dwcm/initialize-comments file-id))
|
||||
(fn []
|
||||
(st/emit! ::dwcm/finalize))))
|
||||
|
||||
[:div.comments-section
|
||||
[:div.workspace-comments-container
|
||||
{:style {:width (str (:width vport) "px")
|
||||
:height (str (:height vport) "px")}}
|
||||
[:div.threads {:style {:transform (str/format "translate(%spx, %spx)" pos-x pos-y)}}
|
||||
(for [item threads]
|
||||
[:& cmt/thread-bubble {:thread item
|
||||
:zoom zoom
|
||||
:on-click on-bubble-click
|
||||
:open? (= (:id item) (:open local))
|
||||
:key (:seqn item)}])
|
||||
|
||||
(when-let [id (:open local)]
|
||||
(when-let [thread (get threads-map id)]
|
||||
[:& cmt/thread-comments {:thread thread
|
||||
:users users
|
||||
:zoom zoom}]))
|
||||
|
||||
(when-let [draft (:comment drawing)]
|
||||
[:& cmt/draft-thread {:draft draft
|
||||
:on-cancel on-draft-cancel
|
||||
:on-submit on-draft-submit
|
||||
:zoom zoom}])]]]))
|
||||
|
||||
|
|
@ -4,7 +4,7 @@
|
|||
;;
|
||||
;; Copyright (c) 2015-2019 Andrey Antukh <niwi@niwi.nz>
|
||||
|
||||
(ns app.main.ui.workspace.drawarea
|
||||
(ns app.main.ui.workspace.viewport.drawarea
|
||||
"Drawing components."
|
||||
(:require
|
||||
[rumext.alpha :as mf]
|
|
@ -7,7 +7,7 @@
|
|||
;;
|
||||
;; Copyright (c) 2020 UXBOX Labs SL
|
||||
|
||||
(ns app.main.ui.workspace.frame-grid
|
||||
(ns app.main.ui.workspace.viewport.frame-grid
|
||||
(:require
|
||||
[rumext.alpha :as mf]
|
||||
[okulary.core :as l]
|
|
@ -7,7 +7,7 @@
|
|||
;;
|
||||
;; Copyright (c) 2020 UXBOX Labs SL
|
||||
|
||||
(ns app.main.ui.workspace.gradients
|
||||
(ns app.main.ui.workspace.viewport.gradients
|
||||
"Gradients handlers and renders"
|
||||
(:require
|
||||
[rumext.alpha :as mf]
|
144
frontend/src/app/main/ui/workspace/viewport/hooks.cljs
Normal file
144
frontend/src/app/main/ui/workspace/viewport/hooks.cljs
Normal file
|
@ -0,0 +1,144 @@
|
|||
; 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/.
|
||||
;;
|
||||
;; This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
;; defined by the Mozilla Public License, v. 2.0.
|
||||
;;
|
||||
;; Copyright (c) 2020-2021 UXBOX Labs SL
|
||||
|
||||
(ns app.main.ui.workspace.viewport.hooks
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.geom.shapes :as gsh]
|
||||
[app.common.pages :as cp]
|
||||
[app.main.data.workspace :as dw]
|
||||
[app.main.store :as st]
|
||||
[app.main.streams :as ms]
|
||||
[app.main.ui.hooks :as hooks]
|
||||
[app.main.ui.workspace.viewport.actions :as actions]
|
||||
[app.main.ui.workspace.viewport.utils :as utils]
|
||||
[app.main.worker :as uw]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.timers :as timers]
|
||||
[beicon.core :as rx]
|
||||
[goog.events :as events]
|
||||
[rumext.alpha :as mf])
|
||||
(:import goog.events.EventType))
|
||||
|
||||
(defn setup-dom-events [viewport-ref zoom disable-paste in-viewport?]
|
||||
(let [on-key-down (actions/on-key-down)
|
||||
on-key-up (actions/on-key-up)
|
||||
on-mouse-move (actions/on-mouse-move viewport-ref zoom)
|
||||
on-mouse-wheel (actions/on-mouse-wheel viewport-ref zoom)
|
||||
on-resize (actions/on-resize viewport-ref)
|
||||
on-paste (actions/on-paste disable-paste in-viewport?)]
|
||||
(mf/use-layout-effect
|
||||
(mf/deps on-key-down on-key-up on-mouse-move on-mouse-wheel on-resize on-paste)
|
||||
(fn []
|
||||
(let [node (mf/ref-val viewport-ref)
|
||||
prnt (dom/get-parent node)
|
||||
|
||||
keys [(events/listen js/document EventType.KEYDOWN on-key-down)
|
||||
(events/listen js/document EventType.KEYUP on-key-up)
|
||||
(events/listen node EventType.MOUSEMOVE on-mouse-move)
|
||||
;; bind with passive=false to allow the event to be cancelled
|
||||
;; https://stackoverflow.com/a/57582286/3219895
|
||||
(events/listen js/window EventType.WHEEL on-mouse-wheel #js {:passive false})
|
||||
(events/listen js/window EventType.RESIZE on-resize)
|
||||
(events/listen js/window EventType.PASTE on-paste)]]
|
||||
|
||||
(fn []
|
||||
(doseq [key keys]
|
||||
(events/unlistenByKey key))))))))
|
||||
|
||||
(defn setup-viewport-size [viewport-ref]
|
||||
(mf/use-layout-effect
|
||||
(fn []
|
||||
(let [node (mf/ref-val viewport-ref)
|
||||
prnt (dom/get-parent node)
|
||||
size (dom/get-client-size prnt)]
|
||||
;; We schedule the event so it fires after `initialize-page` event
|
||||
(timers/schedule #(st/emit! (dw/initialize-viewport size)))))))
|
||||
|
||||
(defn setup-cursor [cursor alt? panning drawing-tool drawing-path?]
|
||||
(mf/use-effect
|
||||
(mf/deps @cursor @alt? panning drawing-tool drawing-path?)
|
||||
(fn []
|
||||
(let [new-cursor
|
||||
(cond
|
||||
panning (utils/get-cursor :hand)
|
||||
(= drawing-tool :comments) (utils/get-cursor :comments)
|
||||
(= drawing-tool :frame) (utils/get-cursor :create-artboard)
|
||||
(= drawing-tool :rect) (utils/get-cursor :create-rectangle)
|
||||
(= drawing-tool :circle) (utils/get-cursor :create-ellipse)
|
||||
(or (= drawing-tool :path)
|
||||
drawing-path?) (utils/get-cursor :pen)
|
||||
(= drawing-tool :curve) (utils/get-cursor :pencil)
|
||||
drawing-tool (utils/get-cursor :create-shape)
|
||||
@alt? (utils/get-cursor :duplicate)
|
||||
:else (utils/get-cursor :pointer-inner))]
|
||||
|
||||
(when (not= @cursor new-cursor)
|
||||
(reset! cursor new-cursor))))))
|
||||
|
||||
(defn setup-resize [layout viewport-ref]
|
||||
(let [on-resize (actions/on-resize viewport-ref)]
|
||||
(mf/use-layout-effect (mf/deps layout) on-resize)))
|
||||
|
||||
(defn setup-keyboard [alt? ctrl?]
|
||||
(hooks/use-stream ms/keyboard-alt #(reset! alt? %))
|
||||
(hooks/use-stream ms/keyboard-ctrl #(reset! ctrl? %)))
|
||||
|
||||
(defn setup-hover-shapes [page-id move-stream selected objects transform selected ctrl? hover hover-ids]
|
||||
(let [query-point
|
||||
(mf/use-callback
|
||||
(mf/deps page-id)
|
||||
(fn [point]
|
||||
(let [rect (gsh/center->rect point 8 8)]
|
||||
(uw/ask! {:cmd :selection/query
|
||||
:page-id page-id
|
||||
:rect rect
|
||||
:include-frames? true}))))
|
||||
|
||||
over-shapes-stream
|
||||
(->> move-stream
|
||||
(rx/switch-map query-point))
|
||||
|
||||
roots (mf/use-memo
|
||||
(mf/deps selected objects)
|
||||
(fn []
|
||||
(let [roots-ids (cp/clean-loops objects selected)]
|
||||
(->> roots-ids (mapv #(get objects %))))))]
|
||||
|
||||
(hooks/use-stream
|
||||
over-shapes-stream
|
||||
(mf/deps page-id objects transform selected @ctrl?)
|
||||
(fn [ids]
|
||||
(let [remove-id? (into #{} (mapcat #(cp/get-parents % objects)) selected)
|
||||
remove-id? (if @ctrl?
|
||||
(d/concat remove-id?
|
||||
(->> ids
|
||||
(filterv #(= :group (get-in objects [% :type])))))
|
||||
remove-id?)
|
||||
ids (->> ids (filterv (comp not remove-id?)))]
|
||||
(when (not transform)
|
||||
(reset! hover (get objects (first ids)))
|
||||
(reset! hover-ids ids)))))))
|
||||
|
||||
(defn setup-viewport-modifiers [modifiers selected objects render-ref]
|
||||
(let [roots (mf/use-memo
|
||||
(mf/deps objects selected)
|
||||
(fn []
|
||||
(let [roots-ids (cp/clean-loops objects selected)]
|
||||
(->> roots-ids (mapv #(get objects %))))))]
|
||||
|
||||
;; Layout effect is important so the code is executed before the modifiers
|
||||
;; are applied to the shape
|
||||
(mf/use-layout-effect
|
||||
(mf/deps modifiers roots)
|
||||
|
||||
#(when-let [render-node (mf/ref-val render-ref)]
|
||||
(if modifiers
|
||||
(utils/update-transform render-node roots modifiers)
|
||||
(utils/remove-transform render-node roots))))))
|
|
@ -7,7 +7,7 @@
|
|||
;;
|
||||
;; Copyright (c) 2020 UXBOX Labs SL
|
||||
|
||||
(ns app.main.ui.workspace.shapes.interactions
|
||||
(ns app.main.ui.workspace.viewport.interactions
|
||||
"Visually show shape interactions in workspace"
|
||||
(:require
|
||||
[app.common.geom.point :as gpt]
|
||||
|
@ -31,8 +31,6 @@
|
|||
[event {:keys [id type] :as shape} selected]
|
||||
(do
|
||||
(dom/stop-propagation event)
|
||||
(when-not (empty? selected)
|
||||
(st/emit! (dw/deselect-all)))
|
||||
(st/emit! (dw/select-shape id))
|
||||
(st/emit! (dw/start-create-interaction))))
|
||||
|
|
@ -7,7 +7,7 @@
|
|||
;;
|
||||
;; Copyright (c) 2020 UXBOX Labs SL
|
||||
|
||||
(ns app.main.ui.workspace.colorpicker.pixel-overlay
|
||||
(ns app.main.ui.workspace.viewport.pixel-overlay
|
||||
(:require
|
||||
[app.common.uuid :as uuid]
|
||||
[app.main.data.colors :as dwc]
|
||||
|
@ -59,7 +59,8 @@
|
|||
[props]
|
||||
(let [vport (unchecked-get props "vport")
|
||||
vbox (unchecked-get props "vbox")
|
||||
viewport-node (unchecked-get props "viewport")
|
||||
viewport-ref (unchecked-get props "viewport-ref")
|
||||
viewport-node (mf/ref-val viewport-ref)
|
||||
options (unchecked-get props "options")
|
||||
svg-ref (mf/use-ref nil)
|
||||
canvas-ref (mf/use-ref nil)
|
||||
|
@ -133,7 +134,7 @@
|
|||
(mf/deps img-ref)
|
||||
(fn []
|
||||
(let [img-node (mf/ref-val img-ref)
|
||||
svg-node (mf/ref-val svg-ref)
|
||||
svg-node #_(mf/ref-val svg-ref) (dom/get-element "render")
|
||||
xml (-> (js/XMLSerializer.)
|
||||
(.serializeToString svg-node)
|
||||
js/encodeURIComponent
|
||||
|
@ -160,30 +161,26 @@
|
|||
#(rx/dispose! sub))))
|
||||
|
||||
(mf/use-effect
|
||||
(mf/deps svg-ref)
|
||||
#_(mf/deps svg-ref)
|
||||
(fn []
|
||||
(when svg-ref
|
||||
(let [config #js {:attributes true
|
||||
:childList true
|
||||
:subtree true
|
||||
:characterData true}
|
||||
svg-node (mf/ref-val svg-ref)
|
||||
observer (js/MutationObserver. handle-svg-change)]
|
||||
(.observe observer svg-node config)
|
||||
(handle-svg-change)
|
||||
(let [config #js {:attributes true
|
||||
:childList true
|
||||
:subtree true
|
||||
:characterData true}
|
||||
svg-node #_(mf/ref-val svg-ref) (dom/get-element "render")
|
||||
observer (js/MutationObserver. handle-svg-change)
|
||||
]
|
||||
(.observe observer svg-node config)
|
||||
(handle-svg-change)
|
||||
|
||||
;; Disconnect on unmount
|
||||
#(.disconnect observer)))))
|
||||
;; Disconnect on unmount
|
||||
#(.disconnect observer)
|
||||
)))
|
||||
|
||||
[:*
|
||||
[:div.overlay
|
||||
[:div.pixel-overlay
|
||||
{:tab-index 0
|
||||
:style {:position "absolute"
|
||||
:top 0
|
||||
:left 0
|
||||
:width "100%"
|
||||
:height "100%"
|
||||
:cursor cur/picker}
|
||||
:style {:cursor cur/picker}
|
||||
:on-mouse-down handle-mouse-down-picker
|
||||
:on-mouse-up handle-mouse-up-picker
|
||||
:on-mouse-move handle-mouse-move-picker}
|
||||
|
@ -200,7 +197,7 @@
|
|||
:width "100%"
|
||||
:height "100%"}}]
|
||||
|
||||
[:& (mf/provider muc/embed-ctx) {:value true}
|
||||
#_[:& (mf/provider muc/embed-ctx) {:value true}
|
||||
[:svg.viewport
|
||||
{:ref svg-ref
|
||||
:preserveAspectRatio "xMidYMid meet"
|
84
frontend/src/app/main/ui/workspace/viewport/presence.cljs
Normal file
84
frontend/src/app/main/ui/workspace/viewport/presence.cljs
Normal 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/.
|
||||
;;
|
||||
;; This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
;; defined by the Mozilla Public License, v. 2.0.
|
||||
;;
|
||||
;; Copyright (c) 2020 UXBOX Labs SL
|
||||
|
||||
(ns app.main.ui.workspace.viewport.presence
|
||||
(:require
|
||||
[app.main.refs :as refs]
|
||||
[app.util.time :as dt]
|
||||
[app.util.timers :as ts]
|
||||
[beicon.core :as rx]
|
||||
[cuerdas.core :as str]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
(def pointer-icon-path
|
||||
(str "M5.292 4.027L1.524.26l-.05-.01L0 0l.258 1.524 3.769 3.768zm-.45 "
|
||||
"0l-.313.314L1.139.95l.314-.314zm-.5.5l-.315.316-3.39-3.39.315-.315 "
|
||||
"3.39 3.39zM1.192.526l-.668.667L.431.646.64.43l.552.094z"))
|
||||
|
||||
(mf/defc session-cursor
|
||||
[{:keys [session profile] :as props}]
|
||||
(let [zoom (mf/deref refs/selected-zoom)
|
||||
point (:point session)
|
||||
color (:color session "#000000")
|
||||
transform (str/fmt "translate(%s, %s) scale(%s)" (:x point) (:y point) (/ 4 zoom))]
|
||||
[:g.multiuser-cursor {:transform transform}
|
||||
[:path {:fill color
|
||||
:d pointer-icon-path
|
||||
}]
|
||||
[:g {:transform "translate(0 -291.708)"}
|
||||
[:rect {:width 25
|
||||
:height 5
|
||||
:x 7
|
||||
:y 291.5
|
||||
:fill color
|
||||
:fill-opacity 0.8
|
||||
:paint-order "stroke fill markers"
|
||||
:rx 1
|
||||
:ry 1}]
|
||||
[:text {:x 8
|
||||
:y 295
|
||||
:width 25
|
||||
:height 5
|
||||
:overflow "hidden"
|
||||
:fill "#fff"
|
||||
:stroke-width 1
|
||||
:font-family "Works Sans"
|
||||
:font-size 3
|
||||
:font-weight 400
|
||||
:letter-spacing 0
|
||||
:style { :line-height 1.25 }
|
||||
:word-spacing 0}
|
||||
(str (str/slice (:fullname profile) 0 14)
|
||||
(when (> (count (:fullname profile)) 14) "..."))]]]))
|
||||
|
||||
(mf/defc active-cursors
|
||||
{::mf/wrap [mf/memo]}
|
||||
[{:keys [page-id] :as props}]
|
||||
(let [counter (mf/use-state 0)
|
||||
users (mf/deref refs/users)
|
||||
sessions (mf/deref refs/workspace-presence)
|
||||
sessions (->> (vals sessions)
|
||||
(filter #(= page-id (:page-id %)))
|
||||
(filter #(>= 5000 (- (inst-ms (dt/now)) (inst-ms (:updated-at %))))))]
|
||||
(mf/use-effect
|
||||
nil
|
||||
(fn []
|
||||
(let [sem (ts/schedule 1000 #(swap! counter inc))]
|
||||
(fn [] (rx/dispose! sem)))))
|
||||
|
||||
(for [session sessions]
|
||||
(when (:point session)
|
||||
[:& session-cursor {:session session
|
||||
:profile (get users (:profile-id session))
|
||||
:key (:id session)}]))))
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -7,7 +7,7 @@
|
|||
;;
|
||||
;; Copyright (c) 2020 UXBOX Labs SL
|
||||
|
||||
(ns app.main.ui.workspace.selection
|
||||
(ns app.main.ui.workspace.viewport.selection
|
||||
"Selection handlers component."
|
||||
(:require
|
||||
[app.common.geom.matrix :as gmt]
|
||||
|
@ -46,7 +46,7 @@
|
|||
(def min-selrect-side 10)
|
||||
(def small-selrect-side 30)
|
||||
|
||||
(mf/defc selection-rect [{:keys [transform rect zoom color]}]
|
||||
(mf/defc selection-rect [{:keys [transform rect zoom color on-move-selected]}]
|
||||
(when rect
|
||||
(let [{:keys [x y width height]} rect]
|
||||
[:rect.main
|
||||
|
@ -55,6 +55,7 @@
|
|||
:width width
|
||||
:height height
|
||||
:transform transform
|
||||
:on-mouse-down on-move-selected
|
||||
:style {:stroke color
|
||||
:stroke-width (/ selection-rect-width zoom)
|
||||
:fill "transparent"}}])))
|
||||
|
@ -237,29 +238,27 @@
|
|||
{::mf/wrap-props false}
|
||||
[props]
|
||||
(let [{:keys [overflow-text type] :as shape} (obj/get props "shape")
|
||||
zoom (obj/get props "zoom")
|
||||
color (obj/get props "color")
|
||||
on-resize (obj/get props "on-resize")
|
||||
on-rotate (obj/get props "on-rotate")
|
||||
disable-handlers (obj/get props "disable-handlers")
|
||||
zoom (obj/get props "zoom")
|
||||
color (obj/get props "color")
|
||||
on-move-selected (obj/get props "on-move-selected")
|
||||
on-resize (obj/get props "on-resize")
|
||||
on-rotate (obj/get props "on-rotate")
|
||||
disable-handlers (obj/get props "disable-handlers")
|
||||
current-transform (mf/deref refs/current-transform)
|
||||
|
||||
hide? (mf/use-state false)
|
||||
selrect (-> (:selrect shape)
|
||||
minimum-selrect)
|
||||
transform (geom/transform-matrix shape {:no-flip true})]
|
||||
|
||||
(hooks/use-stream ms/keyboard-ctrl #(when (= type :group) (reset! hide? %)))
|
||||
|
||||
(when (not (#{:move :rotate} current-transform))
|
||||
[:g.controls {:style {:display (when @hide? "none")}
|
||||
:pointer-events (when disable-handlers "none")}
|
||||
[:g.controls {:pointer-events (when disable-handlers "none")}
|
||||
|
||||
;; Selection rect
|
||||
[:& selection-rect {:rect selrect
|
||||
:transform transform
|
||||
:zoom zoom
|
||||
:color color}]
|
||||
:color color
|
||||
:on-move-selected on-move-selected}]
|
||||
[:& outline {:shape shape :color color}]
|
||||
|
||||
;; Handlers
|
||||
|
@ -296,7 +295,7 @@
|
|||
:fill "transparent"}}]]))
|
||||
|
||||
(mf/defc multiple-selection-handlers
|
||||
[{:keys [shapes selected zoom color show-distances disable-handlers] :as props}]
|
||||
[{:keys [shapes selected zoom color show-distances disable-handlers on-move-selected] :as props}]
|
||||
(let [shape (geom/setup {:type :rect} (geom/selection-rect (->> shapes (map geom/transform-shape))))
|
||||
shape-center (geom/center-shape shape)
|
||||
|
||||
|
@ -318,6 +317,7 @@
|
|||
:zoom zoom
|
||||
:color color
|
||||
:disable-handlers disable-handlers
|
||||
:on-move-selected on-move-selected
|
||||
:on-resize on-resize
|
||||
:on-rotate on-rotate}]
|
||||
|
||||
|
@ -331,7 +331,7 @@
|
|||
[:circle {:cx (:x shape-center) :cy (:y shape-center) :r 5 :fill "yellow"}])]))
|
||||
|
||||
(mf/defc single-selection-handlers
|
||||
[{:keys [shape zoom color show-distances disable-handlers] :as props}]
|
||||
[{:keys [shape zoom color show-distances disable-handlers on-move-selected] :as props}]
|
||||
(let [shape-id (:id shape)
|
||||
shape (geom/transform-shape shape)
|
||||
|
||||
|
@ -357,7 +357,8 @@
|
|||
:color color
|
||||
:on-rotate on-rotate
|
||||
:on-resize on-resize
|
||||
:disable-handlers disable-handlers}]
|
||||
:disable-handlers disable-handlers
|
||||
:on-move-selected on-move-selected}]
|
||||
|
||||
(when show-distances
|
||||
[:& msr/measurement {:bounds vbox
|
||||
|
@ -368,7 +369,7 @@
|
|||
|
||||
(mf/defc selection-handlers
|
||||
{::mf/wrap [mf/memo]}
|
||||
[{:keys [selected edition zoom show-distances disable-handlers] :as props}]
|
||||
[{:keys [selected edition zoom show-distances disable-handlers on-move-selected] :as props}]
|
||||
(let [;; We need remove posible nil values because on shape
|
||||
;; deletion many shape will reamin selected and deleted
|
||||
;; in the same time for small instant of time
|
||||
|
@ -390,7 +391,8 @@
|
|||
:zoom zoom
|
||||
:color color
|
||||
:show-distances show-distances
|
||||
:disable-handlers disable-handlers}]
|
||||
:disable-handlers disable-handlers
|
||||
:on-move-selected on-move-selected}]
|
||||
|
||||
(and (= type :text)
|
||||
(= edition (:id shape)))
|
||||
|
@ -408,4 +410,5 @@
|
|||
:zoom zoom
|
||||
:color color
|
||||
:show-distances show-distances
|
||||
:disable-handlers disable-handlers}])))
|
||||
:disable-handlers disable-handlers
|
||||
:on-move-selected on-move-selected}])))
|
|
@ -7,7 +7,7 @@
|
|||
;;
|
||||
;; Copyright (c) 2020 UXBOX Labs SL
|
||||
|
||||
(ns app.main.ui.workspace.snap-distances
|
||||
(ns app.main.ui.workspace.viewport.snap-distances
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.geom.point :as gpt]
|
||||
|
@ -230,6 +230,7 @@
|
|||
(->> (uw/ask! {:cmd :selection/query
|
||||
:page-id page-id
|
||||
:frame-id (:id frame)
|
||||
:include-frames? true
|
||||
:rect rect})
|
||||
(rx/map #(set/difference % selected))
|
||||
(rx/map #(->> % (map (partial get @refs/workspace-page-objects)))))
|
|
@ -7,7 +7,7 @@
|
|||
;;
|
||||
;; Copyright (c) 2020 UXBOX Labs SL
|
||||
|
||||
(ns app.main.ui.workspace.snap-points
|
||||
(ns app.main.ui.workspace.viewport.snap-points
|
||||
(:require
|
||||
[app.common.math :as mth]
|
||||
[app.common.data :as d]
|
0
frontend/src/app/main/ui/workspace/viewport/streams.cljs
Normal file
0
frontend/src/app/main/ui/workspace/viewport/streams.cljs
Normal file
60
frontend/src/app/main/ui/workspace/viewport/utils.cljs
Normal file
60
frontend/src/app/main/ui/workspace/viewport/utils.cljs
Normal file
|
@ -0,0 +1,60 @@
|
|||
; 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/.
|
||||
;;
|
||||
;; This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
;; defined by the Mozilla Public License, v. 2.0.
|
||||
;;
|
||||
;; Copyright (c) 2020-2021 UXBOX Labs SL
|
||||
|
||||
(ns app.main.ui.workspace.viewport.utils
|
||||
(:require
|
||||
[app.util.dom :as dom]
|
||||
[app.common.geom.point :as gpt]
|
||||
[cuerdas.core :as str]
|
||||
[app.common.data :as d]
|
||||
[app.main.ui.cursors :as cur]
|
||||
))
|
||||
|
||||
(defn update-transform [node shapes modifiers]
|
||||
(doseq [{:keys [id type]} shapes]
|
||||
(when-let [node (dom/get-element (str "shape-" id))]
|
||||
(let [node (if (= :frame type) (.-parentNode node) node)]
|
||||
(dom/set-attribute node "transform" (str (:displacement modifiers)))))))
|
||||
|
||||
(defn remove-transform [node shapes]
|
||||
(doseq [{:keys [id type]} shapes]
|
||||
(when-let [node (dom/get-element (str "shape-" id))]
|
||||
(let [node (if (= :frame type) (.-parentNode node) node)]
|
||||
(dom/remove-attribute node "transform")))))
|
||||
|
||||
(defn format-viewbox [vbox]
|
||||
(str/join " " [(+ (:x vbox 0) (:left-offset vbox 0))
|
||||
(:y vbox 0)
|
||||
(:width vbox 0)
|
||||
(:height vbox 0)]))
|
||||
|
||||
(defn translate-point-to-viewport [viewport zoom pt]
|
||||
(let [vbox (.. ^js viewport -viewBox -baseVal)
|
||||
brect (dom/get-bounding-rect viewport)
|
||||
brect (gpt/point (d/parse-integer (:left brect))
|
||||
(d/parse-integer (:top brect)))
|
||||
box (gpt/point (.-x vbox) (.-y vbox))
|
||||
zoom (gpt/point zoom)]
|
||||
(-> (gpt/subtract pt brect)
|
||||
(gpt/divide zoom)
|
||||
(gpt/add box)
|
||||
(gpt/round 0))))
|
||||
|
||||
(defn get-cursor [cursor]
|
||||
(case cursor
|
||||
:hand cur/hand
|
||||
:comments cur/comments
|
||||
:create-artboard cur/create-artboard
|
||||
:create-rectangle cur/create-rectangle
|
||||
:create-ellipse cur/create-ellipse
|
||||
:pen cur/pen
|
||||
:pencil cur/pencil
|
||||
:create-shape cur/create-shape
|
||||
:duplicate cur/duplicate
|
||||
cur/pointer-inner))
|
173
frontend/src/app/main/ui/workspace/viewport/widgets.cljs
Normal file
173
frontend/src/app/main/ui/workspace/viewport/widgets.cljs
Normal file
|
@ -0,0 +1,173 @@
|
|||
; 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/.
|
||||
;;
|
||||
;; This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
;; defined by the Mozilla Public License, v. 2.0.
|
||||
;;
|
||||
;; Copyright (c) 2020-2021 UXBOX Labs SL
|
||||
|
||||
(ns app.main.ui.workspace.viewport.widgets
|
||||
(:require
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.geom.shapes :as gsh]
|
||||
[app.common.pages :as cp]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.streams :as ms]
|
||||
[app.main.ui.hooks :as hooks]
|
||||
[app.main.ui.workspace.shapes.outline :refer [outline]]
|
||||
[app.main.ui.workspace.shapes.path.actions :refer [path-actions]]
|
||||
[app.util.dom :as dom]
|
||||
[clojure.set :as set]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
(mf/defc shape-outlines
|
||||
{::mf/wrap-props false}
|
||||
[props]
|
||||
(let [objects (unchecked-get props "objects")
|
||||
selected (or (unchecked-get props "selected") #{})
|
||||
hover (or (unchecked-get props "hover") #{})
|
||||
edition (unchecked-get props "edition")
|
||||
outline? (set/union selected hover)
|
||||
show-outline? (fn [shape] (and (not (:hidden shape))
|
||||
(not (:blocked shape))
|
||||
(not= edition (:id shape))
|
||||
(outline? (:id shape))))
|
||||
|
||||
shapes (cond->> (vals objects)
|
||||
show-outline? (filter show-outline?))
|
||||
|
||||
transform (mf/deref refs/current-transform)
|
||||
color (if (or (> (count shapes) 1) (nil? (:shape-ref (first shapes))))
|
||||
"#31EFB8" "#00E0FF")]
|
||||
(when (nil? transform)
|
||||
[:g.outlines
|
||||
(for [shape shapes]
|
||||
[:& outline {:key (str "outline-" (:id shape))
|
||||
:shape (gsh/transform-shape shape)
|
||||
:color color}])])))
|
||||
|
||||
|
||||
(mf/defc pixel-grid
|
||||
[{:keys [vbox zoom]}]
|
||||
[:g.pixel-grid
|
||||
[:defs
|
||||
[:pattern {:id "pixel-grid"
|
||||
:viewBox "0 0 1 1"
|
||||
:width 1
|
||||
:height 1
|
||||
:pattern-units "userSpaceOnUse"}
|
||||
[:path {:d "M 1 0 L 0 0 0 1"
|
||||
:style {:fill "none"
|
||||
:stroke "#59B9E2"
|
||||
:stroke-opacity "0.2"
|
||||
:stroke-width (str (/ 1 zoom))}}]]]
|
||||
[:rect {:x (:x vbox)
|
||||
:y (:y vbox)
|
||||
:width (:width vbox)
|
||||
:height (:height vbox)
|
||||
:fill (str "url(#pixel-grid)")
|
||||
:style {:pointer-events "none"}}]])
|
||||
|
||||
(mf/defc viewport-actions
|
||||
{::mf/wrap [mf/memo]}
|
||||
[]
|
||||
(let [edition (mf/deref refs/selected-edition)
|
||||
selected (mf/deref refs/selected-objects)
|
||||
shape (-> selected first)]
|
||||
(when (and (= (count selected) 1)
|
||||
(= (:id shape) edition)
|
||||
(= :path (:type shape)))
|
||||
[:div.viewport-actions
|
||||
[:& path-actions {:shape shape}]])))
|
||||
|
||||
(mf/defc cursor-tooltip
|
||||
[{:keys [zoom tooltip] :as props}]
|
||||
(let [coords (some-> (hooks/use-rxsub ms/mouse-position)
|
||||
(gpt/divide (gpt/point zoom zoom)))
|
||||
pos-x (- (:x coords) 100)
|
||||
pos-y (+ (:y coords) 30)]
|
||||
[:g {:transform (str "translate(" pos-x "," pos-y ")")}
|
||||
[:foreignObject {:width 200 :height 100 :style {:text-align "center"}}
|
||||
[:span tooltip]]]))
|
||||
|
||||
(mf/defc selection-rect
|
||||
{:wrap [mf/memo]}
|
||||
[{:keys [data] :as props}]
|
||||
(when data
|
||||
[:rect.selection-rect
|
||||
{:x (:x data)
|
||||
:y (:y data)
|
||||
:width (:width data)
|
||||
:height (:height data)}]))
|
||||
|
||||
;; Ensure that the label has always the same font
|
||||
;; size, regardless of zoom
|
||||
;; https://css-tricks.com/transforms-on-svg-elements/
|
||||
(defn text-transform
|
||||
[{:keys [x y]} zoom]
|
||||
(let [inv-zoom (/ 1 zoom)]
|
||||
(str
|
||||
"scale(" inv-zoom ", " inv-zoom ") "
|
||||
"translate(" (* zoom x) ", " (* zoom y) ")")))
|
||||
|
||||
(mf/defc frame-title
|
||||
[{:keys [frame modifiers selected? zoom on-frame-enter on-frame-leave on-frame-select]}]
|
||||
(let [{:keys [width x y]} frame
|
||||
label-pos (gpt/point x (- y (/ 10 zoom)))
|
||||
|
||||
on-mouse-down
|
||||
(mf/use-callback
|
||||
(mf/deps (:id frame) on-frame-select)
|
||||
(fn [event]
|
||||
(dom/prevent-default event)
|
||||
(dom/stop-propagation event)
|
||||
(on-frame-select event (:id frame))))
|
||||
|
||||
on-pointer-enter
|
||||
(mf/use-callback
|
||||
(mf/deps (:id frame) on-frame-enter)
|
||||
(fn [event]
|
||||
(on-frame-enter (:id frame))))
|
||||
|
||||
on-pointer-leave
|
||||
(mf/use-callback
|
||||
(mf/deps (:id frame) on-frame-leave)
|
||||
(fn [event]
|
||||
(on-frame-leave (:id frame))))]
|
||||
|
||||
[:text {:x 0
|
||||
:y 0
|
||||
:width width
|
||||
:height 20
|
||||
:class "workspace-frame-label"
|
||||
:transform (str (when (and selected? modifiers)
|
||||
(str (:displacement modifiers) " " ))
|
||||
(text-transform label-pos zoom))
|
||||
:style {:fill (when selected? "#28c295")}
|
||||
:on-mouse-down on-mouse-down
|
||||
:on-pointer-enter on-pointer-enter
|
||||
:on-pointer-leave on-pointer-leave}
|
||||
(:name frame)]))
|
||||
|
||||
(mf/defc frame-titles
|
||||
{::mf/wrap-props false}
|
||||
[props]
|
||||
(let [objects (unchecked-get props "objects")
|
||||
zoom (unchecked-get props "zoom")
|
||||
modifiers (unchecked-get props "modifiers")
|
||||
selected (or (unchecked-get props "selected") #{})
|
||||
on-frame-enter (unchecked-get props "on-frame-enter")
|
||||
on-frame-leave (unchecked-get props "on-frame-leave")
|
||||
on-frame-select (unchecked-get props "on-frame-select")
|
||||
frames (cp/select-frames objects)]
|
||||
|
||||
[:g.frame-titles
|
||||
(for [frame frames]
|
||||
[:& frame-title {:frame frame
|
||||
:selected? (contains? selected (:id frame))
|
||||
:zoom zoom
|
||||
:modifiers modifiers
|
||||
:on-frame-enter on-frame-enter
|
||||
:on-frame-leave on-frame-leave
|
||||
:on-frame-select on-frame-select}])]))
|
|
@ -118,33 +118,6 @@
|
|||
(into {}))
|
||||
m1))
|
||||
|
||||
(defn with-next
|
||||
"Given a collectin will return a new collection where each element
|
||||
is paried with the next item in the collection
|
||||
(with-next (range 5)) => [[0 1] [1 2] [2 3] [3 4] [4 nil]"
|
||||
[coll]
|
||||
(map vector
|
||||
coll
|
||||
(concat [] (rest coll) [nil])))
|
||||
|
||||
(defn with-prev
|
||||
"Given a collectin will return a new collection where each element
|
||||
is paried with the previous item in the collection
|
||||
(with-prev (range 5)) => [[0 nil] [1 0] [2 1] [3 2] [4 3]"
|
||||
[coll]
|
||||
(map vector
|
||||
coll
|
||||
(concat [nil] coll)))
|
||||
|
||||
(defn with-prev-next
|
||||
"Given a collection will return a new collection where every item is paired
|
||||
with the previous and the next item of a collection
|
||||
(with-prev-next (range 5)) => [[0 nil 1] [1 0 2] [2 1 3] [3 2 4] [4 3 nil]"
|
||||
[coll]
|
||||
(map vector
|
||||
coll
|
||||
(concat [nil] coll)
|
||||
(concat [] (rest coll) [nil])))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Numbers Parsing
|
||||
|
@ -248,7 +221,3 @@
|
|||
;; nil
|
||||
;; (throw e#)))))))
|
||||
|
||||
(defn prefix-keyword [prefix kw]
|
||||
(let [prefix (if (keyword? prefix) (name prefix) prefix)
|
||||
kw (if (keyword? kw) (name kw) kw)]
|
||||
(keyword (str prefix kw))))
|
||||
|
|
|
@ -277,3 +277,9 @@
|
|||
"image/svg+xml" "svg"
|
||||
"image/webp" "webp"
|
||||
nil))
|
||||
|
||||
(defn set-attribute [^js node ^string attr value]
|
||||
(.setAttribute node attr value))
|
||||
|
||||
(defn remove-attribute [^js node ^string attr]
|
||||
(.removeAttribute node attr))
|
||||
|
|
|
@ -9,11 +9,9 @@
|
|||
|
||||
(ns app.util.geom.path
|
||||
(:require
|
||||
[app.common.data :as cd]
|
||||
[app.common.data :as cd]
|
||||
[app.common.data :as d]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.util.a2c :refer [a2c]]
|
||||
[app.util.data :as d]
|
||||
[app.util.geom.path-impl-simplify :as impl-simplify]
|
||||
[app.util.svg :as usvg]
|
||||
[cuerdas.core :as str]))
|
||||
|
@ -262,24 +260,24 @@
|
|||
(cond-> command
|
||||
(:relative command)
|
||||
(-> (assoc :relative false)
|
||||
(cd/update-in-when [:params :c1x] + (:x pos))
|
||||
(cd/update-in-when [:params :c1y] + (:y pos))
|
||||
(d/update-in-when [:params :c1x] + (:x pos))
|
||||
(d/update-in-when [:params :c1y] + (:y pos))
|
||||
|
||||
(cd/update-in-when [:params :c2x] + (:x pos))
|
||||
(cd/update-in-when [:params :c2y] + (:y pos))
|
||||
(d/update-in-when [:params :c2x] + (:x pos))
|
||||
(d/update-in-when [:params :c2y] + (:y pos))
|
||||
|
||||
(cd/update-in-when [:params :cx] + (:x pos))
|
||||
(cd/update-in-when [:params :cy] + (:y pos))
|
||||
(d/update-in-when [:params :cx] + (:x pos))
|
||||
(d/update-in-when [:params :cy] + (:y pos))
|
||||
|
||||
(cd/update-in-when [:params :x] + (:x pos))
|
||||
(cd/update-in-when [:params :y] + (:y pos))
|
||||
(d/update-in-when [:params :x] + (:x pos))
|
||||
(d/update-in-when [:params :y] + (:y pos))
|
||||
|
||||
(cond->
|
||||
(= :line-to-horizontal (:command command))
|
||||
(cd/update-in-when [:params :value] + (:x pos))
|
||||
(d/update-in-when [:params :value] + (:x pos))
|
||||
|
||||
(= :line-to-vertical (:command command))
|
||||
(cd/update-in-when [:params :value] + (:y pos)))))
|
||||
(d/update-in-when [:params :value] + (:y pos)))))
|
||||
|
||||
params (:params command)
|
||||
orig-command command
|
||||
|
@ -313,7 +311,7 @@
|
|||
(update :params merge (quadratic->curve pos (gpt/point params) (calculate-opposite-handler pos prev-qc)))))
|
||||
|
||||
result (if (= :elliptical-arc (:command command))
|
||||
(cd/concat result (arc->beziers pos command))
|
||||
(d/concat result (arc->beziers pos command))
|
||||
(conj result command))
|
||||
|
||||
prev-cc (case (:command orig-command)
|
||||
|
@ -453,7 +451,7 @@
|
|||
[])))
|
||||
|
||||
(group-by first)
|
||||
(cd/mapm #(mapv second %2))))
|
||||
(d/mapm #(mapv second %2))))
|
||||
|
||||
(defn opposite-index
|
||||
"Calculate sthe opposite index given a prefix and an index"
|
||||
|
@ -552,10 +550,10 @@
|
|||
handler (gpt/add point handler-vector)
|
||||
handler-opposite (gpt/add point (gpt/negate handler-vector))]
|
||||
(-> content
|
||||
(cd/update-when index make-curve prev)
|
||||
(cd/update-when index update-handler :c2 handler)
|
||||
(cd/update-when (inc index) make-curve command)
|
||||
(cd/update-when (inc index) update-handler :c1 handler-opposite)))
|
||||
(d/update-when index make-curve prev)
|
||||
(d/update-when index update-handler :c2 handler)
|
||||
(d/update-when (inc index) make-curve command)
|
||||
(d/update-when (inc index) update-handler :c1 handler-opposite)))
|
||||
|
||||
content))]
|
||||
(as-> content $
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
[okulary.core :as l]
|
||||
[app.common.data :as d]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.geom.shapes :as geom]
|
||||
[app.common.geom.shapes :as gsh]
|
||||
[app.common.pages :as cp]
|
||||
[app.common.spec :as us]
|
||||
[app.common.uuid :as uuid]
|
||||
|
@ -44,39 +44,87 @@
|
|||
nil))
|
||||
|
||||
(defmethod impl/handler :selection/query
|
||||
[{:keys [page-id rect frame-id] :as message}]
|
||||
[{:keys [page-id rect frame-id include-frames? include-groups? disabled-masks] :or {include-groups? true
|
||||
disabled-masks #{}} :as message}]
|
||||
(when-let [index (get @state page-id)]
|
||||
(let [result (-> (qdt/search index (clj->js rect))
|
||||
(es6-iterator-seq))
|
||||
matches? (fn [shape]
|
||||
(and
|
||||
;; When not frame-id is passed, we filter the frames
|
||||
(or (and (not frame-id) (not= :frame (:type shape)))
|
||||
;; If we pass a frame-id only get the area for shapes inside that frame
|
||||
(= frame-id (:frame-id shape)))
|
||||
(geom/overlaps? shape rect)))]
|
||||
|
||||
;; Check if the shape matches the filter criteria
|
||||
match-criteria?
|
||||
(fn [shape]
|
||||
(and (not (:hidden shape))
|
||||
(or (not frame-id) (= frame-id (:frame-id shape)))
|
||||
(case (:type shape)
|
||||
:frame include-frames?
|
||||
:group include-groups?
|
||||
true)))
|
||||
|
||||
overlaps?
|
||||
(fn [shape]
|
||||
(gsh/overlaps? shape rect))
|
||||
|
||||
overlaps-masks?
|
||||
(fn [masks]
|
||||
(->> masks
|
||||
(some (comp not overlaps?))
|
||||
not))
|
||||
|
||||
;; Shapes after filters of overlapping and criteria
|
||||
matching-shapes
|
||||
(into []
|
||||
(comp (map #(unchecked-get % "data"))
|
||||
(filter match-criteria?)
|
||||
(filter (comp overlaps? :frame))
|
||||
(filter (comp overlaps-masks? :masks))
|
||||
(filter overlaps?))
|
||||
result)]
|
||||
|
||||
(into (d/ordered-set)
|
||||
(comp (map #(unchecked-get % "data"))
|
||||
(filter matches?)
|
||||
(map :id))
|
||||
result))))
|
||||
(->> matching-shapes
|
||||
(sort-by (comp - :z))
|
||||
(map :id))))))
|
||||
|
||||
(defn create-mask-index
|
||||
"Retrieves the mask information for an object"
|
||||
[objects parents-index]
|
||||
(let [retrieve-masks
|
||||
(fn [id parents]
|
||||
(->> parents
|
||||
(map #(get objects %))
|
||||
(filter #(:masked-group? %))
|
||||
;; Retrieve the masking element
|
||||
(mapv #(get objects (->> % :shapes first)))))]
|
||||
(->> parents-index
|
||||
(d/mapm retrieve-masks))))
|
||||
|
||||
(defn- create-index
|
||||
[objects]
|
||||
(let [shapes (cp/select-toplevel-shapes objects {:include-frames? true})
|
||||
bounds (geom/selection-rect shapes)
|
||||
(let [shapes (-> objects (dissoc uuid/zero) (vals))
|
||||
z-index (cp/calculate-z-index objects)
|
||||
parents-index (cp/generate-child-all-parents-index objects)
|
||||
masks-index (create-mask-index objects parents-index)
|
||||
bounds (gsh/selection-rect shapes)
|
||||
bounds #js {:x (:x bounds)
|
||||
:y (:y bounds)
|
||||
:width (:width bounds)
|
||||
:height (:height bounds)}]
|
||||
(reduce index-object
|
||||
|
||||
(reduce (partial index-object objects z-index parents-index masks-index)
|
||||
(qdt/create bounds)
|
||||
shapes)))
|
||||
|
||||
(defn- index-object
|
||||
[index obj]
|
||||
(let [{:keys [id x y width height]} (:selrect obj)
|
||||
rect #js {:x x :y y :width width :height height}]
|
||||
(qdt/insert index rect obj)))
|
||||
[objects z-index parents-index masks-index index obj]
|
||||
(let [{:keys [x y width height]} (:selrect obj)
|
||||
shape-bound #js {:x x :y y :width width :height height}
|
||||
parents (get parents-index (:id obj))
|
||||
masks (get masks-index (:id obj))
|
||||
z (get z-index (:id obj))
|
||||
frame (when (and (not= :frame (:type obj))
|
||||
(not= (:frame-id obj) uuid/zero))
|
||||
(get objects (:frame-id obj)))]
|
||||
(qdt/insert index
|
||||
shape-bound
|
||||
(assoc obj :frame frame :masks masks :parents parents :z z))))
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue