use super::project::SheetText;
use super::Sheet;
const TITLE_WIDTH_MM: f64 = 110.0;
const ROW_HEIGHTS_MM: [f64; 4] = [11.0, 8.0, 8.0, 8.0];
const SPLIT: f64 = 0.5;
const LABEL_HEIGHT_MM: f64 = 1.8;
const VALUE_HEIGHT_MM: f64 = 3.2;
const TITLE_HEIGHT_MM: f64 = 4.4;
const LABEL_BAND: f64 = 0.42;
const CELL_PAD_MM: f64 = 1.2;
pub const SCALE_FLOOR: f64 = 0.35;
const REVISION_COLUMNS_MM: [f64; 3] = [12.0, 24.0, 74.0];
const REVISION_HEADER_MM: f64 = 6.0;
const REVISION_ROW_MM: f64 = 7.0;
const REVISION_TEXT_MM: f64 = 2.6;
fn title_height_mm() -> f64 {
ROW_HEIGHTS_MM.iter().sum()
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct FrameContext {
pub document: String,
pub date: String,
}
#[derive(Debug, Clone, PartialEq, serde::Serialize)]
pub struct FrameDrawing {
pub lines: Vec<Vec<[f64; 2]>>,
pub texts: Vec<SheetText>,
#[serde(rename = "revisionTable", skip_serializing_if = "Option::is_none")]
pub revision_table: Option<RevisionTableDrawing>,
}
#[derive(Debug, Clone, PartialEq, serde::Serialize)]
pub struct RevisionTableDrawing {
pub lines: Vec<Vec<[f64; 2]>>,
pub texts: Vec<SheetText>,
pub rows: usize,
pub bounds: [f64; 4],
pub placement: String,
}
impl FrameDrawing {
fn is_empty(&self) -> bool {
self.lines.is_empty() && self.texts.is_empty() && self.revision_table.is_none()
}
}
pub fn frame(sheet: &Sheet, context: &FrameContext) -> Option<FrameDrawing> {
let (w, h) = sheet.millimetres();
let inset = sheet.border_inset();
let mut out = FrameDrawing { lines: Vec::new(), texts: Vec::new(), revision_table: None };
if sheet.border {
let (x0, y0, x1, y1) = (inset, inset, w - inset, h - inset);
if x1 > x0 && y1 > y0 {
out.lines.push(vec![[x0, y0], [x1, y0], [x1, y1], [x0, y1], [x0, y0]]);
}
}
if sheet.title_block {
title_block(sheet, context, w, h, inset, &mut out);
}
out.revision_table = revision_table(sheet, w, h, inset);
(!out.is_empty()).then_some(out)
}
fn stamp_scale(w: f64, h: f64, inset: f64) -> f64 {
let usable_w = (w - 2.0 * inset).max(0.0);
let usable_h = (h - 2.0 * inset).max(0.0);
(usable_w / TITLE_WIDTH_MM).min(usable_h / title_height_mm()).min(1.0)
}
fn revision_table(sheet: &Sheet, w: f64, h: f64, inset: f64) -> Option<RevisionTableDrawing> {
if sheet.revisions.is_empty() {
return None;
}
let scale = stamp_scale(w, h, inset);
if scale < SCALE_FLOOR {
return None;
}
let table_w = TITLE_WIDTH_MM * scale;
let table_h = (REVISION_HEADER_MM + REVISION_ROW_MM * sheet.revisions.len() as f64) * scale;
let (left, top) = (inset, inset);
let (right, bottom) = (w - inset, h - inset);
let (x1, y1, placement) = if sheet.title_block {
let stamp_left = right - TITLE_WIDTH_MM * scale;
let stamp_top = bottom - title_height_mm() * scale;
if stamp_left - table_w >= left - 1e-9 && bottom - table_h >= top - 1e-9 {
(stamp_left, bottom, "beside")
} else if stamp_top - table_h >= top - 1e-9 {
(right, stamp_top, "above")
} else {
return None;
}
} else if bottom - table_h >= top - 1e-9 {
(right, bottom, "corner")
} else {
return None;
};
let (x0, y0) = (x1 - table_w, y1 - table_h);
let mut lines = vec![vec![[x0, y0], [x1, y0], [x1, y1], [x0, y1], [x0, y0]]];
let header = REVISION_HEADER_MM * scale;
let row = REVISION_ROW_MM * scale;
for k in 0..sheet.revisions.len() {
let y = y0 + header + row * k as f64;
lines.push(vec![[x0, y], [x1, y]]);
}
let mut columns = vec![x0];
for width in REVISION_COLUMNS_MM {
columns.push(columns[columns.len() - 1] + width * scale);
}
for x in &columns[1..3] {
lines.push(vec![[*x, y0], [*x, y1]]);
}
let pad = CELL_PAD_MM * scale;
let mut table = FrameDrawing { lines: Vec::new(), texts: Vec::new(), revision_table: None };
for (k, label) in ["REV", "DATE", "DESCRIPTION"].into_iter().enumerate() {
push_text(&mut table, label, columns[k] + pad, columns[k + 1] - pad, y0, y0 + header, LABEL_HEIGHT_MM * scale);
}
for (index, revision) in sheet.revisions.iter().enumerate() {
let (top, bottom) = (y0 + header + row * index as f64, y0 + header + row * (index + 1) as f64);
for (k, value) in [&revision.rev, &revision.date, &revision.description].into_iter().enumerate() {
push_text(&mut table, value, columns[k] + pad, columns[k + 1] - pad, top, bottom, REVISION_TEXT_MM * scale);
}
}
Some(RevisionTableDrawing {
lines,
texts: table.texts,
rows: sheet.revisions.len(),
bounds: [x0, y0, x1, y1],
placement: placement.to_string(),
})
}
fn title_block(
sheet: &Sheet,
context: &FrameContext,
w: f64,
h: f64,
inset: f64,
out: &mut FrameDrawing,
) {
let scale = stamp_scale(w, h, inset);
if scale < SCALE_FLOOR {
return;
}
let block_w = TITLE_WIDTH_MM * scale;
let rows: Vec<f64> = ROW_HEIGHTS_MM.iter().map(|mm| mm * scale).collect();
let block_h: f64 = rows.iter().sum();
let x0 = w - inset - block_w;
let y0 = h - inset - block_h;
let x1 = x0 + block_w;
let y1 = y0 + block_h;
out.lines.push(vec![[x0, y0], [x1, y0], [x1, y1], [x0, y1], [x0, y0]]);
let mut y = y0;
let mut edges = Vec::new();
for row in &rows {
edges.push(y);
y += row;
if y < y1 - 1e-9 {
out.lines.push(vec![[x0, y], [x1, y]]);
}
}
edges.push(y1);
let split = x0 + block_w * SPLIT;
out.lines.push(vec![[split, edges[2]], [split, edges[3]]]);
let pad = CELL_PAD_MM * scale;
let mut cell = |x_min: f64, x_max: f64, y_min: f64, y_max: f64, label: &str, value: &str, value_height: f64| {
let band = (y_max - y_min) * LABEL_BAND;
push_text(out, label, x_min + pad, x_max - pad, y_min, y_min + band, LABEL_HEIGHT_MM * scale);
push_text(out, value, x_min + pad, x_max - pad, y_min + band, y_max, value_height * scale);
};
cell(x0, x1, edges[0], edges[1], "SHEET", &sheet.name, TITLE_HEIGHT_MM);
cell(x0, x1, edges[1], edges[2], "DOCUMENT", &context.document, VALUE_HEIGHT_MM);
cell(x0, split, edges[2], edges[3], "SCALE", &scale_text(sheet), VALUE_HEIGHT_MM);
cell(split, x1, edges[2], edges[3], "DATE", &context.date, VALUE_HEIGHT_MM);
cell(x0, x1, edges[3], edges[4], "NOTES", &sheet.notes, VALUE_HEIGHT_MM);
}
fn push_text(
out: &mut FrameDrawing,
text: &str,
x_min: f64,
x_max: f64,
y_min: f64,
y_max: f64,
height: f64,
) {
let text = text.trim();
if text.is_empty() || x_max <= x_min {
return;
}
out.texts.push(SheetText::flat(
text.to_string(),
[(x_min + x_max) * 0.5, (y_min + y_max) * 0.5],
height,
));
}
pub fn scale_text(sheet: &Sheet) -> String {
match sheet.views.first() {
Some(view) => ratio_text(view.scale),
None => "\u{2014}".to_string(),
}
}
pub fn ratio_text(scale: f64) -> String {
if !scale.is_finite() || scale <= 0.0 {
return "\u{2014}".to_string();
}
if scale >= 1.0 {
format!("{}:1", trim(scale))
} else {
format!("1:{}", trim(1.0 / scale))
}
}
fn trim(value: f64) -> String {
let text = format!("{value:.3}");
let text = text.trim_end_matches('0').trim_end_matches('.');
if text.is_empty() { "0".to_string() } else { text.to_string() }
}
pub fn today_iso() -> String {
iso_date(now_unix_seconds())
}
pub fn iso_date(unix_seconds: i64) -> String {
let (year, month, day) = civil_from_days(unix_seconds.div_euclid(86_400));
format!("{year:04}-{month:02}-{day:02}")
}
fn civil_from_days(days: i64) -> (i64, u32, u32) {
let z = days + 719_468;
let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
let day_of_era = (z - era * 146_097) as i64; let year_of_era =
(day_of_era - day_of_era / 1460 + day_of_era / 36_524 - day_of_era / 146_096) / 365; let year = year_of_era + era * 400;
let day_of_year = day_of_era - (365 * year_of_era + year_of_era / 4 - year_of_era / 100); let month_prime = (5 * day_of_year + 2) / 153; let day = (day_of_year - (153 * month_prime + 2) / 5 + 1) as u32; let month = if month_prime < 10 { month_prime + 3 } else { month_prime - 9 } as u32;
(year + i64::from(month <= 2), month, day)
}
#[cfg(not(target_arch = "wasm32"))]
fn now_unix_seconds() -> i64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|since| since.as_secs() as i64)
.unwrap_or(0)
}
#[cfg(target_arch = "wasm32")]
fn now_unix_seconds() -> i64 {
(js_sys::Date::now() / 1000.0) as i64
}