Browse Source

Object config rework

Kevin Lee 11 months ago
parent
commit
b8666de38d

+ 11 - 12
src/config/mod.rs

@@ -1,10 +1,7 @@
 use ezcad::{array_of::ArrayOf, layer::Layer, pen::Pen};
 use serde::{Deserialize, Serialize};
 
-use self::{
-    object::{DeleteObjects, ExportObject, HatchArray, ImportObject},
-    pen::{ClonePen, ImportExportPen, PatchPen, PatternPen},
-};
+use self::pen::{ClonePen, ImportExportPen, PatchPen, PatternPen};
 
 pub mod object;
 pub mod pen;
@@ -16,10 +13,11 @@ pub enum Operation {
     PatternPen(PatternPen),
     ExportPen(ImportExportPen),
     ImportPen(ImportExportPen),
-    ExportObject(ExportObject),
-    ImportObject(ImportObject),
-    DeleteObjects(DeleteObjects),
-    HatchArray(HatchArray),
+    // ExportObject(ExportObject),
+    // ImportObject(ImportObject),
+    // DeleteObjects(DeleteObjects),
+    // MoveObject(MoveObject),
+    // Array(Array),
 }
 
 pub trait Operations {
@@ -35,10 +33,11 @@ impl Operations for Vec<Operation> {
                 Operation::PatternPen(x) => x.pattern(pens),
                 Operation::ImportPen(x) => x.import(pens),
                 Operation::ExportPen(x) => x.export(pens),
-                Operation::ExportObject(x) => x.export(layers),
-                Operation::ImportObject(x) => x.import(layers),
-                Operation::DeleteObjects(x) => x.delete(layers),
-                Operation::HatchArray(x) => x.generate(pens, layers),
+                // Operation::ExportObject(x) => x.export(layers),
+                // Operation::ImportObject(x) => x.import(layers),
+                // Operation::DeleteObjects(x) => x.delete(layers),
+                // Operation::MoveObject(x) => x.r#move(layers),
+                // Operation::Array(x) => x.generate(pens, layers),
             }
         }
     }

+ 259 - 201
src/config/object.rs

@@ -9,90 +9,17 @@ use ezcad::{
     array_of::ArrayOf,
     layer::Layer,
     objects::{
+        circle::Circle,
         hatch::{Hatch, HatchFlag, HatchPattern, HatchSetting, Hatches},
         rectangle::Rectangle,
-        Object, ObjectCore,
+        Object, ObjectCore, Translate,
     },
     pen::Pen,
     types::{ObjectType, Point},
 };
-use log::debug;
+use log::{debug, warn};
 use rand::{seq::SliceRandom, thread_rng};
-use serde::{Deserialize, Serialize};
-
-#[derive(Debug, Serialize, Deserialize)]
-#[serde(rename_all = "PascalCase")]
-pub struct ExportObject {
-    layer: usize,
-    object: usize,
-    path: PathBuf,
-}
-
-impl ExportObject {
-    pub fn export(&self, layers: &ArrayOf<Layer>) {
-        debug!(
-            "Exporting layer #{} object #{} to '{}'",
-            self.layer,
-            self.object,
-            self.path.to_string_lossy()
-        );
-
-        let layer = layers.get(self.layer).expect("Invalid layer index");
-        let object = layer
-            .objects
-            .get(self.object)
-            .expect("Invalid object index");
-
-        let mut buffer: Cursor<Vec<u8>> = Cursor::new(vec![]);
-        buffer.write_le(object).expect("Failed to serialize object");
-
-        let mut output: File = File::create(&self.path).expect("Failed to open output file");
-        output
-            .write_all(buffer.into_inner().as_slice())
-            .expect("Failed to write to output file");
-    }
-}
-
-#[derive(Debug, Serialize, Deserialize)]
-#[serde(rename_all = "PascalCase")]
-pub struct ImportObject {
-    layer: usize,
-    object: Option<usize>,
-    path: PathBuf,
-}
-
-impl ImportObject {
-    pub fn import(&self, layers: &mut ArrayOf<Layer>) {
-        debug!(
-            "Importing layer #{}{} from '{}'",
-            self.layer,
-            match &self.object {
-                Some(object) => format!(" object #{}", object),
-                None => String::new(),
-            },
-            self.path.to_string_lossy(),
-        );
-
-        let mut input: File = File::open(&self.path).expect("Failed to open input file");
-
-        let mut buffer: Cursor<Vec<u8>> = Cursor::new(vec![]);
-        input
-            .read_to_end(buffer.get_mut())
-            .expect("Failed to read input file");
-
-        let object: Object =
-            Object::read_le(&mut buffer).expect("Failed to deserialize input as object");
-
-        let layer: &mut Layer = layers.get_mut(self.layer).expect("Invalid layer index");
-
-        if let Some(index) = self.object {
-            let dst: &mut Object = layer.objects.get_mut(index).expect("Invalid object index");
-            *dst = object;
-        } else {
-            (*layer.objects).push(object);
-        }
-    }
-}
+use serde::{de, Deserialize, Serialize};
 
 #[derive(Debug, Serialize, Deserialize)]
 #[serde(rename_all = "PascalCase")]
