use serde::{Deserialize, Serialize};
use crate::document::layer_kind::{IdMap, LayerKindRegistration, PixelBlobSpec, SerializedEntity};
use crate::format::error::LoadError;
use crate::format::manifest::ManifestPixelRef;
use crate::gpu::blend_mode;
use crate::gpu::params::ParamValue;
use crate::layer::{BlendProps, Layer, LayerId, LayerNode, NodeCommon, VoidLayer};
pub const TYPE_ID: &str = "void";
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
struct VoidBody {
name: String,
visible: bool,
locked: bool,
opacity: f32,
blend_mode: String,
void_type: String,
params: Vec<ParamValue>,
#[serde(default)]
transform: crate::transform::Transform,
#[serde(default)]
modifiers: Vec<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pixels: Option<ManifestPixelRef>,
}
pub fn register() -> LayerKindRegistration {
LayerKindRegistration {
type_id: TYPE_ID,
display_name: "Void Layer",
can_have_mask: true,
can_rename: true,
has_thumbnail: false,
icon: "tabler:galaxy",
serialize,
deserialize,
remap_ids,
}
}
fn serialize(node: &LayerNode) -> SerializedEntity {
let v = match node {
LayerNode::Layer(Layer::Void(v)) => v,
_ => panic!("void::serialize received non-void LayerNode"),
};
let body = VoidBody {
name: v.common.name.clone(),
visible: v.common.visible,
locked: v.common.locked,
opacity: v.blend.opacity,
blend_mode: v.blend.blend_mode.type_id.to_string(),
void_type: v.void_type.clone(),
params: v.params.clone(),
transform: v.transform,
modifiers: v.filters.iter().map(|m| m.to_ffi()).collect(),
pixels: v.frame.clone(),
};
let pixel_blobs = match &v.frame {
Some(pixels) => vec![PixelBlobSpec {
blob_key: pixels.pixels.clone(),
source_node_id: v.id,
pixels: pixels.clone(),
}],
None => Vec::new(),
};
SerializedEntity {
body: serde_json::to_value(&body).expect("derived serde for VoidBody is infallible"),
pixel_blobs,
}
}
fn deserialize(body: &serde_json::Value, id: LayerId) -> Result<LayerNode, LoadError> {
let body: VoidBody =
serde_json::from_value(body.clone()).map_err(|e| LoadError::CorruptManifest {
reason: format!("void body: {e}"),
})?;
let blend_reg = blend_mode::registry()
.get(&body.blend_mode)
.ok_or_else(|| LoadError::CorruptManifest {
reason: format!(
"void {} references undeclared blend_mode/{}",
id.to_ffi(),
body.blend_mode
),
})?;
Ok(LayerNode::Layer(Layer::Void(VoidLayer {
id,
common: NodeCommon {
name: body.name,
visible: body.visible,
locked: body.locked,
},
blend: BlendProps {
opacity: body.opacity,
blend_mode: blend_reg,
},
void_type: body.void_type,
params: body.params,
transform: body.transform,
filters: body.modifiers.into_iter().map(LayerId::from_ffi).collect(),
frame: body.pixels,
})))
}
fn remap_ids(node: &mut LayerNode, id_map: &IdMap) {
let LayerNode::Layer(Layer::Void(v)) = node else {
panic!("void::remap_ids received non-void LayerNode");
};
for m in v.filters.iter_mut() {
let old_ffi = m.to_ffi();
if let Some(new_id) = id_map.get(&old_ffi) {
*m = *new_id;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::document::Document;
use crate::gpu::params::ParamValue;
#[test]
fn void_body_round_trips_through_registration() {
let mut doc = Document::new(64, 64);
let id = doc.add_void_layer(
"noise".to_string(),
"Noise",
vec![
ParamValue::Int(7),
ParamValue::Int(4),
ParamValue::Float(0.01),
ParamValue::Float(2.0),
ParamValue::Float(0.5),
ParamValue::Float(0.1),
],
crate::transform::Transform::identity(),
None,
);
let reg = register();
let node = doc.find_node(id).expect("void exists");
let serialized = (reg.serialize)(node);
assert!(
serialized.pixel_blobs.is_empty(),
"voids must declare no pixel blobs",
);
let restored = (reg.deserialize)(&serialized.body, id).expect("deserialize must succeed");
let v_after = match &restored {
LayerNode::Layer(Layer::Void(v)) => v,
_ => panic!("deserialize must yield a Void layer"),
};
assert_eq!(v_after.void_type, "noise");
assert_eq!(
v_after.params,
vec![
ParamValue::Int(7),
ParamValue::Int(4),
ParamValue::Float(0.01),
ParamValue::Float(2.0),
ParamValue::Float(0.5),
ParamValue::Float(0.1),
],
"all params (including Int variants) must survive round-trip",
);
}
#[test]
fn void_with_frame_emits_pixel_blob_spec() {
use crate::coord::CanvasRect;
use crate::format::manifest::ManifestPixelRef;
let mut doc = Document::new(64, 64);
let id = doc.add_void_layer(
"camera".to_string(),
"Camera",
Vec::new(),
crate::transform::Transform::identity(),
None,
);
let frame = ManifestPixelRef {
format: "rgba8unorm".to_string(),
pixels: format!("layers/{}.pixels", id.to_ffi()),
bounds: CanvasRect::from_xywh(0, 0, 640, 480),
};
if let Some(LayerNode::Layer(Layer::Void(v))) = doc.find_node_mut(id) {
v.frame = Some(frame.clone());
}
let reg = register();
let node = doc.find_node(id).expect("void exists");
let serialized = (reg.serialize)(node);
assert_eq!(
serialized.pixel_blobs.len(),
1,
"void with frame must declare exactly one pixel blob",
);
assert_eq!(serialized.pixel_blobs[0].source_node_id, id);
assert_eq!(serialized.pixel_blobs[0].pixels, frame);
let body_pixels: ManifestPixelRef = serde_json::from_value(
serialized
.body
.get("pixels")
.expect("body has pixels")
.clone(),
)
.expect("body pixels parses as ManifestPixelRef");
assert_eq!(body_pixels, frame);
let restored = (reg.deserialize)(&serialized.body, id).expect("deserialize must succeed");
let v_after = match &restored {
LayerNode::Layer(Layer::Void(v)) => v,
_ => panic!("deserialize must yield a Void layer"),
};
assert_eq!(v_after.frame, Some(frame));
}
#[test]
fn void_transform_round_trips() {
let mut doc = Document::new(64, 64);
let id = doc.add_void_layer(
"camera".to_string(),
"Camera",
Vec::new(),
crate::transform::Transform::identity(),
None,
);
let t = crate::transform::Transform::from_affine([2.0, 0.5, 10.0, -0.25, 1.5, -20.0]);
if let Some(LayerNode::Layer(Layer::Void(v))) = doc.find_node_mut(id) {
v.transform = t;
}
let reg = register();
let serialized = (reg.serialize)(doc.find_node(id).expect("void exists"));
let restored = (reg.deserialize)(&serialized.body, id).expect("deserialize must succeed");
let v_after = match &restored {
LayerNode::Layer(Layer::Void(v)) => v,
_ => panic!("deserialize must yield a Void layer"),
};
assert_eq!(
v_after.transform, t,
"non-identity transform must round-trip"
);
}
#[test]
fn void_missing_transform_defaults_to_identity() {
let reg = register();
let body = serde_json::json!({
"name": "n",
"visible": true,
"locked": false,
"opacity": 1.0,
"blend_mode": blend_mode::registry().default().type_id,
"void_type": "noise",
"params": [],
"modifiers": []
});
let id = Document::new(8, 8).root_id();
let restored = (reg.deserialize)(&body, id).expect("deserialize must succeed");
let v = match &restored {
LayerNode::Layer(Layer::Void(v)) => v,
_ => panic!("expected void"),
};
assert_eq!(v.transform, crate::transform::Transform::identity());
}
#[test]
fn void_without_frame_emits_no_pixel_blob() {
let mut doc = Document::new(64, 64);
let id = doc.add_void_layer(
"noise".to_string(),
"Noise",
Vec::new(),
crate::transform::Transform::identity(),
None,
);
let reg = register();
let node = doc.find_node(id).expect("void exists");
let serialized = (reg.serialize)(node);
assert!(
serialized.pixel_blobs.is_empty(),
"voids without a frame must declare no blobs",
);
assert!(
serialized.body.get("pixels").is_none(),
"voids without a frame must omit the body pixels field entirely",
);
}
#[test]
fn unknown_blend_mode_in_body_returns_corrupt_manifest() {
let reg = register();
let body = serde_json::json!({
"name": "broken",
"visible": true,
"locked": false,
"opacity": 1.0,
"blend_mode": "definitely-not-real",
"voidType": "noise",
"params": [],
"modifiers": []
});
let id = Document::new(8, 8).root_id();
let err = (reg.deserialize)(&body, id);
assert!(
err.is_err(),
"unknown blend_mode must reject the load, not fall through"
);
}
}