From 0b4b2d3814114cf0715f24395c83bbcad0ca644a Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Fri, 10 Sep 2021 10:48:55 +0200 Subject: [PATCH] :sparkles: Add UI for boolean operations --- .../images/icons/boolean-difference.svg | 3 + .../images/icons/boolean-exclude.svg | 3 + .../images/icons/boolean-intersection.svg | 3 + .../resources/images/icons/boolean-union.svg | 3 + .../main/partials/sidebar-align-options.scss | 2 +- .../styles/main/partials/workspace.scss | 11 +++ frontend/src/app/main/data/workspace.cljs | 3 +- .../src/app/main/data/workspace/booleans.cljs | 1 - .../app/main/data/workspace/shortcuts.cljs | 18 ++++- frontend/src/app/main/ui/icons.cljs | 7 ++ .../app/main/ui/workspace/colorpicker.cljs | 8 +- .../app/main/ui/workspace/context_menu.cljs | 78 ++++++++++++++++--- .../app/main/ui/workspace/sidebar/layers.cljs | 8 +- .../main/ui/workspace/sidebar/options.cljs | 4 +- .../sidebar/{ => options/menus}/align.cljs | 2 +- .../sidebar/options/menus/booleans.cljs | 54 +++++++++++++ frontend/src/app/util/dom.cljs | 2 +- frontend/src/app/util/path/bool.cljs | 9 +-- frontend/translations/en.po | 17 +++- frontend/translations/es.po | 15 ++++ 20 files changed, 220 insertions(+), 31 deletions(-) create mode 100644 frontend/resources/images/icons/boolean-difference.svg create mode 100644 frontend/resources/images/icons/boolean-exclude.svg create mode 100644 frontend/resources/images/icons/boolean-intersection.svg create mode 100644 frontend/resources/images/icons/boolean-union.svg rename frontend/src/app/main/ui/workspace/sidebar/{ => options/menus}/align.cljs (98%) create mode 100644 frontend/src/app/main/ui/workspace/sidebar/options/menus/booleans.cljs diff --git a/frontend/resources/images/icons/boolean-difference.svg b/frontend/resources/images/icons/boolean-difference.svg new file mode 100644 index 000000000..4d5c7f6a8 --- /dev/null +++ b/frontend/resources/images/icons/boolean-difference.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/images/icons/boolean-exclude.svg b/frontend/resources/images/icons/boolean-exclude.svg new file mode 100644 index 000000000..6a3865703 --- /dev/null +++ b/frontend/resources/images/icons/boolean-exclude.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/images/icons/boolean-intersection.svg b/frontend/resources/images/icons/boolean-intersection.svg new file mode 100644 index 000000000..3480e6366 --- /dev/null +++ b/frontend/resources/images/icons/boolean-intersection.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/images/icons/boolean-union.svg b/frontend/resources/images/icons/boolean-union.svg new file mode 100644 index 000000000..fdeb117b7 --- /dev/null +++ b/frontend/resources/images/icons/boolean-union.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/styles/main/partials/sidebar-align-options.scss b/frontend/resources/styles/main/partials/sidebar-align-options.scss index 7b9750426..634c40861 100644 --- a/frontend/resources/styles/main/partials/sidebar-align-options.scss +++ b/frontend/resources/styles/main/partials/sidebar-align-options.scss @@ -14,7 +14,7 @@ .align-group { display: flex; justify-content: space-evenly; - width: 100%; + width: 50%; &:not(:last-child) { border-right: solid 1px $color-gray-60; diff --git a/frontend/resources/styles/main/partials/workspace.scss b/frontend/resources/styles/main/partials/workspace.scss index f167e9814..a6ad2df1e 100644 --- a/frontend/resources/styles/main/partials/workspace.scss +++ b/frontend/resources/styles/main/partials/workspace.scss @@ -47,6 +47,17 @@ &:hover { background-color: $color-primary-lighter; } + + .submenu-icon { + position: absolute; + right: 1rem; + + svg { + width: 10px; + height: 10px; + } + } + } } diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index 476046781..cb055c688 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -49,8 +49,7 @@ [cljs.spec.alpha :as s] [clojure.set :as set] [cuerdas.core :as str] - [potok.core :as ptk] - )) + [potok.core :as ptk])) ;; (log/set-level! :trace) diff --git a/frontend/src/app/main/data/workspace/booleans.cljs b/frontend/src/app/main/data/workspace/booleans.cljs index 6702af71d..d4aa7455a 100644 --- a/frontend/src/app/main/data/workspace/booleans.cljs +++ b/frontend/src/app/main/data/workspace/booleans.cljs @@ -42,7 +42,6 @@ :shapes []} (gsh/setup selrect)))) - (defn create-bool [bool-type] (ptk/reify ::create-bool-union diff --git a/frontend/src/app/main/data/workspace/shortcuts.cljs b/frontend/src/app/main/data/workspace/shortcuts.cljs index 7373d6e76..15dd66f66 100644 --- a/frontend/src/app/main/data/workspace/shortcuts.cljs +++ b/frontend/src/app/main/data/workspace/shortcuts.cljs @@ -261,9 +261,21 @@ :type "keyup" :fn #(st/emit! (dw/toggle-distances-display false))} - :create-union {:tooltip (ds/alt "U") - :command ["alt" "u"] - :fn #(st/emit! (dw/create-bool :union))} + :boolean-union {:tooltip (ds/alt "U") + :command ["alt" "u"] + :fn #(st/emit! (dw/create-bool :union))} + + :boolean-difference {:tooltip (ds/alt "D") + :command ["alt" "d"] + :fn #(st/emit! (dw/create-bool :difference))} + + :boolean-intersection {:tooltip (ds/alt "I") + :command ["alt" "i"] + :fn #(st/emit! (dw/create-bool :intersection))} + + :boolean-exclude {:tooltip (ds/alt "E") + :command ["alt" "e"] + :fn #(st/emit! (dw/create-bool :exclude))} }) diff --git a/frontend/src/app/main/ui/icons.cljs b/frontend/src/app/main/ui/icons.cljs index abf60ef6e..35c5285d2 100644 --- a/frontend/src/app/main/ui/icons.cljs +++ b/frontend/src/app/main/ui/icons.cljs @@ -9,6 +9,8 @@ (:require-macros [app.main.ui.icons :refer [icon-xref]]) (:require [rumext.alpha :as mf])) +;; Keep the list of icons sorted + (def action (icon-xref :action)) (def actions (icon-xref :actions)) (def align-bottom (icon-xref :align-bottom)) @@ -23,6 +25,10 @@ (def auto-fix (icon-xref :auto-fix)) (def auto-height (icon-xref :auto-height)) (def auto-width (icon-xref :auto-width)) +(def boolean-difference (icon-xref :boolean-difference)) +(def boolean-exclude (icon-xref :boolean-exclude)) +(def boolean-intersection (icon-xref :boolean-intersection)) +(def boolean-union (icon-xref :boolean-union)) (def box (icon-xref :box)) (def chain (icon-xref :chain)) (def chat (icon-xref :chat)) @@ -152,6 +158,7 @@ (def uppercase (icon-xref :uppercase)) (def user (icon-xref :user)) + (def loader-pencil (mf/html [:svg diff --git a/frontend/src/app/main/ui/workspace/colorpicker.cljs b/frontend/src/app/main/ui/workspace/colorpicker.cljs index 7e71d5c4a..b239a9185 100644 --- a/frontend/src/app/main/ui/workspace/colorpicker.cljs +++ b/frontend/src/app/main/ui/workspace/colorpicker.cljs @@ -202,10 +202,10 @@ h (str (* s 100) "%") (str (* l 100) "%")))] - (dom/set-css-property node "--color" (str/join ", " rgb)) - (dom/set-css-property node "--hue-rgb" (str/join ", " hue-rgb)) - (dom/set-css-property node "--saturation-grad-from" (format-hsl hsl-from)) - (dom/set-css-property node "--saturation-grad-to" (format-hsl hsl-to))))) + (dom/set-css-property! node "--color" (str/join ", " rgb)) + (dom/set-css-property! node "--hue-rgb" (str/join ", " hue-rgb)) + (dom/set-css-property! node "--saturation-grad-from" (format-hsl hsl-from)) + (dom/set-css-property! node "--saturation-grad-to" (format-hsl hsl-to))))) ;; When closing the modal we update the recent-color list (mf/use-effect diff --git a/frontend/src/app/main/ui/workspace/context_menu.cljs b/frontend/src/app/main/ui/workspace/context_menu.cljs index ed9933297..327864592 100644 --- a/frontend/src/app/main/ui/workspace/context_menu.cljs +++ b/frontend/src/app/main/ui/workspace/context_menu.cljs @@ -16,6 +16,7 @@ [app.main.store :as st] [app.main.ui.components.dropdown :refer [dropdown]] [app.main.ui.context :as ctx] + [app.main.ui.icons :as i] [app.util.dom :as dom] [app.util.i18n :refer [tr] :as i18n] [app.util.timers :as timers] @@ -31,10 +32,53 @@ (dom/stop-propagation event)) (mf/defc menu-entry - [{:keys [title shortcut on-click] :as props}] - [:li {:on-click on-click} - [:span.title title] - [:span.shortcut (or shortcut "")]]) + [{:keys [title shortcut submenu-ref on-click children] :as props}] + (let [entry-ref (mf/use-ref nil) + submenu-ref (mf/use-ref nil) + hovering? (mf/use-ref false) + + on-pointer-enter + (mf/use-callback + (fn [event] + (mf/set-ref-val! hovering? true) + (let [submenu-node (mf/ref-val submenu-ref)] + (when (some? submenu-node) + (dom/set-css-property! submenu-node "display" "block"))))) + + on-pointer-leave + (mf/use-callback + (fn [event] + (mf/set-ref-val! hovering? false) + (let [submenu-node (mf/ref-val submenu-ref)] + (when (some? submenu-node) + (timers/schedule + 200 + #(when-not (mf/ref-val hovering?) + (dom/set-css-property! submenu-node "display" "none"))))))) + + set-dom-node + (mf/use-callback + (fn [dom] + (let [submenu-node (mf/ref-val submenu-ref)] + (when (and (some? dom) (some? submenu-node)) + (dom/set-css-property! submenu-node "top" (str (.-offsetTop dom) "px"))))))] + + [:li {:ref set-dom-node + :on-click on-click + :on-pointer-enter on-pointer-enter + :on-pointer-leave on-pointer-leave} + [:span.title title] + [:span.shortcut (or shortcut "")] + + (when (> (count children) 1) + [:span.submenu-icon i/arrow-slide]) + + (when (> (count children) 1) + [:ul.workspace-context-menu + {:ref submenu-ref + :style {:display "none" :left 250} + :on-context-menu prevent-default} + children])])) (mf/defc menu-separator [] @@ -100,12 +144,12 @@ do-navigate-component-file (st/emitf (dwl/nav-to-component-file (:component-file shape))) - do-create-bool-shape (st/emitf (dw/create-bool :union))] + do-boolean-union (st/emitf (dw/create-bool :union)) + do-boolean-difference (st/emitf (dw/create-bool :difference)) + do-boolean-intersection (st/emitf (dw/create-bool :intersection)) + do-boolean-exclude (st/emitf (dw/create-bool :exclude)) + ] [:* - ;; - [:& menu-entry {:title ">BOOL" - :on-click do-create-bool-shape}] - ;; [:& menu-entry {:title (tr "workspace.shape.menu.copy") :shortcut (sc/get-tooltip :copy) :on-click do-copy}] @@ -171,6 +215,20 @@ :shortcut (sc/get-tooltip :start-editing) :on-click do-start-editing}]) + [:& menu-entry {:title (tr "workspace.shape.menu.path")} + [:& menu-entry {:title (tr "workspace.shape.menu.union") + :shortcut (sc/get-tooltip :boolean-union) + :on-click do-boolean-union}] + [:& menu-entry {:title (tr "workspace.shape.menu.difference") + :shortcut (sc/get-tooltip :boolean-difference) + :on-click do-boolean-difference}] + [:& menu-entry {:title (tr "workspace.shape.menu.intersection") + :shortcut (sc/get-tooltip :boolean-intersection) + :on-click do-boolean-intersection}] + [:& menu-entry {:title (tr "workspace.shape.menu.exclude") + :shortcut (sc/get-tooltip :boolean-exclude) + :on-click do-boolean-exclude}]] + (if (:hidden shape) [:& menu-entry {:title (tr "workspace.shape.menu.show") :on-click do-show-shape}] @@ -246,7 +304,7 @@ (when dropdown (let [bounding-rect (dom/get-bounding-rect dropdown) window-size (dom/get-window-size) - delta-x (max (- (:right bounding-rect) (:width window-size)) 0) + delta-x (max (- (+ (:right bounding-rect) 250) (:width window-size)) 0) delta-y (max (- (:bottom bounding-rect) (:height window-size)) 0) new-style (str "top: " (- top delta-y) "px; " "left: " (- left delta-x) "px;")] diff --git a/frontend/src/app/main/ui/workspace/sidebar/layers.cljs b/frontend/src/app/main/ui/workspace/sidebar/layers.cljs index 0f147b1f2..f3c51c963 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/layers.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/layers.cljs @@ -39,6 +39,11 @@ (if (:masked-group? shape) i/mask i/folder)) + :bool (case (:bool-type shape) + :difference i/boolean-difference + :exclude i/boolean-exclude + :intersection i/boolean-intersection + #_:default i/boolean-union) :svg-raw i/file-svg nil)) @@ -292,7 +297,8 @@ :shape-ref :touched :metadata - :masked-group?])) + :masked-group? + :bool-type])) (defn- strip-objects [objects] diff --git a/frontend/src/app/main/ui/workspace/sidebar/options.cljs b/frontend/src/app/main/ui/workspace/sidebar/options.cljs index 89248b58b..e99d25802 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options.cljs @@ -11,7 +11,8 @@ [app.main.store :as st] [app.main.ui.components.tab-container :refer [tab-container tab-element]] [app.main.ui.context :as ctx] - [app.main.ui.workspace.sidebar.align :refer [align-options]] + [app.main.ui.workspace.sidebar.options.menus.align :refer [align-options]] + [app.main.ui.workspace.sidebar.options.menus.booleans :refer [booleans-options]] [app.main.ui.workspace.sidebar.options.menus.exports :refer [exports-menu]] [app.main.ui.workspace.sidebar.options.menus.interactions :refer [interactions-menu]] [app.main.ui.workspace.sidebar.options.page :as page] @@ -60,6 +61,7 @@ :title (tr "workspace.options.design")} [:div.element-options [:& align-options] + [:& booleans-options] (case (count selected) 0 [:& page/options] 1 [:& shape-options {:shape (first shapes) diff --git a/frontend/src/app/main/ui/workspace/sidebar/align.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/align.cljs similarity index 98% rename from frontend/src/app/main/ui/workspace/sidebar/align.cljs rename to frontend/src/app/main/ui/workspace/sidebar/options/menus/align.cljs index acb3c6a42..7b32c969a 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/align.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/align.cljs @@ -4,7 +4,7 @@ ;; ;; Copyright (c) UXBOX Labs SL -(ns app.main.ui.workspace.sidebar.align +(ns app.main.ui.workspace.sidebar.options.menus.align (:require [app.common.uuid :as uuid] [app.main.data.workspace :as dw] diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/booleans.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/booleans.cljs new file mode 100644 index 000000000..0ad6b3ddd --- /dev/null +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/booleans.cljs @@ -0,0 +1,54 @@ +;; 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.sidebar.options.menus.booleans + (:require + + [app.main.data.workspace :as dw] + [app.main.refs :as refs] + [app.main.store :as st] + [app.main.ui.icons :as i] + [app.util.i18n :as i18n :refer [tr]] + [rumext.alpha :as mf] + )) + +(mf/defc booleans-options + [] + (let [selected (mf/deref refs/selected-shapes) + disabled (and (some? selected) + (<= (count selected) 1)) + + do-boolean-union (st/emitf (dw/create-bool :union)) + do-boolean-difference (st/emitf (dw/create-bool :difference)) + do-boolean-intersection (st/emitf (dw/create-bool :intersection)) + do-boolean-exclude (st/emitf (dw/create-bool :exclude))] + + [:div.align-options + [:div.align-group + [:div.align-button.tooltip.tooltip-bottom + {:alt (tr "workspace.shape.menu.union") + :class (when disabled "disabled") + :on-click do-boolean-union} + i/boolean-union] + + [:div.align-button.tooltip.tooltip-bottom + {:alt (tr "workspace.shape.menu.difference") + :class (when disabled "disabled") + :on-click do-boolean-difference} + i/boolean-difference] + + [:div.align-button.tooltip.tooltip-bottom + {:alt (tr "workspace.shape.menu.intersection") + :class (when disabled "disabled") + :on-click do-boolean-intersection} + i/boolean-intersection] + + [:div.align-button.tooltip.tooltip-bottom + {:alt (tr "workspace.shape.menu.exclude") + :class (when disabled "disabled") + :on-click do-boolean-exclude} + i/boolean-exclude]]])) + diff --git a/frontend/src/app/util/dom.cljs b/frontend/src/app/util/dom.cljs index 1f952fb35..a170481fc 100644 --- a/frontend/src/app/util/dom.cljs +++ b/frontend/src/app/util/dom.cljs @@ -281,7 +281,7 @@ (defn set-text! [node text] (set! (.-textContent node) text)) -(defn set-css-property [node property value] +(defn set-css-property! [node property value] (.setProperty (.-style ^js node) property value)) (defn capture-pointer [event] diff --git a/frontend/src/app/util/path/bool.cljs b/frontend/src/app/util/path/bool.cljs index 24e840840..e6a41c884 100644 --- a/frontend/src/app/util/path/bool.cljs +++ b/frontend/src/app/util/path/bool.cljs @@ -16,7 +16,7 @@ [app.util.path.geom :as upg] [cuerdas.core :as str])) -(def ^:const curve-curve-precision 0.001) +(def ^:const curve-curve-precision 0.1) (defn curve->rect [[from-p to-p :as curve]] @@ -133,10 +133,9 @@ r2 (curve-range->rect c2 c2-from c2-to)] (when (gsi/overlaps-rects? r1 r2) - - (if (and (< (mth/abs (- c1-from c1-to)) curve-curve-precision) - (< (mth/abs (- c2-from c2-to)) curve-curve-precision)) - + (if (< (gpt/distance (gpp/curve-values c1 c1-from) + (gpp/curve-values c2 c2-from)) + curve-curve-precision) [(sorted-set (mth/precision c1-from 4)) (sorted-set (mth/precision c2-from 4))] diff --git a/frontend/translations/en.po b/frontend/translations/en.po index e5211a76d..27c40c21c 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -3114,4 +3114,19 @@ msgid "viewer.breaking-change.message" msgstr "Sorry!" msgid "viewer.breaking-change.description" -msgstr "This shareable link is no longer valid. Create a new one or ask the owner for a new one. +msgstr "This shareable link is no longer valid. Create a new one or ask the owner for a new one." + +msgid "workspace.shape.menu.path" +msgstr "Path" + +msgid "workspace.shape.menu.union" +msgstr "Union" + +msgid "workspace.shape.menu.difference" +msgstr "Difference" + +msgid "workspace.shape.menu.intersection" +msgstr "Intersection" + +msgid "workspace.shape.menu.exclude" +msgstr "Exclude" diff --git a/frontend/translations/es.po b/frontend/translations/es.po index 6b716a299..a1020f9c3 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -3000,3 +3000,18 @@ msgstr "¡Lo sentimos!" msgid "viewer.breaking-change.description" msgstr "Este link compartido ya no funciona. Crea uno nuevo o pídelo a la persona que lo creó." + +msgid "workspace.shape.menu.path" +msgstr "Path" + +msgid "workspace.shape.menu.union" +msgstr "Unión" + +msgid "workspace.shape.menu.difference" +msgstr "Diferencia" + +msgid "workspace.shape.menu.intersection" +msgstr "Intersección" + +msgid "workspace.shape.menu.exclude" +msgstr "Exclusión"