Compare commits

...
1 Commits
Author SHA1 Message Date
alonso.torres 60b278f8fa 🐛 Fix problems with pixel precission in WASM renderer 2026-09-08 16:06:53 +02:00
11 changed files with 532 additions and 47 deletions

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))
+24 -2
View File
@@ -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)))))))
+2
View File
@@ -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
+326 -27
View File
@@ -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();
+44 -3
View File
@@ -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)
+14
View File
@@ -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();
+3 -2
View File
@@ -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))
})
}