///|
/// An `SVGPath` is identical to a `Glyph` but has been optimized internally
/// for further manipulation, whereas a `Glyph` is optimized for compact
/// storage of font data within all the font packages.
/// An `SVGPath` can be converted to a `Glyph` and vice versa.
pub(all) struct SVGPath {
/// `char` represents all the glyphs contained in this path.
/// It is identical to the `Glyph.char` field.
char : String
/// `cmds` represents the combination of the `gerber_lp` and pre-parsed
/// `d` fields of a glyph. It is optimized for further graphics-ops
/// manipulation.
/// The `gerber_lp` field ("dark" and "clear" subpath information) is encoded
/// within each `PathCmd`.
cmds : Array[PathCmd]
/// These values represent the minimum bounding box of the glyph in native units.
xmin : Double
ymin : Double
xmax : Double
ymax : Double
} derive(Eq)
///|
pub impl Show for SVGPath with fn output(self, logger) {
let { char, cmds, xmin, ymin, xmax, ymax } = self
logger.write_string(
(
$|{char: \{char.escape(quote=true)}, cmds: \{Repr(cmds)}, xmin: \{xmin}, ymin: \{ymin}, xmax: \{xmax}, ymax: \{ymax}}
),
)
}
///|
test "SVGPath show interface" {
let svgpath = {
char: "A",
cmds: [],
xmin: 0.0,
ymin: 0.0,
xmax: 100.0,
ymax: 100.0,
}
inspect(
svgpath,
content=(
#|{char: "A", cmds: [], xmin: 0, ymin: 0, xmax: 100, ymax: 100}
),
)
}
///|
/// `AbsoluteCmd` represents a supported absolute SVG command.
pub(all) enum AbsoluteCmd {
/// `M` is the MoveTo command:
/// https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d#moveto_path_commands
M
/// `L` is the LineTo command:
/// https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d#lineto_path_commands
L
/// `C` is the Cubic Bézier curve command:
/// https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d#cubic_b%C3%A9zier_curve
C
/// `Q` is the Quadratic Bézier curve command:
/// https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d#quadratic_b%C3%A9zier_curve
Q
/// `Z` is the ClosePath command:
/// https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d#closepath
Z
} derive(Debug, Eq)
///|
pub impl Show for AbsoluteCmd with fn output(self, logger) {
match self {
M =>
logger.write_string(
(
$|M
),
)
L =>
logger.write_string(
(
$|L
),
)
C =>
logger.write_string(
(
$|C
),
)
Q =>
logger.write_string(
(
$|Q
),
)
Z =>
logger.write_string(
(
$|Z
),
)
}
}
///|
test "AbsoluteCmd show interface" {
inspect(
M,
content=(
#|M
),
)
inspect(
L,
content=(
#|L
),
)
inspect(
C,
content=(
#|C
),
)
inspect(
Q,
content=(
#|Q
),
)
inspect(
Z,
content=(
#|Z
),
)
}
///|
/// `GerberLP` represents whether a subpath is `Dark` or `Clear`.
pub(all) enum GerberLP {
/// `Dark` means the subpath is filled.
Dark
/// `Clear` means the subpath is a hole.
Clear
} derive(Debug, Eq)
///|
pub impl Show for GerberLP with fn output(self, logger) {
match self {
Dark =>
logger.write_string(
(
$|Dark
),
)
Clear =>
logger.write_string(
(
$|Clear
),
)
}
}
///|
test "GerberLP show interface" {
inspect(
Dark,
content=(
#|Dark
),
)
inspect(
Clear,
content=(
#|Clear
),
)
}
///|
/// `from_glyph` returns an `SVGPath` from a `Glyph`, optionally processing
/// every `Cmd` with a processing function.
/// Note that apart from `path_cmd_fn`, `from_glyph` makes no attempt to process the
/// individual glyphs and simply transforms the representation.
pub fn SVGPath::from_glyph(
g : Glyph,
path_cmd_fn? : PathCmdFn,
) -> SVGPath raise FontError {
let bbox = @geom.BoundingBox::max_reversed()
let svg_cmds = split_path(g.d)
let cmds = Array::new(capacity=svg_cmds.length())
let mut gerber_index = 0
let mut gerber_lp = Dark
for index, svg_cmd in svg_cmds {
if svg_cmd.c == "M" {
if gerber_index >= g.gerber_lp.length() {
raise FontError(
"SVGPath::from_glyph: gerber_lp index out of range: index=\{gerber_index}, length=\{g.gerber_lp.length()} for path '\{g.char}'",
)
}
let glp = match g.gerber_lp.unsafe_get(gerber_index).to_char() {
Some(c) => c
None =>
raise FontError(
"SVGPath::from_glyph: invalid GerberLP character at index \{gerber_index} for path '\{g.char}'",
)
}
gerber_lp = match glp {
'd' => Dark
'c' => Clear
c => raise FontError("SVGPath::from_glyph: unsupported GerberLP '\{c}'")
}
gerber_index += 1
}
let cmd = PathCmd::from_svg_cmd(svg_cmd, gerber_lp)
let cmd = match path_cmd_fn {
Some(f) => (f.0)(index, cmd)
_ => cmd
}
cmds.push(cmd)
// update bbox
if cmd.cmd != Z {
if index == 0 {
let cmd_bbox = cmd.bbox()
bbox.min.x = cmd_bbox.min.x
bbox.min.y = cmd_bbox.min.y
bbox.max.x = cmd_bbox.max.x
bbox.max.y = cmd_bbox.max.y
} else {
for p in cmd.params {
let _ = bbox.expand_to_include_point(@geom.pt(p.x, p.y))
}
}
}
}
//
let (xmin, ymin, xmax, ymax) = bbox.bounds()
{ char: g.char, cmds, xmin, ymin, xmax, ymax }
}
///|
/// `to_glyph` returns a "super" `Glyph` from an `SVGPath`, optionally processing
/// every `PathCmd` with a processing function.
/// Note that apart from `path_cmd_fn`, `to_glyph` makes no attempt to process the
/// individual glyphs and simply transforms the representation.
pub fn SVGPath::to_glyph(self : SVGPath, path_cmd_fn? : PathCmdFn) -> Glyph {
let gerber_lp = Buffer()
let d = Buffer()
let bbox = @geom.BoundingBox::max_reversed()
//
for index, cmd in self.cmds {
let cmd = match path_cmd_fn {
Some(f) => (f.0)(index, cmd)
_ => cmd
}
let (svg_cmd, glp) = cmd.to_svg_cmd()
d.write_string_utf16le(svg_cmd.c)
if glp != "" {
gerber_lp.write_string_utf16le(glp)
}
let params = Array::makei(svg_cmd.p.0.length(), fn(i) {
svg_num(svg_cmd.p.0[i])
})
d.write_string_utf16le(params.join(" "))
// update bbox
if cmd.cmd != Z {
if index == 0 {
bbox.copy(cmd.bbox())
} else {
for p in cmd.params {
let _ = bbox.expand_to_include_point(@geom.pt(p.x, p.y))
}
}
}
}
//
let gerber_lp = gerber_lp.contents().to_unchecked_string()
let d = d.contents().to_unchecked_string()
let (xmin, ymin, xmax, ymax) = bbox.bounds()
{ char: self.char, horiz_adv_x: 0, gerber_lp, d, xmin, ymin, xmax, ymax }
}
///|
/// `clone` makes a deep copy of an SVGPath.
pub fn SVGPath::clone(self : SVGPath) -> SVGPath {
let cmds = Array::makei(self.cmds.length(), fn(i) { self.cmds[i].clone() })
{
char: self.char,
cmds,
xmin: self.xmin,
ymin: self.ymin,
xmax: self.xmax,
ymax: self.ymax,
}
}