Files
penpot/frontend/src/app/main/ui/workspace/shapes/path.cljs
2020-11-23 17:44:02 +01:00

286 lines
11 KiB
Clojure

;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020 UXBOX Labs SL
(ns app.main.ui.workspace.shapes.path
(:require
[rumext.alpha :as mf]
[okulary.core :as l]
[app.util.data :as d]
[app.util.dom :as dom]
[app.util.timers :as ts]
[app.main.refs :as refs]
[app.main.streams :as ms]
[app.main.constants :as c]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.data.workspace :as dw]
[app.main.data.workspace.drawing :as dr]
[app.main.data.workspace.drawing.path :as drp]
[app.main.ui.keyboard :as kbd]
[app.main.ui.shapes.path :as path]
[app.main.ui.shapes.filters :as filters]
[app.main.ui.shapes.shape :refer [shape-container]]
[app.main.ui.workspace.shapes.common :as common]
[app.util.geom.path :as ugp]
[app.common.geom.point :as gpt]
[app.common.geom.shapes.path :as gsp]
[app.main.ui.cursors :as cur]
[app.main.ui.icons :as i]))
(def primary-color "#1FDEA7")
(def secondary-color "#DB00FF")
(def black-color "#000000")
(def white-color "#FFFFFF")
(def gray-color "#B1B2B5")
(def current-edit-path-ref
(let [selfn (fn [local]
(let [id (:edition local)]
(get-in local [:edit-path id])))]
(l/derived selfn refs/workspace-local)))
(defn make-edit-path-ref [id]
(mf/use-memo
(mf/deps id)
(let [selfn #(get-in % [:edit-path id])]
#(l/derived selfn refs/workspace-local))))
(defn make-content-modifiers-ref [id]
(mf/use-memo
(mf/deps id)
(let [selfn #(get-in % [:edit-path id :content-modifiers])]
#(l/derived selfn refs/workspace-local))))
(mf/defc path-wrapper
{::mf/wrap-props false}
[props]
(let [shape (unchecked-get props "shape")
hover? (or (mf/deref refs/current-hover) #{})
on-mouse-down (mf/use-callback
(mf/deps shape)
#(common/on-mouse-down % shape))
on-context-menu (mf/use-callback
(mf/deps shape)
#(common/on-context-menu % shape))
on-double-click (mf/use-callback
(mf/deps shape)
(fn [event]
(when (not (::dr/initialized? shape))
(do
(dom/stop-propagation event)
(dom/prevent-default event)
(st/emit! (dw/start-edition-mode (:id shape))
(dw/start-path-edit (:id shape)))))))
content-modifiers-ref (make-content-modifiers-ref (:id shape))
content-modifiers (mf/deref content-modifiers-ref)
editing-id (mf/deref refs/selected-edition)
editing? (= editing-id (:id shape))
shape (update shape :content gsp/apply-content-modifiers content-modifiers)]
[:> shape-container {:shape shape
:pointer-events (when editing? "none")
:on-double-click on-double-click
:on-mouse-down on-mouse-down
:on-context-menu on-context-menu}
[:& path/path-shape {:shape shape
:background? true}]]))
(mf/defc path-actions [{:keys [shape]}]
(let [id (mf/deref refs/selected-edition)
{:keys [edit-mode selected snap-toggled] :as all} (mf/deref current-edit-path-ref)]
[:div.path-actions
[:div.viewport-actions-group
[:div.viewport-actions-entry {:class (when (= edit-mode :draw) "is-toggled")
:on-click #(st/emit! (drp/change-edit-mode :draw))} i/pen]
[:div.viewport-actions-entry {:class (when (= edit-mode :move) "is-toggled")
:on-click #(st/emit! (drp/change-edit-mode :move))} i/pointer-inner]]
[:div.viewport-actions-group
[:div.viewport-actions-entry {:class "is-disabled"} i/nodes-add]
[:div.viewport-actions-entry {:class "is-disabled"} i/nodes-remove]]
[:div.viewport-actions-group
[:div.viewport-actions-entry {:class "is-disabled"} i/nodes-merge]
[:div.viewport-actions-entry {:class "is-disabled"} i/nodes-join]
[:div.viewport-actions-entry {:class "is-disabled"} i/nodes-separate]]
[:div.viewport-actions-group
[:div.viewport-actions-entry {:class "is-disabled"} i/nodes-corner]
[:div.viewport-actions-entry {:class "is-disabled"} i/nodes-curve]]
[:div.viewport-actions-group
[:div.viewport-actions-entry {:class (when snap-toggled "is-toggled")} i/nodes-snap]]]))
(mf/defc path-preview [{:keys [zoom command from]}]
(when (not= :move-to (:command command))
[:path {:style {:fill "transparent"
:stroke secondary-color
:stroke-width (/ 1 zoom)}
:d (ugp/content->path [{:command :move-to
:params {:x (:x from)
:y (:y from)}}
command])}]))
(mf/defc path-point [{:keys [index position stroke-color fill-color zoom edit-mode selected]}]
(let [{:keys [x y]} position
on-click (fn [event]
(cond
(= edit-mode :move)
(do
(dom/stop-propagation event)
(dom/prevent-default event)
(st/emit! (drp/select-node index)))))
on-mouse-down (fn [event]
(cond
(= edit-mode :move)
(do
(dom/stop-propagation event)
(dom/prevent-default event)
(st/emit! (drp/start-move-path-point index)))))]
[:g.path-point
[:circle.path-point
{:cx x
:cy y
:r (/ 3 zoom)
:style { ;; :cursor cur/resize-alt
:stroke-width (/ 1 zoom)
:stroke (or stroke-color black-color)
:fill (or fill-color white-color)}}]
[:circle {:cx x
:cy y
:r (/ 10 zoom)
:on-click on-click
:on-mouse-down on-mouse-down
:style {:fill "transparent"}}]]
))
(mf/defc path-handler [{:keys [index point handler zoom selected type edit-mode]}]
(when (and point handler)
(let [{:keys [x y]} handler
on-click (fn [event]
(cond
(= edit-mode :move)
(do
(dom/stop-propagation event)
(dom/prevent-default event)
(drp/select-handler index type))))
on-mouse-down (fn [event]
(cond
(= edit-mode :move)
(do
(dom/stop-propagation event)
(dom/prevent-default event)
(st/emit! (drp/start-move-handler index type)))))]
[:g.handler {:class (name type)}
[:line
{:x1 (:x point)
:y1 (:y point)
:x2 x
:y2 y
:style {:stroke gray-color
:stroke-width (/ 1 zoom)}}]
[:rect
{:x (- x (/ 3 zoom))
:y (- y (/ 3 zoom))
:width (/ 6 zoom)
:height (/ 6 zoom)
:style {;; :cursor cur/resize-alt
:stroke-width (/ 1 zoom)
:stroke (if selected black-color primary-color)
:fill (if selected primary-color white-color)}}]
[:circle {:cx x
:cy y
:r (/ 10 zoom)
:on-click on-click
:on-mouse-down on-mouse-down
:style {:fill "transparent"}}]])))
(mf/defc path-editor
[{:keys [shape zoom]}]
(let [{:keys [content]} shape
edit-path-ref (make-edit-path-ref (:id shape))
{:keys [edit-mode selected drag-handler prev-handler preview content-modifiers]} (mf/deref edit-path-ref)
selected (or selected #{})
content (gsp/apply-content-modifiers content content-modifiers)
points (gsp/content->points content)
last-command (last content)
last-p (last points)]
[:g.path-editor
(when (and preview (not drag-handler))
[:g.preview {:style {:pointer-events "none"}}
[:& path-preview {:command preview
:from last-p
:zoom zoom}]
[:& path-point {:position (:params preview)
:fill-color secondary-color
:zoom zoom}]])
(for [[index [cmd next]] (d/enumerate (d/with-next content))]
(let [point (gpt/point (:params cmd))]
[:g.path-node
(when (= :curve-to (:command cmd))
[:& path-handler {:point point
:handler (gpt/point (-> cmd :params :c2x) (-> cmd :params :c2y))
:zoom zoom
:type :prev
:index index
:selected (selected [index :prev])
:edit-mode edit-mode}])
(when (= :curve-to (:command next))
[:& path-handler {:point point
:handler (gpt/point (-> next :params :c1x) (-> next :params :c1y))
:zoom zoom
:type :next
:index index
:selected (selected [index :next])
:edit-mode edit-mode}])
(when (and (= index (dec (count content)))
prev-handler (not drag-handler))
[:& path-handler {:point point
:handler prev-handler
:zoom zoom
:type :prev
:index index
:selected (selected index)
:edit-mode edit-mode}])
[:& path-point {:position point
:stroke-color (when-not (selected index) primary-color)
:fill-color (when (selected index) primary-color)
:index index
:zoom zoom
:edit-mode edit-mode}]]))
(when drag-handler
[:g.drag-handler
(when (not= :move-to (:command last-command))
[:& path-handler {:point last-p
:handler (ugp/opposite-handler last-p drag-handler)
:zoom zoom
:type :drag-opposite
:selected false}])
[:& path-handler {:point last-p
:handler drag-handler
:zoom zoom
:type :drag
:selected false}]])]))