From fbff2f103e22b85e69a625cde05aea12c6f0ab8f Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Wed, 6 Sep 2023 14:53:41 +0200 Subject: [PATCH] :sparkles: Select through stroke only rectangle --- CHANGES.md | 2 + common/src/app/common/geom/shapes.cljc | 1 + .../src/app/common/geom/shapes/intersect.cljc | 12 ++- .../app/main/data/workspace/selection.cljs | 3 +- .../app/main/ui/workspace/viewport/hooks.cljs | 3 +- frontend/src/app/worker/selection.cljs | 92 +++++++++++++++---- 6 files changed, 88 insertions(+), 25 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index db68dcc08..1a2924453 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,6 +6,8 @@ ### :sparkles: New features +- Select through stroke only rectangle [Taiga #5484](https://tree.taiga.io/project/penpot/issue/5484) + ### :bug: Bugs fixed ### :arrow_up: Deps updates diff --git a/common/src/app/common/geom/shapes.cljc b/common/src/app/common/geom/shapes.cljc index 9ba5de347..757d8f7b3 100644 --- a/common/src/app/common/geom/shapes.cljc +++ b/common/src/app/common/geom/shapes.cljc @@ -185,6 +185,7 @@ ;; Intersection (dm/export gsi/overlaps?) +(dm/export gsi/overlaps-path?) (dm/export gsi/has-point?) (dm/export gsi/has-point-rect?) (dm/export gsi/rect-contains-shape?) diff --git a/common/src/app/common/geom/shapes/intersect.cljc b/common/src/app/common/geom/shapes/intersect.cljc index 2a5f8af20..74da2cc3e 100644 --- a/common/src/app/common/geom/shapes/intersect.cljc +++ b/common/src/app/common/geom/shapes/intersect.cljc @@ -173,7 +173,7 @@ (defn overlaps-path? "Checks if the given rect overlaps with the path in any point" - [shape rect] + [shape rect include-content?] (when (d/not-empty? (:content shape)) (let [ ;; If paths are too complex the intersection is too expensive @@ -189,9 +189,11 @@ (gpp/path->lines shape)) start-point (-> shape :content (first) :params (gpt/point))] - (or (is-point-inside-nonzero? (first rect-points) path-lines) - (is-point-inside-nonzero? start-point rect-lines) - (intersects-lines? rect-lines path-lines))))) + (or (intersects-lines? rect-lines path-lines) + (if include-content? + (or (is-point-inside-nonzero? (first rect-points) path-lines) + (is-point-inside-nonzero? start-point rect-lines)) + false))))) (defn is-point-inside-ellipse? "checks if a point is inside an ellipse" @@ -315,7 +317,7 @@ (cond (cph/path-shape? shape) (and (overlaps-rect-points? rect (:points shape)) - (overlaps-path? shape rect)) + (overlaps-path? shape rect true)) (cph/circle-shape? shape) (and (overlaps-rect-points? rect (:points shape)) diff --git a/frontend/src/app/main/data/workspace/selection.cljs b/frontend/src/app/main/data/workspace/selection.cljs index 31ade4e60..94680787c 100644 --- a/frontend/src/app/main/data/workspace/selection.cljs +++ b/frontend/src/app/main/data/workspace/selection.cljs @@ -303,7 +303,8 @@ :rect selrect :include-frames? true :ignore-groups? ignore-groups? - :full-frame? true}) + :full-frame? true + :using-selrect? true}) (rx/map #(cph/clean-loops objects %)) (rx/map #(into initial-set (comp (filter (complement blocked?)) diff --git a/frontend/src/app/main/ui/workspace/viewport/hooks.cljs b/frontend/src/app/main/ui/workspace/viewport/hooks.cljs index 3423a5da7..5398bd0a7 100644 --- a/frontend/src/app/main/ui/workspace/viewport/hooks.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/hooks.cljs @@ -147,7 +147,8 @@ :page-id page-id :rect rect :include-frames? true - :clip-children? true}) + :clip-children? true + :using-selrect? false}) ;; When the ask-buffered is canceled returns null. We filter them ;; to improve the behavior (rx/filter some?)))))) diff --git a/frontend/src/app/worker/selection.cljs b/frontend/src/app/worker/selection.cljs index bdf827b61..110a9d9e1 100644 --- a/frontend/src/app/worker/selection.cljs +++ b/frontend/src/app/worker/selection.cljs @@ -7,11 +7,13 @@ (ns app.worker.selection (:require [app.common.data :as d] + [app.common.geom.point :as gpt] [app.common.geom.rect :as grc] [app.common.geom.shapes :as gsh] [app.common.geom.shapes.text :as gst] [app.common.pages :as cp] [app.common.pages.helpers :as cph] + [app.common.types.modifiers :as ctm] [app.common.uuid :as uuid] [app.util.quadtree :as qdt] [app.worker.impl :as impl] @@ -117,7 +119,7 @@ (assoc data :index index))) (defn- query-index - [{index :index} rect frame-id full-frame? include-frames? ignore-groups? clip-children?] + [{index :index} rect frame-id full-frame? include-frames? ignore-groups? clip-children? using-selrect?] (let [result (-> (qdt/search index (clj->js rect)) (es6-iterator-seq)) @@ -125,24 +127,78 @@ match-criteria? (fn [shape] (and (not (:hidden shape)) - (or (= :frame (:type shape)) ;; We return frames even if blocked - (not (:blocked shape))) - (or (not frame-id) (= frame-id (:frame-id shape))) - (case (:type shape) - :frame include-frames? - (:bool :group) (not ignore-groups?) - true) + (or (= :frame (:type shape)) ;; We return frames even if blocked + (not (:blocked shape))) + (or (not frame-id) (= frame-id (:frame-id shape))) + (case (:type shape) + :frame include-frames? + (:bool :group) (not ignore-groups?) + true) - (or (not full-frame?) - (not= :frame (:type shape)) - (and (d/not-empty? (:shapes shape)) - (gsh/rect-contains-shape? rect shape)) - (and (empty? (:shapes shape)) - (gsh/overlaps? shape rect))))) + (or (not full-frame?) + (not= :frame (:type shape)) + (and (d/not-empty? (:shapes shape)) + (gsh/rect-contains-shape? rect shape)) + (and (empty? (:shapes shape)) + (gsh/overlaps? shape rect))))) + + overlaps-outer-shape? + (fn [shape] + (let [padding (->> (:strokes shape) + (map #(case (get % :stroke-alignment :center) + :center (:stroke-width % 0) + :outer (* 2 (:stroke-width % 0)) + :inner 0)) + (reduce d/max 0)) + + scalev (gpt/point (/ (+ (:width shape) padding) + (:width shape)) + (/ (+ (:height shape) padding) + (:height shape))) + + outer-shape (-> shape + (gsh/transform-shape (-> (ctm/empty) + (ctm/resize scalev (gsh/shape->center shape)))))] + + (gsh/overlaps? outer-shape rect))) + + overlaps-inner-shape? + (fn [shape] + (let [padding (->> (:strokes shape) + (map #(case (get % :stroke-alignment :center) + :center (:stroke-width % 0) + :outer 0 + :inner (* 2 (:stroke-width % 0)))) + (reduce d/max 0)) + + scalev (gpt/point (/ (- (:width shape) padding) + (:width shape)) + (/ (- (:height shape) padding) + (:height shape))) + + inner-shape (-> shape + (gsh/transform-shape (-> (ctm/empty) + (ctm/resize scalev (gsh/shape->center shape)))))] + (gsh/overlaps? inner-shape rect))) + + overlaps-path? + (fn [shape] + (let [padding (->> (:strokes shape) + (map :stroke-width) + (reduce d/max 0)) + rect (grc/center->rect rect padding padding)] + (gsh/overlaps-path? shape rect false))) overlaps? (fn [shape] - (gsh/overlaps? shape rect)) + (if (and (false? using-selrect?) (empty? (:fills shape))) + (do + (case (:type shape) + ;; If the shape has no fills the overlap depends on the stroke + :rect (and (overlaps-outer-shape? shape) (not (overlaps-inner-shape? shape))) + :circle (and (overlaps-outer-shape? shape) (not (overlaps-inner-shape? shape))) + :path (overlaps-path? shape))) + (gsh/overlaps? shape rect))) overlaps-parent? (fn [clip-parents] @@ -186,8 +242,8 @@ nil) (defmethod impl/handler :selection/query - [{:keys [page-id rect frame-id full-frame? include-frames? ignore-groups? clip-children?] - :or {full-frame? false include-frames? false clip-children? true} + [{:keys [page-id rect frame-id full-frame? include-frames? ignore-groups? clip-children? using-selrect?] + :or {full-frame? false include-frames? false clip-children? true using-selrect? false} :as message}] (when-let [index (get @state page-id)] - (query-index index rect frame-id full-frame? include-frames? ignore-groups? clip-children?))) + (query-index index rect frame-id full-frame? include-frames? ignore-groups? clip-children? using-selrect?)))