├── .gitignore ├── Cargo.toml ├── README.md └── src ├── lib.rs └── main.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "hyperwood" 3 | version = "0.1.3" 4 | edition = "2024" 5 | license = "Apache-2.0" 6 | description = "Hyperwood is an open-source system for crafting furniture from simple wooden slats." 7 | documentation = "https://docs.rs/hyperwood/latest/hyperwood/" 8 | homepage = "https://github.com/jo/hyperwood" 9 | repository = "https://github.com/jo/hyperwood" 10 | authors = ["Johannes J. Schmidt "] 11 | 12 | [[bin]] 13 | name = "hef" 14 | path = "src/main.rs" 15 | 16 | [dependencies] 17 | clap = { version = "4.5.36", features = ["derive"] } 18 | serde = { version = "1.0.219", features = ["derive"] } 19 | serde_json = "1.0.140" 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hyperwood – Open-Source Furniture 2 | 3 | Hyperwood is an open-source system for crafting furniture from simple wooden slats. In the spirit of E.F. Schumacher's _Small is Beautiful_ and inspired by Enzo Mari’s _Autoprogettazione_, Hyperwood empowers anyone — DIY enthusiasts, designers, interior architects, and small manufacturers — to build beautiful, robust furniture using minimal tools and locally sourced materials. 4 | 5 | Algorithms automatically generate personalized construction plans and optimized material lists, making building accessible, sustainable, and waste-efficient. 6 | 7 | 8 | ## Our Designs 9 | * [hyperwood-bench](https://github.com/jo/hyperwood-bench): Our simplistic yet elegant bench is the first-ever Hyperwood design, embodying the project's essence of simplicity, functionality, and aesthetic clarity. 10 | * [hyperwood-trough](https://github.com/jo/hyperwood-trough): This versatile trough demonstrates Hyperwood’s capability to create curved forms from straight slats—perfect as a plant container or decorative piece. 11 | 12 | Discover more Hyperwood designs on crates.io by following the [#hyperwood-design](https://crates.io/keywords/hyperwood-design) keyword. 13 | 14 | ## Hyperwood Exchange Format (HEF) 15 | 16 | Inspired by the [Qubicle Exchange Format](https://getqubicle.com/qubicle/documentation/docs/file/qef/), the Hyperwood Exchange Format (HEF) is the dedicated file structure for the Hyperwood ecosystem. While the Qubicle format is voxel-based, HEF uses lines as its primitives, reflecting the structural essence of Hyperwood’s slat-based construction. HEF facilitates seamless data exchange between various software and applications, functioning as a standardized protocol for Hyperwood. 17 | Data Structure 18 | 19 | HEF files are divided into 3 parts: the header, the part map and the slats data. 20 | 21 | ### Header 22 | 23 | The first part of the header always looks like this: 24 | ``` 25 | Hyperwood Exchange Format 26 | Version 1 27 | https://hyperwood.org 28 | ``` 29 | 30 | It doesn’t hold any valuable information. Use it to test whether this file is really a HEF, or simply skip it. 31 | 32 | Now a line follows describing the name of the model: 33 | ``` 34 | Bench 35 | ``` 36 | 37 | The next line contains the parameters the model has been generated from, as JSON: 38 | ``` 39 | {"width":17,"depth":9,"height":7} 40 | ``` 41 | 42 | Then, the slat variant is included, as JSON: 43 | ``` 44 | {"x":0.06,"y":0.04,"z":0.06} 45 | ``` 46 | 47 | And the properties, calculated during model generation: 48 | ``` 49 | {"width":1.02,"depth":0.35999998,"height":0.42} 50 | ``` 51 | 52 | ### Part Map 53 | 54 | HEF uses an indexed part map that contains all part names used in the following slats data. The first line tells you how many parts are in the parts map. 55 | ``` 56 | 4 57 | ``` 58 | 59 | The following lines store the individual part names (in this case 3). Names must not be longer than 32 characters. 60 | ``` 61 | Shelf 62 | Seat 63 | Keel 64 | Leg 65 | ``` 66 | 67 | ### Lath Data 68 | 69 | The rest of the file stores all slats, one slat per line. 70 | ``` 71 | 3 4 1 11 0 0 4 2 72 | 0 0 7 17 0 0 0 1 73 | ... 74 | 14 7 0 0 0 7 7 3 75 | ``` 76 | 77 | - the first 3 values of each line are the slats’s position in X:Y:Z 78 | - the next 3 values is the slats vector, it's length in each dimension 79 | - the second last value is the layer number 80 | - the very last value is the part index of the partmap (starting with 0) 81 | 82 | ### Complete Example 83 | ``` 84 | Hyperwood Exchange Format 85 | Version 1 86 | hyperwood.org 87 | Bench 88 | {"width":17,"depth":9,"height":7} 89 | {"x":0.06,"y":0.04,"z":0.06} 90 | {"width":1.02,"depth":0.35999998,"height":0.42} 91 | 4 92 | Shelf 93 | Seat 94 | Keel 95 | Leg 96 | 3 4 1 11 0 0 4 2 97 | 0 0 7 17 0 0 0 1 98 | 0 2 7 17 0 0 2 1 99 | 0 4 7 17 0 0 4 1 100 | 0 6 7 17 0 0 6 1 101 | 0 8 7 17 0 0 8 1 102 | 2 2 2 13 0 0 2 0 103 | 2 4 2 13 0 0 4 0 104 | 2 6 2 13 0 0 6 0 105 | 3 1 0 0 0 7 1 3 106 | 14 1 0 0 0 7 1 3 107 | 3 3 1 0 0 6 3 3 108 | 14 3 1 0 0 6 3 3 109 | 3 5 1 0 0 6 5 3 110 | 14 5 1 0 0 6 5 3 111 | 3 7 0 0 0 7 7 3 112 | 14 7 0 0 0 7 7 3 113 | ``` 114 | 115 | ## API 116 | The `hyperwood` crate provides methods for parsing and generating HEF files. It also helps with Bill of Material generation. See [the docs](https://docs.rs/hyperwood/0.1.0/hyperwood/) for details. 117 | 118 | ## CLI 119 | The `hyperwood` crate also comes with a command line tool which provides some basic tasks around HEF files: 120 | ``` 121 | HEF CLI 122 | 123 | Usage: hef [OPTIONS] 124 | 125 | Commands: 126 | parameters Print out a Parameters 127 | variant Print out a Lath Variant 128 | properties Print out a Properties 129 | bom Print out a BOM 130 | requirements Print out the requirements length of slat 131 | help Print this message or the help of the given subcommand(s) 132 | 133 | Options: 134 | -f, --filename HEF filename. If omitted, read STDIN 135 | -h, --help Print help (see more with '--help') 136 | ``` 137 | 138 | © 2025 Johannes J. Schmidt 139 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use std::collections::{HashMap, HashSet}; 3 | use std::fmt::Write; 4 | use std::io::{BufRead, BufReader, Read}; 5 | 6 | static HEADER_LINES: usize = 6; 7 | 8 | /// A `Variant` defines a slat in its 3 dimensions 9 | #[derive(Debug, Serialize, Deserialize)] 10 | pub struct Variant { 11 | pub x: f32, 12 | pub y: f32, 13 | pub z: f32, 14 | } 15 | 16 | impl Default for Variant { 17 | fn default() -> Variant { 18 | Variant { 19 | x: 1.0, 20 | y: 1.0, 21 | z: 1.0, 22 | } 23 | } 24 | } 25 | 26 | impl Variant { 27 | pub fn new(x: f32, y: f32, z: f32) -> Self { 28 | Self { x, y, z } 29 | } 30 | } 31 | 32 | /// The `Slat` is the building block of a rendering. Each slat shares a `name` (which describes its 33 | /// role in the model; the name is not unique), a `layer` which is a zero-based index in the stack 34 | /// of slats, its `origin` point as well as a `vector` defining the slats direction and length. 35 | #[derive(Debug)] 36 | pub struct Slat { 37 | pub name: String, 38 | pub layer: isize, 39 | pub origin: Point, 40 | pub vector: Vector, 41 | } 42 | 43 | impl Slat { 44 | /// Parse a line in a HEF containing slat information, and return a `Slat` object. 45 | pub fn from_hef_line(line: String, name: String) -> Self { 46 | let mut origin_parts: Vec<&str> = line.rsplitn(5, " ").collect(); 47 | let origin_part = origin_parts.pop().unwrap(); 48 | let layer: isize = origin_parts[0].parse().unwrap(); 49 | let origin = Point::from_hef_part(origin_part.to_string()); 50 | let mut vector_parts: Vec<&str> = line.splitn(4, " ").collect(); 51 | let vector_part = vector_parts.pop().unwrap(); 52 | let vector = Vector::from_hef_part(vector_part.to_string()); 53 | Self { 54 | name, 55 | layer, 56 | origin, 57 | vector, 58 | } 59 | } 60 | 61 | /// Format a `Slat` as a HEF line. 62 | pub fn to_hef_line(&self) -> String { 63 | format!( 64 | "{:} {:} {:}", 65 | self.origin.to_hef_part(), 66 | self.vector.to_hef_part(), 67 | self.layer 68 | ) 69 | } 70 | 71 | /// Generate a pretty printed line for use in a Bioll of Material (BOM). 72 | pub fn to_bom_line(&self, variant: &Variant) -> String { 73 | format!("{:} {:} {:}", self.length(variant), self.layer, self.name) 74 | } 75 | 76 | /// Calculate the length of the slat. 77 | pub fn length(&self, variant: &Variant) -> f32 { 78 | self.vector.length(variant) + self.vector.unit().length(variant) 79 | } 80 | } 81 | 82 | /// A `Point` is used to define the origin of the slats. 83 | #[derive(Debug)] 84 | pub struct Point { 85 | pub x: f32, 86 | pub y: f32, 87 | pub z: f32, 88 | } 89 | 90 | impl Point { 91 | /// Parse a point from the part of a slat-line in a HEF, which contains the origin. 92 | pub fn from_hef_part(part: String) -> Self { 93 | let parts: Vec = part.split(' ').map(|no| no.parse().unwrap()).collect(); 94 | 95 | Self { 96 | x: parts[0], 97 | y: parts[1], 98 | z: parts[2], 99 | } 100 | } 101 | 102 | /// Format the point as used in HEF. 103 | pub fn to_hef_part(&self) -> String { 104 | format!("{:} {:} {:}", self.x, self.y, self.z) 105 | } 106 | } 107 | 108 | /// A `Vector` defines the orientation and length of a slat. 109 | #[derive(Debug, Clone)] 110 | pub struct Vector { 111 | pub x: f32, 112 | pub y: f32, 113 | pub z: f32, 114 | } 115 | 116 | impl Vector { 117 | /// Parse the vector part of a HEF line. 118 | pub fn from_hef_part(part: String) -> Self { 119 | let parts: Vec = part.split(' ').map(|no| no.parse().unwrap()).collect(); 120 | 121 | Self { 122 | x: parts[0], 123 | y: parts[1], 124 | z: parts[2], 125 | } 126 | } 127 | 128 | /// Format a `Vector` to be used in HEF. 129 | pub fn to_hef_part(&self) -> String { 130 | format!("{:} {:} {:}", self.x, self.y, self.z) 131 | } 132 | 133 | /// Calculate the absolute length for a given slat `Variant`: 134 | pub fn length(&self, variant: &Variant) -> f32 { 135 | ((self.x * variant.x).powf(2.0) 136 | + (self.y * variant.y).powf(2.0) 137 | + (self.z * variant.z).powf(2.0)) 138 | .sqrt() 139 | } 140 | 141 | /// Construct a unit vector, to be used in slat length calculation. 142 | pub fn unit(&self) -> Vector { 143 | let length = self.length(&Variant::default()); 144 | 145 | Vector { 146 | x: self.x / length, 147 | y: self.y / length, 148 | z: self.z / length, 149 | } 150 | } 151 | } 152 | 153 | /// A model is generated by an algorithm described by `name`, using specific `parameters` and slat `variant`. It holds the actual design, 154 | /// in its calculated `properties`, and the slats. 155 | pub struct Model { 156 | pub parameters: T, 157 | pub properties: F, 158 | pub name: String, 159 | pub variant: Variant, 160 | pub slats: Vec, 161 | } 162 | 163 | impl< 164 | T: Default + Serialize + for<'a> Deserialize<'a>, 165 | F: Default + Serialize + for<'a> Deserialize<'a>, 166 | > Model 167 | { 168 | /// Parse a HEF file, and return a model instance. 169 | pub fn from_hef(reader: R) -> Self 170 | where 171 | Self: Sized, 172 | { 173 | let hef = BufReader::new(reader); 174 | 175 | let mut name = "unknown".to_string(); 176 | let mut parameters = T::default(); 177 | let mut properties = F::default(); 178 | let mut variant = Variant::default(); 179 | let mut slats = vec![]; 180 | 181 | let mut number_of_parts: Option = None; 182 | let mut parts = HashMap::new(); 183 | for (i, line) in hef.lines().enumerate() { 184 | if i < 3 { 185 | continue; 186 | } else if i == 3 { 187 | if let Ok(ref line) = line { 188 | name = line.clone(); 189 | } 190 | } else if i == 4 { 191 | if let Ok(ref line) = line { 192 | parameters = 193 | serde_json::from_str(line).expect("could not parse parameters line"); 194 | } 195 | } else if i == 5 { 196 | if let Ok(ref line) = line { 197 | variant = serde_json::from_str(line).expect("could not parse variant line"); 198 | } 199 | } else if i == 6 { 200 | if let Ok(ref line) = line { 201 | properties = 202 | serde_json::from_str(line).expect("could not parse properties line"); 203 | } 204 | } else if i == HEADER_LINES + 1 { 205 | if let Ok(ref line) = line { 206 | number_of_parts = Some(line.parse().unwrap()); 207 | } 208 | } else if i > HEADER_LINES + 1 && i <= HEADER_LINES + 1 + number_of_parts.unwrap() { 209 | if let Ok(ref line) = line { 210 | parts.insert(i - HEADER_LINES - 2, line.clone()); 211 | } 212 | } else if i > HEADER_LINES + 1 + number_of_parts.unwrap() { 213 | if let Ok(ref line) = line { 214 | let (slat_line, part_index) = line.rsplit_once(" ").unwrap(); 215 | let part = parts.get(&part_index.parse().unwrap()).unwrap(); 216 | let slat = Slat::from_hef_line(slat_line.to_string(), part.to_string()); 217 | slats.push(slat); 218 | } 219 | } 220 | } 221 | 222 | Self { 223 | parameters, 224 | properties, 225 | name, 226 | variant, 227 | slats, 228 | } 229 | } 230 | 231 | /// Format a model instance as a HEF file. 232 | pub fn to_hef(&self) -> String { 233 | let mut hef = String::new(); 234 | 235 | writeln!(hef, "Hyperwood Exchange Format").unwrap(); 236 | writeln!(hef, "Version 1").unwrap(); 237 | writeln!(hef, "hyperwood.org").unwrap(); 238 | 239 | writeln!(hef, "{:}", self.name).unwrap(); 240 | writeln!( 241 | hef, 242 | "{:}", 243 | serde_json::to_string(&self.parameters).expect("could not serialize parameters") 244 | ) 245 | .unwrap(); 246 | writeln!( 247 | hef, 248 | "{:}", 249 | serde_json::to_string(&self.variant).expect("could not serialize variant") 250 | ) 251 | .unwrap(); 252 | writeln!( 253 | hef, 254 | "{:}", 255 | serde_json::to_string(&self.properties).expect("could not serialize properties") 256 | ) 257 | .unwrap(); 258 | 259 | let mut parts = HashSet::new(); 260 | for slat in &self.slats { 261 | parts.insert(slat.name.to_owned()); 262 | } 263 | 264 | writeln!(hef, "{:}", parts.len()).unwrap(); 265 | 266 | let mut parts_indexes = HashMap::new(); 267 | for (i, part) in parts.iter().enumerate() { 268 | parts_indexes.insert(part, i); 269 | writeln!(hef, "{:}", part).unwrap(); 270 | } 271 | 272 | for slat in &self.slats { 273 | writeln!( 274 | hef, 275 | "{:} {:}", 276 | slat.to_hef_line(), 277 | parts_indexes.get(&slat.name).unwrap() 278 | ) 279 | .unwrap(); 280 | } 281 | 282 | hef 283 | } 284 | 285 | /// Return a vector containing BOM lines. 286 | pub fn bom_lines(&self) -> Vec { 287 | self.slats 288 | .iter() 289 | .map(|slat| slat.to_bom_line(&self.variant)) 290 | .collect() 291 | } 292 | 293 | /// Generate a Bill of Material. 294 | pub fn to_bom(&self) -> String { 295 | let mut bom = String::new(); 296 | 297 | for slat in &self.slats { 298 | writeln!(bom, "{:}", slat.to_bom_line(&self.variant)).unwrap(); 299 | } 300 | 301 | bom 302 | } 303 | 304 | /// Returns the total length of all slats combined. 305 | pub fn length_total(&self) -> f32 { 306 | self.slats 307 | .iter() 308 | .map(|slat| slat.length(&self.variant)) 309 | .sum() 310 | } 311 | } 312 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use std::fs::File; 2 | use std::io; 3 | use std::io::Read; 4 | use std::path::PathBuf; 5 | 6 | use clap::{Parser, Subcommand}; 7 | use serde_json::Value; 8 | 9 | use hyperwood::Model; 10 | 11 | fn reader_from_file_or_stdin(filename: &Option) -> Box { 12 | match &filename { 13 | Some(path) => Box::new(File::open(path).unwrap()), 14 | None => Box::new(io::stdin()), 15 | } 16 | } 17 | 18 | #[derive(Parser)] 19 | #[clap(name = "hef")] 20 | #[clap( 21 | about = "HEF CLI", 22 | long_about = "With the HEF CLI, you can generate BOMs (Bill of Material) for given HEF 23 | file, and other HEF related tasks." 24 | )] 25 | pub struct Args { 26 | #[clap(subcommand)] 27 | pub command: Commands, 28 | 29 | /// HEF filename. If omitted, read STDIN 30 | #[clap(short, long)] 31 | filename: Option, 32 | } 33 | 34 | #[derive(Subcommand)] 35 | pub enum Commands { 36 | /// Print out a Parameters 37 | Parameters {}, 38 | 39 | /// Print out a Lath Variant 40 | Variant {}, 41 | 42 | /// Print out a Properties 43 | Properties {}, 44 | 45 | /// Print out a BOM 46 | Bom {}, 47 | 48 | /// Print out the requirements length of slat 49 | Requirements {}, 50 | } 51 | 52 | pub struct Client { 53 | args: Args, 54 | } 55 | 56 | impl Client { 57 | pub fn new(args: Args) -> Self { 58 | Self { args } 59 | } 60 | 61 | pub fn run(&self) { 62 | let reader = reader_from_file_or_stdin(&self.args.filename); 63 | let model: Model = Model::from_hef(reader); 64 | 65 | match &self.args.command { 66 | Commands::Parameters {} => { 67 | println!( 68 | "{}", 69 | serde_json::to_string(&model.parameters) 70 | .expect("could not serialize parameters") 71 | ); 72 | } 73 | 74 | Commands::Variant {} => { 75 | println!( 76 | "{}", 77 | serde_json::to_string(&model.variant).expect("could not serialize variant") 78 | ); 79 | } 80 | 81 | Commands::Properties {} => { 82 | println!( 83 | "{}", 84 | serde_json::to_string(&model.properties) 85 | .expect("could not serialize properties") 86 | ); 87 | } 88 | 89 | Commands::Bom {} => { 90 | print!("{}", model.to_bom()); 91 | } 92 | 93 | Commands::Requirements {} => { 94 | println!("{}", model.length_total()); 95 | } 96 | } 97 | } 98 | } 99 | 100 | fn main() { 101 | let args = Args::parse(); 102 | let client = Client::new(args); 103 | client.run(); 104 | } 105 | --------------------------------------------------------------------------------