Compare commits
782 Commits
bc3f1543ee
...
v0.2.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 3c53d6fecd | |||
| e239967a7b | |||
| 9d57d3875a | |||
| 0e299d7f6f | |||
| c6f544c5d7 | |||
| 9563094c2b | |||
| 091e750e1b | |||
| 87b965f895 | |||
| 08f60690a7 | |||
| a4609c816c | |||
| 5a4272696e | |||
| 2cf03be360 | |||
| 041e184d93 | |||
| 26df3174ea | |||
| 0f5aace126 | |||
| 399f8dda6e | |||
| d921558b9c | |||
| bf3e3e1f42 | |||
| e120ece014 | |||
| 264e8264be | |||
| 24babe353e | |||
| e63be93051 | |||
| ba3c3cbea3 | |||
| 572fa06a21 | |||
| a6c2235647 | |||
| 5c918a0978 | |||
| 92461deb98 | |||
| bc859aa28c | |||
| 09eac96a03 | |||
| df65414a9d | |||
| 4aed231611 | |||
| c641b3b68e | |||
| f3b27c32c3 | |||
| c270d8ea76 | |||
| de6877ac48 | |||
| 3481764416 | |||
| 640814fdf6 | |||
| 6a30828fad | |||
| 786b6e2e88 | |||
| ba89967448 | |||
| b566d984b0 | |||
| c1e6092e83 | |||
| df86d4367b | |||
| 40026ab4dc | |||
| b18a82df7a | |||
| f090a2e299 | |||
| 55192a4888 | |||
| 7c28a35ad8 | |||
| b2a723ca60 | |||
| 3dca25c601 | |||
| ebc1a5f980 | |||
| b729f92cd6 | |||
| 5d6e018b81 | |||
| 5163b02f89 | |||
| a59911b38a | |||
| 810e37cacf | |||
| 8dfa45c446 | |||
| b223f69572 | |||
| 98c574c2ad | |||
| 30f1008fa9 | |||
| 41c20eaf75 | |||
| 3a97253473 | |||
| 3eab3c5946 | |||
| 0e05ad04ea | |||
| 5ac985dc0f | |||
| 865754611c | |||
| 9db326ee5d | |||
| 25faba430c | |||
| 089df67627 | |||
| 11884e712d | |||
| 6bed736cf0 | |||
| c20a079874 | |||
| 804a7fd9c1 | |||
| 3c4d00baa4 | |||
| 959ab15491 | |||
| cca70db547 | |||
| 62d9dce0b1 | |||
| 1f88453d4c | |||
| 0697bebbc2 | |||
| beadb14acc | |||
| 09f1140f54 | |||
| 7c918a2378 | |||
| feb08a5f60 | |||
| f1fd211ba5 | |||
| fd3c2462df | |||
| a4773748a1 | |||
| af57153269 | |||
| 35e89600d0 | |||
| 89a4e6b981 | |||
| ebad3577dd | |||
| a8dc275da4 | |||
| d84becdaee | |||
| 9cba3a6cd7 | |||
| e93523d7a2 | |||
| 3bdbf21881 | |||
| a8e42fb4b5 | |||
| ea3c6afbdd | |||
| ba88ac253a | |||
| 250fdefaea | |||
| e92208b8c0 | |||
| 297ebee45b | |||
| 1eba3e7cde | |||
| d65f3460a9 | |||
| ede06b1bf6 | |||
| 51eea6d1e6 | |||
| 3d23ad8073 | |||
| 107fd86066 | |||
| d12f0cee3e | |||
| d93b69c524 | |||
| a65598615e | |||
| ed082a6799 | |||
| c9b17619ef | |||
| f78cc78a65 | |||
| 37130e8a28 | |||
| 6f19fe1822 | |||
| 81c167320d | |||
| 981188f65e | |||
| ffd060bf61 | |||
| a360452da3 | |||
| b3e9e5e28b | |||
| 7380a43349 | |||
| 59e00cd707 | |||
| 44cb6e4a2b | |||
| 5949c3ca1f | |||
| ef15421915 | |||
| 943c262ad2 | |||
| 301831e096 | |||
| fce287e649 | |||
| 7e86313d7c | |||
| c5943e22eb | |||
| e50a7c82cf | |||
| 7a893ef50f | |||
| 925a1c7751 | |||
| 036b48e273 | |||
| bd9b0369cf | |||
| 93391c4b8f | |||
| ebab795f86 | |||
| 9f9111975d | |||
| 25ee193ae6 | |||
| 5bcad9667b | |||
| 64945220b9 | |||
| ec0baad585 | |||
| f26edb824d | |||
| aae593a73e | |||
| 36d8f7fb11 | |||
| 52ad5b4575 | |||
| 7416f8ae3f | |||
| 46e3104dfc | |||
| 27afa04e4a | |||
| 95b9613e2d | |||
| 3bc9301e22 | |||
| 1040db414f | |||
| 287023d802 | |||
| 3a24e76dbd | |||
| a6e2845261 | |||
| 97d897e885 | |||
| 9db7abcd37 | |||
| 3e340e67e0 | |||
| 7a6c407edd | |||
| 9f76659d5d | |||
| a8341e9e99 | |||
| fb067187b4 | |||
| 5c66fb3b72 | |||
| 5bd4c89999 | |||
| dd93c230dd | |||
| d6ffd8efc9 | |||
| 68c3a904e8 | |||
| d57e2ca54b | |||
| 904eeb38c2 | |||
| e1bb723169 | |||
| aa156fff57 | |||
| d3a439181c | |||
| bb70ae26d3 | |||
| 35dc954017 | |||
| 0cae9e88e7 | |||
| 5d824a1aff | |||
| 8a293bcc9d | |||
| 24b89689c5 | |||
| 3da5d1c70c | |||
| d3ec4eb3e2 | |||
| cb446e1057 | |||
| f3ca021fad | |||
| ffe32fc38c | |||
| 27bbe99e7e | |||
| 5a9a06a6a0 | |||
| c1f1c829dc | |||
| e8fe01aea2 | |||
| 7b7d2cd8d1 | |||
| 6ca0e9da92 | |||
| bcaa4a03ee | |||
| 54c6f1bc89 | |||
| 429e4b63e1 | |||
| 159b54a1ec | |||
| 568539d5b1 | |||
| d7fa4bef43 | |||
| 7c58cfa749 | |||
| 525cbc6f12 | |||
| 134771aa23 | |||
| 59a66173e1 | |||
| a2b7be44f8 | |||
| e94a556f23 | |||
| 428dbdb03c | |||
| e860ca3f4a | |||
| a399c89f58 | |||
| d16ef36d34 | |||
| 5307c5c85a | |||
| 21321740d6 | |||
| 7f8c708d3f | |||
| ab4f806820 | |||
| c9b5ee1918 | |||
| f34dce95da | |||
| a2a19938d3 | |||
| c064c7647a | |||
| 8a712b9755 | |||
| 82de512f44 | |||
| f903cbe18a | |||
| 3d4204db7b | |||
| 722f758e94 | |||
| 9b2322abe9 | |||
| b15375cca5 | |||
| e3b388464d | |||
| ab09f835d3 | |||
| f8b0fb573b | |||
| 6ce501da11 | |||
| 05037bc928 | |||
| f83df3a55a | |||
| 84ad39414a | |||
| fdb4a2373a | |||
| 3a0267c041 | |||
| 036f723876 | |||
| 21a5d3b026 | |||
| 0607c6c7c5 | |||
| c8cfeb3c6b | |||
| d4f424f274 | |||
| 028b1fabfc | |||
| a7c2fcffe6 | |||
| b834813889 | |||
| 4fa6100722 | |||
| 8f2fbee02c | |||
| 230a11d32e | |||
| 953429dae9 | |||
| 1c2b569ff4 | |||
| 048b10a1e9 | |||
| 3022982f6d | |||
| cc85493a0c | |||
| 3da287cdc0 | |||
| d1a701a7f7 | |||
| 17f786c9e8 | |||
| b7a8e2662c | |||
| 912a47c5e8 | |||
| a85213a524 | |||
| fb696aaf58 | |||
| d854a1f5d2 | |||
| abc707f1d9 | |||
| 61b917c398 | |||
| e3e51611d5 | |||
| 8104bd3626 | |||
| ae262b8a77 | |||
| afbbc9ed79 | |||
| 6071e6fa14 | |||
| afdd386456 | |||
| 2db8c49838 | |||
| 80e8693da3 | |||
| d7eb3ebd7a | |||
| 4404d3a5d0 | |||
| d27dee3db9 | |||
| 7081c7b4d0 | |||
| a6e813bc85 | |||
| 98453243fc | |||
| 64874857a1 | |||
| 5d3fcb2dc8 | |||
| ae9a63b5ce | |||
| 596328148d | |||
| 6cd48a623d | |||
| 42243c7df0 | |||
| 4b10d4801c | |||
| f0bdaa14e6 | |||
| 79ddce346b | |||
| 20777541c0 | |||
| 7c8168b002 | |||
| 203bd4eeea | |||
| 02d15dea9c | |||
| a88937b716 | |||
| 986a0412b1 | |||
| e7f2ee80e2 | |||
| 31063d954d | |||
| fc1fee54cd | |||
| 094b522644 | |||
| 45dea4ec2b | |||
| 743bb25f7b | |||
| a34811bb6d | |||
| 9b460f77e5 | |||
| 85bf779f21 | |||
| 641c1cd461 | |||
| 4a5ed1b9c0 | |||
| c40941ed35 | |||
| d6184fdc8f | |||
| d61ec1747a | |||
| 7b815c9579 | |||
| 5568789902 | |||
| fd93cc9db2 | |||
| 740fd79adc | |||
| e1b6752ede | |||
| 18d9bbadfa | |||
| e27def388f | |||
| 356b989424 | |||
| c6652f7707 | |||
| df008081d1 | |||
| 0a294934ae | |||
| f711a2e4d6 | |||
| a4df4027f1 | |||
| 278bbe54ba | |||
| ca5eb53bc1 | |||
| bbc02f6f3f | |||
| 12173204d1 | |||
| cbabf5e9d1 | |||
| 1aac03c9ef | |||
| f46bcd4e4b | |||
| f29f086080 | |||
| 19001ea5be | |||
| 269746b8a4 | |||
| 35218a7435 | |||
| bd973c5f79 | |||
| d042bd1844 | |||
| ebdd489fdc | |||
| 885dec5f0e | |||
| 6106df929e | |||
| 965b9c8c1a | |||
| 98e90cc176 | |||
| d9005cccc3 | |||
| f208569e72 | |||
| 1ffe904892 | |||
| 4cc8b8f9b7 | |||
| 1f159d5dcc | |||
| f626fbe063 | |||
| d5b5ab57e3 | |||
| 6916f5ecca | |||
| e1bcb7498f | |||
| a7f8972722 | |||
| 6d1a3f5e2c | |||
| 52eca5f5c2 | |||
| 3bce45be5f | |||
| 3f0a4c57b5 | |||
| ededc7b6b4 | |||
| 5f74afeda1 | |||
| 574a8f2c38 | |||
| dd2892a9fe | |||
| 7056f8816f | |||
| c2a470f79c | |||
| 39f8a79cfd | |||
| df18b72881 | |||
| cd8adc97d6 | |||
| ba7aa39941 | |||
| 5d93ddb2c4 | |||
| 15b2043048 | |||
| aa8b6f3d9e | |||
| 3686d074e6 | |||
| 8f1a3fb6b7 | |||
| 60ce297d6a | |||
| addd7acc3c | |||
| d91ffccfa3 | |||
| adb8ed12d7 | |||
| 4acd8b8bad | |||
| d7b095cf2d | |||
| 499e0425b5 | |||
| c2c3e23024 | |||
| 5afb311ac7 | |||
| 765a862440 | |||
| b970629a59 | |||
| 072915abf2 | |||
| aeeb2e4074 | |||
| a2f7219db3 | |||
| 7e4040ba08 | |||
| 0246073b31 | |||
| 4801895321 | |||
| 833abfe72e | |||
| 379000bbd8 | |||
| 5936272ce4 | |||
| da8e7e6fd3 | |||
| 53d24ddaf1 | |||
| 8efdc8720c | |||
| ca8a0942ab | |||
| 8c3659a439 | |||
| 95a0815484 | |||
| e9caa9b8eb | |||
| 95a0db1983 | |||
| a323dcc230 | |||
| 24cd18da88 | |||
| 5d26efb552 | |||
| 60c4545a17 | |||
| 4db51b8cdf | |||
| 1c561d880e | |||
| 17fc9c6cab | |||
| 4287c5fa46 | |||
| a735884ee9 | |||
| 22554b0fa3 | |||
| 48b4849a88 | |||
| f79df4d426 | |||
| ebb18d9b49 | |||
| 31a9e6dbad | |||
| a576f9fafa | |||
| 9453bb51ce | |||
| ad58332a5d | |||
| d4f60d5e8e | |||
| 3ea05257eb | |||
| 7e49ed620b | |||
| 57bd0447e9 | |||
| 07d6f08e8b | |||
| 2f19f47a85 | |||
| d58a446eac | |||
| 5fc7d1989a | |||
| 3f6bc2b2a1 | |||
| 7681a1bad0 | |||
| a548d5329a | |||
| 07012033c7 | |||
| 92b17b2963 | |||
| b6ee04f038 | |||
| 8ffdacd6c0 | |||
| ccd402c50f | |||
| b1e872577c | |||
| 9903478d3e | |||
| 93a8981d0a | |||
| 00e7866506 | |||
| 560105f952 | |||
| 266f8a83e6 | |||
| 0b7697e9c0 | |||
| 83124eb38d | |||
| 24beb8ada1 | |||
| ee83f17afe | |||
| 99546e7eef | |||
| 4586a53590 | |||
| 1a41eeb81d | |||
| f894ffd27c | |||
| 0ec22f2207 | |||
| 3f3d95a5e4 | |||
| 811d23510e | |||
| 0597a11a23 | |||
| 2ae1d513cf | |||
| 904d30d05d | |||
| e9678c73b2 | |||
| 4060430757 | |||
| de527cd668 | |||
| 9887cb1aa3 | |||
| cdf8e4e40e | |||
| 4f21fb91a1 | |||
| 7f96d632f3 | |||
| 38dcaf16d3 | |||
| 8c57e43221 | |||
| bc78ddc49c | |||
| c88cec2beb | |||
| b7c7cecd75 | |||
| 4d0d8c453b | |||
| 5f4288a786 | |||
| 707ddb80d9 | |||
| 71f28600d1 | |||
| d39b0ae540 | |||
| ee5c77c645 | |||
| 4615bcb40d | |||
| 7843de145b | |||
| 2d1f2217e5 | |||
| ae88c34361 | |||
| 708d895a04 | |||
| 884817c5f9 | |||
| cf1c5fe120 | |||
| a04586f7df | |||
| 069e966453 | |||
| d9d275b675 | |||
| 9411dd0fdd | |||
| facd07d7de | |||
| 2ed02c2dae | |||
| 3756ea255e | |||
| 33ba40e203 | |||
| 6d66636e3d | |||
| 85278bbb75 | |||
| f0a3547bd1 | |||
| fe2a293128 | |||
| 11f605801f | |||
| 8dc12972f5 | |||
| 8a0ebf8c18 | |||
| c552372f81 | |||
| 683cb3c180 | |||
| 2cb2808c79 | |||
| e969260f3d | |||
| 8bfc13d529 | |||
| ca35945c13 | |||
| fab2214149 | |||
| e3b89f2660 | |||
| 1e9640d4fc | |||
| 116a386152 | |||
| 8957b20bac | |||
| c31ef9f80c | |||
| 3b6e4bdd3a | |||
| ef737ffa6d | |||
| 1bc635acde | |||
| ed555ba56a | |||
| 20aa172f46 | |||
| 9a58782c46 | |||
| e656956c1c | |||
| f13443b6b3 | |||
| a7688f4c9d | |||
| e324e15fc0 | |||
| d7cc08dff7 | |||
| 1c8b35bcfb | |||
| 84679b40ce | |||
| b6bd7eda6e | |||
| cfe8a38620 | |||
| 4be0b0db09 | |||
| 2f5d20f972 | |||
| 0f953b8701 | |||
| 62ec6484c8 | |||
| 0472c12113 | |||
| a9a9dc8a0a | |||
| 4fc8f1f6cf | |||
| 231f97fafc | |||
| 76e30d91c0 | |||
| e789fe312d | |||
| f73bb2bc2f | |||
| 0da970ec9a | |||
| 62f00055b7 | |||
| e695e29355 | |||
| 9012a9fc1c | |||
| b009f195be | |||
| dddc890a96 | |||
| 794ef16629 | |||
| d1d47b5223 | |||
| 24ed878d8e | |||
| c2b8400986 | |||
| 0a33047ad6 | |||
| c98e024f9c | |||
| d6d7ba8480 | |||
| b6cde145e1 | |||
| 9a4f20ca00 | |||
| b5af5a118d | |||
| 60a557bd37 | |||
| 97ab33c899 | |||
| a1810db96d | |||
| 39d656ad21 | |||
| 1d9bcc63d2 | |||
| 6102dd5b85 | |||
| 495ee6f0c3 | |||
| 0e1e619f0a | |||
| 0cba528591 | |||
| 442501828a | |||
| 202f49f368 | |||
| 7bbfe06494 | |||
| 267254dcae | |||
| 5668748f37 | |||
| b7de61e4d1 | |||
| c4d5cfd17b | |||
| 1f965897f2 | |||
| 46fe48870c | |||
| c287e3ec32 | |||
| 4348e5c427 | |||
| e6a7d9b047 | |||
| ddf1686ea5 | |||
| 501fbda762 | |||
| a83efd0b01 | |||
| a1139efecb | |||
| d8373ab135 | |||
| f0b9b51229 | |||
| 76a338f3d0 | |||
| 0ac7b9babd | |||
| f336af5d65 | |||
| 3d6be3900e | |||
| 285e7082fb | |||
| 207cef5423 | |||
| c3b3f24704 | |||
| 6229e5e49d | |||
| 07465d6f0c | |||
| d2eeb23107 | |||
| ccd230568e | |||
| 7a19b78d31 | |||
| 31b293324d | |||
| 7bc9f134f6 | |||
| 6e30e24957 | |||
| 42f2475f3c | |||
| a487d33f52 | |||
| bfd740c81e | |||
| 03efeff85a | |||
| d4222db0e8 | |||
| 21a16e5b7c | |||
| 4616f077d5 | |||
| d210d29554 | |||
| dde07fc256 | |||
| 788996abcf | |||
| 224fbde19a | |||
| a0865405e2 | |||
| 6e5471271d | |||
| 0651f185e3 | |||
| 33377291a6 | |||
| dfd5a15274 | |||
| 09a7608bcb | |||
| 92d2d6d2bc | |||
| 641734ba70 | |||
| 5d0de4a1b1 | |||
| f92d09a863 | |||
| 5670ae79bf | |||
| aedbbbe0a6 | |||
| 5d9d48ebfc | |||
| eb6cb94893 | |||
| 9d40d78562 | |||
| 896cb536dd | |||
| 493f7f837a | |||
| c94beb51a4 | |||
| c2c723f86f | |||
| 29d58cc8af | |||
| 5bb637f3e2 | |||
| 75cb6b2bac | |||
| c077649734 | |||
| 319eace472 | |||
| c8587929b5 | |||
| 14048b0b7c | |||
| a7f27480e9 | |||
| 094b1e9f00 | |||
| 31dbbbeedc | |||
| ba7ded14b5 | |||
| 4c4e8c37fb | |||
| 0fb3a216e1 | |||
| 1ed6401ba3 | |||
| a510ac2acb | |||
| 5553fe6386 | |||
| 79a3410bb9 | |||
| b647769b51 | |||
| 909b697b78 | |||
| 44657a86b8 | |||
| 45bda1b2ac | |||
| 65bde123ed | |||
| d0faf1e2b6 | |||
| e7864f9dc8 | |||
| 66050c68f6 | |||
| 195e29da52 | |||
| 0b9a42e84c | |||
| 00ccf82196 | |||
| a41a08c9af | |||
| 3d23943b69 | |||
| 51b482aefb | |||
| 6419f6b8a2 | |||
| 4911d05869 | |||
| 2b4f7c4e80 | |||
| 2c62f601ca | |||
| 2bda7c9f0f | |||
| 9d99e3a003 | |||
| b42348665f | |||
| 4d30178752 | |||
| 2b578fa006 | |||
| 78c625361e | |||
| dd3a2b0e9a | |||
| 9b21a0c6d7 | |||
| 5b9e6c28e4 | |||
| ecdf571c71 | |||
| f5ab070453 | |||
| 5873bff48b | |||
| 190f2a062f | |||
| 384d53da47 | |||
| 1b62f7af04 | |||
| 48d4220199 | |||
| ff496e4efe | |||
| 1a3e18795b | |||
| 507dff95b8 | |||
| a1f32eda79 | |||
| 545d031ee7 | |||
| 7d198f837c | |||
| 5948dc9cae | |||
| 6dffd8f5ad | |||
| 29b2572f9a | |||
| c1e21abd45 | |||
| edc81ae45e | |||
| 7edf6ee843 | |||
| f568308d1a | |||
| d0351ab765 | |||
| 4f8febde23 | |||
| 00940d1b6e | |||
| 1757e9e01d | |||
| 62140789a7 | |||
| ad877383ce | |||
| b49cdc3e55 | |||
| 1e093a8413 | |||
| 79c6ec340c | |||
| 37c76a720d | |||
| c06758a2bd | |||
| 026227848b | |||
| 5cd2875b35 | |||
| 5e346270c6 | |||
| 5c79fbe73d | |||
| 4e747a8e6c | |||
| 310165db02 | |||
| 48be4d5d46 | |||
| bb703ef8eb | |||
| 7462d1bdca | |||
| cd85857816 | |||
| 4d80710b48 | |||
| 42d404577b | |||
| 81a57dc470 | |||
| bd3984037c | |||
| 01283d2b18 | |||
| a26ab2ba28 | |||
| 4baeb57e84 | |||
| 1bcfe5d031 | |||
| 1d1cf41ba0 | |||
| 9bd262dec0 | |||
| 068de63e83 | |||
| 6c2810ef80 | |||
| 7b01524934 | |||
| 69da8c4632 | |||
| 56a298d95c | |||
| 93bf15c27f | |||
| e017723318 | |||
| 13b01240b1 | |||
| 2881815c7a | |||
| 84d3f90549 | |||
| 7c4eac5460 | |||
| be318bc1c1 | |||
| 09cdb98dfc | |||
| 823320e982 | |||
| 289a2044a6 | |||
| 7508fbf715 | |||
| 4525be302c | |||
| 2042c7d3f2 | |||
| 2af02096e0 | |||
| 744062152e | |||
| ef12cf2966 | |||
| 56592f909a | |||
| 521ada17cc | |||
| 2bde2545f4 | |||
| 00ee205b44 | |||
| 28fb1a1a67 | |||
| 9a17fe97d3 | |||
| cab603c50d | |||
| 40d99b402f | |||
| 45509cfd3f | |||
| 8e0c082876 | |||
| 3c59da17c2 | |||
| ae010212ac | |||
| ce6b25c12a | |||
| 6993d169e4 | |||
| eddcc7602d | |||
| 9783d417bd | |||
| 91281c8813 | |||
| c2f775258d | |||
| 930dd59213 | |||
| 9cc6cfa1b1 | |||
| e33b5ba063 | |||
| 8cc14997af | |||
| eee2d0e3fe | |||
| 2a58a8e123 | |||
| c466a24486 | |||
| 71fc1e61ef | |||
| a145fd3c60 | |||
| dddd81fd90 | |||
| 8e46ed1175 | |||
| 09ed9c228f | |||
| eb21f76ef4 | |||
| 954831664a | |||
| acc75868c0 | |||
| 74272bea80 | |||
| bf104309b4 | |||
| 321c476b8b | |||
| 1db51b1cce | |||
| 10f9b5357c | |||
| a9aaab8337 | |||
| 65ded42120 | |||
| d6ffa77f35 | |||
| 3133228fc9 | |||
| 1440d2a16a | |||
| 152e057a46 | |||
| de70999975 | |||
| 1a9bd795a8 | |||
| 891bb49548 | |||
| 4c1ac418a0 | |||
| 183d169cc1 | |||
| 97dfe27953 | |||
| b509a4139d | |||
| 13264a2f8d | |||
| 9df42d26de | |||
| 9daa768629 | |||
| 3592a4ce59 | |||
| e746afb57f | |||
| 0c98b240c3 | |||
| 56c9b17ff6 | |||
| c4d09f2466 | |||
| bbc3466bc8 | |||
| c18259a348 |
@@ -202,5 +202,14 @@ FakesAssemblies/
|
|||||||
# Git worktrees
|
# Git worktrees
|
||||||
.worktrees/
|
.worktrees/
|
||||||
|
|
||||||
|
# SQLite databases
|
||||||
|
*.db
|
||||||
|
*.db-journal
|
||||||
|
|
||||||
# Claude Code
|
# Claude Code
|
||||||
.claude/
|
.claude/
|
||||||
|
.superpowers/
|
||||||
|
docs/superpowers/
|
||||||
|
|
||||||
|
# Launch settings
|
||||||
|
**/Properties/launchSettings.json
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"mcpServers": {
|
"mcpServers": {
|
||||||
"opennest": {
|
"opennest": {
|
||||||
"command": "C:/Users/AJ/.claude/mcp/OpenNest.Mcp/OpenNest.Mcp.exe",
|
"command": "cmd",
|
||||||
"args": []
|
"args": ["/c", "C:/Users/AJ/.claude/mcp/OpenNest.Mcp/run.cmd"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|||||||
|
|
||||||
## Project Overview
|
## Project Overview
|
||||||
|
|
||||||
OpenNest is a Windows desktop application for CNC nesting — arranging 2D parts on material plates to minimize waste. It imports DXF drawings, places parts onto plates using rectangle-packing algorithms, and can export nest layouts as DXF or post-process them to G-code for CNC cutting machines.
|
OpenNest is a Windows desktop application for CNC nesting — arranging 2D parts on material plates to minimize waste. It imports DXF drawings, places parts onto plates using NFP-based (No Fit Polygon) and rectangle-packing algorithms, and can export nest layouts as DXF or post-process them to G-code for CNC cutting machines.
|
||||||
|
|
||||||
## Build
|
## Build
|
||||||
|
|
||||||
@@ -14,40 +14,60 @@ This is a .NET 8 solution using SDK-style `.csproj` files targeting `net8.0-wind
|
|||||||
dotnet build OpenNest.sln
|
dotnet build OpenNest.sln
|
||||||
```
|
```
|
||||||
|
|
||||||
NuGet dependencies: `ACadSharp` 3.1.32 (DXF/DWG import/export, in OpenNest.IO), `System.Drawing.Common` 8.0.10, `ModelContextProtocol` + `Microsoft.Extensions.Hosting` (in OpenNest.Mcp).
|
NuGet dependencies: `ACadSharp` 3.1.32 (DXF/DWG import/export, in OpenNest.IO), `System.Drawing.Common` 8.0.10, `ModelContextProtocol` + `Microsoft.Extensions.Hosting` (in OpenNest.Mcp), `Microsoft.ML.OnnxRuntime` (in OpenNest.Engine for ML angle prediction), `Microsoft.EntityFrameworkCore.Sqlite` (in OpenNest.Training).
|
||||||
|
|
||||||
No test projects exist in this solution.
|
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
Five projects form a layered architecture:
|
Eight projects form a layered architecture:
|
||||||
|
|
||||||
### OpenNest.Core (class library)
|
### OpenNest.Core (class library)
|
||||||
Domain model, geometry, and CNC primitives organized into namespaces:
|
Domain model, geometry, and CNC primitives organized into namespaces:
|
||||||
|
|
||||||
- **Root** (`namespace OpenNest`): Domain model — `Nest` → `Plate[]` → `Part[]` → `Drawing` → `Program`. A `Nest` is the top-level container. Each `Plate` has a size, material, quadrant, spacing, and contains placed `Part` instances. Each `Part` references a `Drawing` (the template) and has its own location/rotation. A `Drawing` wraps a CNC `Program`. Also contains utilities: `Helper`, `Align`, `Sequence`, `Timing`.
|
- **Root** (`namespace OpenNest`): Domain model — `Nest` → `Plate[]` → `Part[]` → `Drawing` → `Program`. A `Nest` is the top-level container. Each `Plate` has a size, material, quadrant, spacing, and contains placed `Part` instances. Each `Part` references a `Drawing` (the template) and has its own location/rotation. A `Drawing` wraps a CNC `Program`. Also contains utilities: `PartGeometry`, `Align`, `Sequence`, `Timing`.
|
||||||
- **CNC** (`CNC/`, `namespace OpenNest.CNC`): `Program` holds a list of `ICode` instructions (G-code-like: `RapidMove`, `LinearMove`, `ArcMove`, `SubProgramCall`). Programs support absolute/incremental mode conversion, rotation, offset, bounding box calculation, and cloning.
|
- **CNC** (`CNC/`, `namespace OpenNest.CNC`): `Program` holds a list of `ICode` instructions (G-code-like: `RapidMove`, `LinearMove`, `ArcMove`, `SubProgramCall`) and an optional `Variables` dictionary of `VariableDefinition` entries. Programs support absolute/incremental mode conversion, rotation, offset, bounding box calculation, and cloning. `VariableDefinition` stores a named variable's expression, resolved value, and flags (`Inline`, `Global`). `ProgramVariableManager` manages numbered machine variables for post-processor output.
|
||||||
- **Geometry** (`Geometry/`, `namespace OpenNest.Geometry`): Spatial primitives (`Vector`, `Box`, `Size`, `Spacing`, `BoundingBox`, `IBoundable`) and higher-level shapes (`Line`, `Arc`, `Circle`, `Polygon`, `Shape`) used for intersection detection, area calculation, and DXF conversion.
|
- **Geometry** (`Geometry/`, `namespace OpenNest.Geometry`): Spatial primitives (`Vector`, `Box`, `Size`, `Spacing`, `BoundingBox`, `IBoundable`) and higher-level shapes (`Line`, `Arc`, `Circle`, `Polygon`, `Shape`) used for intersection detection, area calculation, and DXF conversion. Also contains `Intersect` (intersection algorithms), `ShapeBuilder` (entity chaining), `GeometryOptimizer` (line/arc merging), `SpatialQuery` (directional distance, ray casting, box queries), `ShapeProfile` (perimeter/area analysis), `NoFitPolygon`, `InnerFitPolygon`, `ConvexHull`, `ConvexDecomposition`, `RotatingCalipers`, and `Collision` (overlap detection with Sutherland-Hodgman polygon clipping and hole subtraction).
|
||||||
- **Converters** (`Converters/`, `namespace OpenNest.Converters`): Bridges between CNC and Geometry — `ConvertProgram` (CNC→Geometry), `ConvertGeometry` (Geometry→CNC), `ConvertMode` (absolute↔incremental).
|
- **Converters** (`Converters/`, `namespace OpenNest.Converters`): Bridges between CNC and Geometry — `ConvertProgram` (CNC→Geometry), `ConvertGeometry` (Geometry→CNC), `ConvertMode` (absolute↔incremental).
|
||||||
- **Math** (`Math/`, `namespace OpenNest.Math`): `Angle` (radian/degree conversion), `Tolerance` (floating-point comparison), `Trigonometry`, `Generic` (swap utility), `EvenOdd`. Note: `OpenNest.Math` shadows `System.Math` — use `System.Math` fully qualified where both are needed.
|
- **Math** (`Math/`, `namespace OpenNest.Math`): `Angle` (radian/degree conversion), `Tolerance` (floating-point comparison), `Trigonometry`, `Generic` (swap utility), `EvenOdd`, `Rounding` (factor-based rounding), `ExpressionEvaluator` (arithmetic expression parser for G-code variable expressions with `$name` references). Note: `OpenNest.Math` shadows `System.Math` — use `System.Math` fully qualified where both are needed.
|
||||||
|
- **CNC/CuttingStrategy** (`CNC/CuttingStrategy/`, `namespace OpenNest.CNC`): `ContourCuttingStrategy` orchestrates cut ordering, lead-ins/lead-outs, and tabs. Includes `LeadIn`/`LeadOut` hierarchies (line, arc, clean-hole variants), `Tab` hierarchy (normal, machine, breaker), and `CuttingParameters`/`AssignmentParameters`/`SequenceParameters` configuration.
|
||||||
- **Collections** (`Collections/`, `namespace OpenNest.Collections`): `ObservableList<T>`, `DrawingCollection`.
|
- **Collections** (`Collections/`, `namespace OpenNest.Collections`): `ObservableList<T>`, `DrawingCollection`.
|
||||||
|
- **CutOffs** (`namespace OpenNest`): `CutOff` (axis-aligned cut line with position, axis, optional start/end limits), `CutOffAxis` enum (`Horizontal`, `Vertical`), `CutOffSettings` (clearance, overtravel, min segment length, direction), `CutDirection` enum (`TowardOrigin`, `AwayFromOrigin`). Cut-offs generate CNC `Program` objects with trimmed line segments that avoid parts.
|
||||||
|
- **Splitting** (`Splitting/`, `namespace OpenNest`): `DrawingSplitter` splits a Drawing into multiple pieces along split lines. `ISplitFeature` strategy pattern with implementations: `StraightSplit` (clean edge), `WeldGapTabSplit` (rectangular tab spacers on one side), `SpikeGrooveSplit` (interlocking spike/V-groove pairs). `AutoSplitCalculator` computes split lines for fit-to-plate and split-by-count modes. Supporting types: `SplitLine`, `SplitParameters`, `SplitFeatureResult`.
|
||||||
- **Quadrant system**: Plates use quadrants 1-4 (like Cartesian quadrants) to determine coordinate origin placement. This affects bounding box calculation, rotation, and part positioning.
|
- **Quadrant system**: Plates use quadrants 1-4 (like Cartesian quadrants) to determine coordinate origin placement. This affects bounding box calculation, rotation, and part positioning.
|
||||||
|
|
||||||
### OpenNest.Engine (class library, depends on Core)
|
### OpenNest.Engine (class library, depends on Core)
|
||||||
Nesting algorithms. `NestEngine` orchestrates filling plates with parts.
|
Nesting algorithms with a pluggable engine architecture. `NestEngineBase` is the abstract base class; `DefaultNestEngine` (formerly `NestEngine`) provides the multi-phase fill strategy. `NestEngineRegistry` manages available engines (built-in + plugins from `Engines/` directory) and the globally active engine.
|
||||||
|
|
||||||
- **RectanglePacking/**: `FillBestFit` (single-item fill, tries horizontal and vertical orientations), `PackBottomLeft` (multi-item bin packing, sorts by area descending). Both operate on `Bin`/`Item` abstractions.
|
- **Engine hierarchy**: `NestEngineBase` (abstract) → `DefaultNestEngine` (Linear, Pairs, RectBestFit, Remainder phases) → `VerticalRemnantEngine` (optimizes for right-side drop), `HorizontalRemnantEngine` (optimizes for top-side drop). Custom engines subclass `NestEngineBase` and register via `NestEngineRegistry.Register()` or as plugin DLLs in `Engines/`.
|
||||||
- **CirclePacking/**: Alternative packing for circular parts.
|
- **IFillComparer**: Interface enabling engine-specific scoring. `DefaultFillComparer` (count-then-density), `VerticalRemnantComparer` (minimize X-extent), `HorizontalRemnantComparer` (minimize Y-extent). Engines provide their comparer via `CreateComparer()` factory, grouped into `FillPolicy` on `FillContext`.
|
||||||
|
- **NestEngineRegistry**: Static registry — `Create(Plate)` factory, `ActiveEngineName` global selection, `LoadPlugins(directory)` for DLL discovery. All callsites use `NestEngineRegistry.Create(plate)` except `BruteForceRunner` which uses `new DefaultNestEngine(plate)` directly for training consistency.
|
||||||
|
- **Fill/** (`namespace OpenNest.Engine.Fill`): Fill algorithms — `FillLinear` (grid-based), `FillExtents` (extents-based pair tiling), `PairFiller` (interlocking pairs), `ShrinkFiller`, `RemnantFiller`/`RemnantFinder`, `Compactor` (post-fill gravity compaction), `FillScore` (lexicographic comparison: count > utilization > compactness), `Pattern`/`PatternTiler`, `PartBoundary`, `RotationAnalysis`, `AngleCandidateBuilder`, `BestCombination`, `AccumulatingProgress`.
|
||||||
|
- **Strategies/** (`namespace OpenNest.Engine.Strategies`): Pluggable fill strategy layer — `IFillStrategy` interface, `FillContext`, `FillStrategyRegistry` (auto-discovers strategies via reflection, supports plugin DLLs), `FillHelpers`. Built-in strategies: `LinearFillStrategy`, `PairsFillStrategy`, `RectBestFitStrategy`, `ExtentsFillStrategy`.
|
||||||
|
- **BestFit/** (`namespace OpenNest.Engine.BestFit`): NFP-based pair evaluation pipeline — `BestFitFinder` orchestrates angle sweeps, `PairEvaluator`/`IPairEvaluator` scores part pairs, `RotationSlideStrategy`/`ISlideComputer` computes slide distances. `BestFitCache` and `BestFitFilter` optimize repeated lookups.
|
||||||
|
- **RectanglePacking/** (`namespace OpenNest.RectanglePacking`): `FillBestFit` (single-item fill, tries horizontal and vertical orientations), `PackBottomLeft` (multi-item bin packing, sorts by area descending). Both operate on `Bin`/`Item` abstractions.
|
||||||
|
- **CirclePacking/** (`namespace OpenNest.CirclePacking`): Alternative packing for circular parts.
|
||||||
|
- **Nfp/** (`namespace OpenNest.Engine.Nfp`): Internal NFP-based single-part placement utilities — `AutoNester` (NFP placement with simulated annealing), `BottomLeftFill` (BLF placement), `NfpCache` (computed NFP caching), `SimulatedAnnealing` (optimizer), `INestOptimizer`/`OptimizationResult`. Not exposed as a nest engine; used internally for individual part placement.
|
||||||
|
- **ML/** (`namespace OpenNest.Engine.ML`): `AnglePredictor` (ONNX model for predicting good rotation angles), `FeatureExtractor` (part geometry features), `BruteForceRunner` (full angle sweep for training data).
|
||||||
- `NestItem`: Input to the engine — wraps a `Drawing` with quantity, priority, and rotation constraints.
|
- `NestItem`: Input to the engine — wraps a `Drawing` with quantity, priority, and rotation constraints.
|
||||||
- `BestCombination`: Finds optimal mix of normal/rotated columns for grid fills.
|
- `NestProgress`: Progress reporting model with `NestPhase` enum for UI feedback.
|
||||||
|
|
||||||
### OpenNest.IO (class library, depends on Core)
|
### OpenNest.IO (class library, depends on Core)
|
||||||
File I/O and format conversion. Uses ACadSharp for DXF/DWG support.
|
File I/O and format conversion. Uses ACadSharp for DXF/DWG support.
|
||||||
|
|
||||||
- `DxfImporter`/`DxfExporter` — DXF file import/export via ACadSharp.
|
- `DxfImporter`/`DxfExporter` — DXF file import/export via ACadSharp.
|
||||||
- `NestReader`/`NestWriter` — custom ZIP-based nest format (XML metadata + G-code programs).
|
- `NestReader`/`NestWriter` — custom ZIP-based nest format (JSON metadata + G-code programs, v2 format).
|
||||||
- `ProgramReader` — G-code text parser.
|
- `ProgramReader` — G-code text parser.
|
||||||
- `Extensions` — conversion helpers between ACadSharp and OpenNest geometry types.
|
- `Extensions` — conversion helpers between ACadSharp and OpenNest geometry types.
|
||||||
|
- `CadImporter` — shared "DXF → Drawing" service used by the UI, console, MCP, API, and training projects. Two-stage API: `Import(path, options)` loads raw entities, runs bend detection, and returns a mutable `CadImportResult`; `BuildDrawing(result, visible, bends, quantity, customer, editedProgram)` produces a fully-populated `Drawing` with `Source.Offset`, `SourceEntities`, `SuppressedEntityIds`, and bends. `ImportDrawing(path, options)` composes both stages for headless callers.
|
||||||
|
- `CadImportOptions`, `CadImportResult` — inputs and intermediate state for `CadImporter`.
|
||||||
|
|
||||||
|
### OpenNest.Console (console app, depends on Core + Engine + IO)
|
||||||
|
Command-line interface for batch nesting. Supports DXF import, plate configuration, linear fill, and NFP-based auto-nesting (`--autonest`).
|
||||||
|
|
||||||
|
### OpenNest.Gpu (class library, depends on Core + Engine)
|
||||||
|
GPU-accelerated pair evaluation for best-fit nesting. `GpuPairEvaluator` implements `IPairEvaluator`, `GpuSlideComputer` implements `ISlideComputer`, and `PartBitmap` handles rasterization. `GpuEvaluatorFactory` provides factory methods.
|
||||||
|
|
||||||
|
### OpenNest.Training (console app, depends on Core + Engine)
|
||||||
|
Training data collection for ML angle prediction. `TrainingDatabase` stores per-angle nesting results in SQLite via EF Core for offline model training.
|
||||||
|
|
||||||
### OpenNest.Mcp (console app, depends on Core + Engine + IO)
|
### OpenNest.Mcp (console app, depends on Core + Engine + IO)
|
||||||
MCP server for Claude Code integration. Exposes nesting operations as MCP tools over stdio transport. Published to `~/.claude/mcp/OpenNest.Mcp/`.
|
MCP server for Claude Code integration. Exposes nesting operations as MCP tools over stdio transport. Published to `~/.claude/mcp/OpenNest.Mcp/`.
|
||||||
@@ -61,19 +81,17 @@ MCP server for Claude Code integration. Exposes nesting operations as MCP tools
|
|||||||
### OpenNest (WinForms WinExe, depends on Core + Engine + IO)
|
### OpenNest (WinForms WinExe, depends on Core + Engine + IO)
|
||||||
The UI application with MDI interface.
|
The UI application with MDI interface.
|
||||||
|
|
||||||
- **Forms/**: `MainForm` (MDI parent), `EditNestForm` (MDI child per nest), plus dialogs for plate editing, auto-nesting, DXF conversion, cut parameters, etc.
|
- **Forms/**: `MainForm` (MDI parent), `EditNestForm` (MDI child per nest), `SplitDrawingForm` (split oversized drawings into smaller pieces, launched from CadConverterForm), plus dialogs for plate editing, auto-nesting, DXF conversion, cut parameters, etc.
|
||||||
- **Controls/**: `PlateView` (2D plate renderer with zoom/pan), `DrawingListBox`, `DrawControl`, `QuadrantSelect`.
|
- **Controls/**: `PlateView` (2D plate renderer with zoom/pan, supports temporary preview parts), `DrawingListBox`, `DrawControl`, `QuadrantSelect`.
|
||||||
- **Actions/**: User interaction modes — `ActionSelect`, `ActionAddPart`, `ActionClone`, `ActionFillArea`, `ActionZoomWindow`, `ActionSetSequence`.
|
- **Actions/**: User interaction modes — `ActionSelect`, `ActionClone`, `ActionFillArea`, `ActionSelectArea`, `ActionZoomWindow`, `ActionSetSequence`, `ActionCutOff`.
|
||||||
- **Post-processing**: `IPostProcessor` plugin interface loaded from DLLs in a `Posts/` directory at runtime.
|
- **Post-processing**: `IPostProcessor` plugin interface loaded from DLLs in a `Posts/` directory at runtime.
|
||||||
|
|
||||||
## File Format
|
## File Format
|
||||||
|
|
||||||
Nest files (`.zip`) contain:
|
Nest files (`.nest`, ZIP-based) use v2 JSON format:
|
||||||
- `info` — XML with nest metadata and plate defaults
|
- `nest.json` — single JSON file containing all nest metadata: nest info (name, units, customer, dates, notes), plate defaults (size, thickness, quadrant, spacing, material, edge spacing), drawings array (id, name, color, quantity, priority, rotation constraints, material, source), and plates array (id, size, material, edge spacing, parts with drawingId/x/y/rotation, cutoffs with x/y/axis/startLimit/endLimit)
|
||||||
- `drawing-info` — XML with drawing metadata (name, material, quantities, colors)
|
- `programs/program-N` — G-code text for each drawing's cut program (N = drawing id)
|
||||||
- `plate-info` — XML with plate metadata (size, material, spacing)
|
- `bestfits/bestfit-N` — JSON array of best-fit pair evaluation results per drawing, keyed by plate size/spacing (optional, only present if best-fit data was computed)
|
||||||
- `program-NNN` — G-code text for each drawing's cut program
|
|
||||||
- `plate-NNN` — G-code text encoding part placements (G00 for position, G65 for sub-program call with rotation)
|
|
||||||
|
|
||||||
## Tool Preferences
|
## Tool Preferences
|
||||||
|
|
||||||
@@ -83,9 +101,22 @@ Always use Roslyn Bridge MCP tools (`mcp__RoslynBridge__*`) as the primary metho
|
|||||||
|
|
||||||
- Always use `var` instead of explicit types (e.g., `var parts = new List<Part>();` not `List<Part> parts = new List<Part>();`).
|
- Always use `var` instead of explicit types (e.g., `var parts = new List<Part>();` not `List<Part> parts = new List<Part>();`).
|
||||||
|
|
||||||
|
## Documentation Maintenance
|
||||||
|
|
||||||
|
Always keep `README.md` and `CLAUDE.md` up to date when making changes that affect project structure, architecture, build instructions, dependencies, or key patterns. If you add a new project, change a namespace, modify the build process, or alter significant behavior, update both files as part of the same change.
|
||||||
|
|
||||||
|
**Do not commit** design specs, implementation plans, or other temporary planning documents (`docs/superpowers/` etc.) to the repository. These are working documents only — keep them local and untracked.
|
||||||
|
|
||||||
## Key Patterns
|
## Key Patterns
|
||||||
|
|
||||||
- OpenNest.Core uses multiple namespaces: `OpenNest` (root domain), `OpenNest.CNC`, `OpenNest.Geometry`, `OpenNest.Converters`, `OpenNest.Math`, `OpenNest.Collections`.
|
- OpenNest.Core uses multiple namespaces: `OpenNest` (root domain), `OpenNest.CNC`, `OpenNest.Geometry`, `OpenNest.Converters`, `OpenNest.Math`, `OpenNest.Collections`.
|
||||||
|
- OpenNest.Engine uses sub-namespaces: `OpenNest.Engine.Fill` (fill algorithms), `OpenNest.Engine.Strategies` (pluggable strategy layer), `OpenNest.Engine.BestFit`, `OpenNest.Engine.Nfp` (NFP-based nesting, not yet integrated), `OpenNest.Engine.ML`, `OpenNest.Engine.RapidPlanning`, `OpenNest.Engine.Sequencing`.
|
||||||
- `ObservableList<T>` provides ItemAdded/ItemRemoved/ItemChanged events used for automatic quantity tracking between plates and drawings.
|
- `ObservableList<T>` provides ItemAdded/ItemRemoved/ItemChanged events used for automatic quantity tracking between plates and drawings.
|
||||||
- Angles throughout the codebase are in **radians** (use `Angle.ToRadians()`/`Angle.ToDegrees()` for conversion).
|
- Angles throughout the codebase are in **radians** (use `Angle.ToRadians()`/`Angle.ToDegrees()` for conversion).
|
||||||
- `Tolerance.Epsilon` is used for floating-point comparisons across geometry operations.
|
- `Tolerance.Epsilon` is used for floating-point comparisons across geometry operations.
|
||||||
|
- Nesting uses async progress/cancellation: `IProgress<NestProgress>` and `CancellationToken` flow through the engine to the UI's `NestProgressForm`.
|
||||||
|
- `Compactor` performs post-fill gravity compaction — after filling, parts are pushed toward a plate edge using directional distance calculations to close gaps between irregular shapes.
|
||||||
|
- `FillScore` uses lexicographic comparison (count > utilization > compactness) to rank fill results consistently across all fill strategies.
|
||||||
|
- **Cut-off materialization lifecycle**: `CutOff` objects live on `Plate.CutOffs`. Each generates a `Drawing` (with `IsCutOff = true`) whose `Program` contains trimmed line segments. `Plate.RegenerateCutOffs(settings)` removes old cut-off Parts, recomputes programs, and re-adds them to `Plate.Parts`. Regeneration triggers: cut-off add/remove/move, part drag complete, fill complete, plate transform. Cut-off Parts are excluded from quantity tracking, utilization, overlap detection, and nest file serialization (programs are regenerated from definitions on load).
|
||||||
|
- **User-defined G-code variables**: Programs can contain named variable definitions (`name = expression [inline] [global]`) referenced in coordinates with `$name`. Variables resolve to doubles at parse time for geometry/nesting. `VariableRefs` on `Motion`/`Feedrate` track the symbolic link so post processors can emit machine variable references. Cincinnati post maps non-inline variables to numbered machine variables (`#200+`) with descriptive comments. Global variables share a number across programs; local variables get per-drawing numbers. `ProgramReader` uses a two-pass parse (collect definitions, then parse G-code with substitution). `NestWriter` serializes definitions and `$references` back to text for round-trip fidelity.
|
||||||
|
- **CAD import pipeline**: All "DXF → Drawing" conversion goes through `OpenNest.IO.CadImporter`. The UI form uses `Import` on file load (storing the mutable result in a `FileListItem`) and `BuildDrawing` on save (passing the user's current visible entities and bends). Console, MCP, API, and Training projects use `ImportDrawing` for headless conversion. This guarantees all callers produce drawings with the same shape: pierce-point `Source.Offset`, stable `SourceEntities` with GUIDs, `SuppressedEntityIds`, detected bends, and metadata.
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 AJ Isaacs
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
|
||||||
|
namespace OpenNest.Api;
|
||||||
|
|
||||||
|
public class NestRequest
|
||||||
|
{
|
||||||
|
public IReadOnlyList<NestRequestPart> Parts { get; init; } = [];
|
||||||
|
public Size SheetSize { get; init; } = new(60, 120);
|
||||||
|
public string Material { get; init; } = "Steel, A1011 HR";
|
||||||
|
public double Thickness { get; init; } = 0.06;
|
||||||
|
public double Spacing { get; init; } = 0.1;
|
||||||
|
public NestStrategy Strategy { get; init; } = NestStrategy.Auto;
|
||||||
|
public CutParameters Cutting { get; init; } = CutParameters.Default;
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
namespace OpenNest.Api;
|
||||||
|
|
||||||
|
public class NestRequestPart
|
||||||
|
{
|
||||||
|
public string DxfPath { get; init; }
|
||||||
|
public int Quantity { get; init; } = 1;
|
||||||
|
public bool AllowRotation { get; init; } = true;
|
||||||
|
public int Priority { get; init; } = 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.IO.Compression;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using OpenNest.IO;
|
||||||
|
|
||||||
|
namespace OpenNest.Api;
|
||||||
|
|
||||||
|
public class NestResponse
|
||||||
|
{
|
||||||
|
public int SheetCount { get; init; }
|
||||||
|
public double Utilization { get; init; }
|
||||||
|
public TimeSpan CutTime { get; init; }
|
||||||
|
public TimeSpan Elapsed { get; init; }
|
||||||
|
public Nest Nest { get; init; }
|
||||||
|
public NestRequest Request { get; init; }
|
||||||
|
|
||||||
|
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||||
|
{
|
||||||
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||||
|
WriteIndented = true,
|
||||||
|
IncludeFields = true // Required for OpenNest.Geometry.Size (public fields)
|
||||||
|
};
|
||||||
|
|
||||||
|
public async Task SaveAsync(string path)
|
||||||
|
{
|
||||||
|
using var fs = new FileStream(path, FileMode.Create);
|
||||||
|
using var zip = new ZipArchive(fs, ZipArchiveMode.Create);
|
||||||
|
|
||||||
|
// Write request.json
|
||||||
|
var requestEntry = zip.CreateEntry("request.json");
|
||||||
|
await using (var stream = requestEntry.Open())
|
||||||
|
{
|
||||||
|
await JsonSerializer.SerializeAsync(stream, Request, JsonOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write response.json (metrics only)
|
||||||
|
var metrics = new
|
||||||
|
{
|
||||||
|
SheetCount,
|
||||||
|
Utilization,
|
||||||
|
CutTimeTicks = CutTime.Ticks,
|
||||||
|
ElapsedTicks = Elapsed.Ticks
|
||||||
|
};
|
||||||
|
var responseEntry = zip.CreateEntry("response.json");
|
||||||
|
await using (var stream = responseEntry.Open())
|
||||||
|
{
|
||||||
|
await JsonSerializer.SerializeAsync(stream, metrics, JsonOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write embedded nest.nest via NestWriter → MemoryStream → ZIP entry
|
||||||
|
var nestEntry = zip.CreateEntry("nest.nest");
|
||||||
|
using var nestMs = new MemoryStream();
|
||||||
|
var writer = new NestWriter(Nest);
|
||||||
|
writer.Write(nestMs);
|
||||||
|
nestMs.Position = 0;
|
||||||
|
await using (var stream = nestEntry.Open())
|
||||||
|
{
|
||||||
|
await nestMs.CopyToAsync(stream);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<NestResponse> LoadAsync(string path)
|
||||||
|
{
|
||||||
|
using var fs = new FileStream(path, FileMode.Open, FileAccess.Read);
|
||||||
|
using var zip = new ZipArchive(fs, ZipArchiveMode.Read);
|
||||||
|
|
||||||
|
// Read request.json
|
||||||
|
var requestEntry = zip.GetEntry("request.json")
|
||||||
|
?? throw new InvalidOperationException("Missing request.json in .nestquote file");
|
||||||
|
NestRequest request;
|
||||||
|
await using (var stream = requestEntry.Open())
|
||||||
|
{
|
||||||
|
request = await JsonSerializer.DeserializeAsync<NestRequest>(stream, JsonOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read response.json
|
||||||
|
var responseEntry = zip.GetEntry("response.json")
|
||||||
|
?? throw new InvalidOperationException("Missing response.json in .nestquote file");
|
||||||
|
JsonElement metricsJson;
|
||||||
|
await using (var stream = responseEntry.Open())
|
||||||
|
{
|
||||||
|
metricsJson = await JsonSerializer.DeserializeAsync<JsonElement>(stream, JsonOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read embedded nest.nest via NestReader(Stream)
|
||||||
|
var nestEntry = zip.GetEntry("nest.nest")
|
||||||
|
?? throw new InvalidOperationException("Missing nest.nest in .nestquote file");
|
||||||
|
Nest nest;
|
||||||
|
using (var nestMs = new MemoryStream())
|
||||||
|
{
|
||||||
|
await using (var stream = nestEntry.Open())
|
||||||
|
{
|
||||||
|
await stream.CopyToAsync(nestMs);
|
||||||
|
}
|
||||||
|
nestMs.Position = 0;
|
||||||
|
var reader = new NestReader(nestMs);
|
||||||
|
nest = reader.Read();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new NestResponse
|
||||||
|
{
|
||||||
|
SheetCount = metricsJson.GetProperty("sheetCount").GetInt32(),
|
||||||
|
Utilization = metricsJson.GetProperty("utilization").GetDouble(),
|
||||||
|
CutTime = TimeSpan.FromTicks(metricsJson.GetProperty("cutTimeTicks").GetInt64()),
|
||||||
|
Elapsed = TimeSpan.FromTicks(metricsJson.GetProperty("elapsedTicks").GetInt64()),
|
||||||
|
Nest = nest,
|
||||||
|
Request = request
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using OpenNest.IO;
|
||||||
|
|
||||||
|
namespace OpenNest.Api;
|
||||||
|
|
||||||
|
public static class NestRunner
|
||||||
|
{
|
||||||
|
public static Task<NestResponse> RunAsync(
|
||||||
|
NestRequest request,
|
||||||
|
IProgress<NestProgress> progress = null,
|
||||||
|
CancellationToken token = default)
|
||||||
|
{
|
||||||
|
if (request.Parts.Count == 0)
|
||||||
|
throw new ArgumentException("Request must contain at least one part.", nameof(request));
|
||||||
|
|
||||||
|
var sw = Stopwatch.StartNew();
|
||||||
|
|
||||||
|
// 1. Import DXFs → Drawings
|
||||||
|
var drawings = new List<Drawing>();
|
||||||
|
foreach (var part in request.Parts)
|
||||||
|
{
|
||||||
|
if (!File.Exists(part.DxfPath))
|
||||||
|
throw new FileNotFoundException($"DXF file not found: {part.DxfPath}", part.DxfPath);
|
||||||
|
|
||||||
|
Drawing drawing;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
drawing = CadImporter.ImportDrawing(part.DxfPath,
|
||||||
|
new CadImportOptions { Quantity = part.Quantity });
|
||||||
|
}
|
||||||
|
catch (System.Exception ex)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"Failed to import DXF: {part.DxfPath}", ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (drawing.Program == null || drawing.Program.Codes.Count == 0)
|
||||||
|
throw new InvalidOperationException($"Failed to import DXF: {part.DxfPath}");
|
||||||
|
|
||||||
|
drawings.Add(drawing);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Build NestItems
|
||||||
|
var items = new List<NestItem>();
|
||||||
|
for (var i = 0; i < request.Parts.Count; i++)
|
||||||
|
{
|
||||||
|
var part = request.Parts[i];
|
||||||
|
items.Add(new NestItem
|
||||||
|
{
|
||||||
|
Drawing = drawings[i],
|
||||||
|
Quantity = part.Quantity,
|
||||||
|
Priority = part.Priority,
|
||||||
|
StepAngle = part.AllowRotation ? 0 : OpenNest.Math.Angle.TwoPI,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Multi-plate loop
|
||||||
|
var nest = new Nest();
|
||||||
|
nest.Thickness = request.Thickness;
|
||||||
|
nest.Material = new Material(request.Material);
|
||||||
|
var remaining = items.Select(item => item.Quantity).ToList();
|
||||||
|
|
||||||
|
while (remaining.Any(q => q > 0))
|
||||||
|
{
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
var plate = new Plate(request.SheetSize)
|
||||||
|
{
|
||||||
|
PartSpacing = request.Spacing,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Build items for this pass with remaining quantities
|
||||||
|
var passItems = new List<NestItem>();
|
||||||
|
for (var i = 0; i < items.Count; i++)
|
||||||
|
{
|
||||||
|
if (remaining[i] <= 0) continue;
|
||||||
|
passItems.Add(new NestItem
|
||||||
|
{
|
||||||
|
Drawing = items[i].Drawing,
|
||||||
|
Quantity = remaining[i],
|
||||||
|
Priority = items[i].Priority,
|
||||||
|
StepAngle = items[i].StepAngle,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run engine
|
||||||
|
var engine = NestEngineRegistry.Create(plate);
|
||||||
|
var parts = engine.Nest(passItems, progress, token);
|
||||||
|
|
||||||
|
if (parts.Count == 0)
|
||||||
|
break; // No progress — part doesn't fit on fresh sheet
|
||||||
|
|
||||||
|
// Add parts to plate and nest
|
||||||
|
foreach (var p in parts)
|
||||||
|
plate.Parts.Add(p);
|
||||||
|
|
||||||
|
nest.Plates.Add(plate);
|
||||||
|
|
||||||
|
// Deduct placed quantities
|
||||||
|
foreach (var p in parts)
|
||||||
|
{
|
||||||
|
var idx = drawings.IndexOf(p.BaseDrawing);
|
||||||
|
if (idx >= 0)
|
||||||
|
remaining[idx]--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Compute timing
|
||||||
|
var timingInfo = Timing.GetTimingInfo(nest);
|
||||||
|
var cutTime = Timing.CalculateTime(timingInfo, request.Cutting);
|
||||||
|
|
||||||
|
sw.Stop();
|
||||||
|
|
||||||
|
// 5. Build response
|
||||||
|
var response = new NestResponse
|
||||||
|
{
|
||||||
|
SheetCount = nest.Plates.Count,
|
||||||
|
Utilization = nest.Plates.Count > 0
|
||||||
|
? nest.Plates.Average(p => p.Utilization())
|
||||||
|
: 0,
|
||||||
|
CutTime = cutTime,
|
||||||
|
Elapsed = sw.Elapsed,
|
||||||
|
Nest = nest,
|
||||||
|
Request = request
|
||||||
|
};
|
||||||
|
|
||||||
|
return Task.FromResult(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
namespace OpenNest.Api;
|
||||||
|
|
||||||
|
public enum NestStrategy { Auto }
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0-windows</TargetFramework>
|
||||||
|
<RootNamespace>OpenNest.Api</RootNamespace>
|
||||||
|
<AssemblyName>OpenNest.Api</AssemblyName>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\OpenNest.Core\OpenNest.Core.csproj" />
|
||||||
|
<ProjectReference Include="..\OpenNest.Engine\OpenNest.Engine.csproj" />
|
||||||
|
<ProjectReference Include="..\OpenNest.IO\OpenNest.IO.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
+511
-225
@@ -1,248 +1,534 @@
|
|||||||
|
using OpenNest;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
using OpenNest.IO;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using OpenNest;
|
using System.Reflection;
|
||||||
using OpenNest.Geometry;
|
using System.Threading;
|
||||||
using OpenNest.IO;
|
|
||||||
|
|
||||||
// Parse arguments.
|
return NestConsole.Run(args);
|
||||||
var nestFile = (string)null;
|
|
||||||
var drawingName = (string)null;
|
|
||||||
var plateIndex = 0;
|
|
||||||
var outputFile = (string)null;
|
|
||||||
var quantity = 0;
|
|
||||||
var spacing = (double?)null;
|
|
||||||
var plateWidth = (double?)null;
|
|
||||||
var plateHeight = (double?)null;
|
|
||||||
var checkOverlaps = false;
|
|
||||||
var noSave = false;
|
|
||||||
var noLog = false;
|
|
||||||
var keepParts = false;
|
|
||||||
var autoNest = false;
|
|
||||||
|
|
||||||
for (var i = 0; i < args.Length; i++)
|
static class NestConsole
|
||||||
{
|
{
|
||||||
switch (args[i])
|
public static int Run(string[] args)
|
||||||
{
|
{
|
||||||
case "--drawing" when i + 1 < args.Length:
|
var options = ParseArgs(args);
|
||||||
drawingName = args[++i];
|
|
||||||
break;
|
if (options == null)
|
||||||
case "--plate" when i + 1 < args.Length:
|
return 0; // --help was requested
|
||||||
plateIndex = int.Parse(args[++i]);
|
|
||||||
break;
|
if (options.ListPosts)
|
||||||
case "--output" when i + 1 < args.Length:
|
{
|
||||||
outputFile = args[++i];
|
ListPostProcessors(options);
|
||||||
break;
|
|
||||||
case "--quantity" when i + 1 < args.Length:
|
|
||||||
quantity = int.Parse(args[++i]);
|
|
||||||
break;
|
|
||||||
case "--spacing" when i + 1 < args.Length:
|
|
||||||
spacing = double.Parse(args[++i]);
|
|
||||||
break;
|
|
||||||
case "--size" when i + 1 < args.Length:
|
|
||||||
var parts = args[++i].Split('x');
|
|
||||||
if (parts.Length == 2)
|
|
||||||
{
|
|
||||||
plateWidth = double.Parse(parts[0]);
|
|
||||||
plateHeight = double.Parse(parts[1]);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case "--check-overlaps":
|
|
||||||
checkOverlaps = true;
|
|
||||||
break;
|
|
||||||
case "--no-save":
|
|
||||||
noSave = true;
|
|
||||||
break;
|
|
||||||
case "--no-log":
|
|
||||||
noLog = true;
|
|
||||||
break;
|
|
||||||
case "--keep-parts":
|
|
||||||
keepParts = true;
|
|
||||||
break;
|
|
||||||
case "--autonest":
|
|
||||||
autoNest = true;
|
|
||||||
break;
|
|
||||||
case "--help":
|
|
||||||
case "-h":
|
|
||||||
PrintUsage();
|
|
||||||
return 0;
|
return 0;
|
||||||
default:
|
}
|
||||||
if (!args[i].StartsWith("--") && nestFile == null)
|
|
||||||
nestFile = args[i];
|
if (options.InputFiles.Count == 0)
|
||||||
break;
|
{
|
||||||
|
PrintUsage();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var f in options.InputFiles)
|
||||||
|
{
|
||||||
|
if (!File.Exists(f))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"Error: file not found: {f}");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
using var log = SetUpLog(options);
|
||||||
|
var nest = LoadOrCreateNest(options);
|
||||||
|
|
||||||
|
if (nest == null)
|
||||||
|
return 1;
|
||||||
|
|
||||||
|
var plate = nest.Plates[options.PlateIndex];
|
||||||
|
|
||||||
|
ApplyTemplate(plate, options);
|
||||||
|
ApplyOverrides(plate, options);
|
||||||
|
|
||||||
|
var drawing = ResolveDrawing(nest, options);
|
||||||
|
|
||||||
|
if (drawing == null)
|
||||||
|
return 1;
|
||||||
|
|
||||||
|
var existingCount = plate.Parts.Count;
|
||||||
|
|
||||||
|
if (!options.KeepParts)
|
||||||
|
plate.Parts.Clear();
|
||||||
|
|
||||||
|
PrintHeader(nest, plate, drawing, existingCount, options);
|
||||||
|
|
||||||
|
var (success, elapsed) = Fill(nest, plate, drawing, options);
|
||||||
|
|
||||||
|
var overlapCount = CheckOverlaps(plate, options);
|
||||||
|
|
||||||
|
// Flush and close the log before printing results.
|
||||||
|
Trace.Flush();
|
||||||
|
log?.Dispose();
|
||||||
|
|
||||||
|
PrintResults(success, plate, elapsed);
|
||||||
|
Save(nest, options);
|
||||||
|
PostProcess(nest, options);
|
||||||
|
|
||||||
|
return options.CheckOverlaps && overlapCount > 0 ? 1 : 0;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(nestFile) || !File.Exists(nestFile))
|
static Options ParseArgs(string[] args)
|
||||||
{
|
|
||||||
PrintUsage();
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set up debug log file.
|
|
||||||
StreamWriter logWriter = null;
|
|
||||||
|
|
||||||
if (!noLog)
|
|
||||||
{
|
|
||||||
var logDir = Path.Combine(Path.GetDirectoryName(nestFile), "test-harness-logs");
|
|
||||||
Directory.CreateDirectory(logDir);
|
|
||||||
var logFile = Path.Combine(logDir, $"debug-{DateTime.Now:yyyyMMdd-HHmmss}.log");
|
|
||||||
logWriter = new StreamWriter(logFile) { AutoFlush = true };
|
|
||||||
Trace.Listeners.Add(new TextWriterTraceListener(logWriter));
|
|
||||||
Console.WriteLine($"Debug log: {logFile}");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load nest.
|
|
||||||
var reader = new NestReader(nestFile);
|
|
||||||
var nest = reader.Read();
|
|
||||||
|
|
||||||
if (nest.Plates.Count == 0)
|
|
||||||
{
|
|
||||||
Console.Error.WriteLine("Error: nest file contains no plates");
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (plateIndex >= nest.Plates.Count)
|
|
||||||
{
|
|
||||||
Console.Error.WriteLine($"Error: plate index {plateIndex} out of range (0-{nest.Plates.Count - 1})");
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
var plate = nest.Plates[plateIndex];
|
|
||||||
|
|
||||||
// Apply overrides.
|
|
||||||
if (spacing.HasValue)
|
|
||||||
plate.PartSpacing = spacing.Value;
|
|
||||||
|
|
||||||
if (plateWidth.HasValue && plateHeight.HasValue)
|
|
||||||
plate.Size = new Size(plateWidth.Value, plateHeight.Value);
|
|
||||||
|
|
||||||
// Find drawing.
|
|
||||||
var drawing = drawingName != null
|
|
||||||
? nest.Drawings.FirstOrDefault(d => d.Name == drawingName)
|
|
||||||
: nest.Drawings.FirstOrDefault();
|
|
||||||
|
|
||||||
if (drawing == null)
|
|
||||||
{
|
|
||||||
Console.Error.WriteLine(drawingName != null
|
|
||||||
? $"Error: drawing '{drawingName}' not found. Available: {string.Join(", ", nest.Drawings.Select(d => d.Name))}"
|
|
||||||
: "Error: nest file contains no drawings");
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear existing parts.
|
|
||||||
var existingCount = plate.Parts.Count;
|
|
||||||
|
|
||||||
if (!keepParts)
|
|
||||||
plate.Parts.Clear();
|
|
||||||
|
|
||||||
Console.WriteLine($"Nest: {nest.Name}");
|
|
||||||
Console.WriteLine($"Plate: {plateIndex} ({plate.Size.Width:F1} x {plate.Size.Length:F1}), spacing={plate.PartSpacing:F2}");
|
|
||||||
Console.WriteLine($"Drawing: {drawing.Name}");
|
|
||||||
|
|
||||||
if (!keepParts)
|
|
||||||
Console.WriteLine($"Cleared {existingCount} existing parts");
|
|
||||||
else
|
|
||||||
Console.WriteLine($"Keeping {existingCount} existing parts");
|
|
||||||
|
|
||||||
Console.WriteLine("---");
|
|
||||||
|
|
||||||
// Run fill or autonest.
|
|
||||||
var sw = Stopwatch.StartNew();
|
|
||||||
bool success;
|
|
||||||
|
|
||||||
if (autoNest)
|
|
||||||
{
|
|
||||||
// AutoNest: use all drawings (or specific drawing if --drawing given).
|
|
||||||
var nestItems = new List<NestItem>();
|
|
||||||
|
|
||||||
if (drawingName != null)
|
|
||||||
{
|
{
|
||||||
nestItems.Add(new NestItem { Drawing = drawing, Quantity = quantity > 0 ? quantity : 1 });
|
var o = new Options();
|
||||||
|
|
||||||
|
for (var i = 0; i < args.Length; i++)
|
||||||
|
{
|
||||||
|
switch (args[i])
|
||||||
|
{
|
||||||
|
case "--drawing" when i + 1 < args.Length:
|
||||||
|
o.DrawingName = args[++i];
|
||||||
|
break;
|
||||||
|
case "--plate" when i + 1 < args.Length:
|
||||||
|
o.PlateIndex = int.Parse(args[++i]);
|
||||||
|
break;
|
||||||
|
case "--output" when i + 1 < args.Length:
|
||||||
|
o.OutputFile = args[++i];
|
||||||
|
break;
|
||||||
|
case "--quantity" when i + 1 < args.Length:
|
||||||
|
o.Quantity = int.Parse(args[++i]);
|
||||||
|
break;
|
||||||
|
case "--spacing" when i + 1 < args.Length:
|
||||||
|
o.Spacing = double.Parse(args[++i]);
|
||||||
|
break;
|
||||||
|
case "--size" when i + 1 < args.Length:
|
||||||
|
if (Size.TryParse(args[++i], out var sz))
|
||||||
|
o.PlateSize = sz;
|
||||||
|
break;
|
||||||
|
case "--check-overlaps":
|
||||||
|
o.CheckOverlaps = true;
|
||||||
|
break;
|
||||||
|
case "--no-save":
|
||||||
|
o.NoSave = true;
|
||||||
|
break;
|
||||||
|
case "--no-log":
|
||||||
|
o.NoLog = true;
|
||||||
|
break;
|
||||||
|
case "--keep-parts":
|
||||||
|
o.KeepParts = true;
|
||||||
|
break;
|
||||||
|
case "--template" when i + 1 < args.Length:
|
||||||
|
o.TemplateFile = args[++i];
|
||||||
|
break;
|
||||||
|
case "--autonest":
|
||||||
|
o.AutoNest = true;
|
||||||
|
break;
|
||||||
|
case "--engine" when i + 1 < args.Length:
|
||||||
|
NestEngineRegistry.ActiveEngineName = args[++i];
|
||||||
|
break;
|
||||||
|
case "--post" when i + 1 < args.Length:
|
||||||
|
o.PostName = args[++i];
|
||||||
|
break;
|
||||||
|
case "--post-output" when i + 1 < args.Length:
|
||||||
|
o.PostOutput = args[++i];
|
||||||
|
break;
|
||||||
|
case "--posts-dir" when i + 1 < args.Length:
|
||||||
|
o.PostsDir = args[++i];
|
||||||
|
break;
|
||||||
|
case "--list-posts":
|
||||||
|
o.ListPosts = true;
|
||||||
|
break;
|
||||||
|
case "--help":
|
||||||
|
case "-h":
|
||||||
|
PrintUsage();
|
||||||
|
return null;
|
||||||
|
default:
|
||||||
|
if (!args[i].StartsWith("--"))
|
||||||
|
o.InputFiles.Add(args[i]);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return o;
|
||||||
}
|
}
|
||||||
else
|
|
||||||
|
static StreamWriter SetUpLog(Options options)
|
||||||
{
|
{
|
||||||
foreach (var d in nest.Drawings)
|
if (options.NoLog)
|
||||||
nestItems.Add(new NestItem { Drawing = d, Quantity = quantity > 0 ? quantity : 1 });
|
return null;
|
||||||
|
|
||||||
|
var baseDir = Path.GetDirectoryName(options.InputFiles[0]);
|
||||||
|
var logDir = Path.Combine(baseDir, "test-harness-logs");
|
||||||
|
Directory.CreateDirectory(logDir);
|
||||||
|
var logFile = Path.Combine(logDir, $"debug-{DateTime.Now:yyyyMMdd-HHmmss}.log");
|
||||||
|
var writer = new StreamWriter(logFile) { AutoFlush = true };
|
||||||
|
Trace.Listeners.Add(new TextWriterTraceListener(writer));
|
||||||
|
Console.WriteLine($"Debug log: {logFile}");
|
||||||
|
return writer;
|
||||||
}
|
}
|
||||||
|
|
||||||
Console.WriteLine($"AutoNest: {nestItems.Count} drawing(s), {nestItems.Sum(i => i.Quantity)} total parts");
|
static Nest LoadOrCreateNest(Options options)
|
||||||
|
|
||||||
var parts = NestEngine.AutoNest(nestItems, plate);
|
|
||||||
plate.Parts.AddRange(parts);
|
|
||||||
success = parts.Count > 0;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var engine = new NestEngine(plate);
|
|
||||||
var item = new NestItem { Drawing = drawing, Quantity = quantity };
|
|
||||||
success = engine.Fill(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
sw.Stop();
|
|
||||||
|
|
||||||
// Check overlaps.
|
|
||||||
var overlapCount = 0;
|
|
||||||
|
|
||||||
if (checkOverlaps && plate.Parts.Count > 0)
|
|
||||||
{
|
|
||||||
List<Vector> overlapPts;
|
|
||||||
var hasOverlaps = plate.HasOverlappingParts(out overlapPts);
|
|
||||||
overlapCount = overlapPts.Count;
|
|
||||||
|
|
||||||
if (hasOverlaps)
|
|
||||||
Console.WriteLine($"OVERLAPS DETECTED: {overlapCount} intersection points");
|
|
||||||
else
|
|
||||||
Console.WriteLine("Overlap check: PASS");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Flush and close the log.
|
|
||||||
Trace.Flush();
|
|
||||||
logWriter?.Dispose();
|
|
||||||
|
|
||||||
// Print results.
|
|
||||||
Console.WriteLine($"Result: {(success ? "success" : "failed")}");
|
|
||||||
Console.WriteLine($"Parts placed: {plate.Parts.Count}");
|
|
||||||
Console.WriteLine($"Utilization: {plate.Utilization():P1}");
|
|
||||||
Console.WriteLine($"Time: {sw.ElapsedMilliseconds}ms");
|
|
||||||
|
|
||||||
// Save output.
|
|
||||||
if (!noSave)
|
|
||||||
{
|
|
||||||
if (outputFile == null)
|
|
||||||
{
|
{
|
||||||
var dir = Path.GetDirectoryName(nestFile);
|
var nestFile = options.InputFiles.FirstOrDefault(f =>
|
||||||
var name = Path.GetFileNameWithoutExtension(nestFile);
|
f.EndsWith(NestFormat.FileExtension, StringComparison.OrdinalIgnoreCase)
|
||||||
outputFile = Path.Combine(dir, $"{name}-result.zip");
|
|| f.EndsWith(".zip", StringComparison.OrdinalIgnoreCase));
|
||||||
|
var dxfFiles = options.InputFiles.Where(f =>
|
||||||
|
f.EndsWith(".dxf", StringComparison.OrdinalIgnoreCase)).ToList();
|
||||||
|
|
||||||
|
// If we have a nest file, load it and optionally add DXFs.
|
||||||
|
if (nestFile != null)
|
||||||
|
{
|
||||||
|
var nest = new NestReader(nestFile).Read();
|
||||||
|
|
||||||
|
if (nest.Plates.Count == 0)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine("Error: nest file contains no plates");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.PlateIndex >= nest.Plates.Count)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"Error: plate index {options.PlateIndex} out of range (0-{nest.Plates.Count - 1})");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var dxf in dxfFiles)
|
||||||
|
{
|
||||||
|
var drawing = ImportDxf(dxf);
|
||||||
|
|
||||||
|
if (drawing == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
nest.Drawings.Add(drawing);
|
||||||
|
Console.WriteLine($"Imported: {drawing.Name}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return nest;
|
||||||
|
}
|
||||||
|
|
||||||
|
// DXF-only mode: create a fresh nest.
|
||||||
|
if (dxfFiles.Count == 0)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine("Error: no nest (.nest) or DXF (.dxf) files specified");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!options.PlateSize.HasValue)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine("Error: --size WxL is required when importing DXF files without a nest");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var newNest = new Nest { Name = "DXF Import" };
|
||||||
|
var plate = new Plate { Size = options.PlateSize.Value };
|
||||||
|
newNest.Plates.Add(plate);
|
||||||
|
|
||||||
|
foreach (var dxf in dxfFiles)
|
||||||
|
{
|
||||||
|
var drawing = ImportDxf(dxf);
|
||||||
|
|
||||||
|
if (drawing == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
newNest.Drawings.Add(drawing);
|
||||||
|
Console.WriteLine($"Imported: {drawing.Name}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return newNest;
|
||||||
}
|
}
|
||||||
|
|
||||||
var writer = new NestWriter(nest);
|
static Drawing ImportDxf(string path)
|
||||||
writer.Write(outputFile);
|
{
|
||||||
Console.WriteLine($"Saved: {outputFile}");
|
try
|
||||||
}
|
{
|
||||||
|
return CadImporter.ImportDrawing(path);
|
||||||
|
}
|
||||||
|
catch (System.Exception ex)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"Error: failed to import DXF '{path}': {ex.Message}");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return checkOverlaps && overlapCount > 0 ? 1 : 0;
|
static void ApplyTemplate(Plate plate, Options options)
|
||||||
|
{
|
||||||
|
if (options.TemplateFile == null)
|
||||||
|
return;
|
||||||
|
|
||||||
void PrintUsage()
|
if (!File.Exists(options.TemplateFile))
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine("Usage: OpenNest.Console <nest-file> [options]");
|
Console.Error.WriteLine($"Error: Template not found: {options.TemplateFile}");
|
||||||
Console.Error.WriteLine();
|
return;
|
||||||
Console.Error.WriteLine("Arguments:");
|
}
|
||||||
Console.Error.WriteLine(" nest-file Path to a .zip nest file");
|
|
||||||
Console.Error.WriteLine();
|
var templateNest = new NestReader(options.TemplateFile).Read();
|
||||||
Console.Error.WriteLine("Options:");
|
var templatePlate = templateNest.PlateDefaults.CreateNew();
|
||||||
Console.Error.WriteLine(" --drawing <name> Drawing name to fill with (default: first drawing)");
|
plate.Quadrant = templatePlate.Quadrant;
|
||||||
Console.Error.WriteLine(" --plate <index> Plate index to fill (default: 0)");
|
plate.EdgeSpacing = templatePlate.EdgeSpacing;
|
||||||
Console.Error.WriteLine(" --quantity <n> Max parts to place (default: 0 = unlimited)");
|
plate.PartSpacing = templatePlate.PartSpacing;
|
||||||
Console.Error.WriteLine(" --spacing <value> Override part spacing");
|
Console.WriteLine($"Template: {options.TemplateFile}");
|
||||||
Console.Error.WriteLine(" --size <WxH> Override plate size (e.g. 120x60)");
|
}
|
||||||
Console.Error.WriteLine(" --output <path> Output nest file path (default: <input>-result.zip)");
|
|
||||||
Console.Error.WriteLine(" --autonest Use NFP-based mixed-part autonesting instead of linear fill");
|
static void ApplyOverrides(Plate plate, Options options)
|
||||||
Console.Error.WriteLine(" --keep-parts Don't clear existing parts before filling");
|
{
|
||||||
Console.Error.WriteLine(" --check-overlaps Run overlap detection after fill (exit code 1 if found)");
|
if (options.Spacing.HasValue)
|
||||||
Console.Error.WriteLine(" --no-save Skip saving output file");
|
plate.PartSpacing = options.Spacing.Value;
|
||||||
Console.Error.WriteLine(" --no-log Skip writing debug log file");
|
|
||||||
Console.Error.WriteLine(" -h, --help Show this help");
|
// Only apply size override when it wasn't already used to create the plate.
|
||||||
|
var hasDxfOnly = !options.InputFiles.Any(f =>
|
||||||
|
f.EndsWith(NestFormat.FileExtension, StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| f.EndsWith(".zip", StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
|
if (options.PlateSize.HasValue && !hasDxfOnly)
|
||||||
|
plate.Size = options.PlateSize.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Drawing ResolveDrawing(Nest nest, Options options)
|
||||||
|
{
|
||||||
|
var drawing = options.DrawingName != null
|
||||||
|
? nest.Drawings.FirstOrDefault(d => d.Name == options.DrawingName)
|
||||||
|
: nest.Drawings.FirstOrDefault();
|
||||||
|
|
||||||
|
if (drawing != null)
|
||||||
|
return drawing;
|
||||||
|
|
||||||
|
Console.Error.WriteLine(options.DrawingName != null
|
||||||
|
? $"Error: drawing '{options.DrawingName}' not found. Available: {string.Join(", ", nest.Drawings.Select(d => d.Name))}"
|
||||||
|
: "Error: nest file contains no drawings");
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void PrintHeader(Nest nest, Plate plate, Drawing drawing, int existingCount, Options options)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Nest: {nest.Name}");
|
||||||
|
var wa = plate.WorkArea();
|
||||||
|
Console.WriteLine($"Plate: {options.PlateIndex} ({plate.Size.Width:F1} x {plate.Size.Length:F1}), spacing={plate.PartSpacing:F2}, edge=({plate.EdgeSpacing.Left},{plate.EdgeSpacing.Bottom},{plate.EdgeSpacing.Right},{plate.EdgeSpacing.Top}), workArea={wa.Width:F1}x{wa.Length:F1}");
|
||||||
|
Console.WriteLine($"Drawing: {drawing.Name}");
|
||||||
|
Console.WriteLine(options.KeepParts
|
||||||
|
? $"Keeping {existingCount} existing parts"
|
||||||
|
: $"Cleared {existingCount} existing parts");
|
||||||
|
Console.WriteLine("---");
|
||||||
|
}
|
||||||
|
|
||||||
|
static (bool success, long elapsedMs) Fill(Nest nest, Plate plate, Drawing drawing, Options options)
|
||||||
|
{
|
||||||
|
var sw = Stopwatch.StartNew();
|
||||||
|
bool success;
|
||||||
|
|
||||||
|
if (options.AutoNest)
|
||||||
|
{
|
||||||
|
var nestItems = new List<NestItem>();
|
||||||
|
var qty = options.Quantity > 0 ? options.Quantity : 1;
|
||||||
|
|
||||||
|
if (options.DrawingName != null)
|
||||||
|
{
|
||||||
|
nestItems.Add(new NestItem { Drawing = drawing, Quantity = qty });
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
foreach (var d in nest.Drawings)
|
||||||
|
nestItems.Add(new NestItem { Drawing = d, Quantity = qty });
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine($"AutoNest: {nestItems.Count} drawing(s), {nestItems.Sum(i => i.Quantity)} total parts");
|
||||||
|
|
||||||
|
var engine = NestEngineRegistry.Create(plate);
|
||||||
|
var nestParts = engine.Nest(nestItems, null, CancellationToken.None);
|
||||||
|
plate.Parts.AddRange(nestParts);
|
||||||
|
success = nestParts.Count > 0;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var engine = NestEngineRegistry.Create(plate);
|
||||||
|
var item = new NestItem { Drawing = drawing, Quantity = options.Quantity };
|
||||||
|
success = engine.Fill(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
sw.Stop();
|
||||||
|
return (success, sw.ElapsedMilliseconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int CheckOverlaps(Plate plate, Options options)
|
||||||
|
{
|
||||||
|
if (!options.CheckOverlaps || plate.Parts.Count == 0)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
var hasOverlaps = plate.HasOverlappingParts(out var overlapPts);
|
||||||
|
Console.WriteLine(hasOverlaps
|
||||||
|
? $"OVERLAPS DETECTED: {overlapPts.Count} intersection points"
|
||||||
|
: "Overlap check: PASS");
|
||||||
|
|
||||||
|
return overlapPts.Count;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void PrintResults(bool success, Plate plate, long elapsedMs)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Result: {(success ? "success" : "failed")}");
|
||||||
|
Console.WriteLine($"Parts placed: {plate.Parts.Count}");
|
||||||
|
Console.WriteLine($"Utilization: {plate.Utilization():P1}");
|
||||||
|
Console.WriteLine($"Time: {elapsedMs}ms");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void Save(Nest nest, Options options)
|
||||||
|
{
|
||||||
|
if (options.NoSave)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var firstInput = options.InputFiles[0];
|
||||||
|
var outputFile = options.OutputFile ?? Path.Combine(
|
||||||
|
Path.GetDirectoryName(firstInput),
|
||||||
|
$"{Path.GetFileNameWithoutExtension(firstInput)}-result{NestFormat.FileExtension}");
|
||||||
|
|
||||||
|
new NestWriter(nest).Write(outputFile);
|
||||||
|
Console.WriteLine($"Saved: {outputFile}");
|
||||||
|
}
|
||||||
|
|
||||||
|
static string ResolvePostsDir(Options options)
|
||||||
|
{
|
||||||
|
if (options.PostsDir != null)
|
||||||
|
return options.PostsDir;
|
||||||
|
|
||||||
|
var exePath = Assembly.GetEntryAssembly()?.Location
|
||||||
|
?? typeof(NestConsole).Assembly.Location;
|
||||||
|
return Path.Combine(Path.GetDirectoryName(exePath), "Posts");
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<IPostProcessor> LoadPostProcessors(string postsDir)
|
||||||
|
{
|
||||||
|
var processors = new List<IPostProcessor>();
|
||||||
|
|
||||||
|
if (!Directory.Exists(postsDir))
|
||||||
|
return processors;
|
||||||
|
|
||||||
|
foreach (var file in Directory.GetFiles(postsDir, "*.dll"))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var assembly = Assembly.LoadFrom(file);
|
||||||
|
|
||||||
|
foreach (var type in assembly.GetTypes())
|
||||||
|
{
|
||||||
|
if (!typeof(IPostProcessor).IsAssignableFrom(type) || type.IsInterface || type.IsAbstract)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (Activator.CreateInstance(type) is IPostProcessor processor)
|
||||||
|
processors.Add(processor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"Warning: failed to load post processor from {Path.GetFileName(file)}: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return processors;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ListPostProcessors(Options options)
|
||||||
|
{
|
||||||
|
var postsDir = ResolvePostsDir(options);
|
||||||
|
var processors = LoadPostProcessors(postsDir);
|
||||||
|
|
||||||
|
if (processors.Count == 0)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"No post processors found in: {postsDir}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine($"Post processors ({postsDir}):");
|
||||||
|
|
||||||
|
foreach (var p in processors)
|
||||||
|
Console.WriteLine($" {p.Name,-30} {p.Description}");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void PostProcess(Nest nest, Options options)
|
||||||
|
{
|
||||||
|
if (options.PostName == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var postsDir = ResolvePostsDir(options);
|
||||||
|
var processors = LoadPostProcessors(postsDir);
|
||||||
|
var post = processors.FirstOrDefault(p =>
|
||||||
|
p.Name.Equals(options.PostName, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
|
if (post == null)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"Error: post processor '{options.PostName}' not found");
|
||||||
|
|
||||||
|
if (processors.Count > 0)
|
||||||
|
Console.Error.WriteLine($"Available: {string.Join(", ", processors.Select(p => p.Name))}");
|
||||||
|
else
|
||||||
|
Console.Error.WriteLine($"No post processors found in: {postsDir}");
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var outputFile = options.PostOutput;
|
||||||
|
|
||||||
|
if (outputFile == null)
|
||||||
|
{
|
||||||
|
var firstInput = options.InputFiles[0];
|
||||||
|
outputFile = Path.Combine(
|
||||||
|
Path.GetDirectoryName(firstInput),
|
||||||
|
$"{Path.GetFileNameWithoutExtension(firstInput)}.cnc");
|
||||||
|
}
|
||||||
|
|
||||||
|
post.Post(nest, outputFile);
|
||||||
|
Console.WriteLine($"Post: {post.Name} -> {outputFile}");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void PrintUsage()
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine("Usage: OpenNest.Console <input-files...> [options]");
|
||||||
|
Console.Error.WriteLine();
|
||||||
|
Console.Error.WriteLine("Arguments:");
|
||||||
|
Console.Error.WriteLine(" input-files One or more .nest nest files or .dxf drawing files");
|
||||||
|
Console.Error.WriteLine();
|
||||||
|
Console.Error.WriteLine("Modes:");
|
||||||
|
Console.Error.WriteLine(" <nest.nest> Load nest and fill (existing behavior)");
|
||||||
|
Console.Error.WriteLine(" <part.dxf> --size WxL Import DXF, create plate, and fill");
|
||||||
|
Console.Error.WriteLine(" <nest.nest> <part.dxf> Load nest and add imported DXF drawings");
|
||||||
|
Console.Error.WriteLine();
|
||||||
|
Console.Error.WriteLine("Options:");
|
||||||
|
Console.Error.WriteLine(" --drawing <name> Drawing name to fill with (default: first drawing)");
|
||||||
|
Console.Error.WriteLine(" --plate <index> Plate index to fill (default: 0)");
|
||||||
|
Console.Error.WriteLine(" --quantity <n> Max parts to place (default: 0 = unlimited)");
|
||||||
|
Console.Error.WriteLine(" --spacing <value> Override part spacing");
|
||||||
|
Console.Error.WriteLine(" --size <WxL> Override plate size (e.g. 60x120); required for DXF-only mode");
|
||||||
|
Console.Error.WriteLine(" --output <path> Output nest file path (default: <input>-result.nest)");
|
||||||
|
Console.Error.WriteLine(" --template <path> Nest template for plate defaults (thickness, quadrant, material, spacing)");
|
||||||
|
Console.Error.WriteLine(" --autonest Use NFP-based mixed-part autonesting instead of linear fill");
|
||||||
|
Console.Error.WriteLine(" --keep-parts Don't clear existing parts before filling");
|
||||||
|
Console.Error.WriteLine(" --check-overlaps Run overlap detection after fill (exit code 1 if found)");
|
||||||
|
Console.Error.WriteLine(" --no-save Skip saving output file");
|
||||||
|
Console.Error.WriteLine(" --no-log Skip writing debug log file");
|
||||||
|
Console.Error.WriteLine(" --post <name> Run a post processor after nesting");
|
||||||
|
Console.Error.WriteLine(" --post-output <path> Output file for post processor (default: <input>.cnc)");
|
||||||
|
Console.Error.WriteLine(" --posts-dir <path> Directory containing post processor DLLs (default: Posts/)");
|
||||||
|
Console.Error.WriteLine(" --list-posts List available post processors and exit");
|
||||||
|
Console.Error.WriteLine(" -h, --help Show this help");
|
||||||
|
}
|
||||||
|
|
||||||
|
class Options
|
||||||
|
{
|
||||||
|
public List<string> InputFiles = new();
|
||||||
|
public string DrawingName;
|
||||||
|
public int PlateIndex;
|
||||||
|
public string OutputFile;
|
||||||
|
public int Quantity;
|
||||||
|
public double? Spacing;
|
||||||
|
public Size? PlateSize;
|
||||||
|
public bool CheckOverlaps;
|
||||||
|
public bool NoSave;
|
||||||
|
public bool NoLog;
|
||||||
|
public bool KeepParts;
|
||||||
|
public bool AutoNest;
|
||||||
|
public string TemplateFile;
|
||||||
|
public string PostName;
|
||||||
|
public string PostOutput;
|
||||||
|
public string PostsDir;
|
||||||
|
public bool ListPosts;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+19
-44
@@ -1,5 +1,5 @@
|
|||||||
using System.Collections.Generic;
|
using OpenNest.Geometry;
|
||||||
using OpenNest.Geometry;
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace OpenNest
|
namespace OpenNest
|
||||||
{
|
{
|
||||||
@@ -125,61 +125,36 @@ namespace OpenNest
|
|||||||
parts.ForEach(part => Bottom(fixedPart, part));
|
parts.ForEach(part => Bottom(fixedPart, part));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void EvenlyDistributeHorizontally(List<Part> parts)
|
public static void EvenlyDistributeHorizontally(List<Part> parts) =>
|
||||||
|
EvenlyDistribute(parts, horizontal: true);
|
||||||
|
|
||||||
|
public static void EvenlyDistributeVertically(List<Part> parts) =>
|
||||||
|
EvenlyDistribute(parts, horizontal: false);
|
||||||
|
|
||||||
|
private static void EvenlyDistribute(List<Part> parts, bool horizontal)
|
||||||
{
|
{
|
||||||
if (parts.Count < 3)
|
if (parts.Count < 3)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var list = new List<Part>(parts);
|
var list = new List<Part>(parts);
|
||||||
list.Sort((p1, p2) => p1.BoundingBox.Center.X.CompareTo(p2.BoundingBox.Center.X));
|
list.Sort((p1, p2) => horizontal
|
||||||
|
? p1.BoundingBox.Center.X.CompareTo(p2.BoundingBox.Center.X)
|
||||||
|
: p1.BoundingBox.Center.Y.CompareTo(p2.BoundingBox.Center.Y));
|
||||||
|
|
||||||
var lastIndex = list.Count - 1;
|
var lastIndex = list.Count - 1;
|
||||||
|
|
||||||
var first = list[0];
|
var start = horizontal ? list[0].BoundingBox.Center.X : list[0].BoundingBox.Center.Y;
|
||||||
var last = list[lastIndex];
|
var end = horizontal ? list[lastIndex].BoundingBox.Center.X : list[lastIndex].BoundingBox.Center.Y;
|
||||||
|
|
||||||
var start = first.BoundingBox.Center.X;
|
var spacing = (end - start) / lastIndex;
|
||||||
var end = last.BoundingBox.Center.X;
|
|
||||||
var diff = end - start;
|
|
||||||
|
|
||||||
var spacing = diff / lastIndex;
|
for (var i = 1; i < lastIndex; ++i)
|
||||||
|
|
||||||
for (int i = 1; i < lastIndex; ++i)
|
|
||||||
{
|
{
|
||||||
var part = list[i];
|
var part = list[i];
|
||||||
var newX = start + i * spacing;
|
var cur = horizontal ? part.BoundingBox.Center.X : part.BoundingBox.Center.Y;
|
||||||
var curX = part.BoundingBox.Center.X;
|
var delta = start + i * spacing - cur;
|
||||||
|
|
||||||
part.Offset(newX - curX, 0);
|
part.Offset(horizontal ? delta : 0, horizontal ? 0 : delta);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void EvenlyDistributeVertically(List<Part> parts)
|
|
||||||
{
|
|
||||||
if (parts.Count < 3)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var list = new List<Part>(parts);
|
|
||||||
list.Sort((p1, p2) => p1.BoundingBox.Center.Y.CompareTo(p2.BoundingBox.Center.Y));
|
|
||||||
|
|
||||||
var lastIndex = list.Count - 1;
|
|
||||||
|
|
||||||
var first = list[0];
|
|
||||||
var last = list[lastIndex];
|
|
||||||
|
|
||||||
var start = first.BoundingBox.Center.Y;
|
|
||||||
var end = last.BoundingBox.Center.Y;
|
|
||||||
var diff = end - start;
|
|
||||||
|
|
||||||
var spacing = diff / lastIndex;
|
|
||||||
|
|
||||||
for (int i = 1; i < lastIndex; ++i)
|
|
||||||
{
|
|
||||||
var part = list[i];
|
|
||||||
var newX = start + i * spacing;
|
|
||||||
var curX = part.BoundingBox.Center.Y;
|
|
||||||
|
|
||||||
part.Offset(0, newX - curX);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
using OpenNest.Geometry;
|
||||||
|
using OpenNest.Math;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Drawing;
|
||||||
|
|
||||||
|
namespace OpenNest.Bending
|
||||||
|
{
|
||||||
|
public class Bend
|
||||||
|
{
|
||||||
|
public static readonly Layer EtchLayer = new Layer("ETCH")
|
||||||
|
{
|
||||||
|
Color = Color.Green,
|
||||||
|
IsVisible = true
|
||||||
|
};
|
||||||
|
|
||||||
|
private const double DefaultEtchLength = 1.0;
|
||||||
|
private const string BendEtchTag = "BendEtch";
|
||||||
|
|
||||||
|
public Vector StartPoint { get; set; }
|
||||||
|
public Vector EndPoint { get; set; }
|
||||||
|
public BendDirection Direction { get; set; }
|
||||||
|
public double? Angle { get; set; }
|
||||||
|
public double? Radius { get; set; }
|
||||||
|
public string NoteText { get; set; }
|
||||||
|
|
||||||
|
[System.Text.Json.Serialization.JsonIgnore]
|
||||||
|
public Entity SourceEntity { get; set; }
|
||||||
|
|
||||||
|
public double Length => StartPoint.DistanceTo(EndPoint);
|
||||||
|
|
||||||
|
public double AngleRadians => Angle.HasValue
|
||||||
|
? OpenNest.Math.Angle.ToRadians(Angle.Value)
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
public Line ToLine() => new Line(StartPoint, EndPoint);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the angle of the bend line itself (not the bend angle).
|
||||||
|
/// Used for grain direction comparison.
|
||||||
|
/// </summary>
|
||||||
|
public double LineAngle => StartPoint.AngleTo(EndPoint);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Generates etch mark entities for this bend (up bends only).
|
||||||
|
/// Returns 1" dashes at each end of the bend line, or the full line if shorter than 3".
|
||||||
|
/// </summary>
|
||||||
|
public List<Line> GetEtchEntities(double etchLength = DefaultEtchLength)
|
||||||
|
{
|
||||||
|
var result = new List<Line>();
|
||||||
|
if (Direction != BendDirection.Up)
|
||||||
|
return result;
|
||||||
|
|
||||||
|
var length = Length;
|
||||||
|
|
||||||
|
if (length < etchLength * 3.0)
|
||||||
|
{
|
||||||
|
result.Add(CreateEtchLine(StartPoint, EndPoint));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var angle = StartPoint.AngleTo(EndPoint);
|
||||||
|
var dx = System.Math.Cos(angle) * etchLength;
|
||||||
|
var dy = System.Math.Sin(angle) * etchLength;
|
||||||
|
|
||||||
|
result.Add(CreateEtchLine(StartPoint, new Vector(StartPoint.X + dx, StartPoint.Y + dy)));
|
||||||
|
result.Add(CreateEtchLine(new Vector(EndPoint.X - dx, EndPoint.Y - dy), EndPoint));
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes existing etch entities from the list and regenerates from the given bends.
|
||||||
|
/// </summary>
|
||||||
|
public static void UpdateEtchEntities(List<Entity> entities, List<Bend> bends)
|
||||||
|
{
|
||||||
|
entities.RemoveAll(e => e.Tag == BendEtchTag);
|
||||||
|
if (bends == null) return;
|
||||||
|
|
||||||
|
foreach (var bend in bends)
|
||||||
|
entities.AddRange(bend.GetEtchEntities());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Line CreateEtchLine(Vector start, Vector end)
|
||||||
|
{
|
||||||
|
return new Line(start, end) { Layer = EtchLayer, Color = Color.Green, Tag = BendEtchTag };
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
var dir = Direction.ToString();
|
||||||
|
var angle = Angle?.ToString("0.##") ?? "?";
|
||||||
|
var radius = Radius?.ToString("0.###") ?? "?";
|
||||||
|
return $"{dir} {angle}° R{radius}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
namespace OpenNest.Bending
|
||||||
|
{
|
||||||
|
public enum BendDirection
|
||||||
|
{
|
||||||
|
Unknown,
|
||||||
|
Up,
|
||||||
|
Down
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using OpenNest.Geometry;
|
using System.Collections.Generic;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
|
||||||
namespace OpenNest.CNC
|
namespace OpenNest.CNC
|
||||||
{
|
{
|
||||||
@@ -65,7 +66,9 @@ namespace OpenNest.CNC
|
|||||||
{
|
{
|
||||||
return new ArcMove(EndPoint, CenterPoint, Rotation)
|
return new ArcMove(EndPoint, CenterPoint, Rotation)
|
||||||
{
|
{
|
||||||
Layer = Layer
|
Layer = Layer,
|
||||||
|
Suppressed = Suppressed,
|
||||||
|
VariableRefs = VariableRefs != null ? new Dictionary<string, string>(VariableRefs) : null
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
using OpenNest.Geometry;
|
using OpenNest.Geometry;
|
||||||
|
using OpenNest.Math;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace OpenNest.CNC.CuttingStrategy
|
namespace OpenNest.CNC.CuttingStrategy
|
||||||
{
|
{
|
||||||
@@ -7,75 +9,330 @@ namespace OpenNest.CNC.CuttingStrategy
|
|||||||
{
|
{
|
||||||
public CuttingParameters Parameters { get; set; }
|
public CuttingParameters Parameters { get; set; }
|
||||||
|
|
||||||
public Program Apply(Program partProgram, Plate plate)
|
private record ContourEntry(Shape Shape, Vector Point, Entity Entity);
|
||||||
|
|
||||||
|
public CuttingResult Apply(Program partProgram, Vector approachPoint)
|
||||||
|
{
|
||||||
|
return Apply(partProgram, approachPoint, Vector.Invalid);
|
||||||
|
}
|
||||||
|
|
||||||
|
public CuttingResult Apply(Program partProgram, Vector approachPoint, Vector nextPartStart)
|
||||||
{
|
{
|
||||||
var exitPoint = GetExitPoint(plate);
|
|
||||||
var entities = partProgram.ToGeometry();
|
var entities = partProgram.ToGeometry();
|
||||||
|
entities.RemoveAll(e => e.Layer == SpecialLayers.Rapid);
|
||||||
|
|
||||||
|
var scribeEntities = entities.FindAll(e => e.Layer == SpecialLayers.Scribe);
|
||||||
|
entities.RemoveAll(e => e.Layer == SpecialLayers.Scribe);
|
||||||
|
|
||||||
var profile = new ShapeProfile(entities);
|
var profile = new ShapeProfile(entities);
|
||||||
|
|
||||||
// Find closest point on perimeter from exit point
|
// Start from the bounding box corner opposite the origin (max X, max Y)
|
||||||
var perimeterPoint = profile.Perimeter.ClosestPointTo(exitPoint, out var perimeterEntity);
|
var bbox = entities.GetBoundingBox();
|
||||||
|
var startCorner = new Vector(bbox.Right, bbox.Top);
|
||||||
|
|
||||||
// Chain cutouts by nearest-neighbor from perimeter point, then reverse
|
// Initial pass: sequence cutouts from bbox corner
|
||||||
// so farthest cutouts are cut first, nearest-to-perimeter cut last
|
var seedPoint = startCorner;
|
||||||
var orderedCutouts = SequenceCutouts(profile.Cutouts, perimeterPoint);
|
var orderedCutouts = SequenceCutouts(profile.Cutouts, seedPoint);
|
||||||
orderedCutouts.Reverse();
|
orderedCutouts.Reverse();
|
||||||
|
|
||||||
// Build output program: cutouts first (farthest to nearest), perimeter last
|
var perimeterSeed = profile.Perimeter.ClosestPointTo(seedPoint, out _);
|
||||||
var result = new Program();
|
var cutoutEntries = ResolveLeadInPoints(orderedCutouts, perimeterSeed);
|
||||||
var currentPoint = exitPoint;
|
|
||||||
|
|
||||||
foreach (var cutout in orderedCutouts)
|
Vector perimeterPt;
|
||||||
|
Entity perimeterEntity;
|
||||||
|
|
||||||
|
if (!double.IsNaN(nextPartStart.X) && cutoutEntries.Count > 0)
|
||||||
{
|
{
|
||||||
var contourType = DetectContourType(cutout);
|
// Iterate: each pass refines the perimeter lead-in which changes
|
||||||
var closestPt = cutout.ClosestPointTo(currentPoint, out var entity);
|
// the internal sequence which changes the last cutout position
|
||||||
var normal = ComputeNormal(closestPt, entity, contourType);
|
for (var iter = 0; iter < 3; iter++)
|
||||||
var winding = DetermineWinding(cutout);
|
{
|
||||||
|
var lastCutoutPt = cutoutEntries[cutoutEntries.Count - 1].Point;
|
||||||
|
perimeterSeed = FindPerimeterIntersection(profile.Perimeter, lastCutoutPt, nextPartStart, out _);
|
||||||
|
|
||||||
var leadIn = SelectLeadIn(contourType);
|
orderedCutouts = SequenceCutouts(profile.Cutouts, perimeterSeed);
|
||||||
var leadOut = SelectLeadOut(contourType);
|
orderedCutouts.Reverse();
|
||||||
|
cutoutEntries = ResolveLeadInPoints(orderedCutouts, perimeterSeed);
|
||||||
|
}
|
||||||
|
|
||||||
result.Codes.AddRange(leadIn.Generate(closestPt, normal, winding));
|
var finalLastCutout = cutoutEntries[cutoutEntries.Count - 1].Point;
|
||||||
var reindexed = cutout.ReindexAt(closestPt, entity);
|
perimeterPt = FindPerimeterIntersection(profile.Perimeter, finalLastCutout, nextPartStart, out perimeterEntity);
|
||||||
result.Codes.AddRange(ConvertShapeToMoves(reindexed, closestPt));
|
}
|
||||||
// TODO: MicrotabLeadOut — trim last cutting move by GapSize
|
else
|
||||||
result.Codes.AddRange(leadOut.Generate(closestPt, normal, winding));
|
{
|
||||||
|
var perimeterRef = cutoutEntries.Count > 0 ? cutoutEntries[0].Point : approachPoint;
|
||||||
|
perimeterPt = profile.Perimeter.ClosestPointTo(perimeterRef, out perimeterEntity);
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = new Program(Mode.Absolute);
|
||||||
|
|
||||||
|
EmitScribeContours(result, scribeEntities);
|
||||||
|
|
||||||
|
foreach (var entry in cutoutEntries)
|
||||||
|
EmitContour(result, entry.Shape, entry.Point, entry.Entity);
|
||||||
|
|
||||||
|
EmitContour(result, profile.Perimeter, perimeterPt, perimeterEntity, ContourType.External);
|
||||||
|
|
||||||
|
result.Mode = Mode.Incremental;
|
||||||
|
|
||||||
|
return new CuttingResult
|
||||||
|
{
|
||||||
|
Program = result,
|
||||||
|
LastCutPoint = perimeterPt
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public CuttingResult ApplySingle(Program partProgram, Vector point, Entity entity, ContourType contourType)
|
||||||
|
{
|
||||||
|
var entities = partProgram.ToGeometry();
|
||||||
|
entities.RemoveAll(e => e.Layer == SpecialLayers.Rapid);
|
||||||
|
|
||||||
|
var scribeEntities = entities.FindAll(e => e.Layer == SpecialLayers.Scribe);
|
||||||
|
entities.RemoveAll(e => e.Layer == SpecialLayers.Scribe);
|
||||||
|
|
||||||
|
var profile = new ShapeProfile(entities);
|
||||||
|
|
||||||
|
var result = new Program(Mode.Absolute);
|
||||||
|
|
||||||
|
EmitScribeContours(result, scribeEntities);
|
||||||
|
|
||||||
|
// Find the target shape that contains the clicked entity
|
||||||
|
var (targetShape, matchedEntity) = FindTargetShape(profile, point, entity);
|
||||||
|
|
||||||
|
// Emit cutouts — only the target gets lead-in/out
|
||||||
|
foreach (var cutout in profile.Cutouts)
|
||||||
|
{
|
||||||
|
if (cutout == targetShape)
|
||||||
|
{
|
||||||
|
var ct = DetectContourType(cutout);
|
||||||
|
EmitContour(result, cutout, point, matchedEntity, ct);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
EmitRawContour(result, cutout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Emit perimeter
|
||||||
|
if (profile.Perimeter == targetShape)
|
||||||
|
{
|
||||||
|
EmitContour(result, profile.Perimeter, point, matchedEntity, ContourType.External);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
EmitRawContour(result, profile.Perimeter);
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Mode = Mode.Incremental;
|
||||||
|
|
||||||
|
return new CuttingResult
|
||||||
|
{
|
||||||
|
Program = result,
|
||||||
|
LastCutPoint = point
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static (Shape Shape, Entity Entity) FindTargetShape(ShapeProfile profile, Vector point, Entity clickedEntity)
|
||||||
|
{
|
||||||
|
var matched = FindMatchingEntity(profile.Perimeter, clickedEntity);
|
||||||
|
if (matched != null)
|
||||||
|
return (profile.Perimeter, matched);
|
||||||
|
|
||||||
|
foreach (var cutout in profile.Cutouts)
|
||||||
|
{
|
||||||
|
matched = FindMatchingEntity(cutout, clickedEntity);
|
||||||
|
if (matched != null)
|
||||||
|
return (cutout, matched);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: closest shape, use closest point to find entity
|
||||||
|
var best = profile.Perimeter;
|
||||||
|
var bestPt = profile.Perimeter.ClosestPointTo(point, out var bestEntity);
|
||||||
|
var bestDist = bestPt.DistanceTo(point);
|
||||||
|
|
||||||
|
foreach (var cutout in profile.Cutouts)
|
||||||
|
{
|
||||||
|
var pt = cutout.ClosestPointTo(point, out var cutoutEntity);
|
||||||
|
var dist = pt.DistanceTo(point);
|
||||||
|
if (dist < bestDist)
|
||||||
|
{
|
||||||
|
best = cutout;
|
||||||
|
bestEntity = cutoutEntity;
|
||||||
|
bestDist = dist;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (best, bestEntity);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Entity FindMatchingEntity(Shape shape, Entity clickedEntity)
|
||||||
|
{
|
||||||
|
foreach (var shapeEntity in shape.Entities)
|
||||||
|
{
|
||||||
|
if (shapeEntity.GetType() != clickedEntity.GetType())
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (shapeEntity is Line sLine && clickedEntity is Line cLine)
|
||||||
|
{
|
||||||
|
if (sLine.StartPoint.DistanceTo(cLine.StartPoint) < Math.Tolerance.Epsilon
|
||||||
|
&& sLine.EndPoint.DistanceTo(cLine.EndPoint) < Math.Tolerance.Epsilon)
|
||||||
|
return shapeEntity;
|
||||||
|
}
|
||||||
|
else if (shapeEntity is Arc sArc && clickedEntity is Arc cArc)
|
||||||
|
{
|
||||||
|
if (System.Math.Abs(sArc.Radius - cArc.Radius) < Math.Tolerance.Epsilon
|
||||||
|
&& sArc.Center.DistanceTo(cArc.Center) < Math.Tolerance.Epsilon)
|
||||||
|
return shapeEntity;
|
||||||
|
}
|
||||||
|
else if (shapeEntity is Circle sCircle && clickedEntity is Circle cCircle)
|
||||||
|
{
|
||||||
|
if (System.Math.Abs(sCircle.Radius - cCircle.Radius) < Math.Tolerance.Epsilon
|
||||||
|
&& sCircle.Center.DistanceTo(cCircle.Center) < Math.Tolerance.Epsilon)
|
||||||
|
return shapeEntity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void EmitRawContour(Program program, Shape shape)
|
||||||
|
{
|
||||||
|
var startPoint = GetShapeStartPoint(shape);
|
||||||
|
program.Codes.Add(new RapidMove(startPoint));
|
||||||
|
program.Codes.AddRange(ConvertShapeToMoves(shape, startPoint));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<ContourEntry> ResolveLeadInPoints(List<Shape> cutouts, Vector startPoint)
|
||||||
|
{
|
||||||
|
var entries = new ContourEntry[cutouts.Count];
|
||||||
|
var currentPoint = startPoint;
|
||||||
|
|
||||||
|
// Walk backward through cutting order (from perimeter outward)
|
||||||
|
// so each cutout's lead-in point faces the next cutout to be cut
|
||||||
|
for (var i = cutouts.Count - 1; i >= 0; i--)
|
||||||
|
{
|
||||||
|
var closestPt = cutouts[i].ClosestPointTo(currentPoint, out var entity);
|
||||||
|
entries[i] = new ContourEntry(cutouts[i], closestPt, entity);
|
||||||
currentPoint = closestPt;
|
currentPoint = closestPt;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Perimeter last
|
return new List<ContourEntry>(entries);
|
||||||
{
|
|
||||||
var perimeterPt = profile.Perimeter.ClosestPointTo(currentPoint, out perimeterEntity);
|
|
||||||
var normal = ComputeNormal(perimeterPt, perimeterEntity, ContourType.External);
|
|
||||||
var winding = DetermineWinding(profile.Perimeter);
|
|
||||||
|
|
||||||
var leadIn = SelectLeadIn(ContourType.External);
|
|
||||||
var leadOut = SelectLeadOut(ContourType.External);
|
|
||||||
|
|
||||||
result.Codes.AddRange(leadIn.Generate(perimeterPt, normal, winding));
|
|
||||||
var reindexed = profile.Perimeter.ReindexAt(perimeterPt, perimeterEntity);
|
|
||||||
result.Codes.AddRange(ConvertShapeToMoves(reindexed, perimeterPt));
|
|
||||||
// TODO: MicrotabLeadOut — trim last cutting move by GapSize
|
|
||||||
result.Codes.AddRange(leadOut.Generate(perimeterPt, normal, winding));
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Vector GetExitPoint(Plate plate)
|
private static Vector FindPerimeterIntersection(Shape perimeter, Vector lastCutout, Vector nextPartStart, out Entity entity)
|
||||||
{
|
{
|
||||||
var w = plate.Size.Width;
|
var ray = new Line(lastCutout, nextPartStart);
|
||||||
var l = plate.Size.Length;
|
|
||||||
|
|
||||||
return plate.Quadrant switch
|
if (perimeter.Intersects(ray, out var pts) && pts.Count > 0)
|
||||||
{
|
{
|
||||||
1 => new Vector(w, l), // Q1 origin BottomLeft -> exit TopRight
|
// Pick the intersection closest to the last cutout
|
||||||
2 => new Vector(0, l), // Q2 origin BottomRight -> exit TopLeft
|
var best = pts[0];
|
||||||
3 => new Vector(0, 0), // Q3 origin TopRight -> exit BottomLeft
|
var bestDist = best.DistanceTo(lastCutout);
|
||||||
4 => new Vector(w, 0), // Q4 origin TopLeft -> exit BottomRight
|
|
||||||
_ => new Vector(w, l)
|
for (var i = 1; i < pts.Count; i++)
|
||||||
};
|
{
|
||||||
|
var dist = pts[i].DistanceTo(lastCutout);
|
||||||
|
if (dist < bestDist)
|
||||||
|
{
|
||||||
|
best = pts[i];
|
||||||
|
bestDist = dist;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return perimeter.ClosestPointTo(best, out entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: closest point on perimeter to the last cutout
|
||||||
|
return perimeter.ClosestPointTo(lastCutout, out entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int ComputeSubProgramKey(double radius, double normalAngle)
|
||||||
|
{
|
||||||
|
var r = System.Math.Round(radius, 6);
|
||||||
|
var a = System.Math.Round(normalAngle, 6);
|
||||||
|
return HashCode.Combine(r, a);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void EmitContour(Program program, Shape shape, Vector point, Entity entity, ContourType? forceType = null)
|
||||||
|
{
|
||||||
|
var contourType = forceType ?? DetectContourType(shape);
|
||||||
|
var winding = DetermineWinding(shape);
|
||||||
|
var normal = ComputeNormal(point, entity, contourType, winding);
|
||||||
|
|
||||||
|
var leadIn = SelectLeadIn(contourType);
|
||||||
|
var leadOut = SelectLeadOut(contourType);
|
||||||
|
|
||||||
|
if (contourType == ContourType.ArcCircle && entity is Circle circle)
|
||||||
|
{
|
||||||
|
if (Parameters.RoundLeadInAngles && Parameters.LeadInAngleIncrement > 0)
|
||||||
|
{
|
||||||
|
var increment = Angle.ToRadians(Parameters.LeadInAngleIncrement);
|
||||||
|
normal = System.Math.Round(normal / increment) * increment;
|
||||||
|
normal = Angle.NormalizeRad(normal);
|
||||||
|
|
||||||
|
var outwardAngle = normal - System.Math.PI;
|
||||||
|
point = new Vector(
|
||||||
|
circle.Center.X + circle.Radius * System.Math.Cos(outwardAngle),
|
||||||
|
circle.Center.Y + circle.Radius * System.Math.Sin(outwardAngle));
|
||||||
|
}
|
||||||
|
|
||||||
|
leadIn = ClampLeadInForCircle(leadIn, circle, point, normal);
|
||||||
|
|
||||||
|
// Build hole sub-program relative to (0,0)
|
||||||
|
var holeCenter = circle.Center;
|
||||||
|
var relativePoint = new Vector(point.X - holeCenter.X, point.Y - holeCenter.Y);
|
||||||
|
var relativeCircle = new Circle(new Vector(0, 0), circle.Radius) { Rotation = circle.Rotation };
|
||||||
|
var relativeShape = new Shape();
|
||||||
|
relativeShape.Entities.Add(relativeCircle);
|
||||||
|
|
||||||
|
var subPgm = new Program(Mode.Absolute);
|
||||||
|
subPgm.Codes.AddRange(leadIn.Generate(relativePoint, normal, winding));
|
||||||
|
var reindexed = relativeShape.ReindexAt(relativePoint, relativeCircle);
|
||||||
|
|
||||||
|
if (Parameters.TabsEnabled && Parameters.TabConfig != null)
|
||||||
|
reindexed = TrimShapeForTab(reindexed, relativePoint, Parameters.TabConfig.Size);
|
||||||
|
|
||||||
|
subPgm.Codes.AddRange(ConvertShapeToMoves(reindexed, relativePoint));
|
||||||
|
subPgm.Codes.AddRange(leadOut.Generate(relativePoint, normal, winding));
|
||||||
|
subPgm.Mode = Mode.Incremental;
|
||||||
|
|
||||||
|
// Deduplicate: check if an identical sub-program already exists
|
||||||
|
var key = ComputeSubProgramKey(circle.Radius, normal);
|
||||||
|
if (!program.SubPrograms.ContainsKey(key))
|
||||||
|
program.SubPrograms[key] = subPgm;
|
||||||
|
|
||||||
|
program.Codes.Add(new SubProgramCall
|
||||||
|
{
|
||||||
|
Id = key,
|
||||||
|
Program = program.SubPrograms[key],
|
||||||
|
Offset = holeCenter
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
program.Codes.AddRange(leadIn.Generate(point, normal, winding));
|
||||||
|
|
||||||
|
var reindexedShape = shape.ReindexAt(point, entity);
|
||||||
|
|
||||||
|
if (Parameters.TabsEnabled && Parameters.TabConfig != null)
|
||||||
|
reindexedShape = TrimShapeForTab(reindexedShape, point, Parameters.TabConfig.Size);
|
||||||
|
|
||||||
|
program.Codes.AddRange(ConvertShapeToMoves(reindexedShape, point));
|
||||||
|
program.Codes.AddRange(leadOut.Generate(point, normal, winding));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void EmitScribeContours(Program program, List<Entity> scribeEntities)
|
||||||
|
{
|
||||||
|
if (scribeEntities.Count == 0) return;
|
||||||
|
|
||||||
|
var shapes = ShapeBuilder.GetShapes(scribeEntities);
|
||||||
|
foreach (var shape in shapes)
|
||||||
|
{
|
||||||
|
var startPt = GetShapeStartPoint(shape);
|
||||||
|
program.Codes.Add(new RapidMove(startPt));
|
||||||
|
program.Codes.AddRange(ConvertShapeToMoves(shape, startPt, LayerType.Scribe));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<Shape> SequenceCutouts(List<Shape> cutouts, Vector startPoint)
|
private List<Shape> SequenceCutouts(List<Shape> cutouts, Vector startPoint)
|
||||||
@@ -110,7 +367,7 @@ namespace OpenNest.CNC.CuttingStrategy
|
|||||||
return ordered;
|
return ordered;
|
||||||
}
|
}
|
||||||
|
|
||||||
private ContourType DetectContourType(Shape cutout)
|
public static ContourType DetectContourType(Shape cutout)
|
||||||
{
|
{
|
||||||
if (cutout.Entities.Count == 1 && cutout.Entities[0] is Circle)
|
if (cutout.Entities.Count == 1 && cutout.Entities[0] is Circle)
|
||||||
return ContourType.ArcCircle;
|
return ContourType.ArcCircle;
|
||||||
@@ -118,23 +375,33 @@ namespace OpenNest.CNC.CuttingStrategy
|
|||||||
return ContourType.Internal;
|
return ContourType.Internal;
|
||||||
}
|
}
|
||||||
|
|
||||||
private double ComputeNormal(Vector point, Entity entity, ContourType contourType)
|
public static double ComputeNormal(Vector point, Entity entity, ContourType contourType,
|
||||||
|
RotationType winding = RotationType.CW)
|
||||||
{
|
{
|
||||||
double normal;
|
double normal;
|
||||||
|
|
||||||
if (entity is Line line)
|
if (entity is Line line)
|
||||||
{
|
{
|
||||||
// Perpendicular to line direction
|
// Perpendicular to line direction: tangent + π/2 = left side.
|
||||||
|
// Left side = outward for CW winding; for CCW winding, outward
|
||||||
|
// is on the right side, so flip.
|
||||||
var tangent = line.EndPoint.AngleFrom(line.StartPoint);
|
var tangent = line.EndPoint.AngleFrom(line.StartPoint);
|
||||||
normal = tangent + Math.Angle.HalfPI;
|
normal = tangent + Math.Angle.HalfPI;
|
||||||
|
if (winding == RotationType.CCW)
|
||||||
|
normal += System.Math.PI;
|
||||||
}
|
}
|
||||||
else if (entity is Arc arc)
|
else if (entity is Arc arc)
|
||||||
{
|
{
|
||||||
// Radial direction from center to point
|
// Radial direction from center to point.
|
||||||
|
// Flip when the arc direction differs from the contour winding —
|
||||||
|
// that indicates a concave feature where radial points inward.
|
||||||
normal = point.AngleFrom(arc.Center);
|
normal = point.AngleFrom(arc.Center);
|
||||||
|
if (arc.Rotation != winding)
|
||||||
|
normal += System.Math.PI;
|
||||||
}
|
}
|
||||||
else if (entity is Circle circle)
|
else if (entity is Circle circle)
|
||||||
{
|
{
|
||||||
|
// Radial outward — always correct regardless of winding
|
||||||
normal = point.AngleFrom(circle.Center);
|
normal = point.AngleFrom(circle.Center);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -149,11 +416,61 @@ namespace OpenNest.CNC.CuttingStrategy
|
|||||||
return Math.Angle.NormalizeRad(normal);
|
return Math.Angle.NormalizeRad(normal);
|
||||||
}
|
}
|
||||||
|
|
||||||
private RotationType DetermineWinding(Shape shape)
|
public static RotationType DetermineWinding(Shape shape)
|
||||||
{
|
{
|
||||||
// Use signed area: positive = CCW, negative = CW
|
if (shape.Entities.Count == 1 && shape.Entities[0] is Circle circle)
|
||||||
var area = shape.Area();
|
return circle.Rotation;
|
||||||
return area >= 0 ? RotationType.CCW : RotationType.CW;
|
|
||||||
|
var polygon = shape.ToPolygon();
|
||||||
|
|
||||||
|
if (polygon.Vertices.Count < 3)
|
||||||
|
return RotationType.CCW;
|
||||||
|
|
||||||
|
return polygon.RotationDirection();
|
||||||
|
}
|
||||||
|
|
||||||
|
private LeadIn ClampLeadInForCircle(LeadIn leadIn, Circle circle, Vector contourPoint, double normalAngle)
|
||||||
|
{
|
||||||
|
if (leadIn is NoLeadIn || Parameters.PierceClearance <= 0)
|
||||||
|
return leadIn;
|
||||||
|
|
||||||
|
var piercePoint = leadIn.GetPiercePoint(contourPoint, normalAngle);
|
||||||
|
var maxRadius = circle.Radius - Parameters.PierceClearance;
|
||||||
|
if (maxRadius <= 0)
|
||||||
|
return leadIn;
|
||||||
|
|
||||||
|
var distFromCenter = piercePoint.DistanceTo(circle.Center);
|
||||||
|
if (distFromCenter <= maxRadius)
|
||||||
|
return leadIn;
|
||||||
|
|
||||||
|
// Compute max distance from contourPoint toward piercePoint that stays
|
||||||
|
// inside a circle of radius maxRadius centered at circle.Center.
|
||||||
|
// Solve: |contourPoint + t*d - center|^2 = maxRadius^2
|
||||||
|
var currentDist = contourPoint.DistanceTo(piercePoint);
|
||||||
|
if (currentDist < Math.Tolerance.Epsilon)
|
||||||
|
return leadIn;
|
||||||
|
|
||||||
|
var dx = (piercePoint.X - contourPoint.X) / currentDist;
|
||||||
|
var dy = (piercePoint.Y - contourPoint.Y) / currentDist;
|
||||||
|
var vx = contourPoint.X - circle.Center.X;
|
||||||
|
var vy = contourPoint.Y - circle.Center.Y;
|
||||||
|
|
||||||
|
var b = 2.0 * (vx * dx + vy * dy);
|
||||||
|
var c = vx * vx + vy * vy - maxRadius * maxRadius;
|
||||||
|
var discriminant = b * b - 4.0 * c;
|
||||||
|
|
||||||
|
if (discriminant < 0)
|
||||||
|
return leadIn;
|
||||||
|
|
||||||
|
var t = (-b + System.Math.Sqrt(discriminant)) / 2.0;
|
||||||
|
if (t <= 0)
|
||||||
|
return leadIn;
|
||||||
|
|
||||||
|
var scale = t / currentDist;
|
||||||
|
if (scale >= 1.0)
|
||||||
|
return leadIn;
|
||||||
|
|
||||||
|
return leadIn.Scale(scale);
|
||||||
}
|
}
|
||||||
|
|
||||||
private LeadIn SelectLeadIn(ContourType contourType)
|
private LeadIn SelectLeadIn(ContourType contourType)
|
||||||
@@ -176,7 +493,71 @@ namespace OpenNest.CNC.CuttingStrategy
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<ICode> ConvertShapeToMoves(Shape shape, Vector startPoint)
|
private static Shape TrimShapeForTab(Shape shape, Vector center, double tabSize)
|
||||||
|
{
|
||||||
|
var tabCircle = new Circle(center, tabSize);
|
||||||
|
var entities = new List<Entity>(shape.Entities);
|
||||||
|
|
||||||
|
// Trim end: walk backward removing entities inside the tab circle
|
||||||
|
while (entities.Count > 0)
|
||||||
|
{
|
||||||
|
var entity = entities[entities.Count - 1];
|
||||||
|
if (entity.Intersects(tabCircle, out var pts) && pts.Count > 0)
|
||||||
|
{
|
||||||
|
// Find intersection furthest from center (furthest along path from end)
|
||||||
|
var best = pts[0];
|
||||||
|
var bestDist = best.DistanceTo(center);
|
||||||
|
for (var j = 1; j < pts.Count; j++)
|
||||||
|
{
|
||||||
|
var dist = pts[j].DistanceTo(center);
|
||||||
|
if (dist > bestDist)
|
||||||
|
{
|
||||||
|
best = pts[j];
|
||||||
|
bestDist = dist;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entity is Line line)
|
||||||
|
{
|
||||||
|
var (first, _) = line.SplitAt(best);
|
||||||
|
entities.RemoveAt(entities.Count - 1);
|
||||||
|
if (first != null)
|
||||||
|
entities.Add(first);
|
||||||
|
}
|
||||||
|
else if (entity is Arc arc)
|
||||||
|
{
|
||||||
|
var (first, _) = arc.SplitAt(best);
|
||||||
|
entities.RemoveAt(entities.Count - 1);
|
||||||
|
if (first != null)
|
||||||
|
entities.Add(first);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No intersection — entity is entirely inside circle, remove it
|
||||||
|
if (EntityStartPoint(entity).DistanceTo(center) <= tabSize + Tolerance.Epsilon)
|
||||||
|
{
|
||||||
|
entities.RemoveAt(entities.Count - 1);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = new Shape();
|
||||||
|
result.Entities.AddRange(entities);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Vector EntityStartPoint(Entity entity)
|
||||||
|
{
|
||||||
|
if (entity is Line line) return line.StartPoint;
|
||||||
|
if (entity is Arc arc) return arc.StartPoint();
|
||||||
|
return Vector.Zero;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<ICode> ConvertShapeToMoves(Shape shape, Vector startPoint, LayerType layer = LayerType.Display)
|
||||||
{
|
{
|
||||||
var moves = new List<ICode>();
|
var moves = new List<ICode>();
|
||||||
|
|
||||||
@@ -184,15 +565,15 @@ namespace OpenNest.CNC.CuttingStrategy
|
|||||||
{
|
{
|
||||||
if (entity is Line line)
|
if (entity is Line line)
|
||||||
{
|
{
|
||||||
moves.Add(new LinearMove(line.EndPoint));
|
moves.Add(new LinearMove(line.EndPoint) { Layer = layer });
|
||||||
}
|
}
|
||||||
else if (entity is Arc arc)
|
else if (entity is Arc arc)
|
||||||
{
|
{
|
||||||
moves.Add(new ArcMove(arc.EndPoint(), arc.Center, arc.IsReversed ? RotationType.CW : RotationType.CCW));
|
moves.Add(new ArcMove(arc.EndPoint(), arc.Center, arc.IsReversed ? RotationType.CW : RotationType.CCW) { Layer = layer });
|
||||||
}
|
}
|
||||||
else if (entity is Circle circle)
|
else if (entity is Circle circle)
|
||||||
{
|
{
|
||||||
moves.Add(new ArcMove(startPoint, circle.Center, circle.Rotation));
|
moves.Add(new ArcMove(startPoint, circle.Center, circle.Rotation) { Layer = layer });
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -202,5 +583,14 @@ namespace OpenNest.CNC.CuttingStrategy
|
|||||||
|
|
||||||
return moves;
|
return moves;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Vector GetShapeStartPoint(Shape shape)
|
||||||
|
{
|
||||||
|
var first = shape.Entities[0];
|
||||||
|
if (first is Line line) return line.StartPoint;
|
||||||
|
if (first is Arc arc) return arc.StartPoint();
|
||||||
|
if (first is Circle circle) return new Vector(circle.Center.X + circle.Radius, circle.Center.Y);
|
||||||
|
return Vector.Zero;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,14 @@ namespace OpenNest.CNC.CuttingStrategy
|
|||||||
public LeadIn ArcCircleLeadIn { get; set; } = new NoLeadIn();
|
public LeadIn ArcCircleLeadIn { get; set; } = new NoLeadIn();
|
||||||
public LeadOut ArcCircleLeadOut { get; set; } = new NoLeadOut();
|
public LeadOut ArcCircleLeadOut { get; set; } = new NoLeadOut();
|
||||||
|
|
||||||
|
public double PierceClearance { get; set; } = 0.0625;
|
||||||
|
|
||||||
|
public bool RoundLeadInAngles { get; set; }
|
||||||
|
public double LeadInAngleIncrement { get; set; } = 5.0;
|
||||||
|
|
||||||
|
public double AutoTabMinSize { get; set; }
|
||||||
|
public double AutoTabMaxSize { get; set; }
|
||||||
|
|
||||||
public Tab TabConfig { get; set; }
|
public Tab TabConfig { get; set; }
|
||||||
public bool TabsEnabled { get; set; }
|
public bool TabsEnabled { get; set; }
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
using OpenNest.Geometry;
|
||||||
|
|
||||||
|
namespace OpenNest.CNC.CuttingStrategy
|
||||||
|
{
|
||||||
|
public readonly struct CuttingResult
|
||||||
|
{
|
||||||
|
public Program Program { get; init; }
|
||||||
|
public Vector LastCutPoint { get; init; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
using OpenNest.Geometry;
|
using OpenNest.Geometry;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace OpenNest.CNC.CuttingStrategy
|
namespace OpenNest.CNC.CuttingStrategy
|
||||||
{
|
{
|
||||||
@@ -19,7 +19,7 @@ namespace OpenNest.CNC.CuttingStrategy
|
|||||||
return new List<ICode>
|
return new List<ICode>
|
||||||
{
|
{
|
||||||
new RapidMove(piercePoint),
|
new RapidMove(piercePoint),
|
||||||
new ArcMove(contourStartPoint, arcCenter, winding)
|
new ArcMove(contourStartPoint, arcCenter, winding) { Layer = LayerType.Leadin }
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,5 +32,8 @@ namespace OpenNest.CNC.CuttingStrategy
|
|||||||
arcCenterX + Radius * System.Math.Cos(contourNormalAngle),
|
arcCenterX + Radius * System.Math.Cos(contourNormalAngle),
|
||||||
arcCenterY + Radius * System.Math.Sin(contourNormalAngle));
|
arcCenterY + Radius * System.Math.Sin(contourNormalAngle));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override LeadIn Scale(double factor) =>
|
||||||
|
new ArcLeadIn { Radius = Radius * factor };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
using OpenNest.Geometry;
|
using OpenNest.Geometry;
|
||||||
using OpenNest.Math;
|
using OpenNest.Math;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace OpenNest.CNC.CuttingStrategy
|
namespace OpenNest.CNC.CuttingStrategy
|
||||||
{
|
{
|
||||||
@@ -27,8 +27,8 @@ namespace OpenNest.CNC.CuttingStrategy
|
|||||||
return new List<ICode>
|
return new List<ICode>
|
||||||
{
|
{
|
||||||
new RapidMove(piercePoint),
|
new RapidMove(piercePoint),
|
||||||
new LinearMove(arcStart),
|
new LinearMove(arcStart) { Layer = LayerType.Leadin },
|
||||||
new ArcMove(contourStartPoint, arcCenter, winding)
|
new ArcMove(contourStartPoint, arcCenter, winding) { Layer = LayerType.Leadin }
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,5 +45,8 @@ namespace OpenNest.CNC.CuttingStrategy
|
|||||||
arcStartX + LineLength * System.Math.Cos(lineAngle),
|
arcStartX + LineLength * System.Math.Cos(lineAngle),
|
||||||
arcStartY + LineLength * System.Math.Sin(lineAngle));
|
arcStartY + LineLength * System.Math.Sin(lineAngle));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override LeadIn Scale(double factor) =>
|
||||||
|
new CleanHoleLeadIn { LineLength = LineLength * factor, ArcRadius = ArcRadius * factor, Kerf = Kerf };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
using OpenNest.Geometry;
|
using OpenNest.Geometry;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace OpenNest.CNC.CuttingStrategy
|
namespace OpenNest.CNC.CuttingStrategy
|
||||||
{
|
{
|
||||||
@@ -9,5 +9,7 @@ namespace OpenNest.CNC.CuttingStrategy
|
|||||||
RotationType winding = RotationType.CW);
|
RotationType winding = RotationType.CW);
|
||||||
|
|
||||||
public abstract Vector GetPiercePoint(Vector contourStartPoint, double contourNormalAngle);
|
public abstract Vector GetPiercePoint(Vector contourStartPoint, double contourNormalAngle);
|
||||||
|
|
||||||
|
public virtual LeadIn Scale(double factor) => this;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
using OpenNest.Geometry;
|
using OpenNest.Geometry;
|
||||||
using OpenNest.Math;
|
using OpenNest.Math;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace OpenNest.CNC.CuttingStrategy
|
namespace OpenNest.CNC.CuttingStrategy
|
||||||
{
|
{
|
||||||
@@ -27,8 +27,8 @@ namespace OpenNest.CNC.CuttingStrategy
|
|||||||
return new List<ICode>
|
return new List<ICode>
|
||||||
{
|
{
|
||||||
new RapidMove(piercePoint),
|
new RapidMove(piercePoint),
|
||||||
new LinearMove(arcStart),
|
new LinearMove(arcStart) { Layer = LayerType.Leadin },
|
||||||
new ArcMove(contourStartPoint, arcCenter, winding)
|
new ArcMove(contourStartPoint, arcCenter, winding) { Layer = LayerType.Leadin }
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,5 +45,8 @@ namespace OpenNest.CNC.CuttingStrategy
|
|||||||
arcStartX + LineLength * System.Math.Cos(lineAngle),
|
arcStartX + LineLength * System.Math.Cos(lineAngle),
|
||||||
arcStartY + LineLength * System.Math.Sin(lineAngle));
|
arcStartY + LineLength * System.Math.Sin(lineAngle));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override LeadIn Scale(double factor) =>
|
||||||
|
new LineArcLeadIn { LineLength = LineLength * factor, ArcRadius = ArcRadius * factor, ApproachAngle = ApproachAngle };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
using OpenNest.Geometry;
|
using OpenNest.Geometry;
|
||||||
using OpenNest.Math;
|
using OpenNest.Math;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace OpenNest.CNC.CuttingStrategy
|
namespace OpenNest.CNC.CuttingStrategy
|
||||||
{
|
{
|
||||||
@@ -17,16 +17,19 @@ namespace OpenNest.CNC.CuttingStrategy
|
|||||||
return new List<ICode>
|
return new List<ICode>
|
||||||
{
|
{
|
||||||
new RapidMove(piercePoint),
|
new RapidMove(piercePoint),
|
||||||
new LinearMove(contourStartPoint)
|
new LinearMove(contourStartPoint) { Layer = LayerType.Leadin }
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Vector GetPiercePoint(Vector contourStartPoint, double contourNormalAngle)
|
public override Vector GetPiercePoint(Vector contourStartPoint, double contourNormalAngle)
|
||||||
{
|
{
|
||||||
var approachAngle = contourNormalAngle + Angle.ToRadians(ApproachAngle);
|
var approachAngle = contourNormalAngle - Angle.HalfPI + Angle.ToRadians(ApproachAngle);
|
||||||
return new Vector(
|
return new Vector(
|
||||||
contourStartPoint.X + Length * System.Math.Cos(approachAngle),
|
contourStartPoint.X + Length * System.Math.Cos(approachAngle),
|
||||||
contourStartPoint.Y + Length * System.Math.Sin(approachAngle));
|
contourStartPoint.Y + Length * System.Math.Sin(approachAngle));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override LeadIn Scale(double factor) =>
|
||||||
|
new LineLeadIn { Length = Length * factor, ApproachAngle = ApproachAngle };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
using OpenNest.Geometry;
|
using OpenNest.Geometry;
|
||||||
using OpenNest.Math;
|
using OpenNest.Math;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace OpenNest.CNC.CuttingStrategy
|
namespace OpenNest.CNC.CuttingStrategy
|
||||||
{
|
{
|
||||||
@@ -16,7 +16,7 @@ namespace OpenNest.CNC.CuttingStrategy
|
|||||||
{
|
{
|
||||||
var piercePoint = GetPiercePoint(contourStartPoint, contourNormalAngle);
|
var piercePoint = GetPiercePoint(contourStartPoint, contourNormalAngle);
|
||||||
|
|
||||||
var secondAngle = contourNormalAngle + Angle.ToRadians(ApproachAngle1);
|
var secondAngle = contourNormalAngle - Angle.HalfPI + Angle.ToRadians(ApproachAngle1);
|
||||||
var midPoint = new Vector(
|
var midPoint = new Vector(
|
||||||
contourStartPoint.X + Length2 * System.Math.Cos(secondAngle),
|
contourStartPoint.X + Length2 * System.Math.Cos(secondAngle),
|
||||||
contourStartPoint.Y + Length2 * System.Math.Sin(secondAngle));
|
contourStartPoint.Y + Length2 * System.Math.Sin(secondAngle));
|
||||||
@@ -24,14 +24,14 @@ namespace OpenNest.CNC.CuttingStrategy
|
|||||||
return new List<ICode>
|
return new List<ICode>
|
||||||
{
|
{
|
||||||
new RapidMove(piercePoint),
|
new RapidMove(piercePoint),
|
||||||
new LinearMove(midPoint),
|
new LinearMove(midPoint) { Layer = LayerType.Leadin },
|
||||||
new LinearMove(contourStartPoint)
|
new LinearMove(contourStartPoint) { Layer = LayerType.Leadin }
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Vector GetPiercePoint(Vector contourStartPoint, double contourNormalAngle)
|
public override Vector GetPiercePoint(Vector contourStartPoint, double contourNormalAngle)
|
||||||
{
|
{
|
||||||
var secondAngle = contourNormalAngle + Angle.ToRadians(ApproachAngle1);
|
var secondAngle = contourNormalAngle - Angle.HalfPI + Angle.ToRadians(ApproachAngle1);
|
||||||
var midX = contourStartPoint.X + Length2 * System.Math.Cos(secondAngle);
|
var midX = contourStartPoint.X + Length2 * System.Math.Cos(secondAngle);
|
||||||
var midY = contourStartPoint.Y + Length2 * System.Math.Sin(secondAngle);
|
var midY = contourStartPoint.Y + Length2 * System.Math.Sin(secondAngle);
|
||||||
|
|
||||||
@@ -40,5 +40,8 @@ namespace OpenNest.CNC.CuttingStrategy
|
|||||||
midX + Length1 * System.Math.Cos(firstAngle),
|
midX + Length1 * System.Math.Cos(firstAngle),
|
||||||
midY + Length1 * System.Math.Sin(firstAngle));
|
midY + Length1 * System.Math.Sin(firstAngle));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override LeadIn Scale(double factor) =>
|
||||||
|
new LineLineLeadIn { Length1 = Length1 * factor, ApproachAngle1 = ApproachAngle1, Length2 = Length2 * factor, ApproachAngle2 = ApproachAngle2 };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
using OpenNest.Geometry;
|
using OpenNest.Geometry;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace OpenNest.CNC.CuttingStrategy
|
namespace OpenNest.CNC.CuttingStrategy
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
using OpenNest.Geometry;
|
using OpenNest.Geometry;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace OpenNest.CNC.CuttingStrategy
|
namespace OpenNest.CNC.CuttingStrategy
|
||||||
{
|
{
|
||||||
@@ -20,7 +20,7 @@ namespace OpenNest.CNC.CuttingStrategy
|
|||||||
|
|
||||||
return new List<ICode>
|
return new List<ICode>
|
||||||
{
|
{
|
||||||
new ArcMove(endPoint, arcCenter, winding)
|
new ArcMove(endPoint, arcCenter, winding) { Layer = LayerType.Leadout }
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
using OpenNest.Geometry;
|
using OpenNest.Geometry;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace OpenNest.CNC.CuttingStrategy
|
namespace OpenNest.CNC.CuttingStrategy
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
using OpenNest.Geometry;
|
using OpenNest.Geometry;
|
||||||
using OpenNest.Math;
|
using OpenNest.Math;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace OpenNest.CNC.CuttingStrategy
|
namespace OpenNest.CNC.CuttingStrategy
|
||||||
{
|
{
|
||||||
@@ -12,14 +12,14 @@ namespace OpenNest.CNC.CuttingStrategy
|
|||||||
public override List<ICode> Generate(Vector contourEndPoint, double contourNormalAngle,
|
public override List<ICode> Generate(Vector contourEndPoint, double contourNormalAngle,
|
||||||
RotationType winding = RotationType.CW)
|
RotationType winding = RotationType.CW)
|
||||||
{
|
{
|
||||||
var overcutAngle = contourNormalAngle + Angle.ToRadians(ApproachAngle);
|
var overcutAngle = contourNormalAngle + Angle.HalfPI - Angle.ToRadians(ApproachAngle);
|
||||||
var endPoint = new Vector(
|
var endPoint = new Vector(
|
||||||
contourEndPoint.X + Length * System.Math.Cos(overcutAngle),
|
contourEndPoint.X + Length * System.Math.Cos(overcutAngle),
|
||||||
contourEndPoint.Y + Length * System.Math.Sin(overcutAngle));
|
contourEndPoint.Y + Length * System.Math.Sin(overcutAngle));
|
||||||
|
|
||||||
return new List<ICode>
|
return new List<ICode>
|
||||||
{
|
{
|
||||||
new LinearMove(endPoint)
|
new LinearMove(endPoint) { Layer = LayerType.Leadout }
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
using OpenNest.Geometry;
|
|
||||||
|
|
||||||
namespace OpenNest.CNC.CuttingStrategy
|
|
||||||
{
|
|
||||||
public class MicrotabLeadOut : LeadOut
|
|
||||||
{
|
|
||||||
public double GapSize { get; set; } = 0.03;
|
|
||||||
|
|
||||||
public override List<ICode> Generate(Vector contourEndPoint, double contourNormalAngle,
|
|
||||||
RotationType winding = RotationType.CW)
|
|
||||||
{
|
|
||||||
return new List<ICode>();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
using OpenNest.Geometry;
|
using OpenNest.Geometry;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace OpenNest.CNC.CuttingStrategy
|
namespace OpenNest.CNC.CuttingStrategy
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
using OpenNest.Geometry;
|
using OpenNest.Geometry;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace OpenNest.CNC.CuttingStrategy
|
namespace OpenNest.CNC.CuttingStrategy
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
using OpenNest.Geometry;
|
using OpenNest.Geometry;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace OpenNest.CNC.CuttingStrategy
|
namespace OpenNest.CNC.CuttingStrategy
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
using OpenNest.Geometry;
|
using OpenNest.Geometry;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace OpenNest.CNC.CuttingStrategy
|
namespace OpenNest.CNC.CuttingStrategy
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
using OpenNest.Geometry;
|
using OpenNest.Geometry;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace OpenNest.CNC.CuttingStrategy
|
namespace OpenNest.CNC.CuttingStrategy
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -17,6 +17,8 @@
|
|||||||
|
|
||||||
public double Value { get; set; }
|
public double Value { get; set; }
|
||||||
|
|
||||||
|
public string VariableRef { get; set; }
|
||||||
|
|
||||||
public CodeType Type
|
public CodeType Type
|
||||||
{
|
{
|
||||||
get { return CodeType.SetFeedrate; }
|
get { return CodeType.SetFeedrate; }
|
||||||
@@ -24,7 +26,7 @@
|
|||||||
|
|
||||||
public ICode Clone()
|
public ICode Clone()
|
||||||
{
|
{
|
||||||
return new Feedrate(Value);
|
return new Feedrate(Value) { VariableRef = VariableRef };
|
||||||
}
|
}
|
||||||
|
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using OpenNest.Geometry;
|
using System.Collections.Generic;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
|
||||||
namespace OpenNest.CNC
|
namespace OpenNest.CNC
|
||||||
{
|
{
|
||||||
@@ -31,7 +32,9 @@ namespace OpenNest.CNC
|
|||||||
{
|
{
|
||||||
return new LinearMove(EndPoint)
|
return new LinearMove(EndPoint)
|
||||||
{
|
{
|
||||||
Layer = Layer
|
Layer = Layer,
|
||||||
|
Suppressed = Suppressed,
|
||||||
|
VariableRefs = VariableRefs != null ? new Dictionary<string, string>(VariableRefs) : null
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using OpenNest.Geometry;
|
using System.Collections.Generic;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
|
||||||
namespace OpenNest.CNC
|
namespace OpenNest.CNC
|
||||||
{
|
{
|
||||||
@@ -12,6 +13,10 @@ namespace OpenNest.CNC
|
|||||||
|
|
||||||
public int Feedrate { get; set; }
|
public int Feedrate { get; set; }
|
||||||
|
|
||||||
|
public bool Suppressed { get; set; }
|
||||||
|
|
||||||
|
public Dictionary<string, string> VariableRefs { get; set; }
|
||||||
|
|
||||||
protected Motion()
|
protected Motion()
|
||||||
{
|
{
|
||||||
Feedrate = CNC.Feedrate.UseDefault;
|
Feedrate = CNC.Feedrate.UseDefault;
|
||||||
@@ -20,21 +25,25 @@ namespace OpenNest.CNC
|
|||||||
public virtual void Rotate(double angle)
|
public virtual void Rotate(double angle)
|
||||||
{
|
{
|
||||||
EndPoint = EndPoint.Rotate(angle);
|
EndPoint = EndPoint.Rotate(angle);
|
||||||
|
VariableRefs = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual void Rotate(double angle, Vector origin)
|
public virtual void Rotate(double angle, Vector origin)
|
||||||
{
|
{
|
||||||
EndPoint = EndPoint.Rotate(angle, origin);
|
EndPoint = EndPoint.Rotate(angle, origin);
|
||||||
|
VariableRefs = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual void Offset(double x, double y)
|
public virtual void Offset(double x, double y)
|
||||||
{
|
{
|
||||||
EndPoint = new Vector(EndPoint.X + x, EndPoint.Y + y);
|
EndPoint = new Vector(EndPoint.X + x, EndPoint.Y + y);
|
||||||
|
VariableRefs = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual void Offset(Vector voffset)
|
public virtual void Offset(Vector voffset)
|
||||||
{
|
{
|
||||||
EndPoint += voffset;
|
EndPoint += voffset;
|
||||||
|
VariableRefs = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract CodeType Type { get; }
|
public abstract CodeType Type { get; }
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using OpenNest.Converters;
|
using OpenNest.Converters;
|
||||||
using OpenNest.Geometry;
|
using OpenNest.Geometry;
|
||||||
using OpenNest.Math;
|
using OpenNest.Math;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace OpenNest.CNC
|
namespace OpenNest.CNC
|
||||||
{
|
{
|
||||||
@@ -10,6 +10,10 @@ namespace OpenNest.CNC
|
|||||||
{
|
{
|
||||||
public List<ICode> Codes;
|
public List<ICode> Codes;
|
||||||
|
|
||||||
|
public Dictionary<string, VariableDefinition> Variables { get; } = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
public Dictionary<int, Program> SubPrograms { get; } = new();
|
||||||
|
|
||||||
private Mode mode;
|
private Mode mode;
|
||||||
|
|
||||||
public Program(Mode mode = Mode.Absolute)
|
public Program(Mode mode = Mode.Absolute)
|
||||||
@@ -52,36 +56,23 @@ namespace OpenNest.CNC
|
|||||||
mode = Mode.Absolute;
|
mode = Mode.Absolute;
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual void Rotate(double angle)
|
public virtual void Rotate(double angle) => Rotate(angle, new Vector(0, 0));
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
{
|
{
|
||||||
var mode = Mode;
|
var sb = new System.Text.StringBuilder();
|
||||||
|
sb.AppendLine(mode == Mode.Absolute ? "G90" : "G91");
|
||||||
SetModeAbs();
|
foreach (var code in Codes)
|
||||||
|
|
||||||
for (int i = 0; i < Codes.Count; ++i)
|
|
||||||
{
|
{
|
||||||
var code = Codes[i];
|
if (code is Motion m)
|
||||||
|
|
||||||
if (code.Type == CodeType.SubProgramCall)
|
|
||||||
{
|
{
|
||||||
var subpgm = (SubProgramCall)code;
|
var cmd = m is RapidMove ? "G00" : (m is ArcMove am ? (am.Rotation == RotationType.CW ? "G02" : "G03") : "G01");
|
||||||
|
sb.Append($"{cmd}X{m.EndPoint.X:F4}Y{m.EndPoint.Y:F4}");
|
||||||
if (subpgm.Program != null)
|
if (m is ArcMove arc) sb.Append($"I{arc.CenterPoint.X:F4}J{arc.CenterPoint.Y:F4}");
|
||||||
subpgm.Program.Rotate(angle);
|
sb.AppendLine();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (code is Motion == false)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
var code2 = (Motion)code;
|
|
||||||
|
|
||||||
code2.Rotate(angle);
|
|
||||||
}
|
}
|
||||||
|
return sb.ToString();
|
||||||
if (mode == Mode.Incremental)
|
|
||||||
SetModeInc();
|
|
||||||
|
|
||||||
Rotation = Angle.NormalizeRad(Rotation + angle);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual void Rotate(double angle, Vector origin)
|
public virtual void Rotate(double angle, Vector origin)
|
||||||
@@ -98,8 +89,19 @@ namespace OpenNest.CNC
|
|||||||
{
|
{
|
||||||
var subpgm = (SubProgramCall)code;
|
var subpgm = (SubProgramCall)code;
|
||||||
|
|
||||||
|
if (subpgm.Offset.X != 0 || subpgm.Offset.Y != 0)
|
||||||
|
{
|
||||||
|
var cos = System.Math.Cos(angle);
|
||||||
|
var sin = System.Math.Sin(angle);
|
||||||
|
var dx = subpgm.Offset.X - origin.X;
|
||||||
|
var dy = subpgm.Offset.Y - origin.Y;
|
||||||
|
subpgm.Offset = new Geometry.Vector(
|
||||||
|
origin.X + dx * cos - dy * sin,
|
||||||
|
origin.Y + dx * sin + dy * cos);
|
||||||
|
}
|
||||||
|
|
||||||
if (subpgm.Program != null)
|
if (subpgm.Program != null)
|
||||||
subpgm.Program.Rotate(angle);
|
subpgm.Program.Rotate(angle, origin);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (code is Motion == false)
|
if (code is Motion == false)
|
||||||
@@ -126,6 +128,12 @@ namespace OpenNest.CNC
|
|||||||
{
|
{
|
||||||
var code = Codes[i];
|
var code = Codes[i];
|
||||||
|
|
||||||
|
if (code is SubProgramCall subpgm)
|
||||||
|
{
|
||||||
|
subpgm.Offset = new Geometry.Vector(
|
||||||
|
subpgm.Offset.X + x, subpgm.Offset.Y + y);
|
||||||
|
}
|
||||||
|
|
||||||
if (code is Motion == false)
|
if (code is Motion == false)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
@@ -148,6 +156,12 @@ namespace OpenNest.CNC
|
|||||||
{
|
{
|
||||||
var code = Codes[i];
|
var code = Codes[i];
|
||||||
|
|
||||||
|
if (code is SubProgramCall subpgm)
|
||||||
|
{
|
||||||
|
subpgm.Offset = new Geometry.Vector(
|
||||||
|
subpgm.Offset.X + voffset.X, subpgm.Offset.Y + voffset.Y);
|
||||||
|
}
|
||||||
|
|
||||||
if (code is Motion == false)
|
if (code is Motion == false)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
@@ -286,6 +300,10 @@ namespace OpenNest.CNC
|
|||||||
|
|
||||||
private Box BoundingBox(ref Vector pos)
|
private Box BoundingBox(ref Vector pos)
|
||||||
{
|
{
|
||||||
|
// Capture the frame origin at entry. Sub-program Offsets and
|
||||||
|
// absolute-mode endpoints are relative to this fixed origin.
|
||||||
|
var frameOrigin = pos;
|
||||||
|
|
||||||
double minX = 0.0;
|
double minX = 0.0;
|
||||||
double minY = 0.0;
|
double minY = 0.0;
|
||||||
double maxX = 0.0;
|
double maxX = 0.0;
|
||||||
@@ -301,7 +319,7 @@ namespace OpenNest.CNC
|
|||||||
{
|
{
|
||||||
var line = (LinearMove)code;
|
var line = (LinearMove)code;
|
||||||
var pt = Mode == Mode.Absolute ?
|
var pt = Mode == Mode.Absolute ?
|
||||||
line.EndPoint :
|
frameOrigin + line.EndPoint :
|
||||||
line.EndPoint + pos;
|
line.EndPoint + pos;
|
||||||
|
|
||||||
if (pt.X > maxX)
|
if (pt.X > maxX)
|
||||||
@@ -323,7 +341,7 @@ namespace OpenNest.CNC
|
|||||||
{
|
{
|
||||||
var line = (RapidMove)code;
|
var line = (RapidMove)code;
|
||||||
var pt = Mode == Mode.Absolute
|
var pt = Mode == Mode.Absolute
|
||||||
? line.EndPoint
|
? frameOrigin + line.EndPoint
|
||||||
: line.EndPoint + pos;
|
: line.EndPoint + pos;
|
||||||
|
|
||||||
if (pt.X > maxX)
|
if (pt.X > maxX)
|
||||||
@@ -356,8 +374,8 @@ namespace OpenNest.CNC
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
endpt = arc.EndPoint;
|
endpt = frameOrigin + arc.EndPoint;
|
||||||
centerpt = arc.CenterPoint;
|
centerpt = frameOrigin + arc.CenterPoint;
|
||||||
}
|
}
|
||||||
|
|
||||||
double minX1;
|
double minX1;
|
||||||
@@ -431,6 +449,12 @@ namespace OpenNest.CNC
|
|||||||
case CodeType.SubProgramCall:
|
case CodeType.SubProgramCall:
|
||||||
{
|
{
|
||||||
var subpgm = (SubProgramCall)code;
|
var subpgm = (SubProgramCall)code;
|
||||||
|
if (subpgm.Program == null)
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Sub-program frame origin in this program's frame
|
||||||
|
// is frameOrigin + Offset, regardless of current pos.
|
||||||
|
pos = frameOrigin + subpgm.Offset;
|
||||||
var box = subpgm.Program.BoundingBox(ref pos);
|
var box = subpgm.Program.BoundingBox(ref pos);
|
||||||
|
|
||||||
if (box.Left < minX)
|
if (box.Left < minX)
|
||||||
@@ -468,6 +492,12 @@ namespace OpenNest.CNC
|
|||||||
|
|
||||||
pgm.Codes.AddRange(codes);
|
pgm.Codes.AddRange(codes);
|
||||||
|
|
||||||
|
foreach (var kvp in Variables)
|
||||||
|
pgm.Variables[kvp.Key] = kvp.Value;
|
||||||
|
|
||||||
|
foreach (var kvp in SubPrograms)
|
||||||
|
pgm.SubPrograms[kvp.Key] = (Program)kvp.Value.Clone();
|
||||||
|
|
||||||
return pgm;
|
return pgm;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
namespace OpenNest.CNC
|
||||||
|
{
|
||||||
|
public sealed class ProgramVariable
|
||||||
|
{
|
||||||
|
public int Number { get; }
|
||||||
|
public string Name { get; }
|
||||||
|
public string Expression { get; set; }
|
||||||
|
|
||||||
|
public ProgramVariable(int number, string name, string expression = null)
|
||||||
|
{
|
||||||
|
Number = number;
|
||||||
|
Name = name;
|
||||||
|
Expression = expression;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Reference => $"#{Number}";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace OpenNest.CNC
|
||||||
|
{
|
||||||
|
public sealed class ProgramVariableManager
|
||||||
|
{
|
||||||
|
private readonly Dictionary<int, ProgramVariable> _variables = new();
|
||||||
|
|
||||||
|
public ProgramVariable GetOrCreate(string name, int number, string expression = null)
|
||||||
|
{
|
||||||
|
if (_variables.TryGetValue(number, out var existing))
|
||||||
|
return existing;
|
||||||
|
|
||||||
|
var variable = new ProgramVariable(number, name, expression);
|
||||||
|
_variables[number] = variable;
|
||||||
|
return variable;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<string> EmitDeclarations()
|
||||||
|
{
|
||||||
|
return _variables.Values
|
||||||
|
.Where(v => v.Expression != null)
|
||||||
|
.OrderBy(v => v.Number)
|
||||||
|
.Select(v => $"{v.Reference}={v.Expression} ({FormatComment(v.Name)})")
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string FormatComment(string name)
|
||||||
|
{
|
||||||
|
// "LeadInFeedrate" -> "LEAD IN FEEDRATE"
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
foreach (var c in name)
|
||||||
|
{
|
||||||
|
if (char.IsUpper(c) && sb.Length > 0)
|
||||||
|
sb.Append(' ');
|
||||||
|
sb.Append(char.ToUpper(c));
|
||||||
|
}
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using OpenNest.Geometry;
|
using System.Collections.Generic;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
|
||||||
namespace OpenNest.CNC
|
namespace OpenNest.CNC
|
||||||
{
|
{
|
||||||
@@ -26,7 +27,11 @@ namespace OpenNest.CNC
|
|||||||
|
|
||||||
public override ICode Clone()
|
public override ICode Clone()
|
||||||
{
|
{
|
||||||
return new RapidMove(EndPoint);
|
return new RapidMove(EndPoint)
|
||||||
|
{
|
||||||
|
Suppressed = Suppressed,
|
||||||
|
VariableRefs = VariableRefs != null ? new Dictionary<string, string>(VariableRefs) : null
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
using OpenNest.Math;
|
using System.Text;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
using OpenNest.Math;
|
||||||
|
|
||||||
namespace OpenNest.CNC
|
namespace OpenNest.CNC
|
||||||
{
|
{
|
||||||
@@ -35,6 +37,12 @@ namespace OpenNest.CNC
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the offset (position) at which the sub-program is executed.
|
||||||
|
/// For hole sub-programs, this is the hole center.
|
||||||
|
/// </summary>
|
||||||
|
public Vector Offset { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the rotation of the program in degrees.
|
/// Gets or sets the rotation of the program in degrees.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -78,12 +86,18 @@ namespace OpenNest.CNC
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public ICode Clone()
|
public ICode Clone()
|
||||||
{
|
{
|
||||||
return new SubProgramCall(program, Rotation);
|
return new SubProgramCall(program, Rotation) { Id = Id, Offset = Offset };
|
||||||
}
|
}
|
||||||
|
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
return string.Format("G65 P{0} R{1}", Id, Rotation);
|
var sb = new StringBuilder();
|
||||||
|
sb.Append($"G65 P{Id}");
|
||||||
|
if (Offset.X != 0 || Offset.Y != 0)
|
||||||
|
sb.Append($" X{Offset.X} Y{Offset.Y}");
|
||||||
|
if (Rotation != 0)
|
||||||
|
sb.Append($" R{Rotation}");
|
||||||
|
return sb.ToString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
namespace OpenNest.CNC
|
||||||
|
{
|
||||||
|
public sealed class VariableDefinition
|
||||||
|
{
|
||||||
|
public string Name { get; }
|
||||||
|
public string Expression { get; }
|
||||||
|
public double Value { get; }
|
||||||
|
public bool Inline { get; }
|
||||||
|
public bool Global { get; }
|
||||||
|
|
||||||
|
public VariableDefinition(string name, string expression, double value,
|
||||||
|
bool inline = false, bool global = false)
|
||||||
|
{
|
||||||
|
Name = name;
|
||||||
|
Expression = expression;
|
||||||
|
Value = value;
|
||||||
|
Inline = inline;
|
||||||
|
Global = global;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -46,7 +46,8 @@ namespace OpenNest.Collections
|
|||||||
public bool Remove(T item)
|
public bool Remove(T item)
|
||||||
{
|
{
|
||||||
var success = items.Remove(item);
|
var success = items.Remove(item);
|
||||||
ItemRemoved?.Invoke(this, new ItemRemovedEventArgs<T>(item, success));
|
if (success)
|
||||||
|
ItemRemoved?.Invoke(this, new ItemRemovedEventArgs<T>(item, success));
|
||||||
return success;
|
return success;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,137 @@
|
|||||||
|
using OpenNest.Geometry;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace OpenNest.Converters
|
||||||
|
{
|
||||||
|
public enum ContourClassification
|
||||||
|
{
|
||||||
|
Perimeter,
|
||||||
|
Hole,
|
||||||
|
Etch,
|
||||||
|
Open
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class ContourInfo
|
||||||
|
{
|
||||||
|
public Shape Shape { get; }
|
||||||
|
public ContourClassification Type { get; private set; }
|
||||||
|
public string Label { get; private set; }
|
||||||
|
|
||||||
|
private ContourInfo(Shape shape, ContourClassification type, string label)
|
||||||
|
{
|
||||||
|
Shape = shape;
|
||||||
|
Type = type;
|
||||||
|
Label = label;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string DirectionLabel
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (Type == ContourClassification.Open || Type == ContourClassification.Etch)
|
||||||
|
return "Open";
|
||||||
|
var poly = Shape.ToPolygon();
|
||||||
|
if (poly == null || poly.Vertices.Count < 3)
|
||||||
|
return "?";
|
||||||
|
return poly.RotationDirection() == RotationType.CW ? "CW" : "CCW";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string DimensionLabel
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (Shape.Entities.Count == 1 && Shape.Entities[0] is Circle c)
|
||||||
|
return $"Circle R{c.Radius:0.#}";
|
||||||
|
Shape.UpdateBounds();
|
||||||
|
var box = Shape.BoundingBox;
|
||||||
|
return $"{box.Width:0.#} x {box.Length:0.#}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Reverse()
|
||||||
|
{
|
||||||
|
Shape.Reverse();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetLabel(string label)
|
||||||
|
{
|
||||||
|
Label = label;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<ContourInfo> Classify(List<Shape> shapes)
|
||||||
|
{
|
||||||
|
if (shapes.Count == 0)
|
||||||
|
return new List<ContourInfo>();
|
||||||
|
|
||||||
|
// Ensure bounding boxes are up to date before comparing
|
||||||
|
foreach (var s in shapes)
|
||||||
|
s.UpdateBounds();
|
||||||
|
|
||||||
|
// Find perimeter — largest bounding box area
|
||||||
|
var perimeterIndex = 0;
|
||||||
|
var maxArea = shapes[0].BoundingBox.Area();
|
||||||
|
for (var i = 1; i < shapes.Count; i++)
|
||||||
|
{
|
||||||
|
var area = shapes[i].BoundingBox.Area();
|
||||||
|
if (area > maxArea)
|
||||||
|
{
|
||||||
|
maxArea = area;
|
||||||
|
perimeterIndex = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = new List<ContourInfo>();
|
||||||
|
var holeCount = 0;
|
||||||
|
var etchCount = 0;
|
||||||
|
var openCount = 0;
|
||||||
|
|
||||||
|
// Non-perimeter shapes first (matches CNC cut order: holes before perimeter)
|
||||||
|
for (var i = 0; i < shapes.Count; i++)
|
||||||
|
{
|
||||||
|
if (i == perimeterIndex) continue;
|
||||||
|
var shape = shapes[i];
|
||||||
|
var type = ClassifyShape(shape);
|
||||||
|
|
||||||
|
string label;
|
||||||
|
switch (type)
|
||||||
|
{
|
||||||
|
case ContourClassification.Hole:
|
||||||
|
holeCount++;
|
||||||
|
label = $"Hole {holeCount}";
|
||||||
|
break;
|
||||||
|
case ContourClassification.Etch:
|
||||||
|
etchCount++;
|
||||||
|
label = etchCount == 1 ? "Etch" : $"Etch {etchCount}";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
openCount++;
|
||||||
|
label = openCount == 1 ? "Open" : $"Open {openCount}";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Add(new ContourInfo(shape, type, label));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perimeter last
|
||||||
|
result.Add(new ContourInfo(shapes[perimeterIndex], ContourClassification.Perimeter, "Perimeter"));
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ContourClassification ClassifyShape(Shape shape)
|
||||||
|
{
|
||||||
|
// Check etch layer — all entities must be on ETCH layer
|
||||||
|
if (shape.Entities.Count > 0 &&
|
||||||
|
shape.Entities.All(e => string.Equals(e.Layer?.Name, "ETCH", StringComparison.OrdinalIgnoreCase)))
|
||||||
|
return ContourClassification.Etch;
|
||||||
|
|
||||||
|
if (shape.IsClosed())
|
||||||
|
return ContourClassification.Hole;
|
||||||
|
|
||||||
|
return ContourClassification.Open;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
using System.Collections.Generic;
|
using OpenNest.CNC;
|
||||||
using OpenNest;
|
|
||||||
using OpenNest.CNC;
|
|
||||||
using OpenNest.Geometry;
|
using OpenNest.Geometry;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace OpenNest.Converters
|
namespace OpenNest.Converters
|
||||||
{
|
{
|
||||||
@@ -9,7 +8,7 @@ namespace OpenNest.Converters
|
|||||||
{
|
{
|
||||||
public static Program ToProgram(IList<Entity> geometry)
|
public static Program ToProgram(IList<Entity> geometry)
|
||||||
{
|
{
|
||||||
var shapes = Helper.GetShapes(geometry);
|
var shapes = ShapeBuilder.GetShapes(geometry);
|
||||||
|
|
||||||
if (shapes.Count == 0)
|
if (shapes.Count == 0)
|
||||||
return null;
|
return null;
|
||||||
@@ -98,7 +97,7 @@ namespace OpenNest.Converters
|
|||||||
if (startpt != lastpt)
|
if (startpt != lastpt)
|
||||||
pgm.MoveTo(startpt);
|
pgm.MoveTo(startpt);
|
||||||
|
|
||||||
pgm.ArcTo(startpt, circle.Center, RotationType.CCW);
|
pgm.ArcTo(startpt, circle.Center, circle.Rotation);
|
||||||
|
|
||||||
lastpt = startpt;
|
lastpt = startpt;
|
||||||
return lastpt;
|
return lastpt;
|
||||||
@@ -109,7 +108,10 @@ namespace OpenNest.Converters
|
|||||||
if (line.StartPoint != lastpt)
|
if (line.StartPoint != lastpt)
|
||||||
pgm.MoveTo(line.StartPoint);
|
pgm.MoveTo(line.StartPoint);
|
||||||
|
|
||||||
pgm.LineTo(line.EndPoint);
|
var move = new LinearMove(line.EndPoint);
|
||||||
|
if (string.Equals(line.Layer?.Name, "ETCH", System.StringComparison.OrdinalIgnoreCase))
|
||||||
|
move.Layer = LayerType.Scribe;
|
||||||
|
pgm.Codes.Add(move);
|
||||||
|
|
||||||
lastpt = line.EndPoint;
|
lastpt = line.EndPoint;
|
||||||
return lastpt;
|
return lastpt;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using OpenNest.CNC;
|
using OpenNest.CNC;
|
||||||
using OpenNest.Geometry;
|
using OpenNest.Geometry;
|
||||||
|
|
||||||
namespace OpenNest.Converters
|
namespace OpenNest.Converters
|
||||||
@@ -9,7 +9,6 @@ namespace OpenNest.Converters
|
|||||||
/// Converts the program to absolute coordinates.
|
/// Converts the program to absolute coordinates.
|
||||||
/// Does NOT check program mode before converting.
|
/// Does NOT check program mode before converting.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="pgm"></param>
|
|
||||||
public static void ToAbsolute(Program pgm)
|
public static void ToAbsolute(Program pgm)
|
||||||
{
|
{
|
||||||
var pos = new Vector(0, 0);
|
var pos = new Vector(0, 0);
|
||||||
@@ -17,21 +16,27 @@ namespace OpenNest.Converters
|
|||||||
for (int i = 0; i < pgm.Codes.Count; ++i)
|
for (int i = 0; i < pgm.Codes.Count; ++i)
|
||||||
{
|
{
|
||||||
var code = pgm.Codes[i];
|
var code = pgm.Codes[i];
|
||||||
var motion = code as Motion;
|
|
||||||
|
|
||||||
if (motion != null)
|
if (code is SubProgramCall subCall && subCall.Program != null)
|
||||||
{
|
{
|
||||||
motion.Offset(pos);
|
// Sub-program is placed at Offset in this program's frame.
|
||||||
|
// After it runs, the tool is at Offset + (sub's end in its own frame).
|
||||||
|
pos = ComputeEndPosition(subCall.Program, subCall.Offset);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (code is Motion motion)
|
||||||
|
{
|
||||||
|
motion.Offset(pos.X, pos.Y);
|
||||||
pos = motion.EndPoint;
|
pos = motion.EndPoint;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Converts the program to intermental coordinates.
|
/// Converts the program to incremental coordinates.
|
||||||
/// Does NOT check program mode before converting.
|
/// Does NOT check program mode before converting.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="pgm"></param>
|
|
||||||
public static void ToIncremental(Program pgm)
|
public static void ToIncremental(Program pgm)
|
||||||
{
|
{
|
||||||
var pos = new Vector(0, 0);
|
var pos = new Vector(0, 0);
|
||||||
@@ -39,9 +44,16 @@ namespace OpenNest.Converters
|
|||||||
for (int i = 0; i < pgm.Codes.Count; ++i)
|
for (int i = 0; i < pgm.Codes.Count; ++i)
|
||||||
{
|
{
|
||||||
var code = pgm.Codes[i];
|
var code = pgm.Codes[i];
|
||||||
var motion = code as Motion;
|
|
||||||
|
|
||||||
if (motion != null)
|
if (code is SubProgramCall subCall && subCall.Program != null)
|
||||||
|
{
|
||||||
|
// Sub-program is placed at Offset in this program's frame,
|
||||||
|
// regardless of where the tool was before the call.
|
||||||
|
pos = ComputeEndPosition(subCall.Program, subCall.Offset);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (code is Motion motion)
|
||||||
{
|
{
|
||||||
var pos2 = motion.EndPoint;
|
var pos2 = motion.EndPoint;
|
||||||
motion.Offset(-pos.X, -pos.Y);
|
motion.Offset(-pos.X, -pos.Y);
|
||||||
@@ -49,5 +61,37 @@ namespace OpenNest.Converters
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Computes the tool position after executing <paramref name="pgm"/>,
|
||||||
|
/// given that the program's frame origin is at <paramref name="startPos"/>
|
||||||
|
/// in the caller's frame. Walks nested sub-program calls recursively.
|
||||||
|
/// </summary>
|
||||||
|
private static Vector ComputeEndPosition(Program pgm, Vector startPos)
|
||||||
|
{
|
||||||
|
var pos = startPos;
|
||||||
|
|
||||||
|
for (int i = 0; i < pgm.Codes.Count; ++i)
|
||||||
|
{
|
||||||
|
var code = pgm.Codes[i];
|
||||||
|
|
||||||
|
if (code is SubProgramCall subCall && subCall.Program != null)
|
||||||
|
{
|
||||||
|
// Nested sub's frame origin in the caller's frame is startPos + Offset.
|
||||||
|
pos = ComputeEndPosition(subCall.Program, startPos + subCall.Offset);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (code is Motion motion)
|
||||||
|
{
|
||||||
|
if (pgm.Mode == Mode.Incremental)
|
||||||
|
pos = pos + motion.EndPoint;
|
||||||
|
else
|
||||||
|
pos = startPos + motion.EndPoint;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return pos;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
using System;
|
using OpenNest.CNC;
|
||||||
using System.Collections.Generic;
|
|
||||||
using OpenNest;
|
|
||||||
using OpenNest.CNC;
|
|
||||||
using OpenNest.Geometry;
|
using OpenNest.Geometry;
|
||||||
using OpenNest.Math;
|
using OpenNest.Math;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace OpenNest.Converters
|
namespace OpenNest.Converters
|
||||||
{
|
{
|
||||||
@@ -22,6 +20,9 @@ namespace OpenNest.Converters
|
|||||||
|
|
||||||
private static void AddProgram(Program program, ref Mode mode, ref Vector curpos, ref List<Entity> geometry)
|
private static void AddProgram(Program program, ref Mode mode, ref Vector curpos, ref List<Entity> geometry)
|
||||||
{
|
{
|
||||||
|
// Capture the frame origin at entry. Sub-program Offsets are relative
|
||||||
|
// to this fixed origin, not to the current tool position.
|
||||||
|
var frameOrigin = curpos;
|
||||||
mode = program.Mode;
|
mode = program.Mode;
|
||||||
|
|
||||||
for (int i = 0; i < program.Length; ++i)
|
for (int i = 0; i < program.Length; ++i)
|
||||||
@@ -43,12 +44,15 @@ namespace OpenNest.Converters
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case CodeType.SubProgramCall:
|
case CodeType.SubProgramCall:
|
||||||
var tmpmode = mode;
|
|
||||||
var subpgm = (SubProgramCall)code;
|
var subpgm = (SubProgramCall)code;
|
||||||
var geoProgram = new Shape();
|
var savedMode = mode;
|
||||||
AddProgram(subpgm.Program, ref mode, ref curpos, ref geoProgram.Entities);
|
|
||||||
geometry.Add(geoProgram);
|
// The sub-program's frame origin in this program's frame is
|
||||||
mode = tmpmode;
|
// frameOrigin + Offset — independent of current tool position.
|
||||||
|
curpos = new Vector(frameOrigin.X + subpgm.Offset.X, frameOrigin.Y + subpgm.Offset.Y);
|
||||||
|
|
||||||
|
AddProgram(subpgm.Program, ref mode, ref curpos, ref geometry);
|
||||||
|
mode = savedMode;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -61,9 +65,11 @@ namespace OpenNest.Converters
|
|||||||
if (mode == Mode.Incremental)
|
if (mode == Mode.Incremental)
|
||||||
pt += curpos;
|
pt += curpos;
|
||||||
|
|
||||||
|
var layer = ConvertLayer(linearMove.Layer);
|
||||||
var line = new Line(curpos, pt)
|
var line = new Line(curpos, pt)
|
||||||
{
|
{
|
||||||
Layer = ConvertLayer(linearMove.Layer)
|
Layer = layer,
|
||||||
|
Color = layer.Color
|
||||||
};
|
};
|
||||||
geometry.Add(line);
|
geometry.Add(line);
|
||||||
curpos = pt;
|
curpos = pt;
|
||||||
@@ -78,7 +84,8 @@ namespace OpenNest.Converters
|
|||||||
|
|
||||||
var line = new Line(curpos, pt)
|
var line = new Line(curpos, pt)
|
||||||
{
|
{
|
||||||
Layer = SpecialLayers.Rapid
|
Layer = SpecialLayers.Rapid,
|
||||||
|
Color = SpecialLayers.Rapid.Color
|
||||||
};
|
};
|
||||||
geometry.Add(line);
|
geometry.Add(line);
|
||||||
curpos = pt;
|
curpos = pt;
|
||||||
@@ -105,9 +112,9 @@ namespace OpenNest.Converters
|
|||||||
var layer = ConvertLayer(arcMove.Layer);
|
var layer = ConvertLayer(arcMove.Layer);
|
||||||
|
|
||||||
if (startAngle.IsEqualTo(endAngle))
|
if (startAngle.IsEqualTo(endAngle))
|
||||||
geometry.Add(new Circle(center, radius) { Layer = layer });
|
geometry.Add(new Circle(center, radius) { Layer = layer, Color = layer.Color, Rotation = arcMove.Rotation });
|
||||||
else
|
else
|
||||||
geometry.Add(new Arc(center, radius, startAngle, endAngle, arcMove.Rotation == RotationType.CW) { Layer = layer });
|
geometry.Add(new Arc(center, radius, startAngle, endAngle, arcMove.Rotation == RotationType.CW) { Layer = layer, Color = layer.Color });
|
||||||
|
|
||||||
curpos = endpt;
|
curpos = endpt;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,211 @@
|
|||||||
|
using OpenNest.CNC;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace OpenNest
|
||||||
|
{
|
||||||
|
public enum CutOffAxis
|
||||||
|
{
|
||||||
|
Horizontal,
|
||||||
|
Vertical
|
||||||
|
}
|
||||||
|
|
||||||
|
public class CutOff
|
||||||
|
{
|
||||||
|
public Vector Position { get; set; }
|
||||||
|
public CutOffAxis Axis { get; set; }
|
||||||
|
public double? StartLimit { get; set; }
|
||||||
|
public double? EndLimit { get; set; }
|
||||||
|
public Drawing Drawing { get; private set; }
|
||||||
|
|
||||||
|
public CutOff(Vector position, CutOffAxis axis)
|
||||||
|
{
|
||||||
|
Position = position;
|
||||||
|
Axis = axis;
|
||||||
|
Drawing = new Drawing(GetName()) { IsCutOff = true };
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Regenerate(Plate plate, CutOffSettings settings, Dictionary<Part, Entity> cache = null)
|
||||||
|
{
|
||||||
|
var segments = ComputeSegments(plate, settings, cache);
|
||||||
|
var program = BuildProgram(segments, settings);
|
||||||
|
Drawing.Program = program;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetName()
|
||||||
|
{
|
||||||
|
var axisChar = Axis == CutOffAxis.Vertical ? "V" : "H";
|
||||||
|
var coord = Axis == CutOffAxis.Vertical ? Position.X : Position.Y;
|
||||||
|
return $"CutOff-{axisChar}-{coord:F2}";
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<(double Start, double End)> ComputeSegments(Plate plate, CutOffSettings settings, Dictionary<Part, Entity> cache)
|
||||||
|
{
|
||||||
|
var bounds = plate.BoundingBox(includeParts: false);
|
||||||
|
|
||||||
|
double lineStart, lineEnd, cutPosition;
|
||||||
|
|
||||||
|
if (Axis == CutOffAxis.Vertical)
|
||||||
|
{
|
||||||
|
cutPosition = Position.X;
|
||||||
|
lineStart = StartLimit ?? bounds.Y;
|
||||||
|
lineEnd = EndLimit ?? (bounds.Y + bounds.Width + settings.Overtravel);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
cutPosition = Position.Y;
|
||||||
|
lineStart = StartLimit ?? bounds.X;
|
||||||
|
lineEnd = EndLimit ?? (bounds.X + bounds.Length + settings.Overtravel);
|
||||||
|
}
|
||||||
|
|
||||||
|
var exclusions = new List<(double Start, double End)>();
|
||||||
|
|
||||||
|
foreach (var part in plate.Parts)
|
||||||
|
{
|
||||||
|
if (part.BaseDrawing.IsCutOff)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
Entity perimeter = null;
|
||||||
|
cache?.TryGetValue(part, out perimeter);
|
||||||
|
var partExclusions = GetPartExclusions(part, perimeter, cutPosition, lineStart, lineEnd, settings.PartClearance);
|
||||||
|
exclusions.AddRange(partExclusions);
|
||||||
|
}
|
||||||
|
|
||||||
|
exclusions.Sort((a, b) => a.Start.CompareTo(b.Start));
|
||||||
|
var merged = new List<(double Start, double End)>();
|
||||||
|
foreach (var ex in exclusions)
|
||||||
|
{
|
||||||
|
if (merged.Count > 0 && ex.Start <= merged[^1].End)
|
||||||
|
merged[^1] = (merged[^1].Start, System.Math.Max(merged[^1].End, ex.End));
|
||||||
|
else
|
||||||
|
merged.Add(ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
var segments = new List<(double Start, double End)>();
|
||||||
|
var current = lineStart;
|
||||||
|
|
||||||
|
foreach (var ex in merged)
|
||||||
|
{
|
||||||
|
var clampedStart = System.Math.Max(ex.Start, lineStart);
|
||||||
|
var clampedEnd = System.Math.Min(ex.End, lineEnd);
|
||||||
|
|
||||||
|
if (clampedStart > current)
|
||||||
|
segments.Add((current, clampedStart));
|
||||||
|
|
||||||
|
current = System.Math.Max(current, clampedEnd);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current < lineEnd)
|
||||||
|
segments.Add((current, lineEnd));
|
||||||
|
|
||||||
|
segments = segments.Where(s => (s.End - s.Start) >= settings.MinSegmentLength).ToList();
|
||||||
|
|
||||||
|
return segments;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static readonly List<(double Start, double End)> EmptyExclusions = new();
|
||||||
|
|
||||||
|
private List<(double Start, double End)> GetPartExclusions(
|
||||||
|
Part part, Entity perimeter, double cutPosition, double lineStart, double lineEnd, double clearance)
|
||||||
|
{
|
||||||
|
var bb = part.BoundingBox;
|
||||||
|
var (partMin, partMax) = AxisBounds(bb, clearance);
|
||||||
|
var (partStart, partEnd) = CrossAxisBounds(bb, clearance);
|
||||||
|
|
||||||
|
if (cutPosition < partMin || cutPosition > partMax)
|
||||||
|
return EmptyExclusions;
|
||||||
|
|
||||||
|
if (perimeter != null)
|
||||||
|
{
|
||||||
|
var perimeterExclusions = IntersectPerimeter(perimeter, cutPosition, lineStart, lineEnd, clearance);
|
||||||
|
if (perimeterExclusions != null)
|
||||||
|
return perimeterExclusions;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new List<(double Start, double End)> { (partStart, partEnd) };
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<(double Start, double End)> IntersectPerimeter(
|
||||||
|
Entity perimeter, double cutPosition, double lineStart, double lineEnd, double clearance)
|
||||||
|
{
|
||||||
|
var target = OffsetOutward(perimeter, clearance) ?? perimeter;
|
||||||
|
var usedOffset = target != perimeter;
|
||||||
|
var cutLine = new Line(MakePoint(cutPosition, lineStart), MakePoint(cutPosition, lineEnd));
|
||||||
|
|
||||||
|
if (!target.Intersects(cutLine, out var pts) || pts.Count < 2)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var coords = pts
|
||||||
|
.Select(pt => Axis == CutOffAxis.Vertical ? pt.Y : pt.X)
|
||||||
|
.OrderBy(c => c)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (coords.Count % 2 != 0)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var padding = usedOffset ? 0 : clearance;
|
||||||
|
var result = new List<(double Start, double End)>();
|
||||||
|
for (var i = 0; i < coords.Count; i += 2)
|
||||||
|
result.Add((coords[i] - padding, coords[i + 1] + padding));
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Entity OffsetOutward(Entity perimeter, double clearance)
|
||||||
|
{
|
||||||
|
if (clearance <= 0)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var offset = perimeter.OffsetEntity(clearance, OffsetSide.Left);
|
||||||
|
offset?.UpdateBounds();
|
||||||
|
return offset;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Vector MakePoint(double cutCoord, double lineCoord) =>
|
||||||
|
Axis == CutOffAxis.Vertical
|
||||||
|
? new Vector(cutCoord, lineCoord)
|
||||||
|
: new Vector(lineCoord, cutCoord);
|
||||||
|
|
||||||
|
private (double Min, double Max) AxisBounds(Box bb, double clearance) =>
|
||||||
|
Axis == CutOffAxis.Vertical
|
||||||
|
? (bb.X - clearance, bb.X + bb.Length + clearance)
|
||||||
|
: (bb.Y - clearance, bb.Y + bb.Width + clearance);
|
||||||
|
|
||||||
|
private (double Start, double End) CrossAxisBounds(Box bb, double clearance) =>
|
||||||
|
Axis == CutOffAxis.Vertical
|
||||||
|
? (bb.Y - clearance, bb.Y + bb.Width + clearance)
|
||||||
|
: (bb.X - clearance, bb.X + bb.Length + clearance);
|
||||||
|
|
||||||
|
private Program BuildProgram(List<(double Start, double End)> segments, CutOffSettings settings)
|
||||||
|
{
|
||||||
|
var program = new Program();
|
||||||
|
|
||||||
|
if (segments.Count == 0)
|
||||||
|
return program;
|
||||||
|
|
||||||
|
var toward = settings.CutDirection == CutDirection.TowardOrigin;
|
||||||
|
segments = toward
|
||||||
|
? segments.OrderByDescending(s => s.Start).ToList()
|
||||||
|
: segments.OrderBy(s => s.Start).ToList();
|
||||||
|
|
||||||
|
var cutPos = Axis == CutOffAxis.Vertical ? Position.X : Position.Y;
|
||||||
|
|
||||||
|
foreach (var seg in segments)
|
||||||
|
{
|
||||||
|
var (from, to) = toward ? (seg.End, seg.Start) : (seg.Start, seg.End);
|
||||||
|
program.Codes.Add(new RapidMove(MakePoint(cutPos, from)));
|
||||||
|
program.Codes.Add(new LinearMove(MakePoint(cutPos, to)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return program;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
namespace OpenNest
|
||||||
|
{
|
||||||
|
public enum CutDirection
|
||||||
|
{
|
||||||
|
TowardOrigin,
|
||||||
|
AwayFromOrigin
|
||||||
|
}
|
||||||
|
|
||||||
|
public class CutOffSettings
|
||||||
|
{
|
||||||
|
public double PartClearance { get; set; } = 0.02;
|
||||||
|
public double Overtravel { get; set; }
|
||||||
|
public double MinSegmentLength { get; set; } = 0.05;
|
||||||
|
public CutDirection CutDirection { get; set; } = CutDirection.AwayFromOrigin;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,15 +1,21 @@
|
|||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace OpenNest
|
namespace OpenNest.Api;
|
||||||
|
|
||||||
|
public class CutParameters
|
||||||
{
|
{
|
||||||
public class CutParameters
|
public double Feedrate { get; set; }
|
||||||
|
public double RapidTravelRate { get; set; }
|
||||||
|
public TimeSpan PierceTime { get; set; }
|
||||||
|
public double LeadInLength { get; set; }
|
||||||
|
public string PostProcessor { get; set; }
|
||||||
|
public Units Units { get; set; }
|
||||||
|
|
||||||
|
public static CutParameters Default => new()
|
||||||
{
|
{
|
||||||
public double Feedrate { get; set; }
|
Feedrate = 100,
|
||||||
|
RapidTravelRate = 300,
|
||||||
public double RapidTravelRate { get; set; }
|
PierceTime = TimeSpan.FromSeconds(0.5),
|
||||||
|
Units = OpenNest.Units.Inches
|
||||||
public TimeSpan PierceTime { get; set; }
|
};
|
||||||
|
|
||||||
public Units Units { get; set; }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,44 @@
|
|||||||
using System.Drawing;
|
using OpenNest.Bending;
|
||||||
using System.Linq;
|
|
||||||
using System.Threading;
|
|
||||||
using OpenNest.CNC;
|
using OpenNest.CNC;
|
||||||
using OpenNest.Converters;
|
using OpenNest.Converters;
|
||||||
using OpenNest.Geometry;
|
using OpenNest.Geometry;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Drawing;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
namespace OpenNest
|
namespace OpenNest
|
||||||
{
|
{
|
||||||
public class Drawing
|
public class Drawing
|
||||||
{
|
{
|
||||||
private static int nextId;
|
private static int nextId;
|
||||||
|
private static int nextColorIndex;
|
||||||
private Program program;
|
private Program program;
|
||||||
|
|
||||||
|
public static readonly Color[] PartColors = new Color[]
|
||||||
|
{
|
||||||
|
Color.FromArgb(205, 92, 92), // Indian Red
|
||||||
|
Color.FromArgb(148, 103, 189), // Medium Purple
|
||||||
|
Color.FromArgb(75, 180, 175), // Teal
|
||||||
|
Color.FromArgb(210, 190, 75), // Goldenrod
|
||||||
|
Color.FromArgb(190, 85, 175), // Orchid
|
||||||
|
Color.FromArgb(185, 115, 85), // Sienna
|
||||||
|
Color.FromArgb(120, 100, 190), // Slate Blue
|
||||||
|
Color.FromArgb(200, 100, 140), // Rose
|
||||||
|
Color.FromArgb(80, 175, 155), // Sea Green
|
||||||
|
Color.FromArgb(195, 160, 85), // Dark Khaki
|
||||||
|
Color.FromArgb(175, 95, 160), // Plum
|
||||||
|
Color.FromArgb(215, 130, 130), // Light Coral
|
||||||
|
};
|
||||||
|
|
||||||
|
public static Color GetNextColor()
|
||||||
|
{
|
||||||
|
var color = PartColors[nextColorIndex % PartColors.Length];
|
||||||
|
nextColorIndex++;
|
||||||
|
return color;
|
||||||
|
}
|
||||||
|
|
||||||
public Drawing()
|
public Drawing()
|
||||||
: this(string.Empty, new Program())
|
: this(string.Empty, new Program())
|
||||||
{
|
{
|
||||||
@@ -56,16 +83,32 @@ namespace OpenNest
|
|||||||
|
|
||||||
public Color Color { get; set; }
|
public Color Color { get; set; }
|
||||||
|
|
||||||
|
public bool IsCutOff { get; set; }
|
||||||
|
|
||||||
public NestConstraints Constraints { get; set; }
|
public NestConstraints Constraints { get; set; }
|
||||||
|
|
||||||
public SourceInfo Source { get; set; }
|
public SourceInfo Source { get; set; }
|
||||||
|
|
||||||
|
public List<Bend> Bends { get; set; } = new List<Bend>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Complete set of source entities with stable GUIDs.
|
||||||
|
/// Null when the drawing was created from G-code or an older nest file.
|
||||||
|
/// </summary>
|
||||||
|
public List<Entity> SourceEntities { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// IDs of entities in <see cref="SourceEntities"/> that are suppressed (hidden).
|
||||||
|
/// Suppressed entities are excluded from the active Program but preserved for re-enabling.
|
||||||
|
/// </summary>
|
||||||
|
public HashSet<Guid> SuppressedEntityIds { get; set; } = new HashSet<Guid>();
|
||||||
|
|
||||||
public double Area { get; protected set; }
|
public double Area { get; protected set; }
|
||||||
|
|
||||||
public void UpdateArea()
|
public void UpdateArea()
|
||||||
{
|
{
|
||||||
var geometry = ConvertProgram.ToGeometry(Program).Where(entity => entity.Layer != SpecialLayers.Rapid);
|
var geometry = ConvertProgram.ToGeometry(Program).Where(entity => entity.Layer != SpecialLayers.Rapid);
|
||||||
var shapes = Helper.GetShapes(geometry);
|
var shapes = ShapeBuilder.GetShapes(geometry);
|
||||||
|
|
||||||
if (shapes.Count == 0)
|
if (shapes.Count == 0)
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -8,10 +8,10 @@
|
|||||||
|
|
||||||
public int Remaining
|
public int Remaining
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
var x = Required - Nested;
|
var x = Required - Nested;
|
||||||
return x < 0 ? 0: x;
|
return x < 0 ? 0 : x;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
using System;
|
using OpenNest.Math;
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using OpenNest.Math;
|
|
||||||
|
|
||||||
namespace OpenNest.Geometry
|
namespace OpenNest.Geometry
|
||||||
{
|
{
|
||||||
@@ -155,6 +155,17 @@ namespace OpenNest.Geometry
|
|||||||
Center.Y + Radius * System.Math.Sin(EndAngle));
|
Center.Y + Radius * System.Math.Sin(EndAngle));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Mid point of the arc (point at the angle midway between start and end).
|
||||||
|
/// </summary>
|
||||||
|
public Vector MidPoint()
|
||||||
|
{
|
||||||
|
var midAngle = StartAngle + (IsReversed ? -SweepAngle() / 2 : SweepAngle() / 2);
|
||||||
|
return new Vector(
|
||||||
|
Center.X + Radius * System.Math.Cos(midAngle),
|
||||||
|
Center.Y + Radius * System.Math.Sin(midAngle));
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Splits the arc at the given point, returning two sub-arcs.
|
/// Splits the arc at the given point, returning two sub-arcs.
|
||||||
/// Either half may be null if the split point coincides with an endpoint.
|
/// Either half may be null if the split point coincides with an endpoint.
|
||||||
@@ -409,8 +420,8 @@ namespace OpenNest.Geometry
|
|||||||
|
|
||||||
boundingBox.X = minX;
|
boundingBox.X = minX;
|
||||||
boundingBox.Y = minY;
|
boundingBox.Y = minY;
|
||||||
boundingBox.Width = maxX - minX;
|
boundingBox.Length = maxX - minX;
|
||||||
boundingBox.Length = maxY - minY;
|
boundingBox.Width = maxY - minY;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Entity OffsetEntity(double distance, OffsetSide side)
|
public override Entity OffsetEntity(double distance, OffsetSide side)
|
||||||
@@ -465,7 +476,7 @@ namespace OpenNest.Geometry
|
|||||||
public override bool Intersects(Arc arc)
|
public override bool Intersects(Arc arc)
|
||||||
{
|
{
|
||||||
List<Vector> pts;
|
List<Vector> pts;
|
||||||
return Helper.Intersects(this, arc, out pts);
|
return Intersect.Intersects(this, arc, out pts);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -476,7 +487,7 @@ namespace OpenNest.Geometry
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public override bool Intersects(Arc arc, out List<Vector> pts)
|
public override bool Intersects(Arc arc, out List<Vector> pts)
|
||||||
{
|
{
|
||||||
return Helper.Intersects(this, arc, out pts); ;
|
return Intersect.Intersects(this, arc, out pts); ;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -487,7 +498,7 @@ namespace OpenNest.Geometry
|
|||||||
public override bool Intersects(Circle circle)
|
public override bool Intersects(Circle circle)
|
||||||
{
|
{
|
||||||
List<Vector> pts;
|
List<Vector> pts;
|
||||||
return Helper.Intersects(this, circle, out pts);
|
return Intersect.Intersects(this, circle, out pts);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -498,7 +509,7 @@ namespace OpenNest.Geometry
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public override bool Intersects(Circle circle, out List<Vector> pts)
|
public override bool Intersects(Circle circle, out List<Vector> pts)
|
||||||
{
|
{
|
||||||
return Helper.Intersects(this, circle, out pts);
|
return Intersect.Intersects(this, circle, out pts);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -509,7 +520,7 @@ namespace OpenNest.Geometry
|
|||||||
public override bool Intersects(Line line)
|
public override bool Intersects(Line line)
|
||||||
{
|
{
|
||||||
List<Vector> pts;
|
List<Vector> pts;
|
||||||
return Helper.Intersects(this, line, out pts);
|
return Intersect.Intersects(this, line, out pts);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -520,7 +531,7 @@ namespace OpenNest.Geometry
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public override bool Intersects(Line line, out List<Vector> pts)
|
public override bool Intersects(Line line, out List<Vector> pts)
|
||||||
{
|
{
|
||||||
return Helper.Intersects(this, line, out pts);
|
return Intersect.Intersects(this, line, out pts);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -531,7 +542,7 @@ namespace OpenNest.Geometry
|
|||||||
public override bool Intersects(Polygon polygon)
|
public override bool Intersects(Polygon polygon)
|
||||||
{
|
{
|
||||||
List<Vector> pts;
|
List<Vector> pts;
|
||||||
return Helper.Intersects(this, polygon, out pts);
|
return Intersect.Intersects(this, polygon, out pts);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -542,7 +553,7 @@ namespace OpenNest.Geometry
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public override bool Intersects(Polygon polygon, out List<Vector> pts)
|
public override bool Intersects(Polygon polygon, out List<Vector> pts)
|
||||||
{
|
{
|
||||||
return Helper.Intersects(this, polygon, out pts);
|
return Intersect.Intersects(this, polygon, out pts);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -553,7 +564,7 @@ namespace OpenNest.Geometry
|
|||||||
public override bool Intersects(Shape shape)
|
public override bool Intersects(Shape shape)
|
||||||
{
|
{
|
||||||
List<Vector> pts;
|
List<Vector> pts;
|
||||||
return Helper.Intersects(this, shape, out pts);
|
return Intersect.Intersects(this, shape, out pts);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -564,7 +575,7 @@ namespace OpenNest.Geometry
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public override bool Intersects(Shape shape, out List<Vector> pts)
|
public override bool Intersects(Shape shape, out List<Vector> pts)
|
||||||
{
|
{
|
||||||
return Helper.Intersects(this, shape, out pts);
|
return Intersect.Intersects(this, shape, out pts);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -0,0 +1,130 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace OpenNest.Geometry
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Shared arc-fitting utilities used by SplineConverter and GeometrySimplifier.
|
||||||
|
/// </summary>
|
||||||
|
internal static class ArcFit
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Fits a circular arc constrained to be tangent to the given direction at the
|
||||||
|
/// first point. The center lies at the intersection of the normal at P1 (perpendicular
|
||||||
|
/// to the tangent) and the perpendicular bisector of the chord P1->Pn, guaranteeing
|
||||||
|
/// the arc passes through both endpoints and departs P1 in the given direction.
|
||||||
|
/// </summary>
|
||||||
|
internal static (Vector center, double radius, double deviation) FitWithStartTangent(
|
||||||
|
List<Vector> points, Vector tangent)
|
||||||
|
{
|
||||||
|
if (points.Count < 3)
|
||||||
|
return (Vector.Invalid, 0, double.MaxValue);
|
||||||
|
|
||||||
|
var p1 = points[0];
|
||||||
|
var pn = points[^1];
|
||||||
|
|
||||||
|
var mx = (p1.X + pn.X) / 2;
|
||||||
|
var my = (p1.Y + pn.Y) / 2;
|
||||||
|
var dx = pn.X - p1.X;
|
||||||
|
var dy = pn.Y - p1.Y;
|
||||||
|
var chordLen = System.Math.Sqrt(dx * dx + dy * dy);
|
||||||
|
if (chordLen < 1e-10)
|
||||||
|
return (Vector.Invalid, 0, double.MaxValue);
|
||||||
|
|
||||||
|
var bx = -dy / chordLen;
|
||||||
|
var by = dx / chordLen;
|
||||||
|
|
||||||
|
var tLen = System.Math.Sqrt(tangent.X * tangent.X + tangent.Y * tangent.Y);
|
||||||
|
if (tLen < 1e-10)
|
||||||
|
return (Vector.Invalid, 0, double.MaxValue);
|
||||||
|
|
||||||
|
var nx = -tangent.Y / tLen;
|
||||||
|
var ny = tangent.X / tLen;
|
||||||
|
|
||||||
|
var det = nx * by - ny * bx;
|
||||||
|
if (System.Math.Abs(det) < 1e-10)
|
||||||
|
return (Vector.Invalid, 0, double.MaxValue);
|
||||||
|
|
||||||
|
var s = ((mx - p1.X) * by - (my - p1.Y) * bx) / det;
|
||||||
|
|
||||||
|
var cx = p1.X + s * nx;
|
||||||
|
var cy = p1.Y + s * ny;
|
||||||
|
var radius = System.Math.Sqrt((cx - p1.X) * (cx - p1.X) + (cy - p1.Y) * (cy - p1.Y));
|
||||||
|
|
||||||
|
if (radius < 1e-10)
|
||||||
|
return (Vector.Invalid, 0, double.MaxValue);
|
||||||
|
|
||||||
|
return (new Vector(cx, cy), radius, MaxRadialDeviation(points, cx, cy, radius));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fits a circular arc constrained to be tangent to the given directions at both
|
||||||
|
/// the first and last points. The center lies at the intersection of the normals
|
||||||
|
/// at P1 and Pn, guaranteeing the arc departs P1 in the start direction and arrives
|
||||||
|
/// at Pn in the end direction. Uses the radius from P1 (exact start tangent);
|
||||||
|
/// deviation includes any endpoint gap at Pn.
|
||||||
|
/// </summary>
|
||||||
|
internal static (Vector center, double radius, double deviation) FitWithDualTangent(
|
||||||
|
List<Vector> points, Vector startTangent, Vector endTangent)
|
||||||
|
{
|
||||||
|
if (points.Count < 3)
|
||||||
|
return (Vector.Invalid, 0, double.MaxValue);
|
||||||
|
|
||||||
|
var p1 = points[0];
|
||||||
|
var pn = points[^1];
|
||||||
|
|
||||||
|
var stLen = System.Math.Sqrt(startTangent.X * startTangent.X + startTangent.Y * startTangent.Y);
|
||||||
|
var etLen = System.Math.Sqrt(endTangent.X * endTangent.X + endTangent.Y * endTangent.Y);
|
||||||
|
if (stLen < 1e-10 || etLen < 1e-10)
|
||||||
|
return (Vector.Invalid, 0, double.MaxValue);
|
||||||
|
|
||||||
|
// Normal to start tangent at P1 (perpendicular)
|
||||||
|
var n1x = -startTangent.Y / stLen;
|
||||||
|
var n1y = startTangent.X / stLen;
|
||||||
|
|
||||||
|
// Normal to end tangent at Pn
|
||||||
|
var n2x = -endTangent.Y / etLen;
|
||||||
|
var n2y = endTangent.X / etLen;
|
||||||
|
|
||||||
|
// Solve: P1 + t1*N1 = Pn + t2*N2
|
||||||
|
var det = n1x * (-n2y) - (-n2x) * n1y;
|
||||||
|
if (System.Math.Abs(det) < 1e-10)
|
||||||
|
return (Vector.Invalid, 0, double.MaxValue);
|
||||||
|
|
||||||
|
var dx = pn.X - p1.X;
|
||||||
|
var dy = pn.Y - p1.Y;
|
||||||
|
var t1 = (dx * (-n2y) - (-n2x) * dy) / det;
|
||||||
|
|
||||||
|
var cx = p1.X + t1 * n1x;
|
||||||
|
var cy = p1.Y + t1 * n1y;
|
||||||
|
|
||||||
|
// Use radius from P1 (guarantees exact start tangent and passes through P1)
|
||||||
|
var r1 = System.Math.Sqrt((cx - p1.X) * (cx - p1.X) + (cy - p1.Y) * (cy - p1.Y));
|
||||||
|
if (r1 < 1e-10)
|
||||||
|
return (Vector.Invalid, 0, double.MaxValue);
|
||||||
|
|
||||||
|
// Measure endpoint gap at Pn
|
||||||
|
var r2 = System.Math.Sqrt((cx - pn.X) * (cx - pn.X) + (cy - pn.Y) * (cy - pn.Y));
|
||||||
|
var endpointDev = System.Math.Abs(r2 - r1);
|
||||||
|
|
||||||
|
var interiorDev = MaxRadialDeviation(points, cx, cy, r1);
|
||||||
|
return (new Vector(cx, cy), r1, System.Math.Max(endpointDev, interiorDev));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Computes the maximum radial deviation of interior points from a circle.
|
||||||
|
/// </summary>
|
||||||
|
internal static double MaxRadialDeviation(List<Vector> points, double cx, double cy, double radius)
|
||||||
|
{
|
||||||
|
var maxDev = 0.0;
|
||||||
|
for (var i = 1; i < points.Count - 1; i++)
|
||||||
|
{
|
||||||
|
var px = points[i].X - cx;
|
||||||
|
var py = points[i].Y - cy;
|
||||||
|
var dist = System.Math.Sqrt(px * px + py * py);
|
||||||
|
var dev = System.Math.Abs(dist - radius);
|
||||||
|
if (dev > maxDev) maxDev = dev;
|
||||||
|
}
|
||||||
|
return maxDev;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,8 +12,8 @@ namespace OpenNest.Geometry
|
|||||||
|
|
||||||
double minX = boxes[0].X;
|
double minX = boxes[0].X;
|
||||||
double minY = boxes[0].Y;
|
double minY = boxes[0].Y;
|
||||||
double maxX = boxes[0].X + boxes[0].Width;
|
double maxX = boxes[0].Right;
|
||||||
double maxY = boxes[0].Y + boxes[0].Length;
|
double maxY = boxes[0].Top;
|
||||||
|
|
||||||
foreach (var box in boxes)
|
foreach (var box in boxes)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -14,15 +14,15 @@ namespace OpenNest.Geometry
|
|||||||
public Box(double x, double y, double w, double h)
|
public Box(double x, double y, double w, double h)
|
||||||
{
|
{
|
||||||
Location = new Vector(x, y);
|
Location = new Vector(x, y);
|
||||||
Width = w;
|
Length = w;
|
||||||
Length = h;
|
Width = h;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Vector Location;
|
public Vector Location;
|
||||||
|
|
||||||
public Vector Center
|
public Vector Center
|
||||||
{
|
{
|
||||||
get { return new Vector(X + Width * 0.5, Y + Length * 0.5); }
|
get { return new Vector(X + Length * 0.5, Y + Width * 0.5); }
|
||||||
}
|
}
|
||||||
|
|
||||||
public Size Size;
|
public Size Size;
|
||||||
@@ -74,6 +74,16 @@ namespace OpenNest.Geometry
|
|||||||
Location += voffset;
|
Location += voffset;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Box Translate(double x, double y)
|
||||||
|
{
|
||||||
|
return new Box(X + x, Y + y, Length, Width);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Box Translate(Vector offset)
|
||||||
|
{
|
||||||
|
return new Box(X + offset.X, Y + offset.Y, Length, Width);
|
||||||
|
}
|
||||||
|
|
||||||
public double Left
|
public double Left
|
||||||
{
|
{
|
||||||
get { return X; }
|
get { return X; }
|
||||||
@@ -81,12 +91,12 @@ namespace OpenNest.Geometry
|
|||||||
|
|
||||||
public double Right
|
public double Right
|
||||||
{
|
{
|
||||||
get { return X + Width; }
|
get { return X + Length; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public double Top
|
public double Top
|
||||||
{
|
{
|
||||||
get { return Y + Length; }
|
get { return Y + Width; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public double Bottom
|
public double Bottom
|
||||||
@@ -197,7 +207,7 @@ namespace OpenNest.Geometry
|
|||||||
|
|
||||||
public Box Offset(double d)
|
public Box Offset(double d)
|
||||||
{
|
{
|
||||||
return new Box(X - d, Y - d, Width + d * 2, Length + d * 2);
|
return new Box(X - d, Y - d, Length + d * 2, Width + d * 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
var x = large.Left;
|
var x = large.Left;
|
||||||
var y = small.Top;
|
var y = small.Top;
|
||||||
var w = large.Width;
|
var w = large.Length;
|
||||||
var h = large.Top - y;
|
var h = large.Top - y;
|
||||||
|
|
||||||
return new Box(x, y, w, h);
|
return new Box(x, y, w, h);
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
var x = large.Left;
|
var x = large.Left;
|
||||||
var y = large.Bottom;
|
var y = large.Bottom;
|
||||||
var w = small.Left - x;
|
var w = small.Left - x;
|
||||||
var h = large.Length;
|
var h = large.Width;
|
||||||
|
|
||||||
return new Box(x, y, w, h);
|
return new Box(x, y, w, h);
|
||||||
}
|
}
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
|
|
||||||
var x = large.Left;
|
var x = large.Left;
|
||||||
var y = large.Bottom;
|
var y = large.Bottom;
|
||||||
var w = large.Width;
|
var w = large.Length;
|
||||||
var h = small.Top - y;
|
var h = small.Top - y;
|
||||||
|
|
||||||
return new Box(x, y, w, h);
|
return new Box(x, y, w, h);
|
||||||
@@ -49,7 +49,7 @@
|
|||||||
var x = small.Right;
|
var x = small.Right;
|
||||||
var y = large.Bottom;
|
var y = large.Bottom;
|
||||||
var w = large.Right - x;
|
var w = large.Right - x;
|
||||||
var h = large.Length;
|
var h = large.Width;
|
||||||
|
|
||||||
return new Box(x, y, w, h);
|
return new Box(x, y, w, h);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
using System;
|
using OpenNest.Math;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using OpenNest.Math;
|
|
||||||
|
|
||||||
namespace OpenNest.Geometry
|
namespace OpenNest.Geometry
|
||||||
{
|
{
|
||||||
@@ -138,7 +137,9 @@ namespace OpenNest.Geometry
|
|||||||
public List<Vector> ToPoints(int segments = 1000, bool circumscribe = false)
|
public List<Vector> ToPoints(int segments = 1000, bool circumscribe = false)
|
||||||
{
|
{
|
||||||
var points = new List<Vector>();
|
var points = new List<Vector>();
|
||||||
var stepAngle = Angle.TwoPI / segments;
|
var stepAngle = Rotation == RotationType.CW
|
||||||
|
? -Angle.TwoPI / segments
|
||||||
|
: Angle.TwoPI / segments;
|
||||||
|
|
||||||
var r = circumscribe && segments > 0
|
var r = circumscribe && segments > 0
|
||||||
? Radius / System.Math.Cos(stepAngle / 2.0)
|
? Radius / System.Math.Cos(stepAngle / 2.0)
|
||||||
@@ -320,7 +321,7 @@ namespace OpenNest.Geometry
|
|||||||
public override bool Intersects(Arc arc)
|
public override bool Intersects(Arc arc)
|
||||||
{
|
{
|
||||||
List<Vector> pts;
|
List<Vector> pts;
|
||||||
return Helper.Intersects(arc, this, out pts);
|
return Intersect.Intersects(arc, this, out pts);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -331,7 +332,7 @@ namespace OpenNest.Geometry
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public override bool Intersects(Arc arc, out List<Vector> pts)
|
public override bool Intersects(Arc arc, out List<Vector> pts)
|
||||||
{
|
{
|
||||||
return Helper.Intersects(arc, this, out pts);
|
return Intersect.Intersects(arc, this, out pts);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -353,7 +354,7 @@ namespace OpenNest.Geometry
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public override bool Intersects(Circle circle, out List<Vector> pts)
|
public override bool Intersects(Circle circle, out List<Vector> pts)
|
||||||
{
|
{
|
||||||
return Helper.Intersects(this, circle, out pts);
|
return Intersect.Intersects(this, circle, out pts);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -364,7 +365,7 @@ namespace OpenNest.Geometry
|
|||||||
public override bool Intersects(Line line)
|
public override bool Intersects(Line line)
|
||||||
{
|
{
|
||||||
List<Vector> pts;
|
List<Vector> pts;
|
||||||
return Helper.Intersects(this, line, out pts);
|
return Intersect.Intersects(this, line, out pts);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -375,7 +376,7 @@ namespace OpenNest.Geometry
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public override bool Intersects(Line line, out List<Vector> pts)
|
public override bool Intersects(Line line, out List<Vector> pts)
|
||||||
{
|
{
|
||||||
return Helper.Intersects(this, line, out pts);
|
return Intersect.Intersects(this, line, out pts);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -386,7 +387,7 @@ namespace OpenNest.Geometry
|
|||||||
public override bool Intersects(Polygon polygon)
|
public override bool Intersects(Polygon polygon)
|
||||||
{
|
{
|
||||||
List<Vector> pts;
|
List<Vector> pts;
|
||||||
return Helper.Intersects(this, polygon, out pts);
|
return Intersect.Intersects(this, polygon, out pts);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -397,7 +398,7 @@ namespace OpenNest.Geometry
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public override bool Intersects(Polygon polygon, out List<Vector> pts)
|
public override bool Intersects(Polygon polygon, out List<Vector> pts)
|
||||||
{
|
{
|
||||||
return Helper.Intersects(this, polygon, out pts);
|
return Intersect.Intersects(this, polygon, out pts);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -408,7 +409,7 @@ namespace OpenNest.Geometry
|
|||||||
public override bool Intersects(Shape shape)
|
public override bool Intersects(Shape shape)
|
||||||
{
|
{
|
||||||
List<Vector> pts;
|
List<Vector> pts;
|
||||||
return Helper.Intersects(this, shape, out pts);
|
return Intersect.Intersects(this, shape, out pts);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -419,7 +420,7 @@ namespace OpenNest.Geometry
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public override bool Intersects(Shape shape, out List<Vector> pts)
|
public override bool Intersects(Shape shape, out List<Vector> pts)
|
||||||
{
|
{
|
||||||
return Helper.Intersects(this, shape, out pts);
|
return Intersect.Intersects(this, shape, out pts);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -0,0 +1,330 @@
|
|||||||
|
using OpenNest.Math;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace OpenNest.Geometry
|
||||||
|
{
|
||||||
|
public static class Collision
|
||||||
|
{
|
||||||
|
public static CollisionResult Check(Polygon a, Polygon b,
|
||||||
|
List<Polygon> holesA = null, List<Polygon> holesB = null)
|
||||||
|
{
|
||||||
|
// Step 1: Bounding box pre-filter
|
||||||
|
if (!BoundingBoxesOverlap(a.BoundingBox, b.BoundingBox))
|
||||||
|
return CollisionResult.None;
|
||||||
|
|
||||||
|
// Step 2: Quick intersection test for crossing points
|
||||||
|
var intersectionPoints = FindCrossingPoints(a, b);
|
||||||
|
|
||||||
|
// Step 3: Convex decomposition
|
||||||
|
var trisA = TriangulateWithBounds(a);
|
||||||
|
var trisB = TriangulateWithBounds(b);
|
||||||
|
|
||||||
|
// Step 4: Clip all triangle pairs
|
||||||
|
var regions = new List<Polygon>();
|
||||||
|
|
||||||
|
foreach (var triA in trisA)
|
||||||
|
{
|
||||||
|
foreach (var triB in trisB)
|
||||||
|
{
|
||||||
|
if (!BoundingBoxesOverlap(triA.BoundingBox, triB.BoundingBox))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var clipped = ClipConvex(triA, triB);
|
||||||
|
if (clipped != null)
|
||||||
|
regions.Add(clipped);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 5: Hole subtraction
|
||||||
|
if (regions.Count > 0)
|
||||||
|
regions = SubtractHoles(regions, holesA, holesB);
|
||||||
|
|
||||||
|
if (regions.Count == 0)
|
||||||
|
return new CollisionResult(false, regions, intersectionPoints);
|
||||||
|
|
||||||
|
// Step 6: Build result
|
||||||
|
return new CollisionResult(true, regions, intersectionPoints);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool HasOverlap(Polygon a, Polygon b,
|
||||||
|
List<Polygon> holesA = null, List<Polygon> holesB = null)
|
||||||
|
{
|
||||||
|
if (!BoundingBoxesOverlap(a.BoundingBox, b.BoundingBox))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Full check is needed: crossing points alone miss containment cases
|
||||||
|
// (one polygon entirely inside another has zero edge crossings).
|
||||||
|
return Check(a, b, holesA, holesB).Overlaps;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<CollisionResult> CheckAll(List<Polygon> polygons,
|
||||||
|
List<List<Polygon>> holes = null)
|
||||||
|
{
|
||||||
|
var results = new List<CollisionResult>();
|
||||||
|
|
||||||
|
for (var i = 0; i < polygons.Count; i++)
|
||||||
|
{
|
||||||
|
for (var j = i + 1; j < polygons.Count; j++)
|
||||||
|
{
|
||||||
|
var holesA = holes != null && i < holes.Count ? holes[i] : null;
|
||||||
|
var holesB = holes != null && j < holes.Count ? holes[j] : null;
|
||||||
|
var result = Check(polygons[i], polygons[j], holesA, holesB);
|
||||||
|
|
||||||
|
if (result.Overlaps)
|
||||||
|
results.Add(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool HasAnyOverlap(List<Polygon> polygons,
|
||||||
|
List<List<Polygon>> holes = null)
|
||||||
|
{
|
||||||
|
for (var i = 0; i < polygons.Count; i++)
|
||||||
|
{
|
||||||
|
for (var j = i + 1; j < polygons.Count; j++)
|
||||||
|
{
|
||||||
|
var holesA = holes != null && i < holes.Count ? holes[i] : null;
|
||||||
|
var holesB = holes != null && j < holes.Count ? holes[j] : null;
|
||||||
|
|
||||||
|
if (HasOverlap(polygons[i], polygons[j], holesA, holesB))
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool BoundingBoxesOverlap(Box a, Box b)
|
||||||
|
{
|
||||||
|
var overlapX = System.Math.Min(a.Right, b.Right)
|
||||||
|
- System.Math.Max(a.Left, b.Left);
|
||||||
|
var overlapY = System.Math.Min(a.Top, b.Top)
|
||||||
|
- System.Math.Max(a.Bottom, b.Bottom);
|
||||||
|
|
||||||
|
return overlapX > Tolerance.Epsilon && overlapY > Tolerance.Epsilon;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<Vector> FindCrossingPoints(Polygon a, Polygon b)
|
||||||
|
{
|
||||||
|
if (!Intersect.Intersects(a, b, out var rawPts))
|
||||||
|
return new List<Vector>();
|
||||||
|
|
||||||
|
// Filter boundary contacts (vertex touches)
|
||||||
|
var vertsA = CollectVertices(a);
|
||||||
|
var vertsB = CollectVertices(b);
|
||||||
|
var filtered = new List<Vector>();
|
||||||
|
|
||||||
|
foreach (var pt in rawPts)
|
||||||
|
{
|
||||||
|
if (IsNearAnyVertex(pt, vertsA) || IsNearAnyVertex(pt, vertsB))
|
||||||
|
continue;
|
||||||
|
filtered.Add(pt);
|
||||||
|
}
|
||||||
|
|
||||||
|
return filtered;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<Vector> CollectVertices(Polygon polygon)
|
||||||
|
{
|
||||||
|
var verts = new List<Vector>(polygon.Vertices.Count);
|
||||||
|
foreach (var v in polygon.Vertices)
|
||||||
|
verts.Add(v);
|
||||||
|
return verts;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsNearAnyVertex(Vector pt, List<Vector> vertices)
|
||||||
|
{
|
||||||
|
foreach (var v in vertices)
|
||||||
|
{
|
||||||
|
if (pt.X.IsEqualTo(v.X) && pt.Y.IsEqualTo(v.Y))
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Triangulates a polygon and ensures each triangle has its bounding box updated.
|
||||||
|
/// </summary>
|
||||||
|
private static List<Polygon> TriangulateWithBounds(Polygon polygon)
|
||||||
|
{
|
||||||
|
var tris = ConvexDecomposition.Triangulate(polygon);
|
||||||
|
foreach (var tri in tris)
|
||||||
|
tri.UpdateBounds();
|
||||||
|
return tris;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sutherland-Hodgman polygon clipping. Clips subject against each edge
|
||||||
|
/// of clip. Both must be convex. Returns null if no overlap.
|
||||||
|
/// </summary>
|
||||||
|
private static Polygon ClipConvex(Polygon subject, Polygon clip)
|
||||||
|
{
|
||||||
|
var output = new List<Vector>(subject.Vertices);
|
||||||
|
|
||||||
|
// Remove closing vertex if present
|
||||||
|
if (output.Count > 1 && output[0].X == output[output.Count - 1].X
|
||||||
|
&& output[0].Y == output[output.Count - 1].Y)
|
||||||
|
output.RemoveAt(output.Count - 1);
|
||||||
|
|
||||||
|
var clipVerts = new List<Vector>(clip.Vertices);
|
||||||
|
if (clipVerts.Count > 1 && clipVerts[0].X == clipVerts[clipVerts.Count - 1].X
|
||||||
|
&& clipVerts[0].Y == clipVerts[clipVerts.Count - 1].Y)
|
||||||
|
clipVerts.RemoveAt(clipVerts.Count - 1);
|
||||||
|
|
||||||
|
for (var i = 0; i < clipVerts.Count; i++)
|
||||||
|
{
|
||||||
|
if (output.Count == 0)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var edgeStart = clipVerts[i];
|
||||||
|
var edgeEnd = clipVerts[(i + 1) % clipVerts.Count];
|
||||||
|
var input = output;
|
||||||
|
output = new List<Vector>();
|
||||||
|
|
||||||
|
for (var j = 0; j < input.Count; j++)
|
||||||
|
{
|
||||||
|
var current = input[j];
|
||||||
|
var next = input[(j + 1) % input.Count];
|
||||||
|
var currentInside = Cross(edgeStart, edgeEnd, current) >= -Tolerance.Epsilon;
|
||||||
|
var nextInside = Cross(edgeStart, edgeEnd, next) >= -Tolerance.Epsilon;
|
||||||
|
|
||||||
|
if (currentInside)
|
||||||
|
{
|
||||||
|
output.Add(current);
|
||||||
|
if (!nextInside)
|
||||||
|
{
|
||||||
|
var ix = LineIntersection(edgeStart, edgeEnd, current, next);
|
||||||
|
if (ix.IsValid())
|
||||||
|
output.Add(ix);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (nextInside)
|
||||||
|
{
|
||||||
|
var ix = LineIntersection(edgeStart, edgeEnd, current, next);
|
||||||
|
if (ix.IsValid())
|
||||||
|
output.Add(ix);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (output.Count < 3)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var result = new Polygon();
|
||||||
|
result.Vertices.AddRange(output);
|
||||||
|
result.Close();
|
||||||
|
result.UpdateBounds();
|
||||||
|
|
||||||
|
// Reject degenerate slivers
|
||||||
|
if (result.Area() < Tolerance.Epsilon)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Cross product of vectors (edgeStart->edgeEnd) and (edgeStart->point).
|
||||||
|
/// Positive = point is left of edge (inside for CCW polygon).
|
||||||
|
/// </summary>
|
||||||
|
private static double Cross(Vector edgeStart, Vector edgeEnd, Vector point)
|
||||||
|
{
|
||||||
|
return (edgeEnd.X - edgeStart.X) * (point.Y - edgeStart.Y)
|
||||||
|
- (edgeEnd.Y - edgeStart.Y) * (point.X - edgeStart.X);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Intersection of lines (a1->a2) and (b1->b2). Returns Vector.Invalid if parallel.
|
||||||
|
/// </summary>
|
||||||
|
private static Vector LineIntersection(Vector a1, Vector a2, Vector b1, Vector b2)
|
||||||
|
{
|
||||||
|
var d1x = a2.X - a1.X;
|
||||||
|
var d1y = a2.Y - a1.Y;
|
||||||
|
var d2x = b2.X - b1.X;
|
||||||
|
var d2y = b2.Y - b1.Y;
|
||||||
|
var cross = d1x * d2y - d1y * d2x;
|
||||||
|
|
||||||
|
if (System.Math.Abs(cross) < Tolerance.Epsilon)
|
||||||
|
return Vector.Invalid;
|
||||||
|
|
||||||
|
var t = ((b1.X - a1.X) * d2y - (b1.Y - a1.Y) * d2x) / cross;
|
||||||
|
return new Vector(a1.X + t * d1x, a1.Y + t * d1y);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Subtracts holes from overlap regions.
|
||||||
|
/// </summary>
|
||||||
|
private static List<Polygon> SubtractHoles(List<Polygon> regions,
|
||||||
|
List<Polygon> holesA, List<Polygon> holesB)
|
||||||
|
{
|
||||||
|
var allHoles = new List<Polygon>();
|
||||||
|
if (holesA != null) allHoles.AddRange(holesA);
|
||||||
|
if (holesB != null) allHoles.AddRange(holesB);
|
||||||
|
|
||||||
|
if (allHoles.Count == 0)
|
||||||
|
return regions;
|
||||||
|
|
||||||
|
foreach (var hole in allHoles)
|
||||||
|
{
|
||||||
|
var holeTris = TriangulateWithBounds(hole);
|
||||||
|
var surviving = new List<Polygon>();
|
||||||
|
|
||||||
|
foreach (var region in regions)
|
||||||
|
{
|
||||||
|
var pieces = SubtractTriangles(region, holeTris);
|
||||||
|
surviving.AddRange(pieces);
|
||||||
|
}
|
||||||
|
|
||||||
|
regions = surviving;
|
||||||
|
|
||||||
|
if (regions.Count == 0)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return regions;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Subtracts hole triangles from a region. Conservative: partial overlaps
|
||||||
|
/// keep the full piece triangle (acceptable for visual shading).
|
||||||
|
/// </summary>
|
||||||
|
private static List<Polygon> SubtractTriangles(Polygon region, List<Polygon> holeTris)
|
||||||
|
{
|
||||||
|
var current = new List<Polygon> { region };
|
||||||
|
|
||||||
|
foreach (var holeTri in holeTris)
|
||||||
|
{
|
||||||
|
if (!BoundingBoxesOverlap(region.BoundingBox, holeTri.BoundingBox))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var next = new List<Polygon>();
|
||||||
|
|
||||||
|
foreach (var piece in current)
|
||||||
|
{
|
||||||
|
var pieceTris = TriangulateWithBounds(piece);
|
||||||
|
|
||||||
|
foreach (var pieceTri in pieceTris)
|
||||||
|
{
|
||||||
|
var inside = ClipConvex(pieceTri, holeTri);
|
||||||
|
if (inside == null)
|
||||||
|
{
|
||||||
|
// No overlap with hole - keep
|
||||||
|
next.Add(pieceTri);
|
||||||
|
}
|
||||||
|
else if (inside.Area() < pieceTri.Area() - Tolerance.Epsilon)
|
||||||
|
{
|
||||||
|
// Partial overlap - keep the piece (conservative)
|
||||||
|
next.Add(pieceTri);
|
||||||
|
}
|
||||||
|
// else: fully inside hole - discard
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
current = next;
|
||||||
|
}
|
||||||
|
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace OpenNest.Geometry
|
||||||
|
{
|
||||||
|
public class CollisionResult
|
||||||
|
{
|
||||||
|
public static readonly CollisionResult None = new(false, new List<Polygon>(), new List<Vector>());
|
||||||
|
|
||||||
|
public CollisionResult(bool overlaps, List<Polygon> overlapRegions, List<Vector> intersectionPoints)
|
||||||
|
{
|
||||||
|
Overlaps = overlaps;
|
||||||
|
OverlapRegions = overlapRegions;
|
||||||
|
IntersectionPoints = intersectionPoints;
|
||||||
|
OverlapArea = overlapRegions.Sum(r => r.Area());
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Overlaps { get; }
|
||||||
|
public IReadOnlyList<Polygon> OverlapRegions { get; }
|
||||||
|
public IReadOnlyList<Vector> IntersectionPoints { get; }
|
||||||
|
public double OverlapArea { get; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,245 @@
|
|||||||
|
using OpenNest.Math;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace OpenNest.Geometry
|
||||||
|
{
|
||||||
|
public static class EllipseConverter
|
||||||
|
{
|
||||||
|
private const int MaxSubdivisionDepth = 12;
|
||||||
|
private const int DeviationSamples = 20;
|
||||||
|
|
||||||
|
internal static Vector EvaluatePoint(double semiMajor, double semiMinor, double rotation, Vector center, double t)
|
||||||
|
{
|
||||||
|
var x = semiMajor * System.Math.Cos(t);
|
||||||
|
var y = semiMinor * System.Math.Sin(t);
|
||||||
|
|
||||||
|
var cos = System.Math.Cos(rotation);
|
||||||
|
var sin = System.Math.Sin(rotation);
|
||||||
|
|
||||||
|
return new Vector(
|
||||||
|
center.X + x * cos - y * sin,
|
||||||
|
center.Y + x * sin + y * cos);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static Vector EvaluateTangent(double semiMajor, double semiMinor, double rotation, double t)
|
||||||
|
{
|
||||||
|
var tx = -semiMajor * System.Math.Sin(t);
|
||||||
|
var ty = semiMinor * System.Math.Cos(t);
|
||||||
|
|
||||||
|
var cos = System.Math.Cos(rotation);
|
||||||
|
var sin = System.Math.Sin(rotation);
|
||||||
|
|
||||||
|
return new Vector(
|
||||||
|
tx * cos - ty * sin,
|
||||||
|
tx * sin + ty * cos);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static Vector EvaluateNormal(double semiMajor, double semiMinor, double rotation, double t)
|
||||||
|
{
|
||||||
|
// Inward normal: perpendicular to tangent, pointing toward center of curvature.
|
||||||
|
// In local coords: N(t) = (-b*cos(t), -a*sin(t))
|
||||||
|
var nx = -semiMinor * System.Math.Cos(t);
|
||||||
|
var ny = -semiMajor * System.Math.Sin(t);
|
||||||
|
|
||||||
|
var cos = System.Math.Cos(rotation);
|
||||||
|
var sin = System.Math.Sin(rotation);
|
||||||
|
|
||||||
|
return new Vector(
|
||||||
|
nx * cos - ny * sin,
|
||||||
|
nx * sin + ny * cos);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static Vector IntersectNormals(Vector p1, Vector n1, Vector p2, Vector n2)
|
||||||
|
{
|
||||||
|
// Solve: p1 + s*n1 = p2 + t*n2
|
||||||
|
var det = n1.X * (-n2.Y) - (-n2.X) * n1.Y;
|
||||||
|
if (System.Math.Abs(det) < 1e-10)
|
||||||
|
return Vector.Invalid;
|
||||||
|
|
||||||
|
var dx = p2.X - p1.X;
|
||||||
|
var dy = p2.Y - p1.Y;
|
||||||
|
var s = (dx * (-n2.Y) - dy * (-n2.X)) / det;
|
||||||
|
|
||||||
|
return new Vector(p1.X + s * n1.X, p1.Y + s * n1.Y);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static Vector Circumcenter(Vector a, Vector b, Vector c)
|
||||||
|
{
|
||||||
|
var ax = a.X - c.X;
|
||||||
|
var ay = a.Y - c.Y;
|
||||||
|
var bx = b.X - c.X;
|
||||||
|
var by = b.Y - c.Y;
|
||||||
|
var D = 2.0 * (ax * by - ay * bx);
|
||||||
|
|
||||||
|
if (System.Math.Abs(D) < 1e-10)
|
||||||
|
return Vector.Invalid;
|
||||||
|
|
||||||
|
var a2 = ax * ax + ay * ay;
|
||||||
|
var b2 = bx * bx + by * by;
|
||||||
|
var ux = (by * a2 - ay * b2) / D;
|
||||||
|
var uy = (ax * b2 - bx * a2) / D;
|
||||||
|
|
||||||
|
return new Vector(ux + c.X, uy + c.Y);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<Entity> Convert(Vector center, double semiMajor, double semiMinor,
|
||||||
|
double rotation, double startParam, double endParam, double tolerance = 0.001)
|
||||||
|
{
|
||||||
|
if (tolerance <= 0)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(tolerance), "Tolerance must be positive.");
|
||||||
|
if (semiMajor <= 0 || semiMinor <= 0)
|
||||||
|
throw new ArgumentOutOfRangeException("Semi-axis lengths must be positive.");
|
||||||
|
|
||||||
|
if (endParam <= startParam)
|
||||||
|
endParam += Angle.TwoPI;
|
||||||
|
|
||||||
|
// True circle — emit a single arc (or two for full circle)
|
||||||
|
if (System.Math.Abs(semiMajor - semiMinor) < Tolerance.Epsilon)
|
||||||
|
return ConvertCircle(center, semiMajor, rotation, startParam, endParam);
|
||||||
|
|
||||||
|
var splits = GetInitialSplits(startParam, endParam);
|
||||||
|
|
||||||
|
var entities = new List<Entity>();
|
||||||
|
for (var i = 0; i < splits.Count - 1; i++)
|
||||||
|
FitSegment(center, semiMajor, semiMinor, rotation,
|
||||||
|
splits[i], splits[i + 1], tolerance, entities, 0);
|
||||||
|
|
||||||
|
return entities;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<Entity> ConvertCircle(Vector center, double radius,
|
||||||
|
double rotation, double startParam, double endParam)
|
||||||
|
{
|
||||||
|
var sweep = endParam - startParam;
|
||||||
|
var isFull = System.Math.Abs(sweep - Angle.TwoPI) < 0.01;
|
||||||
|
|
||||||
|
if (isFull)
|
||||||
|
{
|
||||||
|
var startAngle1 = Angle.NormalizeRad(startParam + rotation);
|
||||||
|
var midAngle = Angle.NormalizeRad(startParam + System.Math.PI + rotation);
|
||||||
|
var endAngle2 = startAngle1;
|
||||||
|
|
||||||
|
return new List<Entity>
|
||||||
|
{
|
||||||
|
new Arc(center, radius, startAngle1, midAngle, false),
|
||||||
|
new Arc(center, radius, midAngle, endAngle2, false)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
var sa = Angle.NormalizeRad(startParam + rotation);
|
||||||
|
var ea = Angle.NormalizeRad(endParam + rotation);
|
||||||
|
return new List<Entity> { new Arc(center, radius, sa, ea, false) };
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<double> GetInitialSplits(double startParam, double endParam)
|
||||||
|
{
|
||||||
|
var splits = new List<double> { startParam };
|
||||||
|
|
||||||
|
var firstQuadrant = System.Math.Ceiling(startParam / (System.Math.PI / 2)) * (System.Math.PI / 2);
|
||||||
|
for (var q = firstQuadrant; q < endParam; q += System.Math.PI / 2)
|
||||||
|
{
|
||||||
|
if (q > startParam + 1e-10 && q < endParam - 1e-10)
|
||||||
|
splits.Add(q);
|
||||||
|
}
|
||||||
|
|
||||||
|
splits.Add(endParam);
|
||||||
|
return splits;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void FitSegment(Vector center, double semiMajor, double semiMinor,
|
||||||
|
double rotation, double t0, double t1, double tolerance, List<Entity> results, int depth)
|
||||||
|
{
|
||||||
|
var p0 = EvaluatePoint(semiMajor, semiMinor, rotation, center, t0);
|
||||||
|
var p1 = EvaluatePoint(semiMajor, semiMinor, rotation, center, t1);
|
||||||
|
|
||||||
|
if (p0.DistanceTo(p1) < 1e-10)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var n0 = EvaluateNormal(semiMajor, semiMinor, rotation, t0);
|
||||||
|
var n1 = EvaluateNormal(semiMajor, semiMinor, rotation, t1);
|
||||||
|
|
||||||
|
var arcCenter = IntersectNormals(p0, n0, p1, n1);
|
||||||
|
|
||||||
|
if (!arcCenter.IsValid() || depth >= MaxSubdivisionDepth)
|
||||||
|
{
|
||||||
|
results.Add(new Line(p0, p1));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var radius = p0.DistanceTo(arcCenter);
|
||||||
|
var maxDev = MeasureDeviation(center, semiMajor, semiMinor, rotation,
|
||||||
|
t0, t1, arcCenter, radius);
|
||||||
|
|
||||||
|
if (maxDev <= tolerance)
|
||||||
|
{
|
||||||
|
results.Add(CreateArc(arcCenter, radius, center, semiMajor, semiMinor, rotation, t0, t1));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var tMid = (t0 + t1) / 2.0;
|
||||||
|
FitSegment(center, semiMajor, semiMinor, rotation, t0, tMid, tolerance, results, depth + 1);
|
||||||
|
FitSegment(center, semiMajor, semiMinor, rotation, tMid, t1, tolerance, results, depth + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double MeasureDeviation(Vector center, double semiMajor, double semiMinor,
|
||||||
|
double rotation, double t0, double t1, Vector arcCenter, double radius)
|
||||||
|
{
|
||||||
|
var maxDev = 0.0;
|
||||||
|
for (var i = 1; i <= DeviationSamples; i++)
|
||||||
|
{
|
||||||
|
var t = t0 + (t1 - t0) * i / DeviationSamples;
|
||||||
|
var p = EvaluatePoint(semiMajor, semiMinor, rotation, center, t);
|
||||||
|
var dist = p.DistanceTo(arcCenter);
|
||||||
|
var dev = System.Math.Abs(dist - radius);
|
||||||
|
if (dev > maxDev) maxDev = dev;
|
||||||
|
}
|
||||||
|
return maxDev;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Arc CreateArc(Vector arcCenter, double radius,
|
||||||
|
Vector ellipseCenter, double semiMajor, double semiMinor, double rotation,
|
||||||
|
double t0, double t1)
|
||||||
|
{
|
||||||
|
var p0 = EvaluatePoint(semiMajor, semiMinor, rotation, ellipseCenter, t0);
|
||||||
|
var p1 = EvaluatePoint(semiMajor, semiMinor, rotation, ellipseCenter, t1);
|
||||||
|
var pMid = EvaluatePoint(semiMajor, semiMinor, rotation, ellipseCenter, (t0 + t1) / 2);
|
||||||
|
|
||||||
|
// Use circumcircle of (p0, pMid, p1) so the arc passes through both
|
||||||
|
// endpoints exactly, eliminating gaps between adjacent arcs.
|
||||||
|
var cc = Circumcenter(p0, pMid, p1);
|
||||||
|
if (cc.IsValid())
|
||||||
|
{
|
||||||
|
arcCenter = cc;
|
||||||
|
radius = p0.DistanceTo(cc);
|
||||||
|
}
|
||||||
|
|
||||||
|
var startAngle = System.Math.Atan2(p0.Y - arcCenter.Y, p0.X - arcCenter.X);
|
||||||
|
var endAngle = System.Math.Atan2(p1.Y - arcCenter.Y, p1.X - arcCenter.X);
|
||||||
|
|
||||||
|
var points = new List<Vector> { p0, pMid, p1 };
|
||||||
|
var isReversed = SumSignedAngles(arcCenter, points) < 0;
|
||||||
|
|
||||||
|
if (startAngle < 0) startAngle += Angle.TwoPI;
|
||||||
|
if (endAngle < 0) endAngle += Angle.TwoPI;
|
||||||
|
|
||||||
|
return new Arc(arcCenter, radius, startAngle, endAngle, isReversed);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double SumSignedAngles(Vector center, List<Vector> points)
|
||||||
|
{
|
||||||
|
var total = 0.0;
|
||||||
|
for (var i = 0; i < points.Count - 1; i++)
|
||||||
|
{
|
||||||
|
var a1 = System.Math.Atan2(points[i].Y - center.Y, points[i].X - center.X);
|
||||||
|
var a2 = System.Math.Atan2(points[i + 1].Y - center.Y, points[i + 1].X - center.X);
|
||||||
|
var da = a2 - a1;
|
||||||
|
while (da > System.Math.PI) da -= Angle.TwoPI;
|
||||||
|
while (da < -System.Math.PI) da += Angle.TwoPI;
|
||||||
|
total += da;
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
using System.Collections.Generic;
|
using OpenNest.Math;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Drawing;
|
using System.Drawing;
|
||||||
using OpenNest.Math;
|
|
||||||
|
|
||||||
namespace OpenNest.Geometry
|
namespace OpenNest.Geometry
|
||||||
{
|
{
|
||||||
@@ -10,10 +11,16 @@ namespace OpenNest.Geometry
|
|||||||
|
|
||||||
protected Entity()
|
protected Entity()
|
||||||
{
|
{
|
||||||
|
Id = Guid.NewGuid();
|
||||||
Layer = OpenNest.Geometry.Layer.Default;
|
Layer = OpenNest.Geometry.Layer.Default;
|
||||||
boundingBox = new Box();
|
boundingBox = new Box();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Unique identifier for this entity, stable across edit sessions.
|
||||||
|
/// </summary>
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Entity color (resolved from DXF ByLayer/ByBlock to actual color).
|
/// Entity color (resolved from DXF ByLayer/ByBlock to actual color).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -29,6 +36,11 @@ namespace OpenNest.Geometry
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public bool IsVisible { get; set; } = true;
|
public bool IsVisible { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Optional tag for identifying generated entities (e.g. bend etch marks).
|
||||||
|
/// </summary>
|
||||||
|
public string Tag { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Smallest box that contains the entity.
|
/// Smallest box that contains the entity.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -247,7 +259,7 @@ namespace OpenNest.Geometry
|
|||||||
|
|
||||||
public static class EntityExtensions
|
public static class EntityExtensions
|
||||||
{
|
{
|
||||||
public static BoundingRectangleResult FindBestRotation(this List<Entity> entities, double startAngle = 0, double endAngle = Angle.TwoPI)
|
public static List<Vector> CollectPoints(this IEnumerable<Entity> entities)
|
||||||
{
|
{
|
||||||
var points = new List<Vector>();
|
var points = new List<Vector>();
|
||||||
|
|
||||||
@@ -286,17 +298,35 @@ namespace OpenNest.Geometry
|
|||||||
|
|
||||||
case EntityType.Shape:
|
case EntityType.Shape:
|
||||||
var shape = (Shape)entity;
|
var shape = (Shape)entity;
|
||||||
var subResult = shape.Entities.FindBestRotation(startAngle, endAngle);
|
points.AddRange(shape.Entities.CollectPoints());
|
||||||
return subResult;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return points;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static BoundingRectangleResult FindBestRotation(this List<Entity> entities, double startAngle = 0, double endAngle = Angle.TwoPI)
|
||||||
|
{
|
||||||
|
// Check for Shape entity first (recursive case returns early)
|
||||||
|
foreach (var entity in entities)
|
||||||
|
{
|
||||||
|
if (entity.Type == EntityType.Shape)
|
||||||
|
{
|
||||||
|
var shape = (Shape)entity;
|
||||||
|
var subResult = shape.Entities.FindBestRotation(startAngle, endAngle);
|
||||||
|
return subResult;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var points = entities.CollectPoints();
|
||||||
|
|
||||||
if (points.Count == 0)
|
if (points.Count == 0)
|
||||||
return new BoundingRectangleResult(startAngle, 0, 0);
|
return new BoundingRectangleResult(startAngle, 0, 0);
|
||||||
|
|
||||||
var hull = ConvexHull.Compute(points);
|
var hull = ConvexHull.Compute(points);
|
||||||
|
|
||||||
bool constrained = !startAngle.IsEqualTo(0) || !endAngle.IsEqualTo(Angle.TwoPI);
|
var constrained = !startAngle.IsEqualTo(0) || !endAngle.IsEqualTo(Angle.TwoPI);
|
||||||
|
|
||||||
return constrained
|
return constrained
|
||||||
? RotatingCalipers.MinimumBoundingRectangle(hull, startAngle, endAngle)
|
? RotatingCalipers.MinimumBoundingRectangle(hull, startAngle, endAngle)
|
||||||
|
|||||||
@@ -0,0 +1,202 @@
|
|||||||
|
using OpenNest.Math;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace OpenNest.Geometry
|
||||||
|
{
|
||||||
|
public static class GeometryOptimizer
|
||||||
|
{
|
||||||
|
public static void Optimize(IList<Arc> arcs) =>
|
||||||
|
MergePass(arcs,
|
||||||
|
(list, item, i) => list.GetCoradialArs(item, i),
|
||||||
|
(Arc a, Arc b, out Arc joined) => TryJoinArcs(a, b, out joined));
|
||||||
|
|
||||||
|
public static void Optimize(IList<Line> lines) =>
|
||||||
|
MergePass(lines,
|
||||||
|
(list, item, i) => list.GetCollinearLines(item, i),
|
||||||
|
(Line a, Line b, out Line joined) => TryJoinLines(a, b, out joined));
|
||||||
|
|
||||||
|
private delegate bool TryJoin<T>(T a, T b, out T joined);
|
||||||
|
|
||||||
|
private static void MergePass<T>(IList<T> items,
|
||||||
|
Func<IList<T>, T, int, List<T>> findCandidates,
|
||||||
|
TryJoin<T> tryJoin) where T : class
|
||||||
|
{
|
||||||
|
for (var i = 0; i < items.Count; ++i)
|
||||||
|
{
|
||||||
|
var item = items[i];
|
||||||
|
var candidates = findCandidates(items, item, i);
|
||||||
|
var index = 0;
|
||||||
|
|
||||||
|
while (index < candidates.Count)
|
||||||
|
{
|
||||||
|
var candidate = candidates[index];
|
||||||
|
|
||||||
|
if (!tryJoin(item, candidate, out var joined))
|
||||||
|
{
|
||||||
|
index++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
candidates.Remove(candidate);
|
||||||
|
items.Remove(candidate);
|
||||||
|
|
||||||
|
item = joined;
|
||||||
|
index = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
items[i] = item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool TryJoinLines(Line line1, Line line2, out Line lineOut)
|
||||||
|
{
|
||||||
|
lineOut = null;
|
||||||
|
|
||||||
|
if (line1 == line2)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (line1.Layer?.Name != line2.Layer?.Name)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (!line1.IsCollinearTo(line2))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
bool onPoint = false;
|
||||||
|
|
||||||
|
if (line1.StartPoint == line2.StartPoint)
|
||||||
|
onPoint = true;
|
||||||
|
else if (line1.StartPoint == line2.EndPoint)
|
||||||
|
onPoint = true;
|
||||||
|
else if (line1.EndPoint == line2.StartPoint)
|
||||||
|
onPoint = true;
|
||||||
|
else if (line1.EndPoint == line2.EndPoint)
|
||||||
|
onPoint = true;
|
||||||
|
|
||||||
|
var t1 = line1.StartPoint.Y > line1.EndPoint.Y ? line1.StartPoint.Y : line1.EndPoint.Y;
|
||||||
|
var t2 = line2.StartPoint.Y > line2.EndPoint.Y ? line2.StartPoint.Y : line2.EndPoint.Y;
|
||||||
|
var b1 = line1.StartPoint.Y < line1.EndPoint.Y ? line1.StartPoint.Y : line1.EndPoint.Y;
|
||||||
|
var b2 = line2.StartPoint.Y < line2.EndPoint.Y ? line2.StartPoint.Y : line2.EndPoint.Y;
|
||||||
|
var l1 = line1.StartPoint.X < line1.EndPoint.X ? line1.StartPoint.X : line1.EndPoint.X;
|
||||||
|
var l2 = line2.StartPoint.X < line2.EndPoint.X ? line2.StartPoint.X : line2.EndPoint.X;
|
||||||
|
var r1 = line1.StartPoint.X > line1.EndPoint.X ? line1.StartPoint.X : line1.EndPoint.X;
|
||||||
|
var r2 = line2.StartPoint.X > line2.EndPoint.X ? line2.StartPoint.X : line2.EndPoint.X;
|
||||||
|
|
||||||
|
if (!onPoint)
|
||||||
|
{
|
||||||
|
if (t1 < b2 - Tolerance.Epsilon) return false;
|
||||||
|
if (b1 > t2 + Tolerance.Epsilon) return false;
|
||||||
|
if (l1 > r2 + Tolerance.Epsilon) return false;
|
||||||
|
if (r1 < l2 - Tolerance.Epsilon) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var l = l1 < l2 ? l1 : l2;
|
||||||
|
var r = r1 > r2 ? r1 : r2;
|
||||||
|
var t = t1 > t2 ? t1 : t2;
|
||||||
|
var b = b1 < b2 ? b1 : b2;
|
||||||
|
|
||||||
|
if (!line1.IsVertical() && line1.Slope() < 0)
|
||||||
|
lineOut = new Line(new Vector(l, t), new Vector(r, b)) { Layer = line1.Layer, Color = line1.Color };
|
||||||
|
else
|
||||||
|
lineOut = new Line(new Vector(l, b), new Vector(r, t)) { Layer = line1.Layer, Color = line1.Color };
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool TryJoinArcs(Arc arc1, Arc arc2, out Arc arcOut)
|
||||||
|
{
|
||||||
|
arcOut = null;
|
||||||
|
|
||||||
|
if (arc1 == arc2)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (arc1.Layer?.Name != arc2.Layer?.Name)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (arc1.Center != arc2.Center)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (!arc1.Radius.IsEqualTo(arc2.Radius))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var start1 = arc1.StartAngle;
|
||||||
|
var end1 = arc1.EndAngle;
|
||||||
|
var start2 = arc2.StartAngle;
|
||||||
|
var end2 = arc2.EndAngle;
|
||||||
|
|
||||||
|
if (start1 > end1)
|
||||||
|
start1 -= Angle.TwoPI;
|
||||||
|
|
||||||
|
if (start2 > end2)
|
||||||
|
start2 -= Angle.TwoPI;
|
||||||
|
|
||||||
|
// Check that arcs are adjacent (endpoints touch), not overlapping
|
||||||
|
var touch1 = end1.IsEqualTo(start2) || (end1 + Angle.TwoPI).IsEqualTo(start2);
|
||||||
|
var touch2 = end2.IsEqualTo(start1) || (end2 + Angle.TwoPI).IsEqualTo(start1);
|
||||||
|
if (!touch1 && !touch2)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var startAngle = start1 < start2 ? start1 : start2;
|
||||||
|
var endAngle = end1 > end2 ? end1 : end2;
|
||||||
|
|
||||||
|
// Don't merge if the result would be a full circle (start == end)
|
||||||
|
var sweep = endAngle - startAngle;
|
||||||
|
if (sweep >= Angle.TwoPI - Tolerance.Epsilon)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (startAngle < 0) startAngle += Angle.TwoPI;
|
||||||
|
if (endAngle < 0) endAngle += Angle.TwoPI;
|
||||||
|
|
||||||
|
arcOut = new Arc(arc1.Center, arc1.Radius, startAngle, endAngle) { Layer = arc1.Layer, Color = arc1.Color };
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<Line> GetCollinearLines(this IList<Line> lines, Line line, int startIndex)
|
||||||
|
{
|
||||||
|
var collinearLines = new List<Line>();
|
||||||
|
|
||||||
|
Parallel.For(startIndex, lines.Count, index =>
|
||||||
|
{
|
||||||
|
var compareLine = lines[index];
|
||||||
|
|
||||||
|
if (Object.ReferenceEquals(line, compareLine))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!line.IsCollinearTo(compareLine))
|
||||||
|
return;
|
||||||
|
|
||||||
|
lock (collinearLines)
|
||||||
|
{
|
||||||
|
collinearLines.Add(compareLine);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return collinearLines;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<Arc> GetCoradialArs(this IList<Arc> arcs, Arc arc, int startIndex)
|
||||||
|
{
|
||||||
|
var coradialArcs = new List<Arc>();
|
||||||
|
|
||||||
|
Parallel.For(startIndex, arcs.Count, index =>
|
||||||
|
{
|
||||||
|
var compareArc = arcs[index];
|
||||||
|
|
||||||
|
if (Object.ReferenceEquals(arc, compareArc))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!arc.IsCoradialTo(compareArc))
|
||||||
|
return;
|
||||||
|
|
||||||
|
lock (coradialArcs)
|
||||||
|
{
|
||||||
|
coradialArcs.Add(compareArc);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return coradialArcs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,648 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using OpenNest.Math;
|
||||||
|
|
||||||
|
namespace OpenNest.Geometry;
|
||||||
|
|
||||||
|
public class ArcCandidate
|
||||||
|
{
|
||||||
|
public int ShapeIndex { get; set; }
|
||||||
|
public int StartIndex { get; set; }
|
||||||
|
public int EndIndex { get; set; }
|
||||||
|
public int LineCount => EndIndex - StartIndex + 1;
|
||||||
|
public Arc FittedArc { get; set; }
|
||||||
|
public double MaxDeviation { get; set; }
|
||||||
|
public Box BoundingBox { get; set; }
|
||||||
|
public bool IsSelected { get; set; } = true;
|
||||||
|
/// <summary>First point of the original line segments this candidate covers.</summary>
|
||||||
|
public Vector FirstPoint { get; set; }
|
||||||
|
/// <summary>Last point of the original line segments this candidate covers.</summary>
|
||||||
|
public Vector LastPoint { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A mirror axis defined by a point on the axis and a unit direction vector.
|
||||||
|
/// </summary>
|
||||||
|
public class MirrorAxisResult
|
||||||
|
{
|
||||||
|
public static readonly MirrorAxisResult None = new(Vector.Invalid, Vector.Invalid, 0);
|
||||||
|
|
||||||
|
public Vector Point { get; }
|
||||||
|
public Vector Direction { get; }
|
||||||
|
public double Score { get; }
|
||||||
|
public bool IsValid => Point.IsValid();
|
||||||
|
|
||||||
|
public MirrorAxisResult(Vector point, Vector direction, double score)
|
||||||
|
{
|
||||||
|
Point = point;
|
||||||
|
Direction = direction;
|
||||||
|
Score = score;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Reflects a point across this axis.</summary>
|
||||||
|
public Vector Reflect(Vector p)
|
||||||
|
{
|
||||||
|
var dx = p.X - Point.X;
|
||||||
|
var dy = p.Y - Point.Y;
|
||||||
|
var dot = dx * Direction.X + dy * Direction.Y;
|
||||||
|
return new Vector(
|
||||||
|
p.X - 2 * (dx - dot * Direction.X),
|
||||||
|
p.Y - 2 * (dy - dot * Direction.Y));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class GeometrySimplifier
|
||||||
|
{
|
||||||
|
public double Tolerance { get; set; } = 0.004;
|
||||||
|
public int MinLines { get; set; } = 3;
|
||||||
|
|
||||||
|
public List<ArcCandidate> Analyze(Shape shape)
|
||||||
|
{
|
||||||
|
var candidates = new List<ArcCandidate>();
|
||||||
|
var entities = shape.Entities;
|
||||||
|
var i = 0;
|
||||||
|
|
||||||
|
while (i < entities.Count)
|
||||||
|
{
|
||||||
|
if (entities[i] is not Line and not Arc)
|
||||||
|
{
|
||||||
|
i++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var runStart = i;
|
||||||
|
var layerName = entities[i].Layer?.Name;
|
||||||
|
var lineCount = 0;
|
||||||
|
while (i < entities.Count && (entities[i] is Line || entities[i] is Arc) && entities[i].Layer?.Name == layerName)
|
||||||
|
{
|
||||||
|
if (entities[i] is Line) lineCount++;
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
var runEnd = i - 1;
|
||||||
|
|
||||||
|
if (lineCount >= MinLines)
|
||||||
|
FindCandidatesInRun(entities, runStart, runEnd, candidates);
|
||||||
|
}
|
||||||
|
|
||||||
|
return candidates;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Shape Apply(Shape shape, List<ArcCandidate> candidates)
|
||||||
|
{
|
||||||
|
var selected = candidates
|
||||||
|
.Where(c => c.IsSelected)
|
||||||
|
.OrderBy(c => c.StartIndex)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var newEntities = new List<Entity>();
|
||||||
|
var i = 0;
|
||||||
|
|
||||||
|
foreach (var candidate in selected)
|
||||||
|
{
|
||||||
|
while (i < candidate.StartIndex)
|
||||||
|
{
|
||||||
|
newEntities.Add(shape.Entities[i]);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
newEntities.Add(candidate.FittedArc);
|
||||||
|
i = candidate.EndIndex + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (i < shape.Entities.Count)
|
||||||
|
{
|
||||||
|
newEntities.Add(shape.Entities[i]);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = new Shape();
|
||||||
|
result.Entities.AddRange(newEntities);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Detects the mirror axis of a shape by testing candidate axes through the
|
||||||
|
/// centroid. Uses PCA to find principal directions, then also tests horizontal
|
||||||
|
/// and vertical. Works for shapes rotated at any angle.
|
||||||
|
/// </summary>
|
||||||
|
public static MirrorAxisResult DetectMirrorAxis(Shape shape)
|
||||||
|
{
|
||||||
|
var midpoints = new List<Vector>();
|
||||||
|
foreach (var e in shape.Entities)
|
||||||
|
midpoints.Add(e.BoundingBox.Center);
|
||||||
|
|
||||||
|
if (midpoints.Count < 4) return MirrorAxisResult.None;
|
||||||
|
|
||||||
|
var centroid = new Vector(
|
||||||
|
midpoints.Average(p => p.X),
|
||||||
|
midpoints.Average(p => p.Y));
|
||||||
|
var cx = centroid.X;
|
||||||
|
var cy = centroid.Y;
|
||||||
|
|
||||||
|
// Covariance matrix for PCA
|
||||||
|
var cxx = 0.0;
|
||||||
|
var cxy = 0.0;
|
||||||
|
var cyy = 0.0;
|
||||||
|
foreach (var p in midpoints)
|
||||||
|
{
|
||||||
|
var dx = p.X - cx;
|
||||||
|
var dy = p.Y - cy;
|
||||||
|
cxx += dx * dx;
|
||||||
|
cxy += dx * dy;
|
||||||
|
cyy += dy * dy;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Eigenvectors of 2x2 symmetric matrix via analytic formula
|
||||||
|
var trace = cxx + cyy;
|
||||||
|
var det = cxx * cyy - cxy * cxy;
|
||||||
|
var disc = System.Math.Sqrt(System.Math.Max(0, trace * trace / 4 - det));
|
||||||
|
var lambda1 = trace / 2 + disc;
|
||||||
|
var lambda2 = trace / 2 - disc;
|
||||||
|
|
||||||
|
var candidates = new List<Vector>();
|
||||||
|
|
||||||
|
// PCA eigenvectors (major and minor axes)
|
||||||
|
if (System.Math.Abs(cxy) > 1e-10)
|
||||||
|
{
|
||||||
|
candidates.Add(Normalize(new Vector(lambda1 - cyy, cxy)));
|
||||||
|
candidates.Add(Normalize(new Vector(lambda2 - cyy, cxy)));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
candidates.Add(new Vector(1, 0));
|
||||||
|
candidates.Add(new Vector(0, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also always test pure horizontal and vertical
|
||||||
|
candidates.Add(new Vector(1, 0));
|
||||||
|
candidates.Add(new Vector(0, 1));
|
||||||
|
|
||||||
|
// Score each candidate axis
|
||||||
|
var bestResult = MirrorAxisResult.None;
|
||||||
|
foreach (var dir in candidates)
|
||||||
|
{
|
||||||
|
var score = MirrorMatchScore(midpoints, centroid, dir);
|
||||||
|
if (score > bestResult.Score)
|
||||||
|
bestResult = new MirrorAxisResult(centroid, dir, score);
|
||||||
|
}
|
||||||
|
|
||||||
|
return bestResult.Score >= 0.8 ? bestResult : MirrorAxisResult.None;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double NormalizeAngle(double angle) =>
|
||||||
|
angle < 0 ? angle + Angle.TwoPI : angle;
|
||||||
|
|
||||||
|
private static Vector Normalize(Vector v)
|
||||||
|
{
|
||||||
|
var len = System.Math.Sqrt(v.X * v.X + v.Y * v.Y);
|
||||||
|
return len < 1e-10 ? new Vector(1, 0) : new Vector(v.X / len, v.Y / len);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double PerpendicularDistance(Vector point, Vector axisPoint, Vector axisDir)
|
||||||
|
{
|
||||||
|
var dx = point.X - axisPoint.X;
|
||||||
|
var dy = point.Y - axisPoint.Y;
|
||||||
|
var dot = dx * axisDir.X + dy * axisDir.Y;
|
||||||
|
var px = dx - dot * axisDir.X;
|
||||||
|
var py = dy - dot * axisDir.Y;
|
||||||
|
return System.Math.Sqrt(px * px + py * py);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double MirrorMatchScore(List<Vector> points, Vector axisPoint, Vector axisDir)
|
||||||
|
{
|
||||||
|
var matchTol = 0.1;
|
||||||
|
var matched = 0;
|
||||||
|
|
||||||
|
for (var i = 0; i < points.Count; i++)
|
||||||
|
{
|
||||||
|
var p = points[i];
|
||||||
|
var dist = PerpendicularDistance(p, axisPoint, axisDir);
|
||||||
|
|
||||||
|
// Points on the axis count as matched
|
||||||
|
if (dist < matchTol)
|
||||||
|
{
|
||||||
|
matched++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reflect across axis and look for partner
|
||||||
|
var reflected = new MirrorAxisResult(axisPoint, axisDir, 0).Reflect(p);
|
||||||
|
|
||||||
|
for (var j = 0; j < points.Count; j++)
|
||||||
|
{
|
||||||
|
if (i == j) continue;
|
||||||
|
var d = reflected.DistanceTo(points[j]);
|
||||||
|
if (d < matchTol)
|
||||||
|
{
|
||||||
|
matched++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (double)matched / points.Count;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Pairs candidates across a mirror axis and forces each pair to use
|
||||||
|
/// the same arc (mirrored). The candidate with more lines or lower
|
||||||
|
/// deviation is kept as the source.
|
||||||
|
/// </summary>
|
||||||
|
public void Symmetrize(List<ArcCandidate> candidates, MirrorAxisResult axis)
|
||||||
|
{
|
||||||
|
if (!axis.IsValid || candidates.Count < 2) return;
|
||||||
|
|
||||||
|
var paired = new HashSet<int>();
|
||||||
|
|
||||||
|
for (var i = 0; i < candidates.Count; i++)
|
||||||
|
{
|
||||||
|
if (paired.Contains(i)) continue;
|
||||||
|
|
||||||
|
var ci = candidates[i];
|
||||||
|
var ciCenter = ci.BoundingBox.Center;
|
||||||
|
if (PerpendicularDistance(ciCenter, axis.Point, axis.Direction) < 0.1) continue; // on the axis
|
||||||
|
|
||||||
|
var mirrorCenter = axis.Reflect(ciCenter);
|
||||||
|
|
||||||
|
var bestJ = -1;
|
||||||
|
var bestDist = double.MaxValue;
|
||||||
|
for (var j = i + 1; j < candidates.Count; j++)
|
||||||
|
{
|
||||||
|
if (paired.Contains(j)) continue;
|
||||||
|
var d = mirrorCenter.DistanceTo(candidates[j].BoundingBox.Center);
|
||||||
|
if (d < bestDist)
|
||||||
|
{
|
||||||
|
bestDist = d;
|
||||||
|
bestJ = j;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var matchTol = System.Math.Max(ci.BoundingBox.Width, ci.BoundingBox.Length) * 0.5;
|
||||||
|
if (bestJ < 0 || bestDist > matchTol) continue;
|
||||||
|
|
||||||
|
paired.Add(i);
|
||||||
|
paired.Add(bestJ);
|
||||||
|
|
||||||
|
var cj = candidates[bestJ];
|
||||||
|
var sourceIdx = i;
|
||||||
|
var targetIdx = bestJ;
|
||||||
|
if (cj.LineCount > ci.LineCount || (cj.LineCount == ci.LineCount && cj.MaxDeviation < ci.MaxDeviation))
|
||||||
|
{
|
||||||
|
sourceIdx = bestJ;
|
||||||
|
targetIdx = i;
|
||||||
|
}
|
||||||
|
|
||||||
|
var source = candidates[sourceIdx];
|
||||||
|
var target = candidates[targetIdx];
|
||||||
|
var mirrored = MirrorArc(source.FittedArc, axis);
|
||||||
|
|
||||||
|
// Only apply the mirrored arc if its endpoints are close enough to the
|
||||||
|
// target's actual boundary points. Otherwise the mirror introduces gaps.
|
||||||
|
var mirroredStart = mirrored.StartPoint();
|
||||||
|
var mirroredEnd = mirrored.EndPoint();
|
||||||
|
var startDist = mirroredStart.DistanceTo(target.FirstPoint);
|
||||||
|
var endDist = mirroredEnd.DistanceTo(target.LastPoint);
|
||||||
|
|
||||||
|
if (startDist <= Tolerance && endDist <= Tolerance)
|
||||||
|
{
|
||||||
|
target.FittedArc = mirrored;
|
||||||
|
target.MaxDeviation = source.MaxDeviation;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Arc MirrorArc(Arc arc, MirrorAxisResult axis)
|
||||||
|
{
|
||||||
|
var mirrorCenter = axis.Reflect(arc.Center);
|
||||||
|
|
||||||
|
// Reflect start and end points, then compute new angles
|
||||||
|
var sp = arc.StartPoint();
|
||||||
|
var ep = arc.EndPoint();
|
||||||
|
var mirrorSp = axis.Reflect(sp);
|
||||||
|
var mirrorEp = axis.Reflect(ep);
|
||||||
|
|
||||||
|
// Mirroring reverses winding — swap start/end to preserve arc direction
|
||||||
|
var mirrorStart = NormalizeAngle(System.Math.Atan2(mirrorEp.Y - mirrorCenter.Y, mirrorEp.X - mirrorCenter.X));
|
||||||
|
var mirrorEnd = NormalizeAngle(System.Math.Atan2(mirrorSp.Y - mirrorCenter.Y, mirrorSp.X - mirrorCenter.X));
|
||||||
|
|
||||||
|
var result = new Arc(mirrorCenter, arc.Radius, mirrorStart, mirrorEnd, arc.IsReversed);
|
||||||
|
result.Layer = arc.Layer;
|
||||||
|
result.Color = arc.Color;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void FindCandidatesInRun(List<Entity> entities, int runStart, int runEnd, List<ArcCandidate> candidates)
|
||||||
|
{
|
||||||
|
var j = runStart;
|
||||||
|
var chainedTangent = Vector.Invalid;
|
||||||
|
|
||||||
|
while (j <= runEnd - MinLines + 1)
|
||||||
|
{
|
||||||
|
var result = TryFitArcAt(entities, j, runEnd, chainedTangent);
|
||||||
|
if (result == null)
|
||||||
|
{
|
||||||
|
j++;
|
||||||
|
chainedTangent = Vector.Invalid;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
chainedTangent = ComputeEndTangent(result.Center, result.Points);
|
||||||
|
var arc = CreateArc(result.Center, result.Radius, result.Points, entities[j]);
|
||||||
|
candidates.Add(new ArcCandidate
|
||||||
|
{
|
||||||
|
StartIndex = j,
|
||||||
|
EndIndex = result.EndIndex,
|
||||||
|
FittedArc = arc,
|
||||||
|
MaxDeviation = result.Deviation,
|
||||||
|
BoundingBox = result.Points.GetBoundingBox(),
|
||||||
|
FirstPoint = arc.StartPoint(),
|
||||||
|
LastPoint = arc.EndPoint(),
|
||||||
|
});
|
||||||
|
|
||||||
|
j = result.EndIndex + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private record ArcFitResult(Vector Center, double Radius, double Deviation, List<Vector> Points, int EndIndex);
|
||||||
|
|
||||||
|
private ArcFitResult TryFitArcAt(List<Entity> entities, int start, int runEnd, Vector chainedTangent)
|
||||||
|
{
|
||||||
|
var k = start + MinLines - 1;
|
||||||
|
if (k > runEnd) return null;
|
||||||
|
|
||||||
|
var points = CollectPoints(entities, start, k);
|
||||||
|
if (points.Count < 3) return null;
|
||||||
|
|
||||||
|
var startTangent = chainedTangent.IsValid()
|
||||||
|
? chainedTangent
|
||||||
|
: new Vector(points[1].X - points[0].X, points[1].Y - points[0].Y);
|
||||||
|
|
||||||
|
var endTangent = GetExitDirection(entities[k]);
|
||||||
|
var (center, radius, dev) = TryFit(points, startTangent, endTangent);
|
||||||
|
if (!center.IsValid()) return null;
|
||||||
|
|
||||||
|
// Extend the arc as far as possible
|
||||||
|
while (k + 1 <= runEnd)
|
||||||
|
{
|
||||||
|
var extPoints = CollectPoints(entities, start, k + 1);
|
||||||
|
var extEndTangent = GetExitDirection(entities[k + 1]);
|
||||||
|
var (nc, nr, nd) = extPoints.Count >= 3 ? TryFit(extPoints, startTangent, extEndTangent) : (Vector.Invalid, 0, 0d);
|
||||||
|
if (!nc.IsValid()) break;
|
||||||
|
|
||||||
|
k++;
|
||||||
|
center = nc;
|
||||||
|
radius = nr;
|
||||||
|
dev = nd;
|
||||||
|
points = extPoints;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reject arcs that subtend a tiny angle — these are nearly-straight lines
|
||||||
|
// that happen to fit a huge circle. Applied after extension so that many small
|
||||||
|
// segments can accumulate enough sweep to qualify.
|
||||||
|
var sweep = System.Math.Abs(SumSignedAngles(center, points));
|
||||||
|
if (sweep < Angle.ToRadians(5))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return new ArcFitResult(center, radius, dev, points, k);
|
||||||
|
}
|
||||||
|
|
||||||
|
private (Vector center, double radius, double deviation) TryFit(List<Vector> points, Vector startTangent, Vector endTangent)
|
||||||
|
{
|
||||||
|
// Try dual-tangent fit first (matches direction at both endpoints)
|
||||||
|
if (endTangent.IsValid())
|
||||||
|
{
|
||||||
|
var (dc, dr, dd) = ArcFit.FitWithDualTangent(points, startTangent, endTangent);
|
||||||
|
if (dc.IsValid() && dd <= Tolerance)
|
||||||
|
{
|
||||||
|
var isRev = SumSignedAngles(dc, points) < 0;
|
||||||
|
var aDev = MaxArcToSegmentDeviation(points, dc, dr, isRev);
|
||||||
|
if (aDev <= Tolerance)
|
||||||
|
return (dc, dr, System.Math.Max(dd, aDev));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back to start-tangent-only, then mirror axis
|
||||||
|
var (center, radius, dev) = ArcFit.FitWithStartTangent(points, startTangent);
|
||||||
|
if (!center.IsValid() || dev > Tolerance)
|
||||||
|
(center, radius, dev) = FitMirrorAxis(points);
|
||||||
|
if (!center.IsValid() || dev > Tolerance)
|
||||||
|
return (Vector.Invalid, 0, 0);
|
||||||
|
|
||||||
|
// Check that the arc doesn't bulge away from the original line segments
|
||||||
|
var isReversed = SumSignedAngles(center, points) < 0;
|
||||||
|
var arcDev = MaxArcToSegmentDeviation(points, center, radius, isReversed);
|
||||||
|
if (arcDev > Tolerance)
|
||||||
|
return (Vector.Invalid, 0, 0);
|
||||||
|
|
||||||
|
return (center, radius, System.Math.Max(dev, arcDev));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Computes the tangent direction at the last point of a fitted arc,
|
||||||
|
/// used to chain tangent continuity to the next arc.
|
||||||
|
/// </summary>
|
||||||
|
private static Vector ComputeEndTangent(Vector center, List<Vector> points)
|
||||||
|
{
|
||||||
|
var lastPt = points[^1];
|
||||||
|
var rx = lastPt.X - center.X;
|
||||||
|
var ry = lastPt.Y - center.Y;
|
||||||
|
var sign = SumSignedAngles(center, points) >= 0 ? 1 : -1;
|
||||||
|
return new Vector(-sign * ry, sign * rx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fits a circular arc using the mirror axis approach. The center is constrained
|
||||||
|
/// to the perpendicular bisector of the chord (P1->Pn), guaranteeing the arc
|
||||||
|
/// passes exactly through both endpoints. Golden section search optimizes position.
|
||||||
|
/// </summary>
|
||||||
|
private (Vector center, double radius, double deviation) FitMirrorAxis(List<Vector> points)
|
||||||
|
{
|
||||||
|
if (points.Count < 3)
|
||||||
|
return (Vector.Invalid, 0, double.MaxValue);
|
||||||
|
|
||||||
|
var p1 = points[0];
|
||||||
|
var pn = points[^1];
|
||||||
|
var mx = (p1.X + pn.X) / 2;
|
||||||
|
var my = (p1.Y + pn.Y) / 2;
|
||||||
|
var dx = pn.X - p1.X;
|
||||||
|
var dy = pn.Y - p1.Y;
|
||||||
|
var chordLen = System.Math.Sqrt(dx * dx + dy * dy);
|
||||||
|
if (chordLen < 1e-10)
|
||||||
|
return (Vector.Invalid, 0, double.MaxValue);
|
||||||
|
|
||||||
|
var halfChord = chordLen / 2;
|
||||||
|
var nx = -dy / chordLen;
|
||||||
|
var ny = dx / chordLen;
|
||||||
|
|
||||||
|
var maxSagitta = 0.0;
|
||||||
|
for (var i = 1; i < points.Count - 1; i++)
|
||||||
|
{
|
||||||
|
var proj = (points[i].X - mx) * nx + (points[i].Y - my) * ny;
|
||||||
|
if (System.Math.Abs(proj) > System.Math.Abs(maxSagitta))
|
||||||
|
maxSagitta = proj;
|
||||||
|
}
|
||||||
|
if (System.Math.Abs(maxSagitta) < 1e-10)
|
||||||
|
return (Vector.Invalid, 0, double.MaxValue);
|
||||||
|
|
||||||
|
var dInit = (maxSagitta * maxSagitta - halfChord * halfChord) / (2 * maxSagitta);
|
||||||
|
var range = System.Math.Max(System.Math.Abs(dInit) * 2, halfChord);
|
||||||
|
|
||||||
|
var dOpt = GoldenSectionMin(dInit - range, dInit + range,
|
||||||
|
d => ArcFit.MaxRadialDeviation(points, mx + d * nx, my + d * ny,
|
||||||
|
System.Math.Sqrt(halfChord * halfChord + d * d)));
|
||||||
|
|
||||||
|
var center = new Vector(mx + dOpt * nx, my + dOpt * ny);
|
||||||
|
var radius = System.Math.Sqrt(halfChord * halfChord + dOpt * dOpt);
|
||||||
|
return (center, radius, ArcFit.MaxRadialDeviation(points, center.X, center.Y, radius));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double GoldenSectionMin(double low, double high, Func<double, double> eval)
|
||||||
|
{
|
||||||
|
var phi = (System.Math.Sqrt(5) - 1) / 2;
|
||||||
|
for (var iter = 0; iter < 30; iter++)
|
||||||
|
{
|
||||||
|
var d1 = high - phi * (high - low);
|
||||||
|
var d2 = low + phi * (high - low);
|
||||||
|
if (eval(d1) < eval(d2))
|
||||||
|
high = d2;
|
||||||
|
else
|
||||||
|
low = d1;
|
||||||
|
if (high - low < 1e-6)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return (low + high) / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<Vector> CollectPoints(List<Entity> entities, int start, int end)
|
||||||
|
{
|
||||||
|
var points = new List<Vector>();
|
||||||
|
|
||||||
|
for (var i = start; i <= end; i++)
|
||||||
|
{
|
||||||
|
switch (entities[i])
|
||||||
|
{
|
||||||
|
case Line line:
|
||||||
|
if (i == start)
|
||||||
|
points.Add(line.StartPoint);
|
||||||
|
points.Add(line.EndPoint);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Arc arc:
|
||||||
|
if (i == start)
|
||||||
|
points.Add(arc.StartPoint());
|
||||||
|
var segments = System.Math.Max(2, arc.SegmentsForTolerance(0.1));
|
||||||
|
var arcPoints = arc.ToPoints(segments);
|
||||||
|
for (var j = 1; j < arcPoints.Count; j++)
|
||||||
|
points.Add(arcPoints[j]);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return points;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Arc CreateArc(Vector center, double radius, List<Vector> points, Entity sourceEntity)
|
||||||
|
{
|
||||||
|
var firstPoint = points[0];
|
||||||
|
var lastPoint = points[^1];
|
||||||
|
|
||||||
|
var startAngle = NormalizeAngle(System.Math.Atan2(firstPoint.Y - center.Y, firstPoint.X - center.X));
|
||||||
|
var endAngle = NormalizeAngle(System.Math.Atan2(lastPoint.Y - center.Y, lastPoint.X - center.X));
|
||||||
|
var isReversed = SumSignedAngles(center, points) < 0;
|
||||||
|
|
||||||
|
var arc = new Arc(center, radius, startAngle, endAngle, isReversed);
|
||||||
|
arc.Layer = sourceEntity.Layer;
|
||||||
|
arc.Color = sourceEntity.Color;
|
||||||
|
return arc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the exit direction (tangent at endpoint) of an entity.
|
||||||
|
/// </summary>
|
||||||
|
private static Vector GetExitDirection(Entity entity) => entity switch
|
||||||
|
{
|
||||||
|
Line line => new Vector(line.EndPoint.X - line.StartPoint.X, line.EndPoint.Y - line.StartPoint.Y),
|
||||||
|
Arc arc => arc.IsReversed
|
||||||
|
? new Vector(System.Math.Sin(arc.EndAngle), -System.Math.Cos(arc.EndAngle))
|
||||||
|
: new Vector(-System.Math.Sin(arc.EndAngle), System.Math.Cos(arc.EndAngle)),
|
||||||
|
_ => Vector.Invalid,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sums signed angular change traversing consecutive points around a center.
|
||||||
|
/// Positive = CCW, negative = CW.
|
||||||
|
/// </summary>
|
||||||
|
private static double SumSignedAngles(Vector center, List<Vector> points)
|
||||||
|
{
|
||||||
|
var total = 0.0;
|
||||||
|
for (var i = 0; i < points.Count - 1; i++)
|
||||||
|
{
|
||||||
|
var a1 = System.Math.Atan2(points[i].Y - center.Y, points[i].X - center.X);
|
||||||
|
var a2 = System.Math.Atan2(points[i + 1].Y - center.Y, points[i + 1].X - center.X);
|
||||||
|
var da = a2 - a1;
|
||||||
|
while (da > System.Math.PI) da -= Angle.TwoPI;
|
||||||
|
while (da < -System.Math.PI) da += Angle.TwoPI;
|
||||||
|
total += da;
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Measures the maximum distance from sampled points along the fitted arc
|
||||||
|
/// back to the original line segments. This catches cases where points lie
|
||||||
|
/// on a large circle but the arc bulges far from the original straight geometry.
|
||||||
|
/// </summary>
|
||||||
|
private static double MaxArcToSegmentDeviation(List<Vector> points, Vector center, double radius, bool isReversed)
|
||||||
|
{
|
||||||
|
var startAngle = System.Math.Atan2(points[0].Y - center.Y, points[0].X - center.X);
|
||||||
|
var endAngle = System.Math.Atan2(points[^1].Y - center.Y, points[^1].X - center.X);
|
||||||
|
|
||||||
|
var sweep = endAngle - startAngle;
|
||||||
|
if (isReversed)
|
||||||
|
{
|
||||||
|
if (sweep > 0) sweep -= Angle.TwoPI;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (sweep < 0) sweep += Angle.TwoPI;
|
||||||
|
}
|
||||||
|
|
||||||
|
var sampleCount = System.Math.Max(10, (int)(System.Math.Abs(sweep) * radius * 10));
|
||||||
|
sampleCount = System.Math.Min(sampleCount, 100);
|
||||||
|
|
||||||
|
var maxDev = 0.0;
|
||||||
|
for (var i = 1; i < sampleCount; i++)
|
||||||
|
{
|
||||||
|
var t = (double)i / sampleCount;
|
||||||
|
var angle = startAngle + sweep * t;
|
||||||
|
var px = center.X + radius * System.Math.Cos(angle);
|
||||||
|
var py = center.Y + radius * System.Math.Sin(angle);
|
||||||
|
var arcPt = new Vector(px, py);
|
||||||
|
|
||||||
|
var minDist = double.MaxValue;
|
||||||
|
for (var j = 0; j < points.Count - 1; j++)
|
||||||
|
{
|
||||||
|
var dist = DistanceToSegment(arcPt, points[j], points[j + 1]);
|
||||||
|
if (dist < minDist) minDist = dist;
|
||||||
|
}
|
||||||
|
if (minDist > maxDev) maxDev = minDist;
|
||||||
|
}
|
||||||
|
return maxDev;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double DistanceToSegment(Vector p, Vector a, Vector b)
|
||||||
|
{
|
||||||
|
var dx = b.X - a.X;
|
||||||
|
var dy = b.Y - a.Y;
|
||||||
|
var lenSq = dx * dx + dy * dy;
|
||||||
|
if (lenSq < 1e-20)
|
||||||
|
return System.Math.Sqrt((p.X - a.X) * (p.X - a.X) + (p.Y - a.Y) * (p.Y - a.Y));
|
||||||
|
|
||||||
|
var t = ((p.X - a.X) * dx + (p.Y - a.Y) * dy) / lenSq;
|
||||||
|
t = System.Math.Max(0, System.Math.Min(1, t));
|
||||||
|
var projX = a.X + t * dx;
|
||||||
|
var projY = a.Y + t * dy;
|
||||||
|
return System.Math.Sqrt((p.X - projX) * (p.X - projX) + (p.Y - projY) * (p.Y - projY));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -52,6 +52,7 @@ namespace OpenNest.Geometry
|
|||||||
result.Vertices.Add(new Vector(ifpRight, ifpTop));
|
result.Vertices.Add(new Vector(ifpRight, ifpTop));
|
||||||
result.Vertices.Add(new Vector(ifpLeft, ifpTop));
|
result.Vertices.Add(new Vector(ifpLeft, ifpTop));
|
||||||
result.Close();
|
result.Close();
|
||||||
|
result.UpdateBounds();
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@@ -62,36 +63,20 @@ namespace OpenNest.Geometry
|
|||||||
/// Returns the polygon representing valid placement positions, or an empty
|
/// Returns the polygon representing valid placement positions, or an empty
|
||||||
/// polygon if no valid position exists.
|
/// polygon if no valid position exists.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static Polygon ComputeFeasibleRegion(Polygon ifp, Polygon[] nfps)
|
public static Polygon ComputeFeasibleRegion(Polygon ifp, PathsD nfpPaths)
|
||||||
{
|
{
|
||||||
if (ifp.Vertices.Count < 3)
|
if (ifp.Vertices.Count < 3)
|
||||||
return new Polygon();
|
return new Polygon();
|
||||||
|
|
||||||
if (nfps == null || nfps.Length == 0)
|
if (nfpPaths == null || nfpPaths.Count == 0)
|
||||||
return ifp;
|
return ifp;
|
||||||
|
|
||||||
var ifpPath = NoFitPolygon.ToClipperPath(ifp);
|
var ifpPath = NoFitPolygon.ToClipperPath(ifp);
|
||||||
var ifpPaths = new PathsD { ifpPath };
|
var ifpPaths = new PathsD { ifpPath };
|
||||||
|
|
||||||
// Union all NFPs.
|
// Subtract the NFPs from the IFP.
|
||||||
var nfpPaths = new PathsD();
|
// Clipper2 handles the implicit union of the clip paths.
|
||||||
|
var feasible = Clipper.Difference(ifpPaths, nfpPaths, FillRule.NonZero);
|
||||||
foreach (var nfp in nfps)
|
|
||||||
{
|
|
||||||
if (nfp.Vertices.Count >= 3)
|
|
||||||
{
|
|
||||||
var path = NoFitPolygon.ToClipperPath(nfp);
|
|
||||||
nfpPaths.Add(path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nfpPaths.Count == 0)
|
|
||||||
return ifp;
|
|
||||||
|
|
||||||
var nfpUnion = Clipper.Union(nfpPaths, FillRule.NonZero);
|
|
||||||
|
|
||||||
// Subtract the NFP union from the IFP.
|
|
||||||
var feasible = Clipper.Difference(ifpPaths, nfpUnion, FillRule.NonZero);
|
|
||||||
|
|
||||||
if (feasible.Count == 0)
|
if (feasible.Count == 0)
|
||||||
return new Polygon();
|
return new Polygon();
|
||||||
@@ -118,6 +103,25 @@ namespace OpenNest.Geometry
|
|||||||
return bestPath != null ? NoFitPolygon.FromClipperPath(bestPath) : new Polygon();
|
return bestPath != null ? NoFitPolygon.FromClipperPath(bestPath) : new Polygon();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Computes the feasible region for placing a part given already-placed parts.
|
||||||
|
/// (Legacy overload for backward compatibility).
|
||||||
|
/// </summary>
|
||||||
|
public static Polygon ComputeFeasibleRegion(Polygon ifp, Polygon[] nfps)
|
||||||
|
{
|
||||||
|
if (nfps == null || nfps.Length == 0)
|
||||||
|
return ifp;
|
||||||
|
|
||||||
|
var nfpPaths = new PathsD(nfps.Length);
|
||||||
|
foreach (var nfp in nfps)
|
||||||
|
{
|
||||||
|
if (nfp.Vertices.Count >= 3)
|
||||||
|
nfpPaths.Add(NoFitPolygon.ToClipperPath(nfp));
|
||||||
|
}
|
||||||
|
|
||||||
|
return ComputeFeasibleRegion(ifp, nfpPaths);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Finds the bottom-left-most point on a polygon boundary.
|
/// Finds the bottom-left-most point on a polygon boundary.
|
||||||
/// "Bottom-left" means: minimize Y first, then minimize X.
|
/// "Bottom-left" means: minimize Y first, then minimize X.
|
||||||
|
|||||||
@@ -0,0 +1,380 @@
|
|||||||
|
using OpenNest.Math;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace OpenNest.Geometry
|
||||||
|
{
|
||||||
|
public static class Intersect
|
||||||
|
{
|
||||||
|
internal static bool Intersects(Arc arc1, Arc arc2, out List<Vector> pts)
|
||||||
|
{
|
||||||
|
var c1 = new Circle(arc1.Center, arc1.Radius);
|
||||||
|
var c2 = new Circle(arc2.Center, arc2.Radius);
|
||||||
|
|
||||||
|
if (!Intersects(c1, c2, out pts))
|
||||||
|
{
|
||||||
|
pts = new List<Vector>();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
pts = pts.Where(pt =>
|
||||||
|
Angle.IsBetweenRad(arc1.Center.AngleTo(pt), arc1.StartAngle, arc1.EndAngle, arc1.IsReversed) &&
|
||||||
|
Angle.IsBetweenRad(arc2.Center.AngleTo(pt), arc2.StartAngle, arc2.EndAngle, arc2.IsReversed))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
return pts.Count > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static bool Intersects(Arc arc, Circle circle, out List<Vector> pts)
|
||||||
|
{
|
||||||
|
var c1 = new Circle(arc.Center, arc.Radius);
|
||||||
|
|
||||||
|
if (!Intersects(c1, circle, out pts))
|
||||||
|
{
|
||||||
|
pts = new List<Vector>();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
pts = pts.Where(pt => Angle.IsBetweenRad(
|
||||||
|
arc.Center.AngleTo(pt),
|
||||||
|
arc.StartAngle,
|
||||||
|
arc.EndAngle,
|
||||||
|
arc.IsReversed)).ToList();
|
||||||
|
|
||||||
|
return pts.Count > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static bool Intersects(Arc arc, Line line, out List<Vector> pts)
|
||||||
|
{
|
||||||
|
var c1 = new Circle(arc.Center, arc.Radius);
|
||||||
|
|
||||||
|
if (!Intersects(c1, line, out pts))
|
||||||
|
{
|
||||||
|
pts = new List<Vector>();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
pts = pts.Where(pt => Angle.IsBetweenRad(
|
||||||
|
arc.Center.AngleTo(pt),
|
||||||
|
arc.StartAngle,
|
||||||
|
arc.EndAngle,
|
||||||
|
arc.IsReversed)).ToList();
|
||||||
|
|
||||||
|
return pts.Count > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static bool Intersects(Arc arc, Shape shape, out List<Vector> pts)
|
||||||
|
{
|
||||||
|
var pts2 = new List<Vector>();
|
||||||
|
|
||||||
|
foreach (var geo in shape.Entities)
|
||||||
|
{
|
||||||
|
List<Vector> pts3;
|
||||||
|
geo.Intersects(arc, out pts3);
|
||||||
|
pts2.AddRange(pts3);
|
||||||
|
}
|
||||||
|
|
||||||
|
pts = pts2.Where(pt => Angle.IsBetweenRad(
|
||||||
|
arc.Center.AngleTo(pt),
|
||||||
|
arc.StartAngle,
|
||||||
|
arc.EndAngle,
|
||||||
|
arc.IsReversed)).ToList();
|
||||||
|
|
||||||
|
return pts.Count > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static bool Intersects(Arc arc, Polygon polygon, out List<Vector> pts)
|
||||||
|
{
|
||||||
|
var pts2 = new List<Vector>();
|
||||||
|
var lines = polygon.ToLines();
|
||||||
|
|
||||||
|
foreach (var line in lines)
|
||||||
|
{
|
||||||
|
List<Vector> pts3;
|
||||||
|
Intersects(arc, line, out pts3);
|
||||||
|
pts2.AddRange(pts3);
|
||||||
|
}
|
||||||
|
|
||||||
|
pts = pts2.Where(pt => Angle.IsBetweenRad(
|
||||||
|
arc.Center.AngleTo(pt),
|
||||||
|
arc.StartAngle,
|
||||||
|
arc.EndAngle,
|
||||||
|
arc.IsReversed)).ToList();
|
||||||
|
|
||||||
|
return pts.Count > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static bool Intersects(Circle circle1, Circle circle2, out List<Vector> pts)
|
||||||
|
{
|
||||||
|
var distance = circle1.Center.DistanceTo(circle2.Center);
|
||||||
|
|
||||||
|
// check if circles are too far apart
|
||||||
|
if (distance > circle1.Radius + circle2.Radius)
|
||||||
|
{
|
||||||
|
pts = new List<Vector>();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if one circle contains the other
|
||||||
|
if (distance < System.Math.Abs(circle1.Radius - circle2.Radius))
|
||||||
|
{
|
||||||
|
pts = new List<Vector>();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var d = circle2.Center - circle1.Center;
|
||||||
|
var a = (circle1.Radius * circle1.Radius - circle2.Radius * circle2.Radius + distance * distance) / (2.0 * distance);
|
||||||
|
var h = System.Math.Sqrt(circle1.Radius * circle1.Radius - a * a);
|
||||||
|
|
||||||
|
var pt = new Vector(
|
||||||
|
circle1.Center.X + (a * d.X) / distance,
|
||||||
|
circle1.Center.Y + (a * d.Y) / distance);
|
||||||
|
|
||||||
|
var i1 = new Vector(
|
||||||
|
pt.X + (h * d.Y) / distance,
|
||||||
|
pt.Y - (h * d.X) / distance);
|
||||||
|
|
||||||
|
var i2 = new Vector(
|
||||||
|
pt.X - (h * d.Y) / distance,
|
||||||
|
pt.Y + (h * d.X) / distance);
|
||||||
|
|
||||||
|
pts = i1 != i2 ? new List<Vector> { i1, i2 } : new List<Vector> { i1 };
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static bool Intersects(Circle circle, Line line, out List<Vector> pts)
|
||||||
|
{
|
||||||
|
var d1 = line.EndPoint - line.StartPoint;
|
||||||
|
var d2 = line.StartPoint - circle.Center;
|
||||||
|
|
||||||
|
var a = d1.X * d1.X + d1.Y * d1.Y;
|
||||||
|
var b = (d1.X * d2.X + d1.Y * d2.Y) * 2;
|
||||||
|
var c = (d2.X * d2.X + d2.Y * d2.Y) - circle.Radius * circle.Radius;
|
||||||
|
|
||||||
|
var det = b * b - 4 * a * c;
|
||||||
|
|
||||||
|
if ((a <= Tolerance.Epsilon) || (det < 0))
|
||||||
|
{
|
||||||
|
pts = new List<Vector>();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
double t;
|
||||||
|
pts = new List<Vector>();
|
||||||
|
|
||||||
|
if (det.IsEqualTo(0))
|
||||||
|
{
|
||||||
|
t = -b / (2 * a);
|
||||||
|
var pt1 = new Vector(line.StartPoint.X + t * d1.X, line.StartPoint.Y + t * d1.Y);
|
||||||
|
|
||||||
|
if (line.BoundingBox.Contains(pt1))
|
||||||
|
pts.Add(pt1);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
t = (-b + System.Math.Sqrt(det)) / (2 * a);
|
||||||
|
var pt2 = new Vector(line.StartPoint.X + t * d1.X, line.StartPoint.Y + t * d1.Y);
|
||||||
|
|
||||||
|
if (line.BoundingBox.Contains(pt2))
|
||||||
|
pts.Add(pt2);
|
||||||
|
|
||||||
|
t = (-b - System.Math.Sqrt(det)) / (2 * a);
|
||||||
|
var pt3 = new Vector(line.StartPoint.X + t * d1.X, line.StartPoint.Y + t * d1.Y);
|
||||||
|
|
||||||
|
if (line.BoundingBox.Contains(pt3))
|
||||||
|
pts.Add(pt3);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static bool Intersects(Circle circle, Shape shape, out List<Vector> pts)
|
||||||
|
{
|
||||||
|
pts = new List<Vector>();
|
||||||
|
|
||||||
|
foreach (var geo in shape.Entities)
|
||||||
|
{
|
||||||
|
List<Vector> pts3;
|
||||||
|
geo.Intersects(circle, out pts3);
|
||||||
|
pts.AddRange(pts3);
|
||||||
|
}
|
||||||
|
|
||||||
|
return pts.Count > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static bool Intersects(Circle circle, Polygon polygon, out List<Vector> pts)
|
||||||
|
{
|
||||||
|
pts = new List<Vector>();
|
||||||
|
var lines = polygon.ToLines();
|
||||||
|
|
||||||
|
foreach (var line in lines)
|
||||||
|
{
|
||||||
|
List<Vector> pts3;
|
||||||
|
Intersects(circle, line, out pts3);
|
||||||
|
pts.AddRange(pts3);
|
||||||
|
}
|
||||||
|
|
||||||
|
return pts.Count > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static bool Intersects(Line line1, Line line2, out Vector pt)
|
||||||
|
{
|
||||||
|
if (!IntersectsUnbounded(line1, line2, out pt))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return line1.BoundingBox.Contains(pt) && line2.BoundingBox.Contains(pt);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static bool IntersectsUnbounded(Line line1, Line line2, out Vector pt)
|
||||||
|
{
|
||||||
|
var a1 = line1.EndPoint.Y - line1.StartPoint.Y;
|
||||||
|
var b1 = line1.StartPoint.X - line1.EndPoint.X;
|
||||||
|
var c1 = a1 * line1.StartPoint.X + b1 * line1.StartPoint.Y;
|
||||||
|
|
||||||
|
var a2 = line2.EndPoint.Y - line2.StartPoint.Y;
|
||||||
|
var b2 = line2.StartPoint.X - line2.EndPoint.X;
|
||||||
|
var c2 = a2 * line2.StartPoint.X + b2 * line2.StartPoint.Y;
|
||||||
|
|
||||||
|
var d = a1 * b2 - a2 * b1;
|
||||||
|
|
||||||
|
if (d.IsEqualTo(0.0))
|
||||||
|
{
|
||||||
|
pt = Vector.Zero;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var x = (b2 * c1 - b1 * c2) / d;
|
||||||
|
var y = (a1 * c2 - a2 * c1) / d;
|
||||||
|
|
||||||
|
pt = new Vector(x, y);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static bool Intersects(Line line, Shape shape, out List<Vector> pts)
|
||||||
|
{
|
||||||
|
pts = new List<Vector>();
|
||||||
|
|
||||||
|
foreach (var geo in shape.Entities)
|
||||||
|
{
|
||||||
|
if (geo.Intersects(line, out var pts3))
|
||||||
|
pts.AddRange(pts3);
|
||||||
|
}
|
||||||
|
|
||||||
|
return pts.Count > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static bool Intersects(Line line, Polygon polygon, out List<Vector> pts)
|
||||||
|
{
|
||||||
|
pts = new List<Vector>();
|
||||||
|
var lines = polygon.ToLines();
|
||||||
|
|
||||||
|
foreach (var line2 in lines)
|
||||||
|
{
|
||||||
|
Vector pt;
|
||||||
|
|
||||||
|
if (Intersects(line, line2, out pt))
|
||||||
|
pts.Add(pt);
|
||||||
|
}
|
||||||
|
|
||||||
|
return pts.Count > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static bool Intersects(Shape shape1, Shape shape2, out List<Vector> pts)
|
||||||
|
{
|
||||||
|
pts = new List<Vector>();
|
||||||
|
|
||||||
|
for (int i = 0; i < shape1.Entities.Count; i++)
|
||||||
|
{
|
||||||
|
var geo1 = shape1.Entities[i];
|
||||||
|
|
||||||
|
for (int j = 0; j < shape2.Entities.Count; j++)
|
||||||
|
{
|
||||||
|
List<Vector> pts2;
|
||||||
|
bool success = false;
|
||||||
|
|
||||||
|
var geo2 = shape2.Entities[j];
|
||||||
|
|
||||||
|
switch (geo2.Type)
|
||||||
|
{
|
||||||
|
case EntityType.Arc:
|
||||||
|
success = geo1.Intersects((Arc)geo2, out pts2);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case EntityType.Circle:
|
||||||
|
success = geo1.Intersects((Circle)geo2, out pts2);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case EntityType.Line:
|
||||||
|
success = geo1.Intersects((Line)geo2, out pts2);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case EntityType.Shape:
|
||||||
|
success = geo1.Intersects((Shape)geo2, out pts2);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case EntityType.Polygon:
|
||||||
|
success = geo1.Intersects((Polygon)geo2, out pts2);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (success)
|
||||||
|
pts.AddRange(pts2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return pts.Count > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static bool Intersects(Shape shape, Polygon polygon, out List<Vector> pts)
|
||||||
|
{
|
||||||
|
pts = new List<Vector>();
|
||||||
|
|
||||||
|
var lines = polygon.ToLines();
|
||||||
|
|
||||||
|
for (int i = 0; i < shape.Entities.Count; i++)
|
||||||
|
{
|
||||||
|
var geo = shape.Entities[i];
|
||||||
|
|
||||||
|
for (int j = 0; j < lines.Count; j++)
|
||||||
|
{
|
||||||
|
var line = lines[j];
|
||||||
|
|
||||||
|
List<Vector> pts2;
|
||||||
|
|
||||||
|
if (geo.Intersects(line, out pts2))
|
||||||
|
pts.AddRange(pts2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return pts.Count > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static bool Intersects(Polygon polygon1, Polygon polygon2, out List<Vector> pts)
|
||||||
|
{
|
||||||
|
pts = new List<Vector>();
|
||||||
|
|
||||||
|
var lines1 = polygon1.ToLines();
|
||||||
|
var lines2 = polygon2.ToLines();
|
||||||
|
|
||||||
|
for (int i = 0; i < lines1.Count; i++)
|
||||||
|
{
|
||||||
|
var line1 = lines1[i];
|
||||||
|
|
||||||
|
for (int j = 0; j < lines2.Count; j++)
|
||||||
|
{
|
||||||
|
var line2 = lines2[j];
|
||||||
|
Vector pt;
|
||||||
|
|
||||||
|
if (Intersects(line1, line2, out pt))
|
||||||
|
pts.Add(pt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return pts.Count > 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using System;
|
using OpenNest.Math;
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using OpenNest.Math;
|
|
||||||
|
|
||||||
namespace OpenNest.Geometry
|
namespace OpenNest.Geometry
|
||||||
{
|
{
|
||||||
@@ -370,23 +370,23 @@ namespace OpenNest.Geometry
|
|||||||
if (StartPoint.X < EndPoint.X)
|
if (StartPoint.X < EndPoint.X)
|
||||||
{
|
{
|
||||||
boundingBox.X = StartPoint.X;
|
boundingBox.X = StartPoint.X;
|
||||||
boundingBox.Width = EndPoint.X - StartPoint.X;
|
boundingBox.Length = EndPoint.X - StartPoint.X;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
boundingBox.X = EndPoint.X;
|
boundingBox.X = EndPoint.X;
|
||||||
boundingBox.Width = StartPoint.X - EndPoint.X;
|
boundingBox.Length = StartPoint.X - EndPoint.X;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (StartPoint.Y < EndPoint.Y)
|
if (StartPoint.Y < EndPoint.Y)
|
||||||
{
|
{
|
||||||
boundingBox.Y = StartPoint.Y;
|
boundingBox.Y = StartPoint.Y;
|
||||||
boundingBox.Length = EndPoint.Y - StartPoint.Y;
|
boundingBox.Width = EndPoint.Y - StartPoint.Y;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
boundingBox.Y = EndPoint.Y;
|
boundingBox.Y = EndPoint.Y;
|
||||||
boundingBox.Length = StartPoint.Y - EndPoint.Y;
|
boundingBox.Width = StartPoint.Y - EndPoint.Y;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -456,7 +456,7 @@ namespace OpenNest.Geometry
|
|||||||
public override bool Intersects(Arc arc)
|
public override bool Intersects(Arc arc)
|
||||||
{
|
{
|
||||||
List<Vector> pts;
|
List<Vector> pts;
|
||||||
return Helper.Intersects(arc, this, out pts);
|
return Intersect.Intersects(arc, this, out pts);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -467,7 +467,7 @@ namespace OpenNest.Geometry
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public override bool Intersects(Arc arc, out List<Vector> pts)
|
public override bool Intersects(Arc arc, out List<Vector> pts)
|
||||||
{
|
{
|
||||||
return Helper.Intersects(arc, this, out pts);
|
return Intersect.Intersects(arc, this, out pts);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -478,7 +478,7 @@ namespace OpenNest.Geometry
|
|||||||
public override bool Intersects(Circle circle)
|
public override bool Intersects(Circle circle)
|
||||||
{
|
{
|
||||||
List<Vector> pts;
|
List<Vector> pts;
|
||||||
return Helper.Intersects(circle, this, out pts);
|
return Intersect.Intersects(circle, this, out pts);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -489,7 +489,7 @@ namespace OpenNest.Geometry
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public override bool Intersects(Circle circle, out List<Vector> pts)
|
public override bool Intersects(Circle circle, out List<Vector> pts)
|
||||||
{
|
{
|
||||||
return Helper.Intersects(circle, this, out pts);
|
return Intersect.Intersects(circle, this, out pts);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -512,7 +512,7 @@ namespace OpenNest.Geometry
|
|||||||
public override bool Intersects(Line line, out List<Vector> pts)
|
public override bool Intersects(Line line, out List<Vector> pts)
|
||||||
{
|
{
|
||||||
Vector pt;
|
Vector pt;
|
||||||
var success = Helper.Intersects(this, line, out pt);
|
var success = Intersect.Intersects(this, line, out pt);
|
||||||
pts = new List<Vector>(new[] { pt });
|
pts = new List<Vector>(new[] { pt });
|
||||||
return success;
|
return success;
|
||||||
}
|
}
|
||||||
@@ -525,7 +525,7 @@ namespace OpenNest.Geometry
|
|||||||
public override bool Intersects(Polygon polygon)
|
public override bool Intersects(Polygon polygon)
|
||||||
{
|
{
|
||||||
List<Vector> pts;
|
List<Vector> pts;
|
||||||
return Helper.Intersects(this, polygon, out pts);
|
return Intersect.Intersects(this, polygon, out pts);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -536,7 +536,7 @@ namespace OpenNest.Geometry
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public override bool Intersects(Polygon polygon, out List<Vector> pts)
|
public override bool Intersects(Polygon polygon, out List<Vector> pts)
|
||||||
{
|
{
|
||||||
return Helper.Intersects(this, polygon, out pts);
|
return Intersect.Intersects(this, polygon, out pts);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -547,7 +547,7 @@ namespace OpenNest.Geometry
|
|||||||
public override bool Intersects(Shape shape)
|
public override bool Intersects(Shape shape)
|
||||||
{
|
{
|
||||||
List<Vector> pts;
|
List<Vector> pts;
|
||||||
return Helper.Intersects(this, shape, out pts);
|
return Intersect.Intersects(this, shape, out pts);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -558,7 +558,7 @@ namespace OpenNest.Geometry
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public override bool Intersects(Shape shape, out List<Vector> pts)
|
public override bool Intersects(Shape shape, out List<Vector> pts)
|
||||||
{
|
{
|
||||||
return Helper.Intersects(this, shape, out pts);
|
return Intersect.Intersects(this, shape, out pts);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using Clipper2Lib;
|
using Clipper2Lib;
|
||||||
|
using OpenNest.Math;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace OpenNest.Geometry
|
namespace OpenNest.Geometry
|
||||||
{
|
{
|
||||||
@@ -23,8 +23,20 @@ namespace OpenNest.Geometry
|
|||||||
return MinkowskiSum(stationary, reflected);
|
return MinkowskiSum(stationary, reflected);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Optimized version of Compute for polygons known to be convex.
|
||||||
|
/// Bypasses expensive triangulation and Clipper unions.
|
||||||
|
/// </summary>
|
||||||
|
public static Polygon ComputeConvex(Polygon stationary, Polygon orbiting)
|
||||||
|
{
|
||||||
|
var reflected = Reflect(orbiting);
|
||||||
|
return ConvexMinkowskiSum(stationary, reflected);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Reflects a polygon through the origin (negates all vertex coordinates).
|
/// Reflects a polygon through the origin (negates all vertex coordinates).
|
||||||
|
/// Point reflection (negating both axes) is equivalent to 180° rotation,
|
||||||
|
/// which preserves winding order. No reversal needed.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static Polygon Reflect(Polygon polygon)
|
private static Polygon Reflect(Polygon polygon)
|
||||||
{
|
{
|
||||||
@@ -33,8 +45,6 @@ namespace OpenNest.Geometry
|
|||||||
foreach (var v in polygon.Vertices)
|
foreach (var v in polygon.Vertices)
|
||||||
result.Vertices.Add(new Vector(-v.X, -v.Y));
|
result.Vertices.Add(new Vector(-v.X, -v.Y));
|
||||||
|
|
||||||
// Reflecting reverses winding order — reverse to maintain CCW.
|
|
||||||
result.Vertices.Reverse();
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,19 +89,24 @@ namespace OpenNest.Geometry
|
|||||||
/// edge vectors sorted by angle. O(n+m) where n and m are vertex counts.
|
/// edge vectors sorted by angle. O(n+m) where n and m are vertex counts.
|
||||||
/// Both polygons must have CCW winding.
|
/// Both polygons must have CCW winding.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal static Polygon ConvexMinkowskiSum(Polygon a, Polygon b)
|
public static Polygon ConvexMinkowskiSum(Polygon a, Polygon b)
|
||||||
{
|
{
|
||||||
var edgesA = GetEdgeVectors(a);
|
var edgesA = GetEdgeVectors(a);
|
||||||
var edgesB = GetEdgeVectors(b);
|
var edgesB = GetEdgeVectors(b);
|
||||||
|
|
||||||
// Find bottom-most (then left-most) vertex for each polygon as starting point.
|
// Find indices of bottom-left vertices for both.
|
||||||
var startA = FindBottomLeft(a);
|
var startA = FindBottomLeft(a);
|
||||||
var startB = FindBottomLeft(b);
|
var startB = FindBottomLeft(b);
|
||||||
|
|
||||||
var result = new Polygon();
|
var result = new Polygon();
|
||||||
|
|
||||||
|
// The starting point of the Minkowski sum A + B is the sum of the
|
||||||
|
// starting points of A and B. For NFP = A + (-B), this is
|
||||||
|
// startA + startReflectedB.
|
||||||
var current = new Vector(
|
var current = new Vector(
|
||||||
a.Vertices[startA].X + b.Vertices[startB].X,
|
a.Vertices[startA].X + b.Vertices[startB].X,
|
||||||
a.Vertices[startA].Y + b.Vertices[startB].Y);
|
a.Vertices[startA].Y + b.Vertices[startB].Y);
|
||||||
|
|
||||||
result.Vertices.Add(current);
|
result.Vertices.Add(current);
|
||||||
|
|
||||||
var ia = 0;
|
var ia = 0;
|
||||||
@@ -99,7 +114,6 @@ namespace OpenNest.Geometry
|
|||||||
var na = edgesA.Count;
|
var na = edgesA.Count;
|
||||||
var nb = edgesB.Count;
|
var nb = edgesB.Count;
|
||||||
|
|
||||||
// Reorder edges to start from the bottom-left vertex.
|
|
||||||
var orderedA = ReorderEdges(edgesA, startA);
|
var orderedA = ReorderEdges(edgesA, startA);
|
||||||
var orderedB = ReorderEdges(edgesB, startB);
|
var orderedB = ReorderEdges(edgesB, startB);
|
||||||
|
|
||||||
@@ -118,7 +132,10 @@ namespace OpenNest.Geometry
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
var angleA = System.Math.Atan2(orderedA[ia].Y, orderedA[ia].X);
|
var angleA = System.Math.Atan2(orderedA[ia].Y, orderedA[ia].X);
|
||||||
|
if (angleA < 0) angleA += Angle.TwoPI;
|
||||||
|
|
||||||
var angleB = System.Math.Atan2(orderedB[ib].Y, orderedB[ib].X);
|
var angleB = System.Math.Atan2(orderedB[ib].Y, orderedB[ib].X);
|
||||||
|
if (angleB < 0) angleB += Angle.TwoPI;
|
||||||
|
|
||||||
if (angleA < angleB)
|
if (angleA < angleB)
|
||||||
{
|
{
|
||||||
@@ -130,7 +147,6 @@ namespace OpenNest.Geometry
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Same angle — merge both edges.
|
|
||||||
edge = new Vector(
|
edge = new Vector(
|
||||||
orderedA[ia].X + orderedB[ib].X,
|
orderedA[ia].X + orderedB[ib].X,
|
||||||
orderedA[ia].Y + orderedB[ib].Y);
|
orderedA[ia].Y + orderedB[ib].Y);
|
||||||
@@ -144,6 +160,7 @@ namespace OpenNest.Geometry
|
|||||||
}
|
}
|
||||||
|
|
||||||
result.Close();
|
result.Close();
|
||||||
|
result.UpdateBounds();
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -251,9 +268,9 @@ namespace OpenNest.Geometry
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Converts an OpenNest Polygon to a Clipper2 PathD.
|
/// Converts an OpenNest Polygon to a Clipper2 PathD, with an optional offset.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal static PathD ToClipperPath(Polygon polygon)
|
public static PathD ToClipperPath(Polygon polygon, Vector offset = default)
|
||||||
{
|
{
|
||||||
var path = new PathD();
|
var path = new PathD();
|
||||||
var verts = polygon.Vertices;
|
var verts = polygon.Vertices;
|
||||||
@@ -264,7 +281,7 @@ namespace OpenNest.Geometry
|
|||||||
n--;
|
n--;
|
||||||
|
|
||||||
for (var i = 0; i < n; i++)
|
for (var i = 0; i < n; i++)
|
||||||
path.Add(new PointD(verts[i].X, verts[i].Y));
|
path.Add(new PointD(verts[i].X + offset.X, verts[i].Y + offset.Y));
|
||||||
|
|
||||||
return path;
|
return path;
|
||||||
}
|
}
|
||||||
@@ -272,7 +289,7 @@ namespace OpenNest.Geometry
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Converts a Clipper2 PathD to an OpenNest Polygon.
|
/// Converts a Clipper2 PathD to an OpenNest Polygon.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal static Polygon FromClipperPath(PathD path)
|
public static Polygon FromClipperPath(PathD path)
|
||||||
{
|
{
|
||||||
var polygon = new Polygon();
|
var polygon = new Polygon();
|
||||||
|
|
||||||
@@ -280,6 +297,7 @@ namespace OpenNest.Geometry
|
|||||||
polygon.Vertices.Add(new Vector(pt.x, pt.y));
|
polygon.Vertices.Add(new Vector(pt.x, pt.y));
|
||||||
|
|
||||||
polygon.Close();
|
polygon.Close();
|
||||||
|
polygon.UpdateBounds();
|
||||||
return polygon;
|
return polygon;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,214 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace OpenNest.Geometry
|
||||||
|
{
|
||||||
|
public static class PolyLabel
|
||||||
|
{
|
||||||
|
public static Vector Find(Polygon outer, IList<Polygon> holes = null, double precision = 0.5)
|
||||||
|
{
|
||||||
|
if (outer.Vertices.Count < 3)
|
||||||
|
return outer.Vertices.Count > 0
|
||||||
|
? outer.Vertices[0]
|
||||||
|
: new Vector();
|
||||||
|
|
||||||
|
var minX = double.MaxValue;
|
||||||
|
var minY = double.MaxValue;
|
||||||
|
var maxX = double.MinValue;
|
||||||
|
var maxY = double.MinValue;
|
||||||
|
|
||||||
|
for (var i = 0; i < outer.Vertices.Count; i++)
|
||||||
|
{
|
||||||
|
var v = outer.Vertices[i];
|
||||||
|
if (v.X < minX) minX = v.X;
|
||||||
|
if (v.Y < minY) minY = v.Y;
|
||||||
|
if (v.X > maxX) maxX = v.X;
|
||||||
|
if (v.Y > maxY) maxY = v.Y;
|
||||||
|
}
|
||||||
|
|
||||||
|
var width = maxX - minX;
|
||||||
|
var height = maxY - minY;
|
||||||
|
var cellSize = System.Math.Min(width, height);
|
||||||
|
|
||||||
|
if (cellSize == 0)
|
||||||
|
return new Vector((minX + maxX) / 2, (minY + maxY) / 2);
|
||||||
|
|
||||||
|
var halfCell = cellSize / 2;
|
||||||
|
|
||||||
|
var queue = new List<Cell>();
|
||||||
|
|
||||||
|
for (var x = minX; x < maxX; x += cellSize)
|
||||||
|
for (var y = minY; y < maxY; y += cellSize)
|
||||||
|
queue.Add(new Cell(x + halfCell, y + halfCell, halfCell, outer, holes));
|
||||||
|
|
||||||
|
queue.Sort((a, b) => b.MaxDist.CompareTo(a.MaxDist));
|
||||||
|
|
||||||
|
var bestCell = GetCentroidCell(outer, holes);
|
||||||
|
|
||||||
|
for (var i = 0; i < queue.Count; i++)
|
||||||
|
if (queue[i].Dist > bestCell.Dist)
|
||||||
|
{
|
||||||
|
bestCell = queue[i];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (queue.Count > 0)
|
||||||
|
{
|
||||||
|
var cell = queue[0];
|
||||||
|
queue.RemoveAt(0);
|
||||||
|
|
||||||
|
if (cell.Dist > bestCell.Dist)
|
||||||
|
bestCell = cell;
|
||||||
|
|
||||||
|
if (cell.MaxDist - bestCell.Dist <= precision)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
halfCell = cell.HalfSize / 2;
|
||||||
|
|
||||||
|
var newCells = new[]
|
||||||
|
{
|
||||||
|
new Cell(cell.X - halfCell, cell.Y - halfCell, halfCell, outer, holes),
|
||||||
|
new Cell(cell.X + halfCell, cell.Y - halfCell, halfCell, outer, holes),
|
||||||
|
new Cell(cell.X - halfCell, cell.Y + halfCell, halfCell, outer, holes),
|
||||||
|
new Cell(cell.X + halfCell, cell.Y + halfCell, halfCell, outer, holes),
|
||||||
|
};
|
||||||
|
|
||||||
|
for (var i = 0; i < newCells.Length; i++)
|
||||||
|
{
|
||||||
|
if (newCells[i].MaxDist > bestCell.Dist + precision)
|
||||||
|
InsertSorted(queue, newCells[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Vector(bestCell.X, bestCell.Y);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void InsertSorted(List<Cell> list, Cell cell)
|
||||||
|
{
|
||||||
|
var idx = 0;
|
||||||
|
while (idx < list.Count && list[idx].MaxDist > cell.MaxDist)
|
||||||
|
idx++;
|
||||||
|
list.Insert(idx, cell);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Cell GetCentroidCell(Polygon outer, IList<Polygon> holes)
|
||||||
|
{
|
||||||
|
var area = 0.0;
|
||||||
|
var cx = 0.0;
|
||||||
|
var cy = 0.0;
|
||||||
|
var verts = outer.Vertices;
|
||||||
|
|
||||||
|
for (int i = 0, j = verts.Count - 1; i < verts.Count; j = i++)
|
||||||
|
{
|
||||||
|
var a = verts[i];
|
||||||
|
var b = verts[j];
|
||||||
|
var cross = a.X * b.Y - b.X * a.Y;
|
||||||
|
cx += (a.X + b.X) * cross;
|
||||||
|
cy += (a.Y + b.Y) * cross;
|
||||||
|
area += cross;
|
||||||
|
}
|
||||||
|
|
||||||
|
area *= 0.5;
|
||||||
|
|
||||||
|
if (System.Math.Abs(area) < 1e-10)
|
||||||
|
return new Cell(verts[0].X, verts[0].Y, 0, outer, holes);
|
||||||
|
|
||||||
|
cx /= (6 * area);
|
||||||
|
cy /= (6 * area);
|
||||||
|
|
||||||
|
return new Cell(cx, cy, 0, outer, holes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double PointToPolygonDist(double x, double y, Polygon polygon)
|
||||||
|
{
|
||||||
|
var minDist = double.MaxValue;
|
||||||
|
var verts = polygon.Vertices;
|
||||||
|
|
||||||
|
for (int i = 0, j = verts.Count - 1; i < verts.Count; j = i++)
|
||||||
|
{
|
||||||
|
var a = verts[i];
|
||||||
|
var b = verts[j];
|
||||||
|
|
||||||
|
var dx = b.X - a.X;
|
||||||
|
var dy = b.Y - a.Y;
|
||||||
|
|
||||||
|
if (dx != 0 || dy != 0)
|
||||||
|
{
|
||||||
|
var t = ((x - a.X) * dx + (y - a.Y) * dy) / (dx * dx + dy * dy);
|
||||||
|
|
||||||
|
if (t > 1)
|
||||||
|
{
|
||||||
|
a = b;
|
||||||
|
}
|
||||||
|
else if (t > 0)
|
||||||
|
{
|
||||||
|
a = new Vector(a.X + dx * t, a.Y + dy * t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var segDx = x - a.X;
|
||||||
|
var segDy = y - a.Y;
|
||||||
|
var dist = System.Math.Sqrt(segDx * segDx + segDy * segDy);
|
||||||
|
|
||||||
|
if (dist < minDist)
|
||||||
|
minDist = dist;
|
||||||
|
}
|
||||||
|
|
||||||
|
return minDist;
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class Cell
|
||||||
|
{
|
||||||
|
public readonly double X;
|
||||||
|
public readonly double Y;
|
||||||
|
public readonly double HalfSize;
|
||||||
|
public readonly double Dist;
|
||||||
|
public readonly double MaxDist;
|
||||||
|
|
||||||
|
public Cell(double x, double y, double halfSize, Polygon outer, IList<Polygon> holes)
|
||||||
|
{
|
||||||
|
X = x;
|
||||||
|
Y = y;
|
||||||
|
HalfSize = halfSize;
|
||||||
|
|
||||||
|
var pt = new Vector(x, y);
|
||||||
|
var inside = outer.ContainsPoint(pt);
|
||||||
|
|
||||||
|
if (inside && holes != null)
|
||||||
|
{
|
||||||
|
for (var i = 0; i < holes.Count; i++)
|
||||||
|
{
|
||||||
|
if (holes[i].ContainsPoint(pt))
|
||||||
|
{
|
||||||
|
inside = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Dist = PointToAllEdgesDist(x, y, outer, holes);
|
||||||
|
|
||||||
|
if (!inside)
|
||||||
|
Dist = -Dist;
|
||||||
|
|
||||||
|
MaxDist = Dist + HalfSize * System.Math.Sqrt(2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double PointToAllEdgesDist(double x, double y, Polygon outer, IList<Polygon> holes)
|
||||||
|
{
|
||||||
|
var minDist = PointToPolygonDist(x, y, outer);
|
||||||
|
|
||||||
|
if (holes != null)
|
||||||
|
{
|
||||||
|
for (var i = 0; i < holes.Count; i++)
|
||||||
|
{
|
||||||
|
var d = PointToPolygonDist(x, y, holes[i]);
|
||||||
|
if (d < minDist)
|
||||||
|
minDist = d;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return minDist;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
using System;
|
using OpenNest.Math;
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using OpenNest.Math;
|
|
||||||
|
|
||||||
namespace OpenNest.Geometry
|
namespace OpenNest.Geometry
|
||||||
{
|
{
|
||||||
@@ -311,18 +311,74 @@ namespace OpenNest.Geometry
|
|||||||
|
|
||||||
boundingBox.X = minX;
|
boundingBox.X = minX;
|
||||||
boundingBox.Y = minY;
|
boundingBox.Y = minY;
|
||||||
boundingBox.Width = maxX - minX;
|
boundingBox.Length = maxX - minX;
|
||||||
boundingBox.Length = maxY - minY;
|
boundingBox.Width = maxY - minY;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Entity OffsetEntity(double distance, OffsetSide side)
|
public override Entity OffsetEntity(double distance, OffsetSide side)
|
||||||
{
|
{
|
||||||
throw new NotImplementedException();
|
if (Vertices.Count < 3)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var isClosed = IsClosed();
|
||||||
|
var count = isClosed ? Vertices.Count - 1 : Vertices.Count;
|
||||||
|
if (count < 3)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var ccw = CalculateArea() > 0;
|
||||||
|
var outward = ccw ? OffsetSide.Left : OffsetSide.Right;
|
||||||
|
var sign = side == outward ? 1.0 : -1.0;
|
||||||
|
var d = distance * sign;
|
||||||
|
|
||||||
|
var normals = new Vector[count];
|
||||||
|
for (var i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
var next = (i + 1) % count;
|
||||||
|
var dx = Vertices[next].X - Vertices[i].X;
|
||||||
|
var dy = Vertices[next].Y - Vertices[i].Y;
|
||||||
|
var len = System.Math.Sqrt(dx * dx + dy * dy);
|
||||||
|
if (len < Tolerance.Epsilon)
|
||||||
|
return null;
|
||||||
|
normals[i] = new Vector(-dy / len * d, dx / len * d);
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = new Polygon();
|
||||||
|
for (var i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
var prev = (i - 1 + count) % count;
|
||||||
|
|
||||||
|
var a1 = new Vector(Vertices[prev].X + normals[prev].X, Vertices[prev].Y + normals[prev].Y);
|
||||||
|
var a2 = new Vector(Vertices[i].X + normals[prev].X, Vertices[i].Y + normals[prev].Y);
|
||||||
|
var b1 = new Vector(Vertices[i].X + normals[i].X, Vertices[i].Y + normals[i].Y);
|
||||||
|
var b2 = new Vector(Vertices[(i + 1) % count].X + normals[i].X, Vertices[(i + 1) % count].Y + normals[i].Y);
|
||||||
|
|
||||||
|
var edgeA = new Line(a1, a2);
|
||||||
|
var edgeB = new Line(b1, b2);
|
||||||
|
|
||||||
|
if (edgeA.Intersects(edgeB, out var pt) && pt.IsValid())
|
||||||
|
result.Vertices.Add(pt);
|
||||||
|
else
|
||||||
|
result.Vertices.Add(new Vector(Vertices[i].X + normals[i].X, Vertices[i].Y + normals[i].Y));
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Close();
|
||||||
|
result.RemoveSelfIntersections();
|
||||||
|
result.UpdateBounds();
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Entity OffsetEntity(double distance, Vector pt)
|
public override Entity OffsetEntity(double distance, Vector pt)
|
||||||
{
|
{
|
||||||
throw new NotImplementedException();
|
var left = OffsetEntity(distance, OffsetSide.Left);
|
||||||
|
var right = OffsetEntity(distance, OffsetSide.Right);
|
||||||
|
|
||||||
|
if (left == null) return right;
|
||||||
|
if (right == null) return left;
|
||||||
|
|
||||||
|
var distLeft = left.ClosestPointTo(pt).DistanceTo(pt);
|
||||||
|
var distRight = right.ClosestPointTo(pt).DistanceTo(pt);
|
||||||
|
|
||||||
|
return distLeft > distRight ? left : right;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -364,7 +420,7 @@ namespace OpenNest.Geometry
|
|||||||
public override bool Intersects(Arc arc)
|
public override bool Intersects(Arc arc)
|
||||||
{
|
{
|
||||||
List<Vector> pts;
|
List<Vector> pts;
|
||||||
return Helper.Intersects(arc, this, out pts);
|
return Intersect.Intersects(arc, this, out pts);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -375,7 +431,7 @@ namespace OpenNest.Geometry
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public override bool Intersects(Arc arc, out List<Vector> pts)
|
public override bool Intersects(Arc arc, out List<Vector> pts)
|
||||||
{
|
{
|
||||||
return Helper.Intersects(arc, this, out pts);
|
return Intersect.Intersects(arc, this, out pts);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -386,7 +442,7 @@ namespace OpenNest.Geometry
|
|||||||
public override bool Intersects(Circle circle)
|
public override bool Intersects(Circle circle)
|
||||||
{
|
{
|
||||||
List<Vector> pts;
|
List<Vector> pts;
|
||||||
return Helper.Intersects(circle, this, out pts);
|
return Intersect.Intersects(circle, this, out pts);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -397,7 +453,7 @@ namespace OpenNest.Geometry
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public override bool Intersects(Circle circle, out List<Vector> pts)
|
public override bool Intersects(Circle circle, out List<Vector> pts)
|
||||||
{
|
{
|
||||||
return Helper.Intersects(circle, this, out pts);
|
return Intersect.Intersects(circle, this, out pts);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -408,7 +464,7 @@ namespace OpenNest.Geometry
|
|||||||
public override bool Intersects(Line line)
|
public override bool Intersects(Line line)
|
||||||
{
|
{
|
||||||
List<Vector> pts;
|
List<Vector> pts;
|
||||||
return Helper.Intersects(line, this, out pts);
|
return Intersect.Intersects(line, this, out pts);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -419,7 +475,7 @@ namespace OpenNest.Geometry
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public override bool Intersects(Line line, out List<Vector> pts)
|
public override bool Intersects(Line line, out List<Vector> pts)
|
||||||
{
|
{
|
||||||
return Helper.Intersects(line, this, out pts);
|
return Intersect.Intersects(line, this, out pts);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -430,7 +486,7 @@ namespace OpenNest.Geometry
|
|||||||
public override bool Intersects(Polygon polygon)
|
public override bool Intersects(Polygon polygon)
|
||||||
{
|
{
|
||||||
List<Vector> pts;
|
List<Vector> pts;
|
||||||
return Helper.Intersects(this, polygon, out pts);
|
return Intersect.Intersects(this, polygon, out pts);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -441,7 +497,7 @@ namespace OpenNest.Geometry
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public override bool Intersects(Polygon polygon, out List<Vector> pts)
|
public override bool Intersects(Polygon polygon, out List<Vector> pts)
|
||||||
{
|
{
|
||||||
return Helper.Intersects(this, polygon, out pts);
|
return Intersect.Intersects(this, polygon, out pts);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -452,7 +508,7 @@ namespace OpenNest.Geometry
|
|||||||
public override bool Intersects(Shape shape)
|
public override bool Intersects(Shape shape)
|
||||||
{
|
{
|
||||||
List<Vector> pts;
|
List<Vector> pts;
|
||||||
return Helper.Intersects(shape, this, out pts);
|
return Intersect.Intersects(shape, this, out pts);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -463,7 +519,7 @@ namespace OpenNest.Geometry
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public override bool Intersects(Shape shape, out List<Vector> pts)
|
public override bool Intersects(Shape shape, out List<Vector> pts)
|
||||||
{
|
{
|
||||||
return Helper.Intersects(shape, this, out pts);
|
return Intersect.Intersects(shape, this, out pts);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -493,13 +549,37 @@ namespace OpenNest.Geometry
|
|||||||
{
|
{
|
||||||
var n = Vertices.Count - 1;
|
var n = Vertices.Count - 1;
|
||||||
|
|
||||||
|
// Pre-calculate edge bounding boxes to speed up intersection checks.
|
||||||
|
var edgeBounds = new (double minX, double maxX, double minY, double maxY)[n];
|
||||||
for (var i = 0; i < n; i++)
|
for (var i = 0; i < n; i++)
|
||||||
{
|
{
|
||||||
|
var v1 = Vertices[i];
|
||||||
|
var v2 = Vertices[i + 1];
|
||||||
|
edgeBounds[i] = (
|
||||||
|
System.Math.Min(v1.X, v2.X) - Tolerance.Epsilon,
|
||||||
|
System.Math.Max(v1.X, v2.X) + Tolerance.Epsilon,
|
||||||
|
System.Math.Min(v1.Y, v2.Y) - Tolerance.Epsilon,
|
||||||
|
System.Math.Max(v1.Y, v2.Y) + Tolerance.Epsilon
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < n; i++)
|
||||||
|
{
|
||||||
|
var bi = edgeBounds[i];
|
||||||
for (var j = i + 2; j < n; j++)
|
for (var j = i + 2; j < n; j++)
|
||||||
{
|
{
|
||||||
if (i == 0 && j == n - 1)
|
if (i == 0 && j == n - 1)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
var bj = edgeBounds[j];
|
||||||
|
|
||||||
|
// Prune with bounding box check.
|
||||||
|
if (bi.maxX < bj.minX || bj.maxX < bi.minX ||
|
||||||
|
bi.maxY < bj.minY || bj.maxY < bi.minY)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (SegmentsIntersect(Vertices[i], Vertices[i + 1], Vertices[j], Vertices[j + 1], out pt))
|
if (SegmentsIntersect(Vertices[i], Vertices[i + 1], Vertices[j], Vertices[j + 1], out pt))
|
||||||
{
|
{
|
||||||
edgeI = i;
|
edgeI = i;
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using OpenNest.Math;
|
using OpenNest.Math;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace OpenNest.Geometry
|
namespace OpenNest.Geometry
|
||||||
{
|
{
|
||||||
|
|||||||
+104
-14
@@ -159,8 +159,8 @@ namespace OpenNest.Geometry
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Helper.Optimize(lines);
|
GeometryOptimizer.Optimize(lines);
|
||||||
Helper.Optimize(arcs);
|
GeometryOptimizer.Optimize(arcs);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -532,9 +532,29 @@ namespace OpenNest.Geometry
|
|||||||
Line line, Line offsetLine,
|
Line line, Line offsetLine,
|
||||||
double distance, OffsetSide side, Shape offsetShape)
|
double distance, OffsetSide side, Shape offsetShape)
|
||||||
{
|
{
|
||||||
Vector intersection;
|
// Determine if this is a convex corner using the cross product of
|
||||||
|
// the original line directions. Convex corners need an arc; concave
|
||||||
|
// corners use the line intersection (miter join).
|
||||||
|
var d1 = lastLine.EndPoint - lastLine.StartPoint;
|
||||||
|
var d2 = line.EndPoint - line.StartPoint;
|
||||||
|
var cross = d1.X * d2.Y - d1.Y * d2.X;
|
||||||
|
|
||||||
if (Helper.Intersects(offsetLine, lastOffsetLine, out intersection))
|
var isConvex = (side == OffsetSide.Left && cross < -OpenNest.Math.Tolerance.Epsilon) ||
|
||||||
|
(side == OffsetSide.Right && cross > OpenNest.Math.Tolerance.Epsilon);
|
||||||
|
|
||||||
|
if (isConvex)
|
||||||
|
{
|
||||||
|
var arc = new Arc(
|
||||||
|
line.StartPoint,
|
||||||
|
distance,
|
||||||
|
line.StartPoint.AngleTo(lastOffsetLine.EndPoint),
|
||||||
|
line.StartPoint.AngleTo(offsetLine.StartPoint),
|
||||||
|
side == OffsetSide.Left
|
||||||
|
);
|
||||||
|
|
||||||
|
offsetShape.Entities.Add(arc);
|
||||||
|
}
|
||||||
|
else if (Intersect.IntersectsUnbounded(offsetLine, lastOffsetLine, out var intersection))
|
||||||
{
|
{
|
||||||
offsetLine.StartPoint = intersection;
|
offsetLine.StartPoint = intersection;
|
||||||
lastOffsetLine.EndPoint = intersection;
|
lastOffsetLine.EndPoint = intersection;
|
||||||
@@ -558,6 +578,76 @@ namespace OpenNest.Geometry
|
|||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Offsets the shape outward by the given distance.
|
||||||
|
/// Normalizes to CW winding before offsetting Left (which is outward for CW),
|
||||||
|
/// making the method independent of the original contour winding direction.
|
||||||
|
/// </summary>
|
||||||
|
public Shape OffsetOutward(double distance)
|
||||||
|
{
|
||||||
|
var poly = ToPolygon();
|
||||||
|
|
||||||
|
if (poly == null || poly.Vertices.Count < 3
|
||||||
|
|| poly.RotationDirection() == RotationType.CW)
|
||||||
|
return OffsetEntity(distance, OffsetSide.Left) as Shape;
|
||||||
|
|
||||||
|
// Shape is CCW — reverse to CW so Left offset goes outward.
|
||||||
|
var copy = new Shape();
|
||||||
|
|
||||||
|
for (var i = Entities.Count - 1; i >= 0; i--)
|
||||||
|
{
|
||||||
|
switch (Entities[i])
|
||||||
|
{
|
||||||
|
case Line l:
|
||||||
|
copy.Entities.Add(new Line(l.EndPoint, l.StartPoint) { Layer = l.Layer });
|
||||||
|
break;
|
||||||
|
case Arc a:
|
||||||
|
copy.Entities.Add(new Arc(a.Center, a.Radius, a.EndAngle, a.StartAngle, !a.IsReversed) { Layer = a.Layer });
|
||||||
|
break;
|
||||||
|
case Circle c:
|
||||||
|
copy.Entities.Add(new Circle(c.Center, c.Radius) { Layer = c.Layer, Rotation = RotationType.CW });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return copy.OffsetEntity(distance, OffsetSide.Left) as Shape;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Offsets the shape inward by the given distance.
|
||||||
|
/// Normalizes to CCW winding before offsetting Left (which is inward for CCW),
|
||||||
|
/// making the method independent of the original contour winding direction.
|
||||||
|
/// </summary>
|
||||||
|
public Shape OffsetInward(double distance)
|
||||||
|
{
|
||||||
|
var poly = ToPolygon();
|
||||||
|
|
||||||
|
if (poly == null || poly.Vertices.Count < 3
|
||||||
|
|| poly.RotationDirection() == RotationType.CCW)
|
||||||
|
return OffsetEntity(distance, OffsetSide.Left) as Shape;
|
||||||
|
|
||||||
|
// Create a reversed copy to avoid mutating shared entity objects.
|
||||||
|
var copy = new Shape();
|
||||||
|
|
||||||
|
for (var i = Entities.Count - 1; i >= 0; i--)
|
||||||
|
{
|
||||||
|
switch (Entities[i])
|
||||||
|
{
|
||||||
|
case Line l:
|
||||||
|
copy.Entities.Add(new Line(l.EndPoint, l.StartPoint) { Layer = l.Layer });
|
||||||
|
break;
|
||||||
|
case Arc a:
|
||||||
|
copy.Entities.Add(new Arc(a.Center, a.Radius, a.EndAngle, a.StartAngle, !a.IsReversed) { Layer = a.Layer });
|
||||||
|
break;
|
||||||
|
case Circle c:
|
||||||
|
copy.Entities.Add(new Circle(c.Center, c.Radius) { Layer = c.Layer, Rotation = RotationType.CCW });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return copy.OffsetEntity(distance, OffsetSide.Left) as Shape;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the closest point on the shape to the given point.
|
/// Gets the closest point on the shape to the given point.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -577,7 +667,7 @@ namespace OpenNest.Geometry
|
|||||||
public override bool Intersects(Arc arc)
|
public override bool Intersects(Arc arc)
|
||||||
{
|
{
|
||||||
List<Vector> pts;
|
List<Vector> pts;
|
||||||
return Helper.Intersects(arc, this, out pts);
|
return Intersect.Intersects(arc, this, out pts);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -588,7 +678,7 @@ namespace OpenNest.Geometry
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public override bool Intersects(Arc arc, out List<Vector> pts)
|
public override bool Intersects(Arc arc, out List<Vector> pts)
|
||||||
{
|
{
|
||||||
return Helper.Intersects(arc, this, out pts);
|
return Intersect.Intersects(arc, this, out pts);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -599,7 +689,7 @@ namespace OpenNest.Geometry
|
|||||||
public override bool Intersects(Circle circle)
|
public override bool Intersects(Circle circle)
|
||||||
{
|
{
|
||||||
List<Vector> pts;
|
List<Vector> pts;
|
||||||
return Helper.Intersects(circle, this, out pts);
|
return Intersect.Intersects(circle, this, out pts);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -610,7 +700,7 @@ namespace OpenNest.Geometry
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public override bool Intersects(Circle circle, out List<Vector> pts)
|
public override bool Intersects(Circle circle, out List<Vector> pts)
|
||||||
{
|
{
|
||||||
return Helper.Intersects(circle, this, out pts);
|
return Intersect.Intersects(circle, this, out pts);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -621,7 +711,7 @@ namespace OpenNest.Geometry
|
|||||||
public override bool Intersects(Line line)
|
public override bool Intersects(Line line)
|
||||||
{
|
{
|
||||||
List<Vector> pts;
|
List<Vector> pts;
|
||||||
return Helper.Intersects(line, this, out pts);
|
return Intersect.Intersects(line, this, out pts);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -632,7 +722,7 @@ namespace OpenNest.Geometry
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public override bool Intersects(Line line, out List<Vector> pts)
|
public override bool Intersects(Line line, out List<Vector> pts)
|
||||||
{
|
{
|
||||||
return Helper.Intersects(line, this, out pts);
|
return Intersect.Intersects(line, this, out pts);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -643,7 +733,7 @@ namespace OpenNest.Geometry
|
|||||||
public override bool Intersects(Polygon polygon)
|
public override bool Intersects(Polygon polygon)
|
||||||
{
|
{
|
||||||
List<Vector> pts;
|
List<Vector> pts;
|
||||||
return Helper.Intersects(this, polygon, out pts);
|
return Intersect.Intersects(this, polygon, out pts);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -654,7 +744,7 @@ namespace OpenNest.Geometry
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public override bool Intersects(Polygon polygon, out List<Vector> pts)
|
public override bool Intersects(Polygon polygon, out List<Vector> pts)
|
||||||
{
|
{
|
||||||
return Helper.Intersects(this, polygon, out pts);
|
return Intersect.Intersects(this, polygon, out pts);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -665,7 +755,7 @@ namespace OpenNest.Geometry
|
|||||||
public override bool Intersects(Shape shape)
|
public override bool Intersects(Shape shape)
|
||||||
{
|
{
|
||||||
List<Vector> pts;
|
List<Vector> pts;
|
||||||
return Helper.Intersects(this, shape, out pts);
|
return Intersect.Intersects(this, shape, out pts);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -676,7 +766,7 @@ namespace OpenNest.Geometry
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public override bool Intersects(Shape shape, out List<Vector> pts)
|
public override bool Intersects(Shape shape, out List<Vector> pts)
|
||||||
{
|
{
|
||||||
return Helper.Intersects(this, shape, out pts);
|
return Intersect.Intersects(this, shape, out pts);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -0,0 +1,150 @@
|
|||||||
|
using OpenNest.Math;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
namespace OpenNest.Geometry
|
||||||
|
{
|
||||||
|
public static class ShapeBuilder
|
||||||
|
{
|
||||||
|
public static List<Shape> GetShapes(IEnumerable<Entity> entities)
|
||||||
|
{
|
||||||
|
var lines = new List<Line>();
|
||||||
|
var arcs = new List<Arc>();
|
||||||
|
var circles = new List<Circle>();
|
||||||
|
var shapes = new List<Shape>();
|
||||||
|
|
||||||
|
var entities2 = new Queue<Entity>(entities);
|
||||||
|
|
||||||
|
while (entities2.Count > 0)
|
||||||
|
{
|
||||||
|
var entity = entities2.Dequeue();
|
||||||
|
|
||||||
|
switch (entity.Type)
|
||||||
|
{
|
||||||
|
case EntityType.Arc:
|
||||||
|
arcs.Add((Arc)entity);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case EntityType.Circle:
|
||||||
|
circles.Add((Circle)entity);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case EntityType.Line:
|
||||||
|
lines.Add((Line)entity);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case EntityType.Shape:
|
||||||
|
var shape = (Shape)entity;
|
||||||
|
shape.Entities.ForEach(e => entities2.Enqueue(e));
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
Debug.Fail("Unhandled geometry type");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var circle in circles)
|
||||||
|
{
|
||||||
|
var shape = new Shape();
|
||||||
|
shape.Entities.Add(circle);
|
||||||
|
shape.UpdateBounds();
|
||||||
|
shapes.Add(shape);
|
||||||
|
}
|
||||||
|
|
||||||
|
var entityList = new List<Entity>();
|
||||||
|
|
||||||
|
entityList.AddRange(lines);
|
||||||
|
entityList.AddRange(arcs);
|
||||||
|
|
||||||
|
while (entityList.Count > 0)
|
||||||
|
{
|
||||||
|
var next = entityList[0];
|
||||||
|
var shape = new Shape();
|
||||||
|
shape.Entities.Add(next);
|
||||||
|
|
||||||
|
entityList.RemoveAt(0);
|
||||||
|
|
||||||
|
Vector startPoint = new Vector();
|
||||||
|
Entity connected;
|
||||||
|
|
||||||
|
switch (next.Type)
|
||||||
|
{
|
||||||
|
case EntityType.Arc:
|
||||||
|
var arc = (Arc)next;
|
||||||
|
startPoint = arc.EndPoint();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case EntityType.Line:
|
||||||
|
var line = (Line)next;
|
||||||
|
startPoint = line.EndPoint;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
while ((connected = GetConnected(startPoint, entityList)) != null)
|
||||||
|
{
|
||||||
|
shape.Entities.Add(connected);
|
||||||
|
entityList.Remove(connected);
|
||||||
|
|
||||||
|
switch (connected.Type)
|
||||||
|
{
|
||||||
|
case EntityType.Arc:
|
||||||
|
var arc = (Arc)connected;
|
||||||
|
startPoint = arc.EndPoint();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case EntityType.Line:
|
||||||
|
var line = (Line)connected;
|
||||||
|
startPoint = line.EndPoint;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
shape.UpdateBounds();
|
||||||
|
shapes.Add(shape);
|
||||||
|
}
|
||||||
|
|
||||||
|
return shapes;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static Entity GetConnected(Vector pt, IEnumerable<Entity> geometry)
|
||||||
|
{
|
||||||
|
var tol = Tolerance.ChainTolerance;
|
||||||
|
|
||||||
|
foreach (var geo in geometry)
|
||||||
|
{
|
||||||
|
switch (geo.Type)
|
||||||
|
{
|
||||||
|
case EntityType.Arc:
|
||||||
|
var arc = (Arc)geo;
|
||||||
|
|
||||||
|
if (arc.StartPoint().DistanceTo(pt) <= tol)
|
||||||
|
return arc;
|
||||||
|
|
||||||
|
if (arc.EndPoint().DistanceTo(pt) <= tol)
|
||||||
|
{
|
||||||
|
arc.Reverse();
|
||||||
|
return arc;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case EntityType.Line:
|
||||||
|
var line = (Line)geo;
|
||||||
|
|
||||||
|
if (line.StartPoint.DistanceTo(pt) <= tol)
|
||||||
|
return line;
|
||||||
|
|
||||||
|
if (line.EndPoint.DistanceTo(pt) <= tol)
|
||||||
|
{
|
||||||
|
line.Reverse();
|
||||||
|
return line;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
namespace OpenNest.Geometry
|
namespace OpenNest.Geometry
|
||||||
{
|
{
|
||||||
@@ -16,14 +17,17 @@ namespace OpenNest.Geometry
|
|||||||
|
|
||||||
private void Update(List<Entity> entities)
|
private void Update(List<Entity> entities)
|
||||||
{
|
{
|
||||||
var shapes = Helper.GetShapes(entities);
|
var shapes = ShapeBuilder.GetShapes(entities);
|
||||||
|
|
||||||
Perimeter = shapes[0];
|
Perimeter = shapes[0];
|
||||||
Cutouts = new List<Shape>();
|
Cutouts = new List<Shape>();
|
||||||
|
|
||||||
for (int i = 1; i < shapes.Count; i++)
|
for (var i = 1; i < shapes.Count; i++)
|
||||||
{
|
{
|
||||||
if (shapes[i].Left < Perimeter.Left)
|
var bb = shapes[i].BoundingBox;
|
||||||
|
var perimBB = Perimeter.BoundingBox;
|
||||||
|
|
||||||
|
if (bb.Width * bb.Length > perimBB.Width * perimBB.Length)
|
||||||
{
|
{
|
||||||
Cutouts.Add(Perimeter);
|
Cutouts.Add(Perimeter);
|
||||||
Perimeter = shapes[i];
|
Perimeter = shapes[i];
|
||||||
@@ -38,5 +42,52 @@ namespace OpenNest.Geometry
|
|||||||
public Shape Perimeter { get; set; }
|
public Shape Perimeter { get; set; }
|
||||||
|
|
||||||
public List<Shape> Cutouts { get; set; }
|
public List<Shape> Cutouts { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ensures CNC-standard winding: perimeter CW (kerf left = outward),
|
||||||
|
/// cutouts CCW (kerf left = inward). Reverses contours in-place as needed.
|
||||||
|
/// </summary>
|
||||||
|
public void NormalizeWinding()
|
||||||
|
{
|
||||||
|
EnsureWinding(Perimeter, RotationType.CW);
|
||||||
|
|
||||||
|
foreach (var cutout in Cutouts)
|
||||||
|
EnsureWinding(cutout, RotationType.CCW);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the entities in normalized winding order (perimeter first, then cutouts).
|
||||||
|
/// </summary>
|
||||||
|
public List<Entity> ToNormalizedEntities()
|
||||||
|
{
|
||||||
|
NormalizeWinding();
|
||||||
|
var result = new List<Entity>(Perimeter.Entities);
|
||||||
|
|
||||||
|
foreach (var cutout in Cutouts)
|
||||||
|
result.AddRange(cutout.Entities);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Convenience method: builds a ShapeProfile from raw entities,
|
||||||
|
/// normalizes winding, and returns the corrected entity list.
|
||||||
|
/// </summary>
|
||||||
|
public static List<Entity> NormalizeEntities(IEnumerable<Entity> entities)
|
||||||
|
{
|
||||||
|
var profile = new ShapeProfile(entities.ToList());
|
||||||
|
return profile.ToNormalizedEntities();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void EnsureWinding(Shape shape, RotationType desired)
|
||||||
|
{
|
||||||
|
var poly = shape.ToPolygon();
|
||||||
|
|
||||||
|
if (poly != null && poly.Vertices.Count >= 3
|
||||||
|
&& poly.RotationDirection() != desired)
|
||||||
|
{
|
||||||
|
shape.Reverse();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,14 +6,14 @@ namespace OpenNest.Geometry
|
|||||||
{
|
{
|
||||||
public Size(double width, double length)
|
public Size(double width, double length)
|
||||||
{
|
{
|
||||||
Length = length;
|
|
||||||
Width = width;
|
Width = width;
|
||||||
|
Length = length;
|
||||||
}
|
}
|
||||||
|
|
||||||
public double Length;
|
|
||||||
|
|
||||||
public double Width;
|
public double Width;
|
||||||
|
|
||||||
|
public double Length;
|
||||||
|
|
||||||
public static Size Parse(string size)
|
public static Size Parse(string size)
|
||||||
{
|
{
|
||||||
var a = size.ToUpper().Split('X');
|
var a = size.ToUpper().Split('X');
|
||||||
@@ -21,8 +21,8 @@ namespace OpenNest.Geometry
|
|||||||
if (a.Length > 2)
|
if (a.Length > 2)
|
||||||
throw new FormatException("Invalid size format.");
|
throw new FormatException("Invalid size format.");
|
||||||
|
|
||||||
var length = double.Parse(a[0]);
|
var width = double.Parse(a[0]);
|
||||||
var width = double.Parse(a[1]);
|
var length = double.Parse(a[1]);
|
||||||
|
|
||||||
return new Size(width, length);
|
return new Size(width, length);
|
||||||
}
|
}
|
||||||
@@ -42,14 +42,8 @@ namespace OpenNest.Geometry
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override string ToString()
|
public override string ToString() => $"{Width} x {Length}";
|
||||||
{
|
|
||||||
return string.Format("{0} x {1}", Length, Width);
|
|
||||||
}
|
|
||||||
|
|
||||||
public string ToString(int decimalPlaces)
|
public string ToString(int decimalPlaces) => $"{System.Math.Round(Width, decimalPlaces)} x {System.Math.Round(Length, decimalPlaces)}";
|
||||||
{
|
|
||||||
return string.Format("{0} x {1}", System.Math.Round(Length, decimalPlaces), System.Math.Round(Width, decimalPlaces));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,899 @@
|
|||||||
|
using OpenNest.Math;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace OpenNest.Geometry
|
||||||
|
{
|
||||||
|
public static class SpatialQuery
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Finds the distance from a vertex to a line segment along a push axis.
|
||||||
|
/// Returns double.MaxValue if the ray does not hit the segment.
|
||||||
|
/// </summary>
|
||||||
|
private static double RayEdgeDistance(Vector vertex, Line edge, PushDirection direction)
|
||||||
|
{
|
||||||
|
return RayEdgeDistance(
|
||||||
|
vertex.X, vertex.Y,
|
||||||
|
edge.pt1.X, edge.pt1.Y, edge.pt2.X, edge.pt2.Y,
|
||||||
|
direction);
|
||||||
|
}
|
||||||
|
|
||||||
|
[System.Runtime.CompilerServices.MethodImpl(
|
||||||
|
System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
|
||||||
|
private static double RayEdgeDistance(
|
||||||
|
double vx, double vy,
|
||||||
|
double p1x, double p1y, double p2x, double p2y,
|
||||||
|
PushDirection direction)
|
||||||
|
{
|
||||||
|
switch (direction)
|
||||||
|
{
|
||||||
|
case PushDirection.Left:
|
||||||
|
case PushDirection.Right:
|
||||||
|
{
|
||||||
|
var dy = p2y - p1y;
|
||||||
|
if (System.Math.Abs(dy) < Tolerance.Epsilon)
|
||||||
|
return double.MaxValue;
|
||||||
|
|
||||||
|
var t = (vy - p1y) / dy;
|
||||||
|
if (t < -Tolerance.Epsilon || t > 1.0 + Tolerance.Epsilon)
|
||||||
|
return double.MaxValue;
|
||||||
|
|
||||||
|
var ix = p1x + t * (p2x - p1x);
|
||||||
|
var dist = direction == PushDirection.Left ? vx - ix : ix - vx;
|
||||||
|
|
||||||
|
if (dist > Tolerance.Epsilon) return dist;
|
||||||
|
if (dist >= -Tolerance.Epsilon) return 0;
|
||||||
|
return double.MaxValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
case PushDirection.Down:
|
||||||
|
case PushDirection.Up:
|
||||||
|
{
|
||||||
|
var dx = p2x - p1x;
|
||||||
|
if (System.Math.Abs(dx) < Tolerance.Epsilon)
|
||||||
|
return double.MaxValue;
|
||||||
|
|
||||||
|
var t = (vx - p1x) / dx;
|
||||||
|
if (t < -Tolerance.Epsilon || t > 1.0 + Tolerance.Epsilon)
|
||||||
|
return double.MaxValue;
|
||||||
|
|
||||||
|
var iy = p1y + t * (p2y - p1y);
|
||||||
|
var dist = direction == PushDirection.Down ? vy - iy : iy - vy;
|
||||||
|
|
||||||
|
if (dist > Tolerance.Epsilon) return dist;
|
||||||
|
if (dist >= -Tolerance.Epsilon) return 0;
|
||||||
|
return double.MaxValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return double.MaxValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Generalized ray-edge distance along an arbitrary unit direction vector.
|
||||||
|
/// Returns double.MaxValue if the ray does not hit the segment.
|
||||||
|
/// </summary>
|
||||||
|
[System.Runtime.CompilerServices.MethodImpl(
|
||||||
|
System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
|
||||||
|
public static double RayEdgeDistance(
|
||||||
|
double vx, double vy,
|
||||||
|
double p1x, double p1y, double p2x, double p2y,
|
||||||
|
double dirX, double dirY)
|
||||||
|
{
|
||||||
|
var ex = p2x - p1x;
|
||||||
|
var ey = p2y - p1y;
|
||||||
|
|
||||||
|
var det = ex * dirY - ey * dirX;
|
||||||
|
if (System.Math.Abs(det) < Tolerance.Epsilon)
|
||||||
|
return double.MaxValue;
|
||||||
|
|
||||||
|
var dvx = p1x - vx;
|
||||||
|
var dvy = p1y - vy;
|
||||||
|
|
||||||
|
var t = (ex * dvy - ey * dvx) / det;
|
||||||
|
if (t < -Tolerance.Epsilon)
|
||||||
|
return double.MaxValue;
|
||||||
|
|
||||||
|
var s = (dirX * dvy - dirY * dvx) / det;
|
||||||
|
if (s < -Tolerance.Epsilon || s > 1.0 + Tolerance.Epsilon)
|
||||||
|
return double.MaxValue;
|
||||||
|
|
||||||
|
if (t > Tolerance.Epsilon) return t;
|
||||||
|
if (t >= -Tolerance.Epsilon) return 0;
|
||||||
|
return double.MaxValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Solves ray-circle intersection, returning the two parametric t values.
|
||||||
|
/// Returns false if no real intersection exists.
|
||||||
|
/// </summary>
|
||||||
|
[System.Runtime.CompilerServices.MethodImpl(
|
||||||
|
System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
|
||||||
|
private static bool SolveRayCircle(
|
||||||
|
double vx, double vy,
|
||||||
|
double cx, double cy, double r,
|
||||||
|
double dirX, double dirY,
|
||||||
|
out double t1, out double t2)
|
||||||
|
{
|
||||||
|
var ox = vx - cx;
|
||||||
|
var oy = vy - cy;
|
||||||
|
|
||||||
|
var a = dirX * dirX + dirY * dirY;
|
||||||
|
var b = 2.0 * (ox * dirX + oy * dirY);
|
||||||
|
var c = ox * ox + oy * oy - r * r;
|
||||||
|
|
||||||
|
var discriminant = b * b - 4.0 * a * c;
|
||||||
|
if (discriminant < 0)
|
||||||
|
{
|
||||||
|
t1 = t2 = double.MaxValue;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var sqrtD = System.Math.Sqrt(discriminant);
|
||||||
|
var inv2a = 1.0 / (2.0 * a);
|
||||||
|
t1 = (-b - sqrtD) * inv2a;
|
||||||
|
t2 = (-b + sqrtD) * inv2a;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Computes the distance from a point along a direction to an arc.
|
||||||
|
/// Solves ray-circle intersection, then constrains hits to the arc's
|
||||||
|
/// angular span. Returns double.MaxValue if no hit.
|
||||||
|
/// </summary>
|
||||||
|
[System.Runtime.CompilerServices.MethodImpl(
|
||||||
|
System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
|
||||||
|
public static double RayArcDistance(
|
||||||
|
double vx, double vy,
|
||||||
|
double cx, double cy, double r,
|
||||||
|
double startAngle, double endAngle, bool reversed,
|
||||||
|
double dirX, double dirY)
|
||||||
|
{
|
||||||
|
if (!SolveRayCircle(vx, vy, cx, cy, r, dirX, dirY, out var t1, out var t2))
|
||||||
|
return double.MaxValue;
|
||||||
|
|
||||||
|
var best = double.MaxValue;
|
||||||
|
|
||||||
|
if (t1 > -Tolerance.Epsilon)
|
||||||
|
{
|
||||||
|
var hitAngle = Angle.NormalizeRad(System.Math.Atan2(
|
||||||
|
vy + t1 * dirY - cy, vx + t1 * dirX - cx));
|
||||||
|
if (Angle.IsBetweenRad(hitAngle, startAngle, endAngle, reversed))
|
||||||
|
best = t1 > Tolerance.Epsilon ? t1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (t2 > -Tolerance.Epsilon && t2 < best)
|
||||||
|
{
|
||||||
|
var hitAngle = Angle.NormalizeRad(System.Math.Atan2(
|
||||||
|
vy + t2 * dirY - cy, vx + t2 * dirX - cx));
|
||||||
|
if (Angle.IsBetweenRad(hitAngle, startAngle, endAngle, reversed))
|
||||||
|
best = t2 > Tolerance.Epsilon ? t2 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Computes the distance from a point along a direction to a full circle.
|
||||||
|
/// Returns double.MaxValue if no hit.
|
||||||
|
/// </summary>
|
||||||
|
[System.Runtime.CompilerServices.MethodImpl(
|
||||||
|
System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
|
||||||
|
public static double RayCircleDistance(
|
||||||
|
double vx, double vy,
|
||||||
|
double cx, double cy, double r,
|
||||||
|
double dirX, double dirY)
|
||||||
|
{
|
||||||
|
if (!SolveRayCircle(vx, vy, cx, cy, r, dirX, dirY, out var t1, out var t2))
|
||||||
|
return double.MaxValue;
|
||||||
|
|
||||||
|
if (t1 > Tolerance.Epsilon) return t1;
|
||||||
|
if (t1 >= -Tolerance.Epsilon) return 0;
|
||||||
|
if (t2 > Tolerance.Epsilon) return t2;
|
||||||
|
if (t2 >= -Tolerance.Epsilon) return 0;
|
||||||
|
|
||||||
|
return double.MaxValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Computes the minimum translation distance along a push direction before
|
||||||
|
/// any edge of movingLines contacts any edge of stationaryLines.
|
||||||
|
/// Returns double.MaxValue if no collision path exists.
|
||||||
|
/// </summary>
|
||||||
|
public static double DirectionalDistance(List<Line> movingLines, List<Line> stationaryLines, PushDirection direction)
|
||||||
|
{
|
||||||
|
return DirectionalDistance(movingLines, 0, 0, stationaryLines, direction);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Computes the minimum directional distance with the moving lines translated
|
||||||
|
/// by (movingDx, movingDy) without creating new Line objects.
|
||||||
|
/// </summary>
|
||||||
|
public static double DirectionalDistance(
|
||||||
|
List<Line> movingLines, double movingDx, double movingDy,
|
||||||
|
List<Line> stationaryLines, PushDirection direction)
|
||||||
|
{
|
||||||
|
var minDist = double.MaxValue;
|
||||||
|
var movingOffset = new Vector(movingDx, movingDy);
|
||||||
|
|
||||||
|
// Case 1: Each moving vertex -> each stationary edge
|
||||||
|
var movingVertices = CollectVertices(movingLines, movingOffset);
|
||||||
|
|
||||||
|
var stationaryEdges = ToEdgeArray(stationaryLines);
|
||||||
|
SortEdgesForPruning(stationaryEdges, direction);
|
||||||
|
|
||||||
|
foreach (var mv in movingVertices)
|
||||||
|
{
|
||||||
|
var d = OneWayDistance(mv, stationaryEdges, Vector.Zero, direction);
|
||||||
|
if (d < minDist) minDist = d;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Case 2: Each stationary vertex -> each moving edge (opposite direction)
|
||||||
|
var opposite = OppositeDirection(direction);
|
||||||
|
var stationaryVertices = CollectVertices(stationaryLines, Vector.Zero);
|
||||||
|
|
||||||
|
var movingEdges = ToEdgeArray(movingLines);
|
||||||
|
SortEdgesForPruning(movingEdges, opposite);
|
||||||
|
|
||||||
|
foreach (var sv in stationaryVertices)
|
||||||
|
{
|
||||||
|
var d = OneWayDistance(sv, movingEdges, movingOffset, opposite);
|
||||||
|
if (d < minDist) minDist = d;
|
||||||
|
}
|
||||||
|
|
||||||
|
return minDist;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Packs line segments into a flat double array [x1,y1,x2,y2, ...] for GPU transfer.
|
||||||
|
/// </summary>
|
||||||
|
public static double[] FlattenLines(List<Line> lines)
|
||||||
|
{
|
||||||
|
var result = new double[lines.Count * 4];
|
||||||
|
for (int i = 0; i < lines.Count; i++)
|
||||||
|
{
|
||||||
|
var line = lines[i];
|
||||||
|
result[i * 4] = line.pt1.X;
|
||||||
|
result[i * 4 + 1] = line.pt1.Y;
|
||||||
|
result[i * 4 + 2] = line.pt2.X;
|
||||||
|
result[i * 4 + 3] = line.pt2.Y;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Computes the minimum directional distance using raw edge arrays and location offsets
|
||||||
|
/// to avoid all intermediate object allocations.
|
||||||
|
/// </summary>
|
||||||
|
public static double DirectionalDistance(
|
||||||
|
(Vector start, Vector end)[] movingEdges, Vector movingOffset,
|
||||||
|
(Vector start, Vector end)[] stationaryEdges, Vector stationaryOffset,
|
||||||
|
PushDirection direction)
|
||||||
|
{
|
||||||
|
var minDist = double.MaxValue;
|
||||||
|
|
||||||
|
SortEdgesForPruning(stationaryEdges, direction);
|
||||||
|
|
||||||
|
// Case 1: Each moving vertex -> each stationary edge
|
||||||
|
var movingVertices = CollectVertices(movingEdges, movingOffset);
|
||||||
|
|
||||||
|
foreach (var mv in movingVertices)
|
||||||
|
{
|
||||||
|
var d = OneWayDistance(mv, stationaryEdges, stationaryOffset, direction);
|
||||||
|
if (d < minDist) minDist = d;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Case 2: Each stationary vertex -> each moving edge (opposite direction)
|
||||||
|
var opposite = OppositeDirection(direction);
|
||||||
|
SortEdgesForPruning(movingEdges, opposite);
|
||||||
|
|
||||||
|
var stationaryVertices = CollectVertices(stationaryEdges, stationaryOffset);
|
||||||
|
|
||||||
|
foreach (var sv in stationaryVertices)
|
||||||
|
{
|
||||||
|
var d = OneWayDistance(sv, movingEdges, movingOffset, opposite);
|
||||||
|
if (d < minDist) minDist = d;
|
||||||
|
}
|
||||||
|
|
||||||
|
return minDist;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static double OneWayDistance(
|
||||||
|
Vector vertex, (Vector start, Vector end)[] edges, Vector edgeOffset,
|
||||||
|
PushDirection direction)
|
||||||
|
{
|
||||||
|
var minDist = double.MaxValue;
|
||||||
|
var vx = vertex.X;
|
||||||
|
var vy = vertex.Y;
|
||||||
|
|
||||||
|
// Pruning: edges are sorted by their perpendicular min-coordinate in PartBoundary.
|
||||||
|
if (direction == PushDirection.Left || direction == PushDirection.Right)
|
||||||
|
{
|
||||||
|
for (var i = 0; i < edges.Length; i++)
|
||||||
|
{
|
||||||
|
var e1 = edges[i].start + edgeOffset;
|
||||||
|
var e2 = edges[i].end + edgeOffset;
|
||||||
|
|
||||||
|
var minY = e1.Y < e2.Y ? e1.Y : e2.Y;
|
||||||
|
var maxY = e1.Y > e2.Y ? e1.Y : e2.Y;
|
||||||
|
|
||||||
|
// Since edges are sorted by minY, if vy < minY, then vy < all subsequent minY.
|
||||||
|
if (vy < minY - Tolerance.Epsilon)
|
||||||
|
break;
|
||||||
|
|
||||||
|
if (vy > maxY + Tolerance.Epsilon)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var d = RayEdgeDistance(vx, vy, e1.X, e1.Y, e2.X, e2.Y, direction);
|
||||||
|
if (d < minDist) minDist = d;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else // Up/Down
|
||||||
|
{
|
||||||
|
for (var i = 0; i < edges.Length; i++)
|
||||||
|
{
|
||||||
|
var e1 = edges[i].start + edgeOffset;
|
||||||
|
var e2 = edges[i].end + edgeOffset;
|
||||||
|
|
||||||
|
var minX = e1.X < e2.X ? e1.X : e2.X;
|
||||||
|
var maxX = e1.X > e2.X ? e1.X : e2.X;
|
||||||
|
|
||||||
|
// Since edges are sorted by minX, if vx < minX, then vx < all subsequent minX.
|
||||||
|
if (vx < minX - Tolerance.Epsilon)
|
||||||
|
break;
|
||||||
|
|
||||||
|
if (vx > maxX + Tolerance.Epsilon)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var d = RayEdgeDistance(vx, vy, e1.X, e1.Y, e2.X, e2.Y, direction);
|
||||||
|
if (d < minDist) minDist = d;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return minDist;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PushDirection OppositeDirection(PushDirection direction)
|
||||||
|
{
|
||||||
|
switch (direction)
|
||||||
|
{
|
||||||
|
case PushDirection.Left: return PushDirection.Right;
|
||||||
|
case PushDirection.Right: return PushDirection.Left;
|
||||||
|
case PushDirection.Up: return PushDirection.Down;
|
||||||
|
case PushDirection.Down: return PushDirection.Up;
|
||||||
|
default: return direction;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool IsHorizontalDirection(PushDirection direction)
|
||||||
|
{
|
||||||
|
return direction is PushDirection.Left or PushDirection.Right;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static double EdgeDistance(Box box, Box boundary, PushDirection direction)
|
||||||
|
{
|
||||||
|
switch (direction)
|
||||||
|
{
|
||||||
|
case PushDirection.Left: return box.Left - boundary.Left;
|
||||||
|
case PushDirection.Right: return boundary.Right - box.Right;
|
||||||
|
case PushDirection.Up: return boundary.Top - box.Top;
|
||||||
|
case PushDirection.Down: return box.Bottom - boundary.Bottom;
|
||||||
|
default: return double.MaxValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Vector DirectionToOffset(PushDirection direction, double distance)
|
||||||
|
{
|
||||||
|
switch (direction)
|
||||||
|
{
|
||||||
|
case PushDirection.Left: return new Vector(-distance, 0);
|
||||||
|
case PushDirection.Right: return new Vector(distance, 0);
|
||||||
|
case PushDirection.Up: return new Vector(0, distance);
|
||||||
|
case PushDirection.Down: return new Vector(0, -distance);
|
||||||
|
default: return new Vector();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static double DirectionalGap(Box from, Box to, PushDirection direction)
|
||||||
|
{
|
||||||
|
switch (direction)
|
||||||
|
{
|
||||||
|
case PushDirection.Left: return from.Left - to.Right;
|
||||||
|
case PushDirection.Right: return to.Left - from.Right;
|
||||||
|
case PushDirection.Up: return to.Bottom - from.Top;
|
||||||
|
case PushDirection.Down: return from.Bottom - to.Top;
|
||||||
|
default: return double.MaxValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Generalized direction (Vector) overloads
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Computes how far a box can travel along the given unit direction
|
||||||
|
/// before exiting the boundary box.
|
||||||
|
/// </summary>
|
||||||
|
public static double EdgeDistance(Box box, Box boundary, Vector direction)
|
||||||
|
{
|
||||||
|
var dist = double.MaxValue;
|
||||||
|
|
||||||
|
if (direction.X < -Tolerance.Epsilon)
|
||||||
|
{
|
||||||
|
var d = (box.Left - boundary.Left) / -direction.X;
|
||||||
|
if (d < dist) dist = d;
|
||||||
|
}
|
||||||
|
else if (direction.X > Tolerance.Epsilon)
|
||||||
|
{
|
||||||
|
var d = (boundary.Right - box.Right) / direction.X;
|
||||||
|
if (d < dist) dist = d;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (direction.Y < -Tolerance.Epsilon)
|
||||||
|
{
|
||||||
|
var d = (box.Bottom - boundary.Bottom) / -direction.Y;
|
||||||
|
if (d < dist) dist = d;
|
||||||
|
}
|
||||||
|
else if (direction.Y > Tolerance.Epsilon)
|
||||||
|
{
|
||||||
|
var d = (boundary.Top - box.Top) / direction.Y;
|
||||||
|
if (d < dist) dist = d;
|
||||||
|
}
|
||||||
|
|
||||||
|
return dist < 0 ? 0 : dist;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Computes the directional gap between two boxes along an arbitrary unit direction.
|
||||||
|
/// Positive means 'to' is ahead of 'from' in the push direction.
|
||||||
|
/// </summary>
|
||||||
|
public static double DirectionalGap(Box from, Box to, Vector direction)
|
||||||
|
{
|
||||||
|
var fromMax = BoxProjectionMax(from, direction.X, direction.Y);
|
||||||
|
var toMin = BoxProjectionMin(to, direction.X, direction.Y);
|
||||||
|
return toMin - fromMax;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns true if two boxes overlap when projected onto the axis
|
||||||
|
/// perpendicular to the given unit direction.
|
||||||
|
/// </summary>
|
||||||
|
public static bool PerpendicularOverlap(Box a, Box b, Vector direction)
|
||||||
|
{
|
||||||
|
var px = -direction.Y;
|
||||||
|
var py = direction.X;
|
||||||
|
|
||||||
|
var aMin = BoxProjectionMin(a, px, py);
|
||||||
|
var aMax = BoxProjectionMax(a, px, py);
|
||||||
|
var bMin = BoxProjectionMin(b, px, py);
|
||||||
|
var bMax = BoxProjectionMax(b, px, py);
|
||||||
|
|
||||||
|
return aMin <= bMax + Tolerance.Epsilon && bMin <= aMax + Tolerance.Epsilon;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Computes the minimum translation distance along an arbitrary unit direction
|
||||||
|
/// before any edge of movingLines contacts any edge of stationaryLines.
|
||||||
|
/// </summary>
|
||||||
|
public static double DirectionalDistance(List<Line> movingLines, List<Line> stationaryLines, Vector direction)
|
||||||
|
{
|
||||||
|
var minDist = double.MaxValue;
|
||||||
|
var dirX = direction.X;
|
||||||
|
var dirY = direction.Y;
|
||||||
|
|
||||||
|
var movingVertices = CollectVertices(movingLines, Vector.Zero);
|
||||||
|
|
||||||
|
foreach (var mv in movingVertices)
|
||||||
|
{
|
||||||
|
for (var i = 0; i < stationaryLines.Count; i++)
|
||||||
|
{
|
||||||
|
var e = stationaryLines[i];
|
||||||
|
var d = RayEdgeDistance(mv.X, mv.Y, e.pt1.X, e.pt1.Y, e.pt2.X, e.pt2.Y, dirX, dirY);
|
||||||
|
if (d < minDist) minDist = d;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var oppX = -dirX;
|
||||||
|
var oppY = -dirY;
|
||||||
|
|
||||||
|
var stationaryVertices = CollectVertices(stationaryLines, Vector.Zero);
|
||||||
|
|
||||||
|
foreach (var sv in stationaryVertices)
|
||||||
|
{
|
||||||
|
for (var i = 0; i < movingLines.Count; i++)
|
||||||
|
{
|
||||||
|
var e = movingLines[i];
|
||||||
|
var d = RayEdgeDistance(sv.X, sv.Y, e.pt1.X, e.pt1.Y, e.pt2.X, e.pt2.Y, oppX, oppY);
|
||||||
|
if (d < minDist) minDist = d;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return minDist;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Computes the minimum translation distance along a push direction
|
||||||
|
/// before any vertex/edge of movingEntities contacts any vertex/edge of
|
||||||
|
/// stationaryEntities. Delegates to the Vector-based overload.
|
||||||
|
/// </summary>
|
||||||
|
public static double DirectionalDistance(
|
||||||
|
List<Entity> movingEntities, List<Entity> stationaryEntities, PushDirection direction)
|
||||||
|
{
|
||||||
|
return DirectionalDistance(movingEntities, stationaryEntities, DirectionToOffset(direction, 1.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Computes the minimum translation distance along an arbitrary unit direction
|
||||||
|
/// before any vertex/edge of movingEntities contacts any vertex/edge of
|
||||||
|
/// stationaryEntities. Works with native Line, Arc, and Circle entities
|
||||||
|
/// without tessellation.
|
||||||
|
/// </summary>
|
||||||
|
public static double DirectionalDistance(
|
||||||
|
List<Entity> movingEntities, List<Entity> stationaryEntities, Vector direction)
|
||||||
|
{
|
||||||
|
var minDist = double.MaxValue;
|
||||||
|
var dirX = direction.X;
|
||||||
|
var dirY = direction.Y;
|
||||||
|
|
||||||
|
var movingVertices = ExtractEntityVertices(movingEntities);
|
||||||
|
|
||||||
|
for (var v = 0; v < movingVertices.Length; v++)
|
||||||
|
{
|
||||||
|
var vx = movingVertices[v].X;
|
||||||
|
var vy = movingVertices[v].Y;
|
||||||
|
|
||||||
|
for (var j = 0; j < stationaryEntities.Count; j++)
|
||||||
|
{
|
||||||
|
var d = RayEntityDistance(vx, vy, stationaryEntities[j], dirX, dirY);
|
||||||
|
if (d < minDist)
|
||||||
|
{
|
||||||
|
minDist = d;
|
||||||
|
if (d <= 0) return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var oppX = -dirX;
|
||||||
|
var oppY = -dirY;
|
||||||
|
|
||||||
|
var stationaryVertices = ExtractEntityVertices(stationaryEntities);
|
||||||
|
|
||||||
|
for (var v = 0; v < stationaryVertices.Length; v++)
|
||||||
|
{
|
||||||
|
var vx = stationaryVertices[v].X;
|
||||||
|
var vy = stationaryVertices[v].Y;
|
||||||
|
|
||||||
|
for (var j = 0; j < movingEntities.Count; j++)
|
||||||
|
{
|
||||||
|
var d = RayEntityDistance(vx, vy, movingEntities[j], oppX, oppY);
|
||||||
|
if (d < minDist)
|
||||||
|
{
|
||||||
|
minDist = d;
|
||||||
|
if (d <= 0) return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 3: Arc-to-line closest-point check.
|
||||||
|
// Phases 1-2 sample arc endpoints and cardinal extremes, but the actual
|
||||||
|
// closest point on a small corner arc to a straight edge may lie between
|
||||||
|
// those samples. Use ClosestPointTo to find it and fire a ray from there.
|
||||||
|
minDist = ArcToLineClosestDistance(movingEntities, stationaryEntities, dirX, dirY, minDist);
|
||||||
|
if (minDist <= 0) return 0;
|
||||||
|
minDist = ArcToLineClosestDistance(stationaryEntities, movingEntities, oppX, oppY, minDist);
|
||||||
|
if (minDist <= 0) return 0;
|
||||||
|
|
||||||
|
// Phase 4: Curve-to-curve direct distance.
|
||||||
|
// The vertex-to-entity approach misses the closest contact between two
|
||||||
|
// curved entities (circles/arcs) because only a few cardinal vertices are
|
||||||
|
// sampled. The true closest contact along the push direction is found by
|
||||||
|
// treating it as a ray from one center to an expanded circle at the other
|
||||||
|
// center (radius = r1 + r2).
|
||||||
|
for (var i = 0; i < movingEntities.Count; i++)
|
||||||
|
{
|
||||||
|
var me = movingEntities[i];
|
||||||
|
if (!TryGetCurveParams(me, out var mcx, out var mcy, out var mr))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
for (var j = 0; j < stationaryEntities.Count; j++)
|
||||||
|
{
|
||||||
|
var se = stationaryEntities[j];
|
||||||
|
if (!TryGetCurveParams(se, out var scx, out var scy, out var sr))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var d = RayCircleDistance(mcx, mcy, scx, scy, mr + sr, dirX, dirY);
|
||||||
|
|
||||||
|
if (d >= minDist)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// For arcs, verify the contact point falls within both arcs' angular ranges.
|
||||||
|
if (me is Arc || se is Arc)
|
||||||
|
{
|
||||||
|
var mx = mcx + d * dirX;
|
||||||
|
var my = mcy + d * dirY;
|
||||||
|
var toCx = scx - mx;
|
||||||
|
var toCy = scy - my;
|
||||||
|
|
||||||
|
if (me is Arc mArc)
|
||||||
|
{
|
||||||
|
var angle = Angle.NormalizeRad(System.Math.Atan2(toCy, toCx));
|
||||||
|
if (!Angle.IsBetweenRad(angle, mArc.StartAngle, mArc.EndAngle, mArc.IsReversed))
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (se is Arc sArc)
|
||||||
|
{
|
||||||
|
var angle = Angle.NormalizeRad(System.Math.Atan2(-toCy, -toCx));
|
||||||
|
if (!Angle.IsBetweenRad(angle, sArc.StartAngle, sArc.EndAngle, sArc.IsReversed))
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
minDist = d;
|
||||||
|
if (d <= 0) return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return minDist;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double ArcToLineClosestDistance(
|
||||||
|
List<Entity> arcEntities, List<Entity> lineEntities,
|
||||||
|
double dirX, double dirY, double minDist)
|
||||||
|
{
|
||||||
|
for (var i = 0; i < arcEntities.Count; i++)
|
||||||
|
{
|
||||||
|
if (arcEntities[i] is Arc arc)
|
||||||
|
{
|
||||||
|
for (var j = 0; j < lineEntities.Count; j++)
|
||||||
|
{
|
||||||
|
if (lineEntities[j] is Line line)
|
||||||
|
{
|
||||||
|
var linePt = line.ClosestPointTo(arc.Center);
|
||||||
|
var arcPt = arc.ClosestPointTo(linePt);
|
||||||
|
var d = RayEdgeDistance(arcPt.X, arcPt.Y,
|
||||||
|
line.pt1.X, line.pt1.Y, line.pt2.X, line.pt2.Y,
|
||||||
|
dirX, dirY);
|
||||||
|
if (d < minDist) { minDist = d; if (d <= 0) return 0; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return minDist;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double RayEntityDistance(
|
||||||
|
double vx, double vy, Entity entity, double dirX, double dirY)
|
||||||
|
{
|
||||||
|
if (entity is Line line)
|
||||||
|
{
|
||||||
|
return RayEdgeDistance(vx, vy,
|
||||||
|
line.pt1.X, line.pt1.Y, line.pt2.X, line.pt2.Y,
|
||||||
|
dirX, dirY);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entity is Arc arc)
|
||||||
|
{
|
||||||
|
return RayArcDistance(vx, vy,
|
||||||
|
arc.Center.X, arc.Center.Y, arc.Radius,
|
||||||
|
arc.StartAngle, arc.EndAngle, arc.IsReversed,
|
||||||
|
dirX, dirY);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entity is Circle circle)
|
||||||
|
{
|
||||||
|
return RayCircleDistance(vx, vy,
|
||||||
|
circle.Center.X, circle.Center.Y, circle.Radius,
|
||||||
|
dirX, dirY);
|
||||||
|
}
|
||||||
|
|
||||||
|
return double.MaxValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Vector[] ExtractEntityVertices(List<Entity> entities)
|
||||||
|
{
|
||||||
|
var vertices = new HashSet<Vector>();
|
||||||
|
|
||||||
|
for (var i = 0; i < entities.Count; i++)
|
||||||
|
{
|
||||||
|
var entity = entities[i];
|
||||||
|
|
||||||
|
if (entity is Line line)
|
||||||
|
{
|
||||||
|
vertices.Add(line.pt1);
|
||||||
|
vertices.Add(line.pt2);
|
||||||
|
}
|
||||||
|
else if (entity is Arc arc)
|
||||||
|
{
|
||||||
|
vertices.Add(arc.StartPoint());
|
||||||
|
vertices.Add(arc.EndPoint());
|
||||||
|
AddArcExtremeVertices(vertices, arc);
|
||||||
|
}
|
||||||
|
else if (entity is Circle circle)
|
||||||
|
{
|
||||||
|
vertices.Add(new Vector(circle.Center.X + circle.Radius, circle.Center.Y));
|
||||||
|
vertices.Add(new Vector(circle.Center.X - circle.Radius, circle.Center.Y));
|
||||||
|
vertices.Add(new Vector(circle.Center.X, circle.Center.Y + circle.Radius));
|
||||||
|
vertices.Add(new Vector(circle.Center.X, circle.Center.Y - circle.Radius));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return vertices.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AddArcExtremeVertices(HashSet<Vector> points, Arc arc)
|
||||||
|
{
|
||||||
|
var a1 = arc.StartAngle;
|
||||||
|
var a2 = arc.EndAngle;
|
||||||
|
|
||||||
|
if (arc.IsReversed)
|
||||||
|
Generic.Swap(ref a1, ref a2);
|
||||||
|
|
||||||
|
if (Angle.IsBetweenRad(Angle.TwoPI, a1, a2))
|
||||||
|
points.Add(new Vector(arc.Center.X + arc.Radius, arc.Center.Y));
|
||||||
|
if (Angle.IsBetweenRad(Angle.HalfPI, a1, a2))
|
||||||
|
points.Add(new Vector(arc.Center.X, arc.Center.Y + arc.Radius));
|
||||||
|
if (Angle.IsBetweenRad(System.Math.PI, a1, a2))
|
||||||
|
points.Add(new Vector(arc.Center.X - arc.Radius, arc.Center.Y));
|
||||||
|
if (Angle.IsBetweenRad(System.Math.PI * 1.5, a1, a2))
|
||||||
|
points.Add(new Vector(arc.Center.X, arc.Center.Y - arc.Radius));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static HashSet<Vector> CollectVertices(List<Line> lines, Vector offset)
|
||||||
|
{
|
||||||
|
return CollectVertices(ToEdgeArray(lines), offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static HashSet<Vector> CollectVertices((Vector start, Vector end)[] edges, Vector offset)
|
||||||
|
{
|
||||||
|
var vertices = new HashSet<Vector>();
|
||||||
|
for (var i = 0; i < edges.Length; i++)
|
||||||
|
{
|
||||||
|
vertices.Add(edges[i].start + offset);
|
||||||
|
vertices.Add(edges[i].end + offset);
|
||||||
|
}
|
||||||
|
return vertices;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static (Vector start, Vector end)[] ToEdgeArray(List<Line> lines)
|
||||||
|
{
|
||||||
|
var edges = new (Vector start, Vector end)[lines.Count];
|
||||||
|
for (var i = 0; i < lines.Count; i++)
|
||||||
|
edges[i] = (lines[i].pt1, lines[i].pt2);
|
||||||
|
return edges;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void SortEdgesForPruning((Vector start, Vector end)[] edges, PushDirection direction)
|
||||||
|
{
|
||||||
|
if (direction == PushDirection.Left || direction == PushDirection.Right)
|
||||||
|
System.Array.Sort(edges, (a, b) =>
|
||||||
|
System.Math.Min(a.start.Y, a.end.Y).CompareTo(System.Math.Min(b.start.Y, b.end.Y)));
|
||||||
|
else
|
||||||
|
System.Array.Sort(edges, (a, b) =>
|
||||||
|
System.Math.Min(a.start.X, a.end.X).CompareTo(System.Math.Min(b.start.X, b.end.X)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryGetCurveParams(Entity entity, out double cx, out double cy, out double r)
|
||||||
|
{
|
||||||
|
if (entity is Circle circle)
|
||||||
|
{
|
||||||
|
cx = circle.Center.X; cy = circle.Center.Y; r = circle.Radius;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (entity is Arc arc)
|
||||||
|
{
|
||||||
|
cx = arc.Center.X; cy = arc.Center.Y; r = arc.Radius;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
cx = cy = r = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double BoxProjectionMin(Box box, double dx, double dy)
|
||||||
|
{
|
||||||
|
var x = dx >= 0 ? box.Left : box.Right;
|
||||||
|
var y = dy >= 0 ? box.Bottom : box.Top;
|
||||||
|
return x * dx + y * dy;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double BoxProjectionMax(Box box, double dx, double dy)
|
||||||
|
{
|
||||||
|
var x = dx >= 0 ? box.Right : box.Left;
|
||||||
|
var y = dy >= 0 ? box.Top : box.Bottom;
|
||||||
|
return x * dx + y * dy;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
public static Box GetLargestBoxVertically(Vector pt, Box bounds, IEnumerable<Box> boxes)
|
||||||
|
{
|
||||||
|
var verticalBoxes = boxes.Where(b => !(b.Left > pt.X || b.Right < pt.X)).ToList();
|
||||||
|
|
||||||
|
if (!FindVerticalLimits(pt, bounds, verticalBoxes, out var top, out var btm))
|
||||||
|
return Box.Empty;
|
||||||
|
|
||||||
|
var horizontalBoxes = boxes.Where(b => !(b.Bottom >= top || b.Top <= btm)).ToList();
|
||||||
|
|
||||||
|
if (!FindHorizontalLimits(pt, bounds, horizontalBoxes, out var lft, out var rgt))
|
||||||
|
return Box.Empty;
|
||||||
|
|
||||||
|
return new Box(lft, btm, rgt - lft, top - btm);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Box GetLargestBoxHorizontally(Vector pt, Box bounds, IEnumerable<Box> boxes)
|
||||||
|
{
|
||||||
|
var horizontalBoxes = boxes.Where(b => !(b.Bottom > pt.Y || b.Top < pt.Y)).ToList();
|
||||||
|
|
||||||
|
if (!FindHorizontalLimits(pt, bounds, horizontalBoxes, out var lft, out var rgt))
|
||||||
|
return Box.Empty;
|
||||||
|
|
||||||
|
var verticalBoxes = boxes.Where(b => !(b.Left >= rgt || b.Right <= lft)).ToList();
|
||||||
|
|
||||||
|
if (!FindVerticalLimits(pt, bounds, verticalBoxes, out var top, out var btm))
|
||||||
|
return Box.Empty;
|
||||||
|
|
||||||
|
return new Box(lft, btm, rgt - lft, top - btm);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool FindVerticalLimits(Vector pt, Box bounds, List<Box> boxes, out double top, out double btm)
|
||||||
|
{
|
||||||
|
top = double.MaxValue;
|
||||||
|
btm = double.MinValue;
|
||||||
|
|
||||||
|
foreach (var box in boxes)
|
||||||
|
{
|
||||||
|
var boxBtm = box.Bottom;
|
||||||
|
var boxTop = box.Top;
|
||||||
|
|
||||||
|
if (boxBtm > pt.Y && boxBtm < top)
|
||||||
|
top = boxBtm;
|
||||||
|
else if (box.Top < pt.Y && boxTop > btm)
|
||||||
|
btm = boxTop;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (top == double.MaxValue)
|
||||||
|
{
|
||||||
|
if (bounds.Top > pt.Y) top = bounds.Top;
|
||||||
|
else return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (btm == double.MinValue)
|
||||||
|
{
|
||||||
|
if (bounds.Bottom < pt.Y) btm = bounds.Bottom;
|
||||||
|
else return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool FindHorizontalLimits(Vector pt, Box bounds, List<Box> boxes, out double lft, out double rgt)
|
||||||
|
{
|
||||||
|
lft = double.MinValue;
|
||||||
|
rgt = double.MaxValue;
|
||||||
|
|
||||||
|
foreach (var box in boxes)
|
||||||
|
{
|
||||||
|
var boxLft = box.Left;
|
||||||
|
var boxRgt = box.Right;
|
||||||
|
|
||||||
|
if (boxLft > pt.X && boxLft < rgt)
|
||||||
|
rgt = boxLft;
|
||||||
|
else if (boxRgt < pt.X && boxRgt > lft)
|
||||||
|
lft = boxRgt;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rgt == double.MaxValue)
|
||||||
|
{
|
||||||
|
if (bounds.Right > pt.X) rgt = bounds.Right;
|
||||||
|
else return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lft == double.MinValue)
|
||||||
|
{
|
||||||
|
if (bounds.Left < pt.X) lft = bounds.Left;
|
||||||
|
else return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,197 @@
|
|||||||
|
using OpenNest.Math;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace OpenNest.Geometry
|
||||||
|
{
|
||||||
|
public static class SplineConverter
|
||||||
|
{
|
||||||
|
private const int MinPointsForArc = 3;
|
||||||
|
|
||||||
|
public static List<Entity> Convert(List<Vector> points, bool isClosed, double tolerance = 0.001)
|
||||||
|
{
|
||||||
|
if (points == null || points.Count < 2)
|
||||||
|
return new List<Entity>();
|
||||||
|
|
||||||
|
var entities = new List<Entity>();
|
||||||
|
var i = 0;
|
||||||
|
var chainedTangent = Vector.Invalid;
|
||||||
|
|
||||||
|
while (i < points.Count - 1)
|
||||||
|
{
|
||||||
|
var result = TryFitArc(points, i, chainedTangent, tolerance);
|
||||||
|
if (result != null)
|
||||||
|
{
|
||||||
|
entities.Add(result.Arc);
|
||||||
|
chainedTangent = result.EndTangent;
|
||||||
|
i = result.EndIndex;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
entities.Add(new Line(points[i], points[i + 1]));
|
||||||
|
chainedTangent = Vector.Invalid;
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return entities;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ArcFitResult TryFitArc(List<Vector> points, int start,
|
||||||
|
Vector chainedTangent, double tolerance)
|
||||||
|
{
|
||||||
|
var minEnd = start + MinPointsForArc - 1;
|
||||||
|
if (minEnd >= points.Count)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var hasTangent = chainedTangent.IsValid();
|
||||||
|
|
||||||
|
var subPoints = points.GetRange(start, MinPointsForArc);
|
||||||
|
var (center, radius, dev) = hasTangent
|
||||||
|
? FitWithStartTangent(subPoints, chainedTangent)
|
||||||
|
: FitCircumscribed(subPoints);
|
||||||
|
|
||||||
|
if (!center.IsValid() || dev > tolerance)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var endIdx = minEnd;
|
||||||
|
while (endIdx + 1 < points.Count)
|
||||||
|
{
|
||||||
|
var extPoints = points.GetRange(start, endIdx + 1 - start + 1);
|
||||||
|
var (nc, nr, nd) = hasTangent
|
||||||
|
? FitWithStartTangent(extPoints, chainedTangent)
|
||||||
|
: FitCircumscribed(extPoints);
|
||||||
|
|
||||||
|
if (!nc.IsValid() || nd > tolerance)
|
||||||
|
break;
|
||||||
|
|
||||||
|
endIdx++;
|
||||||
|
center = nc;
|
||||||
|
radius = nr;
|
||||||
|
dev = nd;
|
||||||
|
}
|
||||||
|
|
||||||
|
var finalPoints = points.GetRange(start, endIdx - start + 1);
|
||||||
|
var sweep = System.Math.Abs(SumSignedAngles(center, finalPoints));
|
||||||
|
if (sweep < Angle.ToRadians(5))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var arc = CreateArc(center, radius, finalPoints);
|
||||||
|
var endTangent = ComputeEndTangent(center, finalPoints);
|
||||||
|
|
||||||
|
return new ArcFitResult(arc, endTangent, endIdx);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static (Vector center, double radius, double deviation) FitCircumscribed(
|
||||||
|
List<Vector> points)
|
||||||
|
{
|
||||||
|
if (points.Count < 3)
|
||||||
|
return (Vector.Invalid, 0, double.MaxValue);
|
||||||
|
|
||||||
|
var p0 = points[0];
|
||||||
|
var pMid = points[points.Count / 2];
|
||||||
|
var pEnd = points[^1];
|
||||||
|
|
||||||
|
// Find circumcenter by intersecting perpendicular bisectors of two chords
|
||||||
|
var (center, radius) = Circumcenter(p0, pMid, pEnd);
|
||||||
|
if (!center.IsValid())
|
||||||
|
return (Vector.Invalid, 0, double.MaxValue);
|
||||||
|
|
||||||
|
return (center, radius, MaxRadialDeviation(points, center.X, center.Y, radius));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static (Vector center, double radius) Circumcenter(Vector a, Vector b, Vector c)
|
||||||
|
{
|
||||||
|
// Perpendicular bisector of chord a-b
|
||||||
|
var m1x = (a.X + b.X) / 2;
|
||||||
|
var m1y = (a.Y + b.Y) / 2;
|
||||||
|
var d1x = -(b.Y - a.Y);
|
||||||
|
var d1y = b.X - a.X;
|
||||||
|
|
||||||
|
// Perpendicular bisector of chord b-c
|
||||||
|
var m2x = (b.X + c.X) / 2;
|
||||||
|
var m2y = (b.Y + c.Y) / 2;
|
||||||
|
var d2x = -(c.Y - b.Y);
|
||||||
|
var d2y = c.X - b.X;
|
||||||
|
|
||||||
|
var det = d1x * d2y - d1y * d2x;
|
||||||
|
if (System.Math.Abs(det) < 1e-10)
|
||||||
|
return (Vector.Invalid, 0);
|
||||||
|
|
||||||
|
var t = ((m2x - m1x) * d2y - (m2y - m1y) * d2x) / det;
|
||||||
|
|
||||||
|
var cx = m1x + t * d1x;
|
||||||
|
var cy = m1y + t * d1y;
|
||||||
|
var radius = System.Math.Sqrt((cx - a.X) * (cx - a.X) + (cy - a.Y) * (cy - a.Y));
|
||||||
|
|
||||||
|
if (radius < 1e-10)
|
||||||
|
return (Vector.Invalid, 0);
|
||||||
|
|
||||||
|
return (new Vector(cx, cy), radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static (Vector center, double radius, double deviation) FitWithStartTangent(
|
||||||
|
List<Vector> points, Vector tangent) =>
|
||||||
|
ArcFit.FitWithStartTangent(points, tangent);
|
||||||
|
|
||||||
|
private static double MaxRadialDeviation(List<Vector> points, double cx, double cy, double radius) =>
|
||||||
|
ArcFit.MaxRadialDeviation(points, cx, cy, radius);
|
||||||
|
|
||||||
|
private static double SumSignedAngles(Vector center, List<Vector> points)
|
||||||
|
{
|
||||||
|
var total = 0.0;
|
||||||
|
for (var i = 0; i < points.Count - 1; i++)
|
||||||
|
{
|
||||||
|
var a1 = System.Math.Atan2(points[i].Y - center.Y, points[i].X - center.X);
|
||||||
|
var a2 = System.Math.Atan2(points[i + 1].Y - center.Y, points[i + 1].X - center.X);
|
||||||
|
var da = a2 - a1;
|
||||||
|
while (da > System.Math.PI) da -= Angle.TwoPI;
|
||||||
|
while (da < -System.Math.PI) da += Angle.TwoPI;
|
||||||
|
total += da;
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Vector ComputeEndTangent(Vector center, List<Vector> points)
|
||||||
|
{
|
||||||
|
var lastPt = points[^1];
|
||||||
|
var totalAngle = SumSignedAngles(center, points);
|
||||||
|
|
||||||
|
var rx = lastPt.X - center.X;
|
||||||
|
var ry = lastPt.Y - center.Y;
|
||||||
|
|
||||||
|
return totalAngle >= 0
|
||||||
|
? new Vector(-ry, rx)
|
||||||
|
: new Vector(ry, -rx);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Arc CreateArc(Vector center, double radius, List<Vector> points)
|
||||||
|
{
|
||||||
|
var firstPoint = points[0];
|
||||||
|
var lastPoint = points[^1];
|
||||||
|
|
||||||
|
var startAngle = System.Math.Atan2(firstPoint.Y - center.Y, firstPoint.X - center.X);
|
||||||
|
var endAngle = System.Math.Atan2(lastPoint.Y - center.Y, lastPoint.X - center.X);
|
||||||
|
var isReversed = SumSignedAngles(center, points) < 0;
|
||||||
|
|
||||||
|
if (startAngle < 0) startAngle += Angle.TwoPI;
|
||||||
|
if (endAngle < 0) endAngle += Angle.TwoPI;
|
||||||
|
|
||||||
|
return new Arc(center, radius, startAngle, endAngle, isReversed);
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class ArcFitResult
|
||||||
|
{
|
||||||
|
public Arc Arc { get; }
|
||||||
|
public Vector EndTangent { get; }
|
||||||
|
public int EndIndex { get; }
|
||||||
|
|
||||||
|
public ArcFitResult(Arc arc, Vector endTangent, int endIndex)
|
||||||
|
{
|
||||||
|
Arc = arc;
|
||||||
|
EndTangent = endTangent;
|
||||||
|
EndIndex = endIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
using System;
|
using OpenNest.Math;
|
||||||
using OpenNest.Math;
|
using System;
|
||||||
|
|
||||||
namespace OpenNest.Geometry
|
namespace OpenNest.Geometry
|
||||||
{
|
{
|
||||||
public struct Vector
|
public struct Vector : IEquatable<Vector>
|
||||||
{
|
{
|
||||||
public static readonly Vector Invalid = new Vector(double.NaN, double.NaN);
|
public static readonly Vector Invalid = new Vector(double.NaN, double.NaN);
|
||||||
public static readonly Vector Zero = new Vector(0, 0);
|
public static readonly Vector Zero = new Vector(0, 0);
|
||||||
@@ -17,6 +17,29 @@ namespace OpenNest.Geometry
|
|||||||
Y = y;
|
Y = y;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool Equals(Vector other)
|
||||||
|
{
|
||||||
|
return X.IsEqualTo(other.X) && Y.IsEqualTo(other.Y);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool Equals(object obj)
|
||||||
|
{
|
||||||
|
return obj is Vector other && Equals(other);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override int GetHashCode()
|
||||||
|
{
|
||||||
|
unchecked
|
||||||
|
{
|
||||||
|
// Use a simple but effective hash combine.
|
||||||
|
// We use a small epsilon-safe rounding if needed, but for uniqueness in HashSet
|
||||||
|
// during a single operation, raw bits or slightly rounded is usually fine.
|
||||||
|
// However, IsEqualTo uses Tolerance.Epsilon, so we should probably round to some precision.
|
||||||
|
// But typically for these geometric algorithms, exact matches (or very close) are what we want to prune.
|
||||||
|
return (X.GetHashCode() * 397) ^ Y.GetHashCode();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public double DistanceTo(Vector pt)
|
public double DistanceTo(Vector pt)
|
||||||
{
|
{
|
||||||
var vx = pt.X - X;
|
var vx = pt.X - X;
|
||||||
@@ -186,21 +209,6 @@ namespace OpenNest.Geometry
|
|||||||
return new Vector(X, Y);
|
return new Vector(X, Y);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override bool Equals(object obj)
|
|
||||||
{
|
|
||||||
if (!(obj is Vector))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
var pt = (Vector)obj;
|
|
||||||
|
|
||||||
return (X.IsEqualTo(pt.X)) && (Y.IsEqualTo(pt.Y));
|
|
||||||
}
|
|
||||||
|
|
||||||
public override int GetHashCode()
|
|
||||||
{
|
|
||||||
return base.GetHashCode();
|
|
||||||
}
|
|
||||||
|
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
return string.Format("[Vector: X:{0}, Y:{1}]", X, Y);
|
return string.Format("[Vector: X:{0}, Y:{1}]", X, Y);
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,9 @@
|
|||||||
|
namespace OpenNest
|
||||||
|
{
|
||||||
|
public interface IConfigurablePostProcessor : IPostProcessor
|
||||||
|
{
|
||||||
|
object Config { get; }
|
||||||
|
|
||||||
|
void SaveConfig();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace OpenNest
|
||||||
|
{
|
||||||
|
public interface IMaterialProvidingPostProcessor
|
||||||
|
{
|
||||||
|
IEnumerable<string> GetMaterialNames();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace OpenNest
|
||||||
|
{
|
||||||
|
public interface IPostProcessorNestAware
|
||||||
|
{
|
||||||
|
void PrepareForNest(Nest nest);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,4 @@
|
|||||||
using System;
|
namespace OpenNest.Math
|
||||||
|
|
||||||
namespace OpenNest.Math
|
|
||||||
{
|
{
|
||||||
public static class Angle
|
public static class Angle
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,158 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
|
|
||||||
|
namespace OpenNest.Math
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Recursive descent parser for simple arithmetic expressions supporting
|
||||||
|
/// +, -, *, /, parentheses, unary minus/plus, and $variable references.
|
||||||
|
/// </summary>
|
||||||
|
public static class ExpressionEvaluator
|
||||||
|
{
|
||||||
|
public static double Evaluate(string expression, IReadOnlyDictionary<string, double> variables)
|
||||||
|
{
|
||||||
|
var parser = new Parser(expression, variables);
|
||||||
|
var result = parser.ParseExpression();
|
||||||
|
parser.SkipWhitespace();
|
||||||
|
if (!parser.IsEnd)
|
||||||
|
throw new FormatException($"Unexpected character at position {parser.Position}: '{parser.Current}'");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ref struct Parser
|
||||||
|
{
|
||||||
|
private readonly ReadOnlySpan<char> _input;
|
||||||
|
private readonly IReadOnlyDictionary<string, double> _variables;
|
||||||
|
private int _pos;
|
||||||
|
|
||||||
|
public Parser(string input, IReadOnlyDictionary<string, double> variables)
|
||||||
|
{
|
||||||
|
_input = input.AsSpan();
|
||||||
|
_variables = variables;
|
||||||
|
_pos = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Position => _pos;
|
||||||
|
public bool IsEnd => _pos >= _input.Length;
|
||||||
|
public char Current => _input[_pos];
|
||||||
|
|
||||||
|
public void SkipWhitespace()
|
||||||
|
{
|
||||||
|
while (_pos < _input.Length && _input[_pos] == ' ')
|
||||||
|
_pos++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expression = Term (('+' | '-') Term)*
|
||||||
|
public double ParseExpression()
|
||||||
|
{
|
||||||
|
SkipWhitespace();
|
||||||
|
var left = ParseTerm();
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
SkipWhitespace();
|
||||||
|
if (IsEnd) break;
|
||||||
|
|
||||||
|
var op = Current;
|
||||||
|
if (op != '+' && op != '-') break;
|
||||||
|
|
||||||
|
_pos++;
|
||||||
|
SkipWhitespace();
|
||||||
|
var right = ParseTerm();
|
||||||
|
|
||||||
|
left = op == '+' ? left + right : left - right;
|
||||||
|
}
|
||||||
|
|
||||||
|
return left;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Term = Unary (('*' | '/') Unary)*
|
||||||
|
private double ParseTerm()
|
||||||
|
{
|
||||||
|
var left = ParseUnary();
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
SkipWhitespace();
|
||||||
|
if (IsEnd) break;
|
||||||
|
|
||||||
|
var op = Current;
|
||||||
|
if (op != '*' && op != '/') break;
|
||||||
|
|
||||||
|
_pos++;
|
||||||
|
SkipWhitespace();
|
||||||
|
var right = ParseUnary();
|
||||||
|
|
||||||
|
left = op == '*' ? left * right : left / right;
|
||||||
|
}
|
||||||
|
|
||||||
|
return left;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unary = ('-' | '+')? Primary
|
||||||
|
private double ParseUnary()
|
||||||
|
{
|
||||||
|
SkipWhitespace();
|
||||||
|
if (!IsEnd && Current == '-')
|
||||||
|
{
|
||||||
|
_pos++;
|
||||||
|
return -ParsePrimary();
|
||||||
|
}
|
||||||
|
if (!IsEnd && Current == '+')
|
||||||
|
{
|
||||||
|
_pos++;
|
||||||
|
}
|
||||||
|
return ParsePrimary();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Primary = '(' Expression ')' | '$' Identifier | Number
|
||||||
|
private double ParsePrimary()
|
||||||
|
{
|
||||||
|
SkipWhitespace();
|
||||||
|
|
||||||
|
if (IsEnd)
|
||||||
|
throw new FormatException("Unexpected end of expression.");
|
||||||
|
|
||||||
|
if (Current == '(')
|
||||||
|
{
|
||||||
|
_pos++; // consume '('
|
||||||
|
var value = ParseExpression();
|
||||||
|
SkipWhitespace();
|
||||||
|
if (IsEnd || Current != ')')
|
||||||
|
throw new FormatException("Expected closing parenthesis.");
|
||||||
|
_pos++; // consume ')'
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Current == '$')
|
||||||
|
{
|
||||||
|
_pos++; // consume '$'
|
||||||
|
var start = _pos;
|
||||||
|
while (_pos < _input.Length && (char.IsLetterOrDigit(_input[_pos]) || _input[_pos] == '_'))
|
||||||
|
_pos++;
|
||||||
|
if (_pos == start)
|
||||||
|
throw new FormatException("Expected variable name after '$'.");
|
||||||
|
var name = _input.Slice(start, _pos - start).ToString();
|
||||||
|
if (!_variables.TryGetValue(name, out var varValue))
|
||||||
|
throw new KeyNotFoundException($"Undefined variable: ${name}");
|
||||||
|
return varValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Number
|
||||||
|
var numStart = _pos;
|
||||||
|
while (_pos < _input.Length && (char.IsDigit(_input[_pos]) || _input[_pos] == '.'))
|
||||||
|
_pos++;
|
||||||
|
|
||||||
|
if (_pos == numStart)
|
||||||
|
throw new FormatException($"Unexpected character '{Current}' at position {_pos}.");
|
||||||
|
|
||||||
|
var numSpan = _input.Slice(numStart, _pos - numStart).ToString();
|
||||||
|
if (!double.TryParse(numSpan, NumberStyles.Float, CultureInfo.InvariantCulture, out var number))
|
||||||
|
throw new FormatException($"Invalid number: '{numSpan}'");
|
||||||
|
|
||||||
|
return number;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
namespace OpenNest.Math
|
||||||
|
{
|
||||||
|
public static class Rounding
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Rounds a number down to the nearest factor.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="num"></param>
|
||||||
|
/// <param name="factor"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static double RoundDownToNearest(double num, double factor)
|
||||||
|
{
|
||||||
|
return factor.IsEqualTo(0) ? num : System.Math.Floor(num / factor) * factor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Rounds a number up to the nearest factor.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="num"></param>
|
||||||
|
/// <param name="factor"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static double RoundUpToNearest(double num, double factor)
|
||||||
|
{
|
||||||
|
return factor.IsEqualTo(0) ? num : System.Math.Ceiling(num / factor) * factor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Rounds a number to the nearest factor using midpoint rounding convention.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="num"></param>
|
||||||
|
/// <param name="factor"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static double RoundToNearest(double num, double factor)
|
||||||
|
{
|
||||||
|
return factor.IsEqualTo(0) ? num : System.Math.Round(num / factor) * factor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,4 @@
|
|||||||
using System;
|
namespace OpenNest.Math
|
||||||
|
|
||||||
namespace OpenNest.Math
|
|
||||||
{
|
{
|
||||||
public static class Tolerance
|
public static class Tolerance
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
using System;
|
namespace OpenNest.Math
|
||||||
|
|
||||||
namespace OpenNest.Math
|
|
||||||
{
|
{
|
||||||
public static class Trigonometry
|
public static class Trigonometry
|
||||||
{
|
{
|
||||||
|
|||||||
+14
-18
@@ -1,6 +1,7 @@
|
|||||||
using System;
|
using OpenNest.Collections;
|
||||||
using OpenNest.Collections;
|
|
||||||
using OpenNest.Geometry;
|
using OpenNest.Geometry;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace OpenNest
|
namespace OpenNest
|
||||||
{
|
{
|
||||||
@@ -21,6 +22,7 @@ namespace OpenNest
|
|||||||
Plates.ItemRemoved += Plates_PlateRemoved;
|
Plates.ItemRemoved += Plates_PlateRemoved;
|
||||||
Drawings = new DrawingCollection();
|
Drawings = new DrawingCollection();
|
||||||
PlateDefaults = new PlateSettings();
|
PlateDefaults = new PlateSettings();
|
||||||
|
Material = new Material();
|
||||||
Customer = string.Empty;
|
Customer = string.Empty;
|
||||||
Notes = string.Empty;
|
Notes = string.Empty;
|
||||||
}
|
}
|
||||||
@@ -36,6 +38,12 @@ namespace OpenNest
|
|||||||
|
|
||||||
public string Notes { get; set; }
|
public string Notes { get; set; }
|
||||||
|
|
||||||
|
public string AssistGas { get; set; } = "";
|
||||||
|
|
||||||
|
public double Thickness { get; set; }
|
||||||
|
|
||||||
|
public Material Material { get; set; }
|
||||||
|
|
||||||
public Units Units { get; set; }
|
public Units Units { get; set; }
|
||||||
|
|
||||||
public DateTime DateCreated { get; set; }
|
public DateTime DateCreated { get; set; }
|
||||||
@@ -44,6 +52,10 @@ namespace OpenNest
|
|||||||
|
|
||||||
public PlateSettings PlateDefaults { get; set; }
|
public PlateSettings PlateDefaults { get; set; }
|
||||||
|
|
||||||
|
public List<PlateOption> PlateOptions { get; set; } = new();
|
||||||
|
|
||||||
|
public double SalvageRate { get; set; } = 0.5;
|
||||||
|
|
||||||
public Plate CreatePlate()
|
public Plate CreatePlate()
|
||||||
{
|
{
|
||||||
var plate = PlateDefaults.CreateNew();
|
var plate = PlateDefaults.CreateNew();
|
||||||
@@ -82,18 +94,6 @@ namespace OpenNest
|
|||||||
set { plate.Quadrant = value; }
|
set { plate.Quadrant = value; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public double Thickness
|
|
||||||
{
|
|
||||||
get { return plate.Thickness; }
|
|
||||||
set { plate.Thickness = value; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public Material Material
|
|
||||||
{
|
|
||||||
get { return plate.Material; }
|
|
||||||
set { plate.Material = value; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public Size Size
|
public Size Size
|
||||||
{
|
{
|
||||||
get { return plate.Size; }
|
get { return plate.Size; }
|
||||||
@@ -114,9 +114,7 @@ namespace OpenNest
|
|||||||
|
|
||||||
public void SetFromExisting(Plate plate)
|
public void SetFromExisting(Plate plate)
|
||||||
{
|
{
|
||||||
Thickness = plate.Thickness;
|
|
||||||
Quadrant = plate.Quadrant;
|
Quadrant = plate.Quadrant;
|
||||||
Material = plate.Material;
|
|
||||||
Size = plate.Size;
|
Size = plate.Size;
|
||||||
EdgeSpacing = plate.EdgeSpacing;
|
EdgeSpacing = plate.EdgeSpacing;
|
||||||
PartSpacing = plate.PartSpacing;
|
PartSpacing = plate.PartSpacing;
|
||||||
@@ -126,11 +124,9 @@ namespace OpenNest
|
|||||||
{
|
{
|
||||||
return new Plate()
|
return new Plate()
|
||||||
{
|
{
|
||||||
Thickness = Thickness,
|
|
||||||
Size = Size,
|
Size = Size,
|
||||||
EdgeSpacing = EdgeSpacing,
|
EdgeSpacing = EdgeSpacing,
|
||||||
PartSpacing = PartSpacing,
|
PartSpacing = PartSpacing,
|
||||||
Material = Material,
|
|
||||||
Quadrant = Quadrant,
|
Quadrant = Quadrant,
|
||||||
Quantity = 1
|
Quantity = 1
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using System;
|
using OpenNest.Math;
|
||||||
using OpenNest.Math;
|
|
||||||
|
|
||||||
namespace OpenNest
|
namespace OpenNest
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -4,6 +4,9 @@
|
|||||||
<RootNamespace>OpenNest</RootNamespace>
|
<RootNamespace>OpenNest</RootNamespace>
|
||||||
<AssemblyName>OpenNest.Core</AssemblyName>
|
<AssemblyName>OpenNest.Core</AssemblyName>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<InternalsVisibleTo Include="OpenNest.Tests" />
|
||||||
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Clipper2" Version="2.0.0" />
|
<PackageReference Include="Clipper2" Version="2.0.0" />
|
||||||
<PackageReference Include="System.Drawing.Common" Version="8.0.10" />
|
<PackageReference Include="System.Drawing.Common" Version="8.0.10" />
|
||||||
|
|||||||
+104
-26
@@ -1,8 +1,9 @@
|
|||||||
using System.Collections.Generic;
|
using OpenNest.CNC;
|
||||||
using System.Linq;
|
|
||||||
using OpenNest.CNC;
|
|
||||||
using OpenNest.Converters;
|
using OpenNest.Converters;
|
||||||
using OpenNest.Geometry;
|
using OpenNest.Geometry;
|
||||||
|
using OpenNest.Math;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
namespace OpenNest
|
namespace OpenNest
|
||||||
{
|
{
|
||||||
@@ -20,6 +21,8 @@ namespace OpenNest
|
|||||||
public class Part : IPart, IBoundable
|
public class Part : IPart, IBoundable
|
||||||
{
|
{
|
||||||
private Vector location;
|
private Vector location;
|
||||||
|
private bool ownsProgram;
|
||||||
|
private double preLeadInRotation;
|
||||||
|
|
||||||
public readonly Drawing BaseDrawing;
|
public readonly Drawing BaseDrawing;
|
||||||
|
|
||||||
@@ -32,6 +35,7 @@ namespace OpenNest
|
|||||||
{
|
{
|
||||||
BaseDrawing = baseDrawing;
|
BaseDrawing = baseDrawing;
|
||||||
Program = baseDrawing.Program.Clone() as Program;
|
Program = baseDrawing.Program.Clone() as Program;
|
||||||
|
ownsProgram = true;
|
||||||
this.location = location;
|
this.location = location;
|
||||||
UpdateBounds();
|
UpdateBounds();
|
||||||
}
|
}
|
||||||
@@ -51,12 +55,63 @@ namespace OpenNest
|
|||||||
|
|
||||||
public Program Program { get; private set; }
|
public Program Program { get; private set; }
|
||||||
|
|
||||||
|
public bool HasManualLeadIns { get; set; }
|
||||||
|
|
||||||
|
public bool LeadInsLocked { get; set; }
|
||||||
|
|
||||||
|
public CNC.CuttingStrategy.CuttingParameters CuttingParameters { get; set; }
|
||||||
|
|
||||||
|
public void ApplyLeadIns(CNC.CuttingStrategy.CuttingParameters parameters, Vector approachPoint)
|
||||||
|
{
|
||||||
|
ApplyLeadIns(parameters, approachPoint, Geometry.Vector.Invalid);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ApplyLeadIns(CNC.CuttingStrategy.CuttingParameters parameters, Vector approachPoint, Vector nextPartStart)
|
||||||
|
{
|
||||||
|
preLeadInRotation = Rotation;
|
||||||
|
var strategy = new CNC.CuttingStrategy.ContourCuttingStrategy { Parameters = parameters };
|
||||||
|
var result = strategy.Apply(Program, approachPoint, nextPartStart);
|
||||||
|
Program = result.Program;
|
||||||
|
CuttingParameters = parameters;
|
||||||
|
HasManualLeadIns = true;
|
||||||
|
UpdateBounds();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ApplySingleLeadIn(CNC.CuttingStrategy.CuttingParameters parameters,
|
||||||
|
Geometry.Vector point, Geometry.Entity entity, CNC.CuttingStrategy.ContourType contourType)
|
||||||
|
{
|
||||||
|
preLeadInRotation = Rotation;
|
||||||
|
var strategy = new CNC.CuttingStrategy.ContourCuttingStrategy { Parameters = parameters };
|
||||||
|
var result = strategy.ApplySingle(Program, point, entity, contourType);
|
||||||
|
Program = result.Program;
|
||||||
|
CuttingParameters = parameters;
|
||||||
|
HasManualLeadIns = true;
|
||||||
|
UpdateBounds();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RemoveLeadIns()
|
||||||
|
{
|
||||||
|
var rotation = preLeadInRotation;
|
||||||
|
var location = Location;
|
||||||
|
Program = BaseDrawing.Program.Clone() as Program;
|
||||||
|
ownsProgram = true;
|
||||||
|
|
||||||
|
if (!Math.Tolerance.IsEqualTo(rotation, 0))
|
||||||
|
Program.Rotate(rotation);
|
||||||
|
|
||||||
|
Location = location;
|
||||||
|
HasManualLeadIns = false;
|
||||||
|
LeadInsLocked = false;
|
||||||
|
CuttingParameters = null;
|
||||||
|
UpdateBounds();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the rotation of the part in radians.
|
/// Gets the rotation of the part in radians.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public double Rotation
|
public double Rotation
|
||||||
{
|
{
|
||||||
get { return Program.Rotation; }
|
get { return HasManualLeadIns ? preLeadInRotation : Program.Rotation; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -65,8 +120,10 @@ namespace OpenNest
|
|||||||
/// <param name="angle">Angle of rotation in radians.</param>
|
/// <param name="angle">Angle of rotation in radians.</param>
|
||||||
public void Rotate(double angle)
|
public void Rotate(double angle)
|
||||||
{
|
{
|
||||||
|
EnsureOwnedProgram();
|
||||||
Program.Rotate(angle);
|
Program.Rotate(angle);
|
||||||
location = Location.Rotate(angle);
|
location = Location.Rotate(angle);
|
||||||
|
preLeadInRotation = Program.Rotation;
|
||||||
UpdateBounds();
|
UpdateBounds();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,8 +134,10 @@ namespace OpenNest
|
|||||||
/// <param name="origin">The origin to rotate the part around.</param>
|
/// <param name="origin">The origin to rotate the part around.</param>
|
||||||
public void Rotate(double angle, Vector origin)
|
public void Rotate(double angle, Vector origin)
|
||||||
{
|
{
|
||||||
|
EnsureOwnedProgram();
|
||||||
Program.Rotate(angle);
|
Program.Rotate(angle);
|
||||||
location = Location.Rotate(angle, origin);
|
location = Location.Rotate(angle, origin);
|
||||||
|
preLeadInRotation = Program.Rotation;
|
||||||
UpdateBounds();
|
UpdateBounds();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,7 +195,14 @@ namespace OpenNest
|
|||||||
{
|
{
|
||||||
var rotation = Rotation;
|
var rotation = Rotation;
|
||||||
Program = BaseDrawing.Program.Clone() as Program;
|
Program = BaseDrawing.Program.Clone() as Program;
|
||||||
Program.Rotate(Program.Rotation - rotation);
|
|
||||||
|
if (!Math.Tolerance.IsEqualTo(rotation, 0))
|
||||||
|
Program.Rotate(rotation);
|
||||||
|
|
||||||
|
HasManualLeadIns = false;
|
||||||
|
LeadInsLocked = false;
|
||||||
|
CuttingParameters = null;
|
||||||
|
UpdateBounds();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -149,31 +215,33 @@ namespace OpenNest
|
|||||||
pts = new List<Vector>();
|
pts = new List<Vector>();
|
||||||
|
|
||||||
var entities1 = ConvertProgram.ToGeometry(Program)
|
var entities1 = ConvertProgram.ToGeometry(Program)
|
||||||
.Where(e => e.Layer != SpecialLayers.Rapid);
|
.Where(e => e.Layer != SpecialLayers.Rapid)
|
||||||
|
.ToList();
|
||||||
var entities2 = ConvertProgram.ToGeometry(part.Program)
|
var entities2 = ConvertProgram.ToGeometry(part.Program)
|
||||||
.Where(e => e.Layer != SpecialLayers.Rapid);
|
.Where(e => e.Layer != SpecialLayers.Rapid)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
var shapes1 = Helper.GetShapes(entities1);
|
if (entities1.Count == 0 || entities2.Count == 0)
|
||||||
var shapes2 = Helper.GetShapes(entities2);
|
return false;
|
||||||
|
|
||||||
shapes1.ForEach(shape => shape.Offset(Location));
|
var perimeter1 = new ShapeProfile(entities1).Perimeter;
|
||||||
shapes2.ForEach(shape => shape.Offset(part.Location));
|
var perimeter2 = new ShapeProfile(entities2).Perimeter;
|
||||||
|
|
||||||
for (int i = 0; i < shapes1.Count; i++)
|
if (perimeter1 == null || perimeter2 == null)
|
||||||
{
|
return false;
|
||||||
var shape1 = shapes1[i];
|
|
||||||
|
|
||||||
for (int j = 0; j < shapes2.Count; j++)
|
var polygon1 = perimeter1.ToPolygon();
|
||||||
{
|
var polygon2 = perimeter2.ToPolygon();
|
||||||
var shape2 = shapes2[j];
|
|
||||||
List<Vector> pts2;
|
|
||||||
|
|
||||||
if (shape1.Intersects(shape2, out pts2))
|
if (polygon1 == null || polygon2 == null)
|
||||||
pts.AddRange(pts2);
|
return false;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return pts.Count > 0;
|
polygon1.Offset(Location);
|
||||||
|
polygon2.Offset(part.Location);
|
||||||
|
|
||||||
|
var result = Geometry.Collision.Check(polygon1, polygon2);
|
||||||
|
pts = result.IntersectionPoints.ToList();
|
||||||
|
return result.Overlaps;
|
||||||
}
|
}
|
||||||
|
|
||||||
public double Left
|
public double Left
|
||||||
@@ -216,15 +284,25 @@ namespace OpenNest
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public Part CloneAtOffset(Vector offset)
|
public Part CloneAtOffset(Vector offset)
|
||||||
{
|
{
|
||||||
var clonedProgram = Program.Clone() as Program;
|
// Share the Program instance — offset-only copies don't modify the program codes.
|
||||||
var part = new Part(BaseDrawing, clonedProgram,
|
// This is a major performance win for tiling large patterns.
|
||||||
|
var part = new Part(BaseDrawing, Program,
|
||||||
location + offset,
|
location + offset,
|
||||||
new Box(BoundingBox.X + offset.X, BoundingBox.Y + offset.Y,
|
new Box(BoundingBox.X + offset.X, BoundingBox.Y + offset.Y,
|
||||||
BoundingBox.Width, BoundingBox.Length));
|
BoundingBox.Length, BoundingBox.Width));
|
||||||
|
|
||||||
return part;
|
return part;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void EnsureOwnedProgram()
|
||||||
|
{
|
||||||
|
if (!ownsProgram)
|
||||||
|
{
|
||||||
|
Program = Program.Clone() as Program;
|
||||||
|
ownsProgram = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private Part(Drawing baseDrawing, Program program, Vector location, Box boundingBox)
|
private Part(Drawing baseDrawing, Program program, Vector location, Box boundingBox)
|
||||||
{
|
{
|
||||||
BaseDrawing = baseDrawing;
|
BaseDrawing = baseDrawing;
|
||||||
|
|||||||
@@ -0,0 +1,326 @@
|
|||||||
|
using OpenNest.Converters;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace OpenNest
|
||||||
|
{
|
||||||
|
public static class PartGeometry
|
||||||
|
{
|
||||||
|
public static List<Line> GetPartLines(Part part, double chordTolerance = 0.001)
|
||||||
|
{
|
||||||
|
var entities = ConvertProgram.ToGeometry(part.Program);
|
||||||
|
var shapes = ShapeBuilder.GetShapes(entities.Where(e => e.Layer != SpecialLayers.Rapid));
|
||||||
|
var lines = new List<Line>();
|
||||||
|
|
||||||
|
foreach (var shape in shapes)
|
||||||
|
{
|
||||||
|
var polygon = shape.ToPolygonWithTolerance(chordTolerance);
|
||||||
|
polygon.Offset(part.Location);
|
||||||
|
lines.AddRange(polygon.ToLines());
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<Line> GetPartLines(Part part, PushDirection facingDirection, double chordTolerance = 0.001)
|
||||||
|
{
|
||||||
|
var entities = ConvertProgram.ToGeometry(part.Program);
|
||||||
|
var shapes = ShapeBuilder.GetShapes(entities.Where(e => e.Layer != SpecialLayers.Rapid));
|
||||||
|
var lines = new List<Line>();
|
||||||
|
|
||||||
|
foreach (var shape in shapes)
|
||||||
|
{
|
||||||
|
var polygon = shape.ToPolygonWithTolerance(chordTolerance);
|
||||||
|
polygon.Offset(part.Location);
|
||||||
|
lines.AddRange(GetDirectionalLines(polygon, facingDirection));
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the perimeter entities (Line, Arc, Circle) with spacing offset applied,
|
||||||
|
/// without tessellation. Much faster than GetOffsetPartLines for parts with many arcs.
|
||||||
|
/// </summary>
|
||||||
|
public static List<Entity> GetOffsetPerimeterEntities(Part part, double spacing)
|
||||||
|
{
|
||||||
|
var geoEntities = ConvertProgram.ToGeometry(part.Program);
|
||||||
|
var profile = new ShapeProfile(
|
||||||
|
geoEntities.Where(e => e.Layer != SpecialLayers.Rapid).ToList());
|
||||||
|
|
||||||
|
var offsetShape = profile.Perimeter.OffsetOutward(spacing);
|
||||||
|
if (offsetShape == null)
|
||||||
|
return new List<Entity>();
|
||||||
|
|
||||||
|
// Offset the shape's entities to the part's location.
|
||||||
|
// OffsetOutward creates a new Shape, so mutating is safe.
|
||||||
|
foreach (var entity in offsetShape.Entities)
|
||||||
|
entity.Offset(part.Location);
|
||||||
|
|
||||||
|
return offsetShape.Entities;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns all entities (perimeter + cutouts) with spacing offset applied,
|
||||||
|
/// without tessellation. Perimeter is offset outward, cutouts inward.
|
||||||
|
/// </summary>
|
||||||
|
public static List<Entity> GetOffsetPartEntities(Part part, double spacing)
|
||||||
|
{
|
||||||
|
var geoEntities = ConvertProgram.ToGeometry(part.Program);
|
||||||
|
var profile = new ShapeProfile(
|
||||||
|
geoEntities.Where(e => e.Layer != SpecialLayers.Rapid).ToList());
|
||||||
|
var entities = new List<Entity>();
|
||||||
|
|
||||||
|
var perimeter = profile.Perimeter.OffsetOutward(spacing);
|
||||||
|
if (perimeter != null)
|
||||||
|
{
|
||||||
|
foreach (var entity in perimeter.Entities)
|
||||||
|
entity.Offset(part.Location);
|
||||||
|
entities.AddRange(perimeter.Entities);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var cutout in profile.Cutouts)
|
||||||
|
{
|
||||||
|
var inset = cutout.OffsetInward(spacing);
|
||||||
|
if (inset == null) continue;
|
||||||
|
foreach (var entity in inset.Entities)
|
||||||
|
entity.Offset(part.Location);
|
||||||
|
entities.AddRange(inset.Entities);
|
||||||
|
}
|
||||||
|
|
||||||
|
return entities;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns perimeter entities at the part's world location, without tessellation
|
||||||
|
/// or spacing offset.
|
||||||
|
/// </summary>
|
||||||
|
public static List<Entity> GetPerimeterEntities(Part part)
|
||||||
|
{
|
||||||
|
var geoEntities = ConvertProgram.ToGeometry(part.Program);
|
||||||
|
var profile = new ShapeProfile(
|
||||||
|
geoEntities.Where(e => e.Layer != SpecialLayers.Rapid).ToList());
|
||||||
|
|
||||||
|
return CopyEntitiesAtLocation(profile.Perimeter.Entities, part.Location);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns all entities (perimeter + cutouts) at the part's world location,
|
||||||
|
/// without tessellation or spacing offset.
|
||||||
|
/// </summary>
|
||||||
|
public static List<Entity> GetPartEntities(Part part)
|
||||||
|
{
|
||||||
|
var geoEntities = ConvertProgram.ToGeometry(part.Program);
|
||||||
|
var profile = new ShapeProfile(
|
||||||
|
geoEntities.Where(e => e.Layer != SpecialLayers.Rapid).ToList());
|
||||||
|
var entities = CopyEntitiesAtLocation(profile.Perimeter.Entities, part.Location);
|
||||||
|
|
||||||
|
foreach (var cutout in profile.Cutouts)
|
||||||
|
entities.AddRange(CopyEntitiesAtLocation(cutout.Entities, part.Location));
|
||||||
|
|
||||||
|
return entities;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<Entity> CopyEntitiesAtLocation(List<Entity> source, Vector location)
|
||||||
|
{
|
||||||
|
var result = new List<Entity>(source.Count);
|
||||||
|
|
||||||
|
for (var i = 0; i < source.Count; i++)
|
||||||
|
{
|
||||||
|
var entity = source[i];
|
||||||
|
Entity copy;
|
||||||
|
|
||||||
|
if (entity is Line line)
|
||||||
|
copy = new Line(line.StartPoint + location, line.EndPoint + location);
|
||||||
|
else if (entity is Arc arc)
|
||||||
|
copy = new Arc(arc.Center + location, arc.Radius, arc.StartAngle, arc.EndAngle, arc.IsReversed);
|
||||||
|
else if (entity is Circle circle)
|
||||||
|
copy = new Circle(circle.Center + location, circle.Radius);
|
||||||
|
else
|
||||||
|
continue;
|
||||||
|
|
||||||
|
result.Add(copy);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<Line> GetOffsetPartLines(Part part, double spacing, double chordTolerance = 0.001,
|
||||||
|
bool perimeterOnly = false)
|
||||||
|
{
|
||||||
|
var entities = ConvertProgram.ToGeometry(part.Program);
|
||||||
|
var profile = new ShapeProfile(
|
||||||
|
entities.Where(e => e.Layer != SpecialLayers.Rapid).ToList());
|
||||||
|
var lines = new List<Line>();
|
||||||
|
var totalSpacing = spacing;
|
||||||
|
|
||||||
|
AddOffsetLines(lines, profile.Perimeter.OffsetOutward(totalSpacing),
|
||||||
|
chordTolerance, part.Location);
|
||||||
|
|
||||||
|
if (!perimeterOnly)
|
||||||
|
{
|
||||||
|
foreach (var cutout in profile.Cutouts)
|
||||||
|
AddOffsetLines(lines, cutout.OffsetInward(totalSpacing),
|
||||||
|
chordTolerance, part.Location);
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<Line> GetOffsetPartLines(Part part, double spacing, PushDirection facingDirection, double chordTolerance = 0.001)
|
||||||
|
{
|
||||||
|
var entities = ConvertProgram.ToGeometry(part.Program);
|
||||||
|
var profile = new ShapeProfile(
|
||||||
|
entities.Where(e => e.Layer != SpecialLayers.Rapid).ToList());
|
||||||
|
var lines = new List<Line>();
|
||||||
|
var totalSpacing = spacing;
|
||||||
|
|
||||||
|
AddOffsetDirectionalLines(lines, profile.Perimeter.OffsetOutward(totalSpacing),
|
||||||
|
chordTolerance, part.Location, facingDirection);
|
||||||
|
|
||||||
|
foreach (var cutout in profile.Cutouts)
|
||||||
|
AddOffsetDirectionalLines(lines, cutout.OffsetInward(totalSpacing),
|
||||||
|
chordTolerance, part.Location, facingDirection);
|
||||||
|
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<Line> GetPartLines(Part part, Vector facingDirection, double chordTolerance = 0.001)
|
||||||
|
{
|
||||||
|
var entities = ConvertProgram.ToGeometry(part.Program);
|
||||||
|
var shapes = ShapeBuilder.GetShapes(entities.Where(e => e.Layer != SpecialLayers.Rapid));
|
||||||
|
var lines = new List<Line>();
|
||||||
|
|
||||||
|
foreach (var shape in shapes)
|
||||||
|
{
|
||||||
|
var polygon = shape.ToPolygonWithTolerance(chordTolerance);
|
||||||
|
polygon.Offset(part.Location);
|
||||||
|
lines.AddRange(GetDirectionalLines(polygon, facingDirection));
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<Line> GetOffsetPartLines(Part part, double spacing, Vector facingDirection, double chordTolerance = 0.001)
|
||||||
|
{
|
||||||
|
var entities = ConvertProgram.ToGeometry(part.Program);
|
||||||
|
var profile = new ShapeProfile(
|
||||||
|
entities.Where(e => e.Layer != SpecialLayers.Rapid).ToList());
|
||||||
|
var lines = new List<Line>();
|
||||||
|
var totalSpacing = spacing;
|
||||||
|
|
||||||
|
AddOffsetDirectionalLines(lines, profile.Perimeter.OffsetOutward(totalSpacing),
|
||||||
|
chordTolerance, part.Location, facingDirection);
|
||||||
|
|
||||||
|
foreach (var cutout in profile.Cutouts)
|
||||||
|
AddOffsetDirectionalLines(lines, cutout.OffsetInward(totalSpacing),
|
||||||
|
chordTolerance, part.Location, facingDirection);
|
||||||
|
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns only polygon edges whose outward normal faces the specified direction vector.
|
||||||
|
/// </summary>
|
||||||
|
private static List<Line> GetDirectionalLines(Polygon polygon, Vector direction)
|
||||||
|
{
|
||||||
|
if (polygon.Vertices.Count < 3)
|
||||||
|
return polygon.ToLines();
|
||||||
|
|
||||||
|
var sign = polygon.RotationDirection() == RotationType.CCW ? 1.0 : -1.0;
|
||||||
|
var lines = new List<Line>();
|
||||||
|
var last = polygon.Vertices[0];
|
||||||
|
|
||||||
|
for (var i = 1; i < polygon.Vertices.Count; i++)
|
||||||
|
{
|
||||||
|
var current = polygon.Vertices[i];
|
||||||
|
var edx = current.X - last.X;
|
||||||
|
var edy = current.Y - last.Y;
|
||||||
|
|
||||||
|
var keep = sign * (edy * direction.X - edx * direction.Y) > 0;
|
||||||
|
|
||||||
|
if (keep)
|
||||||
|
lines.Add(new Line(last, current));
|
||||||
|
|
||||||
|
last = current;
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns only polygon edges whose outward normal faces the specified direction.
|
||||||
|
/// </summary>
|
||||||
|
private static List<Line> GetDirectionalLines(Polygon polygon, PushDirection facingDirection)
|
||||||
|
{
|
||||||
|
if (polygon.Vertices.Count < 3)
|
||||||
|
return polygon.ToLines();
|
||||||
|
|
||||||
|
var sign = polygon.RotationDirection() == RotationType.CCW ? 1.0 : -1.0;
|
||||||
|
var lines = new List<Line>();
|
||||||
|
var last = polygon.Vertices[0];
|
||||||
|
|
||||||
|
for (int i = 1; i < polygon.Vertices.Count; i++)
|
||||||
|
{
|
||||||
|
var current = polygon.Vertices[i];
|
||||||
|
var dx = current.X - last.X;
|
||||||
|
var dy = current.Y - last.Y;
|
||||||
|
|
||||||
|
bool keep;
|
||||||
|
|
||||||
|
switch (facingDirection)
|
||||||
|
{
|
||||||
|
case PushDirection.Left: keep = -sign * dy > 0; break;
|
||||||
|
case PushDirection.Right: keep = sign * dy > 0; break;
|
||||||
|
case PushDirection.Up: keep = -sign * dx > 0; break;
|
||||||
|
case PushDirection.Down: keep = sign * dx > 0; break;
|
||||||
|
default: keep = true; break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keep)
|
||||||
|
lines.Add(new Line(last, current));
|
||||||
|
|
||||||
|
last = current;
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AddOffsetLines(List<Line> lines, Shape offsetEntity,
|
||||||
|
double chordTolerance, Vector location)
|
||||||
|
{
|
||||||
|
if (offsetEntity == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var polygon = offsetEntity.ToPolygonWithTolerance(chordTolerance);
|
||||||
|
polygon.RemoveSelfIntersections();
|
||||||
|
polygon.Offset(location);
|
||||||
|
lines.AddRange(polygon.ToLines());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AddOffsetDirectionalLines(List<Line> lines, Shape offsetEntity,
|
||||||
|
double chordTolerance, Vector location, PushDirection facingDirection)
|
||||||
|
{
|
||||||
|
if (offsetEntity == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var polygon = offsetEntity.ToPolygonWithTolerance(chordTolerance);
|
||||||
|
polygon.RemoveSelfIntersections();
|
||||||
|
polygon.Offset(location);
|
||||||
|
lines.AddRange(GetDirectionalLines(polygon, facingDirection));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AddOffsetDirectionalLines(List<Line> lines, Shape offsetEntity,
|
||||||
|
double chordTolerance, Vector location, Vector facingDirection)
|
||||||
|
{
|
||||||
|
if (offsetEntity == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var polygon = offsetEntity.ToPolygonWithTolerance(chordTolerance);
|
||||||
|
polygon.RemoveSelfIntersections();
|
||||||
|
polygon.Offset(location);
|
||||||
|
lines.AddRange(GetDirectionalLines(polygon, facingDirection));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+207
-145
@@ -1,9 +1,9 @@
|
|||||||
using System;
|
using OpenNest.Collections;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using OpenNest.Collections;
|
|
||||||
using OpenNest.Geometry;
|
using OpenNest.Geometry;
|
||||||
using OpenNest.Math;
|
using OpenNest.Math;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
namespace OpenNest
|
namespace OpenNest
|
||||||
{
|
{
|
||||||
@@ -35,7 +35,7 @@ namespace OpenNest
|
|||||||
}
|
}
|
||||||
|
|
||||||
public Plate(double width, double length)
|
public Plate(double width, double length)
|
||||||
: this(new Size(length, width))
|
: this(new Size(width, length))
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,28 +43,25 @@ namespace OpenNest
|
|||||||
{
|
{
|
||||||
EdgeSpacing = new Spacing();
|
EdgeSpacing = new Spacing();
|
||||||
Size = size;
|
Size = size;
|
||||||
Material = new Material();
|
|
||||||
Parts = new ObservableList<Part>();
|
Parts = new ObservableList<Part>();
|
||||||
Parts.ItemAdded += Parts_PartAdded;
|
Parts.ItemAdded += Parts_PartAdded;
|
||||||
Parts.ItemRemoved += Parts_PartRemoved;
|
Parts.ItemRemoved += Parts_PartRemoved;
|
||||||
|
CutOffs = new ObservableList<CutOff>();
|
||||||
Quadrant = 1;
|
Quadrant = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Parts_PartAdded(object sender, ItemAddedEventArgs<Part> e)
|
private void Parts_PartAdded(object sender, ItemAddedEventArgs<Part> e)
|
||||||
{
|
{
|
||||||
e.Item.BaseDrawing.Quantity.Nested += Quantity;
|
if (!e.Item.BaseDrawing.IsCutOff)
|
||||||
|
e.Item.BaseDrawing.Quantity.Nested += Quantity;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Parts_PartRemoved(object sender, ItemRemovedEventArgs<Part> e)
|
private void Parts_PartRemoved(object sender, ItemRemovedEventArgs<Part> e)
|
||||||
{
|
{
|
||||||
e.Item.BaseDrawing.Quantity.Nested -= Quantity;
|
if (!e.Item.BaseDrawing.IsCutOff)
|
||||||
|
e.Item.BaseDrawing.Quantity.Nested -= Quantity;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Thickness of the plate.
|
|
||||||
/// </summary>
|
|
||||||
public double Thickness { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The spacing between parts.
|
/// The spacing between parts.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -80,16 +77,104 @@ namespace OpenNest
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public Size Size { get; set; }
|
public Size Size { get; set; }
|
||||||
|
|
||||||
|
public CNC.CuttingStrategy.CuttingParameters CuttingParameters { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Material the plate is made out of.
|
/// Material grain direction in radians. 0 = horizontal.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Material Material { get; set; }
|
public double GrainAngle { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The parts that the plate contains.
|
/// The parts that the plate contains.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ObservableList<Part> Parts { get; set; }
|
public ObservableList<Part> Parts { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The cut-off lines defined on this plate.
|
||||||
|
/// </summary>
|
||||||
|
public ObservableList<CutOff> CutOffs { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Regenerates all cut-off drawings and materializes them as parts.
|
||||||
|
/// Existing cut-off parts are removed first, then each cut-off is
|
||||||
|
/// regenerated and added back if it produces any geometry.
|
||||||
|
/// </summary>
|
||||||
|
public void RegenerateCutOffs(CutOffSettings settings)
|
||||||
|
{
|
||||||
|
// Remove existing cut-off parts
|
||||||
|
for (var i = Parts.Count - 1; i >= 0; i--)
|
||||||
|
{
|
||||||
|
if (Parts[i].BaseDrawing.IsCutOff)
|
||||||
|
Parts.RemoveAt(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
var cache = BuildPerimeterCache(this);
|
||||||
|
|
||||||
|
// Regenerate and materialize each cut-off
|
||||||
|
foreach (var cutoff in CutOffs)
|
||||||
|
{
|
||||||
|
cutoff.Regenerate(this, settings, cache);
|
||||||
|
|
||||||
|
if (cutoff.Drawing.Program.Codes.Count == 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var part = new Part(cutoff.Drawing);
|
||||||
|
Parts.Add(part);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Builds a dictionary mapping each non-cut-off part to its perimeter entity.
|
||||||
|
/// Closed shapes use ShapeProfile; open contours fall back to ConvexHull.
|
||||||
|
/// </summary>
|
||||||
|
public static Dictionary<Part, Geometry.Entity> BuildPerimeterCache(Plate plate)
|
||||||
|
{
|
||||||
|
var cache = new Dictionary<Part, Geometry.Entity>();
|
||||||
|
|
||||||
|
foreach (var part in plate.Parts)
|
||||||
|
{
|
||||||
|
if (part.BaseDrawing.IsCutOff)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
Geometry.Entity perimeter = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var entities = Converters.ConvertProgram.ToGeometry(part.Program)
|
||||||
|
.Where(e => e.Layer != SpecialLayers.Rapid)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (entities.Count > 0)
|
||||||
|
{
|
||||||
|
var profile = new Geometry.ShapeProfile(entities);
|
||||||
|
|
||||||
|
if (profile.Perimeter.IsClosed())
|
||||||
|
{
|
||||||
|
perimeter = profile.Perimeter;
|
||||||
|
perimeter.Offset(part.Location);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var points = entities.CollectPoints();
|
||||||
|
if (points.Count >= 3)
|
||||||
|
{
|
||||||
|
var hull = Geometry.ConvexHull.Compute(points);
|
||||||
|
hull.Offset(part.Location);
|
||||||
|
perimeter = hull;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
perimeter = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
cache[part] = perimeter;
|
||||||
|
}
|
||||||
|
|
||||||
|
return cache;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The number of times to cut the plate.
|
/// The number of times to cut the plate.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -119,6 +204,9 @@ namespace OpenNest
|
|||||||
|
|
||||||
Size = new Size(Size.Length, Size.Width);
|
Size = new Size(Size.Length, Size.Width);
|
||||||
|
|
||||||
|
// After Size swap above, new Size.Width = old Length (old X extent),
|
||||||
|
// new Size.Length = old Width (old Y extent).
|
||||||
|
// Convention: Length = X axis, Width = Y axis.
|
||||||
if (rotationDirection == RotationType.CW)
|
if (rotationDirection == RotationType.CW)
|
||||||
{
|
{
|
||||||
Rotate(oneAndHalfPI);
|
Rotate(oneAndHalfPI);
|
||||||
@@ -128,19 +216,19 @@ namespace OpenNest
|
|||||||
switch (Quadrant)
|
switch (Quadrant)
|
||||||
{
|
{
|
||||||
case 1:
|
case 1:
|
||||||
Offset(0, Size.Length);
|
Offset(0, Size.Width);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 2:
|
case 2:
|
||||||
Offset(-Size.Width, 0);
|
Offset(-Size.Length, 0);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 3:
|
case 3:
|
||||||
Offset(0, -Size.Length);
|
Offset(0, -Size.Width);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 4:
|
case 4:
|
||||||
Offset(Size.Width, 0);
|
Offset(Size.Length, 0);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@@ -161,19 +249,19 @@ namespace OpenNest
|
|||||||
switch (Quadrant)
|
switch (Quadrant)
|
||||||
{
|
{
|
||||||
case 1:
|
case 1:
|
||||||
Offset(Size.Width, 0);
|
Offset(Size.Length, 0);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 2:
|
case 2:
|
||||||
Offset(0, Size.Length);
|
Offset(0, Size.Width);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 3:
|
case 3:
|
||||||
Offset(-Size.Width, 0);
|
Offset(-Size.Length, 0);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 4:
|
case 4:
|
||||||
Offset(0, -Size.Length);
|
Offset(0, -Size.Width);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@@ -200,19 +288,19 @@ namespace OpenNest
|
|||||||
switch (Quadrant)
|
switch (Quadrant)
|
||||||
{
|
{
|
||||||
case 1:
|
case 1:
|
||||||
centerpt = new Vector(Size.Width * 0.5, Size.Length * 0.5);
|
centerpt = new Vector(Size.Length * 0.5, Size.Width * 0.5);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 2:
|
case 2:
|
||||||
centerpt = new Vector(-Size.Width * 0.5, Size.Length * 0.5);
|
centerpt = new Vector(-Size.Length * 0.5, Size.Width * 0.5);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 3:
|
case 3:
|
||||||
centerpt = new Vector(-Size.Width * 0.5, -Size.Length * 0.5);
|
centerpt = new Vector(-Size.Length * 0.5, -Size.Width * 0.5);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 4:
|
case 4:
|
||||||
centerpt = new Vector(Size.Width * 0.5, -Size.Length * 0.5);
|
centerpt = new Vector(Size.Length * 0.5, -Size.Width * 0.5);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@@ -237,11 +325,20 @@ namespace OpenNest
|
|||||||
/// <param name="angle"></param>
|
/// <param name="angle"></param>
|
||||||
public void Rotate(double angle)
|
public void Rotate(double angle)
|
||||||
{
|
{
|
||||||
for (int i = 0; i < Parts.Count; ++i)
|
for (var i = Parts.Count - 1; i >= 0; i--)
|
||||||
|
{
|
||||||
|
if (Parts[i].BaseDrawing.IsCutOff)
|
||||||
|
Parts.RemoveAt(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < Parts.Count; ++i)
|
||||||
{
|
{
|
||||||
var part = Parts[i];
|
var part = Parts[i];
|
||||||
part.Rotate(angle);
|
part.Rotate(angle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
foreach (var cutoff in CutOffs)
|
||||||
|
cutoff.Position = cutoff.Position.Rotate(angle);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -251,11 +348,24 @@ namespace OpenNest
|
|||||||
/// <param name="origin"></param>
|
/// <param name="origin"></param>
|
||||||
public void Rotate(double angle, Vector origin)
|
public void Rotate(double angle, Vector origin)
|
||||||
{
|
{
|
||||||
for (int i = 0; i < Parts.Count; ++i)
|
for (var i = Parts.Count - 1; i >= 0; i--)
|
||||||
|
{
|
||||||
|
if (Parts[i].BaseDrawing.IsCutOff)
|
||||||
|
Parts.RemoveAt(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < Parts.Count; ++i)
|
||||||
{
|
{
|
||||||
var part = Parts[i];
|
var part = Parts[i];
|
||||||
part.Rotate(angle, origin);
|
part.Rotate(angle, origin);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
foreach (var cutoff in CutOffs)
|
||||||
|
{
|
||||||
|
var pos = cutoff.Position - origin;
|
||||||
|
pos = pos.Rotate(angle);
|
||||||
|
cutoff.Position = pos + origin;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -265,11 +375,22 @@ namespace OpenNest
|
|||||||
/// <param name="y"></param>
|
/// <param name="y"></param>
|
||||||
public void Offset(double x, double y)
|
public void Offset(double x, double y)
|
||||||
{
|
{
|
||||||
for (int i = 0; i < Parts.Count; ++i)
|
// Remove cut-off parts before transforming
|
||||||
|
for (var i = Parts.Count - 1; i >= 0; i--)
|
||||||
|
{
|
||||||
|
if (Parts[i].BaseDrawing.IsCutOff)
|
||||||
|
Parts.RemoveAt(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < Parts.Count; ++i)
|
||||||
{
|
{
|
||||||
var part = Parts[i];
|
var part = Parts[i];
|
||||||
part.Offset(x, y);
|
part.Offset(x, y);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Transform cut-off positions
|
||||||
|
foreach (var cutoff in CutOffs)
|
||||||
|
cutoff.Position = new Vector(cutoff.Position.X + x, cutoff.Position.Y + y);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -278,11 +399,20 @@ namespace OpenNest
|
|||||||
/// <param name="voffset"></param>
|
/// <param name="voffset"></param>
|
||||||
public void Offset(Vector voffset)
|
public void Offset(Vector voffset)
|
||||||
{
|
{
|
||||||
for (int i = 0; i < Parts.Count; ++i)
|
for (var i = Parts.Count - 1; i >= 0; i--)
|
||||||
|
{
|
||||||
|
if (Parts[i].BaseDrawing.IsCutOff)
|
||||||
|
Parts.RemoveAt(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < Parts.Count; ++i)
|
||||||
{
|
{
|
||||||
var part = Parts[i];
|
var part = Parts[i];
|
||||||
part.Offset(voffset);
|
part.Offset(voffset);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
foreach (var cutoff in CutOffs)
|
||||||
|
cutoff.Position = new Vector(cutoff.Position.X + voffset.X, cutoff.Position.Y + voffset.Y);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -294,6 +424,7 @@ namespace OpenNest
|
|||||||
{
|
{
|
||||||
var plateBox = new Box();
|
var plateBox = new Box();
|
||||||
|
|
||||||
|
// Width = Y axis (vertical), Length = X axis (horizontal)
|
||||||
switch (Quadrant)
|
switch (Quadrant)
|
||||||
{
|
{
|
||||||
case 1:
|
case 1:
|
||||||
@@ -302,18 +433,18 @@ namespace OpenNest
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 2:
|
case 2:
|
||||||
plateBox.X = (float)-Size.Width;
|
plateBox.X = (float)-Size.Length;
|
||||||
plateBox.Y = 0;
|
plateBox.Y = 0;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 3:
|
case 3:
|
||||||
plateBox.X = (float)-Size.Width;
|
plateBox.X = (float)-Size.Length;
|
||||||
plateBox.Y = (float)-Size.Length;
|
plateBox.Y = (float)-Size.Width;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 4:
|
case 4:
|
||||||
plateBox.X = 0;
|
plateBox.X = 0;
|
||||||
plateBox.Y = (float)-Size.Length;
|
plateBox.Y = (float)-Size.Width;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@@ -337,11 +468,11 @@ namespace OpenNest
|
|||||||
? partsBox.Bottom
|
? partsBox.Bottom
|
||||||
: plateBox.Bottom;
|
: plateBox.Bottom;
|
||||||
|
|
||||||
boundingBox.Width = partsBox.Right > plateBox.Right
|
boundingBox.Length = partsBox.Right > plateBox.Right
|
||||||
? partsBox.Right - boundingBox.X
|
? partsBox.Right - boundingBox.X
|
||||||
: plateBox.Right - boundingBox.X;
|
: plateBox.Right - boundingBox.X;
|
||||||
|
|
||||||
boundingBox.Length = partsBox.Top > plateBox.Top
|
boundingBox.Width = partsBox.Top > plateBox.Top
|
||||||
? partsBox.Top - boundingBox.Y
|
? partsBox.Top - boundingBox.Y
|
||||||
: plateBox.Top - boundingBox.Y;
|
: plateBox.Top - boundingBox.Y;
|
||||||
|
|
||||||
@@ -358,8 +489,8 @@ namespace OpenNest
|
|||||||
|
|
||||||
box.X += EdgeSpacing.Left;
|
box.X += EdgeSpacing.Left;
|
||||||
box.Y += EdgeSpacing.Bottom;
|
box.Y += EdgeSpacing.Bottom;
|
||||||
box.Width -= EdgeSpacing.Left + EdgeSpacing.Right;
|
box.Length -= EdgeSpacing.Left + EdgeSpacing.Right;
|
||||||
box.Length -= EdgeSpacing.Top + EdgeSpacing.Bottom;
|
box.Width -= EdgeSpacing.Top + EdgeSpacing.Bottom;
|
||||||
|
|
||||||
return box;
|
return box;
|
||||||
}
|
}
|
||||||
@@ -382,29 +513,30 @@ namespace OpenNest
|
|||||||
|
|
||||||
var bounds = Parts.GetBoundingBox();
|
var bounds = Parts.GetBoundingBox();
|
||||||
|
|
||||||
double width;
|
// Convention: Length = X axis, Width = Y axis
|
||||||
double length;
|
double xExtent;
|
||||||
|
double yExtent;
|
||||||
|
|
||||||
switch (Quadrant)
|
switch (Quadrant)
|
||||||
{
|
{
|
||||||
case 1:
|
case 1:
|
||||||
width = System.Math.Abs(bounds.Right) + EdgeSpacing.Right;
|
xExtent = System.Math.Abs(bounds.Right) + EdgeSpacing.Right;
|
||||||
length = System.Math.Abs(bounds.Top) + EdgeSpacing.Top;
|
yExtent = System.Math.Abs(bounds.Top) + EdgeSpacing.Top;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 2:
|
case 2:
|
||||||
width = System.Math.Abs(bounds.Left) + EdgeSpacing.Left;
|
xExtent = System.Math.Abs(bounds.Left) + EdgeSpacing.Left;
|
||||||
length = System.Math.Abs(bounds.Top) + EdgeSpacing.Top;
|
yExtent = System.Math.Abs(bounds.Top) + EdgeSpacing.Top;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 3:
|
case 3:
|
||||||
width = System.Math.Abs(bounds.Left) + EdgeSpacing.Left;
|
xExtent = System.Math.Abs(bounds.Left) + EdgeSpacing.Left;
|
||||||
length = System.Math.Abs(bounds.Bottom) + EdgeSpacing.Bottom;
|
yExtent = System.Math.Abs(bounds.Bottom) + EdgeSpacing.Bottom;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 4:
|
case 4:
|
||||||
width = System.Math.Abs(bounds.Right) + EdgeSpacing.Right;
|
xExtent = System.Math.Abs(bounds.Right) + EdgeSpacing.Right;
|
||||||
length = System.Math.Abs(bounds.Bottom) + EdgeSpacing.Bottom;
|
yExtent = System.Math.Abs(bounds.Bottom) + EdgeSpacing.Bottom;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@@ -412,8 +544,8 @@ namespace OpenNest
|
|||||||
}
|
}
|
||||||
|
|
||||||
Size = new Size(
|
Size = new Size(
|
||||||
Helper.RoundUpToNearest(width, roundingFactor),
|
Rounding.RoundUpToNearest(yExtent, roundingFactor),
|
||||||
Helper.RoundUpToNearest(length, roundingFactor));
|
Rounding.RoundUpToNearest(xExtent, roundingFactor));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -428,19 +560,17 @@ namespace OpenNest
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the volume of the plate.
|
/// Gets the volume of the plate.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
public double Volume(double thickness)
|
||||||
public double Volume()
|
|
||||||
{
|
{
|
||||||
return Area() * Thickness;
|
return Area() * thickness;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the weight of the plate.
|
/// Gets the weight of the plate.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
public double Weight(double thickness, double density)
|
||||||
public double Weight()
|
|
||||||
{
|
{
|
||||||
return Volume() * Material.Density;
|
return Volume(thickness) * density;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -449,24 +579,37 @@ namespace OpenNest
|
|||||||
/// <returns>Returns a number between 0.0 and 1.0</returns>
|
/// <returns>Returns a number between 0.0 and 1.0</returns>
|
||||||
public double Utilization()
|
public double Utilization()
|
||||||
{
|
{
|
||||||
return Parts.Sum(part => part.BaseDrawing.Area) / Area();
|
return Parts.Where(p => !p.BaseDrawing.IsCutOff).Sum(part => part.BaseDrawing.Area) / Area();
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool HasOverlappingParts(out List<Vector> pts)
|
public bool HasOverlappingParts(out List<Vector> pts)
|
||||||
{
|
{
|
||||||
pts = new List<Vector>();
|
pts = new List<Vector>();
|
||||||
|
var realParts = Parts.Where(p => !p.BaseDrawing.IsCutOff).ToList();
|
||||||
|
|
||||||
for (int i = 0; i < Parts.Count; i++)
|
for (var i = 0; i < realParts.Count; i++)
|
||||||
{
|
{
|
||||||
var part1 = Parts[i];
|
var part1 = realParts[i];
|
||||||
|
var b1 = part1.BoundingBox;
|
||||||
|
|
||||||
for (int j = i + 1; j < Parts.Count; j++)
|
for (var j = i + 1; j < realParts.Count; j++)
|
||||||
{
|
{
|
||||||
var part2 = Parts[j];
|
var part2 = realParts[j];
|
||||||
|
var b2 = part2.BoundingBox;
|
||||||
|
|
||||||
List<Vector> pts2;
|
// Skip pairs whose bounding boxes don't meaningfully overlap.
|
||||||
|
// Floating-point rounding can produce sub-epsilon overlaps for
|
||||||
|
// parts that are merely edge-touching, so require the overlap
|
||||||
|
// region to exceed Epsilon in both dimensions.
|
||||||
|
var overlapX = System.Math.Min(b1.Right, b2.Right)
|
||||||
|
- System.Math.Max(b1.Left, b2.Left);
|
||||||
|
var overlapY = System.Math.Min(b1.Top, b2.Top)
|
||||||
|
- System.Math.Max(b1.Bottom, b2.Bottom);
|
||||||
|
|
||||||
if (part1.Intersects(part2, out pts2))
|
if (overlapX <= Math.Tolerance.Epsilon || overlapY <= Math.Tolerance.Epsilon)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (part1.Intersects(part2, out var pts2))
|
||||||
pts.AddRange(pts2);
|
pts.AddRange(pts2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -474,86 +617,5 @@ namespace OpenNest
|
|||||||
return pts.Count > 0;
|
return pts.Count > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Finds rectangular remnant (empty) regions on the plate.
|
|
||||||
/// Returns strips along edges that are clear of parts.
|
|
||||||
/// </summary>
|
|
||||||
public List<Box> GetRemnants()
|
|
||||||
{
|
|
||||||
var work = WorkArea();
|
|
||||||
var results = new List<Box>();
|
|
||||||
|
|
||||||
if (Parts.Count == 0)
|
|
||||||
{
|
|
||||||
results.Add(work);
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
var obstacles = new List<Box>();
|
|
||||||
foreach (var part in Parts)
|
|
||||||
obstacles.Add(part.BoundingBox.Offset(PartSpacing));
|
|
||||||
|
|
||||||
// Right strip: from the rightmost part edge to the work area right edge
|
|
||||||
var maxRight = double.MinValue;
|
|
||||||
foreach (var box in obstacles)
|
|
||||||
{
|
|
||||||
if (box.Right > maxRight)
|
|
||||||
maxRight = box.Right;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (maxRight < work.Right)
|
|
||||||
{
|
|
||||||
var strip = new Box(maxRight, work.Bottom, work.Right - maxRight, work.Length);
|
|
||||||
if (strip.Area() > 1.0)
|
|
||||||
results.Add(strip);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Top strip: from the topmost part edge to the work area top edge
|
|
||||||
var maxTop = double.MinValue;
|
|
||||||
foreach (var box in obstacles)
|
|
||||||
{
|
|
||||||
if (box.Top > maxTop)
|
|
||||||
maxTop = box.Top;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (maxTop < work.Top)
|
|
||||||
{
|
|
||||||
var strip = new Box(work.Left, maxTop, work.Width, work.Top - maxTop);
|
|
||||||
if (strip.Area() > 1.0)
|
|
||||||
results.Add(strip);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bottom strip: from work area bottom to the lowest part edge
|
|
||||||
var minBottom = double.MaxValue;
|
|
||||||
foreach (var box in obstacles)
|
|
||||||
{
|
|
||||||
if (box.Bottom < minBottom)
|
|
||||||
minBottom = box.Bottom;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (minBottom > work.Bottom)
|
|
||||||
{
|
|
||||||
var strip = new Box(work.Left, work.Bottom, work.Width, minBottom - work.Bottom);
|
|
||||||
if (strip.Area() > 1.0)
|
|
||||||
results.Add(strip);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Left strip: from work area left to the leftmost part edge
|
|
||||||
var minLeft = double.MaxValue;
|
|
||||||
foreach (var box in obstacles)
|
|
||||||
{
|
|
||||||
if (box.Left < minLeft)
|
|
||||||
minLeft = box.Left;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (minLeft > work.Left)
|
|
||||||
{
|
|
||||||
var strip = new Box(work.Left, work.Bottom, minLeft - work.Left, work.Length);
|
|
||||||
if (strip.Area() > 1.0)
|
|
||||||
results.Add(strip);
|
|
||||||
}
|
|
||||||
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,243 @@
|
|||||||
|
using OpenNest.Collections;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace OpenNest
|
||||||
|
{
|
||||||
|
public class PlateChangedEventArgs : EventArgs
|
||||||
|
{
|
||||||
|
public Plate Plate { get; }
|
||||||
|
public int Index { get; }
|
||||||
|
|
||||||
|
public PlateChangedEventArgs(Plate plate, int index)
|
||||||
|
{
|
||||||
|
Plate = plate;
|
||||||
|
Index = index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class PlateManager : IDisposable
|
||||||
|
{
|
||||||
|
private readonly Nest nest;
|
||||||
|
private bool disposed;
|
||||||
|
private bool suppressNavigation;
|
||||||
|
private bool batching;
|
||||||
|
private Plate subscribedLast;
|
||||||
|
private Plate subscribedSecondToLast;
|
||||||
|
|
||||||
|
public event EventHandler<PlateChangedEventArgs> CurrentPlateChanged;
|
||||||
|
public event EventHandler PlateListChanged;
|
||||||
|
|
||||||
|
public PlateManager(Nest nest)
|
||||||
|
{
|
||||||
|
this.nest = nest;
|
||||||
|
nest.Plates.ItemAdded += OnPlateAdded;
|
||||||
|
nest.Plates.ItemRemoved += OnPlateRemoved;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int CurrentIndex { get; private set; }
|
||||||
|
|
||||||
|
public Plate CurrentPlate => nest.Plates.Count > 0 ? nest.Plates[CurrentIndex] : null;
|
||||||
|
|
||||||
|
public int Count => nest.Plates.Count;
|
||||||
|
|
||||||
|
public bool IsFirst => Count == 0 || CurrentIndex <= 0;
|
||||||
|
|
||||||
|
public bool IsLast => CurrentIndex + 1 >= Count;
|
||||||
|
|
||||||
|
public bool CanRemoveCurrent => Count > 1 && CurrentPlate != null && CurrentPlate.Parts.Count > 0;
|
||||||
|
|
||||||
|
public void LoadFirst()
|
||||||
|
{
|
||||||
|
if (Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
CurrentIndex = 0;
|
||||||
|
FireCurrentPlateChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void LoadLast()
|
||||||
|
{
|
||||||
|
if (Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
CurrentIndex = Count - 1;
|
||||||
|
FireCurrentPlateChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool LoadNext()
|
||||||
|
{
|
||||||
|
if (CurrentIndex + 1 >= Count)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
CurrentIndex++;
|
||||||
|
FireCurrentPlateChanged();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool LoadPrevious()
|
||||||
|
{
|
||||||
|
if (Count == 0 || CurrentIndex - 1 < 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
CurrentIndex--;
|
||||||
|
FireCurrentPlateChanged();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void LoadAt(int index)
|
||||||
|
{
|
||||||
|
if (index < 0 || index >= Count)
|
||||||
|
return;
|
||||||
|
|
||||||
|
CurrentIndex = index;
|
||||||
|
FireCurrentPlateChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void EnsureSentinel()
|
||||||
|
{
|
||||||
|
suppressNavigation = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (Count == 0 || nest.Plates[^1].Parts.Count > 0)
|
||||||
|
nest.CreatePlate();
|
||||||
|
|
||||||
|
while (Count > 1
|
||||||
|
&& nest.Plates[^1].Parts.Count == 0
|
||||||
|
&& nest.Plates[^2].Parts.Count == 0)
|
||||||
|
{
|
||||||
|
nest.Plates.RemoveAt(Count - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
suppressNavigation = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
SubscribeToTailPlates();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void BeginBatch()
|
||||||
|
{
|
||||||
|
batching = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void EndBatch()
|
||||||
|
{
|
||||||
|
batching = false;
|
||||||
|
EnsureSentinel();
|
||||||
|
PlateListChanged?.Invoke(this, EventArgs.Empty);
|
||||||
|
FireCurrentPlateChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Plate GetOrCreateEmpty()
|
||||||
|
{
|
||||||
|
for (var i = Count - 1; i >= 0; i--)
|
||||||
|
{
|
||||||
|
if (nest.Plates[i].Parts.Count == 0)
|
||||||
|
return nest.Plates[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
return nest.CreatePlate();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RemoveCurrent()
|
||||||
|
{
|
||||||
|
if (Count < 2)
|
||||||
|
return;
|
||||||
|
|
||||||
|
nest.Plates.RemoveAt(CurrentIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SubscribeToTailPlates()
|
||||||
|
{
|
||||||
|
UnsubscribeFromTailPlates();
|
||||||
|
|
||||||
|
if (Count > 0)
|
||||||
|
{
|
||||||
|
subscribedLast = nest.Plates[^1];
|
||||||
|
subscribedLast.PartAdded += OnTailPartAdded;
|
||||||
|
subscribedLast.PartRemoved += OnTailPartRemoved;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Count > 1)
|
||||||
|
{
|
||||||
|
subscribedSecondToLast = nest.Plates[^2];
|
||||||
|
subscribedSecondToLast.PartAdded += OnTailPartAdded;
|
||||||
|
subscribedSecondToLast.PartRemoved += OnTailPartRemoved;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UnsubscribeFromTailPlates()
|
||||||
|
{
|
||||||
|
if (subscribedLast != null)
|
||||||
|
{
|
||||||
|
subscribedLast.PartAdded -= OnTailPartAdded;
|
||||||
|
subscribedLast.PartRemoved -= OnTailPartRemoved;
|
||||||
|
subscribedLast = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (subscribedSecondToLast != null)
|
||||||
|
{
|
||||||
|
subscribedSecondToLast.PartAdded -= OnTailPartAdded;
|
||||||
|
subscribedSecondToLast.PartRemoved -= OnTailPartRemoved;
|
||||||
|
subscribedSecondToLast = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnTailPartAdded(object sender, ItemAddedEventArgs<Part> e)
|
||||||
|
{
|
||||||
|
if (!batching)
|
||||||
|
EnsureSentinel();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnTailPartRemoved(object sender, ItemRemovedEventArgs<Part> e)
|
||||||
|
{
|
||||||
|
if (!batching)
|
||||||
|
EnsureSentinel();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnPlateAdded(object sender, ItemAddedEventArgs<Plate> e)
|
||||||
|
{
|
||||||
|
if (!suppressNavigation && !batching)
|
||||||
|
EnsureSentinel();
|
||||||
|
|
||||||
|
PlateListChanged?.Invoke(this, EventArgs.Empty);
|
||||||
|
|
||||||
|
if (!suppressNavigation)
|
||||||
|
{
|
||||||
|
CurrentIndex = Count - 1;
|
||||||
|
FireCurrentPlateChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnPlateRemoved(object sender, ItemRemovedEventArgs<Plate> e)
|
||||||
|
{
|
||||||
|
if (CurrentIndex >= Count && Count > 0)
|
||||||
|
CurrentIndex = Count - 1;
|
||||||
|
|
||||||
|
if (!suppressNavigation && !batching)
|
||||||
|
EnsureSentinel();
|
||||||
|
|
||||||
|
PlateListChanged?.Invoke(this, EventArgs.Empty);
|
||||||
|
|
||||||
|
if (!suppressNavigation)
|
||||||
|
FireCurrentPlateChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void FireCurrentPlateChanged()
|
||||||
|
{
|
||||||
|
CurrentPlateChanged?.Invoke(this, new PlateChangedEventArgs(CurrentPlate, CurrentIndex));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (disposed)
|
||||||
|
return;
|
||||||
|
|
||||||
|
disposed = true;
|
||||||
|
UnsubscribeFromTailPlates();
|
||||||
|
nest.Plates.ItemAdded -= OnPlateAdded;
|
||||||
|
nest.Plates.ItemRemoved -= OnPlateRemoved;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace OpenNest
|
||||||
|
{
|
||||||
|
public class PlateOptimizerResult
|
||||||
|
{
|
||||||
|
public List<Part> Parts { get; set; } = new();
|
||||||
|
public PlateOption ChosenSize { get; set; }
|
||||||
|
public double NetCost { get; set; }
|
||||||
|
public double Utilization { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
namespace OpenNest
|
||||||
|
{
|
||||||
|
public class PlateOption
|
||||||
|
{
|
||||||
|
public double Width { get; set; }
|
||||||
|
public double Length { get; set; }
|
||||||
|
public double Cost { get; set; }
|
||||||
|
|
||||||
|
public double Area => Width * Length;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
using System.Collections.Generic;
|
using OpenNest.Geometry;
|
||||||
using OpenNest.Geometry;
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace OpenNest
|
namespace OpenNest
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
using OpenNest.Geometry;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace OpenNest.Shapes
|
||||||
|
{
|
||||||
|
public class CircleShape : ShapeDefinition
|
||||||
|
{
|
||||||
|
public double Diameter { get; set; }
|
||||||
|
|
||||||
|
public override void SetPreviewDefaults()
|
||||||
|
{
|
||||||
|
Diameter = 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Drawing GetDrawing()
|
||||||
|
{
|
||||||
|
var entities = new List<Entity>
|
||||||
|
{
|
||||||
|
new Circle(0, 0, Diameter / 2.0)
|
||||||
|
};
|
||||||
|
|
||||||
|
return CreateDrawing(entities);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
using OpenNest.Geometry;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace OpenNest.Shapes
|
||||||
|
{
|
||||||
|
public class FlangeShape : ShapeDefinition
|
||||||
|
{
|
||||||
|
public double NominalPipeSize { get; set; }
|
||||||
|
public double OD { get; set; }
|
||||||
|
public double HoleDiameter { get; set; }
|
||||||
|
public double HolePatternDiameter { get; set; }
|
||||||
|
public int HoleCount { get; set; }
|
||||||
|
|
||||||
|
public override void SetPreviewDefaults()
|
||||||
|
{
|
||||||
|
NominalPipeSize = 2;
|
||||||
|
OD = 7.5;
|
||||||
|
HoleDiameter = 0.875;
|
||||||
|
HolePatternDiameter = 5.5;
|
||||||
|
HoleCount = 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Drawing GetDrawing()
|
||||||
|
{
|
||||||
|
var entities = new List<Entity>();
|
||||||
|
|
||||||
|
// Outer circle
|
||||||
|
entities.Add(new Circle(0, 0, OD / 2.0));
|
||||||
|
|
||||||
|
// Bolt holes evenly spaced on the bolt circle
|
||||||
|
var boltCircleRadius = HolePatternDiameter / 2.0;
|
||||||
|
var holeRadius = HoleDiameter / 2.0;
|
||||||
|
var angleStep = 2.0 * System.Math.PI / HoleCount;
|
||||||
|
|
||||||
|
for (var i = 0; i < HoleCount; i++)
|
||||||
|
{
|
||||||
|
var angle = i * angleStep;
|
||||||
|
var cx = boltCircleRadius * System.Math.Cos(angle);
|
||||||
|
var cy = boltCircleRadius * System.Math.Sin(angle);
|
||||||
|
entities.Add(new Circle(cx, cy, holeRadius));
|
||||||
|
}
|
||||||
|
|
||||||
|
return CreateDrawing(entities);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user