From 0766938f98a4cc1af5a1fc6a59844a7a52485da5 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Tue, 18 Jan 2022 10:57:51 +0100 Subject: [PATCH 01/12] :sparkles: Add guides UI --- common/src/app/common/colors.cljc | 1 + frontend/src/app/main/data/workspace.cljs | 6 + .../src/app/main/data/workspace/guides.cljs | 70 +++ frontend/src/app/main/ui/workspace.cljs | 34 +- frontend/src/app/main/ui/workspace/rules.cljs | 122 ----- .../src/app/main/ui/workspace/viewport.cljs | 46 +- .../main/ui/workspace/viewport/guides.cljs | 437 ++++++++++++++++++ .../app/main/ui/workspace/viewport/rules.cljs | 137 ++++++ 8 files changed, 693 insertions(+), 160 deletions(-) create mode 100644 frontend/src/app/main/data/workspace/guides.cljs delete mode 100644 frontend/src/app/main/ui/workspace/rules.cljs create mode 100644 frontend/src/app/main/ui/workspace/viewport/guides.cljs create mode 100644 frontend/src/app/main/ui/workspace/viewport/rules.cljs diff --git a/common/src/app/common/colors.cljc b/common/src/app/common/colors.cljc index f8acee0d3..a8db38407 100644 --- a/common/src/app/common/colors.cljc +++ b/common/src/app/common/colors.cljc @@ -15,4 +15,5 @@ (def info "#59B9E2") (def test "#fabada") (def white "#FFFFFF") +(def primary "#31EFB8") diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index d2d08f155..2ed754a81 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -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] @@ -2033,3 +2034,8 @@ ;; Shapes to path (d/export dwps/convert-selected-to-path) + +;; Guides +(d/export dwgu/update-guides) +(d/export dwgu/remove-guide) + diff --git a/frontend/src/app/main/data/workspace/guides.cljs b/frontend/src/app/main/data/workspace/guides.cljs new file mode 100644 index 000000000..612b4adf7 --- /dev/null +++ b/frontend/src/app/main/data/workspace/guides.cljs @@ -0,0 +1,70 @@ +;; 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.main.data.workspace.changes :as dwc] + [app.main.data.workspace.state-helpers :as wsh] + [beicon.core :as rx] + [potok.core :as ptk])) + +(defn make-update-guide [guide] + (fn [other] + (cond-> other + (= (:id other) (:id guide)) + (merge guide)))) + +(defn update-guides [guide] + (ptk/reify ::update-guides + ptk/WatchEvent + (watch [it state _] + (let [page-id (:current-page-id state) + guides (-> state wsh/lookup-page-options (:guides [])) + guides-ids? (into #{} (map :id) guides) + + new-guides + (if (guides-ids? (:id guide)) + ;; Update existing guide + (mapv (make-update-guide guide) guides) + + ;; Add new guide + (conj guides guide)) + + rch [{:type :set-option + :page-id page-id + :option :guides + :value new-guides}] + uch [{:type :set-option + :page-id page-id + :option :guides + :value guides}]] + (rx/of + (dwc/commit-changes + {:redo-changes rch + :undo-changes uch + :origin it})))))) + +(defn remove-guide [guide] + (ptk/reify ::remove-guide + ptk/WatchEvent + (watch [it state _] + (let [page-id (:current-page-id state) + guides (-> state wsh/lookup-page-options (:guides [])) + new-guides (filterv #(not= (:id %) (:id guide)) guides) + + rch [{:type :set-option + :page-id page-id + :option :guides + :value new-guides}] + uch [{:type :set-option + :page-id page-id + :option :guides + :value guides}]] + (rx/of + (dwc/commit-changes + {:redo-changes rch + :undo-changes uch + :origin it})))))) diff --git a/frontend/src/app/main/ui/workspace.cljs b/frontend/src/app/main/ui/workspace.cljs index 43f89e04e..541fb0b84 100644 --- a/frontend/src/app/main/ui/workspace.cljs +++ b/frontend/src/app/main/ui/workspace.cljs @@ -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 diff --git a/frontend/src/app/main/ui/workspace/rules.cljs b/frontend/src/app/main/ui/workspace/rules.cljs deleted file mode 100644 index a9d5b34b2..000000000 --- a/frontend/src/app/main/ui/workspace/rules.cljs +++ /dev/null @@ -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}])) diff --git a/frontend/src/app/main/ui/workspace/viewport.cljs b/frontend/src/app/main/ui/workspace/viewport.cljs index bb1eb3605..015d39e3e 100644 --- a/frontend/src/app/main/ui/workspace/viewport.cljs +++ b/frontend/src/app/main/ui/workspace/viewport.cljs @@ -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,11 @@ (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) + + ;; TODO + show-guides? true] (hooks/setup-dom-events viewport-ref zoom disable-paste in-viewport?) (hooks/setup-viewport-size viewport-ref) @@ -157,6 +170,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 +309,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 +342,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 +352,22 @@ (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}]) + + (when show-guides? + [:& guides/viewport-guides + {:zoom zoom + :vbox vbox + :hover-frame frame-parent}])]]])) diff --git a/frontend/src/app/main/ui/workspace/viewport/guides.cljs b/frontend/src/app/main/ui/workspace/viewport/guides.cljs new file mode 100644 index 000000000..64e846e33 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/viewport/guides.cljs @@ -0,0 +1,437 @@ +;; 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.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.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))] + #_(prn ">>" new-position new-frame-id) + (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)})) + +(mf/defc guide + {::mf/wrap [mf/memo]} + [{:keys [guide hover? on-guide-change get-hover-frame vbox zoom hover-frame]}] + + (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) + + frame (or frame hover-frame) + pos (or (:new-position @state) (:position guide)) + guide-width (/ guide-width zoom) + guide-pill-corner-radius (/ guide-pill-corner-radius zoom)] + + [:g.guide-area + (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) "ew-resize" "ns-resize")} + :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]}] + + (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 + (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) "ew-resize" "ns-resize")}}]) + + (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]}] + + (let [page (mf/deref refs/workspace-page) + guides (->> (get-in page [:options :guides] []) + (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 guides) #(.log js/console (clj->js guides))) + (mf/use-effect + (mf/deps hover-frame) + (fn [] + #_(.log js/console "set" (clj->js hover-frame)) + (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}] + [:& new-guide-area {:vbox vbox :zoom zoom :axis :y :get-hover-frame get-hover-frame}] + + (for [current guides] + [:& guide {:key (str "guide-" (:id current)) + :guide current + :vbox vbox + :zoom zoom + :get-hover-frame get-hover-frame + :on-guide-change on-guide-change}])])) + diff --git a/frontend/src/app/main/ui/workspace/viewport/rules.cljs b/frontend/src/app/main/ui/workspace/viewport/rules.cljs new file mode 100644 index 000000000..6b2145274 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/viewport/rules.cljs @@ -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}]]))) From 64e7cad2927b4bd0ec498029799f4b4dac6f2764 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Thu, 20 Jan 2022 22:20:32 +0100 Subject: [PATCH 02/12] :recycle: Redone the snap calculation and added guides --- common/src/app/common/file_builder.cljc | 28 ++ common/src/app/common/pages/diff.cljc | 170 ++++++++++++ .../src/app/main/data/workspace/guides.cljs | 15 +- frontend/src/app/main/snap.cljs | 10 +- .../main/ui/workspace/viewport/guides.cljs | 3 +- frontend/src/app/util/geom/snap_points.cljs | 9 + frontend/src/app/util/snap_data.cljs | 244 ++++++++++++++++++ frontend/src/app/worker/impl.cljs | 9 +- frontend/src/app/worker/selection.cljs | 6 +- frontend/src/app/worker/snaps.cljs | 174 +------------ frontend/test/app/util/snap_data_test.cljs | 243 +++++++++++++++++ 11 files changed, 726 insertions(+), 185 deletions(-) create mode 100644 common/src/app/common/pages/diff.cljc create mode 100644 frontend/src/app/util/snap_data.cljs create mode 100644 frontend/test/app/util/snap_data_test.cljs diff --git a/common/src/app/common/file_builder.cljc b/common/src/app/common/file_builder.cljc index 0d6c96ff4..b323f51d9 100644 --- a/common/src/app/common/file_builder.cljc +++ b/common/src/app/common/file_builder.cljc @@ -568,4 +568,32 @@ (dissoc :current-component-id) (update :parent-stack pop)))) +(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)] + (commit-change + file + {:type :set-option + :page-id page-id + :option :guides + :value new-guides}))) + +(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 get-current-page + [file] + (let [page-id (:current-page-id file)] + (-> file (get-in [:data :pages-index page-id])))) diff --git a/common/src/app/common/pages/diff.cljc b/common/src/app/common/pages/diff.cljc new file mode 100644 index 000000000..46ba2f46b --- /dev/null +++ b/common/src/app/common/pages/diff.cljc @@ -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})) diff --git a/frontend/src/app/main/data/workspace/guides.cljs b/frontend/src/app/main/data/workspace/guides.cljs index 612b4adf7..45502688a 100644 --- a/frontend/src/app/main/data/workspace/guides.cljs +++ b/frontend/src/app/main/data/workspace/guides.cljs @@ -22,16 +22,9 @@ ptk/WatchEvent (watch [it state _] (let [page-id (:current-page-id state) - guides (-> state wsh/lookup-page-options (:guides [])) - guides-ids? (into #{} (map :id) guides) + guides (-> state wsh/lookup-page-options (:guides {})) - new-guides - (if (guides-ids? (:id guide)) - ;; Update existing guide - (mapv (make-update-guide guide) guides) - - ;; Add new guide - (conj guides guide)) + new-guides (assoc guides (:id guide) guide) rch [{:type :set-option :page-id page-id @@ -52,8 +45,8 @@ ptk/WatchEvent (watch [it state _] (let [page-id (:current-page-id state) - guides (-> state wsh/lookup-page-options (:guides [])) - new-guides (filterv #(not= (:id %) (:id guide)) guides) + guides (-> state wsh/lookup-page-options (:guides {})) + new-guides (dissoc guides (:id guide)) rch [{:type :set-option :page-id page-id diff --git a/frontend/src/app/main/snap.cljs b/frontend/src/app/main/snap.cljs index 41bd7ae79..8fa0dc6ec 100644 --- a/frontend/src/app/main/snap.cljs +++ b/frontend/src/app/main/snap.cljs @@ -19,7 +19,7 @@ [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) @@ -27,12 +27,12 @@ [remove-id?] (fn [query-result] (->> query-result - (map (fn [[value data]] [value (remove (comp remove-id? second) data)])) + (map (fn [[value data]] [value (remove (comp remove-id? :id) data)])) (filter (fn [[_ data]] (seq data)))))) (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 @@ -62,7 +62,7 @@ (->> (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)) @@ -78,7 +78,7 @@ (->> (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)) diff --git a/frontend/src/app/main/ui/workspace/viewport/guides.cljs b/frontend/src/app/main/ui/workspace/viewport/guides.cljs index 64e846e33..1251a71e1 100644 --- a/frontend/src/app/main/ui/workspace/viewport/guides.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/guides.cljs @@ -396,7 +396,8 @@ [{:keys [zoom vbox hover-frame]}] (let [page (mf/deref refs/workspace-page) - guides (->> (get-in page [:options :guides] []) + guides (->> (get-in page [:options :guides] {}) + (vals) (filter (guide-inside-vbox? vbox))) hover-frame-ref (mf/use-ref nil) diff --git a/frontend/src/app/util/geom/snap_points.cljs b/frontend/src/app/util/geom/snap_points.cljs index 669a29113..04ce1ebbf 100644 --- a/frontend/src/app/util/geom/snap_points.cljs +++ b/frontend/src/app/util/geom/snap_points.cljs @@ -28,3 +28,12 @@ (case (:type shape) :frame (-> shape :selrect frame-snap-points) (into #{(gsh/center-shape shape)} (:points shape))))) + +(defn guide-snap-points + [guide] + + ;; TODO: The line will be displayed from the position to the axis. Maybe + ;; revisit this + (if (= :x (:axis guide)) + #{(gpt/point (:position guide) 0)} + #{(gpt/point 0 (:position guide))})) diff --git a/frontend/src/app/util/snap_data.cljs b/frontend/src/app/util/snap_data.cljs new file mode 100644 index 000000000..ed5427234 --- /dev/null +++ b/frontend/src/app/util/snap_data.cljs @@ -0,0 +1,244 @@ +;; 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 + [shape-data axis] + (fn [tree] + (let [tree (or tree (rt/make-tree))] + (as-> tree $ + (reduce (fn [tree data] + (rt/insert tree (get-in data [:pt axis]) data)) + $ shape-data))))) + +(defn make-delete-tree-data + [shape-data axis] + (fn [tree] + (let [tree (or tree (rt/make-tree))] + (as-> tree $ + (reduce (fn [tree data] + (rt/remove tree (get-in data [:pt axis]) data)) + $ 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) + (map #(hash-map :type :shape + :id frame-id + :pt %))) + + grid-x-data (->> (gg/grid-snap-points frame :x) + (map #(hash-map :type :grid-x + :id frame-id + :pt %))) + + grid-y-data (->> (gg/grid-snap-points frame :y) + (map #(hash-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 #(hash-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 #(hash-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 + [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)))) diff --git a/frontend/src/app/worker/impl.cljs b/frontend/src/app/worker/impl.cljs index 13b7d167f..977c8d85e 100644 --- a/frontend/src/app/worker/impl.cljs +++ b/frontend/src/app/worker/impl.cljs @@ -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 diff --git a/frontend/src/app/worker/selection.cljs b/frontend/src/app/worker/selection.cljs index 3b9d92291..d0b229034 100644 --- a/frontend/src/app/worker/selection.cljs +++ b/frontend/src/app/worker/selection.cljs @@ -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)] diff --git a/frontend/src/app/worker/snaps.cljs b/frontend/src/app/worker/snaps.cljs index 66a4dff83..b992b51e7 100644 --- a/frontend/src/app/worker/snaps.cljs +++ b/frontend/src/app/worker/snaps.cljs @@ -6,179 +6,31 @@ (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}] + (->> ranges + (mapcat #(sd/query @state page-id frame-id axis %)) + (set) ;; unique + (into []))) diff --git a/frontend/test/app/util/snap_data_test.cljs b/frontend/test/app/util/snap_data_test.cljs new file mode 100644 index 000000000..ff09693a4 --- /dev/null +++ b/frontend/test/app/util/snap_data_test.cljs @@ -0,0 +1,243 @@ +;; 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) + + ;; frame-id (:last-id 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 (-> 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 shape inside frame, then remove it") + (t/testing "Create guide then remove it") + + (t/testing "Update frame coordinates") + (t/testing "Update shape coordinates") + (t/testing "Update shape inside frame coordinates") + (t/testing "Update global guide") + (t/testing "Update frame guide") + + (t/testing "Change shape frame") + (t/testing "Change guide frame")) From d356a3fa5661eb3b32557c9e7dd4e9bc7c5f292f Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Thu, 20 Jan 2022 22:28:57 +0100 Subject: [PATCH 03/12] :sparkles: Spec definition for guides --- common/src/app/common/types/page_options.cljc | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/common/src/app/common/types/page_options.cljc b/common/src/app/common/types/page_options.cljc index 4901b8722..f7927363f 100644 --- a/common/src/app/common/types/page_options.cljc +++ b/common/src/app/common/types/page_options.cljc @@ -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? ::shape)) + ;; --- Options (s/def ::options (s/keys :opt-un [::background ::saved-grids - ::flows])) + ::flows + ::guides])) ;; --- Helpers for flow From f303d7b33e4277c108f84993213161c795ad15d0 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Fri, 21 Jan 2022 13:52:21 +0100 Subject: [PATCH 04/12] :sparkles: Add support to export/import guides --- common/src/app/common/types/page_options.cljc | 2 +- frontend/src/app/main/data/workspace.cljs | 6 +- frontend/src/app/main/render.cljs | 4 +- frontend/src/app/main/snap.cljs | 54 ++++++----- frontend/src/app/main/ui/shapes/export.cljs | 30 ++++-- .../src/app/main/ui/workspace/viewport.cljs | 19 ++-- .../main/ui/workspace/viewport/guides.cljs | 91 +++++++++++-------- .../ui/workspace/viewport/snap_points.cljs | 37 ++++---- frontend/src/app/util/import/parser.cljs | 22 ++++- frontend/src/app/worker/import.cljs | 9 +- 10 files changed, 165 insertions(+), 109 deletions(-) diff --git a/common/src/app/common/types/page_options.cljc b/common/src/app/common/types/page_options.cljc index f7927363f..687e98985 100644 --- a/common/src/app/common/types/page_options.cljc +++ b/common/src/app/common/types/page_options.cljc @@ -75,7 +75,7 @@ :opt-un [:guides/frame-id])) (s/def ::guides - (s/map-of uuid? ::shape)) + (s/map-of uuid? ::guide)) ;; --- Options diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index 2ed754a81..6af0751cb 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -84,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)) @@ -96,7 +97,8 @@ :display-grid :snap-grid :dynamic-alignment - :display-artboard-names}) + :display-artboard-names + :snap-guides}) (def layout-presets {:assets diff --git a/frontend/src/app/main/render.cljs b/frontend/src/app/main/render.cljs index 26a197e5a..91d6dd72d 100644 --- a/frontend/src/app/main/render.cljs +++ b/frontend/src/app/main/render.cljs @@ -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)] diff --git a/frontend/src/app/main/snap.cljs b/frontend/src/app/main/snap.cljs index 8fa0dc6ec..1de5b1aab 100644 --- a/frontend/src/app/main/snap.cljs +++ b/frontend/src/app/main/snap.cljs @@ -24,12 +24,30 @@ (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? :id) 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 :pt data)) query-result)) @@ -57,7 +75,7 @@ ;; 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 @@ -65,11 +83,11 @@ :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) @@ -81,7 +99,7 @@ :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) diff --git a/frontend/src/app/main/ui/shapes/export.cljs b/frontend/src/app/main/ui/shapes/export.cljs index 1ea58e5ed..efce856f6 100644 --- a/frontend/src/app/main/ui/shapes/export.cljs +++ b/frontend/src/app/main/ui/shapes/export.cljs @@ -155,20 +155,30 @@ :name name :starting-frame starting-frame}])]) +(mf/defc export-guides + [{:keys [guides]}] + [:> "penpot:guides" #js {} + (for [{:keys [id 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 diff --git a/frontend/src/app/main/ui/workspace/viewport.cljs b/frontend/src/app/main/ui/workspace/viewport.cljs index 015d39e3e..41a1a04e2 100644 --- a/frontend/src/app/main/ui/workspace/viewport.cljs +++ b/frontend/src/app/main/ui/workspace/viewport.cljs @@ -158,7 +158,7 @@ show-rules? (contains? layout :rules) ;; TODO - show-guides? true] + disabled-guides? (or drawing-tool transform)] (hooks/setup-dom-events viewport-ref zoom disable-paste in-viewport?) (hooks/setup-viewport-size viewport-ref) @@ -361,13 +361,14 @@ [:& widgets/viewport-actions] (when show-rules? - [:& rules/rules - {:zoom zoom - :vbox vbox}]) + [:* + [:& rules/rules + {:zoom zoom + :vbox vbox}] - (when show-guides? - [:& guides/viewport-guides - {:zoom zoom - :vbox vbox - :hover-frame frame-parent}])]]])) + [:& guides/viewport-guides + {:zoom zoom + :vbox vbox + :hover-frame frame-parent + :disabled-guides? disabled-guides?}]])]]])) diff --git a/frontend/src/app/main/ui/workspace/viewport/guides.cljs b/frontend/src/app/main/ui/workspace/viewport/guides.cljs index 1251a71e1..8c0c7f84b 100644 --- a/frontend/src/app/main/ui/workspace/viewport/guides.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/guides.cljs @@ -234,7 +234,7 @@ (mf/defc guide {::mf/wrap [mf/memo]} - [{:keys [guide hover? on-guide-change get-hover-frame vbox zoom hover-frame]}] + [{:keys [guide hover? on-guide-change get-hover-frame vbox zoom hover-frame disabled-guides?]}] (let [axis (:axis guide) @@ -260,20 +260,21 @@ guide-pill-corner-radius (/ guide-pill-corner-radius zoom)] [:g.guide-area - (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) "ew-resize" "ns-resize")} - :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}]) + (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) "ew-resize" "ns-resize")} + :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 @@ -345,7 +346,7 @@ (str (mth/round pos))]]))])) (mf/defc new-guide-area - [{:keys [vbox zoom axis get-hover-frame]}] + [{:keys [vbox zoom axis get-hover-frame disabled-guides?]}] (let [on-guide-change (mf/use-callback @@ -367,20 +368,21 @@ frame]} (use-guide on-guide-change get-hover-frame zoom {:axis axis})] [:g.new-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) "ew-resize" "ns-resize")}}]) + (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) "ew-resize" "ns-resize")}}])) (when (:new-position @state) [:& guide {:guide {:axis axis @@ -393,12 +395,15 @@ (mf/defc viewport-guides {::mf/wrap [mf/memo]} - [{:keys [zoom vbox hover-frame]}] + [{:keys [zoom vbox hover-frame disabled-guides?]}] (let [page (mf/deref refs/workspace-page) - guides (->> (get-in page [:options :guides] {}) - (vals) - (filter (guide-inside-vbox? vbox))) + + 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) @@ -417,16 +422,23 @@ (st/emit! (dw/update-guides guide)) (st/emit! (dw/remove-guide guide)))))] - #_(mf/use-effect (mf/deps guides) #(.log js/console (clj->js guides))) (mf/use-effect (mf/deps hover-frame) (fn [] - #_(.log js/console "set" (clj->js hover-frame)) (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}] - [:& new-guide-area {:vbox vbox :zoom zoom :axis :y :get-hover-frame get-hover-frame}] + [:& 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)) @@ -434,5 +446,6 @@ :vbox vbox :zoom zoom :get-hover-frame get-hover-frame - :on-guide-change on-guide-change}])])) + :on-guide-change on-guide-change + :disabled-guides? disabled-guides?}])])) diff --git a/frontend/src/app/main/ui/workspace/viewport/snap_points.cljs b/frontend/src/app/main/ui/workspace/viewport/snap_points.cljs index ec95528ee..ee807765f 100644 --- a/frontend/src/app/main/ui/workspace/viewport/snap_points.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/snap_points.cljs @@ -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,26 @@ {::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 + (->> selected + (map #(get objects %)) + (filterv (comp not nil?))) - 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}]))) diff --git a/frontend/src/app/util/import/parser.cljs b/frontend/src/app/util/import/parser.cljs index dec08ba7c..35f9e74f0 100644 --- a/frontend/src/app/util/import/parser.cljs +++ b/frontend/src/app/util/import/parser.cljs @@ -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] diff --git a/frontend/src/app/worker/import.cljs b/frontend/src/app/worker/import.cljs index 983e5d83d..e6bdb5d49 100644 --- a/frontend/src/app/worker/import.cljs +++ b/frontend/src/app/worker/import.cljs @@ -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?) From f0fd1bb40ca172c9e77cac980ac17e16494c04e3 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Fri, 21 Jan 2022 14:04:20 +0100 Subject: [PATCH 05/12] :sparkles: Add menu option for guides --- .../app/main/data/workspace/shortcuts.cljs | 4 ++++ .../src/app/main/ui/workspace/header.cljs | 20 ++++++++++++------- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/frontend/src/app/main/data/workspace/shortcuts.cljs b/frontend/src/app/main/data/workspace/shortcuts.cljs index 939f9e26e..7c6cc2212 100644 --- a/frontend/src/app/main/data/workspace/shortcuts.cljs +++ b/frontend/src/app/main/data/workspace/shortcuts.cljs @@ -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))} diff --git a/frontend/src/app/main/ui/workspace/header.cljs b/frontend/src/app/main/ui/workspace/header.cljs index 6a7c5aaab..f400f2f7b 100644 --- a/frontend/src/app/main/ui/workspace/header.cljs +++ b/frontend/src/app/main/ui/workspace/header.cljs @@ -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 From 3f89baa1fec6597b4f1b74c3a5408f6e4b210c5f Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Fri, 21 Jan 2022 15:49:22 +0100 Subject: [PATCH 06/12] :sparkles: Move guides together with frames --- .../src/app/main/data/workspace/guides.cljs | 37 +++++++++++++++++++ .../app/main/data/workspace/transforms.cljs | 4 ++ .../main/ui/workspace/viewport/guides.cljs | 2 +- .../app/main/ui/workspace/viewport/utils.cljs | 11 ++++-- frontend/src/app/util/dom.cljs | 17 +++++++-- 5 files changed, 63 insertions(+), 8 deletions(-) diff --git a/frontend/src/app/main/data/workspace/guides.cljs b/frontend/src/app/main/data/workspace/guides.cljs index 45502688a..a5f4d44ca 100644 --- a/frontend/src/app/main/data/workspace/guides.cljs +++ b/frontend/src/app/main/data/workspace/guides.cljs @@ -6,9 +6,11 @@ (ns app.main.data.workspace.guides (:require + [app.common.spec :as us] [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] @@ -18,6 +20,7 @@ (merge guide)))) (defn update-guides [guide] + ;; TODO CHECK SPEC (ptk/reify ::update-guides ptk/WatchEvent (watch [it state _] @@ -41,6 +44,7 @@ :origin it})))))) (defn remove-guide [guide] + ;; TODO CHECK SPEC (ptk/reify ::remove-guide ptk/WatchEvent (watch [it state _] @@ -61,3 +65,36 @@ {:redo-changes rch :undo-changes uch :origin it})))))) + +(defn move-frame-guides + [ids] + (us/verify (s/coll-of uuid?) ids) + + (ptk/reify ::move-frame-guides + ptk/WatchEvent + + (watch [_ state _] + (let [objects (wsh/lookup-page-objects state) + frame-ids (->> ids (filter #(= :frame (get-in objects [% :type]))) (into #{})) + object-modifiers (get state :workspace-modifiers) + + moved-guide? + (fn [guide] + (let [frame-id (:frame-id guide)] + (and (contains? frame-ids frame-id) + (some? (get-in object-modifiers [frame-id :modifiers :displacement]))))) + + build-move-event + (fn [guide] + (let [disp (get-in object-modifiers [(:frame-id guide) :modifiers :displacement]) + guide (if (= :x (:axis guide)) + (update guide :position + (:e disp)) + (update guide :position + (:f disp)))] + (update-guides guide)))] + + (->> (wsh/lookup-page-options state) + :guides + (vals) + (filter moved-guide?) + (map build-move-event) + (rx/from)))))) diff --git a/frontend/src/app/main/data/workspace/transforms.cljs b/frontend/src/app/main/data/workspace/transforms.cljs index 94c3c3af0..51ee42708 100644 --- a/frontend/src/app/main/data/workspace/transforms.cljs +++ b/frontend/src/app/main/data/workspace/transforms.cljs @@ -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) @@ -186,6 +189,7 @@ :rotation :flip-x :flip-y]}) + (dwg/move-frame-guides ids-with-children) (clear-local-transform) (dwu/commit-undo-transaction)))))) diff --git a/frontend/src/app/main/ui/workspace/viewport/guides.cljs b/frontend/src/app/main/ui/workspace/viewport/guides.cljs index 8c0c7f84b..854d4d430 100644 --- a/frontend/src/app/main/ui/workspace/viewport/guides.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/guides.cljs @@ -259,7 +259,7 @@ guide-width (/ guide-width zoom) guide-pill-corner-radius (/ guide-pill-corner-radius zoom)] - [:g.guide-area + [:g.guide-area {:data-guide-frame-id (when (some? frame) (str (:id frame)))} (when-not disabled-guides? (let [{:keys [x y width height]} (guide-area-axis pos vbox zoom frame axis)] [:rect {:x x diff --git a/frontend/src/app/main/ui/workspace/viewport/utils.cljs b/frontend/src/app/main/ui/workspace/viewport/utils.cljs index 088d17549..5c9e3334d 100644 --- a/frontend/src/app/main/ui/workspace/viewport/utils.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/utils.cljs @@ -85,13 +85,16 @@ mask? (and group? masked-group?) ;; When the shape is a frame we maybe need to move its thumbnail - thumb-node (when frame? (dom/get-element (str "thumbnail-" id)))] + thumb-node (when frame? (dom/get-element (str "thumbnail-" id))) + + ] (cond frame? - [thumb-node - (dom/query shape-node ".frame-background") - (dom/query shape-node ".frame-clip")] + (into [thumb-node + (dom/query shape-node ".frame-background") + (dom/query shape-node ".frame-clip")] + (dom/query-all (str "[data-guide-frame-id='" id "']"))) ;; For groups we don't want to transform the whole group but only ;; its filters/masks diff --git a/frontend/src/app/util/dom.cljs b/frontend/src/app/util/dom.cljs index 37e75d1a9..e6505c51a 100644 --- a/frontend/src/app/util/dom.cljs +++ b/frontend/src/app/util/dom.cljs @@ -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] From 62f7323acf5349844f2fc5a28e34e3fa438890e2 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Mon, 24 Jan 2022 11:46:04 +0100 Subject: [PATCH 07/12] :sparkles: Move frames with guides move the guides --- .../src/app/main/data/workspace/guides.cljs | 29 +-- .../app/main/data/workspace/transforms.cljs | 2 +- frontend/src/app/main/ui/shapes/export.cljs | 2 +- .../src/app/main/ui/workspace/viewport.cljs | 1 + .../main/ui/workspace/viewport/guides.cljs | 193 ++++++++++-------- 5 files changed, 122 insertions(+), 105 deletions(-) diff --git a/frontend/src/app/main/data/workspace/guides.cljs b/frontend/src/app/main/data/workspace/guides.cljs index a5f4d44ca..703ae08d4 100644 --- a/frontend/src/app/main/data/workspace/guides.cljs +++ b/frontend/src/app/main/data/workspace/guides.cljs @@ -6,7 +6,10 @@ (ns app.main.data.workspace.guides (:require + [app.common.geom.point :as gpt] + [app.common.geom.shapes :as gsh] [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] @@ -20,7 +23,7 @@ (merge guide)))) (defn update-guides [guide] - ;; TODO CHECK SPEC + (us/verify ::tpo/guide guide) (ptk/reify ::update-guides ptk/WatchEvent (watch [it state _] @@ -44,7 +47,7 @@ :origin it})))))) (defn remove-guide [guide] - ;; TODO CHECK SPEC + (us/verify ::tpo/guide guide) (ptk/reify ::remove-guide ptk/WatchEvent (watch [it state _] @@ -75,26 +78,24 @@ (watch [_ state _] (let [objects (wsh/lookup-page-objects state) - frame-ids (->> ids (filter #(= :frame (get-in objects [% :type]))) (into #{})) + frame-ids? (->> ids (filter #(= :frame (get-in objects [% :type]))) (into #{})) object-modifiers (get state :workspace-modifiers) - moved-guide? - (fn [guide] - (let [frame-id (:frame-id guide)] - (and (contains? frame-ids frame-id) - (some? (get-in object-modifiers [frame-id :modifiers :displacement]))))) - build-move-event (fn [guide] - (let [disp (get-in object-modifiers [(:frame-id guide) :modifiers :displacement]) - guide (if (= :x (:axis guide)) - (update guide :position + (:e disp)) - (update guide :position + (:f disp)))] + (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 moved-guide?) + (filter (comp frame-ids? :frame-id)) (map build-move-event) (rx/from)))))) diff --git a/frontend/src/app/main/data/workspace/transforms.cljs b/frontend/src/app/main/data/workspace/transforms.cljs index 51ee42708..5edec0efb 100644 --- a/frontend/src/app/main/data/workspace/transforms.cljs +++ b/frontend/src/app/main/data/workspace/transforms.cljs @@ -171,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] @@ -189,7 +190,6 @@ :rotation :flip-x :flip-y]}) - (dwg/move-frame-guides ids-with-children) (clear-local-transform) (dwu/commit-undo-transaction)))))) diff --git a/frontend/src/app/main/ui/shapes/export.cljs b/frontend/src/app/main/ui/shapes/export.cljs index efce856f6..bde04d442 100644 --- a/frontend/src/app/main/ui/shapes/export.cljs +++ b/frontend/src/app/main/ui/shapes/export.cljs @@ -158,7 +158,7 @@ (mf/defc export-guides [{:keys [guides]}] [:> "penpot:guides" #js {} - (for [{:keys [id position frame-id axis]} (vals guides)] + (for [{:keys [position frame-id axis]} (vals guides)] [:> "penpot:guide" #js {:position position :frame-id frame-id :axis (d/name axis)}])]) diff --git a/frontend/src/app/main/ui/workspace/viewport.cljs b/frontend/src/app/main/ui/workspace/viewport.cljs index 41a1a04e2..ca67ea598 100644 --- a/frontend/src/app/main/ui/workspace/viewport.cljs +++ b/frontend/src/app/main/ui/workspace/viewport.cljs @@ -370,5 +370,6 @@ {:zoom zoom :vbox vbox :hover-frame frame-parent + :modifiers modifiers :disabled-guides? disabled-guides?}]])]]])) diff --git a/frontend/src/app/main/ui/workspace/viewport/guides.cljs b/frontend/src/app/main/ui/workspace/viewport/guides.cljs index 854d4d430..fd5a38918 100644 --- a/frontend/src/app/main/ui/workspace/viewport/guides.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/guides.cljs @@ -7,6 +7,8 @@ (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] @@ -137,11 +139,7 @@ {:x (+ (:x vbox) rules-pos) :y (- pos (/ guide-active-area 2)) :width (:width vbox) - :height guide-active-area} - - - - ))) + :height guide-active-area}))) (defn guide-line-axis ([pos vbox axis] @@ -232,9 +230,19 @@ :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?]}] + [{:keys [guide hover? on-guide-change get-hover-frame vbox zoom hover-frame disabled-guides? frame-modifier]}] (let [axis (:axis guide) @@ -254,96 +262,102 @@ state frame]} (use-guide handle-change-position get-hover-frame zoom guide) - frame (or frame hover-frame) - pos (or (:new-position @state) (:position 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)] - [:g.guide-area {:data-guide-frame-id (when (some? frame) (str (:id frame)))} - (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) "ew-resize" "ns-resize")} - :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}])) + (when (or (nil? frame) (is-guide-inside-frame? (assoc guide :position pos) frame)) + [:g.guide-area {:data-guide-frame-id (when (some? frame) (str (:id frame)))} + (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) "ew-resize" "ns-resize")} + :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 + (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-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"}}])]) + :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)}}])) + (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))]]))])) + (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?]}] @@ -395,7 +409,7 @@ (mf/defc viewport-guides {::mf/wrap [mf/memo]} - [{:keys [zoom vbox hover-frame disabled-guides?]}] + [{:keys [zoom vbox hover-frame disabled-guides? modifiers]}] (let [page (mf/deref refs/workspace-page) @@ -445,6 +459,7 @@ :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?}])])) From 39334b81ace3f7e4bb0dcda80921c7677f21da6f Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Mon, 24 Jan 2022 12:50:28 +0100 Subject: [PATCH 08/12] :sparkles: Guides cursors --- frontend/resources/images/cursors/resize-h-2.svg | 1 + frontend/src/app/main/ui/cursors.cljs | 3 +++ frontend/src/app/main/ui/workspace/viewport/guides.cljs | 9 ++++++--- 3 files changed, 10 insertions(+), 3 deletions(-) create mode 100644 frontend/resources/images/cursors/resize-h-2.svg diff --git a/frontend/resources/images/cursors/resize-h-2.svg b/frontend/resources/images/cursors/resize-h-2.svg new file mode 100644 index 000000000..5522037b1 --- /dev/null +++ b/frontend/resources/images/cursors/resize-h-2.svg @@ -0,0 +1 @@ + diff --git a/frontend/src/app/main/ui/cursors.cljs b/frontend/src/app/main/ui/cursors.cljs index c52be6891..a57575099 100644 --- a/frontend/src/app/main/ui/cursors.cljs +++ b/frontend/src/app/main/ui/cursors.cljs @@ -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} diff --git a/frontend/src/app/main/ui/workspace/viewport/guides.cljs b/frontend/src/app/main/ui/workspace/viewport/guides.cljs index fd5a38918..25afe586c 100644 --- a/frontend/src/app/main/ui/workspace/viewport/guides.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/guides.cljs @@ -15,6 +15,7 @@ [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])) @@ -272,7 +273,9 @@ 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)) + (when (or (nil? frame) + (is-guide-inside-frame? (assoc guide :position pos) frame) + (:hover @state true)) [:g.guide-area {:data-guide-frame-id (when (some? frame) (str (:id frame)))} (when-not disabled-guides? (let [{:keys [x y width height]} (guide-area-axis pos vbox zoom frame axis)] @@ -282,7 +285,7 @@ :height height :style {:fill "none" :pointer-events "fill" - :cursor (if (= axis :x) "ew-resize" "ns-resize")} + :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 @@ -396,7 +399,7 @@ :on-mouse-move on-mouse-move :style {:fill "none" :pointer-events "fill" - :cursor (if (= axis :x) "ew-resize" "ns-resize")}}])) + :cursor (if (= axis :x) (cur/resize-ew 0) (cur/resize-ns 0))}}])) (when (:new-position @state) [:& guide {:guide {:axis axis From d9b1c0e2e637890af33f63021877efbea538332e Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Mon, 24 Jan 2022 14:49:23 +0100 Subject: [PATCH 09/12] :sparkles: More tests for snap data --- common/src/app/common/file_builder.cljc | 78 ++++++-- frontend/src/app/util/snap_data.cljs | 3 +- frontend/test/app/util/snap_data_test.cljs | 212 +++++++++++++++++++-- 3 files changed, 263 insertions(+), 30 deletions(-) diff --git a/common/src/app/common/file_builder.cljc b/common/src/app/common/file_builder.cljc index b323f51d9..e2dd9f30e 100644 --- a/common/src/app/common/file_builder.cljc +++ b/common/src/app/common/file_builder.cljc @@ -568,22 +568,6 @@ (dissoc :current-component-id) (update :parent-stack pop)))) -(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)] - (commit-change - file - {:type :set-option - :page-id page-id - :option :guides - :value new-guides}))) - (defn delete-object [file id] (let [page-id (:current-page-id file)] @@ -593,7 +577,69 @@ :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})))) diff --git a/frontend/src/app/util/snap_data.cljs b/frontend/src/app/util/snap_data.cljs index ed5427234..1874f7793 100644 --- a/frontend/src/app/util/snap_data.cljs +++ b/frontend/src/app/util/snap_data.cljs @@ -102,7 +102,6 @@ :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 @@ -111,7 +110,7 @@ ;; Guide outside the frame. We add the information in the global guides data (-> page-data - (assoc-in [:guides :objects-data (:id guide)] [guide-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 diff --git a/frontend/test/app/util/snap_data_test.cljs b/frontend/test/app/util/snap_data_test.cljs index ff09693a4..6982d4f6f 100644 --- a/frontend/test/app/util/snap_data_test.cljs +++ b/frontend/test/app/util/snap_data_test.cljs @@ -37,7 +37,6 @@ :height 100})) page (fb/get-current-page file) - ;; frame-id (:last-id file) data (-> (sd/make-snap-data) (sd/add-page page)) @@ -217,8 +216,7 @@ data (-> (sd/make-snap-data) (sd/add-page page)) - file (-> file - (fb/delete-object shape-id)) + file (fb/delete-object file shape-id) new-page (fb/get-current-page file) data (sd/update-page data page new-page) @@ -230,14 +228,204 @@ (t/is (= (count result-x) 0)) (t/is (= (count result-y) 0)))) - (t/testing "Create shape inside frame, then remove it") - (t/testing "Create guide then remove it") + (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) - (t/testing "Update frame coordinates") - (t/testing "Update shape coordinates") - (t/testing "Update shape inside frame coordinates") - (t/testing "Update global guide") - (t/testing "Update frame guide") + file (fb/create-rect file {:x 25 :y 25 :width 50 :height 50}) + shape-id (:last-id file) - (t/testing "Change shape frame") - (t/testing "Change guide frame")) + 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))))) From 663358bdae34a66e50f9e2389f158401bc5b4d3a Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Mon, 24 Jan 2022 14:53:47 +0100 Subject: [PATCH 10/12] :books: Update changelog --- CHANGES.md | 12 +++++++++--- version.txt | 2 +- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index d6c0fa10f..634e6a503 100644 --- a/CHANGES.md +++ b/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). diff --git a/version.txt b/version.txt index 4beb19d04..08c36205d 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -1.11.0-beta +1.12.0-beta From 8833e19c7fbe9447db422e94ded99580e58dd6fb Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Mon, 24 Jan 2022 15:04:36 +0100 Subject: [PATCH 11/12] :bug: Small fixes for guides --- .../src/app/main/data/workspace/guides.cljs | 2 +- .../src/app/main/ui/workspace/viewport.cljs | 1 - .../main/ui/workspace/viewport/guides.cljs | 3 +-- .../app/main/ui/workspace/viewport/utils.cljs | 11 +++----- frontend/src/app/util/geom/snap_points.cljs | 3 --- frontend/src/app/util/snap_data.cljs | 26 ++++++++++--------- 6 files changed, 20 insertions(+), 26 deletions(-) diff --git a/frontend/src/app/main/data/workspace/guides.cljs b/frontend/src/app/main/data/workspace/guides.cljs index 703ae08d4..ba614eebd 100644 --- a/frontend/src/app/main/data/workspace/guides.cljs +++ b/frontend/src/app/main/data/workspace/guides.cljs @@ -70,12 +70,12 @@ :origin it})))))) (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) frame-ids? (->> ids (filter #(= :frame (get-in objects [% :type]))) (into #{})) diff --git a/frontend/src/app/main/ui/workspace/viewport.cljs b/frontend/src/app/main/ui/workspace/viewport.cljs index ca67ea598..2ca92bb7f 100644 --- a/frontend/src/app/main/ui/workspace/viewport.cljs +++ b/frontend/src/app/main/ui/workspace/viewport.cljs @@ -157,7 +157,6 @@ show-artboard-names? (contains? layout :display-artboard-names) show-rules? (contains? layout :rules) - ;; TODO disabled-guides? (or drawing-tool transform)] (hooks/setup-dom-events viewport-ref zoom disable-paste in-viewport?) diff --git a/frontend/src/app/main/ui/workspace/viewport/guides.cljs b/frontend/src/app/main/ui/workspace/viewport/guides.cljs index 25afe586c..f18d913f6 100644 --- a/frontend/src/app/main/ui/workspace/viewport/guides.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/guides.cljs @@ -97,7 +97,6 @@ ;; TODO: Change when pixel-grid flag exists new-position (mth/round new-position) new-frame-id (:id (get-hover-frame))] - #_(prn ">>" new-position new-frame-id) (swap! state assoc :new-position new-position :new-frame-id new-frame-id)))))] @@ -276,7 +275,7 @@ (when (or (nil? frame) (is-guide-inside-frame? (assoc guide :position pos) frame) (:hover @state true)) - [:g.guide-area {:data-guide-frame-id (when (some? frame) (str (:id frame)))} + [:g.guide-area (when-not disabled-guides? (let [{:keys [x y width height]} (guide-area-axis pos vbox zoom frame axis)] [:rect {:x x diff --git a/frontend/src/app/main/ui/workspace/viewport/utils.cljs b/frontend/src/app/main/ui/workspace/viewport/utils.cljs index 5c9e3334d..088d17549 100644 --- a/frontend/src/app/main/ui/workspace/viewport/utils.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/utils.cljs @@ -85,16 +85,13 @@ mask? (and group? masked-group?) ;; When the shape is a frame we maybe need to move its thumbnail - thumb-node (when frame? (dom/get-element (str "thumbnail-" id))) - - ] + thumb-node (when frame? (dom/get-element (str "thumbnail-" id)))] (cond frame? - (into [thumb-node - (dom/query shape-node ".frame-background") - (dom/query shape-node ".frame-clip")] - (dom/query-all (str "[data-guide-frame-id='" id "']"))) + [thumb-node + (dom/query shape-node ".frame-background") + (dom/query shape-node ".frame-clip")] ;; For groups we don't want to transform the whole group but only ;; its filters/masks diff --git a/frontend/src/app/util/geom/snap_points.cljs b/frontend/src/app/util/geom/snap_points.cljs index 04ce1ebbf..8fccf5bf3 100644 --- a/frontend/src/app/util/geom/snap_points.cljs +++ b/frontend/src/app/util/geom/snap_points.cljs @@ -31,9 +31,6 @@ (defn guide-snap-points [guide] - - ;; TODO: The line will be displayed from the position to the axis. Maybe - ;; revisit this (if (= :x (:axis guide)) #{(gpt/point (:position guide) 0)} #{(gpt/point 0 (:position guide))})) diff --git a/frontend/src/app/util/snap_data.cljs b/frontend/src/app/util/snap_data.cljs index 1874f7793..9a58cd1b2 100644 --- a/frontend/src/app/util/snap_data.cljs +++ b/frontend/src/app/util/snap_data.cljs @@ -21,7 +21,7 @@ ;; PRIVATE FUNCTIONS -(defn make-insert-tree-data +(defn- make-insert-tree-data [shape-data axis] (fn [tree] (let [tree (or tree (rt/make-tree))] @@ -30,7 +30,7 @@ (rt/insert tree (get-in data [:pt axis]) data)) $ shape-data))))) -(defn make-delete-tree-data +(defn- make-delete-tree-data [shape-data axis] (fn [tree] (let [tree (or tree (rt/make-tree))] @@ -39,7 +39,7 @@ (rt/remove tree (get-in data [:pt axis]) data)) $ shape-data))))) -(defn add-root-frame +(defn- add-root-frame [page-data] (let [frame-id uuid/zero] @@ -47,7 +47,7 @@ (assoc-in [frame-id :x] (rt/make-tree)) (assoc-in [frame-id :y] (rt/make-tree))))) -(defn add-frame +(defn- add-frame [page-data frame] (let [frame-id (:id frame) parent-id (:parent-id frame) @@ -79,7 +79,7 @@ (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 +(defn- add-shape [page-data shape] (let [frame-id (:frame-id shape) snap-points (snap/shape-snap-points shape) @@ -94,7 +94,7 @@ (update-in [frame-id :y] (make-insert-tree-data shape-data :y))))) -(defn add-guide +(defn- add-guide [page-data guide] (let [guide-data (->> (snap/guide-snap-points guide) @@ -113,7 +113,7 @@ (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 +(defn- remove-frame [page-data frame] (let [frame-id (:id frame) root-data (get-in page-data [uuid/zero :objects-data frame-id])] @@ -123,7 +123,7 @@ (update-in [uuid/zero :y] (make-delete-tree-data root-data :y)) (dissoc frame-id)))) -(defn remove-shape +(defn- remove-shape [page-data shape] (let [frame-id (:frame-id shape) @@ -133,7 +133,7 @@ (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 +(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)])] @@ -147,7 +147,7 @@ (d/dissoc-in [:guides :objects-data (:id guide)]) (update-in [:guides (:axis guide)] (make-delete-tree-data guide-data (:axis guide))))))) -(defn update-frame +(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]) @@ -159,19 +159,20 @@ (update-in [frame-id :y] (make-delete-tree-data frame-data :y)) (add-frame new-frame)))) -(defn update-shape +(defn- update-shape [page-data [old-shape new-shape]] (-> page-data (remove-shape old-shape) (add-shape new-shape))) -(defn update-guide +(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" [] @@ -231,6 +232,7 @@ (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 From 6e0433a34bfd90052c1573c8930c17f8eaaeaa74 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Tue, 25 Jan 2022 14:48:54 +0100 Subject: [PATCH 12/12] :sparkles: Review changes --- .../src/app/common/pages/changes_builder.cljc | 38 ++++++++++--- .../src/app/main/data/workspace/guides.cljs | 55 +++++++------------ .../main/data/workspace/state_helpers.cljs | 6 ++ .../ui/workspace/viewport/snap_points.cljs | 5 +- frontend/src/app/util/snap_data.cljs | 48 +++++++++------- frontend/src/app/worker/snaps.cljs | 9 +-- 6 files changed, 91 insertions(+), 70 deletions(-) diff --git a/common/src/app/common/pages/changes_builder.cljc b/common/src/app/common/pages/changes_builder.cljc index d39374224..81e913975 100644 --- a/common/src/app/common/pages/changes_builder.cljc +++ b/common/src/app/common/pages/changes_builder.cljc @@ -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})))) diff --git a/frontend/src/app/main/data/workspace/guides.cljs b/frontend/src/app/main/data/workspace/guides.cljs index ba614eebd..c446d8876 100644 --- a/frontend/src/app/main/data/workspace/guides.cljs +++ b/frontend/src/app/main/data/workspace/guides.cljs @@ -8,6 +8,7 @@ (: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] @@ -27,47 +28,30 @@ (ptk/reify ::update-guides ptk/WatchEvent (watch [it state _] - (let [page-id (:current-page-id state) - guides (-> state wsh/lookup-page-options (:guides {})) - + (let [page (wsh/lookup-page state) + guides (get-in page [:options :guides] {}) new-guides (assoc guides (:id guide) guide) - - rch [{:type :set-option - :page-id page-id - :option :guides - :value new-guides}] - uch [{:type :set-option - :page-id page-id - :option :guides - :value guides}]] - (rx/of - (dwc/commit-changes - {:redo-changes rch - :undo-changes uch - :origin it})))))) + + 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-id (:current-page-id state) - guides (-> state wsh/lookup-page-options (:guides {})) + (let [page (wsh/lookup-page state) + guides (get-in page [:options :guides] {}) new-guides (dissoc guides (:id guide)) - - rch [{:type :set-option - :page-id page-id - :option :guides - :value new-guides}] - uch [{:type :set-option - :page-id page-id - :option :guides - :value guides}]] - (rx/of - (dwc/commit-changes - {:redo-changes rch - :undo-changes uch - :origin it})))))) + + 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" @@ -78,7 +62,10 @@ ptk/WatchEvent (watch [_ state _] (let [objects (wsh/lookup-page-objects state) - frame-ids? (->> ids (filter #(= :frame (get-in objects [% :type]))) (into #{})) + + 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 diff --git a/frontend/src/app/main/data/workspace/state_helpers.cljs b/frontend/src/app/main/data/workspace/state_helpers.cljs index f144670ba..661b272ad 100644 --- a/frontend/src/app/main/data/workspace/state_helpers.cljs +++ b/frontend/src/app/main/data/workspace/state_helpers.cljs @@ -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))) diff --git a/frontend/src/app/main/ui/workspace/viewport/snap_points.cljs b/frontend/src/app/main/ui/workspace/viewport/snap_points.cljs index ee807765f..d36109911 100644 --- a/frontend/src/app/main/ui/workspace/viewport/snap_points.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/snap_points.cljs @@ -152,10 +152,7 @@ {::mf/wrap [mf/memo]} [{:keys [layout zoom objects selected page-id drawing transform modifiers] :as props}] - (let [shapes - (->> selected - (map #(get objects %)) - (filterv (comp not nil?))) + (let [shapes (into [] (keep (d/getf objects)) selected) filter-shapes (into #{} diff --git a/frontend/src/app/util/snap_data.cljs b/frontend/src/app/util/snap_data.cljs index 9a58cd1b2..6c667c217 100644 --- a/frontend/src/app/util/snap_data.cljs +++ b/frontend/src/app/util/snap_data.cljs @@ -22,22 +22,28 @@ ;; 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))] - (as-> tree $ - (reduce (fn [tree data] - (rt/insert tree (get-in data [:pt axis]) data)) - $ shape-data))))) + (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))] - (as-> tree $ - (reduce (fn [tree data] - (rt/remove tree (get-in data [:pt axis]) data)) - $ shape-data))))) + (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] @@ -52,19 +58,19 @@ (let [frame-id (:id frame) parent-id (:parent-id frame) frame-data (->> (snap/shape-snap-points frame) - (map #(hash-map :type :shape - :id frame-id - :pt %))) + (mapv #(array-map :type :shape + :id frame-id + :pt %))) grid-x-data (->> (gg/grid-snap-points frame :x) - (map #(hash-map :type :grid-x - :id frame-id - :pt %))) + (mapv #(array-map :type :grid-x + :id frame-id + :pt %))) grid-y-data (->> (gg/grid-snap-points frame :y) - (map #(hash-map :type :grid-y - :id frame-id - :pt %)))] + (mapv #(array-map :type :grid-y + :id frame-id + :pt %)))] (-> page-data ;; Update root frame information @@ -84,7 +90,7 @@ (let [frame-id (:frame-id shape) snap-points (snap/shape-snap-points shape) shape-data (->> snap-points - (mapv #(hash-map + (mapv #(array-map :type :shape :id (:id shape) :pt %)))] @@ -98,7 +104,7 @@ [page-data guide] (let [guide-data (->> (snap/guide-snap-points guide) - (mapv #(hash-map + (mapv #(array-map :type :guide :id (:id guide) :pt %)))] diff --git a/frontend/src/app/worker/snaps.cljs b/frontend/src/app/worker/snaps.cljs index b992b51e7..da872c2f7 100644 --- a/frontend/src/app/worker/snaps.cljs +++ b/frontend/src/app/worker/snaps.cljs @@ -28,9 +28,10 @@ (defmethod impl/handler :snaps/range-query [{:keys [page-id frame-id axis ranges] :as message}] - (->> ranges - (mapcat #(sd/query @state page-id frame-id axis %)) - (set) ;; unique - (into []))) + + (into [] + (comp (mapcat #(sd/query @state page-id frame-id axis %)) + (distinct)) + ranges))