This commit is contained in:
2026-10-13 00:55:28 -05:00
parent ee726e8c91
commit fc0b6cb454
3 changed files with 415 additions and 108 deletions
+1
View File
@@ -1 +1,2 @@
/target /target
report.pdf
+110 -51
View File
@@ -1,19 +1,20 @@
#[cfg(feature = "pdf")] #[cfg(feature = "pdf")]
pub mod pdf; pub mod pdf;
use clap::{Parser, ValueEnum};
use colored::*;
use chrono::Local; use chrono::Local;
use clap::{CommandFactory, Parser, ValueEnum, error::ErrorKind};
use colored::*;
use std::path::PathBuf; use std::path::PathBuf;
/// Concentration of plaster slurry /// Concentration of plaster slurry
/// in parts of water per 100 parts plaster by weight /// in parts of water per 100 parts plaster by weight
const DEFAULT_CONCENTRATION: f64 = 70.0; 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 DEFAULT_DENSITY: f64 = 1674.0;
const ML_PER_US_GALLON: f64 = 3785.411784;
const PERCENT: f64 = 100.0; const PERCENT: f64 = 100.0;
/// The amount of slack added /// The default amount of slack added
const SLACK: f64 = 0.10; const DEFAULT_SLACK_PERCENT: f64 = 10.0;
/// The multiplier for the water that becomes part of the mold /// The multiplier for the water that becomes part of the mold
/// ///
/// 18 parts water for every part plaster becomes part /// 18 parts water for every part plaster becomes part
@@ -36,11 +37,12 @@ enum ColorChoice {
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
#[command(author, version, about = "Calculates plaster mixing weights", long_about = None)] #[command(author, version, about = "Calculates plaster mixing weights", long_about = None)]
struct Args { struct Args {
/// Volume in cm³ or ml /// Volumes in cm³ or ml, separated by spaces or commas
volume: f64, #[arg(num_args = 1.., required = true)]
volumes: Vec<String>,
/// Optional title for the lab log (written large in the header) /// Optional title for the lab log (written large in the header)
#[arg(short = 't', long)] #[arg(short, long)]
title: Option<String>, title: Option<String>,
/// Concentration (parts water per 100 parts plaster by weight) /// Concentration (parts water per 100 parts plaster by weight)
@@ -51,14 +53,14 @@ struct Args {
#[arg(short, long)] #[arg(short, long)]
density: Option<f64>, 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 /// Control terminal color output
#[arg(long, default_value = "auto")] #[arg(long, default_value = "auto")]
color: ColorChoice, 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"). /// Soak time (for the sheet). Will be recorded; provide as text (e.g. "10m").
#[arg(long)] #[arg(long)]
soak_time: Option<String>, soak_time: Option<String>,
@@ -68,12 +70,36 @@ struct Args {
mix_time: Option<String>, mix_time: Option<String>,
/// If provided, generate a PDF lab log at this path. If omitted no PDF will be written. /// If provided, generate a PDF lab log at this path. If omitted no PDF will be written.
#[arg(long = "pdf-out")] #[arg(short, long)]
pdf_out: Option<String>, 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() { fn main() {
let args = Args::parse(); 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 // Configure color output globally based on the CLI argument
match args.color { match args.color {
@@ -82,7 +108,7 @@ fn main() {
ColorChoice::Auto => {} // Respects standard stream capabilities (default) 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(|| { let density = args.density.or_else(|| {
if (args.concentration - DEFAULT_CONCENTRATION).abs() < f64::EPSILON { if (args.concentration - DEFAULT_CONCENTRATION).abs() < f64::EPSILON {
Some(DEFAULT_DENSITY) Some(DEFAULT_DENSITY)
@@ -91,8 +117,14 @@ fn main() {
} }
}); });
let single_wet_weight = density.map(|d| (args.volume / 1000.0) * d); let total_volume: f64 = volumes.iter().sum();
let total_wet_weight = single_wet_weight.map(|w| w * args.quantity as f64); 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 { let (plaster_total, water_total) = match total_wet_weight {
Some(wet_weight) => { Some(wet_weight) => {
@@ -103,21 +135,36 @@ fn main() {
None => (None, None), None => (None, None),
}; };
let slack_fraction = args.slack / PERCENT;
// precompute slack and single dry weight as Option values so we can pass them // 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 plaster_slack = plaster_total.map(|p| p * (1.0 + slack_fraction));
let water_slack = water_total.map(|w| w * (1.0 + SLACK)); let water_slack = water_total.map(|w| w * (1.0 + slack_fraction));
let single_plaster = plaster_total.map(|p| p / args.quantity as f64); let cast_dry_weights = density.map(|d| {
let single_dry_weight_opt = single_plaster.map(|sp| sp * DRY_WEIGHT_MULTIPLIER); 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 // Output Header
println!("--- Plaster Calculation Parameters ---"); println!("--- Plaster Calculation Parameters ---");
println!( println!(
"Volume (per cast): {}ml", "Volumes: {}ml",
format!("{}", args.volume).color(COLOR_PARAM).bold() volumes
.iter()
.map(|volume| format!("{}", volume).color(COLOR_PARAM).bold().to_string())
.collect::<Vec<_>>()
.join(", ")
); );
println!( println!(
"Quantity: {}", "Total volume: {}ml ({:.2} gal)",
format!("{}", args.quantity).color(COLOR_PARAM).bold() format!("{}", total_volume).color(COLOR_PARAM).bold(),
total_volume / ML_PER_US_GALLON
); );
println!( println!(
"Concentration: {}g water per 100g plaster", "Concentration: {}g water per 100g plaster",
@@ -128,18 +175,23 @@ fn main() {
format!("{}g/L", format!("{}", d).color(COLOR_PARAM).bold()) format!("{}g/L", format!("{}", d).color(COLOR_PARAM).bold())
}); });
println!("Density: {}", density_str); println!("Density: {}", density_str);
println!(
"Slack: {}%",
format!("{}", args.slack).color(COLOR_PARAM).bold()
);
println!("--------------------------------------"); println!("--------------------------------------");
// Output Mixing Amounts and Target Weights // Output Mixing Amounts and Target Weights
if let (Some(p_tot), Some(w_tot)) = (plaster_total, water_total) { 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 p_slack = plaster_slack.unwrap_or(p_tot * (1.0 + slack_fraction));
let w_slack = water_slack.unwrap_or(w_tot * (1.0 + SLACK)); 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 cast_wet_weights = cast_wet_weights.as_ref().unwrap();
let total_dry_weight = single_dry_weight * args.quantity as f64; let cast_dry_weights = cast_dry_weights.as_ref().unwrap();
let total_dry_weight: f64 = cast_dry_weights.iter().sum();
if args.quantity > 1 { if volumes.len() > 1 {
println!("Combined Mixing Amounts (for {} casts):", args.quantity); println!("Combined Mixing Amounts (for {} casts):", volumes.len());
println!(" Nominal Total:"); println!(" Nominal Total:");
} else { } else {
println!("Mixing Amounts:"); println!("Mixing Amounts:");
@@ -148,49 +200,56 @@ fn main() {
println!( println!(
" Plaster: {}g", " Plaster: {}g",
format!("{:.1}", p_tot).color(COLOR_PLASTER) format!("{:.0}", p_tot).color(COLOR_PLASTER)
); );
println!( println!(
" Water: {}g", " Water: {}g",
format!("{:.1}", w_tot).color(COLOR_WATER) format!("{:.0}", w_tot).color(COLOR_WATER)
); );
if args.quantity > 1 { if volumes.len() > 1 {
println!(" {:2}% Slack Total:", SLACK * PERCENT); println!(" {}% Slack Total:", args.slack);
} else { } else {
println!(" {:2}% Slack:", SLACK * PERCENT); println!(" {}% Slack:", args.slack);
} }
println!( println!(
" Plaster: {}g", " Plaster: {}g",
format!("{:.1}", p_slack).color(COLOR_PLASTER).bold() format!("{:.0}", p_slack).color(COLOR_PLASTER).bold()
); );
println!( println!(
" Water: {}g", " Water: {}g",
format!("{:.1}", w_slack).color(COLOR_WATER).bold() format!("{:.0}", w_slack).color(COLOR_WATER).bold()
); );
println!("\nTarget Weights:"); println!("\nTarget Weights:");
if args.quantity > 1 { for (index, ((volume, wet_weight), dry_weight)) in volumes
println!(" Individual Cast:"); .iter()
} .zip(cast_wet_weights.iter())
.zip(cast_dry_weights.iter())
.enumerate()
{
if volumes.len() > 1 {
println!(" Cast {} ({}ml):", index + 1, volume);
}
println!( println!(
" Wet Cast: {}g", " Wet Cast: {}g",
format!("{:.1}", single_wet_weight.unwrap()).color(COLOR_TARGET).bold() format!("{:.0}", wet_weight).color(COLOR_TARGET).bold()
); );
println!( println!(
" Dry Cast: {}g", " 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!(" Total Batch:");
println!( println!(
" Wet Cast: {}g", " Wet Cast: {}g",
format!("{:.1}", total_wet_weight.unwrap()).color(COLOR_TARGET) format!("{:.0}", total_wet_weight.unwrap()).color(COLOR_TARGET)
); );
println!( println!(
" Dry Cast: {}g", " Dry Cast: {}g",
format!("{:.1}", total_dry_weight).color(COLOR_TARGET) format!("{:.0}", total_dry_weight).color(COLOR_TARGET)
); );
} }
} else { } else {
@@ -201,21 +260,21 @@ fn main() {
// If user requested a PDF output path, generate a lab log PDF (or placeholder) // If user requested a PDF output path, generate a lab log PDF (or placeholder)
#[cfg(feature = "pdf")] #[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 out_path = PathBuf::from(pdf_path);
let data = pdf::LabLogData { let data = pdf::LabLogData {
title: args.title.clone(), title: args.title.clone(),
date: Local::now().format("%Y-%m-%d").to_string(), date: Local::now().format("%Y-%m-%d").to_string(),
volume_ml: args.volume, volumes_ml: volumes.clone(),
concentration: args.concentration, concentration: args.concentration,
density, density,
quantity: args.quantity, slack_percent: args.slack,
plaster_total, plaster_total,
water_total, water_total,
plaster_slack, plaster_slack,
water_slack, water_slack,
single_wet: single_wet_weight, cast_wet_weights,
single_dry: single_dry_weight_opt, cast_dry_weights,
soak_time: args.soak_time.clone(), soak_time: args.soak_time.clone(),
mix_time: args.mix_time.clone(), mix_time: args.mix_time.clone(),
}; };
+304 -57
View File
@@ -2,26 +2,30 @@
// This module exposes a `LabLogData` struct and a `generate_lab_log` function. // This module exposes a `LabLogData` struct and a `generate_lab_log` function.
// The actual PDF implementation uses `krilla` when the `pdf` feature is enabled. // 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::path::Path;
use std::sync::Arc; use std::sync::Arc;
use krilla::geom::{Point, Size};
use krilla::page::PageSettings;
use krilla::text::{Font, TextDirection};
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct LabLogData { pub struct LabLogData {
pub title: Option<String>, pub title: Option<String>,
pub date: String, pub date: String,
pub volume_ml: f64, pub volumes_ml: Vec<f64>,
pub concentration: f64, pub concentration: f64,
pub density: Option<f64>, pub density: Option<f64>,
pub quantity: u32, pub slack_percent: f64,
pub plaster_total: Option<f64>, pub plaster_total: Option<f64>,
pub water_total: Option<f64>, pub water_total: Option<f64>,
pub plaster_slack: Option<f64>, pub plaster_slack: Option<f64>,
pub water_slack: Option<f64>, pub water_slack: Option<f64>,
pub single_wet: Option<f64>, pub cast_wet_weights: Option<Vec<f64>>,
pub single_dry: Option<f64>, pub cast_dry_weights: Option<Vec<f64>>,
pub soak_time: Option<String>, pub soak_time: Option<String>,
pub mix_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) 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. // When the `pdf` feature is enabled we provide a krilla-based implementation.
// The code below is compiled only when the feature is active. // The code below is compiled only when the feature is active.
fn generate_lab_log_inner(out_path: &Path, data: &LabLogData) -> Result<(), String> { 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 // Create document and a single page
let mut doc = Document::new(); 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 height = 80.0;
let mut surface = page.surface(); 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 pb.finish().unwrap()
let noto_font = Font::new( };
Arc::new(std::fs::read("font/NotoSans-Regular.ttf").unwrap()).into(),
0,
).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) // 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 { 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 { } 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 // Subheading: volume, concentration, density
let density_str = data.density.map(|d| format!("{} g/L", d)).unwrap_or_else(|| "".to_string()); let density_str = data
let sub = format!("Volume: {} ml Concentration: {} Density: {}", data.volume_ml, data.concentration, density_str); .density
surface.draw_text(Point::from_xy(40.0, 760.0), noto_font.clone(), 16.0, &sub, false, TextDirection::Auto); .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 // 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 // Nominal amounts
let plaster = data.plaster_total.map(|p| format!("Plaster: {:.1} g", p)).unwrap_or_else(|| "Plaster: ______".to_string()); let plaster = data
let water = data.water_total.map(|w| format!("Water: {:.1} g", w)).unwrap_or_else(|| "Water: ______".to_string()); .plaster_total
surface.draw_text(Point::from_xy(60.0, 700.0), noto_font.clone(), 12.0, &plaster, false, TextDirection::Auto); .map(|p| format!("Plaster: {:.1} g", p))
surface.draw_text(Point::from_xy(60.0, 684.0), noto_font.clone(), 12.0, &water, false, TextDirection::Auto); .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 // 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 p_slack = data
let w_slack = data.water_slack.map(|w| format!("{:.1} g", w)).unwrap_or_else(|| "____".to_string()); .plaster_slack
let slack_plaster_text = format!("10% Slack (Plaster): {}", p_slack); .map(|p| format!("{:.1} g", p))
let slack_water_text = format!("10% Slack (Water): {}", w_slack); .unwrap_or_else(|| "____".to_string());
surface.draw_text(Point::from_xy(60.0, 650.0), noto_font.clone(), 12.0, &slack_plaster_text, false, TextDirection::Auto); let w_slack = data
surface.draw_text(Point::from_xy(60.0, 634.0), noto_font.clone(), 12.0, &slack_water_text, false, TextDirection::Auto); .water_slack
surface.draw_text(Point::from_xy(300.0, 650.0), noto_font.clone(), 12.0, "Actual: ______________", false, TextDirection::Auto); .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 // 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 { 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 { 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 // Target weights
surface.draw_text(Point::from_xy(40.0, 520.0), noto_font.clone(), 14.0, "Target Cast Weight:", false, TextDirection::Auto); surface.draw_text(
if let Some(w) = data.single_wet { Point::from_xy(40.0, 520.0),
let wet_text = format!("Wet: {:.1} g", w); noto_font.clone(),
surface.draw_text(Point::from_xy(60.0, 500.0), noto_font.clone(), 12.0, &wet_text, false, TextDirection::Auto); 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 { surface.draw_text(
let dry_text = format!("Dry (theoretical): {:.1} g", d); Point::from_xy(300.0, 500.0),
surface.draw_text(Point::from_xy(60.0, 484.0), noto_font.clone(), 12.0, &dry_text, false, TextDirection::Auto); noto_font.clone(),
} 12.0,
surface.draw_text(Point::from_xy(300.0, 500.0), noto_font.clone(), 12.0, "Actual cast weight: ________", false, TextDirection::Auto); "Actual cast weight: ________",
false,
TextDirection::Auto,
);
// Notes label // 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 surface.finish();
drop(page); // explicitly drop page so it releases the borrow on doc page.finish();
// Finish document and write to file // 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))?; std::fs::write(out_path, bytes).map_err(|e| format!("failed to write pdf: {}", e))?;
Ok(()) Ok(())
} }