]> pintos-os.org Git - pspp/commitdiff
work on styles
authorBen Pfaff <blp@cs.stanford.edu>
Wed, 6 May 2026 14:46:38 +0000 (07:46 -0700)
committerBen Pfaff <blp@cs.stanford.edu>
Wed, 6 May 2026 14:46:38 +0000 (07:46 -0700)
rust/pspp/build.rs
rust/pspp/src/spv/read/graph.rs
rust/pspp/src/spv/read/graph/style.rs [new file with mode: 0644]
rust/pspp/src/spv/read/graph/symbols.rs
rust/pspp/src/spv/read/graph/symbols.svg

index 09f2e017ef82645f8603fbfe7b2490c43b7aac7f..964684f4c8f673f84e84d2e275e44518a2c8ea8c 100644 (file)
@@ -16,7 +16,7 @@
 
 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::{
@@ -25,7 +25,7 @@ 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;
@@ -744,6 +744,13 @@ pub static PATTERNS: [Pattern; 30] = ["
 /// 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)
@@ -763,7 +770,7 @@ fn build_symbols() -> AnyResult<()> {
                     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| {
@@ -780,13 +787,20 @@ fn build_symbols() -> AnyResult<()> {
                         "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,
+                        ));
                     }
                 }
             }
@@ -803,27 +817,25 @@ fn build_symbols() -> AnyResult<()> {
     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();
index 7dd2adb1791f8a606770af6a1d3efaf8bfeb6dca..fd2c73746e43aa96d4656d286a32278543322260 100644 (file)
@@ -57,16 +57,14 @@ use crate::{
     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},
         },
     },
 };
@@ -74,6 +72,7 @@ use crate::{
 mod format;
 mod frames;
 mod patterns;
+mod style;
 mod symbols;
 
 #[derive(Debug)]
@@ -2392,279 +2391,6 @@ struct ValueMapEntry {
     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,
diff --git a/rust/pspp/src/spv/read/graph/style.rs b/rust/pspp/src/spv/read/graph/style.rs
new file mode 100644 (file)
index 0000000..c7dcdfb
--- /dev/null
@@ -0,0 +1,310 @@
+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,
+}
index 376fe034fad396d8ca482b5aede7f37f56cc36d2..812c62024a6d90ca3da06d5ff1071537ec77bde6 100644 (file)
@@ -1,9 +1,111 @@
+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,
@@ -14,13 +116,8 @@ pub struct Symbol {
     /// - 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 {
@@ -28,16 +125,32 @@ 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)]
@@ -45,25 +158,65 @@ mod tests {
     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>"#
         );
     }
 }
index cc8960f645a833eadfc8f32cafbf0c2ba6c2b210..456152de7ae19e35c8aa4236539fc44207d7bb5d 100644 (file)
@@ -8,10 +8,10 @@
         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>