@@ -104,7 +31,7 @@ pub struct DeleteObjects {
 impl DeleteObjects {
     pub fn delete(&self, layers: &mut ArrayOf<Layer>) {
         debug!(
-            "Deleting {} from layer #{}",
+            "Deleting layer #{} {}",
             match &self.object {
                 Some(object) => format!("object #{}", object),
                 None => format!("all objects"),
@@ -146,56 +73,33 @@ pub struct HatchConfig {
 
 impl From<HatchConfig> for HatchSetting {
     fn from(value: HatchConfig) -> Self {
-        let mut ret = Self::default();
-
-        *ret.line_spacing = value.line_spacing;
-
-        if let Some(count) = value.count {
-            *ret.count = count.into();
-        }
-        if let Some(edge_offset) = value.edge_offset {
-            *ret.edge_offset = edge_offset.into();
-        }
-        if let Some(start_offset) = value.start_offset {
-            *ret.start_offset = start_offset.into();
-        }
-        if let Some(end_offset) = value.end_offset {
-            *ret.end_offset = end_offset.into();
-        }
-        if let Some(angle) = value.angle {
-            *ret.angle = angle.into();
-        }
-        if let Some(rotate_angle) = value.rotate_angle {
-            *ret.rotate_angle = rotate_angle.into();
-        }
-        if let Some(line_reduction) = value.line_reduction {
-            *ret.line_reduction = line_reduction.into();
-        }
-        if let Some(line_reduction) = value.line_reduction {
-            *ret.line_reduction = line_reduction.into();
-        }
-        if let Some(loop_distance) = value.loop_distance {
-            *ret.loop_distance = loop_distance.into();
-        }
-        if let Some(loop_count) = value.loop_count {
-            *ret.loop_count = loop_count.into();
-        }
-
         let mut flags: HatchFlag = match value.pattern {
             Some(pattern) => pattern.into(),
             None => HatchPattern::Directional.into(),
         };
 
-        if let Some(follow_edge) = value.follow_edge_once {
-            flags.set_follow_edge_once(follow_edge.into());
-        }
-        if let Some(cross_hatch) = value.cross_hatch {
-            flags.set_cross_hatch(cross_hatch.into());
-        }
+        value
+            .follow_edge_once
+            .map(|x| flags.set_follow_edge_once(x.into()));
+        value.cross_hatch.map(|x| flags.set_cross_hatch(x.into()));
+
         if value.start_offset.is_none() && value.end_offset.is_none() {
             flags.set_average_distribute_line(1);
         }
 
+        let mut ret = Self::default();
+
+        *ret.line_spacing = value.line_spacing;
+        value.count.map(|x| *ret.count = x.into());
+        value.edge_offset.map(|x| *ret.edge_offset = x.into());
+        value.start_offset.map(|x| *ret.start_offset = x.into());
+        value.end_offset.map(|x| *ret.end_offset = x.into());
+        value.angle.map(|x| *ret.angle = x.into());
+        value.rotate_angle.map(|x| *ret.rotate_angle = x.into());
+        value.line_reduction.map(|x| *ret.line_reduction = x.into());
+        value.loop_distance.map(|x| *ret.loop_distance = x.into());
+        value.loop_count.map(|x| *ret.loop_count = x.into());
+
         *ret.flags = flags;
         *ret.enabled = true.into();
 
@@ -205,104 +109,258 @@ impl From<HatchConfig> for HatchSetting {
 
 #[derive(Debug, Serialize, Deserialize)]
 #[serde(rename_all = "PascalCase")]
-pub struct HatchArray {
-    layer: usize,
-    width: f64,
-    height: f64,
+pub struct ArrayConfig {
     columns: usize,
     rows: usize,
     spacing: f64,
-    z: f64,
-    starting_pen: usize,
-    hatch: HatchConfig,
+    randomize_order: bool,
+    starting_pen: u32,
 }
 
-impl HatchArray {
-    pub fn generate(&self, pens: &Vec<Pen>, layers: &mut ArrayOf<Layer>) {
-        debug!(
-            "Generating {} x {} array of {} x {} rectangles with spacing of {} starting from pen #{} on layer #{}",
-            self.columns, self.rows, self.width, self.height, self.spacing, self.starting_pen, self.layer
-        );
-        assert!(
-            self.rows >= 1 && self.columns >= 1,
-            "Invalid row/column value"
-        );
-        assert!(
-            self.starting_pen + (self.rows * self.columns) < pens.len(),
-            "Invalid starting pen"
-        );
+#[derive(Debug, Serialize, Deserialize, strum::Display)]
+#[serde(rename_all = "PascalCase")]
+pub enum InputObject {
+    Rectangle {
+        width: f64,
+        height: f64,
+        round_corner: Option<f64>,
+    },
+    Circle {
+        radius: f64,
+    },
+    Import {
+        path: PathBuf,
+    },
+    Existing {
+        layer: usize,
+        object: usize,
+    },
+}
 
-        let layer: &mut Layer = layers.get_mut(self.layer).expect("Invalid layer index");
+impl InputObject {
+    fn new(&self, layers: &ArrayOf<Layer>) -> Object {
+        match self {
+            InputObject::Rectangle {
+                width,
+                height,
+                round_corner,
+            } => Object::Rectangle(Rectangle {
+                corner_a: Point::from((-width / 2.0, -height / 2.0)).into(),
+                corner_b: Point::from((width / 2.0, height / 2.0)).into(),
+                round_bottom_left: round_corner.unwrap_or(0.0).into(),
+                round_bottom_right: round_corner.unwrap_or(0.0).into(),
+                round_top_left: round_corner.unwrap_or(0.0).into(),
+                round_top_right: round_corner.unwrap_or(0.0).into(),
+                ..Default::default()
+            }),
+            InputObject::Circle { radius } => Object::Circle(Circle {
+                radius: (*radius).into(),
+                ..Default::default()
+            }),
+            InputObject::Import { path } => Object::read_from_file(path),
+            InputObject::Existing { layer, object } => {
+                let layer: &Layer = layers.get(*layer).expect("Invalid layer index");
+                layer
+                    .objects
+                    .get(*object)
+                    .expect("Invalid object index")
+                    .clone()
+            }
+        }
+    }
+}
 
-        let incr_x: f64 = self.width + self.spacing;
-        let incr_y: f64 = self.height + self.spacing;
+#[derive(Debug, Serialize, Deserialize)]
+#[serde(rename_all = "PascalCase")]
+pub struct ObjectOperation {
+    input: InputObject,
+    z: Option<f64>,
+    origin: Option<Point>,
+    pen: Option<u32>,
+    layer: Option<usize>,
+    array: Option<ArrayConfig>,
+    hatch: Option<HatchConfig>,
+    export: Option<PathBuf>,
+    replace_object: Option<usize>,
+}
 
-        let bottom_left: Point = Point {
-            x: (self.columns - 1) as f64 * -incr_x / 2.0,
-            y: (self.rows - 1) as f64 * -incr_y / 2.0,
-        };
+impl ObjectOperation {
+    pub fn process(&self, pens: &Vec<Pen>, layers: &mut ArrayOf<Layer>) {
+        debug!("Processing object {}", self.input);
 
-        // Closure that returns origin point of given index in array, where index starts
-        // from bottom left and increments to the right and wraps around to the row above
-        let calc_pt = |index: usize| {
-            let x_pos: f64 = (index % self.columns) as f64;
-            let y_pos: f64 = (index / self.columns) as f64;
+        let mut object: Object = self.input.new(layers);
 
-            Point {
-                x: bottom_left.x + incr_x * x_pos,
-                y: bottom_left.y + incr_y * y_pos,
-            }
-        };
+        // Process basic transformation
+        object.translate(self.origin, self.z);
+        self.pen.map(|pen| object.set_pen(pen));
 
-        // Randomize draw order
-        let mut seq: Vec<usize> = (0..(self.rows * self.columns)).collect();
-        seq.shuffle(&mut thread_rng());
+        // Process conversion to hatch object
 
-        // Generate objects and append to layer
-        for obj_idx in seq.into_iter() {
-            let pen: u32 = (self.starting_pen + obj_idx).try_into().unwrap();
+        // Process array generation of object
+        let new_objects = self.array.as_ref().map_or(vec![object.clone()], |array| {
+            let bottom_left: Point = Point {
+                x: (array.columns - 1) as f64 * array.spacing / -2.0,
+                y: (array.rows - 1) as f64 * array.spacing / -2.0,
+            };
 
-            // Build outline object
-            let mut rect: Rectangle = Rectangle::default();
-            let origin: Point = calc_pt(obj_idx);
+            // Closure that returns origin point of given index in array, where index starts
+            // from bottom left and increments to the right and wraps around to the row above
+            let calc_pt = |index: usize| {
+                let x_pos: f64 = (index % array.columns) as f64;
+                let y_pos: f64 = (index / array.columns) as f64;
 
-            *rect.corner_a = Point {
-                x: origin.x - self.width / 2.0,
-                y: origin.y - self.height / 2.0,
-            };
-            *rect.corner_b = Point {
-                x: origin.x + self.width / 2.0,
-                y: origin.y + self.height / 2.0,
-            };
-            *rect.core.origin = origin;
-            *rect.core.pen = pen;
-            *rect.core.z = self.z;
-
-            debug!(
-                "Adding hatched rectangle with pen #{} at {} (from {} to {})",
-                *rect.core.pen, *rect.core.origin, *rect.corner_a, *rect.corner_b
-            );
-
-            let mut hatch_setting: HatchSetting = self.hatch.into();
-            *hatch_setting.pen = pen;
-
-            // Build hatch object
-            let hatch: Hatch = Hatch {
-                core: ObjectCore {
-                    origin: origin.into(),
-                    z: self.z.into(),
-                    ..ObjectCore::default(ObjectType::Hatch)
-                },
-                outline: vec![Object::Rectangle(rect)].into(),
-                legacy_setting: vec![hatch_setting.clone()].into(),
-                hatch_settings: vec![hatch_setting.clone()].into(),
-                hatches: Some(Hatches {
-                    core: ObjectCore::default(ObjectType::HatchLine),
-                    hatch_lines: vec![].into(),
-                }),
+                Point {
+                    x: bottom_left.x + array.spacing * x_pos,
+                    y: bottom_left.y + array.spacing * y_pos,
+                }
             };
 
-            layer.objects.push(Object::Hatch(hatch));
+            // Randomize draw order
+            let mut seq: Vec<usize> = (0..(array.rows * array.columns)).collect();
+            if array.randomize_order {
+                seq.shuffle(&mut thread_rng());
+            }
+
+            // Generate objects
+            let mut new_obj: Vec<Object> = vec![];
+
+            for obj_idx in seq.into_iter() {
+                let mut object: Object = object.clone();
+                object.translate(Some(calc_pt(obj_idx)), None);
+                object.set_pen(array.starting_pen + u32::try_from(obj_idx).unwrap());
+
+                new_obj.push(object);
+            }
+
+            new_obj
+        });
+
+        // Process export of object
+        self.export.as_ref().map(|path| {
+            if new_objects.len() > 1 {
+                warn!("Exporting only the first object in list of objects");
+            } else {
+                debug!(
+                    "Exporting object {} to '{}'",
+                    new_objects[0],
+                    path.to_string_lossy()
+                );
+            }
+            new_objects[0].write_to_file(path);
+        });
+
+        // Append or replace object in layer
+        let layer: &mut Layer = layers
+            .get_mut(self.layer.unwrap_or(0))
+            .expect("Invalid layer index");
+        match self.replace_object {
+            Some(object) => {
+                // debug!("Replacing object #{} with {}", object, new_objects);
+                layer.objects.splice(object..=object, new_objects);
+            }
+            None => {
+                layer.objects.extend(new_objects);
+            }
         }
     }
 }
+
+// #[derive(Debug, Serialize, Deserialize)]
+// #[serde(rename_all = "PascalCase")]
+// pub struct Array {
+//     layer: usize,
+//     z: f64,
+//     columns: usize,
+//     rows: usize,
+//     spacing: f64,
+//     starting_pen: usize,
+//     object: InputObject,
+//     hatch: Option<HatchConfig>,
+// }
+
+// impl Array {
+//     pub fn generate(&self, pens: &Vec<Pen>, layers: &mut ArrayOf<Layer>) {
+//         debug!(
+//             "Generating {} x {} array of {:?} with spacing of {} starting from pen #{} on layer #{}",
+//             self.columns, self.rows, self.object, self.spacing, self.starting_pen, self.layer
+//         );
+//         assert!(
+//             self.rows >= 1 && self.columns >= 1,
+//             "Invalid row/column value"
+//         );
+//         assert!(
+//             self.starting_pen + (self.rows * self.columns) < pens.len(),
+//             "Invalid starting pen"
+//         );
+
+//         let layer: &mut Layer = layers.get_mut(self.layer).expect("Invalid layer index");
+
+//         let bottom_left: Point = Point {
+//             x: (self.columns - 1) as f64 * self.spacing / -2.0,
+//             y: (self.rows - 1) as f64 * self.spacing / -2.0,
+//         };
+
+//         // Closure that returns origin point of given index in array, where index starts
+//         // from bottom left and increments to the right and wraps around to the row above
+//         let calc_pt = |index: usize| {
+//             let x_pos: f64 = (index % self.columns) as f64;
+//             let y_pos: f64 = (index / self.columns) as f64;
+
+//             Point {
+//                 x: bottom_left.x + self.spacing * x_pos,
+//                 y: bottom_left.y + self.spacing * y_pos,
+//             }
+//         };
+
+//         // Randomize draw order
+//         let mut seq: Vec<usize> = (0..(self.rows * self.columns)).collect();
+//         seq.shuffle(&mut thread_rng());
+
+//         // Generate objects and append to layer
+//         for obj_idx in seq.into_iter() {
+//             let pen: u32 = (self.starting_pen + obj_idx).try_into().unwrap();
+
+//             //     // Build outline object
+//             //     let mut rect: Rectangle = Rectangle::default();
+//             //     let origin: Point = calc_pt(obj_idx);
+
+//             //     *rect.corner_a = Point {
+//             //         x: origin.x - self.width / 2.0,
+//             //         y: origin.y - self.height / 2.0,
+//             //     };
+//             //     *rect.corner_b = Point {
+//             //         x: origin.x + self.width / 2.0,
+//             //         y: origin.y + self.height / 2.0,
+//             //     };
+//             //     *rect.core.origin = origin;
+//             //     *rect.core.pen = pen;
+//             //     *rect.core.z = self.z;
+
+//             //     debug!(
+//             //         "Adding hatched rectangle with pen #{} at {} (from {} to {})",
+//             //         *rect.core.pen, *rect.core.origin, *rect.corner_a, *rect.corner_b
+//             //     );
+
+//             //     let mut hatch_setting: HatchSetting = self.hatch.into();
+//             //     *hatch_setting.pen = pen;
+
+//             //     // Build hatch object
+//             //     let hatch: Hatch = Hatch {
+//             //         core: ObjectCore {
+//             //             origin: origin.into(),
+//             //             z: self.z.into(),
+//             //             ..ObjectCore::default(ObjectType::Hatch)
+//             //         },
+//             //         outline: vec![Object::Rectangle(rect)].into(),
+//             //         legacy_setting: vec![hatch_setting.clone()].into(),
+//             //         hatch_settings: vec![hatch_setting.clone()].into(),
+//             //         hatches: Some(Hatches {
+//             //             core: ObjectCore::default(ObjectType::HatchLine),
+//             //             hatch_lines: vec![].into(),
+//             //         }),
+//             //     };
+
+//             //     layer.objects.push(Object::Hatch(hatch));
+//         }
+//     }
+// }

+ 16 - 16
src/config/pen.rs

@@ -31,23 +31,23 @@ impl Patch {
     fn patch(&self, id: usize, pens: &mut Vec<Pen>) {
         let to_patch: &mut Pen = pens.get_mut(id).expect("Invalid pen index");
 
-        if let Some(color) = self.color {
+        self.color.map(|color| {
             debug!("Patching color for pen #{} to {:?}", id, color);
             *to_patch.color = color.into()
-        }
+        });
 
-        if let Some(enabled) = self.enabled {
+        self.enabled.map(|enabled| {
             debug!("Patching enablement for pen #{} to {}", id, enabled);
             *to_patch.disabled = !enabled as u32;
-        }
+        });
 
-        if let Some(loop_count) = self.loop_count {
+        self.loop_count.map(|loop_count| {
             debug!("Patching loop count for pen #{} to {}", id, loop_count);
             assert!(loop_count > 0, "Pen loop count must be greater than zero");
             *to_patch.loop_count = loop_count;
-        }
+        });
 
-        if let Some(speed) = self.speed {
+        self.speed.map(|speed| {
             debug!("Patching speed for pen #{} to {}", id, speed);
             assert!(
                 speed > SPEED_MIN && speed <= SPEED_MAX,
@@ -56,9 +56,9 @@ impl Patch {
                 SPEED_MAX
             );
             *to_patch.speed = speed;
-        }
+        });
 
-        if let Some(power) = self.power {
+        self.power.map(|power| {
             debug!("Patching power for pen #{} to {}", id, power);
             assert!(
                 power > POWER_MIN && power <= POWER_MAX,
@@ -67,9 +67,9 @@ impl Patch {
                 POWER_MAX
             );
             *to_patch.power = power;
-        }
+        });
 
-        if let Some(frequency) = self.frequency {
+        self.frequency.map(|frequency| {
             debug!("Patching frequency for pen #{} to {}", id, frequency);
             assert!(
                 frequency >= FREQUENCY_MIN && frequency <= FREQUENCY_MAX,
@@ -79,7 +79,7 @@ impl Patch {
             );
             *to_patch.frequency = frequency;
             *to_patch.frequency_2 = frequency.try_into().unwrap();
-        }
+        });
 
         // Always enable custom settings for pen
         *to_patch.use_default = 0;
@@ -139,9 +139,9 @@ impl ClonePen {
                     *dst.color = Rgba::random().into();
 
                     // Patch pen if needed
-                    if let Some(patch) = &self.patch {
+                    self.patch.as_ref().map(|patch| {
                         patch.patch(idx, pens);
-                    }
+                    });
                 }
             }
             _ => {
@@ -149,9 +149,9 @@ impl ClonePen {
                 *dst = src;
 
                 // Patch pen if needed
-                if let Some(patch) = &self.patch {
+                self.patch.as_ref().map(|patch| {
                     patch.patch(self.to, pens);
-                }
+                });
             }
         }
     }

+ 10 - 3
src/ezcad/objects/circle.rs

@@ -8,10 +8,10 @@ use crate::{
     types::{Field, ObjectType, Point, F64, U32},
 };
 
-use super::ObjectCore;
+use super::{ObjectCore, Translate};
 
 #[cfg_attr(feature = "default-debug", derive(Debug))]
-#[derive(BinRead, BinWrite, Diff, PartialEq)]
+#[derive(BinRead, BinWrite, Clone, Diff, PartialEq)]
 #[diff(attr(
     #[derive(Debug, PartialEq)]
 ))]
@@ -22,7 +22,7 @@ pub struct Circle {
     pub radius: F64,
     pub start_angle: F64, // Radians
     pub clockwise: U32,
-    _unknown_1: [Field; 2],
+    pub _unknown_1: [Field; 2],
 }
 
 // Custom Debug implementation to only print known fields
@@ -59,3 +59,10 @@ impl Default for Circle {
         }
     }
 }
+
+impl Translate for Circle {
+    fn translate(&mut self, origin: Option<Point>, z: Option<f64>) {
+        self.core.translate(origin, z);
+        origin.map(|origin| self.origin = origin.into());
+    }
+}

+ 13 - 3
src/ezcad/objects/ellipse.rs

@@ -8,10 +8,10 @@ use crate::{
     types::{Field, ObjectType, Point, F64, U32},
 };
 
-use super::ObjectCore;
+use super::{ObjectCore, Translate};
 
 #[cfg_attr(feature = "default-debug", derive(Debug))]
-#[derive(BinRead, BinWrite, Diff, PartialEq)]
+#[derive(BinRead, BinWrite, Clone, Diff, PartialEq)]
 #[diff(attr(
     #[derive(Debug, PartialEq)]
 ))]
@@ -23,7 +23,7 @@ pub struct Ellipse {
     pub corner_b: FieldOf<Point>,
     pub start_angle: F64, // Radians
     pub end_angle: F64,   // Radians
-    _unknown_1: [Field; 2],
+    pub _unknown_1: [Field; 2],
     pub open_curve: U32,
 }
 
@@ -65,3 +65,13 @@ impl Default for Ellipse {
         }
     }
 }
+
+impl Translate for Ellipse {
+    fn translate(&mut self, origin: Option<Point>, z: Option<f64>) {
+        self.core.translate(origin, z);
+        origin.map(|origin| {
+            *self.corner_a += origin;
+            *self.corner_b += origin;
+        });
+    }
+}

+ 17 - 9
src/ezcad/objects/hatch.rs

@@ -3,7 +3,7 @@ use std::fmt::Debug;
 use crate::{
     array_of::ArrayOf,
     field_of::FieldOf,
-    types::{Field, F64, U32},
+    types::{Field, Point, F64, U32},
 };
 use binrw::{binrw, BinRead, BinWrite};
 use diff::Diff;
@@ -13,7 +13,7 @@ use modular_bitfield::{
 };
 use serde::{Deserialize, Serialize};
 
-use super::{line::Lines, Object, ObjectCore};
+use super::{line::Lines, Object, ObjectCore, Translate};
 
 #[bitfield(bits = 32)]
 #[derive(BinRead, BinWrite, Copy, Clone, Debug, Default, Diff, PartialEq)]
@@ -82,7 +82,7 @@ impl From<HatchPattern> for HatchFlag {
 }
 
 #[cfg_attr(feature = "default-debug", derive(Debug))]
-#[derive(BinRead, BinWrite, Diff, PartialEq)]
+#[derive(BinRead, BinWrite, Clone, Diff, PartialEq)]
 #[diff(attr(
     #[derive(Debug, PartialEq)]
 ))]
@@ -126,7 +126,7 @@ pub struct LegacyHatchSetting {
     pub hatch_0_loop_distance: F64,
     pub hatch_1_loop_distance: F64,
     pub hatch_2_loop_distance: F64,
-    _unknown_1: [Field; 3],
+    pub _unknown_1: [Field; 3],
     pub contour_priority: U32,
     pub hatch_0_count: U32,
     pub hatch_1_count: U32,
@@ -204,7 +204,7 @@ pub struct HatchSetting {
     pub line_reduction: F64,
     pub loop_distance: F64,
     pub loop_count: U32,
-    _unknown_1: [Field; 2],
+    pub _unknown_1: [Field; 2],
 }
 
 // Custom Debug implementation to only print known fields
@@ -305,7 +305,7 @@ impl From<Vec<HatchSetting>> for LegacyHatchSetting {
     }
 }
 
-#[derive(BinRead, BinWrite, Debug, Diff, PartialEq)]
+#[derive(BinRead, BinWrite, Clone, Debug, Diff, PartialEq)]
 #[diff(attr(
     #[derive(Debug, PartialEq)]
 ))]
@@ -313,7 +313,7 @@ pub struct HatchLine {
     pub lines: ArrayOf<Lines>,
 }
 
-#[derive(BinRead, BinWrite, Debug, Diff, PartialEq)]
+#[derive(BinRead, BinWrite, Clone, Debug, Diff, PartialEq)]
 #[diff(attr(
     #[derive(Debug, PartialEq)]
 ))]
