1#![cfg_attr(
15 not(test),
16 deny(
17 clippy::unwrap_used,
18 clippy::expect_used,
19 clippy::panic,
20 clippy::todo,
21 clippy::unimplemented,
22 clippy::unreachable
23 )
24)]
25
26mod dxf;
27mod svg;
28
29pub use dxf::to_dxf;
30pub use svg::to_svg;
31
32use geop_core_geometry::nurb_curve::NurbCurve2D;
33use geop_core_math::{
34 geop_error::GeopResult,
35 scalars::Scalar,
36 solvers::repeat::{Step, repeat},
37 vector::Vector3,
38};
39
40pub type P = [f64; 2];
42
43pub fn xml_escape(text: &str) -> String {
46 let mut out = String::with_capacity(text.len());
47 for c in text.chars() {
48 match c {
49 '&' => out.push_str("&"),
50 '<' => out.push_str("<"),
51 '>' => out.push_str(">"),
52 '"' => out.push_str("""),
53 '\'' => out.push_str("'"),
54 _ => out.push(c),
55 }
56 }
57 out
58}
59
60#[derive(Clone, Copy, Debug, PartialEq)]
66pub struct Layer {
67 pub name: &'static str,
69 pub width: f64,
71 pub line_type: LineType,
72 pub color: u8,
74}
75
76#[derive(Clone, Copy, Debug, PartialEq, Eq)]
78pub enum LineType {
79 Continuous,
80 Hidden,
82 Center,
84}
85
86impl LineType {
87 pub const ALL: [LineType; 3] = [LineType::Continuous, LineType::Hidden, LineType::Center];
88
89 pub fn name(self) -> &'static str {
91 match self {
92 LineType::Continuous => "CONTINUOUS",
93 LineType::Hidden => "HIDDEN",
94 LineType::Center => "CENTER",
95 }
96 }
97
98 pub fn description(self) -> &'static str {
100 match self {
101 LineType::Continuous => "Solid line",
102 LineType::Hidden => "Hidden __ __ __",
103 LineType::Center => "Center ____ _ ____",
104 }
105 }
106
107 pub fn dashes(self) -> &'static [f64] {
109 match self {
110 LineType::Continuous => &[],
111 LineType::Hidden => &[3.0, -1.5],
112 LineType::Center => &[8.0, -1.5, 1.5, -1.5],
113 }
114 }
115}
116
117#[derive(Clone, Debug, PartialEq)]
119pub enum Shape {
120 Line(P, P),
121 Arc {
124 center: P,
125 radius: f64,
126 start: f64,
127 end: f64,
128 },
129 Circle {
130 center: P,
131 radius: f64,
132 },
133 Polyline(Vec<P>),
134 Filled(Vec<P>),
136}
137
138#[derive(Clone, Debug, PartialEq)]
139pub struct Stroke {
140 pub layer: Layer,
141 pub shape: Shape,
142}
143
144#[derive(Clone, Copy, Debug, PartialEq, Eq)]
146pub enum Anchor {
147 Start,
148 Middle,
149 End,
150}
151
152#[derive(Clone, Debug, PartialEq)]
155pub struct Label {
156 pub layer: Layer,
157 pub at: P,
158 pub height: f64,
159 pub angle: f64,
160 pub anchor: Anchor,
161 pub text: String,
162}
163
164#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
166#[serde(rename_all = "snake_case")]
167pub enum Format {
168 Svg,
169 Dxf,
170}
171
172impl Format {
173 pub fn of_extension(extension: &str) -> Option<Format> {
176 match extension.to_ascii_lowercase().as_str() {
177 "svg" => Some(Format::Svg),
178 "dxf" => Some(Format::Dxf),
179 _ => None,
180 }
181 }
182
183 pub fn extension(self) -> &'static str {
184 match self {
185 Format::Svg => "svg",
186 Format::Dxf => "dxf",
187 }
188 }
189
190 pub fn media_type(self) -> &'static str {
192 match self {
193 Format::Svg => "image/svg+xml",
194 Format::Dxf => "image/vnd.dxf",
195 }
196 }
197}
198
199#[derive(Clone, Debug, Default, PartialEq)]
201pub struct Sheet {
202 pub width: f64,
203 pub height: f64,
204 pub layers: Vec<Layer>,
208 pub strokes: Vec<Stroke>,
209 pub labels: Vec<Label>,
210}
211
212impl Sheet {
213 pub fn layers(&self) -> Vec<Layer> {
216 let mut layers = self.layers.clone();
217 let drawn =
218 (self.strokes.iter().map(|s| s.layer)).chain(self.labels.iter().map(|l| l.layer));
219 for layer in drawn {
220 if !layers.contains(&layer) {
221 layers.push(layer);
222 }
223 }
224 layers
225 }
226
227 pub fn write(&self, format: Format) -> String {
229 match format {
230 Format::Svg => to_svg(self),
231 Format::Dxf => to_dxf(self),
232 }
233 }
234
235 pub fn stroke(&mut self, layer: Layer, shape: Shape) {
236 self.strokes.push(Stroke { layer, shape });
237 }
238
239 pub fn label(
240 &mut self,
241 layer: Layer,
242 at: P,
243 height: f64,
244 anchor: Anchor,
245 text: impl Into<String>,
246 ) {
247 self.labels.push(Label {
248 layer,
249 at,
250 height,
251 angle: 0.0,
252 anchor,
253 text: text.into(),
254 });
255 }
256}
257
258const POINTS_PER_PIECE: usize = 16;
261
262enum Piece {
265 Line(Vector3, Vector3),
266 Arc {
269 center: Vector3,
270 radius: Scalar,
271 start: Vector3,
272 end: Vector3,
273 },
274 Other(Vec<P>),
275}
276
277fn piece(curve: &NurbCurve2D) -> GeopResult<Piece> {
280 let lifted = curve.lifted()?;
281 let (t0, t1) = lifted.domain();
282 if lifted.as_line()?.is_some() {
283 return Ok(Piece::Line(lifted.evaluate(t0)?, lifted.evaluate(t1)?));
284 }
285 if let Some(arc) = lifted.as_arc()? {
286 let (start, end) = if arc.circle.normal[2].to_f64() > 0.0 {
287 (arc.start, arc.end)
288 } else {
289 (arc.end, arc.start)
290 };
291 return Ok(Piece::Arc {
292 center: arc.circle.center,
293 radius: arc.circle.radius,
294 start,
295 end,
296 });
297 }
298 let mut points = Vec::new();
299 for piece in curve.bezier_pieces()? {
300 let (a, b) = piece.domain();
301 let first = if points.is_empty() { 0 } else { 1 };
302 for k in first..=POINTS_PER_PIECE {
303 let t = match k {
304 k if k == POINTS_PER_PIECE => b,
305 k => {
306 let f = Scalar::from_f64(k as f64 / POINTS_PER_PIECE as f64);
307 Scalar::fraction(a, b, f)
308 }
309 };
310 let p = piece.evaluate(t)?;
311 points.push([p[0].to_f64(), p[1].to_f64()]);
312 }
313 }
314 Ok(Piece::Other(points))
315}
316
317fn join(a: &Piece, b: &Piece) -> Option<Piece> {
321 match (a, b) {
322 (Piece::Line(a0, a1), Piece::Line(b0, b1)) => {
323 let (da, db) = (a1.sub(a0), b1.sub(b0));
324 if !da.prod_cross(&db)[2].could_be_equal(Scalar::ZERO) {
325 return None;
326 }
327 let ends = [
328 (a0, a1, b0, b1),
329 (a0, a1, b1, b0),
330 (a1, a0, b0, b1),
331 (a1, a0, b1, b0),
332 ];
333 ends.into_iter().find_map(|(far_a, near_a, near_b, far_b)| {
334 (near_a.could_be_equal(near_b)
337 && far_a
338 .sub(near_a)
339 .prod_dot(&far_b.sub(near_b))
340 .definitely_less(Scalar::ZERO))
341 .then_some(Piece::Line(*far_a, *far_b))
342 })
343 }
344 (
345 Piece::Arc {
346 center: ca,
347 radius: ra,
348 start: sa,
349 end: ea,
350 },
351 Piece::Arc {
352 center: cb,
353 radius: rb,
354 start: sb,
355 end: eb,
356 },
357 ) if ca.could_be_equal(cb) && ra.could_be_equal(*rb) => {
358 let arc = |start: &Vector3, end: &Vector3| Piece::Arc {
359 center: ca.union(cb),
360 radius: ra.union(*rb),
361 start: *start,
362 end: *end,
363 };
364 if ea.could_be_equal(sb) {
365 Some(arc(sa, eb))
366 } else if eb.could_be_equal(sa) {
367 Some(arc(sb, ea))
368 } else {
369 None
370 }
371 }
372 _ => None,
373 }
374}
375
376pub fn decimal(x: f64, places: usize) -> String {
378 let s = format!("{x:.places$}");
379 let s = s.trim_end_matches('0').trim_end_matches('.');
380 if s == "-0" { "0".into() } else { s.into() }
381}
382
383pub fn strokes_of(
388 curves: &[(Layer, &NurbCurve2D)],
389 place: &impl Fn(P) -> P,
390 scale: f64,
391) -> GeopResult<Vec<Stroke>> {
392 let mut pieces: Vec<(Layer, Piece)> = curves
393 .iter()
394 .map(|(layer, curve)| Ok((*layer, piece(curve)?)))
395 .collect::<GeopResult<_>>()?;
396 let mut i = 0;
401 let most = 2 * pieces.len() + 1;
402 repeat(most, || -> GeopResult<_> {
403 if i >= pieces.len() {
404 return Ok(Step::Done(()));
405 }
406 let joined = (0..pieces.len()).find_map(|j| {
407 if j == i || pieces[j].0 != pieces[i].0 {
408 return None;
409 }
410 join(&pieces[i].1, &pieces[j].1).map(|p| (j, p))
411 });
412 match joined {
413 Some((j, p)) => {
414 pieces[i].1 = p;
415 pieces.remove(j);
416 if j < i {
417 i -= 1;
418 }
419 }
420 None => i += 1,
421 }
422 Ok(Step::Again)
423 })?
424 .done("joining the strokes that continue each other")?;
425 let at = |p: &Vector3| place([p[0].to_f64(), p[1].to_f64()]);
426 Ok(pieces
427 .into_iter()
428 .map(|(layer, piece)| {
429 let shape = match piece {
430 Piece::Line(a, b) => Shape::Line(at(&a), at(&b)),
431 Piece::Arc {
432 center,
433 radius,
434 start,
435 end,
436 } => {
437 let c = at(¢er);
438 let radius = radius.to_f64() * scale;
439 if start.could_be_equal(&end) {
440 Shape::Circle { center: c, radius }
441 } else {
442 let angle = |p: P| (p[1] - c[1]).atan2(p[0] - c[0]);
443 Shape::Arc {
444 center: c,
445 radius,
446 start: angle(at(&start)),
447 end: angle(at(&end)),
448 }
449 }
450 }
451 Piece::Other(points) => Shape::Polyline(points.into_iter().map(place).collect()),
452 };
453 Stroke { layer, shape }
454 })
455 .collect())
456}