Browse Source

Update to latest software version

Kevin Lee 11 tháng trước cách đây
mục cha
commit
96bc7f853f
10 tập tin đã thay đổi với 1045 bổ sung952 xóa
  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,
+}