// ============================================================================
// Use/Symbol (Reusable Elements)
// ============================================================================
///|
/// Symbol definition (reusable graphic)
pub(all) struct Symbol {
id : String
content : SVGNode
view_box : ViewBox?
width : Double?
height : Double?
preserve_aspect_ratio : PreserveAspectRatio
display_none : Bool
}
///|
pub fn Symbol::new(id : String, content : SVGNode) -> Symbol {
{
id,
content,
view_box: None,
width: None,
height: None,
preserve_aspect_ratio: PreserveAspectRatio::default(),
display_none: false,
}
}
///|
pub fn Symbol::with_viewbox(
id : String,
content : SVGNode,
view_box : ViewBox,
) -> Symbol {
{
id,
content,
view_box: Some(view_box),
width: None,
height: None,
preserve_aspect_ratio: PreserveAspectRatio::default(),
display_none: false,
}
}
///|
/// Use element (instance of a symbol)
pub(all) struct UseElement {
href : String // Reference to symbol ID (e.g., "#mySymbol")
x : Double
y : Double
width : Double?
height : Double?
transform : Transform
}
///|
pub fn UseElement::new(href : String, x : Double, y : Double) -> UseElement {
{ href, x, y, width: None, height: None, transform: Transform::identity() }
}
///|
pub fn UseElement::with_size(
href : String,
x : Double,
y : Double,
width : Double,
height : Double,
) -> UseElement {
{
href,
x,
y,
width: Some(width),
height: Some(height),
transform: Transform::identity(),
}
}
///|
fn hex_value(c : Char) -> Int? {
if c >= '0' && c <= '9' {
Some(c.to_int() - '0'.to_int())
} else if c >= 'a' && c <= 'f' {
Some(10 + (c.to_int() - 'a'.to_int()))
} else if c >= 'A' && c <= 'F' {
Some(10 + (c.to_int() - 'A'.to_int()))
} else {
None
}
}
///|
fn decode_percent(s : String) -> String {
let mut i = 0
let len = s.length()
let buf = StringBuilder::new()
while i < len {
let c = Int::unsafe_to_char(s[i].to_int())
if c == '%' && i + 2 < len {
let c1 = Int::unsafe_to_char(s[i + 1].to_int())
let c2 = Int::unsafe_to_char(s[i + 2].to_int())
match (hex_value(c1), hex_value(c2)) {
(Some(h1), Some(h2)) => {
let v = h1 * 16 + h2
buf.write_char(Int::unsafe_to_char(v))
i = i + 3
continue
}
_ => ()
}
}
buf.write_char(c)
i = i + 1
}
buf.to_string()
}
///|
/// Get the href ID (strips leading #)
pub fn UseElement::get_id(self : UseElement) -> String {
if self.href.length() > 0 && self.href[0].unsafe_to_char() == '#' {
let buf = StringBuilder::new()
for i in 1.. SymbolRegistry {
{ symbols: Map([]) }
}
///|
fn SymbolRegistry::add(self : SymbolRegistry, symbol : Symbol) -> Unit {
self.symbols.set(symbol.id, symbol)
}
///|
fn SymbolRegistry::get(self : SymbolRegistry, id : String) -> Symbol? {
self.symbols.get(id)
}
///|
/// Registry for reusable elements defined in
priv struct DefsRegistry {
elements : Map[String, SVGNode]
}
///|
fn DefsRegistry::new() -> DefsRegistry {
{ elements: Map([]) }
}
///|
fn DefsRegistry::add(self : DefsRegistry, id : String, node : SVGNode) -> Unit {
if id.length() == 0 {
return
}
self.elements.set(id, node)
}
///|
fn DefsRegistry::get(self : DefsRegistry, id : String) -> SVGNode? {
self.elements.get(id)
}
///|
fn UseElement::instantiate_definition(
self : UseElement,
registry : DefsRegistry,
viewport_fallback : (Double, Double)?,
) -> SVGNode? {
let id = self.get_id()
match registry.get(id) {
Some(target) => {
let node = target.clone()
let (vb, force_non_uniform) = match node.view_box {
Some(vb) => (Some(vb), false)
None =>
match (node.viewport_width, node.viewport_height) {
(Some(w), Some(h)) =>
(Some({ min_x: 0.0, min_y: 0.0, width: w, height: h }), true)
_ =>
match viewport_fallback {
Some((vw, vh)) =>
(
Some({ min_x: 0.0, min_y: 0.0, width: vw, height: vh }),
true,
)
None => (None, false)
}
}
}
if force_non_uniform {
node.preserve_aspect_ratio = { align: None, meet_or_slice: Meet }
}
let vw = match self.width {
Some(w) => Some(w)
None =>
match node.viewport_width {
Some(w) => Some(w)
None =>
match viewport_fallback {
Some((vw, _)) => Some(vw)
None => None
}
}
}
let vh = match self.height {
Some(h) => Some(h)
None =>
match node.viewport_height {
Some(h) => Some(h)
None =>
match viewport_fallback {
Some((_, vh)) => Some(vh)
None => None
}
}
}
node.view_box = vb
node.viewport_width = vw
node.viewport_height = vh
let translate = Transform::translate(self.x, self.y)
let base = self.transform.multiply(translate)
node.transform = base.multiply(node.transform)
Some(node)
}
None => None
}
}
///|
/// Instantiate a use element with the symbol registry
fn UseElement::instantiate(
self : UseElement,
registry : SymbolRegistry,
) -> SVGNode? {
let id = self.get_id()
match registry.get(id) {
Some(symbol) => {
if symbol.display_none {
return None
}
// Create a copy of the symbol content
let node = symbol.content.clone()
let (vb, force_non_uniform) = match symbol.view_box {
Some(vb) => (Some(vb), false)
None =>
match (symbol.width, symbol.height) {
(Some(w), Some(h)) =>
(Some({ min_x: 0.0, min_y: 0.0, width: w, height: h }), true)
_ => (None, false)
}
}
let vw = match self.width {
Some(w) => Some(w)
None => symbol.width
}
let vh = match self.height {
Some(h) => Some(h)
None => symbol.height
}
// Apply viewBox scaling (if viewport is known)
node.view_box = vb
if force_non_uniform {
node.preserve_aspect_ratio = { align: None, meet_or_slice: Meet }
}
node.viewport_width = vw
node.viewport_height = vh
// Apply use element transform
let translate = Transform::translate(self.x, self.y)
let base = self.transform.multiply(translate)
node.transform = base.multiply(node.transform)
Some(node)
}
None => None
}
}
///|
/// Instantiate a reusable element registered on this document.
pub fn SVGDocument::instantiate_use(
self : SVGDocument,
element : UseElement,
) -> SVGNode? {
match element.instantiate(self.symbols) {
Some(node) => Some(node)
None => element.instantiate_definition(self.definitions, None)
}
}
///|
/// Clone an SVGNode (shallow clone of children)
fn SVGNode::clone(self : SVGNode) -> SVGNode {
let children : Array[SVGNode] = []
for child in self.children {
children.push(child.clone())
}
let filters : Array[Filter] = []
for f in self.filters {
filters.push(f)
}
{
id: self.id,
shape: self.shape,
transform: self.transform,
view_box: self.view_box,
viewport_width: self.viewport_width,
viewport_height: self.viewport_height,
preserve_aspect_ratio: self.preserve_aspect_ratio,
preserve_aspect_ratio_is_set: self.preserve_aspect_ratio_is_set,
image_sampling: self.image_sampling,
fill: self.fill,
fill_is_set: self.fill_is_set,
color: self.color,
color_is_set: self.color_is_set,
paint_order: self.paint_order,
fill_rule: self.fill_rule,
clip_rule: self.clip_rule,
fill_opacity: self.fill_opacity,
stroke: self.stroke,
stroke_paint_is_set: self.stroke_paint_is_set,
stroke_width_is_set: self.stroke_width_is_set,
stroke_opacity: self.stroke_opacity,
opacity: self.opacity,
blend_mode: self.blend_mode,
isolation: self.isolation,
marker_start: self.marker_start,
marker_start_is_set: self.marker_start_is_set,
marker_mid: self.marker_mid,
marker_mid_is_set: self.marker_mid_is_set,
marker_end: self.marker_end,
marker_end_is_set: self.marker_end_is_set,
filters,
filter_graph_id: self.filter_graph_id,
mask_id: self.mask_id,
clip_path_id: self.clip_path_id,
clip_overflow: self.clip_overflow,
children,
}
}