Danny Shirely d45c6710b7
🎉 Implement independent image bounds resizing (#11430)
* 🎉 Implement independent image bounds resizing

Add canvas resize interaction mode that allows users to resize an
image object's bounding box independently from the underlying bitmap
content without scaling or distortion while holding the Mod key.

AI-assisted-by: gemini-2.5-pro

* ♻️ Address reviewer feedback from elenatorro

- Remove legacy cfh/image-shape? check in shape-has-image-fill?
- Guard bounds-resize with positive dimensions instead of clamping scalev to preserve flipping
- Remove :metadata from transform-attrs in modifiers.cljs
- Restore preserveAspectRatio logic based on keep-ar? in fills.cljs
- Compute source rect against destination rect for raster and SVG fills in WASM renderer

* 🔧 Fix clippy needless borrow warnings in wasm image fills

---------

Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-09-09 15:16:56 +02:00

217 lines
6.7 KiB
Rust

use macros::{wasm_error, ToJs};
use crate::mem;
use crate::shapes;
use crate::with_current_shape_mut;
mod gradient;
mod image;
mod solid;
pub(crate) const RAW_FILL_DATA_SIZE: usize = std::mem::size_of::<RawFillData>();
#[repr(C, u8, align(4))]
#[derive(Debug, PartialEq, Clone, Copy, ToJs)]
#[allow(dead_code)]
pub enum RawFillData {
Solid(solid::RawSolidData) = 0x00,
Linear(gradient::RawGradientData) = 0x01,
Radial(gradient::RawGradientData) = 0x02,
Image(image::RawImageFillData) = 0x03,
}
impl From<RawFillData> for shapes::Fill {
fn from(fill_data: RawFillData) -> Self {
match fill_data {
RawFillData::Solid(solid_fill_data) => shapes::Fill::Solid(solid_fill_data.into()),
RawFillData::Linear(linear_fill_data) => {
shapes::Fill::LinearGradient(linear_fill_data.into())
}
RawFillData::Radial(radial_fill_data) => {
shapes::Fill::RadialGradient(radial_fill_data.into())
}
RawFillData::Image(image_fill_data) => shapes::Fill::Image(image_fill_data.into()),
}
}
}
impl TryFrom<&shapes::Fill> for RawFillData {
type Error = String;
fn try_from(fill: &shapes::Fill) -> Result<Self, Self::Error> {
match fill {
shapes::Fill::Solid(shapes::SolidColor(color)) => {
Ok(RawFillData::Solid(solid::RawSolidData {
color: ((color.a() as u32) << 24)
| ((color.r() as u32) << 16)
| ((color.g() as u32) << 8)
| (color.b() as u32),
}))
}
shapes::Fill::LinearGradient(linear_gradient) => Ok(RawFillData::Linear(
gradient::RawGradientData::from(linear_gradient),
)),
shapes::Fill::RadialGradient(radial_gradient) => Ok(RawFillData::Radial(
gradient::RawGradientData::from(radial_gradient),
)),
shapes::Fill::Image(image_fill) => Ok(RawFillData::Image(
image::RawImageFillData::from(image_fill),
)),
}
}
}
impl From<[u8; RAW_FILL_DATA_SIZE]> for RawFillData {
fn from(bytes: [u8; RAW_FILL_DATA_SIZE]) -> Self {
unsafe { std::mem::transmute(bytes) }
}
}
impl From<RawFillData> for [u8; RAW_FILL_DATA_SIZE] {
fn from(fill_data: RawFillData) -> Self {
unsafe { std::mem::transmute(fill_data) }
}
}
impl TryFrom<&[u8]> for RawFillData {
type Error = String;
fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
let data: [u8; RAW_FILL_DATA_SIZE] = bytes
.get(0..RAW_FILL_DATA_SIZE)
.and_then(|slice| slice.try_into().ok())
.ok_or("Invalid fill data".to_string())?;
Ok(RawFillData::from(data))
}
}
// FIXME: return Result
pub fn read_fills_from_bytes(buffer: &[u8], num_fills: usize) -> Vec<shapes::Fill> {
buffer
.chunks_exact(RAW_FILL_DATA_SIZE)
.take(num_fills)
.map(|bytes| {
RawFillData::try_from(bytes)
.expect("Invalid fill data")
.into()
})
.collect()
}
#[no_mangle]
#[wasm_error]
pub extern "C" fn set_shape_fills() -> Result<()> {
with_current_shape_mut!(state, |shape: &mut Shape| {
let bytes = mem::bytes();
// The first byte contains the actual number of fills
let num_fills = bytes.first().copied().unwrap_or(0) as usize;
// Skip the first 4 bytes (header with fill count) and parse only the actual fills
let fills = read_fills_from_bytes(&bytes[4..], num_fills);
shape.set_fills(fills);
mem::free_bytes()?;
});
Ok(())
}
#[no_mangle]
pub extern "C" fn add_shape_fill() {
with_current_shape_mut!(state, |shape: &mut Shape| {
let bytes = mem::bytes();
let raw_fill = RawFillData::try_from(&bytes[..]).expect("Invalid fill data");
shape.add_fill(raw_fill.into());
});
}
#[no_mangle]
pub extern "C" fn clear_shape_fills() {
with_current_shape_mut!(state, |shape: &mut Shape| {
shape.clear_fills();
});
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_raw_fill_data_layout() {
assert_eq!(
std::mem::size_of::<RawFillData>(),
4 + std::mem::size_of::<gradient::RawGradientData>()
);
assert_eq!(std::mem::align_of::<RawFillData>(), 4);
}
#[test]
fn test_raw_fill_data_from_bytes_to_solid_fill() {
let mut bytes = vec![0x00; std::mem::size_of::<RawFillData>()];
bytes[0] = 0x00;
bytes[4..8].copy_from_slice(&0xfffabada_u32.to_le_bytes());
let raw_fill = RawFillData::try_from(&bytes[..]);
assert!(raw_fill.is_ok());
assert_eq!(
raw_fill.unwrap(),
RawFillData::Solid(solid::RawSolidData { color: 0xfffabada })
);
}
#[test]
fn test_gradient_fill_round_trip() {
let gradient = shapes::Gradient::new(
(0.0, 0.5),
(1.0, 0.5),
0x80,
0.25,
&[
(shapes::Color::from(0xfffabada), 0.0),
(shapes::Color::from(0xff00ff00), 1.0),
],
);
let fill = shapes::Fill::LinearGradient(gradient.clone());
let raw_fill = RawFillData::try_from(&fill).expect("gradient must be serializable");
let bytes = <[u8; RAW_FILL_DATA_SIZE]>::from(raw_fill);
assert_eq!(bytes[0], 0x01);
assert_eq!(shapes::Fill::from(RawFillData::from(bytes)), fill);
}
#[test]
fn test_image_fill_round_trip() {
let image_fill = shapes::ImageFill::new(crate::uuid::Uuid::nil(), 0x80, 300, 200, true);
let fill = shapes::Fill::Image(image_fill);
let raw_fill = RawFillData::try_from(&fill).expect("image fill must be serializable");
let bytes = <[u8; RAW_FILL_DATA_SIZE]>::from(raw_fill);
assert_eq!(bytes[0], 0x03);
assert_eq!(shapes::Fill::from(RawFillData::from(bytes)), fill);
}
#[test]
fn test_image_fill_with_transform_round_trip() {
let transform = shapes::ImageFillTransform {
x: 0.1,
y: -0.2,
width: 1.5,
height: 2.0,
};
let image_fill = shapes::ImageFill::new_with_transform(
crate::uuid::Uuid::nil(),
0xcc,
400,
300,
false,
Some(transform),
);
let fill = shapes::Fill::Image(image_fill);
let raw_fill =
RawFillData::try_from(&fill).expect("image fill with transform must be serializable");
let bytes = <[u8; RAW_FILL_DATA_SIZE]>::from(raw_fill);
assert_eq!(bytes[0], 0x03);
let deserialized = shapes::Fill::from(RawFillData::from(bytes));
assert_eq!(deserialized, fill);
}
}