updates
This commit is contained in:
@@ -1 +1,2 @@
|
||||
/target
|
||||
report.pdf
|
||||
|
||||
+110
-51
@@ -1,19 +1,20 @@
|
||||
#[cfg(feature = "pdf")]
|
||||
pub mod pdf;
|
||||
|
||||
use clap::{Parser, ValueEnum};
|
||||
use colored::*;
|
||||
use chrono::Local;
|
||||
use clap::{CommandFactory, Parser, ValueEnum, error::ErrorKind};
|
||||
use colored::*;
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Concentration of plaster slurry
|
||||
/// in parts of water per 100 parts plaster by weight
|
||||
const DEFAULT_CONCENTRATION: f64 = 70.0;
|
||||
/// Density of plaster slurry (g/L).
|
||||
/// Density of plaster slurry (g/L) at the [`DEFAULT_CONCENTRATION`].
|
||||
const DEFAULT_DENSITY: f64 = 1674.0;
|
||||
const ML_PER_US_GALLON: f64 = 3785.411784;
|
||||
const PERCENT: f64 = 100.0;
|
||||
/// The amount of slack added
|
||||
const SLACK: f64 = 0.10;
|
||||
/// The default amount of slack added
|
||||
const DEFAULT_SLACK_PERCENT: f64 = 10.0;
|
||||
/// The multiplier for the water that becomes part of the mold
|
||||
///
|
||||
/// 18 parts water for every part plaster becomes part
|
||||
@@ -36,11 +37,12 @@ enum ColorChoice {
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(author, version, about = "Calculates plaster mixing weights", long_about = None)]
|
||||
struct Args {
|
||||
/// Volume in cm³ or ml
|
||||
volume: f64,
|
||||
/// Volumes in cm³ or ml, separated by spaces or commas
|
||||
#[arg(num_args = 1.., required = true)]
|
||||
volumes: Vec<String>,
|
||||
|
||||
/// Optional title for the lab log (written large in the header)
|
||||
#[arg(short = 't', long)]
|
||||
#[arg(short, long)]
|
||||
title: Option<String>,
|
||||
|
||||
/// Concentration (parts water per 100 parts plaster by weight)
|
||||
@@ -51,14 +53,14 @@ struct Args {
|
||||
#[arg(short, long)]
|
||||
density: Option<f64>,
|
||||
|
||||
/// Extra mixing amount to add, as a percentage
|
||||
#[arg(short, long, default_value_t = DEFAULT_SLACK_PERCENT)]
|
||||
slack: f64,
|
||||
|
||||
/// Control terminal color output
|
||||
#[arg(long, default_value = "auto")]
|
||||
color: ColorChoice,
|
||||
|
||||
/// Number of casts to make simultaneously
|
||||
#[arg(short = 'n', long, default_value_t = 1)]
|
||||
quantity: u32,
|
||||
|
||||
/// Soak time (for the sheet). Will be recorded; provide as text (e.g. "10m").
|
||||
#[arg(long)]
|
||||
soak_time: Option<String>,
|
||||
@@ -68,12 +70,36 @@ struct Args {
|
||||
mix_time: Option<String>,
|
||||
|
||||
/// If provided, generate a PDF lab log at this path. If omitted no PDF will be written.
|
||||
#[arg(long = "pdf-out")]
|
||||
pdf_out: Option<String>,
|
||||
#[arg(short, long)]
|
||||
report: Option<String>,
|
||||
}
|
||||
|
||||
fn parse_volumes(raw_volumes: &[String]) -> Result<Vec<f64>, String> {
|
||||
let volumes = raw_volumes
|
||||
.iter()
|
||||
.flat_map(|volume| volume.split(|c: char| c == ',' || c.is_whitespace()))
|
||||
.filter(|volume| !volume.is_empty())
|
||||
.map(|volume| {
|
||||
volume
|
||||
.parse::<f64>()
|
||||
.map_err(|_| format!("invalid volume `{volume}`"))
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
if volumes.is_empty() {
|
||||
Err("provide at least one volume".to_string())
|
||||
} else {
|
||||
Ok(volumes)
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let args = Args::parse();
|
||||
let volumes = parse_volumes(&args.volumes).unwrap_or_else(|err| {
|
||||
Args::command()
|
||||
.error(ErrorKind::ValueValidation, err)
|
||||
.exit();
|
||||
});
|
||||
|
||||
// Configure color output globally based on the CLI argument
|
||||
match args.color {
|
||||
@@ -82,7 +108,7 @@ fn main() {
|
||||
ColorChoice::Auto => {} // Respects standard stream capabilities (default)
|
||||
}
|
||||
|
||||
// Determine density and target wet weight for a SINGLE cast
|
||||
// Determine density and target weights for the requested casts
|
||||
let density = args.density.or_else(|| {
|
||||
if (args.concentration - DEFAULT_CONCENTRATION).abs() < f64::EPSILON {
|
||||
Some(DEFAULT_DENSITY)
|
||||
@@ -91,8 +117,14 @@ fn main() {
|
||||
}
|
||||
});
|
||||
|
||||
let single_wet_weight = density.map(|d| (args.volume / 1000.0) * d);
|
||||
let total_wet_weight = single_wet_weight.map(|w| w * args.quantity as f64);
|
||||
let total_volume: f64 = volumes.iter().sum();
|
||||
let cast_wet_weights = density.map(|d| {
|
||||
volumes
|
||||
.iter()
|
||||
.map(|volume| (volume / 1000.0) * d)
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
let total_wet_weight = density.map(|d| (total_volume / 1000.0) * d);
|
||||
|
||||
let (plaster_total, water_total) = match total_wet_weight {
|
||||
Some(wet_weight) => {
|
||||
@@ -103,21 +135,36 @@ fn main() {
|
||||
None => (None, None),
|
||||
};
|
||||
|
||||
let slack_fraction = args.slack / PERCENT;
|
||||
|
||||
// precompute slack and single dry weight as Option values so we can pass them
|
||||
let plaster_slack = plaster_total.map(|p| p * (1.0 + SLACK));
|
||||
let water_slack = water_total.map(|w| w * (1.0 + SLACK));
|
||||
let single_plaster = plaster_total.map(|p| p / args.quantity as f64);
|
||||
let single_dry_weight_opt = single_plaster.map(|sp| sp * DRY_WEIGHT_MULTIPLIER);
|
||||
let plaster_slack = plaster_total.map(|p| p * (1.0 + slack_fraction));
|
||||
let water_slack = water_total.map(|w| w * (1.0 + slack_fraction));
|
||||
let cast_dry_weights = density.map(|d| {
|
||||
volumes
|
||||
.iter()
|
||||
.map(|volume| {
|
||||
let wet_weight = (volume / 1000.0) * d;
|
||||
let plaster = wet_weight / (1.0 + (args.concentration / PERCENT));
|
||||
plaster * DRY_WEIGHT_MULTIPLIER
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
|
||||
// Output Header
|
||||
println!("--- Plaster Calculation Parameters ---");
|
||||
println!(
|
||||
"Volume (per cast): {}ml",
|
||||
format!("{}", args.volume).color(COLOR_PARAM).bold()
|
||||
"Volumes: {}ml",
|
||||
volumes
|
||||
.iter()
|
||||
.map(|volume| format!("{}", volume).color(COLOR_PARAM).bold().to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
);
|
||||
println!(
|
||||
"Quantity: {}",
|
||||
format!("{}", args.quantity).color(COLOR_PARAM).bold()
|
||||
"Total volume: {}ml ({:.2} gal)",
|
||||
format!("{}", total_volume).color(COLOR_PARAM).bold(),
|
||||
total_volume / ML_PER_US_GALLON
|
||||
);
|
||||
println!(
|
||||
"Concentration: {}g water per 100g plaster",
|
||||
@@ -128,18 +175,23 @@ fn main() {
|
||||
format!("{}g/L", format!("{}", d).color(COLOR_PARAM).bold())
|
||||
});
|
||||
println!("Density: {}", density_str);
|
||||
println!(
|
||||
"Slack: {}%",
|
||||
format!("{}", args.slack).color(COLOR_PARAM).bold()
|
||||
);
|
||||
println!("--------------------------------------");
|
||||
|
||||
// Output Mixing Amounts and Target Weights
|
||||
if let (Some(p_tot), Some(w_tot)) = (plaster_total, water_total) {
|
||||
let p_slack = plaster_slack.unwrap_or(p_tot * (1.0 + SLACK));
|
||||
let w_slack = water_slack.unwrap_or(w_tot * (1.0 + SLACK));
|
||||
let p_slack = plaster_slack.unwrap_or(p_tot * (1.0 + slack_fraction));
|
||||
let w_slack = water_slack.unwrap_or(w_tot * (1.0 + slack_fraction));
|
||||
|
||||
let single_dry_weight = single_dry_weight_opt.unwrap_or((p_tot / args.quantity as f64) * DRY_WEIGHT_MULTIPLIER);
|
||||
let total_dry_weight = single_dry_weight * args.quantity as f64;
|
||||
let cast_wet_weights = cast_wet_weights.as_ref().unwrap();
|
||||
let cast_dry_weights = cast_dry_weights.as_ref().unwrap();
|
||||
let total_dry_weight: f64 = cast_dry_weights.iter().sum();
|
||||
|
||||
if args.quantity > 1 {
|
||||
println!("Combined Mixing Amounts (for {} casts):", args.quantity);
|
||||
if volumes.len() > 1 {
|
||||
println!("Combined Mixing Amounts (for {} casts):", volumes.len());
|
||||
println!(" Nominal Total:");
|
||||
} else {
|
||||
println!("Mixing Amounts:");
|
||||
@@ -148,49 +200,56 @@ fn main() {
|
||||
|
||||
println!(
|
||||
" Plaster: {}g",
|
||||
format!("{:.1}", p_tot).color(COLOR_PLASTER)
|
||||
format!("{:.0}", p_tot).color(COLOR_PLASTER)
|
||||
);
|
||||
println!(
|
||||
" Water: {}g",
|
||||
format!("{:.1}", w_tot).color(COLOR_WATER)
|
||||
format!("{:.0}", w_tot).color(COLOR_WATER)
|
||||
);
|
||||
|
||||
if args.quantity > 1 {
|
||||
println!(" {:2}% Slack Total:", SLACK * PERCENT);
|
||||
if volumes.len() > 1 {
|
||||
println!(" {}% Slack Total:", args.slack);
|
||||
} else {
|
||||
println!(" {:2}% Slack:", SLACK * PERCENT);
|
||||
println!(" {}% Slack:", args.slack);
|
||||
}
|
||||
|
||||
println!(
|
||||
" Plaster: {}g",
|
||||
format!("{:.1}", p_slack).color(COLOR_PLASTER).bold()
|
||||
format!("{:.0}", p_slack).color(COLOR_PLASTER).bold()
|
||||
);
|
||||
println!(
|
||||
" Water: {}g",
|
||||
format!("{:.1}", w_slack).color(COLOR_WATER).bold()
|
||||
format!("{:.0}", w_slack).color(COLOR_WATER).bold()
|
||||
);
|
||||
|
||||
println!("\nTarget Weights:");
|
||||
if args.quantity > 1 {
|
||||
println!(" Individual Cast:");
|
||||
}
|
||||
for (index, ((volume, wet_weight), dry_weight)) in volumes
|
||||
.iter()
|
||||
.zip(cast_wet_weights.iter())
|
||||
.zip(cast_dry_weights.iter())
|
||||
.enumerate()
|
||||
{
|
||||
if volumes.len() > 1 {
|
||||
println!(" Cast {} ({}ml):", index + 1, volume);
|
||||
}
|
||||
println!(
|
||||
" Wet Cast: {}g",
|
||||
format!("{:.1}", single_wet_weight.unwrap()).color(COLOR_TARGET).bold()
|
||||
format!("{:.0}", wet_weight).color(COLOR_TARGET).bold()
|
||||
);
|
||||
println!(
|
||||
" Dry Cast: {}g",
|
||||
format!("{:.1}", single_dry_weight).color(COLOR_TARGET).bold()
|
||||
format!("{:.0}", dry_weight).color(COLOR_TARGET).bold()
|
||||
);
|
||||
if args.quantity > 1 {
|
||||
}
|
||||
if volumes.len() > 1 {
|
||||
println!(" Total Batch:");
|
||||
println!(
|
||||
" Wet Cast: {}g",
|
||||
format!("{:.1}", total_wet_weight.unwrap()).color(COLOR_TARGET)
|
||||
format!("{:.0}", total_wet_weight.unwrap()).color(COLOR_TARGET)
|
||||
);
|
||||
println!(
|
||||
" Dry Cast: {}g",
|
||||
format!("{:.1}", total_dry_weight).color(COLOR_TARGET)
|
||||
format!("{:.0}", total_dry_weight).color(COLOR_TARGET)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
@@ -201,21 +260,21 @@ fn main() {
|
||||
|
||||
// If user requested a PDF output path, generate a lab log PDF (or placeholder)
|
||||
#[cfg(feature = "pdf")]
|
||||
if let Some(pdf_path) = args.pdf_out {
|
||||
if let Some(pdf_path) = args.report {
|
||||
let out_path = PathBuf::from(pdf_path);
|
||||
let data = pdf::LabLogData {
|
||||
title: args.title.clone(),
|
||||
date: Local::now().format("%Y-%m-%d").to_string(),
|
||||
volume_ml: args.volume,
|
||||
volumes_ml: volumes.clone(),
|
||||
concentration: args.concentration,
|
||||
density,
|
||||
quantity: args.quantity,
|
||||
slack_percent: args.slack,
|
||||
plaster_total,
|
||||
water_total,
|
||||
plaster_slack,
|
||||
water_slack,
|
||||
single_wet: single_wet_weight,
|
||||
single_dry: single_dry_weight_opt,
|
||||
cast_wet_weights,
|
||||
cast_dry_weights,
|
||||
soak_time: args.soak_time.clone(),
|
||||
mix_time: args.mix_time.clone(),
|
||||
};
|
||||
|
||||
+304
-57
@@ -2,26 +2,30 @@
|
||||
// This module exposes a `LabLogData` struct and a `generate_lab_log` function.
|
||||
// The actual PDF implementation uses `krilla` when the `pdf` feature is enabled.
|
||||
|
||||
use krilla::color::rgb;
|
||||
use krilla::document::Document;
|
||||
use krilla::geom::{PathBuilder, Point, Size};
|
||||
use krilla::num::NormalizedF32;
|
||||
use krilla::page::PageSettings;
|
||||
use krilla::paint::Fill;
|
||||
use krilla::text::{Font, TextDirection};
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use krilla::geom::{Point, Size};
|
||||
use krilla::page::PageSettings;
|
||||
use krilla::text::{Font, TextDirection};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct LabLogData {
|
||||
pub title: Option<String>,
|
||||
pub date: String,
|
||||
pub volume_ml: f64,
|
||||
pub volumes_ml: Vec<f64>,
|
||||
pub concentration: f64,
|
||||
pub density: Option<f64>,
|
||||
pub quantity: u32,
|
||||
pub slack_percent: f64,
|
||||
pub plaster_total: Option<f64>,
|
||||
pub water_total: Option<f64>,
|
||||
pub plaster_slack: Option<f64>,
|
||||
pub water_slack: Option<f64>,
|
||||
pub single_wet: Option<f64>,
|
||||
pub single_dry: Option<f64>,
|
||||
pub cast_wet_weights: Option<Vec<f64>>,
|
||||
pub cast_dry_weights: Option<Vec<f64>>,
|
||||
pub soak_time: Option<String>,
|
||||
pub mix_time: Option<String>,
|
||||
}
|
||||
@@ -33,92 +37,335 @@ pub fn generate_lab_log<P: AsRef<Path>>(out_path: P, data: &LabLogData) -> Resul
|
||||
generate_lab_log_inner(out_path.as_ref(), data)
|
||||
}
|
||||
|
||||
const PAGE_WIDTH: f32 = 612.0;
|
||||
const PAGE_HEIGHT: f32 = 792.0;
|
||||
const MARGIN: f32 = 36.0;
|
||||
|
||||
// When the `pdf` feature is enabled we provide a krilla-based implementation.
|
||||
// The code below is compiled only when the feature is active.
|
||||
fn generate_lab_log_inner(out_path: &Path, data: &LabLogData) -> Result<(), String> {
|
||||
// Use krilla to build a PDF layout using the correct surface.draw_text() API.
|
||||
use krilla::document::Document;
|
||||
|
||||
// Create document and a single page
|
||||
let mut doc = Document::new();
|
||||
let mut page = doc.start_page_with(PageSettings::new(Size::from_wh(612.0, 792.0).unwrap())); // US Letter
|
||||
let mut page = doc.start_page_with(PageSettings::new(
|
||||
Size::from_wh(PAGE_WIDTH, PAGE_HEIGHT).unwrap(),
|
||||
)); // US Letter
|
||||
let mut surface = page.surface();
|
||||
// Load font
|
||||
let noto_font = Font::new(
|
||||
Arc::new(std::fs::read("font/NotoSans-Regular.ttf").unwrap()).into(),
|
||||
0,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
{
|
||||
let mut surface = page.surface();
|
||||
let height = 80.0;
|
||||
let rect = {
|
||||
let mut pb = PathBuilder::new();
|
||||
pb.move_to(MARGIN, MARGIN);
|
||||
pb.line_to(MARGIN, MARGIN + height);
|
||||
pb.line_to(PAGE_WIDTH - MARGIN, MARGIN + height);
|
||||
pb.line_to(PAGE_WIDTH - MARGIN, MARGIN);
|
||||
pb.close();
|
||||
|
||||
// Load font
|
||||
let noto_font = Font::new(
|
||||
Arc::new(std::fs::read("font/NotoSans-Regular.ttf").unwrap()).into(),
|
||||
0,
|
||||
).unwrap();
|
||||
pb.finish().unwrap()
|
||||
};
|
||||
|
||||
surface.set_fill(Some(Fill {
|
||||
paint: rgb::Color::new(230, 230, 230).into(),
|
||||
opacity: NormalizedF32::new(1.0).unwrap(),
|
||||
rule: Default::default(),
|
||||
}));
|
||||
|
||||
surface.draw_path(&rect);
|
||||
surface.set_fill(None);
|
||||
|
||||
let today = chrono::Local::now().date_naive();
|
||||
let date_string = today.format("%b %d %Y").to_string();
|
||||
|
||||
surface.draw_text(
|
||||
Point::from_xy(MARGIN, MARGIN + 30.0),
|
||||
noto_font.clone(),
|
||||
28.0,
|
||||
&date_string,
|
||||
false,
|
||||
TextDirection::Auto,
|
||||
);
|
||||
|
||||
if true {
|
||||
// Header: Title (large, left) and date (top-right)
|
||||
surface.set_fill(Some(Fill {
|
||||
paint: rgb::Color::new(255, 0, 0).into(),
|
||||
opacity: NormalizedF32::new(0.5).unwrap(),
|
||||
rule: Default::default(),
|
||||
}));
|
||||
if let Some(title) = &data.title {
|
||||
surface.draw_text(Point::from_xy(40.0, 820.0), noto_font.clone(), 28.0, title, false, TextDirection::Auto);
|
||||
surface.draw_text(
|
||||
Point::from_xy(40.0, 700.0),
|
||||
noto_font.clone(),
|
||||
28.0,
|
||||
title,
|
||||
false,
|
||||
TextDirection::Auto,
|
||||
);
|
||||
} else {
|
||||
surface.draw_text(Point::from_xy(40.0, 820.0), noto_font.clone(), 28.0, "Title: __________________________", false, TextDirection::Auto);
|
||||
surface.draw_text(
|
||||
Point::from_xy(40.0, 700.0),
|
||||
noto_font.clone(),
|
||||
28.0,
|
||||
"Title: __________________________",
|
||||
false,
|
||||
TextDirection::Auto,
|
||||
);
|
||||
}
|
||||
surface.draw_text(Point::from_xy(420.0, 800.0), noto_font.clone(), 12.0, &data.date, false, TextDirection::Auto);
|
||||
surface.draw_text(
|
||||
Point::from_xy(420.0, 800.0),
|
||||
noto_font.clone(),
|
||||
12.0,
|
||||
&data.date,
|
||||
false,
|
||||
TextDirection::Auto,
|
||||
);
|
||||
surface.set_fill(None);
|
||||
|
||||
// Subheading: volume, concentration, density
|
||||
let density_str = data.density.map(|d| format!("{} g/L", d)).unwrap_or_else(|| "".to_string());
|
||||
let sub = format!("Volume: {} ml Concentration: {} Density: {}", data.volume_ml, data.concentration, density_str);
|
||||
surface.draw_text(Point::from_xy(40.0, 760.0), noto_font.clone(), 16.0, &sub, false, TextDirection::Auto);
|
||||
let density_str = data
|
||||
.density
|
||||
.map(|d| format!("{} g/L", d))
|
||||
.unwrap_or_else(|| "".to_string());
|
||||
let volumes = data
|
||||
.volumes_ml
|
||||
.iter()
|
||||
.map(|volume| format!("{volume}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
let total_volume: f64 = data.volumes_ml.iter().sum();
|
||||
let sub = if data.volumes_ml.len() > 1 {
|
||||
format!(
|
||||
"Volumes: {} ml Total: {} ml Concentration: {} Density: {}",
|
||||
volumes, total_volume, data.concentration, density_str
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"Volume: {} ml Concentration: {} Density: {}",
|
||||
volumes, data.concentration, density_str
|
||||
)
|
||||
};
|
||||
surface.draw_text(
|
||||
Point::from_xy(40.0, 760.0),
|
||||
noto_font.clone(),
|
||||
16.0,
|
||||
&sub,
|
||||
false,
|
||||
TextDirection::Auto,
|
||||
);
|
||||
|
||||
// Mixing amounts section header
|
||||
surface.draw_text(Point::from_xy(40.0, 720.0), noto_font.clone(), 14.0, "Mixing Amounts:", false, TextDirection::Auto);
|
||||
surface.draw_text(
|
||||
Point::from_xy(40.0, 720.0),
|
||||
noto_font.clone(),
|
||||
14.0,
|
||||
"Mixing Amounts:",
|
||||
false,
|
||||
TextDirection::Auto,
|
||||
);
|
||||
|
||||
// Nominal amounts
|
||||
let plaster = data.plaster_total.map(|p| format!("Plaster: {:.1} g", p)).unwrap_or_else(|| "Plaster: ______".to_string());
|
||||
let water = data.water_total.map(|w| format!("Water: {:.1} g", w)).unwrap_or_else(|| "Water: ______".to_string());
|
||||
surface.draw_text(Point::from_xy(60.0, 700.0), noto_font.clone(), 12.0, &plaster, false, TextDirection::Auto);
|
||||
surface.draw_text(Point::from_xy(60.0, 684.0), noto_font.clone(), 12.0, &water, false, TextDirection::Auto);
|
||||
let plaster = data
|
||||
.plaster_total
|
||||
.map(|p| format!("Plaster: {:.1} g", p))
|
||||
.unwrap_or_else(|| "Plaster: ______".to_string());
|
||||
let water = data
|
||||
.water_total
|
||||
.map(|w| format!("Water: {:.1} g", w))
|
||||
.unwrap_or_else(|| "Water: ______".to_string());
|
||||
surface.draw_text(
|
||||
Point::from_xy(60.0, 700.0),
|
||||
noto_font.clone(),
|
||||
12.0,
|
||||
&plaster,
|
||||
false,
|
||||
TextDirection::Auto,
|
||||
);
|
||||
surface.draw_text(
|
||||
Point::from_xy(60.0, 684.0),
|
||||
noto_font.clone(),
|
||||
12.0,
|
||||
&water,
|
||||
false,
|
||||
TextDirection::Auto,
|
||||
);
|
||||
|
||||
// Slack amounts + spot for actual amounts to the right
|
||||
let p_slack = data.plaster_slack.map(|p| format!("{:.1} g", p)).unwrap_or_else(|| "____".to_string());
|
||||
let w_slack = data.water_slack.map(|w| format!("{:.1} g", w)).unwrap_or_else(|| "____".to_string());
|
||||
let slack_plaster_text = format!("10% Slack (Plaster): {}", p_slack);
|
||||
let slack_water_text = format!("10% Slack (Water): {}", w_slack);
|
||||
surface.draw_text(Point::from_xy(60.0, 650.0), noto_font.clone(), 12.0, &slack_plaster_text, false, TextDirection::Auto);
|
||||
surface.draw_text(Point::from_xy(60.0, 634.0), noto_font.clone(), 12.0, &slack_water_text, false, TextDirection::Auto);
|
||||
surface.draw_text(Point::from_xy(300.0, 650.0), noto_font.clone(), 12.0, "Actual: ______________", false, TextDirection::Auto);
|
||||
let p_slack = data
|
||||
.plaster_slack
|
||||
.map(|p| format!("{:.1} g", p))
|
||||
.unwrap_or_else(|| "____".to_string());
|
||||
let w_slack = data
|
||||
.water_slack
|
||||
.map(|w| format!("{:.1} g", w))
|
||||
.unwrap_or_else(|| "____".to_string());
|
||||
let slack_plaster_text = format!("{}% Slack (Plaster): {}", data.slack_percent, p_slack);
|
||||
let slack_water_text = format!("{}% Slack (Water): {}", data.slack_percent, w_slack);
|
||||
surface.draw_text(
|
||||
Point::from_xy(60.0, 650.0),
|
||||
noto_font.clone(),
|
||||
12.0,
|
||||
&slack_plaster_text,
|
||||
false,
|
||||
TextDirection::Auto,
|
||||
);
|
||||
surface.draw_text(
|
||||
Point::from_xy(60.0, 634.0),
|
||||
noto_font.clone(),
|
||||
12.0,
|
||||
&slack_water_text,
|
||||
false,
|
||||
TextDirection::Auto,
|
||||
);
|
||||
surface.draw_text(
|
||||
Point::from_xy(300.0, 650.0),
|
||||
noto_font.clone(),
|
||||
12.0,
|
||||
"Actual: ______________",
|
||||
false,
|
||||
TextDirection::Auto,
|
||||
);
|
||||
|
||||
// Timings
|
||||
surface.draw_text(Point::from_xy(40.0, 600.0), noto_font.clone(), 12.0, "Soak Time: __________ (actual: ", false, TextDirection::Auto);
|
||||
surface.draw_text(
|
||||
Point::from_xy(40.0, 600.0),
|
||||
noto_font.clone(),
|
||||
12.0,
|
||||
"Soak Time: __________ (actual: ",
|
||||
false,
|
||||
TextDirection::Auto,
|
||||
);
|
||||
if let Some(soak) = &data.soak_time {
|
||||
surface.draw_text(Point::from_xy(220.0, 600.0), noto_font.clone(), 12.0, soak, false, TextDirection::Auto);
|
||||
surface.draw_text(
|
||||
Point::from_xy(220.0, 600.0),
|
||||
noto_font.clone(),
|
||||
12.0,
|
||||
soak,
|
||||
false,
|
||||
TextDirection::Auto,
|
||||
);
|
||||
}
|
||||
surface.draw_text(Point::from_xy(280.0, 600.0), noto_font.clone(), 12.0, ")", false, TextDirection::Auto);
|
||||
surface.draw_text(
|
||||
Point::from_xy(280.0, 600.0),
|
||||
noto_font.clone(),
|
||||
12.0,
|
||||
")",
|
||||
false,
|
||||
TextDirection::Auto,
|
||||
);
|
||||
|
||||
surface.draw_text(Point::from_xy(40.0, 580.0), noto_font.clone(), 12.0, "Mix Time: __________ (actual: ", false, TextDirection::Auto);
|
||||
surface.draw_text(
|
||||
Point::from_xy(40.0, 580.0),
|
||||
noto_font.clone(),
|
||||
12.0,
|
||||
"Mix Time: __________ (actual: ",
|
||||
false,
|
||||
TextDirection::Auto,
|
||||
);
|
||||
if let Some(mix) = &data.mix_time {
|
||||
surface.draw_text(Point::from_xy(220.0, 580.0), noto_font.clone(), 12.0, mix, false, TextDirection::Auto);
|
||||
surface.draw_text(
|
||||
Point::from_xy(220.0, 580.0),
|
||||
noto_font.clone(),
|
||||
12.0,
|
||||
mix,
|
||||
false,
|
||||
TextDirection::Auto,
|
||||
);
|
||||
}
|
||||
surface.draw_text(Point::from_xy(280.0, 580.0), noto_font.clone(), 12.0, ")", false, TextDirection::Auto);
|
||||
surface.draw_text(
|
||||
Point::from_xy(280.0, 580.0),
|
||||
noto_font.clone(),
|
||||
12.0,
|
||||
")",
|
||||
false,
|
||||
TextDirection::Auto,
|
||||
);
|
||||
|
||||
surface.draw_text(Point::from_xy(40.0, 560.0), noto_font.clone(), 12.0, "Set Time: __________", false, TextDirection::Auto);
|
||||
surface.draw_text(
|
||||
Point::from_xy(40.0, 560.0),
|
||||
noto_font.clone(),
|
||||
12.0,
|
||||
"Set Time: __________",
|
||||
false,
|
||||
TextDirection::Auto,
|
||||
);
|
||||
|
||||
// Target weights
|
||||
surface.draw_text(Point::from_xy(40.0, 520.0), noto_font.clone(), 14.0, "Target Cast Weight:", false, TextDirection::Auto);
|
||||
if let Some(w) = data.single_wet {
|
||||
let wet_text = format!("Wet: {:.1} g", w);
|
||||
surface.draw_text(Point::from_xy(60.0, 500.0), noto_font.clone(), 12.0, &wet_text, false, TextDirection::Auto);
|
||||
surface.draw_text(
|
||||
Point::from_xy(40.0, 520.0),
|
||||
noto_font.clone(),
|
||||
14.0,
|
||||
"Target Cast Weight:",
|
||||
false,
|
||||
TextDirection::Auto,
|
||||
);
|
||||
if let (Some(wet_weights), Some(dry_weights)) =
|
||||
(&data.cast_wet_weights, &data.cast_dry_weights)
|
||||
{
|
||||
for (index, ((volume, wet), dry)) in data
|
||||
.volumes_ml
|
||||
.iter()
|
||||
.zip(wet_weights.iter())
|
||||
.zip(dry_weights.iter())
|
||||
.take(4)
|
||||
.enumerate()
|
||||
{
|
||||
let y = 500.0 - (index as f32 * 32.0);
|
||||
let label = if data.volumes_ml.len() > 1 {
|
||||
format!("Cast {} ({} ml):", index + 1, volume)
|
||||
} else {
|
||||
"Cast:".to_string()
|
||||
};
|
||||
let wet_text = format!("{} Wet {:.1} g", label, wet);
|
||||
let dry_text = format!("Dry (theoretical): {:.1} g", dry);
|
||||
surface.draw_text(
|
||||
Point::from_xy(60.0, y),
|
||||
noto_font.clone(),
|
||||
12.0,
|
||||
&wet_text,
|
||||
false,
|
||||
TextDirection::Auto,
|
||||
);
|
||||
surface.draw_text(
|
||||
Point::from_xy(60.0, y - 16.0),
|
||||
noto_font.clone(),
|
||||
12.0,
|
||||
&dry_text,
|
||||
false,
|
||||
TextDirection::Auto,
|
||||
);
|
||||
}
|
||||
}
|
||||
if let Some(d) = data.single_dry {
|
||||
let dry_text = format!("Dry (theoretical): {:.1} g", d);
|
||||
surface.draw_text(Point::from_xy(60.0, 484.0), noto_font.clone(), 12.0, &dry_text, false, TextDirection::Auto);
|
||||
}
|
||||
surface.draw_text(Point::from_xy(300.0, 500.0), noto_font.clone(), 12.0, "Actual cast weight: ________", false, TextDirection::Auto);
|
||||
surface.draw_text(
|
||||
Point::from_xy(300.0, 500.0),
|
||||
noto_font.clone(),
|
||||
12.0,
|
||||
"Actual cast weight: ________",
|
||||
false,
|
||||
TextDirection::Auto,
|
||||
);
|
||||
|
||||
// Notes label
|
||||
surface.draw_text(Point::from_xy(44.0, 420.0), noto_font.clone(), 12.0, "Notes:", false, TextDirection::Auto);
|
||||
surface.draw_text(
|
||||
Point::from_xy(44.0, 420.0),
|
||||
noto_font.clone(),
|
||||
12.0,
|
||||
"Notes:",
|
||||
false,
|
||||
TextDirection::Auto,
|
||||
);
|
||||
}
|
||||
// surface is dropped at the end of the brace scope
|
||||
drop(page); // explicitly drop page so it releases the borrow on doc
|
||||
surface.finish();
|
||||
page.finish();
|
||||
|
||||
// Finish document and write to file
|
||||
let bytes = doc.finish().map_err(|e| format!("failed to finish document: {}", e))?;
|
||||
let bytes = doc
|
||||
.finish()
|
||||
.map_err(|e| format!("failed to finish document: {}", e))?;
|
||||
std::fs::write(out_path, bytes).map_err(|e| format!("failed to write pdf: {}", e))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user