16 Commits 8db860afbd ... 4401ff1da2

Author SHA1 Message Date
  Kevin Lee 4401ff1da2 Object modify and toggle limit check 3 months ago
  Kevin Lee 0e87759622 Add power density to pen query 4 months ago
  Kevin Lee 41a76c99ab Add density range to randomization 4 months ago
  Kevin Lee 86119b86b7 Refactor power density as Hatch fn 4 months ago
  Kevin Lee 3680f61a82 Cleanup 4 months ago
  Kevin Lee 07b1a7423f F64 compare and support for layer queries 4 months ago
  Kevin Lee 6daafb8b80 Add query hatch power 4 months ago
  Kevin Lee cc43d402b2 Add measured pulse power 4 months ago
  Kevin Lee 844bd4781c Update test 4 months ago
  Kevin Lee 122ab2a3fa Use non-iterator version of WindowMut 4 months ago
  Kevin Lee fb16f06b97 Update README 4 months ago
  Kevin Lee c34caeed08 Pattern X then Y for consistency 4 months ago
  Kevin Lee 21228ffce4 Minor refactor 4 months ago
  Kevin Lee 9f14e7e60e Add param to force overwrite 4 months ago
  Kevin Lee b65076bff1 Refactor and print f64 as {:.3} 4 months ago
  Kevin Lee f2f0f28ed9 Add support for patterning hatch settings 4 months ago

BIN
1mmRect.mlp


BIN
2mmRect.mlp


BIN
2mmRectArray.mlp


+ 179 - 25
Cargo.lock

@@ -65,6 +65,12 @@ version = "2.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3d62b7694a562cdf5a74227903507c56ab2cc8bdd1f781ed5cb4cf9c9f810bfc"
 
+[[package]]
+name = "arrayvec"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
+
 [[package]]
 name = "autocfg"
 version = "1.1.0"
@@ -95,6 +101,12 @@ dependencies = [
  "syn 1.0.109",
 ]
 
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
 [[package]]
 name = "bitflags"
 version = "2.4.1"
@@ -169,6 +181,32 @@ version = "1.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
 
