Browse Source

Update to latest software version

Kevin Lee 3 months ago
parent
commit
96bc7f853f
10 changed files with 1045 additions and 952 deletions
  1. 79 9
      Cargo.lock
  2. 2 0
      Cargo.toml
  3. 11 6
      README.md
  4. 52 95
      imhex.txt
  5. 365 364
      src/config/object.rs
  6. 20 7
      src/config/pen.rs
  7. 57 57
      src/ezcad/layer.rs
  8. 414 411
      src/ezcad/objects/hatch.rs
  9. 5 3
      src/ezcad/pen.rs
  10. 40 0
      src/ezcad/types.rs

+ 79 - 9
Cargo.lock

@@ -154,7 +154,7 @@ dependencies = [
  "heck",
  "proc-macro2",
  "quote",
- "syn 2.0.42",
+ "syn 2.0.48",
 ]
 
 [[package]]
@@ -324,8 +324,10 @@ dependencies = [
  "log",
  "modular-bitfield",
  "num",
+ "num_enum",
  "rand",
  "serde",
+ "serde_repr",
  "serde_yaml",
  "strum",
 ]
@@ -427,6 +429,27 @@ dependencies = [
  "autocfg",
 ]
 
+[[package]]
+name = "num_enum"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "683751d591e6d81200c39fb0d1032608b77724f34114db54f571ff1317b337c0"
+dependencies = [
+ "num_enum_derive",
+]
+
+[[package]]
+name = "num_enum_derive"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c11e44798ad209ccdd91fc192f0526a369a01234f7373e1b141c96d7cee4f0e"
+dependencies = [
+ "proc-macro-crate",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.48",
+]
+
 [[package]]
 name = "owo-colors"
 version = "3.5.0"
@@ -439,20 +462,30 @@ version = "0.2.17"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
 
+[[package]]
+name = "proc-macro-crate"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97dc5fea232fc28d2f597b37c4876b348a40e33f3b02cc975c8d006d78d94b1a"
+dependencies = [
+ "toml_datetime",
+ "toml_edit",
+]
+
 [[package]]
 name = "proc-macro2"
-version = "1.0.71"
+version = "1.0.75"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "75cb1540fadbd5b8fbccc4dddad2734eba435053f725621c070711a14bb5f4b8"
+checksum = "907a61bd0f64c2f29cd1cf1dc34d05176426a3f504a78010f08416ddb7b13708"
 dependencies = [
  "unicode-ident",
 ]
 
 [[package]]
 name = "quote"
-version = "1.0.33"
+version = "1.0.35"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae"
+checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef"
 dependencies = [
  "proc-macro2",
 ]
@@ -558,7 +591,18 @@ checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.42",
+ "syn 2.0.48",
+]
+
+[[package]]
+name = "serde_repr"
+version = "0.1.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b2e6b945e9d3df726b65d6ee24060aff8e3533d431f677a9695db04eff9dfdb"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.48",
 ]
 
 [[package]]
@@ -605,7 +649,7 @@ dependencies = [
  "proc-macro2",
  "quote",
  "rustversion",
- "syn 2.0.42",
+ "syn 2.0.48",
 ]
 
 [[package]]
