From 1ab1d4f6ca27aa6819d32e5e09314e117a647dad Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Fri, 13 Mar 2026 12:18:14 +0100 Subject: [PATCH] :bug: Fix problem with snap pixel transforms --- .../workspace/get-file-13272-fragment.json | 20 +++ .../data/workspace/get-file-13272.json | 135 ++++++++++++++++++ .../ui/specs/workspace-modifers.spec.js | 31 +++- .../app/main/data/workspace/modifiers.cljs | 5 +- render-wasm/src/shapes/modifiers.rs | 18 +-- render-wasm/src/shapes/transform.rs | 6 +- 6 files changed, 200 insertions(+), 15 deletions(-) create mode 100644 frontend/playwright/data/workspace/get-file-13272-fragment.json create mode 100644 frontend/playwright/data/workspace/get-file-13272.json diff --git a/frontend/playwright/data/workspace/get-file-13272-fragment.json b/frontend/playwright/data/workspace/get-file-13272-fragment.json new file mode 100644 index 0000000000..6510f95e6e --- /dev/null +++ b/frontend/playwright/data/workspace/get-file-13272-fragment.json @@ -0,0 +1,20 @@ +{ + "~:file-id": "~u3b9773cc-d4f1-81e1-8007-b3f8dcaba770", + "~:id": "~u3ac58b88-38b3-80c9-8007-b4f791c7c36b", + "~:created-at": "~m1773398778658", + "~:modified-at": "~m1773398778658", + "~:type": "fragment", + "~:backend": "db", + "~:data": { + "~:objects": { + "~#penpot/objects-map/v2": { + "~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.0,\"~:y\",0.0]],[\"^:\",[\"^ \",\"~:x\",0.01,\"~:y\",0.0]],[\"^:\",[\"^ \",\"~:x\",0.01,\"~:y\",0.01]],[\"^:\",[\"^ \",\"~:x\",0.0,\"~:y\",0.01]]],\"~:r2\",0,\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^3\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:r3\",0,\"~:r1\",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,\"~:r4\",0,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",0,\"~:y\",0,\"^6\",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,\"^H\",0.01,\"~:flip-y\",null,\"~:shapes\",[\"~ub8c8efc9-e8a3-8018-8007-b4f6cd99ad3a\"]]]", + "~ub8c8efc9-e8a3-8018-8007-b4f6c15a473a": "[\"~#shape\",[\"^ \",\"~:y\",0,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:grow-type\",\"~:fixed\",\"~:hide-in-viewer\",false,\"~:name\",\"Rectangle\",\"~:width\",76,\"~:type\",\"~:rect\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",0,\"~:y\",0]],[\"^<\",[\"^ \",\"~:x\",76,\"~:y\",0]],[\"^<\",[\"^ \",\"~:x\",76,\"~:y\",59]],[\"^<\",[\"^ \",\"~:x\",0,\"~:y\",59]]],\"~:r2\",0,\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:r3\",0,\"~:r1\",0,\"~:id\",\"~ub8c8efc9-e8a3-8018-8007-b4f6c15a473a\",\"~:parent-id\",\"~ub8c8efc9-e8a3-8018-8007-b4f6cd99ad3a\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",0,\"~:proportion\",1,\"~:r4\",0,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",0,\"~:y\",0,\"^8\",76,\"~:height\",59,\"~:x1\",0,\"~:y1\",0,\"~:x2\",76,\"~:y2\",59]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#B1B2B5\",\"~:fill-opacity\",1]],\"~:flip-x\",null,\"^J\",59,\"~:flip-y\",null]]", + "~ub8c8efc9-e8a3-8018-8007-b4f6c7677f74": "[\"~#shape\",[\"^ \",\"~:y\",74,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:grow-type\",\"~:fixed\",\"~:hide-in-viewer\",false,\"~:name\",\"Rectangle\",\"~:width\",95,\"~:type\",\"~:rect\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",57,\"~:y\",74]],[\"^<\",[\"^ \",\"~:x\",152,\"~:y\",74]],[\"^<\",[\"^ \",\"~:x\",152,\"~:y\",123]],[\"^<\",[\"^ \",\"~:x\",57,\"~:y\",123]]],\"~:r2\",0,\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:r3\",0,\"~:r1\",0,\"~:id\",\"~ub8c8efc9-e8a3-8018-8007-b4f6c7677f74\",\"~:parent-id\",\"~ub8c8efc9-e8a3-8018-8007-b4f6cd99ad3a\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",57,\"~:proportion\",1,\"~:r4\",0,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",57,\"~:y\",74,\"^8\",95,\"~:height\",49,\"~:x1\",57,\"~:y1\",74,\"~:x2\",152,\"~:y2\",123]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#B1B2B5\",\"~:fill-opacity\",1]],\"~:flip-x\",null,\"^J\",49,\"~:flip-y\",null]]", + "~ub8c8efc9-e8a3-8018-8007-b4f6cd99ad3a": "[\"~#shape\",[\"^ \",\"~:y\",0,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:index\",2,\"~:name\",\"Group\",\"~:width\",152,\"~:type\",\"~:group\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",0,\"~:y\",0]],[\"^:\",[\"^ \",\"~:x\",152,\"~:y\",0]],[\"^:\",[\"^ \",\"~:x\",152,\"~:y\",123]],[\"^:\",[\"^ \",\"~:x\",0,\"~:y\",123]]],\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:id\",\"~ub8c8efc9-e8a3-8018-8007-b4f6cd99ad3a\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",0,\"~:proportion\",1,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",0,\"~:y\",0,\"^6\",152,\"~:height\",123,\"~:x1\",0,\"~:y1\",0,\"~:x2\",152,\"~:y2\",123]],\"~:fills\",[],\"~:flip-x\",null,\"^D\",123,\"~:flip-y\",null,\"~:shapes\",[\"~ub8c8efc9-e8a3-8018-8007-b4f6c15a473a\",\"~ub8c8efc9-e8a3-8018-8007-b4f6c7677f74\"]]]" + } + }, + "~:id": "~u3b9773cc-d4f1-81e1-8007-b3f8dcaba771", + "~:name": "Page 1" + } +} diff --git a/frontend/playwright/data/workspace/get-file-13272.json b/frontend/playwright/data/workspace/get-file-13272.json new file mode 100644 index 0000000000..50730bfc7e --- /dev/null +++ b/frontend/playwright/data/workspace/get-file-13272.json @@ -0,0 +1,135 @@ +{ + "~:features": { + "~#set": [ + "fdata/path-data", + "plugins/runtime", + "design-tokens/v1", + "variants/v1", + "layout/grid", + "styles/v2", + "fdata/pointer-map", + "fdata/objects-map", + "render-wasm/v1", + "components/v2", + "fdata/shape-data-type" + ] + }, + "~:team-id": "~ud715d0a5-a44e-8056-8005-a79999e18b64", + "~:permissions": { + "~:type": "~:membership", + "~:is-owner": true, + "~:is-admin": true, + "~:can-edit": true, + "~:can-read": true, + "~:is-logged": true + }, + "~:has-media-trimmed": false, + "~:comment-thread-seqn": 0, + "~:name": "New File 13", + "~:revn": 79, + "~:modified-at": "~m1773398778654", + "~:vern": 0, + "~:id": "~u3b9773cc-d4f1-81e1-8007-b3f8dcaba770", + "~:is-shared": false, + "~:migrations": { + "~#ordered-set": [ + "legacy-2", + "legacy-3", + "legacy-5", + "legacy-6", + "legacy-7", + "legacy-8", + "legacy-9", + "legacy-10", + "legacy-11", + "legacy-12", + "legacy-13", + "legacy-14", + "legacy-16", + "legacy-17", + "legacy-18", + "legacy-19", + "legacy-25", + "legacy-26", + "legacy-27", + "legacy-28", + "legacy-29", + "legacy-31", + "legacy-32", + "legacy-33", + "legacy-34", + "legacy-36", + "legacy-37", + "legacy-38", + "legacy-39", + "legacy-40", + "legacy-41", + "legacy-42", + "legacy-43", + "legacy-44", + "legacy-45", + "legacy-46", + "legacy-47", + "legacy-48", + "legacy-49", + "legacy-50", + "legacy-51", + "legacy-52", + "legacy-53", + "legacy-54", + "legacy-55", + "legacy-56", + "legacy-57", + "legacy-59", + "legacy-62", + "legacy-65", + "legacy-66", + "legacy-67", + "0001-remove-tokens-from-groups", + "0002-normalize-bool-content-v2", + "0002-clean-shape-interactions", + "0003-fix-root-shape", + "0003-convert-path-content-v2", + "0005-deprecate-image-type", + "0006-fix-old-texts-fills", + "0008-fix-library-colors-v4", + "0009-clean-library-colors", + "0009-add-partial-text-touched-flags", + "0010-fix-swap-slots-pointing-non-existent-shapes", + "0011-fix-invalid-text-touched-flags", + "0012-fix-position-data", + "0013-fix-component-path", + "0013-clear-invalid-strokes-and-fills", + "0014-fix-tokens-lib-duplicate-ids", + "0014-clear-components-nil-objects", + "0015-fix-text-attrs-blank-strings", + "0015-clean-shadow-color", + "0016-copy-fills-from-position-data-to-text-node", + "0017-fix-layout-flex-dir" + ] + }, + "~:version": 67, + "~:project-id": "~u76eab896-accf-81a5-8007-2b264ebe7817", + "~:created-at": "~m1773332008622", + "~:backend": "legacy-db", + "~:data": { + "~:pages": [ + "~u3b9773cc-d4f1-81e1-8007-b3f8dcaba771" + ], + "~:pages-index": { + "~u3b9773cc-d4f1-81e1-8007-b3f8dcaba771": { + "~#penpot/pointer": [ + "~u3ac58b88-38b3-80c9-8007-b4f791c7c36b", + { + "~:created-at": "~m1773398778656" + } + ] + } + }, + "~:id": "~u3b9773cc-d4f1-81e1-8007-b3f8dcaba770", + "~:options": { + "~:components-v2": true, + "~:base-font-size": "16px" + } + } +} diff --git a/frontend/playwright/ui/specs/workspace-modifers.spec.js b/frontend/playwright/ui/specs/workspace-modifers.spec.js index 448b620330..4b0fbb6f87 100644 --- a/frontend/playwright/ui/specs/workspace-modifers.spec.js +++ b/frontend/playwright/ui/specs/workspace-modifers.spec.js @@ -72,7 +72,7 @@ test("BUG 13468 - Fix problem with flex propagation", async ({ page }) => { fileId: "3a4d7ec7-c391-8146-8007-9a05c41da6b9", pageId: "95b23c15-79f9-81ba-8007-99d81b5290dd", }); -0 + await workspacePage.clickToggableLayer("Parent"); await workspacePage.clickToggableLayer("Container"); @@ -82,4 +82,33 @@ test("BUG 13468 - Fix problem with flex propagation", async ({ page }) => { await expect(workspacePage.rightSidebar.getByTitle("Height").getByRole("textbox")).toHaveValue("76"); }); +test("BUG 13272 - Fix problem with snap to pixel", async ({ page }) => { + const workspacePage = new WasmWorkspacePage(page); + await workspacePage.setupEmptyFile(); + await workspacePage.mockGetFile("workspace/get-file-13272.json"); + await workspacePage.mockRPC( + "get-file-fragment?file-id=*&fragment-id=*", + "workspace/get-file-13272-fragment.json", + ); + + await workspacePage.mockRPC("update-file?id=*", "workspace/update-file-empty.json"); + + await workspacePage.goToWorkspace({ + fileId: "3b9773cc-d4f1-81e1-8007-b3f8dcaba770", + pageId: "3b9773cc-d4f1-81e1-8007-b3f8dcaba771", + }); + + await workspacePage.clickToggableLayer("Group"); + await workspacePage.clickLeafLayer("Group"); + + await workspacePage.page.locator('g:nth-child(11) > .cursor-resize-nesw-0').hover(); + await workspacePage.page.mouse.down(); + + await workspacePage.page.mouse.move(1200, 800); + await workspacePage.page.mouse.up(); + + await workspacePage.clickLeafLayer("Rectangle"); + await expect(workspacePage.rightSidebar.getByTitle("Width").getByRole("textbox")).toHaveValue("197.5"); + await expect(workspacePage.rightSidebar.getByTitle("Height").getByRole("textbox")).toHaveValue("128.28"); +}); diff --git a/frontend/src/app/main/data/workspace/modifiers.cljs b/frontend/src/app/main/data/workspace/modifiers.cljs index f8be9f857b..c7181abbaf 100644 --- a/frontend/src/app/main/data/workspace/modifiers.cljs +++ b/frontend/src/app/main/data/workspace/modifiers.cljs @@ -628,12 +628,13 @@ (let [prev-wasm-props (:prev-wasm-props state) wasm-props (:wasm-props state) objects (dsh/lookup-page-objects state) - pixel-precision false] + snap-pixel? + (and (not ignore-snap-pixel) (contains? (:workspace-layout state) :snap-pixel-grid))] (set-wasm-props! objects prev-wasm-props wasm-props) (let [structure-entries (parse-structure-modifiers modif-tree)] (wasm.api/set-structure-modifiers structure-entries) (let [geometry-entries (parse-geometry-modifiers modif-tree) - modifiers (wasm.api/propagate-modifiers geometry-entries pixel-precision)] + modifiers (wasm.api/propagate-modifiers geometry-entries snap-pixel?)] (wasm.api/set-modifiers modifiers) (let [ids (into [] xf:map-key geometry-entries) selrect (wasm.api/get-selection-rect ids)] diff --git a/render-wasm/src/shapes/modifiers.rs b/render-wasm/src/shapes/modifiers.rs index 0f44d69ae5..9713c066a9 100644 --- a/render-wasm/src/shapes/modifiers.rs +++ b/render-wasm/src/shapes/modifiers.rs @@ -133,21 +133,21 @@ fn set_pixel_precision(transform: &mut Matrix, bounds: &mut Bounds) { let width = bounds.width(); let height = bounds.height(); + let target_width = bounds.width().round(); + let target_height = bounds.height().round(); + let scale_width = if width > 0.1 { - f32::max(0.01, bounds.width().round() / bounds.width()) + f32::max(0.01, target_width / width) } else { 1.0 }; let scale_height = if height > 0.1 { - f32::max(0.01, bounds.height().round() / bounds.height()) + f32::max(0.01, target_height / height) } else { 1.0 }; - if f32::is_finite(scale_width) - && f32::is_finite(scale_height) - && (!math::is_close_to(scale_width, 1.0) || !math::is_close_to(scale_height, 1.0)) - { + if f32::is_finite(scale_width) && f32::is_finite(scale_height) { let mut round_transform = Matrix::scale((scale_width, scale_height)); round_transform.post_concat(&tr); round_transform.pre_concat(&tr_inv); @@ -373,7 +373,7 @@ pub fn propagate_modifiers( if math::identitish(&entry.transform) { Modifier::Reflow(entry.id, false) } else { - Modifier::Transform(*entry) + Modifier::Transform(*entry, pixel_precision) } }) .collect(); @@ -392,9 +392,9 @@ pub fn propagate_modifiers( while !entries.is_empty() { while let Some(modifier) = entries.pop_front() { match modifier { - Modifier::Transform(entry) => propagate_transform( + Modifier::Transform(entry, pixel) => propagate_transform( entry, - pixel_precision, + pixel, state, &mut entries, &mut bounds, diff --git a/render-wasm/src/shapes/transform.rs b/render-wasm/src/shapes/transform.rs index 7e6200c0cb..b0ff2a52d0 100644 --- a/render-wasm/src/shapes/transform.rs +++ b/render-wasm/src/shapes/transform.rs @@ -7,16 +7,16 @@ use skia::Matrix; #[derive(PartialEq, Debug, Clone)] pub enum Modifier { - Transform(TransformEntry), + Transform(TransformEntry, bool), Reflow(Uuid, bool), } impl Modifier { pub fn transform_propagate(id: Uuid, transform: Matrix) -> Self { - Modifier::Transform(TransformEntry::from_propagate(id, transform)) + Modifier::Transform(TransformEntry::from_propagate(id, transform), false) } pub fn parent(id: Uuid, transform: Matrix) -> Self { - Modifier::Transform(TransformEntry::parent(id, transform)) + Modifier::Transform(TransformEntry::parent(id, transform), false) } pub fn reflow(id: Uuid, force_reflow: bool) -> Self { Modifier::Reflow(id, force_reflow)