@@ -323,7 +323,7 @@ pub struct HatchLines {
     pub hatch_line: ArrayOf<HatchLine>,
 }
 
-#[derive(BinRead, BinWrite, Debug, Diff, PartialEq)]
+#[derive(BinRead, BinWrite, Clone, Debug, Diff, PartialEq)]
 #[diff(attr(
     #[derive(Debug, PartialEq)]
 ))]
@@ -333,7 +333,7 @@ pub struct Hatches {
 }
 
 #[binrw]
-#[derive(Debug, Diff, PartialEq)]
+#[derive(Clone, Debug, Diff, PartialEq)]
 #[diff(attr(
     #[derive(Debug, PartialEq)]
 ))]
@@ -352,3 +352,11 @@ pub struct Hatch {
     #[brw(if(legacy_setting.any_hatch_enabled.value == 1))]
     pub hatches: Option<Hatches>,
 }
+
+impl Translate for Hatch {
+    fn translate(&mut self, origin: Option<Point>, z: Option<f64>) {
+        self.core.translate(origin, z);
+        self.outline.iter_mut().for_each(|x| x.translate(origin, z));
+        // Origin of individual hatch lines is always (0, 0)
+    }
+}

+ 25 - 4
src/ezcad/objects/line.rs

@@ -5,10 +5,10 @@ use diff::Diff;
 
 use crate::{array_of::ArrayOf, types::Point};
 
