0
Fork 0
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:
alonso.torres 2021-03-18 21:58:29 +01:00
parent 5c31830edb
commit 136a48a18f
54 changed files with 2081 additions and 1521 deletions

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

@ -349,6 +349,7 @@
z-index: 1000;
pointer-events: none;
overflow: hidden;
user-select: text;
.threads {
position: absolute;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View 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 "-"))]]))

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -50,3 +50,4 @@
(if (= drawing-tool :comments)
[:& comments-sidebar]
[:> options-toolbox props])]]))

View file

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

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

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

@ -0,0 +1,84 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; 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)}]))))

View file

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

View file

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

View file

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

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

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

View file

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

View file

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

View file

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

View file

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