mirror of
https://github.com/penpot/penpot.git
synced 2026-09-18 09:56:15 +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]
|
([shape]
|
||||||
(get-shape-filter-bounds shape false))
|
(get-shape-filter-bounds shape false))
|
||||||
([shape ignore-shadow-margin?]
|
([shape ignore-shadow-margin?]
|
||||||
(if (or (and (cfh/svg-raw-shape? shape)
|
(cond
|
||||||
(not= :svg (dm/get-in shape [:content :tag])))
|
;; SVG raw elements (non-root) don't have proper rotated points; use selrect
|
||||||
;; If no shadows or blur, we return the selrect as is
|
(and (cfh/svg-raw-shape? shape)
|
||||||
(and (empty? (-> shape :shadow))
|
(not= :svg (dm/get-in shape [:content :tag])))
|
||||||
(or (nil? (:blur shape))
|
|
||||||
(not= :layer-blur (-> shape :blur :type))
|
|
||||||
(zero? (-> shape :blur :value (or 0))))))
|
|
||||||
(dm/get-prop shape :selrect)
|
(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)
|
(let [filters (shape->filters shape)
|
||||||
blur-value (case (-> shape :blur :type)
|
blur-value (case (-> shape :blur :type)
|
||||||
:layer-blur (or (-> shape :blur :value) 0)
|
: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() {
|
async setupFileWithComments() {
|
||||||
await this.mockRPC(
|
await this.mockRPC(
|
||||||
/get\-view\-only\-bundle\?/,
|
/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
|
(defn- calculate-size
|
||||||
"Calculate the total size we must reserve for the frame, including possible paddings
|
"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]
|
[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-width width
|
||||||
:base-height height
|
:base-height height
|
||||||
:x x
|
:x x
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user