-use super::ObjectCore;
+use super::{ObjectCore, Translate};
 
 #[cfg_attr(feature = "default-debug", derive(Debug))]
-#[derive(BinRead, BinWrite, Diff, PartialEq)]
+#[derive(BinRead, BinWrite, Clone, Diff, PartialEq)]
 #[diff(attr(
     #[derive(Debug, PartialEq)]
 ))]
@@ -35,8 +35,22 @@ impl Debug for LineType {
     }
 }
 
+impl Translate for LineType {
+    fn translate(&mut self, origin: Option<Point>, _z: Option<f64>) {
+        origin.map(|origin| match self {
+            LineType::Point { _zero, point } => *point = origin,
+            LineType::Line { _zero, points } => {
+                points.iter_mut().for_each(|pt| *pt += origin);
+            }
+            LineType::Bezier { _zero, points } => {
+                points.iter_mut().for_each(|pt| *pt += origin);
+            }
+        });
+    }
+}
+
 #[binrw]
-#[derive(Debug, Diff, PartialEq)]
+#[derive(Clone, Debug, Diff, PartialEq)]
 #[diff(attr(
     #[derive(Debug, PartialEq)]
 ))]
@@ -47,8 +61,15 @@ pub struct Lines {
     #[bw(try_calc(u32::try_from(lines.len())))]
     pub num_lines: u32,
 
-    _unknown_1: u32,
+    pub _unknown_1: u32,
 
     #[br(count = num_lines)]
     pub lines: Vec<LineType>,
 }
