///|
/// Output formats supported by the deterministic export pipeline.
pub(all) enum ArtifactKind {
Svg
Html
DataUri
Css
MetricsCsv
Manifest
} derive(Eq, Debug, ToJson)
///|
/// A named in-memory export artifact.
pub(all) struct ExportArtifact {
name : String
kind : ArtifactKind
content : String
byte_length : Int
checksum : Int
} derive(Eq, Debug, ToJson)
///|
/// Export switches for applications and command-line integrations.
pub(all) struct PipelineOptions {
title : String
filename : String
include_html : Bool
include_data_uri : Bool
include_css : Bool
include_metrics : Bool
include_manifest : Bool
} derive(Eq, Debug, ToJson)
///|
pub fn PipelineOptions::new(
title : String,
filename : String,
) -> PipelineOptions {
{
title,
filename,
include_html: true,
include_data_uri: false,
include_css: true,
include_metrics: true,
include_manifest: true,
}
}
///|
pub fn PipelineOptions::html(
self : PipelineOptions,
enabled : Bool,
) -> PipelineOptions {
{ ..self, include_html: enabled }
}
///|
pub fn PipelineOptions::data_uri(
self : PipelineOptions,
enabled : Bool,
) -> PipelineOptions {
{ ..self, include_data_uri: enabled }
}
///|
pub fn PipelineOptions::css(
self : PipelineOptions,
enabled : Bool,
) -> PipelineOptions {
{ ..self, include_css: enabled }
}
///|
pub fn PipelineOptions::metrics(
self : PipelineOptions,
enabled : Bool,
) -> PipelineOptions {
{ ..self, include_metrics: enabled }
}
///|
pub fn PipelineOptions::manifest(
self : PipelineOptions,
enabled : Bool,
) -> PipelineOptions {
{ ..self, include_manifest: enabled }
}
///|
/// An export pipeline joining a composition with its theme and metadata policy.
pub(all) struct PatternPipeline {
composition : Composition
theme : ColorTheme
options : PipelineOptions
} derive(Debug)
///|
pub fn PatternPipeline::new(
composition : Composition,
theme : ColorTheme,
options : PipelineOptions,
) -> PatternPipeline {
{ composition, theme, options }
}
///|
pub fn PatternPipeline::with_options(
self : PatternPipeline,
options : PipelineOptions,
) -> PatternPipeline {
{ ..self, options, }
}
///|
pub fn PatternPipeline::svg(self : PatternPipeline) -> String {
self.composition.render().to_svg_string()
}
///|
pub fn PatternPipeline::html(self : PatternPipeline) -> String {
html_preview(self.composition.render(), self.options.title)
}
///|
pub fn PatternPipeline::data_uri(self : PatternPipeline) -> String {
svg_data_uri(self.svg())
}
///|
pub fn PatternPipeline::css(self : PatternPipeline) -> String {
self.theme.to_css()
}
///|
fn pipeline_metrics(composition : Composition) -> LayoutMetrics {
let mut count = 0
let mut bounds = Rect::new(0.0, 0.0, composition.width, composition.height)
let mut path_length = 0.0
let mut occupied_area = 0.0
for layer in composition.layers {
if layer.visible {
for motif in layer.motifs {
let path = motif.transformed_path()
count += 1
let path_bounds = path.bounds()
bounds = bounds.union(path_bounds)
path_length += path.length()
occupied_area += path_bounds.width * path_bounds.height
}
}
}
let canvas_area = composition.width * composition.height
{
element_count: count,
occupied_area,
path_length,
density: if canvas_area <= 0.0 {
0.0
} else {
occupied_area / canvas_area
},
bounds,
}
}
///|
pub fn PatternPipeline::metrics(self : PatternPipeline) -> LayoutMetrics {
pipeline_metrics(self.composition)
}
///|
pub fn PatternPipeline::validate(self : PatternPipeline) -> Array[String] {
let errors = []
if self.composition.width <= 0.0 {
errors.push("composition width must be positive")
}
if self.composition.height <= 0.0 {
errors.push("composition height must be positive")
}
if self.options.title.length() == 0 {
errors.push("export title must not be empty")
}
if self.options.filename.length() == 0 {
errors.push("export filename must not be empty")
}
let audit = audit_theme(self.theme, 3.0)
if !audit.passes {
errors.push("theme contains a contrast pair below the requested ratio")
}
errors
}
///|
fn artifact_checksum(value : String) -> Int {
let mut result = 0
for code in value.to_array() {
result = (result * 33 + code.to_int()) % 1000000007
}
result
}
///|
fn make_artifact(
name : String,
kind : ArtifactKind,
content : String,
) -> ExportArtifact {
{
name,
kind,
byte_length: content.length(),
checksum: artifact_checksum(content),
content,
}
}
///|
fn artifact_name(filename : String, suffix : String) -> String {
filename + suffix
}
///|
fn manifest_content(
options : PipelineOptions,
artifacts : Array[ExportArtifact],
) -> String {
let builder = StringBuilder()
builder.write_string("{\n")
builder.write_string(" \"title\": \"")
builder.write_string(escape_svg(options.title))
builder.write_string("\",\n \"artifacts\": [\n")
for index, artifact in artifacts {
builder.write_string(" {\"name\": \"")
builder.write_string(escape_svg(artifact.name))
builder.write_string("\", \"kind\": \"")
builder.write_string(artifact_kind_name(artifact.kind))
builder.write_string("\", \"bytes\": ")
builder.write_string(artifact.byte_length.to_string())
builder.write_string(", \"checksum\": ")
builder.write_string(artifact.checksum.to_string())
builder.write_string("}")
if index + 1 < artifacts.length() {
builder.write_string(",")
}
builder.write_string("\n")
}
builder.write_string(" ]\n}\n")
builder.to_string()
}
///|
fn artifact_kind_name(kind : ArtifactKind) -> String {
match kind {
Svg => "svg"
Html => "html"
DataUri => "data-uri"
Css => "css"
MetricsCsv => "metrics-csv"
Manifest => "manifest"
}
}
///|
/// Exports all enabled artifacts in stable order.
pub fn PatternPipeline::artifacts(
self : PatternPipeline,
) -> Array[ExportArtifact] {
let result = []
let svg = self.svg()
result.push(make_artifact(self.options.filename, Svg, svg))
if self.options.include_html {
result.push(
make_artifact(
artifact_name(self.options.filename, ".html"),
Html,
self.html(),
),
)
}
if self.options.include_data_uri {
result.push(
make_artifact(
artifact_name(self.options.filename, ".uri.txt"),
DataUri,
self.data_uri(),
),
)
}
if self.options.include_css {
result.push(
make_artifact(
artifact_name(self.options.filename, ".css"),
Css,
self.css(),
),
)
}
if self.options.include_metrics {
result.push(
make_artifact(
artifact_name(self.options.filename, ".metrics.csv"),
MetricsCsv,
metrics_csv(self.metrics()),
),
)
}
if self.options.include_manifest {
result.push(
make_artifact(
artifact_name(self.options.filename, ".manifest.json"),
Manifest,
manifest_content(self.options, result),
),
)
}
result
}
///|
/// Returns a concise CSV report for one layout metrics value.
pub fn metrics_csv(metrics : LayoutMetrics) -> String {
"element_count,occupied_area,path_length,density,x,y,width,height\n" +
"\{metrics.element_count},\{metrics.occupied_area},\{metrics.path_length},\{metrics.density},\{metrics.bounds.x},\{metrics.bounds.y},\{metrics.bounds.width},\{metrics.bounds.height}\n"
}
///|
/// Joins artifact metadata into a stable table suitable for build logs.
pub fn artifact_report(artifacts : Array[ExportArtifact]) -> String {
let builder = StringBuilder()
builder.write_string("name,kind,bytes,checksum\n")
for artifact in artifacts {
builder.write_string(artifact.name)
builder.write_string(",")
builder.write_string(artifact_kind_name(artifact.kind))
builder.write_string(",")
builder.write_string(artifact.byte_length.to_string())
builder.write_string(",")
builder.write_string(artifact.checksum.to_string())
builder.write_string("\n")
}
builder.to_string()
}
///|
/// Returns a stable checksum for all artifact metadata and content.
pub fn artifact_bundle_checksum(artifacts : Array[ExportArtifact]) -> Int {
artifact_checksum(artifact_report(artifacts))
}
///|
/// Creates a pipeline around a standard scene recipe.
pub fn scene_pipeline(
seed : UInt,
width : Double,
height : Double,
palette : Palette,
options : PipelineOptions,
) -> PatternPipeline {
let document = make_design_document(seed, width, height, palette)
let theme = ColorTheme::new(palette.name, palette)
PatternPipeline::new(document.composition, theme, options)
}
///|
/// Creates a pipeline for a tile motif and its generated surface.
pub fn tile_pipeline(
bounds : Rect,
path : Path,
style : SvgStyle,
grid : TileGrid,
palette : Palette,
options : PipelineOptions,
) -> PatternPipeline {
let composition = Composition::new(
bounds.width,
bounds.height,
background=palette.at(0).unwrap_or(Color::rgb8(255, 255, 255)),
)
let layer = PatternLayer::new("tile")
for placed in tile_path(path, grid) {
layer.add(Motif::new("tile", placed, style))
}
composition.add_layer(layer)
PatternPipeline::new(
composition,
ColorTheme::new(palette.name, palette),
options,
)
}
///|
/// Returns a one-line quality summary for CI logs.
pub fn pipeline_summary(pipeline : PatternPipeline) -> String {
let metrics = pipeline.metrics()
let errors = pipeline.validate()
"title=\{pipeline.options.title}; elements=\{metrics.element_count}; density=\{metrics.density}; valid=\{errors.length() == 0}"
}