Browse Source

Initial commit

master
Stephen 1 week ago
commit
4d120b90f5
6 changed files with 426 additions and 0 deletions
  1. +1
    -0
      .gitignore
  2. +73
    -0
      Cargo.lock
  3. +11
    -0
      Cargo.toml
  4. +55
    -0
      example.toml
  5. +2
    -0
      rustfmt.toml
  6. +284
    -0
      src/main.rs

+ 1
- 0
.gitignore View File

@ -0,0 +1 @@
/target

+ 73
- 0
Cargo.lock View File

@ -0,0 +1,73 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
[[package]]
name = "optocut"
version = "0.1.0"
dependencies = [
"serde",
"toml",
]
[[package]]
name = "proc-macro2"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71"
dependencies = [
"unicode-xid",
]
[[package]]
name = "quote"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7"
dependencies = [
"proc-macro2",
]
[[package]]
name = "serde"
version = "1.0.123"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92d5161132722baa40d802cc70b15262b98258453e85e5d1d365c757c73869ae"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.123"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9391c295d64fc0abb2c556bad848f33cb8296276b1ad2677d1ae1ace4f258f31"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "syn"
version = "1.0.60"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c700597eca8a5a762beb35753ef6b94df201c81cca676604f547495a0d7f0081"
dependencies = [
"proc-macro2",
"quote",
"unicode-xid",
]
[[package]]
name = "toml"
version = "0.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa"
dependencies = [
"serde",
]
[[package]]
name = "unicode-xid"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564"

+ 11
- 0
Cargo.toml View File