+
+impl Translate for Lines {
+    fn translate(&mut self, origin: Option<Point>, z: Option<f64>) {
+        self.core.translate(origin, z);
+        self.lines.iter_mut().for_each(|x| x.translate(origin, z));
+    }
+}

+ 71 - 4
src/ezcad/objects/mod.rs

@@ -1,7 +1,13 @@
-use std::fmt::Debug;
+use std::{
+    fmt::Debug,
+    fs::File,
+    io::{Cursor, Read, Write},
+    path::PathBuf,
+};
 
-use binrw::{BinRead, BinWrite};
+use binrw::{BinRead, BinWrite, BinWriterExt};
 use diff::Diff;
+use log::error;
 use modular_bitfield::{
     bitfield,
     specifiers::{B1, B14},
@@ -24,6 +30,11 @@ pub mod line;
 pub mod polygon;
 pub mod rectangle;
 
+pub trait Translate {
+    /// Move origin of object to point
+    fn translate(&mut self, origin: Option<Point>, z: Option<f64>);
+}
+
 #[bitfield(bits = 16)]
 #[derive(BinRead, BinWrite, Copy, Clone, Debug, Default, Diff, PartialEq)]
 #[diff(attr(
@@ -40,7 +51,7 @@ pub struct ObjectFlags {
 
 /// Core properties defined for all object types
 #[cfg_attr(feature = "default-debug", derive(Debug))]
-#[derive(BinRead, BinWrite, Diff, PartialEq)]
+#[derive(BinRead, BinWrite, Clone, Diff, PartialEq)]
 #[diff(attr(
     #[derive(Debug, PartialEq)]
 ))]
