Merge pull request #1508 from penpot/feat/guides

Guides
This commit is contained in:
Andrey Antukh 2022-01-25 14:58:36 +01:00 committed by GitHub
commit a10dcbd918
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
33 changed files with 1904 additions and 414 deletions

View file

@ -2,13 +2,19 @@
## :rocket: Next
### :sparkles: New features
- Guides [Taiga #290](https://tree.taiga.io/project/penpot/us/290?milestone=307334)
- Improve file menu by adding semantically groups [Github #1203](https://github.com/penpot/penpot/issues/1203).
- Create e2e tests for drawing basic fors [Taiga #2608](https://tree.taiga.io/project/penpot/task/2608).
- Create firsts e2e test [Taiga #2608](https://tree.taiga.io/project/penpot/task/2608).
## 1.11.0-beta
### :boom: Breaking changes
### :sparkles: New features
- Create e2e tests for drawing basic fors [Taiga #2608](https://tree.taiga.io/project/penpot/task/2608).
- Improve file menu by adding semantically groups [Github #1203](https://github.com/penpot/penpot/issues/1203).
- Create firsts e2e test [Taiga #2608](https://tree.taiga.io/project/penpot/task/2608).
- Add an option to hide artboards names on the viewport [Taiga #2034](https://tree.taiga.io/project/penpot/issue/2034).
- Limit pasted object position to container boundaries [Taiga #2449](https://tree.taiga.io/project/penpot/us/2449).
- Add new options for zoom widget in workspace and viewer mode [Taiga #896](https://tree.taiga.io/project/penpot/us/896).

View file

@ -15,4 +15,5 @@
(def info "#59B9E2")
(def test "#fabada")
(def white "#FFFFFF")
(def primary "#31EFB8")

View file

@ -568,4 +568,78 @@
(dissoc :current-component-id)
(update :parent-stack pop))))
(defn delete-object
[file id]
(let [page-id (:current-page-id file)]
(commit-change
file
{:type :del-obj
:page-id page-id
:id id})))
(defn update-object
[file old-obj new-obj]
(let [page-id (:current-page-id file)
new-obj (setup-selrect new-obj)
attrs (d/concat-set (keys old-obj) (keys new-obj))
generate-operation
(fn [changes attr]
(let [old-val (get old-obj attr)
new-val (get new-obj attr)]
(if (= old-val new-val)
changes
(conj changes {:type :set :attr attr :val new-val}))))]
(-> file
(commit-change
{:type :mod-obj
:operations (reduce generate-operation [] attrs)
:page-id page-id
:id (:id old-obj)}))))
(defn get-current-page
[file]
(let [page-id (:current-page-id file)]
(-> file (get-in [:data :pages-index page-id]))))
(defn add-guide
[file guide]
(let [guide (cond-> guide
(nil? (:id guide))
(assoc :id (uuid/next)))
page-id (:current-page-id file)
old-guides (or (get-in file [:data :pages-index page-id :options :guides]) {})
new-guides (assoc old-guides (:id guide) guide)]
(-> file
(commit-change
{:type :set-option
:page-id page-id
:option :guides
:value new-guides})
(assoc :last-id (:id guide)))))
(defn delete-guide
[file id]
(let [page-id (:current-page-id file)
old-guides (or (get-in file [:data :pages-index page-id :options :guides]) {})
new-guides (dissoc old-guides id)]
(-> file
(commit-change
{:type :set-option
:page-id page-id
:option :guides
:value new-guides}))))
(defn update-guide
[file guide]
(let [page-id (:current-page-id file)
old-guides (or (get-in file [:data :pages-index page-id :options :guides]) {})
new-guides (assoc old-guides (:id guide) guide)]
(-> file
(commit-change
{:type :set-option
:page-id page-id
:option :guides
:value new-guides}))))

View file

@ -13,12 +13,21 @@
;; Auxiliary functions to help create a set of changes (undo + redo)
(defn empty-changes
[origin page-id]
(let [changes {:redo-changes []
:undo-changes []
:origin origin}]
(with-meta changes
{::page-id page-id})))
([origin page-id]
(let [changes (empty-changes origin)]
(with-meta changes
{::page-id page-id})))
([origin]
{:redo-changes []
:undo-changes []
:origin origin}))
(defn with-page [changes page]
(vary-meta changes assoc
::page page
::page-id (:id page)
::objects (:objects page)))
(defn with-objects [changes objects]
(vary-meta changes assoc ::objects objects))
@ -167,10 +176,25 @@
(reduce add-undo-change-parent $ ids)
(reduce add-undo-change-shape $ ids))))))
(defn move-page
[chdata index prev-index]
(let [page-id (::page-id (meta chdata))]
(-> chdata
(update :redo-changes conj {:type :mov-page :id page-id :index index})
(update :undo-changes conj {:type :mov-page :id page-id :index prev-index}))))
(defn set-page-option
[chdata option-key option-val]
(let [page-id (::page-id (meta chdata))
page (::page (meta chdata))
old-val (get-in page [:options option-key])]
(-> chdata
(update :redo-changes conj {:type :set-option
:page-id page-id
:option option-key
:value option-val})
(update :undo-changes conj {:type :set-option
:page-id page-id
:option option-key
:value old-val}))))

View file

@ -0,0 +1,170 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) UXBOX Labs SL
(ns app.common.pages.diff
"Given a page in its old version and the new will retrieve a map with
the differences that will have an impact in the snap data"
(:require
[app.common.data :as d]
[clojure.set :as set]))
(defn calculate-page-diff
[old-page page check-attrs]
(let [old-objects (get old-page :objects)
old-guides (or (get-in old-page [:options :guides]) [])
new-objects (get page :objects)
new-guides (or (get-in page [:options :guides]) [])
changed-object?
(fn [id]
(let [oldv (get old-objects id)
newv (get new-objects id)]
;; Check first without select-keys because is faster if they are
;; the same reference
(and (not= oldv newv)
(not= (select-keys oldv check-attrs)
(select-keys newv check-attrs)))))
frame?
(fn [id]
(or (= :frame (get-in new-objects [id :type]))
(= :frame (get-in old-objects [id :type]))))
changed-guide?
(fn [id]
(not= (get old-guides id)
(get new-guides id)))
deleted-object?
#(and (contains? old-objects %)
(not (contains? new-objects %)))
deleted-guide?
#(and (contains? old-guides %)
(not (contains? new-guides %)))
new-object?
#(and (not (contains? old-objects %))
(contains? new-objects %))
new-guide?
#(and (not (contains? old-guides %))
(contains? new-guides %))
changed-frame-object?
#(and (contains? new-objects %)
(contains? old-objects %)
(not= (get-in old-objects [% :frame-id])
(get-in new-objects [% :frame-id])))
changed-frame-guide?
#(and (contains? new-guides %)
(contains? old-guides %)
(not= (get-in old-objects [% :frame-id])
(get-in new-objects [% :frame-id])))
changed-attrs-object?
#(and (contains? new-objects %)
(contains? old-objects %)
(= (get-in old-objects [% :frame-id])
(get-in new-objects [% :frame-id])))
changed-attrs-guide?
#(and (contains? new-guides %)
(contains? old-guides %)
(= (get-in old-objects [% :frame-id])
(get-in new-objects [% :frame-id])))
changed-object-ids
(into #{}
(filter changed-object?)
(set/union (set (keys old-objects))
(set (keys new-objects))))
changed-guides-ids
(into #{}
(filter changed-guide?)
(set/union (set (keys old-guides))
(set (keys new-guides))))
get-diff-object (fn [id] [(get old-objects id) (get new-objects id)])
get-diff-guide (fn [id] [(get old-guides id) (get new-guides id)])
;; Shapes with different frame owner
change-frame-shapes
(->> changed-object-ids
(into [] (comp (filter changed-frame-object?)
(map get-diff-object))))
;; Guides that changed frames
change-frame-guides
(->> changed-guides-ids
(into [] (comp (filter changed-frame-guide?)
(map get-diff-guide))))
removed-frames
(->> changed-object-ids
(into [] (comp (filter frame?)
(filter deleted-object?)
(map (d/getf old-objects)))))
removed-shapes
(->> changed-object-ids
(into [] (comp (remove frame?)
(filter deleted-object?)
(map (d/getf old-objects)))))
removed-guides
(->> changed-guides-ids
(into [] (comp (filter deleted-guide?)
(map (d/getf old-guides)))))
updated-frames
(->> changed-object-ids
(into [] (comp (filter frame?)
(filter changed-attrs-object?)
(map get-diff-object))))
updated-shapes
(->> changed-object-ids
(into [] (comp (remove frame?)
(filter changed-attrs-object?)
(map get-diff-object))))
updated-guides
(->> changed-guides-ids
(into [] (comp (filter changed-attrs-guide?)
(map get-diff-guide))))
new-frames
(->> changed-object-ids
(into [] (comp (filter frame?)
(filter new-object?)
(map (d/getf new-objects)))))
new-shapes
(->> changed-object-ids
(into [] (comp (remove frame?)
(filter new-object?)
(map (d/getf new-objects)))))
new-guides
(->> changed-guides-ids
(into [] (comp (filter new-guide?)
(map (d/getf new-guides)))))]
{:change-frame-shapes change-frame-shapes
:change-frame-guides change-frame-guides
:removed-frames removed-frames
:removed-shapes removed-shapes
:removed-guides removed-guides
:updated-frames updated-frames
:updated-shapes updated-shapes
:updated-guides updated-guides
:new-frames new-frames
:new-shapes new-shapes
:new-guides new-guides}))

View file

@ -61,12 +61,29 @@
(s/def ::flows
(s/coll-of ::flow :kind vector?))
;; --- Guides
(s/def :guides/id ::us/uuid)
(s/def :guides/axis #{:x :y})
(s/def :guides/position ::us/safe-number)
(s/def :guides/frame-id (s/nilable ::us/uuid))
(s/def ::guide
(s/keys :req-un [:guides/id
:guides/axis
:guides/position]
:opt-un [:guides/frame-id]))
(s/def ::guides
(s/map-of uuid? ::guide))
;; --- Options
(s/def ::options
(s/keys :opt-un [::background
::saved-grids
::flows]))
::flows
::guides]))
;; --- Helpers for flow

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 16 16"><path fill="#fff" d="m 9.8669998,8.8590002 h -3.734 V 10.732 L -0.00100019,8.0000002 6.1329998,5.2680001 v 1.873 h 3.734 v -1.873 L 16.001,8.0000002 9.8669998,10.732"/><path fill="#000" d="M 10.4,8.3900002 H 5.5999998 V 9.9509999 L 0.79999981,8.0000002 5.5999998,6.0490001 v 1.56 H 10.4 v -1.56 l 4.8,1.9510001 -4.8,1.9509997 V 8.3910002 Z"/></svg>

After

Width:  |  Height:  |  Size: 421 B

View file