@ -0,0 +1,11 @@
[package]
name = "optocut"
version = "0.1.0"
authors = ["Stephen <webmaster@scd31.com>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
toml = "0.5.8"
serde = { version = "1.0", features = ["derive"] }

+ 55
- 0
example.toml View File

@ -0,0 +1,55 @@
# Used to define the width of the saw blade
[blade]
inches = 0.125
[[available]]
name = "2x4x8"
feet = 8
cost = 6.98
[[available]]
name = "2x4x104"
feet = 8
inches = 8
cost = 9.15
[[available]]
name = "2x4x10"
feet = 10
cost = 9.55
[[available]]
name = "2x4x12"
feet = 12
cost = 11.45
[[available]]
name = "2x4x16"
feet = 16
cost = 15.36
[[required]]
name = "2'"
feet = 2
amount = 4
[[required]]
name = "42in"
inches = 42
amount = 4
[[required]]
name = "1'"
feet = 1
amount = 6
[[required]]
name = "34in"
inches = 34
amount = 8
[[required]]
name = "10cm"
cm = 10
amount = 1

+ 2
- 0
rustfmt.toml View File

@ -0,0 +1,2 @@
tab_spaces = 4
hard_tabs = true

+ 284
- 0
src/main.rs View File

@ -0,0 +1,284 @@
use serde::Deserialize;
use std::env;
use std::fmt;
use std::fs;
#[derive(Deserialize)]
struct Config {
blade: BladeConfig,
available: Vec<LumberConfig>,
required: Vec<PieceConfig>,
}
#[derive(Deserialize)]
struct BladeConfig {
inches: Option<f64>,
cm: Option<f64>,
}
impl BladeConfig {
fn width_cm(&self) -> f64 {
let mut length: f64 = 0.0;
let mut valid = false;
if let Some(inch) = self.inches {
valid = true;
length += inch * 2.54
}
if let Some(cm) = self.cm {
length += cm;
valid = true;
}
if !valid {
panic!("Error: Invalid blade width");
}
length
}
}
#[derive(Deserialize)]
struct LumberConfig {
name: String,
feet: Option<f64>,
inches: Option<f64>,
cm: Option<f64>,
cost: f64,
}
impl LumberConfig {
fn length_cm(&self) -> f64 {
let mut length: f64 = 0.0;
let mut valid = false;
if let Some(ft) = self.feet {
valid = true;
length += ft * 30.48;
}
if let Some(inch) = self.inches {
valid = true;
length += inch * 2.54
}
if let Some(cm) = self.cm {
length += cm;
valid = true;
}
if !valid {
panic!("Error: Invalid length for available piece '{}'", self.name);
}
length
}
}
#[derive(Deserialize)]
struct PieceConfig {
name: String,
feet: Option<f64>,
inches: Option<f64>,
cm: Option<f64>,
amount: u32,
}
impl PieceConfig {
fn length_cm(&self) -> f64 {
let mut length: f64 = 0.0;
let mut valid = false;
if let Some(ft) = self.feet {
valid = true;
length += ft * 30.48;
}
if let Some(inch) = self.inches {
valid = true;
length += inch * 2.54
}
if let Some(cm) = self.cm {
length += cm;
valid = true;
}
if !valid {
panic!("Error: Invalid length for required piece '{}'", self.name);
}
length
}
}
#[derive(Debug, Clone)]
struct Wood1D<'a> {
name: &'a str,
remaining: f64, // cm
pieces: Vec<&'a Req1D<'a>>,
cost: u32, // cents
}
impl<'a> Wood1D<'a> {
fn new(name: &'a str, length: f64, cost: u32) -> Self {
Self {
name,
remaining: length + 0.001, // To account for floating point garbage
cost,
pieces: Vec::new(),
}
}
fn try_cut(&mut self, req: &'a Req1D<'a>, blade_width: f64) -> bool {
if req.length + blade_width <= self.remaining {
self.remaining -= req.length + blade_width;
self.pieces.push(req);
true
} else {
false
}
}
fn cut(&mut self, req: &'a Req1D<'a>, blade_width: f64) {
assert!(self.try_cut(req, blade_width));
}
}
impl<'a> fmt::Display for Wood1D<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}\t->\t", self.name)?;
let mut count = 0;
for piece in &self.pieces {
write!(f, "{}\t", piece.name)?;
count += 1;
}
// A pretty dirty hack
// We should probably do this dynamically
while count < 10 {
count += 1;
write!(f, "\t")?;
}
write!(f, "[Leftover: {:.2} cm]", self.remaining)
}
}
#[derive(Debug, Clone)]
struct Req1D<'a> {
name: &'a str,
length: f64, // cm
}
impl<'a> Req1D<'a> {
fn new(name: &'a str, length: f64) -> Self {
Self { name, length }
}
}
#[derive(Debug, Clone)]
struct State<'a> {
lumber: Vec<Wood1D<'a>>,
remaining_pieces: Vec<&'a Req1D<'a>>, // cm
}
impl<'a> State<'a> {
fn cost(&self) -> u32 {
self.lumber.iter().map(|wood| wood.cost).sum()
}
}
impl<'a> fmt::Display for State<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for wood in &self.lumber {
write!(f, "\t{}\n", wood)?;
}
Ok(())
}
}
fn main() {
let args: Vec<String> = env::args().collect();
if args.len() != 2 {
println!("Usage: {} config.toml", args[0]);
return;
}
let config_contents = fs::read_to_string(&args[1]).expect("Could not read the specified file");
let config: Config =
toml::from_str(&config_contents).expect("Could not parse the specified file");
let available: Vec<Wood1D> = config
.available
.iter()
.map(|x| Wood1D::new(&x.name, x.length_cm(), (x.cost * 100.0) as u32))
.collect();
let mut required = Vec::new();
for req in &config.required {
let length = req.length_cm();
for _ in 0..req.amount {
required.push(Req1D::new(&req.name, length));
}
}
println!(
"{}",
find_best_cuts(available, required, config.blade.width_cm())
);
}
fn find_best_cuts<'a>(
available: Vec<Wood1D<'a>>,
required: Vec<Req1D<'a>>,
blade_width: f64,
) -> String {
// TODO check that all in required are <= length of available
let start_state = State {
lumber: Vec::new(),
remaining_pieces: required.iter().map(|x| x).collect(),
};
let mut best_cost = u32::MAX;
let mut best_state = start_state.clone();
let mut frontier = Vec::new();
frontier.push(start_state);
while let Some(mut cur) = frontier.pop() {
if cur.cost() >= best_cost {
continue;
}
match cur.remaining_pieces.pop() {
Some(next_piece) => {
// All combinations of adding onto existing wood
let mut found_space = false;
for (i, wood) in cur.lumber.iter().enumerate() {
if wood.remaining > next_piece.length {
let mut new_state = cur.clone();
new_state.lumber[i].cut(&next_piece, blade_width);
frontier.push(new_state);
found_space = true;
}
}
// All combinations of adding onto new wood
if !found_space {
for new_wood in &available {
if new_wood.remaining > next_piece.length
&& new_wood.cost + cur.cost() < best_cost
{
let mut new_state = cur.clone();
let mut new_wood = new_wood.to_owned();
new_wood.cut(&next_piece, blade_width);
new_state.lumber.push(new_wood);
frontier.push(new_state);
}
}
}
}
None => {
best_cost = cur.cost();
best_state = cur.clone();
}
}
}
format!(
"Best cost: ${:.?}\n{}",
best_state.cost() as f64 / 100.0,
best_state
)
}

Loading…
Cancel
Save