mirror of
https://github.com/penpot/penpot.git
synced 2026-09-08 11:54:36 -04:00
Compare commits
1
Commits
develop
...
issue-10751
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
60b278f8fa |
No files matched your search
@@ -703,9 +703,9 @@
|
||||
|
||||
#_:clj-kondo/ignore
|
||||
(defn set-wasm-modifiers
|
||||
[modif-tree & {:keys [ignore-constraints ignore-snap-pixel
|
||||
[modif-tree & {:keys [ignore-constraints ignore-snap-pixel snap-ignore-axis
|
||||
subtree-ids-by-id selection-rect-cache]
|
||||
:or {ignore-constraints false ignore-snap-pixel false}
|
||||
:or {ignore-constraints false ignore-snap-pixel false snap-ignore-axis nil}
|
||||
:as params}]
|
||||
(let [modif-tree (without-nil-ids modif-tree)]
|
||||
(ptk/reify ::set-wasm-modifiers
|
||||
@@ -756,7 +756,7 @@
|
||||
root-modifiers
|
||||
|
||||
:else
|
||||
(let [propagated (wasm.api/propagate-modifiers geometry-entries snap-pixel?)]
|
||||
(let [propagated (wasm.api/propagate-modifiers geometry-entries snap-pixel? snap-ignore-axis)]
|
||||
(if (seq propagated) propagated root-modifiers)))]
|
||||
(when wasm-ready?
|
||||
(wasm.api/set-modifiers modifiers))
|
||||
@@ -831,10 +831,8 @@
|
||||
;; primaries and descendants would snap back to their
|
||||
;; pre-drag positions on drop.
|
||||
;;
|
||||
;; Skipped when `snap-pixel?` is on: WASM applies
|
||||
;; per-shape pixel correction (different scale/translate
|
||||
;; per descendant) which we can't replicate cheaply on
|
||||
;; the CLJS side.
|
||||
;; Only without `snap-pixel?`: the delta that lands
|
||||
;; the shape on the pixel grid is known to WASM alone.
|
||||
(reduce
|
||||
(fn [acc [id data]]
|
||||
(let [t (:transform data)
|
||||
@@ -864,7 +862,7 @@
|
||||
geometry-entries))
|
||||
|
||||
:else
|
||||
(into {} (wasm.api/propagate-modifiers geometry-entries snap-pixel?)))
|
||||
(into {} (wasm.api/propagate-modifiers geometry-entries snap-pixel? snap-ignore-axis)))
|
||||
|
||||
ignore-tree
|
||||
(calculate-ignore-tree-wasm transforms objects)
|
||||
|
||||
@@ -592,12 +592,14 @@
|
||||
(rx/merge
|
||||
(->> angle-stream
|
||||
(rx/sample mconst/rotation-sample-time)
|
||||
(rx/map #(dwm/set-wasm-modifiers (rotation-modifiers % shapes group-center)))
|
||||
(rx/map #(dwm/set-wasm-modifiers (rotation-modifiers % shapes group-center)
|
||||
:ignore-snap-pixel true))
|
||||
(rx/take-until stopper))
|
||||
(->> angle-stream
|
||||
(rx/take-until stopper)
|
||||
(rx/last)
|
||||
(rx/map #(dwm/apply-wasm-modifiers (rotation-modifiers % shapes group-center)))))
|
||||
(rx/map #(dwm/apply-wasm-modifiers (rotation-modifiers % shapes group-center)
|
||||
:ignore-snap-pixel true))))
|
||||
|
||||
(rx/of (finish-transform)))
|
||||
|
||||
@@ -638,7 +640,9 @@
|
||||
modif-tree
|
||||
(dwm/build-modif-tree ids objects get-modifier)]
|
||||
|
||||
(rx/of (dwm/apply-wasm-modifiers modif-tree :ignore-touched (:ignore-touched options))))
|
||||
(rx/of (dwm/apply-wasm-modifiers modif-tree
|
||||
:ignore-touched (:ignore-touched options)
|
||||
:ignore-snap-pixel true)))
|
||||
|
||||
(let [page-id (or (:page-id options)
|
||||
(:current-page-id state))
|
||||
|
||||
@@ -2098,13 +2098,34 @@
|
||||
|
||||
(h/call wasm/internal-module "_set_structure_modifiers"))))
|
||||
|
||||
;; Axes the pixel grid rounds, as `propagate_modifiers` expects them.
|
||||
(def ^:private pixel-precision
|
||||
{:disabled 0
|
||||
:both 1
|
||||
:only-x 2
|
||||
:only-y 3})
|
||||
|
||||
(defn- pixel-precision-mode
|
||||
"Encodes the pixel grid snapping for the renderer. `snap-ignore-axis`
|
||||
names the axis to leave alone (`:x`, `:y` or nil)."
|
||||
[snap-pixel? snap-ignore-axis]
|
||||
(pixel-precision
|
||||
(cond
|
||||
(not snap-pixel?) :disabled
|
||||
(= :x snap-ignore-axis) :only-y
|
||||
(= :y snap-ignore-axis) :only-x
|
||||
:else :both)))
|
||||
|
||||
(defn propagate-modifiers
|
||||
"Propagates geometry modifiers through the WASM shape tree.
|
||||
|
||||
Rounds the resulting geometry to the pixel grid when `snap-pixel?` is set,
|
||||
skipping the axis named by `snap-ignore-axis` (`:x`, `:y` or nil).
|
||||
|
||||
Always returns a vector. When the context is not ready (lost / mid-reload)
|
||||
or `entries` is empty, returns `[]` so callers never receive `nil` (which
|
||||
would trip `set-modifiers`' vector assert)."
|
||||
[entries pixel-precision]
|
||||
[entries snap-pixel? snap-ignore-axis]
|
||||
(if-not (and (initialized?) (not ^boolean (empty? entries)))
|
||||
[]
|
||||
(let [heapf32 (mem/get-heap-f32)
|
||||
@@ -2122,7 +2143,8 @@
|
||||
offset
|
||||
entries)
|
||||
|
||||
(let [offset (-> (h/call wasm/internal-module "_propagate_modifiers" pixel-precision)
|
||||
(let [precision (pixel-precision-mode snap-pixel? snap-ignore-axis)
|
||||
offset (-> (h/call wasm/internal-module "_propagate_modifiers" precision)
|
||||
(mem/->offset-32))
|
||||
length (aget heapu32 offset)
|
||||
max-offset (+ offset 1 (* length MODIFIER-U32-SIZE))
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
This effectively tells the caller \"apply exactly the transform that
|
||||
was requested\", which is what the real WASM engine does for simple
|
||||
moves / resizes without constraints."
|
||||
[entries _pixel-precision]
|
||||
[entries _snap-pixel? _snap-ignore-axis]
|
||||
(track! :propagate-modifiers)
|
||||
(when (d/not-empty? entries)
|
||||
(into []
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
the real implementations."
|
||||
[]
|
||||
(set! wasm.api/propagate-modifiers
|
||||
(fn [entries _pixel-precision]
|
||||
(fn [entries _snap-pixel? _snap-ignore-axis]
|
||||
(swap! captured-geometry-entries into entries)
|
||||
(into []
|
||||
(map (fn [[id data]] [id (:transform data)]))
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
;; 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 SUBSIDIARY SL
|
||||
|
||||
(ns frontend-tests.logic.wasm-pixel-snap-test
|
||||
"Covers which pixel-grid snapping options reach the WASM renderer.
|
||||
|
||||
The rounding happens in Rust, so these tests assert on the arguments
|
||||
crossing the bridge: which axis an axis-locked drag leaves alone, and
|
||||
that rotation does not snap."
|
||||
(:require
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.test-helpers.compositions :as ctho]
|
||||
[app.common.test-helpers.files :as cthf]
|
||||
[app.common.test-helpers.ids-map :as cthi]
|
||||
[app.common.test-helpers.shapes :as cths]
|
||||
[app.common.types.modifiers :as ctm]
|
||||
[app.main.data.workspace.modifiers :as dwm]
|
||||
[app.main.data.workspace.transforms :as dwt]
|
||||
[app.render-wasm.api :as wasm.api]
|
||||
[cljs.test :as t :include-macros true]
|
||||
[frontend-tests.helpers.state :as ths]
|
||||
[frontend-tests.helpers.wasm :as thw]))
|
||||
|
||||
(def ^:private captured-snap-options
|
||||
"One entry per `wasm.api/propagate-modifiers` call during a test."
|
||||
(atom []))
|
||||
|
||||
(defn- install-capturing-spy!
|
||||
"Records the snap options of every propagation. Must run after
|
||||
`thw/setup-wasm-mocks!` so teardown restores the real implementation."
|
||||
[]
|
||||
(set! wasm.api/propagate-modifiers
|
||||
(fn [entries snap-pixel? snap-ignore-axis]
|
||||
(swap! captured-snap-options conj
|
||||
{:snap-pixel? snap-pixel? :snap-ignore-axis snap-ignore-axis})
|
||||
(into []
|
||||
(map (fn [[id data]] [id (:transform data)]))
|
||||
entries))))
|
||||
|
||||
(defn- enable-pixel-grid
|
||||
[]
|
||||
(fn [state]
|
||||
(update state :workspace-layout conj :snap-pixel-grid)))
|
||||
|
||||
(t/use-fixtures :each
|
||||
{:before (fn []
|
||||
(cthi/reset-idmap!)
|
||||
(reset! captured-snap-options [])
|
||||
(thw/setup-wasm-mocks!)
|
||||
(install-capturing-spy!))
|
||||
:after (fn []
|
||||
(thw/teardown-wasm-mocks!))})
|
||||
|
||||
(t/deftest axis-locked-move-tells-the-renderer-which-axis-to-ignore
|
||||
(t/async
|
||||
done
|
||||
(let [file (-> (cthf/sample-file :file1)
|
||||
(ctho/add-rect :rect1 :x 10.4 :y 20.6 :width 100.5 :height 50.3))
|
||||
store (ths/setup-store file)
|
||||
rect (cths/get-shape file :rect1)
|
||||
modif-tree (dwm/create-modif-tree [(:id rect)]
|
||||
(ctm/move-modifiers (gpt/point 5.2 0)))
|
||||
events [(enable-pixel-grid)
|
||||
(dwm/apply-wasm-modifiers modif-tree :snap-ignore-axis :y)]]
|
||||
(ths/run-store
|
||||
store done events
|
||||
(fn [_new-state]
|
||||
(t/is (= [{:snap-pixel? true :snap-ignore-axis :y}]
|
||||
@captured-snap-options)))))))
|
||||
|
||||
(t/deftest move-without-axis-lock-snaps-both-axes
|
||||
(t/async
|
||||
done
|
||||
(let [file (-> (cthf/sample-file :file1)
|
||||
(ctho/add-rect :rect1 :x 10.4 :y 20.6 :width 100.5 :height 50.3))
|
||||
store (ths/setup-store file)
|
||||
rect (cths/get-shape file :rect1)
|
||||
modif-tree (dwm/create-modif-tree [(:id rect)]
|
||||
(ctm/move-modifiers (gpt/point 5.2 3.7)))
|
||||
events [(enable-pixel-grid)
|
||||
(dwm/apply-wasm-modifiers modif-tree)]]
|
||||
(ths/run-store
|
||||
store done events
|
||||
(fn [_new-state]
|
||||
(t/is (= [{:snap-pixel? true :snap-ignore-axis nil}]
|
||||
@captured-snap-options)))))))
|
||||
|
||||
(t/deftest rotation-does-not-snap-to-the-pixel-grid
|
||||
(t/async
|
||||
done
|
||||
(let [file (-> (cthf/sample-file :file1)
|
||||
(ctho/add-rect :rect1 :x 10.4 :y 20.6 :width 100.5 :height 50.3))
|
||||
store (ths/setup-store file)
|
||||
rect (cths/get-shape file :rect1)
|
||||
events [(enable-pixel-grid)
|
||||
(dwt/increase-rotation #{(:id rect)} 15)]]
|
||||
(ths/run-store
|
||||
store done events
|
||||
(fn [_new-state]
|
||||
(t/is (= [{:snap-pixel? false :snap-ignore-axis nil}]
|
||||
@captured-snap-options)))))))
|
||||
@@ -45,6 +45,7 @@
|
||||
[frontend-tests.logic.sidebar-transform-coalescing-test]
|
||||
[frontend-tests.logic.update-position-test]
|
||||
[frontend-tests.logic.wasm-modifiers-nil-id-test]
|
||||
[frontend-tests.logic.wasm-pixel-snap-test]
|
||||
[frontend-tests.main-errors-test]
|
||||
[frontend-tests.plugins.comments-test]
|
||||
[frontend-tests.plugins.context-shapes-test]
|
||||
@@ -152,6 +153,7 @@
|
||||
'frontend-tests.logic.sidebar-transform-coalescing-test
|
||||
'frontend-tests.logic.update-position-test
|
||||
'frontend-tests.logic.wasm-modifiers-nil-id-test
|
||||
'frontend-tests.logic.wasm-pixel-snap-test
|
||||
'frontend-tests.plugins.comments-test
|
||||
'frontend-tests.plugins.context-shapes-test
|
||||
'frontend-tests.plugins.file-test
|
||||
|
||||
@@ -12,8 +12,8 @@ use common::GetBounds;
|
||||
use crate::error::Result;
|
||||
use crate::shapes;
|
||||
use crate::shapes::{
|
||||
ConstraintH, ConstraintV, Frame, Group, GrowType, Layout, Modifier, Shape, TransformEntry,
|
||||
TransformEntrySource, Type,
|
||||
ConstraintH, ConstraintV, Frame, Group, GrowType, Layout, Modifier, PixelPrecision, Shape,
|
||||
TransformEntry, TransformEntrySource, Type,
|
||||
};
|
||||
use crate::state::{ShapesPoolRef, State};
|
||||
use crate::uuid::Uuid;
|
||||
@@ -139,36 +139,84 @@ fn calculate_bool_bounds(
|
||||
Some(result)
|
||||
}
|
||||
|
||||
fn set_pixel_precision(transform: &mut Matrix, bounds: &mut Bounds) {
|
||||
let tr = bounds.transform_matrix().unwrap_or_default();
|
||||
let tr_inv = tr.invert().unwrap_or_default();
|
||||
/// Which parts of the geometry a pixel-grid correction rounds: only the ones
|
||||
/// the transform changes, so a move keeps its dimensions and a resize keeps
|
||||
/// its anchored corner.
|
||||
#[derive(PartialEq, Debug, Clone, Copy)]
|
||||
struct SnapGeometry {
|
||||
x: bool,
|
||||
y: bool,
|
||||
width: bool,
|
||||
height: bool,
|
||||
}
|
||||
|
||||
let x = bounds.min_x().round();
|
||||
let y = bounds.min_y().round();
|
||||
impl SnapGeometry {
|
||||
/// Flags the properties that differ between the two bounds. The axis mask
|
||||
/// in `precision` applies to the position only.
|
||||
fn new(before: &Bounds, after: &Bounds, precision: PixelPrecision) -> Self {
|
||||
SnapGeometry {
|
||||
x: precision.rounds_x() && !is_close_to(before.min_x(), after.min_x()),
|
||||
y: precision.rounds_y() && !is_close_to(before.min_y(), after.min_y()),
|
||||
width: !is_close_to(before.width(), after.width()),
|
||||
height: !is_close_to(before.height(), after.height()),
|
||||
}
|
||||
}
|
||||
|
||||
let width = bounds.width();
|
||||
let height = bounds.height();
|
||||
fn resized(&self) -> bool {
|
||||
self.width || self.height
|
||||
}
|
||||
|
||||
let target_width = bounds.width().round();
|
||||
let target_height = bounds.height().round();
|
||||
fn any(&self) -> bool {
|
||||
self.x || self.y || self.resized()
|
||||
}
|
||||
}
|
||||
|
||||
let scale_width = if width > 0.1 {
|
||||
f32::max(0.01, target_width / width)
|
||||
/// Rounds a transform so the parts of the shape the gesture changed land on
|
||||
/// the pixel grid, leaving everything else exactly where it is.
|
||||
fn set_pixel_precision(transform: &mut Matrix, bounds: &mut Bounds, snap: SnapGeometry) {
|
||||
// Target corner, taken before the size correction: that correction scales
|
||||
// about the bounds center, and the translation below undoes the corner
|
||||
// displacement it causes. An unsnapped axis targets its own value.
|
||||
let x = if snap.x {
|
||||
bounds.min_x().round()
|
||||
} else {
|
||||
1.0
|
||||
bounds.min_x()
|
||||
};
|
||||
let scale_height = if height > 0.1 {
|
||||
f32::max(0.01, target_height / height)
|
||||
let y = if snap.y {
|
||||
bounds.min_y().round()
|
||||
} else {
|
||||
1.0
|
||||
bounds.min_y()
|
||||
};
|
||||
|
||||
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);
|
||||
transform.post_concat(&round_transform);
|
||||
bounds.transform_mut(&round_transform);
|
||||
if snap.resized() {
|
||||
let tr = bounds.transform_matrix().unwrap_or_default();
|
||||
let tr_inv = tr.invert().unwrap_or_default();
|
||||
|
||||
let width = bounds.width();
|
||||
let height = bounds.height();
|
||||
|
||||
// A rounded dimension is never smaller than one pixel.
|
||||
let target_width = f32::max(1.0, width.round());
|
||||
let target_height = f32::max(1.0, height.round());
|
||||
|
||||
let scale_width = if snap.width && width > 0.1 {
|
||||
f32::max(0.01, target_width / width)
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
let scale_height = if snap.height && height > 0.1 {
|
||||
f32::max(0.01, target_height / height)
|
||||
} else {
|
||||
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);
|
||||
transform.post_concat(&round_transform);
|
||||
bounds.transform_mut(&round_transform);
|
||||
}
|
||||
}
|
||||
|
||||
let dx = x - bounds.min_x();
|
||||
@@ -184,7 +232,7 @@ fn set_pixel_precision(transform: &mut Matrix, bounds: &mut Bounds) {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn propagate_transform(
|
||||
entry: TransformEntry,
|
||||
pixel_precision: bool,
|
||||
pixel_precision: PixelPrecision,
|
||||
state: &State,
|
||||
entries: &mut VecDeque<Modifier>,
|
||||
bounds: &mut HashMap<Uuid, Bounds>,
|
||||
@@ -286,8 +334,11 @@ fn propagate_transform(
|
||||
}
|
||||
}
|
||||
|
||||
if pixel_precision {
|
||||
set_pixel_precision(&mut transform, &mut shape_bounds_after);
|
||||
if pixel_precision.enabled() {
|
||||
let snap = SnapGeometry::new(&shape_bounds_before, &shape_bounds_after, pixel_precision);
|
||||
if snap.any() {
|
||||
set_pixel_precision(&mut transform, &mut shape_bounds_after, snap);
|
||||
}
|
||||
}
|
||||
|
||||
if entry.propagate {
|
||||
@@ -417,10 +468,15 @@ fn reflow_shape(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Propagates a set of transforms through the shape tree, returning one
|
||||
/// transform per affected shape.
|
||||
///
|
||||
/// The transforms are relative to the committed geometry, so callers clear
|
||||
/// any transform modifier of their own before propagating.
|
||||
pub fn propagate_modifiers(
|
||||
state: &State,
|
||||
modifiers: &[TransformEntry],
|
||||
pixel_precision: bool,
|
||||
pixel_precision: PixelPrecision,
|
||||
) -> Result<Vec<TransformEntry>> {
|
||||
let mut entries: VecDeque<_> = modifiers
|
||||
.iter()
|
||||
@@ -567,6 +623,249 @@ mod tests {
|
||||
assert_eq!(result.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pixel_precision_move_keeps_size() {
|
||||
let bounds = Bounds::from_rect(&math::Rect::from_xywh(10.4, 20.6, 100.5, 50.3));
|
||||
let mut bounds_after = bounds.transform(&Matrix::translate((5.2, 3.7)));
|
||||
let mut transform = Matrix::translate((5.2, 3.7));
|
||||
|
||||
let snap = SnapGeometry::new(&bounds, &bounds_after, PixelPrecision::Both);
|
||||
set_pixel_precision(&mut transform, &mut bounds_after, snap);
|
||||
|
||||
assert!(is_close_to(bounds_after.width(), 100.5));
|
||||
assert!(is_close_to(bounds_after.height(), 50.3));
|
||||
assert!(is_close_to(bounds_after.min_x(), 16.0));
|
||||
assert!(is_close_to(bounds_after.min_y(), 24.0));
|
||||
assert!(math::is_move_only_matrix(&transform));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pixel_precision_resize_rounds_size() {
|
||||
let bounds = Bounds::from_rect(&math::Rect::from_xywh(10.4, 20.6, 100.5, 50.3));
|
||||
let mut bounds_after = bounds.transform(&Matrix::scale((1.1, 1.1)));
|
||||
let mut transform = Matrix::scale((1.1, 1.1));
|
||||
|
||||
let snap = SnapGeometry::new(&bounds, &bounds_after, PixelPrecision::Both);
|
||||
set_pixel_precision(&mut transform, &mut bounds_after, snap);
|
||||
|
||||
assert!(is_close_to(
|
||||
bounds_after.width(),
|
||||
bounds_after.width().round()
|
||||
));
|
||||
assert!(is_close_to(
|
||||
bounds_after.height(),
|
||||
bounds_after.height().round()
|
||||
));
|
||||
assert!(is_close_to(
|
||||
bounds_after.min_x(),
|
||||
bounds_after.min_x().round()
|
||||
));
|
||||
assert!(is_close_to(
|
||||
bounds_after.min_y(),
|
||||
bounds_after.min_y().round()
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_propagate_pixel_precision_move_only_rounds_position() {
|
||||
let shape_id = Uuid::new_v4();
|
||||
let mut state = State::new();
|
||||
state.shapes.initialize(10);
|
||||
{
|
||||
let shape = state.shapes.add_shape(shape_id);
|
||||
shape.set_selrect(10.4, 20.6, 110.9, 70.9);
|
||||
}
|
||||
|
||||
let entry = TransformEntry::from_input(shape_id, Matrix::translate((5.2, 3.7)));
|
||||
let result = propagate_modifiers(&state, &[entry], PixelPrecision::Both).unwrap();
|
||||
|
||||
let transform = result
|
||||
.iter()
|
||||
.find(|entry| entry.id == shape_id)
|
||||
.map(|entry| entry.transform)
|
||||
.unwrap();
|
||||
|
||||
let shape = state.shapes.get(&shape_id).unwrap();
|
||||
let bounds = shape.bounds().transform(&transform);
|
||||
|
||||
assert!(is_close_to(bounds.width(), 100.5));
|
||||
assert!(is_close_to(bounds.height(), 50.3));
|
||||
assert!(is_close_to(bounds.min_x(), 16.0));
|
||||
assert!(is_close_to(bounds.min_y(), 24.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_propagate_pixel_precision_resize_keeps_anchored_corner() {
|
||||
let shape_id = Uuid::new_v4();
|
||||
let mut state = State::new();
|
||||
state.shapes.initialize(10);
|
||||
{
|
||||
let shape = state.shapes.add_shape(shape_id);
|
||||
shape.set_selrect(10.4, 20.6, 110.4, 70.6);
|
||||
}
|
||||
|
||||
// Drag the bottom-right corner in small steps: the top-left corner
|
||||
// stays put on every step.
|
||||
for step in 1..40 {
|
||||
let delta = step as f32 * 0.05;
|
||||
let mut resize = Matrix::scale(((100.0 + delta) / 100.0, (50.0 + delta) / 50.0));
|
||||
resize.post_translate(Point::new(10.4, 20.6));
|
||||
resize.pre_translate(Point::new(-10.4, -20.6));
|
||||
|
||||
let entry = TransformEntry::from_input(shape_id, resize);
|
||||
let result = propagate_modifiers(&state, &[entry], PixelPrecision::Both).unwrap();
|
||||
|
||||
let transform = result
|
||||
.iter()
|
||||
.find(|entry| entry.id == shape_id)
|
||||
.map(|entry| entry.transform)
|
||||
.unwrap();
|
||||
|
||||
let shape = state.shapes.get(&shape_id).unwrap();
|
||||
let bounds = shape.bounds().transform(&transform);
|
||||
|
||||
assert!(
|
||||
is_close_to(bounds.min_x(), 10.4) && is_close_to(bounds.min_y(), 20.6),
|
||||
"corner moved to ({}, {}) at delta {}",
|
||||
bounds.min_x(),
|
||||
bounds.min_y(),
|
||||
delta
|
||||
);
|
||||
assert!(is_close_to(bounds.width(), bounds.width().round()));
|
||||
assert!(is_close_to(bounds.height(), bounds.height().round()));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pixel_precision_only_x_leaves_y_untouched() {
|
||||
let bounds = Bounds::from_rect(&math::Rect::from_xywh(10.4, 20.6, 100.5, 50.3));
|
||||
let mut bounds_after = bounds.transform(&Matrix::translate((5.2, 0.0)));
|
||||
let mut transform = Matrix::translate((5.2, 0.0));
|
||||
|
||||
let snap = SnapGeometry::new(&bounds, &bounds_after, PixelPrecision::OnlyX);
|
||||
set_pixel_precision(&mut transform, &mut bounds_after, snap);
|
||||
|
||||
assert!(is_close_to(bounds_after.min_x(), 16.0));
|
||||
assert!(is_close_to(bounds_after.min_y(), 20.6));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pixel_precision_only_y_leaves_x_untouched() {
|
||||
let bounds = Bounds::from_rect(&math::Rect::from_xywh(10.4, 20.6, 100.5, 50.3));
|
||||
let mut bounds_after = bounds.transform(&Matrix::translate((0.0, 3.7)));
|
||||
let mut transform = Matrix::translate((0.0, 3.7));
|
||||
|
||||
let snap = SnapGeometry::new(&bounds, &bounds_after, PixelPrecision::OnlyY);
|
||||
set_pixel_precision(&mut transform, &mut bounds_after, snap);
|
||||
|
||||
assert!(is_close_to(bounds_after.min_x(), 10.4));
|
||||
assert!(is_close_to(bounds_after.min_y(), 24.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pixel_precision_resize_never_rounds_below_one_pixel() {
|
||||
let bounds = Bounds::from_rect(&math::Rect::from_xywh(10.0, 20.0, 0.4, 0.3));
|
||||
let mut bounds_after = bounds.transform(&Matrix::scale((1.5, 1.5)));
|
||||
let mut transform = Matrix::scale((1.5, 1.5));
|
||||
|
||||
let snap = SnapGeometry::new(&bounds, &bounds_after, PixelPrecision::Both);
|
||||
set_pixel_precision(&mut transform, &mut bounds_after, snap);
|
||||
|
||||
assert!(is_close_to(bounds_after.width(), 1.0));
|
||||
assert!(is_close_to(bounds_after.height(), 1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_propagate_pixel_precision_snaps_every_frame_of_a_gesture() {
|
||||
let shape_id = Uuid::new_v4();
|
||||
let mut state = State::new();
|
||||
state.shapes.initialize(10);
|
||||
{
|
||||
let shape = state.shapes.add_shape(shape_id);
|
||||
shape.set_selrect(10.4, 20.6, 110.9, 70.9);
|
||||
}
|
||||
|
||||
// One frame of a drag, as the entry point runs it: clear the
|
||||
// modifiers, propagate the delta accumulated since the gesture
|
||||
// started, then push the result back as the active modifier, which is
|
||||
// what the renderer draws.
|
||||
let frame = |state: &mut State, delta: f32| {
|
||||
state.shapes.clear_transform_modifiers();
|
||||
|
||||
let entry = TransformEntry::from_input(shape_id, Matrix::translate((delta, delta)));
|
||||
let result = propagate_modifiers(state, &[entry], PixelPrecision::Both).unwrap();
|
||||
let transform = result
|
||||
.iter()
|
||||
.find(|entry| entry.id == shape_id)
|
||||
.map(|entry| entry.transform)
|
||||
.unwrap();
|
||||
|
||||
let bounds = state
|
||||
.shapes
|
||||
.get_raw(&shape_id)
|
||||
.unwrap()
|
||||
.bounds()
|
||||
.transform(&transform);
|
||||
|
||||
state.set_modifiers(HashMap::from([(shape_id, transform)]));
|
||||
bounds
|
||||
};
|
||||
|
||||
// Every frame lands on the pixel grid and keeps the size.
|
||||
for step in 1..40 {
|
||||
let bounds = frame(&mut state, step as f32 * 0.35);
|
||||
|
||||
assert!(
|
||||
is_close_to(bounds.min_x(), bounds.min_x().round())
|
||||
&& is_close_to(bounds.min_y(), bounds.min_y().round()),
|
||||
"shape landed off the pixel grid at ({}, {}) on frame {}",
|
||||
bounds.min_x(),
|
||||
bounds.min_y(),
|
||||
step
|
||||
);
|
||||
assert!(is_close_to(bounds.width(), 100.5));
|
||||
assert!(is_close_to(bounds.height(), 50.3));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_propagate_pixel_precision_resize_only_rounds_the_changed_dimension() {
|
||||
let shape_id = Uuid::new_v4();
|
||||
let mut state = State::new();
|
||||
state.shapes.initialize(10);
|
||||
{
|
||||
let shape = state.shapes.add_shape(shape_id);
|
||||
shape.set_selrect(10.4, 20.6, 110.9, 70.9);
|
||||
}
|
||||
|
||||
// Drag the right edge: the width lands on the grid, the height and
|
||||
// the top-left corner stay put.
|
||||
let mut resize = Matrix::scale((103.3 / 100.5, 1.0));
|
||||
resize.post_translate(Point::new(10.4, 20.6));
|
||||
resize.pre_translate(Point::new(-10.4, -20.6));
|
||||
|
||||
let entry = TransformEntry::from_input(shape_id, resize);
|
||||
let result = propagate_modifiers(&state, &[entry], PixelPrecision::Both).unwrap();
|
||||
|
||||
let transform = result
|
||||
.iter()
|
||||
.find(|entry| entry.id == shape_id)
|
||||
.map(|entry| entry.transform)
|
||||
.unwrap();
|
||||
|
||||
let bounds = state
|
||||
.shapes
|
||||
.get_raw(&shape_id)
|
||||
.unwrap()
|
||||
.bounds()
|
||||
.transform(&transform);
|
||||
|
||||
assert!(is_close_to(bounds.width(), 103.0));
|
||||
assert!(is_close_to(bounds.height(), 50.3));
|
||||
assert!(is_close_to(bounds.min_x(), 10.4));
|
||||
assert!(is_close_to(bounds.min_y(), 20.6));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_group_bounds() {
|
||||
let parent_id = Uuid::new_v4();
|
||||
|
||||
@@ -5,18 +5,59 @@ use crate::utils::{uuid_from_u32_quartet, uuid_to_u32_quartet};
|
||||
use crate::uuid::Uuid;
|
||||
use skia::Matrix;
|
||||
|
||||
/// Axes the pixel grid rounds. An axis-locked drag rounds only the axis it
|
||||
/// moves along.
|
||||
#[derive(PartialEq, Debug, Clone, Copy)]
|
||||
pub enum PixelPrecision {
|
||||
Disabled,
|
||||
Both,
|
||||
OnlyX,
|
||||
OnlyY,
|
||||
}
|
||||
|
||||
impl PixelPrecision {
|
||||
pub fn enabled(&self) -> bool {
|
||||
*self != PixelPrecision::Disabled
|
||||
}
|
||||
|
||||
pub fn rounds_x(&self) -> bool {
|
||||
matches!(self, PixelPrecision::Both | PixelPrecision::OnlyX)
|
||||
}
|
||||
|
||||
pub fn rounds_y(&self) -> bool {
|
||||
matches!(self, PixelPrecision::Both | PixelPrecision::OnlyY)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u8> for PixelPrecision {
|
||||
fn from(value: u8) -> Self {
|
||||
match value {
|
||||
1 => PixelPrecision::Both,
|
||||
2 => PixelPrecision::OnlyX,
|
||||
3 => PixelPrecision::OnlyY,
|
||||
_ => PixelPrecision::Disabled,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Debug, Clone)]
|
||||
pub enum Modifier {
|
||||
Transform(TransformEntry, bool),
|
||||
Transform(TransformEntry, PixelPrecision),
|
||||
Reflow(Uuid, bool),
|
||||
}
|
||||
|
||||
impl Modifier {
|
||||
pub fn transform_propagate(id: Uuid, transform: Matrix) -> Self {
|
||||
Modifier::Transform(TransformEntry::from_propagate(id, transform), false)
|
||||
Modifier::Transform(
|
||||
TransformEntry::from_propagate(id, transform),
|
||||
PixelPrecision::Disabled,
|
||||
)
|
||||
}
|
||||
pub fn parent(id: Uuid, transform: Matrix) -> Self {
|
||||
Modifier::Transform(TransformEntry::parent(id, transform), false)
|
||||
Modifier::Transform(
|
||||
TransformEntry::parent(id, transform),
|
||||
PixelPrecision::Disabled,
|
||||
)
|
||||
}
|
||||
pub fn reflow(id: Uuid, force_reflow: bool) -> Self {
|
||||
Modifier::Reflow(id, force_reflow)
|
||||
|
||||
@@ -403,6 +403,20 @@ impl ShapesPoolImpl {
|
||||
/// gone, but if we don't touch their tiles they keep pointing at the
|
||||
/// previous modified position and the tile texture cache may serve stale
|
||||
/// pixels.
|
||||
/// Drops the transform modifiers, keeping structure and scale-content
|
||||
/// entries, so the pool serves committed geometry again. Called before
|
||||
/// propagating a new set of transforms, which are relative to that
|
||||
/// geometry.
|
||||
pub fn clear_transform_modifiers(&mut self) {
|
||||
if self.modifiers.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
self.clean_shape_cache();
|
||||
self.modifiers = HashMap::default();
|
||||
self.modifier_uuids.clear();
|
||||
}
|
||||
|
||||
pub fn clean_all(&mut self) -> Vec<Uuid> {
|
||||
self.clean_shape_cache();
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ impl From<RawTransformEntry> for TransformEntry {
|
||||
|
||||
#[no_mangle]
|
||||
#[wasm_error]
|
||||
pub extern "C" fn propagate_modifiers(pixel_precision: bool) -> Result<*mut u8> {
|
||||
pub extern "C" fn propagate_modifiers(pixel_precision: u8) -> Result<*mut u8> {
|
||||
let bytes = mem::bytes();
|
||||
|
||||
let entries: Vec<TransformEntry> = bytes
|
||||
@@ -85,7 +85,8 @@ pub extern "C" fn propagate_modifiers(pixel_precision: bool) -> Result<*mut u8>
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
|
||||
with_state!(state, {
|
||||
let result = shapes::propagate_modifiers(state, &entries, pixel_precision)?;
|
||||
state.shapes.clear_transform_modifiers();
|
||||
let result = shapes::propagate_modifiers(state, &entries, pixel_precision.into())?;
|
||||
Ok(mem::write_vec(result))
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user