+[[package]]
+name = "console"
+version = "0.15.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ea3c6ecd8059b57859df5c69830340ed3c41d30e3da0c1cbed90a96ac853041b"
+dependencies = [
+ "encode_unicode",
+ "libc",
+ "once_cell",
+ "unicode-width",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "dialoguer"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "658bce805d770f407bc62102fca7c2c64ceef2fbcb2b8bd19d2765ce093980de"
+dependencies = [
+ "console",
+ "shell-words",
+ "tempfile",
+ "thiserror",
+ "zeroize",
+]
+
 [[package]]
 name = "diff-struct"
 version = "0.5.3"
@@ -197,6 +235,12 @@ version = "1.9.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07"
 
+[[package]]
+name = "encode_unicode"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"
+
 [[package]]
 name = "env_logger"
 version = "0.10.1"
@@ -226,6 +270,21 @@ dependencies = [
  "windows-sys 0.52.0",
 ]
 
+[[package]]
+name = "fastrand"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
+
+[[package]]
+name = "float-cmp"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b09cf3155332e944990140d967ff5eceb70df778b34f77d8075db46e4704e6d8"
+dependencies = [
+ "num-traits",
+]
+
 [[package]]
 name = "getrandom"
 version = "0.2.11"
@@ -337,13 +396,16 @@ dependencies = [
  "binrw",
  "clap",
  "clap-verbosity-flag",
+ "dialoguer",
  "diff-struct",
  "env_logger",
+ "float-cmp",
  "human-repr",
  "itertools",
  "log",
  "modular-bitfield",
  "num",
+ "num-format",
  "num_enum",
  "rand",
  "regex",
@@ -408,6 +470,16 @@ dependencies = [
  "num-traits",
 ]
 
+[[package]]
+name = "num-format"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a652d9771a63711fd3c3deb670acfbe5c30a4072e664d7a3bf5a9e1056ac72c3"
+dependencies = [
+ "arrayvec",
+ "itoa",
+]
+
 [[package]]
 name = "num-integer"
 version = "0.1.45"
@@ -471,6 +543,12 @@ dependencies = [
  "syn 2.0.48",
 ]
 
+[[package]]
+name = "once_cell"
+version = "1.20.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
+
 [[package]]
 name = "owo-colors"
 version = "3.5.0"
@@ -541,6 +619,15 @@ dependencies = [
  "getrandom",
 ]
 
+[[package]]
+name = "redox_syscall"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa"
+dependencies = [
+ "bitflags 1.3.2",
+]
+
 [[package]]
 name = "regex"
 version = "1.10.4"
@@ -576,7 +663,7 @@ version = "0.38.28"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "72e572a5e8ca657d7366229cdde4bd14c4eb5499a9573d4d366fe1b599daa316"
 dependencies = [
- "bitflags",
+ "bitflags 2.4.1",
  "errno",
  "libc",
  "linux-raw-sys",
@@ -639,6 +726,12 @@ dependencies = [
  "unsafe-libyaml",
 ]
 
+[[package]]
+name = "shell-words"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde"
+
 [[package]]
 name = "static_assertions"
 version = "1.1.0"
@@ -695,6 +788,19 @@ dependencies = [
  "unicode-ident",
 ]
 
+[[package]]
+name = "tempfile"
+version = "3.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "01ce4141aa927a6d1bd34a041795abd0db1cccba5d5f24b009f694bdf3a1f3fa"
+dependencies = [
+ "cfg-if",
+ "fastrand",
+ "redox_syscall",
+ "rustix",
+ "windows-sys 0.52.0",
+]
+
 [[package]]
 name = "termcolor"
 version = "1.4.0"
@@ -704,6 +810,26 @@ dependencies = [
  "winapi-util",
 ]
 
+[[package]]
+name = "thiserror"
+version = "1.0.65"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d11abd9594d9b38965ef50805c5e469ca9cc6f197f883f717e0269a3057b3d5"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.65"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae71770322cbd277e69d762a16c444af02aa0575ac0d174f0b9562d3b37f8602"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.48",
+]
+
 [[package]]
 name = "toml_datetime"
 version = "0.6.3"
@@ -727,6 +853,12 @@ version = "1.0.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
 
+[[package]]
+name = "unicode-width"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd"
+
 [[package]]
 name = "unsafe-libyaml"
 version = "0.2.10"
@@ -791,7 +923,16 @@ version = "0.52.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
 dependencies = [
- "windows-targets 0.52.0",
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.59.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
+dependencies = [
+ "windows-targets 0.52.6",
 ]
 
 [[package]]
@@ -811,17 +952,18 @@ dependencies = [
 
 [[package]]
 name = "windows-targets"
-version = "0.52.0"
+version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd"
+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
 dependencies = [
- "windows_aarch64_gnullvm 0.52.0",
- "windows_aarch64_msvc 0.52.0",
- "windows_i686_gnu 0.52.0",
- "windows_i686_msvc 0.52.0",
- "windows_x86_64_gnu 0.52.0",
- "windows_x86_64_gnullvm 0.52.0",
- "windows_x86_64_msvc 0.52.0",
+ "windows_aarch64_gnullvm 0.52.6",
+ "windows_aarch64_msvc 0.52.6",
+ "windows_i686_gnu 0.52.6",
+ "windows_i686_gnullvm",
+ "windows_i686_msvc 0.52.6",
+ "windows_x86_64_gnu 0.52.6",
+ "windows_x86_64_gnullvm 0.52.6",
+ "windows_x86_64_msvc 0.52.6",
 ]
 
 [[package]]
@@ -832,9 +974,9 @@ checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
 
 [[package]]
 name = "windows_aarch64_gnullvm"
-version = "0.52.0"
+version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea"
+checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
 
 [[package]]
 name = "windows_aarch64_msvc"
@@ -844,9 +986,9 @@ checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
 
 [[package]]
 name = "windows_aarch64_msvc"
-version = "0.52.0"
+version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef"
+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
 
 [[package]]
 name = "windows_i686_gnu"
@@ -856,9 +998,15 @@ checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
 
 [[package]]
 name = "windows_i686_gnu"
-version = "0.52.0"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313"
+checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
 
 [[package]]
 name = "windows_i686_msvc"
@@ -868,9 +1016,9 @@ checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
 
 [[package]]
 name = "windows_i686_msvc"
-version = "0.52.0"
+version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a"
+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
 
 [[package]]
 name = "windows_x86_64_gnu"
@@ -880,9 +1028,9 @@ checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
 
 [[package]]
 name = "windows_x86_64_gnu"
-version = "0.52.0"
+version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd"
+checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
 
 [[package]]
 name = "windows_x86_64_gnullvm"
@@ -892,9 +1040,9 @@ checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
 
 [[package]]
 name = "windows_x86_64_gnullvm"
-version = "0.52.0"
+version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e"
+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
 
 [[package]]
 name = "windows_x86_64_msvc"
@@ -904,9 +1052,9 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
 
 [[package]]
 name = "windows_x86_64_msvc"
-version = "0.52.0"
+version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04"
+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
 
 [[package]]
 name = "winnow"
@@ -916,3 +1064,9 @@ checksum = "8434aeec7b290e8da5c3f0d628cb0eac6cabcb31d14bb74f779a08109a5914d6"
 dependencies = [
  "memchr",
 ]
+
+[[package]]
+name = "zeroize"
+version = "1.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"

+ 3 - 0
Cargo.toml

@@ -17,13 +17,16 @@ path = "src/main.rs"
 binrw = "0.13.3"
 clap = { version = "4.4.11", features = ["derive"] }
 clap-verbosity-flag = "2.1.1"
+dialoguer = "0.11.0"
 diff-struct = "0.5.3"
 env_logger = "0.10.1"
+float-cmp = "0.10.0"
 human-repr = { version = "1.1.0", features = ["serde"] }
 itertools = "0.12.0"
 log = "0.4.20"
 modular-bitfield = "0.11.2"
 num = "0.4.1"
+num-format = "0.4.4"
 num_enum = "0.7.1"
 rand = "0.8.5"
 regex = "1.10.4"

BIN
Data.xlsx


BIN
Pulse Width.png


+ 57 - 8
README.md

@@ -28,7 +28,7 @@ Options:
 
 ### Sub-Commands
 
-Diff two .mlp files and print differences between the two
+Diff two .mlp files and print differences between the two.
 
 ``` text
 Usage: minilase.exe --input <INPUT> diff [OPTIONS] --diff-file <DIFF_FILE>
@@ -40,7 +40,9 @@ Options:
   -h, --help                   Print help
 ```
 
-Queries input .mlp file for pen or object info
+Queries input .mlp file for pen or object info.
+
+`<Pen>` and `<Object>` supports indexes (`6`) and ranges (`..3`, `2..=3`, or `5..`).
 
 ``` text
 Usage: minilase.exe --input <INPUT> query [OPTIONS]
@@ -54,7 +56,7 @@ Options:
   -h, --help                         Print help
 ```
 
-Applies configuration YAML to input .mlp file
+Applies configuration YAML to input .mlp file.
 
 ``` text
 Usage: minilase.exe --input <INPUT> apply [OPTIONS] --config <CONFIG>
@@ -62,6 +64,7 @@ Usage: minilase.exe --input <INPUT> apply [OPTIONS] --config <CONFIG>
 Options:
   -c, --config <CONFIG>  Configuration file
   -o, --output <OUTPUT>  Output file to write to
+  -w, --overwrite        Overwrite output file if it exists
   -v, --verbose...       Increase logging verbosity
   -q, --quiet...         Decrease logging verbosity
   -h, --help             Print help
@@ -71,6 +74,34 @@ Options:
 
 Operations defined in the configuration file are applied in order of definition.
 
+## Machine Parameters
+
+`<PulseWidth>` can be 2/4/6/8/12/20/30/45/60/80/100/150/200/250/350/500.
+
+Note: each pulse width has a minimum and maximum frequency for model YDFLP-20-M7-S-R:
+``` text
+2ns => 850kHz to 4MHz
+4ns => 500kHz to 4MHz
+6ns => 320kHz to 4MHz
+8ns => 250kHz to 4MHz
+12ns => 170kHz to 3MHz
+20ns => 115kHz to 3MHz
+30ns => 90kHz to 3MHz
+45ns => 75kHz to 2MHz
+60ns => 65kHz to 2MHz
+80ns => 60kHz to 2MHz
+100ns => 45kHz to 1MHz
+150ns => 30kHz to 1MHz
+200ns => 25kHz to 1MHz
+250ns => 25kHz to 900kHz
+350ns => 25kHz to 600kHz
+500ns => 25kHz to 500kHz
+```
+
+Power density of a hatch pattern is estimated using the following formula:
+
+Power density = 1/line spacing * power * 1/speed * frequency * pulse width power
+
 ### Pen Operations
 
 Setting values of specific fields for a given pen:
@@ -78,7 +109,7 @@ Setting values of specific fields for a given pen:
 ``` yaml
 Ops: 
   - !PatchPen
-    Pen: 0                  # Target pen
+    Pen: 0 # Target pen
 
     # Specify one or more of the following:
     Color: [127, 127, 127]
@@ -90,8 +121,6 @@ Ops:
     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
@@ -145,6 +174,10 @@ Op:
     Power: [10, 100, 5] # [min, max, step]
     Frequency: [20000, 100000, 1000] # [min, max, step]
     PulseWidth: [2, 350, 2] # [min, max, step]
+
+    PowerDensity: [10000, 1000000] # [min, max]
+
+    EnforceLimits: bool # Enforces pulse width / frequency limitations
 ```
 
 Exporting a pen to a file:
@@ -212,10 +245,26 @@ Array:
   Spacing: 1.0
   RandomizeOrder: True
   StartingPen: 1
-  PatternX: <PenPattern> // Optional
-  PatternY: <PenPattern> // Optional
+  PatternPenX: <PenPattern> // Optional
+  PatternPenY: <PenPattern> // Optional
+  PatternHatchX: <HatchPattern> // Optional
+  PatternHatchY: <HatchPattern> // Optional
 ```
 
+Where `<HatchPattern>` is one of the following:
+
+``` yaml
+Field: !Count 1
+Field: !LineSpacing 0.001
+Field: !EdgeOffset 0.1
+Field: !StartOffset 0.1
+Field: !EndOffset 0.1
+Field: !Angle 45.0
+Field: !RotateAngle 45.0
+Field: !LineReduction 1.0
+Field: !LoopDistance 1.0
+Field: !LoopCount 1
+```
 Where `<HatchOptions>` is:
 
 ``` yaml

+ 38 - 0
config.yml

@@ -0,0 +1,38 @@
+Ops: 
+  - !PatchPen
+    Pen: 0
+    Speed: 2000
+    Frequency: 60000
+    Power: 20.0
+    PulseWidth: 80
+
+  - !ClonePen
+    From: 0
+    To: 255
+    Inclusive: true
+
+  - !RandomizePen
+    Index: 0
+    Count: 256
+    Speed: [1000, 10000, 500]
+    Power: [10, 30, 10]
+    Frequency: [10000, 200000, 1000]
+    PulseWidth: [2, 350, 2]
+    PowerDensity: [1000, 100000000]
+    EnforceLimits: true
+
+  - !Object
+    Input: !Existing { Layer: 0, Object: 0 }
+    Modify:
+      Z: 4.5
+    Array:
+      Columns: 3
+      Rows: 3
+      Spacing: 2.0
+      RandomizeOrder: True
+      StartingPen: 0
+      # PatternPenX: !Speed 100
+      # PatternPenY: !Power 5
+      # PatternHatchX: !LineSpacing 0.01
+      #PatternHatchY: !Count 1
+    ReplaceObject: 0

BIN
export.bin


BIN
samples/Circle2.mlp


BIN
samples/Circle3.mlp


BIN
samples/Circle4.mlp


BIN
samples/Circle5.mlp


BIN
samples/Ellipse.mlp


BIN
samples/Rectangle2.mlp


BIN
samples/Rectangle3.mlp


BIN
samples/Rectangle4.mlp


BIN
samples/RectangleSkewRightUpTopLeft.mlp


+ 130 - 0
src/config/hatch.rs

@@ -0,0 +1,130 @@
+use ezcad::{
+    objects::{hatch::HatchSetting, Object},
+    FP,
+};
+use itertools::Itertools;
+use log::debug;
+use serde::{Deserialize, Serialize};
+
+use super::double_window_mut;
+
+#[derive(Debug, Serialize, Deserialize)]
+
+pub enum PatternHatchField {
+    Count(u32),
+    LineSpacing(f64),
+    EdgeOffset(f64),
+    StartOffset(f64),
+    EndOffset(f64),
+    Angle(f64),
+    RotateAngle(f64),
+    LineReduction(f64),
+    LoopDistance(f64),
+    LoopCount(u32),
+}
+
+impl PatternHatchField {
+    pub fn pattern(&self, objects: &mut dyn Iterator<Item = (usize, &mut Object)>) {
+        let mut objects: Vec<(usize, &mut Object)> = objects.collect_vec();
+
+        double_window_mut(&mut objects[..], |prev, next| {
+            let (_prev_idx, prev): (usize, &mut HatchSetting) = match prev.1 {
+                Object::Hatch(hatch) => (
+                    prev.0,
+                    hatch
+                        .hatch_settings
+                        .iter_mut()
+                        .find(|h| h.enabled.into())
+                        .expect("Hatch missing enabled setting"),
+                ),
+                _ => panic!("Object #{} not a hatch object", prev.0),
+            };
+
+            let (next_idx, next): (usize, &mut HatchSetting) = match next.1 {
+                Object::Hatch(hatch) => (
+                    next.0,
+                    hatch
+                        .hatch_settings
+                        .iter_mut()
+                        .find(|h| h.enabled.into())
+                        .expect("Hatch missing enabled setting"),
+                ),
+                _ => panic!("Object #{} not a hatch object", next.0),
+            };
+
+            match self {
+                PatternHatchField::Count(incr) => {
+                    let value: u32 = *prev.count + incr;
+                    debug!("Patching hatch count for object #{} to {}", next_idx, value);
+                    *next.count = value;
+                }
+                PatternHatchField::LineSpacing(incr) => {
+                    let value: f64 = *prev.line_spacing + incr;
+                    debug!(
+                        "Patching line spacing for object #{} to {:.FP$}",
+                        next_idx, value
+                    );
+                    *next.line_spacing = value;
+                }
+                PatternHatchField::EdgeOffset(incr) => {
+                    let value: f64 = *prev.edge_offset + incr;
+                    debug!(
+                        "Patching edge offset for object #{} to {:.FP$}",
+                        next_idx, value
+                    );
+                    *next.edge_offset = value;
+                }
+                PatternHatchField::StartOffset(incr) => {
+                    let value: f64 = *prev.start_offset + incr;
+                    debug!(
+                        "Patching start offset for object #{} to {:.FP$}",
+                        next_idx, value
+                    );
+                    *next.start_offset = value;
+                }
+                PatternHatchField::EndOffset(incr) => {
+                    let value: f64 = *prev.end_offset + incr;
+                    debug!(
+                        "Patching end offset for object #{} to {:.FP$}",
+                        next_idx, value
+                    );
+                    *next.end_offset = value;
+                }
+                PatternHatchField::Angle(incr) => {
+                    let value: f64 = *prev.angle + incr;
+                    debug!("Patching angle for object #{} to {:.FP$}", next_idx, value);
+                    *next.angle = value;
+                }
+                PatternHatchField::RotateAngle(incr) => {
+                    let value: f64 = *prev.rotate_angle + incr;
+                    debug!(
+                        "Patching rotate angle for object #{} to {:.FP$}",
+                        next_idx, value
+                    );
+                    *next.rotate_angle = value;
+                }
+                PatternHatchField::LineReduction(incr) => {
+                    let value: f64 = *prev.line_reduction + incr;
+                    debug!(
+                        "Patching line reduction for object #{} to {:.FP$}",
+                        next_idx, value
+                    );
+                    *next.line_reduction = value;
+                }
+                PatternHatchField::LoopDistance(incr) => {
+                    let value: f64 = *prev.loop_distance + incr;
+                    debug!(
+                        "Patching loop distance for object #{} to {:.FP$}",
+                        next_idx, value
+                    );
+                    *next.loop_distance = value;
+                }
+                PatternHatchField::LoopCount(incr) => {
+                    let value: u32 = *prev.loop_count + incr;
+                    debug!("Patching loop count for object #{} to {}", next_idx, value);
+                    *next.loop_count = value;
+                }
+            }
+        });
+    }
+}

+ 22 - 0
src/config/mod.rs

@@ -6,6 +6,7 @@ use self::{
     pen::{ClonePen, ImportExportPen, PatchPen, PatternPen, RandomizePen},
 };
 
+pub mod hatch;
 pub mod object;
 pub mod pen;
 
@@ -47,3 +48,24 @@ impl Operations for Vec<Operation> {
 pub struct Config {
     pub ops: Vec<Operation>,
 }
+
+/// Helper function that returns a window of two mutable elements
+pub fn double_window_mut<T, F>(slice: &mut [T], mut function: F)
+where
+    F: FnMut(&mut T, &mut T),
+{
+    for start in 0..(slice.len().saturating_sub(1)) {
+        let (a, b) = slice.split_at_mut(start + 1);
+        function(&mut a[a.len() - 1], &mut b[0])
+    }
+}
+
+#[test]
+fn test_double_window_mut() {
+    let mut data: Vec<u8> = vec![1, 2, 3, 4, 5];
+    double_window_mut(&mut data[..], |prev, next| {
+        println!("Prev: {}, Next: {}", prev, next);
+        *next = *next + *prev;
+    });
+    assert_eq!(data, vec![1, 3, 6, 10, 15]);
+}

+ 121 - 41
src/config/object.rs

@@ -16,7 +16,7 @@ use log::{debug, error, warn};
 use rand::{seq::SliceRandom, thread_rng};
 use serde::{Deserialize, Serialize};
 
-use super::pen::PatternField;
+use super::{hatch::PatternHatchField, pen::PatternPenField};
 
 #[derive(Debug, Serialize, Deserialize)]
 #[serde(rename_all = "PascalCase")]
@@ -85,7 +85,7 @@ impl From<HatchConfig> for HatchSetting {
             flags.set_average_distribute_line(1);
         }
 
-        let mut ret = Self::default();
+        let mut ret: HatchSetting = Self::default();
 
         *ret.line_spacing = value.line_spacing;
         value.pen.map(|x| *ret.pen = x.into());
@@ -114,8 +114,10 @@ pub struct ArrayConfig {
     spacing: f64,
     randomize_order: bool,
     starting_pen: usize,
-    pattern_x: Option<PatternField>,
-    pattern_y: Option<PatternField>,
+    pattern_pen_x: Option<PatternPenField>,
+    pattern_pen_y: Option<PatternPenField>,
+    pattern_hatch_x: Option<PatternHatchField>,
+    pattern_hatch_y: Option<PatternHatchField>,
 }
 
 #[derive(Debug, Serialize, Deserialize, strum::Display)]
@@ -170,24 +172,14 @@ impl InputObject {
 
 #[derive(Debug, Serialize, Deserialize)]
 #[serde(rename_all = "PascalCase")]
-pub struct ObjectOperation {
-    input: InputObject,
+pub struct ObjectModify {
     z: Option<f64>,
     origin: Option<Point>,
     pen: Option<u32>,
-    layer: Option<usize>,
-    array: Option<ArrayConfig>,
-    hatch: Option<HatchConfig>,
-    export: Option<PathBuf>,
-    replace_object: Option<usize>,
 }
 
-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);
-
+impl ObjectModify {
+    pub fn process(&self, object: &mut Object, pens: &mut Vec<Pen>) {
         // Process basic transformation
         if self.origin.is_some() || self.z.is_some() {
             debug!(
@@ -198,14 +190,34 @@ impl ObjectOperation {
         }
 
         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);
-            }
+           
         });
+    }
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+#[serde(rename_all = "PascalCase")]
+pub struct ObjectOperation {
+    input: InputObject,
+    modify: Option<ObjectModify>,
+    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);
+
+        // Modify object properties
+        self.modify.as_ref().map(|modify| modify.process(&mut object, pens));
 
         // Process conversion to hatch object
         let object = self.hatch.as_ref().map_or(object.clone(), |hatch| {
@@ -292,22 +304,65 @@ impl ObjectOperation {
                     }
                 }
 
-                // Generate pens
-                match &array.pattern_y {
-                    None => {
-                        if let Some(pattern_x) = &array.pattern_x {
-                            pattern_x.pattern(
+                if (array.pattern_hatch_x.is_some() && array.pattern_pen_x.is_some())
+                    || (array.pattern_hatch_y.is_some() && array.pattern_pen_y.is_some())
+                {
+                    panic!("Conflict with X or Y axis patterning options");
+                }
+
+                let pattern_x: bool =
+                    array.pattern_hatch_x.is_some() || array.pattern_pen_x.is_some();
+                let pattern_y: bool =
+                    array.pattern_hatch_y.is_some() || array.pattern_pen_y.is_some();
+
+                if let Some(pen_x) = &array.pattern_pen_x {
+                    if pattern_y {
+                        for y in 0..array.rows {
+                            pen_x.pattern(
                                 &mut pens
                                     .iter_mut()
                                     .enumerate()
                                     .skip(array.starting_pen)
-                                    .take(array.columns * array.rows),
-                            );
+                                    .skip(y * array.columns)
+                                    .take(array.columns),
+                            )
+                        }
+                    } else {
+                        pen_x.pattern(
+                            &mut pens
+                                .iter_mut()
+                                .enumerate()
+                                .skip(array.starting_pen)
+                                .take(array.columns * array.rows),
+                        );
+                    }
+                }
+
+                if let Some(hatch_x) = &array.pattern_hatch_x {
+                    if pattern_y {
+                        for y in 0..array.rows {
+                            hatch_x.pattern(
+                                &mut new_obj
+                                    .iter_mut()
+                                    .enumerate()
+                                    .skip(y * array.columns)
+                                    .take(array.columns),
+                            )
                         }
+                    } else {
+                        hatch_x.pattern(
+                            &mut new_obj
+                                .iter_mut()
+                                .enumerate()
+                                .take(array.columns * array.rows),
+                        );
                     }
-                    Some(pattern_y) => {
+                }
+
+                if let Some(pen_y) = &array.pattern_pen_y {
+                    if pattern_x {
                         for x in 0..array.columns {
-                            pattern_y.pattern(
+                            pen_y.pattern(
                                 &mut pens
                                     .iter_mut()
                                     .enumerate()
@@ -316,17 +371,42 @@ impl ObjectOperation {
                                     .take(array.rows),
                             );
                         }
-                        if let Some(pattern_x) = &array.pattern_x {
-                            for y in 0..array.rows {
-                                pattern_x.pattern(
-                                    &mut pens
-                                        .iter_mut()
-                                        .enumerate()
-                                        .skip(array.starting_pen)
-                                        .skip(y * array.columns)
-                                        .take(array.columns),
-                                )
-                            }
+                    } else {
+                        for x in 0..array.columns {
+                            pen_y.pattern(
+                                &mut pens
+                                    .iter_mut()
+                                    .enumerate()
+                                    .skip(x + array.starting_pen)
+                                    .step_by(array.columns)
+                                    .take(array.rows),
+                            );
+                        }
+                    }
+                }
+
+                if let Some(hatch_y) = &array.pattern_hatch_y {
+                    if pattern_x {
+                        for x in 0..array.columns {
+                            hatch_y.pattern(
+                                &mut new_obj
+                                    .iter_mut()
+                                    .enumerate()
+                                    .skip(x)
+                                    .step_by(array.columns)
+                                    .take(array.rows),
+                            );
+                        }
+                    } else {
+                        for x in 0..array.columns {
+                            hatch_y.pattern(
+                                &mut new_obj
+                                    .iter_mut()
+                                    .enumerate()
+                                    .skip(x)
+                                    .step_by(array.columns)
+                                    .take(array.rows),
+                            );
                         }
                     }
                 }

+ 67 - 91
src/config/pen.rs

@@ -10,6 +10,8 @@ use rand::{seq::SliceRandom, Rng};
 use serde::{Deserialize, Serialize};
 use strum::IntoEnumIterator;
 
+use super::double_window_mut;
+
 const SPEED_MIN: f64 = 0.0;
 const SPEED_MAX: f64 = 100000.0;
 const POWER_MIN: f64 = 0.0;
@@ -106,7 +108,7 @@ impl PatchPen {
         debug!("Patching pen #{}", self.pen);
         let pen: &mut Pen = pens.get_mut(self.pen).expect("Invalid pen index");
         self.patch.patch(pen);
-        pen.valid_settings();
+        pen.valid_settings(true);
     }
 }
 
@@ -169,7 +171,7 @@ impl ClonePen {
 }
 
 #[derive(Debug, Serialize, Deserialize)]
-pub enum PatternField {
+pub enum PatternPenField {
     Loops(i32),
     Speed(f64),
     Power(f64),
@@ -177,77 +179,48 @@ pub enum PatternField {
     PulseWidth(u32),
 }
 
-impl PatternField {
+impl PatternPenField {
     pub fn pattern(&self, pens: &mut dyn Iterator<Item = (usize, &mut Pen)>) {
-        // Obtain settings from source (first) pen
-        let (src_idx, src) = pens.next().expect("Pattern must involve at least one pen");
-
-        let mut setting: PatternField = match self {
-            PatternField::Loops(_) => {
-                debug!(
-                    "Initial loop count from pen #{} is {}",
-                    src_idx, *src.loop_count
-                );
-                PatternField::Loops((*src.loop_count).try_into().unwrap())
-            }
-            PatternField::Speed(_) => {
-                debug!("Initial speed from pen #{} is {}", src_idx, *src.speed);
-                PatternField::Speed(*src.speed)
-            }
-            PatternField::Power(_) => {
-                debug!("Initial power from pen #{} is {}", src_idx, *src.power);
-                PatternField::Power(*src.power)
-            }
-            PatternField::Frequency(_) => {
-                debug!(
-                    "Initial frequency from pen #{} is {}",
-                    src_idx, *src.frequency
-                );
-                PatternField::Frequency((*src.frequency).try_into().unwrap())
-            }
-            PatternField::PulseWidth(_) => {
-                debug!(
-                    "Initial pulse width from pen #{} is {}ns",
-                    src_idx, *src.pulse_width
-                );
-                PatternField::PulseWidth(*src.pulse_width)
-            }
-        };
-
-        for (idx, dst) in pens {
-            // Calculate new setting
-            setting = match (setting, self) {
-                (PatternField::Loops(prev), PatternField::Loops(incr)) => {
-                    let value: i32 = prev + incr;
-                    debug!("Patching loop count for pen #{} to {}", idx, value);
+        let mut pens: Vec<(usize, &mut Pen)> = pens.collect_vec();
+
+        double_window_mut(&mut pens[..], |prev, next| {
+            let _prev_idx: usize = prev.0;
+            let prev: &mut Pen = prev.1;
+            let next_idx: usize = next.0;
+            let next: &mut Pen = next.1;
+
+            match self {
+                PatternPenField::Loops(incr) => {
+                    let value: i32 = i32::try_from(*prev.loop_count).unwrap() + incr;
+                    debug!("Patching loop count for pen #{} to {}", next_idx, value);
                     assert!(value > 0, "Pen loop count must be greater than zero");
-                    PatternField::Loops(value)
+                    *next.loop_count = value.try_into().unwrap()
                 }
-                (PatternField::Speed(prev), PatternField::Speed(incr)) => {
-                    let value: f64 = prev + incr;
-                    debug!("Patching speed for pen #{} to {}", idx, value);
+                PatternPenField::Speed(incr) => {
+                    let value: f64 = *prev.speed + incr;
+                    debug!("Patching speed for pen #{} to {}", next_idx, value);
                     assert!(
                         value > SPEED_MIN && value <= SPEED_MAX,
                         "Pen speed must be between {} and {}",
                         SPEED_MIN,
                         SPEED_MAX
                     );
-                    PatternField::Speed(value)
+                    *next.speed = value;
                 }
-                (PatternField::Power(prev), PatternField::Power(incr)) => {
-                    let value: f64 = prev + incr;
-                    debug!("Patching power for pen #{} to {}", idx, value);
+                PatternPenField::Power(incr) => {
+                    let value: f64 = *prev.power + incr;
+                    debug!("Patching power for pen #{} to {}", next_idx, value);
                     assert!(
                         value > POWER_MIN && value <= POWER_MAX,
                         "Pen power must be between {} and {}",
                         POWER_MIN,
                         POWER_MAX
                     );
-                    PatternField::Power(value)
+                    *next.power = value;
                 }
-                (PatternField::Frequency(prev), PatternField::Frequency(incr)) => {
-                    let value: i32 = prev + incr;
-                    debug!("Patching frequency for pen #{} to {}", idx, value);
+                PatternPenField::Frequency(incr) => {
+                    let value: i32 = i32::try_from(*prev.frequency).unwrap() + incr;
+                    debug!("Patching frequency for pen #{} to {}", next_idx, value);
                     assert!(
                         value >= FREQUENCY_MIN.try_into().unwrap()
                             && value <= FREQUENCY_MAX.try_into().unwrap(),
@@ -255,45 +228,31 @@ impl PatternField {
                         FREQUENCY_MIN,
                         FREQUENCY_MAX
                     );
-                    PatternField::Frequency(value)
+                    *next.frequency = value.try_into().unwrap();
+                    *next.frequency_2 = value.try_into().unwrap();
                 }
-                (PatternField::PulseWidth(prev), PatternField::PulseWidth(incr)) => {
+                PatternPenField::PulseWidth(incr) => {
                     let mut pw = PulseWidth::iter();
                     let _ = pw
-                        .find(|x| u32::from(*x) == prev)
+                        .find(|x| u32::from(*x) == *prev.pulse_width)
                         .expect("Unknown pulse width");
 
                     let mut pw = pw.skip((*incr - 1).try_into().unwrap());
-                    let next: u32 = pw.next().expect("Pulse width out of bounds").into();
-                    debug!("Patching pulse width for pen #{} to {}ns", idx, next);
-                    PatternField::PulseWidth(next)
-                }
-                _ => unreachable!(),
-            };
-
-            // Patch updated value
-            match setting {
-                PatternField::Loops(x) => *dst.loop_count = x.try_into().unwrap(),
-                PatternField::Speed(x) => *dst.speed = x,
-                PatternField::Power(x) => *dst.power = x,
-                PatternField::Frequency(x) => {
-                    *dst.frequency = x.try_into().unwrap();
-                    *dst.frequency_2 = x.try_into().unwrap();
-                }
-                PatternField::PulseWidth(x) => {
-                    *dst.pulse_width = x;
-                    *dst.pulse_width_2 = x.try_into().unwrap();
+                    let pw: u32 = pw.next().expect("Pulse width out of bounds").into();
+                    debug!("Patching pulse width for pen #{} to {}ns", next_idx, next);
+                    *next.pulse_width = pw;
+                    *next.pulse_width_2 = pw.try_into().unwrap();
                 }
             }
 
             // Randomize pen color
-            *dst.color = Rgba::random().into();
+            *next.color = Rgba::random().into();
 
             // Always enable custom settings for pen
-            *dst.use_default = 0;
+            *next.use_default = 0;
 
-            dst.valid_settings();
-        }
+            next.valid_settings(true);
+        });
     }
 }
 
@@ -302,7 +261,7 @@ impl PatternField {
 pub struct PatternPen {
     index: usize,
     count: usize,
-    field: PatternField,
+    field: PatternPenField,
 }
 
 impl PatternPen {
@@ -355,6 +314,8 @@ pub struct RandomizePen {
     power: Option<(f64, f64, f64)>,
     frequency: Option<(u32, u32, u32)>,
     pulse_width: Option<(PulseWidth, PulseWidth, usize)>,
+    power_density: Option<(u64, u64)>,
+    enforce_limits: Option<bool>,
 }
 
 impl RandomizePen {
@@ -420,17 +381,32 @@ impl RandomizePen {
                     setting.pulse_width = Some(*width);
                 }
 
+                setting.apply(pen);
+
+                // Check if power density value is within range
+                if let Some((range_min, range_max)) = self.power_density {
+                    let power_density: u64 = pen.power_density();
+                    if power_density < range_min || power_density > range_max {
+                        debug!("Retrying (power density {power_density} out of range)");
+                        continue;
+                    }
+                }
+
+                // Check settings
+                if self.enforce_limits.unwrap_or(true) {
+                    debug!("Checking settings");
+                    if !pen.valid_settings(false) {
+                        debug!("Retrying (invalid setting)");
+                        continue;
+                    }
+                }
+
                 // Check for duplicates
                 if !generated.contains(&setting) {
                     generated.push(setting);
-                    setting.apply(pen);
-                    if !pen.valid_settings() {
-                        debug!("Retrying..");
-                    } else {
-                        break;
-                    }
+                    break;
                 } else {
-                    debug!("Duplicate random setting");
+                    debug!("Retrying (duplicate setting)");
                 }
 
                 // Fail out if max attempts reached (insufficient search space)
@@ -442,7 +418,7 @@ impl RandomizePen {
                 }
             }
 
-            pen.valid_settings();
+            pen.valid_settings(true);
         }
     }
 }

+ 4 - 4
src/ezcad/objects/circle.rs

@@ -7,6 +7,7 @@ use log::warn;
 use crate::{
     field_of::FieldOf,
     types::{ObjectType, Point, F64, U32},
+    FP,
 };
 
 use super::{ObjectCore, ObjectModifier, Translate};
@@ -31,17 +32,16 @@ impl Display for Circle {
             *self.drawn_origin - Point::from(*self.radius),
             *self.drawn_origin + Point::from(*self.radius),
         );
-
-        if format!("{}", origin) != format!("{}", *self.core.origin) {
+        if origin != *self.core.origin {
             warn!(
-                "Origin mismatch! Core: {}, Calculated: {}",
+                "Origin mismatch! Core: {:.FP$}, Calculated: {:.FP$}",
                 *self.core.origin, origin
             );
         }
 
         write!(
             f,
-            "{}, Origin: {}, Width : {:.2}, Height: {:.2}, Start Radian: {:.2}, Clockwise: {}",
+            "{}, Origin: {}, Width : {:.FP$}, Height: {:.FP$}, Start Radian: {:.FP$}, Clockwise: {}",
             self.core,
             origin,
             width,

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

@@ -7,6 +7,7 @@ use log::warn;
 use crate::{
     field_of::FieldOf,
     types::{ObjectType, Point, F64, U32},
+    FP,
 };
 
 use super::{ObjectCore, ObjectModifier, Translate};
@@ -33,16 +34,16 @@ impl Display for Ellipse {
             .modifier
             .corrected(*self.drawn_corner_a, *self.drawn_corner_b);
 
-        if format!("{}", origin) != format!("{}", *self.core.origin) {
+        if origin != *self.core.origin {
             warn!(
-                "Origin mismatch! Core: {}, Calculated: {}",
+                "Origin mismatch! Core: {:.FP$}, Calculated: {:.FP$}",
                 *self.core.origin, origin
             );
         }
 
         write!(
             f,
-            "{}, Origin: {}, Width: {:.2}, Height: {:.2}, Start Radian: {:.2}, End Radian: {:.2}, Open Curve: {}", 
+            "{}, Origin: {}, Width: {:.FP$}, Height: {:.FP$}, Start Radian: {:.FP$}, End Radian: {:.FP$}, Open Curve: {}", 
             self.core,
             origin,
             width,

+ 21 - 2
src/ezcad/objects/hatch.rs

@@ -3,7 +3,9 @@ use std::fmt::{Debug, Display};
 use crate::{
     array_of::ArrayOf,
     field_of::FieldOf,
+    pen::Pen,
     types::{Field, Point, F64, U32},
+    FP,
 };
 use binrw::{binrw, BinRead, BinWrite};
 use diff::Diff;
@@ -410,7 +412,7 @@ impl Display for Hatch {
             if setting.enabled.into() {
                 write!(
                     f,
-                    "\nHatch #{}: All Calc: {}, Follow Edge Once: {}, Cross Hatch: {}, Pattern: {}, Angle: {:.2}, Pen: {}, Count: {}, Line Space: {:.2}, Avg Distribte Line: {}",
+                    "\nHatch #{}: All Calc: {}, Follow Edge Once: {}, Cross Hatch: {}, Pattern: {}, Angle: {:.FP$}, Pen: {}, Count: {}, Line Space: {:.FP$}, Avg Distribte Line: {}",
                     index,
                     setting.flags.all_calc() != 0,
                     setting.flags.follow_edge_once() != 0,
@@ -424,7 +426,7 @@ impl Display for Hatch {
                 )?;
                 write!(
                     f,
-                    "\n          Edge Offset: {:.2}, Start Offset: {:.2}, End Offset: {:.2}, Line Reduction: {:.2}, Loop Count: {}, Loop Distance: {:.2}, Auto Rotate: {}, Auto Rotate Angle: {:.2}",
+                    "\n          Edge Offset: {:.FP$}, Start Offset: {:.FP$}, End Offset: {:.FP$}, Line Reduction: {:.FP$}, Loop Count: {}, Loop Distance: {:.FP$}, Auto Rotate: {}, Auto Rotate Angle: {:.FP$}",
                     *setting.edge_offset,
                     *setting.start_offset,
                     *setting.end_offset,
@@ -482,3 +484,20 @@ impl Translate for Hatch {
         self.core.move_relative(delta, z);
     }
 }
+
+impl Hatch {
+    /// Estimate the power density from the hatch's line spacing and pen speed/frequency/power/pulse
+    pub fn calc_power_density(&self, pens: &Vec<Pen>) -> u64 {
+        let pen: &Pen = pens
+            .get(*self.core.pen as usize)
+            .expect("Invalid pen index");
+        let line_spacing: f64 = *self
+            .hatch_settings
+            .iter()
+            .find(|h| h.enabled.into())
+            .expect("Hatch object does not have enabled settings")
+            .line_spacing;
+
+        (1.0 / line_spacing) as u64 * pen.power_density()
+    }
+}

+ 2 - 1
src/ezcad/objects/mod.rs

@@ -17,6 +17,7 @@ use modular_bitfield::{
 use crate::{
     field_of::FieldOf,
     types::{Field, ObjectType, Point, WString, F64, U16, U32},
+    FP,
 };
 
 use self::{
@@ -79,7 +80,7 @@ impl Display for ObjectCore {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
         write!(
             f,
-            "Type: {}, Enabled: {}, Pen: {}, Count: {}, Z: {:.2}",
+            "Type: {}, Enabled: {}, Pen: {}, Count: {}, Z: {:.FP$}",
             self.obj_type,
             (self.flags.disabled() == 0),
             self.pen,

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

@@ -7,6 +7,7 @@ use log::warn;
 use crate::{
     field_of::FieldOf,
     types::{ObjectType, Point, F64, U32},
+    FP,
 };
 
 use super::{ObjectCore, ObjectModifier, Translate};
@@ -35,16 +36,16 @@ impl Display for Polygon {
             .modifier
             .corrected(*self.drawn_corner_a, *self.drawn_corner_b);
 
-        if format!("{}", origin) != format!("{}", *self.core.origin) {
+        if origin != *self.core.origin {
             warn!(
-                "Origin mismatch! Core: {}, Calculated: {}",
+                "Origin mismatch! Core: {:.FP$}, Calculated: {:.FP$}",
                 *self.core.origin, origin
             );
         }
 
         write!(
             f,
-            "{}, Origin: {}, Width: {:.2}, Height: {:.2}, Inverted: {}, Offset CX: {:.2}, Offset CY: {:.2}, Offset DX: {:.2}, Offset: DY: {:.2}, Edges: {}",
+            "{}, Origin: {}, Width: {:.FP$}, Height: {:.FP$}, Inverted: {}, Offset CX: {:.FP$}, Offset CY: {:.FP$}, Offset DX: {:.FP$}, Offset: DY: {:.FP$}, Edges: {}",
             self.core,
             origin,
             width,

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

@@ -7,6 +7,7 @@ use log::warn;
 use crate::{
     field_of::FieldOf,
     types::{ObjectType, Point, F64},
+    FP,
 };
 
 use super::{ObjectCore, ObjectModifier, Translate};
@@ -33,16 +34,16 @@ impl Display for Rectangle {
             .modifier
             .corrected(*self.drawn_corner_a, *self.drawn_corner_b);
 
-        if format!("{}", origin) != format!("{}", *self.core.origin) {
+        if origin != *self.core.origin {
             warn!(
-                "Origin mismatch! Core: {}, Calculated: {}",
+                "Origin mismatch! Core: {:.FP$}, Calculated: {:.FP$}",
                 *self.core.origin, origin
             );
         }
 
         write!(
             f,
-            "{}, Origin: {}, Width: {:.2}, Height: {:.2}",
+            "{}, Origin: {}, Width: {:.FP$}, Height: {:.FP$}",
             self.core, origin, width, height,
         )
     }

+ 34 - 15
src/ezcad/pen.rs

@@ -9,10 +9,12 @@ use binrw::{BinRead, BinWrite, BinWriterExt, FilePtr64};
 use diff::Diff;
 use human_repr::HumanCount;
 use log::{error, warn};
+use num_enum::TryFromPrimitive;
 
 use crate::{
     field_of::FieldOf,
     types::{Bool, Field, PulseWidth, Rgba, WString, WobbleType, F64, U32},
+    FP,
 };
 
 #[derive(BinRead, Debug)]
@@ -123,12 +125,12 @@ impl Pen {
             .expect("Failed to write to output file");
     }
 
-    pub fn valid_settings(&self) -> bool {
+    pub fn valid_settings(&self, warn_limits: bool) -> bool {
         let mut ret: bool = true;
 
         if *self.frequency != *self.frequency_2 as u32 {
             error!(
-                "Mismatch pen internal frequency setting: ({}, {:.3})",
+                "Mismatch pen internal frequency setting: ({}, {:.FP$})",
                 *self.frequency, *self.frequency_2
             );
             ret = false;
@@ -136,7 +138,7 @@ impl Pen {
 
         if *self.pulse_width != *self.pulse_width_2 as u32 {
             error!(
-                "Mismatch pen internal pulse width setting: ({}, {:.3})",
+                "Mismatch pen internal pulse width setting: ({}, {:.FP$})",
                 *self.pulse_width, *self.pulse_width_2
             );
             ret = false;
@@ -145,19 +147,23 @@ impl Pen {
         match PulseWidth::try_from(*self.pulse_width) {
             Ok(pw) => match *self.frequency {
                 freq if freq < pw.min_freq() => {
-                    warn!(
-                        "Pen frequency of {} lower than pulse width minimum frequency of {}",
-                        *self.frequency,
-                        pw.min_freq()
-                    );
+                    if warn_limits {
+                        warn!(
+                            "Pen frequency of {} lower than pulse width minimum frequency of {}",
+                            *self.frequency,
+                            pw.min_freq()
+                        );
+                    }
                     ret = false;
                 }
                 freq if freq > pw.max_freq() => {
-                    warn!(
-                        "Pen frequency of {} higher than pulse width maximum frequency of {}",
-                        *self.frequency,
-                        pw.max_freq()
-                    );
+                    if warn_limits {
+                        warn!(
+                            "Pen frequency of {} higher than pulse width maximum frequency of {}",
+                            *self.frequency,
+                            pw.max_freq()
+                        );
+                    }
                     ret = false;
                 }
                 _ => (),
@@ -170,17 +176,30 @@ impl Pen {
 
         ret
     }
+
+    /// Estimate the power density from the speed/frequency/power/pulse
+    pub fn power_density(&self) -> u64 {
+        let power: f64 = *self.power;
+        let speed: f64 = *self.speed;
+        let frequency: f64 = f64::from(*self.frequency);
+        let pulse: f64 = PulseWidth::try_from_primitive(*self.pulse_width)
+            .expect("Invalid pen pulse width")
+            .power_ratio();
+
+        (power * (1.0 / speed) * frequency * pulse) as u64
+    }
 }
 
 impl std::fmt::Display for Pen {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
         write!(
             f,
-            "Speed: {}mm/s, Frequency: {}, Power: {}%, Pulse Width: {}ns",
+            "Speed: {}mm/s, Frequency: {}, Power: {}%, Pulse Width: {}ns, Power Density: {}",
             self.speed,
             self.frequency.human_count("Hz"),
             self.power,
-            self.pulse_width
+            self.pulse_width,
+            self.power_density()
         )
     }
 }

+ 34 - 5
src/ezcad/types.rs

@@ -5,13 +5,14 @@ use std::{
 
 use binrw::{binrw, BinRead, BinWrite};
 use diff::{Diff, VecDiff};
+use float_cmp::approx_eq;
 use num_enum::{IntoPrimitive, TryFromPrimitive};
 use rand::{thread_rng, Rng};
 use serde::{Deserialize, Serialize};
 use serde_repr::{Deserialize_repr, Serialize_repr};
 use strum::EnumIter;
 
-use crate::{array_of::ArrayOfPrimitive, field_of::FieldOf};
+use crate::{array_of::ArrayOfPrimitive, field_of::FieldOf, FP};
 
 /// Generic field with structure of length + data
 pub type Field = ArrayOfPrimitive<u8>;
@@ -127,9 +128,7 @@ impl Rgba {
     }
 }
 
-#[derive(
-    Copy, Clone, Debug, Default, Diff, PartialEq, BinRead, BinWrite, Serialize, Deserialize,
-)]
+#[derive(Copy, Clone, Debug, Default, Diff, BinRead, BinWrite, Serialize, Deserialize)]
 #[diff(attr(
     #[derive(Debug, PartialEq)]
 ))]
@@ -139,9 +138,15 @@ pub struct Point {
     pub y: f64,
 }
 
+impl PartialEq for Point {
+    fn eq(&self, other: &Self) -> bool {
+        approx_eq!(f64, self.x, other.x) && approx_eq!(f64, self.y, other.y)
+    }
+}
+
 impl Display for Point {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        write!(f, "({:.2}, {:.2})", self.x, self.y)
+        write!(f, "({:.FP$}, {:.FP$})", self.x, self.y)
     }
 }
 
@@ -356,4 +361,28 @@ impl PulseWidth {
             PulseWidth::Ns500 => 500_000,
         }
     }
+
+    /// Ratio of total output power per pulse from the area under the curve of the pulse
+    /// waveform over time. Pulses below 20ns are assumed to be linear discrete pulses
+    /// with no decay over time, while longer pulse widths will follow natural log profile
+    pub fn power_ratio(&self) -> f64 {
+        match self {
+            PulseWidth::Ns2 => 1.0,
+            PulseWidth::Ns4 => 3.17,
+            PulseWidth::Ns6 => 5.34,
+            PulseWidth::Ns8 => 7.51,
+            PulseWidth::Ns12 => 11.85,
+            PulseWidth::Ns20 => 17.29,
+            PulseWidth::Ns30 => 22.04,
+            PulseWidth::Ns45 => 26.78,
+            PulseWidth::Ns60 => 30.17,
+            PulseWidth::Ns80 => 33.54,
+            PulseWidth::Ns100 => 36.16,
+            PulseWidth::Ns150 => 40.91,
+            PulseWidth::Ns200 => 44.28,
+            PulseWidth::Ns250 => 46.90,
+            PulseWidth::Ns350 => 50.84,
+            PulseWidth::Ns500 => 55.03,
+        }
+    }
 }

+ 6 - 3
src/lib.rs

@@ -1,3 +1,6 @@
-pub mod ezcad;
-
-pub use ezcad::*;
+pub mod ezcad;
+
+pub use ezcad::*;
+
+// Precision when comparing/printing float values
+pub const FP: usize = 3;

+ 95 - 55
src/main.rs

@@ -2,17 +2,20 @@ use std::{
     fs::File,
     io::{Cursor, Read, Write},
     ops::RangeInclusive,
-    path::PathBuf,
+    path::{Path, PathBuf},
     time::Instant,
 };
 
 use binrw::{BinRead, BinWrite, BinWriterExt};
 use clap::{error::ErrorKind, Args, Error, Parser, Subcommand};
 use clap_verbosity_flag::{InfoLevel, Verbosity};
+use dialoguer::Confirm;
 use diff::Diff;
 use env_logger::Target;
-use ezcad::{file::EzCadHeader, layer::Layer, objects::Object};
+use ezcad::{file::EzCadHeader, layer::Layer, objects::Object, pen::Pen};
+use itertools::Itertools;
 use log::{info, trace, warn};
+use num_format::{Locale, ToFormattedString};
 use regex::Regex;
 
 use crate::config::{Config, Operations};
@@ -25,7 +28,7 @@ struct Cli {
     command: SubCommands,
 
     /// Input .mlp file to parse
-    #[arg(short, long)]
+    #[arg(short = 'i', long)]
     input: PathBuf,
 
     #[command(flatten)]
@@ -43,7 +46,7 @@ enum SubCommands {
 #[derive(Debug, Args)]
 struct DiffCmd {
     /// File to diff input against
-    #[arg(short, long)]
+    #[arg(short = 'd', long)]
     diff_file: PathBuf,
 }
 
@@ -51,17 +54,17 @@ struct DiffCmd {
 #[derive(Debug, Args)]
 struct QueryCmd {
     /// Print info for pens
-    #[arg(short, long)]
+    #[arg(short = 'p', long)]
     #[arg(value_parser = parse_range)]
     pen: Option<RangeInclusive<usize>>,
 
     /// Print info for objects
-    #[arg(short, long)]
+    #[arg(short = 'o', long)]
     #[arg(value_parser = parse_range)]
     object: Option<RangeInclusive<usize>>,
 
     /// Object layer to query object on
-    #[arg(short = 'b', long)]
+    #[arg(short = 'l', long)]
     object_layer: Option<usize>,
 }
 
@@ -69,12 +72,16 @@ struct QueryCmd {
 #[derive(Debug, Args)]
 struct ApplyConfig {
     /// Configuration file
-    #[arg(short, long)]
+    #[arg(short = 'c', long)]
     config: PathBuf,
 
     /// Output file to write to
-    #[arg(short, long)]
+    #[arg(short = 'o', long)]
     output: Option<PathBuf>,
+
+    /// Overwrite output file if it exists
+    #[arg(short = 'w', long)]
+    overwrite: bool,
 }
 
 /// Helper function to parse a string as an RangeInclusive<usize>
@@ -181,15 +188,10 @@ fn main() {
             );
         }
         SubCommands::Query(args) => {
+            let pens: &Vec<Pen> = &file.pens_offset.data.pens;
+
             // Print info on pens with non-default settings
-            for (index, pen) in file
-                .pens_offset
-                .data
-                .pens
-                .iter()
-                .filter(|x| *x.use_default == 0)
-                .enumerate()
-            {
+            for (index, pen) in pens.iter().filter(|x| *x.use_default == 0).enumerate() {
                 trace!("Pen {}: {:#?}", index, pen);
             }
 
@@ -211,45 +213,69 @@ fn main() {
                     info!(
                         "Pen #{}: {}",
                         pen,
-                        file.pens_offset
-                            .data
-                            .pens
-                            .get(pen)
-                            .expect("Invalid pen index")
+                        pens.get(pen).expect("Invalid pen index")
                     );
                 }
             });
 
             // Process object query
-            args.object.map(|obj_range| {
-                warn!("Object origin, width, and height values may be incorrect if object is skewed or rotated");
-
-                let layer_index: usize = args.object_layer.unwrap_or(0);
-                let layer: &Layer = file
-                    .layers_offset
-                    .get(layer_index)
-                    .expect("Invalid layer index");
-
-                for object_index in obj_range {
-                    let object: &Object = layer
-                        .objects
-                        .get(object_index)
-                        .expect("Invalid object index");
-                    let pen_index: u32 = *object.core().pen;
-                    info!(
-                        "Layer #{}, Object #{}:\n{}\nPen: #{}: {}",
-                        layer_index,
-                        object_index,
-                        object,
-                        pen_index,
-                        file.pens_offset
-                            .data
-                            .pens
-                            .get(pen_index as usize)
-                            .expect("Invalid pen index in object")
-                    );
-                }
+            let layer_idx: usize = args.object_layer.unwrap_or_else(|| {
+                (*file.layers_offset)
+                    .iter()
+                    .find_position(|x| x.objects.len() != 0)
+                    .expect("No objects defined in any layer")
+                    .0
             });
+            let layer: &Layer = file
+                .layers_offset
+                .get(layer_idx)
+                .expect("Invalid layer index");
+
+            warn!("Object origin, width, and height values may be incorrect if object is skewed or rotated");
+            let objects: Vec<(usize, &Object)> = if let Some(obj_range) = args.object {
+                obj_range
+                    .clone()
+                    .zip(
+                        layer
+                            .objects
+                            .get(obj_range)
+                            .expect("Invalid object query range")
+                            .iter(),
+                    )
+                    .collect_vec()
+            } else {
+                layer.objects.iter().enumerate().collect_vec()
+            };
+
+            for (obj_idx, object) in objects {
+                let pen_idx: u32 = *object.core().pen;
+                let pen: &Pen = pens
+                    .get(pen_idx as usize)
+                    .expect("Invalid pen index in object");
+
+                // Calculate estimated power density if object is hatched
+                let hatch_power: Option<String> = match object {
+                    Object::Hatch(hatch) => Some(
+                        hatch
+                            .calc_power_density(pens)
+                            .to_formatted_string(&Locale::en),
+                    ),
+                    _ => None,
+                };
+
+                // Print info
+                info!(
+                    "Layer #{}, Object #{}:\n{}\nPen: #{}: {}{}",
+                    layer_idx,
+                    obj_idx,
+                    object,
+                    pen_idx,
+                    pen,
+                    hatch_power.map_or(String::new(), |p| format!(
+                        "\nEstimated hatch power density: {p}"
+                    ))
+                );
+            }
         }
         SubCommands::Apply(args) => {
             // Process config
@@ -268,7 +294,6 @@ fn main() {
 
             // Process output
             args.output.map(|output| {
-                info!("Writing output file '{}'", output.to_string_lossy());
                 // Serialize to memory buffer for perf
                 let mut buffer: Cursor<Vec<u8>> = Cursor::new(vec![]);
                 let time: Instant = Instant::now();
@@ -278,10 +303,25 @@ fn main() {
                 trace!("Output file encode time: {:?}", time.elapsed());
 
                 // Write buffer to output file
-                let mut output: File = File::create(output).expect("Failed to open output file");
-                output
-                    .write_all(buffer.into_inner().as_slice())
-                    .expect("Failed to write to output file");
+                if !Path::new(&output).exists()
+                    || args.overwrite
+                    || Confirm::new()
+                        .with_prompt(format!(
+                            "File '{}' exists! Overwrite?",
+                            &output.to_string_lossy()
+                        ))
+                        .interact()
+                        .unwrap()
+                {
+                    info!("Writing output file '{}'", output.to_string_lossy());
+                    let mut output: File =
+                        File::create(output).expect("Failed to open output file");
+                    output
+                        .write_all(buffer.into_inner().as_slice())
+                        .expect("Failed to write to output file");
+                } else {
+                    warn!("Skipping write due to file conflict");
+                }
             });
         }
     }

BIN
test.mlp


BIN
test2.mlp


+ 91 - 0
test2.yml

@@ -0,0 +1,91 @@
+Ops: 
+  - !PatchPen
+    Pen: 0
+    Speed: 500
+    Frequency: 50000
+    Power: 30.0
+    PulseWidth: 2
+
+  - !ClonePen
+    From: 0
+    To: 255
+    Inclusive: True
+
+  # - !PatchPen
+  #   Pen: 0
+  #   Power: 10.0
+
+  # - !PatternPen
+  #   From: 0
+  #   To: 40
+  #   Field: !Power 2
+
+  # - !DeleteObjects
+  #   Layer: 0
+
+  # - !HatchArray
+  #   Layer: 0
+  #   Width: 3.0
+  #   Height: 2.0
+  #   Columns: 8
+  #   Rows: 5
+  #   Spacing: 0.5
+  #   Z: 0.0
+  #   StartingPen: 0
+  #   Hatch:
+  #     LineSpacing: 0.1
+
+  # - !Object
+  #   Input: !Existing { Layer: 0, Object: 0 }
+  #   Export: 'export.bin'
+
+  - !Object
+    # Input: !Rectangle { Width: 2, Height: 3 }
+    Input: !Existing { Layer: 0, Object: 0 }
+    Z: 0.8
+    # Origin: { X: 10.0, Y: 10.0 }
+    # Pen: 0
+    Array:
+      Columns: 15
+      Rows: 10
+      Spacing: 2.3
+      RandomizeOrder: True
+      StartingPen: 0
+      # PatternX: !Frequency 5000
+      # PatternY: !Power 1.0
+      # PatternX: !PulseWidth 1
+      # PatternY: !Frequency 10000
+    # Hatch:
+    #   LineSpacing: 0.01
+    ReplaceObject: 0
+
+  - !RandomizePen
+    Index: 0
+    Count: 30
+    Speed: [100, 1000, 100] # [min, max, step]
+    Power: [10, 100, 5] # [min, max, step]
+    Frequency: [20000, 1000000, 1000] # [min, max, step]
+    PulseWidth: [2, 80, 2] # [min, max, step]
+
+  # - !Object
+  #   Input: !Rectangle { Width: 10, Height: 5}
+  #   Z: 0.0
+  #   Origin: { X: 10.0, Y: 10.0 }
+  #   Pen: 0
+  #   ReplaceObject: 0
+
+  # - !Object
+  #   Input: !Existing { Layer: 0, Object: 0 }
+  #   Origin: { X: 10.0, Y: 10.0 }
+  #   ReplaceObject: 0
+
+  # - !Object
+  #   Input: !Existing { Layer: 0, Object: 1 }
+  #   Origin: { X: 10.0, Y: 10.0 }
+  #   ReplaceObject: 1
+
+  # - !Object
+  #   Input: !Circle { Radius: 3.0 }
+
+  # - !Object
+  #   Input: !Import { Path: 'export.bin' }