@@ -621,9 +665,9 @@ dependencies = [
 
 [[package]]
 name = "syn"
-version = "2.0.42"
+version = "2.0.48"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5b7d0a2c048d661a1a59fcd7355baa232f7ed34e0ee4df2eef3c1c1c0d3852d8"
+checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -639,6 +683,23 @@ dependencies = [
  "winapi-util",
 ]
 
+[[package]]
+name = "toml_datetime"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b"
+
+[[package]]
+name = "toml_edit"
+version = "0.20.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338"
+dependencies = [
+ "indexmap",
+ "toml_datetime",
+ "winnow",
+]
+
 [[package]]
 name = "unicode-ident"
 version = "1.0.12"
@@ -825,3 +886,12 @@ name = "windows_x86_64_msvc"
 version = "0.52.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04"
+
+[[package]]
+name = "winnow"
+version = "0.5.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8434aeec7b290e8da5c3f0d628cb0eac6cabcb31d14bb74f779a08109a5914d6"
+dependencies = [
+ "memchr",
+]

+ 2 - 0
Cargo.toml

@@ -22,8 +22,10 @@ env_logger = "0.10.1"
 log = "0.4.20"
 modular-bitfield = "0.11.2"
 num = "0.4.1"
+num_enum = "0.7.1"
 rand = "0.8.5"
 serde = { version = "1.0.193", features = ["derive"] }
+serde_repr = "0.1.18"
 serde_yaml = "0.9.29"
 strum = { version = "0.25.0", features = ["derive"] }
 

+ 11 - 6
README.md

@@ -38,8 +38,11 @@ Ops:
     Speed: 1.234
     Power: 10.5
     Frequency: 10000
+    PulseWidth: <PulseWidth>
 ```
 
+Where `<PulseWidth>` can be 2/4/6/8/12/20/30/45/60/80/100/150/200/250/350/500.
+
 Cloning pen(s) (and optionally override settings):
 
 ``` yaml
@@ -50,12 +53,14 @@ Ops:
 
     # Specify one or more of the following:
     Inclusive: true
-    Color: [64, 64, 64]
-    Enabled: true
-    LoopCount: 3
-    Speed: 1.234
-    Power: 10.5
-    Frequency: 10000
+    
+    Patch:
+      Color: [64, 64, 64]
+      Enabled: true
+      LoopCount: 3
+      Speed: 1.234
+      Power: 10.5
+      Frequency: 10000
 ```
 
 Incrementing a specific field by some value across a sequence of pens:

+ 52 - 95
imhex.txt

@@ -36,9 +36,7 @@ struct FieldOf<T> {
     u32 length;
     T value;
     
-    if (length != sizeof(value)) {
-        std::error(std::format("Field size mismatch at 0x{:02X}", $));
-    }
+    std::assert(length == sizeof(value), "Size mismatch");
 } [[sealed, format("format_field_of")]];
 
 fn format_field_of(FieldOf<u8> f) {
@@ -54,6 +52,7 @@ struct Field {
         (8): double value;
         (2): u16 value; // ??
         (16): Point value; // ??
+        (1): u32 value; // ??
         (_): {
             std::warning(std::format("Unknown length tag: {} at 0x{:02X}", length, $));
             u8 value[length];
@@ -114,6 +113,7 @@ struct Pen {
     Field unknown_7[12];
     Field wobble_diameter2; // Only with wobble type ellipse
     Field unknown_8[26];
+    Field unknown_9[134];
 };
 
 bitfield ObjectFlags {
@@ -122,36 +122,31 @@ bitfield ObjectFlags {
     padding : 14;
 };
 
-enum ObjectType: u16 {
+enum ObjectType: u32 {
     Curve = 1,
     Point = 2,
     Rectangle = 3,
     Circle = 4,
     Ellipse = 5,
     Polygon = 6,
-    HatchLine = 16,
     Hatch = 32,
 };
 
 struct ObjCommon {
     u32 num_fields; // Number of fields in this struct (17)
     Field pen;
-    //Field type; // Seems to always be same value as object type
-    FieldOf<ObjectType>;
+    Field type; // Seems to always be same value as object type
     FieldOf<ObjectFlags>;
     String name;
     Field count;
-    Field unknown_2;
+    Field unknown_2[1];
     Field io_control_enable_mask;
     Field io_control_disable_mask;
     Field unknown_3[5];
     FieldOf<Point> origin;
     Field z;
     Field a;
-    Field index; // Increments for each hatch line, -1 for certain hatch patterns
-    if (index.value != 0) {
-        std::warning(std::format("Unexpected unknown value {} at 0x{:02X}", index.value, $));
-    }
+    Field unknown_4[1];
 };
 
 enum LineType : u16 {
@@ -198,9 +193,7 @@ struct Rectangle : ObjCommon {
     Field round_bottom_right;
     Field round_top_right;
     Field round_top_left;
-    Field unknown;
-    u32 length;
-    u8 bytes[length];
+    Field unknown[2];
 };
 
 struct Circle : ObjCommon {
@@ -244,79 +237,47 @@ struct HatchLine {
 using Object;
 
 bitfield HatchFlags {
-    all_calc : 1; // Will always be set when background_pattern is set
-    follow_edge_once : 1;
-    continuous_pattern : 1; // Hatch type 4 when combined with bidirectional_pattern
-    bidirectional_pattern : 1; // Hatch type 2
-    ring_pattern : 1; // Hatch type 3
     padding : 1;
+    follow_edge_once : 1;
+    padding : 4;
     auto_rotate_hatch_angle : 1;
     average_distribut_line : 1;
-    padding : 1;
-    gong_pattern : 1; // Hatch type 5 when combined with bidirectional_pattern
+    padding : 2;
     cross_hatch : 1;
-    background_pattern: 1; // Hatch type 6 (TODO, missing parsing)
-    fill_pattern : 1; // Hatch type 7 when combined with continuous_pattern and bidirectional_pattern
-    zigzag_pattern : 1; // Hatch type 8
-    padding : 18;
+    padding : 21;
 };
 
-// TODO: hatch types 1/2 will set last field of ObjCommon in HatchLines to 1
-// TODO: hatch types 3/4/5/7/8 will set last field of ObjCommon in HatchLines to -1 (0xFFFFFFFF)
-
 struct HatchSettings {
-    u32 num_fields; // 46
-    Field mark_contour;
-    Field hatch_1_enable;
-    Field hatch_1_pen;
-    FieldOf<HatchFlags> hatch_1_flags;
-    Field hatch_1_edge_offset;
-    Field hatch_1_line_spacing;
-    Field hatch_1_start_offset;
-    Field hatch_1_end_offset;
-    Field hatch_1_angle;
-    Field hatch_2_enable;
-    Field hatch_2_pen;
-    FieldOf<HatchFlags> hatch_2_flags;
-    Field hatch_2_edge_offset;
-    Field hatch_2_line_space;
-    Field hatch_2_start_offset;
-    Field hatch_2_end_offset;
-    Field hatch_2_angle;
-    Field any_hatch_enabled; 
-    Field hatch_1_auto_rotate_angle;
-    Field hatch_2_auto_rotate_angle;
-    Field hatch_3_enable;
-    Field hatch_3_pen;
-    FieldOf<HatchFlags> hatch_3_flags;
-    Field hatch_3_edge_offset;
-    Field hatch_3_line_space;
-    Field hatch_3_start_offset;
-    Field hatch_3_end_offset;
-    Field hatch_3_angle;
-    Field hatch_3_auto_rotate_angle;
-    Field hatch_1_line_reduction;
-    Field hatch_2_line_reduction;
-    Field hatch_3_line_reduction;
-    Field hatch_1_num_loops;
-    Field hatch_2_num_loops;
-    Field hatch_3_num_loops;
-    Field hatch_1_loop_distance;
-    Field hatch_2_loop_distance;
-    Field hatch_3_loop_distance;
-    Field unknown_1[3];
-    Field contour_priority;
-    Field hatch_1_count;
-    Field hatch_2_count;
-    Field hatch_3_count;
+    u32 num_fields; // 47
+    Field flags; // 1 = mark contour
+    Field unknown_1[1];
+    Field pen;
+    FieldOf<HatchFlags> flags_2;
+    Field edge_offset;
+    Field line_spacing;
+    Field start_offset;
+    Field end_offset;
+    Field angle;
+    Field unknown_4[9];
+    Field auto_rotate_angle;
+    Field unknown_7[10];
+    Field line_reduction;
+    Field unknown_3[2];
+    Field num_loops;
+    Field unknown_5[2];
+    Field loop_distance;
+    Field unknown_6[5];
+    Field contour_first;
+    Field count;
+    Field unknown_2[4];
 };
 
 struct HatchSettings2 {
     u32 num_fields; // 15
     Field count;
-    Field enabled;
+    Field unknown_1[1];
     Field pen;
-    FieldOf<HatchFlags> flags;
+    FieldOf<HatchFlags> flags_2; // Same as HatchSettings.flags_2
     Field edge_offset;
     Field line_spacing;
     Field start_offset;
@@ -326,35 +287,31 @@ struct HatchSettings2 {
     Field line_reduction;
     Field loop_distance;
     Field num_loops;
-    Field unknown_1; // Mirror unknown_1 in settings
-    Field unknown_2;
-};
-
-struct HatchLines {
-    u32; // 16 (tag?)
-    ObjCommon;
-    u32 num_lines;
-    HatchLine lines2[num_lines];
+    Field unknown_5[2];
 };
 
 struct Hatch : ObjCommon {
-    u32 num_outlines;
-    Object outline[num_outlines];
+    u32; // 1
+    Object outline;
     HatchSettings settings;
-    Field num_settings;
-    HatchSettings2 settings_2[num_settings.value];
+    HatchSettings2 settings_2;
+    u32 count_5; //17
+    Field unknown_5[count_5];
+    u32; // 1
+    u32; // 16
+    u32 count_6; // 17
+    Field pen;
+    Field unknown_6[2];
+    String hatch_name;
+    Field unknown_7[count_6 - 4];
+    //Field unknown_6[count_6];
     
-    // Following fields are not defined if hatch is disabled
-    if (settings.any_hatch_enabled.value == 1) {
-        ObjCommon;
-        u32 num_hatches;
-        HatchLines hatches[num_hatches];
-    }
+    u32 num_lines;
+    HatchLine lines[num_lines];    
 };
 
 struct Object {
     ObjectType type;
-    u16; // zero
     
     match (type) {
         (ObjectType::Curve): LineSet;

+ 365 - 364
src/config/object.rs

@@ -1,364 +1,365 @@
-use std::path::PathBuf;
-
-use ezcad::{
-    array_of::ArrayOf,
-    layer::Layer,
-    objects::{
-        circle::Circle,
-        hatch::{Hatch, HatchFlag, HatchPattern, HatchSetting, Hatches},
-        rectangle::Rectangle,
-        Object, ObjectCore, Translate,
-    },
-    pen::Pen,
-    types::{Coordinate, ObjectType},
-};
-use log::{debug, error, warn};
-use rand::{seq::SliceRandom, thread_rng};
-use serde::{Deserialize, Serialize};
-
-use super::pen::PatternField;
-
-#[derive(Debug, Serialize, Deserialize)]
-#[serde(rename_all = "PascalCase")]
-pub struct DeleteObjects {
-    layer: usize,
-    object: Option<usize>,
-}
-
-impl DeleteObjects {
-    pub fn delete(&self, layers: &mut ArrayOf<Layer>) {
-        debug!(
-            "Deleting layer #{} {}",
-            match &self.object {
-                Some(object) => format!("object #{}", object),
-                None => format!("all objects"),
-            },
-            self.layer,
-        );
-
-        let layer: &mut Layer = layers.get_mut(self.layer).expect("Invalid layer index");
-
-        match self.object {
-            Some(index) => {
-                assert!(index < layer.objects.len(), "Invalid object index");
-                layer.objects.remove(index);
-            }
-            None => layer.objects.clear(),
-        }
-    }
-}
-
-#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
-#[serde(rename_all = "PascalCase")]
-pub struct HatchConfig {
-    line_spacing: f64,
-
-    pen: Option<u32>,
-    count: Option<u32>,
-    edge_offset: Option<f64>,
-    start_offset: Option<f64>,
-    end_offset: Option<f64>,
-    angle: Option<f64>,
-    rotate_angle: Option<f64>,
-    line_reduction: Option<f64>,
-    loop_distance: Option<f64>,
-    loop_count: Option<u32>,
-
-    pattern: Option<HatchPattern>,
-    follow_edge_once: Option<bool>,
-    cross_hatch: Option<bool>,
-}
-
-impl From<HatchConfig> for HatchSetting {
-    fn from(value: HatchConfig) -> Self {
-        let mut flags: HatchFlag = match value.pattern {
-            Some(pattern) => pattern.into(),
-            None => HatchPattern::Directional.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.pen.map(|x| *ret.pen = x.into());
-        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();
-
-        ret
-    }
-}
-
-#[derive(Debug, Serialize, Deserialize)]
-#[serde(rename_all = "PascalCase")]
-pub struct ArrayConfig {
-    columns: usize,
-    rows: usize,
-    spacing: f64,
-    randomize_order: bool,
-    starting_pen: usize,
-    pattern_x: PatternField,
-    pattern_y: Option<PatternField>,
-}
-
-#[derive(Debug, Serialize, Deserialize, strum::Display)]
-#[serde(rename_all = "PascalCase")]
-pub enum InputObject {
-    #[serde(rename_all = "PascalCase")]
-    Rectangle {
-        width: f64,
-        height: f64,
-        round_corner: Option<f64>,
-    },
-    #[serde(rename_all = "PascalCase")]
-    Circle { radius: f64 },
-    #[serde(rename_all = "PascalCase")]
-    Import { path: PathBuf },
-    #[serde(rename_all = "PascalCase")]
-    Existing { layer: usize, object: usize },
-}
-
-impl InputObject {
-    fn new(&self, layers: &ArrayOf<Layer>) -> Object {
-        match self {
-            InputObject::Rectangle {
-                width,
-                height,
-                round_corner,
-            } => Object::Rectangle(Rectangle {
-                drawn_corner_a: Coordinate::from((-width / 2.0, -height / 2.0)).into(),
-                drawn_corner_b: Coordinate::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()
-            }
-        }
-    }
-}
-
-#[derive(Debug, Serialize, Deserialize)]
-#[serde(rename_all = "PascalCase")]
-pub struct ObjectOperation {
-    input: InputObject,
-    z: Option<f64>,
-    origin: Option<Coordinate>,
-    pen: Option<u32>,
-    layer: Option<usize>,
-    array: Option<ArrayConfig>,
-    hatch: Option<HatchConfig>,
-    export: Option<PathBuf>,
-    replace_object: Option<usize>,
-}
-
-impl ObjectOperation {
-    pub fn process(&self, pens: &mut Vec<Pen>, layers: &mut ArrayOf<Layer>) {
-        debug!("Begin processing of object {:?}", self.input);
-
-        let mut object: Object = self.input.new(layers);
-
-        // Process basic transformation
-        if self.origin.is_some() || self.z.is_some() {
-            debug!(
-                "Translating object to origin {:?} and Z {:?}",
-                self.origin, self.z
-            );
-            object.move_absolute(self.origin, self.z);
-        }
-
-        self.pen.map(|pen| {
-            if self.array.is_some() {
-                warn!("Ignoring pen setting as values will be overridden by array setting");
-            } else {
-                assert!(pen < pens.len().try_into().unwrap(), "Invalid pen index");
-                debug!("Setting object pen to #{}", pen);
-                object.set_pen(pen);
-            }
-        });
-
-        // Process conversion to hatch object
-        let object = self.hatch.as_ref().map_or(object.clone(), |hatch| {
-            if hatch.pen.is_some() && self.array.is_some() {
-                warn!("Ignoring hatch pen setting as values will be overridden by array setting");
-            }
-            let hatch_setting: HatchSetting = (*hatch).into();
-
-            match object {
-                Object::Curve(_) | Object::Point(_) => {
-                    error!("Points, lines, and curves cannot be hatched");
-                    object
-                }
-                Object::Rectangle(_)
-                | Object::Circle(_)
-                | Object::Ellipse(_)
-                | Object::Polygon(_) => {
-                    debug!("Converting to hatch object");
-                    // Build new hatch object (no hatch lines)
-                    Object::Hatch(Hatch {
-                        core: ObjectCore {
-                            origin: object.core().origin,
-                            z: object.core().z,
-                            ..ObjectCore::default(ObjectType::Hatch)
-                        },
-                        outline: vec![object].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(),
-                        }),
-                    })
-                }
-                Object::Hatch(hatch) => {
-                    debug!("Modifying existing hatch settings");
-                    // Modify existing hatch (delete existing hatch lines)
-                    Object::Hatch(Hatch {
-                        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(),
-                        }),
-                        ..hatch
-                    })
-                }
-            }
-        });
-
-        // Process array generation of object
-        let new_objects = self.array.as_ref().map_or(vec![object.clone()], |array| {
-            let mut new_obj: Vec<Object> = vec![];
-
-            let bottom_left: Coordinate = Coordinate {
-                x: (array.columns - 1) as f64 * array.spacing / -2.0,
-                y: (array.rows - 1) as f64 * array.spacing / -2.0,
-            };
-
-            // Generate objects
-            for y in 0..array.rows {
-                for x in 0..array.columns {
-                    let delta: Coordinate = bottom_left
-                        + Coordinate {
-                            x: array.spacing * x as f64,
-                            y: array.spacing * y as f64,
-                        };
-
-                    let pen: usize = array.starting_pen + y * array.columns + x;
-
-                    let mut object: Object = object.clone();
-                    object.move_relative(Some(delta), None);
-                    object.set_pen(pen.try_into().unwrap());
-
-                    debug!(
-                        "Adding new object at {} with pen #{}",
-                        object.core().origin,
-                        pen
-                    );
-                    new_obj.push(object);
-                }
-            }
-
-            // Generate pens
-            match &array.pattern_y {
-                None => {
-                    array.pattern_x.pattern(
-                        &mut pens
-                            .iter_mut()
-                            .enumerate()
-                            .skip(array.starting_pen)
-                            .take(array.columns * array.rows),
-                    );
-                }
-                Some(pattern_y) => {
-                    pattern_y.pattern(
-                        &mut pens
-                            .iter_mut()
-                            .enumerate()
-                            .skip(array.starting_pen)
-                            .step_by(array.columns)
-                            .take(array.rows),
-                    );
-                    for y in 0..array.rows {
-                        array.pattern_x.pattern(
-                            &mut pens
-                                .iter_mut()
-                                .enumerate()
-                                .skip(array.starting_pen)
-                                .skip(y * array.columns)
-                                .take(array.columns),
-                        )
-                    }
-                }
-            }
-
-            if array.randomize_order {
-                debug!("Randomizing draw order of array objects");
-                new_obj.shuffle(&mut thread_rng());
-            }
-
-            new_obj
-        });
-
-        let layer_id: usize = self.layer.unwrap_or(0);
-        let layer: &mut Layer = layers.get_mut(layer_id).expect("Invalid layer index");
-
-        // Either export the object, replace an existing object, or append if neither
-        if let Some(path) = &self.export {
-            if new_objects.len() > 1 {
-                warn!(
-                    "Exporting only object #0 from layer #{} in list of objects",
-                    layer_id
-                );
-            } else {
-                debug!(
-                    "Exporting object {} in layer #{} to '{}'",
-                    new_objects[0],
-                    layer_id,
-                    path.to_string_lossy()
-                );
-            }
-            new_objects[0].write_to_file(path);
-        } else if let Some(object) = self.replace_object {
-            assert!(object < layer.objects.len(), "Invalid object index");
-            debug!(
-                "Replacing object #{} in layer #{} with new objects",
-                object, layer_id
-            );
-            layer.objects.splice(object..=object, new_objects);
-        } else {
-            debug!("Extending layer #{} with new objects", layer_id);
-            layer.objects.extend(new_objects);
-        }
-    }
-}
+use std::path::PathBuf;
+
+use ezcad::{
+    array_of::ArrayOf,
+    layer::Layer,
+    objects::{
+        circle::Circle,
+        hatch::{Hatch, HatchFlag, HatchPattern, HatchSetting, Hatches},
+        rectangle::Rectangle,
+        Object, ObjectCore, Translate,
+    },
+    pen::Pen,
+    types::{Coordinate, ObjectType},
+};
+use log::{debug, error, warn};
+use rand::{seq::SliceRandom, thread_rng};
+use serde::{Deserialize, Serialize};
+
+use super::pen::PatternField;
+
+#[derive(Debug, Serialize, Deserialize)]
+#[serde(rename_all = "PascalCase")]
+pub struct DeleteObjects {
+    layer: usize,
+    object: Option<usize>,
+}
+
+impl DeleteObjects {
+    pub fn delete(&self, layers: &mut ArrayOf<Layer>) {
+        debug!(
+            "Deleting layer #{} {}",
+            match &self.object {
+                Some(object) => format!("object #{}", object),
+                None => format!("all objects"),
+            },
+            self.layer,
+        );
+
+        let layer: &mut Layer = layers.get_mut(self.layer).expect("Invalid layer index");
+
+        match self.object {
+            Some(index) => {
+                assert!(index < layer.objects.len(), "Invalid object index");
+                layer.objects.remove(index);
+            }
+            None => layer.objects.clear(),
+        }
+    }
+}
+
+#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
+#[serde(rename_all = "PascalCase")]
+pub struct HatchConfig {
+    line_spacing: f64,
+
+    pen: Option<u32>,
+    count: Option<u32>,
+    edge_offset: Option<f64>,
+    start_offset: Option<f64>,
+    end_offset: Option<f64>,
+    angle: Option<f64>,
+    rotate_angle: Option<f64>,
+    line_reduction: Option<f64>,
+    loop_distance: Option<f64>,
+    loop_count: Option<u32>,
+
+    pattern: Option<HatchPattern>,
+    follow_edge_once: Option<bool>,
+    cross_hatch: Option<bool>,
+}
+
+impl From<HatchConfig> for HatchSetting {
+    fn from(value: HatchConfig) -> Self {
+        let mut flags: HatchFlag = match value.pattern {
+            Some(pattern) => pattern.into(),
+            None => HatchPattern::Directional.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.pen.map(|x| *ret.pen = x.into());
+        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();
+
+        ret
+    }
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+#[serde(rename_all = "PascalCase")]
+pub struct ArrayConfig {
+    columns: usize,
+    rows: usize,
+    spacing: f64,
+    randomize_order: bool,
+    starting_pen: usize,
+    pattern_x: PatternField,
+    pattern_y: Option<PatternField>,
+}
+
+#[derive(Debug, Serialize, Deserialize, strum::Display)]
+#[serde(rename_all = "PascalCase")]
+pub enum InputObject {
+    #[serde(rename_all = "PascalCase")]
+    Rectangle {
+        width: f64,
+        height: f64,
+        round_corner: Option<f64>,
+    },
+    #[serde(rename_all = "PascalCase")]
+    Circle { radius: f64 },
+    #[serde(rename_all = "PascalCase")]
+    Import { path: PathBuf },
+    #[serde(rename_all = "PascalCase")]
+    Existing { layer: usize, object: usize },
+}
+
+impl InputObject {
+    fn new(&self, layers: &ArrayOf<Layer>) -> Object {
+        match self {
+            InputObject::Rectangle {
+                width,
+                height,
+                round_corner,
+            } => Object::Rectangle(Rectangle {
+                drawn_corner_a: Coordinate::from((-width / 2.0, -height / 2.0)).into(),
+                drawn_corner_b: Coordinate::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()
+            }
+        }
+    }
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+#[serde(rename_all = "PascalCase")]
+pub struct ObjectOperation {
+    input: InputObject,
+    z: Option<f64>,
+    origin: Option<Coordinate>,
+    pen: Option<u32>,
+    layer: Option<usize>,
+    array: Option<ArrayConfig>,
+    hatch: Option<HatchConfig>,
+    export: Option<PathBuf>,
+    replace_object: Option<usize>,
+}
+
+impl ObjectOperation {
+    pub fn process(&self, pens: &mut Vec<Pen>, layers: &mut ArrayOf<Layer>) {
+        debug!("Begin processing of object {:?}", self.input);
+
+        let mut object: Object = self.input.new(layers);
+
+        // Process basic transformation
+        if self.origin.is_some() || self.z.is_some() {
+            debug!(
+                "Translating object to origin {:?} and Z {:?}",
+                self.origin, self.z
+            );
+            object.move_absolute(self.origin, self.z);
+        }
+
+        self.pen.map(|pen| {
+            if self.array.is_some() {
+                warn!("Ignoring pen setting as values will be overridden by array setting");
+            } else {
+                assert!(pen < pens.len().try_into().unwrap(), "Invalid pen index");
+                debug!("Setting object pen to #{}", pen);
+                object.set_pen(pen);
+            }
+        });
+
+        // Process conversion to hatch object
+        let object = self.hatch.as_ref().map_or(object.clone(), |hatch| {
+            if hatch.pen.is_some() && self.array.is_some() {
+                warn!("Ignoring hatch pen setting as values will be overridden by array setting");
+            }
+            let hatch_setting: HatchSetting = (*hatch).into();
+
+            match object {
+                Object::Curve(_) | Object::Point(_) => {
+                    error!("Points, lines, and curves cannot be hatched");
+                    object
+                }
+                Object::Rectangle(_)
+                | Object::Circle(_)
+                | Object::Ellipse(_)
+                | Object::Polygon(_) => {
+                    debug!("Converting to hatch object");
+                    // Build new hatch object (no hatch lines)
+                    Object::Hatch(Hatch {
+                        core: ObjectCore {
+                            origin: object.core().origin,
+                            z: object.core().z,
+                            ..ObjectCore::default(ObjectType::Hatch)
+                        },
+                        outline: vec![object].into(),
+                        legacy_setting: vec![hatch_setting.clone()].into(),
+                        _unknown_1: vec![0; 960].into(),
+                        hatch_settings: vec![hatch_setting.clone()].into(),
+                        hatches: Some(Hatches {
+                            core: ObjectCore::default(ObjectType::HatchLine),
+                            hatch_lines: vec![].into(),
+                        }),
+                    })
+                }
+                Object::Hatch(hatch) => {
+                    debug!("Modifying existing hatch settings");
+                    // Modify existing hatch (delete existing hatch lines)
+                    Object::Hatch(Hatch {
+                        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(),
+                        }),
+                        ..hatch
+                    })
+                }
+            }
+        });
+
+        // Process array generation of object
+        let new_objects = self.array.as_ref().map_or(vec![object.clone()], |array| {
+            let mut new_obj: Vec<Object> = vec![];
+
+            let bottom_left: Coordinate = Coordinate {
+                x: (array.columns - 1) as f64 * array.spacing / -2.0,
+                y: (array.rows - 1) as f64 * array.spacing / -2.0,
+            };
+
+            // Generate objects
+            for y in 0..array.rows {
+                for x in 0..array.columns {
+                    let delta: Coordinate = bottom_left
+                        + Coordinate {
+                            x: array.spacing * x as f64,
+                            y: array.spacing * y as f64,
+                        };
+
+                    let pen: usize = array.starting_pen + y * array.columns + x;
+
+                    let mut object: Object = object.clone();
+                    object.move_relative(Some(delta), None);
+                    object.set_pen(pen.try_into().unwrap());
+
+                    debug!(
+                        "Adding new object at {} with pen #{}",
+                        object.core().origin,
+                        pen
+                    );
+                    new_obj.push(object);
+                }
+            }
+
+            // Generate pens
+            match &array.pattern_y {
+                None => {
+                    array.pattern_x.pattern(
+                        &mut pens
+                            .iter_mut()
+                            .enumerate()
+                            .skip(array.starting_pen)
+                            .take(array.columns * array.rows),
+                    );
+                }
+                Some(pattern_y) => {
+                    pattern_y.pattern(
+                        &mut pens
+                            .iter_mut()
+                            .enumerate()
+                            .skip(array.starting_pen)
+                            .step_by(array.columns)
+                            .take(array.rows),
+                    );
+                    for y in 0..array.rows {
+                        array.pattern_x.pattern(
+                            &mut pens
+                                .iter_mut()
+                                .enumerate()
+                                .skip(array.starting_pen)
+                                .skip(y * array.columns)
+                                .take(array.columns),
+                        )
+                    }
+                }
+            }
+
+            if array.randomize_order {
+                debug!("Randomizing draw order of array objects");
+                new_obj.shuffle(&mut thread_rng());
+            }
+
+            new_obj
+        });
+
+        let layer_id: usize = self.layer.unwrap_or(0);
+        let layer: &mut Layer = layers.get_mut(layer_id).expect("Invalid layer index");
+
+        // Either export the object, replace an existing object, or append if neither
+        if let Some(path) = &self.export {
+            if new_objects.len() > 1 {
+                warn!(
+                    "Exporting only object #0 from layer #{} in list of objects",
+                    layer_id
+                );
+            } else {
+                debug!(
+                    "Exporting object {} in layer #{} to '{}'",
+                    new_objects[0],
+                    layer_id,
+                    path.to_string_lossy()
+                );
+            }
+            new_objects[0].write_to_file(path);
+        } else if let Some(object) = self.replace_object {
+            assert!(object < layer.objects.len(), "Invalid object index");
+            debug!(
+                "Replacing object #{} in layer #{} with new objects",
+                object, layer_id
+            );
+            layer.objects.splice(object..=object, new_objects);
+        } else {
+            debug!("Extending layer #{} with new objects", layer_id);
+            layer.objects.extend(new_objects);
+        }
+    }
+}

+ 20 - 7
src/config/pen.rs

@@ -1,6 +1,9 @@
 use std::path::PathBuf;
 
-use ezcad::{pen::Pen, types::Rgba};
+use ezcad::{
+    pen::Pen,
+    types::{PulseWidth, Rgba},
+};
 use log::debug;
 use serde::{Deserialize, Serialize};
 
@@ -8,8 +11,8 @@ const SPEED_MIN: f64 = 0.0;
 const SPEED_MAX: f64 = 100000.0;
 const POWER_MIN: f64 = 0.0;
 const POWER_MAX: f64 = 100.0;
-const FREQUENCY_MIN: u32 = 1000;
-const FREQUENCY_MAX: u32 = 999000;
+const FREQUENCY_MIN: u32 = 20_000;
+const FREQUENCY_MAX: u32 = 4_000_000;
 
 #[derive(Debug, Serialize, Deserialize)]
 #[serde(rename_all = "PascalCase")]
@@ -20,6 +23,7 @@ pub struct Patch {
     speed: Option<f64>,
     power: Option<f64>,
     frequency: Option<u32>,
+    pulse_width: Option<PulseWidth>,
 }
 
 impl Patch {
@@ -41,7 +45,7 @@ impl Patch {
         });
 
         self.speed.map(|speed| {
-            debug!("Patching pen speed {}", speed);
+            debug!("Patching pen speed to {}", speed);
             assert!(
                 speed > SPEED_MIN && speed <= SPEED_MAX,
                 "Pen speed must be between {} and {}",
@@ -52,7 +56,7 @@ impl Patch {
         });
 
         self.power.map(|power| {
-            debug!("Patching pen power {}", power);
+            debug!("Patching pen power to {}", power);
             assert!(
                 power > POWER_MIN && power <= POWER_MAX,
                 "Pen power must be between {} and {}",
@@ -63,7 +67,7 @@ impl Patch {
         });
 
         self.frequency.map(|frequency| {
-            debug!("Patching pen frequency {}", frequency);
+            debug!("Patching pen frequency to {}", frequency);
             assert!(
                 frequency >= FREQUENCY_MIN && frequency <= FREQUENCY_MAX,
                 "Pen frequency must be between {} and {}",
@@ -74,6 +78,13 @@ impl Patch {
             *pen.frequency_2 = frequency.try_into().unwrap();
         });
 
+        self.pulse_width.map(|width| {
+            let width: u32 = width.into();
+            debug!("Patching pen pulse width to {}ns", width);
+            *pen.pulse_width = width.into();
+            *pen.pulse_width_2 = width.try_into().unwrap();
+        });
+
         // Always enable custom settings for pen
         *pen.use_default = 0;
     }
@@ -101,7 +112,6 @@ pub struct ClonePen {
     from: usize,
     to: usize,
     inclusive: Option<bool>,
-    #[serde(flatten)]
     patch: Option<Patch>,
 }
 
@@ -249,6 +259,9 @@ impl PatternField {
                 }
             }
 
+            // Randomize pen color
+            *dst.color = Rgba::random().into();
+
             // Always enable custom settings for pen
             *dst.use_default = 0;
         }

+ 57 - 57
src/ezcad/layer.rs

@@ -1,57 +1,57 @@
-use binrw::{BinRead, BinWrite};
-use diff::Diff;
-
-use crate::{
-    array_of::ArrayOf,
-    objects::Object,
-    types::{Field, WString, F64, U16, U32},
-};
-
-#[derive(BinRead, BinWrite, Debug, Diff, PartialEq)]
-#[diff(attr(
-    #[derive(Debug, PartialEq)]
-))]
-#[brw(magic(17u32))] // Number of fields within this struct
-pub struct LayerSettings1 {
-    _unknown_1: [Field; 3],
-    pub name: WString,
-    pub count: U32,
-    _unknown_2: Field,
-    pub input_signal_wait_enable_mask: U16,
-    pub input_signal_wait_disable_mask: U16,
-    _unknown_3: [Field; 9],
-}
-
-#[derive(BinRead, BinWrite, Debug, Diff, PartialEq)]
-#[diff(attr(
-    #[derive(Debug, PartialEq)]
-))]
-#[brw(magic(151u32))] // Number of fields within this struct
-pub struct LayerSettings2 {
-    _unknown_4: [Field; 54],
-    pub output_signal_start_enable_mask: U16,
-    pub output_signal_start_disable_mask: U16,
-    pub output_signal_end_enable_mask: U16,
-    pub output_signal_end_disable_mask: U16,
-    _unknown_5: [Field; 9],
-    pub fixture_offset: F64,
-    _unknown_6: [Field; 64],
-    pub output_signal_start_delay: U32,
-    pub output_signal_end_delay: U32,
-    pub input_signal_wait_enable: U32,
-    _unknown_7: [Field; 2],
-    pub output_signal_start_pulse: U32,
-    pub output_signal_end_pulse: U32,
-    _unknown_8: [Field; 12],
-    footer: u32,
-}
-
-#[derive(BinRead, BinWrite, Debug, Diff, PartialEq)]
-#[diff(attr(
-    #[derive(Debug, PartialEq)]
-))]
-pub struct Layer {
-    pub pre: LayerSettings1,
-    pub objects: ArrayOf<Object>,
-    pub post: LayerSettings2,
-}
+use binrw::{BinRead, BinWrite};
+use diff::Diff;
+
+use crate::{
+    array_of::ArrayOf,
+    objects::Object,
+    types::{Field, WString, F64, U16, U32},
+};
+
+#[derive(BinRead, BinWrite, Debug, Diff, PartialEq)]
+#[diff(attr(
+    #[derive(Debug, PartialEq)]
+))]
+#[brw(magic(17u32))] // Number of fields within this struct
+pub struct LayerSettings1 {
+    _unknown_1: [Field; 3],
+    pub name: WString,
+    pub count: U32,
+    _unknown_2: Field,
+    pub input_signal_wait_enable_mask: U16,
+    pub input_signal_wait_disable_mask: U16,
+    _unknown_3: [Field; 9],
+}
+
+#[derive(BinRead, BinWrite, Debug, Diff, PartialEq)]
+#[diff(attr(
+    #[derive(Debug, PartialEq)]
+))]
+#[brw(magic(152u32))] // Number of fields within this struct
+pub struct LayerSettings2 {
+    _unknown_4: [Field; 54],
+    pub output_signal_start_enable_mask: U16,
+    pub output_signal_start_disable_mask: U16,
+    pub output_signal_end_enable_mask: U16,
+    pub output_signal_end_disable_mask: U16,
+    _unknown_5: [Field; 9],
+    pub fixture_offset: F64,
+    _unknown_6: [Field; 64],
+    pub output_signal_start_delay: U32,
+    pub output_signal_end_delay: U32,
+    pub input_signal_wait_enable: U32,
+    _unknown_7: [Field; 2],
+    pub output_signal_start_pulse: U32,
+    pub output_signal_end_pulse: U32,
+    _unknown_8: [Field; 13],
+    footer: u32,
+}
+
+#[derive(BinRead, BinWrite, Debug, Diff, PartialEq)]
+#[diff(attr(
+    #[derive(Debug, PartialEq)]
+))]
+pub struct Layer {
+    pub pre: LayerSettings1,
+    pub objects: ArrayOf<Object>,
+    pub post: LayerSettings2,
+}

+ 414 - 411
src/ezcad/objects/hatch.rs

@@ -1,411 +1,414 @@
-use std::fmt::Debug;
-
-use crate::{
-    array_of::ArrayOf,
-    field_of::FieldOf,
-    types::{Coordinate, Field, F64, U32},
-};
-use binrw::{binrw, BinRead, BinWrite};
-use diff::Diff;
-use modular_bitfield::{
-    bitfield,
-    specifiers::{B1, B18},
-};
-use serde::{Deserialize, Serialize};
-
-use super::{line::Lines, Object, ObjectCore, Translate};
-
-#[bitfield(bits = 32)]
-#[derive(BinRead, BinWrite, Copy, Clone, Debug, Default, Diff, PartialEq)]
-#[diff(attr(
-    #[derive(Debug, PartialEq)]
-))]
-#[br(map = Self::from_bytes)]
-#[bw(map = |&x| Self::into_bytes(x))]
-pub struct HatchFlag {
-    pub all_calc: B1,
-    pub follow_edge_once: B1,
-    pub continuous_pattern: B1, // Hatch type 3 when combined with bidirectional_pattern
-    pub bidirectional_pattern: B1, // Hatch type 1
-    pub ring_pattern: B1,       // Hatch type 2
-    #[skip]
-    __: B1,
-    pub auto_rotate_hatch_angle: B1,
-    pub average_distribute_line: B1,
-    #[skip]
-    __: B1,
-    pub gong_pattern: B1, // Hatch type 4 when combined with bidirectional_pattern
-    pub cross_hatch: B1,
-    pub background_pattern: B1, // Hatch type 5, must set all_calc as well
-    pub fill_pattern: B1, // Hatch type 6 when combined with continuous_pattern and bidirectional_pattern
-    pub zigzag_pattern: B1, // Hatch type 7
-    #[skip]
-    __: B18,
-}
-
-#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
-pub enum HatchPattern {
-    Directional,
-    Bidirectional,
-    Ring,
-    Continuous,
-    Gong,
-    Background,
-    Fill,
-    Zigzag,
-}
-
-impl From<HatchPattern> for HatchFlag {
-    fn from(value: HatchPattern) -> Self {
-        let mut ret = Self::new();
-        match value {
-            HatchPattern::Directional => (),
-            HatchPattern::Bidirectional => ret.set_bidirectional_pattern(1),
-            HatchPattern::Ring => ret.set_ring_pattern(1),
-            HatchPattern::Continuous => {
-                ret.set_bidirectional_pattern(1);
-                ret.set_continuous_pattern(1);
-            }
-            HatchPattern::Gong => {
-                ret.set_bidirectional_pattern(1);
-                ret.set_gong_pattern(1);
-            }
-            HatchPattern::Background => {
-                ret.set_background_pattern(1);
-                ret.set_all_calc(1);
-            }
-            HatchPattern::Fill => ret.set_fill_pattern(1),
-            HatchPattern::Zigzag => ret.set_zigzag_pattern(1),
-        }
-        ret
-    }
-}
-
-#[cfg_attr(feature = "default-debug", derive(Debug))]
-#[derive(BinRead, BinWrite, Clone, Diff, PartialEq)]
-#[diff(attr(
-    #[derive(Debug, PartialEq)]
-))]
-#[brw(magic(46u32))] // Number of fields in this struct
-pub struct LegacyHatchSetting {
-    pub mark_contour: U32,
-    pub hatch_0_enable: U32,
-    pub hatch_0_pen: U32,
-    pub hatch_0_flags: FieldOf<HatchFlag>,
-    pub hatch_0_edge_offset: F64,
-    pub hatch_0_line_spacing: F64,
-    pub hatch_0_start_offset: F64,
-    pub hatch_0_end_offset: F64,
-    pub hatch_0_angle: F64,
-    pub hatch_1_enable: U32,
-    pub hatch_1_pen: U32,
-    pub hatch_1_flags: FieldOf<HatchFlag>,
-    pub hatch_1_edge_offset: F64,
-    pub hatch_1_line_spacing: F64,
-    pub hatch_1_start_offset: F64,
-    pub hatch_1_end_offset: F64,
-    pub hatch_1_angle: F64,
-    pub any_hatch_enabled: U32,
-    pub hatch_0_rotate_angle: F64,
-    pub hatch_1_rotate_angle: F64,
-    pub hatch_2_enable: U32,
-    pub hatch_2_pen: U32,
-    pub hatch_2_flags: FieldOf<HatchFlag>,
-    pub hatch_2_edge_offset: F64,
-    pub hatch_2_line_spacing: F64,
-    pub hatch_2_start_offset: F64,
-    pub hatch_2_end_offset: F64,
-    pub hatch_2_angle: F64,
-    pub hatch_2_rotate_angle: F64,
-    pub hatch_0_line_reduction: F64,
-    pub hatch_1_line_reduction: F64,
-    pub hatch_2_line_reduction: F64,
-    pub hatch_0_loop_count: U32,
-    pub hatch_1_loop_count: U32,
-    pub hatch_2_loop_count: U32,
-    pub hatch_0_loop_distance: F64,
-    pub hatch_1_loop_distance: F64,
-    pub hatch_2_loop_distance: F64,
-    pub _unknown_1: [Field; 3],
-    pub contour_priority: U32,
-    pub hatch_0_count: U32,
-    pub hatch_1_count: U32,
-    pub hatch_2_count: U32,
-}
-
-// Custom Debug implementation to only print known fields
-#[cfg(not(feature = "default-debug"))]
-impl Debug for LegacyHatchSetting {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        f.debug_struct("LegacyHatchSetting")
-            .field("mark_contour", &self.mark_contour)
-            .field("hatch_0_enable", &self.hatch_0_enable)
-            .field("hatch_0_pen", &self.hatch_0_pen)
-            .field("hatch_0_flags", &self.hatch_0_flags)
-            .field("hatch_0_edge_offset", &self.hatch_0_edge_offset)
-            .field("hatch_0_line_spacing", &self.hatch_0_line_spacing)
-            .field("hatch_0_start_offset", &self.hatch_0_start_offset)
-            .field("hatch_0_end_offset", &self.hatch_0_end_offset)
-            .field("hatch_0_angle", &self.hatch_0_angle)
-            .field("hatch_1_enable", &self.hatch_1_enable)
-            .field("hatch_1_pen", &self.hatch_1_pen)
-            .field("hatch_1_flags", &self.hatch_1_flags)
-            .field("hatch_1_edge_offset", &self.hatch_1_edge_offset)
-            .field("hatch_1_line_spacing", &self.hatch_1_line_spacing)
-            .field("hatch_1_start_offset", &self.hatch_1_start_offset)
-            .field("hatch_1_end_offset", &self.hatch_1_end_offset)
-            .field("hatch_1_angle", &self.hatch_1_angle)
-            .field("any_hatch_enabled", &self.any_hatch_enabled)
-            .field("hatch_0_auto_rotate_angle", &self.hatch_0_rotate_angle)
-            .field("hatch_1_auto_rotate_angle", &self.hatch_1_rotate_angle)
-            .field("hatch_2_enable", &self.hatch_2_enable)
-            .field("hatch_2_pen", &self.hatch_2_pen)
-            .field("hatch_2_flags", &self.hatch_2_flags)
-            .field("hatch_2_edge_offset", &self.hatch_2_edge_offset)
-            .field("hatch_2_line_spacing", &self.hatch_2_line_spacing)
-            .field("hatch_2_start_offset", &self.hatch_2_start_offset)
-            .field("hatch_2_end_offset", &self.hatch_2_end_offset)
-            .field("hatch_2_angle", &self.hatch_2_angle)
-            .field("hatch_2_auto_rotate_angle", &self.hatch_2_rotate_angle)
-            .field("hatch_0_line_reduction", &self.hatch_0_line_reduction)
-            .field("hatch_1_line_reduction", &self.hatch_1_line_reduction)
-            .field("hatch_2_line_reduction", &self.hatch_2_line_reduction)
-            .field("hatch_0_loop_count", &self.hatch_0_loop_count)
-            .field("hatch_1_loop_count", &self.hatch_1_loop_count)
-            .field("hatch_2_loop_count", &self.hatch_2_loop_count)
-            .field("hatch_0_loop_distance", &self.hatch_0_loop_distance)
-            .field("hatch_1_loop_distance", &self.hatch_1_loop_distance)
-            .field("hatch_2_loop_distance", &self.hatch_2_loop_distance)
-            .field("contour_priority", &self.contour_priority)
-            .field("hatch_0_count", &self.hatch_0_count)
-            .field("hatch_1_count", &self.hatch_1_count)
-            .field("hatch_2_count", &self.hatch_2_count)
-            .finish()
-    }
-}
-
-#[cfg_attr(feature = "default-debug", derive(Debug))]
-#[derive(BinRead, BinWrite, Clone, Diff, PartialEq)]
-#[diff(attr(
-    #[derive(Debug, PartialEq)]
-))]
-#[brw(magic(15u32))] // Number of fields in this struct
-pub struct HatchSetting {
-    pub count: U32,
-    pub enabled: U32,
-    pub pen: U32,
-    pub flags: FieldOf<HatchFlag>,
-    pub edge_offset: F64,
-    pub line_spacing: F64,
-    pub start_offset: F64,
-    pub end_offset: F64,
-    pub angle: F64,
-    pub rotate_angle: F64,
-    pub line_reduction: F64,
-    pub loop_distance: F64,
-    pub loop_count: U32,
-    pub _unknown_1: [Field; 2],
-}
-
-// Custom Debug implementation to only print known fields
-#[cfg(not(feature = "default-debug"))]
-impl Debug for HatchSetting {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        f.debug_struct("HatchSettings")
-            .field("count", &self.count)
-            .field("enabled", &self.enabled)
-            .field("pen", &self.pen)
-            .field("flags", &self.flags)
-            .field("edge_offset", &self.edge_offset)
-            .field("line_spacing", &self.line_spacing)
-            .field("start_offset", &self.start_offset)
-            .field("end_offset", &self.end_offset)
-            .field("angle", &self.angle)
-            .field("auto_rotate_angle", &self.rotate_angle)
-            .field("line_reduction", &self.line_reduction)
-            .field("loop_distance", &self.loop_distance)
-            .field("loop_count", &self.loop_count)
-            .finish()
-    }
-}
-
-impl Default for HatchSetting {
-    fn default() -> Self {
-        Self {
-            count: 1.into(),
-            enabled: false.into(),
-            pen: 0.into(),
-            flags: HatchFlag::new().into(),
-            edge_offset: 0.0.into(),
-            line_spacing: 1.0.into(),
-            start_offset: 0.0.into(),
-            end_offset: 0.0.into(),
-            angle: 0.0.into(),
-            rotate_angle: 10.0.into(),
-            line_reduction: 0.0.into(),
-            loop_distance: 0.5.into(),
-            loop_count: 0.into(),
-            _unknown_1: [vec![0, 0, 0, 0, 0, 0, 0, 0].into(), vec![0, 0, 0, 0].into()].into(),
-        }
-    }
-}
-
-impl From<Vec<HatchSetting>> for LegacyHatchSetting {
-    fn from(value: Vec<HatchSetting>) -> Self {
-        LegacyHatchSetting {
-            mark_contour: false.into(),
-            hatch_0_enable: value.get(0).map(|x| x.enabled).unwrap_or_default(),
-            hatch_0_pen: value.get(0).map(|x| x.pen).unwrap_or_default(),
-            hatch_0_flags: value.get(0).map(|x| x.flags).unwrap_or_default(),
-            hatch_0_edge_offset: value.get(0).map(|x| x.edge_offset).unwrap_or_default(),
-            hatch_0_line_spacing: value.get(0).map(|x| x.line_spacing).unwrap_or_default(),
-            hatch_0_start_offset: value.get(0).map(|x| x.start_offset).unwrap_or_default(),
-            hatch_0_end_offset: value.get(0).map(|x| x.end_offset).unwrap_or_default(),
-            hatch_0_angle: value.get(0).map(|x| x.angle).unwrap_or_default(),
-            hatch_1_enable: value.get(1).map(|x| x.enabled).unwrap_or_default(),
-            hatch_1_pen: value.get(1).map(|x| x.pen).unwrap_or_default(),
-            hatch_1_flags: value.get(1).map(|x| x.flags).unwrap_or_default(),
-            hatch_1_edge_offset: value.get(1).map(|x| x.edge_offset).unwrap_or_default(),
-            hatch_1_line_spacing: value.get(1).map(|x| x.line_spacing).unwrap_or_default(),
-            hatch_1_start_offset: value.get(1).map(|x| x.start_offset).unwrap_or_default(),
-            hatch_1_end_offset: value.get(1).map(|x| x.end_offset).unwrap_or_default(),
-            hatch_1_angle: value.get(1).map(|x| x.angle).unwrap_or_default(),
-            any_hatch_enabled: value.iter().any(|x| x.enabled.into()).into(),
-            hatch_0_rotate_angle: value.get(0).map(|x| x.rotate_angle).unwrap_or_default(),
-            hatch_1_rotate_angle: value.get(1).map(|x| x.rotate_angle).unwrap_or_default(),
-            hatch_2_enable: value.get(2).map(|x| x.enabled).unwrap_or_default(),
-            hatch_2_pen: value.get(2).map(|x| x.pen).unwrap_or_default(),
-            hatch_2_flags: value.get(2).map(|x| x.flags).unwrap_or_default(),
-            hatch_2_edge_offset: value.get(2).map(|x| x.edge_offset).unwrap_or_default(),
-            hatch_2_line_spacing: value.get(2).map(|x| x.line_spacing).unwrap_or_default(),
-            hatch_2_start_offset: value.get(2).map(|x| x.start_offset).unwrap_or_default(),
-            hatch_2_end_offset: value.get(2).map(|x| x.end_offset).unwrap_or_default(),
-            hatch_2_angle: value.get(2).map(|x| x.angle).unwrap_or_default(),
-            hatch_2_rotate_angle: value.get(2).map(|x| x.rotate_angle).unwrap_or_default(),
-            hatch_0_line_reduction: value.get(0).map(|x| x.line_reduction).unwrap_or_default(),
-            hatch_1_line_reduction: value.get(1).map(|x| x.line_reduction).unwrap_or_default(),
-            hatch_2_line_reduction: value.get(2).map(|x| x.line_reduction).unwrap_or_default(),
-            hatch_0_loop_count: value.get(0).map(|x| x.loop_count).unwrap_or_default(),
-            hatch_1_loop_count: value.get(1).map(|x| x.loop_count).unwrap_or_default(),
-            hatch_2_loop_count: value.get(2).map(|x| x.loop_count).unwrap_or_default(),
-            hatch_0_loop_distance: value.get(0).map(|x| x.loop_distance).unwrap_or_default(),
-            hatch_1_loop_distance: value.get(1).map(|x| x.loop_distance).unwrap_or_default(),
-            hatch_2_loop_distance: value.get(2).map(|x| x.loop_distance).unwrap_or_default(),
-            _unknown_1: [
-                vec![0, 0, 0, 0, 0, 0, 0, 0].into(),
-                vec![0, 0, 0, 0, 0, 0, 0, 0].into(),
-                vec![0, 0, 0, 0, 0, 0, 0, 0].into(),
-            ]
-            .into(),
-            contour_priority: false.into(),
-            hatch_0_count: value.get(0).map(|x| x.count).unwrap_or_default(),
-            hatch_1_count: value.get(1).map(|x| x.count).unwrap_or_default(),
-            hatch_2_count: value.get(2).map(|x| x.count).unwrap_or_default(),
-        }
-    }
-}
-
-#[derive(BinRead, BinWrite, Clone, Debug, Diff, PartialEq)]
-#[diff(attr(
-    #[derive(Debug, PartialEq)]
-))]
-pub struct HatchLine {
-    pub lines: ArrayOf<Lines>,
-}
-
-#[derive(BinRead, BinWrite, Clone, Debug, Diff, PartialEq)]
-#[diff(attr(
-    #[derive(Debug, PartialEq)]
-))]
-#[brw(magic(16_u32))] // ObjectType::HatchLine
-pub struct HatchLines {
-    pub core: ObjectCore, // HatchType::HatchLine
-    pub hatch_line: ArrayOf<HatchLine>,
-}
-
-#[derive(BinRead, BinWrite, Clone, Debug, Diff, PartialEq)]
-#[diff(attr(
-    #[derive(Debug, PartialEq)]
-))]
-pub struct Hatches {
-    pub core: ObjectCore, // HatchType::HatchLine
-    pub hatch_lines: ArrayOf<HatchLines>,
-}
-
-impl Hatches {
-    pub fn set_pen(&mut self, pen: u32) {
-        // Ignore self.core.pen
-        self.hatch_lines.iter_mut().for_each(|x| {
-            // Ignore HatchLines.core.pen
-            x.hatch_line.iter_mut().for_each(|x| {
-                x.lines.iter_mut().for_each(|x| {
-                    x.core.pen = pen.into();
-                })
-            })
-        });
-    }
-}
-
-#[binrw]
-#[derive(Clone, Debug, Diff, PartialEq)]
-#[diff(attr(
-    #[derive(Debug, PartialEq)]
-))]
-pub struct Hatch {
-    pub core: ObjectCore,
-    pub outline: ArrayOf<Object>,
-    pub legacy_setting: LegacyHatchSetting,
-
-    #[br(temp)]
-    #[bw(try_calc(u32::try_from(hatch_settings.len()).unwrap().try_into()))]
-    pub num_settings: U32,
-
-    #[br(count = num_settings.value)]
-    pub hatch_settings: Vec<HatchSetting>,
-
-    #[brw(if(legacy_setting.any_hatch_enabled.value == 1))]
-    pub hatches: Option<Hatches>,
-}
-
-impl Translate for Hatch {
-    fn move_absolute(&mut self, origin: Option<Coordinate>, z: Option<f64>) {
-        self.outline
-            .iter_mut()
-            .for_each(|x| x.move_absolute(origin, z));
-        origin.map(|origin| {
-            let delta: Coordinate = origin - *self.core.origin;
-            self.hatches.iter_mut().for_each(|x| {
-                // Ignore Hatches.core.origin
-                x.hatch_lines.iter_mut().for_each(|x| {
-                    // Ignore HatchLines.core.origin
-                    x.hatch_line.iter_mut().for_each(|x| {
-                        x.lines
-                            .iter_mut()
-                            .for_each(|x| x.move_relative(Some(delta), None));
-                    })
-                })
-            });
-        });
-        self.core.move_absolute(origin, z);
-    }
-
-    fn move_relative(&mut self, delta: Option<Coordinate>, z: Option<f64>) {
-        self.outline
-            .iter_mut()
-            .for_each(|x| x.move_relative(delta, z));
-        delta.map(|delta| {
-            self.hatches.iter_mut().for_each(|x| {
-                // Ignore Hatches.core.origin
-                x.hatch_lines.iter_mut().for_each(|x| {
-                    // Ignore HatchLines.core.origin
-                    x.hatch_line.iter_mut().for_each(|x| {
-                        x.lines
-                            .iter_mut()
-                            .for_each(|x| x.move_relative(Some(delta), None));
-                    })
-                })
-            });
-        });
-        self.core.move_relative(delta, z);
-    }
-}
+use std::fmt::Debug;
+
+use crate::{
+    array_of::ArrayOf,
+    field_of::FieldOf,
+    types::{Coordinate, Field, F64, U32},
+};
+use binrw::{binrw, BinRead, BinWrite};
+use diff::Diff;
+use modular_bitfield::{
+    bitfield,
+    specifiers::{B1, B18},
+};
+use serde::{Deserialize, Serialize};
+
+use super::{line::Lines, Object, ObjectCore, Translate};
+
+#[bitfield(bits = 32)]
+#[derive(BinRead, BinWrite, Copy, Clone, Debug, Default, Diff, PartialEq)]
+#[diff(attr(
+    #[derive(Debug, PartialEq)]
+))]
+#[br(map = Self::from_bytes)]
+#[bw(map = |&x| Self::into_bytes(x))]
+pub struct HatchFlag {
+    pub all_calc: B1,
+    pub follow_edge_once: B1,
+    pub continuous_pattern: B1, // Hatch type 3 when combined with bidirectional_pattern
+    pub bidirectional_pattern: B1, // Hatch type 1
+    pub ring_pattern: B1,       // Hatch type 2
+    #[skip]
+    __: B1,
+    pub auto_rotate_hatch_angle: B1,
+    pub average_distribute_line: B1,
+    #[skip]
+    __: B1,
+    pub gong_pattern: B1, // Hatch type 4 when combined with bidirectional_pattern
+    pub cross_hatch: B1,
+    pub background_pattern: B1, // Hatch type 5, must set all_calc as well
+    pub fill_pattern: B1, // Hatch type 6 when combined with continuous_pattern and bidirectional_pattern
+    pub zigzag_pattern: B1, // Hatch type 7
+    #[skip]
+    __: B18,
+}
+
+#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
+pub enum HatchPattern {
+    Directional,
+    Bidirectional,
+    Ring,
+    Continuous,
+    Gong,
+    Background,
+    Fill,
+    Zigzag,
+}
+
+impl From<HatchPattern> for HatchFlag {
+    fn from(value: HatchPattern) -> Self {
+        let mut ret = Self::new();
+        match value {
+            HatchPattern::Directional => (),
+            HatchPattern::Bidirectional => ret.set_bidirectional_pattern(1),
+            HatchPattern::Ring => ret.set_ring_pattern(1),
+            HatchPattern::Continuous => {
+                ret.set_bidirectional_pattern(1);
+                ret.set_continuous_pattern(1);
+            }
+            HatchPattern::Gong => {
+                ret.set_bidirectional_pattern(1);
+                ret.set_gong_pattern(1);
+            }
+            HatchPattern::Background => {
+                ret.set_background_pattern(1);
+                ret.set_all_calc(1);
+            }
+            HatchPattern::Fill => ret.set_fill_pattern(1),
+            HatchPattern::Zigzag => ret.set_zigzag_pattern(1),
+        }
+        ret
+    }
+}
+
+#[cfg_attr(feature = "default-debug", derive(Debug))]
+#[derive(BinRead, BinWrite, Clone, Diff, PartialEq)]
+#[diff(attr(
+    #[derive(Debug, PartialEq)]
+))]
+// #[brw(magic(46u32))] // Number of fields in this struct
+#[brw(magic(47u32))] // Number of fields in this struct
+pub struct LegacyHatchSetting {
+    pub mark_contour: U32,
+    pub hatch_0_enable: U32,
+    pub hatch_0_pen: U32,
+    pub hatch_0_flags: FieldOf<HatchFlag>,
+    pub hatch_0_edge_offset: F64,
+    pub hatch_0_line_spacing: F64,
+    pub hatch_0_start_offset: F64,
+    pub hatch_0_end_offset: F64,
+    pub hatch_0_angle: F64,
+    pub hatch_1_enable: U32,
+    pub hatch_1_pen: U32,
+    pub hatch_1_flags: FieldOf<HatchFlag>,
+    pub hatch_1_edge_offset: F64,
+    pub hatch_1_line_spacing: F64,
+    pub hatch_1_start_offset: F64,
+    pub hatch_1_end_offset: F64,
+    pub hatch_1_angle: F64,
+    pub any_hatch_enabled: U32,
+    pub hatch_0_rotate_angle: F64,
+    pub hatch_1_rotate_angle: F64,
+    pub hatch_2_enable: U32,
+    pub hatch_2_pen: U32,
+    pub hatch_2_flags: FieldOf<HatchFlag>,
+    pub hatch_2_edge_offset: F64,
+    pub hatch_2_line_spacing: F64,
+    pub hatch_2_start_offset: F64,
+    pub hatch_2_end_offset: F64,
+    pub hatch_2_angle: F64,
+    pub hatch_2_rotate_angle: F64,
+    pub hatch_0_line_reduction: F64,
+    pub hatch_1_line_reduction: F64,
+    pub hatch_2_line_reduction: F64,
+    pub hatch_0_loop_count: U32,
+    pub hatch_1_loop_count: U32,
+    pub hatch_2_loop_count: U32,
+    pub hatch_0_loop_distance: F64,
+    pub hatch_1_loop_distance: F64,
+    pub hatch_2_loop_distance: F64,
+    pub _unknown_1: [Field; 3],
+    pub contour_priority: U32,
+    pub hatch_0_count: U32,
+    pub hatch_1_count: U32,
+    pub hatch_2_count: U32,
+}
+
+// Custom Debug implementation to only print known fields
+#[cfg(not(feature = "default-debug"))]
+impl Debug for LegacyHatchSetting {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.debug_struct("LegacyHatchSetting")
+            .field("mark_contour", &self.mark_contour)
+            .field("hatch_0_enable", &self.hatch_0_enable)
+            .field("hatch_0_pen", &self.hatch_0_pen)
+            .field("hatch_0_flags", &self.hatch_0_flags)
+            .field("hatch_0_edge_offset", &self.hatch_0_edge_offset)
+            .field("hatch_0_line_spacing", &self.hatch_0_line_spacing)
+            .field("hatch_0_start_offset", &self.hatch_0_start_offset)
+            .field("hatch_0_end_offset", &self.hatch_0_end_offset)
+            .field("hatch_0_angle", &self.hatch_0_angle)
+            .field("hatch_1_enable", &self.hatch_1_enable)
+            .field("hatch_1_pen", &self.hatch_1_pen)
+            .field("hatch_1_flags", &self.hatch_1_flags)
+            .field("hatch_1_edge_offset", &self.hatch_1_edge_offset)
+            .field("hatch_1_line_spacing", &self.hatch_1_line_spacing)
+            .field("hatch_1_start_offset", &self.hatch_1_start_offset)
+            .field("hatch_1_end_offset", &self.hatch_1_end_offset)
+            .field("hatch_1_angle", &self.hatch_1_angle)
+            .field("any_hatch_enabled", &self.any_hatch_enabled)
+            .field("hatch_0_auto_rotate_angle", &self.hatch_0_rotate_angle)
+            .field("hatch_1_auto_rotate_angle", &self.hatch_1_rotate_angle)
+            .field("hatch_2_enable", &self.hatch_2_enable)
+            .field("hatch_2_pen", &self.hatch_2_pen)
+            .field("hatch_2_flags", &self.hatch_2_flags)
+            .field("hatch_2_edge_offset", &self.hatch_2_edge_offset)
+            .field("hatch_2_line_spacing", &self.hatch_2_line_spacing)
+            .field("hatch_2_start_offset", &self.hatch_2_start_offset)
+            .field("hatch_2_end_offset", &self.hatch_2_end_offset)
+            .field("hatch_2_angle", &self.hatch_2_angle)
+            .field("hatch_2_auto_rotate_angle", &self.hatch_2_rotate_angle)
+            .field("hatch_0_line_reduction", &self.hatch_0_line_reduction)
+            .field("hatch_1_line_reduction", &self.hatch_1_line_reduction)
+            .field("hatch_2_line_reduction", &self.hatch_2_line_reduction)
+            .field("hatch_0_loop_count", &self.hatch_0_loop_count)
+            .field("hatch_1_loop_count", &self.hatch_1_loop_count)
+            .field("hatch_2_loop_count", &self.hatch_2_loop_count)
+            .field("hatch_0_loop_distance", &self.hatch_0_loop_distance)
+            .field("hatch_1_loop_distance", &self.hatch_1_loop_distance)
+            .field("hatch_2_loop_distance", &self.hatch_2_loop_distance)
+            .field("contour_priority", &self.contour_priority)
+            .field("hatch_0_count", &self.hatch_0_count)
+            .field("hatch_1_count", &self.hatch_1_count)
+            .field("hatch_2_count", &self.hatch_2_count)
+            .finish()
+    }
+}
+
+#[cfg_attr(feature = "default-debug", derive(Debug))]
+#[derive(BinRead, BinWrite, Clone, Diff, PartialEq)]
+#[diff(attr(
+    #[derive(Debug, PartialEq)]
+))]
+#[brw(magic(15u32))] // Number of fields in this struct
+pub struct HatchSetting {
+    pub count: U32,
+    pub enabled: U32,
+    pub pen: U32,
+    pub flags: FieldOf<HatchFlag>,
+    pub edge_offset: F64,
+    pub line_spacing: F64,
+    pub start_offset: F64,
+    pub end_offset: F64,
+    pub angle: F64,
+    pub rotate_angle: F64,
+    pub line_reduction: F64,
+    pub loop_distance: F64,
+    pub loop_count: U32,
+    pub _unknown_1: [Field; 2],
+}
+
+// Custom Debug implementation to only print known fields
+#[cfg(not(feature = "default-debug"))]
+impl Debug for HatchSetting {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.debug_struct("HatchSettings")
+            .field("count", &self.count)
+            .field("enabled", &self.enabled)
+            .field("pen", &self.pen)
+            .field("flags", &self.flags)
+            .field("edge_offset", &self.edge_offset)
+            .field("line_spacing", &self.line_spacing)
+            .field("start_offset", &self.start_offset)
+            .field("end_offset", &self.end_offset)
+            .field("angle", &self.angle)
+            .field("auto_rotate_angle", &self.rotate_angle)
+            .field("line_reduction", &self.line_reduction)
+            .field("loop_distance", &self.loop_distance)
+            .field("loop_count", &self.loop_count)
+            .finish()
+    }
+}
+
+impl Default for HatchSetting {
+    fn default() -> Self {
+        Self {
+            count: 1.into(),
+            enabled: false.into(),
+            pen: 0.into(),
+            flags: HatchFlag::new().into(),
+            edge_offset: 0.0.into(),
+            line_spacing: 1.0.into(),
+            start_offset: 0.0.into(),
+            end_offset: 0.0.into(),
+            angle: 0.0.into(),
+            rotate_angle: 10.0.into(),
+            line_reduction: 0.0.into(),
+            loop_distance: 0.5.into(),
+            loop_count: 0.into(),
+            _unknown_1: [vec![0, 0, 0, 0, 0, 0, 0, 0].into(), vec![0, 0, 0, 0].into()].into(),
+        }
+    }
+}
+
+impl From<Vec<HatchSetting>> for LegacyHatchSetting {
+    fn from(value: Vec<HatchSetting>) -> Self {
+        LegacyHatchSetting {
+            mark_contour: false.into(),
+            hatch_0_enable: value.get(0).map(|x| x.enabled).unwrap_or_default(),
+            hatch_0_pen: value.get(0).map(|x| x.pen).unwrap_or_default(),
+            hatch_0_flags: value.get(0).map(|x| x.flags).unwrap_or_default(),
+            hatch_0_edge_offset: value.get(0).map(|x| x.edge_offset).unwrap_or_default(),
+            hatch_0_line_spacing: value.get(0).map(|x| x.line_spacing).unwrap_or_default(),
+            hatch_0_start_offset: value.get(0).map(|x| x.start_offset).unwrap_or_default(),
+            hatch_0_end_offset: value.get(0).map(|x| x.end_offset).unwrap_or_default(),
+            hatch_0_angle: value.get(0).map(|x| x.angle).unwrap_or_default(),
+            hatch_1_enable: value.get(1).map(|x| x.enabled).unwrap_or_default(),
+            hatch_1_pen: value.get(1).map(|x| x.pen).unwrap_or_default(),
+            hatch_1_flags: value.get(1).map(|x| x.flags).unwrap_or_default(),
+            hatch_1_edge_offset: value.get(1).map(|x| x.edge_offset).unwrap_or_default(),
+            hatch_1_line_spacing: value.get(1).map(|x| x.line_spacing).unwrap_or_default(),
+            hatch_1_start_offset: value.get(1).map(|x| x.start_offset).unwrap_or_default(),
+            hatch_1_end_offset: value.get(1).map(|x| x.end_offset).unwrap_or_default(),
+            hatch_1_angle: value.get(1).map(|x| x.angle).unwrap_or_default(),
+            any_hatch_enabled: value.iter().any(|x| x.enabled.into()).into(),
+            hatch_0_rotate_angle: value.get(0).map(|x| x.rotate_angle).unwrap_or_default(),
+            hatch_1_rotate_angle: value.get(1).map(|x| x.rotate_angle).unwrap_or_default(),
+            hatch_2_enable: value.get(2).map(|x| x.enabled).unwrap_or_default(),
+            hatch_2_pen: value.get(2).map(|x| x.pen).unwrap_or_default(),
+            hatch_2_flags: value.get(2).map(|x| x.flags).unwrap_or_default(),
+            hatch_2_edge_offset: value.get(2).map(|x| x.edge_offset).unwrap_or_default(),
+            hatch_2_line_spacing: value.get(2).map(|x| x.line_spacing).unwrap_or_default(),
+            hatch_2_start_offset: value.get(2).map(|x| x.start_offset).unwrap_or_default(),
+            hatch_2_end_offset: value.get(2).map(|x| x.end_offset).unwrap_or_default(),
+            hatch_2_angle: value.get(2).map(|x| x.angle).unwrap_or_default(),
+            hatch_2_rotate_angle: value.get(2).map(|x| x.rotate_angle).unwrap_or_default(),
+            hatch_0_line_reduction: value.get(0).map(|x| x.line_reduction).unwrap_or_default(),
+            hatch_1_line_reduction: value.get(1).map(|x| x.line_reduction).unwrap_or_default(),
+            hatch_2_line_reduction: value.get(2).map(|x| x.line_reduction).unwrap_or_default(),
+            hatch_0_loop_count: value.get(0).map(|x| x.loop_count).unwrap_or_default(),
+            hatch_1_loop_count: value.get(1).map(|x| x.loop_count).unwrap_or_default(),
+            hatch_2_loop_count: value.get(2).map(|x| x.loop_count).unwrap_or_default(),
+            hatch_0_loop_distance: value.get(0).map(|x| x.loop_distance).unwrap_or_default(),
+            hatch_1_loop_distance: value.get(1).map(|x| x.loop_distance).unwrap_or_default(),
+            hatch_2_loop_distance: value.get(2).map(|x| x.loop_distance).unwrap_or_default(),
+            _unknown_1: [
+                vec![0, 0, 0, 0, 0, 0, 0, 0].into(),
+                vec![0, 0, 0, 0, 0, 0, 0, 0].into(),
+                vec![0, 0, 0, 0, 0, 0, 0, 0].into(),
+            ]
+            .into(),
+            contour_priority: false.into(),
+            hatch_0_count: value.get(0).map(|x| x.count).unwrap_or_default(),
+            hatch_1_count: value.get(1).map(|x| x.count).unwrap_or_default(),
+            hatch_2_count: value.get(2).map(|x| x.count).unwrap_or_default(),
+        }
+    }
+}
+
+#[derive(BinRead, BinWrite, Clone, Debug, Diff, PartialEq)]
+#[diff(attr(
+    #[derive(Debug, PartialEq)]
+))]
+pub struct HatchLine {
+    pub lines: ArrayOf<Lines>,
+}
+
+#[derive(BinRead, BinWrite, Clone, Debug, Diff, PartialEq)]
+#[diff(attr(
+    #[derive(Debug, PartialEq)]
+))]
+#[brw(magic(16_u32))] // ObjectType::HatchLine
+pub struct HatchLines {
+    pub core: ObjectCore, // HatchType::HatchLine
+    pub hatch_line: ArrayOf<HatchLine>,
+}
+
+#[derive(BinRead, BinWrite, Clone, Debug, Diff, PartialEq)]
+#[diff(attr(
+    #[derive(Debug, PartialEq)]
+))]
+pub struct Hatches {
+    pub core: ObjectCore, // HatchType::HatchLine
+    pub hatch_lines: ArrayOf<HatchLines>,
+}
+
+impl Hatches {
+    pub fn set_pen(&mut self, pen: u32) {
+        // Ignore self.core.pen
+        self.hatch_lines.iter_mut().for_each(|x| {
+            // Ignore HatchLines.core.pen
+            x.hatch_line.iter_mut().for_each(|x| {
+                x.lines.iter_mut().for_each(|x| {
+                    x.core.pen = pen.into();
+                })
+            })
+        });
+    }
+}
+
+#[binrw]
+#[derive(Clone, Debug, Diff, PartialEq)]
+#[diff(attr(
+    #[derive(Debug, PartialEq)]
+))]
+pub struct Hatch {
+    pub core: ObjectCore,
+    pub outline: ArrayOf<Object>,
+    pub legacy_setting: LegacyHatchSetting,
+
+    #[br(temp)]
+    #[bw(try_calc(u32::try_from(hatch_settings.len()).unwrap().try_into()))]
+    pub num_settings: U32,
+
+    pub _unknown_1: Field,
+
+    #[br(count = num_settings.value)]
+    pub hatch_settings: Vec<HatchSetting>,
+
+    #[brw(if(legacy_setting.any_hatch_enabled.value == 1))]
+    pub hatches: Option<Hatches>,
+}
+
+impl Translate for Hatch {
+    fn move_absolute(&mut self, origin: Option<Coordinate>, z: Option<f64>) {
+        self.outline
+            .iter_mut()
+            .for_each(|x| x.move_absolute(origin, z));
+        origin.map(|origin| {
+            let delta: Coordinate = origin - *self.core.origin;
+            self.hatches.iter_mut().for_each(|x| {
+                // Ignore Hatches.core.origin
+                x.hatch_lines.iter_mut().for_each(|x| {
+                    // Ignore HatchLines.core.origin
+                    x.hatch_line.iter_mut().for_each(|x| {
+                        x.lines
+                            .iter_mut()
+                            .for_each(|x| x.move_relative(Some(delta), None));
+                    })
+                })
+            });
+        });
+        self.core.move_absolute(origin, z);
+    }
+
+    fn move_relative(&mut self, delta: Option<Coordinate>, z: Option<f64>) {
+        self.outline
+            .iter_mut()
+            .for_each(|x| x.move_relative(delta, z));
+        delta.map(|delta| {
+            self.hatches.iter_mut().for_each(|x| {
+                // Ignore Hatches.core.origin
+                x.hatch_lines.iter_mut().for_each(|x| {
+                    // Ignore HatchLines.core.origin
+                    x.hatch_line.iter_mut().for_each(|x| {
+                        x.lines
+                            .iter_mut()
+                            .for_each(|x| x.move_relative(Some(delta), None));
+                    })
+                })
+            });
+        });
+        self.core.move_relative(delta, z);
+    }
+}

+ 5 - 3
src/ezcad/pen.rs

@@ -61,7 +61,8 @@ pub struct Pens {
 #[diff(attr(
     #[derive(Debug, PartialEq)]
 ))]
-#[brw(magic(236u32))] // Number of fields within this struct
+// #[brw(magic(236u32))] // Number of fields within this struct
+#[brw(magic(370u32))] // Number of fields within this struct
 pub struct Pen {
     pub color: FieldOf<Rgba>,
     pub name: WString,
@@ -71,7 +72,7 @@ pub struct Pen {
     pub speed: F64, // Changes with wobble relative speed
     pub power: F64,
     pub frequency: U32,
-    _unknown_1: Field,
+    pub pulse_width: U32,
     pub start_tc: U32,
     pub end_tc: U32,
     pub polygon_tc: U32,
@@ -79,7 +80,7 @@ pub struct Pen {
     _unknown_2: [Field; 10],
     pub laser_off_tc: U32,
     pub wave: U32, // Only available if continue_mode is false
-    _unknown_3: Field,
+    pub pulse_width_2: F64,
     pub wobble_enable: U32,
     pub wobble_diameter: F64,
     pub wobble_distance: F64,
@@ -95,6 +96,7 @@ pub struct Pen {
     _unknown_7: [Field; 12],
     pub wobble_diameter_2: F64, // Only with wobble type ellipse
     _unknown_8: [Field; 26],
+    _unknown_9: [Field; 134],
 }
 
 impl Pen {

+ 40 - 0
src/ezcad/types.rs

@@ -5,8 +5,10 @@ use std::{
 
 use binrw::{binrw, BinRead, BinWrite};
 use diff::{Diff, VecDiff};
+use num_enum::{IntoPrimitive, TryFromPrimitive};
 use rand::{thread_rng, Rng};
 use serde::{Deserialize, Serialize};
+use serde_repr::{Deserialize_repr, Serialize_repr};
 
 use crate::{array_of::ArrayOfPrimitive, field_of::FieldOf};
 
@@ -252,3 +254,41 @@ impl Default for ObjectType {
         ObjectType::Unknown
     }
 }
+
+#[derive(
+    Copy,
+    Clone,
+    Debug,
+    Diff,
+    PartialEq,
+    BinRead,
+    BinWrite,
+    strum::Display,
+    Serialize_repr,
+    Deserialize_repr,
+    IntoPrimitive,
+    TryFromPrimitive,
+)]
+#[diff(attr(
+    #[derive(Debug, PartialEq)]
+))]
+#[brw(repr(u32))]
+#[repr(u32)]
+pub enum PulseWidth {
+    Ns2 = 2,
+    Ns4 = 4,
+    Ns6 = 6,
+    Ns8 = 8,
+    Ns12 = 12,
+    Ns20 = 20,
+    Ns30 = 30,
+    Ns45 = 45,
+    Ns60 = 60,
+    Ns80 = 80,
+    Ns100 = 100,
+    Ns150 = 150,
+    Ns200 = 200,
+    Ns250 = 250,
+    Ns350 = 350,
+    Ns500 = 500,
+}