0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-04-13 15:31:26 -05:00

Merge pull request #187 from uxbox/245/shapes_transforms_improvements

245/shapes transforms improvements
This commit is contained in:
Andrey Antukh 2020-04-27 14:13:26 +02:00 committed by GitHub
commit a7cb90919f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 1330 additions and 1062 deletions

View file

@ -108,10 +108,11 @@
}
.workspace-viewport {
height: calc(100% - 40px);
height: 100%;
overflow: scroll;
transition: none;
width: 100%;
margin-top: 20px;
.viewport {
&.drawing {

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,273 @@
(ns uxbox.main.data.workspace.common
(:require
[clojure.set :as set]
[cljs.spec.alpha :as s]
[beicon.core :as rx]
[potok.core :as ptk]
[uxbox.main.worker :as uw]
[uxbox.util.geom.shapes :as geom]
[uxbox.common.data :as d]
[uxbox.common.spec :as us]
[uxbox.common.pages :as cp]
[uxbox.common.uuid :as uuid]))
;; --- Protocols
(defprotocol IBatchedChange)
(defprotocol IUpdateGroup
(get-ids [this]))
(declare append-undo)
(declare reset-undo)
(declare commit-changes)
(declare calculate-shape-to-frame-relationship-changes)
(defn- retrieve-toplevel-shapes
[objects]
(let [lookup #(get objects %)
root (lookup uuid/zero)
childs (:shapes root)]
(loop [id (first childs)
ids (rest childs)
res []]
(if (nil? id)
res
(let [obj (lookup id)
typ (:type obj)]
(recur (first ids)
(rest ids)
(if (= :frame typ)
(into res (:shapes obj))
(conj res id))))))))
(defn rehash-shape-frame-relationship
[ids]
(ptk/reify ::rehash-shape-frame-relationship
ptk/WatchEvent
(watch [_ state stream]
(let [page-id (:current-page-id state)
objects (get-in state [:workspace-data page-id :objects])
ids (retrieve-toplevel-shapes objects)
[rch uch] (calculate-shape-to-frame-relationship-changes objects ids)]
(when-not (empty? rch)
(rx/of (commit-changes rch uch {:commit-local? true})))))))
(defn- generate-operations
[ma mb]
(let [ma-keys (set (keys ma))
mb-keys (set (keys mb))
added (set/difference mb-keys ma-keys)
removed (set/difference ma-keys mb-keys)
both (set/intersection ma-keys mb-keys)]
(d/concat
(mapv #(array-map :type :set :attr % :val (get mb %)) added)
(mapv #(array-map :type :set :attr % :val nil) removed)
(loop [k (first both)
r (rest both)
rs []]
(if k
(let [vma (get ma k)
vmb (get mb k)]
(if (= vma vmb)
(recur (first r) (rest r) rs)
(recur (first r) (rest r) (conj rs {:type :set
:attr k
:val vmb}))))
rs)))))
(defn- generate-changes
[prev curr]
(letfn [(impl-diff [res id]
(let [prev-obj (get-in prev [:objects id])
curr-obj (get-in curr [:objects id])
ops (generate-operations (dissoc prev-obj :shapes :frame-id)
(dissoc curr-obj :shapes :frame-id))]
(if (empty? ops)
res
(conj res {:type :mod-obj
:operations ops
:id id}))))]
(reduce impl-diff [] (set/union (set (keys (:objects prev)))
(set (keys (:objects curr)))))))
(defn- update-selection-index
[page-id]
(ptk/reify ::update-selection-index
ptk/EffectEvent
(effect [_ state stream]
(let [objects (get-in state [:workspace-pages page-id :data :objects])
lookup #(get objects %)]
(uw/ask! {:cmd :selection/update-index
:page-id page-id
:objects objects})))))
(defn commit-changes
([changes undo-changes] (commit-changes changes undo-changes {}))
([changes undo-changes {:keys [save-undo?
commit-local?]
:or {save-undo? true
commit-local? false}
:as opts}]
(us/verify ::cp/changes changes)
(us/verify ::cp/changes undo-changes)
(ptk/reify ::commit-changes
cljs.core/IDeref
(-deref [_] changes)
ptk/UpdateEvent
(update [_ state]
(let [page-id (:current-page-id state)
state (update-in state [:workspace-pages page-id :data] cp/process-changes changes)]
(cond-> state
commit-local? (update-in [:workspace-data page-id] cp/process-changes changes))))
ptk/WatchEvent
(watch [_ state stream]
(let [page (:workspace-page state)
uidx (get-in state [:workspace-local :undo-index] ::not-found)]
(rx/concat
(rx/of (update-selection-index (:id page)))
(when (and save-undo? (not= uidx ::not-found))
(rx/of (reset-undo uidx)))
(when save-undo?
(let [entry {:undo-changes undo-changes
:redo-changes changes}]
(rx/of (append-undo entry))))))))))
(defn diff-and-commit-changes
[page-id]
(ptk/reify ::diff-and-commit-changes
ptk/WatchEvent
(watch [_ state stream]
(let [page-id (:current-page-id state)
curr (get-in state [:workspace-data page-id])
prev (get-in state [:workspace-pages page-id :data])
changes (generate-changes prev curr)
undo-changes (generate-changes curr prev)]
(when-not (empty? changes)
(rx/of (commit-changes changes undo-changes)))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Undo/Redo
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def MAX-UNDO-SIZE 50)
(defn- conj-undo-entry
[undo data]
(let [undo (conj undo data)]
(if (> (count undo) MAX-UNDO-SIZE)
(into [] (take MAX-UNDO-SIZE undo))
undo)))
(defn- materialize-undo
[changes index]
(ptk/reify ::materialize-undo
ptk/UpdateEvent
(update [_ state]
(let [page-id (:current-page-id state)]
(-> state
(update-in [:workspace-data page-id] cp/process-changes changes)
(assoc-in [:workspace-local :undo-index] index))))))
(defn- reset-undo
[index]
(ptk/reify ::reset-undo
ptk/UpdateEvent
(update [_ state]
(-> state
(update :workspace-local dissoc :undo-index)
(update-in [:workspace-local :undo]
(fn [queue]
(into [] (take (inc index) queue))))))))
(s/def ::undo-changes ::cp/changes)
(s/def ::redo-changes ::cp/changes)
(s/def ::undo-entry
(s/keys :req-un [::undo-changes ::redo-changes]))
(defn- append-undo
[entry]
(us/verify ::undo-entry entry)
(ptk/reify ::append-undo
ptk/UpdateEvent
(update [_ state]
(update-in state [:workspace-local :undo] (fnil conj-undo-entry []) entry))))
(def undo
(ptk/reify ::undo
ptk/WatchEvent
(watch [_ state stream]
(let [local (:workspace-local state)
undo (:undo local [])
index (or (:undo-index local)
(dec (count undo)))]
(when-not (or (empty? undo) (= index -1))
(let [changes (get-in undo [index :undo-changes])]
(rx/of (materialize-undo changes (dec index))
(commit-changes changes [] {:save-undo? false}))))))))
(def redo
(ptk/reify ::redo
ptk/WatchEvent
(watch [_ state stream]
(let [local (:workspace-local state)
undo (:undo local [])
index (or (:undo-index local)
(dec (count undo)))]
(when-not (or (empty? undo) (= index (dec (count undo))))
(let [changes (get-in undo [(inc index) :redo-changes])]
(rx/of (materialize-undo changes (inc index))
(commit-changes changes [] {:save-undo? false}))))))))
(def reinitialize-undo
(ptk/reify ::reset-undo
ptk/UpdateEvent
(update [_ state]
(update state :workspace-local dissoc :undo-index :undo))))
(defn- calculate-frame-overlap
[objects shape]
(let [rshp (geom/shape->rect-shape shape)
xfmt (comp
(filter #(= :frame (:type %)))
(filter #(not= (:id shape) (:id %)))
(filter #(not= uuid/zero (:id %)))
(filter #(geom/overlaps? % rshp)))
frame (->> (vals objects)
(sequence xfmt)
(first))]
(or (:id frame) uuid/zero)))
(defn- calculate-shape-to-frame-relationship-changes
[objects ids]
(loop [id (first ids)
ids (rest ids)
rch []
uch []]
(if (nil? id)
[rch uch]
(let [obj (get objects id)
fid (calculate-frame-overlap objects obj)]
(if (not= fid (:frame-id obj))
(recur (first ids)
(rest ids)
(conj rch {:type :mov-objects
:parent-id fid
:shapes [id]})
(conj uch {:type :mov-objects
:parent-id (:frame-id obj)
:shapes [id]}))
(recur (first ids)
(rest ids)
rch
uch))))))

View file

@ -0,0 +1,321 @@
(ns uxbox.main.data.workspace.transforms
"Events related with shapes transformations"
(:require
[cljs.spec.alpha :as s]
[beicon.core :as rx]
[potok.core :as ptk]
[uxbox.common.spec :as us]
[uxbox.common.data :as d]
[uxbox.main.refs :as refs]
[uxbox.main.store :as st]
[uxbox.main.streams :as ms]
[uxbox.util.geom.shapes :as gsh]
[uxbox.util.geom.point :as gpt]
[uxbox.util.geom.matrix :as gmt]
[uxbox.main.data.helpers :as helpers]
[uxbox.main.data.workspace.common :refer [IBatchedChange IUpdateGroup] :as common]))
;; -- Declarations
(declare set-modifiers)
(declare set-rotation)
(declare apply-modifiers)
;; -- Helpers
(defn- apply-zoom
[point]
(gpt/divide point (gpt/point @refs/selected-zoom)))
;; For each of the 8 handlers gives the modifier for resize
;; for example, right will only grow in the x coordinate and left
;; will grow in the inverse of the x coordinate
(def ^:private handler-modifiers
{:right [ 1 0]
:bottom [ 0 1]
:left [-1 0]
:top [ 0 -1]
:top-right [ 1 -1]
:top-left [-1 -1]
:bottom-right [ 1 1]
:bottom-left [-1 1]})
;; Given a handler returns the coordinate origin for resizes
;; this is the opposite of the handler so for right we want the
;; left side as origin of the resize
;; sx, sy => start x/y
;; mx, my => middle x/y
;; ex, ey => end x/y
(defn- handler-resize-origin [{sx :x sy :y :keys [width height]} handler]
(let [mx (+ sx (/ width 2))
my (+ sy (/ height 2))
ex (+ sx width)
ey (+ sy height)
[x y] (case handler
:right [sx my]
:bottom [mx sy]
:left [ex my]
:top [mx ey]
:top-right [sx ey]
:top-left [ex ey]
:bottom-right [sx sy]
:bottom-left [ex sy])]
(gpt/point x y)))
;; -- RESIZE
(defn start-resize
[handler ids shape objects]
(letfn [(resize [shape initial [point lock?]]
(let [frame (get objects (:frame-id shape))
{:keys [width height rotation]} shape
center (gpt/center shape)
shapev (-> (gpt/point width height))
;; Vector modifiers depending on the handler
handler-modif (let [[x y] (handler-modifiers handler)] (gpt/point x y))
;; Difference between the origin point in the coordinate system of the rotation
deltav (-> (gpt/subtract point initial)
(gpt/transform (gmt/rotate-matrix (- rotation)))
(gpt/multiply handler-modif))
;; Resize vector
scalev (gpt/divide (gpt/add shapev deltav) shapev)
scalev (if lock? (let [v (max (:x scalev) (:y scalev))] (gpt/point v v)) scalev)
shape-transform (:transform shape (gmt/matrix))
shape-transform-inverse (:transform-inverse shape (gmt/matrix))
;; Resize origin point given the selected handler
origin (-> (handler-resize-origin shape handler)
(gsh/transform-shape-point shape shape-transform))]
(rx/of (set-modifiers ids
{:resize-vector scalev
:resize-origin origin
:resize-transform shape-transform
:resize-transform-inverse shape-transform-inverse}
false))))
;; Unifies the instantaneous proportion lock modifier
;; activated by Ctrl key and the shapes own proportion
;; lock flag that can be activated on element options.
(normalize-proportion-lock [[point ctrl?]]
(let [proportion-lock? (:proportion-lock shape)]
[point (or proportion-lock? ctrl?)]))
;; Applies alginment to point if it is currently
;; activated on the current workspace
;; (apply-grid-alignment [point]
;; (if @refs/selected-alignment
;; (uwrk/align-point point)
;; (rx/of point)))
]
(reify
ptk/WatchEvent
(watch [_ state stream]
(let [initial (apply-zoom @ms/mouse-position)
shape (gsh/shape->rect-shape shape)
stoper (rx/filter ms/mouse-up? stream)]
(rx/concat
(->> ms/mouse-position
(rx/map apply-zoom)
;; (rx/mapcat apply-grid-alignment)
(rx/with-latest vector ms/mouse-position-ctrl)
(rx/map normalize-proportion-lock)
(rx/mapcat (partial resize shape initial))
(rx/take-until stoper))
#_(rx/empty)
(rx/of (apply-modifiers ids))))))))
;; -- ROTATE
(defn start-rotate
[shapes]
(ptk/reify ::start-rotate
ptk/WatchEvent
(watch [_ state stream]
(let [stoper (rx/filter ms/mouse-up? stream)
group (gsh/selection-rect shapes)
group-center (gpt/center group)
initial-angle (gpt/angle (apply-zoom @ms/mouse-position) group-center)
calculate-angle (fn [pos ctrl?]
(let [angle (- (gpt/angle pos group-center) initial-angle)
angle (if (neg? angle) (+ 360 angle) angle)
modval (mod angle 90)
angle (if ctrl?
(if (< 50 modval)
(+ angle (- 90 modval))
(- angle modval))
angle)
angle (if (= angle 360)
0
angle)]
angle))]
(rx/concat
(->> ms/mouse-position
(rx/map apply-zoom)
(rx/with-latest vector ms/mouse-position-ctrl)
(rx/map (fn [[pos ctrl?]]
(let [delta-angle (calculate-angle pos ctrl?)]
(set-rotation delta-angle shapes))))
(rx/take-until stoper))
(rx/of (apply-modifiers (map :id shapes))))))))
;; -- MOVE
(defn start-move-selected []
(ptk/reify ::start-move-selected
ptk/WatchEvent
(watch [_ state stream]
(let [selected (get-in state [:workspace-local :selected])
stoper (rx/filter ms/mouse-up? stream)
zero-point? #(= % (gpt/point 0 0))
initial (apply-zoom @ms/mouse-position)
position @ms/mouse-position]
(rx/concat
(->> ms/mouse-position
(rx/map apply-zoom)
(rx/filter (complement zero-point?))
(rx/map #(gpt/subtract % initial))
(rx/map #(set-modifiers selected {:displacement (gmt/translate-matrix %)}))
(rx/take-until stoper))
(rx/of (apply-modifiers selected)))))))
(defn- get-displacement-with-grid
"Retrieve the correct displacement delta point for the
provided direction speed and distances thresholds."
[shape direction options]
(let [grid-x (:grid-x options 10)
grid-y (:grid-y options 10)
x-mod (mod (:x shape) grid-x)
y-mod (mod (:y shape) grid-y)]
(case direction
:up (gpt/point 0 (- (if (zero? y-mod) grid-y y-mod)))
:down (gpt/point 0 (- grid-y y-mod))
:left (gpt/point (- (if (zero? x-mod) grid-x x-mod)) 0)
:right (gpt/point (- grid-x x-mod) 0))))
(defn- get-displacement
"Retrieve the correct displacement delta point for the
provided direction speed and distances thresholds."
[shape direction]
(case direction
:up (gpt/point 0 (- 1))
:down (gpt/point 0 1)
:left (gpt/point (- 1) 0)
:right (gpt/point 1 0)))
(defn move-selected
[direction align?]
(us/verify ::direction direction)
(us/verify boolean? align?)
(ptk/reify ::move-selected
ptk/WatchEvent
(watch [_ state stream]
(let [pid (:current-page-id state)
selected (get-in state [:workspace-local :selected])
options (get-in state [:workspace-data pid :options])
shapes (map #(get-in state [:workspace-data pid :objects %]) selected)
shape (gsh/shapes->rect-shape shapes)
displacement (if align?
(get-displacement-with-grid shape direction options)
(get-displacement shape direction))]
(rx/of (set-modifiers selected displacement)
(apply-modifiers selected))))))
;; -- Apply modifiers
(defn set-modifiers
([ids modifiers] (set-modifiers ids modifiers true))
([ids modifiers recurse-frames?]
(us/verify (s/coll-of uuid?) ids)
(ptk/reify ::set-modifiers
ptk/UpdateEvent
(update [_ state]
(let [page-id (:current-page-id state)
objects (get-in state [:workspace-data page-id :objects])
not-frame-id? (fn [shape-id]
(let [shape (get objects shape-id)]
(or recurse-frames? (not (= :frame (:type shape))))))
;; ID's + Children but remove frame children if the flag is set to false
ids-with-children (concat ids (mapcat #(helpers/get-children % objects) (filter not-frame-id? ids)))
;; For each shape updates the modifiers given as arguments
update-shape (fn [state shape-id]
(update-in
state
[:workspace-data page-id :objects shape-id :modifiers]
#(merge % modifiers)))]
(reduce update-shape state ids-with-children))))))
(defn rotation-modifiers [center shape angle]
(let [displacement (let [shape-center (gsh/center shape)]
(-> (gmt/matrix)
(gmt/rotate angle center)
(gmt/rotate (- angle) shape-center)))]
{:rotation angle
:displacement displacement}))
;; Set-rotation is custom because applies different modifiers to each shape adjusting their position
(defn set-rotation
[delta-rotation shapes]
(ptk/reify ::set-rotation
IUpdateGroup
(get-ids [_] (map :id shapes))
ptk/UpdateEvent
(update [_ state]
(let [page-id (:current-page-id state)]
(letfn [(rotate-shape [state angle shape center]
(let [objects (get-in state [:workspace-data page-id :objects])
path [:workspace-data page-id :objects (:id shape) :modifiers]
modifiers (rotation-modifiers center shape angle)]
(-> state
(update-in path merge modifiers))))
(rotate-around-center [state angle center shapes]
(reduce #(rotate-shape %1 angle %2 center) state shapes))]
(let [center (-> shapes gsh/selection-rect gpt/center)
objects (get-in state [:workspace-data page-id :objects])
id->obj #(get objects %)
get-children (fn [shape] (map id->obj (helpers/get-children (:id shape) objects)))
shapes (concat shapes (mapcat get-children shapes))]
(rotate-around-center state delta-rotation center shapes)))))))
(defn apply-modifiers
[ids]
(us/verify (s/coll-of uuid?) ids)
(ptk/reify ::apply-modifiers
IUpdateGroup
(get-ids [_] ids)
ptk/UpdateEvent
(update [_ state]
(let [page-id (:current-page-id state)
objects (get-in state [:workspace-data page-id :objects])
;; ID's + Children
ids-with-children (concat ids (mapcat #(helpers/get-children % objects) ids))
;; For each shape applies the modifiers by transforming the objects
update-shape
(fn [state shape-id]
(update-in state [:workspace-data page-id :objects shape-id] gsh/transform-shape))]
(reduce update-shape state ids-with-children)))
ptk/WatchEvent
(watch [_ state stream]
(let [page-id (:current-page-id state)]
(rx/of (common/diff-and-commit-changes page-id)
(common/rehash-shape-frame-relationship ids))))))

View file

@ -10,7 +10,7 @@
[rumext.alpha :as mf]
[uxbox.common.uuid :as uuid]
[uxbox.util.math :as mth]
[uxbox.main.geom :as geom]
[uxbox.util.geom.shapes :as geom]
[uxbox.util.geom.point :as gpt]
[uxbox.util.geom.matrix :as gmt]
[uxbox.main.ui.shapes.frame :as frame]

View file

@ -0,0 +1,46 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) 2020 UXBOX Labs SL
(ns uxbox.main.ui.shapes.bounding-box
(:require
[cuerdas.core :as str]
[rumext.alpha :as mf]
[uxbox.util.geom.shapes :as geom]
[uxbox.util.debug :as debug]
[uxbox.util.geom.matrix :as gmt]
[uxbox.util.geom.point :as gpt]
[uxbox.util.debug :refer [debug?]]))
(defn fix [num]
(when num (.toFixed num 2)))
(mf/defc bounding-box
{::mf/wrap-props false}
[props]
(when (debug? :bounding-boxes)
(let [shape (unchecked-get props "shape")
frame (unchecked-get props "frame")
selrect (geom/transform-selrect frame shape)
shape-path (-> shape
(geom/transform-apply-modifiers)
(geom/translate-to-frame frame))
shape-center (geom/center shape-path)]
[:g
[:text {:x (:x selrect)
:y (- (:y selrect) 5)
:font-size 10
:fill "red"
:stroke "white"
:stroke-width 0.1}
(str/format "%s - (%s, %s)" (str/slice (str (:id shape)) 0 8) (fix (:x shape)) (fix (:y shape)))]
[:circle {:cx (:x shape-center) :cy (:y shape-center) :r 5 :fill "yellow"}]
[:rect {:x (:x selrect)
:y (:y selrect)
:width (:width selrect)
:height (:height selrect)
:style {:stroke "red" :fill "transparent" :stroke-width "1px" :stroke-opacity 0.5}}]])))

View file

@ -7,13 +7,14 @@
(ns uxbox.main.ui.shapes.circle
(:require
[rumext.alpha :as mf]
[uxbox.main.geom :as geom]
[uxbox.util.geom.shapes :as geom]
[uxbox.main.refs :as refs]
[uxbox.main.ui.shapes.attrs :as attrs]
[uxbox.main.ui.shapes.common :as common]
[uxbox.util.interop :as itr]
[uxbox.util.geom.matrix :as gmt]
[uxbox.util.geom.point :as gpt]))
[uxbox.util.geom.point :as gpt]
[uxbox.main.ui.shapes.bounding-box :refer [bounding-box]]))
;; --- Circle Wrapper
@ -28,19 +29,22 @@
[:g.shape {:class (when selected? "selected")
:on-mouse-down on-mouse-down
:on-context-menu on-context-menu}
[:& circle-shape {:shape (geom/transform-shape frame shape)}]]))
[:& circle-shape {:shape (geom/transform-shape frame shape)}]
[:& bounding-box {:shape shape :frame frame}]]))
;; --- Circle Shape
(mf/defc circle-shape
[{:keys [shape] :as props}]
(let [{:keys [id cx cy rx ry rotation]} shape
{::mf/wrap-props false}
[props]
(let [shape (unchecked-get props "shape")
{:keys [id x y width height]} shape
transform (geom/transform-matrix shape)
center (gpt/point cx cy)
rotation (or rotation 0)
transform (when (pos? rotation)
(str (-> (gmt/matrix)
(gmt/rotate rotation center))))
cx (+ x (/ width 2))
cy (+ y (/ height 2))
rx (/ width 2)
ry (/ height 2)
props (-> (attrs/extract-style-attrs shape)
(itr/obj-assign!

View file

@ -20,44 +20,12 @@
[uxbox.main.store :as st]
[uxbox.main.ui.keyboard :as kbd]
[uxbox.main.streams :as uws]
[uxbox.main.geom :as geom]
[uxbox.util.geom.shapes :as geom]
[uxbox.util.geom.matrix :as gmt]
[uxbox.util.geom.point :as gpt]
[uxbox.util.dom :as dom]))
;; --- Shape Movement (by mouse)
(def start-move-selected
(ptk/reify ::start-move-selected
ptk/WatchEvent
(watch [_ state stream]
(let [selected (get-in state [:workspace-local :selected])
stoper (rx/filter uws/mouse-up? stream)
zero-point? #(= % (gpt/point 0 0))
position @uws/mouse-position]
(rx/concat
(->> (uws/mouse-position-deltas position)
(rx/filter (complement zero-point?))
(rx/map #(dw/apply-displacement-in-bulk selected %))
(rx/take-until stoper))
(rx/of (dw/materialize-displacement-in-bulk selected)))))))
(def start-move-frame
(ptk/reify ::start-move-frame
ptk/WatchEvent
(watch [_ state stream]
(let [selected (get-in state [:workspace-local :selected])
stoper (rx/filter uws/mouse-up? stream)
zero-point? #(= % (gpt/point 0 0))
frame-id (first selected)
position @uws/mouse-position]
(rx/concat
(->> (uws/mouse-position-deltas position)
(rx/filter (complement zero-point?))
(rx/map #(dw/apply-frame-displacement frame-id %))
(rx/take-until stoper))
(rx/of (dw/materialize-frame-displacement frame-id)))))))
(defn on-mouse-down
[event {:keys [id type] :as shape}]
(let [selected @refs/selected-shapes
@ -75,14 +43,14 @@
(= type :frame)
(when selected?
(dom/stop-propagation event)
(st/emit! start-move-frame))
(st/emit! (dw/start-move-selected)))
(and (not selected?) (empty? selected))
(do
(dom/stop-propagation event)
(st/emit! dw/deselect-all
(dw/select-shape id)
start-move-selected))
(dw/start-move-selected)))
(and (not selected?) (not (empty? selected)))
(do
@ -91,11 +59,11 @@
(st/emit! (dw/select-shape id))
(st/emit! dw/deselect-all
(dw/select-shape id)
start-move-selected)))
(dw/start-move-selected))))
:else
(do
(dom/stop-propagation event)
(st/emit! start-move-selected))))))
(st/emit! (dw/start-move-selected)))))))
(defn on-context-menu

View file

@ -14,7 +14,7 @@
[uxbox.common.data :as d]
[uxbox.main.constants :as c]
[uxbox.main.data.workspace :as dw]
[uxbox.main.geom :as geom]
[uxbox.util.geom.shapes :as geom]
[uxbox.main.refs :as refs]
[uxbox.main.store :as st]
[uxbox.main.ui.shapes.attrs :as attrs]

View file

@ -11,17 +11,17 @@
(:require
[cuerdas.core :as str]
[rumext.alpha :as mf]
[uxbox.main.geom :as geom]
[uxbox.util.geom.shapes :as geom]
[uxbox.main.refs :as refs]
[uxbox.util.dom :as dom]
[uxbox.util.interop :as itr]
[uxbox.util.debug :refer [debug?]]
[uxbox.main.ui.shapes.common :as common]
[uxbox.main.ui.shapes.attrs :as attrs]
[uxbox.main.data.workspace :as dw]
[uxbox.main.store :as st]
[uxbox.main.streams :as ms]))
(defonce ^:dynamic *debug* (atom false))
[uxbox.main.streams :as ms]
[uxbox.main.ui.shapes.bounding-box :refer [bounding-box]]))
(defn- equals?
[np op]
@ -75,7 +75,9 @@
{:frame frame
:shape (geom/transform-shape frame shape)
:children children
:is-child-selected? is-child-selected?}]]))))
:is-child-selected? is-child-selected?}]
(when (not is-child-selected?)
[:& bounding-box {:shape shape :frame frame}])]))))
(defn group-shape
[shape-wrapper]
@ -86,28 +88,18 @@
shape (unchecked-get props "shape")
children (unchecked-get props "children")
is-child-selected? (unchecked-get props "is-child-selected?")
{:keys [id x y width height rotation
displacement-modifier
resize-modifier]} shape
transform (when (and rotation (pos? rotation))
(str/format "rotate(%s %s %s)"
rotation
(+ x (/ width 2))
(+ y (/ height 2))))]
[:g {:transform transform}
{:keys [id x y width height]} shape
transform (geom/transform-matrix shape)]
[:g
(for [item children]
[:& shape-wrapper
{:frame frame
:shape (cond-> item
displacement-modifier (assoc :displacement-modifier displacement-modifier)
resize-modifier (assoc :resize-modifier resize-modifier))
:key (:id item)}])
{:frame frame :shape item :key (:id item)}])
(when (not is-child-selected?)
[:rect {:x x
[:rect {:transform transform
:x x
:y y
:fill (if (deref *debug*) "red" "transparent")
:fill (if (debug? :group) "red" "transparent")
:opacity 0.5
:id (str "group-" id)
:width width

View file

@ -8,7 +8,7 @@
(:require
[rumext.alpha :as mf]
[cuerdas.core :as str]
[uxbox.main.geom :as geom]
[uxbox.util.geom.shapes :as geom]
[uxbox.main.refs :as refs]
[uxbox.main.ui.shapes.attrs :as attrs]
[uxbox.main.ui.shapes.common :as common]

View file

@ -9,13 +9,14 @@
[rumext.alpha :as mf]
[cuerdas.core :as str]
[uxbox.main.data.images :as udi]
[uxbox.main.geom :as geom]
[uxbox.util.geom.shapes :as geom]
[uxbox.main.refs :as refs]
[uxbox.main.store :as st]
[uxbox.main.ui.shapes.attrs :as attrs]
[uxbox.main.ui.shapes.common :as common]
[uxbox.util.interop :as itr]
[uxbox.util.geom.matrix :as gmt]))
[uxbox.util.geom.matrix :as gmt]
[uxbox.main.ui.shapes.bounding-box :refer [bounding-box]]))
;; --- Image Wrapper
@ -36,19 +37,15 @@
[:g.shape {:class (when selected? "selected")
:on-mouse-down on-mouse-down
:on-context-menu on-context-menu}
[:& image-shape {:shape (geom/transform-shape frame shape)}]]))
[:& image-shape {:shape (geom/transform-shape frame shape)}]
[:& bounding-box {:shape shape :frame frame}]]))
;; --- Image Shape
(mf/defc image-shape
[{:keys [shape] :as props}]
(let [{:keys [id x y width height rotation metadata]} shape
transform (when (and rotation (pos? rotation))
(str/format "rotate(%s %s %s)"
rotation
(+ x (/ width 2))
(+ y (/ height 2))))
transform (geom/transform-matrix shape)
uri (if (or (> (:thumb-width metadata) width)
(> (:thumb-height metadata) height))
(:thumb-uri metadata)

View file

@ -9,13 +9,14 @@
[cuerdas.core :as str :include-macros true]
[rumext.alpha :as mf]
[uxbox.main.data.workspace :as dw]
[uxbox.main.geom :as geom]
[uxbox.util.geom.shapes :as geom]
[uxbox.main.refs :as refs]
[uxbox.main.store :as st]
[uxbox.main.ui.shapes.attrs :as attrs]
[uxbox.main.ui.shapes.common :as common]
[uxbox.util.interop :as itr]
[uxbox.util.geom.matrix :as gmt]))
[uxbox.util.geom.matrix :as gmt]
[uxbox.main.ui.shapes.bounding-box :refer [bounding-box]]))
;; --- Path Wrapper
@ -25,17 +26,23 @@
[{:keys [shape frame] :as props}]
(let [selected (mf/deref refs/selected-shapes)
selected? (contains? selected (:id shape))
on-mouse-down #(common/on-mouse-down % shape)
on-context-menu #(common/on-context-menu % shape)
on-double-click
(fn [event]
(when selected?
(st/emit! (dw/start-edition-mode (:id shape)))))]
on-mouse-down (mf/use-callback
(mf/deps shape)
#(common/on-mouse-down % shape))
on-context-menu (mf/use-callback
(mf/deps shape)
#(common/on-context-menu % shape))
on-double-click (mf/use-callback
(mf/deps shape)
(fn [event]
(when selected?
(st/emit! (dw/start-edition-mode (:id shape))))))]
[:g.shape {:on-double-click on-double-click
:on-mouse-down on-mouse-down
:on-context-menu on-context-menu}
[:& path-shape {:shape (geom/transform-shape frame shape)
:background? true}]]))
[:& path-shape {:shape (geom/transform-shape frame shape) :background? true}]
[:& bounding-box {:shape shape :frame frame}]]))
;; --- Path Shape
@ -62,14 +69,8 @@
(mf/defc path-shape
[{:keys [shape background?] :as props}]
(let [{:keys [id x y width height rotation]} (geom/shape->rect-shape shape)
transform (when (and rotation (pos? rotation))
(str/format "rotate(%s %s %s)"
rotation
(+ x (/ width 2))
(+ y (/ height 2))))
(let [{:keys [id x y width height]} (geom/shape->rect-shape shape)
transform (geom/transform-matrix shape)
pdata (render-path shape)
props (-> (attrs/extract-style-attrs shape)
(itr/obj-assign!

View file

@ -10,11 +10,12 @@
[cuerdas.core :as str]
[uxbox.util.geom.matrix :as gmt]
[uxbox.util.geom.point :as gpt]
[uxbox.main.geom :as geom]
[uxbox.util.geom.shapes :as geom]
[uxbox.main.refs :as refs]
[uxbox.main.ui.shapes.attrs :as attrs]
[uxbox.main.ui.shapes.common :as common]
[uxbox.util.interop :as itr]))
[uxbox.util.interop :as itr]
[uxbox.main.ui.shapes.bounding-box :refer [bounding-box]]))
;; --- Rect Wrapper
@ -33,7 +34,8 @@
#(common/on-context-menu % shape))]
[:g.shape {:on-mouse-down on-mouse-down
:on-context-menu on-context-menu}
[:& rect-shape {:shape (geom/transform-shape frame shape) }]]))
[:& rect-shape {:shape (geom/transform-shape frame shape) }]
[:& bounding-box {:shape shape :frame frame}]]))
;; --- Rect Shape
@ -41,11 +43,8 @@
{::mf/wrap-props false}
[props]
(let [shape (unchecked-get props "shape")
{:keys [id x y width height rotation]} shape
center (gpt/center shape)
transform (when (pos? rotation)
(str (-> (gmt/matrix)
(gmt/rotate rotation center))))
{:keys [id x y width height]} shape
transform (geom/transform-matrix shape)
props (-> (attrs/extract-style-attrs shape)
(itr/obj-assign!
#js {:x x

View file

@ -12,7 +12,7 @@
[lentes.core :as l]
[rumext.alpha :as mf]
[uxbox.main.data.workspace :as dw]
[uxbox.main.geom :as geom]
[uxbox.util.geom.shapes :as geom]
[uxbox.main.refs :as refs]
[uxbox.main.store :as st]
[uxbox.main.ui.shapes.common :as common]

View file

@ -12,7 +12,7 @@
[rumext.alpha :as mf]
[uxbox.main.constants :as c]
[uxbox.main.data.workspace :as dw]
[uxbox.main.geom :as geom]
[uxbox.util.geom.shapes :as geom]
[uxbox.main.refs :as refs]
[uxbox.main.store :as st]
[uxbox.main.streams :as ms]
@ -112,19 +112,24 @@
(let [shape (get-in state [:workspace-local :drawing])
shape (geom/setup shape {:x (:x point)
:y (:y point)
:width 2
:height 2})]
:width 10
:height 10})]
(assoc-in state [:workspace-local :drawing] (assoc shape ::initialized? true))))
(resize-shape [shape point lock?]
(resize-shape [{:keys [x y] :as shape} initial point lock?]
(let [shape' (geom/shape->rect-shape shape)
result (geom/resize-shape :bottom-right shape' point lock?)
scale (geom/calculate-scale-ratio shape' result)
mtx (geom/generate-resize-matrix :bottom-right shape' scale)]
(assoc shape :resize-modifier mtx)))
shapev (gpt/point (:width shape') (:height shape'))
deltav (gpt/subtract point initial)
scalev (gpt/divide (gpt/add shapev deltav) shapev)
scalev (if lock? (let [v (max (:x scalev) (:y scalev))] (gpt/point v v)) scalev)]
(update-drawing [state point lock?]
(update-in state [:workspace-local :drawing] resize-shape point lock?))]
(-> shape
(assoc-in [:modifiers :resize-vector] scalev)
(assoc-in [:modifiers :resize-origin] (gpt/point x y))
(assoc-in [:modifiers :resize-rotation] 0))))
(update-drawing [state initial point lock?]
(update-in state [:workspace-local :drawing] resize-shape initial point lock?))]
(ptk/reify ::handle-drawing-generic
ptk/WatchEvent
@ -132,7 +137,7 @@
(let [{:keys [zoom flags]} (:workspace-local state)
stoper? #(or (ms/mouse-up? %) (= % :interrupt))
stoper (rx/filter stoper? stream)
initial @ms/mouse-position
mouse (->> ms/mouse-position
(rx/map #(gpt/divide % (gpt/point zoom))))]
(rx/concat
@ -141,7 +146,7 @@
(rx/map (fn [pt] #(initialize-drawing % pt))))
(->> mouse
(rx/with-latest vector ms/mouse-position-ctrl)
(rx/map (fn [[pt ctrl?]] #(update-drawing % pt ctrl?)))
(rx/map (fn [[pt ctrl?]] #(update-drawing % initial pt ctrl?)))
(rx/take-until stoper))
(rx/of handle-finish-drawing)))))))
@ -268,13 +273,12 @@
(rx/concat
(rx/of dw/clear-drawing)
(when (::initialized? shape)
(let [modifier (:resize-modifier shape)
shape (if (gmt/matrix? modifier)
(geom/transform shape modifier)
shape)
shape (dissoc shape ::initialized? :resize-modifier)]
(let [shape (-> shape
(geom/transform-shape)
(dissoc shape ::initialized?))]
;; Add & select the created shape to the workspace
(rx/of dw/deselect-all (dw/add-shape shape)))))))))
(rx/of dw/deselect-all
(dw/add-shape shape)))))))))
(def close-drawing-path
(ptk/reify ::close-drawing-path

View file

@ -149,7 +149,7 @@
;; :x 0 :y 0
:height c/viewport-height}
[:g {:transform (str "translate(0, " translate-y ")")}
[:g {:transform (str "translate(0, " (+ translate-y 20) ")")}
[:& vertical-rule-ticks {:zoom zoom}]]
[:rect {:x 0
:y 0

View file

@ -14,92 +14,14 @@
[potok.core :as ptk]
[rumext.alpha :as mf]
[uxbox.main.data.workspace :as dw]
[uxbox.main.geom :as geom]
[uxbox.util.geom.shapes :as geom]
[uxbox.main.refs :as refs]
[uxbox.main.store :as st]
[uxbox.main.streams :as ms]
[uxbox.util.dom :as dom]
[uxbox.util.geom.point :as gpt]
[uxbox.util.geom.matrix :as gmt]))
(defn- apply-zoom
[point]
(gpt/divide point (gpt/point @refs/selected-zoom)))
;; --- Resize & Rotate
(defn- start-resize
[vid ids shape]
(letfn [(resize [shape [point lock?]]
(let [result (geom/resize-shape vid shape point lock?)
scale (geom/calculate-scale-ratio shape result)
mtx (geom/generate-resize-matrix vid shape scale)]
(rx/of (dw/assoc-resize-modifier-in-bulk ids mtx))))
;; Unifies the instantaneous proportion lock modifier
;; activated by Ctrl key and the shapes own proportion
;; lock flag that can be activated on element options.
(normalize-proportion-lock [[point ctrl?]]
(let [proportion-lock? (:proportion-lock shape)]
[point (or proportion-lock? ctrl?)]))
;; Applies alginment to point if it is currently
;; activated on the current workspace
;; (apply-grid-alignment [point]
;; (if @refs/selected-alignment
;; (uwrk/align-point point)
;; (rx/of point)))
]
(reify
ptk/WatchEvent
(watch [_ state stream]
(let [shape (->> (geom/shape->rect-shape shape)
(geom/size))
stoper (rx/filter ms/mouse-up? stream)]
(rx/concat
(->> ms/mouse-position
(rx/map apply-zoom)
;; (rx/mapcat apply-grid-alignment)
(rx/with-latest vector ms/mouse-position-ctrl)
(rx/map normalize-proportion-lock)
(rx/mapcat (partial resize shape))
(rx/take-until stoper))
(rx/of (dw/materialize-resize-modifier-in-bulk ids))))))))
(defn start-rotate
[shapes]
(ptk/reify ::start-rotate
ptk/WatchEvent
(watch [_ state stream]
(let [stoper (rx/filter ms/mouse-up? stream)
group (geom/selection-rect shapes)
group-center (gpt/center group)
initial-angle (gpt/angle @ms/mouse-position group-center)
calculate-angle (fn [pos ctrl?]
(let [angle (- (gpt/angle pos group-center) initial-angle)
angle (if (neg? angle) (+ 360 angle) angle)
modval (mod angle 90)
angle (if ctrl?
(if (< 50 modval)
(+ angle (- 90 modval))
(- angle modval))
angle)
angle (if (= angle 360)
0
angle)]
angle))]
(rx/concat
(->> ms/mouse-position
(rx/map apply-zoom)
(rx/with-latest vector ms/mouse-position-ctrl)
(rx/map (fn [[pos ctrl?]]
(let [delta-angle (calculate-angle pos ctrl?)]
(dw/apply-rotation delta-angle shapes))))
(rx/take-until stoper))
(rx/of (dw/materialize-rotation shapes))
)))))
[uxbox.util.geom.matrix :as gmt]
[uxbox.util.debug :refer [debug?]]))
;; --- Controls (Component)
@ -149,41 +71,44 @@
[{:keys [shape zoom on-resize on-rotate] :as props}]
(let [{:keys [x y width height rotation] :as shape} (geom/shape->rect-shape shape)
radius (if (> (max width height) handler-size-threshold) 6.0 4.0)
transform (geom/rotation-matrix shape)
transform (geom/transform-matrix shape)
resize-handlers {:top [(+ x (/ width 2 )) (- y 2)]
:right [(+ x width 1) (+ y (/ height 2))]
:bottom [(+ x (/ width 2)) (+ y height 2)]
:left [(- x 3) (+ y (/ height 2))]
resize-handlers {:top [(+ x (/ width 2 )) y]
:right [(+ x width) (+ y (/ height 2))]
:bottom [(+ x (/ width 2)) (+ y height)]
:left [x (+ y (/ height 2))]
:top-left [x y]
:top-right [(+ x width) y]
:bottom-left [x (+ y height)]
:bottom-right [(+ x width) (+ y height)]}]
[:g.controls {:transform transform}
[:rect.main {:x x :y y
[:g.controls
[:rect.main {:transform transform
:x x :y y
:width width
:height height
:stroke-dasharray (str (/ 8.0 zoom) "," (/ 5 zoom))
:vector-effect "non-scaling-stroke"
:style {:stroke "#1FDEA7"
:fill "transparent"
:stroke-opacity "1"}}]
(for [[position [cx cy]] resize-handlers]
[:* {:key (str "fragment-" (name position))}
[:& rotation-handler {:key (str "rotation-" (name position))
:cx cx
:cy cy
:position position
:rotation (:rotation shape)
:on-mouse-down on-rotate}]
(let [tp (gpt/transform (gpt/point cx cy) transform)]
[:* {:key (str "fragment-" (name position))}
[:& rotation-handler {:key (str "rotation-" (name position))
:cx (:x tp)
:cy (:y tp)
:position position
:rotation (:rotation shape)
:on-mouse-down on-rotate}]
[:& control-item {:key (str "resize-" (name position))
:class (name position)
:on-click #(on-resize position %)
:r (/ radius zoom)
:cx cx
:cy cy}]])]))
[:& control-item {:key (str "resize-" (name position))
:class (name position)
:on-click #(on-resize position %)
:r (/ radius zoom)
:cx (:x tp)
:cy (:y tp)}]]))]))
;; --- Selection Handlers (Component)
@ -210,18 +135,20 @@
(on-handler-move [delta index]
(st/emit! (dw/update-path (:id shape) index delta)))]
(let [displacement (:displacement modifiers)
(let [transform (geom/transform-matrix shape)
displacement (:displacement modifiers)
segments (cond->> (:segments shape)
displacement (map #(gpt/transform % displacement)))]
[:g.controls
(for [[index {:keys [x y]}] (map-indexed vector segments)]
[:circle {:cx x :cy y
:r (/ 6.0 zoom)
:key index
:on-mouse-down #(on-mouse-down % index)
:fill "#ffffff"
:stroke "#1FDEA7"
:style {:cursor "pointer"}}])])))
(let [{:keys [x y]} (gpt/transform (gpt/point x y) transform)]
[:circle {:cx x :cy y
:r (/ 6.0 zoom)
:key index
:on-mouse-down #(on-mouse-down % index)
:fill "#ffffff"
:stroke "#1FDEA7"
:style {:cursor "pointer"}}]))])))
;; TODO: add specs for clarity
@ -239,13 +166,13 @@
:fill "transparent"}}]]))
(mf/defc multiple-selection-handlers
[{:keys [shapes selected zoom] :as props}]
[{:keys [shapes selected zoom objects] :as props}]
(let [shape (geom/selection-rect shapes)
on-resize #(do (dom/stop-propagation %2)
(st/emit! (start-resize %1 selected shape)))
(st/emit! (dw/start-resize %1 selected shape objects)))
on-rotate #(do (dom/stop-propagation %)
(st/emit! (start-rotate shapes)))]
(st/emit! (dw/start-rotate shapes)))]
[:& controls {:shape shape
:zoom zoom
@ -254,13 +181,15 @@
(mf/defc single-selection-handlers
[{:keys [shape zoom objects] :as props}]
(let [shape (geom/transform-shape shape)
(let [shape-id (:id shape)
shape (geom/transform-shape shape)
shape' (if (debug? :simple-selection) (geom/selection-rect [shape]) shape)
on-resize #(do (dom/stop-propagation %2)
(st/emit! (start-resize %1 #{(:id shape)} shape)))
(st/emit! (dw/start-resize %1 #{shape-id} shape' objects)))
on-rotate #(do (dom/stop-propagation %)
(st/emit! (start-rotate [shape])))]
(st/emit! (dw/start-rotate [shape])))]
[:& controls {:shape shape
[:& controls {:shape shape'
:zoom zoom
:on-rotate on-rotate
:on-resize on-resize}]))
@ -284,6 +213,7 @@
(> num 1)
[:& multiple-selection-handlers {:shapes shapes
:selected selected
:objects objects
:zoom zoom}]
(and (= type :text)
@ -298,5 +228,5 @@
:else
[:& single-selection-handlers {:shape shape
:objects objects
:zoom zoom}])))
:zoom zoom
:objects objects}])))

