//! The current drawing attributes (xfig's indicator panel) and grid/snap modes. use rustfig_geom::Point; use rustfig_model::{ Arrow, CapStyle, ColorRef, JoinStyle, Justify, LineKind, Object, ObjectKind, PolyKind, Style, Units, }; use serde::{Deserialize, Serialize}; use std::collections::BTreeSet; /// The attribute buttons that carry an update flag in Update mode (xfig's /// `I_UPDATEMASK`, `w_indpanel.h`; `cur_updatemask` in `mode.c`). Only the /// attributes whose flag is on are copied by Update. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum UpdateFlag { TextJust, Font, FontSize, PenColor, FillColor, FillStyle, LineStyle, JoinStyle, CapStyle, LineWidth, Depth, TextAngle, TextFlags, ArrowMode, ArrowType, ArrowSize, BoxRadius, ArcType, } impl UpdateFlag { pub const ALL: [UpdateFlag; 18] = [ UpdateFlag::TextJust, UpdateFlag::Font, UpdateFlag::FontSize, UpdateFlag::PenColor, UpdateFlag::FillColor, UpdateFlag::FillStyle, UpdateFlag::LineStyle, UpdateFlag::JoinStyle, UpdateFlag::CapStyle, UpdateFlag::LineWidth, UpdateFlag::Depth, UpdateFlag::TextAngle, UpdateFlag::TextFlags, UpdateFlag::ArrowMode, UpdateFlag::ArrowType, UpdateFlag::ArrowSize, UpdateFlag::BoxRadius, UpdateFlag::ArcType, ]; /// The flag's name as commands take it (`"line_width"`). pub fn name(self) -> String { serde_json::to_value(self) .ok() .and_then(|v| v.as_str().map(str::to_owned)) .unwrap_or_default() } pub fn from_name(n: &str) -> Option { UpdateFlag::ALL.iter().copied().find(|f| f.name() == n) } } /// Grid spacing / point positioning steps. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] #[serde(rename_all = "snake_case")] pub enum SnapMode { /// Free positioning. Any, /// 1/16 inch or 1 mm. Fine, /// 1/8 inch or 2 mm. #[default] Small, /// 1/4 inch or 5 mm. Medium, /// 1/2 inch or 1 cm. Large, /// 1 inch or 2 cm. Coarse, } impl SnapMode { pub const ALL: [SnapMode; 6] = [ SnapMode::Any, SnapMode::Fine, SnapMode::Small, SnapMode::Medium, SnapMode::Large, SnapMode::Coarse, ]; /// Step in Fig units (0 for free positioning). pub fn step(self, units: Units) -> f64 { let (inch, metric) = match self { SnapMode::Any => (0.0, 0.0), SnapMode::Fine => (75.0, 45.0), SnapMode::Small => (150.0, 90.0), SnapMode::Medium => (300.0, 225.0), SnapMode::Large => (600.0, 450.0), SnapMode::Coarse => (1200.0, 900.0), }; match units { Units::Inches => inch, Units::Metric => metric, } } pub fn label(self, units: Units) -> &'static str { match (self, units) { (SnapMode::Any, _) => "any", (SnapMode::Fine, Units::Inches) => "1/16 in", (SnapMode::Small, Units::Inches) => "1/8 in", (SnapMode::Medium, Units::Inches) => "1/4 in", (SnapMode::Large, Units::Inches) => "1/2 in", (SnapMode::Coarse, Units::Inches) => "1 in", (SnapMode::Fine, Units::Metric) => "1 mm", (SnapMode::Small, Units::Metric) => "2 mm", (SnapMode::Medium, Units::Metric) => "5 mm", (SnapMode::Large, Units::Metric) => "1 cm", (SnapMode::Coarse, Units::Metric) => "2 cm", } } } /// Visible grid (None hides it). pub type GridMode = Option; /// Smart links (xfig's `cur_linkmode`): whether the end points of lines /// attached to a moved shape follow it, and how. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] #[serde(rename_all = "snake_case")] pub enum LinkMode { /// Do not adjust links. #[default] Off, /// Move the attached end point with the box. Move, /// Move the attached end point and slide the point before it so the /// last segment stays axis-aligned (xfig's SMART_SLIDE). Slide, } impl LinkMode { pub const ALL: [LinkMode; 3] = [LinkMode::Off, LinkMode::Move, LinkMode::Slide]; } /// Current attributes applied to new objects, and drawing settings. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(default)] pub struct Indicator { pub line: LineKind, pub thickness: i32, pub pen_color: i32, pub fill_color: i32, /// -1 no fill, 0–40 shade/tint, 41–62 pattern. pub area_fill: i32, pub depth: i32, /// Dash length in 1/80 inch (xfig's `cur_dashlength`). pub style_val: f64, /// Gap between dots of dotted lines in 1/80 inch (xfig's `cur_dotgap`). pub dot_gap: f64, pub cap: CapStyle, pub join: JoinStyle, pub forward_arrow: bool, pub backward_arrow: bool, pub arrow: Arrow, /// Arrow Size panel: the `arrow` thickness, width and length are used as /// they are (xfig's "Absolute Values"); otherwise the multiples below /// of the line width ("Multiple of Line Width", xfig's start state). pub arrow_absolute: bool, /// The multiples of the line width: thickness, width, length /// (`cur_arrow_multthick`, `_multwidth`, `_multheight`: 1, 4, 8). pub arrow_mult: (f64, f64, f64), pub font: i32, pub font_size: f64, /// An installed font (F7) for new texts, `Family Style`; `font` is its /// nearest Fig font. pub local_font: Option, pub postscript: bool, pub justify: Justify, /// Text angle in degrees. pub text_angle: f64, pub hidden_text: bool, pub special_text: bool, pub rigid_text: bool, /// Arc-box corner radius in 1/80 inch. pub box_radius: i32, /// Regular polygon sides. pub sides: usize, /// Rotation step in degrees for the rotate tool. pub rotate_angle: f64, /// Rotated copies made by one copy & rotate click (xfig `cur_numcopies`, 1..=99). pub num_copies: u32, /// Dimension line settings (xfig's `cur_dimline_*`). pub dimline: crate::dimline::DimlineSettings, /// Array copy: columns and rows (xfig `cur_numxcopies`/`cur_numycopies`, 0 = off, 1..=99). pub num_x_copies: u32, pub num_y_copies: u32, pub grid: Option, /// The grid is xfig's isometric one (`cur_gridtype` GRID_ISO): grid lines and /// point positioning follow a 30 degree triangular lattice. pub grid_iso: bool, pub snap: SnapMode, /// Align modes for the align command: "none", "left", "center", "right" / "top", "center", "bottom". pub align_h: String, pub align_v: String, /// Arcs are pie wedges rather than open (xfig's "Arc Type"). pub pie_arc: bool, /// Line spacing of the text tool as a multiple of the font's (xfig's /// "Text Step"). pub text_step: f64, /// Length of the line the Tangent mode adds, in inches or cm (xfig's /// `cur_tangnormlen`, "Tangent/Norm Length": 0 to 20, start 1.0). pub tangent_length: f64, /// Update flags that are switched off (xfig's `cur_updatemask`; all on /// at the start, so the set is empty). pub update_off: BTreeSet, /// Smart links: line end points attached to a moved shape follow it /// (D14: through every geometric operation). pub linkmode: LinkMode, /// Angle geometry: constrains the next point of lines and splines /// (xfig's `cur_anglegeom`). pub angle_geom: crate::AngleGeom, } impl Default for Indicator { fn default() -> Self { Indicator { line: LineKind::Solid, thickness: 1, pen_color: 0, fill_color: 7, area_fill: -1, depth: 50, // xfig's start values (mode.c, main.c's resource defaults). style_val: 4.0, dot_gap: 3.0, cap: CapStyle::Butt, join: JoinStyle::Miter, forward_arrow: false, backward_arrow: false, // xfig's startarrowtype 0: the stick arrow. arrow: Arrow { kind: 0, style: 0, ..Arrow::default() }, arrow_absolute: false, arrow_mult: (1.0, 4.0, 8.0), font: 0, font_size: 12.0, local_font: None, postscript: true, justify: Justify::Left, text_angle: 0.0, hidden_text: false, special_text: false, rigid_text: false, box_radius: 7, sides: 6, rotate_angle: 90.0, num_copies: 1, dimline: crate::dimline::DimlineSettings::default(), num_x_copies: 0, num_y_copies: 0, grid: None, grid_iso: false, // xfig's startposnmode 1: magnet (1/16 inch, 1 mm). snap: SnapMode::Fine, align_h: "none".into(), align_v: "none".into(), pie_arc: false, text_step: 1.0, tangent_length: 1.0, update_off: BTreeSet::new(), // xfig's `cur_linkmode` starts SMART_OFF (mode.c). linkmode: LinkMode::Off, // xfig's `cur_anglegeom` starts L_UNCONSTRAINED. angle_geom: crate::AngleGeom::Unconstrained, } } } impl Indicator { /// The style for a new object. pub fn style(&self) -> Style { Style { line: self.line, thickness: self.thickness.clamp(0, 10_000), pen_color: ColorRef(self.pen_color), fill_color: ColorRef(self.fill_color), depth: self.depth.clamp(0, rustfig_model::MAX_DEPTH), pen_style: -1, area_fill: self.area_fill.clamp(-1, 62), style_val: match self.line { LineKind::Solid | LineKind::Default => 0.0, LineKind::Dotted => self.dot_gap, _ => self.style_val, }, cap: self.cap, join: self.join, } } /// The arrow the indicator describes (type, style and size). /// /// As xfig's `forward_arrow` (`u_create.c`): the absolute values, or /// multiples of the line width (`use_abs_arrowvals`). pub fn arrow_value(&self) -> Arrow { if self.arrow_absolute { return self.arrow; } let line = f64::from(self.thickness.max(1)); let unit = rustfig_geom::FIG_PER_STYLE_UNIT; Arrow { thickness: self.arrow_mult.0 * line, width: self.arrow_mult.1 * line * unit, height: self.arrow_mult.2 * line * unit, ..self.arrow } } pub fn arrows(&self) -> (Option, Option) { let a = self.arrow_value(); (self.forward_arrow.then_some(a), self.backward_arrow.then_some(a)) } /// Whether Update copies this attribute. pub fn updates(&self, f: UpdateFlag) -> bool { !self.update_off.contains(&f) } /// Switch one update flag. pub fn set_update_flag(&mut self, f: UpdateFlag, on: bool) { if on { self.update_off.remove(&f); } else { self.update_off.insert(f); } } /// An arrow slot after Update: the arrow mode decides whether there is /// an arrow, the type and size flags which parts of an existing one /// change (xfig's `up_arrow`). pub fn updated_arrow(&self, old: Option, present: bool) -> Option { let new = self.arrow_value(); let mut a = match old { Some(a) => a, None if self.updates(UpdateFlag::ArrowMode) && present => return Some(new), None => return None, }; if self.updates(UpdateFlag::ArrowMode) && !present { return None; } if self.updates(UpdateFlag::ArrowType) { a.kind = new.kind; a.style = new.style; } if self.updates(UpdateFlag::ArrowSize) { a.thickness = new.thickness; a.width = new.width; a.height = new.height; } Some(a) } /// Update an object's attributes from the indicator, the flags deciding /// which (xfig's `init_update_object`). pub fn update_object(&self, o: &mut Object) { use rustfig_model::text_flags::POSTSCRIPT; use UpdateFlag as F; let style = self.style(); if let ObjectKind::Text(t) = &mut o.kind { if self.updates(F::PenColor) { t.color = ColorRef(self.pen_color); } if self.updates(F::Depth) { t.depth = style.depth; } if self.updates(F::Font) { t.font = self.font; t.local_font = self.local_font.clone(); } if self.updates(F::FontSize) { t.size = self.font_size.clamp(1.0, 2000.0); } if self.updates(F::TextJust) { t.justify = self.justify; } if self.updates(F::TextAngle) && self.text_angle.is_finite() { t.angle = self.text_angle.to_radians(); } // The PostScript/LaTeX flag goes with the font, the others with // the flags (xfig: "use I_FONT instead of I_TEXTFLAGS"). let new = self.text_flags(); let mut f = t.flags; if self.updates(F::TextFlags) { f = (f & POSTSCRIPT) | (new & !POSTSCRIPT); } if self.updates(F::Font) { f = (f & !POSTSCRIPT) | (new & POSTSCRIPT); } t.flags = f; return; } if let ObjectKind::Ellipse(el) = &mut o.kind { if self.updates(F::TextAngle) && self.text_angle.is_finite() { el.angle = self.text_angle.to_radians(); } } // xfig's `update_line`: a picture takes only the line width and the // depth (W22-24). let picture = matches!(&o.kind, ObjectKind::Polyline(pl) if pl.kind == PolyKind::Picture); if let Some(s) = o.style_mut() { if picture { if self.updates(F::LineWidth) { s.thickness = style.thickness; } if self.updates(F::Depth) { s.depth = style.depth; } return; } if self.updates(F::LineStyle) { s.line = style.line; s.style_val = style.style_val; } if self.updates(F::LineWidth) { s.thickness = style.thickness; } if self.updates(F::PenColor) { s.pen_color = style.pen_color; } if self.updates(F::FillColor) { s.fill_color = style.fill_color; } if self.updates(F::FillStyle) { s.area_fill = style.area_fill; } if self.updates(F::Depth) { s.depth = style.depth; } if self.updates(F::CapStyle) { s.cap = style.cap; } if self.updates(F::JoinStyle) { s.join = style.join; } } if let ObjectKind::Polyline(pl) = &mut o.kind { // `update_line` sets the radius of a plain box too, which makes it // an arc-box (W22-24). if matches!(pl.kind, PolyKind::ArcBox | PolyKind::Box) && self.updates(F::BoxRadius) { if pl.kind == PolyKind::Box && self.box_radius > 0 { pl.kind = PolyKind::ArcBox; } if pl.kind == PolyKind::ArcBox { pl.radius = self.box_radius; } } } if let ObjectKind::Arc(a) = &mut o.kind { if self.updates(F::ArcType) { a.pie = self.pie_arc; } // xfig's `update_arc`: a pie-wedge arc has no arrowheads. if a.pie { a.forward_arrow = None; a.backward_arrow = None; } } if let Some((f, b)) = o.arrows_mut() { *f = self.updated_arrow(*f, self.forward_arrow); *b = self.updated_arrow(*b, self.backward_arrow); } } /// Copy an object's attributes into the indicator (xfig's Update mode, /// middle button: "update settings"). The reverse of `object.update`. pub fn take_from(&mut self, o: &Object) { use UpdateFlag as F; let on = |f: F| !self.update_off.contains(&f); let (j, p, fc, fs, ls, jn, cp, lw, dp, ta, tf, am, at, asz, br, atp, fnt, fsz) = ( on(F::TextJust), on(F::PenColor), on(F::FillColor), on(F::FillStyle), on(F::LineStyle), on(F::JoinStyle), on(F::CapStyle), on(F::LineWidth), on(F::Depth), on(F::TextAngle), on(F::TextFlags), on(F::ArrowMode), on(F::ArrowType), on(F::ArrowSize), on(F::BoxRadius), on(F::ArcType), on(F::Font), on(F::FontSize), ); if let ObjectKind::Text(t) = &o.kind { use rustfig_model::text_flags::*; if p { self.pen_color = t.color.0; } if dp { self.depth = t.depth; } if fnt { self.font = t.font; self.local_font = t.local_font.clone(); self.postscript = t.flags & POSTSCRIPT != 0; } if fsz { self.font_size = t.size; } if j { self.justify = t.justify; } if ta { self.text_angle = t.angle.to_degrees(); } if tf { self.rigid_text = t.flags & RIGID != 0; self.special_text = t.flags & SPECIAL != 0; self.hidden_text = t.flags & HIDDEN != 0; } return; } if let (ObjectKind::Ellipse(el), true) = (&o.kind, ta) { self.text_angle = el.angle.to_degrees(); } if let Some(s) = o.style() { if ls { self.line = s.line; if s.style_val > 0.0 { if s.line == LineKind::Dotted { self.dot_gap = s.style_val; } else { self.style_val = s.style_val; } } } if lw { self.thickness = s.thickness; } if p { self.pen_color = s.pen_color.0; } if fc { self.fill_color = s.fill_color.0; } if dp { self.depth = s.depth; } if fs { self.area_fill = s.area_fill; } if cp { self.cap = s.cap; } if jn { self.join = s.join; } } if let Some((f, b)) = o.arrows() { if am { self.forward_arrow = f.is_some(); self.backward_arrow = b.is_some(); } if let Some(a) = f.or(b) { if at { self.arrow.kind = a.kind; self.arrow.style = a.style; } if asz { self.arrow = Arrow { kind: self.arrow.kind, style: self.arrow.style, ..a }; } } } if let (ObjectKind::Polyline(p), true) = (&o.kind, br) { if p.kind == PolyKind::ArcBox { self.box_radius = p.radius; } } if let (ObjectKind::Arc(a), true) = (&o.kind, atp) { self.pie_arc = a.pie; } } pub fn text_flags(&self) -> i32 { use rustfig_model::text_flags::*; let mut f = 0; if self.rigid_text { f |= RIGID; } if self.special_text { f |= SPECIAL; } if self.postscript { f |= POSTSCRIPT; } if self.hidden_text { f |= HIDDEN; } f } /// The point positioning step as an integer, the "magnet" of the LaTeX /// angle modes (xfig: 1 when positioning is free, else /// `max(1, point_spacing())`). pub fn magnet(&self, units: Units) -> i64 { let step = self.snap.step(units); if step.is_finite() && step >= 1.0 { step as i64 } else { 1 } } /// Snap a point to the positioning grid. pub fn snap_point(&self, p: Point, units: Units) -> Point { let step = self.snap.step(units); if step <= 0.0 || !p.is_finite() { return p; } if self.grid_iso && self.grid.is_some() && step >= 2.0 { return snap_iso(p, step); } Point::new((p.x / step).round() * step, (p.y / step).round() * step) } } /// Point positioning on the isometric grid (xfig `round_coords`, `u_draw`/`w_canvas.c`): /// columns `step * cos 30` apart, every other column shifted by half a step. pub fn snap_iso(p: Point, step: f64) -> Point { let c30 = 0.75_f64.sqrt(); let h = (step / 2.0).floor(); let near = |v: f64, unit: f64, half: f64| { let t = v % unit; if t.abs() < half { v - t } else { v + if v >= 0.0 { unit } else { -unit } - t } }; let x = near(p.x, step * c30, h * c30); let odd = ((x / (step * c30)) % 2.0).abs() > 0.5; let y = if odd { let t = (p.y + h) % step; if t.abs() < h { p.y - t } else { p.y + if p.y >= 0.0 { step } else { -step } - t } } else { near(p.y, step, h) }; if x.is_finite() && y.is_finite() { Point::new(x.round(), y.round()) } else { p } } /// xfig's arrow type choices (`arrowtype_choices` in `w_indpanel.c`): 0 is /// the stick arrow, then each type 1–14 hollow and filled. Returns (kind, /// style); the `startarrowtype` resource is such a choice. pub fn arrow_choice(n: i32) -> (i32, i32) { if n <= 0 { (0, 0) } else { let n = n.min(28); ((n + 1) / 2, if n % 2 == 1 { 0 } else { 1 }) } } /// The choice of arrow `kind` and `style` (the inverse of [`arrow_choice`]). pub fn arrow_choice_of(kind: i32, style: i32) -> i32 { if kind <= 0 { 0 } else { kind * 2 - 1 + i32::from(style == 1) } } #[cfg(test)] mod tests { use super::*; #[test] fn snapping() { // xfig's default: magnet, 1/16 inch. let ind = Indicator::default(); assert_eq!(ind.snap, SnapMode::Fine); let p = ind.snap_point(Point::new(160.0, 220.0), Units::Inches); assert_eq!(p, Point::new(150.0, 225.0)); let eighth = Indicator { snap: SnapMode::Small, ..Indicator::default() }; let p = eighth.snap_point(Point::new(160.0, 220.0), Units::Inches); assert_eq!(p, Point::new(150.0, 150.0)); let any = Indicator { snap: SnapMode::Any, ..Indicator::default() }; assert_eq!( any.snap_point(Point::new(1.5, 2.5), Units::Inches), Point::new(1.5, 2.5) ); } /// The start values are xfig's (`mode.c`, `main.c`'s resource defaults, /// `object.h`): gap 23. #[test] fn start_values_are_xfigs() { let i = Indicator::default(); assert_eq!(i.snap, SnapMode::Fine, "startposnmode 1 (magnet)"); assert_eq!(i.grid, None, "startgridmode 0"); assert_eq!((i.arrow.kind, i.arrow.style), (0, 0), "startarrowtype 0"); assert_eq!((i.arrow.width, i.arrow.height), (60.0, 120.0), "DEF_ARROW_WID/HT 4, 8"); assert_eq!((i.style_val, i.dot_gap), (4.0, 3.0), "DEF_DASHLENGTH, DEF_DOTGAP"); assert_eq!(i.text_step, 1.0, "cur_textstep (the man page's 1.2 is wrong)"); assert_eq!((i.font_size, i.thickness, i.depth, i.box_radius), (12.0, 1, 50, 7)); assert_eq!((i.area_fill, i.pen_color, i.fill_color), (-1, 0, 7)); assert_eq!((i.align_h.as_str(), i.align_v.as_str()), ("none", "none")); assert_eq!((i.sides, i.rotate_angle), (6, 90.0)); assert_eq!(i.linkmode, LinkMode::Off, "cur_linkmode 0 (SMART_OFF)"); assert_eq!( i.angle_geom, crate::AngleGeom::Unconstrained, "cur_anglegeom L_UNCONSTRAINED" ); } #[test] fn dotted_lines_use_the_dot_gap() { let i = Indicator { line: LineKind::Dotted, ..Indicator::default() }; assert_eq!(i.style().style_val, 3.0); let d = Indicator { line: LineKind::Dashed, ..Indicator::default() }; assert_eq!(d.style().style_val, 4.0); } #[test] fn json_partial_update() { let v = serde_json::json!({"thickness": 3, "line": "dashed"}); let ind: Indicator = serde_json::from_value(v).unwrap(); assert_eq!(ind.thickness, 3); assert_eq!(ind.line, LineKind::Dashed); assert_eq!(ind.font_size, 12.0); } } .