♻️ Initial refactor of page data structure (wip).

Still work in progress but is a necessary step for a future
(re)introduction of groups.
This commit is contained in:
Andrey Antukh 2020-03-08 12:46:09 +01:00
parent cbad98b783
commit ba373573e0
29 changed files with 1116 additions and 787 deletions

View file

@ -12,6 +12,7 @@
[mount.core :as mount] [mount.core :as mount]
[promesa.core :as p] [promesa.core :as p]
[uxbox.config :as cfg] [uxbox.config :as cfg]
[uxbox.common.pages :as cp]
[uxbox.common.data :as d] [uxbox.common.data :as d]
[uxbox.core] [uxbox.core]
[uxbox.db :as db] [uxbox.db :as db]
@ -149,12 +150,7 @@
create-page create-page
(fn [conn owner-id project-id file-id index] (fn [conn owner-id project-id file-id index]
(p/let [id (mk-uuid "page" project-id file-id index) (p/let [id (mk-uuid "page" project-id file-id index)
data {:version 1 data cp/default-page-data
:shapes []
:canvas []
:options {}
:shapes-by-id {}}
name (str "page " index) name (str "page " index)
version 0 version 0
ordering index ordering index

View file

@ -86,3 +86,80 @@
'(promesa.core/let)]}}}}) '(promesa.core/let)]}}}})
(kondo/print!)))) (kondo/print!))))
(comment
{:version 1
:options {}
:shapes [:id1 :id2]
:canvas [:id3]
:shapes-by-id {:id1 {:canvas :id3} :id2 {} :id3 {}}})
(comment
{:version 2
:options {}
:objects
{:root
{:type :frame
:shapes [:sid0 :frame-0]}
:frame0
{:type :frame
:parent :root
:shapes [:sid1 :sid2]}
:sid0
{:type :rect
:parent :root}
:sid1
{:type :rect
:parent :frame0}
:sid2
{:type :group
:shapes [:sid3 :sid4]
:parent :frame0}
:sid3
{:type :elipse
:parent :sid2}
:sid4
{:type :elipse
:parent :sid2}}})
(comment
{:version 3
:options {}
:rmap
{:id1 :root-frame
:id2 :root-frame
:id3 :frame-id-1
:id4 :frame-id-2
:id5 :frame-id-2
:id6 :frame-id-2}
:frames
{:root-frame
{:type :frame
:shapes [:id1 :id2]
:objects
{:id1 {:type :rect}
:id2 {:type :elipse}}}
:frame-id-1
{:type :frame
:shapes [:id3]
:objects
{:id3 {:type :path}}}
:frame-id-2
{:type :frame
:shapes [:id4]
:objects
{:id4 {:type :group
:shapes [:id5 :id6]}
:id5 {:type :path :parent :id4}
:id6 {:type :elipse :parent :id4}}}}})

View file

