1use super::{Program, Units};
5use crate::model::{Coolant, Stock, ToolKind};
6
7fn esc(s: &str) -> String {
8 let mut out = String::with_capacity(s.len());
9 for c in s.chars() {
10 match c {
11 '&' => out.push_str("&"),
12 '<' => out.push_str("<"),
13 '>' => out.push_str(">"),
14 '"' => out.push_str("""),
15 '\'' => out.push_str("'"),
16 _ => out.push(c),
17 }
18 }
19 out
20}
21
22fn num(v: f64) -> String {
23 let s = format!("{v:.3}");
24 let s = s.trim_end_matches('0').trim_end_matches('.').to_string();
25 if s == "-0" {
26 "0".into()
27 } else {
28 s
29 }
30}
31
32fn time(seconds: f64) -> String {
33 let total = seconds.round().max(0.0) as u64;
34 let (h, m, s) = (total / 3600, (total % 3600) / 60, total % 60);
35 if h > 0 {
36 format!("{h}h {m:02}m {s:02}s")
37 } else {
38 format!("{m}m {s:02}s")
39 }
40}
41
42fn coolant(c: Coolant) -> &'static str {
43 match c {
44 Coolant::Off => "off",
45 Coolant::Flood => "flood",
46 Coolant::Mist => "mist",
47 Coolant::ThroughTool => "through tool",
48 Coolant::Air => "air",
49 Coolant::FloodAndThroughTool => "flood + through tool",
50 Coolant::FloodAndMist => "flood + mist",
51 }
52}
53
54fn stock(s: &Stock, units: Units) -> String {
55 let u = units.as_str();
56 match s {
57 Stock::BoundsOffset { x, y, top, bottom } => format!("model bounds + {} {u} (X), {} {u} (Y), {} {u} top, {} {u} bottom", num(*x), num(*y), num(*top), num(*bottom)),
58 Stock::Box { min, max } => format!("box {} x {} x {} {u}", num(max.x - min.x), num(max.y - min.y), num(max.z - min.z)),
59 Stock::Cylinder { radius, height, .. } => format!("cylinder d{} x {} {u}", num(2.0 * radius), num(*height)),
60 Stock::FromSolid { solid } => format!("body {}", esc(solid)),
61 Stock::FromRest { setup } => format!("rest material of setup {}", esc(setup.as_str())),
62 }
63}
64
65pub fn wcs_name(number: u32) -> String {
67 match number {
68 1..=6 => format!("G{}", 53 + number),
69 n => format!("G54.1 P{}", n - 6),
70 }
71}
72
73pub fn render(program: &Program) -> String {
75 let u = program.units.as_str();
76 let mut h = String::new();
77 h.push_str("<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n");
78 h.push_str(&format!("<title>Setup sheet: {}</title>\n", esc(&program.name)));
79 h.push_str("<style>\nbody{font-family:system-ui,sans-serif;margin:2em;color:#222}\nh1{font-size:1.6em}\nh2{font-size:1.2em;margin-top:1.5em;border-bottom:1px solid #ccc}\ntable{border-collapse:collapse;margin:.5em 0;font-size:.95em}\nth,td{border:1px solid #ccc;padding:.3em .6em;text-align:left;vertical-align:top}\nth{background:#f3f3f3}\ntd.n{text-align:right;font-variant-numeric:tabular-nums}\n.warn{color:#9a6700}\n.ok{color:#1a7f37}\n</style>\n</head>\n<body>\n");
80 h.push_str(&format!("<h1>Setup sheet: {}</h1>\n", esc(&program.name)));
81 h.push_str("<table>\n");
82 if let Some(n) = program.number {
83 h.push_str(&format!("<tr><th>Program number</th><td>O{n}</td></tr>\n"));
84 }
85 if !program.comment.is_empty() {
86 h.push_str(&format!("<tr><th>Comment</th><td>{}</td></tr>\n", esc(&program.comment)));
87 }
88 h.push_str(&format!("<tr><th>Units</th><td>{u}</td></tr>\n"));
89 if let Some(m) = &program.machine {
90 h.push_str(&format!("<tr><th>Machine</th><td>{}</td></tr>\n", esc(&m.name)));
91 if let Some(post) = &m.post {
92 h.push_str(&format!("<tr><th>Post</th><td>{}</td></tr>\n", esc(post)));
93 }
94 }
95 h.push_str(&format!("<tr><th>Operations</th><td>{}</td></tr>\n", program.sections.len()));
96 h.push_str(&format!("<tr><th>Tool changes</th><td>{}</td></tr>\n", program.summary.tool_changes));
97 h.push_str(&format!("<tr><th>Cutting length</th><td>{} {u}</td></tr>\n", num(program.summary.cutting_length)));
98 h.push_str(&format!("<tr><th>Estimated time</th><td>{}</td></tr>\n", time(program.summary.estimated_seconds)));
99 h.push_str("</table>\n");
100
101 h.push_str("<h2>Setups</h2>\n<table>\n<tr><th>#</th><th>Name</th><th>WCS</th><th>Stock</th><th>Clearance</th><th>Retract</th><th>Feed height</th><th>Protected bodies</th></tr>\n");
102 for (i, s) in program.setups.iter().enumerate() {
103 h.push_str(&format!(
104 "<tr><td class=\"n\">{}</td><td>{}</td><td>{}</td><td>{}</td><td class=\"n\">{}</td><td class=\"n\">{}</td><td class=\"n\">{}</td><td>{}</td></tr>\n",
105 i + 1,
106 esc(&s.name),
107 wcs_name(s.wcs.number),
108 stock(&s.stock, program.units),
109 num(s.heights.clearance),
110 num(s.heights.retract),
111 num(s.heights.feed),
112 esc(&s.protected.iter().map(|p| p.solid.clone()).collect::<Vec<_>>().join(", "))
113 ));
114 }
115 h.push_str("</table>\n");
116
117 h.push_str("<h2>Tools</h2>\n<table>\n<tr><th>T</th><th>Name</th><th>Type</th><th>Diameter</th><th>Corner radius</th><th>Flute length</th><th>Flutes</th><th>Holder</th><th>Spindle</th><th>Feed</th><th>Plunge</th><th>Coolant</th></tr>\n");
118 for t in &program.tools {
119 let kind = serde_json::to_value(t.kind).ok().and_then(|v| v.as_str().map(String::from)).unwrap_or_else(|| format!("{:?}", ToolKind::Flat));
120 h.push_str(&format!(
121 "<tr><td class=\"n\">T{}</td><td>{}</td><td>{}</td><td class=\"n\">{}</td><td class=\"n\">{}</td><td class=\"n\">{}</td><td class=\"n\">{}</td><td>{}</td><td class=\"n\">{} rpm</td><td class=\"n\">{} {u}/min</td><td class=\"n\">{} {u}/min</td><td>{}</td></tr>\n",
122 t.number,
123 esc(&t.name),
124 kind,
125 num(t.geometry.diameter),
126 t.geometry.corner_radius.map(num).unwrap_or_default(),
127 num(t.geometry.cutting_length),
128 t.geometry.flute_count.map(|n| n.to_string()).unwrap_or_default(),
129 esc(&t.holder.as_ref().map(|h| h.name.clone()).unwrap_or_default()),
130 num(t.feeds.spindle_rpm),
131 num(t.feeds.cutting_feed),
132 num(t.feeds.plunge_feed),
133 coolant(t.coolant)
134 ));
135 }
136 h.push_str("</table>\n");
137
138 h.push_str("<h2>Operations</h2>\n<table>\n<tr><th>#</th><th>Name</th><th>Strategy</th><th>Setup</th><th>WCS</th><th>Tool</th><th>Spindle</th><th>Coolant</th><th>Tolerance</th><th>Stock to leave</th><th>Cutting length</th><th>Time</th><th>Status</th></tr>\n");
139 for s in &program.sections {
140 let status = match s.report.status {
141 crate::model::OperationStatus::Ok => "<span class=\"ok\">ok</span>".to_string(),
142 crate::model::OperationStatus::Warning => format!(
143 "<span class=\"warn\">{} warning{}</span>",
144 s.report.warnings.len(),
145 if s.report.warnings.len() == 1 { "" } else { "s" }
146 ),
147 other => format!("{other:?}").to_lowercase(),
148 };
149 h.push_str(&format!(
150 "<tr><td class=\"n\">{}</td><td>{}</td><td>{}</td><td>{}</td><td>{}</td><td class=\"n\">T{}</td><td class=\"n\">{} rpm</td><td>{}</td><td class=\"n\">{}</td><td class=\"n\">{} / {}</td><td class=\"n\">{} {u}</td><td class=\"n\">{}</td><td>{}</td></tr>\n",
151 s.index + 1,
152 esc(&s.name),
153 esc(&s.strategy),
154 esc(&s.setup_name),
155 wcs_name(s.wcs.number),
156 s.tool_number,
157 num(s.spindle_rpm),
158 coolant(s.coolant),
159 num(s.tolerance),
160 num(s.stock_to_leave.radial),
161 num(s.stock_to_leave.axial),
162 num(s.cutting_length),
163 time(s.estimated_seconds),
164 status
165 ));
166 }
167 h.push_str("</table>\n");
168 let warnings: Vec<String> = program
169 .sections
170 .iter()
171 .flat_map(|s| s.report.warnings.iter().map(move |w| format!("<li><b>{}</b>: {} <code>{}</code></li>", esc(&s.name), esc(&w.message), esc(&w.code))))
172 .collect();
173 if !warnings.is_empty() {
174 h.push_str("<h2>Warnings</h2>\n<ul>\n");
175 for w in warnings {
176 h.push_str(&w);
177 h.push('\n');
178 }
179 h.push_str("</ul>\n");
180 }
181 h.push_str("</body>\n</html>\n");
182 h
183}