use anyhow::{Result as AnyResult, anyhow};
use enum_map::{Enum, EnumMap};
-use quick_xml::{Writer, events::Event};
+use quick_xml::events::Event;
use serde::{Deserialize, Deserializer};
use serde_json::{Value, from_value};
use std::{
ffi::OsStr,
fmt::{Display, Write as _},
fs::{File, read_to_string},
- io::{Cursor, Error as IoError, Write},
+ io::{Error as IoError, Write},
path::{Path, PathBuf},
};
use zip::ZipArchive;
/// Read `src/spv/read/graph/symbols.svg` and write
/// `src/spv/read/graph/symbols/data.rs`.
fn build_symbols() -> AnyResult<()> {
+ fn title_case(s: &str) -> String {
+ let mut iter = s.chars();
+ let first = iter.next().unwrap().to_ascii_uppercase();
+ let rest = iter.as_str();
+ format!("{first}{rest}")
+ }
+
let input_file = Path::new(env!("CARGO_MANIFEST_DIR")).join("src/spv/read/graph/symbols.svg");
println!("cargo:rerun-if-changed={}", input_file.display());
let input = read_to_string(&input_file)
in_defs = true;
} else if in_defs && local_name.as_ref() == b"g" {
let span = reader.read_to_end(start.to_end().name()).unwrap();
- let content = &input[span.start as usize..span.end as usize];
+ let content = input[span.start as usize..span.end as usize].trim_matches('\n');
let mut attributes = start
.attributes()
.map(|attribute| {
"false" => false,
_ => unreachable!(),
};
- symbols.push((name.clone(), hollow, content));
+ let kind = attributes.remove("kind").unwrap_or_default();
+ let sides = attributes
+ .remove("sides")
+ .map(|sides| sides.parse::<usize>().unwrap());
+ symbols.push((name.clone(), hollow, kind.clone(), sides, content));
if !hollow {
- let mut iter = name.chars();
- let first = iter.next().unwrap().to_ascii_uppercase();
- let rest = iter.as_str();
- symbols.push((format!("hollow{first}{rest}"), true, content));
+ symbols.push((
+ format!("hollow{}", title_case(&name)),
+ true,
+ kind,
+ sides,
+ content,
+ ));
}
}
}
let mut output = String::new();
writeln!(
&mut output,
- "use super::Symbol;
+ "use super::{{Symbol, SymbolKind}};
pub static SYMBOLS: [Symbol; {}] = [",
symbols.len()
)
.unwrap();
- for (name, hollow, content) in &symbols {
- let mut writer = Writer::new(Cursor::new(Vec::new()));
- writer
- .create_element("g")
- .with_attribute(("id", name.as_str()))
- .write_inner_content(|w| w.get_mut().write_all(content.as_bytes()))
- .unwrap();
- let definition = String::from_utf8(writer.into_inner().into_inner()).unwrap();
+ for (name, hollow, kind, sides, content) in &symbols {
+ let kind = if kind == "" {
+ format!("SymbolKind::Fixed({content:?})")
+ } else {
+ format!("SymbolKind::{}({})", title_case(&kind), sides.unwrap())
+ };
writeln!(
&mut output,
" Symbol {{
name: {name:?},
hollow: {hollow:?},
- definition: {definition:?},
+ kind: {kind},
}},"
)
.unwrap();
output::{
SpvGraphMembers, SpvInfo, SpvMembers,
drivers::cairo::xr_to_pt,
- pivot::{
- Axis2, Axis3, Length, ParseLengthError,
- look::{Color, VertAlign},
- },
+ pivot::{Axis2, Axis3, Length, ParseLengthError, look::Color},
},
spv::{
legacy_bin::{DataSeries, DataSource, DataSources, LegacyBin, LegacyBinWarning},
read::graph::{
format::Formatter,
frames::{Component, Frames, Position},
+ style::{Style, StyleCycle},
},
},
};
mod format;
mod frames;
mod patterns;
+mod style;
mod symbols;
#[derive(Debug)]
to: String,
}
-#[derive(Clone, Deserialize, Debug)]
-#[serde(rename_all = "camelCase")]
-struct StyleCycle {
- #[serde(default, rename = "cycle")]
- cycles: Vec<Cycle>,
-}
-
-#[derive(Clone, Deserialize, Debug)]
-#[serde(rename_all = "camelCase")]
-struct Cycle {
- #[serde(default, rename = "style")]
- styles: Vec<Style>,
-}
-
-#[derive(Clone, Deserialize, Debug)]
-#[serde(rename_all = "camelCase")]
-struct Style {
- #[serde(rename = "@id")]
- id: Option<String>,
-
- /// The text color or, in some cases, background color.
- #[serde(rename = "@color")]
- color: Option<Color>,
-
- #[serde(rename = "@color2")]
- color2: Option<Color>,
-
- #[serde(rename = "@font-family")]
- font_family: Option<String>,
-
- #[serde(rename = "@font-size")]
- font_size: Option<LocationLengthRange>,
-
- #[serde(rename = "@font-weight")]
- font_weight: Option<FontWeight>,
-
- #[serde(rename = "@font-style")]
- font_style: Option<FontStyle>,
-
- #[serde(rename = "@font-underline")]
- font_underline: Option<FontUnderline>,
-
- #[serde(rename = "@textAlignment")]
- text_alignment: Option<TextAlignment>,
-
- #[serde(rename = "@labelLocationVertical")]
- label_location_vertical: Option<LabelLocation>,
-
- #[serde(rename = "@visible")]
- visible: Option<bool>,
-
- #[serde(rename = "@decimal-offset")]
- decimal_offset: Option<Length>,
-
- #[serde(rename = "@symbol")]
- symbol: Option<Symbol>,
-
- #[serde(rename = "@stroke-linecap")]
- stroke_linecap: Option<StrokeLinecap>,
-
- #[serde(rename = "@stroke-width")]
- stroke_width: Option<Length>,
-
- #[serde(rename = "@text-fit")]
- text_fit: Option<bool>,
-
- #[serde(rename = "@padding")]
- padding: Option<Length>,
-
- #[serde(rename = "@size")]
- size: Option<LocationLength>,
-
- #[serde(rename = "@pattern")]
- pattern: Option<u32>,
-
- #[serde(default, rename = "$value")]
- children: Vec<Style>,
-}
-
-impl Style {
- /// Updates this style with the overrides in `override`.
- fn overridden_with(mut self, overrides: &Style) -> Self {
- if let Some(id) = &overrides.id {
- self.id = Some(id.clone());
- }
- if let Some(color) = overrides.color {
- self.color = Some(color);
- }
- if let Some(color2) = overrides.color2 {
- self.color2 = Some(color2);
- }
- if let Some(font_family) = &overrides.font_family {
- self.font_family = Some(font_family.clone());
- }
- if let Some(font_size) = &overrides.font_size {
- self.font_size = Some(font_size.clone());
- }
- if let Some(font_weight) = overrides.font_weight {
- self.font_weight = Some(font_weight);
- }
- if let Some(font_style) = overrides.font_style {
- self.font_style = Some(font_style);
- }
- if let Some(font_underline) = overrides.font_underline {
- self.font_underline = Some(font_underline);
- }
- if let Some(text_alignment) = overrides.text_alignment {
- self.text_alignment = Some(text_alignment);
- }
- if let Some(label_location_vertical) = overrides.label_location_vertical {
- self.label_location_vertical = Some(label_location_vertical);
- }
- if let Some(visible) = overrides.visible {
- self.visible = Some(visible);
- }
- if let Some(decimal_offset) = overrides.decimal_offset {
- self.decimal_offset = Some(decimal_offset);
- }
- if let Some(symbol) = &overrides.symbol {
- self.symbol = Some(symbol.clone());
- }
- if let Some(stroke_linecap) = overrides.stroke_linecap {
- self.stroke_linecap = Some(stroke_linecap);
- }
- if let Some(stroke_width) = overrides.stroke_width {
- self.stroke_width = Some(stroke_width);
- }
- if let Some(text_fit) = overrides.text_fit {
- self.text_fit = Some(text_fit);
- }
- if let Some(padding) = overrides.padding {
- self.padding = Some(padding);
- }
- if let Some(size) = overrides.size {
- self.size = Some(size);
- }
- self
- }
-}
-
-#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
-#[serde(rename_all = "camelCase")]
-enum FontWeight {
- Regular,
- Bold,
-}
-
-#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
-#[serde(rename_all = "camelCase")]
-enum FontStyle {
- Regular,
- Italic,
-}
-
-#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
-#[serde(rename_all = "camelCase")]
-enum FontUnderline {
- None,
- Underline,
-}
-
-#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
-#[serde(rename_all = "camelCase")]
-enum TextAlignment {
- Left,
- Right,
- Center,
- Decimal,
- Mixed,
-}
-
-#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
-#[serde(rename_all = "camelCase")]
-enum LabelLocation {
- Positive,
- Negative,
- Center,
-}
-
-impl From<LabelLocation> for VertAlign {
- fn from(value: LabelLocation) -> Self {
- match value {
- LabelLocation::Positive => VertAlign::Top,
- LabelLocation::Negative => VertAlign::Bottom,
- LabelLocation::Center => VertAlign::Middle,
- }
- }
-}
-
-#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
-#[serde(rename_all = "camelCase")]
-enum Symbol {
- #[serde(rename = "ibeam")]
- IBeam,
- Bowtie,
- HollowBowtie,
- Flower,
- Plus,
- Polygon,
- Cross,
- Circle,
- HollowCircle,
- Star,
- HollowStar,
- Square,
- HollowSquare,
- AcrossDown,
- AcrossDownAcross,
- AcrossDownArrow,
- AcrossDownAcrossArrow,
- DownAcross,
- DownAcrossDown,
- DownAcrossArrow,
- DownAcrossDownArrow,
- Arrow,
- Line,
- Male,
- Female,
- Star3,
- Star4,
- Star5,
- Star6,
- Star7,
- Star8,
- Star9,
- Star10,
- HollowStar3,
- HollowStar4,
- HollowStar5,
- HollowStar6,
- HollowStar7,
- HollowStar8,
- HollowStar9,
- HollowStar10,
- Flower3,
- Flower4,
- Flower5,
- Flower6,
- Flower7,
- Flower8,
- Flower9,
- Flower10,
- Triangle,
- Pentagon,
- Hexagon,
- Heptagon,
- Octagon,
- Nonagon,
- Decagon,
- HollowTriangle,
- HollowPentagon,
- HollowHexagon,
- HollowHeptagon,
- HollowOctagon,
- HollowNonagon,
- HollowDecagon,
- RoundRectangle,
- HollowRoundRectangle,
- Elbow,
- ElbowArrow,
- Arc,
- ArcDown,
- None,
-}
-
-#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
-#[serde(rename_all = "camelCase")]
-enum StrokeLinecap {
- Round,
- Square,
- Butt,
-}
-
struct Element<'a> {
raw: &'a RawElement,
graphic: Graphic,
--- /dev/null
+use serde::{Deserialize, Deserializer};
+
+use crate::{
+ output::pivot::{
+ Length,
+ look::{Color, HorzAlign, VertAlign},
+ },
+ spv::read::graph::{LocationLength, LocationLengthRange},
+};
+
+#[derive(Clone, Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+pub struct StyleCycle {
+ #[serde(default, rename = "cycle")]
+ cycles: Vec<Cycle>,
+}
+
+impl StyleCycle {
+ pub fn get_style(&self, mut index: usize) -> Style {
+ let mut style: Option<Style> = None;
+ for cycle in &self.cycles {
+ let len = cycle.styles.len();
+ if len != 0 {
+ let s = &cycle.styles[index % len];
+ style = Some(style.map_or_else(|| s.clone(), |style| style.overridden_with(s)));
+ index /= len;
+ }
+ }
+ style.unwrap_or_default()
+ }
+}
+
+#[derive(Clone, Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+pub struct Cycle {
+ #[serde(default, rename = "style")]
+ styles: Vec<Style>,
+}
+
+#[derive(Clone, Deserialize, Debug, Default)]
+#[serde(rename_all = "camelCase")]
+#[serde(default)]
+pub struct Style {
+ #[serde(rename = "@id")]
+ pub id: Option<String>,
+
+ /// The text color or, in some cases, background color.
+ #[serde(rename = "@color")]
+ pub color: Option<Color>,
+
+ #[serde(rename = "@color2")]
+ pub color2: Option<Color>,
+
+ #[serde(rename = "@visible")]
+ pub visible: Option<bool>,
+
+ #[serde(rename = "@padding")]
+ pub padding: Option<Length>,
+
+ #[serde(rename = "@pattern")]
+ pub pattern: Option<u32>,
+
+ #[serde(flatten)]
+ pub symbol_style: SymbolStyle,
+
+ #[serde(flatten)]
+ pub stroke_style: StrokeStyle,
+
+ #[serde(flatten)]
+ pub label_style: LabelStyle,
+
+ #[serde(default, rename = "$value")]
+ pub children: Vec<Style>,
+}
+
+impl Style {
+ /// Updates this style with the overrides in `override`.
+ pub fn overridden_with(self, overrides: &Style) -> Self {
+ Self {
+ id: overrides.id.clone().or(self.id),
+ color: overrides.color.or(self.color),
+ color2: overrides.color2.or(self.color2),
+ visible: overrides.visible.or(self.visible),
+ padding: overrides.padding.or(self.padding),
+ pattern: overrides.pattern.or(self.pattern),
+ symbol_style: self.symbol_style.overridden_with(&overrides.symbol_style),
+ stroke_style: self.stroke_style.overridden_with(&overrides.stroke_style),
+ label_style: self.label_style.overridden_with(&overrides.label_style),
+ children: self.children,
+ }
+ }
+}
+
+#[derive(Clone, Deserialize, Debug, Default)]
+#[serde(default, rename_all = "camelCase")]
+pub struct SymbolStyle {
+ #[serde(rename = "@symbol")]
+ pub symbol: Option<String>,
+
+ #[serde(rename = "@glyph-sides")]
+ pub sides: Option<usize>,
+
+ #[serde(rename = "@glyph-angle")]
+ pub angle: Option<f64>,
+
+ #[serde(rename = "@size")]
+ pub size: Option<LocationLength>,
+
+ #[serde(rename = "@glyph-aspect")]
+ pub aspect: Option<f64>,
+}
+
+impl SymbolStyle {
+ pub fn overridden_with(self, overrides: &Self) -> Self {
+ Self {
+ symbol: overrides.symbol.clone().or(self.symbol),
+ sides: overrides.sides.or(self.sides),
+ angle: overrides.angle.or(self.angle),
+ size: overrides.size.clone().or(self.size),
+ aspect: overrides.aspect.or(self.aspect),
+ }
+ }
+}
+
+#[derive(Clone, Deserialize, Default, Debug)]
+#[serde(default, rename_all = "camelCase")]
+pub struct StrokeStyle {
+ #[serde(rename = "@stroke-dasharray")]
+ dasharray: Option<String>,
+
+ #[serde(rename = "@stroke-linecap")]
+ linecap: Option<StrokeLinecap>,
+
+ #[serde(rename = "@stroke-width")]
+ width: Option<Length>,
+
+ #[serde(rename = "@stroke-decoration")]
+ decoration: Option<StrokeDecoration>,
+}
+
+impl StrokeStyle {
+ pub fn overridden_with(self, overrides: &Self) -> Self {
+ Self {
+ dasharray: overrides.dasharray.clone().or(self.dasharray),
+ linecap: overrides.linecap.or(self.linecap),
+ width: overrides.width.or(self.width),
+ decoration: overrides.decoration.or(self.decoration),
+ }
+ }
+}
+
+#[derive(Clone, Deserialize, Default, Debug)]
+#[serde(default, rename_all = "camelCase")]
+pub struct LabelStyle {
+ #[serde(rename = "@font-family")]
+ family: Option<String>,
+
+ #[serde(rename = "@font-size")]
+ size: Option<LocationLengthRange>,
+
+ #[serde(rename = "@font-weight")]
+ weight: Option<FontWeight>,
+
+ #[serde(rename = "@font-style")]
+ slant: Option<FontSlant>,
+
+ #[serde(rename = "@font-underline")]
+ underline: Option<FontUnderline>,
+
+ #[serde(rename = "@textAlignment")]
+ text_alignment: Option<TextAlignment>,
+
+ #[serde(rename = "@decimal-offset")]
+ decimal_offset: Option<Length>,
+
+ #[serde(rename = "@labelLocationVertical")]
+ #[serde(deserialize_with = "deserialize_vert_align")]
+ vert_align: Option<VertAlign>,
+
+ #[serde(rename = "@text-fit")]
+ text_fit: Option<bool>,
+
+ #[serde(rename = "@ignoreNewLines")]
+ ignore_new_lines: Option<bool>,
+
+ #[serde(rename = "@labelInside")]
+ placement: Option<LabelPlacement>,
+}
+
+impl LabelStyle {
+ pub fn horz_align(&self) -> Option<HorzAlign> {
+ match self.text_alignment.unwrap_or_default() {
+ TextAlignment::Left => Some(HorzAlign::Left),
+ TextAlignment::Right => Some(HorzAlign::Right),
+ TextAlignment::Center => Some(HorzAlign::Center),
+ TextAlignment::Decimal => match self.decimal_offset {
+ Some(offset) => Some(HorzAlign::Decimal {
+ offset: offset.as_px_f64(),
+ }),
+ None => None,
+ },
+ TextAlignment::Mixed => None,
+ }
+ }
+
+ pub fn overridden_with(self, overrides: &Self) -> Self {
+ Self {
+ family: overrides.family.clone().or(self.family),
+ size: overrides.size.clone().or(self.size),
+ weight: overrides.weight.or(self.weight),
+ slant: overrides.slant.or(self.slant),
+ underline: overrides.underline.or(self.underline),
+ text_alignment: overrides.text_alignment.or(self.text_alignment),
+ vert_align: overrides.vert_align.or(self.vert_align),
+ decimal_offset: overrides.decimal_offset.or(self.decimal_offset),
+ text_fit: overrides.text_fit.or(self.text_fit),
+ ignore_new_lines: overrides.ignore_new_lines.or(self.ignore_new_lines),
+ placement: overrides.placement.or(self.placement),
+ }
+ }
+}
+
+#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
+#[serde(rename_all = "camelCase")]
+enum LabelPlacement {
+ /// Place label inside target.
+ #[serde(rename = "true")]
+ Inside,
+
+ /// Place label outside target.
+ #[serde(rename = "false")]
+ Outside,
+
+ /// Label inside if it fits, otherwise outside.
+ #[serde(rename = "preferred")]
+ PreferInside,
+}
+
+#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
+#[serde(rename_all = "camelCase")]
+enum FontWeight {
+ Regular,
+ Bold,
+}
+
+#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
+#[serde(rename_all = "camelCase")]
+enum FontSlant {
+ Regular,
+ Italic,
+}
+
+#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
+#[serde(rename_all = "camelCase")]
+enum FontUnderline {
+ None,
+ Underline,
+}
+
+#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Deserialize)]
+#[serde(rename_all = "camelCase")]
+enum TextAlignment {
+ Left,
+ Right,
+ Center,
+ Decimal,
+ #[default]
+ Mixed,
+}
+
+fn deserialize_vert_align<'de, D>(deserializer: D) -> Result<Option<VertAlign>, D::Error>
+where
+ D: Deserializer<'de>,
+{
+ #[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
+ #[serde(rename_all = "camelCase")]
+ enum LabelLocationVertical {
+ Positive,
+ Negative,
+ Center,
+ }
+
+ impl From<LabelLocationVertical> for VertAlign {
+ fn from(value: LabelLocationVertical) -> Self {
+ match value {
+ LabelLocationVertical::Positive => VertAlign::Top,
+ LabelLocationVertical::Negative => VertAlign::Bottom,
+ LabelLocationVertical::Center => VertAlign::Middle,
+ }
+ }
+ }
+
+ Ok(<Option<LabelLocationVertical>>::deserialize(deserializer)?.map(VertAlign::from))
+}
+
+#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
+#[serde(rename_all = "camelCase")]
+enum StrokeLinecap {
+ Round,
+ Square,
+ Butt,
+}
+
+#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
+#[serde(rename_all = "camelCase")]
+enum StrokeDecoration {
+ EtchLowered,
+ EtchRaised,
+ Bevel,
+}
+use std::f64::consts::PI;
+use std::fmt::Write as _;
+
+use svg::{
+ Node,
+ node::{
+ Blob,
+ element::{Group, Line, Polygon},
+ },
+};
+
use crate::spv::read::graph::symbols::data::SYMBOLS;
mod data;
+#[derive(Copy, Clone, Debug)]
+pub enum SymbolKind {
+ Fixed(
+ /// Definition as SVG as textual XML.
+ &'static str,
+ ),
+ Polygon(
+ /// Number of sides on the polygon (at least 3).
+ usize,
+ ),
+ Star(
+ /// Number of points on the star (at least 3).
+ usize,
+ ),
+ Flower(
+ /// Number of petals on the flower (at least 3).
+ usize,
+ ),
+}
+
+impl SymbolKind {
+ pub fn with_sides(self, sides: usize) -> Self {
+ match self {
+ SymbolKind::Fixed(_) => self,
+ SymbolKind::Polygon(_) => Self::Polygon(sides),
+ SymbolKind::Star(_) => Self::Star(sides),
+ SymbolKind::Flower(_) => Self::Flower(sides),
+ }
+ }
+
+ pub fn to_svg(&self) -> Group {
+ fn round(f: f64) -> f64 {
+ (f * 100_000.0).round() / 100_000.0
+ }
+
+ match *self {
+ SymbolKind::Fixed(xml) => Group::new().add(Blob::new(xml)),
+ SymbolKind::Polygon(n) => {
+ let n = n.clamp(3, 100);
+ let mut points = String::new();
+ for i in 0..=n {
+ let d = (i * 2) as f64 * PI / n as f64;
+ write!(
+ &mut points,
+ "{},{} ",
+ round(5.0 - 5.0 * d.sin()),
+ round(5.0 - 5.0 * d.cos()),
+ )
+ .unwrap();
+ }
+ points.pop();
+ Group::new().add(Polygon::new().set("points", points))
+ }
+ SymbolKind::Star(n) => {
+ let n = n.clamp(3, 100);
+ let mut points = String::new();
+ for i in 1..=n {
+ let d = (i * 2 - 1) as f64 * PI / n as f64;
+ let d2 = (i * 2) as f64 * PI / n as f64;
+ write!(
+ &mut points,
+ "{},{} {},{} ",
+ round(5.0 + 2.0 * d.sin()),
+ round(5.0 - 2.0 * d.cos()),
+ round(5.0 + 5.0 * d2.sin()),
+ round(5.0 - 5.0 * d2.cos())
+ )
+ .unwrap();
+ }
+ points.pop();
+ Group::new().add(Polygon::new().set("points", points))
+ }
+ SymbolKind::Flower(n) => {
+ let n = n.clamp(3, 100);
+ let mut group = Group::new();
+ for i in 0..n {
+ let d = PI * 2.0 * i as f64 / n as f64;
+ group.append(
+ Line::new()
+ .set("x1", 5)
+ .set("y1", 5)
+ .set("x2", round(5.0 + 5.0 * d.sin()))
+ .set("y2", round(5.0 - 5.0 * d.cos())),
+ );
+ }
+ group
+ }
+ }
+ }
+}
+
/// A symbol for marking points in a graph.
-#[derive(Clone, Debug)]
+#[derive(Copy, Clone, Debug)]
pub struct Symbol {
/// The symbol's name.
pub name: &'static str,
/// - A non-hollow symbol has a stroke color and an optional fill color.
pub hollow: bool,
- /// The XML definition of the symbol, as a complete XML element whose `id`
- /// attribute is `name`.
- ///
- /// The definition does not include `fill` or `stroke` attributes. It is
- /// intended to be included in a `<defs>` element and instantiated with
- /// `<use>`.
- pub definition: &'static str,
+ /// Kind of symbol.
+ pub kind: SymbolKind,
}
impl Symbol {
///
/// Symbol names are matched case-insensitively. Use the `name` member of
/// the returned symbol for the canonical name.
- pub fn for_name(name: &str) -> Option<&'static Self> {
+ ///
+ /// Some symbols have a parameterized number of sides. These are returned
+ /// with a default number of sides. Use [with_sides](Self::with_sides) to
+ /// change the number of sides.
+ pub fn for_name(name: &str) -> Option<Self> {
SYMBOLS
.iter()
.find(|symbol| symbol.name.eq_ignore_ascii_case(name))
+ .copied()
}
/// Returns true if the symbol is hollow.
pub fn is_hollow(&self) -> bool {
self.hollow
}
+
+ pub fn with_sides(self, sides: usize) -> Self {
+ Self {
+ kind: self.kind.with_sides(sides),
+ ..self
+ }
+ }
+
+ pub fn to_svg(&self) -> Group {
+ self.kind.to_svg()
+ }
}
#[cfg(test)]
use crate::spv::read::graph::symbols::Symbol;
#[test]
- fn symbol() {
+ fn flower() {
+ let symbol = Symbol::for_name("flower").unwrap().with_sides(6);
+ assert_eq!(
+ symbol.to_svg().to_string(),
+ r#"<g>
+<line x1="5" x2="5" y1="5" y2="0"/>
+<line x1="5" x2="9.33013" y1="5" y2="2.5"/>
+<line x1="5" x2="9.33013" y1="5" y2="7.5"/>
+<line x1="5" x2="5" y1="5" y2="10"/>
+<line x1="5" x2="0.66987" y1="5" y2="7.5"/>
+<line x1="5" x2="0.66987" y1="5" y2="2.5"/>
+</g>"#
+ );
+ }
+
+ #[test]
+ fn star() {
+ let symbol = Symbol::for_name("star").unwrap().with_sides(6);
+ assert_eq!(
+ symbol.to_svg().to_string(),
+ r#"<g>
+<polygon points="6,3.26795 9.33013,2.5 7,5 9.33013,7.5 6,6.73205 5,10 4,6.73205 0.66987,7.5 3,5 0.66987,2.5 4,3.26795 5,0"/>
+</g>"#
+ );
+ }
+
+ #[test]
+ fn polygon() {
+ let symbol = Symbol::for_name("polygon").unwrap().with_sides(6);
+ assert_eq!(
+ symbol.to_svg().to_string(),
+ r#"<g>
+<polygon points="5,0 0.66987,2.5 0.66987,7.5 5,10 9.33013,7.5 9.33013,2.5 5,0"/>
+</g>"#
+ );
+ }
+
+ #[test]
+ fn square() {
let symbol = Symbol::for_name("SQUARE").unwrap();
assert_eq!(symbol.name, "square");
assert_eq!(symbol.hollow, false);
assert_eq!(
- symbol.definition,
- r#"<g id="square">
+ symbol.to_svg().to_string(),
+ r#"<g>
<rect x="0" y="0" width="10" height="10" />
- </g>"#
+
+</g>"#
);
let symbol = Symbol::for_name("hollowsquare").unwrap();
assert_eq!(symbol.name, "hollowSquare");
assert_eq!(symbol.hollow, true);
assert_eq!(
- symbol.definition,
- r#"<g id="hollowSquare">
+ symbol.to_svg().to_string(),
+ r#"<g>
<rect x="0" y="0" width="10" height="10" />
- </g>"#
+
+</g>"#
);
}
}
Each pattern nominally occupies the box (0,0)-(10,10), although a few
spill over the margin.
-->
+ <g id="none" hollow="true" />
<g id="square" hollow="false">
<rect x="0" y="0" width="10" height="10" />
</g>
- <g id="none" hollow="true" />
<g id="arc" hollow="true">
<path d="M0,5 A5 5 180 1 1 10,5" fill="none" />
</g>
<rect x="0" y="0" width="10" height="10" rx="2.5" ry="2.5" />
</g>
<!--
- These flowers can be regenerated with the following Python program:
-
- #! /usr/bin/env python
- from math import sin, cos, pi
-
- for n in range(3, 11):
- print(f' <g id="flower{n}" hollow="true">')
- for i in range(n):
- d = pi * 2 * i / n
- x = 5 + 5 * sin(d)
- y = 5 - 5 * cos(d)
- print(f' <line x1="5" y1="5" x2="{x}" y2="{y}"/>')
- print(" </g>")
+ The contents for these flowers are ignored; PSPP regenerates them
+ internally. They are supplied only for the names and to show as
+ examples in the palette.
-->
- <g id="flower3" hollow="true">
+ <g id="flower" hollow="true" kind="flower" sides="5"/>
+ <g id="flower3" hollow="true" kind="flower" sides="3">
<line x1="5" y1="5" x2="5.0" y2="0.0" />
<line x1="5" y1="5" x2="9.330127018922195" y2="7.499999999999999" />
<line x1="5" y1="5" x2="0.6698729810778081" y2="7.500000000000002" />
</g>
- <g id="flower4" hollow="true">
+ <g id="flower4" hollow="true" kind="flower" sides="4">
<line x1="5" y1="5" x2="5.0" y2="0.0" />
<line x1="5" y1="5" x2="10.0" y2="5.0" />
<line x1="5" y1="5" x2="5.000000000000001" y2="10.0" />
<line x1="5" y1="5" x2="0.0" y2="5.000000000000001" />
</g>
- <g id="flower5" hollow="true">
+ <g id="flower5" hollow="true" kind="flower" sides="5">
<line x1="5" y1="5" x2="5.0" y2="0.0" />
<line x1="5" y1="5" x2="9.755282581475768" y2="3.4549150281252627" />
<line x1="5" y1="5" x2="7.938926261462367" y2="9.045084971874736" />
<line x1="5" y1="5" x2="2.061073738537635" y2="9.045084971874738" />
<line x1="5" y1="5" x2="0.2447174185242318" y2="3.4549150281252636" />
</g>
- <g id="flower6" hollow="true">
+ <g id="flower6" hollow="true" kind="flower" sides="6">
<line x1="5" y1="5" x2="5.0" y2="0.0" />
<line x1="5" y1="5" x2="9.330127018922193" y2="2.4999999999999996" />
<line x1="5" y1="5" x2="9.330127018922195" y2="7.499999999999999" />
<line x1="5" y1="5" x2="0.6698729810778081" y2="7.500000000000002" />
<line x1="5" y1="5" x2="0.6698729810778072" y2="2.4999999999999996" />
</g>
- <g id="flower7" hollow="true">
+ <g id="flower7" hollow="true" kind="flower" sides="7">
<line x1="5" y1="5" x2="5.0" y2="0.0" />
<line x1="5" y1="5" x2="8.90915741234015" y2="1.8825509907063323" />
<line x1="5" y1="5" x2="9.874639560909118" y2="6.112604669781572" />
<line x1="5" y1="5" x2="0.12536043909088157" y2="6.1126046697815735" />
<line x1="5" y1="5" x2="1.0908425876598504" y2="1.8825509907063331" />
</g>
- <g id="flower8" hollow="true">
+ <g id="flower8" hollow="true" kind="flower" sides="8">
<line x1="5" y1="5" x2="5.0" y2="0.0" />
<line x1="5" y1="5" x2="8.535533905932738" y2="1.4644660940672622" />
<line x1="5" y1="5" x2="10.0" y2="5.0" />
<line x1="5" y1="5" x2="0.0" y2="5.000000000000001" />
<line x1="5" y1="5" x2="1.4644660940672614" y2="1.4644660940672631" />
</g>
- <g id="flower9" hollow="true">
+ <g id="flower9" hollow="true" kind="flower" sides="9">
<line x1="5" y1="5" x2="5.0" y2="0.0" />
<line x1="5" y1="5" x2="8.213938048432697" y2="1.16977778440511" />
<line x1="5" y1="5" x2="9.92403876506104" y2="4.131759111665348" />
<line x1="5" y1="5" x2="0.07596123493895934" y2="4.1317591116653505" />
<line x1="5" y1="5" x2="1.786061951567302" y2="1.169777784405111" />
</g>
- <g id="flower10" hollow="true">
+ <g id="flower10" hollow="true" kind="flower" sides="10">
<line x1="5" y1="5" x2="5.0" y2="0.0" />
<line x1="5" y1="5" x2="7.938926261462366" y2="0.9549150281252627" />
<line x1="5" y1="5" x2="9.755282581475768" y2="3.4549150281252627" />
<line x1="5" y1="5" x2="2.061073738537633" y2="0.9549150281252636" />
</g>
<!--
- These polygons can be regenerated with the following Python program:
-
- #! /usr/bin/env python
- from math import sin, cos, pi
-
- for n, name in (
- (3, "triangle"),
- (5, "pentagon"),
- (6, "hexagon"),
- (7, "heptagon"),
- (8, "octagon"),
- (9, "nonagon"),
- (10, "decagon"),
- ):
- points = []
- for i in range(0, n + 1):
- d = i * 2 * pi / n
- points += [5 - 5 * sin(d), 5 - 5 * cos(d)]
- print(f' <g id="{name}" hollow="false">')
- print(f' <polygon points="{' '.join([str(p) for p in points])}"/>')
- print(" </g>")
+ The contents for these polygons are ignored; PSPP regenerates them
+ internally. They are supplied only for the names and to show as
+ examples in the palette.
-->
- <g id="triangle" hollow="false">
+ <g id="polygon" hollow="false" kind="polygon" sides="5"/>
+ <g id="triangle" hollow="false" kind="polygon" sides="3">
<polygon points="5.0 0.0 0.6698729810778064 7.499999999999999 9.330127018922191 7.500000000000002 5.000000000000001 0.0" />
</g>
- <g id="pentagon" hollow="false">
+ <g id="diamond" hollow="false" kind="polygon" sides="4">
+ <polygon points="5,0 0,5 4.999999999999999,10 10,5.000000000000001 5.000000000000001,0"/>
+ </g>
+ <g id="pentagon" hollow="false" kind="polygon" sides="5">
<polygon points="5.0 0.0 0.24471741852423268 3.4549150281252627 2.0610737385376336 9.045084971874736 7.938926261462365 9.045084971874738 9.755282581475768 3.4549150281252636 5.000000000000001 0.0" />
</g>
- <g id="hexagon" hollow="false">
+ <g id="hexagon" hollow="false" kind="polygon" sides="6">
<polygon points="5.0 0.0 0.6698729810778072 2.4999999999999996 0.6698729810778064 7.499999999999999 4.999999999999999 10.0 9.330127018922191 7.500000000000002 9.330127018922193 2.4999999999999996 5.000000000000001 0.0" />
</g>
- <g id="heptagon" hollow="false">
+ <g id="heptagon" hollow="false" kind="polygon" sides="7">
<polygon points="5.0 0.0 1.0908425876598509 1.8825509907063323 0.12536043909088157 6.112604669781572 2.830581304412209 9.504844339512095 7.16941869558779 9.504844339512097 9.874639560909118 6.1126046697815735 8.90915741234015 1.8825509907063331 5.000000000000001 0.0" />
</g>
- <g id="octagon" hollow="false">
+ <g id="octagon" hollow="false" kind="polygon" sides="8">
<polygon points="5.0 0.0 1.4644660940672627 1.4644660940672622 0.0 5.0 1.4644660940672622 8.535533905932738 4.999999999999999 10.0 8.535533905932738 8.535533905932738 10.0 5.000000000000001 8.535533905932738 1.4644660940672631 5.000000000000001 0.0" />
</g>
- <g id="nonagon" hollow="false">
+ <g id="nonagon" hollow="false" kind="polygon" sides="9">
<polygon points="5.0 0.0 1.7860619515673037 1.16977778440511 0.07596123493896023 4.131759111665348 0.6698729810778064 7.499999999999999 3.2898992833716556 9.69846310392954 6.710100716628343 9.698463103929543 9.330127018922191 7.500000000000002 9.924038765061042 4.1317591116653505 8.213938048432698 1.169777784405111 5.000000000000001 0.0" />
</g>
- <g id="decagon" hollow="false">
+ <g id="decagon" hollow="false" kind="polygon" sides="10">
<polygon points="5.0 0.0 2.061073738537634 0.9549150281252627 0.24471741852423268 3.4549150281252627 0.2447174185242318 6.545084971874736 2.0610737385376336 9.045084971874736 4.999999999999999 10.0 7.938926261462365 9.045084971874738 9.755282581475768 6.545084971874738 9.755282581475768 3.4549150281252636 7.938926261462367 0.9549150281252636 5.000000000000001 0.0" />
</g>
<!--
- These polygons can be regenerated with the following Python program:
-
- #! /usr/bin/env python
- from math import sin, cos, pi
-
- for n in range(3, 11):
- points = []
- for i in range(1, n + 1):
- d = (i * 2 - 1) * pi / n
- d2 = i * 2 * pi / n
- points += [5 + 2 * sin(d), 5 - 2 * cos(d)]
- points += [5 + 5 * sin(d2), 5 - 5 * cos(d2)]
- print(f' <g id="star{n}">')
- print(f' <polygon points="{' '.join([str(p) for p in points])}"/>')
- print(" </g>")
+ The contents for these stars are ignored; PSPP regenerates them
+ internally. They are supplied only for the names and to show as
+ examples in the palette.
-->
- <g id="star3" hollow="false">
+ <g id="star" hollow="false" kind="star" sides="5"/>
+ <g id="star3" hollow="false" kind="star" sides="3">
<polygon points="6.732050807568877 4.0 9.330127018922195 7.499999999999999 5.0 7.0 0.6698729810778081 7.500000000000002 3.267949192431123 4.0 4.999999999999999 0.0" />
</g>
- <g id="star4" hollow="false">
+ <g id="star4" hollow="false" kind="star" sides="4">
<polygon points="6.414213562373095 3.585786437626905 10.0 5.0 6.414213562373095 6.414213562373095 5.000000000000001 10.0 3.585786437626905 6.414213562373096 0.0 5.000000000000001 3.5857864376269046 3.585786437626905 4.999999999999999 0.0" />
</g>
- <g id="star5" hollow="false">
+ <g id="star5" hollow="false" kind="star" sides="5">
<polygon points="6.175570504584947 3.381966011250105 9.755282581475768 3.4549150281252627 6.902113032590307 5.618033988749895 7.938926261462367 9.045084971874736 5.0 7.0 2.061073738537635 9.045084971874738 3.0978869674096927 5.618033988749895 0.2447174185242318 3.4549150281252636 3.8244294954150533 3.381966011250105 4.999999999999999 0.0" />
</g>
- <g id="star6" hollow="false">
+ <g id="star6" hollow="false" kind="star" sides="6">
<polygon points="6.0 3.2679491924311224 9.330127018922193 2.4999999999999996 7.0 5.0 9.330127018922195 7.499999999999999 6.0 6.732050807568878 5.000000000000001 10.0 4.000000000000001 6.732050807568878 0.6698729810778081 7.500000000000002 3.0 5.0 0.6698729810778072 2.4999999999999996 3.999999999999999 3.2679491924311233 4.999999999999999 0.0" />
</g>
- <g id="star7" hollow="false">
+ <g id="star7" hollow="false" kind="star" sides="7">
<polygon points="5.867767478235116 3.198062264195162 8.90915741234015 1.8825509907063323 6.949855824363647 4.554958132087371 9.874639560909118 6.112604669781572 6.56366296493606 6.246979603717467 7.169418695587791 9.504844339512095 5.0 7.0 2.83058130441221 9.504844339512097 3.4363370350639406 6.246979603717468 0.12536043909088157 6.1126046697815735 3.050144175636352 4.554958132087373 1.0908425876598504 1.8825509907063331 4.132232521764885 3.198062264195161 4.999999999999999 0.0" />
</g>
- <g id="star8" hollow="false">
+ <g id="star8" hollow="false" kind="star" sides="8">
<polygon points="5.765366864730179 3.1522409349774265 8.535533905932738 1.4644660940672622 6.847759065022574 4.234633135269821 10.0 5.0 6.847759065022574 5.765366864730179 8.535533905932738 8.535533905932738 5.76536686473018 6.847759065022574 5.000000000000001 10.0 4.234633135269821 6.847759065022574 1.4644660940672627 8.535533905932738 3.152240934977427 5.765366864730181 0.0 5.000000000000001 3.152240934977427 4.23463313526982 1.4644660940672614 1.4644660940672631 4.234633135269819 3.152240934977427 4.999999999999999 0.0" />
</g>
- <g id="star9" hollow="false">
+ <g id="star9" hollow="false" kind="star" sides="9">
<polygon points="5.684040286651338 3.120614758428183 8.213938048432697 1.16977778440511 6.732050807568877 4.0 9.92403876506104 4.131759111665348 6.969615506024416 5.34729635533386 9.330127018922195 7.499999999999999 6.285575219373079 6.532088886237956 6.710100716628345 9.69846310392954 5.0 7.0 3.289899283371657 9.698463103929543 3.7144247806269224 6.532088886237957 0.6698729810778081 7.500000000000002 3.0303844939755837 5.347296355333861 0.07596123493895934 4.1317591116653505 3.267949192431122 4.000000000000002 1.786061951567302 1.169777784405111 4.315959713348663 3.120614758428183 4.999999999999999 0.0" />
</g>
- <g id="star10" hollow="false">
+ <g id="star10" hollow="false" kind="star" sides="10">
<polygon points="5.618033988749895 3.0978869674096927 7.938926261462366 0.9549150281252627 6.618033988749895 3.8244294954150537 9.755282581475768 3.4549150281252627 7.0 5.0 9.755282581475768 6.545084971874736 6.618033988749895 6.175570504584946 7.938926261462367 9.045084971874736 5.618033988749895 6.902113032590307 5.000000000000001 10.0 4.381966011250106 6.902113032590307 2.061073738537635 9.045084971874738 3.381966011250105 6.175570504584947 0.24471741852423268 6.545084971874738 3.0 5.0 0.2447174185242318 3.4549150281252636 3.381966011250105 3.824429495415054 2.061073738537633 0.9549150281252636 4.381966011250105 3.0978869674096927 4.999999999999999 0.0" />
</g>
</defs>