@@ -81,6 +92,13 @@ impl Debug for ObjectCore {
     }
 }
 
+impl Translate for ObjectCore {
+    fn translate(&mut self, origin: Option<Point>, z: Option<f64>) {
+        origin.map(|origin| *self.origin += origin);
+        z.map(|z| self.z = z.into());
+    }
+}
+
 impl ObjectCore {
     pub fn default(obj_type: ObjectType) -> Self {
         Self {
@@ -111,7 +129,7 @@ impl ObjectCore {
     }
 }
 
-#[derive(BinRead, BinWrite, Debug, Diff, PartialEq)]
+#[derive(BinRead, BinWrite, Clone, Debug, Diff, PartialEq, strum::Display)]
 #[diff(attr(
     #[derive(Debug, PartialEq)]
 ))]
@@ -131,3 +149,52 @@ pub enum Object {
     #[brw(magic = 32u32)]
     Hatch(Hatch),
 }
+
+impl Translate for Object {
+    fn translate(&mut self, origin: Option<Point>, z: Option<f64>) {
+        match self {
+            Object::Curve(x) => x.translate(origin, z),
+            Object::Point(x) => x.translate(origin, z),
+            Object::Rectangle(x) => x.translate(origin, z),
+            Object::Circle(x) => x.translate(origin, z),
+            Object::Ellipse(x) => x.translate(origin, z),
+            Object::Polygon(x) => x.translate(origin, z),
+            Object::Hatch(x) => x.translate(origin, z),
+        }
+    }
+}
+
+impl Object {
+    pub fn read_from_file(path: &PathBuf) -> Self {
+        let mut input: File = File::open(path).expect("Failed to open input file");
+
+        let mut buffer: Cursor<Vec<u8>> = Cursor::new(vec![]);
+        input
+            .read_to_end(buffer.get_mut())
+            .expect("Failed to read input file");
+
+        Object::read_le(&mut buffer).expect("Failed to deserialize input as object")
+    }
+
+    pub fn write_to_file(&self, path: &PathBuf) {
+        let mut buffer: Cursor<Vec<u8>> = Cursor::new(vec![]);
+        buffer.write_le(self).expect("Failed to serialize object");
+
+        let mut output: File = File::create(path).expect("Failed to open output file");
+        output
+            .write_all(buffer.into_inner().as_slice())
+            .expect("Failed to write to output file");
+    }
+
+    pub fn set_pen(&mut self, pen: u32) {
+        match self {
+            Object::Curve(x) => x.core.pen = pen.into(),
+            Object::Point(x) => x.core.pen = pen.into(),
+            Object::Rectangle(x) => x.core.pen = pen.into(),
+            Object::Circle(x) => x.core.pen = pen.into(),
+            Object::Ellipse(x) => x.core.pen = pen.into(),
+            Object::Polygon(x) => x.core.pen = pen.into(),
+            Object::Hatch(x) => error!("Cannot set universal pen for hatch"),
+        }
+    }
+}

