mirror of
https://github.com/penpot/penpot.git
synced 2026-09-28 14:56:19 +00:00
* 🎉 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>
217 lines
6.7 KiB
Rust
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);
|
|
}
|
|
}
|