@ -30,6 +30,7 @@
[app.main.data.workspace.drawing :as dwd]
[app.main.data.workspace.fix-bool-contents :as fbc]
[app.main.data.workspace.groups :as dwg]
[app.main.data.workspace.guides :as dwgu]
[app.main.data.workspace.interactions :as dwi]
[app.main.data.workspace.layers :as dwly]
[app.main.data.workspace.libraries :as dwl]
@ -83,7 +84,8 @@
:snap-grid
:scale-text
:dynamic-alignment
:display-artboard-names})
:display-artboard-names
:snap-guides})
(s/def ::layout-flags (s/coll-of ::layout-flag))
@ -95,7 +97,8 @@
:display-grid
:snap-grid
:dynamic-alignment
:display-artboard-names})
:display-artboard-names
:snap-guides})
(def layout-presets
{:assets
@ -2033,3 +2036,8 @@
;; Shapes to path
(d/export dwps/convert-selected-to-path)
;; Guides
(d/export dwgu/update-guides)
(d/export dwgu/remove-guide)

View file

@ -0,0 +1,88 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) UXBOX Labs SL
(ns app.main.data.workspace.guides
(:require
[app.common.geom.point :as gpt]
[app.common.geom.shapes :as gsh]
[app.common.pages.changes-builder :as pcb]
[app.common.spec :as us]
[app.common.types.page-options :as tpo]
[app.main.data.workspace.changes :as dwc]
[app.main.data.workspace.state-helpers :as wsh]
[beicon.core :as rx]
[cljs.spec.alpha :as s]
[potok.core :as ptk]))
(defn make-update-guide [guide]
(fn [other]
(cond-> other
(= (:id other) (:id guide))
(merge guide))))
(defn update-guides [guide]
(us/verify ::tpo/guide guide)
(ptk/reify ::update-guides
ptk/WatchEvent
(watch [it state _]
(let [page (wsh/lookup-page state)
guides (get-in page [:options :guides] {})
new-guides (assoc guides (:id guide) guide)
changes
(-> (pcb/empty-changes it)
(pcb/with-page page)
(pcb/set-page-option :guides new-guides))]
(rx/of (dwc/commit-changes changes))))))
(defn remove-guide [guide]
(us/verify ::tpo/guide guide)
(ptk/reify ::remove-guide
ptk/WatchEvent
(watch [it state _]
(let [page (wsh/lookup-page state)
guides (get-in page [:options :guides] {})
new-guides (dissoc guides (:id guide))
changes
(-> (pcb/empty-changes it)
(pcb/with-page page)
(pcb/set-page-option :guides new-guides))]
(rx/of (dwc/commit-changes changes))))))
(defn move-frame-guides
"Move guides that are inside a frame when that frame is moved"
[ids]
(us/verify (s/coll-of uuid?) ids)
(ptk/reify ::move-frame-guides
ptk/WatchEvent
(watch [_ state _]
(let [objects (wsh/lookup-page-objects state)
is-frame? (fn [id] (= :frame (get-in objects [id :type])))
frame-ids? (into #{} (filter is-frame?) ids)
object-modifiers (get state :workspace-modifiers)
build-move-event
(fn [guide]
(let [frame (get objects (:frame-id guide))
frame' (-> (merge frame (get object-modifiers (:frame-id guide)))
(gsh/transform-shape))
moved (gpt/to-vec (gpt/point (:x frame) (:y frame))
(gpt/point (:x frame') (:y frame')))
guide (update guide :position + (get moved (:axis guide)))]
(update-guides guide)))]
(->> (wsh/lookup-page-options state)
:guides
(vals)
(filter (comp frame-ids? :frame-id))
(map build-move-event)
(rx/from))))))

View file

@ -58,6 +58,10 @@
:command (ds/c-mod "shift+'")
:fn #(st/emit! (dw/toggle-layout-flags :snap-grid))}
:toggle-snap-guide {:tooltip (ds/meta-shift "G")
:command (ds/c-mod "shift+G")
:fn #(st/emit! (dw/toggle-layout-flags :snap-guides))}
:toggle-alignment {:tooltip (ds/meta "\\")
:command (ds/c-mod "\\")
:fn #(st/emit! (dw/toggle-layout-flags :dynamic-alignment))}

View file

@ -9,6 +9,12 @@
[app.common.data :as d]
[app.common.pages :as cp]))
(defn lookup-page
([state]
(lookup-page state (:current-page-id state)))
([state page-id]
(get-in state [:workspace-data :pages-index page-id])))
(defn lookup-page-objects
([state]
(lookup-page-objects state (:current-page-id state)))

View file

@ -16,6 +16,7 @@
[app.common.spec :as us]
[app.main.data.workspace.changes :as dch]
[app.main.data.workspace.common :as dwc]
[app.main.data.workspace.guides :as dwg]
[app.main.data.workspace.selection :as dws]
[app.main.data.workspace.state-helpers :as wsh]
[app.main.data.workspace.undo :as dwu]
@ -155,6 +156,8 @@
(update state :workspace-modifiers #(reduce update-shape % shapes)))))))
(defn- apply-modifiers
[ids]
(us/verify (s/coll-of uuid?) ids)
@ -168,6 +171,7 @@
ignore-tree (get-ignore-tree object-modifiers objects ids)]
(rx/of (dwu/start-undo-transaction)
(dwg/move-frame-guides ids-with-children)
(dch/update-shapes
ids-with-children
(fn [shape]

View file

@ -204,7 +204,9 @@
:height "100%"
:background background-color}}
[:& export/export-page {:options (:options data)}]
(when include-metadata?
[:& export/export-page {:options (:options data)}])
[:& ff/fontfaces-style {:shapes root-children}]
(for [item shapes]
(let [frame? (= (:type item) :frame)]

View file

@ -19,20 +19,38 @@
[beicon.core :as rx]
[clojure.set :as set]))
(def ^:const snap-accuracy 5)
(def ^:const snap-accuracy 10)
(def ^:const snap-path-accuracy 10)
(def ^:const snap-distance-accuracy 10)
(defn- remove-from-snap-points
[remove-id?]
[remove-snap?]
(fn [query-result]
(->> query-result
(map (fn [[value data]] [value (remove (comp remove-id? second) data)]))
(map (fn [[value data]] [value (remove remove-snap? data)]))
(filter (fn [[_ data]] (seq data))))))
(defn make-remove-snap
"Creates a filter for the snap data. Used to disable certain layouts"
[layout filter-shapes]
(fn [{:keys [type id]}]
(cond
(= type :layout)
(or (not (contains? layout :display-grid))
(not (contains? layout :snap-grid)))
(= type :guide)
(or (not (contains? layout :rules))
(not (contains? layout :snap-guides)))
:else
(or (contains? filter-shapes id)
(not (contains? layout :dynamic-alignment))))))
(defn- flatten-to-points
[query-result]
(mapcat (fn [[_ data]] (map (fn [[point _]] point) data)) query-result))
(mapcat (fn [[_ data]] (map :pt data)) query-result))
(defn- calculate-distance [query-result point coord]
(->> query-result
@ -57,19 +75,19 @@
;; Otherwise the root frame is the common
:else zero)))
(defn get-snap-points [page-id frame-id filter-shapes point coord]
(defn get-snap-points [page-id frame-id remove-snap? point coord]
(let [value (get point coord)]
(->> (uw/ask! {:cmd :snaps/range-query
:page-id page-id
:frame-id frame-id
:coord coord
:axis coord
:ranges [[(- value 0.5) (+ value 0.5)]]})
(rx/first)
(rx/map (remove-from-snap-points filter-shapes))
(rx/map (remove-from-snap-points remove-snap?))
(rx/map flatten-to-points))))
(defn- search-snap
[page-id frame-id points coord filter-shapes zoom]
[page-id frame-id points coord remove-snap? zoom]
(let [snap-accuracy (/ snap-accuracy zoom)
ranges (->> points
(map coord)
@ -78,10 +96,10 @@
(->> (uw/ask! {:cmd :snaps/range-query
:page-id page-id
:frame-id frame-id
:coord coord
:axis coord
:ranges ranges})
(rx/first)
(rx/map (remove-from-snap-points filter-shapes))
(rx/map (remove-from-snap-points remove-snap?))
(rx/map (get-min-distance-snap points coord)))))
(defn snap->vector [[[from-x to-x] [from-y to-y]]]
@ -91,13 +109,12 @@
(gpt/to-vec from to))))
(defn- closest-snap
[page-id frame-id points filter-shapes zoom]
(let [snap-x (search-snap page-id frame-id points :x filter-shapes zoom)
snap-y (search-snap page-id frame-id points :y filter-shapes zoom)]
[page-id frame-id points remove-snap? zoom]
(let [snap-x (search-snap page-id frame-id points :x remove-snap? zoom)
snap-y (search-snap page-id frame-id points :y remove-snap? zoom)]
(->> (rx/combine-latest snap-x snap-y)
(rx/map snap->vector))))
(defn sr-distance [coord sr1 sr2]
(let [c1 (if (= coord :x) :x1 :y1)
c2 (if (= coord :x) :x2 :y2)
@ -209,12 +226,8 @@
[page-id shapes layout zoom point]
(let [frame-id (snap-frame-id shapes)
filter-shapes (into #{} (map :id shapes))
filter-shapes (fn [id] (if (= id :layout)
(or (not (contains? layout :display-grid))
(not (contains? layout :snap-grid)))
(or (filter-shapes id)
(not (contains? layout :dynamic-alignment)))))]
(->> (closest-snap page-id frame-id [point] filter-shapes zoom)
remove-snap? (make-remove-snap layout filter-shapes)]
(->> (closest-snap page-id frame-id [point] remove-snap? zoom)
(rx/map #(or % (gpt/point 0 0)))
(rx/map #(gpt/add point %)))))
@ -222,11 +235,8 @@
[page-id shapes objects layout zoom movev]
(let [frame-id (snap-frame-id shapes)
filter-shapes (into #{} (map :id shapes))
filter-shapes (fn [id] (if (= id :layout)
(or (not (contains? layout :display-grid))
(not (contains? layout :snap-grid)))
(or (filter-shapes id)
(not (contains? layout :dynamic-alignment)))))
remove-snap? (make-remove-snap layout filter-shapes)
shape (if (> (count shapes) 1)
(->> shapes (map gsh/transform-shape) gsh/selection-rect (gsh/setup {:type :rect}))
(->> shapes (first)))
@ -236,7 +246,7 @@
;; Move the points in the translation vector
(map #(gpt/add % movev)))]
(->> (rx/merge (closest-snap page-id frame-id shapes-points filter-shapes zoom)
(->> (rx/merge (closest-snap page-id frame-id shapes-points remove-snap? zoom)
(when (contains? layout :dynamic-alignment)
(closest-distance-snap page-id shapes objects zoom movev)))
(rx/reduce gpt/min)

View file

@ -38,6 +38,9 @@
(def resize-nwse (cursor-fn :resize-h 135))
(def rotate (cursor-fn :rotate 90))
;;
(def resize-ew-2 (cursor-fn :resize-h-2 0))
(def resize-ns-2 (cursor-fn :resize-h-2 90))
(mf/defc debug-preview
{::mf/wrap-props false}

View file

@ -155,20 +155,30 @@
:name name
:starting-frame starting-frame}])])
(mf/defc export-guides
[{:keys [guides]}]
[:> "penpot:guides" #js {}
(for [{:keys [position frame-id axis]} (vals guides)]
[:> "penpot:guide" #js {:position position
:frame-id frame-id
:axis (d/name axis)}])])
(mf/defc export-page
[{:keys [options]}]
(let [saved-grids (get options :saved-grids)
flows (get options :flows)]
(when (or (seq saved-grids) (seq flows))
(let [parse-grid
(fn [[type params]]
{:type type :params params})
flows (get options :flows)
guides (get options :guides)]
[:> "penpot:page" #js {}
(when (d/not-empty? saved-grids)
(let [parse-grid (fn [[type params]] {:type type :params params})
grids (->> saved-grids (mapv parse-grid))]
[:> "penpot:page" #js {}
(when (seq saved-grids)
[:& export-grid-data {:grids grids}])
(when (seq flows)
[:& export-flows {:flows flows}])]))))
[:& export-grid-data {:grids grids}]))
(when (d/not-empty? flows)
[:& export-flows {:flows flows}])
(when (d/not-empty? guides)
[:& export-guides {:guides guides}])]))
(defn- export-shadow-data [{:keys [shadow]}]
(mf/html

View file

@ -20,7 +20,6 @@
[app.main.ui.workspace.header :refer [header]]
[app.main.ui.workspace.left-toolbar :refer [left-toolbar]]
[app.main.ui.workspace.libraries]
[app.main.ui.workspace.rules :refer [horizontal-rule vertical-rule]]
[app.main.ui.workspace.sidebar :refer [left-sidebar right-sidebar]]
[app.main.ui.workspace.viewport :refer [viewport]]
[app.util.dom :as dom]
@ -31,45 +30,22 @@
;; --- Workspace
(mf/defc workspace-rules
{::mf/wrap-props false
::mf/wrap [mf/memo]}
[props]
(let [zoom (or (obj/get props "zoom") 1)
vbox (obj/get props "vbox")
vport (obj/get props "vport")
colorpalette? (obj/get props "colorpalette?")]
[:*
[:div.empty-rule-square]
[:& horizontal-rule {:zoom zoom
:vbox vbox
:vport vport}]
[:& vertical-rule {:zoom zoom
:vbox vbox
:vport vport}]
[:& coordinates/coordinates {:colorpalette? colorpalette?}]]))
(mf/defc workspace-content
{::mf/wrap-props false}
[props]
(let [selected (mf/deref refs/selected-shapes)
local (mf/deref refs/viewport-data)
{:keys [zoom vbox vport options-mode]} local
{:keys [options-mode]} local
file (obj/get props "file")
layout (obj/get props "layout")]
layout (obj/get props "layout")
colorpalette? (:colorpalette layout)]
[:*
(when (:colorpalette layout)
[:& colorpalette])
(when colorpalette? [:& colorpalette])
[:section.workspace-content
[:section.workspace-viewport
(when (contains? layout :rules)
[:& workspace-rules {:zoom zoom
:vbox vbox
:vport vport
:colorpalette? (contains? layout :colorpalette)}])
[:& coordinates/coordinates {:colorpalette? colorpalette?}]
[:& viewport {:file file
:local local

View file

@ -194,7 +194,13 @@
(fn [_error]
(st/emit! (dm/error (tr "errors.unexpected-error"))))
(st/emitf dm/hide)))))))
on-item-click (fn [item] (fn [event] (do (dom/stop-propagation event) (reset! show-sub-menu? item))))]
on-item-click
(mf/use-callback
(fn [item]
(fn [event]
(dom/stop-propagation event)
(reset! show-sub-menu? item))))]
(mf/use-effect
(mf/deps @editing?)
@ -314,12 +320,12 @@
[:& dropdown {:show (= @show-sub-menu? :preferences)
:on-close #(reset! show-sub-menu? false)}
[:ul.sub-menu.preferences
#_[:li {:on-click #()}
[:span
(if (contains? layout :snap-guide)
(tr "workspace.header.menu.disable-snap-guides")
(tr "workspace.header.menu.enable-snap-guides"))]
[:span.shortcut (sc/get-tooltip :toggle-snap-grid)]]
[:li {:on-click #(st/emit! (dw/toggle-layout-flags :snap-guides))}
[:span
(if (contains? layout :snap-guides)
(tr "workspace.header.menu.disable-snap-guides")
(tr "workspace.header.menu.enable-snap-guides"))]
[:span.shortcut (sc/get-tooltip :toggle-snap-guide)]]
[:li {:on-click #(st/emit! (dw/toggle-layout-flags :snap-grid))}
[:span

View file

@ -1,122 +0,0 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) UXBOX Labs SL
(ns app.main.ui.workspace.rules
(:require
[app.common.colors :as colors]
[app.common.math :as mth]
[app.util.object :as obj]
[rumext.alpha :as mf]))
(defn- calculate-step-size
[zoom]
(cond
(< 0 zoom 0.008) 10000
(< 0.008 zoom 0.015) 5000
(< 0.015 zoom 0.04) 2500
(< 0.04 zoom 0.07) 1000
(< 0.07 zoom 0.2) 500
(< 0.2 zoom 0.5) 250
(< 0.5 zoom 1) 100
(<= 1 zoom 2) 50
(< 2 zoom 4) 25
(< 4 zoom 6) 10
(< 6 zoom 15) 5
(< 15 zoom 25) 2
(< 25 zoom) 1
:else 1))
(defn draw-rule!
[dctx {:keys [zoom size start type]}]
(when start
(let [txfm (- (* (- 0 start) zoom) 20)
step (calculate-step-size zoom)
minv (max (mth/round start) -100000)
minv (* (mth/ceil (/ minv step)) step)
maxv (min (mth/round (+ start (/ size zoom))) 100000)
maxv (* (mth/floor (/ maxv step)) step)
path (js/Path2D.)]
(if (= type :horizontal)
(.translate dctx txfm 0)
(.translate dctx 0 txfm))
(obj/set! dctx "font" "12px worksans")
(obj/set! dctx "fillStyle" colors/gray-30)
(obj/set! dctx "strokeStyle" colors/gray-30)
(obj/set! dctx "textAlign" "center")
(loop [i minv]
(if (<= i maxv)
(let [pos (+ (* i zoom) 0)]
(.save dctx)
(if (= type :horizontal)
(do
;; Write the rule numbers
(.fillText dctx (str i) pos 13)
;; Build the rules lines
(.moveTo path pos 17)
(.lineTo path pos 20))
(do
;; Write the rule numbers
(.translate dctx 12 pos)
(.rotate dctx (/ (* 270 js/Math.PI) 180))
(.fillText dctx (str i) 0 0)
;; Build the rules lines
(.moveTo path 17 pos)
(.lineTo path 20 pos)))
(.restore dctx)
(recur (+ i step)))
;; Put the path in the canvas
(.stroke dctx path))))))
(mf/defc horizontal-rule
[{:keys [zoom vbox vport] :as props}]
(let [canvas (mf/use-ref)
width (- (:width vport) 20)]
(mf/use-layout-effect
(mf/deps zoom width (:x vbox))
(fn []
(let [node (mf/ref-val canvas)
dctx (.getContext ^js node "2d")]
(obj/set! node "width" width)
(draw-rule! dctx {:zoom zoom
:type :horizontal
:size width
:start (+ (:x vbox) (:left-offset vbox))}))))
[:canvas.horizontal-rule
{:ref canvas
:width width
:height 20}]))
(mf/defc vertical-rule
[{:keys [zoom vbox vport] :as props}]
(let [canvas (mf/use-ref)
height (- (:height vport) 20)]
(mf/use-layout-effect
(mf/deps zoom height (:y vbox))
(fn []
(let [node (mf/ref-val canvas)
dctx (.getContext ^js node "2d")]
(obj/set! node "height" height)
(draw-rule! dctx {:zoom zoom
:type :vertical
:size height
:count 100
:start (:y vbox)}))))
[:canvas.vertical-rule
{:ref canvas
:width 20
:height height}]))

View file

@ -21,11 +21,13 @@
[app.main.ui.workspace.viewport.drawarea :as drawarea]
[app.main.ui.workspace.viewport.frame-grid :as frame-grid]
[app.main.ui.workspace.viewport.gradients :as gradients]
[app.main.ui.workspace.viewport.guides :as guides]
[app.main.ui.workspace.viewport.hooks :as hooks]
[app.main.ui.workspace.viewport.interactions :as interactions]
[app.main.ui.workspace.viewport.outline :as outline]
[app.main.ui.workspace.viewport.pixel-overlay :as pixel-overlay]
[app.main.ui.workspace.viewport.presence :as presence]
[app.main.ui.workspace.viewport.rules :as rules]
[app.main.ui.workspace.viewport.selection :as selection]
[app.main.ui.workspace.viewport.snap-distances :as snap-distances]
[app.main.ui.workspace.viewport.snap-points :as snap-points]
@ -89,6 +91,13 @@
;; STREAMS
move-stream (mf/use-memo #(rx/subject))
frame-parent (mf/use-memo
(mf/deps @hover-ids base-objects)
(fn []
(let [parent (get base-objects (last @hover-ids))]
(when (= :frame (:type parent))
parent))))
zoom (d/check-num zoom 1)
drawing-tool (:tool drawing)
drawing-obj (:object drawing)
@ -145,7 +154,10 @@
(or drawing-obj transform))
show-selrect? (and selrect (empty? drawing))
show-measures? (and (not transform) (not node-editing?) show-distances?)
show-artboard-names? (contains? layout :display-artboard-names)]
show-artboard-names? (contains? layout :display-artboard-names)
show-rules? (contains? layout :rules)
disabled-guides? (or drawing-tool transform)]
(hooks/setup-dom-events viewport-ref zoom disable-paste in-viewport?)
(hooks/setup-viewport-size viewport-ref)
@ -157,6 +169,8 @@
(hooks/setup-shortcuts node-editing? drawing-path?)
(hooks/setup-active-frames base-objects vbox hover active-frames)
[:div.viewport
[:div.viewport-overlays
@ -294,7 +308,9 @@
(when show-grids?
[:& frame-grid/frame-grid
{:zoom zoom :selected selected :transform transform}])
{:zoom zoom
:selected selected
:transform transform}])
(when show-pixel-grid?
[:& widgets/pixel-grid
@ -325,12 +341,6 @@
{:zoom zoom
:tooltip tooltip}])
(when show-presence?
[:& presence/active-cursors
{:page-id page-id}])
[:& widgets/viewport-actions]
(when show-prototypes?
[:& interactions/interactions
{:selected selected
@ -341,5 +351,24 @@
(when show-selrect?
[:& widgets/selection-rect {:data selrect
:zoom zoom}])]]]))
:zoom zoom}])
(when show-presence?
[:& presence/active-cursors
{:page-id page-id}])
[:& widgets/viewport-actions]
(when show-rules?
[:*
[:& rules/rules
{:zoom zoom
:vbox vbox}]
[:& guides/viewport-guides
{:zoom zoom
:vbox vbox
:hover-frame frame-parent
:modifiers modifiers
:disabled-guides? disabled-guides?}]])]]]))

View file

@ -0,0 +1,468 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) UXBOX Labs SL
(ns app.main.ui.workspace.viewport.guides
(:require
[app.common.colors :as colors]
[app.common.geom.point :as gpt]
[app.common.geom.shapes :as gsh]
[app.common.math :as mth]
[app.common.uuid :as uuid]
[app.main.data.workspace :as dw]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.streams :as ms]
[app.main.ui.cursors :as cur]
[app.main.ui.workspace.viewport.rules :as rules]
[app.util.dom :as dom]
[rumext.alpha :as mf]))
(def guide-width 1)
(def guide-opacity 0.7)
(def guide-opacity-hover 1)
(def guide-color colors/primary)
(def guide-pill-width 34)
(def guide-pill-height 20)
(def guide-pill-corner-radius 4)
(def guide-active-area 16)
(defn use-guide
"Hooks to support drag/drop for existing guides and new guides"
[on-guide-change get-hover-frame zoom {:keys [position axis frame-id]}]
(let [dragging-ref (mf/use-ref false)
start-ref (mf/use-ref nil)
start-pos-ref (mf/use-ref nil)
state (mf/use-state {:hover false
:new-position nil
:new-frame-id frame-id})
frame-id (:new-frame-id @state)
frame-ref (mf/use-memo (mf/deps frame-id) #(refs/object-by-id frame-id))
frame (mf/deref frame-ref)
on-pointer-enter
(mf/use-callback
(fn []
(swap! state assoc :hover true)))
on-pointer-leave
(mf/use-callback
(fn []
(swap! state assoc :hover false)))
on-pointer-down
(mf/use-callback
(fn [event]
(dom/capture-pointer event)
(mf/set-ref-val! dragging-ref true)
(mf/set-ref-val! start-ref (dom/get-client-position event))
(mf/set-ref-val! start-pos-ref (get @ms/mouse-position axis))))
on-pointer-up
(mf/use-callback
(mf/deps (select-keys @state [:new-position :new-frame-id]) on-guide-change)
(fn []
(when (some? on-guide-change)
(when (some? (:new-position @state))
(on-guide-change {:position (:new-position @state)
:frame-id (:new-frame-id @state)})))))
on-lost-pointer-capture
(mf/use-callback
(fn [event]
(dom/release-pointer event)
(mf/set-ref-val! dragging-ref false)
(mf/set-ref-val! start-ref nil)
(mf/set-ref-val! start-pos-ref nil)
(swap! state assoc :new-position nil)))
on-mouse-move
(mf/use-callback
(mf/deps position zoom)
(fn [event]
(when-let [_ (mf/ref-val dragging-ref)]
(let [start-pt (mf/ref-val start-ref)
start-pos (mf/ref-val start-pos-ref)
current-pt (dom/get-client-position event)
delta (/ (- (get current-pt axis) (get start-pt axis)) zoom)
new-position (if (some? position)
(+ position delta)
(+ start-pos delta))
;; TODO: Change when pixel-grid flag exists
new-position (mth/round new-position)
new-frame-id (:id (get-hover-frame))]
(swap! state assoc
:new-position new-position
:new-frame-id new-frame-id)))))]
{:on-pointer-enter on-pointer-enter
:on-pointer-leave on-pointer-leave
:on-pointer-down on-pointer-down
:on-pointer-up on-pointer-up
:on-lost-pointer-capture on-lost-pointer-capture
:on-mouse-move on-mouse-move
:state state
:frame frame}))
;; This functions are auxiliary to get the coords of components depending on the axis
;; we're handling
(defn guide-area-axis
[pos vbox zoom frame axis]
(let [rules-pos (/ rules/rules-pos zoom)
guide-active-area (/ guide-active-area zoom)]
(cond
(and (some? frame) (= axis :x))
{:x (- pos (/ guide-active-area 2))
:y (:y frame)
:width guide-active-area
:height (:height frame)}
(some? frame)
{:x (:x frame)
:y (- pos (/ guide-active-area 2))
:width (:width frame)
:height guide-active-area}
(= axis :x)
{:x (- pos (/ guide-active-area 2))
:y (+ (:y vbox) rules-pos)
:width guide-active-area
:height (:height vbox)}
:else
{:x (+ (:x vbox) rules-pos)
:y (- pos (/ guide-active-area 2))
:width (:width vbox)
:height guide-active-area})))
(defn guide-line-axis
([pos vbox axis]
(if (= axis :x)
{:x1 pos
:y1 (:y vbox)
:x2 pos
:y2 (+ (:y vbox) (:height vbox))}
{:x1 (:x vbox)
:y1 pos
:x2 (+ (:x vbox) (:width vbox))
:y2 pos}))
([pos vbox frame axis]
(if (= axis :x)
{:l1-x1 pos
:l1-y1 (:y vbox)
:l1-x2 pos
:l1-y2 (:y frame)
:l2-x1 pos
:l2-y1 (:y frame)
:l2-x2 pos
:l2-y2 (+ (:y frame) (:height frame))
:l3-x1 pos
:l3-y1 (+ (:y frame) (:height frame))
:l3-x2 pos
:l3-y2 (+ (:y vbox) (:height vbox))}
{:l1-x1 (:x vbox)
:l1-y1 pos
:l1-x2 (:x frame)
:l1-y2 pos
:l2-x1 (:x frame)
:l2-y1 pos
:l2-x2 (+ (:x frame) (:width frame))
:l2-y2 pos
:l3-x1 (+ (:x frame) (:width frame))
:l3-y1 pos
:l3-x2 (+ (:x vbox) (:width vbox))
:l3-y2 pos})))
(defn guide-pill-axis
[pos vbox zoom axis]
(let [rules-pos (/ rules/rules-pos zoom)
guide-pill-width (/ guide-pill-width zoom)
guide-pill-height (/ guide-pill-height zoom)]
(if (= axis :x)
{:rect-x (- pos (/ guide-pill-width 2))
:rect-y (+ (:y vbox) rules-pos (- (/ guide-pill-width 2)) (/ 2 zoom))
:rect-width guide-pill-width
:rect-height guide-pill-height
:text-x pos
:text-y (+ (:y vbox) rules-pos (- (/ 3 zoom)))}
{:rect-x (+ (:x vbox) rules-pos (- (/ guide-pill-height 2)) (- (/ 5 zoom)))
:rect-y (- pos (/ guide-pill-width 2))
:rect-width guide-pill-height
:rect-height guide-pill-width
:text-x (+ (:x vbox) rules-pos (- (/ 3 zoom)))
:text-y pos})))
(defn guide-inside-vbox?
([vbox]
(partial guide-inside-vbox? vbox))
([{:keys [x y width height]} {:keys [axis position]}]
(let [x1 x
x2 (+ x width)
y1 y
y2 (+ y height)]
(if (= axis :x)
(and (>= position x1)
(<= position x2))
(and (>= position y1)
(<= position y2))))))
(defn guide-creation-area
[vbox zoom axis]
(if (= axis :x)
{:x (:x vbox)
:y (:y vbox)
:width (/ 24 zoom)
:height (:height vbox)}
{:x (:x vbox)
:y (:y vbox)
:width (:width vbox)
:height (/ 24 zoom)}))
(defn is-guide-inside-frame?
[guide frame]
(if (= :x (:axis guide))
(and (>= (:position guide) (:x frame) )
(<= (:position guide) (+ (:x frame) (:width frame)) ))
(and (>= (:position guide) (:y frame) )
(<= (:position guide) (+ (:y frame) (:height frame)) ))))
(mf/defc guide
{::mf/wrap [mf/memo]}
[{:keys [guide hover? on-guide-change get-hover-frame vbox zoom hover-frame disabled-guides? frame-modifier]}]
(let [axis (:axis guide)
handle-change-position
(mf/use-callback
(mf/deps on-guide-change)
(fn [changes]
(when on-guide-change
(on-guide-change (merge guide changes)))))
{:keys [on-pointer-enter
on-pointer-leave
on-pointer-down
on-pointer-up
on-lost-pointer-capture
on-mouse-move
state
frame]} (use-guide handle-change-position get-hover-frame zoom guide)
base-frame (or frame hover-frame)
frame (gsh/transform-shape (merge base-frame frame-modifier))
move-vec (gpt/to-vec (gpt/point (:x base-frame) (:y base-frame))
(gpt/point (:x frame) (:y frame)))
pos (+ (or (:new-position @state) (:position guide)) (get move-vec axis))
guide-width (/ guide-width zoom)
guide-pill-corner-radius (/ guide-pill-corner-radius zoom)]
(when (or (nil? frame)
(is-guide-inside-frame? (assoc guide :position pos) frame)
(:hover @state true))
[:g.guide-area
(when-not disabled-guides?
(let [{:keys [x y width height]} (guide-area-axis pos vbox zoom frame axis)]
[:rect {:x x
:y y
:width width
:height height
:style {:fill "none"
:pointer-events "fill"
:cursor (if (= axis :x) (cur/resize-ew 0) (cur/resize-ns 0))}
:on-pointer-enter on-pointer-enter
:on-pointer-leave on-pointer-leave
:on-pointer-down on-pointer-down
:on-pointer-up on-pointer-up
:on-lost-pointer-capture on-lost-pointer-capture
:on-mouse-move on-mouse-move}]))
(if (some? frame)
(let [{:keys [l1-x1 l1-y1 l1-x2 l1-y2
l2-x1 l2-y1 l2-x2 l2-y2
l3-x1 l3-y1 l3-x2 l3-y2]}
(guide-line-axis pos vbox frame axis)]
[:g
(when (or hover? (:hover @state))
[:line {:x1 l1-x1
:y1 l1-y1
:x2 l1-x2
:y2 l1-y2
:style {:stroke guide-color
:stroke-opacity guide-opacity-hover
:stroke-dasharray (str "0, " (/ 6 zoom))
:stroke-linecap "round"
:stroke-width guide-width}}])
[:line {:x1 l2-x1
:y1 l2-y1
:x2 l2-x2
:y2 l2-y2
:style {:stroke guide-color
:stroke-width guide-width
:stroke-opacity (if (or hover? (:hover @state))
guide-opacity-hover
guide-opacity)}}]
(when (or hover? (:hover @state))
[:line {:x1 l3-x1
:y1 l3-y1
:x2 l3-x2
:y2 l3-y2
:style {:stroke guide-color
:stroke-opacity guide-opacity-hover
:stroke-width guide-width
:stroke-dasharray (str "0, " (/ 6 zoom))
:stroke-linecap "round"}}])])
(let [{:keys [x1 y1 x2 y2]} (guide-line-axis pos vbox axis)]
[:line {:x1 x1
:y1 y1
:x2 x2
:y2 y2
:style {:stroke guide-color
:stroke-width guide-width
:stroke-opacity (if (or hover? (:hover @state))
guide-opacity-hover
guide-opacity)}}]))
(when (or hover? (:hover @state))
(let [{:keys [rect-x rect-y rect-width rect-height text-x text-y]}
(guide-pill-axis pos vbox zoom axis)]
[:g.guide-pill
[:rect {:x rect-x
:y rect-y
:width rect-width
:height rect-height
:rx guide-pill-corner-radius
:ry guide-pill-corner-radius
:style {:fill guide-color}}]
[:text {:x text-x
:y text-y
:text-anchor "middle"
:dominant-baseline "middle"
:transform (when (= axis :y) (str "rotate(-90 " text-x "," text-y ")"))
:style {:font-size (/ 13 zoom)
:font-family "sourcesanspro"
:fill colors/black}}
(str (mth/round pos))]]))])))
(mf/defc new-guide-area
[{:keys [vbox zoom axis get-hover-frame disabled-guides?]}]
(let [on-guide-change
(mf/use-callback
(mf/deps vbox)
(fn [guide]
(let [guide (-> guide
(assoc :id (uuid/next)
:axis axis))]
(when (guide-inside-vbox? vbox guide)
(st/emit! (dw/update-guides guide))))))
{:keys [on-pointer-enter
on-pointer-leave
on-pointer-down
on-pointer-up
on-lost-pointer-capture
on-mouse-move
state
frame]} (use-guide on-guide-change get-hover-frame zoom {:axis axis})]
[:g.new-guides
(when-not disabled-guides?
(let [{:keys [x y width height]} (guide-creation-area vbox zoom axis)]
[:rect {:x x
:y y
:width width
:height height
:on-pointer-enter on-pointer-enter
:on-pointer-leave on-pointer-leave
:on-pointer-down on-pointer-down
:on-pointer-up on-pointer-up
:on-lost-pointer-capture on-lost-pointer-capture
:on-mouse-move on-mouse-move
:style {:fill "none"
:pointer-events "fill"
:cursor (if (= axis :x) (cur/resize-ew 0) (cur/resize-ns 0))}}]))
(when (:new-position @state)
[:& guide {:guide {:axis axis
:position (:new-position @state)}
:get-hover-frame get-hover-frame
:vbox vbox
:zoom zoom
:hover? true
:hover-frame frame}])]))
(mf/defc viewport-guides
{::mf/wrap [mf/memo]}
[{:keys [zoom vbox hover-frame disabled-guides? modifiers]}]
(let [page (mf/deref refs/workspace-page)
guides (mf/use-memo
(mf/deps page vbox)
#(->> (get-in page [:options :guides] {})
(vals)
(filter (guide-inside-vbox? vbox))))
hover-frame-ref (mf/use-ref nil)
;; We use the ref to not redraw every guide everytime the hovering frame change
;; we're only interested to get the frame in the guide we're moving
get-hover-frame
(mf/use-callback
(fn []
(mf/ref-val hover-frame-ref)))
on-guide-change
(mf/use-callback
(mf/deps vbox)
(fn [guide]
(if (guide-inside-vbox? vbox guide)
(st/emit! (dw/update-guides guide))
(st/emit! (dw/remove-guide guide)))))]
(mf/use-effect
(mf/deps hover-frame)
(fn []
(mf/set-ref-val! hover-frame-ref hover-frame)))
[:g.guides {:pointer-events "none"}
[:& new-guide-area {:vbox vbox
:zoom zoom
:axis :x
:get-hover-frame get-hover-frame
:disabled-guides? disabled-guides?}]
[:& new-guide-area {:vbox vbox
:zoom zoom
:axis :y
:get-hover-frame get-hover-frame
:disabled-guides? disabled-guides?}]
(for [current guides]
[:& guide {:key (str "guide-" (:id current))
:guide current
:vbox vbox
:zoom zoom
:frame-modifier (get modifiers (:frame-id current))
:get-hover-frame get-hover-frame
:on-guide-change on-guide-change
:disabled-guides? disabled-guides?}])]))

View file

@ -0,0 +1,137 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) UXBOX Labs SL
(ns app.main.ui.workspace.viewport.rules
(:require
[app.common.colors :as colors]
[app.common.data :as d]
[app.common.math :as mth]
[app.util.object :as obj]
[rumext.alpha :as mf]))
(def rules-pos 15)
(def rules-size 4)
(def rules-width 1)
;; ----------------
;; RULES
;; ----------------
(defn- calculate-step-size
[zoom]
(cond
(< 0 zoom 0.008) 10000
(< 0.008 zoom 0.015) 5000
(< 0.015 zoom 0.04) 2500
(< 0.04 zoom 0.07) 1000
(< 0.07 zoom 0.2) 500
(< 0.2 zoom 0.5) 250
(< 0.5 zoom 1) 100
(<= 1 zoom 2) 50
(< 2 zoom 4) 25
(< 4 zoom 6) 10
(< 6 zoom 15) 5
(< 15 zoom 25) 2
(< 25 zoom) 1
:else 1))
(defn get-clip-area
[vbox zoom axis]
(if (= axis :x)
(let [x (+ (:x vbox) (/ 25 zoom))
y (:y vbox)
width (- (:width vbox) (/ 21 zoom))
height (/ 25 zoom)]
{:x x :y y :width width :height height})
(let [x (:x vbox)
y (+ (:y vbox) (/ 25 zoom))
width (/ 25 zoom)
height (- (:height vbox) (/ 21 zoom))]
{:x x :y y :width width :height height})))
(defn get-rule-params
[vbox axis]
(if (= axis :x)
(let [start (:x vbox)
end (+ start (:width vbox))]
{:start start :end end})
(let [start (:y vbox)
end (+ start (:height vbox))]
{:start start :end end})))
(defn get-rule-axis
[val vbox zoom axis]
(let [rules-pos (/ rules-pos zoom)
rules-size (/ rules-size zoom)]
(if (= axis :x)
{:text-x val
:text-y (+ (:y vbox) (- rules-pos (/ 4 zoom)))
:line-x1 val
:line-y1 (+ (:y vbox) rules-pos (/ 2 zoom))
:line-x2 val
:line-y2 (+ (:y vbox) rules-pos (/ 2 zoom) rules-size)}
{:text-x (+ (:x vbox) (- rules-pos (/ 4 zoom)))
:text-y val
:line-x1 (+ (:x vbox) rules-pos (/ 2 zoom))
:line-y1 val
:line-x2 (+ (:x vbox) rules-pos (/ 2 zoom) rules-size)
:line-y2 val})))
(mf/defc rules-axis
[{:keys [zoom vbox axis]}]
(let [rules-width (/ rules-width zoom)
step (calculate-step-size zoom)
clip-id (str "clip-rule-" (d/name axis))]
[:g.rules {:clipPath (str "url(#" clip-id ")")}
[:defs
[:clipPath {:id clip-id}
(let [{:keys [x y width height]} (get-clip-area vbox zoom axis)]
[:rect {:x x :y y :width width :height height}])]]
(let [{:keys [start end]} (get-rule-params vbox axis)
minv (max (mth/round start) -100000)
minv (* (mth/ceil (/ minv step)) step)
maxv (min (mth/round end) 100000)
maxv (* (mth/floor (/ maxv step)) step)]
(for [step-val (range minv (inc maxv) step)]
(let [{:keys [text-x text-y line-x1 line-y1 line-x2 line-y2]}
(get-rule-axis step-val vbox zoom axis)]
[:*
[:text {:key (str "text-" (d/name axis) "-" step-val)
:x text-x
:y text-y
:text-anchor "middle"
:dominant-baseline "middle"
:transform (when (= axis :y) (str "rotate(-90 " text-x "," text-y ")"))
:style {:font-size (/ 13 zoom)
:font-family "sourcesanspro"
:fill colors/gray-30}}
(str (mth/round step-val))]
[:line {:key (str "line-" (d/name axis) "-" step-val)
:x1 line-x1
:y1 line-y1
:x2 line-x2
:y2 line-y2
:style {:stroke colors/gray-30
:stroke-width rules-width}}]])))]))
(mf/defc rules
{::mf/wrap-props false
::mf/wrap [mf/memo]}
[props]
(let [zoom (obj/get props "zoom")
vbox (obj/get props "vbox")]
(when (some? vbox)
[:g.rules {:pointer-events "none"}
[:& rules-axis {:zoom zoom :vbox vbox :axis :x}]
[:& rules-axis {:zoom zoom :vbox vbox :axis :y}]])))

View file

@ -52,7 +52,7 @@
:opacity line-opacity}])
(defn get-snap
[coord {:keys [shapes page-id filter-shapes modifiers]}]
[coord {:keys [shapes page-id remove-snap? modifiers]}]
(let [shape (if (> (count shapes) 1)
(->> shapes (map gsh/transform-shape) gsh/selection-rect (gsh/setup {:type :rect}))
(->> shapes (first)))
@ -68,7 +68,7 @@
(->> (sp/shape-snap-points shape)
(map #(vector frame-id %)))))
(rx/flat-map (fn [[frame-id point]]
(->> (snap/get-snap-points page-id frame-id filter-shapes point coord)
(->> (snap/get-snap-points page-id frame-id remove-snap? point coord)
(rx/map #(vector point % coord)))))
(rx/reduce conj []))))
@ -104,7 +104,7 @@
(hash-map coord fixedv (flip coord) maxv)]))))
(mf/defc snap-feedback
[{:keys [shapes filter-shapes zoom modifiers] :as props}]
[{:keys [shapes remove-snap? zoom modifiers] :as props}]
(let [state (mf/use-state [])
subject (mf/use-memo #(rx/subject))
@ -129,7 +129,7 @@
#(rx/dispose! sub))))
(mf/use-effect
(mf/deps shapes filter-shapes modifiers)
(mf/deps shapes remove-snap? modifiers)
(fn []
(rx/push! subject props)))
@ -152,29 +152,23 @@
{::mf/wrap [mf/memo]}
[{:keys [layout zoom objects selected page-id drawing transform modifiers] :as props}]
(let [;; shapes (mf/deref (refs/objects-by-id selected))
;; filter-shapes (mf/deref refs/selected-shapes-with-children)
(let [shapes (into [] (keep (d/getf objects)) selected)
shapes (->> selected
(map #(get objects %))
(filterv (comp not nil?)))
filter-shapes (into #{}
(comp (mapcat #(cp/get-object-with-children % objects))
(map :id))
selected)
filter-shapes
(into #{}
(comp (mapcat #(cp/get-object-with-children % objects))
(map :id))
selected)
filter-shapes (fn [id]
(if (= id :layout)
(or (not (contains? layout :display-grid))
(not (contains? layout :snap-grid)))
(or (filter-shapes id)
(not (contains? layout :dynamic-alignment)))))
remove-snap? (mf/use-memo
(mf/deps layout filter-shapes)
#(snap/make-remove-snap layout filter-shapes))
shapes (if drawing [drawing] shapes)]
(when (or drawing transform)
[:& snap-feedback {:shapes shapes
:page-id page-id
:filter-shapes filter-shapes
:remove-snap? remove-snap?
:zoom zoom
:modifiers modifiers}])))

View file

@ -213,9 +213,20 @@
(.-innerText el)))
(defn query
[^js el ^string query]
(when (some? el)
(.querySelector el query)))
([^string query]
(query globals/document query))
([^js el ^string query]
(when (some? el)
(.querySelector el query))))
(defn query-all
([^string query]
(query-all globals/document query))
([^js el ^string query]
(when (some? el)
(.querySelectorAll el query))))
(defn get-client-position
[^js event]

View file

@ -28,3 +28,9 @@
(case (:type shape)
:frame (-> shape :selrect frame-snap-points)
(into #{(gsh/center-shape shape)} (:points shape)))))
(defn guide-snap-points
[guide]
(if (= :x (:axis guide))
#{(gpt/point (:position guide) 0)}
#{(gpt/point 0 (:position guide))}))

View file

@ -515,6 +515,20 @@
(let [flows-node (get-data node :penpot:flows)]
(->> flows-node :content (mapv parse-flow-node))))
(defn parse-guide-node [node]
(let [attrs (-> node :attrs remove-penpot-prefix)]
(println attrs)
(let [id (uuid/next)]
[id
{:id id
:frame-id (when (:frame-id attrs) (-> attrs :frame-id uuid))
:axis (-> attrs :axis keyword)
:position (-> attrs :position d/parse-double)}])))
(defn parse-guides [node]
(let [guides-node (get-data node :penpot:guides)]
(->> guides-node :content (map parse-guide-node) (into {}))))
(defn extract-from-data
([node tag]
(extract-from-data node tag identity))
@ -764,7 +778,8 @@
grids (->> (parse-grids node)
(group-by :type)
(d/mapm (fn [_ v] (-> v first :params))))
flows (parse-flows node)]
flows (parse-flows node)
guides (parse-guides node)]
(cond-> {}
(some? background)
(assoc-in [:options :background] background)
@ -773,7 +788,10 @@
(assoc-in [:options :saved-grids] grids)
(d/not-empty? flows)
(assoc-in [:options :flows] flows))))
(assoc-in [:options :flows] flows)
(d/not-empty? guides)
(assoc-in [:options :guides] guides))))
(defn parse-interactions
[node]

View file

@ -0,0 +1,251 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) UXBOX Labs SL
(ns app.util.snap-data
"Data structure that holds and retrieves the data to make the snaps. Internaly
is implemented with a balanced binary tree that queries by range.
https://en.wikipedia.org/wiki/Range_tree"
(:require
[app.common.data :as d]
[app.common.pages :as cp]
[app.common.pages.diff :as diff]
[app.common.uuid :as uuid]
[app.util.geom.grid :as gg]
[app.util.geom.snap-points :as snap]
[app.util.range-tree :as rt]))
(def snap-attrs [:frame-id :x :y :width :height :hidden :selrect :grids])
;; PRIVATE FUNCTIONS
(defn- make-insert-tree-data
"Inserts all data in it's corresponding axis bucket"
[shape-data axis]
(fn [tree]
(let [tree (or tree (rt/make-tree))
insert-data
(fn [tree data]
(rt/insert tree (get-in data [:pt axis]) data))]
(reduce insert-data tree shape-data))))
(defn- make-delete-tree-data
"Removes all data in it's corresponding axis bucket"
[shape-data axis]
(fn [tree]
(let [tree (or tree (rt/make-tree))
remove-data
(fn [tree data]
(rt/remove tree (get-in data [:pt axis]) data))]
(reduce remove-data tree shape-data))))
(defn- add-root-frame
[page-data]
(let [frame-id uuid/zero]
(-> page-data
(assoc-in [frame-id :x] (rt/make-tree))
(assoc-in [frame-id :y] (rt/make-tree)))))
(defn- add-frame
[page-data frame]
(let [frame-id (:id frame)
parent-id (:parent-id frame)
frame-data (->> (snap/shape-snap-points frame)
(mapv #(array-map :type :shape
:id frame-id
:pt %)))
grid-x-data (->> (gg/grid-snap-points frame :x)
(mapv #(array-map :type :grid-x
:id frame-id
:pt %)))
grid-y-data (->> (gg/grid-snap-points frame :y)
(mapv #(array-map :type :grid-y
:id frame-id
:pt %)))]
(-> page-data
;; Update root frame information
(assoc-in [uuid/zero :objects-data frame-id] frame-data)
(update-in [parent-id :x] (make-insert-tree-data frame-data :x))
(update-in [parent-id :y] (make-insert-tree-data frame-data :y))
;; Update frame information
(assoc-in [frame-id :objects-data frame-id] (d/concat-vec frame-data grid-x-data grid-y-data))
(update-in [frame-id :x] #(or % (rt/make-tree)))
(update-in [frame-id :y] #(or % (rt/make-tree)))
(update-in [frame-id :x] (make-insert-tree-data (d/concat-vec frame-data grid-x-data) :x))
(update-in [frame-id :y] (make-insert-tree-data (d/concat-vec frame-data grid-y-data) :y)))))
(defn- add-shape
[page-data shape]
(let [frame-id (:frame-id shape)
snap-points (snap/shape-snap-points shape)
shape-data (->> snap-points
(mapv #(array-map
:type :shape
:id (:id shape)
:pt %)))]
(-> page-data
(assoc-in [frame-id :objects-data (:id shape)] shape-data)
(update-in [frame-id :x] (make-insert-tree-data shape-data :x))
(update-in [frame-id :y] (make-insert-tree-data shape-data :y)))))
(defn- add-guide
[page-data guide]
(let [guide-data (->> (snap/guide-snap-points guide)
(mapv #(array-map
:type :guide
:id (:id guide)
:pt %)))]
(if-let [frame-id (:frame-id guide)]
;; Guide inside frame, we add the information only on that frame
(-> page-data
(assoc-in [frame-id :objects-data (:id guide)] guide-data)
(update-in [frame-id (:axis guide)] (make-insert-tree-data guide-data (:axis guide))))
;; Guide outside the frame. We add the information in the global guides data
(-> page-data
(assoc-in [:guides :objects-data (:id guide)] guide-data)
(update-in [:guides (:axis guide)] (make-insert-tree-data guide-data (:axis guide)))))))
(defn- remove-frame
[page-data frame]
(let [frame-id (:id frame)
root-data (get-in page-data [uuid/zero :objects-data frame-id])]
(-> page-data
(d/dissoc-in [uuid/zero :objects-data frame-id])
(update-in [uuid/zero :x] (make-delete-tree-data root-data :x))
(update-in [uuid/zero :y] (make-delete-tree-data root-data :y))
(dissoc frame-id))))
(defn- remove-shape
[page-data shape]
(let [frame-id (:frame-id shape)
shape-data (get-in page-data [frame-id :objects-data (:id shape)])]
(-> page-data
(d/dissoc-in [frame-id :objects-data (:id shape)])
(update-in [frame-id :x] (make-delete-tree-data shape-data :x))
(update-in [frame-id :y] (make-delete-tree-data shape-data :y)))))
(defn- remove-guide
[page-data guide]
(if-let [frame-id (:frame-id guide)]
(let [guide-data (get-in page-data [frame-id :objects-data (:id guide)])]
(-> page-data
(d/dissoc-in [frame-id :objects-data (:id guide)])
(update-in [frame-id (:axis guide)] (make-delete-tree-data guide-data (:axis guide)))))
;; Guide outside the frame. We add the information in the global guides data
(let [guide-data (get-in page-data [:guides :objects-data (:id guide)])]
(-> page-data
(d/dissoc-in [:guides :objects-data (:id guide)])
(update-in [:guides (:axis guide)] (make-delete-tree-data guide-data (:axis guide)))))))
(defn- update-frame
[page-data [_ new-frame]]
(let [frame-id (:id new-frame)
root-data (get-in page-data [uuid/zero :objects-data frame-id])
frame-data (get-in page-data [frame-id :objects-data frame-id])]
(-> page-data
(update-in [uuid/zero :x] (make-delete-tree-data root-data :x))
(update-in [uuid/zero :y] (make-delete-tree-data root-data :y))
(update-in [frame-id :x] (make-delete-tree-data frame-data :x))
(update-in [frame-id :y] (make-delete-tree-data frame-data :y))
(add-frame new-frame))))
(defn- update-shape
[page-data [old-shape new-shape]]
(-> page-data
(remove-shape old-shape)
(add-shape new-shape)))
(defn- update-guide
[page-data [old-guide new-guide]]
(-> page-data
(remove-guide old-guide)
(add-guide new-guide)))
;; PUBLIC API
(defn make-snap-data
"Creates an empty snap index"
[]
{})
(defn add-page
"Adds page information"
[snap-data {:keys [objects options] :as page}]
(let [frames (cp/select-frames objects)
shapes (cp/select-objects #(not= :frame (:type %)) page)
guides (vals (:guides options))
page-data
(as-> {} $
(add-root-frame $)
(reduce add-frame $ frames)
(reduce add-shape $ shapes)
(reduce add-guide $ guides))]
(assoc snap-data (:id page) page-data)))
(defn update-page
"Updates a previously inserted page with new data"
[snap-data old-page page]
(if (contains? snap-data (:id page))
;; Update page
(update snap-data (:id page)
(fn [page-data]
(let [{:keys [change-frame-shapes
change-frame-guides
removed-frames
removed-shapes
removed-guides
updated-frames
updated-shapes
updated-guides
new-frames
new-shapes
new-guides]}
(diff/calculate-page-diff old-page page snap-attrs)]
(as-> page-data $
(reduce update-shape $ change-frame-shapes)
(reduce remove-frame $ removed-frames)
(reduce remove-shape $ removed-shapes)
(reduce update-frame $ updated-frames)
(reduce update-shape $ updated-shapes)
(reduce add-frame $ new-frames)
(reduce add-shape $ new-shapes)
(reduce update-guide $ change-frame-guides)
(reduce remove-guide $ removed-guides)
(reduce update-guide $ updated-guides)
(reduce add-guide $ new-guides)))))
;; Page doesn't exist, we create a new entry
(add-page snap-data page)))
(defn query
"Retrieve the shape data for the snaps in that range"
[snap-data page-id frame-id axis [from to]]
(d/concat-vec
(-> snap-data
(get-in [page-id frame-id axis])
(rt/range-query from to))
(-> snap-data
(get-in [page-id :guides axis])
(rt/range-query from to))))

View file

@ -40,14 +40,13 @@
(defmethod handler :update-page-indices
[{:keys [page-id changes] :as message}]
(let [old-objects (get-in @state [:pages-index page-id :objects])]
(let [old-page (get-in @state [:pages-index page-id])]
(swap! state ch/process-changes changes false)
(let [new-objects (get-in @state [:pages-index page-id :objects])
(let [new-page (get-in @state [:pages-index page-id])
message (assoc message
:objects new-objects
:new-objects new-objects
:old-objects old-objects)]
:old-page old-page
:new-page new-page)]
(handler (-> message
(assoc :cmd :selection/update-index)))
(handler (-> message

View file

@ -283,7 +283,6 @@
(defn setup-interactions
[file]
(letfn [(add-interactions
[file [id interactions]]
(->> interactions
@ -294,7 +293,6 @@
(let [interactions (:interactions file)
file (dissoc file :interactions)]
(->> interactions (reduce add-interactions file))))]
(-> file process-interactions)))
(defn resolve-media
@ -328,7 +326,12 @@
(assoc :id (resolve page-id)))
flows (->> (get-in page-data [:options :flows])
(mapv #(update % :starting-frame resolve)))
page-data (d/assoc-in-when page-data [:options :flows] flows)
guides (->> (get-in page-data [:options :guides])
(d/mapm #(update %2 :frame-id resolve)))
page-data (-> page-data
(d/assoc-in-when [:options :flows] flows)
(d/assoc-in-when [:options :guides] guides))
file (-> file (fb/add-page page-data))]
(->> (rx/from nodes)
(rx/filter cip/shape?)

View file

@ -170,8 +170,10 @@
nil))
(defmethod impl/handler :selection/update-index
[{:keys [page-id old-objects new-objects] :as message}]
(let [update-page-index
[{:keys [page-id old-page new-page] :as message}]
(let [old-objects (:objects old-page)
new-objects (:objects new-page)
update-page-index
(fn [index]
(let [old-bounds (:bounds index)
new-bounds (objects-bounds new-objects)]

View file

@ -6,179 +6,32 @@
(ns app.worker.snaps
(:require
[app.common.data :as d]
[app.common.uuid :as uuid]
[app.util.geom.grid :as gg]
[app.util.geom.snap-points :as snap]
[app.util.range-tree :as rt]
[app.util.snap-data :as sd]
[app.worker.impl :as impl]
[clojure.set :as set]
[okulary.core :as l]))
(defonce state (l/atom {}))
(defn process-shape [frame-id coord]
(fn [shape]
(let [points (when-not (:hidden shape) (snap/shape-snap-points shape))
shape-data (->> points (mapv #(vector % (:id shape))))]
(if (= (:id shape) frame-id)
(into shape-data
;; The grid points are only added by the "root" of the coord-dat
(->> (gg/grid-snap-points shape coord)
(map #(vector % :layout))))
shape-data))))
(defn- add-coord-data
"Initializes the range tree given the shapes"
[data frame-id shapes coord]
(letfn [(into-tree [tree [point _ :as data]]
(rt/insert tree (coord point) data))]
(->> shapes
(mapcat (process-shape frame-id coord))
(reduce into-tree (or data (rt/make-tree))))))
(defn remove-coord-data
[data frame-id shapes coord]
(letfn [(remove-tree [tree [point _ :as data]]
(rt/remove tree (coord point) data))]
(->> shapes
(mapcat (process-shape frame-id coord))
(reduce remove-tree (or data (rt/make-tree))))))
(defn aggregate-data
([objects]
(aggregate-data objects (keys objects)))
([objects ids]
(->> ids
(filter #(contains? objects %))
(map #(get objects %))
(filter :frame-id)
(group-by :frame-id)
;; Adds the frame
(d/mapm #(conj %2 (get objects %1))))))
(defn- initialize-snap-data
"Initialize the snap information with the current workspace information"
[objects]
(let [shapes-data (aggregate-data objects)
create-index
(fn [frame-id shapes]
{:x (-> (rt/make-tree) (add-coord-data frame-id shapes :x))
:y (-> (rt/make-tree) (add-coord-data frame-id shapes :y))})]
(d/mapm create-index shapes-data)))
;; Attributes that will change the values of their snap
(def snap-attrs [:x :y :width :height :hidden :selrect :grids])
(defn- update-snap-data
[snap-data old-objects new-objects]
(let [changed? (fn [id]
(let [oldv (get old-objects id)
newv (get new-objects id)]
;; Check first without select-keys because is faster if they are
;; the same reference
(and (not= oldv newv)
(not= (select-keys oldv snap-attrs)
(select-keys newv snap-attrs)))))
is-deleted-frame? #(and (not= uuid/zero %)
(contains? old-objects %)
(not (contains? new-objects %))
(= :frame (get-in old-objects [% :type])))
is-new-frame? #(and (not= uuid/zero %)
(contains? new-objects %)
(not (contains? old-objects %))
(= :frame (get-in new-objects [% :type])))
changed-ids (into #{}
(filter changed?)
(set/union (set (keys old-objects))
(set (keys new-objects))))
to-delete (aggregate-data old-objects changed-ids)
to-add (aggregate-data new-objects changed-ids)
frames-to-delete (->> changed-ids (filter is-deleted-frame?))
frames-to-add (->> changed-ids (filter is-new-frame?))
delete-data
(fn [snap-data [frame-id shapes]]
(-> snap-data
(update-in [frame-id :x] remove-coord-data frame-id shapes :x)
(update-in [frame-id :y] remove-coord-data frame-id shapes :y)))
add-data
(fn [snap-data [frame-id shapes]]
(-> snap-data
(update-in [frame-id :x] add-coord-data frame-id shapes :x)
(update-in [frame-id :y] add-coord-data frame-id shapes :y)))
delete-frames
(fn [snap-data frame-id]
(dissoc snap-data frame-id))
add-frames
(fn [snap-data frame-id]
(assoc snap-data frame-id {:x (rt/make-tree)
:y (rt/make-tree)}))]
(as-> snap-data $
(reduce delete-data $ to-delete)
(reduce add-frames $ frames-to-add)
(reduce add-data $ to-add)
(reduce delete-frames $ frames-to-delete))))
;; (defn- log-state
;; "Helper function to print a friendly version of the snap tree. Debugging purposes"
;; []
;; (let [process-frame-data #(d/mapm rt/as-map %)
;; process-page-data #(d/mapm process-frame-data %)]
;; (js/console.log "STATE" (clj->js (d/mapm process-page-data @state)))))
(defn- index-page [state page-id objects]
(let [snap-data (initialize-snap-data objects)]
(assoc state page-id snap-data)))
(defn- update-page [state page-id old-objects new-objects]
(let [snap-data (get state page-id)
snap-data (update-snap-data snap-data old-objects new-objects)]
(assoc state page-id snap-data)))
;; Public API
(defmethod impl/handler :snaps/initialize-index
[{:keys [data] :as message}]
;; Create the index
(letfn [(process-page [state page]
(let [id (:id page)
objects (:objects page)]
(index-page state id objects)))]
(swap! state #(reduce process-page % (vals (:pages-index data))))
;; (log-state)
;; Return nil so the worker will not answer anything back
nil))
(let [pages (vals (:pages-index data))]
(reset! state (reduce sd/add-page (sd/make-snap-data) pages)))
nil)
(defmethod impl/handler :snaps/update-index
[{:keys [page-id old-objects new-objects] :as message}]
(swap! state update-page page-id old-objects new-objects)
;; Uncomment this to regenerate the index everytime
#_(swap! state index-page page-id new-objects)
;; (log-state)
[{:keys [old-page new-page] :as message}]
(swap! state sd/update-page old-page new-page)
nil)
(defmethod impl/handler :snaps/range-query
[{:keys [page-id frame-id coord ranges] :as message}]
(letfn [(calculate-range [[from to]]
(-> @state
(get-in [page-id frame-id coord])
(rt/range-query from to)))]
(->> ranges
(mapcat calculate-range)
set ;; unique
(into []))))
[{:keys [page-id frame-id axis ranges] :as message}]
(into []
(comp (mapcat #(sd/query @state page-id frame-id axis %))
(distinct))
ranges))

View file

@ -0,0 +1,431 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) UXBOX Labs SL
(ns app.util.snap-data-test
(:require
[app.common.uuid :as uuid]
[cljs.test :as t :include-macros true]
[cljs.pprint :refer [pprint]]
[app.common.pages.init :as init]
[app.common.file-builder :as fb]
[app.util.snap-data :as sd]))
(t/deftest test-create-index
(t/testing "Create empty data"
(let [data (sd/make-snap-data)]
(t/is (some? data))))
(t/testing "Add empty page (only root-frame)"
(let [page (-> (fb/create-file "Test")
(fb/add-page {:name "Page-1"})
(fb/get-current-page))
data (-> (sd/make-snap-data)
(sd/add-page page))]
(t/is (some? data))))
(t/testing "Create simple shape on root"
(let [file (-> (fb/create-file "Test")
(fb/add-page {:name "Page-1"})
(fb/create-rect
{:x 0
:y 0
:width 100
:height 100}))
page (fb/get-current-page file)
data (-> (sd/make-snap-data)
(sd/add-page page))
result-x (sd/query data (:id page) uuid/zero :x [0 100])]
(t/is (some? data))
;; 3 = left side, center and right side
(t/is (= (count result-x) 3))
;; Left side: two points
(t/is (= (first (nth result-x 0)) 0))
;; Center one point
(t/is (= (first (nth result-x 1)) 50))
;; Right side two points
(t/is (= (first (nth result-x 2)) 100))))
(t/testing "Add page with single empty frame"
(let [file (-> (fb/create-file "Test")
(fb/add-page {:name "Page-1"})
(fb/add-artboard
{:x 0
:y 0
:width 100
:height 100})
(fb/close-artboard))
frame-id (:last-id file)
page (fb/get-current-page file)
;; frame-id (:last-id file)
data (-> (sd/make-snap-data)
(sd/add-page page))
result-zero-x (sd/query data (:id page) uuid/zero :x [0 100])
result-frame-x (sd/query data (:id page) frame-id :x [0 100])]
(t/is (some? data))
(t/is (= (count result-zero-x) 3))
(t/is (= (count result-frame-x) 3))))
(t/testing "Add page with some shapes inside frames"
(let [file (-> (fb/create-file "Test")
(fb/add-page {:name "Page-1"})
(fb/add-artboard
{:x 0
:y 0
:width 100
:height 100}))
frame-id (:last-id file)
file (-> file
(fb/create-rect
{:x 25
:y 25
:width 50
:height 50})
(fb/close-artboard))
page (fb/get-current-page file)
;; frame-id (:last-id file)
data (-> (sd/make-snap-data)
(sd/add-page page))
result-zero-x (sd/query data (:id page) uuid/zero :x [0 100])
result-frame-x (sd/query data (:id page) frame-id :x [0 100])]
(t/is (some? data))
(t/is (= (count result-zero-x) 3))
(t/is (= (count result-frame-x) 5))))
(t/testing "Add a global guide"
(let [file (-> (fb/create-file "Test")
(fb/add-page {:name "Page-1"})
(fb/add-guide {:position 50 :axis :x})
(fb/add-artboard {:x 200 :y 200 :width 100 :height 100})
(fb/close-artboard))
frame-id (:last-id file)
page (fb/get-current-page file)
;; frame-id (:last-id file)
data (-> (sd/make-snap-data)
(sd/add-page page))
result-zero-x (sd/query data (:id page) uuid/zero :x [0 100])
result-zero-y (sd/query data (:id page) uuid/zero :y [0 100])
result-frame-x (sd/query data (:id page) frame-id :x [0 100])
result-frame-y (sd/query data (:id page) frame-id :y [0 100])]
(t/is (some? data))
;; We can snap in the root
(t/is (= (count result-zero-x) 1))
(t/is (= (count result-zero-y) 0))
;; We can snap in the frame
(t/is (= (count result-frame-x) 1))
(t/is (= (count result-frame-y) 0))))
(t/testing "Add a frame guide"
(let [file (-> (fb/create-file "Test")
(fb/add-page {:name "Page-1"})
(fb/add-artboard {:x 200 :y 200 :width 100 :height 100})
(fb/close-artboard))
frame-id (:last-id file)
file (-> file
(fb/add-guide {:position 50 :axis :x :frame-id frame-id}))
page (fb/get-current-page file)
;; frame-id (:last-id file)
data (-> (sd/make-snap-data)
(sd/add-page page))
result-zero-x (sd/query data (:id page) uuid/zero :x [0 100])
result-zero-y (sd/query data (:id page) uuid/zero :y [0 100])
result-frame-x (sd/query data (:id page) frame-id :x [0 100])
result-frame-y (sd/query data (:id page) frame-id :y [0 100])]
(t/is (some? data))
;; We can snap in the root
(t/is (= (count result-zero-x) 0))
(t/is (= (count result-zero-y) 0))
;; We can snap in the frame
(t/is (= (count result-frame-x) 1))
(t/is (= (count result-frame-y) 0)))))
(t/deftest test-update-index
(t/testing "Create frame on root and then remove it."
(let [file (-> (fb/create-file "Test")
(fb/add-page {:name "Page-1"})
(fb/add-artboard
{:x 0
:y 0
:width 100
:height 100})
(fb/close-artboard))
shape-id (:last-id file)
page (fb/get-current-page file)
;; frame-id (:last-id file)
data (-> (sd/make-snap-data)
(sd/add-page page))
file (-> file
(fb/delete-object shape-id))
new-page (fb/get-current-page file)
data (sd/update-page data page new-page)
result-x (sd/query data (:id page) uuid/zero :x [0 100])
result-y (sd/query data (:id page) uuid/zero :y [0 100])]
(t/is (some? data))
(t/is (= (count result-x) 0))
(t/is (= (count result-y) 0))))
(t/testing "Create simple shape on root. Then remove it"
(let [file (-> (fb/create-file "Test")
(fb/add-page {:name "Page-1"})
(fb/create-rect
{:x 0
:y 0
:width 100
:height 100}))
shape-id (:last-id file)
page (fb/get-current-page file)
;; frame-id (:last-id file)
data (-> (sd/make-snap-data)
(sd/add-page page))
file (fb/delete-object file shape-id)
new-page (fb/get-current-page file)
data (sd/update-page data page new-page)
result-x (sd/query data (:id page) uuid/zero :x [0 100])
result-y (sd/query data (:id page) uuid/zero :y [0 100])]
(t/is (some? data))
(t/is (= (count result-x) 0))
(t/is (= (count result-y) 0))))
(t/testing "Create shape inside frame, then remove it"
(let [file (-> (fb/create-file "Test")
(fb/add-page {:name "Page-1"})
(fb/add-artboard
{:x 0
:y 0
:width 100
:height 100}))
frame-id (:last-id file)
file (fb/create-rect file {:x 25 :y 25 :width 50 :height 50})
shape-id (:last-id file)
file (fb/close-artboard file)
page (fb/get-current-page file)
data (-> (sd/make-snap-data)
(sd/add-page page))
file (fb/delete-object file shape-id)
new-page (fb/get-current-page file)
data (sd/update-page data page new-page)
result-zero-x (sd/query data (:id page) uuid/zero :x [0 100])
result-frame-x (sd/query data (:id page) frame-id :x [0 100])]
(t/is (some? data))
(t/is (= (count result-zero-x) 3))
(t/is (= (count result-frame-x) 3))))
(t/testing "Create global guide then remove it"
(let [file (-> (fb/create-file "Test")
(fb/add-page {:name "Page-1"})
(fb/add-guide {:position 50 :axis :x}))
guide-id (:last-id file)
file (-> (fb/add-artboard file {:x 200 :y 200 :width 100 :height 100})
(fb/close-artboard))
frame-id (:last-id file)
page (fb/get-current-page file)
data (-> (sd/make-snap-data) (sd/add-page page))
new-page (-> (fb/delete-guide file guide-id)
(fb/get-current-page))
data (sd/update-page data page new-page)
result-zero-x (sd/query data (:id page) uuid/zero :x [0 100])
result-zero-y (sd/query data (:id page) uuid/zero :y [0 100])
result-frame-x (sd/query data (:id page) frame-id :x [0 100])
result-frame-y (sd/query data (:id page) frame-id :y [0 100])]
(t/is (some? data))
;; We can snap in the root
(t/is (= (count result-zero-x) 0))
(t/is (= (count result-zero-y) 0))
;; We can snap in the frame
(t/is (= (count result-frame-x) 0))
(t/is (= (count result-frame-y) 0))))
(t/testing "Create frame guide then remove it"
(let [file (-> (fb/create-file "Test")
(fb/add-page {:name "Page-1"})
(fb/add-artboard {:x 200 :y 200 :width 100 :height 100})
(fb/close-artboard))
frame-id (:last-id file)
file (fb/add-guide file {:position 50 :axis :x :frame-id frame-id})
guide-id (:last-id file)
page (fb/get-current-page file)
data (-> (sd/make-snap-data) (sd/add-page page))
new-page (-> (fb/delete-guide file guide-id)
(fb/get-current-page))
data (sd/update-page data page new-page)
result-zero-x (sd/query data (:id page) uuid/zero :x [0 100])
result-zero-y (sd/query data (:id page) uuid/zero :y [0 100])
result-frame-x (sd/query data (:id page) frame-id :x [0 100])
result-frame-y (sd/query data (:id page) frame-id :y [0 100])]
(t/is (some? data))
;; We can snap in the root
(t/is (= (count result-zero-x) 0))
(t/is (= (count result-zero-y) 0))
;; We can snap in the frame
(t/is (= (count result-frame-x) 0))
(t/is (= (count result-frame-y) 0))))
(t/testing "Update frame coordinates"
(let [file (-> (fb/create-file "Test")
(fb/add-page {:name "Page-1"})
(fb/add-artboard
{:x 0
:y 0
:width 100
:height 100})
(fb/close-artboard))
frame-id (:last-id file)
page (fb/get-current-page file)
data (-> (sd/make-snap-data) (sd/add-page page))
frame (fb/lookup-shape file frame-id)
new-frame (-> frame
(assoc :x 200 :y 200))
file (fb/update-object file frame new-frame)
new-page (fb/get-current-page file)
data (sd/update-page data page new-page)
result-zero-x-1 (sd/query data (:id page) uuid/zero :x [0 100])
result-frame-x-1 (sd/query data (:id page) frame-id :x [0 100])
result-zero-x-2 (sd/query data (:id page) uuid/zero :x [200 300])
result-frame-x-2 (sd/query data (:id page) frame-id :x [200 300])]
(t/is (some? data))
(t/is (= (count result-zero-x-1) 0))
(t/is (= (count result-frame-x-1) 0))
(t/is (= (count result-zero-x-2) 3))
(t/is (= (count result-frame-x-2) 3))))
(t/testing "Update shape coordinates"
(let [file (-> (fb/create-file "Test")
(fb/add-page {:name "Page-1"})
(fb/create-rect
{:x 0
:y 0
:width 100
:height 100}))
shape-id (:last-id file)
page (fb/get-current-page file)
data (-> (sd/make-snap-data) (sd/add-page page))
shape (fb/lookup-shape file shape-id)
new-shape (-> shape
(assoc :x 200 :y 200))
file (fb/update-object file shape new-shape)
new-page (fb/get-current-page file)
data (sd/update-page data page new-page)
result-zero-x-1 (sd/query data (:id page) uuid/zero :x [0 100])
result-zero-x-2 (sd/query data (:id page) uuid/zero :x [200 300])]
(t/is (some? data))
(t/is (= (count result-zero-x-1) 0))
(t/is (= (count result-zero-x-2) 3))))
(t/testing "Update global guide"
(let [guide {:position 50 :axis :x}
file (-> (fb/create-file "Test")
(fb/add-page {:name "Page-1"})
(fb/add-guide guide))
guide-id (:last-id file)
guide (assoc guide :id guide-id)
file (-> (fb/add-artboard file {:x 500 :y 500 :width 100 :height 100})
(fb/close-artboard))
frame-id (:last-id file)
page (fb/get-current-page file)
data (-> (sd/make-snap-data) (sd/add-page page))
new-page (-> (fb/update-guide file (assoc guide :position 150))
(fb/get-current-page))
data (sd/update-page data page new-page)
result-zero-x-1 (sd/query data (:id page) uuid/zero :x [0 100])
result-zero-y-1 (sd/query data (:id page) uuid/zero :y [0 100])
result-frame-x-1 (sd/query data (:id page) frame-id :x [0 100])
result-frame-y-1 (sd/query data (:id page) frame-id :y [0 100])
result-zero-x-2 (sd/query data (:id page) uuid/zero :x [0 200])
result-zero-y-2 (sd/query data (:id page) uuid/zero :y [0 200])
result-frame-x-2 (sd/query data (:id page) frame-id :x [0 200])
result-frame-y-2 (sd/query data (:id page) frame-id :y [0 200])
]
(t/is (some? data))
(t/is (= (count result-zero-x-1) 0))
(t/is (= (count result-zero-y-1) 0))
(t/is (= (count result-frame-x-1) 0))
(t/is (= (count result-frame-y-1) 0))
(t/is (= (count result-zero-x-2) 1))
(t/is (= (count result-zero-y-2) 0))
(t/is (= (count result-frame-x-2) 1))
(t/is (= (count result-frame-y-2) 0)))))

View file

@ -1 +1 @@
1.11.0-beta
1.12.0-beta