+ 13 - 3
src/ezcad/objects/polygon.rs

@@ -8,10 +8,10 @@ use crate::{
     types::{Field, ObjectType, Point, F64, U32},
 };
 
-use super::ObjectCore;
+use super::{ObjectCore, Translate};
 
 #[cfg_attr(feature = "default-debug", derive(Debug))]
-#[derive(BinRead, BinWrite, Diff, PartialEq)]
+#[derive(BinRead, BinWrite, Clone, Diff, PartialEq)]
 #[diff(attr(
     #[derive(Debug, PartialEq)]
 ))]
@@ -26,7 +26,7 @@ pub struct Polygon {
     pub offset_dx: F64,
     pub offset_dy: F64,
     pub edges: U32,
-    _unknown_1: [Field; 2],
+    pub _unknown_1: [Field; 2],
 }
 
 // Custom Debug implementation to only print known fields
@@ -71,3 +71,13 @@ impl Default for Polygon {
         }
     }
 }
+
+impl Translate for Polygon {
+    fn translate(&mut self, origin: Option<Point>, z: Option<f64>) {
+        self.core.translate(origin, z);
+        origin.map(|origin| {
+            *self.corner_a += origin;
+            *self.corner_b += origin;
+        });
+    }
+}

+ 13 - 3
src/ezcad/objects/rectangle.rs

