mirror of
https://github.com/penpot/penpot.git
synced 2026-06-09 08:52:05 +00:00
🐛 Fix cropped outer stroke of rotated board in view mode
This commit is contained in:
parent
3444c0589f
commit
70e8dbb38a
@ -89,14 +89,23 @@
|
||||
([shape]
|
||||
(get-shape-filter-bounds shape false))
|
||||
([shape ignore-shadow-margin?]
|
||||
(if (or (and (cfh/svg-raw-shape? shape)
|
||||
(not= :svg (dm/get-in shape [:content :tag])))
|
||||
;; If no shadows or blur, we return the selrect as is
|
||||
(and (empty? (-> shape :shadow))
|
||||
(or (nil? (:blur shape))
|
||||
(not= :layer-blur (-> shape :blur :type))
|
||||
(zero? (-> shape :blur :value (or 0))))))
|
||||
(cond
|
||||
;; SVG raw elements (non-root) don't have proper rotated points; use selrect
|
||||
(and (cfh/svg-raw-shape? shape)
|
||||
(not= :svg (dm/get-in shape [:content :tag])))
|
||||
(dm/get-prop shape :selrect)
|
||||
|
||||
;; No shadows or blur: use the axis-aligned bounding box from the actual
|
||||
;; (possibly rotated) points. Using selrect here would be wrong for rotated
|
||||
;; shapes because selrect stores the unrotated rectangle, not the screen-space bbox.
|
||||
(and (empty? (-> shape :shadow))
|
||||
(or (nil? (:blur shape))
|
||||
(not= :layer-blur (-> shape :blur :type))
|
||||
(zero? (-> shape :blur :value (or 0)))))
|
||||
(-> (dm/get-prop shape :points)
|
||||
(grc/points->rect))
|
||||
|
||||
:else
|
||||
(let [filters (shape->filters shape)
|
||||
blur-value (case (-> shape :blur :type)
|
||||
:layer-blur (or (-> shape :blur :value) 0)
|
||||
|
||||
@ -0,0 +1,195 @@
|
||||
{
|
||||
"~:id": "~uaa5cc0bb-91ff-81b9-8004-77dfae2d9e7c",
|
||||
"~:file-id": "~uaa5cc0bb-91ff-81b9-8004-77df9cd3edb1",
|
||||
"~:created-at": "~m1717759268004",
|
||||
"~:data": {
|
||||
"~:options": {},
|
||||
"~:objects": {
|
||||
"~u00000000-0000-0000-0000-000000000000": {
|
||||
"~#shape": {
|
||||
"~:y": 0,
|
||||
"~:hide-fill-on-export": false,
|
||||
"~:transform": {
|
||||
"~#matrix": {
|
||||
"~:a": 1.0,
|
||||
"~:b": 0.0,
|
||||
"~:c": 0.0,
|
||||
"~:d": 1.0,
|
||||
"~:e": 0.0,
|
||||
"~:f": 0.0
|
||||
}
|
||||
},
|
||||
"~:rotation": 0,
|
||||
"~:name": "Root Frame",
|
||||
"~:width": 0.01,
|
||||
"~:type": "~:frame",
|
||||
"~:points": [
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 0,
|
||||
"~:y": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 0.01,
|
||||
"~:y": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 0.01,
|
||||
"~:y": 0.01
|
||||
}
|
||||
},
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 0,
|
||||
"~:y": 0.01
|
||||
}
|
||||
}
|
||||
],
|
||||
"~:proportion-lock": false,
|
||||
"~:transform-inverse": {
|
||||
"~#matrix": {
|
||||
"~:a": 1.0,
|
||||
"~:b": 0.0,
|
||||
"~:c": 0.0,
|
||||
"~:d": 1.0,
|
||||
"~:e": 0.0,
|
||||
"~:f": 0.0
|
||||
}
|
||||
},
|
||||
"~:id": "~u00000000-0000-0000-0000-000000000000",
|
||||
"~:parent-id": "~u00000000-0000-0000-0000-000000000000",
|
||||
"~:frame-id": "~u00000000-0000-0000-0000-000000000000",
|
||||
"~:strokes": [],
|
||||
"~:x": 0,
|
||||
"~:proportion": 1.0,
|
||||
"~:selrect": {
|
||||
"~#rect": {
|
||||
"~:x": 0,
|
||||
"~:y": 0,
|
||||
"~:width": 0.01,
|
||||
"~:height": 0.01,
|
||||
"~:x1": 0,
|
||||
"~:y1": 0,
|
||||
"~:x2": 0.01,
|
||||
"~:y2": 0.01
|
||||
}
|
||||
},
|
||||
"~:fills": [
|
||||
{
|
||||
"~:fill-color": "#FFFFFF",
|
||||
"~:fill-opacity": 1
|
||||
}
|
||||
],
|
||||
"~:flip-x": null,
|
||||
"~:height": 0.01,
|
||||
"~:flip-y": null,
|
||||
"~:shapes": [
|
||||
"~ubc508673-9e3b-80bf-8004-77dfa30a2b13"
|
||||
]
|
||||
}
|
||||
},
|
||||
"~ubc508673-9e3b-80bf-8004-77dfa30a2b13": {
|
||||
"~#shape": {
|
||||
"~:y": 100,
|
||||
"~:hide-fill-on-export": false,
|
||||
"~:transform": {
|
||||
"~#matrix": {
|
||||
"~:a": 0.5735764363510460,
|
||||
"~:b": 0.8191520442889918,
|
||||
"~:c": -0.8191520442889918,
|
||||
"~:d": 0.5735764363510460,
|
||||
"~:e": 0.0,
|
||||
"~:f": 0.0
|
||||
}
|
||||
},
|
||||
"~:rotation": 55,
|
||||
"~:grow-type": "~:fixed",
|
||||
"~:hide-in-viewer": false,
|
||||
"~:name": "Board",
|
||||
"~:width": 80,
|
||||
"~:type": "~:frame",
|
||||
"~:points": [
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 166.208,
|
||||
"~:y": 92.816
|
||||
}
|
||||
},
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 212.096,
|
||||
"~:y": 158.352
|
||||
}
|
||||
},
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 113.792,
|
||||
"~:y": 227.184
|
||||
}
|
||||
},
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 67.904,
|
||||
"~:y": 161.648
|
||||
}
|
||||
}
|
||||
],
|
||||
"~:proportion-lock": false,
|
||||
"~:transform-inverse": {
|
||||
"~#matrix": {
|
||||
"~:a": 0.5735764363510460,
|
||||
"~:b": -0.8191520442889918,
|
||||
"~:c": 0.8191520442889918,
|
||||
"~:d": 0.5735764363510460,
|
||||
"~:e": 0.0,
|
||||
"~:f": 0.0
|
||||
}
|
||||
},
|
||||
"~:id": "~ubc508673-9e3b-80bf-8004-77dfa30a2b13",
|
||||
"~:parent-id": "~u00000000-0000-0000-0000-000000000000",
|
||||
"~:frame-id": "~u00000000-0000-0000-0000-000000000000",
|
||||
"~:strokes": [
|
||||
{
|
||||
"~:stroke-color": "#FF0000",
|
||||
"~:stroke-opacity": 1,
|
||||
"~:stroke-style": "~:solid",
|
||||
"~:stroke-alignment": "~:outer",
|
||||
"~:stroke-width": 20
|
||||
}
|
||||
],
|
||||
"~:x": 100,
|
||||
"~:proportion": 1,
|
||||
"~:selrect": {
|
||||
"~#rect": {
|
||||
"~:x": 100,
|
||||
"~:y": 100,
|
||||
"~:width": 80,
|
||||
"~:height": 120,
|
||||
"~:x1": 100,
|
||||
"~:y1": 100,
|
||||
"~:x2": 180,
|
||||
"~:y2": 220
|
||||
}
|
||||
},
|
||||
"~:fills": [
|
||||
{
|
||||
"~:fill-color": "#FFFFFF",
|
||||
"~:fill-opacity": 1
|
||||
}
|
||||
],
|
||||
"~:flip-x": null,
|
||||
"~:height": 120,
|
||||
"~:flip-y": null,
|
||||
"~:shapes": [],
|
||||
"~:show-content": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"~:id": "~uaa5cc0bb-91ff-81b9-8004-77df9cd3edb2",
|
||||
"~:name": "Page 1"
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,86 @@
|
||||
{
|
||||
"~:users": [
|
||||
{
|
||||
"~:id": "~u0515a066-e303-8169-8004-73eb4018f4e0",
|
||||
"~:email": "leia@example.com",
|
||||
"~:name": "Princesa Leia",
|
||||
"~:fullname": "Princesa Leia",
|
||||
"~:is-active": true
|
||||
}
|
||||
],
|
||||
"~:fonts": [],
|
||||
"~:project": {
|
||||
"~:id": "~u0515a066-e303-8169-8004-73eb401b5d55",
|
||||
"~:name": "Drafts",
|
||||
"~:team-id": "~u0515a066-e303-8169-8004-73eb401977a6"
|
||||
},
|
||||
"~:share-links": [],
|
||||
"~:libraries": [],
|
||||
"~:file": {
|
||||
"~:features": {
|
||||
"~#set": [
|
||||
"layout/grid",
|
||||
"styles/v2",
|
||||
"fdata/pointer-map",
|
||||
"fdata/objects-map",
|
||||
"components/v2",
|
||||
"fdata/shape-data-type"
|
||||
]
|
||||
},
|
||||
"~:has-media-trimmed": false,
|
||||
"~:comment-thread-seqn": 0,
|
||||
"~:name": "Rotated Board Stroke Test",
|
||||
"~:revn": 1,
|
||||
"~:modified-at": "~m1717759268010",
|
||||
"~:id": "~uaa5cc0bb-91ff-81b9-8004-77df9cd3edb1",
|
||||
"~:is-shared": false,
|
||||
"~:version": 48,
|
||||
"~:project-id": "~u0515a066-e303-8169-8004-73eb401b5d55",
|
||||
"~:created-at": "~m1717759250257",
|
||||
"~:data": {
|
||||
"~:id": "~uaa5cc0bb-91ff-81b9-8004-77df9cd3edb1",
|
||||
"~:options": {
|
||||
"~:components-v2": true
|
||||
},
|
||||
"~:pages": [
|
||||
"~uaa5cc0bb-91ff-81b9-8004-77df9cd3edb2"
|
||||
],
|
||||
"~:pages-index": {
|
||||
"~uaa5cc0bb-91ff-81b9-8004-77df9cd3edb2": {
|
||||
"~#penpot/pointer": [
|
||||
"~uaa5cc0bb-91ff-81b9-8004-77dfae2d9e7c",
|
||||
{
|
||||
"~:created-at": "~m1717759268024"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"~:team": {
|
||||
"~:id": "~u0515a066-e303-8169-8004-73eb401977a6",
|
||||
"~:created-at": "~m1717493865581",
|
||||
"~:modified-at": "~m1717493865581",
|
||||
"~:name": "Default",
|
||||
"~:is-default": true,
|
||||
"~:features": {
|
||||
"~#set": [
|
||||
"layout/grid",
|
||||
"styles/v2",
|
||||
"fdata/pointer-map",
|
||||
"fdata/objects-map",
|
||||
"components/v2",
|
||||
"fdata/shape-data-type"
|
||||
]
|
||||
}
|
||||
},
|
||||
"~:permissions": {
|
||||
"~:type": "~:membership",
|
||||
"~:is-owner": true,
|
||||
"~:is-admin": true,
|
||||
"~:can-edit": true,
|
||||
"~:can-read": true,
|
||||
"~:is-logged": true,
|
||||
"~:in-team": true
|
||||
}
|
||||
}
|
||||
@ -71,6 +71,21 @@ export class ViewerPage extends BaseWebSocketPage {
|
||||
);
|
||||
}
|
||||
|
||||
async setupFileWithRotatedBoardStroke() {
|
||||
await this.mockRPC(
|
||||
/get\-view\-only\-bundle\?/,
|
||||
"viewer/get-view-only-bundle-rotated-board-stroke.json",
|
||||
);
|
||||
await this.mockRPC(
|
||||
"get-comment-threads?file-id=*",
|
||||
"workspace/get-comment-threads-empty.json",
|
||||
);
|
||||
await this.mockRPC(
|
||||
"get-file-fragment?file-id=*&fragment-id=*",
|
||||
"viewer/get-file-fragment-rotated-board-stroke.json",
|
||||
);
|
||||
}
|
||||
|
||||
async setupFileWithComments() {
|
||||
await this.mockRPC(
|
||||
/get\-view\-only\-bundle\?/,
|
||||
|
||||
@ -0,0 +1,47 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { ViewerPage } from "../pages/ViewerPage";
|
||||
|
||||
// Issue 8257: outer stroke of a rotated board is cropped in View Mode.
|
||||
// The SVG viewport must be large enough to contain the stroke of a rotated board.
|
||||
// A 55° rotated board (80×120) with a 20px outer stroke has a rotated bounding box
|
||||
// of ~144×134px. The viewport must be at least ~202×192px (bbox + stroke margin).
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await ViewerPage.init(page);
|
||||
});
|
||||
|
||||
const rotatedBoardFileId = "aa5cc0bb-91ff-81b9-8004-77df9cd3edb1";
|
||||
const rotatedBoardPageId = "aa5cc0bb-91ff-81b9-8004-77df9cd3edb2";
|
||||
|
||||
test("Viewer shows full outer stroke of a rotated board without clipping", async ({
|
||||
page,
|
||||
}) => {
|
||||
const viewer = new ViewerPage(page);
|
||||
await viewer.setupLoggedInUser();
|
||||
await viewer.setupFileWithRotatedBoardStroke();
|
||||
|
||||
await viewer.goToViewer({
|
||||
fileId: rotatedBoardFileId,
|
||||
pageId: rotatedBoardPageId,
|
||||
});
|
||||
|
||||
// Wait for the viewer SVG to be rendered
|
||||
const svg = page.locator("svg[class*='not-fixed']").first();
|
||||
await expect(svg).toBeVisible();
|
||||
|
||||
// The SVG viewBox must be large enough to contain the rotated board plus its
|
||||
// 20px outer stroke. For a 55° rotated board (80×120):
|
||||
// - The axis-aligned bounding box of the rotated frame is ~144×134px
|
||||
// - The outer stroke (20px) adds sqrt(2)*20 ≈ 29px margin on each side
|
||||
// - So the viewport must be at least ~202×192px
|
||||
//
|
||||
// Before the fix, the viewer used the unrotated selrect (80×120) as the viewport,
|
||||
// causing the stroke to be heavily clipped.
|
||||
const viewBox = await svg.getAttribute("viewBox");
|
||||
const [, , vbWidth, vbHeight] = viewBox.split(" ").map(Number);
|
||||
|
||||
// The unrotated selrect is 80×120. If the viewport is close to those dimensions,
|
||||
// the stroke is being clipped (bug). The fixed viewport should be much larger.
|
||||
expect(vbWidth).toBeGreaterThan(150);
|
||||
expect(vbHeight).toBeGreaterThan(150);
|
||||
});
|
||||
@ -53,9 +53,9 @@
|
||||
|
||||
(defn- calculate-size
|
||||
"Calculate the total size we must reserve for the frame, including possible paddings
|
||||
added because shadows or blur."
|
||||
added because shadows, blur, or strokes."
|
||||
[objects frame zoom]
|
||||
(let [{:keys [x y width height]} (gsb/get-object-bounds objects frame)]
|
||||
(let [{:keys [x y width height]} (gsb/get-object-bounds objects frame {:ignore-margin? false})]
|
||||
{:base-width width
|
||||
:base-height height
|
||||
:x x
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user