0
Fork 0
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:
alonso.torres 2021-09-10 15:54:11 +02:00
parent 0b4b2d3814
commit df60ee06a1
8 changed files with 142 additions and 55 deletions

View file

@ -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

View file

@ -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}])))))

View file

@ -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")

View file

@ -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

View file

@ -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])}]]))

View file

@ -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'))))

View file

@ -199,3 +199,4 @@
(if (= prefix :c1)
(command->point (get content (dec index)))
(command->point (get content index))))

View file

@ -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 [])))