///|
/// A local production workflow that validates, renders, measures and packages artwork.
pub(all) enum WorkflowStatus {
WorkflowReady
WorkflowRendered
WorkflowValidated
WorkflowPackaged
WorkflowFailed
} derive(Eq, Debug, ToJson)
///|
pub(all) struct WorkflowStep {
name : String
status : WorkflowStatus
duration_hint : Int
detail : String
} derive(Eq, Debug, ToJson)
///|
pub(all) struct ProductionWorkflow {
name : String
seed : UInt
palette : Palette
width : Double
height : Double
steps : Array[WorkflowStep]
warnings : Array[String]
} derive(Debug)
///|
pub(all) struct WorkflowArtifact {
document : DesignDocument
plan : ExportPlan
quality : DesignQualityMetrics
validation : Array[String]
warnings : Array[String]
status : WorkflowStatus
} derive(Debug)
///|
pub fn ProductionWorkflow::new(
name : String,
seed : UInt,
width : Double,
height : Double,
palette : Palette,
) -> ProductionWorkflow {
{
name,
seed,
palette,
width: width.max(1.0),
height: height.max(1.0),
steps: [],
warnings: [],
}
}
///|
pub fn ProductionWorkflow::step(
self : ProductionWorkflow,
name : String,
status : WorkflowStatus,
detail : String,
) -> Unit {
self.steps.push({ name, status, duration_hint: detail.length(), detail })
}
///|
pub fn ProductionWorkflow::warning(
self : ProductionWorkflow,
message : String,
) -> Unit {
if message.length() > 0 && !self.warnings.contains(message) {
self.warnings.push(message)
}
}
///|
pub fn ProductionWorkflow::validate_input(
self : ProductionWorkflow,
) -> Array[String] {
let errors = []
if self.name.length() == 0 {
errors.push("workflow name is empty")
}
if self.width <= 0.0 || self.height <= 0.0 {
errors.push("workflow canvas must be positive")
}
if self.palette.colors.length() == 0 {
errors.push("workflow palette is empty")
}
if self.palette.colors.length() > 24 {
self.warning("palette contains more than 24 colors")
}
errors
}
///|
pub fn workflow_default_job(workflow : ProductionWorkflow) -> ExportJob {
ExportJob::new(
workflow.name,
workflow.seed,
workflow.width,
workflow.height,
workflow.palette,
)
.tag("workflow")
.tag("validated")
.ensure_default_variants()
}
///|
pub fn workflow_compose(workflow : ProductionWorkflow) -> DesignDocument {
let composition = SceneRecipe::new(
workflow.seed,
workflow.width,
workflow.height,
24,
24,
workflow.palette,
).build()
let metadata = DocumentMetadata::new(
workflow.name,
"MoonBit Generative Patterns",
workflow.seed,
"deterministic production workflow",
)
.tag("workflow")
.tag("deterministic")
.tag("svg")
DesignDocument::new(composition, metadata)
}
///|
pub fn workflow_validate_document(document : DesignDocument) -> Array[String] {
let errors = document.validate()
let metrics = composition_quality_metrics(document.composition)
if metrics.path_count == 0 {
errors.push("document contains no motifs")
}
if metrics.coverage > 1.0 {
errors.push("document coverage exceeds canvas")
}
errors
}
///|
pub fn workflow_render_asset(
workflow : ProductionWorkflow,
document : DesignDocument,
) -> SvgAsset {
svg_asset_from_document(
"workflow",
workflow.name,
1,
document.composition.render(),
"workflow master",
)
}
///|
pub fn workflow_build(workflow : ProductionWorkflow) -> WorkflowArtifact {
let document = workflow_compose(workflow)
let validation = workflow_validate_document(document)
let job = workflow_default_job(workflow)
let plan = export_plan(job)
let quality = composition_quality_metrics(document.composition)
let status = if validation.length() == 0 &&
export_plan_validate(plan).length() == 0 {
WorkflowPackaged
} else {
WorkflowFailed
}
{ document, plan, quality, validation, warnings: workflow.warnings, status }
}
///|
pub fn workflow_artifact_svg(artifact : WorkflowArtifact) -> String {
artifact.document.to_svg()
}
///|
pub fn workflow_artifact_manifest(artifact : WorkflowArtifact) -> String {
export_plan_manifest(artifact.plan)
}
///|
pub fn workflow_artifact_report(artifact : WorkflowArtifact) -> String {
let builder = StringBuilder()
builder.write_string("status=" + workflow_status_name(artifact.status) + "\n")
builder.write_string(design_quality_report_text(artifact.quality) + "\n")
builder.write_string(export_plan_report(artifact.plan) + "\n")
if artifact.validation.length() > 0 {
builder.write_string("validation=" + artifact.validation.join("|") + "\n")
}
if artifact.warnings.length() > 0 {
builder.write_string("warnings=" + artifact.warnings.join("|") + "\n")
}
builder.to_string()
}
///|
pub fn workflow_artifact_checksum(artifact : WorkflowArtifact) -> UInt {
svg_document_checksum(artifact.document.composition.render())
}
///|
pub fn workflow_status_name(status : WorkflowStatus) -> String {
match status {
WorkflowReady => "ready"
WorkflowRendered => "rendered"
WorkflowValidated => "validated"
WorkflowPackaged => "packaged"
WorkflowFailed => "failed"
}
}
///|
pub fn workflow_steps_csv(workflow : ProductionWorkflow) -> String {
let builder = StringBuilder()
builder.write_string("name,status,duration_hint,detail\n")
for step in workflow.steps {
builder.write_string(
step.name +
"," +
workflow_status_name(step.status) +
"," +
step.duration_hint.to_string() +
"," +
step.detail +
"\n",
)
}
builder.to_string()
}
///|
pub fn workflow_validate_palette(
workflow : ProductionWorkflow,
) -> Array[String] {
let errors = []
let report = analyze_palette(workflow.palette)
if report.color_count == 0 {
errors.push("palette has no colors")
}
if report.contrast_max < 2.0 && report.color_count > 1 {
errors.push("palette contrast range is too narrow")
}
for item in audit_palette_for_modes(workflow.palette) {
let mode = item.0
let collision = item.1
if collision > workflow.palette.colors.length() {
errors.push("color vision collision in " + ca_mode_name(mode))
}
}
errors
}
///|
pub fn workflow_quality_gate(
artifact : WorkflowArtifact,
minimum : Double,
) -> Bool {
artifact.status is WorkflowPackaged &&
artifact.quality.score >= minimum &&
artifact.validation.length() == 0
}
///|
pub fn workflow_preview(
artifact : WorkflowArtifact,
width : Int,
height : Int,
) -> Raster {
let raster = Raster::new(width, height)
let composition = artifact.document.composition
for layer in composition.layers {
for motif in layer.motifs {
raster.fill_polygon(
motif.transformed_path().flatten(subdivisions=8),
Color::rgb8(40, 40, 40),
)
}
}
raster
}
///|
pub fn workflow_preview_quality(
artifact : WorkflowArtifact,
width : Int,
height : Int,
) -> RasterQualityMetrics {
raster_quality_metrics(workflow_preview(artifact, width, height), 0.02)
}
///|
pub fn workflow_with_standard_steps(
workflow : ProductionWorkflow,
) -> ProductionWorkflow {
workflow.step("input", WorkflowReady, "parameters accepted")
workflow.step("compose", WorkflowRendered, "scene recipe expanded")
workflow.step("validate", WorkflowValidated, "document and palette audited")
workflow.step("package", WorkflowPackaged, "SVG variants prepared")
workflow
}
///|
pub fn workflow_run(
name : String,
seed : UInt,
width : Double,
height : Double,
palette : Palette,
) -> WorkflowArtifact {
let workflow = workflow_with_standard_steps(
ProductionWorkflow::new(name, seed, width, height, palette),
)
workflow_build(workflow)
}
///|
pub fn workflow_batch(
jobs : Array[ProductionWorkflow],
) -> Array[WorkflowArtifact] {
let result = []
for job in jobs {
result.push(workflow_build(job))
}
result
}
///|
pub fn workflow_batch_report(artifacts : Array[WorkflowArtifact]) -> String {
let builder = StringBuilder()
builder.write_string("index,status,quality,coverage,bytes,checksum,errors\n")
for index, artifact in artifacts {
builder.write_string(
index.to_string() +
"," +
workflow_status_name(artifact.status) +
"," +
artifact.quality.score.to_string() +
"," +
artifact.quality.coverage.to_string() +
"," +
artifact.plan.total_bytes.to_string() +
"," +
workflow_artifact_checksum(artifact).to_string() +
"," +
artifact.validation.length().to_string() +
"\n",
)
}
builder.to_string()
}
///|
pub fn workflow_compare(
left : WorkflowArtifact,
right : WorkflowArtifact,
) -> String {
let delta = right.quality.score - left.quality.score
"quality_delta=" +
delta.to_string() +
" bytes_delta=" +
(right.plan.total_bytes - left.plan.total_bytes).to_string() +
" checksum_left=" +
workflow_artifact_checksum(left).to_string() +
" checksum_right=" +
workflow_artifact_checksum(right).to_string()
}
///|
pub fn workflow_document_bundle(artifact : WorkflowArtifact) -> SvgAssetBundle {
let result = SvgAssetBundle::new(artifact.plan.job.name, "workflow-1")
let _ = result.add(
workflow_render_asset(
ProductionWorkflow::new(
artifact.plan.job.name,
artifact.plan.job.seed,
artifact.document.composition.width,
artifact.document.composition.height,
artifact.plan.job.palette,
),
artifact.document,
),
)
for asset in artifact.plan.bundle.assets {
let _ = result.add(asset)
}
result
}
///|
pub fn workflow_document_css(artifact : WorkflowArtifact) -> String {
color_analysis_css(artifact.plan.job.palette, artifact.plan.job.name)
}
///|
pub fn workflow_reproducible_pair(
left : WorkflowArtifact,
right : WorkflowArtifact,
) -> Bool {
workflow_artifact_checksum(left) == workflow_artifact_checksum(right) &&
left.quality.score == right.quality.score
}
///|
pub fn workflow_safe_to_publish(artifact : WorkflowArtifact) -> Bool {
workflow_quality_gate(artifact, 0.2) &&
svg_bundle_validate(artifact.plan.bundle).length() == 0
}