View file

@ -149,7 +149,6 @@
on-drop
(fn [side {:keys [id name] :as data}]
(let [index (if (= :top side) (inc index) index)]
;; (println "droping" name "on" side "of" (:name item) "/" index)
(st/emit! (dw/relocate-shape id (:id item) index))))
[dprops dref] (hooks/use-sortable

View file

@ -18,6 +18,6 @@
[{:keys [shape] :as props}]
[:div
[:& measures-menu {:shape shape
:options #{:circle-size :position :rotation}}]
:options #{:size :position :rotation}}]
[:& fill-menu {:shape shape}]
[:& stroke-menu {:shape shape}]])

View file

@ -64,8 +64,10 @@
delta (if (= attr :x)
(gpt/point (math/neg (- pval cval)) 0)
(gpt/point 0 (math/neg (- pval cval))))]
(st/emit! (udw/apply-frame-displacement (:id shape) delta)
(udw/materialize-frame-displacement (:id shape)))))
;; TODO: Change so not apply the modifiers until blur
(st/emit! (udw/set-modifiers #{(:id shape)} {:displacement delta})
(udw/apply-modifiers #{(:id shape)}))))
on-width-change #(on-size-change % :width)
on-height-change #(on-size-change % :height)

View file

@ -67,7 +67,8 @@
(let [value (-> (dom/get-target event)
(dom/get-value)
(d/parse-integer 0))]
(st/emit! (udw/update-shape (:id shape) {:rotation value}))))
(st/emit! (udw/set-rotation (- value (:rotation shape)) [shape])
(udw/apply-modifiers #{(:id shape)}))))
on-radius-change
(fn [event]

View file

@ -111,6 +111,9 @@
not-found))
not-found coll)))
(defn zip [col1 col2]
(map vector col1 col2))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Numbers Parsing
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

