mirror of
https://github.com/penpot/penpot.git
synced 2025-01-25 07:58:49 -05:00
🎉 Duplicate move
This commit is contained in:
parent
d6c97f9d19
commit
174b9db1d2
10 changed files with 428 additions and 320 deletions
|
@ -25,6 +25,7 @@
|
|||
[uxbox.main.data.workspace.persistence :as dwp]
|
||||
[uxbox.main.data.workspace.texts :as dwtxt]
|
||||
[uxbox.main.data.workspace.transforms :as dwt]
|
||||
[uxbox.main.data.workspace.selection :as dws]
|
||||
[uxbox.main.repo :as rp]
|
||||
[uxbox.main.store :as st]
|
||||
[uxbox.main.streams :as ms]
|
||||
|
@ -438,101 +439,6 @@
|
|||
(assoc :zoom zoom)
|
||||
(update :vbox merge srect)))))))))))
|
||||
|
||||
;; --- Selection Rect
|
||||
|
||||
(declare select-shapes-by-current-selrect)
|
||||
(declare deselect-all)
|
||||
|
||||
(defn update-selrect
|
||||
[selrect]
|
||||
(ptk/reify ::update-selrect
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(assoc-in state [:workspace-local :selrect] selrect))))
|
||||
|
||||
(def handle-selection
|
||||
(letfn [(data->selrect [data]
|
||||
(let [start (:start data)
|
||||
stop (:stop data)
|
||||
start-x (min (:x start) (:x stop))
|
||||
start-y (min (:y start) (:y stop))
|
||||
end-x (max (:x start) (:x stop))
|
||||
end-y (max (:y start) (:y stop))]
|
||||
{:type :rect
|
||||
:x start-x
|
||||
:y start-y
|
||||
:width (mth/abs (- end-x start-x))
|
||||
:height (mth/abs (- end-y start-y))}))]
|
||||
(ptk/reify ::handle-selection
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(let [stoper (rx/filter #(or (interrupt? %)
|
||||
(ms/mouse-up? %))
|
||||
stream)]
|
||||
(rx/concat
|
||||
(rx/of deselect-all)
|
||||
(->> ms/mouse-position
|
||||
(rx/scan (fn [data pos]
|
||||
(if data
|
||||
(assoc data :stop pos)
|
||||
{:start pos :stop pos}))
|
||||
nil)
|
||||
(rx/map data->selrect)
|
||||
(rx/filter #(or (> (:width %) 10)
|
||||
(> (:height %) 10)))
|
||||
(rx/map update-selrect)
|
||||
(rx/take-until stoper))
|
||||
(rx/of select-shapes-by-current-selrect)))))))
|
||||
|
||||
;; --- Toggle shape's selection status (selected or deselected)
|
||||
|
||||
(declare expand-all-parents)
|
||||
|
||||
(defn select-shape
|
||||
([id] (select-shape id false))
|
||||
([id toggle?]
|
||||
(us/verify ::us/uuid id)
|
||||
(ptk/reify ::select-shape
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(update-in state [:workspace-local :selected]
|
||||
(fn [selected]
|
||||
(if-not toggle?
|
||||
(conj selected id)
|
||||
(if (contains? selected id)
|
||||
(disj selected id)
|
||||
(conj selected id))))))
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(let [page-id (get-in state [:workspace-page :id])
|
||||
objects (get-in state [:workspace-data page-id :objects])]
|
||||
(rx/of (expand-all-parents [id] objects)))))))
|
||||
|
||||
(defn select-shapes
|
||||
[ids]
|
||||
(us/verify ::set-of-uuid ids)
|
||||
(ptk/reify ::select-shapes
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(assoc-in state [:workspace-local :selected] ids))
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(let [page-id (get-in state [:workspace-page :id])
|
||||
objects (get-in state [:workspace-data page-id :objects])]
|
||||
(rx/of (expand-all-parents ids objects))))))
|
||||
|
||||
(def deselect-all
|
||||
"Clear all possible state of drawing, edition
|
||||
or any similar action taken by the user."
|
||||
(ptk/reify ::deselect-all
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(update state :workspace-local #(-> %
|
||||
(assoc :selected #{})
|
||||
(dissoc :selected-frame))))))
|
||||
|
||||
|
||||
;; --- Add shape to Workspace
|
||||
|
||||
|
@ -598,161 +504,12 @@
|
|||
:id id}]
|
||||
|
||||
(rx/of (dwc/commit-changes [rchange] [uchange] {:commit-local? true})
|
||||
(select-shapes #{id})
|
||||
(dws/select-shapes #{id})
|
||||
(when (= :text (:type attrs))
|
||||
(start-edition-mode id)))))))
|
||||
|
||||
|
||||
;; --- Duplicate Shapes
|
||||
|
||||
(declare prepare-duplicate-changes)
|
||||
(declare prepare-duplicate-change)
|
||||
(declare prepare-duplicate-frame-change)
|
||||
(declare prepare-duplicate-shape-change)
|
||||
|
||||
(def ^:private change->name #(get-in % [:obj :name]))
|
||||
|
||||
(defn- prepare-duplicate-changes
|
||||
"Prepare objects to paste: generate new id, give them unique names,
|
||||
move to the position of mouse pointer, and find in what frame they
|
||||
fit."
|
||||
[objects names ids delta]
|
||||
(loop [names names
|
||||
chgs []
|
||||
id (first ids)
|
||||
ids (rest ids)]
|
||||
(if (nil? id)
|
||||
chgs
|
||||
(let [result (prepare-duplicate-change objects names id delta)
|
||||
result (if (vector? result) result [result])]
|
||||
(recur
|
||||
(into names (map change->name) result)
|
||||
(into chgs result)
|
||||
(first ids)
|
||||
(rest ids))))))
|
||||
|
||||
(defn- prepare-duplicate-change
|
||||
[objects names id delta]
|
||||
(let [obj (get objects id)]
|
||||
(if (= :frame (:type obj))
|
||||
(prepare-duplicate-frame-change objects names obj delta)
|
||||
(prepare-duplicate-shape-change objects names obj delta nil nil))))
|
||||
|
||||
(defn- prepare-duplicate-shape-change
|
||||
[objects names obj delta frame-id parent-id]
|
||||
(let [id (uuid/next)
|
||||
name (generate-unique-name names (:name obj))
|
||||
renamed-obj (assoc obj :id id :name name)
|
||||
moved-obj (geom/move renamed-obj delta)
|
||||
frames (cp/select-frames objects)
|
||||
frame-id (if frame-id
|
||||
frame-id
|
||||
(dwc/calculate-frame-overlap frames moved-obj))
|
||||
|
||||
parent-id (or parent-id frame-id)
|
||||
|
||||
children-changes
|
||||
(loop [names names
|
||||
result []
|
||||
cid (first (:shapes obj))
|
||||
cids (rest (:shapes obj))]
|
||||
(if (nil? cid)
|
||||
result
|
||||
(let [obj (get objects cid)
|
||||
changes (prepare-duplicate-shape-change objects names obj delta frame-id id)]
|
||||
(recur
|
||||
(into names (map change->name changes))
|
||||
(into result changes)
|
||||
(first cids)
|
||||
(rest cids)))))
|
||||
|
||||
reframed-obj (-> moved-obj
|
||||
(assoc :frame-id frame-id)
|
||||
(dissoc :shapes))]
|
||||
(into [{:type :add-obj
|
||||
:id id
|
||||
:old-id (:id obj)
|
||||
:frame-id frame-id
|
||||
:parent-id parent-id
|
||||
:obj (dissoc reframed-obj :shapes)}]
|
||||
children-changes)))
|
||||
|
||||
(defn- prepare-duplicate-frame-change
|
||||
[objects names obj delta]
|
||||
(let [frame-id (uuid/next)
|
||||
frame-name (generate-unique-name names (:name obj))
|
||||
sch (->> (map #(get objects %) (:shapes obj))
|
||||
(mapcat #(prepare-duplicate-shape-change objects names % delta frame-id frame-id)))
|
||||
|
||||
renamed-frame (-> obj
|
||||
(assoc :id frame-id)
|
||||
(assoc :name frame-name)
|
||||
(assoc :frame-id uuid/zero)
|
||||
(dissoc :shapes))
|
||||
|
||||
moved-frame (geom/move renamed-frame delta)
|
||||
|
||||
fch {:type :add-obj
|
||||
:old-id (:id obj)
|
||||
:id frame-id
|
||||
:frame-id uuid/zero
|
||||
:obj moved-frame}]
|
||||
|
||||
(into [fch] sch)))
|
||||
|
||||
(declare select-shapes)
|
||||
|
||||
(def duplicate-selected
|
||||
(ptk/reify ::duplicate-selected
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(let [page-id (:current-page-id state)
|
||||
selected (get-in state [:workspace-local :selected])
|
||||
objects (get-in state [:workspace-data page-id :objects])
|
||||
delta (gpt/point 0 0)
|
||||
unames (retrieve-used-names objects)
|
||||
|
||||
rchanges (prepare-duplicate-changes objects unames selected delta)
|
||||
uchanges (mapv #(array-map :type :del-obj :id (:id %))
|
||||
(reverse rchanges))
|
||||
|
||||
selected (->> rchanges
|
||||
(filter #(selected (:old-id %)))
|
||||
(map #(get-in % [:obj :id]))
|
||||
(into #{}))]
|
||||
|
||||
(rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true})
|
||||
(select-shapes selected))))))
|
||||
|
||||
|
||||
;; --- Select Shapes (By selrect)
|
||||
|
||||
(def select-shapes-by-current-selrect
|
||||
(ptk/reify ::select-shapes-by-current-selrect
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(let [page-id (get-in state [:workspace-page :id])
|
||||
selrect (get-in state [:workspace-local :selrect])]
|
||||
(rx/merge
|
||||
(rx/of (update-selrect nil))
|
||||
(when selrect
|
||||
(->> (uw/ask! {:cmd :selection/query
|
||||
:page-id page-id
|
||||
:rect selrect})
|
||||
(rx/map select-shapes))))))))
|
||||
|
||||
(defn select-inside-group
|
||||
[group-id position]
|
||||
(ptk/reify ::select-inside-group
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(let [page-id (:current-page-id state)
|
||||
objects (get-in state [:workspace-data page-id :objects])
|
||||
group (get objects group-id)
|
||||
children (map #(get objects %) (:shapes group))
|
||||
selected (->> children (filter #(geom/has-point? % position)) first)]
|
||||
(when selected
|
||||
(rx/of deselect-all (select-shape (:id selected))))))))
|
||||
|
||||
;; --- Update Shape Attrs
|
||||
|
||||
|
@ -906,7 +663,7 @@
|
|||
shapes (map lookup selected)
|
||||
shape? #(not= (:type %) :frame)]
|
||||
(rx/of (delete-shapes selected)
|
||||
deselect-all)))))
|
||||
dws/deselect-all)))))
|
||||
|
||||
|
||||
;; --- Rename Shape
|
||||
|
@ -1186,21 +943,6 @@
|
|||
(update [_ state]
|
||||
(update state :workspace-local dissoc :expanded))))
|
||||
|
||||
(defn expand-all-parents
|
||||
[ids objects]
|
||||
(ptk/reify ::expand-all-parents
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(let [expand-fn (fn [expanded]
|
||||
(merge expanded
|
||||
(->> ids
|
||||
(map #(cp/get-all-parents % objects))
|
||||
flatten
|
||||
(filter #(not= % uuid/zero))
|
||||
(map (fn [id] {id true}))
|
||||
(into {}))))]
|
||||
(update-in state [:workspace-local :expanded] expand-fn)))))
|
||||
|
||||
(defn recursive-assign
|
||||
"A helper for assign recursively a shape attr."
|
||||
[id attr value]
|
||||
|
@ -1284,7 +1026,7 @@
|
|||
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(rx/of (select-shape (:id shape))))))
|
||||
(rx/of (dws/select-shape (:id shape))))))
|
||||
|
||||
(def hide-context-menu
|
||||
(ptk/reify ::hide-context-menu
|
||||
|
@ -1342,7 +1084,7 @@
|
|||
unames (-> (get-in state [:workspace-data page-id :objects])
|
||||
(retrieve-used-names))
|
||||
|
||||
rchanges (prepare-duplicate-changes objects unames selected delta)
|
||||
rchanges (dws/prepare-duplicate-changes objects unames selected delta)
|
||||
uchanges (mapv #(array-map :type :del-obj :id (:id %))
|
||||
(reverse rchanges))
|
||||
|
||||
|
@ -1351,7 +1093,7 @@
|
|||
(map #(get-in % [:obj :id]))
|
||||
(into #{}))]
|
||||
(rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true})
|
||||
(select-shapes selected))))))
|
||||
(dws/select-shapes selected))))))
|
||||
|
||||
(def paste
|
||||
(ptk/reify ::paste
|
||||
|
@ -1432,7 +1174,7 @@
|
|||
{:type :del-obj
|
||||
:id id}]]
|
||||
(rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true})
|
||||
(select-shapes #{id}))))))))
|
||||
(dws/select-shapes #{id}))))))))
|
||||
|
||||
(def remove-group
|
||||
(ptk/reify ::remove-group
|
||||
|
@ -1551,6 +1293,14 @@
|
|||
(def delete-page dwp/delete-page)
|
||||
(def create-empty-page dwp/create-empty-page)
|
||||
|
||||
;; Selection
|
||||
(def select-shape dws/select-shape)
|
||||
(def deselect-all dws/deselect-all)
|
||||
(def select-shapes dws/select-shapes)
|
||||
(def duplicate-selected dws/duplicate-selected)
|
||||
(def handle-selection dws/handle-selection)
|
||||
(def select-inside-group dws/select-inside-group)
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Shortcuts
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
|
|
@ -298,3 +298,17 @@
|
|||
(update state :workspace-local dissoc :undo-index :undo))))
|
||||
|
||||
|
||||
(defn expand-all-parents
|
||||
[ids objects]
|
||||
(ptk/reify ::expand-all-parents
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(let [expand-fn (fn [expanded]
|
||||
(merge expanded
|
||||
(->> ids
|
||||
(map #(cp/get-all-parents % objects))
|
||||
flatten
|
||||
(filter #(not= % uuid/zero))
|
||||
(map (fn [id] {id true}))
|
||||
(into {}))))]
|
||||
(update-in state [:workspace-local :expanded] expand-fn)))))
|
||||
|
|
299
frontend/src/uxbox/main/data/workspace/selection.cljs
Normal file
299
frontend/src/uxbox/main/data/workspace/selection.cljs
Normal file
|
@ -0,0 +1,299 @@
|
|||
;; 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 uxbox.main.data.workspace.selection
|
||||
(:require
|
||||
[beicon.core :as rx]
|
||||
[cljs.spec.alpha :as s]
|
||||
[potok.core :as ptk]
|
||||
[uxbox.main.data.workspace.common :as dwc]
|
||||
[uxbox.main.worker :as uw]
|
||||
[uxbox.main.streams :as ms]
|
||||
[uxbox.common.pages :as cp]
|
||||
[uxbox.common.spec :as us]
|
||||
[uxbox.common.uuid :as uuid]
|
||||
[uxbox.common.data :as d]
|
||||
[uxbox.common.geom.shapes :as geom]
|
||||
[uxbox.common.geom.point :as gpt]
|
||||
[uxbox.common.math :as mth]))
|
||||
|
||||
(s/def ::set-of-uuid
|
||||
(s/every uuid? :kind set?))
|
||||
|
||||
(s/def ::set-of-string
|
||||
(s/every string? :kind set?))
|
||||
|
||||
;; Duplicate from workspace.
|
||||
;; FIXME: Move these functions to a common place
|
||||
|
||||
(defn interrupt? [e] (= e :interrupt))
|
||||
|
||||
(defn- retrieve-used-names
|
||||
[objects]
|
||||
(into #{} (map :name) (vals objects)))
|
||||
|
||||
(defn- extract-numeric-suffix
|
||||
[basename]
|
||||
(if-let [[match p1 p2] (re-find #"(.*)-([0-9]+)$" basename)]
|
||||
[p1 (+ 1 (d/parse-integer p2))]
|
||||
[basename 1]))
|
||||
|
||||
(defn- generate-unique-name
|
||||
"A unique name generator"
|
||||
[used basename]
|
||||
(s/assert ::set-of-string used)
|
||||
(s/assert ::us/string basename)
|
||||
(let [[prefix initial] (extract-numeric-suffix basename)]
|
||||
(loop [counter initial]
|
||||
(let [candidate (str prefix "-" counter)]
|
||||
(if (contains? used candidate)
|
||||
(recur (inc counter))
|
||||
candidate)))))
|
||||
|
||||
;; --- Selection Rect
|
||||
|
||||
(declare select-shapes-by-current-selrect)
|
||||
(declare deselect-all)
|
||||
|
||||
(defn update-selrect
|
||||
[selrect]
|
||||
(ptk/reify ::update-selrect
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(assoc-in state [:workspace-local :selrect] selrect))))
|
||||
|
||||
(def handle-selection
|
||||
(letfn [(data->selrect [data]
|
||||
(let [start (:start data)
|
||||
stop (:stop data)
|
||||
start-x (min (:x start) (:x stop))
|
||||
start-y (min (:y start) (:y stop))
|
||||
end-x (max (:x start) (:x stop))
|
||||
end-y (max (:y start) (:y stop))]
|
||||
{:type :rect
|
||||
:x start-x
|
||||
:y start-y
|
||||
:width (mth/abs (- end-x start-x))
|
||||
:height (mth/abs (- end-y start-y))}))]
|
||||
(ptk/reify ::handle-selection
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(let [stoper (rx/filter #(or (interrupt? %)
|
||||
(ms/mouse-up? %))
|
||||
stream)]
|
||||
(rx/concat
|
||||
(rx/of deselect-all)
|
||||
(->> ms/mouse-position
|
||||
(rx/scan (fn [data pos]
|
||||
(if data
|
||||
(assoc data :stop pos)
|
||||
{:start pos :stop pos}))
|
||||
nil)
|
||||
(rx/map data->selrect)
|
||||
(rx/filter #(or (> (:width %) 10)
|
||||
(> (:height %) 10)))
|
||||
(rx/map update-selrect)
|
||||
(rx/take-until stoper))
|
||||
(rx/of select-shapes-by-current-selrect)))))))
|
||||
|
||||
;; --- Toggle shape's selection status (selected or deselected)
|
||||
|
||||
(defn select-shape
|
||||
([id] (select-shape id false))
|
||||
([id toggle?]
|
||||
(us/verify ::us/uuid id)
|
||||
(ptk/reify ::select-shape
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(update-in state [:workspace-local :selected]
|
||||
(fn [selected]
|
||||
(if-not toggle?
|
||||
(conj selected id)
|
||||
(if (contains? selected id)
|
||||
(disj selected id)
|
||||
(conj selected id))))))
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(let [page-id (get-in state [:workspace-page :id])
|
||||
objects (get-in state [:workspace-data page-id :objects])]
|
||||
(rx/of (dwc/expand-all-parents [id] objects)))))))
|
||||
|
||||
(defn select-shapes
|
||||
[ids]
|
||||
(us/verify ::set-of-uuid ids)
|
||||
(ptk/reify ::select-shapes
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(assoc-in state [:workspace-local :selected] ids))
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(let [page-id (get-in state [:workspace-page :id])
|
||||
objects (get-in state [:workspace-data page-id :objects])]
|
||||
(rx/of (dwc/expand-all-parents ids objects))))))
|
||||
|
||||
(def deselect-all
|
||||
"Clear all possible state of drawing, edition
|
||||
or any similar action taken by the user."
|
||||
(ptk/reify ::deselect-all
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(update state :workspace-local #(-> %
|
||||
(assoc :selected #{})
|
||||
(dissoc :selected-frame))))))
|
||||
|
||||
|
||||
;; --- Select Shapes (By selrect)
|
||||
|
||||
(def select-shapes-by-current-selrect
|
||||
(ptk/reify ::select-shapes-by-current-selrect
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(let [page-id (get-in state [:workspace-page :id])
|
||||
selrect (get-in state [:workspace-local :selrect])]
|
||||
(rx/merge
|
||||
(rx/of (update-selrect nil))
|
||||
(when selrect
|
||||
(->> (uw/ask! {:cmd :selection/query
|
||||
:page-id page-id
|
||||
:rect selrect})
|
||||
(rx/map select-shapes))))))))
|
||||
|
||||
(defn select-inside-group
|
||||
[group-id position]
|
||||
(ptk/reify ::select-inside-group
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(let [page-id (:current-page-id state)
|
||||
objects (get-in state [:workspace-data page-id :objects])
|
||||
group (get objects group-id)
|
||||
children (map #(get objects %) (:shapes group))
|
||||
selected (->> children (filter #(geom/has-point? % position)) first)]
|
||||
(when selected
|
||||
(rx/of deselect-all (select-shape (:id selected))))))))
|
||||
|
||||
|
||||
;; --- Duplicate Shapes
|
||||
(declare prepare-duplicate-changes)
|
||||
(declare prepare-duplicate-change)
|
||||
(declare prepare-duplicate-frame-change)
|
||||
(declare prepare-duplicate-shape-change)
|
||||
|
||||
(def ^:private change->name #(get-in % [:obj :name]))
|
||||
|
||||
(defn- prepare-duplicate-changes
|
||||
"Prepare objects to paste: generate new id, give them unique names,
|
||||
move to the position of mouse pointer, and find in what frame they
|
||||
fit."
|
||||
[objects names ids delta]
|
||||
(loop [names names
|
||||
chgs []
|
||||
id (first ids)
|
||||
ids (rest ids)]
|
||||
(if (nil? id)
|
||||
chgs
|
||||
(let [result (prepare-duplicate-change objects names id delta)
|
||||
result (if (vector? result) result [result])]
|
||||
(recur
|
||||
(into names (map change->name) result)
|
||||
(into chgs result)
|
||||
(first ids)
|
||||
(rest ids))))))
|
||||
|
||||
(defn- prepare-duplicate-change
|
||||
[objects names id delta]
|
||||
(let [obj (get objects id)]
|
||||
(if (= :frame (:type obj))
|
||||
(prepare-duplicate-frame-change objects names obj delta)
|
||||
(prepare-duplicate-shape-change objects names obj delta nil nil))))
|
||||
|
||||
(defn- prepare-duplicate-shape-change
|
||||
[objects names obj delta frame-id parent-id]
|
||||
(let [id (uuid/next)
|
||||
name (generate-unique-name names (:name obj))
|
||||
renamed-obj (assoc obj :id id :name name)
|
||||
moved-obj (geom/move renamed-obj delta)
|
||||
frames (cp/select-frames objects)
|
||||
frame-id (if frame-id
|
||||
frame-id
|
||||
(dwc/calculate-frame-overlap frames moved-obj))
|
||||
|
||||
parent-id (or parent-id frame-id)
|
||||
|
||||
children-changes
|
||||
(loop [names names
|
||||
result []
|
||||
cid (first (:shapes obj))
|
||||
cids (rest (:shapes obj))]
|
||||
(if (nil? cid)
|
||||
result
|
||||
(let [obj (get objects cid)
|
||||
changes (prepare-duplicate-shape-change objects names obj delta frame-id id)]
|
||||
(recur
|
||||
(into names (map change->name changes))
|
||||
(into result changes)
|
||||
(first cids)
|
||||
(rest cids)))))
|
||||
|
||||
reframed-obj (-> moved-obj
|
||||
(assoc :frame-id frame-id)
|
||||
(dissoc :shapes))]
|
||||
(into [{:type :add-obj
|
||||
:id id
|
||||
:old-id (:id obj)
|
||||
:frame-id frame-id
|
||||
:parent-id parent-id
|
||||
:obj (dissoc reframed-obj :shapes)}]
|
||||
children-changes)))
|
||||
|
||||
(defn- prepare-duplicate-frame-change
|
||||
[objects names obj delta]
|
||||
(let [frame-id (uuid/next)
|
||||
frame-name (generate-unique-name names (:name obj))
|
||||
sch (->> (map #(get objects %) (:shapes obj))
|
||||
(mapcat #(prepare-duplicate-shape-change objects names % delta frame-id frame-id)))
|
||||
|
||||
renamed-frame (-> obj
|
||||
(assoc :id frame-id)
|
||||
(assoc :name frame-name)
|
||||
(assoc :frame-id uuid/zero)
|
||||
(dissoc :shapes))
|
||||
|
||||
moved-frame (geom/move renamed-frame delta)
|
||||
|
||||
fch {:type :add-obj
|
||||
:old-id (:id obj)
|
||||
:id frame-id
|
||||
:frame-id uuid/zero
|
||||
:obj moved-frame}]
|
||||
|
||||
(into [fch] sch)))
|
||||
|
||||
(def duplicate-selected
|
||||
(ptk/reify ::duplicate-selected
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(let [page-id (:current-page-id state)
|
||||
selected (get-in state [:workspace-local :selected])
|
||||
objects (get-in state [:workspace-data page-id :objects])
|
||||
delta (gpt/point 0 0)
|
||||
unames (retrieve-used-names objects)
|
||||
|
||||
rchanges (prepare-duplicate-changes objects unames selected delta)
|
||||
uchanges (mapv #(array-map :type :del-obj :id (:id %))
|
||||
(reverse rchanges))
|
||||
|
||||
selected (->> rchanges
|
||||
(filter #(selected (:old-id %)))
|
||||
(map #(get-in % [:obj :id]))
|
||||
(into #{}))]
|
||||
|
||||
(rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true})
|
||||
(select-shapes selected))))))
|
|
@ -18,6 +18,7 @@
|
|||
[uxbox.common.spec :as us]
|
||||
[uxbox.common.pages :as cp]
|
||||
[uxbox.main.data.workspace.common :as dwc]
|
||||
[uxbox.main.data.workspace.selection :as dws]
|
||||
[uxbox.main.refs :as refs]
|
||||
[uxbox.main.store :as st]
|
||||
[uxbox.main.streams :as ms]
|
||||
|
@ -191,6 +192,7 @@
|
|||
;; -- MOVE
|
||||
|
||||
(declare start-move)
|
||||
(declare start-move-duplicate)
|
||||
|
||||
(defn start-move-selected
|
||||
[]
|
||||
|
@ -206,33 +208,52 @@
|
|||
(rx/map #(gpt/length %))
|
||||
(rx/filter #(> % 1))
|
||||
(rx/take 1)
|
||||
(rx/map #(start-move initial selected)))))))
|
||||
|
||||
(defn start-move
|
||||
[from-position ids]
|
||||
(ptk/reify ::start-move
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(-> state
|
||||
(assoc-in [:workspace-local :transform] :move)))
|
||||
(rx/with-latest vector ms/mouse-position-alt)
|
||||
(rx/flat-map
|
||||
(fn [[_ alt?]]
|
||||
(if alt?
|
||||
;; When alt is down we start a duplicate+move
|
||||
(rx/of (start-move-duplicate initial)
|
||||
dws/duplicate-selected)
|
||||
;; Otherwise just plain old move
|
||||
(rx/of (start-move initial selected))))))))))
|
||||
|
||||
(defn start-move-duplicate [from-position]
|
||||
(ptk/reify ::start-move-selected
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(let [page-id (get state :current-page-id)
|
||||
objects (get-in state [:workspace-data page-id :objects])
|
||||
shapes (mapv #(get-in state [:workspace-data page-id :objects %]) ids)
|
||||
stopper (rx/filter ms/mouse-up? stream)
|
||||
layout (get state :workspace-layout)]
|
||||
(rx/concat
|
||||
(->> ms/mouse-position
|
||||
(rx/take-until stopper)
|
||||
(rx/map #(gpt/to-vec from-position %))
|
||||
(rx/switch-map #(snap/closest-snap-move page-id shapes objects layout %))
|
||||
(rx/map gmt/translate-matrix)
|
||||
(rx/map #(set-modifiers ids {:displacement %})))
|
||||
(->> stream
|
||||
(rx/filter (ptk/type? ::dws/duplicate-selected))
|
||||
(rx/first)
|
||||
(rx/map #(start-move from-position))))))
|
||||
|
||||
(rx/of (apply-modifiers ids)
|
||||
finish-transform))))))
|
||||
(defn start-move
|
||||
([from-position] (start-move from-position nil))
|
||||
([from-position ids]
|
||||
(ptk/reify ::start-move
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(-> state
|
||||
(assoc-in [:workspace-local :transform] :move)))
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(let [page-id (get state :current-page-id)
|
||||
objects (get-in state [:workspace-data page-id :objects])
|
||||
ids (if (nil? ids) (get-in state [:workspace-local :selected]) ids)
|
||||
shapes (mapv #(get-in state [:workspace-data page-id :objects %]) ids)
|
||||
stopper (rx/filter ms/mouse-up? stream)
|
||||
layout (get state :workspace-layout)]
|
||||
(rx/concat
|
||||
(->> ms/mouse-position
|
||||
(rx/take-until stopper)
|
||||
(rx/map #(gpt/to-vec from-position %))
|
||||
(rx/switch-map #(snap/closest-snap-move page-id shapes objects layout %))
|
||||
(rx/map gmt/translate-matrix)
|
||||
(rx/map #(set-modifiers ids {:displacement %})))
|
||||
|
||||
(rx/of (apply-modifiers ids)
|
||||
finish-transform)))))))
|
||||
|
||||
(defn- get-displacement-with-grid
|
||||
"Retrieve the correct displacement delta point for the
|
||||
|
|
|
@ -14,13 +14,13 @@
|
|||
|
||||
;; --- User Events
|
||||
|
||||
(defrecord KeyboardEvent [type key shift ctrl])
|
||||
(defrecord KeyboardEvent [type key shift ctrl alt])
|
||||
|
||||
(defn keyboard-event?
|
||||
[v]
|
||||
(instance? KeyboardEvent v))
|
||||
|
||||
(defrecord MouseEvent [type ctrl shift])
|
||||
(defrecord MouseEvent [type ctrl shift alt])
|
||||
|
||||
(defn mouse-event?
|
||||
[v]
|
||||
|
@ -36,7 +36,7 @@
|
|||
(and (mouse-event? v)
|
||||
(= :click (:type v))))
|
||||
|
||||
(defrecord PointerEvent [source pt ctrl shift])
|
||||
(defrecord PointerEvent [source pt ctrl shift alt])
|
||||
|
||||
(defn pointer-event?
|
||||
[v]
|
||||
|
@ -73,6 +73,24 @@
|
|||
(rx/subscribe-with ob sub)
|
||||
sub))
|
||||
|
||||
(defonce mouse-position-alt
|
||||
(let [sub (rx/behavior-subject nil)
|
||||
ob (->> st/stream
|
||||
(rx/filter pointer-event?)
|
||||
(rx/map :alt)
|
||||
(rx/dedupe))]
|
||||
(rx/subscribe-with ob sub)
|
||||
sub))
|
||||
|
||||
(defonce keyboard-alt
|
||||
(let [sub (rx/behavior-subject nil)
|
||||
ob (->> st/stream
|
||||
(rx/filter keyboard-event?)
|
||||
(rx/map :alt)
|
||||
(rx/dedupe))]
|
||||
(rx/subscribe-with ob sub)
|
||||
sub))
|
||||
|
||||
(defn mouse-position-deltas
|
||||
[current]
|
||||
(->> (rx/concat (rx/of current)
|
||||
|
@ -97,4 +115,3 @@
|
|||
(rx/map :point))]
|
||||
(rx/subscribe-with sob sub)
|
||||
sub))
|
||||
|
||||
|
|
|
@ -59,7 +59,7 @@
|
|||
transform (if rotation (str " transform='rotate(" rotation ")'") "")
|
||||
data (clojure.pprint/cl-format
|
||||
nil
|
||||
"url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' width='24px' height='24px'~A%3E~A%3C/svg%3E\") ~A ~A, auto"
|
||||
"url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' width='20px' height='20px'~A%3E~A%3C/svg%3E\") ~A ~A, auto"
|
||||
transform data x y)]
|
||||
data))
|
||||
|
||||
|
|
|
@ -53,4 +53,3 @@
|
|||
|
||||
[:span {:style {:white-space "nowrap"
|
||||
:margin-right "1rem"}} (pr-str key)]])))]))
|
||||
|
||||
|
|
|
@ -227,3 +227,10 @@
|
|||
|
||||
[(deref state) ref]))
|
||||
|
||||
|
||||
(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)))))
|
||||
|
|
|
@ -11,6 +11,10 @@
|
|||
"A workspace specific shapes wrappers."
|
||||
(:require
|
||||
[rumext.alpha :as mf]
|
||||
[beicon.core :as rx]
|
||||
[uxbox.main.streams :as ms]
|
||||
[uxbox.main.ui.hooks :as hooks]
|
||||
[uxbox.main.ui.cursors :as cur]
|
||||
[uxbox.main.ui.shapes.rect :as rect]
|
||||
[uxbox.main.ui.shapes.circle :as circle]
|
||||
[uxbox.main.ui.shapes.icon :as icon]
|
||||
|
@ -55,9 +59,13 @@
|
|||
(let [shape (unchecked-get props "shape")
|
||||
frame (unchecked-get props "frame")
|
||||
opts #js {:shape (->> shape (geom/transform-shape frame))
|
||||
:frame frame}]
|
||||
:frame frame}
|
||||
alt? (mf/use-state false)]
|
||||
|
||||
(hooks/use-stream ms/keyboard-alt #(reset! alt? %))
|
||||
|
||||
(when (and shape (not (:hidden shape)))
|
||||
[:*
|
||||
[:g.shape {:style {:cursor (if @alt? cur/duplicate nil)}}
|
||||
(case (:type shape)
|
||||
:curve [:> path/path-wrapper opts]
|
||||
:path [:> path/path-wrapper opts]
|
||||
|
|
|
@ -154,9 +154,8 @@
|
|||
(let [event (.-nativeEvent event)
|
||||
ctrl? (kbd/ctrl? event)
|
||||
shift? (kbd/shift? event)
|
||||
opts {:shift? shift?
|
||||
:ctrl? ctrl?}]
|
||||
(st/emit! (ms/->MouseEvent :down ctrl? shift?))
|
||||
alt? (kbd/alt? event)]
|
||||
(st/emit! (ms/->MouseEvent :down ctrl? shift? alt?))
|
||||
|
||||
(cond
|
||||
(and (not edition) (= 1 (.-which event)))
|
||||
|
@ -183,9 +182,8 @@
|
|||
(let [event (.-nativeEvent event)
|
||||
ctrl? (kbd/ctrl? event)
|
||||
shift? (kbd/shift? event)
|
||||
opts {:shift? shift?
|
||||
:ctrl? ctrl?}]
|
||||
(st/emit! (ms/->MouseEvent :up ctrl? shift?))
|
||||
alt? (kbd/alt? event)]
|
||||
(st/emit! (ms/->MouseEvent :up ctrl? shift? alt?))
|
||||
|
||||
(when (= 2 (.-which event))
|
||||
(st/emit! dw/finish-pan
|
||||
|
@ -213,9 +211,8 @@
|
|||
(dom/stop-propagation event)
|
||||
(let [ctrl? (kbd/ctrl? event)
|
||||
shift? (kbd/shift? event)
|
||||
opts {:shift? shift?
|
||||
:ctrl? ctrl?}]
|
||||
(st/emit! (ms/->MouseEvent :click ctrl? shift?)))))
|
||||
alt? (kbd/alt? event)]
|
||||
(st/emit! (ms/->MouseEvent :click ctrl? shift? alt?)))))
|
||||
|
||||
on-double-click
|
||||
(mf/use-callback
|
||||
|
@ -223,9 +220,8 @@
|
|||
(dom/stop-propagation event)
|
||||
(let [ctrl? (kbd/ctrl? event)
|
||||
shift? (kbd/shift? event)
|
||||
opts {:shift? shift?
|
||||
:ctrl? ctrl?}]
|
||||
(st/emit! (ms/->MouseEvent :double-click ctrl? shift?)))))
|
||||
alt? (kbd/alt? event)]
|
||||
(st/emit! (ms/->MouseEvent :double-click ctrl? shift? alt?)))))
|
||||
|
||||
on-key-down
|
||||
(mf/use-callback
|
||||
|
@ -234,13 +230,11 @@
|
|||
key (.-keyCode event)
|
||||
ctrl? (kbd/ctrl? event)
|
||||
shift? (kbd/shift? event)
|
||||
opts {:key key
|
||||
:shift? shift?
|
||||
:ctrl? ctrl?}
|
||||
alt? (kbd/alt? event)
|
||||
target (dom/get-target event)]
|
||||
|
||||
(when-not (.-repeat bevent)
|
||||
(st/emit! (ms/->KeyboardEvent :down key ctrl? shift?))
|
||||
(st/emit! (ms/->KeyboardEvent :down key ctrl? shift? alt?))
|
||||
(when (and (kbd/space? event)
|
||||
(not= "rich-text" (obj/get target "className")))
|
||||
(handle-viewport-positioning viewport-ref))))))
|
||||
|
@ -251,13 +245,10 @@
|
|||
(let [key (.-keyCode event)
|
||||
ctrl? (kbd/ctrl? event)
|
||||
shift? (kbd/shift? event)
|
||||
opts {:key key
|
||||
:shift? shift?
|
||||
:ctrl? ctrl?}]
|
||||
alt? (kbd/alt? event)]
|
||||
(when (kbd/space? event)
|
||||
(st/emit! dw/finish-pan
|
||||
::finish-positioning))
|
||||
(st/emit! (ms/->KeyboardEvent :up key ctrl? shift?)))))
|
||||
(st/emit! dw/finish-pan ::finish-positioning))
|
||||
(st/emit! (ms/->KeyboardEvent :up key ctrl? shift? alt?)))))
|
||||
|
||||
translate-point-to-viewport
|
||||
(fn [pt]
|
||||
|
@ -282,10 +273,12 @@
|
|||
(.-movementY event))]
|
||||
(st/emit! (ms/->PointerEvent :delta delta
|
||||
(kbd/ctrl? event)
|
||||
(kbd/shift? event)))
|
||||
(kbd/shift? event)
|
||||
(kbd/alt? event)))
|
||||
(st/emit! (ms/->PointerEvent :viewport pt
|
||||
(kbd/ctrl? event)
|
||||
(kbd/shift? event)))))
|
||||
(kbd/shift? event)
|
||||
(kbd/alt? event)))))
|
||||
|
||||
on-mouse-wheel
|
||||
(mf/use-callback
|
||||
|
|
Loading…
Add table
Reference in a new issue