diff --git a/Cargo.lock b/Cargo.lock index 410b727..f411cae 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -18,12 +18,27 @@ dependencies = [ "winapi", ] +[[package]] +name = "arrayref" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544" + [[package]] name = "arrayvec" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" +[[package]] +name = "ascii-canvas" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff8eb72df928aafb99fe5d37b383f2fe25bd2a765e3e5f7c365916b6f2463a29" +dependencies = [ + "term", +] + [[package]] name = "atty" version = "0.2.14" @@ -41,12 +56,50 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" +[[package]] +name = "base64" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" + +[[package]] +name = "bit-set" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e11e16035ea35e4e5997b393eacbf6f63983188f7a2ad25bfb13465f5ad59de" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + [[package]] name = "bitflags" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" +[[package]] +name = "blake2b_simd" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afa748e348ad3be8263be728124b24a24f268266f6f5d58af9d75f6a40b5c587" +dependencies = [ + "arrayref", + "arrayvec", + "constant_time_eq", +] + +[[package]] +name = "byteorder" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae44d1a3d5a19df61dd0c8beb138458ac2a53a7ac09eba97d55592540004306b" + [[package]] name = "cfg-if" version = "1.0.0" @@ -68,11 +121,101 @@ dependencies = [ "vec_map", ] +[[package]] +name = "codespan" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "991d34632921756cbe9e3e1736b2e1f12f16166a9c9cd91979d96d4c0a086b63" +dependencies = [ + "codespan-reporting", +] + +[[package]] +name = "codespan-reporting" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6ce42b8998a383572e0a802d859b1f00c79b7b7474e62fff88ee5c2845d9c13" +dependencies = [ + "termcolor", + "unicode-width", +] + +[[package]] +name = "constant_time_eq" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" + +[[package]] +name = "crossbeam-utils" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02d96d1e189ef58269ebe5b97953da3274d83a93af647c2ddd6f9dab28cedb8d" +dependencies = [ + "autocfg", + "cfg-if", + "lazy_static", +] + +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + +[[package]] +name = "ctor" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8f45d9ad417bcef4817d614a501ab55cdd96a6fdb24f49aab89a54acfd66b19" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "diff" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e25ea47919b1560c4e3b7fe0aaab9becf5b84a10325ddf7db0f0ba5e1026499" + +[[package]] +name = "difference" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198" + +[[package]] +name = "dirs" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fd78930633bd1c6e35c4b42b1df7b0cbc6bc191146e512bb3bedf243fcc3901" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "either" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" + +[[package]] +name = "ena" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7402b94a93c24e742487327a7cd839dc9d36fec9de9fb25b09f2dae459f36c3" +dependencies = [ + "log", +] + [[package]] name = "env_logger" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f26ecb66b4bdca6c1409b40fb255eefc2bd4f6d135dab3c3124f80ffa2a9661e" +checksum = "17392a012ea30ef05a610aa97dfb49496e71c9f676b27879922ea5bdf60d9d3f" dependencies = [ "atty", "humantime", @@ -102,12 +245,48 @@ version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75224bec9bfe1a65e2d34132933f2de7fe79900c96a0174307554244ece8150e" +[[package]] +name = "g-code" +version = "0.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f365da003f4befd25377840dbb6d49a3eca29e033ae2687e6d6d7460471aacdd" +dependencies = [ + "codespan", + "codespan-reporting", + "lalrpop", + "lalrpop-util", + "lazy_static", + "num", + "num-rational", + "paste", +] + +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "hashbrown" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04" +[[package]] +name = "heck" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cbf45460356b7deeb5e3415b5563308c0a9b057c85e12b06ad551f98d0a6ac" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "hermit-abi" version = "0.1.18" @@ -133,11 +312,54 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "itertools" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "284f18f85651fe11e8a991b2adb42cb078325c996ed026d994719efcfca1d54b" +dependencies = [ + "either", +] + +[[package]] +name = "lalrpop" +version = "0.19.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a71d75b267b3299da9ccff4dd80d73325b5d8adcd76fe97cf92725eb7c6f122" +dependencies = [ + "ascii-canvas", + "atty", + "bit-set", + "diff", + "ena", + "itertools", + "lalrpop-util", + "petgraph", + "regex", + "regex-syntax", + "string_cache", + "term", + "tiny-keccak", + "unicode-xid", +] + +[[package]] +name = "lalrpop-util" +version = "0.19.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ebbd90154472db6267a7d28ca08fea7788e5619fef10f2398155cb74c08f77a" + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + [[package]] name = "libc" -version = "0.2.84" +version = "0.2.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cca32fa0182e8c0989459524dc356b8f2b5c10f1b9eb521b7d182c03cf8c5ff" +checksum = "b7282d924be3275cec7f6756ff4121987bc6481325397dde6ba3e7802b1a8b1c" [[package]] name = "log" @@ -165,6 +387,66 @@ version = "2.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525" +[[package]] +name = "new_debug_unreachable" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54" + +[[package]] +name = "num" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b7a8e9be5e039e2ff869df49155f1c06bd01ade2117ec783e56ab0932b67a8f" +dependencies = [ + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-complex" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "747d632c0c558b87dbabbe6a82f3b4ae03720d0646ac5b7b4dae89394be5f2c5" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2021c8337a54d21aca0d59a92577a029af9431cb59b909b03252b9c164fad59" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12ac428b1cb17fce6f731001d307d351ec70a6d202fc2e60f7d4c5e42d8f4f07" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.14" @@ -181,23 +463,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13bd41f508810a131401606d54ac32a467c97172d74ba7662562ebba5ad07fa0" [[package]] -name = "paste" -version = "0.1.18" +name = "output_vt100" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45ca20c77d80be666aef2b45486da86238fabe33e38306bd3118fe4af33fa880" +checksum = "53cdc5b785b7a58c5aad8216b3dfa114df64b0b06ae6e1501cef91df2fbdf8f9" dependencies = [ - "paste-impl", - "proc-macro-hack", + "winapi", ] [[package]] -name = "paste-impl" -version = "0.1.18" +name = "paste" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d95a7db200b97ef370c8e6de0088252f7e0dfff7d047a28528e47456c0fc98b6" -dependencies = [ - "proc-macro-hack", -] +checksum = "c5d65c4d95931acda4498f675e332fcbdc9a06705cd07086c510e9b6009cd1c1" [[package]] name = "petgraph" @@ -210,10 +488,90 @@ dependencies = [ ] [[package]] -name = "proc-macro-hack" -version = "0.5.19" +name = "phf_shared" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7" +dependencies = [ + "siphasher 0.3.3", +] + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "pretty_assertions" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f81e1644e1b54f5a68959a29aa86cde704219254669da328ecfdf6a1f09d427" +dependencies = [ + "ansi_term", + "ctor", + "difference", + "output_vt100", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quote" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_syscall" +version = "0.1.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" +checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" + +[[package]] +name = "redox_users" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de0737333e7a9502c789a36d7c7fa6092a49895d4faa31ca5df163857ded2e9d" +dependencies = [ + "getrandom", + "redox_syscall", + "rust-argon2", +] [[package]] name = "regex" @@ -242,12 +600,42 @@ dependencies = [ "xmlparser", ] +[[package]] +name = "rust-argon2" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b18820d944b33caa75a71378964ac46f58517c92b6ae5f762636247c09e78fb" +dependencies = [ + "base64", + "blake2b_simd", + "constant_time_eq", + "crossbeam-utils", +] + [[package]] name = "siphasher" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b8de496cf83d4ed58b6be86c3a275b8602f6ffe98d3024a869e124147a9a3ac" +[[package]] +name = "siphasher" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa8f3741c7372e75519bd9346068370c9cdaabcc1f9599cbcf2a2719352286b7" + +[[package]] +name = "string_cache" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ddb1139b5353f96e429e1a5e19fbaf663bddedaa06d1dbd49f82e352601209a" +dependencies = [ + "lazy_static", + "new_debug_unreachable", + "phf_shared", + "precomputed-hash", +] + [[package]] name = "strsim" version = "0.8.0" @@ -255,16 +643,43 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" [[package]] -name = "svg2gcode" -version = "0.0.1" +name = "structopt" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5277acd7ee46e63e5168a80734c9f6ee81b1367a7d8772a2d765df2a3705d28c" dependencies = [ "clap", + "lazy_static", + "structopt-derive", +] + +[[package]] +name = "structopt-derive" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ba9cdfda491b814720b6b06e0cac513d922fc407582032e8706e9f137976f90" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "svg2gcode" +version = "0.0.2" +dependencies = [ + "codespan-reporting", "env_logger", + "euclid", + "g-code", "log", "lyon_geom", "paste", - "petgraph", + "pretty_assertions", "roxmltree", + "structopt", "svgtypes", "uom", ] @@ -276,7 +691,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c536faaff1a10837cfe373142583f6e27d81e96beba339147e77b67c9f260ff" dependencies = [ "float-cmp", - "siphasher", + "siphasher 0.2.3", +] + +[[package]] +name = "syn" +version = "1.0.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c700597eca8a5a762beb35753ef6b94df201c81cca676604f547495a0d7f0081" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "term" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd106a334b7657c10b7c540a0106114feadeb4dc314513e97df481d5d966f42" +dependencies = [ + "byteorder", + "dirs", + "winapi", ] [[package]] @@ -299,25 +736,46 @@ dependencies = [ [[package]] name = "thread_local" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8208a331e1cb318dd5bd76951d2b8fc48ca38a69f5f4e4af1b6a9f8c6236915" +checksum = "8018d24e04c95ac8790716a5987d0fec4f8b27249ffa0f7d33f1369bdfb88cbd" dependencies = [ "once_cell", ] +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + [[package]] name = "typenum" version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "373c8a200f9e67a0c95e62a4f52fbf80c23b4381c05a17845531982fa99e6b33" +[[package]] +name = "unicode-segmentation" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0d2e7be6ae3a5fa87eed5fb451aff96f2573d2694942e40543ae0bbe19c796" + [[package]] name = "unicode-width" version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" +[[package]] +name = "unicode-xid" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" + [[package]] name = "uom" version = "0.31.0" @@ -334,6 +792,18 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" +[[package]] +name = "version_check" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed" + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + [[package]] name = "winapi" version = "0.3.9" diff --git a/Cargo.toml b/Cargo.toml index bed4061..a4e7c73 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,17 +1,22 @@ [package] name = "svg2gcode" -version = "0.0.1" +version = "0.0.2" authors = ["Sameer Puri "] edition = "2018" -description = "Convert paths in SVG files to GCode for a pen plotter or laser engraver" +description = "Convert paths in SVG files to GCode for a pen plotter, laser engraver, or other machine." [dependencies] +g-code = "0.0.1" lyon_geom = "0" -clap = "2" +euclid = "0.22" +structopt = "0.3" log = "0" env_logger = "0" uom = "0.31.0" -paste = "0" roxmltree = "0" svgtypes = "0" -petgraph = "0" +codespan-reporting = "0.11" +paste = "1" + +[dev-dependencies] +pretty_assertions = "0.6" diff --git a/src/converter.rs b/src/converter.rs index 13077eb..e37215a 100644 --- a/src/converter.rs +++ b/src/converter.rs @@ -1,5 +1,6 @@ use std::str::FromStr; +use g_code::{command, emit::Token}; use lyon_geom::{ euclid::{default::Transform2D, Angle, Transform3D}, vector, @@ -9,9 +10,6 @@ use svgtypes::{ LengthListParser, PathParser, PathSegment, TransformListParser, TransformListToken, ViewBox, }; -#[macro_use] -use crate::*; -use crate::gcode::*; use crate::machine::*; use crate::turtle::*; @@ -36,15 +34,17 @@ impl Default for ProgramOptions { } } -pub fn svg2program(doc: &Document, options: ProgramOptions, mach: Machine) -> Vec { +pub fn svg2program(doc: &Document, options: ProgramOptions, mach: Machine) -> Vec { let mut turtle = Turtle::new(mach); - let mut program = vec![ - command!(CommandWord::UnitsMillimeters, {}), - command!(CommandWord::FeedRateUnitsPerMinute, {}), - ]; - program.append(&mut turtle.machine.program_begin()); - program.append(&mut turtle.machine.absolute()); + let mut program = command!(UnitsMillimeters {}) + .as_token_vec() + .drain(..) + .chain(command!(FeedRateUnitsPerMinute {}).as_token_vec()) + .collect::>(); + + program.extend(turtle.machine.program_begin()); + program.extend(turtle.machine.absolute()); program.append(&mut turtle.move_to(true, 0.0, 0.0)); // Depth-first SVG DOM traversal @@ -121,8 +121,11 @@ pub fn svg2program(doc: &Document, options: ProgramOptions, mach: Machine) -> Ve comment += " > "; }); comment += &node_name(&node); - program.push(command!(CommandWord::Comment(Box::new(comment)), {})); - program.append(&mut apply_path(&mut turtle, &options, d)); + program.push(Token::Comment { + is_inline: false, + inner: comment, + }); + program.extend(apply_path(&mut turtle, &options, d)); } else { warn!("There is a path node containing no actual path: {:?}", node); } @@ -139,11 +142,11 @@ pub fn svg2program(doc: &Document, options: ProgramOptions, mach: Machine) -> Ve // Critical step for actually moving the machine back to the origin, just in case SVG is malformed turtle.pop_all_transforms(); - program.append(&mut turtle.machine.tool_off()); - program.append(&mut turtle.machine.absolute()); + program.extend(turtle.machine.tool_off()); + program.extend(turtle.machine.absolute()); program.append(&mut turtle.move_to(true, 0.0, 0.0)); - program.append(&mut turtle.machine.program_end()); - program.push(command!(CommandWord::ProgramEnd, {})); + program.extend(turtle.machine.program_end()); + program.append(&mut command!(ProgramEnd {}).as_token_vec()); program } @@ -185,11 +188,11 @@ fn width_and_height_into_transform( } } -fn apply_path<'a>(turtle: &mut Turtle, options: &ProgramOptions, path: &'a str) -> Vec { +fn apply_path(turtle: &mut Turtle, options: &ProgramOptions, path: &str) -> Vec { use PathSegment::*; PathParser::from(path) .map(|segment| segment.expect("could not parse path segment")) - .map(|segment| { + .flat_map(|segment| { match segment { MoveTo { abs, x, y } => turtle.move_to(abs, x, y), ClosePath { abs: _ } => { @@ -271,7 +274,6 @@ fn apply_path<'a>(turtle: &mut Turtle, options: &ProgramOptions, path: &'a str) ), } }) - .flatten() .collect() } diff --git a/src/gcode/mod.rs b/src/gcode/mod.rs deleted file mode 100644 index 20e6fee..0000000 --- a/src/gcode/mod.rs +++ /dev/null @@ -1,107 +0,0 @@ -use core::convert::TryFrom; -use std::io::{self, Write}; - -#[macro_use] -mod spec; -pub use spec::*; - -/// Collapses GCode words into higher-level commands -pub struct CommandVecIntoIterator { - vec: Vec, - index: usize, -} - -impl Iterator for CommandVecIntoIterator { - type Item = Command; - fn next(&mut self) -> Option { - if self.vec.len() <= self.index { - return None; - } - let mut i = self.index + 1; - while i < self.vec.len() { - if CommandWord::is_command(&self.vec[i]) { - break; - } - i += 1; - } - let command = Command::try_from(&self.vec[self.index..i]).ok(); - self.index = i; - command - } -} - -impl From> for CommandVecIntoIterator { - fn from(vec: Vec) -> Self { - Self { vec, index: 0 } - } -} - -pub fn parse_gcode(gcode: &str) -> Vec { - let mut vec = vec![]; - let mut in_string = false; - let mut letter: Option = None; - let mut value_range = 0..0; - gcode.char_indices().for_each(|(i, c)| { - if (c.is_alphabetic() || c.is_ascii_whitespace()) && !in_string { - if let Some(l) = letter { - vec.push(Word { - letter: l, - value: parse_value(&gcode[value_range.clone()]), - }); - letter = None; - } - if c.is_alphabetic() { - letter = Some(c); - } - value_range = (i + 1)..(i + 1); - } else if in_string { - value_range = value_range.start..(i + 1); - } else { - if c == '"' { - in_string = !in_string; - } - value_range = value_range.start..(i + 1); - } - }); - if let Some(l) = letter { - vec.push(Word { - letter: l, - value: parse_value(&gcode[value_range]), - }); - } - vec -} - -fn parse_value(word: &str) -> Value { - if word.starts_with('"') && word.ends_with('"') { - Value::String(Box::new(word.to_string())) - } else { - let index_of_dot = word.find('.'); - Value::Fractional( - word[..index_of_dot.unwrap_or_else(|| word.len())] - .parse::() - .unwrap(), - index_of_dot.map(|j| word[j + 1..].parse::().unwrap()), - ) - } -} - -/// Writes a GCode program or sequence to a Writer -/// Each command is placed on a separate line -pub fn program2gcode(program: Vec, mut w: W) -> io::Result<()> { - for command in program.into_iter() { - let words: Vec = command.into(); - let mut it = words.iter(); - if let Some(command_word) = it.next() { - write!(w, "{}{}", command_word.letter, command_word.value)?; - for (i, word) in it.enumerate() { - write!(w, " {}{}", word.letter, word.value)?; - if i != words.len() - 2 { - write!(w, " ")?; - } - } - writeln!(w, "")?; - } - } - Ok(()) -} diff --git a/src/gcode/spec.rs b/src/gcode/spec.rs deleted file mode 100644 index 95a13f2..0000000 --- a/src/gcode/spec.rs +++ /dev/null @@ -1,300 +0,0 @@ -use std::convert::TryFrom; - -/// Fundamental unit of GCode: a value preceded by a descriptive letter. -#[derive(Clone, PartialEq, Debug)] -pub struct Word { - pub letter: char, - pub value: Value, -} - -/// All the possible variations of a word's value. -/// Fractional is needed to support commands like G91.1 which would be changed by float arithmetic. -/// Some flavors of GCode also allow for strings. -#[derive(Clone, PartialEq, Debug)] -pub enum Value { - Fractional(u32, Option), - Float(f64), - String(Box), -} - -impl Into for &Value { - fn into(self) -> f64 { - match self { - Value::Float(f) => *f, - _ => panic!("Unwrapping a non-float"), - } - } -} - -impl std::fmt::Display for Value { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Fractional(number, Some(fraction)) => write!(f, "{}.{}", number, fraction), - Self::Fractional(number, None) => write!(f, "{}", number), - Self::Float(float) => write!(f, "{}", float), - Self::String(string) => write!(f, "{}", string), - } - } -} - -/// A macro for quickly instantiating a float-valued command -#[macro_export] -macro_rules! command { - ($commandWord: expr, {$($argument: ident : $value: expr,)*}) => { - paste::expr! (Command::new($commandWord, vec![$(Word { - letter: stringify!([<$argument:upper>]).chars().next().unwrap(), - value: Value::Float($value), - },)*])) - }; -} - -macro_rules! commands { - ($($(#[$outer:meta])* $commandName: ident {$letter: expr, $number: expr, $fraction: path, {$($(#[$inner:meta])* $argument: ident), *} },)*) => { - - /// Commands are the operational unit of GCode - /// They consist of an identifying word followed by arguments - #[derive(Clone, PartialEq, Debug)] - pub struct Command { - command_word: CommandWord, - arguments: Vec - } - - impl Command { - pub fn new(command_word: CommandWord, mut arguments: Vec) -> Self { - Self { - command_word: command_word.clone(), - arguments: arguments.drain(..).filter(|w| { - match command_word { - $(CommandWord::$commandName => match w.letter.to_lowercase() { - $($argument => true,)* - _ => false - },)* - _ => false - } - }).collect() - } - } - - pub fn push(&mut self, argument: Word) { - match self.command_word { - $(CommandWord::$commandName => match argument.letter.to_lowercase() { - $($argument => { - self.arguments.push(argument); - })* - _ => {} - },)* - _ => {} - } - } - - pub fn word(&'_ self) -> &'_ CommandWord { - &self.command_word - } - - pub fn get(&'_ self, letter: char) -> Option<&'_ Word> { - let letter = letter.to_ascii_uppercase(); - self.arguments.iter().find(|arg| arg.letter == letter) - } - - pub fn set(&mut self, letter: char, value: Value) { - let letter = letter.to_ascii_uppercase(); - for i in 0..self.arguments.len() { - if self.arguments[i].letter == letter { - self.arguments[i].value = value; - break; - } - } - } - } - - impl Into> for Command { - fn into(self) -> Vec { - let mut args = self.arguments; - args.insert(0, self.command_word.into()); - args - } - } - - impl TryFrom<&[Word]> for Command { - type Error = (); - fn try_from(words: &[Word]) -> Result { - if words.len() == 0 { - return Err(()); - } - let command_word = CommandWord::try_from(&words[0])?; - let mut arguments = Vec::with_capacity(words.len() - 1); - for i in 1..words.len() { - match command_word { - $(CommandWord::$commandName => match words[i].letter.to_lowercase() { - $($argument => { - arguments.push(words[i].clone()); - })* - _ => {} - },)* - _ => {} - } - } - Ok(Self { - command_word, - arguments - }) - } - } - - #[derive(Clone, PartialEq, Eq, Debug)] - pub enum CommandWord { - $( - $(#[$outer])* - $commandName, - )* - /// A comment is a special command: it is a semicolon followed by text until the end of the line - Comment(Box), - /// Letter N followed by an integer (with no sign) between 0 and 99999 written with no more than five digits - LineNumber(u16), - /// Byte-sized checksums are used by some GCode generators at the end of each line - Checksum(u8), - } - - impl CommandWord { - pub fn is_command(word: &Word) -> bool { - let (number, fraction) = match &word.value { - Value::Fractional(number, fraction) => (number, fraction), - _other => return false - }; - match (word.letter, number, fraction) { - $(($letter, $number, $fraction) => true,)* - ('*', _checksum, None) => true, - ('N', _line_number, None) => true, - (_, _, _) => false - } - } - } - - impl TryFrom<&Word> for CommandWord { - type Error = (); - fn try_from(word: &Word) -> Result { - let (number, fraction) = match &word.value { - Value::Fractional(number, fraction) => (number, fraction), - _other => return Err(()) - }; - match (word.letter, number, fraction) { - $(($letter, $number, $fraction) => Ok(Self::$commandName),)* - ('*', checksum, None) => Ok(Self::Checksum(*checksum as u8)), - ('N', line_number, None) => Ok(Self::LineNumber(*line_number as u16)), - (_, _, _) => Err(()) - } - } - } - - impl Into for CommandWord { - fn into(self) -> Word { - match self { - $( - Self::$commandName {} => Word { - letter: $letter, - // TODO: fix fraction - value: Value::Fractional($number, $fraction) - }, - )* - Self::Checksum(value) => Word { - letter: '*', - value: Value::Fractional(value as u32, None) - }, - Self::LineNumber(value) => Word { - letter: 'N', - value: Value::Fractional(value as u32, None) - }, - Self::Comment(string) => Word { - letter: ';', - value: Value::String(string) - } - } - } - } - }; -} - -commands!( - /// Moves the head at the fastest possible speed to the desired speed - /// Never enter a cut with rapid positioning - /// Some older machines may "dog leg" rapid positioning, moving one axis at a time - RapidPositioning { - 'G', 0, None, { - x, - y, - z, - e, - f, - h, - r, - s, - a, - b, - c - } - }, - /// Typically used for "cutting" motion - LinearInterpolation { - 'G', 1, None, { - x, - y, - z, - e, - f, - h, - r, - s, - a, - b, - c - } - }, - /// This will keep the axes unmoving for the period of time in seconds specified by the P number - Dwell { - 'G', 4, None, { - /// Time in seconds - p - } - }, - /// Use inches for length units - UnitsInches { - 'G', 20, None, {} - }, - /// Use millimeters for length units - UnitsMillimeters { - 'G', 21, None, {} - }, - /// In absolute distance mode, axis numbers usually represent positions in terms of the currently active coordinate system. - AbsoluteDistanceMode { - 'G', 90, None, {} - }, - /// In relative distance mode, axis numbers usually represent increments from the current values of the numbers - RelativeDistanceMode { - 'G', 91, None, {} - }, - FeedRateUnitsPerMinute { - 'G', 94, None, {} - }, - /// Start spinning the spindle clockwise with speed `p` - StartSpindleClockwise { - 'M', 3, None, { - /// Speed - p - } - }, - /// Start spinning the spindle counterclockwise with speed `p` - StartSpindleCounterclockwise { - 'M', 4, None, { - /// Speed - p - } - }, - /// Stop spinning the spindle - StopSpindle { - 'M', 5, None, {} - }, - /// Signals the end of a program - ProgramEnd { - 'M', 20, None, {} - }, -); diff --git a/src/machine.rs b/src/machine.rs index 825b946..582c80e 100644 --- a/src/machine.rs +++ b/src/machine.rs @@ -1,11 +1,8 @@ -use crate::gcode::*; - -//// Direction of the machine spindle -#[derive(Clone, PartialEq, Eq)] -pub enum Direction { - Clockwise, - Counterclockwise, -} +use g_code::{ + command, + emit::Token, + parse::{ast::Snippet, token::Field}, +}; /// Whether the tool is active (i.e. cutting) #[derive(Copy, Clone, PartialEq, Eq, Debug)] @@ -14,6 +11,16 @@ pub enum Tool { On, } +impl std::ops::Not for Tool { + type Output = Self; + fn not(self) -> Self { + match self { + Self::Off => Self::On, + Self::On => Self::Off, + } + } +} + /// The distance mode for movement commands #[derive(Copy, Clone, PartialEq, Eq, Debug)] pub enum Distance { @@ -21,80 +28,87 @@ pub enum Distance { Relative, } -/// Generic machine state simulation, assuming nothing is known about the machine when initialized. -/// This is used to reduce output GCode verbosity and run repetitive actions. -#[derive(Debug, Default)] -pub struct Machine { - tool_state: Option, - distance_mode: Option, - tool_on_action: Vec, - tool_off_action: Vec, - program_begin_sequence: Vec, - program_end_sequence: Vec, -} - -impl Machine { - /// Create a generic machine, given a tool on/off GCode sequence. - pub fn new( - tool_on_action: Vec, - tool_off_action: Vec, - program_begin_sequence: Vec, - program_end_sequence: Vec, - ) -> Self { - Self { - tool_state: None, - distance_mode: None, - tool_on_action: CommandVecIntoIterator::from(tool_on_action).collect(), - tool_off_action: CommandVecIntoIterator::from(tool_off_action).collect(), - program_begin_sequence: CommandVecIntoIterator::from(program_begin_sequence).collect(), - program_end_sequence: CommandVecIntoIterator::from(program_end_sequence).collect(), +impl std::ops::Not for Distance { + type Output = Self; + fn not(self) -> Self { + match self { + Self::Absolute => Self::Relative, + Self::Relative => Self::Absolute, } } } -impl Machine { +/// Generic machine state simulation, assuming nothing is known about the machine when initialized. +/// This is used to reduce output GCode verbosity and run repetitive actions. +#[derive(Debug)] +pub struct Machine<'input> { + pub(crate) tool_state: Option, + pub(crate) distance_mode: Option, + pub(crate) tool_on_action: Option>, + pub(crate) tool_off_action: Option>, + pub(crate) program_begin_sequence: Option>, + pub(crate) program_end_sequence: Option>, +} + +impl<'input> Machine<'input> { /// Output gcode to turn the tool on. - pub fn tool_on(&mut self) -> Vec { + pub fn tool_on<'a>(&'a mut self) -> Vec { if self.tool_state == Some(Tool::Off) || self.tool_state == None { self.tool_state = Some(Tool::On); - self.tool_on_action.clone() + self.tool_on_action + .iter() + .flat_map(|s| s.iter_fields()) + .map(|f: &Field| Token::from(f)) + .collect() } else { vec![] } } /// Output gcode to turn the tool off. - pub fn tool_off(&mut self) -> Vec { + pub fn tool_off<'a>(&'a mut self) -> Vec { if self.tool_state == Some(Tool::On) || self.tool_state == None { self.tool_state = Some(Tool::Off); - self.tool_off_action.clone() + self.tool_on_action + .iter() + .flat_map(|s| s.iter_fields()) + .map(|f: &Field| Token::from(f)) + .collect() } else { vec![] } } - pub fn program_begin(&self) -> Vec { - self.program_begin_sequence.clone() + pub fn program_begin<'a>(&'a self) -> Vec { + self.program_begin_sequence + .iter() + .flat_map(|s| s.iter_fields()) + .map(|f: &Field| Token::from(f)) + .collect() } - pub fn program_end(&self) -> Vec { - self.program_end_sequence.clone() + pub fn program_end<'a>(&'a self) -> Vec { + self.program_end_sequence + .iter() + .flat_map(|s| s.iter_fields()) + .map(|f: &Field| Token::from(f)) + .collect() } - /// Output relative distance field if mode was absolute or unknown. - pub fn absolute(&mut self) -> Vec { + /// Output absolute distance field if mode was relative or unknown. + pub fn absolute(&mut self) -> Vec { if self.distance_mode == Some(Distance::Relative) || self.distance_mode == None { self.distance_mode = Some(Distance::Absolute); - vec![command!(CommandWord::AbsoluteDistanceMode, {})] + command!(AbsoluteDistanceMode {}).as_token_vec() } else { vec![] } } - /// Output absolute distance field if mode was relative or unknown. - pub fn relative(&mut self) -> Vec { + /// Output relative distance field if mode was absolute or unknown. + pub fn relative(&mut self) -> Vec { if self.distance_mode == Some(Distance::Absolute) || self.distance_mode == None { self.distance_mode = Some(Distance::Relative); - vec![command!(CommandWord::RelativeDistanceMode, {})] + command!(RelativeDistanceMode {}).as_token_vec() } else { vec![] } diff --git a/src/main.rs b/src/main.rs index 9bd93d9..a624a2e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,17 +1,16 @@ #[macro_use] -extern crate clap; -#[macro_use] extern crate log; use std::env; use std::fs::File; use std::io::{self, Read}; +use std::path::PathBuf; + +use g_code::parse::{ast::Snippet, lexer::Lexer, ParseError, SnippetParser}; +use structopt::StructOpt; /// Converts an SVG to GCode in an internal representation mod converter; -/// Defines an internal GCode representation -#[macro_use] -mod gcode; /// Emulates the state of an arbitrary machine that can run GCode mod machine; /// Operations that are easier to implement after GCode is generated, or would @@ -24,29 +23,49 @@ mod turtle; use converter::ProgramOptions; use machine::Machine; +#[derive(Debug, StructOpt)] +#[structopt(name = "svg2gcode", author, about)] +struct Opt { + /// Curve interpolation tolerance + #[structopt(long, default_value = "0.002")] + tolerance: f64, + /// Machine feed rate in mm/min + #[structopt(long, default_value = "300")] + feedrate: f64, + /// Dots per inch (DPI) for pixels, points, picas, etc. + #[structopt(long, default_value = "96")] + dpi: f64, + #[structopt(short = "on", long)] + /// Tool on GCode sequence + tool_on_sequence: Option, + #[structopt(short = "off", long)] + /// Tool off GCode sequence + tool_off_sequence: Option, + /// Optional GCode begin sequence (i.e. change to a cutter tool) + #[structopt(short = "begin", long)] + begin_sequence: Option, + /// Optional GCode end sequence, prior to program end (i.e. put away a cutter tool) + #[structopt(short = "end", long)] + end_sequence: Option, + /// A file path for an SVG, else reads from stdin + file: Option, + /// Output file path (overwrites old files), else writes to stdout + #[structopt(short, long)] + out: Option, + /// Set where the bottom left corner of the SVG will be placed + #[structopt(long, default_value = "0,0")] + origin: String, +} + fn main() -> io::Result<()> { if env::var("RUST_LOG").is_err() { env::set_var("RUST_LOG", "svg2gcode=info") } env_logger::init(); - let matches = clap_app!(svg2gcode => - (version: crate_version!()) - (author: crate_authors!()) - (about: crate_description!()) - (@arg FILE: "A file path for an SVG, else reads from stdin") - (@arg tolerance: --tolerance +takes_value "Curve interpolation tolerance (default: 0.002mm)") - (@arg feedrate: --feedrate +takes_value "Machine feed rate in mm/min (default: 300mm/min)") - (@arg dpi: --dpi +takes_value "Dots per inch (DPI) for pixels, points, picas, etc. (default: 96dpi)") - (@arg tool_on_sequence: --on +takes_value +required "Tool on GCode sequence") - (@arg tool_off_sequence: --off +takes_value +required "Tool off GCode sequence") - (@arg begin_sequence: --begin +takes_value "Optional GCode begin sequence (i.e. change to a tool)") - (@arg end_sequence: --end +takes_value "Optional GCode end sequence, prior to program end (i.e. change to a tool)") - (@arg out: --out -o +takes_value "Output file path (overwrites old files), else writes to stdout") - (@arg origin: --origin +takes_value "Set where the bottom left corner of the SVG will be placed (default: 0,0)") - ) - .get_matches(); - - let input = match matches.value_of("FILE") { + + let opt = Opt::from_args(); + + let input = match opt.file { Some(filename) => { let mut f = File::open(filename)?; let len = f.metadata()?.len(); @@ -63,79 +82,148 @@ fn main() -> io::Result<()> { }; let mut options = ProgramOptions::default(); - - if let Some(tolerance) = matches - .value_of("tolerance") - .map(|tolerance| tolerance.parse().expect("could not parse tolerance")) - { - options.tolerance = tolerance; - } - if let Some(feedrate) = matches - .value_of("feedrate") - .map(|feedrate| feedrate.parse().expect("could not parse tolerance")) + options.tolerance = opt.tolerance; + options.feedrate = opt.feedrate; + options.dpi = opt.dpi; + + let snippets = [ + opt.tool_on_sequence.as_ref().map(parse_snippet).transpose(), + opt.tool_off_sequence + .as_ref() + .map(parse_snippet) + .transpose(), + opt.begin_sequence.as_ref().map(parse_snippet).transpose(), + opt.end_sequence.as_ref().map(parse_snippet).transpose(), + ]; + + let machine = if let [Ok(tool_on_action), Ok(tool_off_action), Ok(program_begin_sequence), Ok(program_end_sequence)] = + snippets { - options.feedrate = feedrate; - } - if let Some(dpi) = matches - .value_of("dpi") - .map(|dpi| dpi.parse().expect("could not parse tolerance")) - { - options.dpi = dpi; - } - - let machine = machine::Machine::new( - matches - .value_of("tool_on_sequence") - .map(gcode::parse_gcode) - .unwrap_or_default(), - matches - .value_of("tool_off_sequence") - .map(gcode::parse_gcode) - .unwrap_or_default(), - matches - .value_of("begin_sequence") - .map(gcode::parse_gcode) - .unwrap_or_default(), - matches - .value_of("end_sequence") - .map(gcode::parse_gcode) - .unwrap_or_default(), - ); + Machine { + tool_on_action, + tool_off_action, + program_begin_sequence, + program_end_sequence, + tool_state: None, + distance_mode: None, + } + } else { + use codespan_reporting::term::{ + emit, + termcolor::{ColorChoice, StandardStream}, + }; + let mut writer = StandardStream::stderr(ColorChoice::Auto); + let config = codespan_reporting::term::Config::default(); + + for (i, (filename, gcode)) in [ + ("tool_on_sequence", &opt.tool_on_sequence), + ("tool_off_sequence", &opt.tool_off_sequence), + ("begin_sequence", &opt.begin_sequence), + ("end_sequence", &opt.end_sequence), + ] + .iter() + .enumerate() + { + if let Err(err) = &snippets[i] { + emit( + &mut writer, + &config, + &codespan_reporting::files::SimpleFile::new(filename, gcode.as_ref().unwrap()), + &g_code::parse::into_diagnostic(&err), + ) + .unwrap(); + } + } + std::process::exit(1) + }; let document = roxmltree::Document::parse(&input).expect("Invalid or unsupported SVG file"); let mut program = converter::svg2program(&document, options, machine); - let origin = matches - .value_of("origin") - .map(|coords| coords.split(',')) - .map(|coords| coords.map(|point| point.parse().expect("could not parse coordinate"))) - .map(|coords| coords.collect::>()) - .map(|coords| (coords[0], coords[1])) - .unwrap_or((0., 0.)); - postprocess::set_origin(&mut program, lyon_geom::point(origin.0, origin.1)); - - if let Some(out_path) = matches.value_of("out") { - gcode::program2gcode(program, File::create(out_path)?) + let origin = opt + .origin + .split(',') + .map(|point| point.parse().expect("could not parse coordinate")) + .collect::>(); + postprocess::set_origin(&mut program, lyon_geom::point(origin[0], origin[1])); + + if let Some(out_path) = opt.out { + tokens_into_gcode(program, File::create(out_path)?) } else { - gcode::program2gcode(program, std::io::stdout()) + tokens_into_gcode(program, std::io::stdout()) + } +} + +fn parse_snippet<'input>(gcode: &'input String) -> Result, ParseError<'input>> { + SnippetParser::new().parse(gcode, Lexer::new(gcode)) +} + +fn tokens_into_gcode( + program: Vec, + mut w: W, +) -> io::Result<()> { + use g_code::emit::Token::*; + let mut preceded_by_newline = true; + for token in program { + match token { + Field(f) => { + if !preceded_by_newline { + if matches!(f.letters.as_str(), "G" | "M") { + writeln!(w, "")?; + } else { + write!(w, " ")?; + } + } + write!(w, "{}", f)?; + preceded_by_newline = false; + } + Comment { + is_inline: true, + inner, + } => { + write!(w, "({})", inner)?; + preceded_by_newline = false; + } + Comment { + is_inline: false, + inner, + } => { + writeln!(w, ";{}", inner)?; + preceded_by_newline = true; + } + _ => {} + } + } + // Ensure presence of trailing newline + if !preceded_by_newline { + writeln!(w, "")?; } + Ok(()) } #[cfg(test)] mod test { use super::*; + use pretty_assertions::assert_eq; fn get_actual(input: &str) -> String { let options = ProgramOptions::default(); - let machine = Machine::default(); + let machine = Machine { + tool_state: None, + distance_mode: None, + tool_on_action: None, + tool_off_action: None, + program_begin_sequence: None, + program_end_sequence: None, + }; let document = roxmltree::Document::parse(input).unwrap(); let mut program = converter::svg2program(&document, options, machine); postprocess::set_origin(&mut program, lyon_geom::point(0., 0.)); let mut actual = vec![]; - assert!(gcode::program2gcode(program, &mut actual).is_ok()); + assert!(tokens_into_gcode(program, &mut actual).is_ok()); String::from_utf8(actual).unwrap() } diff --git a/src/postprocess.rs b/src/postprocess.rs index b07f1ad..cdd983a 100644 --- a/src/postprocess.rs +++ b/src/postprocess.rs @@ -1,65 +1,86 @@ -use crate::gcode::CommandWord::*; -use crate::gcode::*; +use euclid::default::Box2D; +use g_code::emit::{ + Field, Token, Value, ABSOLUTE_DISTANCE_MODE_FIELD, RELATIVE_DISTANCE_MODE_FIELD, +}; use lyon_geom::{point, vector, Point}; type F64Point = Point; /// Moves all the commands so that they are beyond a specified position -pub fn set_origin(commands: &mut [Command], origin: F64Point) { - let offset = -get_bounding_box(commands).0.to_vector() + origin.to_vector(); +pub fn set_origin(tokens: &mut [Token], origin: F64Point) { + let offset = -get_bounding_box(tokens.iter()).min.to_vector() + origin.to_vector(); let mut is_relative = false; let mut current_position = point(0f64, 0f64); - - for command in commands { - match command.word() { - RapidPositioning | LinearInterpolation => { - let x: f64 = (&command.get('X').unwrap().value).into(); - let y: f64 = (&command.get('Y').unwrap().value).into(); - if is_relative { - current_position += vector(x, y); - } else { - current_position = point(x, y); - command.set('X', Value::Float((current_position + offset).x)); - command.set('Y', Value::Float((current_position + offset).y)); + let x = "X".to_string(); + let y = "Y".to_string(); + let abs_tok = Token::Field(ABSOLUTE_DISTANCE_MODE_FIELD.clone()); + let rel_tok = Token::Field(RELATIVE_DISTANCE_MODE_FIELD.clone()); + for token in tokens { + match token { + abs if *abs == abs_tok => is_relative = false, + rel if *rel == rel_tok => is_relative = true, + Token::Field(Field { letters, value }) if *letters == x => { + if let Some(float) = value.as_f64() { + if is_relative { + current_position += vector(float, 0.) + } else { + current_position = point(float, 0.); + } + *value = Value::Float(current_position.x + offset.x) } } - AbsoluteDistanceMode => { - is_relative = false; - } - RelativeDistanceMode => { - is_relative = true; + Token::Field(Field { letters, value }) if *letters == y => { + if let Some(float) = value.as_f64() { + if is_relative { + current_position += vector(0., float) + } else { + current_position = point(0., float); + } + *value = Value::Float(current_position.y + offset.y) + } } _ => {} } } } -fn get_bounding_box(commands: &[Command]) -> (F64Point, F64Point) { +fn get_bounding_box<'a, I: Iterator>(tokens: I) -> Box2D { let (mut minimum, mut maximum) = (point(0f64, 0f64), point(0f64, 0f64)); let mut is_relative = false; let mut current_position = point(0f64, 0f64); - for command in commands { - match command.word() { - AbsoluteDistanceMode => { - is_relative = false; - } - RelativeDistanceMode => { - is_relative = true; + let x = "X".to_string(); + let y = "Y".to_string(); + let abs_tok = Token::Field(ABSOLUTE_DISTANCE_MODE_FIELD.clone()); + let rel_tok = Token::Field(RELATIVE_DISTANCE_MODE_FIELD.clone()); + for token in tokens { + match token { + abs if *abs == abs_tok => is_relative = false, + rel if *rel == rel_tok => is_relative = true, + Token::Field(Field { letters, value }) if *letters == x => { + if let Some(value) = value.as_f64() { + if is_relative { + current_position += vector(value, 0.) + } else { + current_position = point(value, 0.); + } + minimum = minimum.min(current_position); + maximum = maximum.max(current_position); + } } - LinearInterpolation | RapidPositioning => { - let x: f64 = (&command.get('X').unwrap().value).into(); - let y: f64 = (&command.get('Y').unwrap().value).into(); - if is_relative { - current_position += vector(x, y) - } else { - current_position = point(x, y); + Token::Field(Field { letters, value }) if *letters == y => { + if let Some(value) = value.as_f64() { + if is_relative { + current_position += vector(0., value) + } else { + current_position = point(0., value); + } + minimum = minimum.min(current_position); + maximum = maximum.max(current_position); } - minimum = minimum.min(current_position); - maximum = maximum.max(current_position); } - _ => (), + _ => {} } } - (minimum, maximum) + Box2D::new(minimum, maximum) } diff --git a/src/turtle.rs b/src/turtle.rs index f1a7b16..68cdb0b 100644 --- a/src/turtle.rs +++ b/src/turtle.rs @@ -1,5 +1,8 @@ -use crate::gcode::*; use crate::machine::Machine; +use g_code::{ + command, + emit::{Field, Token, Value}, +}; use lyon_geom::euclid::{default::Transform2D, Angle}; use lyon_geom::{point, vector, Point}; use lyon_geom::{ArcFlags, CubicBezierSegment, QuadraticBezierSegment, SvgArc}; @@ -9,18 +12,18 @@ type F64Point = Point; /// Turtle graphics simulator for paths that outputs the gcode representation for each operation. /// Handles transforms, position, offsets, etc. See https://www.w3.org/TR/SVG/paths.html #[derive(Debug)] -pub struct Turtle { +pub struct Turtle<'input> { current_position: F64Point, initial_position: F64Point, current_transform: Transform2D, transform_stack: Vec>, - pub machine: Machine, + pub machine: Machine<'input>, previous_control: Option, } -impl Turtle { +impl<'input> Turtle<'input> { /// Create a turtle at the origin with no transform - pub fn new(machine: Machine) -> Self { + pub fn new(machine: Machine<'input>) -> Self { Self { current_position: point(0.0, 0.0), initial_position: point(0.0, 0.0), @@ -30,12 +33,10 @@ impl Turtle { previous_control: None, } } -} -impl Turtle { /// Move the turtle to the given absolute/relative coordinates in the current transform /// https://www.w3.org/TR/SVG/paths.html#PathDataMovetoCommands - pub fn move_to(&mut self, abs: bool, x: X, y: Y) -> Vec + pub fn move_to(&mut self, abs: bool, x: X, y: Y) -> Vec where X: Into>, Y: Into>, @@ -71,39 +72,38 @@ impl Turtle { self.machine .tool_off() - .iter() - .chain(self.machine.absolute().iter()) - .chain(std::iter::once(&command!(CommandWord::RapidPositioning, { - x : to.x as f64, - y : to.y as f64, - }))) - .map(Clone::clone) + .drain(..) + .chain(self.machine.absolute().drain(..)) + .chain( + command!(RapidPositioning { + X: to.x as f64, + Y: to.y as f64, + }) + .as_token_vec(), + ) .collect() } - fn linear_interpolation(x: f64, y: f64, z: Option, f: Option) -> Command { - let mut linear_interpolation = command!(CommandWord::LinearInterpolation, { - x: x, - y: y, - }); + fn linear_interpolation(x: f64, y: f64, z: Option, f: Option) -> Vec { + let mut linear_interpolation = command! {LinearInterpolation { X: x, Y: y, }}; if let Some(z) = z { - linear_interpolation.push(Word { - letter: 'Z', + linear_interpolation.push(Field { + letters: "Z".to_string(), value: Value::Float(z), }); } if let Some(f) = f { - linear_interpolation.push(Word { - letter: 'F', + linear_interpolation.push(Field { + letters: "F".into(), value: Value::Float(f), }); } - linear_interpolation + linear_interpolation.as_token_vec() } /// Close an SVG path, cutting back to its initial position /// https://www.w3.org/TR/SVG/paths.html#PathDataClosePathCommand - pub fn close(&mut self, z: Z, f: F) -> Vec + pub fn close(&mut self, z: Z, f: F) -> Vec where Z: Into>, F: Into>, @@ -121,21 +121,20 @@ impl Turtle { self.machine .tool_on() - .iter() - .chain(self.machine.absolute().iter()) - .chain(std::iter::once(&Self::linear_interpolation( + .drain(..) + .chain(self.machine.absolute()) + .chain(Self::linear_interpolation( self.initial_position.x, self.initial_position.y, z.into(), f.into(), - ))) - .map(Clone::clone) + )) .collect() } /// Draw a line from the current position in the current transform to the specified position /// https://www.w3.org/TR/SVG/paths.html#PathDataLinetoCommands - pub fn line(&mut self, abs: bool, x: X, y: Y, z: Z, f: F) -> Vec + pub fn line(&mut self, abs: bool, x: X, y: Y, z: Z, f: F) -> Vec where X: Into>, Y: Into>, @@ -172,15 +171,9 @@ impl Turtle { self.machine .tool_on() - .iter() - .chain(self.machine.absolute().iter()) - .chain(std::iter::once(&Self::linear_interpolation( - to.x, - to.y, - z.into(), - f.into(), - ))) - .map(Clone::clone) + .drain(..) + .chain(self.machine.absolute()) + .chain(Self::linear_interpolation(to.x, to.y, z.into(), f.into())) .collect() } @@ -193,13 +186,13 @@ impl Turtle { tolerance: f64, z: Z, f: F, - ) -> Vec { + ) -> Vec { let z = z.into(); let f = f.into(); let last_point = std::cell::Cell::new(self.current_position); - let cubic: Vec = cbs + let cubic: Vec = cbs .flattened(tolerance) - .map(|point| { + .flat_map(|point| { last_point.set(point); Self::linear_interpolation(point.x, point.y, z, f) }) @@ -214,10 +207,9 @@ impl Turtle { self.machine .tool_on() - .iter() - .chain(self.machine.absolute().iter()) - .chain(cubic.iter()) - .map(Clone::clone) + .drain(..) + .chain(self.machine.absolute()) + .chain(cubic) .collect() } @@ -235,7 +227,7 @@ impl Turtle { tolerance: f64, z: Z, f: F, - ) -> Vec + ) -> Vec where Z: Into>, F: Into>, @@ -277,7 +269,7 @@ impl Turtle { tolerance: f64, z: Z, f: F, - ) -> Vec + ) -> Vec where Z: Into>, F: Into>, @@ -315,7 +307,7 @@ impl Turtle { tolerance: f64, z: Z, f: F, - ) -> Vec + ) -> Vec where Z: Into>, F: Into>, @@ -347,7 +339,7 @@ impl Turtle { tolerance: f64, z: Z, f: F, - ) -> Vec + ) -> Vec where Z: Into>, F: Into>, @@ -384,7 +376,7 @@ impl Turtle { z: Z, f: F, tolerance: f64, - ) -> Vec + ) -> Vec where Z: Into>, F: Into>, @@ -419,7 +411,7 @@ impl Turtle { let mut ellipse = vec![]; arc.for_each_flattened(tolerance, &mut |point: F64Point| { - ellipse.push(Self::linear_interpolation(point.x, point.y, z, f)); + ellipse.append(&mut Self::linear_interpolation(point.x, point.y, z, f)); last_point.set(point); }); self.current_position = last_point.get(); @@ -427,10 +419,9 @@ impl Turtle { self.machine .tool_on() - .iter() - .chain(self.machine.absolute().iter()) - .chain(ellipse.iter()) - .map(Clone::clone) + .drain(..) + .chain(self.machine.absolute()) + .chain(ellipse) .collect() } diff --git a/tests/square.gcode b/tests/square.gcode index a6ff7d9..577caee 100644 --- a/tests/square.gcode +++ b/tests/square.gcode @@ -1,12 +1,11 @@ G21 G94 G90 -G0 X0 Y0 -;svg#svg8 > g#layer1 > path#path838 -G0 X1 Y9 -G1 X9 Y9 F300 -G1 X9 Y1 F300 -G1 X1 Y1 F300 -G1 X1 Y9 F300 -G0 X0 Y0 +G0 X0 Y0;svg#svg8 > g#layer1 > path#path838 +G0 X1 Y9 +G1 X9 Y9 F300 +G1 X9 Y1 F300 +G1 X1 Y1 F300 +G1 X1 Y9 F300 +G0 X0 Y0 M20 diff --git a/tests/square_transformed.gcode b/tests/square_transformed.gcode index 94f0e72..bb3c042 100644 --- a/tests/square_transformed.gcode +++ b/tests/square_transformed.gcode @@ -1,12 +1,11 @@ G21 G94 G90 -G0 X0 Y19 -;svg#svg8 > g#layer1 > path#path838 -G0 X9 Y8 -G1 X9 Y0 F300 -G1 X1 Y0 F300 -G1 X0.9999999999999982 Y8 F300 -G1 X9 Y8 F300 -G0 X0 Y19 +G0 X0 Y19;svg#svg8 > g#layer1 > path#path838 +G0 X9 Y8 +G1 X9 Y0 F300 +G1 X1 Y0 F300 +G1 X0.9999999999999982 Y8 F300 +G1 X9 Y8 F300 +G0 X0 Y19 M20