mirror of
https://github.com/penpot/penpot.git
synced 2025-05-25 21:56:13 +02: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]
|
[cuerdas.core :as str]
|
||||||
[potok.core :as ptk]))
|
[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
|
(defn selected-shapes
|
||||||
[state]
|
[state]
|
||||||
(let [objects (wsh/lookup-page-objects state)]
|
(let [objects (wsh/lookup-page-objects state)]
|
||||||
|
@ -31,6 +49,7 @@
|
||||||
(defn create-bool-data
|
(defn create-bool-data
|
||||||
[type name shapes]
|
[type name shapes]
|
||||||
(let [head (first shapes)
|
(let [head (first shapes)
|
||||||
|
head-data (select-keys head style-properties)
|
||||||
selrect (gsh/selection-rect shapes)]
|
selrect (gsh/selection-rect shapes)]
|
||||||
(-> {:id (uuid/next)
|
(-> {:id (uuid/next)
|
||||||
:type :bool
|
:type :bool
|
||||||
|
@ -40,6 +59,7 @@
|
||||||
:name name
|
:name name
|
||||||
::index (::index head)
|
::index (::index head)
|
||||||
:shapes []}
|
:shapes []}
|
||||||
|
(merge head-data)
|
||||||
(gsh/setup selrect))))
|
(gsh/setup selrect))))
|
||||||
|
|
||||||
(defn create-bool
|
(defn create-bool
|
||||||
|
|
|
@ -6,70 +6,38 @@
|
||||||
|
|
||||||
(ns app.main.ui.shapes.bool
|
(ns app.main.ui.shapes.bool
|
||||||
(:require
|
(:require
|
||||||
[app.common.data :as d]
|
|
||||||
[app.common.geom.shapes :as gsh]
|
[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.object :as obj]
|
||||||
[app.util.path.bool :as pb]
|
[app.util.path.bool :as pb]
|
||||||
[app.util.path.geom :as upg]
|
|
||||||
[app.util.path.shapes-to-path :as stp]
|
[app.util.path.shapes-to-path :as stp]
|
||||||
[clojure.set :as set]
|
|
||||||
[rumext.alpha :as mf]))
|
[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
|
(defn bool-shape
|
||||||
[shape-wrapper]
|
[shape-wrapper]
|
||||||
(mf/fnc bool-shape
|
(mf/fnc bool-shape
|
||||||
{::mf/wrap-props false}
|
{::mf/wrap-props false}
|
||||||
[props]
|
[props]
|
||||||
(let [frame (obj/get props "frame")
|
(let [frame (obj/get props "frame")
|
||||||
childs (obj/get props "childs")
|
shape (obj/get props "shape")
|
||||||
shape-1 (stp/convert-to-path (nth childs 0))
|
childs (obj/get props "childs")]
|
||||||
|
|
||||||
|
(when (> (count childs) 1)
|
||||||
|
(let [shape-1 (stp/convert-to-path (nth childs 0))
|
||||||
shape-2 (stp/convert-to-path (nth childs 1))
|
shape-2 (stp/convert-to-path (nth childs 1))
|
||||||
|
|
||||||
content-1 (-> shape-1 gsh/transform-shape (gsh/translate-to-frame frame) :content)
|
content-1 (use-equal-memo (-> shape-1 :content gsh/transform-shape))
|
||||||
content-2 (-> shape-2 gsh/transform-shape (gsh/translate-to-frame frame) :content)
|
content-2 (use-equal-memo (-> shape-2 :content gsh/transform-shape))
|
||||||
|
|
||||||
|
content
|
||||||
|
(mf/use-memo
|
||||||
|
(mf/deps content-1 content-2)
|
||||||
|
#(pb/content-bool (:bool-type shape) content-1 content-2))]
|
||||||
|
|
||||||
[content-1' content-2'] (pb/content-intersect-split content-1 content-2)
|
[:& shape-wrapper {:shape (-> shape
|
||||||
|
(assoc :type :path)
|
||||||
points-1 (->> (upg/content->points content-1')
|
(assoc :content content))
|
||||||
(map #(hash-map :x (mth/round (:x %))
|
:frame frame}])))))
|
||||||
:y (mth/round (:y %))))
|
|
||||||
(into #{}))
|
|
||||||
|
|
||||||
points-2 (->> (upg/content->points content-2')
|
|
||||||
(map #(hash-map :x (mth/round (:x %))
|
|
||||||
:y (mth/round (:y %))))
|
|
||||||
(into #{}))
|
|
||||||
|
|
||||||
points-3 (set/intersection points-1 points-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"}]
|
|
||||||
|
|
||||||
|
|
||||||
])))
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -32,14 +32,13 @@
|
||||||
(dom/stop-propagation event))
|
(dom/stop-propagation event))
|
||||||
|
|
||||||
(mf/defc menu-entry
|
(mf/defc menu-entry
|
||||||
[{:keys [title shortcut submenu-ref on-click children] :as props}]
|
[{:keys [title shortcut on-click children] :as props}]
|
||||||
(let [entry-ref (mf/use-ref nil)
|
(let [submenu-ref (mf/use-ref nil)
|
||||||
submenu-ref (mf/use-ref nil)
|
|
||||||
hovering? (mf/use-ref false)
|
hovering? (mf/use-ref false)
|
||||||
|
|
||||||
on-pointer-enter
|
on-pointer-enter
|
||||||
(mf/use-callback
|
(mf/use-callback
|
||||||
(fn [event]
|
(fn []
|
||||||
(mf/set-ref-val! hovering? true)
|
(mf/set-ref-val! hovering? true)
|
||||||
(let [submenu-node (mf/ref-val submenu-ref)]
|
(let [submenu-node (mf/ref-val submenu-ref)]
|
||||||
(when (some? submenu-node)
|
(when (some? submenu-node)
|
||||||
|
@ -47,7 +46,7 @@
|
||||||
|
|
||||||
on-pointer-leave
|
on-pointer-leave
|
||||||
(mf/use-callback
|
(mf/use-callback
|
||||||
(fn [event]
|
(fn []
|
||||||
(mf/set-ref-val! hovering? false)
|
(mf/set-ref-val! hovering? false)
|
||||||
(let [submenu-node (mf/ref-val submenu-ref)]
|
(let [submenu-node (mf/ref-val submenu-ref)]
|
||||||
(when (some? submenu-node)
|
(when (some? submenu-node)
|
||||||
|
@ -227,7 +226,12 @@
|
||||||
:on-click do-boolean-intersection}]
|
:on-click do-boolean-intersection}]
|
||||||
[:& menu-entry {:title (tr "workspace.shape.menu.exclude")
|
[:& menu-entry {:title (tr "workspace.shape.menu.exclude")
|
||||||
:shortcut (sc/get-tooltip :boolean-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)
|
(if (:hidden shape)
|
||||||
[:& menu-entry {:title (tr "workspace.shape.menu.show")
|
[:& 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.exports :refer [exports-menu]]
|
||||||
[app.main.ui.workspace.sidebar.options.menus.interactions :refer [interactions-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.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.circle :as circle]
|
||||||
[app.main.ui.workspace.sidebar.options.shapes.frame :as frame]
|
[app.main.ui.workspace.sidebar.options.shapes.frame :as frame]
|
||||||
[app.main.ui.workspace.sidebar.options.shapes.group :as group]
|
[app.main.ui.workspace.sidebar.options.shapes.group :as group]
|
||||||
|
@ -44,6 +45,7 @@
|
||||||
:path [:& path/options {:shape shape}]
|
:path [:& path/options {:shape shape}]
|
||||||
:image [:& image/options {:shape shape}]
|
:image [:& image/options {:shape shape}]
|
||||||
:svg-raw [:& svg-raw/options {:shape shape}]
|
:svg-raw [:& svg-raw/options {:shape shape}]
|
||||||
|
:bool [:& bool/options {:shape shape}]
|
||||||
nil)
|
nil)
|
||||||
[:& exports-menu
|
[:& exports-menu
|
||||||
{:shape shape
|
{: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.geom.shapes.rect :as gpr]
|
||||||
[app.common.math :as mth]
|
[app.common.math :as mth]
|
||||||
[app.util.path.geom :as upg]
|
[app.util.path.geom :as upg]
|
||||||
[cuerdas.core :as str]))
|
[app.util.path.subpaths :as ups]))
|
||||||
|
|
||||||
(def ^:const curve-curve-precision 0.1)
|
(def ^:const curve-curve-precision 0.1)
|
||||||
|
|
||||||
|
@ -267,3 +267,39 @@
|
||||||
(rest new-pending)
|
(rest new-pending)
|
||||||
new-content-b
|
new-content-b
|
||||||
(conj new-content-a new-current))))))))
|
(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)
|
(if (= prefix :c1)
|
||||||
(command->point (get content (dec index)))
|
(command->point (get content (dec index)))
|
||||||
(command->point (get content index))))
|
(command->point (get content index))))
|
||||||
|
|
||||||
|
|
|
@ -134,3 +134,14 @@
|
||||||
(->> closed-subpaths
|
(->> closed-subpaths
|
||||||
(mapcat :data)
|
(mapcat :data)
|
||||||
(into []))))
|
(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
Add a link
Reference in a new issue