16 Commits 8db860afbd ... 4401ff1da2

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