diff --git a/common/src/app/common/types/shape_tree.cljc b/common/src/app/common/types/shape_tree.cljc index 92a889a6d5..90109d5b46 100644 --- a/common/src/app/common/types/shape_tree.cljc +++ b/common/src/app/common/types/shape_tree.cljc @@ -272,6 +272,20 @@ -1)))) items)))) +(defn- clipped-by-ancestor? + "Checks whether position falls outside the visible (clipped) bounds of + some ancestor frame with clip content enabled. Used so that a nested + frame that extends beyond a clipping ancestor's own bounds is never + considered hit/reachable in the invisible, clipped-away region." + [objects shape position] + (->> (cfh/get-parent-ids objects (dm/get-prop shape :id)) + (keep (d/getf objects)) + (some (fn [ancestor] + (and (not= (dm/get-prop ancestor :id) uuid/zero) + ^boolean (cfh/frame-shape? ancestor) + (not (:show-content ancestor)) + (not ^boolean (gsh/has-point? ancestor position))))))) + (defn get-frame-by-position ([objects position] (get-frame-by-position objects position nil)) @@ -287,6 +301,7 @@ validator (or (get options :validator) #(-> true))] (or (d/seek #(and ^boolean (some? position) ^boolean (gsh/has-point? % position) + ^boolean (not (clipped-by-ancestor? objects % position)) ^boolean (validator %)) frames) (get objects uuid/zero))))) @@ -302,7 +317,8 @@ ([objects position options] (->> (get-frames objects options) (filter #(and ^boolean (some? position) - ^boolean (gsh/has-point? % position))) + ^boolean (gsh/has-point? % position) + ^boolean (not (clipped-by-ancestor? objects % position)))) (sort-z-index-objects objects)))) (defn top-nested-frame diff --git a/common/test/common_tests/types_shape_tree_test.cljc b/common/test/common_tests/types_shape_tree_test.cljc new file mode 100644 index 0000000000..423dbce37d --- /dev/null +++ b/common/test/common_tests/types_shape_tree_test.cljc @@ -0,0 +1,58 @@ +;; 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) KALEIDOS INC Sucursal en EspaƱa SL + +(ns common-tests.types-shape-tree-test + (:require + [app.common.geom.point :as gpt] + [app.common.types.shape-tree :as ctt] + [app.common.uuid :as uuid] + [clojure.test :as t])) + +(defn- make-frame + [id parent-id shapes x y width height show-content] + {:id id + :type :frame + :parent-id parent-id + :frame-id parent-id + :shapes (vec shapes) + :x x + :y y + :width width + :height height + :rotation nil + :hidden false + :blocked false + :show-content show-content}) + +(t/deftest top-nested-frame-clip-content-test + (t/testing "board A (clip) contains a wider board B; point inside both resolves to B" + (let [a-id (uuid/next) + b-id (uuid/next) + objects {a-id (make-frame a-id uuid/zero [b-id] 0 0 200 200 false) + b-id (make-frame b-id a-id [] 50 50 300 300 false)} + position (gpt/point 150 150) + result (ctt/top-nested-frame objects position)] + (t/is (= b-id result)))) + + (t/testing "point inside B but outside A's clipped bounds is not reachable at all" + (let [a-id (uuid/next) + b-id (uuid/next) + objects {a-id (make-frame a-id uuid/zero [b-id] 0 0 200 200 false) + b-id (make-frame b-id a-id [] 50 50 300 300 false)} + position (gpt/point 300 300) + result (ctt/top-nested-frame objects position)] + ;; Outside A (the clip ancestor) and B's visible/clipped region there is + ;; not visible either, so no frame should be resolved at that point. + (t/is (= uuid/zero result)))) + + (t/testing "with show-content true on A, the same point can resolve into B" + (let [a-id (uuid/next) + b-id (uuid/next) + objects {a-id (make-frame a-id uuid/zero [b-id] 0 0 200 200 true) + b-id (make-frame b-id a-id [] 50 50 300 300 false)} + position (gpt/point 300 300) + result (ctt/top-nested-frame objects position)] + (t/is (= b-id result))))) diff --git a/frontend/src/app/worker/selection.cljs b/frontend/src/app/worker/selection.cljs index 4fc1039be8..d5a5ee06a0 100644 --- a/frontend/src/app/worker/selection.cljs +++ b/frontend/src/app/worker/selection.cljs @@ -245,16 +245,20 @@ overlaps-parent? (fn [clip-parents] - (->> clip-parents (some (comp not overlaps?)) not))] + (->> clip-parents + ;; When clip-children? is false (e.g. deep/penetrate selection with + ;; a modifier key held) we still must not reach into the clipped-away, + ;; invisible area of an ancestor board with clip content enabled. + ;; Only the bool/mask clip-parents (non-frame) are relaxed in that case. + (remove #(and (not clip-children?) (not ^boolean (cfh/frame-shape? %)))) + (every? overlaps?)))] ;; Shapes after filters of overlapping and criteria (into (d/ordered-set) (comp (map #(unchecked-get % "data")) (filter match-criteria?) (filter overlaps?) - (filter (if clip-children? - (comp overlaps-parent? :clip-parents) - (constantly true))) + (filter (comp overlaps-parent? :clip-parents)) (keep :id)) result)))