mirror of
https://github.com/penpot/penpot.git
synced 2025-06-02 19:31:38 +02:00
commit
a10dcbd918
33 changed files with 1904 additions and 414 deletions
12
CHANGES.md
12
CHANGES.md
|
@ -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).
|
||||
|
|
|
@ -15,4 +15,5 @@
|
|||
(def info "#59B9E2")
|
||||
(def test "#fabada")
|
||||
(def white "#FFFFFF")
|
||||
(def primary "#31EFB8")
|
||||
|
||||
|
|
|
@ -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}))))
|
||||
|
|
|
@ -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}))))
|
||||
|
|
170
common/src/app/common/pages/diff.cljc
Normal file
170
common/src/app/common/pages/diff.cljc
Normal 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}))
|
|
@ -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
|
||||
|
||||
|
|
1
frontend/resources/images/cursors/resize-h-2.svg
Normal file
1
frontend/resources/images/cursors/resize-h-2.svg
Normal 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 |
|
@ -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)
|
||||
|
||||
|
|
88
frontend/src/app/main/data/workspace/guides.cljs
Normal file
88
frontend/src/app/main/data/workspace/guides.cljs
Normal 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))))))
|
|
@ -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))}
|
||||
|
|
|
@ -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)))
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}]))
|
|
@ -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?}]])]]]))
|
||||
|
||||
|
|
468
frontend/src/app/main/ui/workspace/viewport/guides.cljs
Normal file
468
frontend/src/app/main/ui/workspace/viewport/guides.cljs
Normal 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?}])]))
|
||||
|
137
frontend/src/app/main/ui/workspace/viewport/rules.cljs
Normal file
137
frontend/src/app/main/ui/workspace/viewport/rules.cljs
Normal 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}]])))
|
|
@ -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}])))
|
||||
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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))}))
|
||||
|
|
|
@ -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]
|
||||
|
|
251
frontend/src/app/util/snap_data.cljs
Normal file
251
frontend/src/app/util/snap_data.cljs
Normal 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))))
|
|
@ -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
|
||||
|
|
|
@ -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?)
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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))
|
||||
|
||||
|
||||
|
|
431
frontend/test/app/util/snap_data_test.cljs
Normal file
431
frontend/test/app/util/snap_data_test.cljs
Normal 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)))))
|
|
@ -1 +1 @@
|
|||
1.11.0-beta
|
||||
1.12.0-beta
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue