///|
/// Cross-module validation helpers used before a production artifact is published.
pub(all) struct SceneValidationReport {
document_errors : Array[String]
palette_errors : Array[String]
layout_errors : Array[String]
quality_errors : Array[String]
passed : Bool
score : Double
} derive(Debug)
///|
pub(all) struct SceneLayerSummary {
name : String
visible : Bool
motif_count : Int
opacity : Double
bounds : Rect
coverage : Double
} derive(Eq, Debug, ToJson)
///|
pub fn scene_layer_summary(
layer : PatternLayer,
bounds : Rect,
) -> SceneLayerSummary {
let points = []
for motif in layer.motifs {
for point in motif.transformed_path().flatten(subdivisions=6) {
points.push(point)
}
}
let motif_bounds = bounds_of_points(points)
let coverage = if bounds.width * bounds.height == 0.0 {
0.0
} else {
motif_bounds.intersect(bounds).width *
motif_bounds.intersect(bounds).height /
(bounds.width * bounds.height)
}
{
name: layer.name,
visible: layer.visible,
motif_count: layer.motifs.length(),
opacity: layer.opacity,
bounds: motif_bounds,
coverage,
}
}
///|
pub fn scene_layer_summaries(
composition : Composition,
) -> Array[SceneLayerSummary] {
let result = []
let bounds = Rect::new(0.0, 0.0, composition.width, composition.height)
for layer in composition.layers {
result.push(scene_layer_summary(layer, bounds))
}
result
}
///|
pub fn scene_document_palette(document : DesignDocument) -> Palette {
let colors = []
for layer in document.composition.layers {
for motif in layer.motifs {
let color = motif.style.fill
if !colors.contains(color) {
colors.push(color)
}
}
}
let parsed = []
for _ in colors {
parsed.push(Color::rgb8(128, 128, 128))
}
Palette::new("scene-extracted", parsed)
}
///|
pub fn scene_validate_layers(composition : Composition) -> Array[String] {
let errors = []
for layer in composition.layers {
if layer.name.length() == 0 {
errors.push("layer name is empty")
}
if layer.opacity < 0.0 || layer.opacity > 1.0 {
errors.push("layer opacity outside [0,1]")
}
if layer.visible && layer.motifs.length() == 0 {
errors.push("visible layer has no motifs: " + layer.name)
}
}
errors
}
///|
pub fn scene_validate_bounds(composition : Composition) -> Array[String] {
let errors = []
let bounds = Rect::new(0.0, 0.0, composition.width, composition.height)
for layer in composition.layers {
for motif in layer.motifs {
let motif_bounds = bounds_of_points(
motif.transformed_path().flatten(subdivisions=5),
)
if motif_bounds.width.is_nan() || motif_bounds.height.is_nan() {
errors.push("motif has non-finite bounds: " + motif.name)
}
if motif_bounds.intersect(bounds).width == 0.0 &&
motif_bounds.intersect(bounds).height == 0.0 {
errors.push("motif lies outside canvas: " + motif.name)
}
}
}
errors
}
///|
pub fn scene_validate_render(document : DesignDocument) -> Array[String] {
let errors = []
let svg = document.to_svg()
if svg.length() < 100 {
errors.push("rendered SVG is unexpectedly small")
}
if !svg.contains("