diff --git a/.gitignore b/.gitignore index a7dd0b4d..389af24f 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ vite.config.js.timestamp-* vite.config.ts.timestamp-* .turbo /.pnpm-store/ +.vscode diff --git a/Cargo.lock b/Cargo.lock index b525bfbc..ca7b5063 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -41,11 +41,11 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ - "memchr 2.6.4", + "memchr 2.7.2", ] [[package]] @@ -87,6 +87,15 @@ dependencies = [ "num-traits", ] +[[package]] +name = "approx" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" +dependencies = [ + "num-traits", +] + [[package]] name = "array-macro" version = "2.1.8" @@ -95,24 +104,24 @@ checksum = "220a2c618ab466efe41d0eace94dfeff1c35e3aa47891bdb95e1c0fefffd3c99" [[package]] name = "atomic-polyfill" -version = "0.1.11" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3ff7eb3f316534d83a8a2c3d1674ace8a5a71198eba31e2e2b597833f699b28" +checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4" dependencies = [ "critical-section", ] [[package]] name = "autocfg" -version = "1.1.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" [[package]] name = "backtrace" -version = "0.3.69" +version = "0.3.71" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" dependencies = [ "addr2line", "cc", @@ -131,9 +140,9 @@ checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" [[package]] name = "base64" -version = "0.21.5" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bitflags" @@ -149,9 +158,9 @@ checksum = "7958fb9748a08a6f46ef773e87c43997a844709bc293b4c3de48135debaf9d2a" [[package]] name = "bumpalo" -version = "3.13.0" +version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "bytemuck" @@ -164,9 +173,9 @@ dependencies = [ [[package]] name = "bytemuck_derive" -version = "1.5.0" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "965ab7eb5f8f97d2a083c799f3a1b994fc397b2fe2da5d1da1626ce15a39f2b1" +checksum = "369cfaf2a5bed5d8f8202073b2e093c9f508251de1551a0deb4253e4c7d80909" dependencies = [ "proc-macro2", "quote", @@ -175,20 +184,23 @@ dependencies = [ [[package]] name = "byteorder" -version = "1.4.3" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "cadmium" version = "0.1.0" dependencies = [ "anyhow", + "cadmium-macros", "console_error_panic_hook", "crc32fast", - "geo", - "indexmap 2.1.0", + "geo 0.26.0", + "indexmap 2.2.6", + "isotope", "itertools 0.12.1", + "paste", "serde", "serde_json", "serde_with", @@ -206,14 +218,21 @@ dependencies = [ ] [[package]] -name = "cc" -version = "1.0.83" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +name = "cadmium-macros" +version = "0.1.0" dependencies = [ - "libc", + "convert_case 0.6.0", + "proc-macro2", + "quote", + "syn 1.0.109", ] +[[package]] +name = "cc" +version = "1.0.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41c270e7540d725e65ac7f1b212ac8ce349719624d7bcff99f8e2e488e8cf03f" + [[package]] name = "cfg-if" version = "1.0.0" @@ -226,7 +245,7 @@ version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a98d30140e3296250832bbaaff83b27dcd6fa3cc70fb6f1f3e5c9c0023b5317" dependencies = [ - "approx", + "approx 0.4.0", "num-traits", "serde", ] @@ -243,7 +262,7 @@ dependencies = [ "num-traits", "serde", "wasm-bindgen", - "windows-targets 0.52.5", + "windows-targets", ] [[package]] @@ -262,17 +281,26 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "core-foundation-sys" -version = "0.8.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" [[package]] name = "crc32fast" -version = "1.3.2" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" dependencies = [ "cfg-if", ] @@ -285,42 +313,34 @@ checksum = "7059fff8937831a9ae6f0fe4d658ffabf58f2ca96aa9dec1c889f936f705f216" [[package]] name = "crossbeam-deque" -version = "0.8.3" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef" +checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" dependencies = [ - "cfg-if", "crossbeam-epoch", "crossbeam-utils", ] [[package]] name = "crossbeam-epoch" -version = "0.9.15" +version = "0.9.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" dependencies = [ - "autocfg", - "cfg-if", "crossbeam-utils", - "memoffset", - "scopeguard", ] [[package]] name = "crossbeam-utils" -version = "0.8.16" +version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" -dependencies = [ - "cfg-if", -] +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" [[package]] name = "darling" -version = "0.20.3" +version = "0.20.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0209d94da627ab5605dcccf08bb18afa5009cfbef48d8a8b7d7bdbc79be25c5e" +checksum = "83b2eb4d90d12bdda5ed17de686c2acb4c57914f8f921b8da7e112b5a36f3fe1" dependencies = [ "darling_core", "darling_macro", @@ -328,9 +348,9 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.20.3" +version = "0.20.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "177e3443818124b357d8e76f53be906d60937f0d3a90773a664fa63fa253e621" +checksum = "622687fe0bac72a04e5599029151f5796111b90f1baaa9b544d807a5e31cd120" dependencies = [ "fnv", "ident_case", @@ -342,9 +362,9 @@ dependencies = [ [[package]] name = "darling_macro" -version = "0.20.3" +version = "0.20.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5" +checksum = "733cabb43482b1a1b53eee8583c2b9e8684d592215ea83efd305dd31bc2f0178" dependencies = [ "darling_core", "quote", @@ -353,9 +373,9 @@ dependencies = [ [[package]] name = "deranged" -version = "0.3.10" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eb30d70a07a3b04884d2677f06bec33509dc67ca60d92949e5535352d3191dc" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" dependencies = [ "powerfmt", "serde", @@ -378,7 +398,7 @@ version = "0.99.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" dependencies = [ - "convert_case", + "convert_case 0.4.0", "proc-macro2", "quote", "rustc_version", @@ -387,19 +407,19 @@ dependencies = [ [[package]] name = "earcutr" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0812b44697951d35fde8fcb0da81c9de7e809e825a66bbf1ecb79d9829d4ca3d" +checksum = "79127ed59a85d7687c409e9978547cffb7dc79675355ed22da6b66fd5f6ead01" dependencies = [ - "itertools 0.10.5", + "itertools 0.11.0", "num-traits", ] [[package]] name = "either" -version = "1.9.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" +checksum = "3dca9240753cf90908d7e4aac30f630662b02aebaa1b58a3cadabdb23385b58b" [[package]] name = "equivalent" @@ -442,28 +462,47 @@ dependencies = [ "log", "num-traits", "robust", - "rstar", + "rstar 0.11.0", +] + +[[package]] +name = "geo" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f811f663912a69249fa620dcd2a005db7254529da2d8a0b23942e81f47084501" +dependencies = [ + "earcutr", + "float_next_after", + "geo-types", + "geographiclib-rs", + "log", + "num-traits", + "robust", + "rstar 0.12.0", + "serde", + "spade", ] [[package]] name = "geo-types" -version = "0.7.11" +version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9705398c5c7b26132e74513f4ee7c1d7dafd786004991b375c172be2be0eecaa" +checksum = "9ff16065e5720f376fbced200a5ae0f47ace85fd70b7e54269790281353b6d61" dependencies = [ - "approx", + "approx 0.5.1", "num-traits", - "rstar", + "rstar 0.11.0", + "rstar 0.12.0", "serde", ] [[package]] name = "geographiclib-rs" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ea804e7bd3c6a4ca6a01edfa35231557a8a81d4d3f3e1e2b650d028c42592be" +checksum = "e6e5ed84f8089c70234b0a8e0aedb6dc733671612ddc0d37c6066052f9781960" dependencies = [ - "lazy_static", + "libm", ] [[package]] @@ -507,6 +546,15 @@ dependencies = [ "byteorder", ] +[[package]] +name = "hash32" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" +dependencies = [ + "byteorder", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -515,9 +563,9 @@ checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "hashbrown" -version = "0.14.2" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f93e7192158dbcda357bdec5fb5788eebf8bbac027f3f33e719d29135ae84156" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" dependencies = [ "ahash", "allocator-api2", @@ -525,17 +573,27 @@ dependencies = [ [[package]] name = "heapless" -version = "0.7.16" +version = "0.7.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db04bc24a18b9ea980628ecf00e6c0264f3c1426dac36c00cb49b6fbad8b0743" +checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f" dependencies = [ "atomic-polyfill", - "hash32", + "hash32 0.2.1", "rustc_version", "spin", "stable_deref_trait", ] +[[package]] +name = "heapless" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" +dependencies = [ + "hash32 0.3.1", + "stable_deref_trait", +] + [[package]] name = "heck" version = "0.4.1" @@ -550,9 +608,9 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "iana-time-zone" -version = "0.1.58" +version = "0.1.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8326b86b6cff230b97d0d312a6c40a60726df3332e721f72a1b035f451663b20" +checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -590,15 +648,28 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.1.0" +version = "2.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" +checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" dependencies = [ "equivalent", - "hashbrown 0.14.2", + "hashbrown 0.14.5", "serde", ] +[[package]] +name = "isotope" +version = "0.1.0" +source = "git+https://github.com/dzervas/ISOtope.git?branch=sketch-helpers#618996f9482ddf56d54981fd195dad57c8f5bcf9" +dependencies = [ + "geo 0.28.0", + "nalgebra", + "serde", + "thiserror", + "tsify", + "wasm-bindgen", +] + [[package]] name = "itertools" version = "0.10.5" @@ -608,6 +679,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +dependencies = [ + "either", +] + [[package]] name = "itertools" version = "0.12.1" @@ -619,15 +699,15 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.9" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" [[package]] name = "js-sys" -version = "0.3.64" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" +checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" dependencies = [ "wasm-bindgen", ] @@ -657,15 +737,15 @@ checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" [[package]] name = "libm" -version = "0.2.7" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7012b1bbb0719e1097c47611d3898568c546d597c2e74d66f6087edd5233ff4" +checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" [[package]] name = "lock_api" -version = "0.4.10" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" dependencies = [ "autocfg", "scopeguard", @@ -673,9 +753,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.20" +version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" [[package]] name = "lz4_flex" @@ -705,6 +785,16 @@ dependencies = [ "num-complex", ] +[[package]] +name = "matrixmultiply" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7574c1cf36da4798ab73da5b215bbf444f50718207754cb522201d78d1cd0ff2" +dependencies = [ + "autocfg", + "rawpointer", +] + [[package]] name = "memchr" version = "1.0.2" @@ -716,18 +806,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.6.4" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" - -[[package]] -name = "memoffset" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" -dependencies = [ - "autocfg", -] +checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" [[package]] name = "minimal-lexical" @@ -737,13 +818,41 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" +checksum = "87dfd01fe195c66b572b37921ad8803d010623c0aca821bea2302239d155cdae" dependencies = [ "adler", ] +[[package]] +name = "nalgebra" +version = "0.32.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ea4908d4f23254adda3daa60ffef0f1ac7b8c3e9a864cf3cc154b251908a2ef" +dependencies = [ + "approx 0.5.1", + "matrixmultiply", + "nalgebra-macros", + "num-complex", + "num-rational", + "num-traits", + "serde", + "simba", + "typenum", +] + +[[package]] +name = "nalgebra-macros" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91761aed67d03ad966ef783ae962ef9bbaca728d2dd7ceb7939ec110fffad998" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "nom" version = "3.2.1" @@ -759,19 +868,26 @@ version = "7.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" dependencies = [ - "memchr 2.6.4", + "memchr 2.7.2", "minimal-lexical", ] [[package]] name = "num-complex" -version = "0.4.4" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ba157ca0885411de85d6ca030ba7e2a83a28636056c7c699b07c8b6f7383214" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" dependencies = [ "num-traits", + "serde", ] +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + [[package]] name = "num-derive" version = "0.3.3" @@ -783,11 +899,30 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" -version = "0.2.16" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", "libm", @@ -799,14 +934,14 @@ version = "0.32.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" dependencies = [ - "memchr 2.6.4", + "memchr 2.7.2", ] [[package]] name = "once_cell" -version = "1.18.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "parking_lot" @@ -828,9 +963,15 @@ dependencies = [ "libc", "redox_syscall", "smallvec", - "windows-targets 0.52.5", + "windows-targets", ] +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + [[package]] name = "pkg-config" version = "0.3.30" @@ -898,7 +1039,7 @@ version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8533f14c8382aaad0d592c812ac3b826162128b65662331e1127b45c3d18536b" dependencies = [ - "memchr 2.6.4", + "memchr 2.7.2", "serde", ] @@ -941,6 +1082,12 @@ dependencies = [ "getrandom", ] +[[package]] +name = "rawpointer" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" + [[package]] name = "rayon" version = "1.10.0" @@ -981,32 +1128,32 @@ dependencies = [ [[package]] name = "regex" -version = "1.10.2" +version = "1.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" +checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" dependencies = [ "aho-corasick", - "memchr 2.6.4", + "memchr 2.7.2", "regex-automata", "regex-syntax", ] [[package]] name = "regex-automata" -version = "0.4.3" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" +checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" dependencies = [ "aho-corasick", - "memchr 2.6.4", + "memchr 2.7.2", "regex-syntax", ] [[package]] name = "regex-syntax" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" +checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" [[package]] name = "robust" @@ -1020,16 +1167,27 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73111312eb7a2287d229f06c00ff35b51ddee180f017ab6dec1f69d62ac098d6" dependencies = [ - "heapless", + "heapless 0.7.17", + "num-traits", + "smallvec", +] + +[[package]] +name = "rstar" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "133315eb94c7b1e8d0cb097e5a710d850263372fd028fff18969de708afc7008" +dependencies = [ + "heapless 0.8.0", "num-traits", "smallvec", ] [[package]] name = "rustc-demangle" -version = "0.1.23" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] name = "rustc-hash" @@ -1082,9 +1240,18 @@ checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" [[package]] name = "ryu" -version = "1.0.15" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "safe_arch" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f398075ce1e6a179b46f51bd88d0598b92b00d3551f1a2d4ac49e771b56ac354" +dependencies = [ + "bytemuck", +] [[package]] name = "scopeguard" @@ -1094,9 +1261,9 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "semver" -version = "1.0.18" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" [[package]] name = "serde" @@ -1120,9 +1287,9 @@ dependencies = [ [[package]] name = "serde_derive_internals" -version = "0.29.0" +version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "330f01ce65a3a5fe59a60c82f3c9a024b573b8a6e875bd233fe5f934e71d54e3" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", @@ -1142,16 +1309,17 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.4.0" +version = "3.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64cd236ccc1b7a29e7e2739f27c0b2dd199804abc4290e32f59f3b68d6405c23" +checksum = "0ad483d2ab0149d5a5ebcd9972a3852711e0153d863bf5a5d0391d28883c4a20" dependencies = [ - "base64 0.21.5", + "base64 0.22.1", "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.1.0", + "indexmap 2.2.6", "serde", + "serde_derive", "serde_json", "serde_with_macros", "time", @@ -1159,9 +1327,9 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.4.0" +version = "3.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93634eb5f75a2323b16de4748022ac4297f9e76b6dced2be287a099f41b5e788" +checksum = "65569b702f41443e8bc8bbb1c5779bd0450bbe723b56198980e80ec45780bce2" dependencies = [ "darling", "proc-macro2", @@ -1169,11 +1337,24 @@ dependencies = [ "syn 2.0.66", ] +[[package]] +name = "simba" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "061507c94fc6ab4ba1c9a0305018408e312e17c041eb63bef8aa726fa33aceae" +dependencies = [ + "approx 0.5.1", + "num-complex", + "num-traits", + "paste", + "wide", +] + [[package]] name = "smallvec" -version = "1.11.0" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "spade" @@ -1181,7 +1362,7 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b20a809169ae442497e41a997fc5f14e2eea04e6ac590816a910d5d8068c8c0" dependencies = [ - "hashbrown 0.14.2", + "hashbrown 0.14.5", "num-traits", "robust", "smallvec", @@ -1204,9 +1385,9 @@ checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" [[package]] name = "strsim" -version = "0.10.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "strum" @@ -1280,12 +1461,13 @@ dependencies = [ [[package]] name = "time" -version = "0.3.31" +version = "0.3.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f657ba42c3f86e7680e53c8cd3af8abbe56b5491790b46e22e19c0d57463583e" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" dependencies = [ "deranged", "itoa", + "num-conv", "powerfmt", "serde", "time-core", @@ -1300,18 +1482,19 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.16" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26197e33420244aeb70c3e8c78376ca46571bc4e701e4791c2cd9f57dcb3a43f" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" dependencies = [ + "num-conv", "time-core", ] [[package]] name = "toml_datetime" -version = "0.6.5" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" +checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf" [[package]] name = "toml_edit" @@ -1319,7 +1502,7 @@ version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap 2.1.0", + "indexmap 2.2.6", "toml_datetime", "winnow", ] @@ -1489,11 +1672,23 @@ dependencies = [ "syn 2.0.66", ] +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + [[package]] name = "unicode-ident" -version = "1.0.11" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unicode-segmentation" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" +checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" [[package]] name = "version_check" @@ -1528,9 +1723,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.87" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" +checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -1538,9 +1733,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.87" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" +checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" dependencies = [ "bumpalo", "log", @@ -1553,9 +1748,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.87" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" +checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1563,9 +1758,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.87" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" +checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", @@ -1576,42 +1771,37 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.87" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" +checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" [[package]] name = "web-sys" -version = "0.3.64" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b" +checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" dependencies = [ "js-sys", "wasm-bindgen", ] [[package]] -name = "windows-core" -version = "0.51.1" +name = "wide" +version = "0.7.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" +checksum = "cd8dc749a1b03f3c255a3064a4f5c0ee5ed09b7c6bc6d4525d31f779cd74d7fc" dependencies = [ - "windows-targets 0.48.5", + "bytemuck", + "safe_arch", ] [[package]] -name = "windows-targets" -version = "0.48.5" +name = "windows-core" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ - "windows_aarch64_gnullvm 0.48.5", - "windows_aarch64_msvc 0.48.5", - "windows_i686_gnu 0.48.5", - "windows_i686_msvc 0.48.5", - "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm 0.48.5", - "windows_x86_64_msvc 0.48.5", + "windows-targets", ] [[package]] @@ -1620,46 +1810,28 @@ version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" dependencies = [ - "windows_aarch64_gnullvm 0.52.5", - "windows_aarch64_msvc 0.52.5", - "windows_i686_gnu 0.52.5", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", "windows_i686_gnullvm", - "windows_i686_msvc 0.52.5", - "windows_x86_64_gnu 0.52.5", - "windows_x86_64_gnullvm 0.52.5", - "windows_x86_64_msvc 0.52.5", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", ] -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" - [[package]] name = "windows_aarch64_gnullvm" version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" - [[package]] name = "windows_aarch64_msvc" version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" -[[package]] -name = "windows_i686_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" - [[package]] name = "windows_i686_gnu" version = "0.52.5" @@ -1672,48 +1844,24 @@ version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" -[[package]] -name = "windows_i686_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" - [[package]] name = "windows_i686_msvc" version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" - [[package]] name = "windows_x86_64_gnu" version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" - [[package]] name = "windows_x86_64_gnullvm" version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" - [[package]] name = "windows_x86_64_msvc" version = "0.52.5" @@ -1722,11 +1870,11 @@ checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" [[package]] name = "winnow" -version = "0.5.17" +version = "0.5.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3b801d0e0a6726477cc207f60162da452f3a95adb368399bef20a946e06f65c" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" dependencies = [ - "memchr 2.6.4", + "memchr 2.7.2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 7dc51f7c..25e92bde 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["packages/cadmium"] +members = [ "packages/cadmium", "packages/cadmium-macros"] resolver = "2" [patch.crates-io] diff --git a/applications/web/src/components/AppBar.svelte b/applications/web/src/components/AppBar.svelte index 12184d88..8b07642a 100644 --- a/applications/web/src/components/AppBar.svelte +++ b/applications/web/src/components/AppBar.svelte @@ -19,11 +19,7 @@ export let newFileContent: string | null = null - $: project, - (() => { - // log("[project]", project) - project && !isProject(project) && console.error("[AppBar.svelte] [project] fails isProject(project) typecheck", project) - })() + $: project function fileInput(e: WithTarget) { const target = e.target as HTMLInputElement diff --git a/applications/web/src/components/NewCircleTool.svelte b/applications/web/src/components/NewCircleTool.svelte index 8adc7fc4..3bdf8478 100644 --- a/applications/web/src/components/NewCircleTool.svelte +++ b/applications/web/src/components/NewCircleTool.svelte @@ -11,26 +11,21 @@ export let active: boolean export let projectToPlane: ProjectToPlane - // log("[props]", "pointsById:", pointsById, "sketchIndex:", sketchIndex, "active:", active) - - let centerPoint: PointLikeById | null - - $: if ($sketchTool !== "circle") centerPoint = null - // $: centerPoint, log("[centerPoint]", centerPoint) - - function processPoint(point: PointLikeById) { - if (!centerPoint) { - // if there is no center point, set one - if (point.id) { - // nothing to do, the point exists! - // log('nothing to do the point exists!') - } else { - // again, don't actually DO anything yet to the sketch - point.id = null - } - centerPoint = point - } else { - // there WAS an center point, so we should create a circle! + let centerPoint: PointLikeById | null + + $: if ($sketchTool !== "circle") centerPoint = null + + function processPoint(point: PointLikeById) { + if (!centerPoint) { + // if there is no center point, set one + if (!point.id) { + // don't actually DO anything yet to the sketch + point.id = null + } + // else nothing to do, the point exists! + centerPoint = point + } else { + // there WAS an center point, so we should create a circle! // if the center point doesn't exist, then we should create a point if (centerPoint.id === null) centerPoint.id = addPointToSketch(sketchIndex, centerPoint.twoD!, false) @@ -97,17 +92,15 @@ } } - // if (snappedTo) log("[snappedTo]", snappedTo) - - // only reset $snapPoints if something has changed - if (snappedTo) { - // @ts-ignore todo rework snapping - $snapPoints = [snappedTo] // todo all these different point representations need work! - } else { - if ($snapPoints.length > 0) { - $snapPoints = [] - } - } + // only reset $snapPoints if something has changed + if (snappedTo) { + // @ts-ignore todo rework snapping + $snapPoints = [snappedTo] // todo all these different point representations need work! + } else { + if ($snapPoints.length > 0) { + $snapPoints = [] + } + } if (centerPoint) { function calcDeltas(a: Vector2Like | Point2D | {x: number; y: number}, b: Vector2Like | undefined) { diff --git a/packages/cadmium-macros/Cargo.toml b/packages/cadmium-macros/Cargo.toml new file mode 100644 index 00000000..b5ac1f56 --- /dev/null +++ b/packages/cadmium-macros/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "cadmium-macros" +version = "0.1.0" +edition = "2021" + +[dependencies] +syn = { version = "1.0", features = ["parsing", "derive"] } +quote = "1.0" +proc-macro2 = "1.0" +convert_case = "0.6.0" + +[lib] +proc-macro = true diff --git a/packages/cadmium-macros/src/lib.rs b/packages/cadmium-macros/src/lib.rs new file mode 100644 index 00000000..9b6fa65f --- /dev/null +++ b/packages/cadmium-macros/src/lib.rs @@ -0,0 +1,302 @@ +use std::collections::HashMap; + +use convert_case::{Case, Casing}; +use proc_macro2::TokenStream; +// use proc_macro::TokenStream; +use quote::{quote, TokenStreamExt}; +use syn::punctuated::Punctuated; +use syn::{parse_macro_input, Attribute, DeriveInput, Fields, Ident, MetaNameValue, Token}; +use syn::spanned::Spanned; + +const ATTR_NAME: &str = "step_data"; + +fn get_meta_kv(attrs: &Vec) -> HashMap { + let mut result = HashMap::new(); + + for attr in attrs { + if !attr.path.is_ident(ATTR_NAME) { + continue; + } + + let Ok(name_values): Result, _> = attr + .parse_args_with(Punctuated::parse_terminated) else { continue; }; + + for nv in name_values { + let Some(ident) = nv.path.get_ident() else { continue; }; + result.insert(ident.clone(), nv); + } + } + + result +} + +fn get_function_body( + func_name: Ident, + name: &Ident, + variant_name: &Ident, + fields: &Fields, + id_ident: Ident, + wb_var: TokenStream, + self_field_code: TokenStream, + self_field_var: TokenStream, + operation: TokenStream, + skip_history: bool +) -> (TokenStream, TokenStream) { + // Function arguments - both on definition and call + let function_defs = fields.iter().map(|field| { + let field_name = &field.ident; + let field_type = &field.ty; + + quote! { #field_name: #field_type } + }).collect::>(); + let function_args_full = fields.iter().map(|field| { + let field_name = &field.ident; + + quote! { #field_name } + }).collect::>(); + + let function_args2 = function_args_full.clone(); + let function_args_noauto = function_args2 + .iter() + .filter(|field| + field.to_string() != "workbench_id" + && field.to_string() != id_ident.to_string() + ).collect::>(); + + // Generate history entry + let history_code = if skip_history { + quote! {} + } else { + quote! { + let step_ = crate::step::Step { + name, + id: result_id_, + operation: operation_, + unique_id: format!(concat!("Add:", stringify!(#variant_name), "-{}"), result_id_), + suppressed: false, + data: #name::#variant_name { + #( #function_args_full ),* + }, + }; + + wb_.history.push(step_); + } + }; + + // Code to run during `do_action` + let action = quote! { + #name::#variant_name { + #( #function_args_full ),* + } => project.#func_name( + name, + #( #function_args_full.clone() ),* + ), + }; + + // The actual function body + let body = quote! { + pub fn #func_name(&mut self, name: String, #( #function_defs ),*) -> Result { + let operation_ = #operation; + #wb_var + #self_field_code + let result_id_ = #self_field_var.#func_name(#( #function_args_noauto.clone() ),*)?; + + #history_code + + Ok(result_id_) + } + }; + + (body, action) +} + +#[proc_macro_derive(StepDataActions, attributes(step_data))] +pub fn derive_step_data(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + let input = parse_macro_input!(input as DeriveInput); + + let name = &input.ident; + let data = match input.data { + syn::Data::Enum(data) => data, + _ => panic!("StepData can only be derived for enums"), + }; + let mut actions = vec![]; + + let variants = data.variants.iter().map(|variant| { + let variant_name = &variant.ident; + + let mut workbench_field = None; + let mut parent_type = None; + let mut skip_history = false; + let mut skip_add = false; + let mut skip_update = false; + let mut skip_delete = false; + + // Parse the attributes above each variant + for (k, v) in get_meta_kv(&variant.attrs).iter() { + match k.to_string().as_str() { + "workbench_field" => { + if let syn::Lit::Str(value) = &v.lit { + workbench_field = Some(value.value()); + } else { + panic!("workbench_field must be a string literal"); + } + }, + "type_name" => { + if let syn::Lit::Str(value) = &v.lit { + parent_type = Some(value.value()); + } else { + panic!("type_name must be a string literal"); + } + }, + "skip_history" => { + if let syn::Lit::Bool(_value) = &v.lit { + skip_history = true; + } else { + panic!("skip_history must be a bool literal"); + } + }, + "skip_add" => { + if let syn::Lit::Bool(_value) = &v.lit { + skip_add = true; + } else { + panic!("skip_add must be a bool literal"); + } + }, + "skip_update" => { + if let syn::Lit::Bool(_value) = &v.lit { + skip_update = true; + } else { + panic!("skip_update must be a bool literal"); + } + }, + "skip_delete" => { + if let syn::Lit::Bool(_value) = &v.lit { + skip_delete = true; + } else { + panic!("skip_delete must be a bool literal"); + } + }, + &_ => {} + } + } + + let needs_workbench = variant.fields.iter().any(|field| field.ident.as_ref().unwrap().to_string() == "workbench_id"); + + // Process not skipped workbench + let mut wb_var = quote! {}; + if needs_workbench { + wb_var = quote! { + let mut wb_ = self.workbenches + .get_mut(workbench_id as usize) + .ok_or(anyhow::anyhow!("Could not find workbench ID {}", workbench_id))?; + }; + } + + // Process type_name to expected id field (e.g. sketch_id for Sketch) + let mut field_var = quote! {}; + let mut parent_var = quote! { wb_ }; + let id_arg_name = if let Some(f) = parent_type.clone() { + Ident::new(format!("{}_id", f.to_string().to_case(Case::Snake)).as_str(), f.span()) + } else { + Ident::new("id", variant_name.span()) + }; + + // Generate the parent variable of which the actual function will be called on + if let Some(field_ident) = workbench_field.clone() { + let field_name = Ident::new(field_ident.as_str(), field_ident.span()); + field_var = quote! { + let parent_ref_ = wb_.#field_name + .get(& #id_arg_name) + .ok_or(anyhow::anyhow!(concat!("Could not find parent ", stringify!(#parent_type), " with ID {}"), #id_arg_name))?; + let mut parent_ = parent_ref_.borrow_mut(); + }; + parent_var = quote! { parent_ }; + } else if needs_workbench { + parent_var = quote! { wb_ }; + } else { + parent_var = quote! { self }; + } + + // Generated function names + + + // Populate the `do_action` function of StepData + + let add_func = if !skip_add { + let func_name = Ident::new(format!("add_{}", variant_name.to_string().to_case(Case::Snake)).as_str(), variant_name.span()); + let gen = get_function_body( + func_name, + name, + variant_name, + &variant.fields, + id_arg_name.clone(), + wb_var.clone(), + field_var.clone(), + parent_var.clone(), + quote! { crate::step::StepOperation::Add }.into(), + skip_history); + + actions.push(gen.1); + gen.0 + } else { quote! {} }; + + let update_func = if !skip_update { + let func_name = Ident::new(format!("update_{}", variant_name.to_string().to_case(Case::Snake)).as_str(), variant_name.span()); + let gen = get_function_body( + func_name, + name, + variant_name, + &variant.fields, + id_arg_name.clone(), + wb_var.clone(), + field_var.clone(), + parent_var.clone(), + quote! { crate::step::StepOperation::Add }.into(), + skip_history); + + actions.push(gen.1); + gen.0 + } else { quote! {} }; + + let delete_func = if !skip_delete { + let func_name = Ident::new(format!("delete_{}", variant_name.to_string().to_case(Case::Snake)).as_str(), variant_name.span()); + let gen = get_function_body( + func_name, + name, + variant_name, + &variant.fields, + id_arg_name, + wb_var, + field_var, + parent_var, + quote! { crate::step::StepOperation::Add }.into(), + skip_history); + + actions.push(gen.1); + gen.0 + } else { quote! {} }; + + + quote! { + #add_func + #update_func + #delete_func + } + }); + + let expanded = quote! { + impl crate::project::Project { + #( #variants )* + } + + impl #name { + pub fn do_action(&self, project: &mut crate::project::Project, name: String) -> Result { + match self { + #( #actions )* + } + } + } + }; + + TokenStream::from(expanded).into() +} diff --git a/packages/cadmium/Cargo.toml b/packages/cadmium/Cargo.toml index e191311c..3fb35405 100644 --- a/packages/cadmium/Cargo.toml +++ b/packages/cadmium/Cargo.toml @@ -30,6 +30,11 @@ indexmap = "2.1.0" anyhow = { version = "1.0.86", features = ["backtrace"] } thiserror = "1.0.61" strum = { version = "0.26.2", features = ["derive"] } +# isotope = { git = "https://github.com/CADmium-Co/ISOtope.git", version = "*", features = ["tsify"] } +isotope = { git = "https://github.com/dzervas/ISOtope.git", branch = "sketch-helpers", version = "*", features = ["tsify"] } +# isotope = { path = "../../../isotope", version = "*", features = ["tsify"]} +paste = "1.0.15" +cadmium-macros = { path = "../cadmium-macros", version = "*" } [lib] crate-type = ["cdylib", "rlib"] diff --git a/packages/cadmium/src/archetypes.rs b/packages/cadmium/src/archetypes.rs index 3440a0c6..14ca6de7 100644 --- a/packages/cadmium/src/archetypes.rs +++ b/packages/cadmium/src/archetypes.rs @@ -1,15 +1,17 @@ +use isotope::primitives::point2::Point2 as ISOPoint2; use tsify::Tsify; use serde::{Deserialize, Serialize}; use truck_modeling::Plane as TruckPlane; use truck_modeling::InnerSpace; -use crate::sketch::Point2; +use crate::solid::point::Point3; +use crate::IDType; -#[derive(Tsify, Debug, Serialize, Deserialize)] +#[derive(Tsify, Debug, Clone, Serialize, Deserialize)] #[tsify(into_wasm_abi, from_wasm_abi)] pub enum PlaneDescription { - PlaneId(String), - SolidFace { solid_id: String, normal: Vector3 }, + PlaneId(IDType), + SolidFace { solid_id: IDType, normal: Vector3 }, } #[derive(Tsify, Debug, Clone, Serialize, Deserialize)] @@ -87,20 +89,34 @@ impl Plane { } } - pub fn project(&self, point: &Point3) -> Point2 { + pub fn project(&self, point: &Point3) -> ISOPoint2 { let minus_origin = point.minus(&self.origin); let x = minus_origin.dot(&self.primary); let y = minus_origin.dot(&self.secondary); - Point2::new(x, y) + ISOPoint2::new(x, y) } - pub fn unproject(&self, point: &Point2) -> Point3 { - let x = self.origin.plus(self.primary.times(point.x)); - let y = self.origin.plus(self.secondary.times(point.y)); + pub fn unproject(&self, point: &ISOPoint2) -> Point3 { + let x = self.origin.plus(self.primary.times(point.x())); + let y = self.origin.plus(self.secondary.times(point.y())); x.plus(y).to_point3() } } + +#[derive(Tsify, Debug, Clone, Serialize, Deserialize)] +#[tsify(into_wasm_abi, from_wasm_abi)] +pub struct Vector2 { + pub x: f64, + pub y: f64, +} + +impl Vector2 { + pub fn new(x: f64, y: f64) -> Self { + Vector2 { x, y } + } +} + #[derive(Tsify, Debug, Clone, Serialize, Deserialize)] #[tsify(into_wasm_abi, from_wasm_abi)] pub struct Vector3 { @@ -141,44 +157,15 @@ impl Vector3 { #[derive(Tsify, Debug, Clone, Serialize, Deserialize)] #[tsify(into_wasm_abi, from_wasm_abi)] -pub struct Point3 { +pub struct Point2 { pub x: f64, pub y: f64, - pub z: f64, pub hidden: bool, } -impl Point3 { - pub fn new(x: f64, y: f64, z: f64) -> Self { - Point3 { - x, - y, - z, - hidden: false, - } - } - - pub fn plus(&self, v: Vector3) -> Vector3 { - Vector3 { - x: self.x + v.x, - y: self.y + v.y, - z: self.z + v.z, - } - } - - pub fn minus(&self, other: &Point3) -> Vector3 { - Vector3 { - x: self.x - other.x, - y: self.y - other.y, - z: self.z - other.z, - } - } - - pub fn distance_to(&self, other: &Point3) -> f64 { - let dx = self.x - other.x; - let dy = self.y - other.y; - let dz = self.z - other.z; - (dx * dx + dy * dy + dz * dz).sqrt() +impl Into for Point2 { + fn into(self) -> ISOPoint2 { + ISOPoint2::new(self.x, self.y) } } diff --git a/packages/cadmium/src/error.rs b/packages/cadmium/src/error.rs index 87939e3b..ddd064be 100644 --- a/packages/cadmium/src/error.rs +++ b/packages/cadmium/src/error.rs @@ -1,7 +1,5 @@ use thiserror::Error; -use crate::sketch::SketchFeatureType; - #[derive(Error, Debug)] pub enum CADmiumError { // Message errors @@ -16,21 +14,18 @@ pub enum CADmiumError { #[error("The sketch ID {0} was not found")] SketchIDNotFound(u64), + // RealSketch errors + #[error("The primitive could not be found inside the sketch")] + PrimitiveNotInSketch, + #[error("Couldn't calculate the 3D position of the supplied point")] + Point3DCalculationFailed, + #[error("The calculated 3D point was not found in the sketch")] + Point3DNotFound, + // StepData errors #[error("The step {0} data type is not as expected")] IncorrectStepDataType(String), - // Sketch errors - #[error("The {0} with ID {1} already exists in the sketch")] - SketchFeatureAlreadyExists(SketchFeatureType, u64), - #[error("The {0} ID is too low for {1}")] - SketchFeatureIDTooLow(SketchFeatureType, u64), - #[error("The {0} with ID {1} has a start point that doesn't exist in the current sketch")] - SketchFeatureMissingStart(SketchFeatureType, u64), - #[error("The {0} with ID {1} has an end point that doesn't exist in the current sketch")] - SketchFeatureMissingEnd(SketchFeatureType, u64), - - #[error("This function is not implemented yet")] NotImplemented, } diff --git a/packages/cadmium/src/extrusion.rs b/packages/cadmium/src/extrusion.rs deleted file mode 100644 index 2b9f6d41..00000000 --- a/packages/cadmium/src/extrusion.rs +++ /dev/null @@ -1,447 +0,0 @@ -use geo::Contains; -use geo::InteriorPoint; -use geo::Polygon; - -use serde::{Deserialize, Serialize}; -use tsify::Tsify; - -use truck_polymesh::InnerSpace; -use truck_polymesh::Invertible; -use truck_polymesh::Tolerance; -use truck_shapeops::ShapeOpsCurve; -use truck_shapeops::ShapeOpsSurface; -use truck_topology::Shell; - -use crate::archetypes::{Point3, Vector3}; -use crate::project::{RealPlane, RealSketch}; -use crate::sketch::{arc_to_points, Face, Sketch}; - -use truck_modeling::{Point3 as TruckPoint3, Surface, Plane}; - -use truck_topology::Solid as TruckSolid; - -#[derive(Tsify, Debug, Clone, Serialize, Deserialize)] -#[tsify(into_wasm_abi, from_wasm_abi)] -pub struct Extrusion { - pub sketch_id: String, - pub face_ids: Vec, - pub length: f64, - pub offset: f64, - pub direction: Direction, - pub mode: ExtrusionMode, -} - -#[derive(Tsify, Debug, Clone, Serialize, Deserialize)] -#[tsify(into_wasm_abi, from_wasm_abi)] -pub enum ExtrusionMode { - New, - Add(Vec), - Remove(Vec), -} - -impl Extrusion { - pub fn new( - sketch_id: String, - face_ids: Vec, - length: f64, - offset: f64, - direction: Direction, - mode: ExtrusionMode, - ) -> Self { - Extrusion { - sketch_id, - face_ids, - length, - offset, - direction, - mode, - } - } -} - -#[derive(Tsify, Debug, Clone, Serialize, Deserialize)] -#[tsify(into_wasm_abi, from_wasm_abi)] -pub enum Direction { - Normal, - NegativeNormal, - Specified(Vector3), -} - -pub fn find_enveloped_shapes(faces: &Vec) -> Vec<(usize, usize)> { - let mut retval = vec![]; - for (a, face_a) in faces.iter().enumerate() { - for (b, face_b) in faces.iter().enumerate() { - if a == b { - continue; - } - let face_b_exterior = &face_b.exterior.canonical_form(); - - // check if b's exterior is equal to any of a's holes - for (_hole_index, hole) in face_a.holes.iter().enumerate() { - let hole = hole.canonical_form(); - - if face_b_exterior.equals(&hole) { - retval.push((b, a)); // (small, big) - } - } - } - } - - return retval; -} - -pub fn merge_faces(faces: &Vec, real_sketch: &RealSketch) -> Vec { - // create new sketch using these faces - let sketch = Sketch::from_faces(faces, real_sketch); - - let (mut all_sketch_faces, _unused_segments) = sketch.find_faces(); - - // check whether each of these new faces should returned or not by picking a - // random point on the new face and then checking every one of the original faces - // to see if it contains that point. If so, we can keep that new face - let mut faces_to_remove: Vec = vec![]; - let old_faces_as_polygons: Vec = faces - .iter() - .map(|face| sketch.face_as_polygon(face)) - .collect::>(); - for (new_face_idx, face) in all_sketch_faces.iter().enumerate() { - // println!("\nNew face: {}: {:?}", new_face_idx, face); - - let as_geo_polygon = sketch.face_as_polygon(face); - // println!("as_geo_polygon: {:?}", as_geo_polygon); - - let random_point_on_face = as_geo_polygon - .interior_point() - .expect("Every polygon should be able to yield an interior point"); - // println!("Random point on face: {:?}", random_point_on_face); - - let mut located = false; - for (_old_face_idx, old_face) in old_faces_as_polygons.iter().enumerate() { - if old_face.contains(&random_point_on_face) { - // println!( - // "Old face {} contains point {:?}", - // old_face_idx, random_point_on_face - // ); - // this means the old face contains the random point on the new face - // so we can keep this new face - located = true; - break; - } - } - if !located { - // println!( - // "Random point from new face {} is not contained by any old faces", - // new_face_idx - // ); - faces_to_remove.push(new_face_idx); - } - } - - // remove the faces that we don't want - faces_to_remove.sort(); - faces_to_remove.reverse(); - // println!("New Faces to remove: {:?}", faces_to_remove); - for face_to_remove in faces_to_remove { - all_sketch_faces.remove(face_to_remove); - } - - // println!("Merge faces 2 output: {}", faces.len()); - all_sketch_faces -} - -// pub fn find_adjacent_shapes(faces: &Vec) -> Option<(usize, usize, Vec, Vec)> { -// for (a, face_a) in faces.iter().enumerate() { -// for (b, face_b) in faces.iter().enumerate() { -// if a == b || a > b { -// continue; -// } - -// let adjacent_edges = face_a.exterior.adjacent_edges(&face_b.exterior); - -// match adjacent_edges { -// None => continue, -// Some(matched_edges) => return Some((a, b, matched_edges.0, matched_edges.1)), -// } -// } -// } - -// None -// } - -// pub fn merge_faces(faces: Vec) -> Vec { -// let mut faces = faces.clone(); -// // adjacency: -// // check if this shape's exterior is adjacent to any other shape's exterior -// // if so, merge them into a single shape by deleting any shared sides -// // and recomputing the faces - -// while let Some((a, b, a_indices, b_indices)) = find_adjacent_shapes(&faces) { -// println!("touching_shapes: {:?}", (a, b, a_indices, b_indices)); -// let face_a = &faces[a]; -// let face_b = &faces[b]; - -// match (&face_a.exterior, &face_b.exterior) { -// (Ring::Segments(segments_a), Ring::Segments(segments_b)) => { -// let mut face_a_location = 0; -// let mut face_b_location = 0; -// let mut pulling_from_a = true; -// let mut new_exterior_segments: Vec = vec![]; - -// loop { -// if pulling_from_a { -// let segment = segments_a[face_a_location].clone(); -// new_exterior_segments.push(segment); -// face_a_location += 1; -// } else { -// // pull from b -// let segment = segments_b[face_b_location].clone(); -// new_exterior_segments.push(segment); -// face_b_location += 1; -// } -// } -// } -// _ => panic!("Only Rings made of Segments can have adjacent edges!"), -// } - -// // let mut new_face = Face { -// // exterior: new_exterior_segments, -// // holes: vec![], -// // }; - -// // remove face a and face b -// // add new_face - -// break; -// } - -// // envelopment: -// // check if this shape's exterior is equal to any other shape's hole -// // if so, merge them into a single shape by deleting that hole from the -// // other shape, and adding this shape's holes to that shape's holes -// while let Some((a, b, c)) = find_enveloped_shapes(&faces) { -// // this means a's exterior is equal to one of b's holes. Hole c in particular -// let face_a = &faces[a]; -// let face_b = &faces[b]; - -// // to fix this we need to remove the information contained in a's exterior completely. -// // to do that we remove the c indexed hole from face_b's list of holes -// let mut b_new_holes = face_b.holes.clone(); -// b_new_holes.remove(c); -// b_new_holes.append(&mut face_a.holes.clone()); - -// let mut new_face_b = Face { -// exterior: face_b.exterior.clone(), -// holes: b_new_holes, -// }; - -// let mut new_faces = faces.clone(); - -// // replace the larger face with our modified face -// new_faces[b] = new_face_b.clone(); - -// // remove the smaller face from the list of faces -// new_faces.remove(a); -// faces = new_faces; -// } - -// faces -// } - -pub fn find_transit( - real_plane: &RealPlane, - start: &Point3, - end: &Point3, - center: &Point3, - clockwise: bool, -) -> Point3 { - // let radius = start.distance_to(center); - - let start = real_plane.plane.project(start); - let end = real_plane.plane.project(end); - let center = real_plane.plane.project(center); - - let pts = arc_to_points(&start, &end, ¢er, clockwise); - - let transit = &pts[pts.len() / 2]; - - let transit_3d = real_plane.plane.unproject(&transit); - transit_3d -} - -pub fn fuse + std::fmt::Debug, S: ShapeOpsSurface + std::fmt::Debug>( - solid0: &TruckSolid, - solid1: &TruckSolid, -) -> Option> { - println!("Okay let's fuse!"); - - let solid0_boundaries = solid0.boundaries(); - let solid1_boundaries = solid1.boundaries(); - assert!(solid0_boundaries.len() == 1); - assert!(solid1_boundaries.len() == 1); - - let boundary0 = &solid0_boundaries[0]; - let boundary1 = &solid1_boundaries[0]; - let fusable_faces = find_coplanar_face_pairs(boundary0, boundary1, true); - assert!(fusable_faces.len() == 1); - let fusable_faces = fusable_faces[0]; - // TODO: support the case where more than one is fusable - println!("fusable_faces: {:?}", fusable_faces); - - let secondary_mergeable_faces = find_coplanar_face_pairs(boundary0, boundary1, false); - println!("secondary_mergeable_faces: {:?}", secondary_mergeable_faces); - - // There's only one fused solid at the end. Create it by cloning solid0 - // and then removing the fusable face from it. - let mut combined = boundary0.clone(); - combined.remove(fusable_faces.0); - - // Meanwhile, make a copy of solid1 and remove the fusable face from it too. - let mut boundary1_copy = boundary1.clone(); - boundary1_copy.remove(fusable_faces.1); - - // Then, add every face from solid1 to the combined solid. - combined.append(&mut boundary1_copy); - - // Lastly, merge the two fusable faces together. This is complicated because - // one might be bigger than the other, or they might be the same size, or - // they might overlap somewhat. We'll need to figure out how to merge them. - // println!("How do I merge these two? {:?}", fusable_faces); - // println!("First:"); - // for edge in boundary0[fusable_faces.0].edge_iter() { - // println!( - // "Edge: {:?} to {:?}", - // edge.front().get_point(), - // edge.back().get_point() - // ); - // } - let mut outer_face = boundary0[fusable_faces.0].clone(); - let inner_face = boundary1[fusable_faces.1].clone(); - outer_face.add_boundary(inner_face.boundaries().first().unwrap().clone()); - - // Then add that merged face to the solid and we've fused! - combined.push(outer_face); - - // After that, we need to merge the secondary_mergeable_faces together. - for (face_0_idx, face_1_idx) in secondary_mergeable_faces { - let mut face_0 = boundary0[face_0_idx].clone(); - let face_1 = boundary1[face_1_idx].clone(); - face_0.add_boundary(face_1.boundaries().first().unwrap().clone()); - combined.push(face_0); - } - - // And then we're done! - // None - Some(TruckSolid::new(vec![combined])) -} - -fn find_coplanar_face_pairs, S: ShapeOpsSurface>( - boundary0: &Shell, - boundary1: &Shell, - flip_second: bool, -) -> Vec<(usize, usize)> { - let mut coplanar_faces: Vec<(usize, usize)> = vec![]; - for (face_0_idx, face_0) in boundary0.face_iter().enumerate() { - let surface_0 = face_0.oriented_surface(); - - match surface_0 { - Surface::Plane(p0) => { - for (face_1_idx, face_1) in boundary1.face_iter().enumerate() { - let mut surface_1 = face_1.oriented_surface(); - - if flip_second { - surface_1 = surface_1.inverse(); - } - - match surface_1 { - Surface::Plane(p1) => { - if are_coplanar(p0, p1) { - coplanar_faces.push((face_0_idx, face_1_idx)); - } - } - _ => {} - } - } - } - _ => {} - } - } - - coplanar_faces -} - -fn are_coplanar(p0: Plane, p1: Plane) -> bool { - let normal0 = p0.normal(); - let normal1 = p1.normal(); - - if !normal0.near(&normal1) { - return false; - } - - let difference = p0.origin() - p1.origin(); - let dot = normal0.dot(difference); - dot.abs() < 0.0001 -} - -#[cfg(test)] -mod tests { - use crate::project::Project; - use crate::project::tests::create_test_project; - - #[allow(unused_imports)] - use super::*; - - #[test] - fn create_project_solid() { - let mut p = Project::new("Test Extrusion"); - - // now get solids? save as obj or stl or step? - let workbench = p.workbenches.get_mut(0).unwrap(); - let realization = workbench.realize(100); - let solids = realization.solids; - println!("solids: {:?}", solids); - } - - #[test] - fn project_from_files() { - let file_list = [ - // this file contains three shapes which are adjacent to each other and - // thus should result in a single output solid - ("src/test_inputs/three_adjacent_faces.cadmium", 1), - // this file contains one square nested inside another - // and thus should result in a single output solid - ("src/test_inputs/nested_squares.cadmium", 1), - // this file contains one circle nested inside another - // and thus should result in a single output solid - ("src/test_inputs/nested_circles.cadmium", 1), - ("src/test_inputs/two_Es.cadmium", 1), - ("src/test_inputs/lots_of_nesting.cadmium", 4), - ]; - - for (file, expected_solids) in file_list.iter() { - let contents = std::fs::read_to_string(file).unwrap(); - - // deserialize the contents into a Project - let mut p: Project = serde_json::from_str(&contents).unwrap(); - - // get a realization - let workbench = p.workbenches.get_mut(0).unwrap(); - let realization = workbench.realize(100); - let solids = realization.solids; - println!("[{}] solids: {:?}", file, solids.len()); - - assert_eq!(solids.len(), *expected_solids); // doesn't work yet! - } - } - - #[test] - fn step_export() { - let p = create_test_project(); - let workbench = &p.workbenches[0 as usize]; - let realization = workbench.realize(1000); - let keys = Vec::from_iter(realization.solids.keys()); - - realization.save_solid_as_step_file(keys[0], "pkg/test.step"); - realization.save_solid_as_obj_file(keys[0], "pkg/test.obj", 0.001); - } - -} diff --git a/packages/cadmium/src/isketch.rs b/packages/cadmium/src/isketch.rs new file mode 100644 index 00000000..c7eeee98 --- /dev/null +++ b/packages/cadmium/src/isketch.rs @@ -0,0 +1,163 @@ +use std::cell::RefCell; +use std::collections::BTreeMap; +use std::rc::Rc; + +use isotope::decompose::face::Face; +use isotope::primitives::line::Line; +use isotope::primitives::point2::Point2 as ISOPoint2; +use isotope::primitives::PrimitiveCell; +use isotope::sketch::Sketch; +use serde::{Deserialize, Serialize}; +use tsify::Tsify; + +use crate::archetypes::{Plane, Point2}; +use crate::error::CADmiumError; +use crate::solid::point::Point3; +use crate::IDType; + +#[derive(Tsify, Debug, Clone, Serialize, Deserialize)] +#[tsify(into_wasm_abi, from_wasm_abi)] +pub struct IPlane { + // TODO: Should hold its own ID + // pub id: String, + pub plane: Plane, + pub name: String, + pub width: f64, + pub height: f64, +} + +#[derive(Tsify, Debug, Clone, Serialize, Deserialize)] +#[tsify(into_wasm_abi, from_wasm_abi)] +pub struct ISketch { + // TODO: Make it private with a setter + pub plane: Rc>, + + sketch: Rc>, + points_3d: BTreeMap, +} + +impl ISketch { + // TODO: Maybe pass the plane as refcell? + pub fn new(plane: Rc>) -> Self { + // The key difference between Sketch and RealSketch is that Sketch lives + // in 2D and RealSketch lives in 3D. So we need to convert the points + + let mut real_sketch = Self { + plane: plane.clone(), + points_3d: BTreeMap::new(), + // primitives: sketch.borrow().primitives().iter().map(|(id, prim)| (*id, prim.borrow().to_primitive())).collect(), + // constraints: sketch.borrow().constraints().iter().map(|c| c.borrow().get_type()).collect(), + sketch: Rc::new(RefCell::new(Sketch::new())), + }; + + for (id, point) in real_sketch.sketch.borrow().get_all_points().iter() { + real_sketch.points_3d.insert(*id, Point3::from_plane_point(&plane.borrow().clone(), point)); + } + + real_sketch + } + + /// Helper function to go from an isotope point2D to a point_3D, as calculated during new + pub fn get_point_3d(&self, point: Rc>) -> Result<(u64, Point3), CADmiumError> { + let cell = PrimitiveCell::Point2(point.clone()); + let point_id = self.sketch.borrow().get_primitive_id(&cell).unwrap(); + + if let Some(result) = self.points_3d.get(&point_id) { + Ok((point_id, result.clone())) + } else { + // TODO: While I'd like to calculate and add the point_3d here, we'll pollute everything with mut + // let point_3d = Point3::from_plane_point(&self.plane.borrow(), &point.borrow()); + + // Ok((point_id, + // self.points_3d + // .insert(point_id, point_3d) + // .ok_or(CADmiumError::Point3DCalculationFailed)?)) + Err(CADmiumError::Point3DCalculationFailed) + } + } + + pub fn sketch(&self) -> Rc> { + self.sketch.clone() + } + + pub fn faces(&self) -> Vec { + // TODO: How do we keep track of faces vs IDs? + self.sketch.borrow().get_merged_faces() + } + + pub fn find_point_ref(&self, x: f64, y: f64) -> Option>> { + self.sketch.borrow().primitives().iter().find_map(|(_, prim)| { + if let PrimitiveCell::Point2(point_ref) = prim { + let point = point_ref.borrow(); + if (point.x() - x).abs() < 0.0001 && (point.y() - y).abs() < 0.0001 { + Some(point_ref.clone()) + } else { + None + } + } else { + None + } + }) + } +} + +impl ISketch { + pub(super) fn add_sketch_point(&mut self, point: Point2) -> Result { + let iso_point = PrimitiveCell::Point2(Rc::new(RefCell::new(point.clone().into()))); + + let mut sketch = self.sketch.borrow_mut(); + let point_id = sketch.add_primitive(iso_point)?; + self.points_3d.insert(point_id, Point3::from_plane_point(&self.plane.borrow(), &point.into())); + Ok(point_id) + } + + pub(super) fn add_sketch_arc(&mut self, center: IDType, radius: f64, clockwise: bool, start_angle: f64, end_angle: f64) -> Result { + let mut sketch = self.sketch.borrow_mut(); + + let center_point = if let PrimitiveCell::Point2(point) = sketch.get_primitive_by_id(center).unwrap() { + point + } else { + return Err(anyhow::anyhow!("Center point is not a point")); + }; + + let arc = PrimitiveCell::Arc(Rc::new(RefCell::new(isotope::primitives::arc::Arc::new(center_point.clone(), radius, clockwise, start_angle, end_angle)))); + + let point_id = sketch.add_primitive(arc)?; + Ok(point_id) + } + + pub(super) fn add_sketch_circle(&mut self, center: IDType, radius: f64) -> Result { + let mut sketch = self.sketch.borrow_mut(); + + let center_point = if let PrimitiveCell::Point2(point) = sketch.get_primitive_by_id(center).unwrap() { + point + } else { + return Err(anyhow::anyhow!("Center point is not a point")); + }; + + let circle = PrimitiveCell::Circle(Rc::new(RefCell::new(isotope::primitives::circle::Circle::new(center_point.clone(), radius)))); + + let point_id = sketch.add_primitive(circle)?; + Ok(point_id) + } + + pub(super) fn add_sketch_line(&mut self, start: IDType, end: IDType) -> Result { + let mut sketch = self.sketch.borrow_mut(); + + let start_point = if let PrimitiveCell::Point2(point) = sketch.get_primitive_by_id(start).unwrap() { + point + } else { + return Err(anyhow::anyhow!("Start point is not a point")); + }; + let end_point = if let PrimitiveCell::Point2(point) = sketch.get_primitive_by_id(end).unwrap() { + point + } else { + return Err(anyhow::anyhow!("End point is not a point")); + }; + + let line = PrimitiveCell::Line(Rc::new(RefCell::new(Line::new(start_point.clone(), end_point.clone())))); + + let point_id = sketch.add_primitive(line)?; + Ok(point_id) + } +} diff --git a/packages/cadmium/src/lib.rs b/packages/cadmium/src/lib.rs index 38ce23ab..48d75480 100644 --- a/packages/cadmium/src/lib.rs +++ b/packages/cadmium/src/lib.rs @@ -1,18 +1,25 @@ use message::{Message, MessageResult}; +use tsify::declare; use wasm_bindgen::prelude::*; extern crate console_error_panic_hook; pub mod archetypes; pub mod error; -pub mod extrusion; +pub mod isketch; pub mod message; pub mod project; pub mod realization; pub mod solid; -pub mod sketch; +#[macro_use] pub mod step; pub mod workbench; +// pub use isotope::primitives::ParametricCell; +// pub use isotope::constraints::ConstraintCell; + +#[declare] +pub type IDType = u64; + #[wasm_bindgen] pub struct Project { native: project::Project, @@ -57,22 +64,23 @@ impl Project { #[wasm_bindgen] pub fn compute_constraint_errors(&mut self) { - self.native.compute_constraint_errors(); + // self.native.compute_constraint_errors(); } #[wasm_bindgen] - pub fn get_realization(&self, workbench_id: u32, max_steps: u32) -> Realization { + pub fn get_realization(&mut self, workbench_id: u32, max_steps: u32) -> Result { let realized = self .native - .get_realization(workbench_id as u64, max_steps as u64); + .get_realization(workbench_id as IDType, max_steps as u64) + .map_err(|e| format!("Realization Error: {}", e))?; - Realization { native: realized } + Ok(Realization { native: realized }) } #[wasm_bindgen] - pub fn get_workbench(&self, workbench_index: u32) -> String { - let wb = &self.native.workbenches[workbench_index as usize]; - wb.json() + pub fn get_workbench(&self, workbench_index: u32) -> workbench::Workbench { + // TODO: Use get() and return a Result + self.native.workbenches.get(workbench_index as usize).unwrap().clone() } #[wasm_bindgen] @@ -108,12 +116,12 @@ impl Realization { } #[wasm_bindgen] - pub fn solid_to_obj(&self, solid_name: String, tolerance: f64) -> String { - self.native.solid_to_obj(&solid_name, tolerance) + pub fn solid_to_obj(&self, solid_name: IDType, tolerance: f64) -> String { + self.native.solid_to_obj(solid_name, tolerance) } #[wasm_bindgen] - pub fn solid_to_step(&self, solid_name: String) -> String { - self.native.solid_to_step(&solid_name) + pub fn solid_to_step(&self, solid_name: IDType) -> String { + self.native.solid_to_step(solid_name) } } diff --git a/packages/cadmium/src/main.rs b/packages/cadmium/src/main.rs index 1759992a..cbd771de 100644 --- a/packages/cadmium/src/main.rs +++ b/packages/cadmium/src/main.rs @@ -2,7 +2,7 @@ use std::ops::{Sub, SubAssign}; -use cadmium::extrusion::fuse; +use cadmium::solid::extrusion::fuse; use truck_meshalgo::filters::OptimizingFilter; use truck_meshalgo::tessellation::{MeshableShape, MeshedShape}; use truck_modeling::builder::{translated, tsweep, vertex}; diff --git a/packages/cadmium/src/message.rs b/packages/cadmium/src/message.rs index 433b6183..3545a192 100644 --- a/packages/cadmium/src/message.rs +++ b/packages/cadmium/src/message.rs @@ -1,29 +1,31 @@ -use itertools::Itertools as _; - use serde::{Deserialize, Serialize}; use tsify::Tsify; -use crate::archetypes::PlaneDescription; use crate::error::CADmiumError; -use crate::extrusion::{Direction, Extrusion, ExtrusionMode}; +use crate::solid::extrusion::Direction; use crate::project::Project; use crate::step::StepData; +use crate::IDType; #[derive(Tsify, Debug, Serialize, Deserialize)] #[tsify(into_wasm_abi, from_wasm_abi)] -pub enum MessageResult { - #[serde(rename = "success")] - Success(String), - #[serde(rename = "error")] - Error(String), +pub struct MessageResult { + pub success: bool, + pub data: String, } impl From> for MessageResult { fn from(result: Result) -> Self { match result { // TODO: The Success should be a stable enum - Ok(msg) => MessageResult::Success(format!("{{ {} }}", msg)), - Err(e) => MessageResult::Error(e.backtrace().to_string()), + Ok(msg) => Self { + success: true, + data: format!("{{ {} }}", msg) + }, + Err(e) => Self { + success: false, + data: e.backtrace().to_string() + }, } } } @@ -31,6 +33,10 @@ impl From> for MessageResult { #[derive(Tsify, Debug, Serialize, Deserialize)] #[tsify(into_wasm_abi, from_wasm_abi)] pub enum Message { + StepAction { + name: String, + data: StepData, + }, RenameWorkbench { workbench_id: u64, new_name: String, @@ -43,96 +49,20 @@ pub enum Message { RenameProject { new_name: String, }, - DeleteLines { - workbench_id: u64, - sketch_id: String, - line_ids: Vec, - }, - DeleteArcs { - workbench_id: u64, - sketch_id: String, - arc_ids: Vec, - }, - DeleteCircles { - workbench_id: u64, - sketch_id: String, - circle_ids: Vec, - }, - NewPointOnSketch { - workbench_id: u64, - sketch_id: String, - point_id: u64, - x: f64, - y: f64, - }, - NewPointOnSketch2 { - workbench_id: u64, - sketch_id: String, - x: f64, - y: f64, - hidden: bool, - }, - NewCircleBetweenPoints { - workbench_id: u64, - sketch_id: String, - center_id: u64, - edge_id: u64, - }, - NewRectangleBetweenPoints { - workbench_id: u64, - sketch_id: String, - start_id: u64, - end_id: u64, - }, - NewLineOnSketch { - workbench_id: u64, - sketch_id: String, - start_point_id: u64, - end_point_id: u64, - }, - DeleteLineSegment { - workbench_id: u64, - sketch_name: String, - line_segment_id: u64, - }, - StepSketch { - workbench_id: u64, - sketch_name: String, - steps: u64, - }, - SolveSketch { - workbench_id: u64, - sketch_name: String, - max_steps: u64, - }, - NewSketchOnPlane { - workbench_id: u64, - sketch_name: String, - plane_id: String, - }, SetSketchPlane { workbench_id: u64, - sketch_id: String, - plane_id: String, + sketch_id: IDType, + plane_id: IDType, }, DeleteStep { workbench_id: u64, step_name: String, }, - NewExtrusion { - workbench_id: u64, - extrusion_name: String, - sketch_id: String, - face_ids: Vec, - length: f64, - offset: f64, - direction: Direction, - }, UpdateExtrusion { - workbench_id: u64, + workbench_id: IDType, extrusion_name: String, - extrusion_id: String, - sketch_id: String, + extrusion_id: IDType, + sketch_id: IDType, face_ids: Vec, length: f64, offset: f64, @@ -159,6 +89,13 @@ impl Message { pub fn handle(&self, project: &mut Project) -> Result { match self { + Message::StepAction { + name, + data, + } => { + let id = data.do_action(project, name.clone())?; + Ok(format!("\"id\": \"{}\"", id)) + } Message::RenameProject { new_name } => { project.name = new_name.to_owned(); Ok(format!("\"name\": \"{}\"", new_name)) @@ -185,172 +122,18 @@ impl Message { Ok(format!("\"name\": \"{}\"", new_name)) } - Message::DeleteLines { - workbench_id, - sketch_id, - line_ids, - } => { - let workbench = project.get_workbench_by_id_mut(*workbench_id)?; - let sketch = workbench.get_sketch_by_id_mut(sketch_id)?; - for line_id in line_ids { - sketch.delete_line_segment(*line_id); - } - Ok("".to_owned()) - } - Message::DeleteArcs { - workbench_id, - sketch_id, - arc_ids, - } => { - let workbench = project.get_workbench_by_id_mut(*workbench_id)?; - let sketch = workbench.get_sketch_by_id_mut(sketch_id)?; - for arc_id in arc_ids { - sketch.delete_arc(*arc_id); - } - Ok("".to_owned()) - } - Message::DeleteCircles { - workbench_id, - sketch_id, - circle_ids, - } => { - let workbench = project.get_workbench_by_id_mut(*workbench_id)?; - let sketch = workbench.get_sketch_by_id_mut(sketch_id)?; - for circle_id in circle_ids { - sketch.delete_circle(*circle_id); - } - Ok("".to_owned()) - } - Message::NewPointOnSketch2 { - workbench_id, - sketch_id, - x, - y, - hidden, - } => { - let workbench = project.get_workbench_by_id_mut(*workbench_id)?; - let sketch = workbench.get_sketch_by_id_mut(sketch_id)?; - let point_id; - if *hidden { - point_id = sketch.add_hidden_point(*x, *y); - } else { - point_id = sketch.add_point(*x, *y); - } - - Ok(format!("\"id\": \"{}\"", point_id)) - } - Message::NewCircleBetweenPoints { - workbench_id, - sketch_id, - center_id, - edge_id, - } => { - let workbench = project.get_workbench_by_id_mut(*workbench_id)?; - let sketch = workbench.get_sketch_by_id_mut(sketch_id)?; - let circle_id = sketch.add_circle_between_points(*center_id, *edge_id); - Ok(format!("\"id\": \"{}\"", circle_id)) - } - Message::NewRectangleBetweenPoints { - workbench_id, - sketch_id, - start_id, - end_id, - } => { - let workbench = project.get_workbench_by_id_mut(*workbench_id)?; - let sketch = workbench.get_sketch_by_id_mut(sketch_id)?; - let (point_ids, line_ids) = sketch.add_rectangle_between_points(*start_id, *end_id); - Ok(format!( - "\"point_ids\": [{}], \"line_ids\": [{}]", - point_ids.iter().join(","), - line_ids.iter().join(",") - )) - } - Message::NewPointOnSketch { - workbench_id, - sketch_id, - point_id, - x, - y, - } => { - let workbench = project.get_workbench_by_id_mut(*workbench_id)?; - let sketch = workbench.get_sketch_by_id_mut(sketch_id)?; - sketch.add_point_with_id(*x, *y, *point_id)?; - Ok("".to_owned()) - } - Message::NewLineOnSketch { - workbench_id, - sketch_id, - start_point_id, - end_point_id, - } => { - let workbench = project.get_workbench_by_id_mut(*workbench_id)?; - let sketch = workbench.get_sketch_by_id_mut(sketch_id)?; - let line_id = sketch.add_segment(*start_point_id, *end_point_id); - Ok(format!("\"id\": \"{}\"", line_id)) - } - Message::DeleteLineSegment { - workbench_id, - sketch_name, - line_segment_id, - } => { - let workbench = project.get_workbench_by_id_mut(*workbench_id)?; - let sketch = workbench.get_sketch_mut(sketch_name)?; - sketch.delete_line_segment(*line_segment_id); - Ok("".to_owned()) - } - Message::StepSketch { - workbench_id, - sketch_name, - steps, - } => { - let workbench = project.get_workbench_by_id_mut(*workbench_id)?; - let sketch = workbench.get_sketch_mut(sketch_name)?; - let mut max_change = 0.0; - for _ in 0..*steps { - max_change = sketch.take_a_step(); - } - Ok(format!("{}", max_change)) - } - Message::SolveSketch { - workbench_id, - sketch_name, - max_steps, - } => { - let workbench = project.get_workbench_by_id_mut(*workbench_id)?; - let sketch = workbench.get_sketch_mut(sketch_name)?; - sketch.solve(*max_steps); - Ok("".to_owned()) - } - Message::NewSketchOnPlane { - workbench_id, - sketch_name, - plane_id, - } => { - let workbench = project.get_workbench_by_id_mut(*workbench_id)?; - - let new_sketch_id = workbench.add_sketch_to_plane(&sketch_name, &plane_id); - Ok(format!("\"sketch_id\": \"{}\"", new_sketch_id)) - } Message::SetSketchPlane { workbench_id, sketch_id, plane_id: pid, } => { let workbench = project.get_workbench_by_id_mut(*workbench_id)?; - let step = workbench.get_step_by_id_mut(&sketch_id)?; - let plane_description: &mut PlaneDescription = if let StepData::Sketch { plane_description, .. } = &mut step.data { - plane_description - } else { - return Err(CADmiumError::IncorrectStepDataType("Sketch".to_owned()).into()); - }; + let plane = workbench.planes.iter().find(|(p, _)| *p == pid).ok_or(anyhow::anyhow!(""))?; + let sketch_ref = workbench.get_sketch_by_id(*sketch_id)?; + let mut sketch = sketch_ref.borrow_mut(); + sketch.plane = plane.1.clone(); - match plane_description { - PlaneDescription::PlaneId(ref mut plane_id) => { - *plane_id = pid.to_owned(); - Ok(format!("\"plane_id\": \"{}\"", pid)) - } - _ => Err(CADmiumError::NotImplemented.into()) - } + Ok(format!("\"plane_id\": \"{}\"", plane.0)) } Message::DeleteStep { workbench_id, @@ -366,64 +149,54 @@ impl Message { workbench.history.remove(index); Ok("".to_owned()) } - Message::NewExtrusion { - workbench_id, - extrusion_name, - sketch_id, - face_ids, - length, - offset, - direction, - } => { - let workbench = project.get_workbench_by_id_mut(*workbench_id)?; - let extrusion = Extrusion::new( - sketch_id.to_owned(), - face_ids.to_owned(), - *length, - *offset, - direction.to_owned(), - ExtrusionMode::New, - ); - let extrusion_id = workbench.add_extrusion(extrusion_name, extrusion); - Ok(format!("\"id\": \"{}\"", extrusion_id)) - } Message::UpdateExtrusion { workbench_id, extrusion_name: _extrusion_name, extrusion_id, sketch_id, face_ids, - length, - offset, - direction, + length:_, + offset:_, + direction:_, } => { let workbench = project.get_workbench_by_id_mut(*workbench_id)?; - let extrusion = Extrusion::new( - sketch_id.to_owned(), - face_ids.to_owned(), - *length, - *offset, - direction.to_owned(), - ExtrusionMode::New, - ); - let as_step_data = StepData::Extrusion { extrusion }; - workbench.update_step_data(extrusion_id, as_step_data); - Ok(format!("\"id\": \"{}\"", extrusion_id)) + let sketch = workbench.get_sketch_by_id(*sketch_id)?; + let _faces = sketch + .borrow() + .faces() + .iter() + .enumerate() + .filter_map(|(k, v)| { + if face_ids.contains(&(k as u64)) { + Some(v.clone()) + } else { + None + } + }).collect::>(); + let _extrusion = workbench.solids.get(extrusion_id).ok_or(anyhow::anyhow!("Could not find extrusion ID!"))?.borrow_mut(); + + todo!("Update Extrusion") + // let new_extrusion = extrusion::Extrusion::new(faces, sketch, *length, *offset, *direction, extrusion.mode).to_feature().as_solid_like(); + + // let as_step_data = StepData::Extrusion { extrusion }; + // workbench.update_step_data(extrusion_id, as_step_data); + // Ok(format!("\"id\": \"{}\"", extrusion_id)) } Message::UpdateExtrusionLength { - workbench_id, - extrusion_name, - length, + workbench_id:_, + extrusion_name:_, + length:_, } => { - let workbench = project.get_workbench_by_id_mut(*workbench_id)?; - let step = workbench.get_step_mut(&extrusion_name)?; + // let workbench = project.get_workbench_by_id_mut(*workbench_id)?; + // let step = workbench.get_step_mut(&extrusion_name)?; - if let StepData::Extrusion { extrusion } = &mut step.data { - extrusion.length = *length; - return Ok(format!("\"length\": {}", length)); - } + // if let StepData::Extrusion { extrusion } = &mut step.data { + // extrusion.length = *length; + // return Ok(format!("\"length\": {}", length)); + // } - Err(CADmiumError::IncorrectStepDataType("Extrusion".to_owned()).into()) + // Err(CADmiumError::IncorrectStepDataType("Extrusion".to_owned()).into()) + todo!("Update Extrusion Length") } } } diff --git a/packages/cadmium/src/project.rs b/packages/cadmium/src/project.rs index 67dd21ec..462d36e2 100644 --- a/packages/cadmium/src/project.rs +++ b/packages/cadmium/src/project.rs @@ -2,14 +2,9 @@ use serde::{Deserialize, Serialize}; use tsify::Tsify; use wasm_bindgen::prelude::*; -use crate::archetypes::*; use crate::error::CADmiumError; use crate::realization::Realization; -use crate::sketch::constraints::Constraint; -use crate::sketch::{Face, Point2, Sketch}; -use crate::step::StepData; use crate::workbench::Workbench; -use std::collections::HashMap; #[derive(Tsify, Debug, Serialize, Deserialize)] #[tsify(into_wasm_abi, from_wasm_abi)] @@ -52,18 +47,18 @@ impl Project { } } - pub fn compute_constraint_errors(&mut self) { - for workbench in self.workbenches.iter_mut() { - for step in workbench.history.iter_mut() { - match &mut step.data { - StepData::Sketch { sketch, .. } => { - sketch.compute_constraint_errors(); - } - _ => {} - } - } - } - } + // pub fn compute_constraint_errors(&mut self) { + // for workbench in self.workbenches.iter_mut() { + // for step in workbench.history.iter_mut() { + // match &mut step.data { + // StepData::Sketch { sketch, .. } => { + // sketch.compute_constraint_errors(); + // } + // _ => {} + // } + // } + // } + // } pub fn get_workbench_mut(&mut self, name: &str) -> Result<&mut Workbench, CADmiumError> { self.workbenches @@ -78,10 +73,9 @@ impl Project { .ok_or(CADmiumError::WorkbenchIDNotFound(id)) } - pub fn get_realization(&self, workbench_id: u64, max_steps: u64) -> Realization { - let workbench = &self.workbenches[workbench_id as usize]; - let realization = workbench.realize(max_steps); - realization + pub fn get_realization(&mut self, workbench_id: u64, max_steps: u64) -> Result { + let workbench = &mut self.workbenches.get_mut(workbench_id as usize).unwrap(); + workbench.realize(max_steps) } } @@ -91,181 +85,53 @@ pub struct Assembly { name: String, } -#[derive(Tsify, Debug, Clone, Serialize, Deserialize)] -#[tsify(into_wasm_abi, from_wasm_abi)] -pub struct RealSketch { - pub plane_id: String, - pub plane_name: String, - pub points: HashMap, - pub points_2d: HashMap, - pub highest_point_id: u64, - pub line_segments: HashMap, - pub highest_line_segment_id: u64, - pub circles: HashMap, - pub highest_circle_id: u64, - pub arcs: HashMap, - pub highest_arc_id: u64, - pub constraints: HashMap, - pub highest_constraint_id: u64, - pub faces: Vec, -} - -impl RealSketch { - pub fn new(plane_name: &str, plane_id: &str, plane: &RealPlane, sketch: &Sketch) -> Self { - // The key difference between Sketch and RealSketch is that Sketch lives - // in 2D and RealSketch lives in 3D. So we need to convert the points - - let mut real_sketch = RealSketch { - plane_name: plane_name.to_owned(), - plane_id: plane_id.to_owned(), - points_2d: HashMap::new(), - points: HashMap::new(), - highest_point_id: 0, - line_segments: HashMap::new(), - highest_line_segment_id: 0, - circles: HashMap::new(), - highest_circle_id: 0, - arcs: HashMap::new(), - highest_arc_id: 0, - constraints: HashMap::new(), - highest_constraint_id: 0, - faces: vec![], - }; - - let o = plane.plane.origin.clone(); - let x = plane.plane.primary.clone(); - let y = plane.plane.secondary.clone(); - - for (point_id, point) in sketch.points.iter() { - let pt3 = o.plus(x.times(point.x)).plus(y.times(point.y)); - let mut real_point = Point3::new(pt3.x, pt3.y, pt3.z); - if point.hidden { - real_point.hidden = true; - } - real_sketch.points.insert(*point_id, real_point); - - let pt2 = point.clone(); - real_sketch.points_2d.insert(*point_id, pt2); - } - real_sketch.highest_point_id = sketch.highest_point_id; - - for (line_id, line) in sketch.line_segments.iter() { - let real_line = Line3 { - start: line.start, - end: line.end, - }; - real_sketch.line_segments.insert(*line_id, real_line); - } - - for (circle_id, circle) in sketch.circles.iter() { - let real_circle = Circle3 { - center: circle.center, - radius: circle.radius, - top: circle.top, - }; - real_sketch.circles.insert(*circle_id, real_circle); - } - - // let mut arc3_lookup: HashMap<(u64, u64, u64), Arc3> = HashMap::new(); - for (arc_id, arc) in sketch.arcs.iter() { - // println!("\nConverting arc to points"); - // let as_points = sketch.arc_to_points(arc); - // println!("How many points? {}", as_points.len()); - // let transit = as_points[as_points.len() / 2].clone(); - - // let transit_3d = o.plus(x.times(transit.x)).plus(y.times(transit.y)); - // let mut real_point = Point3::new(transit_3d.x, transit_3d.y, transit_3d.z); - // real_point.hidden = true; - - // let point_id = real_sketch.highest_point_id + 1; - // real_sketch.points.insert(point_id, real_point); - // real_sketch.points_2d.insert(point_id, transit); - // real_sketch.highest_point_id += 1; - - let real_arc = Arc3 { - center: arc.center, - start: arc.start, - end: arc.end, - // transit: point_id, - clockwise: arc.clockwise, - }; - real_sketch.arcs.insert(*arc_id, real_arc); - // arc3_lookup.insert((arc.start, arc.end, arc.center), real_arc); - } - - for (constraint_id, constraint) in sketch.constraints.iter() { - let real_constraint = constraint.clone(); - real_sketch - .constraints - .insert(*constraint_id, real_constraint); - } - - let (faces, _unused_segments) = sketch.find_faces(); - real_sketch.faces = faces; - - real_sketch - } -} - -#[derive(Tsify, Debug, Clone, Serialize, Deserialize)] -#[tsify(into_wasm_abi, from_wasm_abi)] -pub struct RealPlane { - pub plane: Plane, - pub name: String, - pub width: f64, - pub height: f64, -} - #[cfg(test)] pub mod tests { - use truck_polymesh::obj; - use crate::extrusion::Direction; - use crate::extrusion::Extrusion; - use crate::extrusion::ExtrusionMode; + use crate::archetypes::PlaneDescription; + use crate::archetypes::Point2; + use crate::solid::extrusion::Direction; + use crate::solid::extrusion::Mode; use crate::message::Message; - use truck_meshalgo::filters::*; - use truck_meshalgo::tessellation::*; use super::*; pub fn create_test_project() -> Project { let mut p = Project::new("Test Project"); - let wb = p.workbenches.get_mut(0).unwrap(); - wb.add_sketch_to_plane("Sketch 1", "Plane-0"); - let s = wb.get_sketch_mut("Sketch 1").unwrap(); - let ll = s.add_point(0.0, 0.0); - let lr = s.add_point(40.0, 0.0); - let ul = s.add_point(0.0, 40.0); - let ur = s.add_point(40.0, 40.0); - s.add_segment(ll, lr); - s.add_segment(lr, ur); - s.add_segment(ur, ul); - s.add_segment(ul, ll); - - let extrusion = Extrusion::new( - "Sketch-0".to_owned(), + let plane_desc = PlaneDescription::PlaneId(0); + let sid = p.add_workbench_sketch("Sketch 1".to_string(), 0, plane_desc).unwrap(); + + let ll = p.add_sketch_point("bottom left".to_string(), 0, sid, Point2 { x: 0.0, y: 0.0, hidden: false }).unwrap(); + let lr = p.add_sketch_point("bottom right".to_string(), 0, sid, Point2 { x: 40.0, y: 0.0, hidden: false }).unwrap(); + let ul = p.add_sketch_point("top left".to_string(), 0, sid, Point2 { x: 0.0, y: 40.0, hidden: false }).unwrap(); + let ur = p.add_sketch_point("top right".to_string(), 0, sid, Point2 { x: 40.0, y: 40.0, hidden: false }).unwrap(); + p.add_sketch_line("bottom".to_string(), 0, sid, ll, lr).unwrap(); + p.add_sketch_line("right".to_string(), 0, sid, lr, ur).unwrap(); + p.add_sketch_line("up".to_string(), 0, sid, ur, ul).unwrap(); + p.add_sketch_line("left".to_string(), 0, sid, ul, ll).unwrap(); + + p.add_solid_extrusion( + "Extrusion 1".to_string(), + 0, vec![0], + 0, 25.0, 0.0, + Mode::New, Direction::Normal, - ExtrusionMode::New, - ); - wb.add_extrusion("Ext1", extrusion); + ).unwrap(); p } #[test] fn one_extrusion() { - let p = create_test_project(); + let mut p = create_test_project(); - let realization = p.get_realization(0, 1000); + let realization = p.get_realization(0, 1000).unwrap(); let solids = realization.solids; - let solid = &solids["Ext1:0"]; - - println!("{:?}", solid); + assert_eq!(solids.len(), 1); } // #[test] @@ -326,124 +192,124 @@ pub mod tests { let file_contents = std::fs::read_to_string("src/test_inputs/circle_crashing_2.cadmium").unwrap(); - let p2 = Project::from_json(&file_contents); + let mut p2 = Project::from_json(&file_contents); let realization = p2.get_realization(0, 1000); println!("{:?}", realization); } // #[test] - fn bruno() { - let mut p = create_test_project(); - let wb = p.workbenches.get_mut(0).unwrap(); - - let s2_id = wb.add_sketch_to_solid_face("Sketch-2", "Ext1:0", Vector3::new(0.0, 0.0, 1.0)); - let s2 = wb.get_sketch_mut("Sketch-2").unwrap(); - - // smaller - let ll = s2.add_point(12.0, 12.0); - let lr = s2.add_point(32.0, 12.0); - let ul = s2.add_point(12.0, 32.0); - let ur = s2.add_point(32.0, 32.0); - // bigger! - // let ll = s2.add_point(-10.0, -10.0); - // let lr = s2.add_point(50.0, -10.0); - // let ul = s2.add_point(-10.0, 50.0); - // let ur = s2.add_point(50.0, 50.0); - s2.add_segment(ll, lr); - s2.add_segment(lr, ur); - s2.add_segment(ur, ul); - s2.add_segment(ul, ll); - - // println!("S2: {:?}", s2); - - let extrusion2 = Extrusion::new( - s2_id.to_owned(), - vec![0], - 25.0, - 0.0, - Direction::Normal, - ExtrusionMode::Add(vec!["Ext1:0".to_string()]), - ); - wb.add_extrusion("Ext2", extrusion2); + // fn bruno() { + // let mut p = create_test_project(); + // let wb = p.workbenches.get_mut(0).unwrap(); + + // let s2_id = wb.add_sketch_to_solid_face("Sketch-2", "Ext1:0", Vector3::new(0.0, 0.0, 1.0)); + // let s2 = wb.get_sketch_mut("Sketch-2").unwrap(); + + // // smaller + // let ll = s2.add_point(12.0, 12.0); + // let lr = s2.add_point(32.0, 12.0); + // let ul = s2.add_point(12.0, 32.0); + // let ur = s2.add_point(32.0, 32.0); + // // bigger! + // // let ll = s2.add_point(-10.0, -10.0); + // // let lr = s2.add_point(50.0, -10.0); + // // let ul = s2.add_point(-10.0, 50.0); + // // let ur = s2.add_point(50.0, 50.0); + // s2.add_segment(ll, lr); + // s2.add_segment(lr, ur); + // s2.add_segment(ur, ul); + // s2.add_segment(ul, ll); + + // // println!("S2: {:?}", s2); + + // let extrusion2 = Extrusion::new( + // s2_id.to_owned(), + // vec![0], + // 25.0, + // 0.0, + // Direction::Normal, + // ExtrusionMode::Add(vec!["Ext1:0".to_string()]), + // ); + // wb.add_extrusion("Ext2", extrusion2); + + // wb.add_sketch_to_plane("Sketch 3", "Plane-1"); + // let s3 = wb.get_sketch_mut("Sketch 3").unwrap(); + // let center = s3.add_point(20.0, 15.0); + // s3.add_circle(center, 5.0); + + // let extrusion3 = Extrusion::new( + // "Sketch-2".to_owned(), + // vec![0], + // 50.0, + // 0.0, + // Direction::NegativeNormal, + // ExtrusionMode::Remove(vec!["Ext1:0".to_string()]), + // ); + // wb.add_extrusion("Ext3", extrusion3); - wb.add_sketch_to_plane("Sketch 3", "Plane-1"); - let s3 = wb.get_sketch_mut("Sketch 3").unwrap(); - let center = s3.add_point(20.0, 15.0); - s3.add_circle(center, 5.0); - - let extrusion3 = Extrusion::new( - "Sketch-2".to_owned(), - vec![0], - 50.0, - 0.0, - Direction::NegativeNormal, - ExtrusionMode::Remove(vec!["Ext1:0".to_string()]), - ); - wb.add_extrusion("Ext3", extrusion3); - - let realization = p.get_realization(0, 1000); - let solids = realization.solids; + // let realization = p.get_realization(0, 1000); + // let solids = realization.solids; - let num_solids = solids.len(); - println!("Num Solids: {:?}", num_solids); - assert!(num_solids == 1); + // let num_solids = solids.len(); + // println!("Num Solids: {:?}", num_solids); + // assert!(num_solids == 1); - let final_solid = &solids["Ext1:0"]; - println!("Final solid: {:?}", final_solid.truck_solid); - let mut mesh = final_solid.truck_solid.triangulation(0.02).to_polygon(); - mesh.put_together_same_attrs(0.1); - let file = std::fs::File::create("pkg/bruno.obj").unwrap(); - obj::write(&mesh, file).unwrap(); + // let final_solid = &solids["Ext1:0"]; + // println!("Final solid: {:?}", final_solid.truck_solid); + // let mut mesh = final_solid.truck_solid.triangulation(0.02).to_polygon(); + // mesh.put_together_same_attrs(); + // let file = std::fs::File::create("pkg/bruno.obj").unwrap(); + // obj::write(&mesh, file).unwrap(); - let file = std::fs::File::create("pkg/bruno.json").unwrap(); - serde_json::to_writer(file, &p).unwrap(); - } + // let file = std::fs::File::create("pkg/bruno.json").unwrap(); + // serde_json::to_writer(file, &p).unwrap(); + // } // #[test] - fn secondary_extrusion_with_merge() { - let mut p = create_test_project(); - let wb = p.workbenches.get_mut(0).unwrap(); - - let s2_id = wb.add_sketch_to_solid_face("Sketch-2", "Ext1:0", Vector3::new(0.0, 0.0, 1.0)); - let s2 = wb.get_sketch_mut("Sketch-2").unwrap(); - - // smaller - let ll = s2.add_point(12.0, 0.0); - let lr = s2.add_point(32.0, 0.0); - let ul = s2.add_point(12.0, 32.0); - let ur = s2.add_point(32.0, 32.0); - s2.add_segment(ll, lr); - s2.add_segment(lr, ur); - s2.add_segment(ur, ul); - s2.add_segment(ul, ll); - - // println!("S2: {:?}", s2); - - let extrusion2 = Extrusion::new( - s2_id.to_owned(), - vec![0], - 25.0, - 0.0, - Direction::Normal, - ExtrusionMode::Add(vec!["Ext1:0".to_string()]), - ); - wb.add_extrusion("Ext2", extrusion2); + // fn secondary_extrusion_with_merge() { + // let mut p = create_test_project(); + // let wb = p.workbenches.get_mut(0).unwrap(); + + // let s2_id = wb.add_sketch_to_solid_face("Sketch-2", "Ext1:0", Vector3::new(0.0, 0.0, 1.0)); + // let s2 = wb.get_sketch_mut("Sketch-2").unwrap(); + + // // smaller + // let ll = s2.add_point(12.0, 0.0); + // let lr = s2.add_point(32.0, 0.0); + // let ul = s2.add_point(12.0, 32.0); + // let ur = s2.add_point(32.0, 32.0); + // s2.add_segment(ll, lr); + // s2.add_segment(lr, ur); + // s2.add_segment(ur, ul); + // s2.add_segment(ul, ll); + + // // println!("S2: {:?}", s2); + + // let extrusion2 = Extrusion::new( + // s2_id.to_owned(), + // vec![0], + // 25.0, + // 0.0, + // Direction::Normal, + // ExtrusionMode::Add(vec!["Ext1:0".to_string()]), + // ); + // wb.add_extrusion("Ext2", extrusion2); - let realization = p.get_realization(0, 1000); - let solids = realization.solids; + // let realization = p.get_realization(0, 1000); + // let solids = realization.solids; - let num_solids = solids.len(); - println!("Num Solids: {:?}", num_solids); - assert!(num_solids == 1); + // let num_solids = solids.len(); + // println!("Num Solids: {:?}", num_solids); + // assert!(num_solids == 1); - let final_solid = &solids["Ext1:0"]; - let mut mesh = final_solid.truck_solid.triangulation(0.02).to_polygon(); - mesh.put_together_same_attrs(0.1); - let file = std::fs::File::create("secondary_extrusion.obj").unwrap(); - obj::write(&mesh, file).unwrap(); + // let final_solid = &solids["Ext1:0"]; + // let mut mesh = final_solid.truck_solid.triangulation(0.02).to_polygon(); + // mesh.put_together_same_attrs(); + // let file = std::fs::File::create("secondary_extrusion.obj").unwrap(); + // obj::write(&mesh, file).unwrap(); - let file = std::fs::File::create("secondary_extrusion.json").unwrap(); - serde_json::to_writer(file, &p).unwrap(); - } + // let file = std::fs::File::create("secondary_extrusion.json").unwrap(); + // serde_json::to_writer(file, &p).unwrap(); + // } } diff --git a/packages/cadmium/src/realization.rs b/packages/cadmium/src/realization.rs index e0cd1e49..5756e30a 100644 --- a/packages/cadmium/src/realization.rs +++ b/packages/cadmium/src/realization.rs @@ -2,51 +2,52 @@ use serde::{Deserialize, Serialize}; use tsify::Tsify; use wasm_bindgen::prelude::*; -use crate::archetypes::Point3; -use crate::project::{RealPlane, RealSketch}; +use crate::solid::point::Point3; +use crate::isketch::{IPlane, ISketch}; use crate::solid::Solid; -use std::collections::HashMap; +use crate::IDType; +use std::collections::BTreeMap; #[derive(Tsify, Debug, Serialize, Deserialize)] #[tsify(into_wasm_abi, from_wasm_abi)] pub struct Realization { // a Realization is what you get if you apply the steps in a Workbench's // history and build a bunch of geometry - pub planes: HashMap, - pub points: HashMap, - pub sketches: HashMap, - pub solids: HashMap, + pub planes: BTreeMap, + pub points: BTreeMap, + pub sketches: BTreeMap, + pub solids: BTreeMap, } impl Realization { pub fn new() -> Self { Realization { - planes: HashMap::new(), - points: HashMap::new(), - sketches: HashMap::new(), - solids: HashMap::new(), + planes: BTreeMap::new(), + points: BTreeMap::new(), + sketches: BTreeMap::new(), + solids: BTreeMap::new(), } } - pub fn solid_to_obj(&self, solid_name: &str, tolerance: f64) -> String { - let solid = &self.solids[solid_name]; + pub fn solid_to_obj(&self, solid_name: IDType, tolerance: f64) -> String { + let solid = &self.solids[&solid_name]; let obj_text = solid.to_obj_string(tolerance); obj_text } - pub fn save_solid_as_obj_file(&self, solid_name: &str, filename: &str, tolerance: f64) { - let solid = &self.solids[solid_name]; + pub fn save_solid_as_obj_file(&self, solid_name: IDType, filename: &str, tolerance: f64) { + let solid = &self.solids[&solid_name]; solid.save_as_obj(filename, tolerance); } - pub fn solid_to_step(&self, solid_name: &str) -> String { - let solid = &self.solids[solid_name]; + pub fn solid_to_step(&self, solid_name: IDType) -> String { + let solid = &self.solids[&solid_name]; let step_text = solid.to_step_string(); step_text } - pub fn save_solid_as_step_file(&self, solid_name: &str, filename: &str) { - let solid = &self.solids[solid_name]; + pub fn save_solid_as_step_file(&self, solid_name: IDType, filename: &str) { + let solid = &self.solids[&solid_name]; solid.save_as_step(filename) } } diff --git a/packages/cadmium/src/sketch/constraints.rs b/packages/cadmium/src/sketch/constraints.rs deleted file mode 100644 index f12382c9..00000000 --- a/packages/cadmium/src/sketch/constraints.rs +++ /dev/null @@ -1,294 +0,0 @@ -use serde::{Deserialize, Serialize}; -use tsify::Tsify; - -use crate::sketch::{Arc2, Circle2, IncrementingMap, Line2, Point2, Sketch}; -use std::collections::HashMap; -use std::f64::consts::{PI, TAU}; - -#[derive(Tsify, Debug, Clone, Serialize, Deserialize)] -#[serde(tag = "type")] -pub enum Constraint { - SegmentLength { - segment_id: u64, - length: f64, - normal_offset: f64, - parallel_offset: f64, - kp: f64, // kp is the proportional gain, the spring constant - kd: f64, // kd is the derivative gain, the damping constant - error: f64, - }, - SegmentAngle { - segment_id: u64, - angle: f64, - x_offset: f64, - y_offset: f64, - kp: f64, - kd: f64, - error: f64, - }, - CircleDiameter { - circle_id: u64, - diameter: f64, - angle_offset: f64, - r_offset: f64, - kp: f64, - kd: f64, - error: f64, - }, - SegmentsEqual { - segment_a_id: u64, - segment_b_id: u64, - kp: f64, - kd: f64, - error: f64, - }, -} - -impl Sketch { - pub fn add_segment_length_constraint(&mut self, segment_id: u64, length: f64) -> u64 { - let mut constraint = Constraint::SegmentLength { - segment_id, - length, - normal_offset: 0.15, - parallel_offset: 0.0, - kp: 2.0, - kd: 0.3, - error: 0.0, - }; - - let id = self.highest_constraint_id + 1; - self.constraints.insert(id, constraint); - self.highest_constraint_id += 1; - - let err = self.constraint_error(id); - let c = self.constraints.get_mut(&id).unwrap(); - if let Constraint::SegmentLength { error, .. } = c { - *error = err; - } - - id - } - - pub fn add_segment_vertical_constraint(&mut self, segment_id: u64) -> u64 { - let current_angle = self.segment_angle(segment_id); - if current_angle >= 0.0 { - // it roughly points up - self.add_segment_angle_constraint(segment_id, PI / 2.0) - } else { - self.add_segment_angle_constraint(segment_id, -PI / 2.0) - } - } - - pub fn add_segment_horizontal_constraint(&mut self, segment_id: u64) -> u64 { - let current_angle = self.segment_angle(segment_id); - if current_angle.abs() <= PI / 2.0 { - // it roughly points right - self.add_segment_angle_constraint(segment_id, 0.0) - } else { - self.add_segment_angle_constraint(segment_id, PI) - } - } - - pub fn add_segment_angle_constraint(&mut self, segment_id: u64, angle: f64) -> u64 { - let constraint = Constraint::SegmentAngle { - segment_id, - angle, - x_offset: 0.0, - y_offset: 0.0, - kp: 2.0, - kd: 0.3, - error: 0.0, - }; - - let id = self.highest_constraint_id + 1; - self.constraints.insert(id, constraint); - self.highest_constraint_id += 1; - - let err = self.constraint_error(id); - let c = self.constraints.get_mut(&id).unwrap(); - if let Constraint::SegmentAngle { error, .. } = c { - *error = err; - } - - id - } - - pub fn add_circle_diameter_constraint(&mut self, circle_id: u64, diameter: f64) -> u64 { - let constraint = Constraint::CircleDiameter { - circle_id, - diameter, - angle_offset: 3.0 * PI / 4.0, - r_offset: 0.20, - kp: 2.0, - kd: 0.3, - error: 0.0, - }; - - let id = self.highest_constraint_id + 1; - self.constraints.insert(id, constraint); - self.highest_constraint_id += 1; - - let err = self.constraint_error(id); - let c = self.constraints.get_mut(&id).unwrap(); - if let Constraint::CircleDiameter { error, .. } = c { - *error = err; - } - - id - } - - pub fn add_segments_equal_constraint(&mut self, segment_a_id: u64, segment_b_id: u64) -> u64 { - let constraint = Constraint::SegmentsEqual { - segment_a_id, - segment_b_id, - kp: 2.0, - kd: 0.3, - error: 0.0, - }; - - let id = self.highest_constraint_id + 1; - self.constraints.insert(id, constraint); - self.highest_constraint_id += 1; - - let err = self.constraint_error(id); - let c = self.constraints.get_mut(&id).unwrap(); - if let Constraint::SegmentsEqual { error, .. } = c { - *error = err; - } - - id - } - - pub fn compute_constraint_errors(&mut self) { - let key_to_errors = self - .constraints - .iter() - .map(|(k, _v)| (*k, self.constraint_error(*k))) - .collect::>(); - for (constraint_id, err) in key_to_errors.iter() { - let constraint = self.constraints.get_mut(constraint_id).unwrap(); - match constraint { - Constraint::SegmentLength { error, .. } => { - *error = *err; - } - Constraint::CircleDiameter { error, .. } => { - *error = *err; - } - Constraint::SegmentAngle { error, .. } => { - *error = *err; - } - Constraint::SegmentsEqual { error, .. } => { - *error = *err; - } - } - } - } - - pub fn constraint_error(&self, constraint_id: u64) -> f64 { - let constraint = self.constraints.get(&constraint_id).unwrap(); - let value = self.constraint_value(constraint_id); - match constraint { - Constraint::SegmentLength { length, .. } => value - length, - Constraint::CircleDiameter { diameter, .. } => value - diameter, - Constraint::SegmentAngle { angle, .. } => value - angle, - Constraint::SegmentsEqual { .. } => value, - } - } - - pub fn constraint_value(&self, constraint_id: u64) -> f64 { - let constraint = self.constraints.get(&constraint_id).unwrap(); - match constraint { - Constraint::SegmentLength { - segment_id, length, .. - } => { - let segment = self.line_segments.get(&segment_id).unwrap(); - let start = self.points.get(&segment.start).unwrap(); - let end = self.points.get(&segment.end).unwrap(); - start.distance_to(end) - } - - Constraint::CircleDiameter { - circle_id, - diameter, - .. - } => { - let circle = self.circles.get(&circle_id).unwrap(); - circle.radius * 2.0 - } - - Constraint::SegmentAngle { - segment_id, angle, .. - } => { - let segment = self.line_segments.get(&segment_id).unwrap(); - let start = self.points.get(&segment.start).unwrap(); - let end = self.points.get(&segment.end).unwrap(); - start.angle_to(end) - } - - Constraint::SegmentsEqual { - segment_a_id, - segment_b_id, - .. - } => { - let a = self.segment_length(*segment_a_id); - let b = self.segment_length(*segment_b_id); - a - b - } - } - } - - pub fn constraint_is_satisfied(&self, constraint_id: u64) -> bool { - let tolerance = 1e-10; - let constraint = self.constraints.get(&constraint_id).unwrap(); - let error = self.constraint_error(constraint_id); - error.abs() < tolerance - } - - pub fn all_constraints_are_satisfied(&self) -> bool { - for (constraint_id, _constraint) in self.constraints.iter() { - if !self.constraint_is_satisfied(*constraint_id) { - return false; - } - } - true - } -} - -#[cfg(test)] -mod tests { - use crate::project::Project; - - use super::*; - - #[test] - fn segment_length_constraint() { - let mut sketch = Sketch::new(); - - let a = sketch.add_point(0.0, 0.0); - let b = sketch.add_point(1.0, 0.0); - - let segment_id = sketch.add_segment(a, b); - - let constraint_id = sketch.add_segment_length_constraint(segment_id, 2.0); - - assert!(sketch.solve(1000)); - println!("Segment length: {}", sketch.segment_length(segment_id)); - assert!(sketch.constraint_is_satisfied(constraint_id)); - } - - #[test] - fn segment_angle_constraint() { - let mut sketch = Sketch::new(); - - let a = sketch.add_point(0.0, 0.0); - let b = sketch.add_point(1.0, 0.0); - - let segment_id = sketch.add_segment(a, b); - - let constraint_id = sketch.add_segment_angle_constraint(segment_id, PI / 4.0); - - sketch.solve(10000); - - assert!(sketch.constraint_is_satisfied(constraint_id)); - } -} diff --git a/packages/cadmium/src/sketch/intersections.rs b/packages/cadmium/src/sketch/intersections.rs deleted file mode 100644 index 7fe4b802..00000000 --- a/packages/cadmium/src/sketch/intersections.rs +++ /dev/null @@ -1,1985 +0,0 @@ -use std::{collections::VecDeque, f32::EPSILON}; - -use crate::sketch::{Arc2, Circle2, IncrementingMap, Line2, Point2, Sketch}; -use itertools::Itertools; -use std::f64::consts::{PI, TAU}; - -use serde::{Deserialize, Serialize}; -use tsify::Tsify; - - -#[derive(Tsify, Debug, Clone, Serialize, Deserialize, PartialEq)] -#[tsify(into_wasm_abi, from_wasm_abi)] -pub enum Shape { - Circle(Circle2), - Arc(Arc2), - Line(Line2), -} - -impl Shape { - pub fn split_at_point_id(&self, new_point_id: u64) -> (Shape, Shape) { - match self { - Shape::Line(line) => { - let new_line_1 = Line2 { - start: line.start, - end: new_point_id, - }; - let new_line_2 = Line2 { - start: new_point_id, - end: line.end, - }; - (Shape::Line(new_line_1), Shape::Line(new_line_2)) - } - Shape::Circle(circle) => todo!(), - Shape::Arc(_) => todo!(), - } - } -} - -#[derive(Tsify, Debug, Clone, Serialize, Deserialize, PartialEq)] -#[tsify(into_wasm_abi, from_wasm_abi)] -pub struct Collision { - point: Point2, - shape_a: u64, - shape_b: u64, - shape_a_degeneracy: Degeneracy, - shape_b_degeneracy: Degeneracy, -} - -impl Collision { - pub fn new(point: Point2, shape_a: u64, shape_b: u64) -> Self { - Collision { - point, - shape_a, - shape_b, - shape_a_degeneracy: Degeneracy::None, - shape_b_degeneracy: Degeneracy::None, - } - } - pub fn degenerate(shape_a: u64, shape_b: u64) -> Self { - Collision { - point: Point2::new(f64::NAN, f64::NAN), - shape_a, - shape_b, - shape_a_degeneracy: Degeneracy::Complete, - shape_b_degeneracy: Degeneracy::Complete, - } - } -} - -#[derive(Tsify, Debug, Clone, Serialize, Deserialize, PartialEq)] -#[tsify(into_wasm_abi, from_wasm_abi)] -pub enum Degeneracy { - None, - IsStart, - IsEnd, - Complete, -} - -use Degeneracy::*; - -#[derive(Tsify, Debug, Clone, Serialize, Deserialize, PartialEq)] -#[tsify(into_wasm_abi, from_wasm_abi)] -pub enum Intersection { - None, - OnePoint(Point2, bool), - TwoPoints(Point2, bool, Point2, bool), - Line(Point2, Point2), - Arc(Arc2), - Circle(Circle2), -} - -impl Sketch { - pub fn identify_collisions( - &self, - temp_sketch: &Sketch, - all_shapes: &IncrementingMap, - shape_a_id: u64, - shape_b_id: u64, - debug: bool, - ) -> Vec { - let shape_a = all_shapes.items.get(&(shape_a_id)).unwrap(); - let shape_b = all_shapes.items.get(&(shape_b_id)).unwrap(); - - if (debug) { - println!("Shape A ({}): {:?}", shape_a_id, shape_a); - println!("Shape B ({}): {:?}", shape_b_id, shape_b); - } - - match (shape_a, shape_b) { - (Shape::Circle(circle_a), Shape::Circle(circle_b)) => { - temp_sketch.circle_circle_collisions(circle_a, shape_a_id, circle_b, shape_b_id) - } - (Shape::Circle(circle_a), Shape::Arc(arc_b)) => { - temp_sketch.circle_arc_collisions(circle_a, shape_a_id, arc_b, shape_b_id) - } - (Shape::Circle(circle_a), Shape::Line(line_b)) => { - temp_sketch.line_circle_collisions(line_b, shape_b_id, circle_a, shape_a_id) - } - (Shape::Arc(arc_a), Shape::Circle(circle_b)) => { - temp_sketch.circle_arc_collisions(circle_b, shape_b_id, arc_a, shape_a_id) - } - (Shape::Arc(arc_a), Shape::Arc(arc_b)) => { - temp_sketch.arc_arc_collisions(arc_a, shape_a_id, arc_b, shape_b_id) - } - (Shape::Arc(arc_a), Shape::Line(line_b)) => { - temp_sketch.line_arc_collisions(line_b, shape_b_id, arc_a, shape_a_id) - } - (Shape::Line(line_a), Shape::Circle(circle_b)) => { - temp_sketch.line_circle_collisions(line_a, shape_a_id, circle_b, shape_b_id) - } - (Shape::Line(line_a), Shape::Arc(arc_b)) => { - temp_sketch.line_arc_collisions(line_a, shape_a_id, arc_b, shape_b_id) - } - (Shape::Line(line_a), Shape::Line(line_b)) => { - temp_sketch.line_line_collisions(line_a, shape_a_id, line_b, shape_b_id, false) - } - } - } - - pub fn process_collision( - &self, - temp_sketch: &mut Sketch, - all_shapes: &mut IncrementingMap, - possible_shape_collisions: &mut Vec, - new_shapes: &mut Vec, - recently_deleted: &mut Vec, - collision: Collision, - debug: bool, - ) { - if (debug) { - println!("Processing collision: {:?}", collision); - } - - let shape_a_id = collision.shape_a; - let shape_b_id = collision.shape_b; - let point = collision.point; - - let shape_a = all_shapes.get_item(shape_a_id).unwrap().clone(); - let shape_b = all_shapes.get_item(shape_b_id).unwrap().clone(); - - match (shape_a, shape_b) { - (Shape::Circle(circle_a), Shape::Circle(circle_b)) => { - let new_point_id = temp_sketch.add_point(point.x, point.y); - - let arc_a = temp_sketch.split_circle_at_point(&circle_a, &new_point_id, &point); - let arc_b = temp_sketch.split_circle_at_point(&circle_b, &new_point_id, &point); - - // this is a unique case. We're making substitutions here, not deleting or creating - all_shapes.items.insert(shape_a_id, Shape::Arc(arc_a)); - all_shapes.items.insert(shape_b_id, Shape::Arc(arc_b)); - - if (debug) { - println!("Replaced two circles with two arcs, keeping the same IDs"); - } - } - (Shape::Circle(circle_a), Shape::Arc(arc_b)) => { - let new_point_id = temp_sketch.add_point(point.x, point.y); - - // the circle can be converted to an arc - let arc_a = temp_sketch.split_circle_at_point(&circle_a, &new_point_id, &point); - // the arc must be split into two arcs - let (arc_b1, arc_b2) = - temp_sketch.split_arc_at_point(&arc_b, &new_point_id, &point); - - // the circle -> arc amounts to a substitution not a deletion + creation - all_shapes.items.insert(shape_a_id, Shape::Arc(arc_a)); - - // but the arc -> 2 arcs is a deletion + creation - let new_arc_b1_id = all_shapes.add_item(Shape::Arc(arc_b1)); - new_shapes.push(new_arc_b1_id); - let new_arc_b2_id = all_shapes.add_item(Shape::Arc(arc_b2)); - new_shapes.push(new_arc_b2_id); - recently_deleted.push(all_shapes.remove_item(shape_b_id)); - - if (debug) { - println!("Replaced a circle with an arc ({})", shape_a_id); - println!( - "AND replaced an arc ({}) with 2 arcs ({}), ({})", - shape_b_id, new_arc_b1_id, new_arc_b2_id - ); - } - } - (Shape::Circle(_), Shape::Line(_)) => todo!(), - (Shape::Arc(_), Shape::Circle(_)) => todo!(), - (Shape::Arc(arc_a), Shape::Arc(arc_b)) => { - let new_point_id = temp_sketch.add_point(point.x, point.y); - - let (arc_a1, arc_a2) = - temp_sketch.split_arc_at_point(&arc_a, &new_point_id, &point); - let (arc_b1, arc_b2) = - temp_sketch.split_arc_at_point(&arc_b, &new_point_id, &point); - - new_shapes.push(all_shapes.add_item(Shape::Arc(arc_a1))); - new_shapes.push(all_shapes.add_item(Shape::Arc(arc_a2))); - new_shapes.push(all_shapes.add_item(Shape::Arc(arc_b1))); - new_shapes.push(all_shapes.add_item(Shape::Arc(arc_b2))); - - recently_deleted.push(all_shapes.remove_item(shape_a_id)); - recently_deleted.push(all_shapes.remove_item(shape_b_id)); - - if (debug) { - println!("replaced two arcs with four arcs"); - println!( - "Replaced arc {} with arcs {} and {}", - shape_a_id, new_shapes[0], new_shapes[1] - ); - - println!( - "Replaced arc {} with arcs {} and {}", - shape_b_id, new_shapes[2], new_shapes[3] - ); - } - } - (Shape::Arc(_), Shape::Line(_)) => todo!(), - (Shape::Line(line_a), Shape::Circle(circle_b)) => { - let new_point_id = temp_sketch.add_point(point.x, point.y); - - let (line_a1, line_a2) = - temp_sketch.split_line_at_point(&line_a, &new_point_id, &point); - let arc_b = temp_sketch.split_circle_at_point(&circle_b, &new_point_id, &point); - - // convert the circle into an arc in place - all_shapes.items.insert(shape_b_id, Shape::Arc(arc_b)); - - // delete the old line and replace with two smaller lines - new_shapes.push(all_shapes.add_item(Shape::Line(line_a1))); - new_shapes.push(all_shapes.add_item(Shape::Line(line_a2))); - recently_deleted.push(all_shapes.remove_item(shape_a_id)); - - if (debug) { - println!( - "Broke line {} into {} and {}", - shape_a_id, new_shapes[0], new_shapes[1] - ); - println!("Replaced circle {} with arc in place", shape_b_id); - } - } - (Shape::Line(line_a), Shape::Arc(arc_b)) => { - let new_point_id = temp_sketch.add_point(point.x, point.y); - - let (line_a1, line_a2) = - temp_sketch.split_line_at_point(&line_a, &new_point_id, &point); - let (arc_b1, arc_b2) = - temp_sketch.split_arc_at_point(&arc_b, &new_point_id, &point); - - new_shapes.push(all_shapes.add_item(Shape::Line(line_a1))); - new_shapes.push(all_shapes.add_item(Shape::Line(line_a2))); - new_shapes.push(all_shapes.add_item(Shape::Arc(arc_b1))); - new_shapes.push(all_shapes.add_item(Shape::Arc(arc_b2))); - - recently_deleted.push(all_shapes.remove_item(shape_a_id)); - recently_deleted.push(all_shapes.remove_item(shape_b_id)); - - if (debug) { - println!( - "Replaced line {} with {} and {}", - recently_deleted[0], new_shapes[0], new_shapes[1] - ); - println!( - "Replaced arc {} with {} and {}", - recently_deleted[1], new_shapes[2], new_shapes[3] - ); - } - } - (Shape::Line(line_a), Shape::Line(line_b)) => { - let collision_point_id = - match (&collision.shape_a_degeneracy, &collision.shape_b_degeneracy) { - (IsStart, None) => line_a.start, - (IsEnd, None) => line_a.end, - (None, IsStart) => line_b.start, - (None, IsEnd) => line_b.end, - (None, None) => temp_sketch.add_point(point.x, point.y), - (Complete, Complete) => { - if (debug) { - println!("COMPLETE degeneracy found. Removing line {}", shape_b_id); - } - all_shapes.remove_item(shape_b_id); - - recently_deleted.push(shape_b_id); - - return; - } - (_, _) => { - if (debug) { - println!("One line continues the other. Nothing to be done!"); - } - return; - } - }; - - if collision.shape_a_degeneracy == None { - let (line_a1, line_a2) = - temp_sketch.split_line_at_point(&line_a, &collision_point_id, &point); - - new_shapes.push(all_shapes.add_item(Shape::Line(line_a1))); - new_shapes.push(all_shapes.add_item(Shape::Line(line_a2))); - recently_deleted.push(all_shapes.remove_item(shape_a_id)); - if (debug) { - println!( - "Replaced line {} with lines {} and {}", - shape_a_id, new_shapes[0], new_shapes[1] - ); - } - } - - if collision.shape_b_degeneracy == None { - let (line_b1, line_b2) = - temp_sketch.split_line_at_point(&line_b, &collision_point_id, &point); - - new_shapes.push(all_shapes.add_item(Shape::Line(line_b1))); - new_shapes.push(all_shapes.add_item(Shape::Line(line_b2))); - recently_deleted.push(all_shapes.remove_item(shape_b_id)); - if (debug) { - println!( - "Replaced line {} with lines {} and {}", - shape_b_id, - new_shapes[new_shapes.len() - 2], - new_shapes[new_shapes.len() - 1] - ); - } - } - - if collision.shape_a_degeneracy == IsEnd || collision.shape_a_degeneracy == IsStart - { - if !possible_shape_collisions.contains(&collision.shape_a) { - possible_shape_collisions.push(collision.shape_a); - } - } - - if collision.shape_b_degeneracy == IsEnd || collision.shape_b_degeneracy == IsStart - { - if !possible_shape_collisions.contains(&collision.shape_b) { - possible_shape_collisions.push(collision.shape_b); - } - } - } - } - } - - pub fn step_process( - &self, - temp_sketch: &mut Sketch, - all_shapes: &mut IncrementingMap, - pairs_to_check: &mut VecDeque<(u64, u64)>, - collisions: &mut VecDeque, - possible_shape_collisions: &mut Vec, - new_shapes: &mut Vec, - recently_deleted: &mut Vec, - debug: bool, - ) -> bool { - // the bool we return indicates whether any work was done - - if (debug) { - println!("----- Okay let's process:"); - } - - if !recently_deleted.is_empty() { - println!("Something was recently deleted, let's fix it"); - // any collisions with the old shape are simply deleted - let mut indices_to_delete: Vec = vec![]; - for (i, c) in collisions.iter().enumerate() { - if recently_deleted.contains(&c.shape_a) { - indices_to_delete.push(i); - - if !possible_shape_collisions.contains(&c.shape_b) { - possible_shape_collisions.push(c.shape_b); - println!("Pushed a possible shape collision against {}", c.shape_b); - } - } - - if recently_deleted.contains(&c.shape_b) { - indices_to_delete.push(i); - - if !possible_shape_collisions.contains(&c.shape_a) { - possible_shape_collisions.push(c.shape_a); - println!("Pushed a possible shape collision against {}", c.shape_a); - } - } - } - for i in indices_to_delete.iter().rev() { - collisions.remove(*i); - } - println!("I removed {} collisions", indices_to_delete.len()); - - // We also need to comb through pairs_to_check to remove anything that - // references these recently removed shape IDs - indices_to_delete.clear(); - for (i, (shape_a_id, shape_b_id)) in pairs_to_check.iter().enumerate() { - let shape_a_deleted = recently_deleted.contains(shape_a_id); - let shape_b_deleted = recently_deleted.contains(shape_b_id); - - if !shape_a_deleted && !shape_b_deleted { - //nothing to do, move on - continue; - } - - if shape_a_deleted && !shape_b_deleted { - // shape A was deleted but B wasn't, we we'll need to consider whether any of the - // new shapes are colliding with shape B - if !possible_shape_collisions.contains(shape_b_id) { - possible_shape_collisions.push(*shape_b_id); - } - } else if shape_b_deleted && !shape_a_deleted { - if !possible_shape_collisions.contains(shape_a_id) { - possible_shape_collisions.push(*shape_a_id); - } - } else if shape_a_deleted && shape_b_deleted { - panic!("I didn't think both shapes could be deleted at once!"); - } - indices_to_delete.push(i); - } - for i in indices_to_delete.iter().rev() { - pairs_to_check.remove(*i); - } - println!("I removed {} pairs to check", indices_to_delete.len()); - - recently_deleted.clear(); - return true; - } - - if !possible_shape_collisions.is_empty() || !new_shapes.is_empty() { - if (debug) { - println!("Possible shape collisions + new shapes! Let's add some pairs to check"); - } - for possible_shape_id in possible_shape_collisions.iter() { - for new_shape_id in new_shapes.iter() { - pairs_to_check.push_front((*possible_shape_id, *new_shape_id)); - } - } - possible_shape_collisions.clear(); - new_shapes.clear(); - return true; - } - - if !collisions.is_empty() { - if (debug) { - println!("We have a collision to process"); - } - let collision = collisions.pop_front().unwrap(); - self.process_collision( - temp_sketch, - all_shapes, - possible_shape_collisions, - new_shapes, - recently_deleted, - collision, - debug, - ); - - return true; - } - - if !pairs_to_check.is_empty() { - if (debug) { - println!("We have pairs to check!"); - } - let (shape_a_id, shape_b_id) = pairs_to_check.pop_front().unwrap(); - - let new_collisions = - self.identify_collisions(temp_sketch, all_shapes, shape_a_id, shape_b_id, debug); - - if (debug) { - println!("Adding {} collisions", new_collisions.len()); - } - for c in new_collisions { - // println!("Adding a collision"); - collisions.push_back(c); - } - return true; - } - - if (debug) { - println!("There was nothing to do!\n"); - } - return false; - } - - pub fn split_intersections(&self, debug: bool) -> Self { - let mut temp_sketch = self.clone(); - - // set up the necessary data structures: - // First put all segments: Arcs, Lines, Circles into one big collection called all_shapes - let mut all_shapes: IncrementingMap = IncrementingMap::new(); - let line_ids: Vec = temp_sketch.line_segments.keys().cloned().sorted().collect(); - for line_id in line_ids { - let line = temp_sketch.line_segments.get(&line_id).unwrap(); - all_shapes.add_item(Shape::Line(line.clone())); - } - let circle_ids: Vec = temp_sketch.circles.keys().cloned().sorted().collect(); - for circle_id in circle_ids { - let circle = temp_sketch.circles.get(&circle_id).unwrap(); - all_shapes.add_item(Shape::Circle(circle.clone())); - } - let arc_ids: Vec = temp_sketch.arcs.keys().cloned().sorted().collect(); - for arc_id in arc_ids { - let arc = temp_sketch.arcs.get(&arc_id).unwrap(); - all_shapes.add_item(Shape::Arc(arc.clone())); - } - - let mut pairs_to_check: VecDeque<(u64, u64)> = VecDeque::new(); - let mut collisions: VecDeque = VecDeque::new(); - let mut possible_shape_collisions: Vec = vec![]; - let mut new_shapes: Vec = vec![]; - let mut recently_deleted: Vec = vec![]; - - // inject all the pairs of shapes that need to be checked: - for shape_id_a in all_shapes.items.keys() { - for shape_id_b in all_shapes.items.keys() { - if shape_id_a < shape_id_b { - pairs_to_check.push_back((*shape_id_a, *shape_id_b)) - } - } - } - - // While there's anything to do, step the process forward - let mut count = 0; - loop { - if (debug) { - println!("\nstep: {} Here's the setup:", count); - } - count += 1; - - if (debug) { - println!("Pairs to check: {:?}", pairs_to_check); - println!("Collisions: {:?}", collisions); - println!("Possible shape collisions: {:?}", possible_shape_collisions); - println!("New shapes: {:?}", new_shapes); - println!("Recently deleted: {:?}", recently_deleted); - } - // let mut input = String::new(); - // io::stdin() - // .read_line(&mut input) - // .expect("error: unable to read user input"); - // println!("{}", input); - - let result = self.step_process( - &mut temp_sketch, - &mut all_shapes, - &mut pairs_to_check, - &mut collisions, - &mut possible_shape_collisions, - &mut new_shapes, - &mut recently_deleted, - debug, - ); - // let mut input = String::new(); - // io::stdin() - // .read_line(&mut input) - // .expect("error: unable to read user input"); - // println!("{}", input); - if result == false { - break; - } - } - - // Lastly, consolidate all the shapes into a final sketch and return it - let mut final_sketch = Sketch::new(); - final_sketch.points = temp_sketch.points; - final_sketch.highest_point_id = temp_sketch.highest_point_id; - for shape in all_shapes.items.iter() { - match shape { - (id, Shape::Line(line)) => { - final_sketch.add_segment(line.start, line.end); - } - (id, Shape::Circle(circle)) => { - final_sketch.add_circle(circle.center, circle.radius); - } - (id, Shape::Arc(arc)) => { - final_sketch.add_arc(arc.center, arc.start, arc.end, arc.clockwise); - } - _ => {} - } - } - if (debug) { - println!("So, in summary I've generated these shapes:"); - for shape in all_shapes.items.iter() { - println!("{:?}", shape); - } - } - final_sketch - } - - pub fn line_line_collisions( - &self, - line_a: &Line2, - line_a_id: u64, - line_b: &Line2, - line_b_id: u64, - debug: bool, - ) -> Vec { - // catch the case where the lines are completely degenerate-their start and end points are the same - if line_a.start == line_b.start || line_a.start == line_b.end { - if line_a.end == line_b.start || line_a.end == line_b.end { - return vec![Collision::degenerate(line_a_id, line_b_id)]; - } - } - - let a_start = self.points.get(&line_a.start).unwrap(); - let a_end = self.points.get(&line_a.end).unwrap(); - let b_start = self.points.get(&line_b.start).unwrap(); - let b_end = self.points.get(&line_b.end).unwrap(); - - fn normal_form(start: &Point2, end: &Point2) -> (f64, f64, f64) { - let a = start.y - end.y; - let b = end.x - start.x; - let c = (start.x - end.x) * start.y + (end.y - start.y) * start.x; - return (a, b, c); - } - - let (a1, b1, c1) = normal_form(&a_start, &a_end); - let (a2, b2, c2) = normal_form(&b_start, &b_end); - - if (debug) { - println!("a1, b1, c1: {} {} {}", a1, b1, c1); - println!("a2, b2, c2: {} {} {}", a2, b2, c2); - } - - let x_intercept = (b1 * c2 - b2 * c1) / (a1 * b2 - a2 * b1); - let y_intercept = (c1 * a2 - c2 * a1) / (a1 * b2 - a2 * b1); - - if (debug) { - println!("Intercept: {} {}", x_intercept, y_intercept); - } - - if x_intercept.is_nan() || y_intercept.is_nan() { - // something degenerate happened. The lines may be parallel. - if (debug) { - println!("NAN intercept, so lines are be parallel"); - } - - let tol = 1e-8; - let a_start_colinear_with_b = (a2 * a_start.x + b2 * a_start.y + c2).abs() < tol; - let a_end_colinear_with_b = (a2 * a_end.x + b2 * a_end.y + c2).abs() < tol; - - if a_start_colinear_with_b && a_end_colinear_with_b { - if (debug) { - println!("The lines are perfectly colinear!"); - } - - let mut collisions: Vec = vec![]; - - // it is possible these lines overlap somewhat. - if within_range(a_start.x, b_start.x, b_end.x, -tol) - && within_range(a_start.y, b_start.y, b_end.y, -tol) - { - // a_start is WITHIN b! - if (debug) { - println!("A start is within B"); - } - let mut collision = Collision::new(a_start.clone(), line_a_id, line_b_id); - collision.shape_a_degeneracy = IsStart; - collisions.push(collision); - } - if within_range(a_end.x, b_start.x, b_end.x, -tol) - && within_range(a_end.y, b_start.y, b_end.y, -tol) - { - // a_end is WITHIN b! - if (debug) { - println!("A end is within B"); - } - let mut collision = Collision::new(a_end.clone(), line_a_id, line_b_id); - collision.shape_a_degeneracy = IsEnd; - collisions.push(collision); - } - - if within_range(b_start.x, a_start.x, a_end.x, -tol) - && within_range(b_start.y, a_start.y, a_end.y, -tol) - { - // b_start is WITHIN a! - if (debug) { - println!("B start is within A"); - } - let mut collision = Collision::new(b_start.clone(), line_a_id, line_b_id); - collision.shape_b_degeneracy = IsStart; - collisions.push(collision); - } - if within_range(b_end.x, a_start.x, a_end.x, -tol) - && within_range(b_end.y, a_start.y, a_end.y, -tol) - { - // b_end is WITHIN a! - if (debug) { - println!("B end is within A"); - } - let mut collision = Collision::new(b_end.clone(), line_a_id, line_b_id); - collision.shape_b_degeneracy = IsEnd; - collisions.push(collision); - } - - return collisions; - } else { - // If the lines are parallel but not colinear, then they can have no collisions - return vec![]; - } - } - - if x_intercept.is_infinite() || y_intercept.is_infinite() { - if (debug) { - println!("Infinite intercept, so lines are parallel"); - } - return vec![]; - } - - // it only counts as an intersection if it falls within both the segments - // Check that the x-intercept is within the x-range of the first segment - let epsilon = 1e-12; - if within_range(x_intercept, a_start.x, a_end.x, epsilon) - && within_range(y_intercept, a_start.y, a_end.y, epsilon) - { - if within_range(x_intercept, b_start.x, b_end.x, epsilon) - && within_range(y_intercept, b_start.y, b_end.y, epsilon) - { - let collision_point = Point2::new(x_intercept, y_intercept); - let mut collision = Collision::new(collision_point.clone(), line_a_id, line_b_id); - if points_almost_equal(&a_start, &collision_point) { - collision.shape_a_degeneracy = Degeneracy::IsStart; - } - if points_almost_equal(&a_end, &collision_point) { - collision.shape_a_degeneracy = Degeneracy::IsEnd; - } - if points_almost_equal(&b_start, &collision_point) { - collision.shape_b_degeneracy = Degeneracy::IsStart; - } - if points_almost_equal(&b_end, &collision_point) { - collision.shape_b_degeneracy = Degeneracy::IsEnd; - } - return vec![collision]; - } - } - - vec![] - } - - pub fn line_arc_collisions( - &self, - line_a: &Line2, - line_a_id: u64, - arc_b: &Arc2, - arc_b_id: u64, - ) -> Vec { - // treat this is circle/circle collision, then just do some checks - // afterwards to make sure the collision points really do fall within - // the bounds of the arc - println!("LINE AGAINST ARC"); - let arc_center = self.points.get(&arc_b.center).unwrap(); - // println!("Getting arc start: {}", &arc.start); - let arc_start = self.points.get(&arc_b.start).unwrap(); - let arc_dx = arc_center.x - arc_start.x; - let arc_dy = arc_center.y - arc_start.y; - let arc_radius = arc_dx.hypot(arc_dy); - let fake_circle = Circle2 { - center: arc_b.center, - radius: arc_radius, - top: arc_b.start, - }; - - println!("Fake circle: {:?}", &fake_circle); - println!("Line Details:"); - let line_start = &self.points[&line_a.start]; - println!("start: {:?}", line_start); - let line_end = &self.points[&line_a.end]; - println!("end: {:?}", line_end); - - let fake_collisions = - self.line_circle_collisions(line_a, line_a_id, &fake_circle, arc_b_id); - - println!("Fake collisions: {:?}", fake_collisions); - - let mut real_collisions: Vec = vec![]; - - for c in fake_collisions { - // check to make sure the point falls within the arc. - if self.point_within_arc(arc_b, &c.point) { - real_collisions.push(c); - } - } - - real_collisions - } - - pub fn line_circle_collisions( - &self, - line_a: &Line2, - line_a_id: u64, - circle_b: &Circle2, - circle_b_id: u64, - ) -> Vec { - // to make the math easier, let's assume the circle's center point is the origin - // let's translate the line's start and end points - let mut start = self.points[&line_a.start].clone(); - let mut end = self.points[&line_a.end].clone(); - let center = self.points[&circle_b.center].clone(); - let r = circle_b.radius; - println!("Radius as reported by circle: {}", r); - start.x -= center.x; - start.y -= center.y; - end.x -= center.x; - end.y -= center.y; - - // get the line in normal form - let (a, b, c) = normal_form(&start, &end); - - println!("Line from {:?} to {:?}", &start, &end); - println!("In normal form: {} {} {}", a, b, c); - let (mut y1, mut y2, mut x1, mut x2); - - let dy = (end.y - start.y).abs(); - - if a == 0.0 || dy < 1e-10 { - println!("It's a horizontal line"); - // oh, it's a horizontal line! that makes the math easier - y1 = -c / b; - y2 = -c / b; - - println!("Y1 and Y2: {} {}", y1, y2); - - x1 = (r * r - y1 * y1).sqrt(); - x2 = -(r * r - y1 * y1).sqrt(); - - // println!("X1 and X2: {} {}", x1, x2); - } else { - println!("It's not a special case"); - let det = a * a + b * b; - let d = (a * a * ((r * r * det) - c * c)).sqrt(); - - y1 = (-d - b * c) / det; - // println!("y1 {}", y1); - y2 = (d - b * c) / det; - // println!("y2 {}", y2); - - x1 = -(b * y1 + c) / a; - // println!("x1 {}", x1); - x2 = -(b * y2 + c) / a; - // println!("x2 {}", x2); - } - - println!("In shifted coordinates:"); - println!("x1 y1 {} {}", x1, y1); - println!("x2 y2 {} {}", x2, y2); - - if (x1.hypot(y1) - r).abs() > 1e-10 { - panic!( - "Somehow the radius is not equal! Got {} expected {}", - x1.hypot(y1), - r - ); - } - - if (x2.hypot(y2) - r).abs() > 1e-10 { - panic!( - "Somehow the radius is not equal! Got {} expected {}", - x2.hypot(y2), - r - ); - } - - y1 += center.y; - y2 += center.y; - x1 += center.x; - x2 += center.x; - start.x += center.x; - start.y += center.y; - end.x += center.x; - end.y += center.y; - - println!("In real coordinates:"); - println!("x1 y1 {} {}", x1, y1); - println!("x2 y2 {} {}", x2, y2); - - // println!("X1 Y1: {} {}", x1, y1); - // println!("X2 Y2: {} {}", x2, y2); - - let mut valid_collisions: Vec = vec![]; - - let start = &self.points[&line_a.start]; - let end = &self.points[&line_a.end]; - - let epsilon = 1e-10; - - println!("Checking that X {} is within {} {}", x1, start.x, end.x); - println!("Checking that Y {} is within {} {}", y1, start.y, end.y); - - if within_range(x1, start.x, end.x, epsilon) && within_range(y1, start.y, end.y, epsilon) { - println!("Added that point!"); - valid_collisions.push(Collision::new(Point2::new(x1, y1), line_a_id, circle_b_id)); - } - - println!("Checking that X {} is within {} {}", x2, start.x, end.x); - println!("Checking that Y {} is within {} {}", y2, start.y, end.y); - if within_range(x2, start.x, end.x, epsilon) && within_range(y2, start.y, end.y, epsilon) { - println!("Added that point!"); - valid_collisions.push(Collision::new(Point2::new(x2, y2), line_a_id, circle_b_id)); - } - - valid_collisions - } - - pub fn circle_circle_collisions( - &self, - circle_a: &Circle2, - circle_a_id: u64, - circle_b: &Circle2, - circle_b_id: u64, - ) -> Vec { - let center_a = self.points.get(&circle_a.center).unwrap(); - let center_b = self.points.get(&circle_b.center).unwrap(); - let r_a = circle_a.radius; - let r_b = circle_b.radius; - - // compute distance between centers - let center_dx = center_b.x - center_a.x; - let center_dy = center_b.y - center_a.y; - let center_dist = center_dx.hypot(center_dy); - - // if the circles are too far away OR too close, they don't intersect - if center_dist > r_a + r_b { - return vec![]; - } - if center_dist < (r_a - r_b).abs() { - return vec![]; - } - - let epsilon = 1e-10; - if center_dist > r_a + r_b - epsilon && center_dist < r_a + r_b + epsilon { - // draw a straight line from a to b, of length r_a - let collision = Collision::new( - Point2::new( - center_a.x + r_a * center_dx / center_dist, - center_a.y + r_a * center_dy / center_dist, - ), - circle_a_id, - circle_b_id, - ); - return vec![collision]; - } - - let r_2 = center_dist * center_dist; - let r_4 = r_2 * r_2; - let a = (r_a * r_a - r_b * r_b) / (2.0 * r_2); - let r_2_r_2 = r_a * r_a - r_b * r_b; - let c = (2.0 * (r_a * r_a + r_b * r_b) / r_2 - r_2_r_2 * r_2_r_2 / r_4 - 1.0).sqrt(); - - let fx = (center_a.x + center_b.x) / 2.0 + a * (center_b.x - center_a.x); - let gx = c * (center_b.y - center_a.y) / 2.0; - let ix1 = fx + gx; - let ix2 = fx - gx; - - let fy = (center_a.y + center_b.y) / 2.0 + a * (center_b.y - center_a.y); - let gy = c * (center_a.x - center_b.x) / 2.0; - let iy1 = fy + gy; - let iy2 = fy - gy; - - let collision_a = Collision::new(Point2::new(ix1, iy1), circle_a_id, circle_b_id); - - let collision_b = Collision::new(Point2::new(ix2, iy2), circle_a_id, circle_b_id); - - return vec![collision_a, collision_b]; - } - - pub fn circle_circle_intersection( - &self, - circle_a: &Circle2, - circle_b: &Circle2, - ) -> Intersection { - let center_a = self.points.get(&circle_a.center).unwrap(); - let center_b = self.points.get(&circle_b.center).unwrap(); - let r_a = circle_a.radius; - let r_b = circle_b.radius; - - // compute distance between centers - let center_dx = center_b.x - center_a.x; - let center_dy = center_b.y - center_a.y; - let center_dist = center_dx.hypot(center_dy); - - // if the circles are too far away OR too close, they don't intersect - if center_dist > r_a + r_b { - return Intersection::None; - } - if center_dist < (r_a - r_b).abs() { - return Intersection::None; - } - - let r_2 = center_dist * center_dist; - let r_4 = r_2 * r_2; - let a = (r_a * r_a - r_b * r_b) / (2.0 * r_2); - let r_2_r_2 = r_a * r_a - r_b * r_b; - let c = (2.0 * (r_a * r_a + r_b * r_b) / r_2 - r_2_r_2 * r_2_r_2 / r_4 - 1.0).sqrt(); - - let fx = (center_a.x + center_b.x) / 2.0 + a * (center_b.x - center_a.x); - let gx = c * (center_b.y - center_a.y) / 2.0; - let ix1 = fx + gx; - let ix2 = fx - gx; - - let fy = (center_a.y + center_b.y) / 2.0 + a * (center_b.y - center_a.y); - let gy = c * (center_a.x - center_b.x) / 2.0; - let iy1 = fy + gy; - let iy2 = fy - gy; - - Intersection::TwoPoints(Point2::new(ix1, iy1), false, Point2::new(ix2, iy2), false) - } - - pub fn circle_arc_collisions( - &self, - circle: &Circle2, - circle_id: u64, - arc: &Arc2, - arc_id: u64, - ) -> Vec { - // treat this is circle/circle collision, then just do some checks - // afterwards to make sure the collision points really do fall within - // the bounds of the arc - let arc_center = self.points.get(&arc.center).unwrap(); - // println!("Getting arc start: {}", &arc.start); - let arc_start = self.points.get(&arc.start).unwrap(); - let arc_dx = arc_center.x - arc_start.x; - let arc_dy = arc_center.y - arc_start.y; - let arc_radius = arc_dx.hypot(arc_dy); - let fake_circle = Circle2 { - center: arc.center, - radius: arc_radius, - top: arc.start, - }; - - let fake_collisions: Vec = - self.circle_circle_collisions(circle, circle_id, &fake_circle, arc_id); - println!("Fake collision: {:?}", fake_collisions); - - let mut real_collisions: Vec = vec![]; - - for c in fake_collisions { - // check to make sure the point falls within the arc. - if self.point_within_arc(arc, &c.point) { - real_collisions.push(c); - } - } - - real_collisions - } - - pub fn circle_arc_intersection(&self, circle: &Circle2, arc: &Arc2) -> Intersection { - // treat this is circle/circle intersection, then just do some checks - // afterwards to make sure the intersection points really do fall within - // the bounds of the arc - let arc_center = self.points.get(&arc.center).unwrap(); - let arc_start = self.points.get(&arc.start).unwrap(); - let arc_dx = arc_center.x - arc_start.x; - let arc_dy = arc_center.y - arc_start.y; - let arc_radius = arc_dx.hypot(arc_dy); - let fake_circle = Circle2 { - center: arc.center, - radius: arc_radius, - top: arc.start, - }; - - let fake_intersection = self.circle_circle_intersection(circle, &fake_circle); - println!("Fake intersection: {:?}", fake_intersection); - - match fake_intersection { - Intersection::None => Intersection::None, - Intersection::OnePoint(_, _) => todo!(), - Intersection::TwoPoints(point_a, is_degenerate_a, point_b, is_degenerate_b) => { - // check to make sure that both points fall within the arc. If only one - // of them does, just return that one. if none do, return none. - // if both do, return both. - let point_a_good = self.point_within_arc(arc, &point_a); - let point_b_good = self.point_within_arc(arc, &point_b); - - match (point_a_good, point_b_good) { - (true, true) => { - Intersection::TwoPoints(point_a, is_degenerate_a, point_b, is_degenerate_b) - } - (true, false) => Intersection::OnePoint(point_a, is_degenerate_a), - (false, true) => Intersection::OnePoint(point_b, is_degenerate_b), - (false, false) => Intersection::None, - } - } - Intersection::Line(_, _) => todo!(), - Intersection::Arc(_) => todo!(), - Intersection::Circle(_) => todo!(), - } - } - - pub fn point_within_arc(&self, arc: &Arc2, point: &Point2) -> bool { - let center = self.points.get(&arc.center).unwrap(); - let mut start = self.points.get(&arc.start).unwrap(); - let mut end = self.points.get(&arc.end).unwrap(); - - // clockwise arcs are weird and unconventional. Within this function, pretend all arcs are CCW. - // doing this destroys 1 bit of information about the arc, but it's irrelevant for the purposes of this function - if arc.clockwise { - (start, end) = (end, start); - } - - // cool, so you only have to imagine this math working for CCW arcs - let start_dx = start.x - center.x; - let start_dy = start.y - center.y; - let start_angle = start_dy.atan2(start_dx); - - let end_dx = end.x - center.x; - let end_dy = end.y - center.y; - let mut end_angle = end_dy.atan2(end_dx); - - if end_angle <= start_angle { - end_angle += TAU; - } - - let point_dx = point.x - center.x; - let point_dy = point.y - center.y; - let mut point_angle = point_dy.atan2(point_dx); - - if point_angle < start_angle { - point_angle += TAU; - } - - if point_angle >= start_angle && point_angle <= end_angle { - // okay the angles work out, but we gotta run one last check: - // make sure the point is the right distance from center! - let arc_radius = start_dy.hypot(start_dx); - let point_radius = point_dy.hypot(point_dx); - let radius_diff = (arc_radius - point_radius).abs(); - - // floats are never really *equal*, just nearly equal - radius_diff < 1e-10 - } else { - false - } - } - - pub fn arc_arc_collisions( - &self, - arc_a: &Arc2, - arc_a_id: u64, - arc_b: &Arc2, - arc_b_id: u64, - ) -> Vec { - // treat this is circle/circle collision, then just do some checks - // afterwards to make sure the collision points really do fall within - // the bounds of the arc - let arc_a_center = self.points.get(&arc_a.center).unwrap(); - // println!("Getting arc start: {}", &arc.start); - let arc_a_start = self.points.get(&arc_a.start).unwrap(); - let arc_a_end = self.points.get(&arc_a.end).unwrap(); - let arc_a_dx = arc_a_center.x - arc_a_start.x; - let arc_a_dy = arc_a_center.y - arc_a_start.y; - let arc_a_radius = arc_a_dx.hypot(arc_a_dy); - let fake_circle_a = Circle2 { - center: arc_a.center, - radius: arc_a_radius, - top: arc_a.start, - }; - - let arc_b_center = self.points.get(&arc_b.center).unwrap(); - // println!("Getting arc start: {}", &arc.start); - let arc_b_start = self.points.get(&arc_b.start).unwrap(); - let arc_b_end = self.points.get(&arc_b.end).unwrap(); - let arc_b_dx = arc_b_center.x - arc_b_start.x; - let arc_b_dy = arc_b_center.y - arc_b_start.y; - let arc_b_radius = arc_b_dx.hypot(arc_b_dy); - let fake_circle_b = Circle2 { - center: arc_b.center, - radius: arc_b_radius, - top: arc_b.start, - }; - - let mut forbidden_points: Vec = vec![]; - if arc_a.start == arc_b.start || arc_a.start == arc_b.end { - forbidden_points.push(arc_a_start.clone()); - } - if arc_a.end == arc_b.end || arc_a.end == arc_b.start { - forbidden_points.push(arc_a_end.clone()); - } - - let fake_collisions: Vec = - self.circle_circle_collisions(&fake_circle_a, arc_a_id, &fake_circle_b, arc_b_id); - println!("Fake collisions: {:?}", fake_collisions); - - let mut real_collisions: Vec = vec![]; - - for c in fake_collisions { - // check to make sure the point falls within both arcs. - if self.point_within_arc(arc_a, &c.point) && self.point_within_arc(arc_b, &c.point) { - // check to make sure the collision point is not approximately equal to any of - // the start or end points - - let mut point_was_forbidden = false; - for forbidden_point in forbidden_points.iter() { - if points_almost_equal(&c.point, forbidden_point) { - point_was_forbidden = true; - break; - } - } - - if !point_was_forbidden { - real_collisions.push(c); - } else { - println!("A point was forbidden! {:?}", &c.point); - } - } - } - - real_collisions - } - - pub fn split_circle_at_point(&self, circle: &Circle2, point_id: &u64, point: &Point2) -> Arc2 { - // this converts a single circle into a single arc - let new_arc = Arc2 { - center: circle.center, - start: *point_id, - end: *point_id, - clockwise: false, - }; - - new_arc - } - - pub fn split_arc_at_point(&self, arc: &Arc2, point_id: &u64, point: &Point2) -> (Arc2, Arc2) { - // this converts a single arc into a two arcs - let new_arc_1 = Arc2 { - center: arc.center, - start: arc.start, - end: *point_id, - clockwise: arc.clockwise, - }; - - let new_arc_2 = Arc2 { - center: arc.center, - start: *point_id, - end: arc.end, - clockwise: arc.clockwise, - }; - - (new_arc_1, new_arc_2) - } - - pub fn split_line_at_point( - &self, - line: &Line2, - point_id: &u64, - point: &Point2, - ) -> (Line2, Line2) { - let new_line_1 = Line2 { - start: line.start, - end: *point_id, - }; - - let new_line_2 = Line2 { - start: *point_id, - end: line.end, - }; - - (new_line_1, new_line_2) - } -} - -pub fn points_almost_equal(point_a: &Point2, point_b: &Point2) -> bool { - let dx = (point_b.x - point_a.x).abs(); - let dy = (point_b.y - point_a.y).abs(); - dx < 1e-10 && dy < 1e-10 -} - -fn normal_form(start: &Point2, end: &Point2) -> (f64, f64, f64) { - // finds the normal form of a line: - // ax + by + c = 0 - let a = start.y - end.y; - let b = end.x - start.x; - let c = (start.x - end.x) * start.y + (end.y - start.y) * start.x; - return (a, b, c); -} - -fn within_range(x: f64, a: f64, b: f64, epsilon: f64) -> bool { - if a == b && b == x { - return true; - } - let mut retval; - if a < b { - retval = x >= a - epsilon && x <= b + epsilon; - } else { - retval = x >= b - epsilon && x <= a + epsilon; - } - retval -} - -#[cfg(test)] -mod tests { - use crate::archetypes::Plane; - use crate::project::Project; - use crate::workbench::Workbench; - - use super::*; - - #[test] - fn line_through_rectangle() { - let contents = - std::fs::read_to_string("src/test_inputs/line_through_rectangle.cadmium").unwrap(); - let p: Project = serde_json::from_str(&contents).unwrap(); - // println!("{:?}", p); - - let realized = p.get_realization(0, 1000); - let (sketch_unsplit, sketch_split, _) = realized.sketches.get("Sketch-0").unwrap(); - println!("Sketch: {:?}", sketch_split); - println!("Faces: {:?}", sketch_split.faces); - println!("Number of faces: {:?}", sketch_split.faces.len()); - assert_eq!(sketch_split.faces.len(), 2); - } - - #[test] - fn line_through_many_rectangles() { - let contents = - std::fs::read_to_string("src/test_inputs/line_through_many_rectangles.cadmium") - .unwrap(); - let p: Project = serde_json::from_str(&contents).unwrap(); - // println!("{:?}", p); - - let realized = p.get_realization(0, 1000); - let (sketch_unsplit, sketch_split, _) = realized.sketches.get("Sketch-0").unwrap(); - // println!("Sketch: {:?}", sketch_split); - // println!("Faces: {:?}", sketch_split.faces); - println!("Number of faces: {:?}", sketch_split.faces.len()); - assert_eq!(sketch_split.faces.len(), 8); - } - - #[test] - fn two_circles_two_intersections() { - // two intersecting circles should yield 3 extrudable faces - let contents = std::fs::read_to_string( - "src/test_inputs/sketches/circle_circle/two_circles_two_intersections.cadmium", - ) - .unwrap(); - let p: Project = serde_json::from_str(&contents).unwrap(); - - let realized = p.get_realization(0, 1000); - let (sketch_unsplit, sketch_split, _) = realized.sketches.get("Sketch-0").unwrap(); - - println!("Number of faces: {:?}", sketch_split.faces.len()); - assert_eq!(sketch_split.faces.len(), 3); - } - - #[test] - fn four_circles() { - // three intersecting circles should yield 5 extrudable faces - let contents = std::fs::read_to_string( - "src/test_inputs/sketches/circle_circle/four_circles_chained.cadmium", - ) - .unwrap(); - let p: Project = serde_json::from_str(&contents).unwrap(); - - let realized = p.get_realization(0, 1000); - let (sketch_unsplit, sketch_split, _) = realized.sketches.get("Sketch-0").unwrap(); - - println!("Number of faces: {:?}", sketch_split.faces.len()); - assert_eq!(sketch_split.faces.len(), 7); - } - - #[test] - fn three_circles() { - // three intersecting circles should yield 5 extrudable faces - let contents = - std::fs::read_to_string("src/test_inputs/sketches/circle_circle/three_circles.cadmium") - .unwrap(); - let p: Project = serde_json::from_str(&contents).unwrap(); - - let realized = p.get_realization(0, 1000); - let (sketch_unsplit, sketch_split, _) = realized.sketches.get("Sketch-0").unwrap(); - - println!("Number of faces: {:?}", sketch_split.faces.len()); - assert_eq!(sketch_split.faces.len(), 5); - } - - #[test] - fn circle_rectangle() { - // a circle that intersects with a rectangle on two lines - // let contents = std::fs::read_to_string( - // "src/test_inputs/sketches/circle_line/circle_rectangle.cadmium", - // ) - // .unwrap(); - // let p: Project = serde_json::from_str(&contents).unwrap(); - - // let realized = p.get_realization(0, 1000); - // let (sketch_unsplit, sketch_split, _) = realized.sketches.get("Sketch-0").unwrap(); - - // println!("Number of faces: {:?}", sketch_split.faces.len()); - // assert_eq!(sketch_split.faces.len(), 3); - - let contents = std::fs::read_to_string( - "src/test_inputs/sketches/circle_line/circle_rect_changing_size.cadmium", - ) - .unwrap(); - let p: Project = serde_json::from_str(&contents).unwrap(); - - let realized = p.get_realization(0, 1000); - let (sketch_unsplit, sketch_split, _) = realized.sketches.get("Sketch-0").unwrap(); - - println!("Number of faces: {:?}", sketch_split.faces.len()); - assert_eq!(sketch_split.faces.len(), 3); - } - - #[test] - fn circle_quadrangle() { - // a circle that intersects with a rectangle on two lines - let contents = std::fs::read_to_string( - "src/test_inputs/sketches/circle_line/circle_quadrangle.cadmium", - ) - .unwrap(); - let p: Project = serde_json::from_str(&contents).unwrap(); - - let realized = p.get_realization(0, 1000); - let (sketch_unsplit, sketch_split, _) = realized.sketches.get("Sketch-0").unwrap(); - - println!("Number of faces: {:?}", sketch_split.faces.len()); - assert_eq!(sketch_split.faces.len(), 3); - } - - #[test] - fn circle_rect_circle() { - // a circle that intersects with a rectangle on two lines - let contents = std::fs::read_to_string( - "src/test_inputs/sketches/circle_line/circle_rect_circle.cadmium", - ) - .unwrap(); - let p: Project = serde_json::from_str(&contents).unwrap(); - - let realized = p.get_realization(0, 1000); - let (sketch_unsplit, sketch_split, _) = realized.sketches.get("Sketch-0").unwrap(); - - println!("Number of faces: {:?}", sketch_split.faces.len()); - assert_eq!(sketch_split.faces.len(), 5); - } - - #[test] - fn points_are_in_arcs() { - let mut sketch = Sketch::new(); - - let origin = sketch.add_point(0.0, 0.0); - let right = sketch.add_point(1.0, 0.0); - let left = sketch.add_point(-1.0, 0.0); - let arc_top = Arc2 { - center: origin, - start: right, - end: left, - clockwise: false, - }; - let arc_bottom = Arc2 { - center: origin, - start: left, - end: right, - clockwise: false, - }; - let arc_top_cw = Arc2 { - center: origin, - start: left, - end: right, - clockwise: true, - }; - let arc_bottom_cw = Arc2 { - center: origin, - start: right, - end: left, - clockwise: true, - }; - - let up_top = Point2::new(0.0, 1.0); - let down_low = Point2::new(0.0, -1.0); - - // counterclockwise, as god intended - assert_eq!(sketch.point_within_arc(&arc_top, &up_top), true); - assert_eq!(sketch.point_within_arc(&arc_top, &down_low), false); - - assert_eq!(sketch.point_within_arc(&arc_bottom, &up_top), false); - assert_eq!(sketch.point_within_arc(&arc_bottom, &down_low), true); - - // clockwise, like a hooligan - assert_eq!(sketch.point_within_arc(&arc_top_cw, &up_top), true); - assert_eq!(sketch.point_within_arc(&arc_top_cw, &down_low), false); - - assert_eq!(sketch.point_within_arc(&arc_bottom_cw, &up_top), false); - assert_eq!(sketch.point_within_arc(&arc_bottom_cw, &down_low), true); - - let way_up_top = Point2::new(0.0, 100.0); - assert_eq!(sketch.point_within_arc(&arc_top, &way_up_top), false); - } - - #[test] - fn circle_circle_collisions() { - let mut sketch = Sketch::new(); - - // two touching normally - println!("two circles touching normally at two points"); - let a_radius = 1.0; - let a = sketch.add_point(0.0, 0.0); - let a_top = sketch.add_point(0.0, a_radius); - let b_radius = 1.0; - let b = sketch.add_point(1.0, 0.0); - let b_top = sketch.add_point(1.0, b_radius); - let circle_a = Circle2 { - center: a, - radius: a_radius, - top: a_top, - }; - let circle_b = Circle2 { - center: b, - radius: b_radius, - top: b_top, - }; - let collisions = sketch.circle_circle_collisions(&circle_a, 7, &circle_b, 8); - assert_eq!( - collisions, - vec![ - Collision::new(Point2::new(0.5, -0.8660254037844386), 7, 8,), - Collision::new(Point2::new(0.5, 0.8660254037844386), 7, 8,) - ] - ); - - println!("Two circles touching at one point"); - let a_radius = 2.0; - let a = sketch.add_point(0.0, 0.0); - let a_top = sketch.add_point(0.0, a_radius); - let b_radius = 3.0; - let b = sketch.add_point(a_radius + b_radius, 0.0); - let b_top = sketch.add_point(1.0, b_radius); - let circle_a = Circle2 { - center: a, - radius: a_radius, - top: a_top, - }; - let circle_b = Circle2 { - center: b, - radius: b_radius, - top: b_top, - }; - let collisions = sketch.circle_circle_collisions(&circle_a, 7, &circle_b, 8); - assert_eq!( - collisions, - vec![Collision::new(Point2::new(2.0, 0.0), 7, 8,)] - ); - - println!("Two circles not touching--too far away"); - let a_radius = 2.0; - let a = sketch.add_point(0.0, 0.0); - let a_top = sketch.add_point(0.0, a_radius); - let b_radius = 3.0; - let b = sketch.add_point(a_radius + b_radius + 1.0, 0.0); - let b_top = sketch.add_point(1.0, b_radius); - let circle_a = Circle2 { - center: a, - radius: a_radius, - top: a_top, - }; - let circle_b = Circle2 { - center: b, - radius: b_radius, - top: b_top, - }; - let collisions = sketch.circle_circle_collisions(&circle_a, 7, &circle_b, 8); - assert_eq!(collisions, vec![]); - - println!("Two circles not touching--too close"); - let a_radius = 2.0; - let a = sketch.add_point(0.0, 0.0); - let a_top = sketch.add_point(0.0, a_radius); - let b_radius = 3.0; - let b = sketch.add_point(0.5, 0.0); - let b_top = sketch.add_point(1.0, b_radius); - let circle_a = Circle2 { - center: a, - radius: a_radius, - top: a_top, - }; - let circle_b = Circle2 { - center: b, - radius: b_radius, - top: b_top, - }; - let collisions = sketch.circle_circle_collisions(&circle_a, 7, &circle_b, 8); - assert_eq!(collisions, vec![]); - } - - #[test] - fn line_line_overlap_tests() { - let all_tests = [ - [ - /* - |--------| - | | - |--------| | - | | | - | |--------| - | | - |--------| - */ - [0.0, 0.0], - [2.0, 0.0], - [2.0, 2.0], - [0.0, 2.0], - - [2.0, 1.0], - [4.0, 1.0], - [4.0, 3.0], - [2.0, 3.0], - - [2.0, 9.0] - ], [ - /* - |--------| - | |----| - | | | - | |----| - |--------| - */ - [0.0, 0.0], - [2.0, 0.0], - [2.0, 2.0], - [0.0, 2.0], - - [2.0, 0.5], - [3.0, 0.5], - [3.0, 1.5], - [2.0, 1.5], - - [2.0, 9.0] - ], [ - /* - |--------|----| - | | | - | |----| - | | - |--------| - */ - [0.0, 0.0], - [0.0, -2.0], - [2.0, -2.0], - [2.0, 0.0], - - [3.0, 0.0], - [3.0, -1.0], - [2.0, -1.0], - [2.0, 0.0], - - [2.0, 8.0] - ] - ]; - - for test in all_tests { - let mut p = Project::new("A"); - let mut w = p.workbenches.get_mut(0).unwrap(); - - let top_plane_id = w.get_first_plane_id().unwrap(); - let sketch_id = w.add_sketch_to_plane("Sketch 1", &top_plane_id); - let sketch = w.get_sketch_mut("Sketch 1").unwrap(); - - let a = sketch.add_point(test[0][0], test[0][1]); - let b = sketch.add_point(test[1][0], test[1][1]); - let c = sketch.add_point(test[2][0], test[2][1]); - let d = sketch.add_point(test[3][0], test[3][1]); - - let e = sketch.add_point(test[4][0], test[4][1]); - let f = sketch.add_point(test[5][0], test[5][1]); - let g = sketch.add_point(test[6][0], test[6][1]); - let h = if test[7] == test[3] { - // For the last test, we want to make sure the line is closed - d.clone() - } else { - sketch.add_point(test[7][0], test[7][1]) - }; - - // big one - sketch.add_segment(a, b); - sketch.add_segment(b, c); - sketch.add_segment(c, d); - sketch.add_segment(d, a); - - // small one - sketch.add_segment(e, f); - sketch.add_segment(f, g); - sketch.add_segment(g, h); - sketch.add_segment(h, e); - - let realized = p.get_realization(0, 1000); - let (sketch_unsplit, sketch_split, _) = realized.sketches.get("Sketch-0").unwrap(); - - println!("Number of faces: {:?}", sketch_split.faces.len()); - assert_eq!(sketch_split.faces.len(), test[8][0] as usize); - - assert_eq!(sketch_split.line_segments.len(), test[8][1] as usize); - } - } - - #[test] - fn line_circle_collisions() { - let mut sketch = Sketch::new(); - - println!("simple crossing point horizontal line"); - let a = sketch.add_point(0.0, 0.0); - let b = sketch.add_point(2.0, 0.0); - let c = sketch.add_point(0.0, 1.0); - let line_ab = Line2 { start: a, end: b }; - let circle_a = Circle2 { - center: a, - radius: 1.0, - top: c, - }; - let collisions = sketch.line_circle_collisions(&line_ab, 1, &circle_a, 2); - assert_eq!( - collisions, - vec![Collision::new(Point2::new(1.0, 0.0), 1, 2,)] - ); - - println!("simple crossing point horizontal line but backwards"); - let a = sketch.add_point(0.0, 0.0); - let b = sketch.add_point(-2.0, 0.0); - let c = sketch.add_point(0.0, 1.0); - let line_ab = Line2 { start: a, end: b }; - let circle_a = Circle2 { - center: a, - radius: 1.0, - top: c, - }; - let collisions = sketch.line_circle_collisions(&line_ab, 1, &circle_a, 2); - assert_eq!( - collisions, - vec![Collision::new(Point2::new(-1.0, 0.0), 1, 2,)] - ); - - println!("simple crossing point 45 degree angle"); - let a = sketch.add_point(0.0, 0.0); - let b = sketch.add_point(2.0, 2.0); - let c = sketch.add_point(0.0, 1.0); - let line_ab = Line2 { start: a, end: b }; - let circle_a = Circle2 { - center: a, - radius: 1.0, - top: c, - }; - let collisions = sketch.line_circle_collisions(&line_ab, 1, &circle_a, 2); - assert_eq!( - collisions, - vec![Collision::new( - Point2::new(0.7071067811865476, 0.7071067811865476), // sqrt(2)/2 - 1, - 2, - )] - ); - - println!("simple crossing point 45 degree angle but away from origin"); - let a = sketch.add_point(10.0, 10.0); - let b = sketch.add_point(12.0, 12.0); - let c = sketch.add_point(10.0, 11.0); - let line_ab = Line2 { start: a, end: b }; - let circle_a = Circle2 { - center: a, - radius: 1.0, - top: c, - }; - let collisions = sketch.line_circle_collisions(&line_ab, 1, &circle_a, 2); - assert_eq!( - collisions, - vec![Collision::new( - Point2::new(10.7071067811865476, 10.7071067811865476), - 1, - 2, - )] - ); - - println!("simple crossing point vertical line"); - let a = sketch.add_point(0.0, 0.0); - let b = sketch.add_point(0.0, 2.0); - let c = sketch.add_point(0.0, 1.0); - let line_ab = Line2 { start: a, end: b }; - let circle_a = Circle2 { - center: a, - radius: 1.0, - top: c, - }; - let collisions = sketch.line_circle_collisions(&line_ab, 1, &circle_a, 2); - assert_eq!( - collisions, - vec![Collision::new(Point2::new(0.0, 1.0), 1, 2,)] - ); - } - - #[test] - fn line_line_collisions() { - let mut sketch = Sketch::new(); - - // simple cross - println!("simple cross"); - let a = sketch.add_point(-1.0, 0.0); - let b = sketch.add_point(1.0, 0.0); - let c = sketch.add_point(0.0, -1.0); - let d = sketch.add_point(0.0, 1.0); - let line_ab = Line2 { start: a, end: b }; - let line_cd = Line2 { start: c, end: d }; - let collisions = sketch.line_line_collisions(&line_ab, 1, &line_cd, 2, false); - assert_eq!( - collisions, - vec![Collision::new(Point2::new(0.0, 0.0), 1, 2,)] - ); - - // a T - println!("an upside down T"); - let a = sketch.add_point(-1.0, 0.0); - let b = sketch.add_point(1.0, 0.0); - let c = sketch.add_point(0.0, 0.0); - let d = sketch.add_point(0.0, 1.0); - let line_ab = Line2 { start: a, end: b }; - let line_cd = Line2 { start: c, end: d }; - let collisions = sketch.line_line_collisions(&line_ab, 1, &line_cd, 2, false); - let mut expected_collision = Collision::new(Point2::new(0.0, 0.0), 1, 2); - expected_collision.shape_b_degeneracy = IsStart; - - // println!("Got this collision: {:?}", collisions[0]); - assert_eq!(collisions, vec![expected_collision]); - - // parallel horizontal - println!("parallel horizontal"); - let a = sketch.add_point(-1.0, 0.0); - let b = sketch.add_point(1.0, 0.0); - let c = sketch.add_point(-1.0, 1.0); - let d = sketch.add_point(1.0, 1.0); - let line_ab = Line2 { start: a, end: b }; - let line_cd = Line2 { start: c, end: d }; - let collisions = sketch.line_line_collisions(&line_ab, 1, &line_cd, 2, false); - assert_eq!(collisions, vec![]); - - // parallel vertical - println!("parallel vertical"); - let a = sketch.add_point(0.0, -1.0); - let b = sketch.add_point(0.0, 1.0); - let c = sketch.add_point(1.0, -1.0); - let d = sketch.add_point(1.0, 1.0); - let line_ab = Line2 { start: a, end: b }; - let line_cd = Line2 { start: c, end: d }; - let collisions = sketch.line_line_collisions(&line_ab, 1, &line_cd, 2, false); - assert_eq!(collisions, vec![]); - - // perpendicular but not intersecting - println!("perpendicular but not intersecting"); - let a = sketch.add_point(-1.0, 0.0); - let b = sketch.add_point(1.0, 0.0); - let c = sketch.add_point(3.0, 0.0); - let d = sketch.add_point(3.0, 1.0); - let line_ab = Line2 { start: a, end: b }; - let line_cd = Line2 { start: c, end: d }; - let collisions = sketch.line_line_collisions(&line_ab, 1, &line_cd, 2, false); - assert_eq!(collisions, vec![]); - - // share 1 point but only in the === sense not the == sense - println!("share 1 point but only in the === sense not the == sense"); - let a = sketch.add_point(-1.0, 1.0); - let b = sketch.add_point(0.0, 0.0); - let c = sketch.add_point(0.0, 0.0); - let d = sketch.add_point(1.0, 1.0); - let line_ab = Line2 { start: a, end: b }; - let line_cd = Line2 { start: c, end: d }; - let collisions = sketch.line_line_collisions(&line_ab, 1, &line_cd, 2, false); - let mut expected_collision = Collision::new(Point2::new(0.0, 0.0), 1, 2); - expected_collision.shape_a_degeneracy = IsEnd; - expected_collision.shape_b_degeneracy = IsStart; - assert_eq!(collisions, vec![expected_collision]); - - // share 1 point in the == sense - println!("share 1 point in the == sense"); - let a = sketch.add_point(-1.0, 1.0); - let b = sketch.add_point(0.0, 0.0); - let d = sketch.add_point(1.0, 1.0); - let line_ab = Line2 { start: a, end: b }; - let line_cd = Line2 { start: b, end: d }; - let collisions = sketch.line_line_collisions(&line_ab, 1, &line_cd, 2, false); - let mut expected_collision = Collision::new(Point2::new(0.0, 0.0), 1, 2); - expected_collision.shape_a_degeneracy = IsEnd; - expected_collision.shape_b_degeneracy = IsStart; - assert_eq!(collisions, vec![expected_collision]); - - // colinear, horizontal no intersection - println!("colinear horizontal no intersection"); - let a = sketch.add_point(-1.0, 0.0); - let b = sketch.add_point(0.0, 0.0); - let c = sketch.add_point(1.0, 0.0); - let d = sketch.add_point(2.0, 0.0); - let line_ab = Line2 { start: a, end: b }; - let line_cd = Line2 { start: c, end: d }; - let collisions = sketch.line_line_collisions(&line_ab, 1, &line_cd, 2, false); - assert_eq!(collisions, vec![]); - - // colinear, vertical no intersection - println!("colinear vertical no intersection"); - let a = sketch.add_point(0.0, 0.0); - let b = sketch.add_point(0.0, 1.0); - let c = sketch.add_point(0.0, 2.0); - let d = sketch.add_point(0.0, 3.0); - let line_ab = Line2 { start: a, end: b }; - let line_cd = Line2 { start: c, end: d }; - let collisions = sketch.line_line_collisions(&line_ab, 1, &line_cd, 2, false); - assert_eq!(collisions, vec![]); - - // Lines are exactly equal - // println!("Exactly equal"); - // let a = sketch.add_point(0.0, 0.0); - // let b = sketch.add_point(0.0, 1.0); - // let line_ab = Line2 { start: a, end: b }; - // let collisions = sketch.line_line_collisions(&line_ab, 1, &line_ab, 2, false); - // assert_eq!( - // collisions, - // Intersection::Line(Point2::new(0.0, 0.0), Point2::new(0.0, 1.0)) - // ); - - // println!("\nLine Overlap:"); - // // lines overlap somewhat, both vertical - // println!("lines overlap somewhat, both vertical"); - // let a = sketch.add_point(0.0, 0.0); - // let b = sketch.add_point(0.0, 2.0); - // let c = sketch.add_point(0.0, 1.0); - // let d = sketch.add_point(0.0, 3.0); - // let line_ab = Line2 { start: a, end: b }; - // let line_cd = Line2 { start: c, end: d }; - // let collisions = sketch.line_line_collisions(&line_ab, 1, &line_cd, 2, false); - // assert_eq!( - // collisions, - // Intersection::Line(Point2::new(0.0, 2.0), Point2::new(0.0, 1.0)) - // ); - // for future reference: the ordering of points here and for all of the tests below is inconsequential - // Feel free to swap the order here if the implementation changes. Maybe these should always come - // in a canonical order? - - // lines overlap somewhat, both horizontal - // println!("lines overlap somewhat, both horizontal"); - // let a = sketch.add_point(0.0, 0.0); - // let b = sketch.add_point(2.0, 0.0); - // let c = sketch.add_point(1.0, 0.0); - // let d = sketch.add_point(3.0, 0.0); - // let line_ab = Line2 { start: a, end: b }; - // let line_cd = Line2 { start: c, end: d }; - // let collisions = sketch.line_line_collisions(&line_ab, 1, &line_cd, 2); - // assert_eq!( - // collisions, - // Intersection::Line(Point2::new(2.0, 0.0), Point2::new(1.0, 0.0)) - // ); - - // // lines overlap somewhat, both angled - // println!("lines overlap somewhat, both angled"); - // let a = sketch.add_point(0.0, 0.0); - // let b = sketch.add_point(2.0, 2.0); - // let c = sketch.add_point(1.0, 1.0); - // let d = sketch.add_point(3.0, 3.0); - // let line_ab = Line2 { start: a, end: b }; - // let line_cd = Line2 { start: c, end: d }; - // let collisions = sketch.line_line_collisions(&line_ab, 1, &line_cd, 2); - // assert_eq!( - // collisions, - // Intersection::Line(Point2::new(2.0, 2.0), Point2::new(1.0, 1.0)) - // ); - - // one line engulfs the other, both angled - // println!("one line engulfs the other, both angled"); - // let a = sketch.add_point(1.0, 1.0); - // let b = sketch.add_point(2.0, 2.0); - // let c = sketch.add_point(0.0, 0.0); - // let d = sketch.add_point(3.0, 3.0); - // let line_ab = Line2 { start: a, end: b }; - // let line_cd = Line2 { start: c, end: d }; - // let collisions = sketch.line_line_collisions(&line_ab, 1, &line_cd, 2); - // assert_eq!( - // collisions, - // Intersection::Line(Point2::new(1.0, 1.0), Point2::new(2.0, 2.0)) - // ); - } -} diff --git a/packages/cadmium/src/sketch/mod.rs b/packages/cadmium/src/sketch/mod.rs deleted file mode 100644 index 84f733e3..00000000 --- a/packages/cadmium/src/sketch/mod.rs +++ /dev/null @@ -1,1911 +0,0 @@ -#![allow(unused)] -use geo::line_intersection::{line_intersection, LineIntersection}; -use geo::Line; -use geo::{point, Contains}; -use geo::{within, Intersects}; -use serde::{Deserialize, Serialize}; -use serde_with::{serde_as, DisplayFromStr}; -use truck_polymesh::stl::PolygonMeshStlFaceIterator; -use tsify::Tsify; - -use core::panic; -use geo::LineString; -use geo::Polygon; -use indexmap::IndexMap; -use itertools::Itertools; -use std::collections::hash_map::DefaultHasher; -use std::collections::{HashMap, HashSet, VecDeque}; -use std::f64::consts::{PI, TAU}; -use std::hash::{Hash, Hasher}; - -use crate::archetypes::{Circle3, Plane}; -use crate::error::CADmiumError; -use crate::project::{Project, RealSketch}; - -pub(crate) mod constraints; -mod intersections; -mod svg; - -use crate::sketch::constraints::Constraint; - -#[derive(strum::Display, Debug, Serialize, Deserialize)] -pub enum SketchFeatureType { - Point, - Line, - Circle, - Arc, - Constraint, -} - -#[serde_as] -#[derive(Tsify, Debug, Clone, Serialize, Deserialize)] -#[tsify(into_wasm_abi, from_wasm_abi)] -pub struct Sketch { - #[serde_as(as = "HashMap")] - pub points: HashMap, - pub highest_point_id: u64, - - #[serde_as(as = "HashMap")] - pub line_segments: HashMap, - pub highest_line_segment_id: u64, - - #[serde_as(as = "HashMap")] - pub circles: HashMap, - pub highest_circle_id: u64, - - #[serde_as(as = "HashMap")] - pub arcs: HashMap, - pub highest_arc_id: u64, - - #[serde_as(as = "HashMap")] - pub constraints: HashMap, - pub highest_constraint_id: u64, -} - -impl Sketch { - pub fn new() -> Self { - Sketch { - points: HashMap::new(), - highest_point_id: 0, - line_segments: HashMap::new(), - highest_line_segment_id: 0, - circles: HashMap::new(), - highest_circle_id: 0, - arcs: HashMap::new(), - highest_arc_id: 0, - constraints: HashMap::new(), - highest_constraint_id: 0, - } - } - - pub fn from_faces(faces: &Vec, real_sketch: &RealSketch) -> Self { - let mut new_sketch = Sketch::new(); - - // println!("Creating a sketch just from faces"); - // for face in faces { - // println!("Face: {:?}", face); - // } - - new_sketch.points = real_sketch.points_2d.clone(); - - let mut circles: HashMap = HashMap::new(); - let mut line_segments: HashMap = HashMap::new(); - let mut arcs: HashMap = HashMap::new(); - - fn include_ring( - ring: &Ring, - circles: &mut HashMap, - line_segments: &mut HashMap, - arcs: &mut HashMap, - ) { - match ring { - Ring::Circle(circle) => { - let cs = circle.canonical_string(); - let search_result = circles.get(&cs); - match search_result { - Some(existing_circle) => { - circles.remove(&cs); - } - None => { - circles.insert(cs.clone(), circle.clone()); - } - } - } - Ring::Segments(segments) => { - for segment in segments { - match segment { - Segment::Line(line) => { - let cs = line.canonical_string(); - let search_result = line_segments.get(&cs); - - match search_result { - Some(existing_line) => { - line_segments.remove(&cs); - } - None => { - line_segments.insert(cs.clone(), line.clone()); - } - } - } - Segment::Arc(arc) => { - let cs = arc.canonical_string(); - let search_result = arcs.get(&cs); - - match search_result { - Some(existing_arc) => { - arcs.remove(&cs); - } - None => { - arcs.insert(cs.clone(), arc.clone()); - } - } - } - _ => {} - } - } - } - } - } - - for face in faces { - include_ring(&face.exterior, &mut circles, &mut line_segments, &mut arcs); - for ring in &face.holes { - include_ring(ring, &mut circles, &mut line_segments, &mut arcs) - } - } - - for (index, circle) in circles.values().enumerate() { - new_sketch.circles.insert(index as u64, circle.clone()); - } - - for (index, line) in line_segments.values().enumerate() { - new_sketch.line_segments.insert(index as u64, line.clone()); - } - - for (index, arc) in arcs.values().enumerate() { - new_sketch.arcs.insert(index as u64, arc.clone()); - } - - new_sketch - } - - pub fn arc_angle(&self, arc: &Arc2) -> f64 { - let center = self.points.get(&arc.center).unwrap(); - let start = self.points.get(&arc.start).unwrap(); - let end = self.points.get(&arc.end).unwrap(); - - match arc.clockwise { - false => angle(start, center, end), - true => TAU - angle(start, center, end), - } - } - - pub fn arc_end_angle(&self, arc: &Arc2) -> f64 { - let center = self.points.get(&arc.center).unwrap(); - let end = self.points.get(&arc.end).unwrap(); - - let dx = end.x - center.x; - let dy = end.y - center.y; - - if arc.clockwise { - dy.atan2(dx) - PI / 2.0 - } else { - dy.atan2(dx) + PI / 2.0 - } - } - - pub fn arc_start_angle(&self, arc: &Arc2) -> f64 { - let center = self.points.get(&arc.center).unwrap(); - let start = self.points.get(&arc.start).unwrap(); - - let dx = start.x - center.x; - let dy = start.y - center.y; - - if arc.clockwise { - dy.atan2(dx) - PI / 2.0 - } else { - dy.atan2(dx) + PI / 2.0 - } - } - - pub fn line_start_angle(&self, line: &Line2) -> f64 { - let start = self.points.get(&line.start).unwrap(); - let end = self.points.get(&line.end).unwrap(); - - let dx = end.x - start.x; - let dy = end.y - start.y; - - dy.atan2(dx) - } - - pub fn line_end_angle(&self, line: &Line2) -> f64 { - self.line_start_angle(line) - } - - pub fn pretty_print_arc(&self, arc: &Arc2) { - let center = self.points.get(&arc.center).unwrap(); - let start = self.points.get(&arc.start).unwrap(); - let end = self.points.get(&arc.end).unwrap(); - - println!( - "Arc: center: {}: ({}, {}), start: {}: ({}, {}), end: {}: ({}, {}) CW: {}", - arc.center, - center.x, - center.y, - arc.start, - start.x, - start.y, - arc.end, - end.x, - end.y, - arc.clockwise - ); - println!("Start angle:\t{}", self.arc_start_angle(arc) * 180.0 / PI); - println!("End angle: \t{}", self.arc_end_angle(arc) * 180.0 / PI); - println!("Angle: \t{}", self.arc_angle(arc) * 180.0 / PI); - } - - pub fn face_as_polygon(&self, face: &Face) -> Polygon { - let binding = self.as_polygon(&face.exterior); - let exterior = binding.exterior(); - - let mut interiors: Vec> = vec![]; - for ring in &face.holes { - interiors.push(self.as_polygon(ring).exterior().clone()); - } - - Polygon::new(exterior.clone(), interiors) - } - - pub fn as_polygon(&self, ring: &Ring) -> Polygon { - match ring { - Ring::Circle(circle) => { - let mut b: Vec<(f64, f64)> = vec![]; - let center = self.points.get(&circle.center).unwrap(); - - let num_pts = 36; - for i in 0..num_pts { - let angle = i as f64 / num_pts as f64 * TAU; - let x = center.x + circle.radius * angle.cos(); - let y = center.y + circle.radius * angle.sin(); - b.push((x, y)); - } - - let polygon = Polygon::new(LineString::from(b), vec![]); - polygon - } - Ring::Segments(segments) => { - let mut b: Vec<(f64, f64)> = vec![]; - for segment in segments { - match segment { - Segment::Line(line) => { - // we only ever push the start point. Imagine what happens for a closed - // square--the final closing segment is inferred. - let start = self.points.get(&segment.get_start()).unwrap(); - b.push((start.x, start.y)); - } - Segment::Arc(arc) => { - // similarly, we push all the points except the final one. The final - // segment is inferred. - let points = self.arc_to_points(arc); - for point in points { - b.push((point.x, point.y)); - } - b.pop(); - } - } - } - let polygon = Polygon::new(LineString::from(b), vec![]); - polygon - } - } - } - - pub fn arc_to_points(&self, arc: &Arc2) -> Vec { - // println!("An arc to points: {:?}", arc); - let center = self.points.get(&arc.center).unwrap(); - let start = self.points.get(&arc.start).unwrap(); - let end = self.points.get(&arc.end).unwrap(); - let clockwise = arc.clockwise; - - arc_to_points(start, end, center, clockwise) - } - - pub fn signed_area(&self, ring: &Ring) -> f64 { - match ring { - Ring::Circle(circle) => circle.radius * circle.radius * std::f64::consts::PI, - Ring::Segments(segments) => { - let mut area: f64 = 0.0; - - for segment in segments { - match segment { - Segment::Line(line) => { - let end = self.points.get(&segment.get_end()).unwrap(); - let start = self.points.get(&segment.get_start()).unwrap(); - area += (end.x - start.x) * (end.y + start.y); - } - Segment::Arc(arc) => { - let points = self.arc_to_points(arc); - for i in 0..points.len() - 1 { - let end = &points[i + 1]; - let start = &points[i]; - area += (end.x - start.x) * (end.y + start.y); - } - } - } - } - return area / -2.0; - } - } - } - - pub fn add_point(&mut self, x: f64, y: f64) -> u64 { - let id = self.highest_point_id + 1; - self.points.insert(id, Point2::new(x, y)); - self.highest_point_id += 1; - id - } - - pub fn add_hidden_point(&mut self, x: f64, y: f64) -> u64 { - let id = self.highest_point_id + 1; - self.points.insert(id, Point2::new_hidden(x, y)); - self.highest_point_id += 1; - id - } - - pub fn add_point_with_id(&mut self, x: f64, y: f64, id0: u64) -> Result<(), CADmiumError> { - if self.points.contains_key(&id0) { - return Err(CADmiumError::SketchFeatureAlreadyExists( - SketchFeatureType::Point, - id0, - )); - } - if self.highest_point_id >= id0 { - return Err(CADmiumError::SketchFeatureIDTooLow( - SketchFeatureType::Point, - id0, - )); - } - self.points.insert(id0, Point2::new(x, y)); - self.highest_point_id = id0; - Ok(()) - } - - pub fn add_fixed_point(&mut self, x: f64, y: f64) -> u64 { - let id = self.highest_point_id + 1; - self.points.insert(id, Point2::new_fixed(x, y)); - self.highest_point_id += 1; - id - } - - pub fn add_arc(&mut self, center_id: u64, start_id: u64, end_id: u64, clockwise: bool) -> u64 { - let a = Arc2 { - center: center_id, - start: start_id, - end: end_id, - clockwise, - }; - let id = self.highest_arc_id + 1; - self.arcs.insert(id, a); - self.highest_arc_id += 1; - id - } - - pub fn add_circle(&mut self, point_id: u64, radius: f64) -> u64 { - let center_pt = self.points.get(&point_id).unwrap(); - let top = self.add_point(center_pt.x, center_pt.y + radius); - let top_point = self.points.get_mut(&top).unwrap(); - top_point.hidden = true; // sneaky! - let c = Circle2 { - center: point_id, - radius, - top, - }; - let id = self.highest_circle_id + 1; - self.circles.insert(id, c); - self.highest_circle_id += 1; - id - } - - pub fn add_circle_between_points(&mut self, center_id: u64, edge_id: u64) -> u64 { - let center_pt = self.points.get(¢er_id).unwrap(); - let edge_pt = self.points.get(&edge_id).unwrap(); - let radius = center_pt.distance_to(edge_pt); - let c = Circle2 { - center: center_id, - radius, - top: edge_id, - }; - let id = self.highest_circle_id + 1; - self.circles.insert(id, c); - self.highest_circle_id += 1; - id - } - - pub fn add_rectangle_between_points( - &mut self, - start_id: u64, - end_id: u64, - ) -> (Vec, Vec) { - let start = self.points.get(&start_id).unwrap(); - let end = self.points.get(&end_id).unwrap(); - - let dx = end.x - start.x; - let dy = end.y - start.y; - - let mut points = vec![]; - let mut segments = vec![]; - - // create the two missing points - let p0 = { - let start_point = self.points.get(&start_id).unwrap(); - self.add_point(start_point.x + dx, start_point.y) - }; - let p1 = { - let start_point = self.points.get(&start_id).unwrap(); - self.add_point(start_point.x, start_point.y + dy) - }; - - points.push(p0); - points.push(p1); - - let s0 = self.add_segment(start_id, p1); - let s1 = self.add_segment(p1, end_id); - let s2 = self.add_segment(end_id, p0); - let s3 = self.add_segment(p0, start_id); - - segments.push(s0); - segments.push(s1); - segments.push(s2); - segments.push(s3); - - (points, segments) - } - - pub fn add_segment(&mut self, id0: u64, id1: u64) -> u64 { - let l = Line2 { - start: id0, - end: id1, - }; - let id = self.highest_line_segment_id + 1; - self.line_segments.insert(id, l); - self.highest_line_segment_id += 1; - id - } - - pub fn add_line_segment(&mut self, x0: f64, y0: f64, x1: f64, y1: f64) -> u64 { - let id0 = self.add_point(x0, y0); - let id1 = self.add_point(x1, y1); - let l = Line2 { - start: id0, - end: id1, - }; - let id = self.highest_line_segment_id + 1; - self.line_segments.insert(id, l); - self.highest_line_segment_id += 1; - id - } - - pub fn delete_circle(&mut self, id: u64) { - let center_point_id = self.circles.get(&id).unwrap().center; - let top_point_id = self.circles.get(&id).unwrap().top; - let mut center_is_safe = false; - let mut top_is_safe = false; - - for (line_id, line) in self.line_segments.iter() { - if line.start == center_point_id || line.end == center_point_id { - center_is_safe = true; - } - if line.start == top_point_id || line.end == top_point_id { - top_is_safe = true; - } - } - - for (arc_id, arc) in self.arcs.iter() { - if arc.start == center_point_id - || arc.end == center_point_id - || arc.center == center_point_id - { - center_is_safe = true; - } - if arc.start == top_point_id || arc.end == top_point_id || arc.center == top_point_id { - top_is_safe = true; - } - } - - for (circle_id, circle) in self.circles.iter() { - if *circle_id != id { - if circle.center == center_point_id || circle.top == center_point_id { - center_is_safe = true; - } - if circle.center == top_point_id || circle.top == top_point_id { - top_is_safe = true; - } - } - } - - if !center_is_safe { - self.points.remove(¢er_point_id); - } - if !top_is_safe { - self.points.remove(&top_point_id); - } - - self.circles.remove(&id); - } - - pub fn delete_arc(&mut self, id: u64) { - // TODO: return a result instead of crashing if the arc doesn't exist - // TODO: remove any constraints that reference this arc - let start_point_id = self.arcs.get(&id).unwrap().start; - let end_point_id = self.arcs.get(&id).unwrap().end; - let center_point_id = self.arcs.get(&id).unwrap().center; - let mut start_is_safe = false; - let mut end_is_safe = false; - let mut center_is_safe = false; - - for (line_id, line) in self.line_segments.iter() { - if line.start == start_point_id || line.end == start_point_id { - start_is_safe = true; - } - if line.start == end_point_id || line.end == end_point_id { - end_is_safe = true; - } - if line.start == center_point_id || line.end == center_point_id { - center_is_safe = true; - } - } - for (arc_id, arc) in self.arcs.iter() { - if (*arc_id != id) { - if arc.start == start_point_id - || arc.end == start_point_id - || arc.center == start_point_id - { - start_is_safe = true; - } - if arc.start == end_point_id - || arc.end == end_point_id - || arc.center == end_point_id - { - end_is_safe = true; - } - if arc.start == center_point_id - || arc.end == center_point_id - || arc.center == center_point_id - { - center_is_safe = true; - } - } - } - for (circle_id, circle) in self.circles.iter() { - if circle.center == start_point_id || circle.top == start_point_id { - start_is_safe = true; - } - if circle.center == end_point_id || circle.top == end_point_id { - end_is_safe = true; - } - if circle.center == center_point_id || circle.top == center_point_id { - center_is_safe = true; - } - } - if !start_is_safe { - self.points.remove(&start_point_id); - } - if !end_is_safe { - self.points.remove(&end_point_id); - } - if !center_is_safe { - self.points.remove(¢er_point_id); - } - - self.arcs.remove(&id); - } - - pub fn delete_line_segment(&mut self, id: u64) { - // TODO: return a result instead of crashing if the line segment doesn't exist - // TODO: remove any constraints that reference this line segment - let start_point_id = self.line_segments.get(&id).unwrap().start; - let end_point_id = self.line_segments.get(&id).unwrap().end; - let mut start_is_safe = false; - let mut end_is_safe = false; - for (line_id, line) in self.line_segments.iter() { - if *line_id != id { - if line.start == start_point_id || line.end == start_point_id { - start_is_safe = true; - } - if line.start == end_point_id || line.end == end_point_id { - end_is_safe = true; - } - } - } - for (arc_id, arc) in self.arcs.iter() { - if arc.start == start_point_id - || arc.end == start_point_id - || arc.center == start_point_id - { - start_is_safe = true; - } - if arc.start == end_point_id || arc.end == end_point_id || arc.center == end_point_id { - end_is_safe = true; - } - } - for (circle_id, circle) in self.circles.iter() { - if circle.center == start_point_id || circle.top == start_point_id { - start_is_safe = true; - } - if circle.center == end_point_id || circle.top == end_point_id { - end_is_safe = true; - } - } - if !start_is_safe { - self.points.remove(&start_point_id); - } - if !end_is_safe { - self.points.remove(&end_point_id); - } - - self.line_segments.remove(&id); - } - - pub fn add_line_with_id( - &mut self, - start_id: u64, - end_id: u64, - id: u64, - ) -> Result<(), CADmiumError> { - if self.line_segments.contains_key(&id) { - return Err(CADmiumError::SketchFeatureAlreadyExists( - SketchFeatureType::Line, - id, - )); - } - if self.highest_line_segment_id >= id { - return Err(CADmiumError::SketchFeatureIDTooLow( - SketchFeatureType::Line, - id, - )); - } - if !self.points.contains_key(&start_id) { - return Err(CADmiumError::SketchFeatureMissingStart( - SketchFeatureType::Line, - id, - )); - } - if !self.points.contains_key(&end_id) { - return Err(CADmiumError::SketchFeatureMissingEnd( - SketchFeatureType::Line, - id, - )); - } - - let l = Line2 { - start: start_id, - end: end_id, - }; - self.line_segments.insert(id, l); - self.highest_line_segment_id = id; - Ok(()) - } - - pub fn segment_length(&self, segment_id: u64) -> f64 { - let segment = self.line_segments.get(&segment_id).unwrap(); - let start = self.points.get(&segment.start).unwrap(); - let end = self.points.get(&segment.end).unwrap(); - let dx = end.x - start.x; - let dy = end.y - start.y; - dx.hypot(dy) - } - - pub fn segment_angle(&self, segment_id: u64) -> f64 { - let segment = self.line_segments.get(&segment_id).unwrap(); - let start = self.points.get(&segment.start).unwrap(); - let end = self.points.get(&segment.end).unwrap(); - start.angle_to(end) - } - - fn apply_length_forces( - &mut self, - point_a_id: u64, - point_b_id: u64, - rest: f64, - kp: f64, - kd: f64, - ) { - let mut fx = 0.0; - let mut fy = 0.0; - let mut pa_hidden = false; - let mut pb_hidden = false; - { - let point_a = self.points.get(&point_a_id).unwrap(); - let point_b = self.points.get(&point_b_id).unwrap(); - - let dx = point_b.x - point_a.x; - let dy = point_b.y - point_a.y; - let dist = dx.hypot(dy); - let err = dist - rest; - - let relative_dx = point_b.dx - point_a.dx; - let relative_dy = point_b.dy - point_a.dy; - - // project the relative velocity onto the vector between the points - // a is the velocity - // b is the vector between the points - // a dot b / |b| - let closing_velocity = (relative_dx * dx + relative_dy * dy) / dist; - - let f = kp * err + kd * closing_velocity; - fx = f * dx / dist; - fy = f * dy / dist; - - pa_hidden = point_a.hidden; - pb_hidden = point_b.hidden; - } - - // if a point is hidden, it feels forces but does not exert them - if !pa_hidden { - let point_b = self.points.get_mut(&point_b_id).unwrap(); - point_b.fx -= fx; - point_b.fy -= fy; - } - if !pb_hidden { - let point_a = self.points.get_mut(&point_a_id).unwrap(); - point_a.fx += fx; - point_a.fy += fy; - } - } - - fn apply_torsion_forces( - &mut self, - point_a_id: u64, - point_b_id: u64, - rest: f64, - kp: f64, - kd: f64, - ) { - let mut fx = 0.0; - let mut fy = 0.0; - { - let point_a = self.points.get(&point_a_id).unwrap(); - let point_b = self.points.get(&point_b_id).unwrap(); - - let dt = 0.01; - - let angle = (point_b.y - point_a.y).atan2(point_b.x - point_a.x); - - let mut err = rest - angle; - // println!("Err: {}", err); - if err > PI { - err = err - PI * 2.0; - } - if err < -PI { - err = err + PI * 2.0; - } - - let point_a_stepped = point_a.step(dt); - let point_b_stepped = point_b.step(dt); - let angle_stepped = (point_b_stepped.1 - point_a_stepped.1) - .atan2(point_b_stepped.0 - point_a_stepped.0); - let mut angle_change = angle_stepped - angle; - // println!("Dangle: {}", angle_change); - - if angle_change > PI { - angle_change = angle_change - PI * 2.0; - } - if angle_change < -PI { - angle_change = angle_change + PI * 2.0; - } - - let d_angle = angle_change / dt; - let torque = kp * err - kd * d_angle; - - let dx = point_b.x - point_a.x; - let dy = point_b.y - point_a.y; - let dist = dx.hypot(dy); - - let f_mag = torque / dist; - - fx = f_mag * dy; - fy = -f_mag * dx; - } - - let point_a = self.points.get_mut(&point_a_id).unwrap(); - point_a.fx += fx; - point_a.fy += fy; - - let point_b = self.points.get_mut(&point_b_id).unwrap(); - point_b.fx -= fx; - point_b.fy -= fy; - } - - pub fn solve(&mut self, steps: u64) -> bool { - let tolerance = 1e-12; - - for _ in 0..steps { - let retval = self.take_a_step(); - if retval < tolerance { - return true; - } - } - return false; - } - - pub fn apply_forces(&mut self, constraint_id: u64) { - let constraint = self.constraints.get(&constraint_id).unwrap().clone(); - - match constraint { - Constraint::SegmentsEqual { - segment_a_id, - segment_b_id, - kp, - kd, - .. - } => { - let a = self.line_segments.get(&segment_a_id).unwrap(); - let b = self.line_segments.get(&segment_b_id).unwrap(); - - // TODO: is there a better way to satisfy the borrow checker? - let mut average_length = 0.0; - let mut a_start = 0; - let mut b_start = 0; - let mut a_end = 0; - let mut b_end = 0; - { - average_length = (self.segment_length(segment_a_id) - + self.segment_length(segment_b_id)) - / 2.0; - a_start = a.start; - b_start = b.start; - a_end = a.end; - b_end = b.end; - } - self.apply_length_forces(a_start, a_end, average_length, kp, kd); - self.apply_length_forces(b_start, b_end, average_length, kp, kd); - } - Constraint::SegmentLength { - segment_id, - length, - kp, - kd, - .. - } => { - let segment = self.line_segments.get(&segment_id).unwrap(); - self.apply_length_forces(segment.start, segment.end, length, kp, kd) - } - Constraint::CircleDiameter { - circle_id, - diameter, - kp, - kd, - .. - } => { - let circle = self.circles.get(&circle_id).unwrap(); - let center = self.points.get(&circle.center).unwrap(); - let top = self.points.get(&circle.top).unwrap(); - let radius = center.distance_to(top); - - self.apply_length_forces(circle.center, circle.top, diameter / 2.0, kp, kd) - } - Constraint::SegmentAngle { - segment_id, - angle, - kp, - kd, - .. - } => { - let segment = self.line_segments.get(&segment_id).unwrap(); - self.apply_torsion_forces(segment.start, segment.end, angle, kp, kd); - } - } - } - - pub fn take_a_step(&mut self) -> f64 { - let dt = 0.02; // at 0.04 the system can be unstable! especially manual_rectangle() - // TODO: switch to RK4? - let mut biggest_change = 0.0; - for (_point_id, point) in self.points.iter_mut() { - point.reset_forces(); - } - - let constraint_keys = self - .constraints - .keys() - .sorted() - .map(|k| k.clone()) - .collect::>(); - for constraint_id in constraint_keys { - self.apply_forces(constraint_id); - } - - for point in self.points.values_mut() { - point.apply_drag_force(); - } - - for (point_id, point) in self.points.iter_mut() { - if point.fixed { - continue; - } - let ax = point.fx / point.m; - let ay = point.fy / point.m; - point.dx += ax; - point.dy += ay; - let delta_x = 0.5 * ax * dt * dt + point.dx * dt; - let delta_y = 0.5 * ay * dt * dt + point.dy * dt; - - if delta_x.abs() > biggest_change { - biggest_change = delta_x.abs(); - } - if delta_y.abs() > biggest_change { - biggest_change = delta_y.abs(); - } - - point.x += delta_x; - point.y += delta_y; - } - - // update any circles whose radii might have changed! - for (_circle_id, circle) in self.circles.iter_mut() { - let center = self.points.get(&circle.center).unwrap(); - let top = self.points.get(&circle.top).unwrap(); - circle.radius = center.distance_to(top); - } - - biggest_change - } - - pub fn print_state_minimal(&self) { - let mut data = vec![]; - for (point_id, point) in self.points.iter().sorted_by_key(|(id, _)| *id) { - data.push(*point_id as f64); - data.push(point.x); - data.push(point.y); - } - let strings = data.iter().map(|x| x.to_string()).collect::>(); - println!("{}", strings.join(",")); - } - - pub fn print_state(&self) { - let mut data = vec![]; - for (_point_id, point) in self.points.iter() { - data.push(point.x); - data.push(point.y); - data.push(point.dx); - data.push(point.dy); - data.push(point.fx); - data.push(point.fy); - } - let strings = data.iter().map(|x| x.to_string()).collect::>(); - println!("{}", strings.join(",")); - } - - pub fn find_faces(&self) -> (Vec, Vec) { - let mut segments_overall: Vec = vec![]; - - for line_id in self.line_segments.keys().sorted() { - let line = self.line_segments.get(line_id).unwrap(); - segments_overall.push(Segment::Line(line.clone())); - } - for arc_id in self.arcs.keys().sorted() { - let arc = self.arcs.get(arc_id).unwrap(); - segments_overall.push(Segment::Arc(arc.clone())); - } - - let (rings, unused_segments) = self.find_rings(segments_overall, false); - // println!("Found {} rings", rings.len()); - // for ring in &rings { - // println!("{:?}", ring); - // } - // println!("Found {} unused segments", unused_segments.len()); - let mut faces: Vec = rings.iter().map(|r| Face::from_ring(r)).collect(); - - if rings.len() == 0 { - return (faces, unused_segments); - } - - // this next block of code converts everything to Polygons just so we can - // determine what faces contain which other faces. It's a bit of a waste - // because geo is a relatively heavy dependency and we don't need all of it - let polygons: Vec = rings.iter().map(|r| self.as_polygon(r)).collect(); - // they are already sorted from smallest to largest area - self.find_rings does this - let mut what_contains_what: Vec<(usize, usize)> = vec![]; - - for smaller_polygon_index in 0..polygons.len() - 1 { - let smaller_polygon = &polygons[smaller_polygon_index]; - - for bigger_polygon_index in smaller_polygon_index + 1..polygons.len() { - let bigger_polygon = &polygons[bigger_polygon_index]; - let inside = bigger_polygon.contains(smaller_polygon); - - if inside { - what_contains_what.push((bigger_polygon_index, smaller_polygon_index)); - break; - } - } - } - - // cool, now we know what faces contain which other faces. Let's just add the holes - for (bigger_index, smaller_index) in what_contains_what { - let smaller_face = &faces[smaller_index].clone(); - faces[bigger_index].add_hole(smaller_face) - } - - // let faces: Vec = polygons.iter().map(|p| Face::from_polygon(p)).collect(); - (faces, unused_segments) - } - - pub fn find_rings(&self, segments: Vec, debug: bool) -> (Vec, Vec) { - // We are handed all of the segments to consider - let mut segments_overall = segments.clone(); - let num_segments = segments_overall.len(); - - // Let's double it by reversing each one and adding it to the list of - // segments to consider - let segments_reversed: Vec = - segments_overall.iter().map(|s| s.reverse()).collect(); - segments_overall.extend(segments_reversed); - - // keep track of every index we've already used--each segment can only be used once - let mut used_indices: Vec = vec![]; - // this is the output variable - let mut new_rings: Vec> = vec![]; - - for (seg_idx, s) in segments_overall.iter().enumerate() { - if debug { - // println!("Starting a loop with segment: {:?}", s); - match s { - Segment::Line(line) => { - println!( - "Line: ({}, {}) to ({}, {})", - self.points.get(&line.start).unwrap().x, - self.points.get(&line.start).unwrap().y, - self.points.get(&line.end).unwrap().x, - self.points.get(&line.end).unwrap().y - ); - } - Segment::Arc(arc) => { - println!( - "Arc: center: ({}, {}), start: ({}, {}), end: ({}, {})", - self.points.get(&arc.center).unwrap().x, - self.points.get(&arc.center).unwrap().y, - self.points.get(&arc.start).unwrap().x, - self.points.get(&arc.start).unwrap().y, - self.points.get(&arc.end).unwrap().x, - self.points.get(&arc.end).unwrap().y - ); - } - } - } - if used_indices.contains(&seg_idx) { - if debug { - println!("Skipping because it's been used"); - } - continue; - } - // this variable will accumulate the indices of our new ring - let mut new_ring_indices: Vec = vec![]; - let starting_point = s.get_start(); - if debug { - println!("Starting point: {:?}", starting_point); - } - - let mut next_segment_index: usize = seg_idx; - for _i in 1..segments_overall.len() { - let next_segment = segments_overall.get(next_segment_index).unwrap(); - if debug { - println!("next segment: {:?}", next_segment); - } - new_ring_indices.push(next_segment_index); - - match self.find_next_segment_index( - &segments_overall, - next_segment, - &used_indices, - debug, - ) { - None => { - if debug { - println!("\tno viable next segments!"); - } - break; - } - Some(idx) => next_segment_index = idx, - } - if next_segment.get_end() == starting_point { - if debug { - println!("\tomg finished!"); - println!("\tring indices: {:?}", new_ring_indices); - } - new_rings.push(new_ring_indices.clone()); - used_indices.extend(new_ring_indices); - break; - } - } - } - - let used_indices_set = used_indices.iter().cloned().collect::>(); - let all_indices_set = (0..segments_overall.len()).collect::>(); - - let unused_indices_set = all_indices_set - .difference(&used_indices_set) - .collect::>(); - let unused_indices = unused_indices_set - .iter() - .cloned() - .filter(|idx| return *idx < &num_segments) - .collect::>(); - let unused_segments = unused_indices - .iter() - .cloned() - .map(|idx| segments_overall.get(*idx).unwrap().clone()) - .collect::>(); - - let mut all_rings: Vec = vec![]; - for ring_indices in new_rings.iter() { - // let mut this_ring: Ring = Ring::Segments(vec![]); - let mut this_ring: Vec = vec![]; - for segment_index in ring_indices { - let actual_segment: &Segment = segments_overall.get(*segment_index).unwrap(); - this_ring.push(actual_segment.clone()); - } - all_rings.push(Ring::Segments(this_ring)); - } - - // println!("--Found {} rings", all_rings.len()); - - // Circles are trivially rings! - for (_circle_id, circle) in self.circles.iter() { - all_rings.push(Ring::Circle(circle.clone())); - } - - all_rings.sort_by(|r1, r2| { - // TODO: implement signed_area for a ring which is made of arcs - self.signed_area(r1) - .partial_cmp(&self.signed_area(r2)) - .unwrap() - }); - - // filter out to only the positive-valued ones - all_rings = all_rings - .iter() - .filter(|r| self.signed_area(r) > 0.0) - .cloned() - .collect(); - - // println!("--Found {} rings", all_rings.len()); - - (all_rings, unused_segments) - } - - pub fn find_next_segment_index( - &self, - segments: &Vec, - starting_segment: &Segment, - used_indices: &Vec, - debug: bool, - ) -> Option { - // println!("Finding next segment index"); - let mut matches: Vec<(usize, f64, f64)> = vec![]; - let mut this_segment_end_angle = match starting_segment { - Segment::Line(line) => self.line_end_angle(line), - Segment::Arc(arc) => self.arc_end_angle(arc), - }; - this_segment_end_angle = (this_segment_end_angle + PI) % (2.0 * PI); - - for (idx, s2) in segments.iter().enumerate() { - if used_indices.contains(&idx) { - continue; - } - if s2.continues(&starting_segment) && !s2.equals_or_reverse_equals(&starting_segment) { - let starting_angle = match s2 { - Segment::Line(line) => self.line_start_angle(line), - Segment::Arc(arc) => self.arc_start_angle(arc), - }; - let angle_diff = angle_difference(this_segment_end_angle, starting_angle); - matches.push((idx, starting_angle, angle_diff)); - // angle_diff measures how hard you'd have to turn left to continue the path from - // starting_segment to s2, where a straight line would be 180, a left turn 270, a right turn 90. - // This is important later because to make the smallest loops possible, we always want to be - // turning left as hard as possible when finding rings. - } - } - - if matches.len() == 0 { - None - } else if matches.len() == 1 { - Some(matches[0].0) - } else { - if debug { - println!("\tMultiple options! Deciding which one to take..."); - } - - let mut best_option = 0; - let mut hardest_left_turn = 0.0; - for o in matches.iter() { - // println!("Option: {:?}", segments.get(o.0).unwrap()); - // println!("Option: {} angle {}", o.0, o.1 * 180.0 / PI); - // println!("Option: {}", o.2 * 180.0 / PI); - // println!(); - - if o.2 > hardest_left_turn { - hardest_left_turn = o.2; - best_option = o.0; - } - } - // println!("Best option: {}", best_option); - Some(best_option) - } - } - - pub fn circle_intersection(&self, circle_a: &Circle2, circle_b: &Circle2) -> Vec { - // See https://math.stackexchange.com/questions/256100/how-can-i-find-the-points-at-which-two-circles-intersect#comment4306998_1367732 - // See https://gist.github.com/jupdike/bfe5eb23d1c395d8a0a1a4ddd94882ac - let center_a = self.points.get(&circle_a.center).unwrap(); - let center_b = self.points.get(&circle_b.center).unwrap(); - let r_a = circle_a.radius; - let r_b = circle_b.radius; - - let center_dx = center_b.x - center_a.x; - let center_dy = center_b.y - center_a.y; - let center_dist = center_dx.hypot(center_dy); - - if !(center_dist <= r_a + r_b && center_dist >= r_a - r_b) { - return vec![]; - } - - let r_2 = center_dist * center_dist; - let r_4 = r_2 * r_2; - let a = (r_a * r_a - r_b * r_b) / (2.0 * r_2); - let r_2_r_2 = r_a * r_a - r_b * r_b; - let c = (2.0 * (r_a * r_a + r_b * r_b) / r_2 - r_2_r_2 * r_2_r_2 / r_4 - 1.0).sqrt(); - - let fx = (center_a.x + center_b.x) / 2.0 + a * (center_b.x - center_a.x); - let gx = c * (center_b.y - center_a.y) / 2.0; - let ix1 = fx + gx; - let ix2 = fx - gx; - - let fy = (center_a.y + center_b.y) / 2.0 + a * (center_b.y - center_a.y); - let gy = c * (center_a.x - center_b.x) / 2.0; - let iy1 = fy + gy; - let iy2 = fy - gy; - - vec![Point2::new(ix1, iy1), Point2::new(ix2, iy2)] - } - - pub fn line_intersection(&self, line_a: &Line2, line_b: &Line2) -> Option { - let start_a = self.points.get(&line_a.start).unwrap(); - let end_a = self.points.get(&line_a.end).unwrap(); - let start_b = self.points.get(&line_b.start).unwrap(); - let end_b = self.points.get(&line_b.end).unwrap(); - - let line_a = Line::new( - geo::Coord { - x: start_a.x, - y: start_a.y, - }, - geo::Coord { - x: end_a.x, - y: end_a.y, - }, - ); - let line_b = Line::new( - geo::Coord { - x: start_b.x, - y: start_b.y, - }, - geo::Coord { - x: end_b.x, - y: end_b.y, - }, - ); - - let intersection = line_intersection(line_a, line_b); - - match intersection { - Some(line_intersection) => match line_intersection { - LineIntersection::SinglePoint { - intersection, - is_proper, - } => Some(Point2::new(intersection.x, intersection.y)), - LineIntersection::Collinear { intersection } => panic!("Collinear!"), - }, - None => None, - } - } -} - -pub fn arc_to_points( - start: &Point2, - end: &Point2, - center: &Point2, - clockwise: bool, -) -> Vec { - let r = (center.x - start.x).hypot(center.y - start.y); - let circle_tolerance: f64 = 0.001; // in meters - let k = circle_tolerance / r; - let mut n = (PI / (2.0 * k).sqrt()).ceil() as i64; - - let segment_angle = (2.0 * PI) / n as f64; - let segment_length = r * segment_angle; - let start_angle = (start.y - center.y).atan2(start.x - center.x); - - let mut line_vertices: Vec = vec![]; - line_vertices.push(Point2::new(start.x, start.y)); - - if clockwise { - n = -n; - } - - for i in 1..n.abs() { - let theta = ((2.0 * PI) / n as f64) * i as f64 + start_angle; - let x_component = r * theta.cos(); - let y_component = r * theta.sin(); - let point = Point2::new(x_component + center.x, y_component + center.y); - line_vertices.push(point.clone()); - - let distance_to_end = point.distance_to(end); - if (distance_to_end <= segment_length) { - line_vertices.push(Point2::new(end.x, end.y)); - break; - } - } - - line_vertices -} - -pub struct IncrementingMap { - pub items: IndexMap, - next_id: u64, -} - -impl IncrementingMap { - pub fn new() -> Self { - IncrementingMap { - items: IndexMap::new(), - next_id: 0, - } - } - - pub fn add_item(&mut self, item: T) -> u64 { - let id = self.next_id; - self.items.insert(id, item); - self.next_id += 1; - id - } - - pub fn remove_item(&mut self, id: u64) -> u64 { - self.items.remove(&id); - id - } - - pub fn get_item(&self, id: u64) -> Option<&T> { - self.items.get(&id) - } -} - -#[derive(Tsify, Debug, Clone, Serialize, Deserialize, PartialEq)] -#[tsify(into_wasm_abi, from_wasm_abi)] -pub struct Point2 { - pub x: f64, - pub y: f64, - m: f64, - dx: f64, - dy: f64, - fx: f64, - fy: f64, - fixed: bool, - pub hidden: bool, -} - -impl Point2 { - pub fn new(x: f64, y: f64) -> Self { - Point2 { - x, - y, - m: 1.0, - dx: 0.0, - dy: 0.0, - fx: 0.0, - fy: 0.0, - fixed: false, - hidden: false, - } - } - - pub fn new_fixed(x: f64, y: f64) -> Self { - Point2 { - x, - y, - m: 1.0, - dx: 0.0, - dy: 0.0, - fx: 0.0, - fy: 0.0, - fixed: true, - hidden: false, - } - } - - pub fn new_hidden(x: f64, y: f64) -> Self { - Point2 { - x, - y, - m: 1.0, - dx: 0.0, - dy: 0.0, - fx: 0.0, - fy: 0.0, - fixed: false, - hidden: true, - } - } - - fn reset_forces(&mut self) { - self.fx = 0.0; - self.fy = 0.0; - } - - pub fn apply_drag_force(&mut self) { - let drag_coefficient = 0.1; - let drag_force = -drag_coefficient * self.dx; - self.fx += drag_force; - let drag_force = -drag_coefficient * self.dy; - self.fy += drag_force; - } - - fn step(&self, dt: f64) -> (f64, f64) { - (self.x + self.dx * dt, self.y + self.dy * dt) - } - - fn distance_to(&self, other: &Point2) -> f64 { - let dx = self.x - other.x; - let dy = self.y - other.y; - dx.hypot(dy) - } - - fn angle_to(&self, other: &Point2) -> f64 { - (other.y - self.y).atan2(other.x - self.x) - } -} - -#[derive(Tsify, Debug, Clone, Serialize, Deserialize)] -#[tsify(into_wasm_abi, from_wasm_abi)] -pub struct Vector2 { - pub x: f64, - pub y: f64, -} - -impl Vector2 { - pub fn new(x: f64, y: f64) -> Self { - Vector2 { x, y } - } -} - -#[derive(Tsify, Debug, Clone, Serialize, Deserialize, PartialEq)] -#[tsify(into_wasm_abi, from_wasm_abi)] -pub struct Circle2 { - pub center: u64, - pub radius: f64, - pub top: u64, -} - -impl Circle2 { - pub fn equals(&self, other: &Self) -> bool { - self.center == other.center && self.radius == other.radius - } - - pub fn canonical_string(&self) -> String { - format!("{}-{}", self.center, self.radius) - } -} - -#[derive(Tsify, Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)] -#[tsify(into_wasm_abi, from_wasm_abi)] -pub struct Arc2 { - pub center: u64, - pub start: u64, - pub end: u64, - pub clockwise: bool, -} - -impl Arc2 { - pub fn reverse(&self) -> Self { - Arc2 { - center: self.center, - start: self.end, - end: self.start, - clockwise: !self.clockwise, - } - } - - pub fn canonical_string(&self) -> String { - if self.start < self.end { - format!( - "{}-{}-{}-{}", - self.start, self.end, self.center, self.clockwise - ) - } else { - self.reverse().canonical_string() - } - } -} - -#[derive(Tsify, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[tsify(into_wasm_abi, from_wasm_abi)] -pub struct Line2 { - pub start: u64, - pub end: u64, -} - -impl Line2 { - pub fn reverse(&self) -> Self { - Line2 { - start: self.end, - end: self.start, - } - } - - pub fn canonical_string(&self) -> String { - if self.start < self.end { - format!("{}-{}", self.start, self.end) - } else { - format!("{}-{}", self.end, self.start) - } - } -} - -#[derive(Tsify, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[serde(tag = "type")] -#[tsify(into_wasm_abi, from_wasm_abi)] -pub enum Segment { - Line(Line2), - Arc(Arc2), -} - -impl Segment { - pub fn reverse(&self) -> Self { - match self { - Segment::Line(line) => Segment::Line(line.reverse()), - Segment::Arc(arc) => Segment::Arc(arc.reverse()), - } - } - - pub fn get_start(&self) -> u64 { - match self { - Segment::Line(line) => line.start, - Segment::Arc(arc) => arc.start, - } - } - - pub fn get_end(&self) -> u64 { - match self { - Segment::Line(line) => line.end, - Segment::Arc(arc) => arc.end, - } - } - - pub fn continues(&self, prior_segment: &Segment) -> bool { - // determines if this segment continues the prior segment - prior_segment.get_end() == self.get_start() - } - - pub fn equals_or_reverse_equals(&self, other: &Self) -> bool { - self == other || self == &other.reverse() - } - - pub fn reverse_equals(&self, other: &Self) -> bool { - self == &other.reverse() - } -} - -#[derive(Tsify, Debug, Clone, Serialize, Deserialize)] -#[tsify(into_wasm_abi, from_wasm_abi)] -pub enum Ring { - Circle(Circle2), - Segments(Vec), -} - -impl Ring { - pub fn adjacent_edges(&self, other: &Self) -> Option<(Vec, Vec)> { - match (self, other) { - (Ring::Segments(segments_a), Ring::Segments(segments_b)) => { - let mut edge_indices_a: Vec = vec![]; - let mut edge_indices_b: Vec = vec![]; - for (index_a, segment_a) in segments_a.iter().enumerate() { - for (index_b, segment_b) in segments_b.iter().enumerate() { - if segment_a.reverse_equals(segment_b) { - edge_indices_a.push(index_a); - edge_indices_b.push(index_b); - } - } - } - if edge_indices_a.len() == 0 { - return None; - } else { - Some((edge_indices_a, edge_indices_b)) - } - } - _ => None, - } - } - - pub fn equals(&self, other: &Self) -> bool { - match (self, other) { - (Ring::Circle(circle_a), Ring::Circle(circle_b)) => circle_a.equals(circle_b), - (Ring::Segments(segments_a), Ring::Segments(segments_b)) => { - segments_a.len() == segments_b.len() - && segments_a - .iter() - .zip(segments_b.iter()) - .all(|(a, b)| a == b) - } - _ => false, - } - } - - pub fn canonical_form(&self) -> Self { - // sort the segments in order by first finding the segment with the smallest start point - // and then rotating the list so that that segment is first - match self { - Ring::Circle(circle) => Ring::Circle(circle.clone()), - Ring::Segments(segments) => { - let mut canonical_segments: Vec = vec![]; - let mut min_index = 0; - let mut min_segment = segments.get(0).unwrap(); - for (i, segment) in segments.iter().enumerate() { - if segment.get_start() < min_segment.get_start() { - min_index = i; - min_segment = segment; - } - } - - for i in 0..segments.len() { - canonical_segments.push( - segments - .get((i + min_index) % segments.len()) - .unwrap() - .clone(), - ); - } - - Ring::Segments(canonical_segments) - } - } - } - - pub fn reverse(&self) -> Self { - match self { - Ring::Circle(circle) => Ring::Circle(circle.clone()), - Ring::Segments(segments) => { - let mut reversed_segments: Vec = vec![]; - for segment in segments.iter().rev() { - reversed_segments.push(segment.reverse()); - } - Ring::Segments(reversed_segments) - } - } - } -} - -#[derive(Tsify, Debug, Clone, Serialize, Deserialize)] -#[tsify(into_wasm_abi, from_wasm_abi)] -pub struct Face { - pub exterior: Ring, - pub holes: Vec, -} - -impl Face { - pub fn from_ring(ring: &Ring) -> Face { - Face { - exterior: ring.clone(), - holes: vec![], - } - } - - pub fn add_hole(&mut self, hole: &Face) { - self.holes.push(hole.exterior.clone()); - } -} - -pub fn angle(a: &Point2, b: &Point2, c: &Point2) -> f64 { - // output range is (0, 2*PI] - let ba_dx: f64 = a.x - b.x; - let ba_dy: f64 = a.y - b.y; - let ba_angle: f64 = ba_dy.atan2(ba_dx); - - let bc_dx = c.x - b.x; - let bc_dy = c.y - b.y; - let bc_angle = bc_dy.atan2(bc_dx); - - let mut naive_angle = bc_angle - ba_angle; - if naive_angle <= 0.0 { - naive_angle += TAU; - } - naive_angle -} - -pub fn min_angle_diff(a0: f64, a1: f64) -> f64 { - let path_a = angle_difference(a0, a1); - let path_b = angle_difference(a1, a0); - if path_a < path_b { - path_a - } else { - path_b - } -} - -pub fn angle_difference(mut a0: f64, mut a1: f64) -> f64 { - if a0 > TAU { - a0 -= TAU; - } - if a0 < 0.0 { - a0 += TAU; - } - - if a1 > TAU { - a1 -= TAU; - } - if a1 < 0.0 { - a1 += TAU; - } - - let mut naive_diff = a1 - a0; - if naive_diff > TAU { - naive_diff -= TAU; - } - if naive_diff < 0.0 { - naive_diff += TAU; - } - - naive_diff -} - -#[cfg(test)] -mod tests { - use crate::project::Project; - - use super::*; - - #[test] - fn arc_to_points_90() { - let mut sketch = Sketch::new(); - let center = sketch.add_point(0.0, 0.0); - let start = sketch.add_point(1.0, 0.0); - let end = sketch.add_point(0.0, 1.0); - let arc_id = sketch.add_arc(center, start, end, false); - let arc = sketch.arcs.get(&arc_id).unwrap(); - - let points = sketch.arc_to_points(&arc); - assert_eq!(points.len(), 19); - } - - #[test] - fn arc_to_points_neg_90() { - let mut sketch = Sketch::new(); - let center = sketch.add_point(0.0, 0.0); - let start = sketch.add_point(0.0, 1.0); - let end = sketch.add_point(1.0, 0.0); - let arc_id = sketch.add_arc(center, start, end, true); - let arc = sketch.arcs.get(&arc_id).unwrap(); - - let points = sketch.arc_to_points(&arc); - assert_eq!(points.len(), 19); - - for point in points { - println!("Point: ({}, {})", point.x, point.y); - } - } - - #[test] - fn arc_to_points_180() { - let mut sketch = Sketch::new(); - let center = sketch.add_point(0.0, 0.0); - let start = sketch.add_point(1.0, 0.0); - let end = sketch.add_point(-1.0, 0.0); - let arc_id = sketch.add_arc(center, start, end, false); - let arc = sketch.arcs.get(&arc_id).unwrap(); - - let points = sketch.arc_to_points(&arc); - assert_eq!(points.len(), 37); - } - - #[test] - fn arc_to_points70() { - let mut sketch = Sketch::new(); - let center = sketch.add_point(0.0, 0.0); - let start = sketch.add_point(1.0, 0.0); - let end = sketch.add_point(0.0, -1.0); - let arc_id = sketch.add_arc(center, start, end, false); - let arc = sketch.arcs.get(&arc_id).unwrap(); - - let points = sketch.arc_to_points(&arc); - assert_eq!(points.len(), 55); - } - - #[test] - fn delete_lines() { - let mut sketch = Sketch::new(); - - let a = sketch.add_fixed_point(0.0, 0.0); - let b = sketch.add_point(1.0, -0.1); - let c = sketch.add_point(1.1, 0.9); - let d = sketch.add_point(-0.1, 0.9); - - let segment_ab = sketch.add_segment(a, b); - let segment_bc = sketch.add_segment(b, c); - let segment_cd = sketch.add_segment(c, d); - let segment_da = sketch.add_segment(d, a); - - println!("points: {:?}", sketch.points.len()); - assert_eq!(sketch.points.len(), 4); - - sketch.delete_line_segment(segment_ab); - println!("points: {:?}", sketch.points.len()); - assert_eq!(sketch.points.len(), 4); - - sketch.delete_line_segment(segment_bc); - println!("points: {:?}", sketch.points.len()); - assert_eq!(sketch.points.len(), 3); - - sketch.delete_line_segment(segment_cd); - println!("points: {:?}", sketch.points.len()); - assert_eq!(sketch.points.len(), 2); - - sketch.delete_line_segment(segment_da); - println!("points: {:?}", sketch.points.len()); - assert_eq!(sketch.points.len(), 0); - } - - #[test] - fn delete_arcs() { - let mut sketch = Sketch::new(); - - let a = sketch.add_point(1.0, 0.0); - let b = sketch.add_point(2.0, 1.0); - let c = sketch.add_point(1.0, 2.0); - let d = sketch.add_point(0.0, 1.0); - let center = sketch.add_point(1.0, 1.0); - - let arc_ab = sketch.add_arc(center, a, b, false); - let arc_bc = sketch.add_arc(center, b, c, false); - let arc_cd = sketch.add_arc(center, c, d, false); - let arc_da = sketch.add_arc(center, d, a, false); - - println!("points: {:?}", sketch.points.len()); - assert_eq!(sketch.points.len(), 5); - - sketch.delete_arc(arc_ab); - println!("points: {:?}", sketch.points.len()); - assert_eq!(sketch.points.len(), 5); - - sketch.delete_arc(arc_bc); - println!("points: {:?}", sketch.points.len()); - assert_eq!(sketch.points.len(), 4); - - sketch.delete_arc(arc_cd); - println!("points: {:?}", sketch.points.len()); - assert_eq!(sketch.points.len(), 3); - - sketch.delete_arc(arc_da); - println!("points: {:?}", sketch.points.len()); - assert_eq!(sketch.points.len(), 0); - } - - #[test] - fn delete_circles() { - let mut sketch = Sketch::new(); - - let center = sketch.add_point(1.0, 1.0); - let circle_a = sketch.add_circle(center, 1.0); - let circle_b = sketch.add_circle(center, 2.0); - - println!("points: {:?}", sketch.points.len()); - assert_eq!(sketch.points.len(), 3); - - sketch.delete_circle(circle_a); - println!("points: {:?}", sketch.points.len()); - assert_eq!(sketch.points.len(), 2); - - sketch.delete_circle(circle_b); - println!("points: {:?}", sketch.points.len()); - assert_eq!(sketch.points.len(), 0); - } -} diff --git a/packages/cadmium/src/sketch/svg.rs b/packages/cadmium/src/sketch/svg.rs deleted file mode 100644 index d13ea554..00000000 --- a/packages/cadmium/src/sketch/svg.rs +++ /dev/null @@ -1,640 +0,0 @@ -use crate::sketch::{Arc2, Circle2, IncrementingMap, Line2, Point2, Ring, Segment, Sketch}; -use std::f64::consts::PI; - -use svg::node::element::path::Data; -// use svg::node::element::Circle; -use std::fs; -use svg::node::element::Path; -use svg::Document; - -impl Sketch { - pub fn save_svg(&self, filename: &str) { - // Find the maximum extent of the points so we can set a viewport - let mut extended_points: Vec = self.points.values().map(|p| p.clone()).collect(); - - for (_circle_id, circle) in self.circles.iter() { - let center = self.points.get(&circle.center).unwrap(); - let left = Point2::new(center.x - circle.radius, center.y); - let right = Point2::new(center.x + circle.radius, center.y); - let top = Point2::new(center.x, center.y + circle.radius); - let bottom = Point2::new(center.x, center.y - circle.radius); - extended_points.extend(vec![left, right, top, bottom]); - } - - for (_arc_id, arc) in self.arcs.iter() { - let center = self.points.get(&arc.center).unwrap(); - let start = self.points.get(&arc.start).unwrap(); - let r = (center.x - start.x).hypot(center.y - start.y); - let left = Point2::new(center.x - r, center.y); - let right = Point2::new(center.x + r, center.y); - let top = Point2::new(center.x, center.y + r); - let bottom = Point2::new(center.x, center.y - r); - extended_points.extend(vec![left, right, top, bottom]); - } - - if extended_points.len() == 0 { - extended_points.push(Point2::new(0.0, 0.0)); - extended_points.push(Point2::new(1.0, 1.0)); - } - let point0 = &extended_points[0]; - let mut min_x = point0.x; - let mut min_y = point0.y; - let mut max_x = point0.x; - let mut max_y = point0.y; - for point in extended_points { - if point.x < min_x { - min_x = point.x; - } - if point.y < min_y { - min_y = point.y; - } - if point.x > max_x { - max_x = point.x; - } - if point.y > max_y { - max_y = point.y; - } - } - - let dx = max_x - min_x; - let dy = max_y - min_y; - let buffer_percent = 10.0; - let buffer_x = dx * buffer_percent / 100.0; - let buffer_y = dy * buffer_percent / 100.0; - - let mut document = Document::new().set( - "viewBox", - ( - min_x - buffer_x, - -(max_y + buffer_y), - dx + buffer_x * 2.0, - dy + buffer_y * 2.0, - ), - ); - - // Start by creating shapes for each face - let (faces, unused_segments) = self.find_faces(); - - // println!("Making SVG. Faces:"); - // for face in faces.iter() { - // println!("{:?}", face); - // } - for face in faces.iter() { - let exterior = &face.exterior; - - let mut data = self.ring_to_data(exterior, Data::new()); - - for hole in face.holes.iter() { - data = self.ring_to_data(hole, data); - } - - let path = Path::new() - .set("fill", "none") - .set("stroke", "black") - .set("stroke-width", 0.01) - .set("fill-rule", "evenodd") - .set("d", data); - - document = document.add(path); - } - - for segment in unused_segments.iter() { - let mut data = Data::new(); - - match segment { - Segment::Line(line) => { - let start = self.points.get(&line.start).unwrap(); - let end = self.points.get(&line.end).unwrap(); - data = data.move_to((start.x, -start.y)); - data = data.line_to((end.x, -end.y)); - } - Segment::Arc(arc) => { - let center = self.points.get(&arc.center).unwrap(); - let start = self.points.get(&arc.start).unwrap(); - let end = self.points.get(&arc.end).unwrap(); - - let r = (center.x - start.x).hypot(center.y - start.y); - - data = data.move_to((start.x, -start.y)); - - let arc_angle_degrees = self.arc_angle(arc) * 180.0 / PI; - println!("arc_angle: {}", arc_angle_degrees); - - if arc_angle_degrees > 180.0 { - println!("large arc flag!"); - //A rx ry x-axis-rotation large-arc-flag sweep-flag x y - data = data.elliptical_arc_to((r, r, 0.0, 1, 0, end.x, -end.y)); - } else { - //A rx ry x-axis-rotation large-arc-flag sweep-flag x y - data = data.elliptical_arc_to((r, r, 0.0, 0, 0, end.x, -end.y)); - } - } - } - - let path = Path::new() - .set("fill", "none") - .set("stroke", "black") - .set("stroke-width", 0.01) - .set("d", data); - - document = document.add(path); - } - - // for (_circle_id, circle) in self.circles.iter() { - // let center = self.points.get(&circle.center).unwrap(); - - // let svg_circle = Circle::new() - // .set("cx", center.x) - // .set("cy", -center.y) - // .set("r", circle.radius) - // .set("fill", "none") - // .set("stroke", "black") - // .set("stroke-width", 0.01); - - // document = document.add(svg_circle); - // } - - svg::save(filename, &document).unwrap(); - } - - pub fn ring_to_data(&self, ring: &Ring, mut data: Data) -> Data { - match ring { - Ring::Circle(circle) => { - let center = self.points.get(&circle.center).unwrap(); - let radius = circle.radius; - data = data.move_to((center.x, -center.y + radius)); // starts at bottom - data = data.elliptical_arc_to(( - radius, - radius, - 0.0, - 0, - 0, - center.x, - -center.y - radius, - )); // arc to top - - data = data.elliptical_arc_to(( - radius, - radius, - 0.0, - 0, - 0, - center.x, - -center.y + radius, - )); // arc back to bottom - - data - } - Ring::Segments(segments) => { - let mut first = true; - for segment in segments { - match segment { - Segment::Line(line) => { - let start = self.points.get(&line.start).unwrap(); - let end = self.points.get(&line.end).unwrap(); - - if first { - data = data.move_to((start.x, -start.y)); - first = false; - } - data = data.line_to((end.x, -end.y)); - } - Segment::Arc(arc) => { - let center = self.points.get(&arc.center).unwrap(); - let start = self.points.get(&arc.start).unwrap(); - let end = self.points.get(&arc.end).unwrap(); - - let r = (center.x - start.x).hypot(center.y - start.y); - - if first { - data = data.move_to((start.x, -start.y)); - first = false; - } - - let arc_angle_degrees = self.arc_angle(arc) * 180.0 / PI; - println!("arc_angle: {}", arc_angle_degrees); - - // most small simple arcs should have this flag set to 0 - let mut large_arc_flag = 0; - // most arcs are counterclockwise, so this flag is usually 0 - let mut sweep_flag = 0; - - if arc_angle_degrees > 180.0 { - println!("large arc flag!"); - large_arc_flag = 1; - } - - if arc.clockwise { - sweep_flag = 1; - } - - //A rx ry x-axis-rotation large-arc-flag sweep-flag x y - data = data.elliptical_arc_to(( - r, - r, - 0.0, - large_arc_flag, - sweep_flag, - end.x, - -end.y, - )); - } - } - } - data - } - } - } -} - -#[cfg(test)] -mod tests { - use crate::project::Project; - - use super::*; - - #[test] - fn empty_to_svg() { - let mut sketch = Sketch::new(); - fs::create_dir_all("test_svgs"); - sketch.save_svg("test_svgs/empty.svg"); - } - - #[test] - fn no_rings_to_svg() { - let mut sketch = Sketch::new(); - - let center = sketch.add_point(0.0, 0.0); - let right = sketch.add_point(1.0, 0.0); - let top = sketch.add_point(0.0, 1.0); - let left = sketch.add_point(-1.0, 0.0); - let bottom = sketch.add_point(0.0, -1.0); - - sketch.add_segment(center, right); - sketch.add_segment(center, top); - sketch.add_segment(center, left); - sketch.add_segment(center, bottom); - - fs::create_dir_all("test_svgs"); - sketch.save_svg("test_svgs/no_rings.svg"); - } - - #[test] - fn circle_to_svg() { - let mut sketch = Sketch::new(); - - let id0 = sketch.add_point(1.0, 0.0); - sketch.add_circle(id0, 1.0); - - fs::create_dir_all("test_svgs"); - sketch.save_svg("test_svgs/circle.svg"); - } - - #[test] - fn square_to_svg() { - let mut sketch = Sketch::new(); - - let id0 = sketch.add_point(0.0, 0.0); - let id1 = sketch.add_point(1.0, 0.0); - let id2 = sketch.add_point(1.0, 1.0); - let id3 = sketch.add_point(0.0, 1.0); - - sketch.add_segment(id0, id1); - sketch.add_segment(id1, id2); - sketch.add_segment(id2, id3); - sketch.add_segment(id3, id0); - - fs::create_dir_all("test_svgs"); - sketch.save_svg("test_svgs/square.svg"); - } - - #[test] - fn rounded_square_to_svg() { - let mut sketch = Sketch::new(); - - let a = sketch.add_point(0.25, 0.0); - let b = sketch.add_point(0.75, 0.0); - let c = sketch.add_point(1.0, 0.25); - let d = sketch.add_point(1.0, 0.75); - let e = sketch.add_point(0.75, 1.0); - let f = sketch.add_point(0.25, 1.0); - let g = sketch.add_point(0.0, 0.75); - let h = sketch.add_point(0.0, 0.25); - let i = sketch.add_point(0.75, 0.25); - let j = sketch.add_point(0.75, 0.75); - let k = sketch.add_point(0.25, 0.75); - let l = sketch.add_point(0.25, 0.25); - - sketch.add_segment(a, b); - sketch.add_arc(i, b, c, false); - sketch.add_segment(c, d); - sketch.add_arc(j, d, e, false); - sketch.add_segment(e, f); - sketch.add_arc(k, f, g, false); - sketch.add_segment(g, h); - sketch.add_arc(l, h, a, false); - - fs::create_dir_all("test_svgs"); - sketch.save_svg("test_svgs/rounded_square.svg"); - } - - #[test] - fn square_with_hole_to_svg() { - let mut sketch = Sketch::new(); - - let a = sketch.add_point(0.0, 0.0); - let b = sketch.add_point(1.0, 0.0); - let c = sketch.add_point(1.0, 1.0); - let d = sketch.add_point(0.0, 1.0); - - let e = sketch.add_point(0.25, 0.25); - let f = sketch.add_point(0.75, 0.25); - let g = sketch.add_point(0.75, 0.75); - let h = sketch.add_point(0.25, 0.75); - - sketch.add_segment(a, b); - sketch.add_segment(b, c); - sketch.add_segment(c, d); - sketch.add_segment(d, a); - - sketch.add_segment(e, f); - sketch.add_segment(f, g); - sketch.add_segment(g, h); - sketch.add_segment(h, e); - - fs::create_dir_all("test_svgs"); - sketch.save_svg("test_svgs/square_with_hole.svg"); - } - - #[test] - fn square_with_circular_hole_to_svg() { - let mut sketch = Sketch::new(); - - let a = sketch.add_point(0.0, 0.0); - let b = sketch.add_point(1.0, 0.0); - let c = sketch.add_point(1.0, 1.0); - let d = sketch.add_point(0.0, 1.0); - let center = sketch.add_point(0.5, 0.5); - - sketch.add_segment(a, b); - sketch.add_segment(b, c); - sketch.add_segment(c, d); - sketch.add_segment(d, a); - - sketch.add_circle(center, 0.4); - - fs::create_dir_all("test_svgs"); - sketch.save_svg("test_svgs/square_with_circular_hole.svg"); - } - - #[test] - fn circle_with_circular_hole_to_svg() { - let mut sketch = Sketch::new(); - - let center = sketch.add_point(0.5, 0.5); - - sketch.add_circle(center, 0.5); - sketch.add_circle(center, 0.25); - - fs::create_dir_all("test_svgs"); - sketch.save_svg("test_svgs/circle_with_circular_hole.svg"); - } - - #[test] - fn circle_with_square_hole_to_svg() { - let mut sketch = Sketch::new(); - - let a = sketch.add_point(0.0, 0.0); - let b = sketch.add_point(1.0, 0.0); - let c = sketch.add_point(1.0, 1.0); - let d = sketch.add_point(0.0, 1.0); - let center = sketch.add_point(0.5, 0.5); - - sketch.add_segment(a, b); - sketch.add_segment(b, c); - sketch.add_segment(c, d); - sketch.add_segment(d, a); - - sketch.add_circle(center, 1.0); - - fs::create_dir_all("test_svgs"); - sketch.save_svg("test_svgs/circle_with_square_hole.svg"); - } - - #[test] - fn two_intersecting_squares_to_svg() { - let mut sketch = Sketch::new(); - - let a = sketch.add_point(0.0, 0.0); - let b = sketch.add_point(1.0, 0.0); - let c = sketch.add_point(1.0, 1.0); - let d = sketch.add_point(0.0, 1.0); - sketch.add_segment(a, b); - sketch.add_segment(b, c); - sketch.add_segment(c, d); - sketch.add_segment(d, a); - - let e = sketch.add_point(0.5, 0.5); - let f = sketch.add_point(1.5, 0.5); - let g = sketch.add_point(1.5, 1.5); - let h = sketch.add_point(0.5, 1.5); - sketch.add_segment(e, f); - sketch.add_segment(f, g); - sketch.add_segment(g, h); - sketch.add_segment(h, e); - - fs::create_dir_all("test_svgs"); - sketch.save_svg("test_svgs/two_intersecting_squares_unsplit.svg"); - - let sketch = sketch.split_intersections(false); - sketch.save_svg("test_svgs/two_intersecting_squares_split.svg"); - } - - #[test] - fn two_intersecting_circles_to_svg() { - // Create a new sketch - let mut sketch = Sketch::new(); - - // Add two circles which happen to intersect - let center_a = sketch.add_point(0.0, 0.0); - sketch.add_circle(center_a, 1.0); - let center_b = sketch.add_point(1.0, 0.0); - sketch.add_circle(center_b, 1.0); - - // Save the naive svg: just two circular paths - fs::create_dir_all("test_svgs"); - sketch.save_svg("test_svgs/two_intersecting_circles_unsplit.svg"); - - // Split the intersections, creating a new and different sketch - let sketch = sketch.split_intersections(false); - - // Save this one as an SVG, it will have three non-overlapping paths of two arcs each - sketch.save_svg("test_svgs/two_intersecting_circles_split.svg"); - } - - #[test] - fn circle_diameter() { - let mut sketch = Sketch::new(); - - let center = sketch.add_point(0.0, 0.0); - let circle_id = sketch.add_circle(center, 0.5); - let constraint_id = sketch.add_circle_diameter_constraint(circle_id, 4.0); - - sketch.solve(4000); - - println!("Value: {}", sketch.constraint_value(constraint_id)); - println!("Error: {}", sketch.constraint_error(constraint_id)); - assert!(sketch.constraint_is_satisfied(constraint_id)); - - fs::create_dir_all("test_svgs"); - sketch.save_svg("test_svgs/constraint_circle_diameter.svg"); - } - - #[test] - fn segments_equal() { - let mut sketch = Sketch::new(); - - let a = sketch.add_point(0.0, 0.0); - let b = sketch.add_point(0.5, 0.0); - - let c = sketch.add_point(0.0, 1.0); - let d = sketch.add_point(1.0, 1.0); - - let segment_ab = sketch.add_segment(a, b); // length 0.5 - let segment_cd = sketch.add_segment(c, d); // length 1.0 - - let constraint_id = sketch.add_segments_equal_constraint(segment_ab, segment_cd); - - fs::create_dir_all("test_svgs"); - sketch.save_svg("test_svgs/equality_constraint_unsolved.svg"); - assert!(sketch.solve(1000)); - sketch.save_svg("test_svgs/equality_constraint_solved.svg"); - println!("equality error: {}", sketch.constraint_error(constraint_id)); - } - - #[test] - fn triangle_constraint() { - let mut sketch = Sketch::new(); - - // initialized as a right triangle with right angle at origin - let a = sketch.add_point(0.0, 0.0); - let b = sketch.add_point(1.0, 0.0); - let c = sketch.add_point(0.0, 1.0); - - let segment_ab = sketch.add_segment(a, b); - let segment_bc = sketch.add_segment(b, c); - let segment_ca = sketch.add_segment(c, a); - - let constraint_ab = sketch.add_segment_length_constraint(segment_ab, 2.0); - let constraint_bc = sketch.add_segment_length_constraint(segment_bc, 2.0); - let constraint_ca = sketch.add_segment_length_constraint(segment_ca, 2.0); - - assert!(sketch.solve(1000)); - - assert!(sketch.all_constraints_are_satisfied()); - - fs::create_dir_all("test_svgs"); - sketch.save_svg("test_svgs/constraint_triangle.svg"); - } - - #[test] - fn two_arcs_in_a_circle_90() { - let mut sketch = Sketch::new(); - - let center = sketch.add_point(0.0, 0.0); - let top = sketch.add_point(0.0, 1.0); - let right = sketch.add_point(1.0, 0.0); - - sketch.add_arc(center, right, top, false); - sketch.add_arc(center, top, right, false); - - fs::create_dir_all("test_svgs"); - sketch.save_svg("test_svgs/two_arcs_in_a_circle_90.svg"); - } - - #[test] - fn two_arcs_in_a_circle_180() { - let mut sketch = Sketch::new(); - - let center = sketch.add_point(0.0, 0.0); - let top = sketch.add_point(0.0, 1.0); - let bottom = sketch.add_point(0.0, -1.0); - - sketch.add_arc(center, bottom, top, false); - sketch.add_arc(center, top, bottom, false); - - fs::create_dir_all("test_svgs"); - sketch.save_svg("test_svgs/two_arcs_in_a_circle_180.svg"); - } - - #[test] - fn manual_square() { - let mut sketch = Sketch::new(); - - let a = sketch.add_fixed_point(0.0, 0.0); - let b = sketch.add_point(1.0, -0.1); - let c = sketch.add_point(1.1, 0.9); - let d = sketch.add_point(-0.1, 0.9); - - let segment_ab = sketch.add_segment(a, b); - let segment_bc = sketch.add_segment(b, c); - let segment_cd = sketch.add_segment(c, d); - let segment_da = sketch.add_segment(d, a); - - let length = 2.0; - sketch.add_segment_length_constraint(segment_ab, length); - sketch.add_segment_length_constraint(segment_bc, length); - sketch.add_segment_length_constraint(segment_cd, length); - sketch.add_segment_length_constraint(segment_da, length); - - sketch.add_segment_horizontal_constraint(segment_ab); - sketch.add_segment_horizontal_constraint(segment_cd); - sketch.add_segment_vertical_constraint(segment_da); - sketch.add_segment_vertical_constraint(segment_bc); - - // for i in 0..100 { - // sketch.step(); - // sketch.save_svg(&format!("test_svgs/manual_square/{}.svg", i)); - // } - - sketch.solve(1000); - fs::create_dir_all("test_svgs"); - sketch.save_svg("test_svgs/manual_square_solved.svg"); - } - - #[test] - fn manual_rectangle() { - let mut sketch = Sketch::new(); - - let a = sketch.add_point(0.0, 0.0); - let b = sketch.add_point(1.0, 0.0); - let c = sketch.add_point(1.0, 1.0); - let d = sketch.add_point(0.0, 1.0); - - let segment_ab = sketch.add_segment(a, b); - let segment_bc = sketch.add_segment(b, c); - let segment_cd = sketch.add_segment(c, d); - let segment_da = sketch.add_segment(d, a); - - sketch.add_segment_horizontal_constraint(segment_ab); - sketch.add_segment_horizontal_constraint(segment_cd); - sketch.add_segment_vertical_constraint(segment_da); - sketch.add_segment_vertical_constraint(segment_bc); - - // fixed width of 1.0 - sketch.add_segment_length_constraint(segment_ab, 1.0); - sketch.add_segment_length_constraint(segment_cd, 1.0); - // This should cause it to adjust! - sketch.add_segment_length_constraint(segment_da, 0.5); - - // for i in 0..800 { - // sketch.save_svg(&format!("test_svgs/manual_square/{}.svg", i)); - // sketch.step(); - // } - - let solved = sketch.solve(1000); - println!("did solve? {}", solved); - fs::create_dir_all("test_svgs"); - sketch.save_svg("test_svgs/manual_rectangle_solved.svg"); - } -} diff --git a/packages/cadmium/src/solid.rs b/packages/cadmium/src/solid.rs deleted file mode 100644 index df9a2d32..00000000 --- a/packages/cadmium/src/solid.rs +++ /dev/null @@ -1,331 +0,0 @@ -use std::collections::HashMap; -use std::f64::consts::PI; - -use serde::{Deserialize, Serialize}; -use tsify::Tsify; - -use truck_meshalgo::prelude::OptimizingFilter; -use truck_meshalgo::tessellation::MeshableShape; -use truck_meshalgo::tessellation::MeshedShape; -use truck_polymesh::obj; -use truck_polymesh::Rad; -use truck_stepio::out; - -use crate::archetypes::Vector3; -use crate::extrusion::find_transit; -use crate::extrusion::merge_faces; -use crate::extrusion::Direction; -use crate::extrusion::Extrusion; -use crate::project::{RealPlane, RealSketch}; -use crate::sketch::Vector2; -use crate::sketch::{Face, Ring, Segment}; - -use truck_modeling::{builder, builder::translated, Edge, Face as TruckFace, Vertex, Wire}; - -use truck_polymesh::Point3 as TruckPoint3; -use truck_polymesh::Vector3 as TruckVector3; -use truck_topology::Solid as TruckSolid; - -const MESH_TOLERANCE: f64 = 0.1; - -#[derive(Tsify, Debug, Serialize, Deserialize, Clone)] -#[tsify(into_wasm_abi, from_wasm_abi)] -pub struct Solid { - pub name: String, - pub crc32: String, - pub vertices: Vec, - pub normals: Vec, - pub uvs: Vec, - pub indices: Vec, - pub triangles: Vec>, - pub truck_solid: TruckSolid< - truck_polymesh::cgmath::Point3, - truck_modeling::Curve, - truck_modeling::Surface, - >, -} - -impl Solid { - pub fn from_truck_solid( - name: String, - truck_solid: TruckSolid< - truck_meshalgo::prelude::cgmath::Point3, - truck_modeling::Curve, - truck_modeling::Surface, - >, - ) -> Self { - let mut solid = Solid { - name, - crc32: "".to_owned(), - vertices: vec![], - normals: vec![], - triangles: vec![], - uvs: vec![], - indices: vec![], - truck_solid, - }; - let mut mesh = solid.truck_solid.triangulation(MESH_TOLERANCE).to_polygon(); - mesh.put_together_same_attrs(0.1); - - // the mesh is prepared for obj export, but we need to convert it - // to a format compatible for rendering - // We have to brute force this. Go through every single triangle - // and emit three positions, three normals, and three uvs. - let mut index = 0 as usize; - for face in mesh.tri_faces() { - for v in face.iter() { - let vertex_index = v.pos; - let normal_index = v.nor.unwrap(); - let uv_index = v.uv.unwrap(); - let vertex = mesh.positions()[vertex_index]; - let normal = mesh.normals()[normal_index]; - let uv = mesh.uv_coords()[uv_index]; - - let pt = Vector3::new(vertex.x, vertex.y, vertex.z); - solid.vertices.push(pt); - solid - .normals - .push(Vector3::new(normal.x, normal.y, normal.z)); - solid.uvs.push(Vector2::new(uv.x, uv.y)); - solid.indices.push(index); - - index += 1; - } - } - - // compute the crc32 of the vertices - let mut hasher = crc32fast::Hasher::new(); - for vertex in solid.vertices.iter() { - hasher.update(&vertex.x.to_be_bytes()); - hasher.update(&vertex.y.to_be_bytes()); - hasher.update(&vertex.z.to_be_bytes()); - } - solid.crc32 = format!("{:x}", hasher.finalize()); - - solid - } - - pub fn get_face_by_normal(&self, normal: &Vector3) -> Option { - let truck_solid = &self.truck_solid; - let boundaries = &truck_solid.boundaries()[0]; - - let mut candidate_faces: Vec = vec![]; - - boundaries.face_iter().for_each(|face| { - let oriented_surface = face.oriented_surface(); - - match oriented_surface { - truck_modeling::geometry::Surface::Plane(p) => { - let this_face_normal = p.normal(); - - if (normal.x - this_face_normal.x).abs() < 0.0001 - && (normal.y - this_face_normal.y).abs() < 0.0001 - && (normal.z - this_face_normal.z).abs() < 0.0001 - { - candidate_faces.push(face.clone()); - } - } - _ => {} - } - }); - - match candidate_faces.len() { - 0 => None, - 1 => Some(candidate_faces[0].clone()), - _ => panic!("More than one face with the same normal!"), - } - } - - pub fn from_extrusion( - name: String, - plane: &RealPlane, - sketch: &RealSketch, - extrusion: &Extrusion, - ) -> HashMap { - let mut retval = HashMap::new(); - - let extrusion_direction = match &extrusion.direction { - Direction::Normal => plane.plane.tertiary.clone(), - Direction::NegativeNormal => plane.plane.tertiary.times(-1.0), - Direction::Specified(vector) => vector.clone(), - }; - - let extrusion_vector = extrusion_direction.times(extrusion.length - extrusion.offset); - let offset_vector = extrusion_direction.times(extrusion.offset); - - let vector = TruckVector3::new(extrusion_vector.x, extrusion_vector.y, extrusion_vector.z); - let offset_vector = TruckVector3::new(offset_vector.x, offset_vector.y, offset_vector.z); - - // Sometimes the chosen faces are touching, or one even envelops another. Let's - // merge those faces together so that we have single solid wherever possible - let unmerged_faces: Vec = extrusion - .face_ids - .iter() - .map(|face_id| sketch.faces.get(*face_id as usize).unwrap().clone()) - .collect(); - let merged_faces = merge_faces(&unmerged_faces, sketch); - - for (f_index, face) in merged_faces.iter().enumerate() { - // let face = sketch.faces.get(*face_id as usize).unwrap(); - - // println!("face: {:?}", face); - let exterior = &face.exterior; - let mut wires: Vec = Vec::new(); - - // the exterior wire comes first - wires.push(Self::to_wire(plane, sketch, extrusion, exterior)); - - // then the interior wires - for interior in &face.holes { - wires.push(Self::to_wire(plane, sketch, extrusion, interior).inverse()); - } - - let face = builder::try_attach_plane(&wires).unwrap(); - - let truck_solid = builder::tsweep(&face, vector); - let truck_solid = translated(&truck_solid, offset_vector); - - let solid = Solid::from_truck_solid(format!("{}:{}", name, f_index), truck_solid); - - retval.insert(format!("{}:{}", name, f_index), solid); - } - - retval - } - - pub fn to_wire( - plane: &RealPlane, - sketch: &RealSketch, - _extrusion: &Extrusion, - exterior: &Ring, - ) -> Wire { - match exterior { - Ring::Circle(circle) => { - println!("circle: {:?}", circle); - - let center = sketch.points.get(&circle.center).unwrap(); - let center_point = TruckPoint3::new(center.x, center.y, center.z); - - let top = sketch.points.get(&circle.top).unwrap(); - let top_point = TruckPoint3::new(top.x, top.y, top.z); - - let vector = TruckVector3::new( - plane.plane.tertiary.x, - plane.plane.tertiary.y, - plane.plane.tertiary.z, - ); - - // we actually achieve this with an rsweep! - let vertex = builder::vertex(top_point); - let circle = builder::rsweep(&vertex, center_point, vector, Rad(2.0 * PI)); - circle - } - Ring::Segments(segments) => { - // println!("segments: {:?}", segments); - // let mut builder = builder::FaceBuilder::new(); - let mut vertices: HashMap = HashMap::new(); - - // First just collect all the points as Truck Vertices - // This is important because for shapes to be closed, - // Truck requires that the start point IS the end point - for segment in segments.iter() { - match segment { - Segment::Line(line) => { - let start = sketch.points.get(&line.start).unwrap(); - let start_vertex = - builder::vertex(TruckPoint3::new(start.x, start.y, start.z)); - let end = sketch.points.get(&line.end).unwrap(); - let end_vertex = builder::vertex(TruckPoint3::new(end.x, end.y, end.z)); - vertices.insert(line.start, start_vertex); - vertices.insert(line.end, end_vertex); - } - Segment::Arc(arc) => { - let start = sketch.points.get(&arc.start).unwrap(); - let start_vertex = - builder::vertex(TruckPoint3::new(start.x, start.y, start.z)); - let end = sketch.points.get(&arc.end).unwrap(); - let end_vertex = builder::vertex(TruckPoint3::new(end.x, end.y, end.z)); - let center = sketch.points.get(&arc.center).unwrap(); - let center_vertex = - builder::vertex(TruckPoint3::new(center.x, center.y, center.z)); - vertices.insert(arc.start, start_vertex); - vertices.insert(arc.end, end_vertex); - vertices.insert(arc.center, center_vertex); - } - } - } - - let mut edges: Vec = Vec::new(); - // Now add the segments to the wire - for segment in segments.iter() { - match segment { - Segment::Line(line) => { - let start_vertex = vertices.get(&line.start).unwrap(); - let end_vertex = vertices.get(&line.end).unwrap(); - let edge = builder::line(start_vertex, end_vertex); - edges.push(edge); - } - Segment::Arc(arc) => { - let start_point = sketch.points.get(&arc.start).unwrap(); - let end_point = sketch.points.get(&arc.end).unwrap(); - let center_point = sketch.points.get(&arc.center).unwrap(); - let transit = find_transit( - plane, - start_point, - end_point, - center_point, - arc.clockwise, - ); - - let start_vertex = vertices.get(&arc.start).unwrap(); - let end_vertex = vertices.get(&arc.end).unwrap(); - let transit_point = TruckPoint3::new(transit.x, transit.y, transit.z); - - // center point is not a vertex, but a point - let edge = builder::circle_arc(start_vertex, end_vertex, transit_point); - edges.push(edge); - } - } - } - - let wire = Wire::from_iter(edges.into_iter()); - wire - } - } - } - - pub fn to_obj_string(&self, tolerance: f64) -> String { - let mut mesh = self.truck_solid.triangulation(tolerance).to_polygon(); - mesh.put_together_same_attrs(0.1); - let mut buf = Vec::new(); - obj::write(&mesh, &mut buf).unwrap(); - let string = String::from_utf8(buf).unwrap(); - string - } - - pub fn save_as_obj(&self, filename: &str, tolerance: f64) { - let mut mesh = self.truck_solid.triangulation(tolerance).to_polygon(); - mesh.put_together_same_attrs(0.1); - let file = std::fs::File::create(filename).unwrap(); - obj::write(&mesh, file).unwrap(); - } - - pub fn to_step_string(&self) -> String { - let compressed = self.truck_solid.compress(); - let step_string = out::CompleteStepDisplay::new( - out::StepModel::from(&compressed), - out::StepHeaderDescriptor { - organization_system: "cadmium-shape-to-step".to_owned(), - ..Default::default() - }, - ) - .to_string(); - step_string - } - - pub fn save_as_step(&self, filename: &str) { - let step_text = self.to_step_string(); - let mut step_file = std::fs::File::create(filename).unwrap(); - std::io::Write::write_all(&mut step_file, step_text.as_ref()).unwrap(); - } -} diff --git a/packages/cadmium/src/solid/extrusion.rs b/packages/cadmium/src/solid/extrusion.rs new file mode 100644 index 00000000..e191fb4f --- /dev/null +++ b/packages/cadmium/src/solid/extrusion.rs @@ -0,0 +1,349 @@ +use std::cell::RefCell; +use std::rc::Rc; + +use isotope::decompose::face::Face; +use serde::{Deserialize, Serialize}; +use truck_modeling::builder; +use tsify::Tsify; + +use truck_polymesh::InnerSpace; +use truck_polymesh::Invertible; +use truck_polymesh::Tolerance; +use truck_shapeops::ShapeOpsCurve; +use truck_shapeops::ShapeOpsSurface; +use truck_topology::Shell; + +use super::prelude::*; + +use crate::archetypes::Vector3; +use crate::isketch::ISketch; +use crate::IDType; + +use super::get_isoface_wires; +use super::Feature; +use super::FeatureCell; +use super::SolidLike; + +#[derive(Tsify, Debug, Clone, Serialize, Deserialize)] +#[tsify(into_wasm_abi, from_wasm_abi)] +pub enum Mode { + New, + Add(Vec), + Remove(Vec), +} + +#[derive(Tsify, Debug, Clone, Serialize, Deserialize)] +#[tsify(into_wasm_abi, from_wasm_abi)] +pub enum Direction { + Normal, + NegativeNormal, + Specified(Vector3), +} + +#[derive(Tsify, Debug, Clone, Serialize, Deserialize)] +#[tsify(into_wasm_abi, from_wasm_abi)] +pub struct Extrusion { + pub faces: Vec, + pub sketch: Rc>, + pub length: f64, + pub offset: f64, + pub direction: Direction, + pub mode: Mode, +} + +impl Extrusion { + pub fn new( + faces: Vec, + sketch: Rc>, + length: f64, + offset: f64, + direction: Direction, + mode: Mode, + ) -> Self { + Extrusion { + faces, + sketch, + length, + offset, + direction, + mode, + } + } +} + +impl SolidLike for Extrusion { + fn references(&self) -> Vec { + // self.faces.iter().map(|f| FeatureCell::Face(f.clone())).collect() + todo!("Extrusion::references") + } + + fn to_feature(&self) -> Feature { + Feature::Extrusion(self.clone()) + } + + fn get_truck_solids(&self) -> anyhow::Result> { + let plane = self.sketch.borrow().plane.borrow().clone(); + + let extrusion_direction = match &self.direction { + Direction::Normal => plane.tertiary.clone(), + Direction::NegativeNormal => plane.tertiary.times(-1.0), + Direction::Specified(vector) => vector.clone(), + }; + + let extrusion_vector = extrusion_direction.times(self.length - self.offset); + let offset_vector = extrusion_direction.times(self.offset); + let extrusion_tvector = TruckVector3::new(extrusion_vector.x, extrusion_vector.y, extrusion_vector.z); + let offset_tvector = TruckVector3::new(offset_vector.x, offset_vector.y, offset_vector.z); + + Ok(self.faces + .iter() + .map(|f| { + let wires = get_isoface_wires(self.sketch.clone(), f).unwrap(); + let face = builder::try_attach_plane(&wires).unwrap(); + + // Can we calculate ALL the wires at once and not iter-sweep? + let sweep = builder::tsweep(&face, extrusion_tvector); + let translated = builder::translated(&sweep, offset_tvector); + + translated + }).collect()) + } +} + +pub fn find_enveloped_shapes(faces: &Vec) -> Vec<(usize, usize)> { + let mut retval = vec![]; + for (a, face_a) in faces.iter().enumerate() { + for (b, face_b) in faces.iter().enumerate() { + if a == b { + continue; + } + + // check if b's exterior is equal to any of a's holes + for (_hole_index, hole) in face_a.holes.iter().enumerate() { + if hole == face_b { + retval.push((b, a)); // (small, big) + } + } + } + } + + return retval; +} + +pub fn find_adjacent_shapes(faces: &Vec) -> Option<(usize, usize, Vec, Vec)> { + for (a, face_a) in faces.iter().enumerate() { + for (b, face_b) in faces.iter().enumerate() { + if a == b || a > b { + continue; + } + + let adjacent_edges = face_a.exterior.adjacent_edges(&face_b.exterior); + + match adjacent_edges { + None => continue, + Some(matched_edges) => return Some((a, b, matched_edges.0, matched_edges.1)), + } + } + } + + None +} + +// pub fn find_transit( +// real_plane: &RealPlane, +// start: &Point3, +// end: &Point3, +// center: &Point3, +// clockwise: bool, +// ) -> Point3 { +// // let radius = start.distance_to(center); + +// let start = real_plane.plane.project(start); +// let end = real_plane.plane.project(end); +// let center = real_plane.plane.project(center); + +// let pts = arc_to_points(&start, &end, ¢er, clockwise); + +// let transit = &pts[pts.len() / 2]; + +// let transit_3d = real_plane.plane.unproject(&transit); +// transit_3d +// } + +pub fn fuse + std::fmt::Debug, S: ShapeOpsSurface + std::fmt::Debug>( + solid0: &TruckTopoSolid, + solid1: &TruckTopoSolid, +) -> Option> { + println!("Okay let's fuse!"); + + let solid0_boundaries = solid0.boundaries(); + let solid1_boundaries = solid1.boundaries(); + assert!(solid0_boundaries.len() == 1); + assert!(solid1_boundaries.len() == 1); + + let boundary0 = &solid0_boundaries[0]; + let boundary1 = &solid1_boundaries[0]; + let fusable_faces = find_coplanar_face_pairs(boundary0, boundary1, true); + assert!(fusable_faces.len() == 1); + let fusable_faces = fusable_faces[0]; + // TODO: support the case where more than one is fusable + println!("fusable_faces: {:?}", fusable_faces); + + let secondary_mergeable_faces = find_coplanar_face_pairs(boundary0, boundary1, false); + println!("secondary_mergeable_faces: {:?}", secondary_mergeable_faces); + + // There's only one fused solid at the end. Create it by cloning solid0 + // and then removing the fusable face from it. + let mut combined = boundary0.clone(); + combined.remove(fusable_faces.0); + + // Meanwhile, make a copy of solid1 and remove the fusable face from it too. + let mut boundary1_copy = boundary1.clone(); + boundary1_copy.remove(fusable_faces.1); + + // Then, add every face from solid1 to the combined solid. + combined.append(&mut boundary1_copy); + + // Lastly, merge the two fusable faces together. This is complicated because + // one might be bigger than the other, or they might be the same size, or + // they might overlap somewhat. We'll need to figure out how to merge them. + // println!("How do I merge these two? {:?}", fusable_faces); + // println!("First:"); + // for edge in boundary0[fusable_faces.0].edge_iter() { + // println!( + // "Edge: {:?} to {:?}", + // edge.front().get_point(), + // edge.back().get_point() + // ); + // } + let mut outer_face = boundary0[fusable_faces.0].clone(); + let inner_face = boundary1[fusable_faces.1].clone(); + outer_face.add_boundary(inner_face.boundaries().first().unwrap().clone()); + + // Then add that merged face to the solid and we've fused! + combined.push(outer_face); + + // After that, we need to merge the secondary_mergeable_faces together. + for (face_0_idx, face_1_idx) in secondary_mergeable_faces { + let mut face_0 = boundary0[face_0_idx].clone(); + let face_1 = boundary1[face_1_idx].clone(); + face_0.add_boundary(face_1.boundaries().first().unwrap().clone()); + combined.push(face_0); + } + + // And then we're done! + // None + Some(TruckTopoSolid::new(vec![combined])) +} + +fn find_coplanar_face_pairs, S: ShapeOpsSurface>( + boundary0: &Shell, + boundary1: &Shell, + flip_second: bool, +) -> Vec<(usize, usize)> { + let mut coplanar_faces: Vec<(usize, usize)> = vec![]; + for (face_0_idx, face_0) in boundary0.face_iter().enumerate() { + let surface_0 = face_0.oriented_surface(); + + match surface_0 { + TruckSurface::Plane(p0) => { + for (face_1_idx, face_1) in boundary1.face_iter().enumerate() { + let mut surface_1 = face_1.oriented_surface(); + + if flip_second { + surface_1 = surface_1.inverse(); + } + + match surface_1 { + TruckSurface::Plane(p1) => { + if are_coplanar(p0, p1) { + coplanar_faces.push((face_0_idx, face_1_idx)); + } + } + _ => {} + } + } + } + _ => {} + } + } + + coplanar_faces +} + +fn are_coplanar(p0: TruckPlane, p1: TruckPlane) -> bool { + let normal0 = p0.normal(); + let normal1 = p1.normal(); + + if !normal0.near(&normal1) { + return false; + } + + let difference = p0.origin() - p1.origin(); + let dot = normal0.dot(difference); + dot.abs() < 0.0001 +} + +#[cfg(test)] +mod tests { + use crate::project::Project; + use crate::project::tests::create_test_project; + + #[allow(unused_imports)] + use super::*; + + #[test] + fn create_project_solid() { + let mut p = Project::new("Test Extrusion"); + + // now get solids? save as obj or stl or step? + let workbench = p.workbenches.get_mut(0).unwrap(); + let realization = workbench.realize(100).unwrap(); + let solids = realization.solids; + println!("solids: {:?}", solids); + } + + #[test] + fn project_from_files() { + let file_list = [ + // this file contains three shapes which are adjacent to each other and + // thus should result in a single output solid + ("src/test_inputs/three_adjacent_faces.cadmium", 1), + // this file contains one square nested inside another + // and thus should result in a single output solid + ("src/test_inputs/nested_squares.cadmium", 1), + // this file contains one circle nested inside another + // and thus should result in a single output solid + ("src/test_inputs/nested_circles.cadmium", 1), + ("src/test_inputs/two_Es.cadmium", 1), + ("src/test_inputs/lots_of_nesting.cadmium", 4), + ]; + + for (file, expected_solids) in file_list.iter() { + let contents = std::fs::read_to_string(file).unwrap(); + + // deserialize the contents into a Project + let mut p: Project = serde_json::from_str(&contents).unwrap(); + + // get a realization + let workbench = p.workbenches.get_mut(0).unwrap(); + let realization = workbench.realize(100).unwrap(); + let solids = realization.solids; + println!("[{}] solids: {:?}", file, solids.len()); + + assert_eq!(solids.len(), *expected_solids); // doesn't work yet! + } + } + + #[test] + fn step_export() { + let mut p = create_test_project(); + let workbench = p.get_workbench_by_id_mut(0).unwrap(); + let realization = workbench.realize(1000).unwrap(); + let keys = Vec::from_iter(realization.solids.keys()); + + realization.save_solid_as_step_file(*keys[0], "pkg/test.step"); + realization.save_solid_as_obj_file(*keys[0], "pkg/test.obj", 0.001); + } + +} diff --git a/packages/cadmium/src/solid/helpers.rs b/packages/cadmium/src/solid/helpers.rs new file mode 100644 index 00000000..a740403b --- /dev/null +++ b/packages/cadmium/src/solid/helpers.rs @@ -0,0 +1,50 @@ +use std::cell::RefCell; +use std::rc::Rc; + +use geo::LineString; +use truck_modeling::{builder, Edge, Vertex, Wire}; + +use crate::isketch::ISketch; +use super::prelude::*; + +pub fn geopoint_to_truckpoint(point: geo::Point, sketch: Rc>) -> Result { + let sketch_ref = sketch.borrow(); + let sketch_point = sketch_ref.find_point_ref(point.x(), point.y()).ok_or(anyhow::anyhow!("geo::Point not found in sketch"))?; + let point_3d = sketch_ref.get_point_3d(sketch_point)?.1; + Ok(point_3d.into()) +} + +pub fn linestring_to_wire(line: &LineString, sketch: Rc>) -> Result { + let mut vertices: Vec = Vec::new(); + for point in line.points() { + let vertex = builder::vertex(geopoint_to_truckpoint(point, sketch.clone())?); + vertices.push(vertex); + } + + let mut edges: Vec = Vec::new(); + for i in 0..vertices.len() - 1 { + let edge = builder::line(&vertices[i], &vertices[i + 1]); + edges.push(edge); + } + + Ok(Wire::from_iter(edges.into_iter())) +} + +// It assumes that the feature will start from the same plane as the sketch +// To change this, geopoint_to_truckpoint should be modified to accept a plane +// and calculate the 3d point from that plane on-demand +pub fn get_isoface_wires( + sketch: Rc>, + face: &ISOFace, +) -> Result, anyhow::Error> { + let polygon = face.as_polygon(); + let exterior = linestring_to_wire(polygon.exterior(), sketch.clone())?; + let mut interiors = polygon + .interiors() + .iter() + .map(|line| linestring_to_wire(line, sketch.clone())) + .collect::, anyhow::Error>>()?; + interiors.insert(0, exterior); + + Ok(interiors) +} diff --git a/packages/cadmium/src/solid/mod.rs b/packages/cadmium/src/solid/mod.rs new file mode 100644 index 00000000..f5607515 --- /dev/null +++ b/packages/cadmium/src/solid/mod.rs @@ -0,0 +1,223 @@ +use std::cell::Ref; +use std::cell::RefCell; +use std::cell::RefMut; +use std::fmt::Debug; +use std::rc::Rc; + +use serde::{Deserialize, Serialize}; +use tsify::Tsify; + +use truck_meshalgo::prelude::OptimizingFilter; +use truck_meshalgo::tessellation::MeshableShape; +use truck_meshalgo::tessellation::MeshedShape; +use truck_polymesh::obj; +use truck_stepio::out; + +use crate::archetypes::Vector2; +use crate::archetypes::Vector3; + +pub mod extrusion; +pub mod helpers; +pub mod point; +pub mod prelude; + +use prelude::*; + +const MESH_TOLERANCE: f64 = 0.1; + +pub trait SolidLike: Debug { + fn references(&self) -> Vec; + fn get_truck_solids(&self) -> anyhow::Result>; + fn to_feature(&self) -> Feature; + + fn to_solids(&self) -> anyhow::Result> { + let truck_solids = self.get_truck_solids()?; + + Ok(truck_solids.iter().map(|truck_solid| { + Solid::from_truck_solid("".to_owned(), truck_solid.clone()) + }).collect()) + } +} + +#[derive(Tsify, Debug, Serialize, Deserialize, Clone)] +#[tsify(into_wasm_abi, from_wasm_abi)] +pub enum Feature { + Extrusion(extrusion::Extrusion), +} + +impl Feature { + pub fn as_solid_like(&self) -> &dyn SolidLike { + match self { + Feature::Extrusion(extrusion) => extrusion, + } + } +} + +#[derive(Tsify, Debug, Serialize, Deserialize, Clone)] +#[tsify(into_wasm_abi, from_wasm_abi)] +pub enum FeatureCell { + Extrusion(Rc>), +} + +impl FeatureCell { + pub fn borrow(&self) -> Ref { + match self { + FeatureCell::Extrusion(e) => e.borrow(), + } + } + + pub fn borrow_mut(&self) -> RefMut { + match self { + FeatureCell::Extrusion(e) => e.borrow_mut(), + } + } + + pub fn as_ptr(&self) -> *const dyn SolidLike { + match self { + FeatureCell::Extrusion(e) => e.as_ptr(), + } + } +} + +impl PartialEq for FeatureCell { + fn eq(&self, other: &Self) -> bool { + std::ptr::eq(self.as_ptr(), other.as_ptr()) + } +} + +#[derive(Tsify, Debug, Serialize, Deserialize, Clone)] +#[tsify(into_wasm_abi, from_wasm_abi)] +pub struct Solid { + pub name: String, + pub crc32: String, + pub vertices: Vec, + pub normals: Vec, + pub uvs: Vec, + pub indices: Vec, + pub triangles: Vec>, + pub truck_solid: TruckClosedSolid, +} + +impl Solid { + pub fn from_truck_solid( + name: String, + truck_solid: TruckClosedSolid, + ) -> Self { + let mut solid = Solid { + name, + crc32: "".to_owned(), + vertices: vec![], + normals: vec![], + triangles: vec![], + uvs: vec![], + indices: vec![], + truck_solid, + }; + let mut mesh = solid.truck_solid.triangulation(MESH_TOLERANCE).to_polygon(); + mesh.put_together_same_attrs(MESH_TOLERANCE); + + // the mesh is prepared for obj export, but we need to convert it + // to a format compatible for rendering + // We have to brute force this. Go through every single triangle + // and emit three positions, three normals, and three uvs. + let mut index = 0 as usize; + for face in mesh.tri_faces() { + for v in face.iter() { + let vertex_index = v.pos; + let normal_index = v.nor.unwrap(); + let uv_index = v.uv.unwrap(); + let vertex = mesh.positions()[vertex_index]; + let normal = mesh.normals()[normal_index]; + let uv = mesh.uv_coords()[uv_index]; + + let pt = Vector3::new(vertex.x, vertex.y, vertex.z); + solid.vertices.push(pt); + solid + .normals + .push(Vector3::new(normal.x, normal.y, normal.z)); + solid.uvs.push(Vector2::new(uv.x, uv.y)); + solid.indices.push(index); + + index += 1; + } + } + + // compute the crc32 of the vertices + let mut hasher = crc32fast::Hasher::new(); + for vertex in solid.vertices.iter() { + hasher.update(&vertex.x.to_be_bytes()); + hasher.update(&vertex.y.to_be_bytes()); + hasher.update(&vertex.z.to_be_bytes()); + } + solid.crc32 = format!("{:x}", hasher.finalize()); + + solid + } + + pub fn get_face_by_normal(&self, normal: &Vector3) -> Option { + let truck_solid = &self.truck_solid; + let boundaries = &truck_solid.boundaries()[0]; + + let mut candidate_faces: Vec = vec![]; + + boundaries.face_iter().for_each(|face| { + let oriented_surface = face.oriented_surface(); + + match oriented_surface { + truck_modeling::geometry::Surface::Plane(p) => { + let this_face_normal = p.normal(); + + if (normal.x - this_face_normal.x).abs() < 0.0001 + && (normal.y - this_face_normal.y).abs() < 0.0001 + && (normal.z - this_face_normal.z).abs() < 0.0001 + { + candidate_faces.push(face.clone()); + } + } + _ => {} + } + }); + + match candidate_faces.len() { + 0 => None, + 1 => Some(candidate_faces[0].clone()), + _ => panic!("More than one face with the same normal!"), + } + } + + pub fn to_obj_string(&self, tolerance: f64) -> String { + let mut mesh = self.truck_solid.triangulation(tolerance).to_polygon(); + mesh.put_together_same_attrs(MESH_TOLERANCE); + let mut buf = Vec::new(); + obj::write(&mesh, &mut buf).unwrap(); + let string = String::from_utf8(buf).unwrap(); + string + } + + pub fn save_as_obj(&self, filename: &str, tolerance: f64) { + let mut mesh = self.truck_solid.triangulation(tolerance).to_polygon(); + mesh.put_together_same_attrs(MESH_TOLERANCE); + let file = std::fs::File::create(filename).unwrap(); + obj::write(&mesh, file).unwrap(); + } + + pub fn to_step_string(&self) -> String { + let compressed = self.truck_solid.compress(); + let step_string = out::CompleteStepDisplay::new( + out::StepModel::from(&compressed), + out::StepHeaderDescriptor { + organization_system: "cadmium-shape-to-step".to_owned(), + ..Default::default() + }, + ) + .to_string(); + step_string + } + + pub fn save_as_step(&self, filename: &str) { + let step_text = self.to_step_string(); + let mut step_file = std::fs::File::create(filename).unwrap(); + std::io::Write::write_all(&mut step_file, step_text.as_ref()).unwrap(); + } + +} diff --git a/packages/cadmium/src/solid/point.rs b/packages/cadmium/src/solid/point.rs new file mode 100644 index 00000000..bd820408 --- /dev/null +++ b/packages/cadmium/src/solid/point.rs @@ -0,0 +1,102 @@ +use std::ops::{Add, Sub}; + +use serde::{Deserialize, Serialize}; +use truck_polymesh::Point3 as PolyTruckPoint3; +use isotope::primitives::point2::Point2 as ISOPoint2; +use tsify::Tsify; + +use crate::archetypes::{Plane, Vector3}; + +#[derive(Tsify, Debug, Clone, Serialize, Deserialize)] +#[tsify(into_wasm_abi, from_wasm_abi)] +pub struct Point3 { + pub x: f64, + pub y: f64, + pub z: f64, + pub hidden: bool, +} + +impl Point3 { + pub fn new(x: f64, y: f64, z: f64) -> Self { + Point3 { + x, + y, + z, + hidden: false, + } + } + + pub fn plus(&self, v: Vector3) -> Vector3 { + Vector3 { + x: self.x + v.x, + y: self.y + v.y, + z: self.z + v.z, + } + } + + pub fn minus(&self, other: &Point3) -> Vector3 { + Vector3 { + x: self.x - other.x, + y: self.y - other.y, + z: self.z - other.z, + } + } + + pub fn distance_to(&self, other: &Point3) -> f64 { + let dx = self.x - other.x; + let dy = self.y - other.y; + let dz = self.z - other.z; + (dx * dx + dy * dy + dz * dz).sqrt() + } + + pub fn from_plane_point(plane: &Plane, point: &ISOPoint2) -> Point3 { + let o = plane.origin.clone(); + let x = plane.primary.clone(); + let y = plane.secondary.clone(); + + let pt3 = o.plus(x.times(point.x())).plus(y.times(point.y())); + Point3::new(pt3.x, pt3.y, pt3.z) + } +} + +impl Into for Point3 { + fn into(self) -> PolyTruckPoint3 { + PolyTruckPoint3 { + x: self.x, + y: self.y, + z: self.z, + } + } +} + +impl Add for Point3 { + type Output = Point3; + + fn add(self, other: Point3) -> Point3 { + Point3 { + x: self.x + other.x, + y: self.y + other.y, + z: self.z + other.z, + hidden: false, + } + } +} + +impl Sub for Point3 { + type Output = Point3; + + fn sub(self, other: Point3) -> Point3 { + Point3 { + x: self.x - other.x, + y: self.y - other.y, + z: self.z - other.z, + hidden: false, + } + } +} + +impl PartialEq for Point3 { + fn eq(&self, other: &Self) -> bool { + self.x == other.x && self.y == other.y && self.z == other.z + } +} diff --git a/packages/cadmium/src/solid/prelude.rs b/packages/cadmium/src/solid/prelude.rs new file mode 100644 index 00000000..e7e9c058 --- /dev/null +++ b/packages/cadmium/src/solid/prelude.rs @@ -0,0 +1,24 @@ +pub use isotope::decompose::face::Face as ISOFace; + +pub use truck_modeling::Face as TruckFace; +pub use truck_modeling::Solid as TruckSolid; +pub use truck_modeling::Vertex as TruckVertex; +pub use truck_modeling::Point1 as TruckPoint1; +pub use truck_modeling::Point2 as TruckPoint2; +pub use truck_modeling::Point3 as TruckPoint3; +pub use truck_modeling::Vector1 as TruckVector1; +pub use truck_modeling::Vector2 as TruckVector2; +pub use truck_modeling::Vector3 as TruckVector3; +pub use truck_modeling::Plane as TruckPlane; +pub use truck_modeling::Line as TruckLine; +pub use truck_modeling::Surface as TruckSurface; + +pub use truck_topology::Solid as TruckTopoSolid; + +pub type TruckClosedSolid = TruckTopoSolid< + truck_modeling::Point3, + truck_modeling::Curve, + truck_modeling::Surface, + >; + +pub use super::helpers::*; diff --git a/packages/cadmium/src/step.rs b/packages/cadmium/src/step.rs index f9802c78..01385a5e 100644 --- a/packages/cadmium/src/step.rs +++ b/packages/cadmium/src/step.rs @@ -1,111 +1,105 @@ +use cadmium_macros::StepDataActions; use serde::{Deserialize, Serialize}; use tsify::Tsify; use wasm_bindgen::prelude::*; -use crate::archetypes::{Plane, PlaneDescription, Point3, Vector3}; -use crate::sketch::Sketch; -use crate::extrusion::Extrusion; +use crate::archetypes::{Plane, PlaneDescription, Point2}; +use crate::solid::extrusion; +use crate::solid::point::Point3; +use crate::IDType; -#[derive(Tsify, Debug, Serialize, Deserialize)] -#[serde(tag = "type")] +#[derive(Tsify, Debug, Clone, Serialize, Deserialize)] #[tsify(into_wasm_abi, from_wasm_abi)] -pub enum StepData { - Point { - point: Point3, - }, - Plane { - plane: Plane, - width: f64, - height: f64, - }, - Sketch { - plane_description: PlaneDescription, - width: f64, - height: f64, - sketch: Sketch, - }, - Extrusion { - extrusion: Extrusion, - }, +pub enum StepOperation { + Add, + Update, + Delete, } -#[derive(Tsify, Debug, Serialize, Deserialize)] +#[derive(Tsify, Debug, Clone, Serialize, Deserialize)] #[tsify(into_wasm_abi, from_wasm_abi)] pub struct Step { + pub(crate) id: IDType, + pub(crate) operation: StepOperation, pub(crate) name: String, - pub(crate) unique_id: String, + pub(crate) unique_id: String, // TODO: remove this field, it's not needed pub(crate) suppressed: bool, pub(crate) data: StepData, } -impl Step { - pub fn new_point(name: &str, point: Point3, point_id: u64) -> Self { - Step { - name: name.to_owned(), - unique_id: format!("Point-{}", point_id), - suppressed: false, - data: StepData::Point { - point: point.clone(), - }, - } - } - - pub fn new_plane(name: &str, plane: Plane, plane_id: u64) -> Self { - Step { - name: name.to_owned(), - unique_id: format!("Plane-{}", plane_id), - suppressed: false, - data: StepData::Plane { - plane, - height: 100.0, - width: 100.0, - }, - } - } - - pub fn new_sketch(name: &str, plane_id: &str, sketch_id: u64) -> Self { - Step { - name: name.to_owned(), - unique_id: format!("Sketch-{}", sketch_id), - suppressed: false, - data: StepData::Sketch { - plane_description: PlaneDescription::PlaneId(plane_id.to_owned()), - width: 1.25, - height: 0.75, - sketch: Sketch::new(), - }, - } - } - - pub fn new_sketch_on_solid_face( - name: &str, - solid_id: &str, - normal: Vector3, - sketch_id: u64, - ) -> Self { - Step { - name: name.to_owned(), - unique_id: format!("Sketch-{}", sketch_id), - suppressed: false, - data: StepData::Sketch { - plane_description: PlaneDescription::SolidFace { - solid_id: solid_id.to_owned(), - normal, - }, - width: 12.5, - height: 7.5, - sketch: Sketch::new(), - }, - } - } +#[derive(StepDataActions, Tsify, Debug, Clone, Serialize, Deserialize)] +#[tsify(into_wasm_abi, from_wasm_abi)] +pub enum StepData { + // Workbench Primitives + #[step_data(skip_update = true, skip_delete = true)] + WorkbenchPoint { + workbench_id: IDType, + point: Point3, + }, + #[step_data(skip_update = true, skip_delete = true)] + WorkbenchPlane { + workbench_id: IDType, + plane: Plane, + width: f64, + height: f64, + }, + #[step_data(skip_update = true, skip_delete = true)] + WorkbenchSketch { + workbench_id: IDType, + plane_description: PlaneDescription, + // sketch: ISketch, + // width: f64, + // height: f64, + }, - pub fn new_extrusion(name: &str, extrusion: Extrusion, extrusion_id: u64) -> Self { - Step { - name: name.to_owned(), - unique_id: format!("Extrusion-{}", extrusion_id), - suppressed: false, - data: StepData::Extrusion { extrusion }, - } - } + // Sketch Primitives + #[step_data(workbench_field = "sketches", type_name = "Sketch", skip_update = true, skip_delete = true)] + SketchPoint { + workbench_id: IDType, + sketch_id: IDType, + point: Point2, + }, + #[step_data(workbench_field = "sketches", type_name = "Sketch", skip_update = true, skip_delete = true)] + SketchArc { + workbench_id: IDType, + sketch_id: IDType, + center: IDType, + radius: f64, + clockwise: bool, + start_angle: f64, + end_angle: f64, + }, + #[step_data(workbench_field = "sketches", type_name = "Sketch", skip_update = true, skip_delete = true)] + SketchCircle { + workbench_id: IDType, + sketch_id: IDType, + center: IDType, + radius: f64, + }, + #[step_data(workbench_field = "sketches", type_name = "Sketch", skip_update = true, skip_delete = true)] + SketchLine { + workbench_id: IDType, + sketch_id: IDType, + start: IDType, + end: IDType, + }, + // #[step_data(workbench_field = "sketches", type_name = "Sketch")] + // SketchRectangle { + // workbench_id: IDType, + // sketch_id: IDType, + // start: IDType, + // end: IDType, + // }, + // #[step_data(workbench_field = "solids", type_name = "Solid")] + #[step_data(skip_update = true, skip_delete = true)] + SolidExtrusion { + workbench_id: IDType, + face_ids: Vec, + sketch_id: IDType, + length: f64, + offset: f64, + mode: extrusion::Mode, + direction: extrusion::Direction, + }, } diff --git a/packages/cadmium/src/workbench.rs b/packages/cadmium/src/workbench.rs index 58649e20..fba3adb8 100644 --- a/packages/cadmium/src/workbench.rs +++ b/packages/cadmium/src/workbench.rs @@ -2,63 +2,88 @@ use serde::{Deserialize, Serialize}; use tsify::Tsify; use wasm_bindgen::prelude::*; -use crate::archetypes::{Plane, PlaneDescription, Point3, Vector3}; +use crate::archetypes::{Plane, PlaneDescription}; use crate::error::CADmiumError; -use crate::extrusion::{fuse, Extrusion, ExtrusionMode}; -use crate::project::{RealPlane, RealSketch}; +use crate::solid::extrusion::{self, fuse, Extrusion}; +use crate::isketch::{IPlane, ISketch}; use crate::realization::Realization; -use crate::sketch::Sketch; +use crate::solid::point::Point3; use crate::solid::Solid; +use crate::solid::SolidLike; use crate::step::{Step, StepData}; +use crate::IDType; -use std::collections::HashMap; +use std::cell::RefCell; +use std::collections::BTreeMap; +use std::rc::Rc; // use truck_base::math::Vector3 as truck_vector3; use truck_shapeops::and as solid_and; -#[derive(Tsify, Debug, Serialize, Deserialize)] +#[derive(Tsify, Debug, Clone, Serialize, Deserialize)] #[tsify(into_wasm_abi, from_wasm_abi)] pub struct Workbench { pub(crate) name: String, pub(crate) history: Vec, - pub(crate) step_counters: HashMap, + + // These are free-standing points in 3D space, not part of sketches + pub(crate) points: BTreeMap, + pub(crate) points_next_id: IDType, + + pub(crate) planes: BTreeMap>>, + pub(crate) planes_next_id: IDType, + + pub(crate) sketches: BTreeMap>>, + pub(crate) sketches_next_id: IDType, + pub(crate) solids: BTreeMap>>, + pub(crate) solids_next_id: IDType, } impl Workbench { pub fn new(name: &str) -> Self { + println!("Creating new workbench: {:?}", name); let mut wb = Workbench { name: name.to_owned(), history: vec![], - step_counters: HashMap::from([ - ("Point".to_owned(), 0), - ("Plane".to_owned(), 0), - ("Sketch".to_owned(), 0), - ("Extrusion".to_owned(), 0), - ]), + + points: BTreeMap::new(), + points_next_id: 0, + planes: BTreeMap::new(), + planes_next_id: 0, + + sketches: BTreeMap::new(), + sketches_next_id: 0, + solids: BTreeMap::new(), + solids_next_id: 0, }; - wb.add_point("Origin", Point3::new(0.0, 0.0, 0.0)); - wb.add_plane("Front", Plane::front()); - wb.add_plane("Right", Plane::right()); - wb.add_plane("Top", Plane::top()); + wb.add_workbench_point(Point3::new(0.0, 0.0, 0.0)).unwrap(); + wb.add_workbench_plane(Plane::front(), 100.0, 100.0).unwrap(); + wb.add_workbench_plane(Plane::right(), 100.0, 100.0).unwrap(); + wb.add_workbench_plane(Plane::top(), 100.0, 100.0).unwrap(); wb } - pub fn get_first_plane_id(&self) -> Option { - for step in self.history.iter() { - match &step.data { - StepData::Plane { - plane: _, - width: _, - height: _, - } => { - return Some(step.unique_id.clone()); - } - _ => {} - } + pub fn get_first_plane_id(&self) -> Option { + if self.planes.len() > 0 { + Some(self.planes.keys().next().unwrap().to_owned()) + } else { + None + } + } + + pub fn get_last_plane_id(&self) -> Option { + if self.planes.len() > 0 { + Some(self.planes.keys().last().unwrap().to_owned()) + } else { + None } - None + } + + pub fn get_sketch_by_id(&self, id: IDType) -> Result>, CADmiumError> { + println!("Getting sketch by id: {:?} {:?}", id, self.sketches); + self.sketches.get(&id).ok_or(CADmiumError::SketchIDNotFound(id)).cloned() } pub fn update_step_data(&mut self, step_id: &str, new_step_data: StepData) { @@ -73,174 +98,12 @@ impl Workbench { self.history[index].data = new_step_data; } - pub fn last_plane_id(&self) -> Option { - let mut last_plane_id = None; - for step in self.history.iter() { - match &step.data { - StepData::Plane { - plane: _, - width: _, - height: _, - } => { - last_plane_id = Some(step.unique_id.clone()); - } - _ => {} - } - } - last_plane_id - } - - pub fn json(&self) -> String { - let result = serde_json::to_string(self); - match result { - Ok(json) => json, - Err(e) => format!("Error: {}", e), - } - } - - pub fn get_step_mut(&mut self, name: &str) -> Result<&mut Step, CADmiumError> { - self.history - .iter_mut() - .find(|step| step.name == name) - .ok_or(CADmiumError::StepNameNotFound(name.to_string())) - } - - pub fn get_step_by_id_mut(&mut self, id: &str) -> Result<&mut Step, CADmiumError> { - self.history - .iter_mut() - .find(|step| step.unique_id == id) - .ok_or(CADmiumError::StepIDNotFound(id.to_string())) - } - - pub fn get_sketch_mut(&mut self, name: &str) -> Result<&mut Sketch, CADmiumError> { - let step = self.get_step_mut(name)?; - - match step.data { - StepData::Sketch { ref mut sketch, .. } => Ok(sketch), - _ => Err(CADmiumError::IncorrectStepDataType(step.unique_id.clone())), - } - } - - pub fn get_sketch_by_id_mut(&mut self, id: &str) -> Result<&mut Sketch, CADmiumError> { - let step = self.get_step_by_id_mut(id)?; - - match step.data { - StepData::Sketch { ref mut sketch, .. } => Ok(sketch), - _ => Err(CADmiumError::IncorrectStepDataType(step.unique_id.clone())), - } - } - - pub fn add_point(&mut self, name: &str, point: Point3) { - let counter = self.step_counters.get_mut("Point").unwrap(); - self.history.push(Step::new_point(name, point, *counter)); - *counter += 1; - } - - pub fn add_plane(&mut self, name: &str, plane: Plane) -> String { - let counter = self.step_counters.get_mut("Plane").unwrap(); - self.history.push(Step::new_plane(name, plane, *counter)); - *counter += 1; - - self.plane_name_to_id(name).unwrap() - } - - pub fn plane_name_to_id(&self, plane_name: &str) -> Option { - for step in self.history.iter() { - if step.name == plane_name { - match &step.data { - StepData::Plane { - plane: _, - width: _, - height: _, - } => { - return Some(step.unique_id.clone()); - } - _ => {} - } - } - } - None - } - - pub fn add_sketch_to_solid_face( - &mut self, - new_sketch_name: &str, - solid_id: &str, - normal: Vector3, - ) -> String { - // TODO: maybe this shouldn't just take in a normal. Maybe it should take in the o, p, q points as well - // that way it could try to match even if there are multiple faces on this solid which have the same normal vector - // println!("New Normal! {:?}", normal); - // called like: wb.add_sketch_to_solid_face("Sketch-2", "Ext1:0", Vector3::new(0.0, 0.0, 1.0)); - - let counter = self.step_counters.get_mut("Sketch").unwrap(); - let new_step = Step::new_sketch_on_solid_face(&new_sketch_name, solid_id, normal, *counter); - let new_step_id = new_step.unique_id.clone(); - self.history.push(new_step); - *counter += 1; - - new_step_id - } - - pub fn add_sketch_to_plane(&mut self, name: &str, plane_id: &str) -> String { - if plane_id != "" { - // if the plane id is specified, check to make sure a plane with that ID exists - let mut plane_exists = false; - for step in self.history.iter() { - if step.unique_id == plane_id { - match &step.data { - StepData::Plane { - plane: _, - width: _, - height: _, - } => { - plane_exists = true; - } - _ => {} - } - } - } - - if !plane_exists { - return format!("failed to find plane with id {}", plane_id); - } - } - // if the plane id is empty string, that's okay it's a placeholder - - // If the sketch name is empty string, then we need to generate a new name - // Let's use "Sketch n" where n is the number of sketches - let counter = self.step_counters.get_mut("Sketch").unwrap(); - let sketch_name = if name == "" { - format!("Sketch {}", *counter + 1) - } else { - name.to_owned() - }; - - let new_step = Step::new_sketch(&sketch_name, &plane_id, *counter); - let new_step_id = new_step.unique_id.clone(); - self.history.push(new_step); - *counter += 1; - - new_step_id - } - - pub fn add_extrusion(&mut self, name: &str, extrusion: Extrusion) -> u64 { - // If the extrusion name is empty string, then we need to generate a new name - // Let's use "Extrusion n" where n is the number of extrusions - let counter = self.step_counters.get_mut("Extrusion").unwrap(); - let extrusion_name = if name == "" { - format!("Extrusion {}", *counter + 1) - } else { - name.to_owned() - }; - self.history - .push(Step::new_extrusion(&extrusion_name, extrusion, *counter)); - *counter += 1; - *counter - 1 - } - - pub fn realize(&self, max_steps: u64) -> Realization { + pub fn realize(&mut self, max_steps: u64) -> Result { let mut realized = Realization::new(); + realized.planes.insert(0, IPlane { plane: Plane::front(), width: 100.0, height: 100.0, name: "front".to_string() }); + realized.planes.insert(1, IPlane { plane: Plane::right(), width: 100.0, height: 100.0, name: "right".to_string() }); + realized.planes.insert(2, IPlane { plane: Plane::top(), width: 100.0, height: 100.0, name: "top".to_string() }); + realized.points.insert(0, Point3::new(0.0, 0.0, 0.0)); let max_steps = max_steps as usize; // just coerce the type once for (step_n, step) in self.history.iter().enumerate() { @@ -252,244 +115,224 @@ impl Workbench { let step_data = &step.data; // println!("{:?}", step_data); match step_data { - StepData::Point { point } => { + StepData::WorkbenchPoint { point, .. } => { realized .points - .insert(step.unique_id.to_owned(), point.clone()); + .insert(step.id, point.clone()); } - StepData::Plane { + StepData::WorkbenchPlane { plane, width, height, + .. } => { - let rp = RealPlane { + // Do we need to store the IPlane or just the Plane? + let rp = IPlane { plane: plane.clone(), width: *width, height: *height, name: step.name.clone(), }; - realized.planes.insert(step.unique_id.to_owned(), rp); + realized.planes.insert(step.id, rp); } - StepData::Sketch { - width: _, - height: _, + StepData::WorkbenchSketch { plane_description, - sketch, + // sketch_id, + // width: _, + // height: _, + .. } => match plane_description { PlaneDescription::PlaneId(plane_id) => { - if plane_id == "" { - println!("Sketch {} has no plane", step.name); - continue; - } - - let plane = &realized.planes[plane_id]; + let plane = self.planes.get(&plane_id).ok_or(anyhow::anyhow!("Failed to find plane with id {}", plane_id))?; + let plane_ref = plane.clone(); + let sketch = ISketch::new(plane_ref); realized.sketches.insert( - step.unique_id.to_owned(), + step.id, ( - RealSketch::new(&plane.name, plane_id, plane, sketch), - RealSketch::new( - &plane.name, - plane_id, - plane, - &sketch.split_intersections(false), - ), + sketch.clone(), + sketch.clone(), step.name.clone(), ), ); } - PlaneDescription::SolidFace { solid_id, normal } => { - let solid = &realized.solids[solid_id]; - let face = solid.get_face_by_normal(normal).unwrap(); - let oriented_surface = face.oriented_surface(); - - println!("Surface: {:?}", oriented_surface); - let sketch_plane; - match oriented_surface { - truck_modeling::geometry::Surface::Plane(p) => { - let plane = Plane::from_truck(p); - println!("Plane: {:?}", plane); - sketch_plane = plane; - } - _ => { - panic!("I only know how to put sketches on planes"); - } - } - - let new_plane_id = format!("derived_plane_for:{}", step.name); - - let rp = RealPlane { - plane: sketch_plane.clone(), - width: 90.0, - height: 60.0, - name: new_plane_id.clone(), - }; - realized.planes.insert(new_plane_id.clone(), rp); - let rp = &realized.planes[&new_plane_id]; - - realized.sketches.insert( - step.unique_id.to_owned(), - ( - RealSketch::new(&new_plane_id, &new_plane_id, &rp, sketch), - RealSketch::new( - &new_plane_id, - &new_plane_id, - &rp, - &sketch.split_intersections(false), - ), - step.name.clone(), - ), - ); + PlaneDescription::SolidFace { solid_id: _, normal: _ } => { + // let solid = &realized.solids[&solid_id]; + // let face = solid.get_face_by_normal(&normal).unwrap(); + // let oriented_surface = face.oriented_surface(); + + // println!("Surface: {:?}", oriented_surface); + // let sketch_plane; + // match oriented_surface { + // truck_modeling::geometry::Surface::Plane(p) => { + // let plane = Plane::from_truck(p); + // println!("Plane: {:?}", plane); + // sketch_plane = plane; + // } + // _ => { + // panic!("I only know how to put sketches on planes"); + // } + // } + + // let new_plane_id = format!("derived_plane_for:{}", step.name); + + // let rp = IPlane { + // plane: sketch_plane.clone(), + // width: 90.0, + // height: 60.0, + // name: new_plane_id.clone(), + // }; + // realized.planes.insert(new_plane_id.clone(), rp); + // let rp = &realized.planes[&new_plane_id]; + // let sketch_ref = Rc::new(RefCell::new(sketch.clone())); + + // // TODO: There's no way this is correct. Also a lot of prelude is the same fo Plane case + // realized.sketches.insert( + // step.unique_id.to_owned(), + // ( + // ISketch::new(&new_plane_id, &rp, sketch_ref.clone()), + // ISketch::new( + // &new_plane_id, + // &rp, + // // TODO: &sketch.split_intersections(false), + // sketch_ref, + // ), + // step.name.clone(), + // ), + // ); } }, - StepData::Extrusion { extrusion } => { - let (_sketch, split_sketch, _name) = &realized.sketches[&extrusion.sketch_id]; - let plane = &realized.planes[&split_sketch.plane_id]; - - match &extrusion.mode { - ExtrusionMode::New => { - // if this extrusion is in mode "New" then this old behavior is correct! - - let solids = Solid::from_extrusion( - step.name.clone(), - plane, - split_sketch, - extrusion, - ); - - for (name, solid) in solids { - realized.solids.insert(name, solid); - } + StepData::SolidExtrusion { + face_ids, + sketch_id, + length, + offset, + mode, + direction, + .. + } => { + // TODO: Make realization a trait and implement it for Extrusion + let sketch_ref = self.sketches.get(sketch_id).unwrap(); + let sketch = sketch_ref.borrow(); + let faces = face_ids.iter().map(|id| sketch.faces().get(*id as usize).unwrap().clone()).collect(); + + let new_extrusion = Extrusion::new(faces, sketch_ref.clone(), *length, *offset, direction.clone(), mode.clone()); + let feature = new_extrusion.to_feature(); + let solid_like = feature.as_solid_like(); + let new_solids = solid_like.to_solids()?; + + match &new_extrusion.mode { + extrusion::Mode::New => { + new_solids.iter().for_each(|s| { + realized.solids.insert(self.solids_next_id, s.clone()); + self.solids_next_id += 1; + }); } - ExtrusionMode::Add(merge_scope) => { - // if this extrusion is in mode "Add" Then we need to merge the resulting solids - // with each of the solids listed in the merge scope - - let new_solids = Solid::from_extrusion( - step.name.clone(), - plane, - split_sketch, - extrusion, - ); - - // NO LONGER NEEDED - // // this is some bullshit, but bear with me. To make the solids merge properly we need to - // // lengthen the extrusion a tiny bit, basically build in some buffer - // let mut longer_extrusion = extrusion.clone(); - // longer_extrusion.length += 0.001; - // longer_extrusion.offset -= 0.001; - // let solids = Solid::from_extrusion( - // step.name.clone(), - // plane, - // split_sketch, - // &longer_extrusion, - // ); - - for existing_solid_name in merge_scope { + extrusion::Mode::Add(merge_scope) => { + for existing_solid_id in merge_scope { + let existing_solid = realized.solids.get(&existing_solid_id).unwrap().clone(); let mut existing_solid_to_merge_with = - realized.solids.remove(existing_solid_name).unwrap(); + realized.solids.remove(&existing_solid_id).unwrap(); // merge this existing solid with as many of the new solids as possible - for (_, new_solid) in new_solids.iter() { - // let new_candidate = translated( - // &solid.truck_solid, - // TruckVector3::new(0.0, 0.0, 1.0), - // ); - // println!("\nTranslated new candidate: {:?}", new_candidate); - - // let result = - // solid_or(&existing_solid.truck_solid, &new_candidate, 0.1); - + for new_solid in new_solids.iter() { let fused = fuse( &existing_solid_to_merge_with.truck_solid, &new_solid.truck_solid, - ); - - match fused { - Some(s) => { - existing_solid_to_merge_with = Solid::from_truck_solid( - existing_solid_name.to_owned(), - s, - ); - } - None => { - println!("Failed to merge with OR"); - } - } + ).unwrap(); + + let new_merged_sold = Solid::from_truck_solid(existing_solid.name.clone(), fused); + existing_solid_to_merge_with = new_merged_sold; } realized.solids.insert( - existing_solid_name.to_owned(), + existing_solid_id.to_owned(), existing_solid_to_merge_with, ); } } - - ExtrusionMode::Remove(merge_scope) => { + extrusion::Mode::Remove(merge_scope) => { // If this extrusion is in mode "Remove" then we need to subtract the resulting solid // with each of the solids listed in the merge scope - println!("Okay, let's remove"); - let new_solids = Solid::from_extrusion( - step.name.clone(), - plane, - split_sketch, - extrusion, - ); - - for existing_solid_name in merge_scope { + for existing_solid_id in merge_scope { + let existing_solid = realized.solids.get(&existing_solid_id).unwrap().clone(); let mut existing_solid_to_merge_with = - realized.solids.remove(existing_solid_name).unwrap(); + realized.solids.remove(&existing_solid_id).unwrap(); // merge this existing solid with as many of the new solids as possible - for (_, new_solid) in new_solids.iter() { - // let translated_solid = translated( - // &solid.truck_solid, - // TruckVector3::new(0.0, 0.0, 1.0), - // ); - // println!("\nTranslated new candidate: {:?}", new_candidate); - - // let result = - // solid_or(&existing_solid.truck_solid, &new_candidate, 0.1); - + for new_solid in new_solids.iter() { let punch = new_solid.truck_solid.clone(); - // punch.not(); - println!("Have a punch"); let cleared = solid_and( &existing_solid_to_merge_with.truck_solid, &punch, 0.1, - ); - - println!("have cleared"); - - match cleared { - Some(s) => { - println!("Merged with AND"); - // println!("{:?}", s); - existing_solid_to_merge_with = Solid::from_truck_solid( - existing_solid_name.to_owned(), - s, - ); - } - None => { - println!("Failed to merge with AND"); - } - } + ).unwrap(); + + let new_merged_sold = Solid::from_truck_solid(existing_solid.name.clone(), cleared); + existing_solid_to_merge_with = new_merged_sold; } realized.solids.insert( - existing_solid_name.to_owned(), + existing_solid_id.to_owned(), existing_solid_to_merge_with, ); - println!("inserted the solid back in") } } } } + _ => {} } } - realized + Ok(realized) + } +} + +// Step operations +impl Workbench { + pub(super) fn add_workbench_point(&mut self, point: Point3) -> Result { + self.points.insert(self.points_next_id, point); + self.points_next_id += 1; + Ok(self.points_next_id - 1) + } + + pub(super) fn add_workbench_plane(&mut self, plane: Plane, _width: f64, _height: f64) -> Result { + let plane_cell = Rc::new(RefCell::new(plane)); + self.planes.insert(self.planes_next_id, plane_cell); + self.planes_next_id += 1; + Ok(self.planes_next_id - 1) + } + + pub(super) fn add_workbench_sketch( + &mut self, + plane_description: PlaneDescription, + ) -> Result { + println!("Adding sketch with plane description: {:?}", plane_description); + let plane = match plane_description { + PlaneDescription::PlaneId(plane_id) => + self.planes.get(&plane_id).ok_or(anyhow::anyhow!("Failed to find plane with id {}", plane_id))?, + PlaneDescription::SolidFace { solid_id: _, normal: _ } => todo!("Implement SolidFace"), + }.clone(); + + let sketch = ISketch::new(plane); + self.sketches.insert(self.sketches_next_id, Rc::new(RefCell::new(sketch))); + println!("Added sketch with id: {:?}", self.sketches); + self.sketches_next_id += 1; + Ok(self.sketches_next_id - 1) + } + + pub(crate) fn add_solid_extrusion( + &mut self, + _face_ids: Vec, + _sketch_id: IDType, + _length: f64, + _offset: f64, + _mode: extrusion::Mode, + _direction: extrusion::Direction, + ) -> Result { + // I guess nothing to do? only realization? + // TODO: What ID should be returned here? + Ok(0) } } diff --git a/packages/shared/projectUtils.ts b/packages/shared/projectUtils.ts index bf0ab47d..5f9229e8 100644 --- a/packages/shared/projectUtils.ts +++ b/packages/shared/projectUtils.ts @@ -1,47 +1,31 @@ import { - workbenchIsStale, - workbenchIndex, - workbench, - project, - featureIndex, - wasmProject, - projectIsStale, - realizationIsStale, - wasmRealization, - realization, - messageHistory, + workbenchIsStale, + workbenchIndex, + workbench, + project, + featureIndex, + wasmProject, + projectIsStale, + realizationIsStale, + wasmRealization, + realization, + messageHistory, } from "./stores" import {get} from "svelte/store" import {Vector2, Vector3, type Vector2Like} from "three" import type { - Entity, - ExtrusionHistoryStep, - HistoryStep, - MessageHistory, - PlaneHistoryStep, - PointHistoryStep, - Point2D, - SketchHistoryStep, - WithTarget, - WorkBench, + Entity, + ExtrusionHistoryStep, + HistoryStep, + MessageHistory, + PlaneHistoryStep, + PointHistoryStep, + SketchHistoryStep, + WithTarget, + WorkBench } from "./types" -import type {Realization as WasmRealization, Message} from "cadmium" -import { - isDeleteArcs, - isDeleteCircles, - isDeleteLines, - isNewCircleBetweenPoints, - isNewExtrusion, - isNewLineOnSketch, - isNewPointOnSketch2, - isNewRectangleBetweenPoints, - isNewSketchOnPlane, - isRenameProject, - isRenameStep, - isRenameWorkbench, - isSetSketchPlane, - isUpdateExtrusion, -} from "./typeGuards" +import type { Realization as WasmRealization, Message, Primitive, StepData, Workbench } from "cadmium" +import { isMessage } from "./typeGuards" // import { isDevelopment } from "../+layout" // prettier-ignore @@ -50,577 +34,458 @@ const log = (function () { const context = "[projectUtils.ts]"; const color = "a export const CIRCLE_TOLERANCE = 0.05 export function isPoint(feature: HistoryStep): feature is PointHistoryStep { - return feature.data.type === "Point" + return feature.data.type === "Point" } export function isPlane(feature: HistoryStep): feature is PlaneHistoryStep { - return feature.data.type === "Plane" + return feature.data.type === "Plane" } export function isExtrusion(feature: HistoryStep): feature is ExtrusionHistoryStep { - return feature.data.type === "Extrusion" + return feature.data.type === "Extrusion" } export function isSketch(feature: HistoryStep): feature is SketchHistoryStep { - return feature.data.type === "Sketch" + return feature.data.type === "Sketch" } export function arraysEqual(a: any[], b: any[]) { - if (a.length !== b.length) return false - for (let i = 0; i < a.length; i++) { - if (a[i] !== b[i]) return false - } - return true + if (a.length !== b.length) return false + for (let i = 0; i < a.length; i++) { + if (a[i] !== b[i]) return false + } + return true } function sendWasmMessage(message: Message) { - let wp = get(wasmProject) - log("[sendWasmMessage] sending message:", message) - let result = wp.send_message(message) - log("[sendWasmMessage] reply:", result) - - messageHistory.update((history: MessageHistory[]) => { - log("[sendWasmMessage] [messageHistory.update] update:", {message, result}) - return [...history, {message, result}] - }) - return result + let wp = get(wasmProject) + log("[sendWasmMessage] sending message:", message) + let result = wp.send_message(message) + log("[sendWasmMessage] reply:", result) + + messageHistory.update((history: MessageHistory[]) => { + log("[sendWasmMessage] [messageHistory.update] update:", {message, result}) + return [...history, {message, result}] + }) + return result } -export function updateExtrusion(extrusionId: string, sketchId: string, length: number, faceIds: string[]) { - const message: Message = { - UpdateExtrusion: { - workbench_id: get(workbenchIndex), - sketch_id: sketchId, - face_ids: faceIds.map(id => +id), // on browser side ids are strings - coerce face ids to numbers here to suit rust - length, - offset: 0.0, - extrusion_name: "Extra", - direction: "Normal", - extrusion_id: extrusionId, - }, - } - const isValid = checkWasmMessage(message) - const hasFaceIds = notEmpty(message.UpdateExtrusion.face_ids) - if (isValid) { - sendWasmMessage(message) - workbenchIsStale.set(true) - if (hasFaceIds) { - log("[updateExtrusion]", "[checkWasmMessage]", "is valid,", "sending message...", message) - // sendWasmMessage(message) - } else log("[updateExtrusion]", "[checkWasmMessage]", "is valid,", "but face_ids is empty,", "NOT sending message:", message) - } else log("[updateExtrusion]", "[checkWasmMessage]", "is bogus,", "abort message send!", message) - - // sendWasmMessage(message) - - // should this be set stale when not sending the wasm message? todo - // workbenchIsStale.set(true) +export function updateExtrusion(extrusionId: number, sketchId: number, length: number, faceIds: string[]) { + const message: Message = { + UpdateExtrusion: { + workbench_id: get(workbenchIndex), + sketch_id: sketchId, + face_ids: faceIds.map(id => +id), // on browser side ids are strings - coerce face ids to numbers here to suit rust + length, + offset: 0.0, + extrusion_name: "Extra", + direction: "Normal", + extrusion_id: extrusionId, + }, + } + const isValid = checkWasmMessage(message) + const hasFaceIds = notEmpty(message.UpdateExtrusion.face_ids) + if (isValid) { + sendWasmMessage(message) + workbenchIsStale.set(true) + if (hasFaceIds) { + log("[updateExtrusion]", "[checkWasmMessage]", "is valid,", "sending message...", message) + // sendWasmMessage(message) + } else log("[updateExtrusion]", "[checkWasmMessage]", "is valid,", "but face_ids is empty,", "NOT sending message:", message) + } else log("[updateExtrusion]", "[checkWasmMessage]", "is bogus,", "abort message send!", message) + + // sendWasmMessage(message) + + // should this be set stale when not sending the wasm message? todo + // workbenchIsStale.set(true) } -export function setSketchPlane(sketchId: string, planeId: string) { - const message: Message = { - SetSketchPlane: { - workbench_id: get(workbenchIndex), - sketch_id: sketchId, - plane_id: planeId, - }, - } - checkWasmMessage(message) - sendWasmMessage(message) - workbenchIsStale.set(true) +export function setSketchPlane(sketchId: number, planeId: number) { + const message: Message = { + SetSketchPlane: { + workbench_id: get(workbenchIndex), + sketch_id: sketchId, + plane_id: planeId + } + } + checkWasmMessage(message) + sendWasmMessage(message) } export function newSketchOnPlane() { - const message: Message = { - NewSketchOnPlane: { - workbench_id: get(workbenchIndex), - plane_id: "", // leave it floating at first - sketch_name: "", // a sensible name will be generated by the rust code - }, - } - checkWasmMessage(message) - sendWasmMessage(message) - workbenchIsStale.set(true) + const message: Message = { + StepAction: { + name: "", + data: { + workbench_id: get(workbenchIndex), + plane_id: "", // leave it floating at first + } + } + } + checkWasmMessage(message) + sendWasmMessage(message) } export function newExtrusion() { - const bench: WorkBench = get(workbench) - // log("[newExtrusion] workbench:", workbench) - // log("[newExtrusion] bench:", bench) - - let sketchId = null - for (let step of bench.history) { - if (step.data.type === "Sketch") { - sketchId = step.unique_id - } - } - if (sketchId === null) { - log("No sketch found in history") - return - } - - const message: Message = { - NewExtrusion: { - workbench_id: get(workbenchIndex), - sketch_id: sketchId, - face_ids: [], - length: 25, - offset: 0.0, - extrusion_name: "", - direction: "Normal", - }, - } - - // we check for face_ids: [] to contain numbers but we send an empty array - // todo: maybe change isNewExtrusion? although with the rust api it is possible to send an array of faceids so we ought to check them... - // probably best to alter isNewExtrusion to allow an empty array or a number[] - checkWasmMessage(message) - sendWasmMessage(message) - workbenchIsStale.set(true) + const bench: Workbench = get(workbench) + // log("[newExtrusion] workbench:", workbench) + // log("[newExtrusion] bench:", bench) + + let sketchId = "" + for (let step of bench.history) { + if (step.data.type === "Sketch") { + sketchId = step.unique_id + } + } + if (sketchId === "") { + log("No sketch found in history") + return + } + + const message: Message = { + StepAction: { + data: { + workbench_id: get(workbenchIndex), + sketch_id: sketchId, + face_ids: [], + length: 25, + offset: 0.0, + extrusion_name: "", + direction: "Normal", + }, + } + } + + // we check for face_ids: [] to contain numbers but we send an empty array + // todo: maybe change isNewExtrusion? although with the rust api it is possible to send an array of faceids so we ought to check them... + // probably best to alter isNewExtrusion to allow an empty array or a number[] + checkWasmMessage(message) + sendWasmMessage(message) } export function deleteEntities(sketchIdx: string, selection: Entity[]) { - const workbenchIdx = get(workbenchIndex) - - // log("[deleteEntities]", "sketchIdx:", sketchIdx, "selection:", selection, "workbenchIdx:", workbenchIdx, "sketchIdx:", sketchIdx, "selection:", selection) - const lines = selection.filter(e => e.type === "line") - const arcs = selection.filter(e => e.type === "arc") - const circles = selection.filter(e => e.type === "circle") - // const points = selection.filter((e) => e.type === 'point') - - const lineIds = reduceToInts( - lines.map(e => e.id), - (id: any) => console.error(`[deleteEntities] line id is not an int: ${id}`), - ) - const arcIds = reduceToInts( - arcs.map(e => e.id), - (id: any) => console.error(`[deleteEntities] arc id is not an int: ${id}`), - ) - const circleIds = reduceToInts( - circles.map(e => e.id), - (id: any) => console.error(`[deleteEntities] circle id is not an int: ${id}`), - ) - - if (notEmpty(lineIds)) deleteLines(workbenchIdx, sketchIdx, lineIds) - if (notEmpty(arcIds)) deleteArcs(workbenchIdx, sketchIdx, arcIds) - if (notEmpty(circleIds)) deleteCircles(workbenchIdx, sketchIdx, circleIds) - - // only refresh the workbench once, after all deletions are done - workbenchIsStale.set(true) -} - -function deleteLines(workbenchIdx: number, sketchIdx: string, lineIds: number[]) { - const message: Message = { - DeleteLines: { - workbench_id: workbenchIdx, - sketch_id: sketchIdx, - line_ids: lineIds, - }, - } - checkWasmMessage(message) - sendWasmMessage(message) -} - -function deleteArcs(workbenchIdx: number, sketchIdx: string, arcIds: number[]) { - const message: Message = { - DeleteArcs: { - workbench_id: workbenchIdx, - sketch_id: sketchIdx, - arc_ids: arcIds, - }, - } - checkWasmMessage(message) - sendWasmMessage(message) -} - -function deleteCircles(workbenchIdx: number, sketchIdx: string, circleIds: number[]) { - const message: Message = { - DeleteCircles: { - workbench_id: workbenchIdx, - sketch_id: sketchIdx, - circle_ids: circleIds, - }, - } - checkWasmMessage(message) - sendWasmMessage(message) + const workbenchIdx = get(workbenchIndex) + + // log("[deleteEntities]", "sketchIdx:", sketchIdx, "selection:", selection, "workbenchIdx:", workbenchIdx, "sketchIdx:", sketchIdx, "selection:", selection) + const lines = selection.filter(e => e.type === "line") + const arcs = selection.filter(e => e.type === "arc") + const circles = selection.filter(e => e.type === "circle") + // const points = selection.filter((e) => e.type === 'point') + + const lineIds = reduceToInts( + lines.map(e => e.id), + (id: any) => console.error(`[deleteEntities] line id is not an int: ${id}`), + ) + const arcIds = reduceToInts( + arcs.map(e => e.id), + (id: any) => console.error(`[deleteEntities] arc id is not an int: ${id}`), + ) + const circleIds = reduceToInts( + circles.map(e => e.id), + (id: any) => console.error(`[deleteEntities] circle id is not an int: ${id}`), + ) + + if (notEmpty(lineIds)) deleteLines(workbenchIdx, sketchIdx, lineIds) + if (notEmpty(arcIds)) deleteArcs(workbenchIdx, sketchIdx, arcIds) + if (notEmpty(circleIds)) deleteCircles(workbenchIdx, sketchIdx, circleIds) + + // only refresh the workbench once, after all deletions are done + workbenchIsStale.set(true) } export function addRectangleBetweenPoints(sketchIdx: string, point1: number, point2: number) { - log("[addRectangleBetweenPoints] sketchIdx, point1, point2", sketchIdx, point1, point2) - const message: Message = { - NewRectangleBetweenPoints: { - workbench_id: get(workbenchIndex), - sketch_id: sketchIdx, - start_id: point1, - end_id: point2, - }, - } - checkWasmMessage(message) - sendWasmMessage(message) - workbenchIsStale.set(true) + log("[addRectangleBetweenPoints] sketchIdx, point1, point2", sketchIdx, point1, point2) + const message: Message = { + NewRectangleBetweenPoints: { + workbench_id: get(workbenchIndex), + sketch_id: sketchIdx, + start_id: point1, + end_id: point2 + } + } + checkWasmMessage(message) + sendWasmMessage(message) } export function addCircleBetweenPoints(sketchIdx: string, point1: string, point2: string) { - log("[addCircleBetweenPoints]", "sketchIdx:", sketchIdx, "point1:", point1, "point2", point2) - - const p1Valid = isStringInt(point1, id => console.error("[projectUtils.ts] [addCircleBetweenPoints]", "id is not an int:", id)) - const p2Valid = isStringInt(point2, id => console.error("[projectUtils.ts] [addCircleBetweenPoints]", "id is not an int:", id)) - - if (!p1Valid || !p2Valid) return - - const message: Message = { - NewCircleBetweenPoints: { - workbench_id: get(workbenchIndex), - sketch_id: sketchIdx, - center_id: parseInt(point1, 10), - edge_id: parseInt(point2, 10), - }, - } - checkWasmMessage(message) - sendWasmMessage(message) - workbenchIsStale.set(true) + log("[addCircleBetweenPoints]", "sketchIdx:", sketchIdx, "point1:", point1, "point2", point2) + + const p1Valid = isStringInt(point1, id => console.error("[projectUtils.ts] [addCircleBetweenPoints]", "id is not an int:", id)) + const p2Valid = isStringInt(point2, id => console.error("[projectUtils.ts] [addCircleBetweenPoints]", "id is not an int:", id)) + + if (!p1Valid || !p2Valid) return + + const message: Message = { + NewCircleBetweenPoints: { + workbench_id: get(workbenchIndex), + sketch_id: sketchIdx, + center_id: parseInt(point1, 10), + edge_id: parseInt(point2, 10) + } + } + checkWasmMessage(message) + sendWasmMessage(message) } export function addLineToSketch(sketchIdx: string, point1: number, point2: number) { - const message: Message = { - NewLineOnSketch: { - workbench_id: get(workbenchIndex), - sketch_id: sketchIdx, - start_point_id: point1, - end_point_id: point2, - }, - } - checkWasmMessage(message) - sendWasmMessage(message) - workbenchIsStale.set(true) + const message: Message = { + NewLineOnSketch: { + workbench_id: get(workbenchIndex), + sketch_id: sketchIdx, + start_point_id: point1, + end_point_id: point2 + } + } + checkWasmMessage(message) + sendWasmMessage(message) } export function addPointToSketch(sketchIdx: string, point: Vector2Like, hidden: boolean) { - log("[addPointToSketch] sketchIdx, point, hidden", sketchIdx, point, hidden) - const message: Message = { - NewPointOnSketch2: { - workbench_id: get(workbenchIndex), - sketch_id: sketchIdx, - x: point.x, - y: point.y, - hidden: hidden, - }, - } - checkWasmMessage(message) - const reply = sendWasmMessage(message) - // log("[addPointToSketch sendWasmMessage]", "message:", message, "reply:", reply) - - if (!reply.success) console.error("ERROR [projectUtils.ts addPointToSketch sendWasmMessage]", "message:", message, "reply:", reply) - - workbenchIsStale.set(true) - return JSON.parse(reply.success).id + log("[addPointToSketch] sketchIdx, point, hidden", sketchIdx, point, hidden) + const message: Message = { + NewPointOnSketch2: { + workbench_id: get(workbenchIndex), + sketch_id: sketchIdx, + x: point.x, + y: point.y, + hidden: hidden, + }, + } + checkWasmMessage(message) + const reply = sendWasmMessage(message) + // log("[addPointToSketch sendWasmMessage]", "message:", message, "reply:", reply) + + if (!reply.success) console.error("ERROR [projectUtils.ts addPointToSketch sendWasmMessage]", "message:", message, "reply:", reply) + + workbenchIsStale.set(true) + return JSON.parse(reply.data).id +} + +export function addPrimitiveToSketch(sketchIdx: string, primitive: Primitive): number { + const message: Message = { + AddSketchPrimitive: { + workbench_id: get(workbenchIndex), + sketch_id: sketchIdx, + primitive + } + } + checkWasmMessage(message) + const reply = sendWasmMessage(message) + + if (!reply.success) + console.error("ERROR [projectUtils.ts addPrimitiveToSketch sendWasmMessage]", "message:", message, "reply:", reply) + + return JSON.parse(reply.data).id } export function renameStep(stepIdx: number, newName: string): void { - log("[renameStep] stepIdx, newName", stepIdx, newName) - const message: Message = { - RenameStep: { - workbench_id: get(workbenchIndex), - step_id: stepIdx, - new_name: newName, - }, - } - checkWasmMessage(message) - sendWasmMessage(message) + log("[renameStep] stepIdx, newName", stepIdx, newName) + const message: Message = { + RenameStep: { + workbench_id: get(workbenchIndex), + step_id: stepIdx, + new_name: newName, + }, + } + checkWasmMessage(message) + sendWasmMessage(message) } export function renameWorkbench(newName: string): void { - log("[renameWorkbench] newName", newName) - const message: Message = { - RenameWorkbench: { - workbench_id: get(workbenchIndex), - new_name: newName, - }, - } - checkWasmMessage(message) - sendWasmMessage(message) + log("[renameWorkbench] newName", newName) + const message: Message = { + RenameWorkbench: { + workbench_id: get(workbenchIndex), + new_name: newName, + }, + } + checkWasmMessage(message) + sendWasmMessage(message) } export function renameProject(newName: string): void { - log("[renameProject] newName", newName) - const message: Message = { - RenameProject: { - new_name: newName, - }, - } - checkWasmMessage(message) - sendWasmMessage(message) + log("[renameProject] newName", newName) + const message: Message = { + RenameProject: { + new_name: newName, + }, + } + checkWasmMessage(message) + sendWasmMessage(message) } // If the project ever becomes stale, refresh it. This should be pretty rare. projectIsStale.subscribe(value => { - if (value) { - const wp = get(wasmProject) - project.set(JSON.parse(wp.to_json())) - - workbenchIndex.set(0) - workbenchIsStale.set(true) - projectIsStale.set(false) - // @ts-ignore - log("[projectIsStale] Refreshing project", "value:", value, "wasmProject:", wp, "project:", project) - } + if (value) { + const wp = get(wasmProject) + project.set(JSON.parse(wp.to_json())) + + workbenchIndex.set(0) + workbenchIsStale.set(true) + projectIsStale.set(false) + // @ts-ignore + log("[projectIsStale] Refreshing project", "value:", value, "wasmProject:", wp, "project:", project) + } }) // If the workbench ever becomes stale, refresh it. This should be very common. // Every time you edit any part of the feature history, for example workbenchIsStale.subscribe(value => { - if (value) { - log("[workbenchIsStale] Workbench:", get(workbench)) - const workbenchIdx = get(workbenchIndex) - const wasmProj = get(wasmProject) - const workbenchJson = wasmProj.get_workbench(workbenchIdx) - // TODO: reach inside of project and set its representation - // of the workbench to the new one that we just got - workbench.set(JSON.parse(workbenchJson)) - workbenchIsStale.set(false) - // log("Workbench:", get(workbench)) - realizationIsStale.set(true) - } + if (value) { + log("[workbenchIsStale] Workbench:", get(workbench)) + const workbenchIdx = get(workbenchIndex) + const wasmProj = get(wasmProject) + const workbenchJson = wasmProj.get_workbench(workbenchIdx) + // TODO: reach inside of project and set its representation + // of the workbench to the new one that we just got + workbench.set(workbenchJson) + workbenchIsStale.set(false) + // log("Workbench:", get(workbench)) + realizationIsStale.set(true) + } }) // If the realization ever becomes stale, refresh it. This should be very common. // Every time you edit any part of the feature history, for example realizationIsStale.subscribe(value => { - if (value) { - // log("[realizationIsStale] Refreshing realization") - - const wasmProj = get(wasmProject) - const workbenchIdx = get(workbenchIndex) - const wasmReal: WasmRealization = wasmProj.get_realization(workbenchIdx, get(featureIndex) + 1) - wasmRealization.set(wasmReal) - realization.set(JSON.parse(wasmReal.to_json())) - // log("[realizationIsStale] New realization:", get(realization)) - // log("[wasmProj]", wasmProj) - - realizationIsStale.set(false) - } + if (value) { + // log("[realizationIsStale] Refreshing realization") + + const wasmProj = get(wasmProject) + const workbenchIdx = get(workbenchIndex) + const wasmReal: WasmRealization = wasmProj.get_realization(workbenchIdx, get(featureIndex) + 1) + wasmRealization.set(wasmReal) + realization.set(JSON.parse(wasmReal.to_json())) + // log("[realizationIsStale] New realization:", get(realization)) + // log("[wasmProj]", wasmProj) + + realizationIsStale.set(false) + } }) export function getObjectString(solidId: string): string { - // log("[getObjectString] solidId:", solidId) - const wasmReal = get(wasmRealization) - const objString = wasmReal.solid_to_obj(solidId, 0.1) - return objString + // log("[getObjectString] solidId:", solidId) + const wasmReal = get(wasmRealization) + const objString = wasmReal.solid_to_obj(solidId, 0.1) + return objString } export function readFile(e: WithTarget): void { - const target = e.target as HTMLInputElement - const file = target.files![0] - const reader = new FileReader() - reader.onload = function (e) { - // log("[readFile] file contents", e.target?.result) - } - reader.readAsText(file) + const target = e.target as HTMLInputElement + const file = target.files![0] + const reader = new FileReader() + reader.onload = function (e) { + // log("[readFile] file contents", e.target?.result) + } + reader.readAsText(file) } export function arcToPoints(center: Vector2, start: Vector2, end: Vector2, clockwise: boolean = false): Vector2[] { - // log("[arcToPoints] center, start, end, clockwise", center, start, end, clockwise) - // see https://math.stackexchange.com/a/4132095/816177 - const tolerance = CIRCLE_TOLERANCE // in meters - const radius = start.distanceTo(center) - const k = tolerance / radius - // more precise but slower to calculate: - // const n = Math.ceil(Math.PI / Math.acos(1 - k)) - // faster to calculate, at most only overestimates by 1: - let n = Math.ceil(Math.PI / Math.sqrt(2 * k)) - const segmentAngle = (2 * Math.PI) / n - const segmentLength = radius * segmentAngle - if (clockwise) n = -n - - const startAngle = Math.atan2(start.y - center.y, start.x - center.x) - - const lineVertices = [] - lineVertices.push(start.clone()) - for (let i = 1; i <= Math.abs(n); i++) { - const theta = ((2 * Math.PI) / n) * i + startAngle - const xComponent = radius * Math.cos(theta) - const yComponent = radius * Math.sin(theta) - const point = new Vector2(xComponent, yComponent).add(center) - lineVertices.push(point) - - const distanceToEnd = point.distanceTo(end) - if (distanceToEnd <= segmentLength) { - lineVertices.push(end.clone()) - break - } - } - return lineVertices + // log("[arcToPoints] center, start, end, clockwise", center, start, end, clockwise) + // see https://math.stackexchange.com/a/4132095/816177 + const tolerance = CIRCLE_TOLERANCE // in meters + const radius = start.distanceTo(center) + const k = tolerance / radius + // more precise but slower to calculate: + // const n = Math.ceil(Math.PI / Math.acos(1 - k)) + // faster to calculate, at most only overestimates by 1: + let n = Math.ceil(Math.PI / Math.sqrt(2 * k)) + const segmentAngle = (2 * Math.PI) / n + const segmentLength = radius * segmentAngle + if (clockwise) n = -n + + const startAngle = Math.atan2(start.y - center.y, start.x - center.x) + + const lineVertices: Vector2[] = [] + lineVertices.push(start.clone()) + for (let i = 1; i <= Math.abs(n); i++) { + const theta = ((2 * Math.PI) / n) * i + startAngle + const xComponent = radius * Math.cos(theta) + const yComponent = radius * Math.sin(theta) + const point = new Vector2(xComponent, yComponent).add(center) + lineVertices.push(point) + + const distanceToEnd = point.distanceTo(end) + if (distanceToEnd <= segmentLength) { + lineVertices.push(end.clone()) + break + } + } + return lineVertices } export function circleToPoints(centerPoint: Vector2Like, radius: number): Vector2[] { - // this is 2D function - - // see https://math.stackexchange.com/a/4132095/816177 - const tolerance = CIRCLE_TOLERANCE // in meters - const k = tolerance / radius - // more precise but slower to calculate: - // const n = Math.ceil(Math.PI / Math.acos(1 - k)) - // faster to calculate, at most only overestimates by 1: - const n = Math.ceil(Math.PI / Math.sqrt(2 * k)) - - const lineVertices = [] - for (let i = 0; i <= n; i++) { - const theta = ((2 * Math.PI) / n) * i - const xComponent = radius * Math.cos(theta) - const yComponent = radius * Math.sin(theta) - const point = new Vector2(xComponent, yComponent).add(centerPoint) - lineVertices.push(point) - } - return lineVertices + // this is 2D function + + // see https://math.stackexchange.com/a/4132095/816177 + const tolerance = CIRCLE_TOLERANCE // in meters + const k = tolerance / radius + // more precise but slower to calculate: + // const n = Math.ceil(Math.PI / Math.acos(1 - k)) + // faster to calculate, at most only overestimates by 1: + const n = Math.ceil(Math.PI / Math.sqrt(2 * k)) + + const lineVertices: Vector2[] = [] + for (let i = 0; i <= n; i++) { + const theta = ((2 * Math.PI) / n) * i + const xComponent = radius * Math.cos(theta) + const yComponent = radius * Math.sin(theta) + const point = new Vector2(xComponent, yComponent).add(centerPoint) + lineVertices.push(point) + } + return lineVertices } export function promoteTo3(points: Vector2[]): Vector3[] { - const points3 = [] - for (const point of points) { - points3.push(new Vector3(point.x, point.y, 0)) - } - return points3 + const points3: Vector3[] = [] + for (const point of points) { + points3.push(new Vector3(point.x, point.y, 0)) + } + return points3 } export function flatten(points: Vector3[]): number[] { - const pointsFlat = [] - for (const point of points) { - pointsFlat.push(point.x, point.y, point.z) - } - return pointsFlat + const pointsFlat: number[] = [] + for (const point of points) { + pointsFlat.push(point.x, point.y, point.z) + } + return pointsFlat } function isStringInt(s: string, errorCallback: {(id: any): void; (arg0: string): void}): boolean { - if (typeof s !== "string") console.error("[proectUtils.ts] [isStringInt]", s, "is not a string:", typeof s) - const isInt = !Number.isNaN(parseInt(s, 10)) - if (!isInt) errorCallback(s) - return isInt + if (typeof s !== "string") console.error("[proectUtils.ts] [isStringInt]", s, "is not a string:", typeof s) + const isInt = !Number.isNaN(parseInt(s, 10)) + if (!isInt) errorCallback(s) + return isInt } function reduceToInts(data: string[], errorCallback: (id: any) => void): number[] { - function reducer(acc: number[], id: string): number[] { - return isStringInt(id, errorCallback) ? [...acc, parseInt(id, 10)] : acc - } - return data.reduce(reducer, []) + function reducer(acc: number[], id: string): number[] { + return isStringInt(id, errorCallback) ? [...acc, parseInt(id, 10)] : acc + } + return data.reduce(reducer, []) } function notEmpty(array: unknown[]): boolean { - return array && Array.isArray(array) && array.length > 0 + return array && Array.isArray(array) && array.length > 0 } -function checkWasmMessage(message: Message, abort = true, logError = true): boolean { - const key = Object.keys(message)[0] - const command = message[key as keyof Message] - if (!command) { - console.error("[projectUtils.ts] [checkWasmMessage]", "messageType not found:", key, message) - return false - } - log("[checkWasmMessage]", "checking...", key, message) - - switch (key) { - case "UpdateExtrusion": - if (!isUpdateExtrusion(command)) { - logOrAbort() - return false - } - return true - - case "SetSketchPlane": - if (!isSetSketchPlane(command)) { - logOrAbort() - return false - } - return true - - case "NewSketchOnPlane": - if (!isNewSketchOnPlane(command)) { - logOrAbort() - return false - } - return true - - case "NewExtrusion": - if (!isNewExtrusion(command)) { - logOrAbort() - return false - } - return true - - case "DeleteLines": - if (!isDeleteLines(command)) { - logOrAbort() - return false - } - return true - - case "DeleteArcs": - if (!isDeleteArcs(command)) { - logOrAbort() - return false - } - return true - - case "DeleteCircles": - if (!isDeleteCircles(command)) { - logOrAbort() - return false - } - return true - - case "NewRectangleBetweenPoints": - if (!isNewRectangleBetweenPoints(command)) { - logOrAbort() - return false - } - return true - - case "NewCircleBetweenPoints": - if (!isNewCircleBetweenPoints(command)) { - logOrAbort() - return false - } - return true - - case "NewLineOnSketch": - if (!isNewLineOnSketch(command)) { - logOrAbort() - return false - } - return true - - case "NewPointOnSketch2": - if (!isNewPointOnSketch2(command)) { - logOrAbort() - return false - } - return true - - case "RenameStep": - if (!isRenameStep(command)) { - logOrAbort() - return false - } - return true - - case "RenameWorkbench": - if (!isRenameWorkbench(command)) { - logOrAbort() - return false - } - return true - - case "RenameProject": - if (!isRenameProject(command)) { - logOrAbort() - return false - } - return true - - default: - console.error("[projectUtils.ts] [checkWasmMessage]", "messageType typeGuard not implemented:", key) - return false - } - - function logOrAbort() { - const error = `[${key}] message failed typecheck:` - if (logError) console.error("[projectUtils.ts]", error, message) - // if (abort && isDevelopment()) throw new Error(`"[projectUtils.ts]" ${error}`) - return false - } +export function checkWasmMessage(message: Message, abort = true, logError = true): boolean { + const key = Object.keys(message)[0] + const command = message[key as keyof Message] + if (!command) { + console.error("[projectUtils.ts] [checkWasmMessage]", "messageType not found:", key, message) + return false + } + log("[checkWasmMessage]", "checking...", key, message) + + function logOrAbort() { + const error = `[${key}] message failed typecheck:` + if (logError) console.error("[projectUtils.ts]", error, message) + // if (abort && isDevelopment()) throw new Error(`"[projectUtils.ts]" ${error}`) + return false + } + + if (!isMessage(command)) { + logOrAbort() + return false + } + return true } diff --git a/packages/shared/sketch.ts b/packages/shared/sketch.ts new file mode 100644 index 00000000..6c21901f --- /dev/null +++ b/packages/shared/sketch.ts @@ -0,0 +1,59 @@ +import { Message, Primitive } from "cadmium"; +import { get } from "svelte/store"; +import { workbenchIndex } from "./stores"; +import { sendWasmMessage } from "./projectUtils"; + +export class ISketch { + id: string; + + constructor(id: string) { + this.id = id + } + + addPrimitive(primitive: Primitive): number { + const message: Message = { + AddSketchPrimitive: { + workbench_id: get(workbenchIndex), + sketch_id: this.id, + primitive + } + } + + const reply = sendWasmMessage(message) + + if (!reply.success) + console.error("ERROR [projectUtils.ts addPrimitiveToSketch sendWasmMessage]", "message:", message, "reply:", reply) + + return JSON.parse(reply.data).id + } + + deletePrimitives(ids: number[]) { + const message: Message = { + DeleteSketchPrimitives: { + workbench_id: get(workbenchIndex), + sketch_id: this.id, + ids + } + } + + sendWasmMessage(message) + } + + setPlane(plane_id: number) { + const message: Message = { + SetSketchPlane: { + workbench_id: get(workbenchIndex), + sketch_id: this.id, + plane_id: `${plane_id}` + } + } + + sendWasmMessage(message) + } + + addCircle(center: number, external: number): number { + return this.addPrimitive({ + Circle.new() + }) + } +} diff --git a/packages/shared/stores.ts b/packages/shared/stores.ts index 086cfa0c..44ede424 100644 --- a/packages/shared/stores.ts +++ b/packages/shared/stores.ts @@ -1,6 +1,6 @@ -import {Project as WasmProject, Realization as WasmRealization} from "cadmium" +import {Project as WasmProject, Realization as WasmRealization, Workbench} from "cadmium" import {writable} from "svelte/store" -import type {WorkBench, MessageHistory, Project, Realization, Entity, EntityType, SnapEntity, PointLikeById, PreviewGeometry} from "./types" +import type {MessageHistory, Project, Realization, Entity, EntityType, SnapEntity, PointLikeById, PreviewGeometry} from "./types" import {isArcEntity, isCircleEntity, isEntity, isFaceEntity, isLineEntity, isMeshFaceEntity, isPlaneEntity, isPoint3DEntity, isPointEntity} from "./typeGuards" // import { isDevelopment } from "../+layout" @@ -13,7 +13,7 @@ export const project = writable(emptyProject()) export const projectIsStale = writable(false) export const workbenchIndex = writable(0) -export const workbench = writable(emptyWorkBench()) +export const workbench = writable(emptyWorkBench()) export const workbenchIsStale = writable(false) export const featureIndex = writable(1000) diff --git a/packages/shared/types.d.ts b/packages/shared/types.d.ts index 15e5fdc2..a28d547b 100644 --- a/packages/shared/types.d.ts +++ b/packages/shared/types.d.ts @@ -1,4 +1,4 @@ -import { Message, MessageResult } from "cadmium" +import { Message, MessageResult, Workbench } from "cadmium" import type { Vector2, Vector3, Vector2Like, Vector3Like } from "three" interface IDictionary { @@ -60,16 +60,7 @@ type MeshFaceEntity = { interface Project { name: string assemblies: [] - workbenches: WorkBench[] -} - -interface WorkBench { - name: string - history: HistoryStep[] - renaming: boolean - step_counters: { - Extrusion: number, Plane: number, Point: number, Sketch: number - } + workbenches: Workbench[] } interface Realization {