///|
/// A reusable motif with a local path, visual style and placement metadata.
pub(all) struct Motif {
name : String
path : Path
style : SvgStyle
anchor : Vec2
scale : Double
rotation : Double
} derive(Eq, Debug, ToJson)
///|
pub fn Motif::new(name : String, path : Path, style : SvgStyle) -> Motif {
{ name, path, style, anchor: Vec2::zero(), scale: 1.0, rotation: 0.0 }
}
///|
pub fn Motif::placed(
self : Motif,
x : Double,
y : Double,
scale : Double,
rotation : Double,
) -> Motif {
{ ..self, anchor: Vec2::new(x, y), scale, rotation }
}
///|
pub fn Motif::transformed_path(self : Motif) -> Path {
let transform = Matrix2D::translate(self.anchor.x, self.anchor.y)
.multiply(Matrix2D::rotate(self.rotation))
.multiply(Matrix2D::scale(self.scale, self.scale))
self.path.transform(transform)
}
///|
/// A design layer allows motifs to be grouped and reordered without rebuilding SVG.
pub(all) struct PatternLayer {
name : String
opacity : Double
mut visible : Bool
motifs : Array[Motif]
} derive(Eq, Debug, ToJson)
///|
pub fn PatternLayer::new(
name : String,
opacity? : Double = 1.0,
) -> PatternLayer {
{ name, opacity, visible: true, motifs: [] }
}
///|
pub fn PatternLayer::add(self : PatternLayer, motif : Motif) -> Unit {
self.motifs.push(motif)
}
///|
pub fn PatternLayer::hide(self : PatternLayer) -> Unit {
self.visible = false
}
///|
pub fn PatternLayer::show(self : PatternLayer) -> Unit {
self.visible = true
}
///|
/// A complete procedural composition with named layers and export metadata.
pub(all) struct Composition {
width : Double
height : Double
background : Color
layers : Array[PatternLayer]
} derive(Eq, Debug, ToJson)
///|
pub fn Composition::new(
width : Double,
height : Double,
background? : Color = Color::rgb8(255, 255, 255),
) -> Composition {
{ width, height, background, layers: [] }
}
///|
pub fn Composition::add_layer(self : Composition, layer : PatternLayer) -> Unit {
self.layers.push(layer)
}
///|
pub fn Composition::render(self : Composition) -> SvgDocument {
let document = SvgDocument::new(self.width, self.height)
let background = Path::rect(self.width, self.height)
document.add(
SvgElement::SvgPath(
background,
SvgStyle::new(fill=self.background.to_hex(), stroke="none"),
),
)
for layer in self.layers {
if layer.visible {
let children = []
for motif in layer.motifs {
let style = {
..motif.style,
opacity: motif.style.opacity * layer.opacity,
}
children.push(SvgElement::SvgPath(motif.transformed_path(), style))
}
document.add(
SvgElement::SvgGroup(children, transform=Matrix2D::identity()),
)
}
}
document
}
///|
/// Places a motif at every generated grid cell, applying cell transforms.
pub fn stamp_on_grid(motif : Motif, grid : Grid) -> Array[Motif] {
let result = []
for cell in grid.generate_cells() {
let (x, y) = cell.transform.transform_point(motif.anchor.x, motif.anchor.y)
result.push(motif.placed(x, y, motif.scale, motif.rotation))
}
result
}
///|
/// Applies a finite symmetry set to a motif around its local anchor.
pub fn stamp_with_symmetry(
motif : Motif,
transforms : Array[Matrix2D],
) -> Array[Motif] {
let result = []
for transform in transforms {
let (x, y) = transform.transform_point(motif.anchor.x, motif.anchor.y)
result.push(motif.placed(x, y, motif.scale, motif.rotation))
}
result
}