@ -57,27 +57,6 @@
not-found)) not-found))
not-found coll))) not-found coll)))
(defn diff-maps
[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)]
(concat
(mapv #(vector :set % (get mb %)) added)
(mapv #(vector :set % 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 [:set k vmb]))))
rs)))))
(defn index-by (defn index-by
"Return a indexed map of the collection keyed by the result of "Return a indexed map of the collection keyed by the result of
executing the getter over each element of the collection." executing the getter over each element of the collection."

View file

@ -1,9 +1,18 @@
;; 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) 2019-2020 Andrey Antukh <niwi@niwi.nz>
(ns uxbox.common.pages (ns uxbox.common.pages
"A common (clj/cljs) functions and specs for pages." "A common (clj/cljs) functions and specs for pages."
(:require (:require
[uxbox.common.spec :as us]
[clojure.spec.alpha :as s] [clojure.spec.alpha :as s]
[uxbox.common.data :as d])) [uxbox.common.data :as d]
[uxbox.common.exceptions :as ex]
[uxbox.common.spec :as us]))
;; --- Specs ;; --- Specs
@ -46,7 +55,7 @@
(s/def ::stroke-style #{:none :solid :dotted :dashed :mixed}) (s/def ::stroke-style #{:none :solid :dotted :dashed :mixed})
(s/def ::stroke-width number?) (s/def ::stroke-width number?)
(s/def ::text-align #{"left" "right" "center" "justify"}) (s/def ::text-align #{"left" "right" "center" "justify"})
(s/def ::type #{:rect :path :circle :image :text :canvas :curve :icon}) (s/def ::type #{:rect :path :circle :image :text :canvas :curve :icon :frame})
(s/def ::x number?) (s/def ::x number?)
(s/def ::y number?) (s/def ::y number?)
(s/def ::cx number?) (s/def ::cx number?)
@ -92,162 +101,243 @@
(s/def ::shapes (s/coll-of uuid? :kind vector?)) (s/def ::shapes (s/coll-of uuid? :kind vector?))
(s/def ::canvas (s/coll-of uuid? :kind vector?)) (s/def ::canvas (s/coll-of uuid? :kind vector?))
(s/def ::shapes-by-id (s/def ::objects
(s/map-of uuid? ::shape)) (s/map-of uuid? ::shape))
(s/def ::data (s/def ::data
(s/keys :req-un [::shapes (s/keys :req-un [::options
::canvas ::version
::options ::objects]))
::shapes-by-id]))
;; Changes related (s/def ::attr keyword?)
(s/def ::operation (s/tuple #{:set} keyword? any?)) (s/def ::val any?)
(s/def ::parent-id uuid?)
(s/def ::frame-id uuid?)
(s/def ::operations (defmulti operation-spec-impl :type)
(s/coll-of ::operation :kind vector?))
(defmethod operation-spec-impl :set [_]
(s/keys :req-un [::attr ::val]))
(defmethod operation-spec-impl :mov [_]
(s/keys :req-un [::id ::index]))
(s/def ::operation (s/multi-spec operation-spec-impl :type))
(s/def ::operations (s/coll-of ::operation))
(defmulti change-spec-impl :type) (defmulti change-spec-impl :type)
(defmethod change-spec-impl :add-shape [_] (defmethod change-spec-impl :add-obj [_]
(s/keys :req-un [::shape ::id ::session-id] (s/keys :req-un [::id ::frame-id ::obj]
:opt-un [::index])) :opt-un [::session-id]))
(defmethod change-spec-impl :add-canvas [_] (defmethod change-spec-impl :mod-obj [_]
(s/keys :req-un [::shape ::id ::session-id] (s/keys :req-un [::id ::operations]
:opt-un [::index])) :opt-un [::session-id]))
(defmethod change-spec-impl :mod-shape [_] (defmethod change-spec-impl :del-obj [_]
(s/keys :req-un [::id ::operations ::session-id])) (s/keys :req-un [::id]
:opt-un [::session-id]))
(defmethod change-spec-impl :mov-shape [_] (defmethod change-spec-impl :mov-obj [_]
(s/keys :req-un [::id ::index ::session-id])) (s/keys :req-un [::id ::frame-id]
:opt-un [::session-id]))
(defmethod change-spec-impl :mod-opts [_] ;; (defmethod change-spec-impl :mod-shape [_]
(s/keys :req-un [::operations ::session-id])) ;; (s/keys :req-un [::id ::operations ::session-id]))
(defmethod change-spec-impl :del-shape [_] ;; (defmethod change-spec-impl :mov-shape [_]
(s/keys :req-un [::id ::session-id])) ;; (s/keys :req-un [::id ::index ::session-id]))
(defmethod change-spec-impl :del-canvas [_] ;; (defmethod change-spec-impl :mod-opts [_]
(s/keys :req-un [::id ::session-id])) ;; (s/keys :req-un [::operations ::session-id]))
;; (defmethod change-spec-impl :del-shape [_]
;; (s/keys :req-un [::id ::session-id]))
;; (defmethod change-spec-impl :del-canvas [_]
;; (s/keys :req-un [::id ::session-id]))
(s/def ::change (s/multi-spec change-spec-impl :type)) (s/def ::change (s/multi-spec change-spec-impl :type))
(s/def ::changes (s/coll-of ::change)) (s/def ::changes (s/coll-of ::change))
(def root #uuid "00000000-0000-0000-0000-000000000000")
(def default-page-data (def default-page-data
"A reference value of the empty page data." "A reference value of the empty page data."
{:version 1 {:version 3
:shapes []
:canvas []
:options {} :options {}
:shapes-by-id {}}) :objects
{root
{:id root
:type :frame
:name "root"
:shapes []}}})
;; --- Changes Processing Impl ;; --- Changes Processing Impl
(declare process-change) (defmulti process-change
(declare process-mod-shape) (fn [data change] (:type change)))
(declare process-mod-opts)
(declare process-mov-shape)
(declare process-add-shape)
(declare process-add-canvas)
(declare process-del-shape)
(declare process-del-canvas)
(defn process-changes (defn process-changes
[data items] [data items]
(->> (us/verify ::changes items) (->> (us/verify ::changes items)
(reduce process-change data))) (reduce #(or (process-change %1 %2) %1) data)))
(defn- process-change (defmethod process-change :add-obj
[data {:keys [type] :as change}] [data {:keys [id obj frame-id index] :as change}]
(case type (us/assert! (contains? (:objects data) frame-id) "process-change/add-obj")
:add-shape (process-add-shape data change) (let [obj (assoc obj
:add-canvas (process-add-canvas data change) :frame-id frame-id
:mod-shape (process-mod-shape data change) :id id)]
:mov-shape (process-mov-shape data change) (-> data
:del-shape (process-del-shape data change) (update :objects assoc id obj)
:del-canvas (process-del-canvas data change) (update-in [:objects frame-id :shapes]
:mod-opts (process-mod-opts data change))) (fn [shapes]
(cond
(some #{id} shapes)
shapes
(defn- process-add-shape (nil? index)
[data {:keys [id index shape] :as change}] (conj shapes id)
(-> data
(update :shapes (fn [shapes]
(cond
(some #{id} shapes)
shapes
(nil? index) :else
(conj shapes id) (let [[before after] (split-at index shapes)]
(d/concat [] before [id] after))))))))
:else (defn- process-obj-operation
(let [[before after] (split-at index shapes)] [shape op]
(d/concat [] before [id] after))))) (case (:type op)
(update :shapes-by-id assoc id shape))) :set
(let [attr (:attr op)
val (:val op)]
(if (nil? val)
(dissoc shape attr)
(assoc shape attr val)))
(defn- process-add-canvas (ex/raise :type :not-implemented
[data {:keys [id shape index] :as change}] :hint "TODO")))
(-> data
(update :canvas (fn [shapes]
(cond
(some #{id} shapes)
shapes
(nil? index) (defmethod process-change :mod-obj
(conj shapes id)
:else
(let [[before after] (split-at index shapes)]
(d/concat [] before [id] after)))))
(update :shapes-by-id assoc id shape)))
(defn- process-mod-shape
[data {:keys [id operations] :as change}] [data {:keys [id operations] :as change}]
(if (get-in data [:shapes-by-id id]) (us/assert! (contains? (:objects data) id) "process-change/mod-obj")
(update-in data [:shapes-by-id id] (update-in data [:objects id]
#(reduce (fn [shape [_ att val]] #(reduce process-obj-operation % operations)))
(if (nil? val)
(dissoc shape att)
(assoc shape att val)))
% operations))
data))
(defn- process-mod-opts (defmethod process-change :mov-obj
[data {:keys [operations]}] [data {:keys [id frame-id] :as change}]
(update data :options (us/assert! (contains? (:objects data) frame-id))
#(reduce (fn [options [_ att val]] (let [frame-id' (get-in data [:objects id :frame-id])]
(if (nil? val) (when (not= frame-id frame-id')
(dissoc options att) (-> data
(assoc options att val))) (update-in [:objects frame-id' :shapes] (fn [s] (filterv #(not= % id) s)))
% operations))) (update-in [:objects id] assoc :frame-id frame-id)
(update-in [:objects frame-id :shapes] conj id)))))
(defn- process-mov-shape (defmethod process-change :del-obj
[data {:keys [id index]}]
(let [shapes (:shapes data)
current-index (d/index-of shapes id)
shapes' (into [] (remove #(= % id) shapes))]
(cond
(= index current-index)
data
(nil? current-index)
(assoc data :shapes (d/concat [id] shapes'))
:else
(let [[before after] (split-at index shapes')]
(assoc data :shapes (d/concat [] before [id] after))))))
(defn- process-del-shape
[data {:keys [id] :as change}] [data {:keys [id] :as change}]
(-> data (when-let [{:keys [frame-id] :as obj} (get-in data [:objects id])]
(update :shapes (fn [s] (filterv #(not= % id) s))) (-> data
(update :shapes-by-id dissoc id))) (update :objects dissoc id)
(update-in [:objects frame-id :shapes]
(fn [s] (filterv #(not= % id) s))))))
(defn- process-del-canvas ;; (defn- process-change
[data {:keys [id] :as change}] ;; [data {:keys [type] :as change}]
(-> data ;; (case type
(update :canvas (fn [s] (filterv #(not= % id) s))) ;; :add-obj (process-add-obj data change)
(update :shapes-by-id dissoc id))) ;; :mod-obj (process-mod-obj data change)
;; ;; :add-shape (process-add-shape data change)
;; ;; :add-canvas (process-add-canvas data change)
;; ;; :mod-shape (process-mod-shape data change)
;; ;; :mov-shape (process-mov-shape data change)
;; ;; :del-shape (process-del-shape data change)
;; ;; :del-canvas (process-del-canvas data change)
;; ;; :mod-opts (process-mod-opts data change)
;; ))
;; (defn- process-add-obj
;; (defn- process-add-shape
;; [data {:keys [id index shape] :as change}]
;; (-> data
;; (update :shapes (fn [shapes]
;; (cond
;; (some #{id} shapes)
;; shapes
;; (nil? index)
;; (conj shapes id)
;; :else
;; (let [[before after] (split-at index shapes)]
;; (d/concat [] before [id] after)))))
;; (update :shapes-by-id assoc id shape)))
;; (defn- process-add-canvas
;; [data {:keys [id shape index] :as change}]
;; (-> data
;; (update :canvas (fn [shapes]
;; (cond
;; (some #{id} shapes)
;; shapes
;; (nil? index)
;; (conj shapes id)
;; :else
;; (let [[before after] (split-at index shapes)]
;; (d/concat [] before [id] after)))))
;; (update :shapes-by-id assoc id shape)))
;; (defn- process-mod-shape
;; [data {:keys [id operations] :as change}]
;; (if (get-in data [:shapes-by-id id])
;; (update-in data [:shapes-by-id id]
;; #(reduce (fn [shape [_ att val]]
;; (if (nil? val)
;; (dissoc shape att)
;; (assoc shape att val)))
;; % operations))
;; data))
;; (defn- process-mod-opts
;; [data {:keys [operations]}]
;; (update data :options
;; #(reduce (fn [options [_ att val]]
;; (if (nil? val)
;; (dissoc options att)
;; (assoc options att val)))
;; % operations)))
;; (defn- process-mov-shape
;; [data {:keys [id index]}]
;; (let [shapes (:shapes data)
;; current-index (d/index-of shapes id)
;; shapes' (into [] (remove #(= % id) shapes))]
;; (cond
;; (= index current-index)
;; data
;; (nil? current-index)
;; (assoc data :shapes (d/concat [id] shapes'))
;; :else
;; (let [[before after] (split-at index shapes')]
;; (assoc data :shapes (d/concat [] before [id] after))))))
;; (defn- process-del-shape
;; [data {:keys [id] :as change}]
;; (-> data
;; (update :shapes (fn [s] (filterv #(not= % id) s)))
;; (update :shapes-by-id dissoc id)))
;; (defn- process-del-canvas
;; [data {:keys [id] :as change}]
;; (-> data
;; (update :canvas (fn [s] (filterv #(not= % id) s)))
;; (update :shapes-by-id dissoc id)))

View file

@ -110,6 +110,21 @@
;; --- Macros ;; --- Macros
(defmacro assert!
"Evaluates expr and throws an exception if it does not evaluate to
logical true."
([x]
(when *assert*
`(when-not ~x
(throw (ex/error :type :assertion-error
:hint (str "Assert failed: " (pr-str '~x)))))))
([x message]
(when *assert*
`(when-not ~x
(throw (ex/error :type :assertion-error
:hint (str "Assert failed: " (pr-str '~x))
:message ~message))))))
(defn spec-assert (defn spec-assert
[spec x] [spec x]
(s/assert* spec x)) (s/assert* spec x))

View file

@ -9,35 +9,64 @@ and persistence protocol.
This is a page data structure: This is a page data structure:
``` ```
{:version 1 {:version 2
:options {} :options {}
:shapes [<id>, ...]
:canvas [<id>, ...] :rmap
:shapes-by-id {<id> <object>, ...}} {:id1 :default
:id2 :default
:id3 :id1}
:objects
{:root
{:type :root
:shapes [:id1 :id2]}
:id1
{:type :canvas
:shapes [:id3]}
:id2 {:type :rect}
:id3 {:type :circle}}}
``` ```
This is a potential list of persistent ops: This is a potential list of persistent ops:
``` ```
;; Generic (Shapes & Canvas) {:type :mod-opts
[:mod-shape <id> [:set <attr> <val?>], ...] :operations [<op>, ...]
;; Example: {:type :add-obj
;; [:mod-shape 1 [:set :x 2] [:set :y 3]] :id <uuid>
:parent <uuid>
:obj <shape-object>}
;; Specific {:type :mod-obj
[:add-shape <id> <object>] :id <uuid>
[:add-canvas <id> <object>] :operations [<op>, ...]}
[:del-shape <id>] {:type :mov-obj
[:del-canvas <id>] :id <uuid>
:dest <uuid>}
[:mov-canvas <id> :after <id|null>] ;; null implies at first position {:type :del-obj
[:mov-shape <id> :after <id|null>] :id <uuid>}
[:mod-opts [:set <attr> <val>], [:del <attr> nil], ...]
``` ```
This is a potential list of operations:
```
{:type :set
:attr <any>
:val <any>}
{:type :mov
:id <uuid>
:index <int>}
```
## Ephemeral communication (Websocket protocol) ## Ephemeral communication (Websocket protocol)

View file

@ -3,9 +3,9 @@
org.clojure/clojure {:mvn/version "1.10.1"} org.clojure/clojure {:mvn/version "1.10.1"}
com.cognitect/transit-cljs {:mvn/version "0.8.256"} com.cognitect/transit-cljs {:mvn/version "0.8.256"}
cljsjs/react {:mvn/version "16.12.0-1"} cljsjs/react {:mvn/version "16.13.0-0"}
cljsjs/react-dom {:mvn/version "16.12.0-1"} cljsjs/react-dom {:mvn/version "16.13.0-0"}
cljsjs/react-dom-server {:mvn/version "16.12.0-1"} cljsjs/react-dom-server {:mvn/version "16.13.0-0"}
environ/environ {:mvn/version "1.1.0"} environ/environ {:mvn/version "1.1.0"}
metosin/reitit-core {:mvn/version "0.3.10"} metosin/reitit-core {:mvn/version "0.3.10"}

View file

@ -9,8 +9,8 @@
(def viewport-width 4000) (def viewport-width 4000)
(def viewport-height 4000) (def viewport-height 4000)
(def canvas-start-x 1200) (def frame-start-x 1200)
(def canvas-start-y 1200) (def frame-start-y 1200)
(def grid-x-axis 10) (def grid-x-axis 10)
(def grid-y-axis 10) (def grid-y-axis 10)

File diff suppressed because it is too large Load diff

View file

@ -11,7 +11,7 @@
[rumext.alpha :as mf] [rumext.alpha :as mf]
[uxbox.util.math :as mth] [uxbox.util.math :as mth]
[uxbox.main.geom :as geom] [uxbox.main.geom :as geom]
[uxbox.main.ui.shapes.canvas :as canvas] [uxbox.main.ui.shapes.frame :as frame]
[uxbox.main.ui.shapes.circle :as circle] [uxbox.main.ui.shapes.circle :as circle]
[uxbox.main.ui.shapes.icon :as icon] [uxbox.main.ui.shapes.icon :as icon]
[uxbox.main.ui.shapes.image :as image] [uxbox.main.ui.shapes.image :as image]
@ -40,7 +40,7 @@
[{:keys [shape] :as props}] [{:keys [shape] :as props}]
(when (and shape (not (:hidden shape))) (when (and shape (not (:hidden shape)))
(case (:type shape) (case (:type shape)
:canvas [:& rect/rect-shape {:shape shape}] :frame [:& rect/rect-shape {:shape shape}]
:curve [:& path/path-shape {:shape shape}] :curve [:& path/path-shape {:shape shape}]
:text [:& text/text-shape {:shape shape}] :text [:& text/text-shape {:shape shape}]
:icon [:& icon/icon-shape {:shape shape}] :icon [:& icon/icon-shape {:shape shape}]
@ -53,7 +53,7 @@
[{:keys [data] :as props}] [{:keys [data] :as props}]
(let [shapes-by-id (:shapes-by-id data) (let [shapes-by-id (:shapes-by-id data)
shapes (map #(get shapes-by-id %) (:shapes data [])) shapes (map #(get shapes-by-id %) (:shapes data []))
canvas (map #(get shapes-by-id %) (:canvas data [])) frame (map #(get shapes-by-id %) (:frame data []))
dim (calculate-dimensions data)] dim (calculate-dimensions data)]
[:svg {:view-box (str "0 0 " (:width dim 0) " " (:height dim 0)) [:svg {:view-box (str "0 0 " (:width dim 0) " " (:height dim 0))
:version "1.1" :version "1.1"
@ -61,7 +61,7 @@
:xmlns "http://www.w3.org/2000/svg"} :xmlns "http://www.w3.org/2000/svg"}
(background) (background)
[:* [:*
(for [item canvas] (for [item frame]
[:& shape-wrapper {:shape item :key (:id item)}]) [:& shape-wrapper {:shape item :key (:id item)}])
(for [item shapes] (for [item shapes]
[:& shape-wrapper {:shape item :key (:id item)}])]])) [:& shape-wrapper {:shape item :key (:id item)}])]]))

View file

@ -27,7 +27,7 @@
:icon (move-rect shape dpoint) :icon (move-rect shape dpoint)
:image (move-rect shape dpoint) :image (move-rect shape dpoint)
:rect (move-rect shape dpoint) :rect (move-rect shape dpoint)
:canvas (move-rect shape dpoint) :frame (move-rect shape dpoint)
:text (move-rect shape dpoint) :text (move-rect shape dpoint)
:curve (move-path shape dpoint) :curve (move-path shape dpoint)
:path (move-path shape dpoint) :path (move-path shape dpoint)
@ -69,7 +69,7 @@
[shape position] [shape position]
(case (:type shape) (case (:type shape)
:icon (absolute-move-rect shape position) :icon (absolute-move-rect shape position)
:canvas (absolute-move-rect shape position) :frame (absolute-move-rect shape position)
:image (absolute-move-rect shape position) :image (absolute-move-rect shape position)
:rect (absolute-move-rect shape position) :rect (absolute-move-rect shape position)
:circle (absolute-move-circle shape position))) :circle (absolute-move-circle shape position)))
@ -482,6 +482,25 @@
:width width :width width
:height height))) :height height)))
;; --- Resolve Shape
(declare resolve-rect-shape)
(declare translate-from-frame)
(declare translate-to-frame)
(defn resolve-shape
[objects shape]
(case (:type shape)
:rect (resolve-rect-shape objects shape)
:frame (resolve-rect-shape objects shape)))
(defn- resolve-rect-shape
[objects {:keys [parent] :as shape}]
(loop [pobj (get objects parent)]
(if (= :frame (:type pobj))
(translate-from-frame shape pobj)
(recur (get objects (:parent pobj))))))
;; --- Transform Shape ;; --- Transform Shape
(declare transform-rect) (declare transform-rect)
@ -492,7 +511,7 @@
"Apply the matrix transformation to shape." "Apply the matrix transformation to shape."
[{:keys [type] :as shape} xfmt] [{:keys [type] :as shape} xfmt]
(case type (case type
:canvas (transform-rect shape xfmt) :frame (transform-rect shape xfmt)
:rect (transform-rect shape xfmt) :rect (transform-rect shape xfmt)
:icon (transform-rect shape xfmt) :icon (transform-rect shape xfmt)
:text (transform-rect shape xfmt) :text (transform-rect shape xfmt)
@ -599,6 +618,14 @@
:height (- maxy miny) :height (- maxy miny)
:type :rect})) :type :rect}))
(defn translate-to-frame
[shape {:keys [x y] :as frame}]
(move shape (gpt/point (- x) (- y))))
(defn translate-from-frame
[shape {:keys [x y] :as frame}]
(move shape (gpt/point (+ x) (+ y))))
;; --- Helpers ;; --- Helpers
(defn contained-in? (defn contained-in?

View file

@ -59,8 +59,8 @@
(-> (l/lens #(contains? % id)) (-> (l/lens #(contains? % id))
(l/derive selected-shapes))) (l/derive selected-shapes)))
(def selected-canvas (def selected-frame
(-> (l/key :selected-canvas) (-> (l/key :selected-frame)
(l/derive workspace-local))) (l/derive workspace-local)))
(def toolboxes (def toolboxes

View file

@ -13,7 +13,7 @@
[rumext.alpha :as mf] [rumext.alpha :as mf]
[uxbox.main.refs :as refs] [uxbox.main.refs :as refs]
[uxbox.main.store :as st] [uxbox.main.store :as st]
[uxbox.main.ui.shapes.canvas :as canvas])) [uxbox.main.ui.shapes.frame :as frame]))
(def shape-wrapper canvas/shape-wrapper) (def shape-wrapper frame/shape-wrapper)
(def canvas-wrapper canvas/canvas-wrapper) (def frame-wrapper frame/frame-wrapper)

View file

@ -5,7 +5,9 @@
;; Copyright (c) 2016-2017 Andrey Antukh <niwi@niwi.nz> ;; Copyright (c) 2016-2017 Andrey Antukh <niwi@niwi.nz>
(ns uxbox.main.ui.shapes.attrs (ns uxbox.main.ui.shapes.attrs
(:require [cuerdas.core :as str])) (:require
[cuerdas.core :as str]
[uxbox.util.interop :as interop]))
;; (defn camel-case ;; (defn camel-case
@ -58,7 +60,8 @@
(case style (case style
:mixed "5,5,1,5" :mixed "5,5,1,5"
:dotted "5,5" :dotted "5,5"
:dashed "10,10")) :dashed "10,10"
nil))
(defn- transform-stroke-attrs (defn- transform-stroke-attrs
[{:keys [stroke-style] :or {stroke-style :none} :as attrs}] [{:keys [stroke-style] :or {stroke-style :none} :as attrs}]
@ -75,3 +78,21 @@
(-> (select-keys shape shape-style-attrs) (-> (select-keys shape shape-style-attrs)
(transform-stroke-attrs) (transform-stroke-attrs)
(process-attrs))) (process-attrs)))
;; TODO: migrate all the code to use this function and then, rename.
(defn extract-style-attrs2
[shape]
(let [stroke-style (:stroke-style shape :none)
attrs #js {:fill (:fill-color shape nil)
:opacity (:opacity shape nil)
:rx (:rx shape nil)
:ry (:ry shape nil)}]
(when (not= :none stroke-style)
(interop/obj-assign! attrs
#js {:stroke (:stroke-color shape nil)
:strokeWidth (:stroke-width shape nil)
:strokeOpacity (:stroke-opacity shape nil)
:strokeDasharray (stroke-type->dasharray stroke-style)}))
attrs))

View file

@ -43,21 +43,21 @@
(rx/of (dw/materialize-displacement-in-bulk selected) (rx/of (dw/materialize-displacement-in-bulk selected)
::dw/page-data-update)))))) ::dw/page-data-update))))))
(def start-move-canvas (def start-move-frame
(ptk/reify ::start-move-selected (ptk/reify ::start-move-selected
ptk/WatchEvent ptk/WatchEvent
(watch [_ state stream] (watch [_ state stream]
(let [flags (get-in state [:workspace-local :flags]) (let [flags (get-in state [:workspace-local :flags])
selected (get-in state [:workspace-local :selected]) selected (get-in state [:workspace-local :selected])
stoper (rx/filter uws/mouse-up? stream) stoper (rx/filter uws/mouse-up? stream)
canvas-id (first selected) frame-id (first selected)
position @uws/mouse-position] position @uws/mouse-position]
(rx/concat (rx/concat
(->> (uws/mouse-position-deltas position) (->> (uws/mouse-position-deltas position)
(rx/map #(dw/apply-canvas-displacement canvas-id %)) (rx/map #(dw/apply-frame-displacement frame-id %))
(rx/take-until stoper)) (rx/take-until stoper))
(rx/of (dw/materialize-canvas-displacement canvas-id))))))) (rx/of (dw/materialize-frame-displacement frame-id)))))))
(defn on-mouse-down (defn on-mouse-down
([event shape] (on-mouse-down event shape nil)) ([event shape] (on-mouse-down event shape nil))
@ -70,10 +70,10 @@
drawing? drawing?
nil nil
(= type :canvas) (= type :frame)
(when selected? (when selected?
(dom/stop-propagation event) (dom/stop-propagation event)
(st/emit! start-move-canvas)) (st/emit! start-move-frame))
(and (not selected?) (empty? selected)) (and (not selected?) (empty? selected))
(do (do

View file

@ -8,7 +8,7 @@
;; Copyright (c) 2015-2020 Andrey Antukh <niwi@niwi.nz> ;; Copyright (c) 2015-2020 Andrey Antukh <niwi@niwi.nz>
;; Copyright (c) 2015-2020 Juan de la Cruz <delacruzgarciajuan@gmail.com> ;; Copyright (c) 2015-2020 Juan de la Cruz <delacruzgarciajuan@gmail.com>
(ns uxbox.main.ui.shapes.canvas (ns uxbox.main.ui.shapes.frame
(:require (:require
[lentes.core :as l] [lentes.core :as l]
[rumext.alpha :as mf] [rumext.alpha :as mf]
@ -30,14 +30,24 @@
[uxbox.util.geom.matrix :as gmt] [uxbox.util.geom.matrix :as gmt]
[uxbox.util.geom.point :as gpt])) [uxbox.util.geom.point :as gpt]))
(declare canvas-wrapper) (declare frame-wrapper)
(defn wrap-memo-shape
([component]
(js/React.memo
component
(fn [np op]
(let [n-shape (aget np "shape")
o-shape (aget op "shape")]
(= n-shape o-shape))))))
(mf/defc shape-wrapper (mf/defc shape-wrapper
{:wrap [#(mf/wrap-memo % =)]} {:wrap [wrap-memo-shape]}
[{:keys [shape] :as props}] [{:keys [shape] :as props}]
(when (and shape (not (:hidden shape))) (when (and shape (not (:hidden shape)))
(case (:type shape) (case (:type shape)
:canvas [:& canvas-wrapper {:shape shape :childs []}] :frame [:& frame-wrapper {:shape shape :childs []}]
:curve [:& path/path-wrapper {:shape shape}] :curve [:& path/path-wrapper {:shape shape}]
:text [:& text/text-wrapper {:shape shape}] :text [:& text/text-wrapper {:shape shape}]
:icon [:& icon/icon-wrapper {:shape shape}] :icon [:& icon/icon-wrapper {:shape shape}]
@ -46,22 +56,48 @@
:image [:& image/image-wrapper {:shape shape}] :image [:& image/image-wrapper {:shape shape}]
:circle [:& circle/circle-wrapper {:shape shape}]))) :circle [:& circle/circle-wrapper {:shape shape}])))
(def canvas-default-props (def frame-default-props
{:fill-color "#ffffff"}) {:fill-color "#ffffff"})
(declare canvas-shape) (declare frame-shape)
(declare translate-to-canvas) (declare translate-to-frame)
(mf/defc canvas-wrapper (def kaka [1 2 3])
{:wrap [#(mf/wrap-memo % =)]}
[{:keys [shape childs] :as props}] (defn wrap-memo-frame
([component]
(js/React.memo
component
(fn [np op]
(let [n-shape (aget np "shape")
o-shape (aget op "shape")
n-objs (aget np "objects")
o-objs (aget op "objects")
ids (:shapes n-shape)]
(and (identical? n-shape o-shape)
(loop [id (first ids)
ids (rest ids)]
(if (nil? id)
true
(if (identical? (get n-objs id)
(get o-objs id))
(recur (first ids) (rest ids))
false)))))))))
(mf/defc frame-wrapper
{:wrap [wrap-memo-frame]}
[{:keys [shape objects] :as props}]
(when (and shape (not (:hidden shape))) (when (and shape (not (:hidden shape)))
(let [selected-iref (mf/use-memo (let [selected-iref (mf/use-memo
{:fn #(refs/make-selected (:id shape)) {:fn #(refs/make-selected (:id shape))
:deps (mf/deps (:id shape))}) :deps (mf/deps (:id shape))})
selected? (mf/deref selected-iref) selected? (mf/deref selected-iref)
on-mouse-down #(common/on-mouse-down % shape) on-mouse-down #(common/on-mouse-down % shape)
shape (merge canvas-default-props shape) shape (merge frame-default-props shape)
childs (mapv #(get objects %) (:shapes shape))
on-double-click on-double-click
(fn [event] (fn [event]
@ -71,9 +107,9 @@
[:g {:class (when selected? "selected") [:g {:class (when selected? "selected")
:on-double-click on-double-click :on-double-click on-double-click
:on-mouse-down on-mouse-down} :on-mouse-down on-mouse-down}
[:& canvas-shape {:shape shape :childs childs}]]))) [:& frame-shape {:shape shape :childs childs}]])))
(mf/defc canvas-shape (mf/defc frame-shape
[{:keys [shape childs] :as props}] [{:keys [shape childs] :as props}]
(let [rotation (:rotation shape) (let [rotation (:rotation shape)
ds-modifier (:displacement-modifier shape) ds-modifier (:displacement-modifier shape)
@ -93,7 +129,7 @@
:height height :height height
)) ))
translate #(translate-to-canvas % ds-modifier (gpt/point (- x) (- y))) translate #(translate-to-frame % ds-modifier (gpt/point (- x) (- y)))
] ]
[:svg {:x x :y y :width width :height height} [:svg {:x x :y y :width width :height height}
@ -101,12 +137,12 @@
(for [item childs] (for [item childs]
[:& shape-wrapper {:shape (translate item) :key (:id item)}])])) [:& shape-wrapper {:shape (translate item) :key (:id item)}])]))
(defn- translate-to-canvas (defn- translate-to-frame
[shape canvas-ds-modifier pt] [shape frame-ds-modifier pt]
(let [rz-modifier (:resize-modifier shape) (let [rz-modifier (:resize-modifier shape)
shape (cond-> shape shape (cond-> shape
(gmt/matrix? canvas-ds-modifier) (gmt/matrix? frame-ds-modifier)
(geom/transform canvas-ds-modifier) (geom/transform frame-ds-modifier)
(gmt/matrix? rz-modifier) (gmt/matrix? rz-modifier)
(-> (geom/transform rz-modifier) (-> (geom/transform rz-modifier)

View file

@ -12,8 +12,7 @@
[uxbox.main.refs :as refs] [uxbox.main.refs :as refs]
[uxbox.main.ui.shapes.attrs :as attrs] [uxbox.main.ui.shapes.attrs :as attrs]
[uxbox.main.ui.shapes.common :as common] [uxbox.main.ui.shapes.common :as common]
[uxbox.util.data :refer [classnames normalize-props]] [uxbox.util.interop :as interop]
[uxbox.util.dom :as dom]
[uxbox.util.geom.matrix :as gmt] [uxbox.util.geom.matrix :as gmt]
[uxbox.util.geom.point :as gpt])) [uxbox.util.geom.point :as gpt]))
@ -21,23 +20,19 @@
(declare rect-shape) (declare rect-shape)
(mf/defc rect-wrapper (mf/defrc rect-wrapper
{:wrap [#(mf/wrap-memo % =)]} [props]
[{:keys [shape] :as props}] (let [shape (unchecked-get props "shape")
(let [selected-iref (mf/use-memo
{:fn #(refs/make-selected (:id shape))
:deps (mf/deps (:id shape))})
selected? (mf/deref selected-iref)
on-mouse-down #(common/on-mouse-down % shape)] on-mouse-down #(common/on-mouse-down % shape)]
[:g.shape {:class (when selected? "selected") [:g.shape {:on-mouse-down on-mouse-down}
:on-mouse-down on-mouse-down}
[:& rect-shape {:shape shape}]])) [:& rect-shape {:shape shape}]]))
;; --- Rect Shape ;; --- Rect Shape
(mf/defc rect-shape (mf/defrc rect-shape
[{:keys [shape] :as props}] [props]
(let [ds-modifier (:displacement-modifier shape) (let [shape (unchecked-get props "shape")
ds-modifier (:displacement-modifier shape)
rz-modifier (:resize-modifier shape) rz-modifier (:resize-modifier shape)
shape (cond-> shape shape (cond-> shape
@ -52,12 +47,13 @@
(+ x (/ width 2)) (+ x (/ width 2))
(+ y (/ height 2)))) (+ y (/ height 2))))
props (-> (attrs/extract-style-attrs shape) props (-> (attrs/extract-style-attrs2 shape)
(assoc :x x (interop/obj-assign!
:y y #js {:x x
:transform transform :y y
:id (str "shape-" id) :transform transform
:width width :id (str "shape-" id)
:height height :width width
))] :height height}))]
[:& "rect" props]))
[:> "rect" props]))

View file

@ -45,10 +45,10 @@
(st/emit! (ms/->ScrollEvent (gpt/point left top))))) (st/emit! (ms/->ScrollEvent (gpt/point left top)))))
(defn- on-wheel (defn- on-wheel
[event canvas] [event frame]
(when (kbd/ctrl? event) (when (kbd/ctrl? event)
(let [prev-zoom @refs/selected-zoom (let [prev-zoom @refs/selected-zoom
dom (mf/ref-node canvas) dom (mf/ref-node frame)
scroll-position (scroll/get-current-position-absolute dom) scroll-position (scroll/get-current-position-absolute dom)
mouse-point @ms/mouse-position] mouse-point @ms/mouse-position]
(dom/prevent-default event) (dom/prevent-default event)
@ -60,7 +60,7 @@
(mf/defc workspace-content (mf/defc workspace-content
[{:keys [page file flags] :as params}] [{:keys [page file flags] :as params}]
(let [canvas (mf/use-ref nil) (let [frame (mf/use-ref nil)
layout (mf/deref refs/workspace-layout) layout (mf/deref refs/workspace-layout)
left-sidebar? (not (empty? (keep layout [:layers :sitemap left-sidebar? (not (empty? (keep layout [:layers :sitemap
:document-history]))) :document-history])))
@ -77,7 +77,7 @@
[:section.workspace-content [:section.workspace-content
{:class classes {:class classes
:on-scroll on-scroll :on-scroll on-scroll
:on-wheel #(on-wheel % canvas)} :on-wheel #(on-wheel % frame)}
[:& history-dialog] [:& history-dialog]
@ -87,7 +87,7 @@
[:& horizontal-rule] [:& horizontal-rule]
[:& vertical-rule]]) [:& vertical-rule]])
[:section.workspace-viewport {:id "workspace-viewport" :ref canvas} [:section.workspace-viewport {:id "workspace-viewport" :ref frame}
[:& viewport {:page page}]]] [:& viewport {:page page}]]]
;; Aside ;; Aside

View file

@ -52,7 +52,7 @@
:fill-color "#000000" :fill-color "#000000"
:fill-opacity 0 :fill-opacity 0
:segments []} :segments []}
{:type :canvas {:type :frame
:name "Canvas"} :name "Canvas"}
{:type :curve {:type :curve
:name "Path" :name "Path"
@ -281,8 +281,8 @@
shape (dissoc shape ::initialized? :resize-modifier)] shape (dissoc shape ::initialized? :resize-modifier)]
;; Add & select the created shape to the workspace ;; Add & select the created shape to the workspace
(rx/of dw/deselect-all (rx/of dw/deselect-all
(if (= :canvas (:type shape)) (if (= :frame (:type shape))
(dw/add-canvas shape) (dw/add-frame shape)
(dw/add-shape shape)))))))))) (dw/add-shape shape))))))))))
(def close-drawing-path (def close-drawing-path

View file

@ -94,9 +94,9 @@
[:div.workspace-options [:div.workspace-options
[:ul.options-btn [:ul.options-btn
[:li.tooltip.tooltip-bottom [:li.tooltip.tooltip-bottom
{:alt (tr "workspace.header.canvas") {:alt (tr "workspace.header.frame")
:class (when (= selected-drawtool :canvas) "selected") :class (when (= selected-drawtool :frame) "selected")
:on-click (partial select-drawtool :canvas)} :on-click (partial select-drawtool :frame)}
i/artboard] i/artboard]
[:li.tooltip.tooltip-bottom [:li.tooltip.tooltip-bottom
{:alt (tr "workspace.header.rect") {:alt (tr "workspace.header.rect")

View file

@ -133,7 +133,7 @@
:stroke-opacity "1"}}] :stroke-opacity "1"}}]
(when (and (fn? on-rotate) (when (and (fn? on-rotate)
(not= :canvas (:type shape))) (not= :frame (:type shape)))
[:* [:*
[:path {:stroke "#31EFB8" [:path {:stroke "#31EFB8"
:stroke-opacity "1" :stroke-opacity "1"
@ -255,7 +255,7 @@
:on-resize on-resize}])) :on-resize on-resize}]))
(mf/defc single-selection-handlers (mf/defc single-selection-handlers
[{:keys [shape zoom] :as props}] [{:keys [shape zoom objects] :as props}]
(let [on-resize #(do (dom/stop-propagation %2) (let [on-resize #(do (dom/stop-propagation %2)
(st/emit! (start-resize %1 #{(:id shape)} shape))) (st/emit! (start-resize %1 #{(:id shape)} shape)))
on-rotate #(do (dom/stop-propagation %) on-rotate #(do (dom/stop-propagation %)
@ -263,6 +263,7 @@
ds-modifier (:displacement-modifier shape) ds-modifier (:displacement-modifier shape)
rz-modifier (:resize-modifier shape) rz-modifier (:resize-modifier shape)
;; shape (geom/resolve-shape objects shape)
shape (cond-> (geom/shape->rect-shape shape) shape (cond-> (geom/shape->rect-shape shape)
(gmt/matrix? rz-modifier) (geom/transform rz-modifier) (gmt/matrix? rz-modifier) (geom/transform rz-modifier)
(gmt/matrix? ds-modifier) (geom/transform ds-modifier))] (gmt/matrix? ds-modifier) (geom/transform ds-modifier))]
@ -274,11 +275,13 @@
(mf/defc selection-handlers (mf/defc selection-handlers
[{:keys [selected edition zoom] :as props}] [{:keys [selected edition zoom] :as props}]
(let [data (mf/deref refs/workspace-data) (let [data (mf/deref refs/workspace-data)
objects (:objects data)
;; We need remove posible nil values because on shape ;; We need remove posible nil values because on shape
;; deletion many shape will reamin selected and deleted ;; deletion many shape will reamin selected and deleted
;; in the same time for small instant of time ;; in the same time for small instant of time
shapes (->> (map #(get-in data [:shapes-by-id %]) selected) shapes (->> (map #(get objects %) selected)
(remove nil?)) (remove nil?))
num (count shapes) num (count shapes)
{:keys [id type] :as shape} (first shapes)] {:keys [id type] :as shape} (first shapes)]
@ -303,4 +306,5 @@
:else :else
[:& single-selection-handlers {:shape shape [:& single-selection-handlers {:shape shape
:objects objects
:zoom zoom}]))) :zoom zoom}])))

View file

@ -21,6 +21,7 @@
[uxbox.main.ui.shapes.icon :as icon] [uxbox.main.ui.shapes.icon :as icon]
[uxbox.main.ui.workspace.sortable :refer [use-sortable]] [uxbox.main.ui.workspace.sortable :refer [use-sortable]]
[uxbox.util.dom :as dom] [uxbox.util.dom :as dom]
[uxbox.util.uuid :as uuid]
[uxbox.util.i18n :as i18n :refer [t]])) [uxbox.util.i18n :as i18n :refer [t]]))
(def ^:private shapes-iref (def ^:private shapes-iref
@ -76,34 +77,35 @@
{:on-double-click on-click} {:on-double-click on-click}
(:name shape "")]))) (:name shape "")])))
;; --- Layer Item
(def strip-attrs
#(select-keys % [:id :frame :name :type :hidden :blocked]))
(mf/defc layer-item (mf/defc layer-item
{:wrap [#(mf/wrap-memo % =)]} {:wrap [mf/wrap-memo]}
[{:keys [shape selected index] :as props}] [{:keys [index item selected] :as props}]
(let [selected? (contains? selected (:id shape)) (let [selected? (contains? selected (:id item))
toggle-blocking toggle-blocking
(fn [event] (fn [event]
(dom/stop-propagation event) (dom/stop-propagation event)
(if (:blocked shape) (if (:blocked item)
(st/emit! (dw/unblock-shape (:id shape))) (st/emit! (dw/unblock-shape (:id item)))
(st/emit! (dw/block-shape (:id shape))))) (st/emit! (dw/block-shape (:id item)))))
toggle-visibility toggle-visibility
(fn [event] (fn [event]
(dom/stop-propagation event) (dom/stop-propagation event)
(if (:hidden shape) (if (:hidden item)
(st/emit! (dw/show-shape (:id shape))) (st/emit! (dw/show-shape (:id item)))
(st/emit! (dw/hide-shape (:id shape))))) (st/emit! (dw/hide-shape (:id item)))))
select-shape select-shape
(fn [event] (fn [event]
(dom/prevent-default event) (dom/prevent-default event)
(let [id (:id shape)] (let [id (:id item)]
(cond (cond
(or (:blocked shape) (or (:blocked item)
(:hidden shape)) (:hidden item))
nil nil
(.-ctrlKey event) (.-ctrlKey event)
@ -118,7 +120,7 @@
on-drop on-drop
(fn [item monitor] (fn [item monitor]
(st/emit! (dw/commit-shape-order-change (:shape-id item)))) #_(st/emit! (dw/commit-shape-order-change (:shape-id item))))
on-hover on-hover
(fn [item monitor] (fn [item monitor]
@ -126,8 +128,8 @@
[dprops dnd-ref] (use-sortable [dprops dnd-ref] (use-sortable
{:type "layer-item" {:type "layer-item"
:data {:shape-id (:id shape) :data {:shape-id (:id item)
:page-id (:page shape) :page-id (:page item)
:index index} :index index}
:on-hover on-hover :on-hover on-hover
:on-drop on-drop})] :on-drop on-drop})]
@ -139,24 +141,22 @@
:on-click select-shape :on-click select-shape
:on-double-click #(dom/stop-propagation %)} :on-double-click #(dom/stop-propagation %)}
[:div.element-actions [:div.element-actions
[:div.toggle-element {:class (when-not (:hidden shape) "selected") [:div.toggle-element {:class (when-not (:hidden item) "selected")
:on-click toggle-visibility} :on-click toggle-visibility}
i/eye] i/eye]
[:div.block-element {:class (when (:blocked shape) "selected") [:div.block-element {:class (when (:blocked item) "selected")
:on-click toggle-blocking} :on-click toggle-blocking}
i/lock]] i/lock]]
[:& element-icon {:shape shape}] [:& element-icon {:shape item}]
[:& layer-name {:shape shape}]]])) [:& layer-name {:shape item}]]]))
(mf/defc canvas-item (mf/defc layer-frame-item
{:wrap [#(mf/wrap-memo % =)]} {:wrap [#(mf/wrap-memo % =)]}
[{:keys [canvas shapes selected index] :as props}] [{:keys [item selected index objects] :as props}]
(let [selected? (contains? selected (:id canvas)) (let [selected? (contains? selected (:id item))
local (mf/use-state {:collapsed false}) local (mf/use-state {:collapsed false})
collapsed? (:collapsed @local) collapsed? (:collapsed @local)
shapes (filter #(= (:canvas (second %)) (:id canvas)) shapes)
toggle-collapse toggle-collapse
(fn [event] (fn [event]
(dom/stop-propagation event) (dom/stop-propagation event)
@ -165,24 +165,24 @@
toggle-blocking toggle-blocking
(fn [event] (fn [event]
(dom/stop-propagation event) (dom/stop-propagation event)
(if (:blocked canvas) (if (:blocked item)
(st/emit! (dw/unblock-shape (:id canvas))) (st/emit! (dw/unblock-shape (:id item)))
(st/emit! (dw/block-shape (:id canvas))))) (st/emit! (dw/block-shape (:id item)))))
toggle-visibility toggle-visibility
(fn [event] (fn [event]
(dom/stop-propagation event) (dom/stop-propagation event)
(if (:hidden canvas) (if (:hidden item)
(st/emit! (dw/show-canvas (:id canvas))) (st/emit! (dw/show-frame (:id item)))
(st/emit! (dw/hide-canvas (:id canvas))))) (st/emit! (dw/hide-frame (:id item)))))
select-shape select-shape
(fn [event] (fn [event]
(dom/prevent-default event) (dom/prevent-default event)
(let [id (:id canvas)] (let [id (:id item)]
(cond (cond
(or (:blocked canvas) (or (:blocked item)
(:hidden canvas)) (:hidden item))
nil nil
(.-ctrlKey event) (.-ctrlKey event)
@ -201,13 +201,13 @@
on-hover on-hover
(fn [item monitor] (fn [item monitor]
(st/emit! (dw/change-canvas-order {:id (:canvas-id item) (st/emit! (dw/change-frame-order {:id (:frame-id item)
:index index}))) :index index})))
[dprops dnd-ref] (use-sortable [dprops dnd-ref] (use-sortable
{:type "canvas-item" {:type "frame-item"
:data {:canvas-id (:id canvas) :data {:frame-id (:id item)
:page-id (:page canvas) :page-id (:page item)
:index index} :index index}
:on-hover on-hover :on-hover on-hover
:on-drop on-drop})] :on-drop on-drop})]
@ -219,84 +219,68 @@
:on-click select-shape :on-click select-shape
:on-double-click #(dom/stop-propagation %)} :on-double-click #(dom/stop-propagation %)}
[:div.element-actions [:div.element-actions
[:div.toggle-element {:class (when-not (:hidden canvas) "selected") [:div.toggle-element {:class (when-not (:hidden item) "selected")
:on-click toggle-visibility} :on-click toggle-visibility}
i/eye] i/eye]
#_[:div.block-element {:class (when (:blocked canvas) "selected") #_[:div.block-element {:class (when (:blocked item) "selected")
:on-click toggle-blocking} :on-click toggle-blocking}
i/lock]] i/lock]]
[:div.element-icon i/folder] [:div.element-icon i/folder]
[:& layer-name {:shape canvas}] [:& layer-name {:shape item}]
[:span.toggle-content [:span.toggle-content
{:on-click toggle-collapse {:on-click toggle-collapse
:class (when-not collapsed? "inverse")} :class (when-not collapsed? "inverse")}
i/arrow-slide]] i/arrow-slide]]
(when-not collapsed? (when-not collapsed?
[:ul [:ul
(for [[index shape] shapes] (for [[index id] (d/enumerate (:shapes item))]
[:& layer-item {:shape shape (let [item (get objects id)]
:selected selected (if (= (:type item) :frame)
:index index [:& layer-frame-item
:key (:id shape)}])])])) {:item item
:key (:id item)
:objects objects
:index index}]
[:& layer-item
{:item item
:index index
:key (:id item)}])))])]))
;; --- Layers List (mf/defc layers-tree
{:wrap [mf/wrap-memo]}
(mf/defc layers-list [props]
{:wrap [#(mf/wrap-memo % =)]} (let [selected (mf/deref refs/selected-shapes)
[{:keys [shapes selected] :as props}] data (mf/deref refs/workspace-data)
[:ul.element-list objects (:objects data)
(for [[index shape] shapes] root (get objects uuid/zero)]
[:& layer-item {:shape shape [:ul.element-list
:selected selected (for [[index id] (d/enumerate (:shapes root))]
:index index (let [item (get objects id)]
:key (:id shape)}])]) (if (= (:type item) :frame)
[:& layer-frame-item
(mf/defc canvas-list {:item item
{:wrap [#(mf/wrap-memo % =)]} :key (:id item)
[{:keys [shapes canvas selected] :as props}] :objects objects
[:ul.element-list :index index}]
(for [[index item] canvas] [:& layer-item
[:& canvas-item {:canvas item {:item item
:shapes shapes :index index
:selected selected :key (:id item)}])))]))
:index index
:key (:id item)}])])
;; --- Layers Toolbox ;; --- Layers Toolbox
;; NOTE: we need to consider using something like react window for
;; only render visible items instead of all.
(mf/defc layers-toolbox (mf/defc layers-toolbox
{:wrap [mf/wrap-memo]} {:wrap [mf/wrap-memo]}
[{:keys [page] :as props}] [{:keys [page] :as props}]
(let [locale (i18n/use-locale) (let [locale (i18n/use-locale)
on-click #(st/emit! (dw/toggle-layout-flag :layers)) on-click #(st/emit! (dw/toggle-layout-flag :layers))]
selected (mf/deref refs/selected-shapes)
data (mf/deref refs/workspace-data)
shapes-map (:shapes-by-id data)
strip #(select-keys % [:id :canvas :name :type :hidden :blocked])
canvas (->> (:canvas data)
(map #(get shapes-map %))
(map strip)
(d/enumerate))
shapes (->> (:shapes data)
(map #(get shapes-map %))
(map strip))
all-shapes (d/enumerate shapes)
unc-shapes (->> shapes
(filter #(nil? (:canvas %)))
(d/enumerate))]
[:div#layers.tool-window [:div#layers.tool-window
[:div.tool-window-bar [:div.tool-window-bar
[:div.tool-window-icon i/layers] [:div.tool-window-icon i/layers]
[:span (t locale "workspace.sidebar.layers")] [:span (t locale "workspace.sidebar.layers")]
#_[:div.tool-window-close {:on-click on-click} i/close]] #_[:div.tool-window-close {:on-click on-click} i/close]]
[:div.tool-window-content [:div.tool-window-content
[:& canvas-list {:canvas canvas [:& layers-tree]]]))
:shapes all-shapes
:selected selected}]
[:& layers-list {:shapes unc-shapes
:selected selected}]]]))

View file

@ -13,7 +13,7 @@
[uxbox.main.data.workspace :as udw] [uxbox.main.data.workspace :as udw]
[uxbox.main.store :as st] [uxbox.main.store :as st]
[uxbox.main.refs :as refs] [uxbox.main.refs :as refs]
[uxbox.main.ui.workspace.sidebar.options.canvas :as canvas] [uxbox.main.ui.workspace.sidebar.options.frame :as frame]
[uxbox.main.ui.workspace.sidebar.options.rect :as rect] [uxbox.main.ui.workspace.sidebar.options.rect :as rect]
[uxbox.main.ui.workspace.sidebar.options.icon :as icon] [uxbox.main.ui.workspace.sidebar.options.icon :as icon]
[uxbox.main.ui.workspace.sidebar.options.circle :as circle] [uxbox.main.ui.workspace.sidebar.options.circle :as circle]
@ -29,7 +29,7 @@
[{:keys [shape] :as props}] [{:keys [shape] :as props}]
[:div [:div
(case (:type shape) (case (:type shape)
:canvas [:& canvas/options {:shape shape}] :frame [:& frame/options {:shape shape}]
:text [:& text/options {:shape shape}] :text [:& text/options {:shape shape}]
:rect [:& rect/options {:shape shape}] :rect [:& rect/options {:shape shape}]
:icon [:& icon/options {:shape shape}] :icon [:& icon/options {:shape shape}]
@ -43,7 +43,7 @@
[{:keys [shape-id] :as props}] [{:keys [shape-id] :as props}]
(let [shape-iref (mf/use-memo (let [shape-iref (mf/use-memo
{:deps (mf/deps shape-id) {:deps (mf/deps shape-id)
:fn #(-> (l/in [:workspace-data :shapes-by-id shape-id]) :fn #(-> (l/in [:workspace-data :objects shape-id])
(l/derive st/state))}) (l/derive st/state))})
shape (mf/deref shape-iref)] shape (mf/deref shape-iref)]
[:& shape-options {:shape shape}])) [:& shape-options {:shape shape}]))

View file

@ -8,7 +8,7 @@
;; Copyright (c) 2015-2020 Andrey Antukh <niwi@niwi.nz> ;; Copyright (c) 2015-2020 Andrey Antukh <niwi@niwi.nz>
;; Copyright (c) 2015-2020 Juan de la Cruz <delacruzgarciajuan@gmail.com> ;; Copyright (c) 2015-2020 Juan de la Cruz <delacruzgarciajuan@gmail.com>
(ns uxbox.main.ui.workspace.sidebar.options.canvas (ns uxbox.main.ui.workspace.sidebar.options.frame
(:require (:require
[rumext.alpha :as mf] [rumext.alpha :as mf]
[uxbox.common.data :as d] [uxbox.common.data :as d]
@ -43,8 +43,8 @@
delta (if (= attr :x) delta (if (= attr :x)
(gpt/point (math/neg (- pval cval)) 0) (gpt/point (math/neg (- pval cval)) 0)
(gpt/point 0 (math/neg (- pval cval))))] (gpt/point 0 (math/neg (- pval cval))))]
(st/emit! (udw/apply-canvas-displacement (:id shape) delta) (st/emit! (udw/apply-frame-displacement (:id shape) delta)
(udw/materialize-canvas-displacement (:id shape))))) (udw/materialize-frame-displacement (:id shape)))))
on-width-change #(on-size-change % :width) on-width-change #(on-size-change % :width)
on-height-change #(on-size-change % :height) on-height-change #(on-size-change % :height)

View file

@ -22,13 +22,13 @@
[uxbox.main.ui.workspace.grid :refer [grid]] [uxbox.main.ui.workspace.grid :refer [grid]]
[uxbox.main.ui.workspace.ruler :refer [ruler]] [uxbox.main.ui.workspace.ruler :refer [ruler]]
[uxbox.main.ui.workspace.drawarea :refer [start-drawing]] [uxbox.main.ui.workspace.drawarea :refer [start-drawing]]
[uxbox.main.ui.shapes :refer [shape-wrapper frame-wrapper]]
[uxbox.main.ui.shapes :refer [shape-wrapper canvas-wrapper]]
[uxbox.main.ui.workspace.drawarea :refer [draw-area]] [uxbox.main.ui.workspace.drawarea :refer [draw-area]]
[uxbox.main.ui.workspace.selection :refer [selection-handlers]] [uxbox.main.ui.workspace.selection :refer [selection-handlers]]
[uxbox.util.data :refer [parse-int]] [uxbox.util.data :refer [parse-int]]
[uxbox.util.perf :as perf]
[uxbox.util.components :refer [use-rxsub]] [uxbox.util.components :refer [use-rxsub]]
[uxbox.util.uuid :as uuid]
[uxbox.util.dom :as dom] [uxbox.util.dom :as dom]
[uxbox.util.geom.point :as gpt]) [uxbox.util.geom.point :as gpt])
(:import goog.events.EventType)) (:import goog.events.EventType))
@ -142,22 +142,23 @@
(declare remote-user-cursors) (declare remote-user-cursors)
(mf/defc canvas-and-shapes (mf/defc frame-and-shapes
{:wrap [mf/wrap-memo]} {:wrap [mf/wrap-memo]}
[props] [props]
(let [data (mf/deref refs/workspace-data) (let [data (mf/deref refs/workspace-data)
shapes-map (:shapes-by-id data) objects (:objects data)
shapes (->> (map #(get shapes-map %) (:shapes data [])) root (get objects uuid/zero)
(group-by :canvas)) shapes (->> (:shapes root)
canvas (map #(get shapes-map %) (:canvas data []))] (map #(get objects %)))]
[:g.shapes [:g.shapes
(for [item canvas] (for [item shapes]
[:& canvas-wrapper {:shape item (if (= (:type item) :frame)
:key (:id item) [:& frame-wrapper {:shape item
:childs (reverse (get shapes (:id item)))}]) :key (:id item)
(for [item (reverse (get shapes nil))] :objects objects}]
[:& shape-wrapper {:shape item [:& shape-wrapper {:shape item
:key (:id item)}])])) :key (:id item)}]))]))
(mf/defc viewport (mf/defc viewport
[{:keys [page] :as props}] [{:keys [page] :as props}]
@ -169,139 +170,157 @@
selected] selected]
:as local} (mf/deref refs/workspace-local) :as local} (mf/deref refs/workspace-local)
viewport-ref (mf/use-ref nil) viewport-ref (mf/use-ref nil)
zoom (or zoom 1)] zoom (or zoom 1)
(letfn [(on-mouse-down [event]
(dom/stop-propagation event)
(let [ctrl? (kbd/ctrl? event)
shift? (kbd/shift? event)
opts {:shift? shift?
:ctrl? ctrl?}]
(st/emit! (ms/->MouseEvent :down ctrl? shift?)))
(when (not edition)
(if drawing-tool
(st/emit! (start-drawing drawing-tool))
(st/emit! handle-selrect))))
(on-context-menu [event] on-mouse-down
(dom/prevent-default event) (fn [event]
(dom/stop-propagation event) (dom/stop-propagation event)
(let [ctrl? (kbd/ctrl? event) (let [ctrl? (kbd/ctrl? event)
shift? (kbd/shift? event) shift? (kbd/shift? event)
opts {:shift? shift? opts {:shift? shift?
:ctrl? ctrl?}] :ctrl? ctrl?}]
(st/emit! (ms/->MouseEvent :context-menu ctrl? shift?)))) (st/emit! (ms/->MouseEvent :down ctrl? shift?)))
(when (not edition)
(if drawing-tool
(st/emit! (start-drawing drawing-tool))
(st/emit! handle-selrect))))
(on-mouse-up [event] on-context-menu
(dom/stop-propagation event) (fn [event]
(let [ctrl? (kbd/ctrl? event) (dom/prevent-default event)
shift? (kbd/shift? event) (dom/stop-propagation event)
opts {:shift? shift? (let [ctrl? (kbd/ctrl? event)
:ctrl? ctrl?}] shift? (kbd/shift? event)
(st/emit! (ms/->MouseEvent :up ctrl? shift?)))) opts {:shift? shift?
:ctrl? ctrl?}]
(st/emit! (ms/->MouseEvent :context-menu ctrl? shift?))))
(on-click [event] on-mouse-up
(dom/stop-propagation event) (fn [event]
(let [ctrl? (kbd/ctrl? event) (dom/stop-propagation event)
shift? (kbd/shift? event) (let [ctrl? (kbd/ctrl? event)
opts {:shift? shift? shift? (kbd/shift? event)
:ctrl? ctrl?}] opts {:shift? shift?
(st/emit! (ms/->MouseEvent :click ctrl? shift?)))) :ctrl? ctrl?}]
(st/emit! (ms/->MouseEvent :up ctrl? shift?))))
(on-double-click [event] on-click
(dom/stop-propagation event) (fn [event]
(let [ctrl? (kbd/ctrl? event) (dom/stop-propagation event)
shift? (kbd/shift? event) (let [ctrl? (kbd/ctrl? event)
opts {:shift? shift? shift? (kbd/shift? event)
:ctrl? ctrl?}] opts {:shift? shift?
(st/emit! (ms/->MouseEvent :double-click ctrl? shift?)))) :ctrl? ctrl?}]
(st/emit! (ms/->MouseEvent :click ctrl? shift?))))
(translate-point-to-viewport [pt] on-double-click
(let [viewport (mf/ref-node viewport-ref) (fn [event]
brect (.getBoundingClientRect viewport) (dom/stop-propagation event)
brect (gpt/point (parse-int (.-left brect)) (let [ctrl? (kbd/ctrl? event)
(parse-int (.-top brect)))] shift? (kbd/shift? event)
(gpt/subtract pt brect))) opts {:shift? shift?
:ctrl? ctrl?}]
(st/emit! (ms/->MouseEvent :double-click ctrl? shift?))))
(on-key-down [event] on-key-down
(let [bevent (.getBrowserEvent event) (fn [event]
key (.-keyCode event) (let [bevent (.getBrowserEvent event)
ctrl? (kbd/ctrl? event) key (.-keyCode event)
shift? (kbd/shift? event) ctrl? (kbd/ctrl? event)
opts {:key key shift? (kbd/shift? event)
:shift? shift? opts {:key key
:ctrl? ctrl?}] :shift? shift?
(when-not (.-repeat bevent) :ctrl? ctrl?}]
(st/emit! (ms/->KeyboardEvent :down key ctrl? shift?)) (when-not (.-repeat bevent)
(when (kbd/space? event) (st/emit! (ms/->KeyboardEvent :down key ctrl? shift?))
(st/emit! handle-viewport-positioning) (when (kbd/space? event)
#_(st/emit! (dw/start-viewport-positioning)))))) (st/emit! handle-viewport-positioning)
#_(st/emit! (dw/start-viewport-positioning))))))
(on-key-up [event] on-key-up
(let [key (.-keyCode event) (fn [event]
ctrl? (kbd/ctrl? event) (let [key (.-keyCode event)
shift? (kbd/shift? event) ctrl? (kbd/ctrl? event)
opts {:key key shift? (kbd/shift? event)
:shift? shift? opts {:key key
:ctrl? ctrl?}] :shift? shift?
(when (kbd/space? event) :ctrl? ctrl?}]
(st/emit! ::finish-positioning #_(dw/stop-viewport-positioning))) (when (kbd/space? event)
(st/emit! (ms/->KeyboardEvent :up key ctrl? shift?)))) (st/emit! ::finish-positioning #_(dw/stop-viewport-positioning)))
(st/emit! (ms/->KeyboardEvent :up key ctrl? shift?))))
(on-mouse-move [event] ;; translate-point-to-viewport
(let [pt (gpt/point (.-clientX event) ;; (fn [pt]
(.-clientY event)) ;; (let [viewport (mf/ref-node viewport-ref)
pt (translate-point-to-viewport pt)] ;; brect (.getBoundingClientRect viewport)
(st/emit! (ms/->PointerEvent :viewport pt ;; brect (gpt/point (parse-int (.-left brect))
(kbd/ctrl? event) ;; (parse-int (.-top brect)))]
(kbd/shift? event))))) ;; (gpt/subtract pt brect)))
(on-mount [] on-mouse-move
(let [key1 (events/listen js/document EventType.KEYDOWN on-key-down) (fn [event]
key2 (events/listen js/document EventType.KEYUP on-key-up)] ;; NOTE: offsetX and offsetY are marked as "experimental" on
(fn [] ;; MDN site but seems like they are supported on all
(events/unlistenByKey key1) ;; browsers so we can avoid translation opetation just using
(events/unlistenByKey key2))))] ;; this attributes.
(mf/use-effect on-mount) (let [;; pt (gpt/point (.-clientX event)
[:* ;; (.-clientY event))
[:& coordinates {:zoom zoom}] ;; pt (translate-point-to-viewport pt)
[:svg.viewport {:width (* c/viewport-width zoom) pt (gpt/point (.-offsetX (.-nativeEvent event))
:height (* c/viewport-height zoom) (.-offsetY (.-nativeEvent event)))]
:ref viewport-ref (st/emit! (ms/->PointerEvent :viewport pt
:class (when drawing-tool "drawing") (kbd/ctrl? event)
:on-context-menu on-context-menu (kbd/shift? event)))))
:on-click on-click
:on-double-click on-double-click
:on-mouse-move on-mouse-move
:on-mouse-down on-mouse-down
:on-mouse-up on-mouse-up}
[:g.zoom {:transform (str "scale(" zoom ", " zoom ")")}
[:*
[:& canvas-and-shapes]
(when (seq selected) on-mount
[:& selection-handlers {:selected selected (fn []
:zoom zoom (let [key1 (events/listen js/document EventType.KEYDOWN on-key-down)
:edition edition}]) key2 (events/listen js/document EventType.KEYUP on-key-up)]
(fn []
(events/unlistenByKey key1)
(events/unlistenByKey key2))))]
(when-let [drawing-shape (:drawing local)] (mf/use-effect on-mount)
[:& draw-area {:shape drawing-shape [:*
:zoom zoom [:& coordinates {:zoom zoom}]
:modifiers (:modifiers local)}])] [:svg.viewport {:width (* c/viewport-width zoom)
:height (* c/viewport-height zoom)
:ref viewport-ref
:class (when drawing-tool "drawing")
:on-context-menu on-context-menu
:on-click on-click
:on-double-click on-double-click
:on-mouse-move on-mouse-move
:on-mouse-down on-mouse-down
:on-mouse-up on-mouse-up}
[:g.zoom {:transform (str "scale(" zoom ", " zoom ")")}
;; [:> js/React.Profiler
;; {:id "foobar"
;; :on-render (perf/react-on-profile)}
;; [:& frame-and-shapes]]
[:& frame-and-shapes]
(if (contains? flags :grid) (when (seq selected)
[:& grid])] [:& selection-handlers {:selected selected
:zoom zoom
:edition edition}])
(when tooltip (when-let [drawing-shape (:drawing local)]
[:& cursor-tooltip {:zoom zoom :tooltip tooltip}]) [:& draw-area {:shape drawing-shape
:zoom zoom
:modifiers (:modifiers local)}])
(when (contains? flags :ruler) (if (contains? flags :grid)
[:& ruler {:zoom zoom :ruler (:ruler local)}]) [:& grid])]
(when tooltip
[:& cursor-tooltip {:zoom zoom :tooltip tooltip}])
;; -- METER CURSOR MULTIUSUARIO (when (contains? flags :ruler)
[:& remote-user-cursors {:page page}] [:& ruler {:zoom zoom :ruler (:ruler local)}])
[:& selrect {:data (:selrect local)}]]]))) [:& remote-user-cursors {:page page}]
[:& selrect {:data (:selrect local)}]]]))
(mf/defc remote-user-cursor (mf/defc remote-user-cursor

View file

@ -11,3 +11,7 @@
"Convert an es6 iterable into cljs Seq." "Convert an es6 iterable into cljs Seq."
[v] [v]
(seq (js/Array.from v))) (seq (js/Array.from v)))
(defn obj-assign!
[obj1 obj2]
(js/Object.assign obj1 obj2))

View file

@ -6,7 +6,8 @@
(ns uxbox.util.perf (ns uxbox.util.perf
"Performance and debugging tools." "Performance and debugging tools."
#?(:cljs (:require-macros [uxbox.util.perf]))) #?(:cljs (:require-macros [uxbox.util.perf]))
#?(:cljs (:require [uxbox.util.math :as math])))
#?(:clj #?(:clj
(defmacro with-measure (defmacro with-measure
@ -17,3 +18,25 @@
time# (.toFixed (- end# start#) 2)] time# (.toFixed (- end# start#) 2)]
(println (str "[perf|" ~name "] => " time#)) (println (str "[perf|" ~name "] => " time#))
res#))) res#)))
;; id, // the "id" prop of the Profiler tree that has just committed
;; phase, // either "mount" (if the tree just mounted) or "update" (if it re-rendered)
;; actualDuration, // time spent rendering the committed update
;; baseDuration, // estimated time to render the entire subtree without memoization
;; startTime, // when React began rendering this update
;; commitTime, // when React committed this update
;; interactions // the Set of interactions belonging to this update
#?(:cljs
(defn react-on-profile
[]
(let [sum (volatile! 0)
ctr (volatile! 0)]
(fn [id phase adur, bdur, st, ct, itx]
(vswap! sum (fn [prev] (+ prev adur)))
(vswap! ctr inc)
(js/console.log (str "[profile:" id ":" phase "]")
""
(str "time=" (math/precision adur 4))
(str "avg=" (math/precision (/ @sum @ctr) 4)))))))

View file

@ -13,7 +13,7 @@
[uxbox.util.data :refer [seek]] [uxbox.util.data :refer [seek]]
[uxbox.view.data.viewer :as dv] [uxbox.view.data.viewer :as dv]
[uxbox.view.store :as st] [uxbox.view.store :as st]
[uxbox.view.ui.viewer.canvas :refer [canvas]] [uxbox.view.ui.viewer.frame :refer [frame]]
[uxbox.view.ui.viewer.nav :refer [nav]] [uxbox.view.ui.viewer.nav :refer [nav]]
[uxbox.view.ui.viewer.sitemap :refer [sitemap]] [uxbox.view.ui.viewer.sitemap :refer [sitemap]]
[lentes.core :as l])) [lentes.core :as l]))
@ -45,4 +45,4 @@
:pages pages :pages pages
:selected id}]) :selected id}])
[:& nav {:flags flags}] [:& nav {:flags flags}]
[:& canvas {:page (seek #(= id (:id %)) pages)}]]))) [:& frame {:page (seek #(= id (:id %)) pages)}]])))

View file

@ -5,7 +5,7 @@
;; Copyright (c) 2016-2017 Andrey Antukh <niwi@niwi.nz> ;; Copyright (c) 2016-2017 Andrey Antukh <niwi@niwi.nz>
;; Copyright (c) 2016-2017 Juan de la Cruz <delacruzgarciajuan@gmail.com> ;; Copyright (c) 2016-2017 Juan de la Cruz <delacruzgarciajuan@gmail.com>
(ns uxbox.view.ui.viewer.canvas (ns uxbox.view.ui.viewer.frame
(:require (:require
[rumext.alpha :as mf] [rumext.alpha :as mf]
[uxbox.view.ui.viewer.shapes :as shapes])) [uxbox.view.ui.viewer.shapes :as shapes]))
@ -25,12 +25,12 @@
(declare shape) (declare shape)
(mf/defc canvas (mf/defc frame
{:wrap [mf/wrap-memo]} {:wrap [mf/wrap-memo]}
[{:keys [page] :as props}] [{:keys [page] :as props}]
#_(let [{:keys [metadata id]} page #_(let [{:keys [metadata id]} page
{:keys [width height]} metadata] {:keys [width height]} metadata]
[:div.view-canvas [:div.view-frame
[:svg.page-layout {:width width [:svg.page-layout {:width width
:height height} :height height}
[:& background metadata] [:& background metadata]