Skip to main content

brep_cam/program/
sheet.rs

1//! Setup sheet (lane C4, row 7): one HTML page per program, from the same
2//! stream the post reads. Deterministic: no dates, no random ids.
3
4use 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("&lt;"),
13            '>' => out.push_str("&gt;"),
14            '"' => out.push_str("&quot;"),
15            '\'' => out.push_str("&#39;"),
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
65/// Work offset name as controllers print it.
66pub 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
73/// Render the setup sheet.
74pub 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}