updates
This commit is contained in:
@@ -1 +1,2 @@
|
|||||||
/target
|
/target
|
||||||
|
report.pdf
|
||||||
|
|||||||
+109
-50
@@ -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(),
|
||||||
};
|
};
|
||||||
|
|||||||
+300
-53
@@ -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();
|
let mut surface = page.surface();
|
||||||
|
|
||||||
// Load font
|
// Load font
|
||||||
let noto_font = Font::new(
|
let noto_font = Font::new(
|
||||||
Arc::new(std::fs::read("font/NotoSans-Regular.ttf").unwrap()).into(),
|
Arc::new(std::fs::read("font/NotoSans-Regular.ttf").unwrap()).into(),
|
||||||
0,
|
0,
|
||||||
).unwrap();
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
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)
|
// 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 {
|
|
||||||
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
|
// 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(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user