mirror of
https://github.com/penpot/penpot.git
synced 2025-04-13 07:21:40 -05:00
✨ Add exclusion boolean operation
This commit is contained in:
parent
0b4b2d3814
commit
df60ee06a1
8 changed files with 142 additions and 55 deletions
|
@ -18,6 +18,24 @@
|
|||
[cuerdas.core :as str]
|
||||
[potok.core :as ptk]))
|
||||
|
||||
(def ^:const style-properties
|
||||
[:fill-color
|
||||
:fill-opacity
|
||||
:fill-color-gradient
|
||||
:fill-color-ref-file
|
||||
:fill-color-ref-id
|
||||
:stroke-color
|
||||
:stroke-color-ref-file
|
||||
:stroke-color-ref-id
|
||||
:stroke-opacity
|
||||
:stroke-style
|
||||
:stroke-width
|
||||
:stroke-alignment
|
||||
:stroke-cap-start
|
||||
:stroke-cap-end
|
||||
:shadow
|
||||
:blur])
|
||||
|
||||
(defn selected-shapes
|
||||
[state]
|
||||
(let [objects (wsh/lookup-page-objects state)]
|
||||
|
@ -31,6 +49,7 @@
|
|||
(defn create-bool-data
|
||||
[type name shapes]
|
||||
(let [head (first shapes)
|
||||
head-data (select-keys head style-properties)
|
||||
selrect (gsh/selection-rect shapes)]
|
||||
(-> {:id (uuid/next)
|
||||
:type :bool
|
||||
|
@ -40,6 +59,7 @@
|
|||
:name name
|
||||
::index (::index head)
|
||||
:shapes []}
|
||||
(merge head-data)
|
||||
(gsh/setup selrect))))
|
||||
|
||||
(defn create-bool
|
||||
|
|
|
@ -6,70 +6,38 @@
|
|||
|
||||
(ns app.main.ui.shapes.bool
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.geom.shapes :as gsh]
|
||||
[app.common.math :as mth]
|
||||
[app.main.ui.hooks :refer [use-equal-memo]]
|
||||
[app.util.object :as obj]
|
||||
[app.util.path.bool :as pb]
|
||||
[app.util.path.geom :as upg]
|
||||
[app.util.path.shapes-to-path :as stp]
|
||||
[clojure.set :as set]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
(mf/defc path-points
|
||||
[{:keys [points color]}]
|
||||
|
||||
[:*
|
||||
(for [[idx {:keys [x y]}] (d/enumerate points)]
|
||||
[:circle {:key (str "circle-" idx)
|
||||
:cx x
|
||||
:cy y
|
||||
:r 5
|
||||
:style {:fill color
|
||||
;;:fillOpacity 0.5
|
||||
}}])])
|
||||
|
||||
(defn bool-shape
|
||||
[shape-wrapper]
|
||||
(mf/fnc bool-shape
|
||||
{::mf/wrap-props false}
|
||||
[props]
|
||||
(let [frame (obj/get props "frame")
|
||||
childs (obj/get props "childs")
|
||||
shape-1 (stp/convert-to-path (nth childs 0))
|
||||
shape-2 (stp/convert-to-path (nth childs 1))
|
||||
shape (obj/get props "shape")
|
||||
childs (obj/get props "childs")]
|
||||
|
||||
content-1 (-> shape-1 gsh/transform-shape (gsh/translate-to-frame frame) :content)
|
||||
content-2 (-> shape-2 gsh/transform-shape (gsh/translate-to-frame frame) :content)
|
||||
|
||||
(when (> (count childs) 1)
|
||||
(let [shape-1 (stp/convert-to-path (nth childs 0))
|
||||
shape-2 (stp/convert-to-path (nth childs 1))
|
||||
|
||||
[content-1' content-2'] (pb/content-intersect-split content-1 content-2)
|
||||
|
||||
points-1 (->> (upg/content->points content-1')
|
||||
(map #(hash-map :x (mth/round (:x %))
|
||||
:y (mth/round (:y %))))
|
||||
(into #{}))
|
||||
|
||||
points-2 (->> (upg/content->points content-2')
|
||||
(map #(hash-map :x (mth/round (:x %))
|
||||
:y (mth/round (:y %))))
|
||||
(into #{}))
|
||||
content-1 (use-equal-memo (-> shape-1 :content gsh/transform-shape))
|
||||
content-2 (use-equal-memo (-> shape-2 :content gsh/transform-shape))
|
||||
|
||||
points-3 (set/intersection points-1 points-2)]
|
||||
content
|
||||
(mf/use-memo
|
||||
(mf/deps content-1 content-2)
|
||||
#(pb/content-bool (:bool-type shape) content-1 content-2))]
|
||||
|
||||
[:*
|
||||
[:& shape-wrapper {:shape (-> shape-1 #_(assoc :content content-1'))
|
||||
:frame frame}]
|
||||
|
||||
[:& shape-wrapper {:shape (-> shape-2 #_(assoc :content content-2'))
|
||||
:frame frame}]
|
||||
|
||||
[:& path-points {:points points-1 :color "#FF0000"}]
|
||||
[:& path-points {:points points-2 :color "#0000FF"}]
|
||||
[:& path-points {:points points-3 :color "#FF00FF"}]
|
||||
|
||||
|
||||
])))
|
||||
[:& shape-wrapper {:shape (-> shape
|
||||
(assoc :type :path)
|
||||
(assoc :content content))
|
||||
:frame frame}])))))
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -32,14 +32,13 @@
|
|||
(dom/stop-propagation event))
|
||||
|
||||
(mf/defc menu-entry
|
||||
[{:keys [title shortcut submenu-ref on-click children] :as props}]
|
||||
(let [entry-ref (mf/use-ref nil)
|
||||
submenu-ref (mf/use-ref nil)
|
||||
[{:keys [title shortcut on-click children] :as props}]
|
||||
(let [submenu-ref (mf/use-ref nil)
|
||||
hovering? (mf/use-ref false)
|
||||
|
||||
on-pointer-enter
|
||||
(mf/use-callback
|
||||
(fn [event]
|
||||
(fn []
|
||||
(mf/set-ref-val! hovering? true)
|
||||
(let [submenu-node (mf/ref-val submenu-ref)]
|
||||
(when (some? submenu-node)
|
||||
|
@ -47,7 +46,7 @@
|
|||
|
||||
on-pointer-leave
|
||||
(mf/use-callback
|
||||
(fn [event]
|
||||
(fn []
|
||||
(mf/set-ref-val! hovering? false)
|
||||
(let [submenu-node (mf/ref-val submenu-ref)]
|
||||
(when (some? submenu-node)
|
||||
|
@ -227,7 +226,12 @@
|
|||
:on-click do-boolean-intersection}]
|
||||
[:& menu-entry {:title (tr "workspace.shape.menu.exclude")
|
||||
:shortcut (sc/get-tooltip :boolean-exclude)
|
||||
:on-click do-boolean-exclude}]]
|
||||
:on-click do-boolean-exclude}]
|
||||
|
||||
[:& menu-separator]
|
||||
;; TODO
|
||||
[:& menu-entry {:title "Flatten"}]
|
||||
[:& menu-entry {:title "Transform to path"}]]
|
||||
|
||||
(if (:hidden shape)
|
||||
[:& menu-entry {:title (tr "workspace.shape.menu.show")
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
[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]
|
||||
[app.main.ui.workspace.sidebar.options.shapes.bool :as bool]
|
||||
[app.main.ui.workspace.sidebar.options.shapes.circle :as circle]
|
||||
[app.main.ui.workspace.sidebar.options.shapes.frame :as frame]
|
||||
[app.main.ui.workspace.sidebar.options.shapes.group :as group]
|
||||
|
@ -44,6 +45,7 @@
|
|||
:path [:& path/options {:shape shape}]
|
||||
:image [:& image/options {:shape shape}]
|
||||
:svg-raw [:& svg-raw/options {:shape shape}]
|
||||
:bool [:& bool/options {:shape shape}]
|
||||
nil)
|
||||
[:& exports-menu
|
||||
{:shape shape
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
;; 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.shapes.bool
|
||||
(:require
|
||||
[app.main.ui.workspace.sidebar.options.menus.blur :refer [blur-menu]]
|
||||
[app.main.ui.workspace.sidebar.options.menus.constraints :refer [constraint-attrs constraints-menu]]
|
||||
[app.main.ui.workspace.sidebar.options.menus.fill :refer [fill-attrs fill-menu]]
|
||||
[app.main.ui.workspace.sidebar.options.menus.layer :refer [layer-attrs layer-menu]]
|
||||
[app.main.ui.workspace.sidebar.options.menus.measures :refer [measure-attrs measures-menu]]
|
||||
[app.main.ui.workspace.sidebar.options.menus.shadow :refer [shadow-menu]]
|
||||
[app.main.ui.workspace.sidebar.options.menus.stroke :refer [stroke-attrs stroke-menu]]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
(mf/defc options
|
||||
[{:keys [shape] :as props}]
|
||||
(let [ids [(:id shape)]
|
||||
type (:type shape)
|
||||
measure-values (select-keys shape measure-attrs)
|
||||
stroke-values (select-keys shape stroke-attrs)
|
||||
layer-values (select-keys shape layer-attrs)
|
||||
constraint-values (select-keys shape constraint-attrs)]
|
||||
[:*
|
||||
[:& measures-menu {:ids ids
|
||||
:type type
|
||||
:values measure-values}]
|
||||
[:& constraints-menu {:ids ids
|
||||
:values constraint-values}]
|
||||
[:& layer-menu {:ids ids
|
||||
:type type
|
||||
:values layer-values}]
|
||||
[:& fill-menu {:ids ids
|
||||
:type type
|
||||
:values (select-keys shape fill-attrs)}]
|
||||
[:& stroke-menu {:ids ids
|
||||
:type type
|
||||
:show-caps true
|
||||
:values stroke-values}]
|
||||
[:& shadow-menu {:ids ids
|
||||
:values (select-keys shape [:shadow])}]
|
||||
[:& blur-menu {:ids ids
|
||||
:values (select-keys shape [:blur])}]]))
|
|
@ -14,7 +14,7 @@
|
|||
[app.common.geom.shapes.rect :as gpr]
|
||||
[app.common.math :as mth]
|
||||
[app.util.path.geom :as upg]
|
||||
[cuerdas.core :as str]))
|
||||
[app.util.path.subpaths :as ups]))
|
||||
|
||||
(def ^:const curve-curve-precision 0.1)
|
||||
|
||||
|
@ -267,3 +267,39 @@
|
|||
(rest new-pending)
|
||||
new-content-b
|
||||
(conj new-content-a new-current))))))))
|
||||
|
||||
|
||||
(defn create-union [content-a content-b]
|
||||
(d/concat
|
||||
[]
|
||||
content-a
|
||||
(ups/reverse-content content-b)))
|
||||
|
||||
(defn create-difference [content-a content-b]
|
||||
(d/concat
|
||||
[]
|
||||
content-a
|
||||
(ups/reverse-content content-b)))
|
||||
|
||||
(defn create-intersection [content-a content-b]
|
||||
(d/concat
|
||||
[]
|
||||
content-a
|
||||
(ups/reverse-content content-b)))
|
||||
|
||||
|
||||
(defn create-exclusion [content-a content-b]
|
||||
(d/concat
|
||||
[]
|
||||
content-a
|
||||
(ups/reverse-content content-b)))
|
||||
|
||||
(defn content-bool
|
||||
[bool-type content-a content-b]
|
||||
|
||||
(let [[content-a' content-b'] (content-intersect-split content-a content-b)]
|
||||
(case bool-type
|
||||
:union (create-union content-a' content-b')
|
||||
:difference (create-difference content-a' content-b')
|
||||
:intersection (create-intersection content-a' content-b')
|
||||
:exclusion (create-exclusion content-a' content-b'))))
|
||||
|
|
|
@ -199,3 +199,4 @@
|
|||
(if (= prefix :c1)
|
||||
(command->point (get content (dec index)))
|
||||
(command->point (get content index))))
|
||||
|
||||
|
|
|
@ -134,3 +134,14 @@
|
|||
(->> closed-subpaths
|
||||
(mapcat :data)
|
||||
(into []))))
|
||||
|
||||
(defn reverse-content
|
||||
"Given a content reverse the order of the commands"
|
||||
[content]
|
||||
|
||||
(->> content
|
||||
(get-subpaths)
|
||||
(mapv reverse-subpath)
|
||||
(reverse)
|
||||
(mapcat :data)
|
||||
(into [])))
|
||||
|
|
Loading…
Add table
Reference in a new issue