use super::diagram::DiagramImage;
use indicatrix::{
geometry::meet_solver::{Block, SolvedTier, classify_blocks},
optics::materials::GemMaterial,
};
use indicatrix_cut_core::{CutSheetRow, Design};
use std::fmt::Write as _;
const BASE64_ALPHABET: &[u8; 64] =
b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
#[must_use]
pub(super) fn base64_encode(data: &[u8]) -> String {
let mut out = String::with_capacity(data.len().div_ceil(3) * 4);
for chunk in data.chunks(3) {
let b0 = u32::from(chunk[0]);
let b1 = chunk.get(1).copied().map_or(0, u32::from);
let b2 = chunk.get(2).copied().map_or(0, u32::from);
let n = (b0 << 16) | (b1 << 8) | b2;
out.push(BASE64_ALPHABET[((n >> 18) & 0x3f) as usize] as char);
out.push(BASE64_ALPHABET[((n >> 12) & 0x3f) as usize] as char);
out.push(if chunk.len() > 1 {
BASE64_ALPHABET[((n >> 6) & 0x3f) as usize] as char
} else {
'='
});
out.push(if chunk.len() > 2 {
BASE64_ALPHABET[(n & 0x3f) as usize] as char
} else {
'='
});
}
out
}
fn png_data_uri(image: &DiagramImage) -> String {
format!("data:image/png;base64,{}", base64_encode(&image.png_bytes))
}
pub(super) fn html_escape(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for c in s.chars() {
match c {
'&' => out.push_str("&"),
'<' => out.push_str("<"),
'>' => out.push_str(">"),
'"' => out.push_str("""),
'\'' => out.push_str("'"),
_ => out.push(c),
}
}
out
}
const fn block_label(block: Block) -> &'static str {
match block {
Block::Crown => "Crown",
Block::Pavilion => "Pavilion",
Block::Girdle => "Girdle",
}
}
const fn block_css_class(block: Block) -> &'static str {
match block {
Block::Crown => "block-crown",
Block::Pavilion => "block-pavilion",
Block::Girdle => "block-girdle",
}
}
fn format_indices_html(indices: &[f64]) -> String {
if indices.is_empty() {
return "-".to_string();
}
indices
.iter()
.map(|&i| format_index(i))
.collect::<Vec<_>>()
.join(", ")
}
fn format_index(index: f64) -> String {
if (index - index.round()).abs() < 1e-9 {
format!("{:.0}", index.round())
} else {
format!("{index:.2}")
}
}
#[must_use]
pub fn cutting_sheet_html(
design: &Design,
solved: &[SolvedTier],
diagram: Option<&DiagramImage>,
custom: &[GemMaterial],
) -> String {
let sheet = design.cutting_sheet_with(solved, custom);
let blocks = classify_blocks(&design.meet_tier_inputs());
let mm_per_unit = design.yield_report(solved).mm_per_unit;
let show_mm_column = design.girdle_diameter_mm.is_some();
let show_cheater_column = sheet
.rows
.iter()
.any(|row| row.cheater_offset_deg.is_some());
let mut out = String::new();
out.push_str(HTML_HEAD);
out.push_str("<h1>Cutting Sheet</h1>\n");
push_header_block(&mut out, design, custom);
push_carat_weight_row(&mut out, design, solved);
push_diagram_block(&mut out, diagram);
push_tier_table(
&mut out,
&sheet.rows,
&blocks,
TierTableColumns {
show_mm: show_mm_column,
show_cheater: show_cheater_column,
},
mm_per_unit,
);
out.push_str(HTML_TAIL);
out
}
fn push_header_block(out: &mut String, design: &Design, custom: &[GemMaterial]) {
out.push_str("<table class=\"meta\">\n");
let n_d = design.effective_refractive_index_with(custom);
push_meta_row(out, "Material", &material_header_text(design, n_d));
push_meta_row(out, "Refractive index", &format!("{n_d:.4}"));
push_meta_row(
out,
"Index gear",
&format!(
"{} teeth, symmetry {}{}",
design.meta.gear_teeth_abs(),
design.meta.symmetry_order,
if design.meta.mirror { ", mirrored" } else { "" }
),
);
if let Some(mm) = design.girdle_diameter_mm {
push_meta_row(out, "Girdle diameter", &format!("{mm:.3} mm"));
}
out.push_str("</table>\n");
}
fn push_carat_weight_row(out: &mut String, design: &Design, solved: &[SolvedTier]) {
let Some(carat) = design.yield_report(solved).carat_weight else {
return;
};
out.push_str("<table class=\"meta\">\n");
push_meta_row(
out,
"Carat weight (estimate)",
&format!("{carat:.3} ct -- assumes the authored facets reach the preform's own walls"),
);
out.push_str("</table>\n");
}
pub(super) fn material_header_text(design: &Design, n_d: f64) -> String {
if let Some(name) = design.material.name.as_deref() {
return name.to_string();
}
crate::gui::editor::material_lookup::material_for_refractive_index(n_d).map_or_else(
|| "(unset)".to_string(),
|(name, _)| format!("{name}? (from RI {n_d:.2})"),
)
}
fn push_meta_row(out: &mut String, label: &str, value: &str) {
let _ = writeln!(
out,
"<tr><td class=\"label\">{label}</td><td>{value}</td></tr>"
);
}
fn push_diagram_block(out: &mut String, diagram: Option<&DiagramImage>) {
match diagram {
Some(image) => {
let _ = writeln!(
out,
"<div class=\"diagram\"><img src=\"{}\" width=\"{}\" height=\"{}\" \
alt=\"2D facet diagram\"></div>",
png_data_uri(image),
image.width,
image.height
);
}
None => out.push_str(
"<p class=\"diagram-missing\">Diagram unavailable: this design does not \
currently close to a solid.</p>\n",
),
}
}
#[derive(Clone, Copy)]
struct TierTableColumns {
show_mm: bool,
show_cheater: bool,
}
fn push_tier_table(
out: &mut String,
rows: &[CutSheetRow],
blocks: &[Block],
columns: TierTableColumns,
mm_per_unit: Option<f64>,
) {
out.push_str("<table class=\"tiers\">\n<thead><tr>");
out.push_str(
"<th>#</th><th>Tier</th><th>Block</th><th>Angle</th><th>Elevation</th><th>Indices</th>\
<th>Mast</th>",
);
if columns.show_mm {
out.push_str("<th>Mast (mm)</th>");
}
if columns.show_cheater {
out.push_str("<th>Cheater</th>");
}
out.push_str("<th>Meet</th></tr></thead>\n<tbody>\n");
for (row, &block) in rows.iter().zip(blocks) {
push_tier_row(out, row, block, columns, mm_per_unit);
}
out.push_str("</tbody>\n</table>\n");
}
fn push_tier_row(
out: &mut String,
row: &CutSheetRow,
block: Block,
columns: TierTableColumns,
mm_per_unit: Option<f64>,
) {
let name = if row.name.is_empty() {
"(unnamed)"
} else {
row.name.as_str()
};
out.push_str("<tr>");
let _ = write!(out, "<td>{}</td>", row.sequence);
let _ = write!(out, "<td>{}</td>", html_escape(name));
let _ = write!(
out,
"<td class=\"{}\">{}</td>",
block_css_class(block),
block_label(block)
);
let _ = write!(out, "<td>{:+.2}°</td>", row.angle_deg);
let _ = write!(out, "<td>{:.2}°</td>", row.angle_of_elevation_deg);
let _ = write!(out, "<td>{}</td>", format_indices_html(&row.indices));
let _ = write!(out, "<td>{:.4}</td>", row.mast);
if columns.show_mm {
let cell = mm_per_unit.map_or_else(
|| "-".to_string(),
|scale| format!("{:.4}", row.mast * scale),
);
let _ = write!(out, "<td>{cell}</td>");
}
if columns.show_cheater {
let cell = row
.cheater_offset_deg
.map_or_else(|| "-".to_string(), |deg| format!("{deg:+.2}°"));
let _ = write!(out, "<td>{cell}</td>");
}
let _ = write!(out, "<td>{}</td>", html_escape(&row.meet_instruction));
out.push_str("</tr>\n");
}
const HTML_HEAD: &str = r#"<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Cutting Sheet</title>
<style>
:root { color-scheme: light; }
* { box-sizing: border-box; }
body {
font-family: Arial, Helvetica, sans-serif;
background: #ffffff;
color: #111111;
margin: 24px;
}
h1 { font-size: 20px; margin: 0 0 12px; }
table.meta { border-collapse: collapse; margin-bottom: 16px; }
table.meta td { padding: 2px 12px 2px 0; font-size: 13px; vertical-align: top; }
table.meta td.label { font-weight: 700; color: #333333; white-space: nowrap; }
.diagram { text-align: center; margin: 8px 0 20px; }
.diagram img { max-width: 100%; height: auto; }
.diagram-missing { color: #b91c1c; font-style: italic; }
table.tiers { border-collapse: collapse; width: 100%; font-size: 12px; }
table.tiers th, table.tiers td {
border: 1px solid #999999;
padding: 4px 6px;
text-align: left;
}
table.tiers thead { background: #eeeeee; }
table.tiers tr { page-break-inside: avoid; break-inside: avoid; }
td.block-crown { color: #92400e; font-weight: 600; }
td.block-pavilion { color: #1e3a8a; font-weight: 600; }
td.block-girdle { color: #444444; font-weight: 600; }
@page { size: A4; margin: 14mm; }
@media print {
body { margin: 0; background: #ffffff; }
table.tiers thead { display: table-header-group; }
.diagram { break-inside: avoid; page-break-inside: avoid; }
}
</style>
</head>
<body>
"#;
const HTML_TAIL: &str = "</body>\n</html>\n";