diff --git a/common/src/app/common/types/shape/interactions.cljc b/common/src/app/common/types/shape/interactions.cljc index 6b06b68897..99f25fad00 100644 --- a/common/src/app/common/types/shape/interactions.cljc +++ b/common/src/app/common/types/shape/interactions.cljc @@ -9,7 +9,6 @@ [app.common.data :as d] [app.common.files.helpers :as cfh] [app.common.geom.point :as gpt] - [app.common.geom.shapes.bounds :as gsb] [app.common.schema :as sm] [app.common.schema.generators :as sg])) @@ -482,7 +481,13 @@ (if (nil? dest-frame) [(gpt/point 0 0) [:top :left]] - (let [overlay-size (gsb/get-object-bounds objects dest-frame) + (let [;; Use the destination frame selrect (the visible frame box) to compute + ;; the overlay position, not its full object bounds. Bounds include + ;; padding for shadows, blur, strokes and overflowing children, which + ;; would make centered/right/bottom positions off by half that padding + ;; (the visible frame ends up shifted). The viewer reserves the bounds + ;; size and re-aligns the selrect separately (see viewer/calculate-delta). + overlay-size (:selrect dest-frame) base-frame-size (:selrect base-frame) relative-to-shape-size (:selrect relative-to-shape) relative-to-adjusted-to-base-frame {:x (- (:x relative-to-shape-size) (:x base-frame-size)) diff --git a/common/test/common_tests/types/shape_interactions_test.cljc b/common/test/common_tests/types/shape_interactions_test.cljc index da056ae136..7daac0ab8a 100644 --- a/common/test/common_tests/types/shape_interactions_test.cljc +++ b/common/test/common_tests/types/shape_interactions_test.cljc @@ -10,6 +10,7 @@ [app.common.geom.point :as gpt] [app.common.geom.rect :as grc] [app.common.geom.shapes :as gsh] + [app.common.geom.shapes.bounds :as gsb] [app.common.math :as mth] [app.common.types.shape :as cts] [app.common.types.shape.interactions :as ctsi] @@ -1078,3 +1079,49 @@ [overlay-pos snap] (ctsi/calc-overlay-position frame-relative base-frame objects base-frame base-frame overlay-frame frame-offset)] (t/is (= (gpt/point 18 22) overlay-pos)) (t/is (= [:top :left] snap)))))) + +(t/deftest calc-overlay-position-ignores-filter-bounds + ;; Regression for #9048: the overlay position must be computed from the + ;; destination frame selrect (the visible frame box), not from its + ;; filter-inflated object bounds. Shadows, blur, strokes or overflowing + ;; children make get-object-bounds larger than the selrect, which used to + ;; shift centered/right/bottom overlays by half that extra padding (the + ;; overlay appeared offset, e.g. "a bit to the left"). + (let [base-frame (cts/setup-shape {:type :frame :width 100 :height 100}) + overlay-plain (cts/setup-shape {:type :frame :width 30 :height 20}) + ;; same selrect as overlay-plain, but with a drop shadow that widens + ;; and heightens its object bounds well beyond the selrect. + overlay-shadow (-> (cts/setup-shape {:type :frame :width 30 :height 20}) + (assoc :shadow [{:style :drop-shadow + :offset-x 0 :offset-y 0 + :spread 10 :blur 0 :hidden false}])) + objects {(:id base-frame) base-frame + (:id overlay-plain) overlay-plain + (:id overlay-shadow) overlay-shadow} + frame-offset (gpt/point 5 5) + interaction (-> ctsi/default-interaction + (ctsi/set-action-type :open-overlay) + (ctsi/set-position-relative-to (:id base-frame)))] + + ;; Precondition: the shadow really does inflate the object bounds, so the + ;; assertions below are meaningful (otherwise the test would be vacuous). + (t/is (> (:width (gsb/get-object-bounds objects overlay-shadow)) + (:width (:selrect overlay-shadow)))) + (t/is (> (:height (gsb/get-object-bounds objects overlay-shadow)) + (:height (:selrect overlay-shadow)))) + + ;; For every position type that depends on the overlay size, the computed + ;; position must be identical whether or not the destination frame has a + ;; bounds-inflating shadow. + (doseq [pos-type [:center :top-center :top-right :bottom-center :bottom-right]] + (let [i-plain (-> interaction + (ctsi/set-destination (:id overlay-plain)) + (ctsi/set-overlay-pos-type pos-type base-frame objects)) + i-shadow (-> interaction + (ctsi/set-destination (:id overlay-shadow)) + (ctsi/set-overlay-pos-type pos-type base-frame objects)) + [pos-plain snap-plain] (ctsi/calc-overlay-position i-plain base-frame objects base-frame base-frame overlay-plain frame-offset) + [pos-shadow snap-shadow] (ctsi/calc-overlay-position i-shadow base-frame objects base-frame base-frame overlay-shadow frame-offset)] + (t/testing (str "overlay position ignores filter bounds for " pos-type) + (t/is (= pos-plain pos-shadow)) + (t/is (= snap-plain snap-shadow)))))))