@@ -8,10 +8,10 @@ use crate::{
     types::{Field, ObjectType, Point, F64},
 };
 
-use super::ObjectCore;
+use super::{ObjectCore, Translate};
 
 #[cfg_attr(feature = "default-debug", derive(Debug))]
-#[derive(BinRead, BinWrite, Diff, PartialEq)]
+#[derive(BinRead, BinWrite, Clone, Diff, PartialEq)]
 #[diff(attr(
     #[derive(Debug, PartialEq)]
 ))]
@@ -24,7 +24,7 @@ pub struct Rectangle {
     pub round_bottom_right: F64,
     pub round_top_right: F64,
     pub round_top_left: F64,
-    _unknown_1: [Field; 2],
+    pub _unknown_1: [Field; 2],
 }
 
 // Custom Debug implementation to only print known fields
@@ -66,3 +66,13 @@ impl Default for Rectangle {
         }
     }
 }
+
+impl Translate for Rectangle {
+    fn translate(&mut self, origin: Option<Point>, z: Option<f64>) {
+        self.core.translate(origin, z);
+        origin.map(|origin| {
+            *self.corner_a += origin;
+            *self.corner_b += origin;
+        });
+    }
+}

+ 46 - 2
src/ezcad/types.rs

@@ -1,8 +1,12 @@
-use std::fmt::{Debug, Display};
+use std::{
+    fmt::{Debug, Display},
+    ops::{Add, AddAssign, Sub},
+};
 
 use binrw::{binrw, BinRead, BinWrite};
 use diff::{Diff, VecDiff};
 use rand::{thread_rng, Rng};
+use serde::{Deserialize, Serialize};
 
 use crate::{array_of::ArrayOfPrimitive, field_of::FieldOf};
 
@@ -120,7 +124,9 @@ impl Rgba {
     }
 }
 
-#[derive(Copy, Clone, Debug, Default, Diff, PartialEq, BinRead, BinWrite)]
+#[derive(
+    Copy, Clone, Debug, Default, Diff, PartialEq, BinRead, BinWrite, Serialize, Deserialize,
+)]
 #[diff(attr(
     #[derive(Debug, PartialEq)]
 ))]
@@ -135,6 +141,44 @@ impl Display for Point {
     }
 }
 
+impl Sub for Point {
+    type Output = Point;
+
+    fn sub(self, rhs: Self) -> Self::Output {
+        Point {
+            x: self.x - rhs.x,
+            y: self.y - rhs.y,
+        }
+    }
+}
+
+impl Add for Point {
+    type Output = Point;
+
+    fn add(self, rhs: Self) -> Self::Output {
+        Point {
+            x: self.x + rhs.x,
+            y: self.y + rhs.y,
+        }
+    }
+}
+
+impl AddAssign for Point {
+    fn add_assign(&mut self, rhs: Self) {
+        self.x += rhs.x;
+        self.y += rhs.y;
+    }
+}
+
+impl From<(f64, f64)> for Point {
+    fn from(value: (f64, f64)) -> Self {
+        Self {
+            x: value.0,
+            y: value.1,
+        }
+    }
+}
+
 #[derive(Clone, Debug, Diff, PartialEq, BinRead, BinWrite, strum::Display)]
 #[diff(attr(
     #[derive(Debug, PartialEq)]

+ 6 - 6
src/main.rs

@@ -102,7 +102,7 @@ fn main() {
     }
 
     // Process diff
-    if let Some(diff) = cli.diff {
+    cli.diff.map(|diff| {
         info!("Processing diff file '{}'", diff.to_string_lossy());
         let mut diff: File = File::open(diff).expect("Failed to open diff file");
         let diff_file: EzCadHeader =
@@ -124,10 +124,10 @@ fn main() {
                 .value
                 .diff(&diff_file.layers_offset.value)
         );
-    }
+    });
 
     // Process config
-    if let Some(config) = cli.config {
+    cli.config.map(|config| {
         info!("Processing config file '{}'", config.to_string_lossy());
         let config: String = std::fs::read_to_string(config).expect("Failed to open config file");
         let config: Config = serde_yaml::from_str(&config).expect("Failed to parse config file");
@@ -138,10 +138,10 @@ fn main() {
             .ops
             .apply(&mut file.pens_offset.data.pens, &mut file.layers_offset);
         trace!("Config processing time: {:?}", time.elapsed());
-    }
+    });
 
     // Process output
-    if let Some(output) = cli.output {
+    cli.output.map(|output| {
         info!("Writing output file '{}'", output.to_string_lossy());
         // Serialize to memory buffer for perf
         let mut buffer: Cursor<Vec<u8>> = Cursor::new(vec![]);
@@ -156,5 +156,5 @@ fn main() {
         output
             .write_all(buffer.into_inner().as_slice())
             .expect("Failed to write to output file");
-    }
+    });
 }