View file

@ -0,0 +1,39 @@
(ns uxbox.util.debug
"Debugging utils"
(:require
[uxbox.main.store :as store]))
(def debug-options #{:bounding-boxes :group :events #_:simple-selection})
(defonce ^:dynamic *debug* (atom #{}))
(defn debug-all! [] (reset! *debug* debug-options))
(defn debug-none! [] (reset! *debug* #{}))
(defn debug! [option] (swap! *debug* conj option))
(defn -debug! [option] (swap! *debug* disj option))
(defn debug? [option] (@*debug* option))
(defn tap
"Transducer function that can execute a side-effect `effect-fn` per input"
[effect-fn]
(fn [rf]
(fn
([] (rf))
([result] (rf result))
([result input]
(effect-fn input)
(rf result input)))))
(defn logjs
([str] (tap (partial logjs str)))
([str val]
(js/console.log str (clj->js val))
val))
(defn dump-state []
(logjs "state" @store/state))
(defn dump-objects []
(let [page-id (get @store/state :page-id)]
(logjs "state" (get-in @store/state [:workspace-data page-id :objects]))))

View file

@ -33,6 +33,13 @@
([m1 m2 & others]
(reduce multiply (multiply m1 m2) others)))
(defn substract
[{m1a :a m1b :b m1c :c m1d :d m1e :e m1f :f :as m1}
{m2a :a m2b :b m2c :c m2d :d m2e :e m2f :f :as m2}]
(Matrix.
(- m1a m2a) (- m1b m2b) (- m1c m2c)
(- m1d m2d) (- m1e m2e) (- m1f m2f)))
(defn ^boolean matrix?
"Return true if `v` is Matrix instance."
[v]
@ -51,44 +58,63 @@
(Matrix. 1 0 0 1 x y))
(defn scale-matrix
[{x :x y :y :as pt}]
(assert (gpt/point? pt))
(Matrix. x 0 0 y 0 0))
([pt center]
(multiply (translate-matrix center)
(scale-matrix pt)
(translate-matrix (gpt/negate center))))
([{x :x y :y :as pt}]
(assert (gpt/point? pt))
(Matrix. x 0 0 y 0 0)))
(defn rotate-matrix
[a]
(let [a (mth/radians a)]
(Matrix. (mth/cos a)
(mth/sin a)
(- (mth/sin a))
(mth/cos a)
0
0)))
([angle point] (multiply (translate-matrix point)
(rotate-matrix angle)
(translate-matrix (gpt/negate point))))
([angle]
(let [a (mth/radians angle)]
(Matrix. (mth/cos a)
(mth/sin a)
(- (mth/sin a))
(mth/cos a)
0
0))))
(defn skew-matrix
([angle-x angle-y point]
(multiply (translate-matrix point)
(skew-matrix angle-y angle-y)
(translate-matrix (gpt/negate point))))
([angle-x angle-y]
(let [m1 (mth/tan (mth/radians angle-x))
m2 (mth/tan (mth/radians angle-y))]
(Matrix. 1 m2 m1 1 0 0))))
(defn rotate
"Apply rotation transformation to the matrix."
([m angle]
(multiply m (rotate-matrix angle)))
([m angle center]
(multiply m
(translate-matrix center)
(rotate-matrix angle)
(translate-matrix (gpt/negate center)))))
(multiply m (rotate-matrix angle center))))
(defn scale
"Apply scale transformation to the matrix."
([m scale] (multiply m (scale-matrix scale)))
([m scale]
(multiply m (scale-matrix scale)))
([m scale center]
(multiply m
(translate-matrix center)
(scale-matrix scale)
(translate-matrix (gpt/negate center)))))
(multiply m (scale-matrix scale center))))
(defn translate
"Apply translate transformation to the matrix."
[m pt]
(multiply m (translate-matrix pt)))
(defn skew
"Apply translate transformation to the matrix."
([m angle-x angle-y]
(multiply m (skew-matrix angle-x angle-y)))
([m angle-x angle-y p]
(multiply m (skew-matrix angle-x angle-y p))))
;; --- Transit Adapter
(def matrix-write-handler
@ -100,22 +126,3 @@
(t/read-handler
(fn [value]
(map->Matrix value))))
;; Calculates the delta vector to move the figure when scaling after rotation
;; https://math.stackexchange.com/questions/1449672/determine-shift-between-scaled-rotated-object-and-additional-scale-step
(defn correct-rotation [handler lx ly kx ky angle]
(let [[s1 s2 s3]
;; Different sign configurations change the anchor corner
(cond
(#{:right :bottom :bottom-right} handler) [-1 1 1]
(#{:left :top :top-left} handler) [1 -1 1]
(#{:bottom-left} handler) [-1 -1 -1]
(#{:top-right} handler) [1 1 -1])
rad (* (or angle 0) (/ Math/PI 180))
kx' (* (/ (- kx 1.) 2.) lx)
ky' (* (/ (- ky 1.) 2.) ly)
dx (+ (* s3 (* kx' (- 1 (Math/cos rad))))
(* ky' (Math/sin rad)))
dy (+ (* (- s3) (* ky' (- 1 (Math/cos rad))))
(* kx' (Math/sin rad)))]
(gpt/point (* s1 dx) (* s2 dy))))

View file

@ -9,13 +9,17 @@
(ns uxbox.util.geom.point
(:refer-clojure :exclude [divide])
(:require [uxbox.util.math :as mth]
[cognitect.transit :as t]))
(:require
[cuerdas.core :as str]
[uxbox.util.math :as mth]
[cognitect.transit :as t]))
;; --- Point Impl
(defrecord Point [x y])
(defn s [{:keys [x y]}] (str "(" x "," y ")"))
(defn ^boolean point?
"Return true if `v` is Point instance."
[v]
@ -71,6 +75,11 @@
(assert (point? other))
(Point. (/ x ox) (/ y oy)))
(defn inverse
[{:keys [x y] :as p}]
(assert (point? p))
(Point. (/ 1 x) (/ 1 y)))
(defn negate
[{x :x y :y :as p}]
(assert (point? p))
@ -161,3 +170,23 @@
(t/read-handler
(fn [value]
(map->Point value))))
;; Vector functions
(defn to-vec [p1 p2]
(subtract p2 p1))
(defn dot [{x1 :x y1 :y} {x2 :x y2 :y}]
(+ (* x1 x2) (* y1 y2)))
(defn unit [v]
(let [v-length (length v)]
(divide v (point v-length v-length))))
(defn project [v1 v2]
(let [v2-unit (unit v2)
scalar-projection (dot v1 (unit v2))]
(multiply
v2-unit
(point scalar-projection scalar-projection))))

View file

@ -2,54 +2,46 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) 2016 Andrey Antukh <niwi@niwi.nz>
;; Copyright (c) 2020 UXBOX Labs SL
(ns uxbox.main.geom
(ns uxbox.util.geom.shapes
(:require
[clojure.spec.alpha :as s]
[uxbox.common.spec :as us]
[uxbox.util.geom.matrix :as gmt]
[uxbox.util.geom.point :as gpt]
[uxbox.util.math :as mth]
[uxbox.util.data :as d]
[uxbox.util.debug :as debug]
[uxbox.main.data.helpers :as helpers]))
;; --- Relative Movement
(declare move-rect)
(declare move-path)
(declare move-circle)
(defn- _chk
"Function that checks if a number is nil or nan. Will return 0 when not
valid and the number otherwise."
[v]
(if (or (not v) (mth/nan? v)) 0 v))
(defn move
"Move the shape relativelly to its current
position applying the provided delta."
[shape dpoint]
(case (:type shape)
:icon (move-rect shape dpoint)
:image (move-rect shape dpoint)
:rect (move-rect shape dpoint)
:frame (move-rect shape dpoint)
:text (move-rect shape dpoint)
:curve (move-path shape dpoint)
:path (move-path shape dpoint)
:circle (move-circle shape dpoint)
:group (move-rect shape dpoint)
shape))
(move-rect shape dpoint)))
(defn- move-rect
"A specialized function for relative movement
for rect-like shapes."
[shape {dx :x dy :y}]
(assoc shape
:x (mth/round (+ (:x shape) dx))
:y (mth/round (+ (:y shape) dy))))
(defn- move-circle
"A specialized function for relative movement
for circle shapes."
[shape {dx :x dy :y}]
(assoc shape
:cx (mth/round (+ (:cx shape) dx))
:cy (mth/round (+ (:cy shape) dy))))
:x (mth/round (+ (_chk (:x shape)) (_chk dx)))
:y (mth/round (+ (_chk (:y shape)) (_chk dy)))))
(defn- move-path
"A specialized function for relative movement
@ -71,35 +63,21 @@
;; --- Absolute Movement
(declare absolute-move-rect)
(declare absolute-move-circle)
(defn absolute-move
"Move the shape to the exactly specified position."
[shape position]
(case (:type shape)
:icon (absolute-move-rect shape position)
:frame (absolute-move-rect shape position)
:image (absolute-move-rect shape position)
:rect (absolute-move-rect shape position)
:text (absolute-move-rect shape position)
:group (absolute-move-rect shape position)
:circle (absolute-move-circle shape position)
shape))
:path shape
:curve shape
(absolute-move-rect shape position)))
(defn- absolute-move-rect
"A specialized function for absolute moviment
for rect-like shapes."
[shape {:keys [x y] :as pos}]
(let [dx (if x (- x (:x shape)) 0)
dy (if y (- y (:y shape)) 0)]
(move shape (gpt/point dx dy))))
(defn- absolute-move-circle
"A specialized function for absolute moviment
for rect-like shapes."
[shape {:keys [x y] :as pos}]
(let [dx (if x (- x (:cx shape)) 0)
dy (if y (- y (:cy shape)) 0)]
(let [dx (if x (- (_chk x) (_chk (:x shape))) 0)
dy (if y (- (_chk y) (_chk (:y shape))) 0)]
(move shape (gpt/point dx dy))))
;; --- Rotation
@ -113,16 +91,23 @@
[shape rotation]
(assoc shape :rotation rotation))
;; --- Corner points
(defn corner-points [points]
(let [minx (apply min (map :x points))
miny (apply min (map :y points))
maxx (apply max (map :x points))
maxy (apply max (map :y points))]
{:x1 minx :y1 miny :x2 maxx :y2 maxy}))
;; --- Size
(declare size-circle)
(declare size-path)
(defn size
"Calculate the size of the shape."
[shape]
(case (:type shape)
:circle (size-circle shape)
:curve (size-path shape)
:path (size-path shape)
shape))
@ -141,24 +126,15 @@
:width (- maxx minx)
:height (- maxy miny)))))
(defn- size-circle
"A specialized function for calculate size
for circle shape."
[{:keys [rx ry] :as shape}]
(merge shape {:width (* rx 2)
:height (* ry 2)}))
;; --- Center
(declare center-rect)
(declare center-circle)
(declare center-path)
(defn center
"Calculate the center of the shape."
[shape]
(case (:type shape)
:circle (center-circle shape)
:curve (center-path shape)
:path (center-path shape)
(center-rect shape)))
@ -167,30 +143,30 @@
[{:keys [x y width height] :as shape}]
(gpt/point (+ x (/ width 2)) (+ y (/ height 2))))
(defn- center-circle
[{:keys [cx cy] :as shape}]
(gpt/point cx cy))
(defn- center-path
[{:keys [segments x1 y1 x2 y2] :as shape}]
(if (and x1 y1 x2 y2)
(gpt/point (/ (+ x1 x2) 2) (/ (+ y1 y2) 2))
(let [minx (apply min (map :x segments))
miny (apply min (map :y segments))
maxx (apply max (map :x segments))
maxy (apply max (map :y segments))]
(gpt/point (/ (+ minx maxx) 2) (/ (+ miny maxy) 2)))))
[{:keys [segments] :as shape}]
(let [minx (apply min (map :x segments))
miny (apply min (map :y segments))
maxx (apply max (map :x segments))
maxy (apply max (map :y segments))]
(gpt/point (/ (+ minx maxx) 2) (/ (+ miny maxy) 2))))
(defn center->rect
"Creates a rect given a center and a width and height"
[center width height]
{:x (- (:x center) (/ width 2))
:y (- (:y center) (/ height 2))
:width width
:height height})
;; --- Proportions
(declare assign-proportions-path)
(declare assign-proportions-circle)
(declare assign-proportions-rect)
(defn assign-proportions
[{:keys [type] :as shape}]
(case type
:circle (assign-proportions-circle shape)
:path (assign-proportions-path shape)
(assign-proportions-rect shape)))
@ -198,10 +174,6 @@
[{:keys [width height] :as shape}]
(assoc shape :proportion (/ width height)))
(defn- assign-proportions-circle
[{:as shape}]
(assoc shape :proportion 1))
;; TODO: implement the rest of shapes
;; --- Paths
@ -259,22 +231,6 @@
(assoc :height value)
(assoc :width (* value proportion)))))))
(defn resize-circle
[shape attr value]
(us/assert map? shape)
(us/assert #{:rx :ry} attr)
(us/assert number? value)
(let [{:keys [proportion proportion-lock]} shape]
(if-not proportion-lock
(assoc shape attr value)
(if (= attr :rx)
(-> shape
(assoc :rx value)
(assoc :ry (/ value proportion)))
(-> shape
(assoc :ry value)
(assoc :rx (* value proportion)))))))
;; --- Resize
(defn calculate-scale-ratio
@ -297,26 +253,6 @@
:right [:x1 :y1]
:left [:x2 :y1]))
(defn generate-resize-matrix
"Generate the resize transformation matrix given a corner-id, shape
and the scale factor vector. The shape should be of rect-like type.
Mainly used by drawarea and shape resize on workspace."
[vid shape [scalex scaley]]
(let [[cor-x cor-y] (get-vid-coords vid)
{:keys [x y width height rotation]} shape
cx (+ x (/ width 2))
cy (+ y (/ height 2))
center (gpt/point cx cy)
]
(-> (gmt/matrix)
;; Correction first otherwise the scale is going to deform the correction
(gmt/translate (gmt/correct-rotation
vid width height scalex scaley rotation))
(gmt/scale (gpt/point scalex scaley)
(gpt/point (cor-x shape)
(cor-y shape))))))
(defn resize-shape
"Apply a resize transformation to a rect-like shape. The shape
should have the `width` and `height` attrs, because these attrs
@ -325,9 +261,10 @@
Mainly used in drawarea and interactive resize on workspace
with the main objective that on the end of resize have a way
a calculte the resize ratio with `calculate-scale-ratio`."
[vid shape {:keys [x y] :as point} lock?]
[vid shape initial target lock?]
(let [[cor-x cor-y] (get-vid-coords vid)]
(let [{:keys [x y]} (gpt/subtract target initial)
[cor-x cor-y] (get-vid-coords vid)]
(let [final-x (if (#{:top :bottom} vid) (:x2 shape) x)
final-y (if (#{:right :left} vid) (:y2 shape) y)
width (Math/abs (- final-x (cor-x shape)))
@ -341,7 +278,6 @@
(declare setup-rect)
(declare setup-image)
(declare setup-circle)
(defn setup
"A function that initializes the first coordinates for
@ -349,7 +285,6 @@
[shape props]
(case (:type shape)
:image (setup-image shape props)
:circle (setup-circle shape props)
(setup-rect shape props)))
(defn- setup-rect
@ -361,15 +296,6 @@
:width width
:height height))
(defn- setup-circle
"A specialized function for setup circle shapes."
[shape {:keys [x y width height]}]
(assoc shape
:cx x
:cy y
:rx (mth/abs width)
:ry (mth/abs height)))
(defn- setup-image
[{:keys [metadata] :as shape} {:keys [x y width height] :as props}]
(assoc shape
@ -383,7 +309,6 @@
;; --- Coerce to Rect-like shape.
(declare circle->rect-shape)
(declare path->rect-shape)
(declare group->rect-shape)
(declare rect->rect-shape)
@ -392,7 +317,6 @@
"Coerce shape to rect like shape."
[{:keys [type] :as shape}]
(case type
:circle (circle->rect-shape shape)
:path (path->rect-shape shape)
:curve (path->rect-shape shape)
(rect->rect-shape shape)))
@ -414,6 +338,29 @@
:height (- maxy miny)
:type :rect}))
(declare rect->path)
(defn shape->path
[shape]
(case (:type shape)
:path shape
:curve shape
(rect->path shape)))
(defn rect->path
[{:keys [x y width height] :as shape}]
(let [points [(gpt/point x y)
(gpt/point (+ x width) y)
(gpt/point (+ x width) (+ y height))
(gpt/point x (+ y height))
(gpt/point x y)]]
(-> shape
(assoc :type :path)
(assoc :segments points))))
;; --- SHAPE -> RECT
(defn- rect->rect-shape
[{:keys [x y width height] :as shape}]
(assoc shape
@ -438,22 +385,6 @@
:width (- maxx minx)
:height (- maxy miny))))
(defn- circle->rect-shape
[{:keys [cx cy rx ry] :as shape}]
(let [width (* rx 2)
height (* ry 2)
x1 (- cx rx)
y1 (- cy ry)]
(assoc shape
:x1 x1
:y1 y1
:x2 (+ x1 width)
:y2 (+ y1 height)
:x x1
:y y1
:width width
:height height)))
;; --- Resolve Shape
(declare resolve-rect-shape)
@ -477,7 +408,6 @@
;; --- Transform Shape
(declare transform-rect)
(declare transform-circle)
(declare transform-path)
(defn transform
@ -485,18 +415,20 @@
[{:keys [type] :as shape} xfmt]
(if (gmt/matrix? xfmt)
(case type
:frame (transform-rect shape xfmt)
:group (transform-rect shape xfmt)
:rect (transform-rect shape xfmt)
:icon (transform-rect shape xfmt)
:text (transform-rect shape xfmt)
:image (transform-rect shape xfmt)
:path (transform-path shape xfmt)
:curve (transform-path shape xfmt)
:circle (transform-circle shape xfmt)
shape)
(transform-rect shape xfmt))
shape))
(defn center-transform [shape matrix]
(let [shape-center (center shape)]
(-> shape
(transform
(-> (gmt/matrix)
(gmt/translate shape-center)
(gmt/multiply matrix)
(gmt/translate (gpt/negate shape-center)))))))
(defn- transform-rect
[{:keys [x y width height] :as shape} mx]
(let [tl (gpt/transform (gpt/point x y) mx)
@ -514,27 +446,6 @@
:width (- maxx minx)
:height (- maxy miny))))
(defn- transform-circle
[{:keys [cx cy rx ry] :as shape} xfmt]
(let [{:keys [x1 y1 x2 y2]} (shape->rect-shape shape)
tl (gpt/transform (gpt/point x1 y1) xfmt)
tr (gpt/transform (gpt/point x2 y1) xfmt)
bl (gpt/transform (gpt/point x1 y2) xfmt)
br (gpt/transform (gpt/point x2 y2) xfmt)
;; TODO: replace apply with transduce (performance)
x (apply min (map :x [tl tr bl br]))
y (apply min (map :y [tl tr bl br]))
maxx (apply max (map :x [tl tr bl br]))
maxy (apply max (map :y [tl tr bl br]))
width (- maxx x)
height (- maxy y)
cx (+ x (/ width 2))
cy (+ y (/ height 2))
rx (/ width 2)
ry (/ height 2)]
(assoc shape :cx cx :cy cy :rx rx :ry ry)))
(defn- transform-path
[{:keys [segments] :as shape} xfmt]
(let [segments (mapv #(gpt/transform % xfmt) segments)]
@ -551,39 +462,19 @@
(and rotation (pos? rotation))
(gmt/rotate rotation (gpt/point cx cy)))))
(defn resolve-rotation
[shape]
(transform shape (rotation-matrix shape)))
(declare transform-apply-modifiers)
(defn resolve-modifier
[{:keys [resize-modifier displacement-modifier rotation-modifier] :as shape}]
(cond-> shape
(gmt/matrix? resize-modifier)
(transform resize-modifier)
(gmt/matrix? displacement-modifier)
(transform displacement-modifier)
rotation-modifier
(update :rotation #(+ (or % 0) rotation-modifier))))
;; NOTE: we need apply `shape->rect-shape` 3 times because we need to
;; update the x1 x2 y1 y2 attributes on each step; this is because
;; some transform functions still uses that attributes. WE NEED TO
;; REFACTOR this, and remove any usage of the old xN yN attributes.
(def ^:private xf-resolve-shape
(comp (map shape->rect-shape)
(map resolve-modifier)
(map shape->rect-shape)
(map resolve-rotation)
(map shape->rect-shape)))
(defn selection-rect-shape [shape]
(-> shape
(transform-apply-modifiers)
(shape->rect-shape)))
(defn selection-rect
"Returns a rect that contains all the shapes and is aware of the
rotation of each shape. Mainly used for multiple selection."
[shapes]
(let [shapes (into [] xf-resolve-shape shapes)
(let [xf-resolve-shape (map selection-rect-shape)
shapes (into [] xf-resolve-shape shapes)
minx (transduce (map :x1) min shapes)
miny (transduce (map :y1) min shapes)
maxx (transduce (map :x2) max shapes)
@ -744,16 +635,200 @@
:type :rect}]
(overlaps? shape selrect)))
(defn calculate-rec-path-skew-angle
[path-shape]
(let [p1 (get-in path-shape [:segments 2])
p2 (get-in path-shape [:segments 3])
p3 (get-in path-shape [:segments 4])
v1 (gpt/to-vec p1 p2)
v2 (gpt/to-vec p2 p3)]
(- 90 (gpt/angle-with-other v1 v2))))
(defn calculate-rec-path-height
"Calculates the height of a paralelogram given by the path"
[path-shape]
(let [p1 (get-in path-shape [:segments 2])
p2 (get-in path-shape [:segments 3])
p3 (get-in path-shape [:segments 4])
v1 (gpt/to-vec p1 p2)
v2 (gpt/to-vec p2 p3)
angle (gpt/angle-with-other v1 v2)]
(* (gpt/length v2) (mth/sin (mth/radians angle)))))
(defn calculate-rec-path-rotation
[path-shape1 path-shape2 resize-vector]
(let [idx-1 0
idx-2 (cond (and (neg? (:x resize-vector)) (pos? (:y resize-vector))) 1
(and (neg? (:x resize-vector)) (neg? (:y resize-vector))) 2
(and (pos? (:x resize-vector)) (neg? (:y resize-vector))) 3
:else 0)
p1 (get-in path-shape1 [:segments idx-1])
p2 (get-in path-shape2 [:segments idx-2])
v1 (gpt/to-vec (center path-shape1) p1)
v2 (gpt/to-vec (center path-shape2) p2)
rot-angle (gpt/angle-with-other v1 v2)
rot-sign (if (> (* (:y v1) (:x v2)) (* (:x v1) (:y v2))) -1 1)]
(* rot-sign rot-angle)))
(defn transform-shape-point
"Transform a point around the shape center"
[point shape transform]
(let [shape-center (center shape)]
(gpt/transform
point
(-> (gmt/multiply
(gmt/translate-matrix shape-center)
transform
(gmt/translate-matrix (gpt/negate shape-center)))))))
(defn- transform-apply-modifiers
[shape]
(let [modifiers (:modifiers shape)
ds-modifier (:displacement modifiers (gmt/matrix))
resize (:resize-vector modifiers (gpt/point 1 1))
origin (:resize-origin modifiers (gpt/point 0 0))
resize-transform (:resize-transform modifiers (gmt/matrix))
resize-transform-inverse (:resize-transform-inverse modifiers (gmt/matrix))
rt-modif (:rotation modifiers 0)
shape (-> shape
(transform ds-modifier))
shape-center (center shape)]
(-> (shape->path shape)
(transform (-> (gmt/matrix)
;; Applies the current resize transformation
(gmt/translate origin)
(gmt/multiply resize-transform)
(gmt/scale resize)
(gmt/multiply resize-transform-inverse)
(gmt/translate (gpt/negate origin))
;; Applies the stacked transformations
(gmt/translate shape-center)
(gmt/multiply (gmt/rotate-matrix rt-modif))
(gmt/multiply (:transform shape (gmt/matrix)))
(gmt/translate (gpt/negate shape-center)))))))
(defn rect-path-dimensions [rect-path]
(let [seg (:segments rect-path)
[width height] (mapv (fn [[c1 c2]] (gpt/distance c1 c2)) (take 2 (d/zip seg (rest seg))))]
{:width width
:height height}))
(defn calculate-stretch [shape-path transform-inverse]
(let [shape-center (center shape-path)
shape-path-temp (transform
shape-path
(-> (gmt/matrix)
(gmt/translate shape-center)
(gmt/multiply transform-inverse)
(gmt/translate (gpt/negate shape-center))))
shape-path-temp-rec (shape->rect-shape shape-path-temp)
shape-path-temp-dim (rect-path-dimensions shape-path-temp)]
(gpt/divide (gpt/point (:width shape-path-temp-rec) (:height shape-path-temp-rec))
(gpt/point (:width shape-path-temp-dim) (:height shape-path-temp-dim)))))
(defn transform-selrect
[frame shape]
(-> shape
(transform-apply-modifiers)
(translate-to-frame frame)
(shape->rect-shape)))
(defn transform-rect-shape
[shape]
(let [;; Apply modifiers to the rect as a path so we have the end shape expected
shape-path (transform-apply-modifiers shape)
shape-center (center shape-path)
resize-vector (get-in shape [:modifiers :resize-vector] (gpt/point 1 1))
;; Reverse the current transformation stack to get the base rectangle
shape-path-temp (center-transform shape-path (:transform-inverse shape (gmt/matrix)))
shape-path-temp-dim (rect-path-dimensions shape-path-temp)
shape-path-temp-rec (shape->rect-shape shape-path-temp)
;; This rectangle is the new data for the current rectangle. We want to change our rectangle
;; to have this width, height, x, y
rec (center->rect shape-center (:width shape-path-temp-dim) (:height shape-path-temp-dim))
rec-path (rect->path rec)
;; The next matrix is a series of transformations we have to do to the previous rec so that
;; after applying them the end result is the `shape-path-temp`
;; This is compose of three transformations: skew, resize and rotation
stretch-matrix (gmt/matrix)
skew-angle (calculate-rec-path-skew-angle shape-path-temp)
;; When one of the axis is flipped we have to reverse the skew
skew-angle (if (neg? (* (:x resize-vector) (:y resize-vector))) (- skew-angle) skew-angle )
stretch-matrix (gmt/multiply stretch-matrix (gmt/skew-matrix skew-angle 0))
h1 (calculate-rec-path-height shape-path-temp)
h2 (calculate-rec-path-height (center-transform rec-path stretch-matrix))
stretch-matrix (gmt/multiply stretch-matrix (gmt/scale-matrix (gpt/point 1 (/ h1 h2))))
rotation-angle (calculate-rec-path-rotation (center-transform rec-path stretch-matrix) shape-path-temp resize-vector)
stretch-matrix (gmt/multiply (gmt/rotate-matrix rotation-angle) stretch-matrix)
;; This is the inverse to be able to remove the transformation
stretch-matrix-inverse (-> (gmt/matrix)
(gmt/scale (gpt/point 1 (/ h2 h1)))
(gmt/skew (- skew-angle) 0)
(gmt/rotate (- rotation-angle)))
new-shape (-> shape
(merge rec)
(update :transform #(gmt/multiply (or % (gmt/matrix)) stretch-matrix))
(update :transform-inverse #(gmt/multiply stretch-matrix-inverse (or % (gmt/matrix)))))]
new-shape))
(defn transform-path-shape
[shape]
(transform-apply-modifiers shape)
;; TODO: Addapt for paths is not working
#_(let [shape-path (transform-apply-modifiers shape)
shape-path-center (center shape-path)
shape-transform-inverse' (-> (gmt/matrix)
(gmt/translate shape-path-center)
(gmt/multiply (:transform-inverse shape (gmt/matrix)))
(gmt/multiply (gmt/rotate-matrix (- (:rotation-modifier shape 0))))
(gmt/translate (gpt/negate shape-path-center)))]
(-> shape-path
(transform shape-transform-inverse')
(add-rotate-transform (:rotation-modifier shape 0)))))
(defn transform-shape
"Transform the shape properties given the modifiers"
([shape] (transform-shape nil shape))
([frame shape]
(let [ds-modifier (:displacement-modifier shape)
rz-modifier (:resize-modifier shape)
frame-ds-modifier (:displacement-modifier frame)
rt-modifier (:rotation-modifier shape)]
(cond-> shape
(gmt/matrix? rz-modifier) (transform rz-modifier)
frame (move (gpt/point (- (:x frame)) (- (:y frame))))
(gmt/matrix? frame-ds-modifier) (transform frame-ds-modifier)
(gmt/matrix? ds-modifier) (transform ds-modifier)
rt-modifier (update :rotation #(+ (or % 0) rt-modifier))))))
(let [new-shape (case (:type shape)
:path (transform-path-shape shape)
:curve (transform-path-shape shape)
(transform-rect-shape shape))]
(-> new-shape
(translate-to-frame frame)
(update :rotation #(mod (+ % (get-in shape [:modifiers :rotation] 0)) 360))
(dissoc :modifiers)))))
(defn transform-matrix
"Returns a transformation matrix without changing the shape properties.
The result should be used in a `transform` attribute in svg"
([{:keys [x y] :as shape}]
(let [shape-center (center shape)]
(-> (gmt/matrix)
(gmt/translate shape-center)
(gmt/multiply (:transform shape (gmt/matrix)))
(gmt/translate (gpt/negate shape-center))))))

View file

@ -15,7 +15,7 @@
(defn- load
[alias]
(if (= *target* "nodejs")
(if (or (= *target* "nodejs") (not (exists? js/window)))
{}
(let [data (.getItem js/localStorage (name alias))]
(if data

View file

@ -17,8 +17,8 @@
[uxbox.common.exceptions :as ex]
[uxbox.common.spec :as us]
[uxbox.common.uuid :as uuid]
[uxbox.main.geom :as geom]
[uxbox.worker.impl :as impl]
[uxbox.util.geom.shapes :as geom]
[uxbox.util.quadtree :as qdt]))
(defonce state (l/atom {}))
@ -92,10 +92,8 @@
(defn- resolve-object
[state {:keys [id] :as item}]
(let [item (-> item
(geom/shape->rect-shape)
(geom/resolve-rotation)
(geom/shape->rect-shape))
(let [selection-rect (geom/selection-rect-shape item)
item (merge item (select-keys selection-rect [:x :y :width :height]))
width (+ (:x item 0) (:width item 0))
height (+ (:y item 0) (:height item 0))
max (fnil max 0)]