Compare commits
742 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f75afc2b37 | |||
| 34c922feb1 | |||
| cee05260c1 | |||
| add470699b | |||
| f5bcd805e8 | |||
| cf4bb14240 | |||
| 1ee1650fef | |||
| 70f5f672a6 | |||
| b329ec8c97 | |||
| b54cfe16e8 | |||
| 357859827c | |||
| f35457dff8 | |||
| f993c38ea9 | |||
| 624537cc5d | |||
| ebb7f1b3bf | |||
| ac2a95d968 | |||
| 2440c44f44 | |||
| 33dfa1229c | |||
| ea61bd06b8 | |||
| e071aca299 | |||
| 4a8a980034 | |||
| b05447dc30 | |||
| 7a36f9ec08 | |||
| 80e3971706 | |||
| dc770b7a79 | |||
| f8e7d391be | |||
| 3092ba3056 | |||
| 9d10006116 | |||
| 95f4d051d6 | |||
| c79ad8e87d | |||
| 9797dfbfc0 | |||
| 63b00a6257 | |||
| 36091845a6 | |||
| 89e1855e2c | |||
| b7f5725282 | |||
| dc8c8cedf4 | |||
| 31e8b838dd | |||
| baf7f32c2a | |||
| ad1d58bd4c | |||
| ec86688750 | |||
| 84098f4c42 | |||
| 77f9d73698 | |||
| 812e8dd2f3 | |||
| 5c19d9aa16 | |||
| 29d706e1b6 | |||
| 25d02673a7 | |||
| 6a4dc62622 | |||
| bb9a5ea604 | |||
| 2e995bd0fa | |||
| 38e6096ea9 | |||
| 30e3398cd4 | |||
| 3f7e98aff5 | |||
| 1ad8429928 | |||
| a6c820183b | |||
| 56b6c813c0 | |||
| 8ab110f7ec | |||
| 11ff018f18 | |||
| 227f63d266 | |||
| 9e8aaf4f3b | |||
| afcb760bbf | |||
| 58283439ab | |||
| 0c29a2ec0a | |||
| 4ec62d4168 | |||
| 8d596823ed | |||
| ccd62e3452 | |||
| 1bd08497e6 | |||
| d23cc5a738 | |||
| 3e2ac4b5b6 | |||
| 928bc15ff1 | |||
| e410e40060 | |||
| d1f2c938b1 | |||
| 388ec2bdfd | |||
| b057c6c0da | |||
| 40089bdbb8 | |||
| 49dd688219 | |||
| 6993e8cb83 | |||
| 709015b9d8 | |||
| c5e0dfa36b | |||
| 1f72741b62 | |||
| 0f8f7e02be | |||
| f3e188b4f8 | |||
| 346b0210a5 | |||
| 5d072bbb1d | |||
| fcc8527c9f | |||
| 2814dabe14 | |||
| 0f6150b272 | |||
| e3bc770369 | |||
| 1bb6ce7b63 | |||
| 6bf05f6ffd | |||
| 684c868cd0 | |||
| 27009a61e9 | |||
| 27ff32f584 | |||
| 223ede3ec7 | |||
| 3b2e609888 | |||
| f4caa55298 | |||
| 166d686b9d | |||
| f3f43eebd5 | |||
| 3c16b35232 | |||
| 86fb480a15 | |||
| abef8f85d9 | |||
| 33a7009585 | |||
| 0249ecb313 | |||
| bbf8a3b45e | |||
| f1eec55633 | |||
| f2bc9a9701 | |||
| 1eb662c6a7 | |||
| c08d4c398f | |||
| a81a44ca1d | |||
| c4c4431ed2 | |||
| fd23d40a5c | |||
| f2d6e03ad8 | |||
| d5c9097afa | |||
| 90cd0706ba | |||
| 2daf0de073 | |||
| 3653ece109 | |||
| c29e0bf8ee | |||
| 95a84eda53 | |||
| 8116402023 | |||
| f6238eb29c | |||
| 0a4c9ec380 | |||
| 4b8b1393ba | |||
| ee01b28645 | |||
| 06225a8aab | |||
| 1129d169cb | |||
| a787bfb8a3 | |||
| 700ec92ed2 | |||
| 7fbf0a827b | |||
| b6f659e91f | |||
| e9628cde77 | |||
| cd919353f7 | |||
| 6400138a3c | |||
| 9b8ddcc706 | |||
| ba3e0b275e | |||
| 45a68b3c5f | |||
| 49379a3e33 | |||
| b30ffdc7d1 | |||
| 2e53a2fea4 | |||
| 9dc51ee1dc | |||
| 58e7ac5138 | |||
| 6bc9bfdc28 | |||
| cce1f3b092 | |||
| ebcbe29398 | |||
| 59b6b21736 | |||
| 6c9823776a | |||
| c77e7642b7 | |||
| 9240fce897 | |||
| a87b7a830e | |||
| aad7943f1a | |||
| 589b309d91 | |||
| 95cb7c35de | |||
| aab3f1792a | |||
| 80a6e424e4 | |||
| f08cac36e7 | |||
| aa01b5ccf4 | |||
| da7d260542 | |||
| 64e0f65e29 | |||
| fedf036e2f | |||
| b9df9b09de | |||
| 3afdb916b3 | |||
| 8d7b0adea4 | |||
| 7a6d956ccf | |||
| d272743642 | |||
| c3ce77fe4d | |||
| 564aa58ebf | |||
| 48fe756fcd | |||
| 8e2f546cd6 | |||
| 3219172fed | |||
| 098d576a0b | |||
| 1950c412c2 | |||
| b646b2c7b1 | |||
| 19f22296b4 | |||
| 240fb6cbbe | |||
| 5ca6a3f2c2 | |||
| 06fef6ca91 | |||
| dcbe2d7814 | |||
| b66850b994 | |||
| 6a337ccdad | |||
| 92fd5387ef | |||
| 6205afdda5 | |||
| d060c3b8cf | |||
| 5dd3dd713a | |||
| 4fc1053af7 | |||
| 80204faedf | |||
| 06a47a2f9b | |||
| b65a7a3d4a | |||
| 7064574387 | |||
| 169ae4d32a | |||
| 4d44b41e2f | |||
| a1f835dc77 | |||
| e2172216a5 | |||
| 965c3e9c6e | |||
| 65e4812ba1 | |||
| 87df86f723 | |||
| fd32371be3 | |||
| 19c1334bb3 | |||
| 7a36450143 | |||
| d37fce644b | |||
| 00bcefba56 | |||
| d53bc064e1 | |||
| 50affbedf1 | |||
| f00aeecead | |||
| bbf2ee79be | |||
| abb1f042d4 | |||
| 04b4e02d05 | |||
| 144990db7a | |||
| a5592cdd7d | |||
| 249e3a0a84 | |||
| f869139d2a | |||
| 3954369db5 | |||
| 7165d99b76 | |||
| 8234db872d | |||
| 9acc84387f | |||
| 4eb1f2b773 | |||
| 4d60607a92 | |||
| 09f809fc8f | |||
| 9f6833c36e | |||
| 1cc8ed804a | |||
| eb0f036641 | |||
| f140c41af6 | |||
| 8a91634f55 | |||
| b025819307 | |||
| 3ec98d3dd2 | |||
| ba182d48c4 | |||
| f35853cff1 | |||
| d64ad50d32 | |||
| ab0b012f58 | |||
| dfa8ff2929 | |||
| 14b628cd3d | |||
| dfc1d4aaf7 | |||
| 8c39da370f | |||
| 532eba37a5 | |||
| 39207e5716 | |||
| 48ee5c1ffe | |||
| b41eb812ab | |||
| fece1a0151 | |||
| 3a9e03b982 | |||
| 96f0db5f63 | |||
| 56a43bca06 | |||
| 5f451f0143 | |||
| 4cda3ff143 | |||
| 167a691294 | |||
| 2f320d360e | |||
| 25ebdb8c3b | |||
| afd52e788f | |||
| 7997eedb05 | |||
| e98aecaa82 | |||
| 73e44c6b0e | |||
| c2b7752539 | |||
| 050fc61377 | |||
| 3ab4774dc8 | |||
| d9727005cf | |||
| f73fc604d0 | |||
| 4bd3dcaf61 | |||
| a2ecf1d20e | |||
| 74228b13dd | |||
| 5aa39fefa4 | |||
| 0205636b28 | |||
| 9869ed3b63 | |||
| 6a29e0c7cd | |||
| 011e1b90c1 | |||
| a1189e3878 | |||
| 4fb801b44c | |||
| 80eeb2de39 | |||
| 753760fee4 | |||
| 8fbb01dee4 | |||
| 41f2b93347 | |||
| 98a6f3251d | |||
| 8287ed2e57 | |||
| 3f5cf49971 | |||
| efc2c0b07c | |||
| 242625ebe3 | |||
| 5f5af2b15a | |||
| ff33b82013 | |||
| dbe059f507 | |||
| 9d19e52037 | |||
| f0495ab093 | |||
| e57dacc6ef | |||
| 69dbf2f117 | |||
| 3cf3867877 | |||
| 3577f3fd6a | |||
| 242af16e81 | |||
| 20f36ffe19 | |||
| b103d0da09 | |||
| 4add9c5d72 | |||
| aecb15e549 | |||
| bff8140559 | |||
| fe98f71be2 | |||
| f2008502f3 | |||
| 2981c1270b | |||
| 3011921df9 | |||
| 79ecba213a | |||
| 06db6653fb | |||
| 86200091dc | |||
| 4b2a678605 | |||
| 1ca2c03ec0 | |||
| 528b81d294 | |||
| d0dd57a731 | |||
| 25258fd39f | |||
| a884f7a6ea | |||
| 6e0df0d4e4 | |||
| 58ff3e667e | |||
| 3f5c933ee9 | |||
| d137c7b991 | |||
| 5eec02296b | |||
| 681acd81c8 | |||
| 09dd819504 | |||
| 211296c803 | |||
| 0fd5448342 | |||
| 3002d7a31b | |||
| 8eee1ede58 | |||
| 75763d0929 | |||
| 2f2a67a569 | |||
| 7246d239be | |||
| 4d1fe83c1e | |||
| 1097bba539 | |||
| eb25b6d821 | |||
| 80515fdc69 | |||
| 4a340ef1ae | |||
| b6a96802a8 | |||
| f2a0ffa9ee | |||
| 24479811f7 | |||
| 2153db9704 | |||
| dbcc820304 | |||
| 830632fa1e | |||
| 2f57ca96ca | |||
| 1b9ee91880 | |||
| 8fb7519e7b | |||
| 9dce548a05 | |||
| 487253babe | |||
| f245584428 | |||
| 1ac784d290 | |||
| 314a962014 | |||
| 6b07a0fff4 | |||
| d3208de754 | |||
| 4ba19231b7 | |||
| a93ac797fc | |||
| 4569b1e623 | |||
| ae3222683e | |||
| 780184c562 | |||
| 310b29292d | |||
| 8593200a48 | |||
| e53ede3349 | |||
| 892396f1e9 | |||
| c2ba9bc8f5 | |||
| 3f8ec1d259 | |||
| fd203bbfea | |||
| 5ac0c64c9e | |||
| db367d92cb | |||
| 289ed58812 | |||
| d1a65510ef | |||
| 4f58f77c92 | |||
| 10084b30da | |||
| 88f1365fa6 | |||
| f767f53c24 | |||
| 9322e4bfe8 | |||
| 31a9452ef0 | |||
| c69f70c7ac | |||
| 7ff0778c7e | |||
| 0cd08b5cef | |||
| 46eed71a56 | |||
| 623270117e | |||
| c5d081a765 | |||
| b2d9aba778 | |||
| 12b8bb20d5 | |||
| 6e6214133a | |||
| 2c35c4c1b2 | |||
| 80f9c54111 | |||
| 508cf6790f | |||
| 1c20949f58 | |||
| 88e17d66b8 | |||
| 625dd64f87 | |||
| b22d94606c | |||
| 309a657420 | |||
| 95d79391dd | |||
| 49ec641f7d | |||
| 3410e1613b | |||
| b013855c97 | |||
| 78f1a4e23e | |||
| bf4afe0180 | |||
| 7714e8ba28 | |||
| 5ff0965ee6 | |||
| 0add4fabec | |||
| 8df7559e76 | |||
| a1f5d0f21f | |||
| 1fe08136de | |||
| 7ef35cd4e1 | |||
| f9a75772ed | |||
| b85f805330 | |||
| 6af53c56b3 | |||
| 100dc9213b | |||
| 8068962cdf | |||
| aad9246463 | |||
| 0dbf691f16 | |||
| 9628ab5bdf | |||
| db195a4285 | |||
| 7d52ac9568 | |||
| 8c08bf87cc | |||
| 1c979f821c | |||
| 8589c7b467 | |||
| 64bf623ee4 | |||
| 36069e2908 | |||
| 54e129d0b4 | |||
| cffcaf74a1 | |||
| 1bd60a92b6 | |||
| 37fc1ff57f | |||
| 9bfc73a53d | |||
| 32f3415680 | |||
| ede16df1b1 | |||
| 0f067ce968 | |||
| 0e03226a55 | |||
| f5b436ea62 | |||
| 70de8b7a45 | |||
| 87dea86ba6 | |||
| d8eef2b51c | |||
| f452562fff | |||
| a559158931 | |||
| cc08df5c88 | |||
| 4aff5aaca9 | |||
| ea2243a14a | |||
| 15f88773d2 | |||
| e703116e78 | |||
| ae081295d5 | |||
| 2ae25f22be | |||
| 5c2ecc6f97 | |||
| 2ee6636dde | |||
| fdd05f7958 | |||
| 62e3e80f65 | |||
| 6953a34645 | |||
| 8d704b1034 | |||
| 816ff52669 | |||
| 105ef9b713 | |||
| a38f82a91a | |||
| 1388fb3c5a | |||
| 5d1be43263 | |||
| 1da76dadf8 | |||
| 188efd7ea5 | |||
| f5f5c513a6 | |||
| 70e0e1e727 | |||
| 9301186b63 | |||
| 0689cf34a1 | |||
| 8299ca84af | |||
| 5f1228091e | |||
| 7afb2ec18a | |||
| 758c6f48cd | |||
| 44ba054e07 | |||
| 0f2b208b90 | |||
| accdd00880 | |||
| de4ed8e55a | |||
| 5bd46de070 | |||
| fb23cc3eaf | |||
| fafa529df4 | |||
| 5ed98f7acf | |||
| e542a7d99b | |||
| 9635f04db8 | |||
| cd40e32b4e | |||
| 8319a73edf | |||
| aca7547f6c | |||
| c4061182f9 | |||
| d1dcf86357 | |||
| 70d52db17f | |||
| d2392b0d2b | |||
| 14bf10f1bb | |||
| 10d94e5d28 | |||
| dff6b3101e | |||
| 497b04a70b | |||
| 31137a9fd8 | |||
| f3f65a037a | |||
| a680bea021 | |||
| 1138cc12d4 | |||
| 13e995db53 | |||
| 572eeecb3a | |||
| f0d39dc39f | |||
| 74e84f803d | |||
| 79f09e5364 | |||
| 57f671dd60 | |||
| d862ace188 | |||
| fd46280130 | |||
| 0c68eb1a6a | |||
| 11e4860364 | |||
| f8474777c0 | |||
| 242476a43a | |||
| 75398aa830 | |||
| 9827c46988 | |||
| 8c7908b4ef | |||
| 76aaf2fd41 | |||
| a8b2500c0a | |||
| 3a863cd0dd | |||
| a57fbe6e3d | |||
| f346a3918c | |||
| e13a910700 | |||
| 71320a5acc | |||
| 6f409ee228 | |||
| 44f3c88c81 | |||
| 8b3aaefe8c | |||
| f63345e304 | |||
| f81752b41e | |||
| 482a3189d8 | |||
| c8714d0df8 | |||
| fe69091c5c | |||
| 457380bc3c | |||
| 6613e1a7a6 | |||
| 027df4f5d9 | |||
| 171105f827 | |||
| f2363586aa | |||
| 28a7a02ee5 | |||
| dce78ceeca | |||
| a5824702ab | |||
| bb8486c94a | |||
| d32fc23e14 | |||
| 3e85a018fc | |||
| dd2e222c84 | |||
| bcdabb1226 | |||
| 8c1a5b4463 | |||
| 4811329d9e | |||
| 6c3ab4baef | |||
| a7295e7b25 | |||
| fb8fda27c5 | |||
| 32b114274c | |||
| 02a9da050f | |||
| c6ba0ff86d | |||
| 0d386aa93d | |||
| d439f65463 | |||
| 345bae9463 | |||
| cb1f9c0480 | |||
| 2be74ce617 | |||
| 56675ef88d | |||
| 4c381bd809 | |||
| 8c3885ece8 | |||
| 15bd3f5070 | |||
| 6fd4571d34 | |||
| 5081177653 | |||
| 0d0ca2c811 | |||
| 230e27a162 | |||
| 669fd19c2e | |||
| 139f8ccb33 | |||
| 7f625bd468 | |||
| 4d07ba7637 | |||
| 7431b57e0e | |||
| 57c19ba3c5 | |||
| 8e3c2ece2f | |||
| cfc58d9456 | |||
| e74a6cebb1 | |||
| 5e01d5a976 | |||
| a2f7ab422f | |||
| 8c58d9d14c | |||
| 90e61b6dc1 | |||
| 5c479e4c0e | |||
| 97d20ad7b1 | |||
| 2800b84747 | |||
| 511d69314e | |||
| 24e7f02213 | |||
| c1d7947085 | |||
| 21281e5d77 | |||
| 29bbdc69a2 | |||
| efe6e6a4a0 | |||
| f036109020 | |||
| 86bc2dc590 | |||
| a34b6ad0c2 | |||
| e436949ef9 | |||
| 6d8f4a4a80 | |||
| dabb65427a | |||
| 57ece17e8b | |||
| 4362f737d0 | |||
| b55df58313 | |||
| afa6722253 | |||
| a3d4881578 | |||
| 1af158a5e0 | |||
| 47857a9db0 | |||
| 3b026e6027 | |||
| d572609f75 | |||
| 37032cc7aa | |||
| 6027325878 | |||
| 5ddb2aa052 | |||
| 67a18821cc | |||
| 2688a69286 | |||
| 56216a6137 | |||
| 319cbf8960 | |||
| d7ac6c0b95 | |||
| 201ca5f26e | |||
| 89528437b1 | |||
| 91bde24fe9 | |||
| 991b0f9ff1 | |||
| ee1715ff8a | |||
| 70ee09b9bb | |||
| 83dd62d03f | |||
| 94cc0ac3f7 | |||
| 36cb94d3d7 | |||
| c60baf78c5 | |||
| d72cfd3522 | |||
| a26618a4f7 | |||
| eaf370407d | |||
| a2b50fe5a1 | |||
| 7e62f76841 | |||
| fc804f16d3 | |||
| 6c7da24595 | |||
| b284d39328 | |||
| 907185c9bb | |||
| a189a2e1c0 | |||
| 1fad926275 | |||
| 99c147fe2f | |||
| e2adf710b3 | |||
| 9509344533 | |||
| 6fabc6cae6 | |||
| c39298687d | |||
| bcdddbf930 | |||
| b5684e34f6 | |||
| 2203fe98f8 | |||
| bbfdf2863b | |||
| f25f6cb16c | |||
| 9e4e533ba8 | |||
| 8db12ca9b9 | |||
| 366c864247 | |||
| 52136d3ef6 | |||
| fe764d7f0c | |||
| 669dd67521 | |||
| f74b2c37f0 | |||
| ebb4e32fff | |||
| 25903baf83 | |||
| cb6d6d7ad8 | |||
| fd2687aa3c | |||
| f5c65068de | |||
| 235b49d8c6 | |||
| a1ec137c67 | |||
| b95f621272 | |||
| a1fcdad0e3 | |||
| 584e543964 | |||
| ef20c5240c | |||
| 9fe12a018a | |||
| 8411c01f1b | |||
| 63b82a30cb | |||
| 0b6e39cf38 | |||
| b7efcec517 | |||
| 0ab0096aac | |||
| cbd8fc99bb | |||
| 2a9287e762 | |||
| 98c70f237c | |||
| 048047cf05 | |||
| 6118215cae | |||
| 8e06432fe6 | |||
| cb8620ff8a | |||
| e0eddea8ab | |||
| 8497d0c195 | |||
| 1ce394c08f | |||
| aabbf87dda | |||
| 7e31787d37 | |||
| f1259587fd | |||
| 7f913e3af0 | |||
| 5433c34a4d | |||
| 2c52e93660 | |||
| 3c3b24cf98 | |||
| 3012b0ebcb | |||
| 35b96eaa4e | |||
| c229bb2414 | |||
| 318bd086c0 | |||
| e3eb29daa4 | |||
| 3359c8f275 | |||
| cfc5007d00 | |||
| 0a630457be | |||
| fa124c2312 | |||
| 977994e141 | |||
| d9cbc80ee7 | |||
| 45038fad79 | |||
| cda621d735 | |||
| 2d052d1379 | |||
| 46ce2bc0df | |||
| 80e2216aa0 | |||
| 10d69aa44b | |||
| b1481bd259 | |||
| e19fc9ef4e | |||
| 448eb856b2 | |||
| 06a7d416c7 | |||
| 0753436899 | |||
| 40e0a49b11 | |||
| b17b9c9de4 | |||
| f035111ffa | |||
| fa073f754f | |||
| 83919375f9 | |||
| defb11bc89 | |||
| 548b3ad352 | |||
| d2deecf099 | |||
| fbda0110cd | |||
| ba9ad9ac07 | |||
| d13db2b44a | |||
| 2c127c85f3 | |||
| cc099c0ff1 | |||
| 448ab70c3b | |||
| 020eba2727 | |||
| d8117e2c30 | |||
| 57a6d88e6b | |||
| 74cb3e5f85 | |||
| d0aeecc863 | |||
| 2803308c5e | |||
| 326f04f67d | |||
| 68972ba8f9 | |||
| 73a028b56f | |||
| 33890ef0ee | |||
| 53fe676ab6 | |||
| f027c7eca4 | |||
| ea2b6dfb0e | |||
| 83610cec38 | |||
| 2ed0685b10 | |||
| 877e616188 | |||
| 2e3e258a9a | |||
| 1e1c790eb4 | |||
| 2148fe8dee | |||
| 41c87a8661 | |||
| 96c9add526 | |||
| f425316e60 | |||
| abf363894e | |||
| 8fc8b97b7a | |||
| 64b4175585 | |||
| 4eefda3305 | |||
| f7275140d5 | |||
| 17a36f4fc2 | |||
| ba931e8205 | |||
| 572925cfbb | |||
| 03f94d6371 | |||
| c220337aec | |||
| 1b67304cfc | |||
| 25a1ec1cd1 | |||
| 8296e48c28 | |||
| ca81ac6fd6 | |||
| 88212b94b6 | |||
| 3375a04007 | |||
| c62538fc8f | |||
| fd83e80bbd | |||
| 0a2c1f9f32 | |||
| 2975737377 | |||
| 72dea53eee | |||
| a1aa267a30 | |||
| df30b65fdb | |||
| 6176abb4a3 | |||
| 5c2d2d195f | |||
| 7a784a4a8d | |||
| 2c8f2c96a9 | |||
| bc7400cb36 | |||
| 421c68a143 | |||
| 409adda784 | |||
| f5f1c6a8d0 | |||
| 1eca91a030 | |||
| 6b872f11d9 | |||
| fd3742b800 |
+9
-134
@@ -1,142 +1,17 @@
|
||||
version: 2.1
|
||||
|
||||
orbs:
|
||||
python: circleci/python@1.3.2
|
||||
# Using windows for builds
|
||||
win: circleci/windows@2.4.0
|
||||
# Upload artifacts to s3
|
||||
aws-s3: circleci/aws-s3@2.0.0
|
||||
|
||||
# Define the jobs we want to run for this project
|
||||
jobs:
|
||||
build-connector: # Reusable job for basic connectors
|
||||
executor:
|
||||
name: win/default # comes with python 3.7.3
|
||||
shell: cmd.exe
|
||||
parameters:
|
||||
slug:
|
||||
type: string
|
||||
default: ""
|
||||
steps:
|
||||
- checkout
|
||||
- attach_workspace:
|
||||
at: ./
|
||||
- run:
|
||||
name: Install specklepy with python 3.7
|
||||
shell: powershell.exe
|
||||
command: |
|
||||
$pyarr=(python --version).split(' ')[1].split('.')
|
||||
$pyver=($pyarr[0..1] -join '.')
|
||||
echo "using python version:" $pyver
|
||||
$specklepy=(python patch_version.py)
|
||||
python -m pip install --target=./modules-$pyver specklepy==$specklepy
|
||||
- run:
|
||||
name: Install python 3.9 and specklepy
|
||||
shell: powershell.exe
|
||||
command: |
|
||||
choco install python --version=3.9.2
|
||||
$pyarr=(C:\Python39\python.exe --version).split(' ')[1].split('.')
|
||||
$pyver=($pyarr[0..1] -join '.')
|
||||
echo "using python version:" $pyver
|
||||
$specklepy=(python patch_version.py)
|
||||
C:\Python39\python.exe -m pip install --target=./modules-$pyver specklepy==$specklepy
|
||||
- run:
|
||||
name: Patch
|
||||
shell: powershell.exe
|
||||
command:
|
||||
| # If no tag, use 0.0.0.1 and don't make any YML (for testing only!)
|
||||
$tag = if([string]::IsNullOrEmpty($env:CIRCLE_TAG)) { "0.0.1" } else { $env:CIRCLE_TAG }
|
||||
$semver = $tag.replace("-beta","")
|
||||
$version = "$($semver).$($env:CIRCLE_BUILD_NUM)"
|
||||
$channel = "latest"
|
||||
if($tag -like "*-beta") { $channel = "beta" }
|
||||
# only create the yml if we have a tag
|
||||
New-Item -Force "speckle-sharp-ci-tools/Installers/blender/$channel.yml" -ItemType File -Value "version: $version"
|
||||
echo $version
|
||||
ls
|
||||
python patch_version.py $version
|
||||
speckle-sharp-ci-tools\InnoSetup\ISCC.exe speckle-sharp-ci-tools\blender.iss
|
||||
- persist_to_workspace:
|
||||
root: ./
|
||||
paths:
|
||||
- speckle-sharp-ci-tools/Installers
|
||||
|
||||
install-specklepy: # due to ujson dep, we need to match the py version to the install
|
||||
build:
|
||||
docker:
|
||||
- image: "cimg/python:<<parameters.tag>>"
|
||||
parameters:
|
||||
tag:
|
||||
default: "3.9"
|
||||
type: string
|
||||
- image: cimg/base:2023.03
|
||||
steps:
|
||||
- checkout
|
||||
- run:
|
||||
name: upgrade pip and install specklepy
|
||||
command: |
|
||||
specklepyver=$(python patch_version.py)
|
||||
echo installing specklepy $specklepyver
|
||||
python -m pip install --target=./modules-<<parameters.tag>> specklepy==$specklepyver
|
||||
- persist_to_workspace:
|
||||
root: ./
|
||||
paths:
|
||||
- modules-*
|
||||
|
||||
get-ci-tools: # Clones our ci tools and persists them to the workspace
|
||||
docker:
|
||||
- image: cimg/base:2021.01
|
||||
steps:
|
||||
- run: # Could not get ssh to work, so using a personal token
|
||||
name: Clone
|
||||
command: git clone https://$GITHUB_TOKEN@github.com/specklesystems/speckle-sharp-ci-tools.git speckle-sharp-ci-tools
|
||||
- persist_to_workspace:
|
||||
root: ./
|
||||
paths:
|
||||
- speckle-sharp-ci-tools
|
||||
|
||||
deploy: # Uploads all installers found to S3
|
||||
docker:
|
||||
- image: cimg/base:2021.01
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: ./
|
||||
- run:
|
||||
name: List contents
|
||||
command: ls -R speckle-sharp-ci-tools/Installers
|
||||
- aws-s3/copy:
|
||||
arguments: "--recursive --endpoint=https://$SPACES_REGION.digitaloceanspaces.com --acl public-read"
|
||||
aws-access-key-id: SPACES_KEY
|
||||
aws-region: SPACES_REGION
|
||||
aws-secret-access-key: SPACES_SECRET
|
||||
from: '"speckle-sharp-ci-tools/Installers/"'
|
||||
to: s3://speckle-releases/installers/
|
||||
- run: echo "so long and thanks for all the fish"
|
||||
|
||||
# Orchestrate our job run sequence
|
||||
workflows:
|
||||
main:
|
||||
build_and_test:
|
||||
when:
|
||||
false
|
||||
jobs:
|
||||
- get-ci-tools:
|
||||
filters:
|
||||
tags:
|
||||
only: /.*/
|
||||
branches:
|
||||
only:
|
||||
- main
|
||||
- /ci\/.*/
|
||||
- build-connector:
|
||||
slug: blender
|
||||
requires:
|
||||
- get-ci-tools
|
||||
filters:
|
||||
tags:
|
||||
only: /.*/
|
||||
branches:
|
||||
only:
|
||||
- main
|
||||
- /ci\/.*/
|
||||
- deploy:
|
||||
requires:
|
||||
- get-ci-tools
|
||||
- build-connector
|
||||
filters:
|
||||
tags:
|
||||
only: /[0-9]+(\.[0-9]+)*/
|
||||
branches:
|
||||
ignore: /.*/ # For testing only! /ci\/.*/
|
||||
- build
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
name: Update issue Status
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [closed]
|
||||
|
||||
jobs:
|
||||
update_issue:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Get project data
|
||||
env:
|
||||
GITHUB_TOKEN: ${{secrets.GHPROJECT_TOKEN}}
|
||||
ORGANIZATION: specklesystems
|
||||
PROJECT_NUMBER: 9
|
||||
run: |
|
||||
gh api graphql --header 'GraphQL-Features: projects_next_graphql' -f query='
|
||||
query($org: String!, $number: Int!) {
|
||||
organization(login: $org){
|
||||
projectNext(number: $number) {
|
||||
id
|
||||
fields(first:20) {
|
||||
nodes {
|
||||
id
|
||||
name
|
||||
settings
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}' -f org=$ORGANIZATION -F number=$PROJECT_NUMBER > project_data.json
|
||||
|
||||
echo 'PROJECT_ID='$(jq '.data.organization.projectNext.id' project_data.json) >> $GITHUB_ENV
|
||||
echo 'STATUS_FIELD_ID='$(jq '.data.organization.projectNext.fields.nodes[] | select(.name== "Status") | .id' project_data.json) >> $GITHUB_ENV
|
||||
|
||||
echo "$PROJECT_ID"
|
||||
echo "$STATUS_FIELD_ID"
|
||||
|
||||
echo 'DONE_ID='$(jq '.data.organization.projectNext.fields.nodes[] | select(.name== "Status") | .settings | fromjson | .options[] | select(.name== "Done") | .id' project_data.json) >> $GITHUB_ENV
|
||||
echo "$DONE_ID"
|
||||
|
||||
- name: Add Issue to project #it's already in the project, but we do this to get its node id!
|
||||
env:
|
||||
GITHUB_TOKEN: ${{secrets.GHPROJECT_TOKEN}}
|
||||
ISSUE_ID: ${{ github.event.issue.node_id }}
|
||||
run: |
|
||||
item_id="$( gh api graphql --header 'GraphQL-Features: projects_next_graphql' -f query='
|
||||
mutation($project:ID!, $id:ID!) {
|
||||
addProjectNextItem(input: {projectId: $project, contentId: $id}) {
|
||||
projectNextItem {
|
||||
id
|
||||
}
|
||||
}
|
||||
}' -f project=$PROJECT_ID -f id=$ISSUE_ID --jq '.data.addProjectNextItem.projectNextItem.id')"
|
||||
|
||||
echo 'ITEM_ID='$item_id >> $GITHUB_ENV
|
||||
|
||||
- name: Update Status
|
||||
env:
|
||||
GITHUB_TOKEN: ${{secrets.GHPROJECT_TOKEN}}
|
||||
ISSUE_ID: ${{ github.event.issue.node_id }}
|
||||
run: |
|
||||
gh api graphql --header 'GraphQL-Features: projects_next_graphql' -f query='
|
||||
mutation($project:ID!, $status:ID!, $id:ID!, $value:String!) {
|
||||
set_status: updateProjectNextItemField(
|
||||
input: {
|
||||
projectId: $project
|
||||
itemId: $id
|
||||
fieldId: $status
|
||||
value: $value
|
||||
}
|
||||
) {
|
||||
projectNextItem {
|
||||
id
|
||||
}
|
||||
}
|
||||
}' -f project=$PROJECT_ID -f status=$STATUS_FIELD_ID -f id=$ITEM_ID -f value=${{ env.DONE_ID }}
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
name: Move new issues into Project
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
jobs:
|
||||
track_issue:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Get project data
|
||||
env:
|
||||
GITHUB_TOKEN: ${{secrets.GHPROJECT_TOKEN}}
|
||||
ORGANIZATION: specklesystems
|
||||
PROJECT_NUMBER: 9
|
||||
run: |
|
||||
gh api graphql --header 'GraphQL-Features: projects_next_graphql' -f query='
|
||||
query($org: String!, $number: Int!) {
|
||||
organization(login: $org){
|
||||
projectNext(number: $number) {
|
||||
id
|
||||
fields(first:20) {
|
||||
nodes {
|
||||
id
|
||||
name
|
||||
settings
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}' -f org=$ORGANIZATION -F number=$PROJECT_NUMBER > project_data.json
|
||||
|
||||
echo 'PROJECT_ID='$(jq '.data.organization.projectNext.id' project_data.json) >> $GITHUB_ENV
|
||||
echo 'STATUS_FIELD_ID='$(jq '.data.organization.projectNext.fields.nodes[] | select(.name== "Status") | .id' project_data.json) >> $GITHUB_ENV
|
||||
|
||||
- name: Add Issue to project
|
||||
env:
|
||||
GITHUB_TOKEN: ${{secrets.GHPROJECT_TOKEN}}
|
||||
ISSUE_ID: ${{ github.event.issue.node_id }}
|
||||
run: |
|
||||
item_id="$( gh api graphql --header 'GraphQL-Features: projects_next_graphql' -f query='
|
||||
mutation($project:ID!, $id:ID!) {
|
||||
addProjectNextItem(input: {projectId: $project, contentId: $id}) {
|
||||
projectNextItem {
|
||||
id
|
||||
}
|
||||
}
|
||||
}' -f project=$PROJECT_ID -f id=$ISSUE_ID --jq '.data.addProjectNextItem.projectNextItem.id')"
|
||||
|
||||
echo 'ITEM_ID='$item_id >> $GITHUB_ENV
|
||||
@@ -0,0 +1,31 @@
|
||||
name: "PR workflow"
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- "v3-dev"
|
||||
jobs:
|
||||
build:
|
||||
name: Pre-commit Checks
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install uv and set the python version
|
||||
uses: astral-sh/setup-uv@v5
|
||||
with:
|
||||
python-version: "3.11"
|
||||
enable-cache: true
|
||||
cache-dependency-glob: "uv.lock"
|
||||
|
||||
- name: Install the project
|
||||
run: uv sync --all-extras --dev
|
||||
|
||||
- uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/.cache/pre-commit/
|
||||
key: ${{ hashFiles('.pre-commit-config.yaml') }}
|
||||
|
||||
- name: Run pre-commit
|
||||
run: uv run pre-commit run --all-files
|
||||
|
||||
- name: Minimize uv cache
|
||||
run: uv cache prune --ci
|
||||
@@ -0,0 +1,96 @@
|
||||
name: "Release workflow"
|
||||
on:
|
||||
push:
|
||||
branches: ["main", "installer-test/**"]
|
||||
tags: ["v3.*.*"]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build Zip
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
ZIP_NAME: "blender.zip"
|
||||
SEMVER: null
|
||||
FILE_VERSION: null
|
||||
outputs:
|
||||
semver: ${{ steps.set-version.outputs.semver }}
|
||||
fileVersion: ${{ steps.set-version.outputs.fileVersion }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: 🐍 Install uv and set the python version
|
||||
uses: astral-sh/setup-uv@v5
|
||||
with:
|
||||
python-version: "3.11"
|
||||
enable-cache: true
|
||||
cache-dependency-glob: "uv.lock"
|
||||
|
||||
- id: set-version
|
||||
name: Set version to output
|
||||
run: |
|
||||
TAG=${{ github.ref_name }}
|
||||
if [[ "${{ github.ref }}" != refs/tags/* ]]; then
|
||||
TAG="v3.0.99.${{ github.run_number }}"
|
||||
fi
|
||||
SEMVER="${TAG#v}"
|
||||
FILE_VERSION=$(echo "$TAG" | sed -E 's/^v([0-9]+\.[0-9]+\.[0-9]+).*/\1/')
|
||||
FILE_VERSION="$FILE_VERSION.${{ github.run_number }}"
|
||||
|
||||
echo "semver=$SEMVER" >> "$GITHUB_OUTPUT"
|
||||
echo "fileVersion=$FILE_VERSION" >> "$GITHUB_OUTPUT"
|
||||
|
||||
echo $SEMVER
|
||||
echo $FILE_VERSION
|
||||
|
||||
- name: ✏ Patch Version
|
||||
run: python patch_version.py ${{ steps.set-version.outputs.fileVersion }}
|
||||
|
||||
- name: 🔄 UV Sync
|
||||
run: uv sync --all-extras --dev
|
||||
|
||||
- name: 📄 Export Package Dependencies
|
||||
run: ./export_dependencies.sh
|
||||
|
||||
- name: 🗃 Zip Package
|
||||
run: zip -r ${{env.ZIP_NAME}} bpy_speckle
|
||||
|
||||
- name: ⬆️ Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: output-${{ steps.set-version.outputs.semver }}
|
||||
path: ${{env.ZIP_NAME}}
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
compression-level: 0 # no compression
|
||||
|
||||
- name: 💾 Minimize uv cache
|
||||
run: uv cache prune --ci
|
||||
|
||||
deploy-installers:
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
env:
|
||||
IS_PUBLIC_RELEASE: ${{ github.ref_type == 'tag' }}
|
||||
steps:
|
||||
- name: 🔫 Trigger Build Installer(s)
|
||||
uses: the-actions-org/workflow-dispatch@v4.0.0
|
||||
with:
|
||||
workflow: Build Installers
|
||||
repo: specklesystems/connector-installers
|
||||
token: ${{ secrets.CONNECTORS_GH_TOKEN }}
|
||||
inputs: '{
|
||||
"run_id": "${{ github.run_id }}",
|
||||
"semver": "${{ needs.build.outputs.semver }}",
|
||||
"file_version": "${{ needs.build.outputs.fileVersion }}",
|
||||
"repo": "${{ github.repository }}",
|
||||
"is_public_release": ${{ env.IS_PUBLIC_RELEASE }}
|
||||
}'
|
||||
ref: main
|
||||
wait-for-completion: true
|
||||
wait-for-completion-interval: 10s
|
||||
wait-for-completion-timeout: 10m
|
||||
display-workflow-run-url: true
|
||||
display-workflow-run-url-interval: 10s
|
||||
|
||||
- uses: geekyeggo/delete-artifact@v5
|
||||
with:
|
||||
name: output-*
|
||||
+5
-1
@@ -10,4 +10,8 @@ __pycache__/
|
||||
# dev
|
||||
.venv
|
||||
Installers/
|
||||
modules/
|
||||
modules/
|
||||
.tool-versions
|
||||
requirements.txt
|
||||
SEMVER
|
||||
dui3/
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
repos:
|
||||
- repo: local
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
name: ruff lint
|
||||
entry: uv run ruff check --force-exclude
|
||||
language: system
|
||||
types_or: [python, pyi]
|
||||
# Run the formatter.
|
||||
- id: ruff-format
|
||||
name: ruff format
|
||||
entry: uv run ruff format --force-exclude
|
||||
language: system
|
||||
types_or: [python, pyi]
|
||||
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v5.0.0
|
||||
hooks:
|
||||
- id: end-of-file-fixer
|
||||
- id: trailing-whitespace
|
||||
@@ -3,11 +3,11 @@
|
||||
Speckle | Blender
|
||||
</h1>
|
||||
<h3 align="center">
|
||||
Connector for Blender 2.92 & 2.93
|
||||
Connector for Blender
|
||||
</h3>
|
||||
<p align="center"><b>Speckle</b> is the data infrastructure for the AEC industry.</p><br/>
|
||||
|
||||
<p align="center"><a href="https://twitter.com/SpeckleSystems"><img src="https://img.shields.io/twitter/follow/SpeckleSystems?style=social" alt="Twitter Follow"></a> <a href="https://speckle.community"><img src="https://img.shields.io/discourse/users?server=https%3A%2F%2Fspeckle.community&style=flat-square&logo=discourse&logoColor=white" alt="Community forum users"></a> <a href="https://speckle.systems"><img src="https://img.shields.io/badge/https://-speckle.systems-royalblue?style=flat-square" alt="website"></a> <a href="https://speckle.guide/dev/"><img src="https://img.shields.io/badge/docs-speckle.guide-orange?style=flat-square&logo=read-the-docs&logoColor=white" alt="docs"></a></p>
|
||||
<p align="center"><a href="https://twitter.com/SpeckleSystems"><img src="https://img.shields.io/twitter/follow/SpeckleSystems?style=social" alt="Twitter Follow"></a> <a href="https://speckle.community"><img src="https://img.shields.io/discourse/users?server=https%3A%2F%2Fspeckle.community&style=flat-square&logo=discourse&logoColor=white" alt="Community forum users"></a> <a href="https://speckle.systems"><img src="https://img.shields.io/badge/https://-speckle.systems-royalblue?style=flat-square" alt="website"></a> <a href="https://docs.speckle.systems/dev/"><img src="https://img.shields.io/badge/docs-docs.speckle.systems-orange?style=flat-square&logo=read-the-docs&logoColor=white" alt="docs"></a></p>
|
||||
<p align="center"><a href="https://github.com/specklesystems/speckle-blender/"><img src="https://circleci.com/gh/specklesystems/speckle-blender.svg?style=svg&circle-token=76eabd350ea243575cbb258b746ed3f471f7ac29" alt="Speckle-Next"></a> </p>
|
||||
|
||||
# About Speckle
|
||||
@@ -25,58 +25,90 @@ What is Speckle? Check our ](https://speckle.xyz) ⇒ creating an account at our public server
|
||||
- [](https://marketplace.digitalocean.com/apps/speckle-server?refcode=947a2b5d7dc1) ⇒ deploying an instance in 1 click
|
||||
- [](https://app.speckle.systems) ⇒ creating an account at our public server
|
||||
|
||||
### Resources
|
||||
|
||||
- [](https://speckle.community) for help, feature requests or just to hang with other speckle enthusiasts, check out our community forum!
|
||||
- [](https://speckle.systems) our tutorials portal is full of resources to get you started using Speckle
|
||||
- [](https://speckle.guide/user/blender.html) reference on almost any end-user and developer functionality
|
||||
- [](https://docs.speckle.systems/connectors/blender) reference on almost any end-user and developer functionality
|
||||
|
||||
|
||||
# Repo structure
|
||||
# Blender Connector
|
||||
|
||||
The Speckle UI can be found in the 3d viewport toolbar (N), under the Speckle tab.
|
||||
|
||||
Head to the [**📚 documentation**](https://speckle.guide/user/blender.html) for more information.
|
||||
|
||||
## Disclaimer
|
||||
This code is WIP and as such should be used with extreme caution on non-sensitive projects.
|
||||
|
||||
## Installation
|
||||
|
||||
1. Place `bpy_speckle` folder in your `addons` folder. On Windows this is typically `%APPDATA%/Blender Foundation/Blender/2.80/scripts/addons`.
|
||||
2. Go to `Edit->Preferences` (Ctrl + Alt + U)
|
||||
3. Go to the `Add-ons` tab
|
||||
4. Find and enable `SpeckleBlender 2.0` in the `Scene` category. <!-- **If enabling for the first time, expect the UI to freeze for bit while it silently installs all the dependencies.** -->
|
||||
5. The Speckle UI can be found in the 3d viewport toolbar (N), under the `Speckle` tab.
|
||||
We officially support Blender 4.2 and newer, on Windows.
|
||||
|
||||
## Usage
|
||||
- Available user accounts are automatically detected and made available. To add user accounts use **Speckle Manager**.
|
||||
- Select the user from the dropdown list in the `Users` panel. This will populate the `Streams` list with available streams for the selected user.
|
||||
- Select a branch and commit from the dropdown menus.
|
||||
- Click on `Receive` to download the objects from the selected stream, branch, and commit. The stream objects will be loaded into a Blender Collection, named `<STREAM_NAME> [ <STREAM_BRANCH> @ <BRANCH_COMMIT> ]`. <!-- You can filter the stream by entering a query into the `Filter` field (i.e. `properties.weight>10` or `type="Mesh"`). -->
|
||||
- Click on `Open Stream in Web` to view the stream in your web browser.
|
||||
|
||||
## Caveats
|
||||
Once enabled in `Preferences -> Addons`,
|
||||
The Speckle connector UI can be found in the 3d viewport toolbar (N), under the `Speckle` tab.
|
||||
|
||||
- Mesh objects are supported. Breps are imported as meshes using their `displayValue` data.
|
||||
- Curves have limited support: `Polylines` are supported; `NurbsCurves` are supported, though they are not guaranteed to look the same; `Lines` are supported; `Arcs` are not supported, though they are very roughly approximated; `PolyCurves` are supported for linear / polyline segments and very approximate arc segments. These conversions are a point of focus for further development.
|
||||
- Available user accounts are automatically detected and made available.
|
||||
- Select the account from the dropdown list in the `Accounts` panel. This will populate the `Projects` list with available projects for the selected user account.
|
||||
- Select a model and version from the dropdown menus.
|
||||
- Click on `Load` to download and convert the objects from the selected model version. The objects will be linked into a Blender Collection.
|
||||
- Click on `Open Model in Web` to view the model in your web browser.
|
||||
|
||||
## Custom properties
|
||||
## Supported Elements
|
||||
|
||||
- **SpeckleBlender** will look for a `texture_coordinates` property and use that to create a UV layer for the imported object. These texture coordinates are a space-separated list of floats (`[u v u v u v etc...]`) that is encoded as a base64 blob. This is subject to change as **SpeckleBlender** develops.
|
||||
- If a `renderMaterial` property is found, **SpeckleBlender** will create a material named using the sub-property `renderMaterial.name`. If a material with that name already exists in Blender, **SpeckleBlender** will just assign that existing material to the object. This allows geometry to be updated without having to re-assign and re-create materials.
|
||||
- Vertex colors are supported. The `colors` list from Speckle meshes is translated to a vertex color layer.
|
||||
- Speckle properties will be imported as custom properties on Blender objects. Nested dictionaries are expanded to individual properties by flattening their key hierarchy. I.e. `propA:{'propB': {'propC':10, 'propD':'foobar'}}` is flattened to `propA.propB.propC = 10` and `propA.propB.propD = "foobar"`.
|
||||
The Blender Connector is still a work in progress and, as such, data sent from the Blender connector is a highly lossy exchange. Our connectors are ever evolving to facilitate more and more Speckle usecases. We welcome feedback, requests, edge cases, and contributions!
|
||||
|
||||
## Dependency Installation and Compatibility with Other Blender Addons
|
||||
|
||||
Upon first launch of the addon, the Speckle connector installs its SpecklePy dependencies in `%appdata%/Speckle/connector_installations` on Windows.
|
||||
|
||||
This is done through our [`installer.py`](https://github.com/specklesystems/speckle-blender/blob/main/bpy_speckle/installer.py). Through pip, we install the correct version of each dependency for your blender python version, host OS, and system architecture.
|
||||
As such, an internet connection is required for first launch of the connector.
|
||||
|
||||
Other blender addons may require dependencies that conflict with specklepy. In these cases, one or both addons may fail to load.
|
||||
If you suspect you're seeing a conflict, Please uninstall other third party addons one at a time to identify which addon is conflicting.
|
||||
|
||||
If you find an addon that conflicts, please try using a different version of that addon (newer or older).
|
||||
|
||||
If you can't find a version of an addon that works, please let us know on [our forums](https://speckle.community/) the name of the addon, the versions you've tried, the version of the Speckle connector you've tried, and your OS (win/mac/linux).
|
||||
|
||||
## Local Development
|
||||
|
||||
Pre-resquisits:
|
||||
|
||||
- [uv](https://docs.astral.sh/uv/getting-started/installation/)
|
||||
- A supported [blender](https://www.blender.org/download/) version
|
||||
- [Blender Development](https://marketplace.visualstudio.com/items?itemName=JacquesLucke.blender-development) extension for VS Code (recommended)
|
||||
|
||||
First time setup (or anytime you change pyproject.toml)
|
||||
Run the following commands
|
||||
|
||||
```sh
|
||||
uv sync
|
||||
./export_dependencies.sh
|
||||
```
|
||||
|
||||
🪟 To activate the environment in a terminal (Windows):
|
||||
|
||||
```powershell
|
||||
.venv\Scripts\activate
|
||||
```
|
||||
|
||||
🐧 To activate the environment in a terminal (Linux / macOS):
|
||||
|
||||
```sh
|
||||
source .venv/bin/activate.fish
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
To run the blender plugin, run the `>Blender: Start`
|
||||
from VS code (`ctrl + shift + p`)
|
||||
<img width="1469" height="379" alt="image" src="https://github.com/user-attachments/assets/9dc174a0-07fc-47c7-85d1-bd5a04d8f8c7" />
|
||||
|
||||
## Contributing
|
||||
|
||||
@@ -89,6 +121,3 @@ The Speckle Community hangs out on [the forum](https://discourse.speckle.works),
|
||||
## License
|
||||
|
||||
Unless otherwise described, the code in this repository is licensed under the Apache-2.0 License. Please note that some modules, extensions or code herein might be otherwise licensed. This is indicated either in the root of the containing folder under a different license file, or in the respective file's header. If you have any questions, don't hesitate to get in touch with us via [email](mailto:hello@speckle.systems).
|
||||
|
||||
## Notes
|
||||
SpeckleBlender is written and maintained by [Tom Svilans](http://tomsvilans.com) ([Github](https://github.com/tsvilans)).
|
||||
|
||||
+223
-110
@@ -1,142 +1,255 @@
|
||||
# MIT License
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but
|
||||
# WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTIBILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
# General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
# ruff: noqa
|
||||
import bpy
|
||||
from bpy.types import WindowManager
|
||||
from .connector.ui import icons
|
||||
|
||||
# Copyright (c) 2018-2021 Tom Svilans
|
||||
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
# Ensure dependencies
|
||||
from .installer import ensure_dependencies
|
||||
|
||||
ensure_dependencies(f"Blender {bpy.app.version[0]}.{bpy.app.version[1]}")
|
||||
|
||||
bl_info = {
|
||||
"name": "SpeckleBlender 2.0",
|
||||
"author": "Speckle Systems",
|
||||
"version": (0, 2, 0),
|
||||
"blender": (2, 92, 0),
|
||||
"name": "Speckle Connector",
|
||||
"author": "Speckle",
|
||||
"version": (3, 999, 999),
|
||||
"blender": (4, 2, 0),
|
||||
"location": "3d viewport toolbar (N), under the Speckle tab.",
|
||||
"description": "The Speckle Connector using specklepy 2.0!",
|
||||
"warning": "This add-on is WIP and should be used with caution",
|
||||
"wiki_url": "https://github.com/specklesystems/speckle-blender",
|
||||
"description": "Publish models to and load models from other AEC apps.",
|
||||
"wiki_url": "https://speckle.systems/connectors/blender",
|
||||
"category": "Scene",
|
||||
}
|
||||
|
||||
import bpy
|
||||
|
||||
"""
|
||||
Import PySpeckle and attempt install if not found
|
||||
"""
|
||||
# UI
|
||||
from .connector.ui.main_panel import SPECKLE_PT_main_panel
|
||||
from .connector.ui.update_panel import SPECKLE_PT_update_panel
|
||||
from .connector.ui.model_cards_panel import SPECKLE_PT_model_cards_panel
|
||||
from .connector.utils.account_manager import speckle_workspace
|
||||
from .connector.ui.project_selection_dialog import (
|
||||
SPECKLE_OT_project_selection_dialog,
|
||||
SPECKLE_UL_projects_list,
|
||||
)
|
||||
from .connector.ui.model_selection_dialog import (
|
||||
SPECKLE_OT_model_selection_dialog,
|
||||
SPECKLE_UL_models_list,
|
||||
)
|
||||
from .connector.ui.version_selection_dialog import (
|
||||
SPECKLE_OT_version_selection_dialog,
|
||||
SPECKLE_UL_versions_list,
|
||||
)
|
||||
from .connector.ui.selection_filter_dialog import SPECKLE_OT_selection_filter_dialog
|
||||
from .connector.utils.property_groups import (
|
||||
speckle_project,
|
||||
speckle_model,
|
||||
speckle_version,
|
||||
speckle_object,
|
||||
speckle_collection,
|
||||
speckle_model_card,
|
||||
)
|
||||
|
||||
try:
|
||||
import specklepy
|
||||
except ModuleNotFoundError as error:
|
||||
print("Speckle not found.")
|
||||
# TODO: Implement automatic installation of speckle and dependencies
|
||||
# to the local Blender module folder
|
||||
# Operators
|
||||
from .connector.blender_operators.publish_button import SPECKLE_OT_publish
|
||||
from .connector.blender_operators.load_button import SPECKLE_OT_load
|
||||
from .connector.blender_operators.model_card_settings import (
|
||||
SPECKLE_OT_model_card_settings,
|
||||
SPECKLE_OT_view_in_browser,
|
||||
SPECKLE_OT_view_model_versions,
|
||||
SPECKLE_OT_delete_model_card,
|
||||
)
|
||||
from .connector.blender_operators.select_objects import SPECKLE_OT_select_objects
|
||||
from .connector.blender_operators.add_account_button import SPECKLE_OT_add_account
|
||||
from .connector.blender_operators.add_account_button import (
|
||||
SPECKLE_OT_show_auth_error,
|
||||
SPECKLE_OT_dismiss_popup,
|
||||
)
|
||||
from .connector.blender_operators.model_card_load_button import (
|
||||
SPECKLE_OT_load_model_card,
|
||||
)
|
||||
from .connector.blender_operators.model_card_publish_button import (
|
||||
SPECKLE_OT_publish_model_card,
|
||||
)
|
||||
from .connector.blender_operators.add_project_by_url import (
|
||||
SPECKLE_OT_add_project_by_url,
|
||||
)
|
||||
|
||||
# from .install_dependencies import install_dependencies
|
||||
# install_dependencies()
|
||||
from .connector.blender_operators.create_project import SPECKLE_OT_create_project
|
||||
from .connector.blender_operators.create_model import SPECKLE_OT_create_model
|
||||
from .connector.blender_operators.version_check import SPECKLE_OT_version_check
|
||||
from .connector.blender_operators.update_button import SPECKLE_OT_update_button
|
||||
from .connector.utils.account_manager import (
|
||||
speckle_account,
|
||||
get_default_account_id,
|
||||
_client_cache,
|
||||
)
|
||||
|
||||
"""
|
||||
Import SpeckleBlender classes
|
||||
"""
|
||||
|
||||
from specklepy.api.client import SpeckleClient # , SpeckleCache
|
||||
from specklepy.logging import metrics
|
||||
|
||||
from bpy_speckle.ui import *
|
||||
from bpy_speckle.properties import *
|
||||
from bpy_speckle.operators import *
|
||||
from bpy_speckle.callbacks import *
|
||||
from bpy.app.handlers import persistent
|
||||
|
||||
"""
|
||||
Add load handler to initialize Speckle when
|
||||
loading a Blender file
|
||||
"""
|
||||
# States
|
||||
from .connector.states.speckle_state import (
|
||||
register as register_speckle_state,
|
||||
unregister as unregister_speckle_state,
|
||||
)
|
||||
|
||||
|
||||
@persistent
|
||||
def load_handler(dummy):
|
||||
bpy.ops.speckle.users_load()
|
||||
from .connector.ui.workspace_selection_dialog import (
|
||||
SPECKLE_OT_workspace_selection_dialog,
|
||||
SPECKLE_UL_workspaces_list,
|
||||
)
|
||||
|
||||
# Utils
|
||||
from .connector.ui.account_selection_dialog import (
|
||||
SPECKLE_OT_account_selection_dialog,
|
||||
SPECKLE_UL_accounts_list,
|
||||
)
|
||||
|
||||
|
||||
"""
|
||||
Permanent handle on callbacks
|
||||
"""
|
||||
|
||||
callbacks = {}
|
||||
|
||||
"""
|
||||
Add Speckle classes for registering
|
||||
"""
|
||||
|
||||
speckle_classes = []
|
||||
speckle_classes.extend(operator_classes)
|
||||
speckle_classes.extend(property_classes)
|
||||
speckle_classes.extend(ui_classes)
|
||||
def delayed_version_check():
|
||||
"""Timer function to check for updates after addon startup"""
|
||||
try:
|
||||
bpy.ops.speckle.version_check()
|
||||
except Exception as e:
|
||||
print(f"[Speckle] Failed to check for updates: {e}")
|
||||
|
||||
|
||||
def register():
|
||||
from bpy.utils import register_class
|
||||
|
||||
for cls in speckle_classes:
|
||||
register_class(cls)
|
||||
|
||||
metrics.set_host_app("Blender")
|
||||
|
||||
"""
|
||||
Register all new properties
|
||||
"""
|
||||
|
||||
bpy.types.Scene.speckle = bpy.props.PointerProperty(type=SpeckleSceneSettings)
|
||||
bpy.types.Collection.speckle = bpy.props.PointerProperty(
|
||||
type=SpeckleCollectionSettings
|
||||
def invoke_window_manager_properties():
|
||||
# Accounts
|
||||
WindowManager.speckle_accounts = bpy.props.CollectionProperty(type=speckle_account)
|
||||
WindowManager.selected_account_id = bpy.props.StringProperty()
|
||||
# Workspaces
|
||||
WindowManager.speckle_workspaces = bpy.props.CollectionProperty(
|
||||
type=speckle_workspace
|
||||
)
|
||||
bpy.types.Object.speckle = bpy.props.PointerProperty(type=SpeckleObjectSettings)
|
||||
WindowManager.selected_workspace = bpy.props.PointerProperty(type=speckle_workspace)
|
||||
WindowManager.can_create_project_in_workspace = bpy.props.BoolProperty()
|
||||
# Projects
|
||||
WindowManager.speckle_projects = bpy.props.CollectionProperty(type=speckle_project)
|
||||
WindowManager.selected_project_id = bpy.props.StringProperty()
|
||||
WindowManager.selected_project_name = bpy.props.StringProperty()
|
||||
# Models
|
||||
WindowManager.speckle_models = bpy.props.CollectionProperty(type=speckle_model)
|
||||
WindowManager.selected_model_id = bpy.props.StringProperty()
|
||||
WindowManager.selected_model_name = bpy.props.StringProperty()
|
||||
# Versions
|
||||
WindowManager.speckle_versions = bpy.props.CollectionProperty(type=speckle_version)
|
||||
WindowManager.selected_version_id = bpy.props.StringProperty()
|
||||
WindowManager.selected_version_load_option = bpy.props.StringProperty()
|
||||
# Send / Publish buttons
|
||||
WindowManager.ui_mode = bpy.props.EnumProperty( # type: ignore
|
||||
name="UI Mode",
|
||||
description="Publish or Load a model",
|
||||
items=[
|
||||
("PUBLISH", "Publish", "Publish a model to Speckle", "EXPORT", 0),
|
||||
("LOAD", "Load", "Load a model from Speckle", "IMPORT", 1),
|
||||
],
|
||||
default="PUBLISH",
|
||||
)
|
||||
# Objects
|
||||
WindowManager.speckle_objects = bpy.props.CollectionProperty(type=speckle_object)
|
||||
# Update checking
|
||||
WindowManager.update_available = bpy.props.BoolProperty(default=False)
|
||||
WindowManager.latest_version = bpy.props.StringProperty(default="")
|
||||
WindowManager.update_url = bpy.props.StringProperty(default="")
|
||||
|
||||
"""
|
||||
Add callbacks
|
||||
"""
|
||||
|
||||
# Callback for displaying the current user account on top of the 3d view
|
||||
# callbacks['view3d_status'] = ((
|
||||
# bpy.types.SpaceView3D.draw_handler_remove, # Function pointer for removal
|
||||
# bpy.types.SpaceView3D.draw_handler_add(draw_speckle_info, (None, None), 'WINDOW', 'POST_PIXEL'), # Add handler
|
||||
# 'WINDOW' # Callback space for removal
|
||||
# ))
|
||||
# Classes to load
|
||||
classes = (
|
||||
SPECKLE_PT_update_panel,
|
||||
SPECKLE_PT_main_panel,
|
||||
SPECKLE_PT_model_cards_panel,
|
||||
SPECKLE_OT_publish,
|
||||
SPECKLE_OT_load,
|
||||
SPECKLE_OT_project_selection_dialog,
|
||||
speckle_project,
|
||||
SPECKLE_UL_projects_list,
|
||||
speckle_workspace,
|
||||
SPECKLE_OT_model_selection_dialog,
|
||||
speckle_model,
|
||||
SPECKLE_UL_models_list,
|
||||
SPECKLE_OT_version_selection_dialog,
|
||||
speckle_version,
|
||||
SPECKLE_UL_versions_list,
|
||||
SPECKLE_OT_selection_filter_dialog,
|
||||
speckle_object,
|
||||
speckle_collection,
|
||||
speckle_model_card,
|
||||
SPECKLE_OT_model_card_settings,
|
||||
SPECKLE_OT_view_in_browser,
|
||||
SPECKLE_OT_view_model_versions,
|
||||
SPECKLE_OT_delete_model_card,
|
||||
SPECKLE_OT_select_objects,
|
||||
SPECKLE_OT_add_account,
|
||||
SPECKLE_OT_show_auth_error,
|
||||
SPECKLE_OT_dismiss_popup,
|
||||
SPECKLE_OT_load_model_card,
|
||||
SPECKLE_OT_publish_model_card,
|
||||
SPECKLE_OT_add_project_by_url,
|
||||
SPECKLE_OT_create_project,
|
||||
SPECKLE_OT_create_model,
|
||||
SPECKLE_OT_version_check,
|
||||
SPECKLE_OT_update_button,
|
||||
speckle_account,
|
||||
SPECKLE_UL_workspaces_list,
|
||||
SPECKLE_OT_workspace_selection_dialog,
|
||||
SPECKLE_OT_account_selection_dialog,
|
||||
SPECKLE_UL_accounts_list,
|
||||
)
|
||||
|
||||
bpy.app.handlers.load_post.append(load_handler)
|
||||
|
||||
# Register and Unregister
|
||||
def register():
|
||||
icons.load_icons()
|
||||
|
||||
for cls in classes:
|
||||
bpy.utils.register_class(cls)
|
||||
register_speckle_state() # Register SpeckleState
|
||||
|
||||
invoke_window_manager_properties()
|
||||
|
||||
# Pre-warm client cache for default account
|
||||
try:
|
||||
default_account_id = get_default_account_id()
|
||||
if default_account_id:
|
||||
print(
|
||||
f"[Speckle] Pre-warming client for default account: {default_account_id}"
|
||||
)
|
||||
_client_cache.get_client(default_account_id)
|
||||
print(
|
||||
f"[Speckle] Client pre-warming complete for account: {default_account_id}"
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"[Speckle] Failed to pre-warm client: {e}")
|
||||
|
||||
# Use a timer to delay the version check
|
||||
bpy.app.timers.register(delayed_version_check, first_interval=2.0)
|
||||
|
||||
|
||||
def unregister():
|
||||
# Clear any pending timers to prevent duplicate calls
|
||||
if bpy.app.timers.is_registered(delayed_version_check):
|
||||
bpy.app.timers.unregister(delayed_version_check)
|
||||
|
||||
bpy.app.handlers.load_post.remove(load_handler)
|
||||
# Clean up authentication server
|
||||
from .connector.blender_operators.add_account_button import cleanup_auth_server
|
||||
|
||||
"""
|
||||
Remove callbacks
|
||||
"""
|
||||
cleanup_auth_server()
|
||||
|
||||
for cb in callbacks.values():
|
||||
cb[0](cb[1], cb[2])
|
||||
|
||||
from bpy.utils import unregister_class
|
||||
|
||||
for cls in reversed(speckle_classes):
|
||||
unregister_class(cls)
|
||||
icons.unload_icons()
|
||||
unregister_speckle_state() # Unregister SpeckleState
|
||||
_client_cache.clear()
|
||||
for cls in classes:
|
||||
bpy.utils.unregister_class(cls)
|
||||
|
||||
|
||||
# Run the register function when the script is executed
|
||||
if __name__ == "__main__":
|
||||
register()
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
schema_version = "1.0.0"
|
||||
|
||||
# Example of manifest file for a Blender extension
|
||||
# Change the values according to your extension
|
||||
id = "speckle_blender_addon"
|
||||
version = "3.0.0"
|
||||
name = "Speckle Connector"
|
||||
tagline = "Load models from other AEC apps into Blender with Speckle."
|
||||
maintainer = "Speckle"
|
||||
# Supported types: "add-on", "theme"
|
||||
type = "add-on"
|
||||
|
||||
# Optional link to documentation, support, source files, etc
|
||||
website = "https://speckle.systems/connectors/blender"
|
||||
|
||||
# Optional list defined by Blender and server, see:
|
||||
# https://docs.blender.org/manual/en/dev/advanced/extensions/tags.html
|
||||
tags = ["Scene"]
|
||||
|
||||
blender_version_min = "4.2.0"
|
||||
# # Optional: Blender version that the extension does not support, earlier versions are supported.
|
||||
# # This can be omitted and defined later on the extensions platform if an issue is found.
|
||||
# blender_version_max = "5.1.0"
|
||||
|
||||
# License conforming to https://spdx.org/licenses/ (use "SPDX: prefix)
|
||||
# https://docs.blender.org/manual/en/dev/advanced/extensions/licenses.html
|
||||
license = ["SPDX:Apache-2.0"]
|
||||
# Optional: required by some licenses.
|
||||
copyright = ["2022-2025 AEC SYSTEMS LTD"]
|
||||
|
||||
# Optional list of supported platforms. If omitted, the extension will be available in all operating systems.
|
||||
# platforms = ["windows-x64", "macos-arm64", "linux-x64"]
|
||||
# Other supported platforms: "windows-arm64", "macos-x64"
|
||||
|
||||
# Optional: bundle 3rd party Python modules.
|
||||
# https://docs.blender.org/manual/en/dev/advanced/extensions/python_wheels.html
|
||||
# wheels = [
|
||||
# "./wheels/hexdump-3.3-py3-none-any.whl",
|
||||
# "./wheels/jsmin-3.0.1-py3-none-any.whl",
|
||||
# ]
|
||||
|
||||
# Optional: add-ons can list which resources they will require:
|
||||
# * files (for access of any filesystem operations)
|
||||
# * network (for internet access)
|
||||
# * clipboard (to read and/or write the system clipboard)
|
||||
# * camera (to capture photos and videos)
|
||||
# * microphone (to capture audio)
|
||||
# permissions = ["files", "network", "clipboard"]
|
||||
|
||||
|
||||
# If using network, remember to also check `bpy.app.online_access`
|
||||
# https://docs.blender.org/manual/en/dev/advanced/extensions/addons.html#internet-access
|
||||
#
|
||||
# For each permission it is important to also specify the reason why it is required.
|
||||
# Keep this a single short sentence without a period (.) at the end.
|
||||
# For longer explanations use the documentation or detail page.
|
||||
#
|
||||
[permissions]
|
||||
network = "Speckle Server comms, and PyPI for dependency management"
|
||||
files = "Data caching, Account Management, Python dependency management"
|
||||
clipboard = "Copy and paste URLs and Names (UI)"
|
||||
|
||||
# Optional: build settings.
|
||||
# https://docs.blender.org/manual/en/dev/advanced/extensions/command_line_arguments.html#command-line-args-extension-build
|
||||
[build]
|
||||
paths_exclude_pattern = ["__pycache__/", "/.vscode", "*.code-workspace"]
|
||||
@@ -1,2 +0,0 @@
|
||||
from .on_mesh_edit import scb_on_mesh_edit
|
||||
from .draw_speckle_info import draw_speckle_info
|
||||
@@ -1,23 +0,0 @@
|
||||
"""
|
||||
Drawing callback to display active Speckle user
|
||||
"""
|
||||
|
||||
import blf
|
||||
import bpy
|
||||
|
||||
|
||||
def draw_speckle_info(self, context):
|
||||
"""
|
||||
Draw active user info on the 3d viewport
|
||||
"""
|
||||
scn = bpy.context.scene
|
||||
if len(scn.speckle.users) > 0:
|
||||
user = scn.speckle.users[int(scn.speckle.active_user)]
|
||||
dpi = bpy.context.preferences.system.dpi
|
||||
|
||||
blf.position(0, 100, 50, 0)
|
||||
blf.size(0, 20, dpi)
|
||||
blf.draw(0, "Active Speckle user: {} ({})".format(user.name, user.email))
|
||||
blf.position(0, 100, 20, 0)
|
||||
blf.size(0, 16, dpi)
|
||||
blf.draw(0, "Server: {}".format(user.server))
|
||||
@@ -1,14 +0,0 @@
|
||||
import bpy
|
||||
from bpy.app.handlers import persistent
|
||||
|
||||
|
||||
@persistent
|
||||
def scb_on_mesh_edit(context):
|
||||
"""
|
||||
DEPRECATED
|
||||
Do something whenever a mesh is updated
|
||||
"""
|
||||
edit_obj = bpy.context.edit_object
|
||||
if edit_obj is not None and edit_obj.is_updated_data is True:
|
||||
print("Mesh edited: {}".format(edit_obj))
|
||||
# print('>>> Update')
|
||||
@@ -1,4 +0,0 @@
|
||||
"""
|
||||
Permanent handle on all user clients
|
||||
"""
|
||||
speckle_clients = []
|
||||
@@ -0,0 +1,9 @@
|
||||
from ..blender_operators.load_button import SPECKLE_OT_load # noqa: F401
|
||||
from .model_card_load_button import SPECKLE_OT_load_model_card # noqa: F401
|
||||
from ..blender_operators.publish_button import SPECKLE_OT_publish # noqa: F401
|
||||
from ..blender_operators.model_card_settings import (
|
||||
SPECKLE_OT_model_card_settings, # noqa: F401
|
||||
SPECKLE_OT_view_in_browser, # noqa: F401
|
||||
SPECKLE_OT_view_model_versions, # noqa: F401
|
||||
SPECKLE_OT_delete_model_card, # noqa: F401
|
||||
)
|
||||
@@ -0,0 +1,259 @@
|
||||
import bpy
|
||||
import textwrap
|
||||
from bpy.types import Event, Context
|
||||
from typing import Optional
|
||||
from ..utils.authentication import (
|
||||
AuthenticationServer,
|
||||
SPECKLE_AUTH_PORT,
|
||||
)
|
||||
|
||||
|
||||
# Global auth server instance
|
||||
_auth_server = None
|
||||
|
||||
|
||||
class SPECKLE_OT_add_account(bpy.types.Operator):
|
||||
"""Operator for adding a new Speckle account."""
|
||||
|
||||
bl_idname = "speckle.add_account"
|
||||
bl_label = "Add New Account"
|
||||
bl_description = "Add a new account"
|
||||
|
||||
server_url: bpy.props.StringProperty( # type: ignore
|
||||
name="Server URL",
|
||||
description="Speckle server URL to connect to",
|
||||
default="https://app.speckle.systems",
|
||||
)
|
||||
|
||||
_timer = None
|
||||
_timeout_counter = 0
|
||||
_max_timeout = 300 # 5 minutes in seconds (300 checks at ~1 sec intervals)
|
||||
|
||||
def invoke(self, context: Context, event: Event) -> set[str]:
|
||||
return context.window_manager.invoke_props_dialog(self)
|
||||
|
||||
def draw(self, context: Context):
|
||||
layout = self.layout
|
||||
# Server URL textbox
|
||||
layout.prop(self, "server_url", text="Server URL")
|
||||
|
||||
def execute(self, context: Context) -> set[str]:
|
||||
print(f"[Add Account] Starting authentication for server: {self.server_url}")
|
||||
cleanup_auth_server()
|
||||
|
||||
# Try to start own auth server first - it will fail gracefully if port is in use
|
||||
global _auth_server
|
||||
_auth_server = AuthenticationServer(port=SPECKLE_AUTH_PORT)
|
||||
|
||||
if _auth_server.start():
|
||||
return self._initiate_own_server_flow(context)
|
||||
|
||||
# Server failed to start - port is in use
|
||||
_auth_server = None
|
||||
print(f"[Add Account] Port {SPECKLE_AUTH_PORT} is already in use")
|
||||
self.report(
|
||||
{"ERROR"},
|
||||
f"Port {SPECKLE_AUTH_PORT} is already in use. Please close any application using it and try again.",
|
||||
)
|
||||
return {"CANCELLED"}
|
||||
|
||||
def _initiate_own_server_flow(self, context: Context) -> set[str]:
|
||||
"""Start auth flow with our own server."""
|
||||
try:
|
||||
_auth_server.open_auth_url(self.server_url)
|
||||
self._start_modal_timer(context)
|
||||
return {"RUNNING_MODAL"}
|
||||
except Exception as e:
|
||||
print(f"[Add Account] Failed to open browser: {e}")
|
||||
self.report({"ERROR"}, f"Failed to open browser: {e}")
|
||||
cleanup_auth_server()
|
||||
return {"CANCELLED"}
|
||||
|
||||
def _start_modal_timer(self, context: Context):
|
||||
"""Start modal timer for auth polling."""
|
||||
self._timeout_counter = 0
|
||||
wm = context.window_manager
|
||||
self._timer = wm.event_timer_add(1.0, window=context.window)
|
||||
wm.modal_handler_add(self)
|
||||
|
||||
def modal(self, context: Context, event: Event) -> set[str]:
|
||||
global _auth_server
|
||||
|
||||
if event.type != "TIMER":
|
||||
return {"PASS_THROUGH"}
|
||||
|
||||
# Check for timeout
|
||||
self._timeout_counter += 1
|
||||
if self._timeout_counter >= self._max_timeout:
|
||||
print("[Add Account] Authentication timed out after 5 minutes")
|
||||
self._cleanup(context)
|
||||
self.report(
|
||||
{"WARNING"},
|
||||
"Authentication timed out after 5 minutes. Please try again.",
|
||||
)
|
||||
return {"CANCELLED"}
|
||||
|
||||
# Check for no active auth server
|
||||
if not _auth_server:
|
||||
print("[Add Account] No active auth server, cancelling")
|
||||
self._cleanup(context)
|
||||
return {"CANCELLED"}
|
||||
|
||||
# Check auth server completion
|
||||
if _auth_server.is_complete():
|
||||
return self._finish_auth(
|
||||
context,
|
||||
_auth_server.is_successful(),
|
||||
_auth_server.get_error_message(),
|
||||
"Auth server",
|
||||
)
|
||||
|
||||
# Still waiting
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
def _finish_auth(
|
||||
self,
|
||||
context: Context,
|
||||
is_successful: bool,
|
||||
error_msg: Optional[str],
|
||||
auth_type: str,
|
||||
) -> set[str]:
|
||||
"""Complete authentication and cleanup."""
|
||||
print(
|
||||
f"[Add Account] {auth_type} authentication complete. Success: {is_successful}"
|
||||
)
|
||||
self._cleanup(context)
|
||||
return self._handle_auth_complete(context, is_successful, error_msg)
|
||||
|
||||
def _handle_auth_complete(
|
||||
self, context: Context, is_successful: bool, error_msg: Optional[str]
|
||||
) -> set[str]:
|
||||
"""Handle authentication completion and update UI state."""
|
||||
if is_successful:
|
||||
print("[Add Account] Account added successfully - refreshing UI")
|
||||
|
||||
# Import account management functions
|
||||
from ..utils.account_manager import get_account_enum_items, _client_cache
|
||||
from ..ui.account_selection_dialog import (
|
||||
update_workspaces_list,
|
||||
update_projects_list,
|
||||
)
|
||||
|
||||
# Get the newly added account (most recent one)
|
||||
accounts = get_account_enum_items()
|
||||
if accounts and accounts[0][0] != "NO_ACCOUNTS":
|
||||
new_account_id = accounts[-1][0] # Last account added
|
||||
|
||||
# Set as selected account
|
||||
context.window_manager.selected_account_id = new_account_id
|
||||
|
||||
# Clear client cache to force re-authentication
|
||||
_client_cache.clear()
|
||||
|
||||
# Refresh UI state
|
||||
try:
|
||||
update_workspaces_list(context)
|
||||
update_projects_list(context)
|
||||
except Exception as e:
|
||||
print(f"[Add Account] Error refreshing UI state: {e}")
|
||||
|
||||
self.report({"INFO"}, "Account added successfully and is now active!")
|
||||
else:
|
||||
self.report({"INFO"}, "Account added successfully!")
|
||||
|
||||
return {"FINISHED"}
|
||||
else:
|
||||
error_details = error_msg if error_msg else "Unknown error"
|
||||
print(f"[Add Account] Authentication failed: {error_details}")
|
||||
self.report({"ERROR"}, f"Authentication failed: {error_details}")
|
||||
|
||||
# Show persistent error popup with details
|
||||
# Store error in window manager for the popup operator
|
||||
context.window_manager["speckle_auth_error"] = error_details
|
||||
bpy.ops.speckle.show_auth_error("INVOKE_DEFAULT")
|
||||
|
||||
return {"CANCELLED"}
|
||||
|
||||
def _cleanup(self, context: Context):
|
||||
# Remove timer
|
||||
if self._timer is not None:
|
||||
context.window_manager.event_timer_remove(self._timer)
|
||||
self._timer = None
|
||||
|
||||
# Shutdown auth server/authenticator
|
||||
cleanup_auth_server()
|
||||
|
||||
|
||||
def cleanup_auth_server():
|
||||
"""Shutdown auth server on addon unload."""
|
||||
global _auth_server
|
||||
|
||||
if _auth_server is not None:
|
||||
try:
|
||||
_auth_server.shutdown()
|
||||
except Exception as e:
|
||||
print(f"[Add Account] Failed to cleanup auth server: {e}")
|
||||
print(f"[Add Account] Port {SPECKLE_AUTH_PORT} may still be occupied")
|
||||
_auth_server = None
|
||||
|
||||
|
||||
class SPECKLE_OT_show_auth_error(bpy.types.Operator):
|
||||
"""Show persistent error dialog for authentication failures."""
|
||||
|
||||
bl_idname = "speckle.show_auth_error"
|
||||
bl_label = "Authentication Error"
|
||||
bl_options = {"INTERNAL"}
|
||||
|
||||
def execute(self, context: Context) -> set[str]:
|
||||
# Clean up the temporary error message
|
||||
if "speckle_auth_error" in context.window_manager:
|
||||
del context.window_manager["speckle_auth_error"]
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context: Context, event: Event) -> set[str]:
|
||||
return context.window_manager.invoke_popup(self, width=450)
|
||||
|
||||
def draw(self, context: Context):
|
||||
layout = self.layout
|
||||
|
||||
# Error header
|
||||
box = layout.box()
|
||||
row = box.row()
|
||||
row.label(text="", icon="ERROR")
|
||||
row.label(text="Authentication Failed", icon="NONE")
|
||||
|
||||
layout.separator()
|
||||
|
||||
# Error details
|
||||
error_details = context.window_manager.get(
|
||||
"speckle_auth_error", "Unknown error"
|
||||
)
|
||||
col = layout.column(align=True)
|
||||
|
||||
# Wrap long error messages
|
||||
wrapper = textwrap.TextWrapper(width=60)
|
||||
for line in error_details.split("\n"):
|
||||
if line:
|
||||
for wrapped_line in wrapper.wrap(line):
|
||||
col.label(text=wrapped_line)
|
||||
else:
|
||||
col.label(text="")
|
||||
|
||||
layout.separator()
|
||||
|
||||
# Close button
|
||||
layout.operator("speckle.dismiss_popup", text="Close", icon="X")
|
||||
|
||||
|
||||
class SPECKLE_OT_dismiss_popup(bpy.types.Operator):
|
||||
"""Dismiss popup dialog."""
|
||||
|
||||
bl_idname = "speckle.dismiss_popup"
|
||||
bl_label = "Dismiss"
|
||||
bl_options = {"INTERNAL"}
|
||||
|
||||
def execute(self, context: Context) -> set[str]:
|
||||
# Clean up any temporary data
|
||||
if "speckle_auth_error" in context.window_manager:
|
||||
del context.window_manager["speckle_auth_error"]
|
||||
return {"FINISHED"}
|
||||
@@ -0,0 +1,75 @@
|
||||
import bpy
|
||||
from bpy.types import Context, Event, UILayout
|
||||
from ..utils.account_manager import (
|
||||
get_model_details_by_wrapper,
|
||||
get_project_from_url,
|
||||
can_load,
|
||||
)
|
||||
|
||||
|
||||
class SPECKLE_OT_add_project_by_url(bpy.types.Operator):
|
||||
"""
|
||||
operator for adding a Speckle project by URL
|
||||
"""
|
||||
|
||||
bl_idname = "speckle.add_project_by_url"
|
||||
bl_label = "Add Project by URL"
|
||||
bl_description = "Add a project from a URL"
|
||||
|
||||
url: bpy.props.StringProperty( # type: ignore
|
||||
name="Project URL", description="Enter the Speckle project URL", default=""
|
||||
)
|
||||
|
||||
def execute(self, context: Context) -> set[str]:
|
||||
self.report({"INFO"}, f"Adding project from URL: {self.url}")
|
||||
|
||||
wm = context.window_manager
|
||||
|
||||
# Get project from URL
|
||||
wrapper, client, project, error_message = get_project_from_url(self.url)
|
||||
|
||||
if error_message:
|
||||
self.report({"ERROR"}, error_message)
|
||||
return {"CANCELLED"}
|
||||
|
||||
# Get model details from the wrapper
|
||||
(
|
||||
account_id,
|
||||
project_id,
|
||||
project_name,
|
||||
model_id,
|
||||
model_name,
|
||||
version_id,
|
||||
load_option,
|
||||
) = get_model_details_by_wrapper(wrapper)
|
||||
|
||||
# Check permissions
|
||||
can_load_permission, permission_error = can_load(client, project)
|
||||
if not can_load_permission:
|
||||
self.report({"ERROR"}, permission_error)
|
||||
return {"CANCELLED"}
|
||||
|
||||
# Update the window manager with the selected project/model/version
|
||||
wm.selected_account_id = account_id
|
||||
|
||||
if project_id:
|
||||
wm.selected_project_id = project_id
|
||||
wm.selected_project_name = project_name
|
||||
if model_id:
|
||||
wm.selected_model_id = model_id
|
||||
wm.selected_model_name = model_name
|
||||
if version_id:
|
||||
wm.selected_version_id = version_id
|
||||
wm.selected_version_id = version_id
|
||||
wm.selected_version_load_option = load_option
|
||||
|
||||
context.window.screen = context.window.screen
|
||||
context.area.tag_redraw()
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context: Context, event: Event) -> set[str]:
|
||||
return context.window_manager.invoke_props_dialog(self)
|
||||
|
||||
def draw(self, context: Context) -> None:
|
||||
layout: UILayout = self.layout
|
||||
layout.prop(self, "url", text="")
|
||||
@@ -0,0 +1,85 @@
|
||||
import bpy
|
||||
from bpy.types import Context, Event, UILayout
|
||||
from specklepy.core.api.inputs import CreateModelInput
|
||||
from typing import Tuple
|
||||
|
||||
from ..utils.account_manager import _client_cache, can_create_model
|
||||
|
||||
|
||||
class SPECKLE_OT_create_model(bpy.types.Operator):
|
||||
bl_idname = "speckle.create_model"
|
||||
bl_label = "Create Model"
|
||||
bl_description = "Create a new Speckle model"
|
||||
|
||||
_can_create: bool = True
|
||||
|
||||
model_name: bpy.props.StringProperty(name="Model Name") # type: ignore
|
||||
|
||||
@classmethod
|
||||
def description(cls, context: Context, properties) -> str:
|
||||
if not cls._can_create:
|
||||
return "Workspace limits have been reached"
|
||||
return "Create a new Speckle model"
|
||||
|
||||
def execute(self, context: Context) -> set[str]:
|
||||
wm = context.window_manager
|
||||
|
||||
authorized, auth_message = can_create_model(
|
||||
wm.selected_account_id, wm.selected_project_id
|
||||
)
|
||||
if not authorized:
|
||||
self.report({"ERROR"}, auth_message)
|
||||
return {"CANCELLED"}
|
||||
|
||||
if not self.model_name.strip():
|
||||
self.report({"ERROR"}, "Model name cannot be empty")
|
||||
return {"CANCELLED"}
|
||||
|
||||
try:
|
||||
model_id, model_name = create_model(
|
||||
wm.selected_account_id, wm.selected_project_id, self.model_name
|
||||
)
|
||||
wm.selected_model_id = model_id
|
||||
wm.selected_model_name = model_name
|
||||
self.report({"INFO"}, f"Created model: {model_name} -> ID: {model_id}")
|
||||
# Force redraw
|
||||
context.window.screen = context.window.screen
|
||||
context.area.tag_redraw()
|
||||
except Exception as e:
|
||||
self.report({"ERROR"}, f"Failed to create model: {str(e)}")
|
||||
return {"CANCELLED"}
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context: Context, event: Event) -> set[str]:
|
||||
return context.window_manager.invoke_props_dialog(self)
|
||||
|
||||
def draw(self, context: Context) -> None:
|
||||
layout: UILayout = self.layout
|
||||
layout.prop(self, "model_name")
|
||||
|
||||
|
||||
def register() -> None:
|
||||
bpy.utils.register_class(SPECKLE_OT_create_model)
|
||||
|
||||
|
||||
def unregister() -> None:
|
||||
bpy.utils.unregister_class(SPECKLE_OT_create_model)
|
||||
|
||||
|
||||
def create_model(account_id: str, project_id: str, model_name: str) -> Tuple[str, str]:
|
||||
try:
|
||||
# Get cached client
|
||||
client = _client_cache.get_client(account_id)
|
||||
if not client:
|
||||
raise ValueError(f"Could not get client for account: {account_id}")
|
||||
|
||||
model = client.model.create(
|
||||
input=CreateModelInput(
|
||||
name=model_name, description="", project_id=project_id
|
||||
)
|
||||
)
|
||||
return (model.id, model.name)
|
||||
except Exception as e:
|
||||
# Clear cache on error to prevent stale clients
|
||||
_client_cache.clear()
|
||||
raise e
|
||||
@@ -0,0 +1,75 @@
|
||||
import bpy
|
||||
from bpy.types import Context, Event, UILayout
|
||||
|
||||
from specklepy.core.api.inputs.project_inputs import WorkspaceProjectCreateInput
|
||||
from specklepy.core.api.enums import ProjectVisibility
|
||||
from typing import Tuple
|
||||
|
||||
from ..utils.account_manager import _client_cache
|
||||
|
||||
|
||||
class SPECKLE_OT_create_project(bpy.types.Operator):
|
||||
"""
|
||||
operator for adding a Speckle project by URL
|
||||
"""
|
||||
|
||||
bl_idname = "speckle.create_project"
|
||||
bl_label = "Create Project"
|
||||
bl_description = "Create a new Speckle project"
|
||||
|
||||
project_name: bpy.props.StringProperty(name="Project Name") # type: ignore
|
||||
|
||||
def execute(self, context: Context) -> set[str]:
|
||||
wm = context.window_manager
|
||||
project_id, project_name = create_project(
|
||||
wm.selected_account_id,
|
||||
self.project_name,
|
||||
wm.selected_workspace.id,
|
||||
)
|
||||
wm.selected_project_id = project_id
|
||||
wm.selected_project_name = project_name
|
||||
self.report({"INFO"}, f"Created project: {project_name} -> ID: {project_id}")
|
||||
# Force redraw
|
||||
context.window.screen = context.window.screen
|
||||
context.area.tag_redraw()
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context: Context, event: Event) -> set[str]:
|
||||
return context.window_manager.invoke_props_dialog(self)
|
||||
|
||||
def draw(self, context: Context) -> None:
|
||||
layout: UILayout = self.layout
|
||||
layout.prop(self, "project_name")
|
||||
|
||||
|
||||
def register() -> None:
|
||||
bpy.utils.register_class(SPECKLE_OT_create_project)
|
||||
|
||||
|
||||
def unregister() -> None:
|
||||
bpy.utils.unregister_class(SPECKLE_OT_create_project)
|
||||
|
||||
|
||||
def create_project(
|
||||
account_id: str, project_name: str, workspace_id: str
|
||||
) -> Tuple[str, str]:
|
||||
try:
|
||||
# Get cached client
|
||||
client = _client_cache.get_client(account_id)
|
||||
if not client:
|
||||
raise Exception(f"Could not get client for account: {account_id}")
|
||||
project = client.project.create_in_workspace(
|
||||
input=WorkspaceProjectCreateInput(
|
||||
name=project_name,
|
||||
description="",
|
||||
visibility=ProjectVisibility("PUBLIC"),
|
||||
workspaceId=workspace_id,
|
||||
)
|
||||
)
|
||||
|
||||
return (project.id, project.name)
|
||||
except Exception as e:
|
||||
print(f"Failed to create project: {str(e)}")
|
||||
# Clear cache on error to prevent stale clients
|
||||
_client_cache.clear()
|
||||
raise
|
||||
@@ -0,0 +1,79 @@
|
||||
import bpy
|
||||
from typing import Set
|
||||
from bpy.types import Context, Event
|
||||
from ..operations.load_operation import load_operation
|
||||
from ..utils.account_manager import get_server_url_by_account_id
|
||||
from ..utils.model_card_utils import (
|
||||
update_model_card_objects,
|
||||
delete_model_card_objects,
|
||||
model_card_exists,
|
||||
)
|
||||
|
||||
|
||||
class SPECKLE_OT_load(bpy.types.Operator):
|
||||
bl_idname = "speckle.load"
|
||||
bl_label = "Load model"
|
||||
bl_description = "Load selection from Speckle"
|
||||
|
||||
instance_loading_mode: bpy.props.EnumProperty( # type: ignore
|
||||
name="Instance Loading",
|
||||
description="Choose how to load instances",
|
||||
items=[
|
||||
(
|
||||
"INSTANCE_PROXIES",
|
||||
"Collection Instances",
|
||||
"Load objects as collection instances",
|
||||
),
|
||||
(
|
||||
"LINKED_DUPLICATES",
|
||||
"Linked Duplicates",
|
||||
"Get objects as linked duplicates",
|
||||
),
|
||||
],
|
||||
default="INSTANCE_PROXIES",
|
||||
)
|
||||
|
||||
def draw(self, context: Context) -> None:
|
||||
layout = self.layout
|
||||
row = layout.row()
|
||||
row.label(text="Instance Loading:")
|
||||
row.prop(self, "instance_loading_mode", text="")
|
||||
|
||||
def invoke(self, context: Context, event: Event) -> Set[str]:
|
||||
return context.window_manager.invoke_props_dialog(self)
|
||||
|
||||
def execute(self, context: Context) -> Set[str]:
|
||||
wm = context.window_manager
|
||||
if model_card_exists(
|
||||
wm.selected_project_id, wm.selected_model_id, False, context
|
||||
):
|
||||
model_card = context.scene.speckle_state.get_model_card_by_id(
|
||||
f"{wm.ui_mode}-{wm.selected_project_id}-{wm.selected_model_id}"
|
||||
)
|
||||
delete_model_card_objects(model_card, context)
|
||||
else:
|
||||
model_card = context.scene.speckle_state.model_cards.add()
|
||||
model_card.account_id = wm.selected_account_id
|
||||
model_card.server_url = get_server_url_by_account_id(wm.selected_account_id)
|
||||
model_card.project_id = wm.selected_project_id
|
||||
model_card.project_name = wm.selected_project_name
|
||||
model_card.model_id = wm.selected_model_id
|
||||
model_card.model_name = wm.selected_model_name
|
||||
model_card.is_publish = False
|
||||
model_card.load_option = wm.selected_version_load_option
|
||||
model_card.version_id = wm.selected_version_id
|
||||
model_card.instance_loading_mode = self.instance_loading_mode
|
||||
|
||||
converted_objects = load_operation(context, self.instance_loading_mode)
|
||||
update_model_card_objects(model_card, converted_objects)
|
||||
|
||||
# Clear selected model details from Window Manager
|
||||
wm.selected_account_id = ""
|
||||
wm.selected_project_id = ""
|
||||
wm.selected_project_name = ""
|
||||
wm.selected_model_id = ""
|
||||
wm.selected_model_name = ""
|
||||
wm.selected_version_load_option = ""
|
||||
wm.selected_version_id = ""
|
||||
|
||||
return {"FINISHED"}
|
||||
@@ -0,0 +1,81 @@
|
||||
import bpy
|
||||
from typing import Set
|
||||
from bpy.types import Context
|
||||
from ..utils.version_manager import get_latest_version
|
||||
from ..operations.load_operation import load_operation
|
||||
from ..utils.model_card_utils import (
|
||||
delete_model_card_objects,
|
||||
update_model_card_objects,
|
||||
collect_objects_with_properties,
|
||||
)
|
||||
|
||||
|
||||
class SPECKLE_OT_load_model_card(bpy.types.Operator):
|
||||
bl_idname = "speckle.model_card_load"
|
||||
bl_label = "Load Latest from Speckle"
|
||||
bl_description = "Depending on the load option, loads the latest or a specific version from Speckle"
|
||||
|
||||
model_card_id: bpy.props.StringProperty(name="Model Card ID", default="") # type: ignore
|
||||
|
||||
def execute(self, context: Context) -> Set[str]:
|
||||
wm = context.window_manager
|
||||
|
||||
# Get the model card
|
||||
model_card = context.scene.speckle_state.get_model_card_by_id(
|
||||
self.model_card_id
|
||||
)
|
||||
if model_card is None:
|
||||
self.report({"ERROR"}, "Model card not found")
|
||||
return {"CANCELLED"}
|
||||
|
||||
old_properties = collect_objects_with_properties(model_card)
|
||||
delete_model_card_objects(model_card, context)
|
||||
|
||||
# set wm
|
||||
wm.selected_account_id = model_card.account_id
|
||||
wm.selected_project_id = model_card.project_id
|
||||
wm.selected_model_name = model_card.model_name
|
||||
|
||||
# if load option is set to "LATEST"
|
||||
if model_card.load_option == "LATEST":
|
||||
# get latest version from speckle
|
||||
latest_version_id, message, timestamp = get_latest_version(
|
||||
model_card.account_id, model_card.project_id, model_card.model_id
|
||||
)
|
||||
# set version id in wm
|
||||
wm.selected_version_id = latest_version_id
|
||||
|
||||
# load latest version
|
||||
converted_objects = load_operation(
|
||||
context, model_card.instance_loading_mode
|
||||
)
|
||||
# update model card details
|
||||
update_model_card_objects(model_card, converted_objects, old_properties)
|
||||
model_card.version_id = latest_version_id
|
||||
|
||||
else:
|
||||
# set version id in wm
|
||||
wm.selected_version_id = model_card.version_id
|
||||
|
||||
# load version id
|
||||
converted_objects = load_operation(
|
||||
context, model_card.instance_loading_mode
|
||||
)
|
||||
if not converted_objects:
|
||||
self.report({"ERROR"}, "Load operation failed")
|
||||
return {"CANCELLED"}
|
||||
# update model card details
|
||||
update_model_card_objects(model_card, converted_objects, old_properties)
|
||||
|
||||
# Clear selected model details from Window Manager
|
||||
wm.selected_account_id = ""
|
||||
wm.selected_project_id = ""
|
||||
wm.selected_version_id = ""
|
||||
wm.selected_model_name = ""
|
||||
|
||||
self.report(
|
||||
{"INFO"},
|
||||
f"{len(converted_objects)} objects loaded from Speckle. Model: {model_card.model_name}, Version: {model_card.version_id}",
|
||||
)
|
||||
|
||||
return {"FINISHED"}
|
||||
@@ -0,0 +1,81 @@
|
||||
import bpy
|
||||
from typing import Set
|
||||
from bpy.types import Context, Event
|
||||
from ..operations.publish_operation import publish_operation
|
||||
from ..utils.account_manager import can_create_version
|
||||
|
||||
|
||||
class SPECKLE_OT_publish_model_card(bpy.types.Operator):
|
||||
bl_idname = "speckle.model_card_publish"
|
||||
bl_label = "Publish model"
|
||||
bl_description = "Publish tracked objects to Speckle"
|
||||
|
||||
model_card_id: bpy.props.StringProperty(name="Model Card ID", default="") # type: ignore
|
||||
version_message: bpy.props.StringProperty(name="Version Message", default="") # type: ignore
|
||||
|
||||
def draw(self, context: Context) -> None:
|
||||
layout = self.layout
|
||||
layout.prop(self, "version_message")
|
||||
|
||||
def invoke(self, context: Context, event: Event) -> Set[str]:
|
||||
return context.window_manager.invoke_props_dialog(self)
|
||||
|
||||
def execute(self, context: Context) -> Set[str]:
|
||||
wm = context.window_manager
|
||||
|
||||
# Get the model card
|
||||
model_card = context.scene.speckle_state.get_model_card_by_id(
|
||||
self.model_card_id
|
||||
)
|
||||
|
||||
# On-demand permission check
|
||||
authorized, auth_message = can_create_version(
|
||||
model_card.account_id, model_card.project_id, model_card.model_id
|
||||
)
|
||||
if not authorized:
|
||||
self.report({"ERROR"}, auth_message)
|
||||
return {"CANCELLED"}
|
||||
|
||||
# set wm
|
||||
wm.selected_account_id = model_card.account_id
|
||||
wm.selected_project_id = model_card.project_id
|
||||
wm.selected_model_id = model_card.model_id
|
||||
|
||||
# get model card objects
|
||||
objects_to_convert = []
|
||||
for speckle_obj in model_card.objects:
|
||||
blender_obj = bpy.data.objects.get(speckle_obj.name)
|
||||
if blender_obj:
|
||||
objects_to_convert.append(blender_obj)
|
||||
else:
|
||||
self.report(
|
||||
{"WARNING"}, f"Object '{speckle_obj.name}' not found, skipping"
|
||||
)
|
||||
|
||||
if not objects_to_convert:
|
||||
self.report({"ERROR"}, "No objects to publish")
|
||||
return {"CANCELLED"}
|
||||
|
||||
# publish to speckle
|
||||
success, message, version_id = publish_operation(
|
||||
context,
|
||||
objects_to_convert,
|
||||
self.version_message,
|
||||
model_card.apply_modifiers,
|
||||
)
|
||||
|
||||
if not success:
|
||||
self.report({"ERROR"}, message)
|
||||
return {"CANCELLED"}
|
||||
|
||||
model_card.version_id = version_id
|
||||
model_card.is_publish = True
|
||||
|
||||
# Clear selected model details from Window Manager
|
||||
wm.selected_account_id = ""
|
||||
wm.selected_project_id = ""
|
||||
wm.selected_model_id = ""
|
||||
|
||||
self.report({"INFO"}, message)
|
||||
|
||||
return {"FINISHED"}
|
||||
@@ -0,0 +1,127 @@
|
||||
import bpy
|
||||
import webbrowser
|
||||
from typing import Set
|
||||
from bpy.types import Event, Context, UILayout
|
||||
|
||||
|
||||
class SPECKLE_OT_model_card_settings(bpy.types.Operator):
|
||||
"""
|
||||
manages settings and actions for a Speckle model card
|
||||
"""
|
||||
|
||||
bl_idname = "speckle.model_card_settings"
|
||||
bl_label = "Model Card Settings"
|
||||
bl_description = "More options for the model card"
|
||||
model_card_id: bpy.props.StringProperty(name="Model Card ID", default="") # type:ignore
|
||||
|
||||
def execute(self, context: Context) -> Set[str]:
|
||||
return {"FINISHED"}
|
||||
|
||||
def draw(self, context: Context) -> None:
|
||||
layout: UILayout = self.layout
|
||||
layout.operator(
|
||||
"speckle.view_in_browser", text="View in Browser"
|
||||
).model_card_id = self.model_card_id
|
||||
layout.operator(
|
||||
"speckle.view_model_versions", text="View Model Versions"
|
||||
).model_card_id = self.model_card_id
|
||||
layout.separator()
|
||||
row = layout.row()
|
||||
# add a button for deleting the model card
|
||||
row.alert = True
|
||||
delete_op = row.operator(
|
||||
"speckle.delete_model_card", text="Delete Model Card", icon="TRASH"
|
||||
)
|
||||
delete_op.model_card_id = self.model_card_id
|
||||
|
||||
def invoke(self, context: Context, event: Event) -> Set[str]:
|
||||
wm = context.window_manager
|
||||
return wm.invoke_popup(self)
|
||||
|
||||
|
||||
class SPECKLE_OT_view_in_browser(bpy.types.Operator):
|
||||
"""
|
||||
opens the current model in the Speckle web viewer
|
||||
"""
|
||||
|
||||
bl_idname = "speckle.view_in_browser"
|
||||
bl_label = "View in Browser"
|
||||
bl_description = "View the model in the browser"
|
||||
|
||||
model_card_id: bpy.props.StringProperty() # type: ignore
|
||||
|
||||
def execute(self, context: Context) -> Set[str]:
|
||||
model_card = context.scene.speckle_state.get_model_card_by_id(
|
||||
self.model_card_id
|
||||
)
|
||||
if model_card is None:
|
||||
self.report({"ERROR"}, "Model card not found")
|
||||
return {"CANCELLED"}
|
||||
|
||||
url = f"{model_card.server_url}/projects/{model_card.project_id}/models/{model_card.model_id}"
|
||||
webbrowser.open(url)
|
||||
self.report({"INFO"}, f"Viewing in the browser: {url}")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class SPECKLE_OT_view_model_versions(bpy.types.Operator):
|
||||
"""
|
||||
opens the model's version history in the Speckle web app
|
||||
"""
|
||||
|
||||
bl_idname = "speckle.view_model_versions"
|
||||
bl_label = "View Model Versions"
|
||||
bl_description = "View the model versions in the browser"
|
||||
|
||||
model_card_id: bpy.props.StringProperty() # type: ignore
|
||||
|
||||
def execute(self, context: Context) -> Set[str]:
|
||||
model_card = context.scene.speckle_state.get_model_card_by_id(
|
||||
self.model_card_id
|
||||
)
|
||||
if model_card is None:
|
||||
self.report({"ERROR"}, "Model card not found")
|
||||
return {"CANCELLED"}
|
||||
|
||||
url = f"{model_card.server_url}/projects/{model_card.project_id}/models/{model_card.model_id}/versions"
|
||||
webbrowser.open(url)
|
||||
|
||||
self.report({"INFO"}, "Viewing model's versions in the browser")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class SPECKLE_OT_delete_model_card(bpy.types.Operator):
|
||||
"""
|
||||
deletes a Speckle model card from the Blender UI
|
||||
"""
|
||||
|
||||
bl_idname = "speckle.delete_model_card"
|
||||
bl_label = "Delete Model Card"
|
||||
bl_description = "Delete this model card"
|
||||
|
||||
model_card_id: bpy.props.StringProperty() # type: ignore
|
||||
|
||||
def execute(self, context: Context) -> Set[str]:
|
||||
model_card = context.scene.speckle_state.get_model_card_by_id(
|
||||
self.model_card_id
|
||||
)
|
||||
if model_card is None:
|
||||
self.report({"ERROR"}, "Model card not found")
|
||||
return {"CANCELLED"}
|
||||
|
||||
model_name = model_card.model_name
|
||||
|
||||
# find the index of the model card and remove it
|
||||
for i, card in enumerate(context.scene.speckle_state.model_cards):
|
||||
if card.get_model_card_id() == self.model_card_id:
|
||||
context.scene.speckle_state.model_cards.remove(i)
|
||||
break
|
||||
|
||||
self.report({"INFO"}, f"Model card '{model_name}' has been deleted")
|
||||
context.window.screen = context.window.screen
|
||||
|
||||
context.area.tag_redraw()
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context: Context, event: Event) -> Set[str]:
|
||||
return self.execute(context)
|
||||
@@ -0,0 +1,122 @@
|
||||
import bpy
|
||||
from bpy.types import Context
|
||||
from bpy.types import Event
|
||||
from typing import Set
|
||||
|
||||
from ..operations.publish_operation import publish_operation
|
||||
from ..utils.account_manager import get_server_url_by_account_id, can_create_version
|
||||
from ..utils.model_card_utils import model_card_exists, update_model_card_objects
|
||||
|
||||
|
||||
class SPECKLE_OT_publish(bpy.types.Operator):
|
||||
bl_idname = "speckle.publish"
|
||||
bl_label = "Publish to Speckle"
|
||||
bl_description = "Publish selected objects to Speckle"
|
||||
|
||||
version_message: bpy.props.StringProperty(name="Version Message") # type: ignore
|
||||
apply_modifiers: bpy.props.BoolProperty( # type: ignore
|
||||
name="Apply Modifiers",
|
||||
description="Apply all modifiers to objects before conversion",
|
||||
default=True,
|
||||
)
|
||||
|
||||
def draw(self, context: Context) -> None:
|
||||
layout = self.layout
|
||||
layout.prop(self, "version_message")
|
||||
layout.prop(self, "apply_modifiers")
|
||||
|
||||
def invoke(self, context: Context, event: Event) -> Set[str]:
|
||||
return context.window_manager.invoke_props_dialog(self)
|
||||
|
||||
def execute(self, context: Context) -> Set[str]:
|
||||
wm = context.window_manager
|
||||
|
||||
# check if we have stored objects from selection dialog
|
||||
if not wm.speckle_objects:
|
||||
self.report(
|
||||
{"ERROR"},
|
||||
"No objects selected to publish. Please use 'Select Objects' first.",
|
||||
)
|
||||
return {"CANCELLED"}
|
||||
|
||||
account_id = getattr(wm, "selected_account_id", "")
|
||||
project_id = getattr(wm, "selected_project_id", "")
|
||||
model_id = getattr(wm, "selected_model_id", "")
|
||||
|
||||
if not account_id:
|
||||
self.report({"ERROR"}, "No account selected")
|
||||
return {"CANCELLED"}
|
||||
|
||||
if not project_id:
|
||||
self.report({"ERROR"}, "No project selected")
|
||||
return {"CANCELLED"}
|
||||
|
||||
if not model_id:
|
||||
self.report({"ERROR"}, "No model selected")
|
||||
return {"CANCELLED"}
|
||||
|
||||
authorized, auth_message = can_create_version(account_id, project_id, model_id)
|
||||
if not authorized:
|
||||
self.report({"ERROR"}, auth_message)
|
||||
return {"CANCELLED"}
|
||||
|
||||
objects_to_convert = []
|
||||
for speckle_obj in wm.speckle_objects:
|
||||
blender_obj = bpy.data.objects.get(speckle_obj.name)
|
||||
if blender_obj:
|
||||
objects_to_convert.append(blender_obj)
|
||||
else:
|
||||
self.report(
|
||||
{"WARNING"}, f"Object '{speckle_obj.name}' not found, skipping"
|
||||
)
|
||||
|
||||
if not objects_to_convert:
|
||||
self.report({"ERROR"}, "None of the selected objects could be found")
|
||||
return {"CANCELLED"}
|
||||
|
||||
success, message, version_id = publish_operation(
|
||||
context, objects_to_convert, self.version_message, self.apply_modifiers
|
||||
)
|
||||
|
||||
if not success:
|
||||
self.report({"ERROR"}, message)
|
||||
return {"CANCELLED"}
|
||||
|
||||
# create model card if operation was successful
|
||||
if hasattr(context.scene, "speckle_state") and hasattr(
|
||||
context.scene.speckle_state, "model_cards"
|
||||
):
|
||||
if model_card_exists(
|
||||
wm.selected_project_id, wm.selected_model_id, True, context
|
||||
):
|
||||
model_card = context.scene.speckle_state.get_model_card_by_id(
|
||||
f"{wm.ui_mode}-{wm.selected_project_id}-{wm.selected_model_id}"
|
||||
)
|
||||
else:
|
||||
model_card = context.scene.speckle_state.model_cards.add()
|
||||
|
||||
model_card.account_id = account_id
|
||||
model_card.server_url = get_server_url_by_account_id(account_id)
|
||||
model_card.project_id = project_id
|
||||
model_card.project_name = getattr(wm, "selected_project_name", "")
|
||||
model_card.model_id = model_id
|
||||
model_card.model_name = getattr(wm, "selected_model_name", "")
|
||||
model_card.is_publish = True
|
||||
model_card.load_option = "SPECIFIC" # published versions are specific
|
||||
model_card.version_id = version_id
|
||||
model_card.apply_modifiers = self.apply_modifiers
|
||||
update_model_card_objects(model_card, objects_to_convert)
|
||||
|
||||
# clear selected model details from Window Manager
|
||||
wm.selected_account_id = ""
|
||||
wm.selected_project_id = ""
|
||||
wm.selected_project_name = ""
|
||||
wm.selected_model_id = ""
|
||||
wm.selected_model_name = ""
|
||||
wm.selected_version_load_option = ""
|
||||
wm.selected_version_id = ""
|
||||
wm.speckle_objects.clear()
|
||||
|
||||
self.report({"INFO"}, message)
|
||||
context.area.tag_redraw()
|
||||
return {"FINISHED"}
|
||||
@@ -0,0 +1,34 @@
|
||||
from bpy.types import Operator
|
||||
from bpy.props import StringProperty
|
||||
from ..utils.model_card_utils import select_model_card_objects, zoom_to_selected_objects
|
||||
|
||||
|
||||
class SPECKLE_OT_select_objects(Operator):
|
||||
"""
|
||||
select all objects imported from this Speckle model
|
||||
"""
|
||||
|
||||
bl_idname = "speckle.select_objects"
|
||||
bl_label = "Select Objects"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = (
|
||||
"Selects and zooms extents to objects loaded from this Speckle model"
|
||||
)
|
||||
|
||||
model_card_id: StringProperty(
|
||||
name="Model Card ID", description="ID of the model card", default=""
|
||||
) # type: ignore
|
||||
|
||||
def execute(self, context):
|
||||
model_card = context.scene.speckle_state.get_model_card_by_id(
|
||||
self.model_card_id
|
||||
)
|
||||
if model_card is None:
|
||||
self.report({"ERROR"}, "Model card not found")
|
||||
return {"CANCELLED"}
|
||||
|
||||
select_model_card_objects(model_card, context)
|
||||
zoom_to_selected_objects(context)
|
||||
|
||||
self.report({"INFO"}, f"Selected {len(context.selected_objects)} objects")
|
||||
return {"FINISHED"}
|
||||
@@ -0,0 +1,27 @@
|
||||
import bpy
|
||||
import webbrowser
|
||||
from bpy.types import Context
|
||||
|
||||
|
||||
class SPECKLE_OT_update_button(bpy.types.Operator):
|
||||
"""Operator for opening the download URL for the latest Speckle Blender connector"""
|
||||
|
||||
bl_idname = "speckle.update_button"
|
||||
bl_label = "Update"
|
||||
bl_description = "Download the latest version of the Speckle Blender connector"
|
||||
|
||||
def execute(self, context: Context) -> set[str]:
|
||||
wm = context.window_manager
|
||||
|
||||
if not wm.update_url:
|
||||
self.report({"ERROR"}, "No update URL available")
|
||||
return {"CANCELLED"}
|
||||
|
||||
try:
|
||||
webbrowser.open(wm.update_url)
|
||||
self.report({"INFO"}, f"Opening download page for v{wm.latest_version}")
|
||||
except Exception as e:
|
||||
self.report({"ERROR"}, f"Failed to open download page: {str(e)}")
|
||||
return {"CANCELLED"}
|
||||
|
||||
return {"FINISHED"}
|
||||
@@ -0,0 +1,51 @@
|
||||
import bpy
|
||||
from bpy.types import Context
|
||||
from specklepy.core.api.connector_versions import get_latest_version
|
||||
|
||||
# Get current version from bl_info
|
||||
from ... import bl_info
|
||||
|
||||
|
||||
class SPECKLE_OT_version_check(bpy.types.Operator):
|
||||
"""Operator for checking if a newer version of the Speckle Blender connector is available"""
|
||||
|
||||
bl_idname = "speckle.version_check"
|
||||
bl_label = "Check for Updates"
|
||||
bl_description = (
|
||||
"Check if a newer version of the Speckle Blender connector is available"
|
||||
)
|
||||
|
||||
def execute(self, context: Context) -> set[str]:
|
||||
wm = context.window_manager
|
||||
|
||||
# Reset previous state
|
||||
wm.update_available = False
|
||||
wm.latest_version = ""
|
||||
wm.update_url = ""
|
||||
|
||||
try:
|
||||
current_version = bl_info["version"]
|
||||
current_version_str = (
|
||||
f"{current_version[0]}.{current_version[1]}.{current_version[2]}"
|
||||
)
|
||||
|
||||
# Get latest version info
|
||||
latest_version_info = get_latest_version("blender", False)
|
||||
latest_version_str = latest_version_info.number # semantic version string
|
||||
|
||||
# Compare versions - if they're different, show update
|
||||
if latest_version_str != current_version_str:
|
||||
wm.update_available = True
|
||||
wm.latest_version = latest_version_str
|
||||
wm.update_url = str(
|
||||
latest_version_info.url
|
||||
) # Convert HttpUrl to string
|
||||
self.report({"INFO"}, f"Update available: v{latest_version_str}")
|
||||
else:
|
||||
self.report({"INFO"}, "You have the latest version")
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Failed to check for updates: {str(e)}"
|
||||
self.report({"ERROR"}, error_msg)
|
||||
|
||||
return {"FINISHED"}
|
||||
@@ -0,0 +1,2 @@
|
||||
from ..operations.load_operation import load_operation # noqa: F401
|
||||
from ..operations.publish_operation import publish_operation # noqa: F401
|
||||
@@ -0,0 +1,310 @@
|
||||
from typing import Dict, Union
|
||||
|
||||
import bpy
|
||||
from bpy.types import Context
|
||||
from specklepy.core.api import host_applications, operations
|
||||
from specklepy.core.api.inputs.version_inputs import MarkReceivedVersionInput
|
||||
from specklepy.logging import metrics
|
||||
from specklepy.objects.graph_traversal.default_traversal import (
|
||||
create_default_traversal_function,
|
||||
)
|
||||
from specklepy.objects.models.collections.collection import Collection as SCollection
|
||||
from specklepy.transports.server import ServerTransport
|
||||
|
||||
from ... import bl_info
|
||||
from ...converter.to_native import (
|
||||
convert_to_native,
|
||||
find_instance_definitions,
|
||||
instance_definition_proxy_to_native,
|
||||
render_material_proxy_to_native,
|
||||
)
|
||||
from ...converter.utils import (
|
||||
build_object_id_map,
|
||||
get_project_workspace_id,
|
||||
)
|
||||
from ..utils.account_manager import _client_cache
|
||||
from ..utils.get_ascendants import get_ascendants
|
||||
|
||||
|
||||
def load_operation(
|
||||
context: Context, instance_loading_mode: str = "INSTANCE_PROXIES"
|
||||
) -> Dict[str, Union[bpy.types.Collection, bpy.types.Object]]:
|
||||
"""
|
||||
load objects from Speckle and maintain hierarchy.
|
||||
"""
|
||||
|
||||
wm = context.window_manager
|
||||
accountId: str = wm.selected_account_id # type: ignore
|
||||
projectId: str = wm.selected_project_id # type: ignore
|
||||
versionId: str = wm.selected_version_id # type: ignore
|
||||
|
||||
# get cached client
|
||||
client = _client_cache.get_client(accountId)
|
||||
if not client:
|
||||
print("No Speckle client found")
|
||||
return {}
|
||||
|
||||
print(f"Using client for account: {accountId}")
|
||||
|
||||
transport = ServerTransport(stream_id=projectId, client=client)
|
||||
|
||||
version = client.version.get(versionId, projectId)
|
||||
obj_id = version.referenced_object
|
||||
if not obj_id:
|
||||
raise ValueError("Unable to receive version beyond workspaces limit")
|
||||
|
||||
version_data = operations.receive(obj_id, transport)
|
||||
|
||||
metrics.set_host_app("blender")
|
||||
client.version.received(
|
||||
MarkReceivedVersionInput(
|
||||
version_id=version.id,
|
||||
project_id=projectId,
|
||||
source_application="blender",
|
||||
)
|
||||
)
|
||||
|
||||
metrics.track(
|
||||
metrics.RECEIVE,
|
||||
client.account,
|
||||
{
|
||||
"ui": "dui3",
|
||||
"hostAppVersion": ".".join(map(str, bl_info["blender"])),
|
||||
"core_version": ".".join(map(str, bl_info["version"])),
|
||||
"sourceHostApp": host_applications.get_host_app_from_string(
|
||||
version.source_application
|
||||
).slug,
|
||||
"isMultiplayer": version.author_user.id != client.account.userInfo.id,
|
||||
"workspace_id": get_project_workspace_id(client, wm.selected_project_id),
|
||||
},
|
||||
)
|
||||
|
||||
# Build object ID map once
|
||||
object_id_map = build_object_id_map(version_data)
|
||||
|
||||
# Create material mapping first
|
||||
material_mapping = render_material_proxy_to_native(version_data)
|
||||
|
||||
definition_collections, definition_objects = instance_definition_proxy_to_native(
|
||||
version_data,
|
||||
material_mapping,
|
||||
instance_loading_mode=instance_loading_mode,
|
||||
object_id_map=object_id_map,
|
||||
)
|
||||
|
||||
definitions_root_collection = None
|
||||
if definition_collections:
|
||||
definitions_root_collection = bpy.data.collections.new("InstanceDefinitions")
|
||||
|
||||
for collection in definition_collections.values():
|
||||
definitions_root_collection.children.link(collection)
|
||||
|
||||
definition_object_ids = set()
|
||||
for definition in find_instance_definitions(version_data).values():
|
||||
definition_object_ids.update(definition.objects)
|
||||
for obj_id in definition.objects:
|
||||
# Use ID map
|
||||
found_obj = object_id_map.get(obj_id)
|
||||
if found_obj:
|
||||
if hasattr(found_obj, "id"):
|
||||
definition_object_ids.add(found_obj.id)
|
||||
if hasattr(found_obj, "applicationId"):
|
||||
definition_object_ids.add(found_obj.applicationId)
|
||||
|
||||
traversal_function = create_default_traversal_function()
|
||||
|
||||
root_collection_name = f"{wm.selected_model_name} - {wm.selected_version_id}"
|
||||
root_collection = bpy.data.collections.new(root_collection_name)
|
||||
context.scene.collection.children.link(root_collection)
|
||||
|
||||
context.window_manager.progress_begin(0, 100)
|
||||
|
||||
converted_objects = definition_objects.copy()
|
||||
|
||||
created_collections = {}
|
||||
created_collections[root_collection_name] = root_collection
|
||||
|
||||
collection_hierarchy = {}
|
||||
all_objects = {}
|
||||
|
||||
speckle_root_id = None
|
||||
|
||||
for traversal_item in traversal_function.traverse(version_data):
|
||||
speckle_obj = traversal_item.current
|
||||
|
||||
# Skip objects that are part of instance definitions
|
||||
if speckle_obj.id in definition_object_ids or (
|
||||
hasattr(speckle_obj, "applicationId")
|
||||
and speckle_obj.applicationId in definition_object_ids
|
||||
):
|
||||
continue
|
||||
|
||||
all_objects[speckle_obj.id] = speckle_obj
|
||||
|
||||
# get all ascendants in order (current to root)
|
||||
ascendants = list(get_ascendants(traversal_item))
|
||||
parent_ascendants = ascendants[1:] if len(ascendants) > 1 else []
|
||||
|
||||
if isinstance(speckle_obj, SCollection):
|
||||
if not parent_ascendants and speckle_root_id is None:
|
||||
speckle_root_id = speckle_obj.id
|
||||
|
||||
collection_name = getattr(
|
||||
speckle_obj, "name", f"Collection_{speckle_obj.id}"
|
||||
)
|
||||
|
||||
parent_id = None
|
||||
for parent in parent_ascendants:
|
||||
if isinstance(parent, SCollection) and hasattr(parent, "id"):
|
||||
parent_id = parent.id
|
||||
break
|
||||
|
||||
collection_hierarchy[speckle_obj.id] = {
|
||||
"id": speckle_obj.id,
|
||||
"name": collection_name,
|
||||
"parent_id": parent_id,
|
||||
"applicationId": getattr(speckle_obj, "applicationId", ""),
|
||||
"blender_collection": None,
|
||||
"full_path": [collection_name],
|
||||
}
|
||||
|
||||
if parent_id in collection_hierarchy:
|
||||
collection_hierarchy[speckle_obj.id]["full_path"] = (
|
||||
collection_hierarchy[parent_id]["full_path"] + [collection_name]
|
||||
)
|
||||
|
||||
else:
|
||||
pass
|
||||
|
||||
def get_collection_depth(coll_id):
|
||||
parent_id = collection_hierarchy[coll_id]["parent_id"]
|
||||
if parent_id is None:
|
||||
return 0
|
||||
if parent_id not in collection_hierarchy:
|
||||
return 0
|
||||
return 1 + get_collection_depth(parent_id)
|
||||
|
||||
sorted_collections = sorted(
|
||||
collection_hierarchy.keys(),
|
||||
key=lambda coll_id: (
|
||||
get_collection_depth(coll_id),
|
||||
collection_hierarchy[coll_id]["name"],
|
||||
),
|
||||
)
|
||||
|
||||
if speckle_root_id and speckle_root_id in collection_hierarchy:
|
||||
collection_hierarchy[speckle_root_id]["blender_collection"] = root_collection
|
||||
converted_objects[speckle_root_id] = root_collection
|
||||
|
||||
# create collections in depth order (skip the root that's already mapped)
|
||||
for coll_id in sorted_collections:
|
||||
if coll_id == speckle_root_id:
|
||||
continue
|
||||
|
||||
coll_info = collection_hierarchy[coll_id]
|
||||
coll_name = coll_info["name"]
|
||||
parent_id = coll_info["parent_id"]
|
||||
full_path = coll_info["full_path"]
|
||||
|
||||
collection_key = tuple(full_path)
|
||||
|
||||
parent_collection = root_collection
|
||||
if parent_id and parent_id in collection_hierarchy:
|
||||
parent_info = collection_hierarchy[parent_id]
|
||||
if parent_info["blender_collection"]:
|
||||
parent_collection = parent_info["blender_collection"]
|
||||
|
||||
if collection_key in created_collections:
|
||||
print(f"Collection already exists: {coll_name}")
|
||||
blender_collection = created_collections[collection_key]
|
||||
else:
|
||||
blender_collection = bpy.data.collections.new(coll_name)
|
||||
if coll_info.get("applicationId"):
|
||||
blender_collection["applicationId"] = coll_info["applicationId"]
|
||||
parent_collection.children.link(blender_collection)
|
||||
created_collections[collection_key] = blender_collection
|
||||
|
||||
coll_info["blender_collection"] = blender_collection
|
||||
converted_objects[coll_id] = blender_collection
|
||||
|
||||
conversion_count = 0
|
||||
for traversal_item in traversal_function.traverse(version_data):
|
||||
speckle_obj = traversal_item.current
|
||||
|
||||
if isinstance(speckle_obj, SCollection):
|
||||
continue
|
||||
|
||||
if not hasattr(speckle_obj, "id"):
|
||||
print("Skipping object without ID")
|
||||
continue
|
||||
|
||||
# Skip objects that are part of instance definitions
|
||||
if speckle_obj.id in definition_object_ids or (
|
||||
hasattr(speckle_obj, "applicationId")
|
||||
and speckle_obj.applicationId in definition_object_ids
|
||||
):
|
||||
continue
|
||||
|
||||
if speckle_obj.id in converted_objects:
|
||||
continue
|
||||
|
||||
try:
|
||||
target_collection = root_collection
|
||||
ascendants = list(get_ascendants(traversal_item))
|
||||
|
||||
for parent in ascendants[1:] if len(ascendants) > 1 else []:
|
||||
if isinstance(parent, SCollection) and hasattr(parent, "id"):
|
||||
parent_id = parent.id
|
||||
if parent_id in collection_hierarchy:
|
||||
coll_info = collection_hierarchy[parent_id]
|
||||
if coll_info["blender_collection"]:
|
||||
target_collection = coll_info["blender_collection"]
|
||||
break
|
||||
|
||||
blender_obj = convert_to_native(
|
||||
speckle_obj,
|
||||
material_mapping,
|
||||
definition_collections=definition_collections,
|
||||
root_collection=target_collection,
|
||||
instance_loading_mode=instance_loading_mode,
|
||||
)
|
||||
|
||||
if blender_obj is None:
|
||||
continue
|
||||
|
||||
converted_objects[speckle_obj.id] = blender_obj
|
||||
if hasattr(speckle_obj, "applicationId"):
|
||||
converted_objects[speckle_obj.applicationId] = blender_obj
|
||||
|
||||
if not isinstance(blender_obj, bpy.types.Collection):
|
||||
try:
|
||||
already_linked = False
|
||||
for coll in bpy.data.collections:
|
||||
if blender_obj.name in coll.objects:
|
||||
already_linked = True
|
||||
|
||||
if not already_linked:
|
||||
target_collection.objects.link(blender_obj)
|
||||
|
||||
except RuntimeError as e:
|
||||
print(f"Error linking object to collection: {e}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error converting {speckle_obj.speckle_type}: {str(e)}")
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
|
||||
conversion_count += 1
|
||||
if conversion_count % 10 == 0:
|
||||
context.window_manager.progress_update(min(conversion_count, 100))
|
||||
|
||||
context.window_manager.progress_end()
|
||||
|
||||
for area in context.screen.areas:
|
||||
if area.type == "OUTLINER":
|
||||
area.tag_redraw()
|
||||
|
||||
print(f"\nLoad process completed. Imported {len(converted_objects)} objects.")
|
||||
|
||||
return converted_objects
|
||||
@@ -0,0 +1,452 @@
|
||||
import bpy
|
||||
from bpy.types import Context, Collection as BlenderCollection
|
||||
from typing import List, Optional, Dict, Tuple
|
||||
|
||||
from specklepy.objects import Base
|
||||
from specklepy.objects.models.collections.collection import Collection
|
||||
from specklepy.core.api import operations
|
||||
from specklepy.transports.server import ServerTransport
|
||||
from specklepy.core.api.inputs.version_inputs import CreateVersionInput
|
||||
from specklepy.objects.models.units import Units
|
||||
from specklepy.logging.exceptions import GraphQLException, WorkspacePermissionException
|
||||
from specklepy.core.api.inputs.model_ingestion_inputs import (
|
||||
ModelIngestionCreateInput,
|
||||
ModelIngestionStartProcessingInput,
|
||||
ModelIngestionSuccessInput,
|
||||
ModelIngestionFailedInput,
|
||||
SourceDataInput,
|
||||
)
|
||||
|
||||
from ...converter.to_speckle import convert_to_speckle
|
||||
from ...converter.to_speckle.material_to_speckle import (
|
||||
add_render_material_proxies_to_base,
|
||||
)
|
||||
from ...converter.utils import get_project_workspace_id
|
||||
from ..utils.account_manager import _client_cache
|
||||
from specklepy.logging import metrics
|
||||
from ... import bl_info
|
||||
|
||||
|
||||
def _check_use_model_ingestion_send(client, project_id: str, model_id: str) -> bool:
|
||||
"""Check if the server supports model ingestion and the user is authorized."""
|
||||
try:
|
||||
result = client.model.can_create_model_ingestion(project_id, model_id)
|
||||
result.ensure_authorised()
|
||||
return True
|
||||
except GraphQLException:
|
||||
return False
|
||||
|
||||
|
||||
def _build_source_data() -> SourceDataInput:
|
||||
"""Build data input for model ingestion."""
|
||||
file_name = bpy.path.basename(bpy.data.filepath)
|
||||
if not file_name:
|
||||
file_name = "Untitled.blend"
|
||||
|
||||
file_size_bytes: Optional[int] = None
|
||||
if bpy.data.filepath:
|
||||
import os
|
||||
|
||||
try:
|
||||
file_size_bytes = os.path.getsize(bpy.data.filepath)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
blender_version = ".".join(map(str, bl_info["blender"]))
|
||||
return SourceDataInput(
|
||||
source_application_slug="blender",
|
||||
source_application_version=blender_version,
|
||||
file_name=file_name,
|
||||
file_size_bytes=file_size_bytes,
|
||||
)
|
||||
|
||||
|
||||
def _send_via_ingestion(
|
||||
client,
|
||||
project_id: str,
|
||||
model_id: str,
|
||||
obj_id: str,
|
||||
version_message: str,
|
||||
) -> str:
|
||||
"""Send via the model ingestion. Returns version_id."""
|
||||
source_data = _build_source_data()
|
||||
|
||||
create_input = ModelIngestionCreateInput(
|
||||
project_id=project_id,
|
||||
model_id=model_id,
|
||||
source_data=source_data,
|
||||
progress_message="Model ingestion created",
|
||||
)
|
||||
ingestion = client.model_ingestion.create(create_input)
|
||||
ingestion_id = ingestion.id
|
||||
|
||||
try:
|
||||
start_input = ModelIngestionStartProcessingInput(
|
||||
project_id=project_id,
|
||||
ingestion_id=ingestion_id,
|
||||
progress_message="Processing model ingestion",
|
||||
source_data=source_data,
|
||||
)
|
||||
client.model_ingestion.start_processing(start_input)
|
||||
|
||||
success_input = ModelIngestionSuccessInput(
|
||||
project_id=project_id,
|
||||
ingestion_id=ingestion_id,
|
||||
root_object_id=obj_id,
|
||||
version_message=version_message,
|
||||
)
|
||||
version_id = client.model_ingestion.complete(success_input)
|
||||
return version_id
|
||||
except Exception:
|
||||
try:
|
||||
fail_input = ModelIngestionFailedInput(
|
||||
project_id=project_id,
|
||||
ingestion_id=ingestion_id,
|
||||
error_reason="Failed during processing",
|
||||
)
|
||||
client.model_ingestion.fail_with_error(fail_input)
|
||||
except Exception:
|
||||
pass
|
||||
raise
|
||||
|
||||
|
||||
def _send_via_version_create(
|
||||
client,
|
||||
project_id: str,
|
||||
model_id: str,
|
||||
obj_id: str,
|
||||
version_message: str,
|
||||
) -> str:
|
||||
"""Send via the legacy version.create() flow. Returns version_id."""
|
||||
version_input = CreateVersionInput(
|
||||
objectId=obj_id,
|
||||
modelId=model_id,
|
||||
projectId=project_id,
|
||||
message=version_message,
|
||||
sourceApplication="blender",
|
||||
)
|
||||
version = client.version.create(version_input)
|
||||
return version.id
|
||||
|
||||
|
||||
def publish_operation(
|
||||
context: Context,
|
||||
objects_to_convert: List,
|
||||
version_message: str = "",
|
||||
apply_modifiers: bool = True,
|
||||
) -> Tuple[bool, str, Optional[str]]:
|
||||
"""
|
||||
publish objects to speckle
|
||||
"""
|
||||
wm = context.window_manager
|
||||
|
||||
try:
|
||||
# get cached client
|
||||
client = _client_cache.get_client(wm.selected_account_id)
|
||||
if not client:
|
||||
return False, "No Speckle client found", None
|
||||
|
||||
project_id = wm.selected_project_id
|
||||
model_id = wm.selected_model_id
|
||||
|
||||
# check ingestion support before sending data (fail fast on permission errors)
|
||||
use_ingestion = _check_use_model_ingestion_send(client, project_id, model_id)
|
||||
|
||||
transport = ServerTransport(stream_id=project_id, client=client)
|
||||
|
||||
# build collection hierarchy and convert objects
|
||||
root_collection = build_collection_hierarchy(
|
||||
context, objects_to_convert, apply_modifiers
|
||||
)
|
||||
|
||||
if not root_collection:
|
||||
return False, "No objects could be converted to Speckle format", None
|
||||
|
||||
# add material proxies
|
||||
add_render_material_proxies_to_base(root_collection, objects_to_convert)
|
||||
|
||||
obj_id = operations.send(root_collection, [transport])
|
||||
|
||||
if use_ingestion:
|
||||
version_id = _send_via_ingestion(
|
||||
client, project_id, model_id, obj_id, version_message
|
||||
)
|
||||
else:
|
||||
version_id = _send_via_version_create(
|
||||
client, project_id, model_id, obj_id, version_message
|
||||
)
|
||||
|
||||
# Get account for metrics tracking
|
||||
from specklepy.core.api.credentials import get_local_accounts
|
||||
|
||||
account = next(
|
||||
(acc for acc in get_local_accounts() if acc.id == wm.selected_account_id),
|
||||
None,
|
||||
)
|
||||
|
||||
if account:
|
||||
# track metrics
|
||||
metrics.set_host_app("blender")
|
||||
metrics.track(
|
||||
metrics.SEND,
|
||||
account,
|
||||
{
|
||||
"ui": "dui3",
|
||||
"hostAppVersion": ".".join(map(str, bl_info["blender"])),
|
||||
"core_version": ".".join(map(str, bl_info["version"])),
|
||||
"workspace_id": get_project_workspace_id(client, project_id),
|
||||
},
|
||||
)
|
||||
|
||||
# count total objects for success message
|
||||
total_objects = count_objects_in_collection(root_collection)
|
||||
|
||||
return (
|
||||
True,
|
||||
f"Successfully published {total_objects} objects with hierarchy to Speckle",
|
||||
version_id,
|
||||
)
|
||||
|
||||
except WorkspacePermissionException as e:
|
||||
return False, f"Permission denied: {str(e)}", None
|
||||
|
||||
except Exception as e:
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
# Clear cache on error to prevent stale clients
|
||||
_client_cache.clear()
|
||||
return False, f"Failed to publish: {str(e)}", None
|
||||
|
||||
|
||||
def build_collection_hierarchy(
|
||||
context: Context, objects_to_convert: List, apply_modifiers: bool = True
|
||||
) -> Optional[Collection]:
|
||||
"""
|
||||
build a speckle collection hierarchy that mimicks blender's collection structure
|
||||
"""
|
||||
# set name for root collection
|
||||
file_name = bpy.path.basename(bpy.data.filepath)
|
||||
collection_name = file_name if file_name else "Untitled.blend"
|
||||
|
||||
collection_data = analyze_collection_structure(objects_to_convert)
|
||||
|
||||
if not collection_data["objects"] and not collection_data["collections"]:
|
||||
return None
|
||||
|
||||
converted_objects = convert_selected_objects(
|
||||
context, objects_to_convert, apply_modifiers
|
||||
)
|
||||
if not converted_objects:
|
||||
return None
|
||||
|
||||
# create the root Speckle collection
|
||||
root_collection = Collection(name=collection_name)
|
||||
root_collection.units = get_scene_units(context.scene).value
|
||||
root_collection["version"] = 3
|
||||
|
||||
# maps Blender collection to Speckle collection
|
||||
collection_mapping = {} #
|
||||
|
||||
# create Speckle collections for each blender collection
|
||||
for blender_coll in collection_data["collections"]:
|
||||
speckle_coll = Collection(name=blender_coll.name)
|
||||
speckle_coll.units = root_collection.units
|
||||
collection_mapping[blender_coll] = speckle_coll
|
||||
|
||||
for blender_coll in collection_data["collections"]:
|
||||
speckle_coll = collection_mapping[blender_coll]
|
||||
|
||||
parent_coll = find_parent_collection(
|
||||
blender_coll, collection_data["collections"]
|
||||
)
|
||||
|
||||
if parent_coll and parent_coll in collection_mapping:
|
||||
parent_speckle_coll = collection_mapping[parent_coll]
|
||||
parent_speckle_coll.elements.append(speckle_coll)
|
||||
else:
|
||||
root_collection.elements.append(speckle_coll)
|
||||
|
||||
# assign objects to their collections
|
||||
object_mapping = {}
|
||||
for i, blender_obj in enumerate(objects_to_convert):
|
||||
if i < len(converted_objects) and converted_objects[i] is not None:
|
||||
object_mapping[blender_obj] = converted_objects[i]
|
||||
|
||||
for blender_obj, speckle_obj in object_mapping.items():
|
||||
placed = False
|
||||
|
||||
target_collection = find_target_collection_for_object(
|
||||
blender_obj, collection_data["collections"]
|
||||
)
|
||||
|
||||
if target_collection and target_collection in collection_mapping:
|
||||
collection_mapping[target_collection].elements.append(speckle_obj)
|
||||
placed = True
|
||||
|
||||
# if not placed in any subcollection, add to root
|
||||
if not placed:
|
||||
root_collection.elements.append(speckle_obj)
|
||||
|
||||
return root_collection
|
||||
|
||||
|
||||
def analyze_collection_structure(objects: List) -> Dict:
|
||||
"""
|
||||
analyze the collection structure of the given objects
|
||||
"""
|
||||
collections_set = set()
|
||||
objects_collections = {}
|
||||
|
||||
direct_collections = set()
|
||||
for obj in objects:
|
||||
obj_collections = []
|
||||
for collection in bpy.data.collections:
|
||||
if obj.name in collection.objects:
|
||||
direct_collections.add(collection)
|
||||
obj_collections.append(collection)
|
||||
objects_collections[obj] = obj_collections
|
||||
|
||||
# find all ancestor collections
|
||||
def find_all_ancestors(collection):
|
||||
"""recursively find all ancestor collections"""
|
||||
ancestors = set()
|
||||
|
||||
for potential_parent in bpy.data.collections:
|
||||
if collection.name in potential_parent.children:
|
||||
ancestors.add(potential_parent)
|
||||
# Recursively find ancestors of the parent
|
||||
ancestors.update(find_all_ancestors(potential_parent))
|
||||
|
||||
return ancestors
|
||||
|
||||
for collection in direct_collections:
|
||||
collections_set.add(collection)
|
||||
ancestors = find_all_ancestors(collection)
|
||||
collections_set.update(ancestors)
|
||||
|
||||
collections_list = list(collections_set)
|
||||
collections_list.sort(key=lambda c: get_collection_depth(c))
|
||||
|
||||
return {
|
||||
"collections": collections_list,
|
||||
"objects": objects,
|
||||
"object_collections": objects_collections,
|
||||
}
|
||||
|
||||
|
||||
def get_collection_depth(collection: BlenderCollection) -> int:
|
||||
"""
|
||||
get the depth of a collection in the hierarchy
|
||||
"""
|
||||
depth = 0
|
||||
for scene in bpy.data.scenes:
|
||||
if collection.name in scene.collection.children:
|
||||
return depth
|
||||
|
||||
for parent_coll in bpy.data.collections:
|
||||
if collection.name in parent_coll.children:
|
||||
return get_collection_depth(parent_coll) + 1
|
||||
|
||||
return depth
|
||||
|
||||
|
||||
def find_parent_collection(
|
||||
collection: BlenderCollection, all_collections: List[BlenderCollection]
|
||||
) -> Optional[BlenderCollection]:
|
||||
"""
|
||||
find the parent collection
|
||||
"""
|
||||
for potential_parent in all_collections:
|
||||
if collection.name in potential_parent.children:
|
||||
return potential_parent
|
||||
return None
|
||||
|
||||
|
||||
def find_target_collection_for_object(
|
||||
obj, collections: List[BlenderCollection]
|
||||
) -> Optional[BlenderCollection]:
|
||||
"""
|
||||
find the deepest collection that contains this object
|
||||
"""
|
||||
target_collection = None
|
||||
max_depth = -1
|
||||
|
||||
for collection in collections:
|
||||
if obj.name in collection.objects:
|
||||
depth = get_collection_depth(collection)
|
||||
if depth > max_depth:
|
||||
max_depth = depth
|
||||
target_collection = collection
|
||||
|
||||
return target_collection
|
||||
|
||||
|
||||
def convert_selected_objects(
|
||||
context: Context, objects_to_convert: List, apply_modifiers: bool = True
|
||||
) -> List[Optional[Base]]:
|
||||
"""
|
||||
convert selected objects to Speckle format with proper units
|
||||
"""
|
||||
scene = context.scene
|
||||
units = get_scene_units(scene)
|
||||
scale_factor = scene.unit_settings.scale_length
|
||||
|
||||
speckle_objects = []
|
||||
for obj in objects_to_convert:
|
||||
if not obj or obj.type not in ["MESH", "CURVE", "EMPTY"]:
|
||||
speckle_objects.append(None)
|
||||
continue
|
||||
|
||||
speckle_obj = convert_to_speckle(
|
||||
obj, scale_factor, units.value, apply_modifiers
|
||||
)
|
||||
speckle_objects.append(speckle_obj)
|
||||
|
||||
return speckle_objects
|
||||
|
||||
|
||||
def get_scene_units(scene) -> Units:
|
||||
"""
|
||||
get units from Blender's unit system
|
||||
"""
|
||||
unit_settings = scene.unit_settings
|
||||
|
||||
if unit_settings.system == "METRIC":
|
||||
if unit_settings.length_unit == "METERS":
|
||||
return Units.m
|
||||
elif unit_settings.length_unit == "CENTIMETERS":
|
||||
return Units.cm
|
||||
elif unit_settings.length_unit == "MILLIMETERS":
|
||||
return Units.mm
|
||||
elif unit_settings.length_unit == "KILOMETERS":
|
||||
return Units.km
|
||||
else:
|
||||
return Units.m
|
||||
elif unit_settings.system == "IMPERIAL":
|
||||
if unit_settings.length_unit == "FEET":
|
||||
return Units.feet
|
||||
elif unit_settings.length_unit == "INCHES":
|
||||
return Units.inches
|
||||
elif unit_settings.length_unit == "YARDS":
|
||||
return Units.yards
|
||||
elif unit_settings.length_unit == "MILES":
|
||||
return Units.miles
|
||||
else:
|
||||
return Units.feet
|
||||
else:
|
||||
return Units.m # default to meters
|
||||
|
||||
|
||||
def count_objects_in_collection(collection: Collection) -> int:
|
||||
"""
|
||||
recursively count all objects in a collection and its sub-collections
|
||||
"""
|
||||
count = 0
|
||||
if hasattr(collection, "elements"):
|
||||
for element in collection.elements:
|
||||
if isinstance(element, Collection):
|
||||
count += count_objects_in_collection(element)
|
||||
else:
|
||||
count += 1
|
||||
return count
|
||||
@@ -0,0 +1,31 @@
|
||||
import bpy
|
||||
from bpy.props import CollectionProperty
|
||||
from bpy.types import PropertyGroup
|
||||
from typing import Optional
|
||||
|
||||
from ..utils.property_groups import speckle_model_card
|
||||
|
||||
|
||||
class SpeckleState(PropertyGroup):
|
||||
"""
|
||||
manages the state of the Speckle addon in Blender
|
||||
"""
|
||||
|
||||
model_cards: CollectionProperty(type=speckle_model_card) # type: ignore
|
||||
|
||||
def get_model_card_by_id(self, model_card_id: str) -> Optional[speckle_model_card]:
|
||||
"""Find a model card by its ID."""
|
||||
for model_card in self.model_cards:
|
||||
if model_card.get_model_card_id() == model_card_id:
|
||||
return model_card
|
||||
return None
|
||||
|
||||
|
||||
def register() -> None:
|
||||
bpy.utils.register_class(SpeckleState)
|
||||
bpy.types.Scene.speckle_state = bpy.props.PointerProperty(type=SpeckleState) # type: ignore
|
||||
|
||||
|
||||
def unregister() -> None:
|
||||
del bpy.types.Scene.speckle_state
|
||||
bpy.utils.unregister_class(SpeckleState)
|
||||
@@ -0,0 +1 @@
|
||||
from .main_panel import SPECKLE_PT_main_panel # noqa: F401
|
||||
@@ -0,0 +1,147 @@
|
||||
import bpy
|
||||
from bpy.types import Context, Event
|
||||
from typing import List, Tuple
|
||||
from ..utils.account_manager import (
|
||||
get_account_enum_items,
|
||||
speckle_account,
|
||||
speckle_workspace,
|
||||
get_workspaces,
|
||||
get_active_workspace,
|
||||
get_account_from_id,
|
||||
)
|
||||
from ..utils.project_manager import get_projects_for_account
|
||||
from ..ui.project_selection_dialog import speckle_project
|
||||
|
||||
|
||||
class SPECKLE_UL_accounts_list(bpy.types.UIList):
|
||||
"""
|
||||
UIList for displaying accounts
|
||||
"""
|
||||
|
||||
def draw_item(
|
||||
self,
|
||||
context: Context,
|
||||
layout: bpy.types.UILayout,
|
||||
data: bpy.types.PropertyGroup,
|
||||
item: bpy.types.PropertyGroup,
|
||||
icon: str,
|
||||
active_data: bpy.types.PropertyGroup,
|
||||
active_propname: str,
|
||||
) -> None:
|
||||
if self.layout_type in {"DEFAULT", "COMPACT"}:
|
||||
row = layout.row()
|
||||
row.label(text=item.user_name)
|
||||
row.label(text=item.server_url)
|
||||
row.label(text=item.user_email)
|
||||
elif self.layout_type == "GRID":
|
||||
layout.alignment = "CENTER"
|
||||
layout.label(text=item.user_name)
|
||||
|
||||
|
||||
class SPECKLE_OT_account_selection_dialog(bpy.types.Operator):
|
||||
"""
|
||||
operator for displaying and handling the account selection dialog
|
||||
"""
|
||||
|
||||
bl_idname = "speckle.account_selection_dialog"
|
||||
bl_label = "Select Account"
|
||||
bl_description = "Select account"
|
||||
|
||||
account_index: bpy.props.IntProperty(default=0) # type: ignore
|
||||
|
||||
def invoke(self, context: Context, event: Event) -> set[str]:
|
||||
wm = context.window_manager
|
||||
# Clear existing accounts
|
||||
wm.speckle_accounts.clear()
|
||||
|
||||
# Save selected account
|
||||
current_account_index = 0
|
||||
|
||||
# Fetch accounts
|
||||
for i, (id, user_name, server_url, user_email) in enumerate(
|
||||
get_account_enum_items()
|
||||
):
|
||||
account: speckle_account = wm.speckle_accounts.add()
|
||||
account.id = id
|
||||
account.user_name = user_name
|
||||
account.server_url = server_url
|
||||
account.user_email = user_email
|
||||
if id == wm.selected_account_id:
|
||||
current_account_index = i
|
||||
|
||||
self.account_index = current_account_index
|
||||
return context.window_manager.invoke_props_dialog(self)
|
||||
|
||||
def draw(self, context: Context) -> None:
|
||||
layout = self.layout
|
||||
wm = context.window_manager
|
||||
row = layout.row()
|
||||
# add account button
|
||||
if wm.selected_account_id == "NO_ACCOUNTS":
|
||||
add_account_button_text = "Sign In"
|
||||
add_account_button_icon = "WORLD"
|
||||
else:
|
||||
add_account_button_text = "Add Account"
|
||||
add_account_button_icon = "ADD"
|
||||
row.operator(
|
||||
"speckle.add_account",
|
||||
icon=add_account_button_icon,
|
||||
text=add_account_button_text,
|
||||
)
|
||||
|
||||
if wm.selected_account_id != "NO_ACCOUNTS":
|
||||
layout.template_list(
|
||||
"SPECKLE_UL_accounts_list",
|
||||
"",
|
||||
context.window_manager,
|
||||
"speckle_accounts",
|
||||
self,
|
||||
"account_index",
|
||||
)
|
||||
|
||||
def execute(self, context: Context) -> set[str]:
|
||||
wm = context.window_manager
|
||||
# update the selected account id
|
||||
account = get_account_from_id(wm.speckle_accounts[self.account_index].id)
|
||||
wm.selected_account_id = account.id
|
||||
self.report(
|
||||
{"INFO"},
|
||||
f"Selected account: {account.userInfo.name} - {account.userInfo.email} - {account.serverInfo.url}",
|
||||
)
|
||||
update_workspaces_list(context)
|
||||
update_projects_list(context)
|
||||
# redraw the area
|
||||
context.area.tag_redraw()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
def update_workspaces_list(context: Context) -> None:
|
||||
wm = context.window_manager
|
||||
wm.speckle_workspaces.clear()
|
||||
workspaces = get_workspaces(wm.selected_account_id)
|
||||
for id, name in workspaces:
|
||||
workspace: speckle_workspace = wm.speckle_workspaces.add()
|
||||
workspace.id = id
|
||||
workspace.name = name
|
||||
active_workspace = get_active_workspace(wm.selected_account_id)
|
||||
if active_workspace:
|
||||
wm.selected_workspace.id = active_workspace["id"]
|
||||
elif wm.speckle_workspaces:
|
||||
wm.selected_workspace.id = wm.speckle_workspaces[0].id
|
||||
print("Updated Workspaces List!")
|
||||
|
||||
|
||||
def update_projects_list(context: Context) -> None:
|
||||
wm = context.window_manager
|
||||
wm.speckle_projects.clear()
|
||||
projects: List[Tuple[str, str, str, str, bool]] = get_projects_for_account(
|
||||
wm.selected_account_id, workspace_id=wm.selected_workspace.id
|
||||
)
|
||||
for name, role, updated, id, can_receive in projects:
|
||||
project: speckle_project = wm.speckle_projects.add()
|
||||
project.name = name
|
||||
project.role = role
|
||||
project.updated = updated
|
||||
project.id = id
|
||||
project.can_receive = can_receive
|
||||
print("Updated Projects List!")
|
||||
@@ -0,0 +1,34 @@
|
||||
from typing import Optional, Dict
|
||||
import os
|
||||
import bpy.utils.previews
|
||||
|
||||
speckle_icons: Optional[Dict[str, bpy.types.ImagePreview]] = None
|
||||
|
||||
|
||||
def load_icons() -> None:
|
||||
global speckle_icons
|
||||
speckle_icons = bpy.utils.previews.new()
|
||||
icons_dir = os.path.dirname(__file__)
|
||||
speckle_logo_icon_path = os.path.join(icons_dir, "speckle-logo.png")
|
||||
if os.path.exists(speckle_logo_icon_path):
|
||||
speckle_icons.load("speckle_logo", speckle_logo_icon_path, "IMAGE")
|
||||
else:
|
||||
print(f"[Speckle] WARNING ‑ icon file not found: {speckle_logo_icon_path}")
|
||||
object_highlight_icon_path = os.path.join(icons_dir, "object-highlight.png")
|
||||
if os.path.exists(object_highlight_icon_path):
|
||||
speckle_icons.load("object_highlight", object_highlight_icon_path, "IMAGE")
|
||||
else:
|
||||
print(f"[Speckle] WARNING ‑ icon file not found: {object_highlight_icon_path}")
|
||||
|
||||
|
||||
def unload_icons() -> None:
|
||||
global speckle_icons
|
||||
if speckle_icons is not None:
|
||||
bpy.utils.previews.remove(speckle_icons)
|
||||
|
||||
|
||||
def get_icon(icon_name: str) -> int:
|
||||
global speckle_icons
|
||||
if speckle_icons is None:
|
||||
raise ValueError("Icons not loaded")
|
||||
return speckle_icons[icon_name].icon_id
|
||||
@@ -0,0 +1,106 @@
|
||||
import bpy
|
||||
from bpy.types import UILayout, Context
|
||||
from .icons import get_icon
|
||||
|
||||
|
||||
class SPECKLE_PT_main_panel(bpy.types.Panel):
|
||||
"""
|
||||
main panel for the Speckle addon.
|
||||
"""
|
||||
|
||||
bl_label = "Speckle"
|
||||
|
||||
bl_idname = "SPECKLE_PT_main_panel"
|
||||
bl_space_type = "VIEW_3D"
|
||||
bl_region_type = "UI"
|
||||
bl_category = "Speckle"
|
||||
|
||||
def draw(self, context: Context) -> None:
|
||||
layout: UILayout = self.layout
|
||||
layout.label(text="Speckle Connector", icon_value=get_icon("speckle_logo"))
|
||||
|
||||
# check to see if there are any speckle models in the file
|
||||
if not context.scene.speckle_state.model_cards:
|
||||
layout.label(text="Hello!")
|
||||
layout.label(text="There are no Speckle models in this file yet.")
|
||||
|
||||
layout.separator()
|
||||
|
||||
wm = context.window_manager
|
||||
project_selected = bool(getattr(wm, "selected_project_name", None))
|
||||
model_selected = bool(getattr(wm, "selected_model_name", None))
|
||||
version_selected = bool(getattr(wm, "selected_version_id", None))
|
||||
selection_made = bool(getattr(wm, "speckle_objects", None))
|
||||
|
||||
# UI Mode Switch
|
||||
row = layout.row()
|
||||
row.prop(wm, "ui_mode", expand=True)
|
||||
|
||||
# select Project button
|
||||
row = layout.row()
|
||||
project_name = getattr(wm, "selected_project_name", "")
|
||||
project_button_text = project_name if project_selected else "Select Project"
|
||||
project_button_icon = "CHECKMARK" if project_selected else "PLUS"
|
||||
row.operator(
|
||||
"speckle.project_selection_dialog",
|
||||
text=project_button_text,
|
||||
icon=project_button_icon,
|
||||
)
|
||||
|
||||
# select Model button
|
||||
row = layout.row()
|
||||
model_name = getattr(wm, "selected_model_name", "")
|
||||
model_button_text = model_name if model_selected else "Select Model"
|
||||
model_button_icon = "CHECKMARK" if model_selected else "PLUS"
|
||||
row.enabled = project_selected
|
||||
row.operator(
|
||||
"speckle.model_selection_dialog",
|
||||
text=model_button_text,
|
||||
icon=model_button_icon,
|
||||
)
|
||||
if wm.ui_mode == "PUBLISH":
|
||||
# TODO: implement Publish flow
|
||||
# Selection filter
|
||||
row = layout.row()
|
||||
row.enabled = project_selected and model_selected
|
||||
selection_button_text = (
|
||||
f"{len(wm.speckle_objects)} Objects"
|
||||
if wm.speckle_objects
|
||||
else "Select Objects"
|
||||
)
|
||||
row.operator(
|
||||
"speckle.selection_filter_dialog",
|
||||
text=selection_button_text,
|
||||
icon="PLUS",
|
||||
).model_card_id = ""
|
||||
|
||||
# Publish button
|
||||
row = layout.row()
|
||||
row.enabled = project_selected and model_selected and selection_made
|
||||
row.operator("speckle.publish", text="Publish Model", icon="EXPORT")
|
||||
pass
|
||||
|
||||
if wm.ui_mode == "LOAD":
|
||||
# select Version button
|
||||
row = layout.row()
|
||||
version_id = getattr(wm, "selected_version_id", "")
|
||||
load_option = getattr(wm, "selected_version_load_option", "")
|
||||
if load_option == "LATEST":
|
||||
version_button_text = "Latest"
|
||||
elif load_option == "SPECIFIC":
|
||||
version_button_text = version_id
|
||||
else:
|
||||
version_button_text = "Select Version"
|
||||
|
||||
version_button_icon = "CHECKMARK" if version_selected else "PLUS"
|
||||
row.enabled = project_selected and model_selected
|
||||
row.operator(
|
||||
"speckle.version_selection_dialog",
|
||||
text=version_button_text,
|
||||
icon=version_button_icon,
|
||||
).model_card_id = ""
|
||||
|
||||
# load button
|
||||
row = layout.row()
|
||||
row.enabled = project_selected and model_selected and version_selected
|
||||
row.operator("speckle.load", text="Load Model", icon="IMPORT")
|
||||
@@ -0,0 +1,89 @@
|
||||
import bpy
|
||||
from bpy.types import UILayout, Context
|
||||
from .icons import get_icon
|
||||
|
||||
|
||||
class SPECKLE_PT_model_cards_panel(bpy.types.Panel):
|
||||
"""
|
||||
Panel for displaying Speckle model cards.
|
||||
"""
|
||||
|
||||
bl_label = "Model Cards"
|
||||
bl_idname = "SPECKLE_PT_model_cards_panel"
|
||||
bl_space_type = "VIEW_3D"
|
||||
bl_region_type = "UI"
|
||||
bl_category = "Speckle"
|
||||
bl_order = 1
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: Context) -> bool:
|
||||
"""Only show panel when model cards exist"""
|
||||
return bool(context.scene.speckle_state.model_cards)
|
||||
|
||||
def draw(self, context: Context) -> None:
|
||||
layout: UILayout = self.layout
|
||||
|
||||
# group model cards by project name
|
||||
project_groups = {}
|
||||
for model_card in context.scene.speckle_state.model_cards:
|
||||
project_name = (
|
||||
model_card.project_name if model_card.project_name else "No Project"
|
||||
)
|
||||
if project_name not in project_groups:
|
||||
project_groups[project_name] = []
|
||||
project_groups[project_name].append(model_card)
|
||||
|
||||
for project_name, model_cards in project_groups.items():
|
||||
project_box = layout.box()
|
||||
project_row = project_box.row()
|
||||
project_row.label(text=f"Project: {project_name}", icon="TRIA_RIGHT")
|
||||
|
||||
for model_card in model_cards:
|
||||
box: UILayout = project_box.box()
|
||||
row_1: UILayout = box.row()
|
||||
row_2: UILayout = box.row()
|
||||
|
||||
if model_card.is_publish:
|
||||
# Publish button in the model card
|
||||
row_1.operator(
|
||||
"speckle.model_card_publish", text="", icon="EXPORT"
|
||||
).model_card_id = model_card.get_model_card_id()
|
||||
# Selection filter button in the model card
|
||||
row_2.operator(
|
||||
"speckle.selection_filter_dialog",
|
||||
text=f"Selection: {len(model_card.objects)} objects",
|
||||
).model_card_id = model_card.get_model_card_id()
|
||||
elif not model_card.is_publish:
|
||||
# Load button in the model card
|
||||
row_1.operator(
|
||||
"speckle.model_card_load", text="", icon="IMPORT"
|
||||
).model_card_id = model_card.get_model_card_id()
|
||||
version_button_text = (
|
||||
f"Latest: {model_card.version_id}"
|
||||
if model_card.load_option == "LATEST"
|
||||
else f"{model_card.version_id}"
|
||||
)
|
||||
row_2.operator(
|
||||
"speckle.version_selection_dialog",
|
||||
text=version_button_text,
|
||||
).model_card_id = model_card.get_model_card_id()
|
||||
# TODO: Get last updated time
|
||||
|
||||
else:
|
||||
print({"ERROR"}, "Model card state unknown")
|
||||
return
|
||||
|
||||
row_1.label(text=f"{model_card.model_name}")
|
||||
|
||||
# Select button in the model card
|
||||
select_op = row_1.operator(
|
||||
"speckle.select_objects",
|
||||
text="",
|
||||
icon_value=get_icon("object_highlight"),
|
||||
)
|
||||
select_op.model_card_id = model_card.get_model_card_id()
|
||||
|
||||
# Settings button in the model card
|
||||
row_1.operator(
|
||||
"speckle.model_card_settings", text="", icon="COLLAPSEMENU"
|
||||
).model_card_id = model_card.get_model_card_id()
|
||||
@@ -0,0 +1,127 @@
|
||||
import bpy
|
||||
from bpy.types import UILayout, Context, PropertyGroup, Event
|
||||
from ..utils.model_manager import get_models_for_project
|
||||
from ..utils.version_manager import get_latest_version
|
||||
from ..utils.account_manager import can_create_model
|
||||
from ..blender_operators.create_model import SPECKLE_OT_create_model
|
||||
|
||||
|
||||
class SPECKLE_UL_models_list(bpy.types.UIList):
|
||||
"""
|
||||
UIList for displaying a list of Speckle models
|
||||
"""
|
||||
|
||||
def draw_item(
|
||||
self,
|
||||
context: Context,
|
||||
layout: UILayout,
|
||||
data: PropertyGroup,
|
||||
item: PropertyGroup,
|
||||
icon: str,
|
||||
active_data: PropertyGroup,
|
||||
active_propname: str,
|
||||
) -> None:
|
||||
if self.layout_type in {"DEFAULT", "COMPACT"}:
|
||||
row = layout.row(align=True)
|
||||
split = row.split(factor=0.5)
|
||||
split.label(text=item.name)
|
||||
|
||||
right_split = split.split(factor=0.25)
|
||||
right_split.label(text=item.id)
|
||||
right_split.label(text=item.updated)
|
||||
|
||||
elif self.layout_type == "GRID":
|
||||
layout.alignment = "CENTER"
|
||||
layout.label(text=item.name)
|
||||
|
||||
|
||||
class SPECKLE_OT_model_selection_dialog(bpy.types.Operator):
|
||||
"""
|
||||
operator for displaying and handling the model selection dialog
|
||||
"""
|
||||
|
||||
bl_idname = "speckle.model_selection_dialog"
|
||||
bl_label = "Select Model"
|
||||
bl_description = "Select a model to load"
|
||||
|
||||
def update_models_list(self, context: Context) -> None:
|
||||
wm = context.window_manager
|
||||
|
||||
wm.speckle_models.clear()
|
||||
|
||||
search = self.search_query if self.search_query.strip() else None
|
||||
models = get_models_for_project(
|
||||
wm.selected_account_id, wm.selected_project_id, search=search
|
||||
)
|
||||
|
||||
for name, id, updated in models:
|
||||
model = wm.speckle_models.add()
|
||||
model.name = name
|
||||
model.updated = updated
|
||||
model.id = id
|
||||
|
||||
return None
|
||||
|
||||
search_query: bpy.props.StringProperty( # type: ignore
|
||||
name="Search",
|
||||
description="Search a model",
|
||||
default="",
|
||||
update=update_models_list,
|
||||
)
|
||||
|
||||
model_index: bpy.props.IntProperty(name="Model Index", default=0) # type: ignore
|
||||
|
||||
def execute(self, context: Context) -> set[str]:
|
||||
wm = context.window_manager
|
||||
if 0 <= self.model_index < len(wm.speckle_models):
|
||||
selected_model = wm.speckle_models[self.model_index]
|
||||
|
||||
wm.selected_model_id = selected_model.id
|
||||
wm.selected_model_name = selected_model.name
|
||||
|
||||
latest_version = get_latest_version(
|
||||
account_id=wm.selected_account_id,
|
||||
project_id=wm.selected_project_id,
|
||||
model_id=wm.selected_model_id,
|
||||
)
|
||||
if latest_version:
|
||||
wm.selected_version_load_option = "LATEST"
|
||||
wm.selected_version_id = latest_version[0]
|
||||
|
||||
print(f"Selected model: {selected_model.name} ({selected_model.id})")
|
||||
|
||||
context.area.tag_redraw()
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context: Context, event: Event) -> set[str]:
|
||||
self.update_models_list(context)
|
||||
|
||||
wm = context.window_manager
|
||||
authorized, _ = can_create_model(wm.selected_account_id, wm.selected_project_id)
|
||||
self._can_create_model = authorized
|
||||
SPECKLE_OT_create_model._can_create = authorized
|
||||
|
||||
return context.window_manager.invoke_props_dialog(self)
|
||||
|
||||
def draw(self, context: Context) -> None:
|
||||
layout: UILayout = self.layout
|
||||
wm = context.window_manager
|
||||
layout.label(text=f"Project: {wm.selected_project_name}")
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.prop(self, "search_query", icon="VIEWZOOM", text="") # search bar
|
||||
if wm.ui_mode != "LOAD":
|
||||
sub = row.row(align=True)
|
||||
sub.enabled = getattr(self, "_can_create_model", True)
|
||||
sub.operator("speckle.create_model", icon="ADD", text="")
|
||||
|
||||
layout.template_list(
|
||||
"SPECKLE_UL_models_list",
|
||||
"",
|
||||
context.window_manager,
|
||||
"speckle_models",
|
||||
self,
|
||||
"model_index",
|
||||
)
|
||||
|
||||
layout.separator()
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1,199 @@
|
||||
import bpy
|
||||
from bpy.types import UILayout, Context, PropertyGroup, Event
|
||||
from typing import List, Tuple
|
||||
from ..utils.account_manager import (
|
||||
can_create_project_in_workspace,
|
||||
get_active_workspace,
|
||||
get_default_account_id,
|
||||
get_account_from_id,
|
||||
)
|
||||
from ..utils.project_manager import get_projects_for_account
|
||||
from ..utils.property_groups import speckle_project
|
||||
|
||||
|
||||
class SPECKLE_UL_projects_list(bpy.types.UIList):
|
||||
"""
|
||||
UIList for displaying a list of Speckle projects
|
||||
"""
|
||||
|
||||
def draw_item(
|
||||
self,
|
||||
context: Context,
|
||||
layout: UILayout,
|
||||
data: PropertyGroup,
|
||||
item: PropertyGroup,
|
||||
icon: str,
|
||||
active_data: PropertyGroup,
|
||||
active_propname: str,
|
||||
) -> None:
|
||||
if self.layout_type in {"DEFAULT", "COMPACT"}:
|
||||
row = layout.row(align=True)
|
||||
# enable/disable the row based on permission
|
||||
row.enabled = item.can_receive
|
||||
|
||||
split = row.split(factor=0.5)
|
||||
split.label(text=item.name)
|
||||
|
||||
right_split = split.split(factor=0.5)
|
||||
right_split.label(text=item.role)
|
||||
right_split.label(text=item.updated)
|
||||
|
||||
# handles when the list is in a grid layout
|
||||
elif self.layout_type == "GRID":
|
||||
layout.alignment = "CENTER"
|
||||
layout.enabled = item.can_receive
|
||||
layout.label(text=item.name)
|
||||
|
||||
|
||||
class SPECKLE_OT_project_selection_dialog(bpy.types.Operator):
|
||||
"""
|
||||
operator for displaying and handling the project selection dialog
|
||||
"""
|
||||
|
||||
bl_idname = "speckle.project_selection_dialog"
|
||||
bl_label = "Select Project"
|
||||
bl_description = "Select a project to load models from"
|
||||
|
||||
def update_projects_list(self, context: Context) -> None:
|
||||
"""
|
||||
updates the list of projects based on the selected account and search query
|
||||
"""
|
||||
wm = context.window_manager
|
||||
|
||||
wm.can_create_project_in_workspace = can_create_project_in_workspace(
|
||||
wm.selected_account_id, wm.selected_workspace.id
|
||||
)
|
||||
wm.speckle_projects.clear()
|
||||
|
||||
# get projects for the selected account, using search if provided
|
||||
search = self.search_query if self.search_query.strip() else None
|
||||
projects: List[Tuple[str, str, str, str, bool]] = get_projects_for_account(
|
||||
wm.selected_account_id, search=search, workspace_id=wm.selected_workspace.id
|
||||
)
|
||||
|
||||
for name, role, updated, id, can_receive in projects:
|
||||
project: speckle_project = wm.speckle_projects.add()
|
||||
project.name = name
|
||||
project.role = role
|
||||
project.updated = updated
|
||||
project.id = id
|
||||
project.can_receive = can_receive
|
||||
print("Updated Projects List!")
|
||||
return None
|
||||
|
||||
search_query: bpy.props.StringProperty( # type: ignore
|
||||
name="Search or Paste a URL",
|
||||
description="Search a project or paste a URL to add a project",
|
||||
default="",
|
||||
update=update_projects_list,
|
||||
)
|
||||
|
||||
project_index: bpy.props.IntProperty(name="Project Index", default=0) # type: ignore
|
||||
|
||||
def execute(self, context: Context) -> set[str]:
|
||||
wm = context.window_manager
|
||||
if 0 <= self.project_index < len(wm.speckle_projects):
|
||||
selected_project = wm.speckle_projects[self.project_index]
|
||||
|
||||
# verify the user has permission to receive from this project
|
||||
if not selected_project.can_receive:
|
||||
self.report(
|
||||
{"ERROR"},
|
||||
"Your role on this project doesn't give you permission to load.",
|
||||
)
|
||||
return {"CANCELLED"}
|
||||
|
||||
wm.selected_project_id = selected_project.id
|
||||
wm.selected_project_name = selected_project.name
|
||||
|
||||
print(f"Selected project: {selected_project.name} ({selected_project.id})")
|
||||
|
||||
context.area.tag_redraw()
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context: Context, event: Event) -> set[str]:
|
||||
wm = context.window_manager
|
||||
|
||||
# Clear existing projects
|
||||
wm.speckle_projects.clear()
|
||||
|
||||
if wm.selected_account_id == "":
|
||||
wm.selected_account_id = get_default_account_id()
|
||||
|
||||
active_workspace = get_active_workspace(wm.selected_account_id)
|
||||
if active_workspace:
|
||||
wm.selected_workspace.id = active_workspace["id"]
|
||||
wm.selected_workspace.name = active_workspace["name"]
|
||||
else:
|
||||
from .account_selection_dialog import update_workspaces_list
|
||||
|
||||
update_workspaces_list(context)
|
||||
workspaces = list(wm.speckle_workspaces)
|
||||
if workspaces:
|
||||
wm.selected_workspace.id = workspaces[0].id
|
||||
wm.selected_workspace.name = workspaces[0].name
|
||||
|
||||
# Fetch projects from server
|
||||
projects: List[Tuple[str, str, str, str, bool]] = get_projects_for_account(
|
||||
wm.selected_account_id, wm.selected_workspace.id
|
||||
)
|
||||
|
||||
for name, role, updated, id, can_receive in projects:
|
||||
project: speckle_project = wm.speckle_projects.add()
|
||||
project.name = name
|
||||
project.role = role
|
||||
project.updated = updated
|
||||
project.id = id
|
||||
project.can_receive = can_receive
|
||||
|
||||
return context.window_manager.invoke_props_dialog(self)
|
||||
|
||||
def draw(self, context: Context) -> None:
|
||||
layout: UILayout = self.layout
|
||||
wm = context.window_manager
|
||||
|
||||
# Account selection
|
||||
row = layout.row()
|
||||
|
||||
if wm.selected_account_id == "NO_ACCOUNTS":
|
||||
row.operator("speckle.add_account", icon="WORLD", text="Sign In")
|
||||
|
||||
# if no accounts then don't show workspaces or projects list
|
||||
if wm.selected_account_id != "NO_ACCOUNTS":
|
||||
account = get_account_from_id(wm.selected_account_id)
|
||||
|
||||
row.operator(
|
||||
"speckle.account_selection_dialog",
|
||||
icon="USER",
|
||||
text=f"{account.userInfo.name} - {account.userInfo.email} - {account.serverInfo.url}",
|
||||
)
|
||||
# Workspace selection
|
||||
row = layout.row()
|
||||
row.operator(
|
||||
"speckle.workspace_selection_dialog",
|
||||
icon="WORKSPACE",
|
||||
text=wm.selected_workspace.name,
|
||||
)
|
||||
|
||||
# Search field
|
||||
row = layout.row(align=True)
|
||||
row.prop(self, "search_query", icon="VIEWZOOM", text="")
|
||||
# add project by url button
|
||||
split = row.split()
|
||||
split.operator("speckle.add_project_by_url", icon="LINKED", text="")
|
||||
# create project button
|
||||
# hide if in load mode
|
||||
if wm.ui_mode != "LOAD":
|
||||
split = row.split()
|
||||
split.operator("speckle.create_project", icon="ADD", text="")
|
||||
split.enabled = wm.can_create_project_in_workspace
|
||||
|
||||
layout.template_list(
|
||||
"SPECKLE_UL_projects_list",
|
||||
"",
|
||||
context.window_manager,
|
||||
"speckle_projects",
|
||||
self,
|
||||
"project_index",
|
||||
)
|
||||
layout.separator()
|
||||
@@ -0,0 +1,143 @@
|
||||
import bpy
|
||||
from typing import List
|
||||
from bpy.types import Operator, Context, Object
|
||||
from bpy.props import EnumProperty
|
||||
from ..utils.model_card_utils import update_model_card_objects
|
||||
from ..utils.account_manager import can_create_version
|
||||
|
||||
|
||||
class SPECKLE_OT_selection_filter_dialog(Operator):
|
||||
"""
|
||||
operator for handling object selection and filtering
|
||||
"""
|
||||
|
||||
bl_idname = "speckle.selection_filter_dialog"
|
||||
bl_label = "Select Objects"
|
||||
bl_description = "Select objects to publish"
|
||||
|
||||
selection_type: EnumProperty(
|
||||
name="Selection",
|
||||
items=[
|
||||
("SELECTION", "Selection", "Select objects manually"),
|
||||
],
|
||||
default="SELECTION",
|
||||
) # type: ignore
|
||||
|
||||
model_card_id: bpy.props.StringProperty(
|
||||
name="Model Card ID",
|
||||
description="This is used to indicate the function is called from a model card",
|
||||
default="",
|
||||
) # type: ignore
|
||||
|
||||
version_message: bpy.props.StringProperty(
|
||||
name="Version Message",
|
||||
description="Message to be used for the version",
|
||||
default="",
|
||||
) # type: ignore
|
||||
|
||||
def execute(self, context: Context) -> set:
|
||||
wm = context.window_manager
|
||||
wm.speckle_objects.clear()
|
||||
user_selection = context.selected_objects
|
||||
if self.model_card_id != "":
|
||||
model_card = context.scene.speckle_state.get_model_card_by_id(
|
||||
self.model_card_id
|
||||
)
|
||||
update_model_card_objects(model_card, user_selection)
|
||||
self.report({"INFO"}, "Selection updated")
|
||||
|
||||
# On-demand permission check before publishing
|
||||
authorized, auth_message = can_create_version(
|
||||
model_card.account_id, model_card.project_id, model_card.model_id
|
||||
)
|
||||
if not authorized:
|
||||
self.report({"ERROR"}, auth_message)
|
||||
return {"CANCELLED"}
|
||||
|
||||
# Call the publish operator
|
||||
bpy.ops.speckle.model_card_publish(
|
||||
model_card_id=self.model_card_id, version_message=self.version_message
|
||||
)
|
||||
|
||||
context.area.tag_redraw()
|
||||
return {"FINISHED"}
|
||||
|
||||
for sel in user_selection:
|
||||
obj = wm.speckle_objects.add()
|
||||
obj.name = sel.name
|
||||
context.area.tag_redraw()
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context: Context, event: bpy.types.Event) -> set:
|
||||
return context.window_manager.invoke_props_dialog(self)
|
||||
|
||||
def draw(self, context: Context):
|
||||
layout = self.layout
|
||||
wm = context.window_manager
|
||||
|
||||
project_name = wm.selected_project_name
|
||||
model_name = wm.selected_model_name
|
||||
if self.model_card_id != "":
|
||||
model_card = context.scene.speckle_state.get_model_card_by_id(
|
||||
self.model_card_id
|
||||
)
|
||||
project_name = model_card.project_name
|
||||
model_name = model_card.model_name
|
||||
|
||||
layout.label(text=f"Project: {project_name}")
|
||||
layout.label(text=f"Model: {model_name}")
|
||||
|
||||
# layout.prop(self, "selection_type")
|
||||
layout.separator()
|
||||
|
||||
selected_objects: List[Object] = context.selected_objects
|
||||
total_selected: int = len(selected_objects)
|
||||
|
||||
box = layout.box()
|
||||
row = box.row()
|
||||
row.label(text="Selection Summary", icon="OUTLINER_OB_GROUP_INSTANCE")
|
||||
row.label(text=f"Total: {total_selected}", icon="OBJECT_DATA")
|
||||
|
||||
object_types: dict[str, int] = {}
|
||||
for obj in selected_objects:
|
||||
if obj.type not in object_types:
|
||||
object_types[obj.type] = 1
|
||||
else:
|
||||
object_types[obj.type] += 1
|
||||
|
||||
col = box.column(align=True)
|
||||
for obj_type, count in object_types.items():
|
||||
row = col.row()
|
||||
row.label(text=f"{obj_type}:", icon=self.get_icon_for_type(obj_type))
|
||||
row.label(text=str(count))
|
||||
|
||||
layout.separator()
|
||||
|
||||
if self.model_card_id != "":
|
||||
layout.label(text="Version Message")
|
||||
layout.prop(self, "version_message", text="")
|
||||
layout.label(
|
||||
text="New version will be published after updating selection",
|
||||
icon="INFO_LARGE",
|
||||
)
|
||||
|
||||
def get_icon_for_type(self, obj_type: str) -> str:
|
||||
icon_map: dict[str, str] = {
|
||||
"MESH": "OUTLINER_OB_MESH",
|
||||
"CURVE": "OUTLINER_OB_CURVE",
|
||||
"SURFACE": "OUTLINER_OB_SURFACE",
|
||||
"META": "OUTLINER_OB_META",
|
||||
"FONT": "OUTLINER_OB_FONT",
|
||||
"ARMATURE": "OUTLINER_OB_ARMATURE",
|
||||
"LATTICE": "OUTLINER_OB_LATTICE",
|
||||
"EMPTY": "OUTLINER_OB_EMPTY",
|
||||
"GPENCIL": "OUTLINER_OB_GREASEPENCIL",
|
||||
"CAMERA": "OUTLINER_OB_CAMERA",
|
||||
"LIGHT": "OUTLINER_OB_LIGHT",
|
||||
"SPEAKER": "OUTLINER_OB_SPEAKER",
|
||||
"LIGHT_PROBE": "OUTLINER_OB_LIGHTPROBE",
|
||||
}
|
||||
return icon_map.get(obj_type, "OBJECT_DATA")
|
||||
|
||||
def check(self, context: Context) -> bool:
|
||||
return True # this forces the dialog to redraw
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 446 B |
@@ -0,0 +1,48 @@
|
||||
import bpy
|
||||
from bpy.types import UILayout, Context
|
||||
|
||||
|
||||
class SPECKLE_PT_update_panel(bpy.types.Panel):
|
||||
"""Panel for displaying connector update notifications"""
|
||||
|
||||
bl_label = "Update Speckle"
|
||||
bl_idname = "SPECKLE_PT_update_panel"
|
||||
bl_space_type = "VIEW_3D"
|
||||
bl_region_type = "UI"
|
||||
bl_category = "Speckle"
|
||||
bl_order = 0 # This ensures it appears above the main panel
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: Context) -> bool:
|
||||
"""Only show this panel when an update is available"""
|
||||
wm = context.window_manager
|
||||
return getattr(wm, "update_available", False)
|
||||
|
||||
def draw(self, context: Context) -> None:
|
||||
layout: UILayout = self.layout
|
||||
wm = context.window_manager
|
||||
|
||||
# Get current version from bl_info
|
||||
from ... import bl_info
|
||||
|
||||
current_version = bl_info["version"]
|
||||
current_version_str = (
|
||||
f"{current_version[0]}.{current_version[1]}.{current_version[2]}"
|
||||
)
|
||||
|
||||
# Update notification
|
||||
box = layout.box()
|
||||
box.alert = True # Makes the box stand out with alert styling
|
||||
|
||||
col = box.column()
|
||||
col.label(text="New version available!", icon="INFO")
|
||||
|
||||
row = col.row()
|
||||
row.label(text=f"Current: v{current_version_str}")
|
||||
|
||||
row = col.row()
|
||||
row.label(text=f"Latest: v{wm.latest_version}")
|
||||
|
||||
# Update button
|
||||
row = col.row()
|
||||
row.operator("speckle.update_button", text="Download Update", icon="LINKED")
|
||||
@@ -0,0 +1,185 @@
|
||||
import bpy
|
||||
from bpy.types import UILayout, Context, PropertyGroup, Event
|
||||
from ..utils.version_manager import get_versions_for_model, get_latest_version
|
||||
|
||||
|
||||
class SPECKLE_UL_versions_list(bpy.types.UIList):
|
||||
"""
|
||||
UIList for displaying a list of Speckle versions
|
||||
"""
|
||||
|
||||
# TODO: Adjust column widths so message has the most space.
|
||||
def draw_item(
|
||||
self,
|
||||
context: Context,
|
||||
layout: UILayout,
|
||||
data: PropertyGroup,
|
||||
item: PropertyGroup,
|
||||
icon: str,
|
||||
active_data: PropertyGroup,
|
||||
active_propname: str,
|
||||
) -> None:
|
||||
if self.layout_type in {"DEFAULT", "COMPACT"}:
|
||||
row = layout.row(align=True)
|
||||
split = row.split(factor=0.166)
|
||||
split.label(text=item.id)
|
||||
right_split = split.split(factor=0.7)
|
||||
right_split.label(text=item.message)
|
||||
right_split.label(text=item.updated)
|
||||
|
||||
elif self.layout_type == "GRID":
|
||||
layout.alignment = "CENTER"
|
||||
layout.label(text=item.id)
|
||||
|
||||
|
||||
class SPECKLE_OT_version_selection_dialog(bpy.types.Operator):
|
||||
bl_idname = "speckle.version_selection_dialog"
|
||||
bl_label = "Select Version"
|
||||
bl_description = "Select a model version to load. Default is the latest version. You can also select a specific version."
|
||||
|
||||
version_index: bpy.props.IntProperty(name="Model Index", default=0) # type: ignore
|
||||
|
||||
load_option: bpy.props.EnumProperty( # type: ignore
|
||||
name="Load Option",
|
||||
description="Choose how to load the version",
|
||||
items=[
|
||||
("LATEST", "Load latest version", "Load the latest version available"),
|
||||
(
|
||||
"SPECIFIC",
|
||||
"Load a specific version",
|
||||
"Load a specific version from the list",
|
||||
),
|
||||
],
|
||||
default="LATEST",
|
||||
)
|
||||
|
||||
model_card_id: bpy.props.StringProperty(
|
||||
name="Model Card ID",
|
||||
description="This is used to indicate the function is called from a model card",
|
||||
default="",
|
||||
) # type: ignore
|
||||
|
||||
def update_versions_list(self, context: Context) -> None:
|
||||
wm = context.window_manager
|
||||
wm.speckle_versions.clear()
|
||||
|
||||
versions = get_versions_for_model(
|
||||
account_id=wm.selected_account_id,
|
||||
project_id=wm.selected_project_id,
|
||||
model_id=wm.selected_model_id,
|
||||
)
|
||||
|
||||
for id, message, updated in versions:
|
||||
version = wm.speckle_versions.add()
|
||||
version.id = id
|
||||
version.message = message
|
||||
version.updated = updated
|
||||
|
||||
return None
|
||||
|
||||
def execute(self, context: Context) -> set[str]:
|
||||
wm = context.window_manager
|
||||
|
||||
version_id_to_store = ""
|
||||
|
||||
if self.load_option == "LATEST":
|
||||
latest_version = get_latest_version(
|
||||
account_id=wm.selected_account_id,
|
||||
project_id=wm.selected_project_id,
|
||||
model_id=wm.selected_model_id,
|
||||
)
|
||||
if latest_version:
|
||||
version_id_to_store = latest_version[0]
|
||||
else:
|
||||
print(
|
||||
f"Could not fetch latest version for model {wm.selected_model_id}"
|
||||
)
|
||||
return {"CANCELLED"}
|
||||
|
||||
elif self.load_option == "SPECIFIC":
|
||||
if 0 <= self.version_index < len(wm.speckle_versions):
|
||||
selected_version = wm.speckle_versions[self.version_index]
|
||||
version_id_to_store = selected_version.id
|
||||
else:
|
||||
print(f"Invalid version index {self.version_index}")
|
||||
return {"CANCELLED"}
|
||||
wm.selected_version_id = version_id_to_store
|
||||
|
||||
if self.model_card_id != "":
|
||||
model_card = context.scene.speckle_state.get_model_card_by_id(
|
||||
self.model_card_id
|
||||
)
|
||||
if model_card is None:
|
||||
self.report({"ERROR"}, f"Model card '{self.model_card_id}' not found")
|
||||
return {"CANCELLED"}
|
||||
|
||||
model_card.load_option = self.load_option
|
||||
model_card.version_id = version_id_to_store
|
||||
self.report(
|
||||
{"INFO"},
|
||||
f"Model card updated: Selected version: {model_card.version_id}, Option: {self.load_option}",
|
||||
)
|
||||
bpy.ops.speckle.model_card_load(model_card_id=self.model_card_id)
|
||||
context.area.tag_redraw()
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
wm.selected_version_load_option = self.load_option
|
||||
self.report(
|
||||
{"INFO"},
|
||||
f"Selected version: {version_id_to_store} (Option: {self.load_option})",
|
||||
)
|
||||
|
||||
context.area.tag_redraw()
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context: Context, event: Event) -> set[str]:
|
||||
if self.model_card_id != "":
|
||||
wm = context.window_manager
|
||||
model_card = context.scene.speckle_state.get_model_card_by_id(
|
||||
self.model_card_id
|
||||
)
|
||||
self.load_option = model_card.load_option
|
||||
wm.selected_account_id = model_card.account_id
|
||||
wm.selected_project_id = model_card.project_id
|
||||
wm.selected_model_id = model_card.model_id
|
||||
wm.selected_version_id = model_card.version_id
|
||||
|
||||
self.update_versions_list(context)
|
||||
|
||||
return context.window_manager.invoke_props_dialog(self)
|
||||
|
||||
def draw(self, context: Context) -> None:
|
||||
layout: UILayout = self.layout
|
||||
wm = context.window_manager
|
||||
project_name = wm.selected_project_name
|
||||
model_name = wm.selected_model_name
|
||||
if self.model_card_id != "":
|
||||
model_card = context.scene.speckle_state.get_model_card_by_id(
|
||||
self.model_card_id
|
||||
)
|
||||
project_name = model_card.project_name
|
||||
model_name = model_card.model_name
|
||||
|
||||
layout.label(text=f"Project: {project_name}")
|
||||
layout.label(text=f"Model: {model_name}")
|
||||
|
||||
layout.prop(
|
||||
self,
|
||||
"load_option",
|
||||
expand=True,
|
||||
)
|
||||
|
||||
if self.load_option == "SPECIFIC":
|
||||
# Versions UIList
|
||||
layout.template_list(
|
||||
"SPECKLE_UL_versions_list",
|
||||
"",
|
||||
context.window_manager,
|
||||
"speckle_versions",
|
||||
self,
|
||||
"version_index",
|
||||
)
|
||||
|
||||
layout.separator()
|
||||
@@ -0,0 +1,108 @@
|
||||
import bpy
|
||||
from bpy.types import Context, UILayout, Event, PropertyGroup
|
||||
from typing import List, Tuple
|
||||
from ..utils.account_manager import get_workspaces, speckle_workspace
|
||||
from ..utils.project_manager import get_projects_for_account
|
||||
from ..utils.account_manager import can_create_project_in_workspace
|
||||
|
||||
|
||||
class SPECKLE_UL_workspaces_list(bpy.types.UIList):
|
||||
"""
|
||||
UIList for workspaces
|
||||
"""
|
||||
|
||||
def draw_item(
|
||||
self,
|
||||
context: Context,
|
||||
layout: UILayout,
|
||||
data: PropertyGroup,
|
||||
item: PropertyGroup,
|
||||
icon: str,
|
||||
active_data: PropertyGroup,
|
||||
active_propname: str,
|
||||
) -> None:
|
||||
if self.layout_type in {"DEFAULT", "COMPACT"}:
|
||||
row = layout.row(align=True)
|
||||
row.label(text=item.name)
|
||||
|
||||
elif self.layout_type == "GRID":
|
||||
layout.alignment = "CENTER"
|
||||
layout.label(text=item.name)
|
||||
|
||||
|
||||
class SPECKLE_OT_workspace_selection_dialog(bpy.types.Operator):
|
||||
"""
|
||||
Operator for selecting a workspace
|
||||
"""
|
||||
|
||||
bl_idname = "speckle.workspace_selection_dialog"
|
||||
bl_label = "Select Workspace"
|
||||
bl_description = "Select a workspace to load projects from"
|
||||
|
||||
workspace_index: bpy.props.IntProperty(name="Workspace Index", default=0) # type: ignore
|
||||
|
||||
def invoke(self, context: Context, event: Event) -> set[str]:
|
||||
wm = context.window_manager
|
||||
wm.speckle_workspaces.clear()
|
||||
workspaces: List[Tuple[str, str]] = get_workspaces(wm.selected_account_id)
|
||||
current_workspace_index = 0
|
||||
for i, (id, name) in enumerate(workspaces):
|
||||
workspace: speckle_workspace = wm.speckle_workspaces.add()
|
||||
workspace.id = id
|
||||
workspace.name = name
|
||||
if id == wm.selected_workspace.id:
|
||||
current_workspace_index = i
|
||||
self.workspace_index = current_workspace_index
|
||||
return context.window_manager.invoke_props_dialog(self)
|
||||
|
||||
def draw(self, context: Context) -> None:
|
||||
layout: UILayout = self.layout
|
||||
wm = context.window_manager
|
||||
layout.label(text=f"Selected Workspace: {wm.selected_workspace.name}")
|
||||
layout.template_list(
|
||||
"SPECKLE_UL_workspaces_list",
|
||||
"",
|
||||
context.window_manager,
|
||||
"speckle_workspaces",
|
||||
self,
|
||||
"workspace_index",
|
||||
)
|
||||
|
||||
def execute(self, context: Context) -> set[str]:
|
||||
wm = context.window_manager
|
||||
if 0 <= self.workspace_index < len(wm.speckle_workspaces):
|
||||
selected_workspace = wm.speckle_workspaces[self.workspace_index]
|
||||
wm.selected_workspace.id = selected_workspace.id
|
||||
wm.selected_workspace.name = selected_workspace.name
|
||||
update_projects_list(context)
|
||||
context.area.tag_redraw()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
def update_projects_list(context):
|
||||
"""Update projects list when workspace changes"""
|
||||
|
||||
wm = context.window_manager
|
||||
wm.speckle_projects.clear()
|
||||
|
||||
# get projects for the selected account and workspace
|
||||
projects = get_projects_for_account(
|
||||
wm.selected_account_id, wm.selected_workspace.id
|
||||
)
|
||||
|
||||
for name, role, updated, id, can_receive in projects:
|
||||
project = wm.speckle_projects.add()
|
||||
project.name = name
|
||||
project.role = role
|
||||
project.updated = updated
|
||||
project.id = id
|
||||
project.can_receive = can_receive
|
||||
|
||||
# Update can_create_project_in_workspace flag
|
||||
wm.can_create_project_in_workspace = can_create_project_in_workspace(
|
||||
wm.selected_account_id, wm.selected_workspace.id
|
||||
)
|
||||
print(f"Workspace changed to: {wm.selected_workspace.id}")
|
||||
print("Projects list updated")
|
||||
|
||||
context.area.tag_redraw()
|
||||
@@ -0,0 +1,325 @@
|
||||
import bpy
|
||||
from specklepy.core.api.credentials import get_local_accounts
|
||||
from typing import List, Tuple, Optional, Dict
|
||||
from urllib.parse import urlparse
|
||||
from specklepy.core.api.credentials import Account
|
||||
from specklepy.core.api.client import SpeckleClient
|
||||
from specklepy.core.api.wrapper import StreamWrapper
|
||||
from .misc import strip_non_ascii
|
||||
|
||||
|
||||
class SpeckleClientCache:
|
||||
def __init__(self):
|
||||
self._clients: Dict[str, SpeckleClient] = {}
|
||||
|
||||
def get_client(self, account_id: str) -> SpeckleClient:
|
||||
# Check cache first
|
||||
if account_id in self._clients:
|
||||
print(f"[Cache HIT] Using cached client for account {account_id}")
|
||||
return self._clients[account_id]
|
||||
|
||||
# Create new client if needed
|
||||
print(f"[Cache MISS] Creating new client for account {account_id}")
|
||||
account = get_account_from_id(account_id)
|
||||
if not account:
|
||||
raise ValueError(f"No account found for ID: {account_id}")
|
||||
|
||||
url = account.serverInfo.url
|
||||
use_ssl = urlparse(url).scheme.lower() != "http"
|
||||
client = SpeckleClient(host=url, use_ssl=use_ssl)
|
||||
client.authenticate_with_account(account)
|
||||
self._clients[account_id] = client
|
||||
return client
|
||||
|
||||
def clear(self) -> None:
|
||||
"""Clear all cached clients."""
|
||||
print("[Cache] Clearing all cached clients")
|
||||
self._clients.clear()
|
||||
|
||||
|
||||
# Global cache instance
|
||||
_client_cache = SpeckleClientCache()
|
||||
|
||||
|
||||
class speckle_account(bpy.types.PropertyGroup):
|
||||
id: bpy.props.StringProperty() # type: ignore
|
||||
user_name: bpy.props.StringProperty() # type: ignore
|
||||
server_url: bpy.props.StringProperty() # type: ignore
|
||||
user_email: bpy.props.StringProperty() # type: ignore
|
||||
|
||||
|
||||
class speckle_workspace(bpy.types.PropertyGroup):
|
||||
"""
|
||||
PropertyGroup for storing workspace information
|
||||
"""
|
||||
|
||||
id: bpy.props.StringProperty(name="ID") # type: ignore
|
||||
name: bpy.props.StringProperty() # type: ignore
|
||||
|
||||
|
||||
def get_account_enum_items() -> List[Tuple[str, str, str, str]]:
|
||||
accounts: List[Account] = get_local_accounts()
|
||||
if not accounts:
|
||||
print("No accounts found!")
|
||||
return [("NO_ACCOUNTS", "No accounts found!", "", "")]
|
||||
print("Accounts added")
|
||||
speckle_accounts = []
|
||||
for acc in accounts:
|
||||
speckle_accounts.append(
|
||||
(
|
||||
acc.id,
|
||||
strip_non_ascii(acc.userInfo.name),
|
||||
acc.serverInfo.url,
|
||||
acc.userInfo.email,
|
||||
)
|
||||
)
|
||||
return speckle_accounts
|
||||
|
||||
|
||||
def get_workspaces(account_id: str) -> List[Tuple[str, str]]:
|
||||
"""
|
||||
retrieves the workspaces for a given account ID
|
||||
"""
|
||||
|
||||
try:
|
||||
# Get client from cache
|
||||
client = _client_cache.get_client(account_id)
|
||||
|
||||
workspaces_enabled = client.server.get().workspaces.workspaces_enabled
|
||||
|
||||
if workspaces_enabled:
|
||||
workspaces = client.active_user.get_workspaces().items
|
||||
|
||||
workspace_list = [
|
||||
(ws.id, strip_non_ascii(ws.name))
|
||||
for ws in workspaces
|
||||
if ws.creation_state is None or ws.creation_state.completed
|
||||
]
|
||||
|
||||
active_workspace = client.active_user.get_active_workspace()
|
||||
default_workspace_id = (
|
||||
active_workspace.id
|
||||
if active_workspace
|
||||
else (workspaces[0].id if workspaces else None)
|
||||
)
|
||||
|
||||
if default_workspace_id:
|
||||
result = reorder_tuple(workspace_list, default_workspace_id)
|
||||
else:
|
||||
result = workspace_list
|
||||
else:
|
||||
result = []
|
||||
|
||||
return result
|
||||
except Exception as e:
|
||||
print(f"Error in get_workspaces: {str(e)}")
|
||||
_client_cache.clear() # Clear cache on error
|
||||
return [("", "")]
|
||||
|
||||
|
||||
def get_default_account_id() -> Optional[str]:
|
||||
"""
|
||||
retrieves the ID of the default Speckle account
|
||||
"""
|
||||
return next(
|
||||
(acc.id for acc in get_local_accounts() if acc.isDefault), "NO_ACCOUNTS"
|
||||
)
|
||||
|
||||
|
||||
def get_server_url_by_account_id(account_id: str) -> Optional[str]:
|
||||
"""
|
||||
retrieves the server URL for a given account ID
|
||||
"""
|
||||
accounts: List[Account] = get_local_accounts()
|
||||
for acc in accounts:
|
||||
if acc.id == account_id:
|
||||
return acc.serverInfo.url
|
||||
return None
|
||||
|
||||
|
||||
def get_active_workspace(account_id: str) -> Optional[Dict[str, str]]:
|
||||
"""
|
||||
retrieves the ID of the default workspace for a given account ID
|
||||
"""
|
||||
try:
|
||||
client = _client_cache.get_client(account_id)
|
||||
active_workspace = client.active_user.get_active_workspace()
|
||||
if active_workspace:
|
||||
return {"id": active_workspace.id, "name": active_workspace.name}
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f"Error in get_active_workspace: {str(e)}")
|
||||
_client_cache.clear()
|
||||
return None
|
||||
|
||||
|
||||
def get_account_from_id(account_id: str) -> Optional[Account]:
|
||||
return next((acc for acc in get_local_accounts() if acc.id == account_id), None)
|
||||
|
||||
|
||||
def reorder_tuple(tuple_list, target_id):
|
||||
for i, (id, value) in enumerate(tuple_list):
|
||||
if id == target_id:
|
||||
# Remove the tuple from its current position
|
||||
target_tuple = tuple_list.pop(i)
|
||||
# Insert it at the beginning of the list
|
||||
tuple_list.insert(0, target_tuple)
|
||||
return tuple_list
|
||||
|
||||
# If the target_id wasn't found
|
||||
print(f"Tuple with ID {target_id} not found in the list")
|
||||
return tuple_list
|
||||
|
||||
|
||||
def get_project_from_url(
|
||||
url: str,
|
||||
) -> Tuple[Optional[StreamWrapper], Optional[object], Optional[object], str]:
|
||||
"""
|
||||
get a project from a URL, handling all the client setup.
|
||||
"""
|
||||
try:
|
||||
wrapper = StreamWrapper(url)
|
||||
account = wrapper.get_account()
|
||||
client = _client_cache.get_client(account.id)
|
||||
|
||||
# get the stream_id (project_id) from the wrapper
|
||||
if not wrapper.stream_id:
|
||||
return wrapper, client, None, "Could not extract project ID from URL"
|
||||
|
||||
project = client.project.get(wrapper.stream_id)
|
||||
|
||||
if not project:
|
||||
return wrapper, client, None, "Could not access project"
|
||||
|
||||
return wrapper, client, project, ""
|
||||
|
||||
except Exception as e:
|
||||
return None, None, None, f"Failed to process URL: {str(e)}"
|
||||
|
||||
|
||||
def get_model_details_by_wrapper(
|
||||
wrapper: StreamWrapper,
|
||||
) -> Tuple[str, str, str, str, str, str, str]:
|
||||
"""
|
||||
extract model details from a StreamWrapper object.
|
||||
"""
|
||||
client = wrapper.get_client()
|
||||
client.authenticate_with_account(wrapper.get_account())
|
||||
(
|
||||
account_id,
|
||||
project_id,
|
||||
project_name,
|
||||
model_id,
|
||||
model_name,
|
||||
version_id,
|
||||
load_option,
|
||||
) = "", "", "", "", "", "", ""
|
||||
account_id = wrapper.get_account().id
|
||||
if wrapper.stream_id:
|
||||
project_id = wrapper.stream_id
|
||||
project_name = client.project.get(project_id).name
|
||||
if wrapper.model_id:
|
||||
model_id = wrapper.model_id
|
||||
model = client.model.get(model_id, project_id)
|
||||
model_name = model.name
|
||||
load_option = "LATEST" if not wrapper.commit_id else "SPECIFIC"
|
||||
if wrapper.commit_id:
|
||||
version_id = wrapper.commit_id
|
||||
else:
|
||||
versions = client.version.get_versions(
|
||||
wrapper.model_id, wrapper.stream_id, limit=1
|
||||
)
|
||||
if versions.items and len(versions.items) > 0:
|
||||
version_id = versions.items[0].id
|
||||
else:
|
||||
version_id = ""
|
||||
return (
|
||||
account_id,
|
||||
project_id,
|
||||
project_name,
|
||||
model_id,
|
||||
model_name,
|
||||
version_id,
|
||||
load_option,
|
||||
)
|
||||
|
||||
|
||||
def can_load(client, project) -> Tuple[bool, str]:
|
||||
try:
|
||||
permissions = client.project.get_permissions(project.id)
|
||||
|
||||
if permissions.can_load.authorized:
|
||||
return True, ""
|
||||
else:
|
||||
return (
|
||||
False,
|
||||
"Your role on this project doesn't give you permission to load.",
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Failed to check permissions: {str(e)}"
|
||||
print(error_msg)
|
||||
return False, error_msg
|
||||
|
||||
|
||||
def can_create_version(
|
||||
account_id: str, project_id: str, model_id: str
|
||||
) -> Tuple[bool, str]:
|
||||
try:
|
||||
client = _client_cache.get_client(account_id)
|
||||
permissions = client.model.get_permissions(project_id, model_id)
|
||||
|
||||
if permissions.can_create_version.authorized:
|
||||
return True, ""
|
||||
else:
|
||||
message = getattr(permissions.can_create_version, "message", None)
|
||||
return (
|
||||
False,
|
||||
message
|
||||
or "Your role on this project doesn't give you permission to publish.",
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Failed to check permissions: {str(e)}"
|
||||
print(error_msg)
|
||||
return False, error_msg
|
||||
|
||||
|
||||
def can_create_model(account_id: str, project_id: str) -> Tuple[bool, str]:
|
||||
try:
|
||||
client = _client_cache.get_client(account_id)
|
||||
permissions = client.project.get_permissions(project_id)
|
||||
|
||||
if permissions.can_create_model.authorized:
|
||||
return True, ""
|
||||
else:
|
||||
message = getattr(permissions.can_create_model, "message", None)
|
||||
return (
|
||||
False,
|
||||
message
|
||||
or "You don't have permission to create models in this project.",
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Failed to check permissions: {str(e)}"
|
||||
print(error_msg)
|
||||
return False, error_msg
|
||||
|
||||
|
||||
def can_create_project_in_workspace(account_id: str, workspace_id: str) -> bool:
|
||||
"""
|
||||
Check if the user can create a project in the specified workspace.
|
||||
"""
|
||||
try:
|
||||
client = _client_cache.get_client(account_id)
|
||||
|
||||
try:
|
||||
workspace = client.workspace.get(workspace_id)
|
||||
return workspace.permissions.can_create_project.authorized
|
||||
except Exception as e:
|
||||
print(f"Failed to get workspace: {str(e)}")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"Error in can_create_project_in_workspace: {str(e)}")
|
||||
_client_cache.clear() # Clear cache on error
|
||||
return False
|
||||
@@ -0,0 +1,575 @@
|
||||
"""
|
||||
Speckle authentication module for Blender connector.
|
||||
|
||||
Implements OAuth-style authentication flow with a local HTTP server,
|
||||
eliminating the dependency on the desktop service.
|
||||
"""
|
||||
|
||||
import errno
|
||||
import json
|
||||
import secrets
|
||||
import string
|
||||
import sys
|
||||
import threading
|
||||
import webbrowser
|
||||
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||
from typing import Optional, Dict, Any, Tuple
|
||||
from urllib.parse import urlparse, parse_qs
|
||||
from urllib.request import Request, urlopen
|
||||
from urllib.error import URLError, HTTPError
|
||||
|
||||
|
||||
# Speckle Blender dedicated app constants (registered server-side)
|
||||
SPECKLE_APP_ID = "sblndrdui" # Dedicated app ID for Blender connector
|
||||
SPECKLE_AUTH_PORT = 29365 # Port for local auth callback server
|
||||
|
||||
|
||||
def get_user_agent() -> str:
|
||||
"""Get User-Agent string identifying the Blender connector to prevent Cloudflare blocking."""
|
||||
try:
|
||||
from pathlib import Path
|
||||
|
||||
# Get the extension directory
|
||||
addon_dir = Path(__file__).parent.parent.parent
|
||||
|
||||
# Try to read version from blender_manifest.toml
|
||||
manifest_path = addon_dir / "blender_manifest.toml"
|
||||
if manifest_path.exists():
|
||||
with open(manifest_path, "r") as f:
|
||||
for line in f:
|
||||
if line.startswith("version = "):
|
||||
version = line.split("=")[1].strip().strip('"')
|
||||
break
|
||||
else:
|
||||
version = "3.0.0"
|
||||
else:
|
||||
version = "3.0.0"
|
||||
except Exception:
|
||||
# Fallback if we can't determine version
|
||||
version = "3.0.0"
|
||||
|
||||
python_version = f"{sys.version_info.major}.{sys.version_info.minor}"
|
||||
return f"Speckle-Blender-Connector/{version} (Python/{python_version})"
|
||||
|
||||
|
||||
class AuthenticationError(Exception):
|
||||
"""Raised when authentication fails."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
def generate_challenge() -> str:
|
||||
"""Generate a random 12-character alphanumeric challenge string."""
|
||||
chars = string.ascii_letters + string.digits
|
||||
return "".join(secrets.choice(chars) for _ in range(12))
|
||||
|
||||
|
||||
class ThreadSafeAuthServer(HTTPServer):
|
||||
"""Thread-safe HTTP server for Speckle authentication with locked state management."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self._lock = threading.Lock()
|
||||
self._server_url: Optional[str] = None
|
||||
self._challenge: Optional[str] = None
|
||||
self._auth_complete: bool = False
|
||||
self._auth_success: bool = False
|
||||
self._error_message: Optional[str] = None
|
||||
self._request_count: int = 0
|
||||
|
||||
@property
|
||||
def server_url(self) -> Optional[str]:
|
||||
with self._lock:
|
||||
return self._server_url
|
||||
|
||||
@server_url.setter
|
||||
def server_url(self, value: str) -> None:
|
||||
with self._lock:
|
||||
self._server_url = value
|
||||
|
||||
@property
|
||||
def challenge(self) -> Optional[str]:
|
||||
with self._lock:
|
||||
return self._challenge
|
||||
|
||||
@challenge.setter
|
||||
def challenge(self, value: str) -> None:
|
||||
with self._lock:
|
||||
self._challenge = value
|
||||
|
||||
@property
|
||||
def is_complete(self) -> bool:
|
||||
with self._lock:
|
||||
return self._auth_complete
|
||||
|
||||
@property
|
||||
def is_successful(self) -> bool:
|
||||
with self._lock:
|
||||
return self._auth_success
|
||||
|
||||
@property
|
||||
def error_message(self) -> Optional[str]:
|
||||
with self._lock:
|
||||
return self._error_message
|
||||
|
||||
def set_auth_success(self) -> None:
|
||||
"""Mark authentication as successful (sets auth_complete LAST for atomicity)."""
|
||||
with self._lock:
|
||||
self._auth_success = True
|
||||
self._error_message = None
|
||||
self._auth_complete = True # Set LAST to prevent partial reads
|
||||
|
||||
def set_auth_failure(self, error_message: str) -> None:
|
||||
"""Mark authentication as failed (sets auth_complete LAST for atomicity)."""
|
||||
with self._lock:
|
||||
self._auth_success = False
|
||||
self._error_message = error_message
|
||||
self._auth_complete = True # Set LAST to prevent partial reads
|
||||
|
||||
def increment_request_count(self) -> int:
|
||||
"""Increment and return request count."""
|
||||
with self._lock:
|
||||
self._request_count += 1
|
||||
return self._request_count
|
||||
|
||||
@property
|
||||
def request_count(self) -> int:
|
||||
with self._lock:
|
||||
return self._request_count
|
||||
|
||||
|
||||
class SpeckleAuthHandler(BaseHTTPRequestHandler):
|
||||
"""HTTP request handler for Speckle authentication flow with /auth/add-account and callback routes."""
|
||||
|
||||
def log_message(self, format, *args):
|
||||
print(f"[Auth Server] {format % args}")
|
||||
|
||||
def do_GET(self):
|
||||
self.server.increment_request_count()
|
||||
|
||||
parsed_path = urlparse(self.path)
|
||||
query_params = parse_qs(parsed_path.query)
|
||||
|
||||
if parsed_path.path == "/auth/add-account":
|
||||
self._handle_add_account(query_params)
|
||||
elif parsed_path.path == "/":
|
||||
self._handle_callback(query_params)
|
||||
else:
|
||||
self._send_error_response(404, "Not Found")
|
||||
|
||||
def _handle_add_account(self, query_params: Dict[str, list]):
|
||||
"""Handle initial add-account request, generate challenge and redirect to Speckle server."""
|
||||
# Get server URL from query params
|
||||
server_url = query_params.get("serverUrl", ["https://app.speckle.systems"])[0]
|
||||
self.server.server_url = server_url.rstrip("/")
|
||||
|
||||
# Generate challenge
|
||||
self.server.challenge = generate_challenge()
|
||||
|
||||
# Construct redirect URL
|
||||
auth_url = f"{self.server.server_url}/authn/verify/{SPECKLE_APP_ID}/{self.server.challenge}"
|
||||
|
||||
print(f"[Auth Server] Redirecting to: {auth_url}")
|
||||
|
||||
# Send redirect response
|
||||
self.send_response(302)
|
||||
self.send_header("Location", auth_url)
|
||||
self.end_headers()
|
||||
|
||||
def _handle_callback(self, query_params: Dict[str, list]):
|
||||
"""Handle callback from Speckle server, exchange access code for tokens and save account."""
|
||||
# Get access code from query params
|
||||
access_code_list = query_params.get("access_code", [])
|
||||
|
||||
if not access_code_list:
|
||||
self._redirect_to_failure("fail-no-access-code")
|
||||
return
|
||||
|
||||
access_code = access_code_list[0]
|
||||
|
||||
try:
|
||||
# Exchange access code for tokens
|
||||
tokens = exchange_access_code_for_tokens(
|
||||
access_code, self.server.challenge, self.server.server_url
|
||||
)
|
||||
|
||||
# Get user and server info
|
||||
user_info, server_info = get_user_and_server_info(
|
||||
tokens["token"], self.server.server_url
|
||||
)
|
||||
|
||||
# Save account
|
||||
save_account_to_storage(
|
||||
tokens["token"], tokens["refreshToken"], user_info, server_info
|
||||
)
|
||||
|
||||
# Mark as successful (sets auth_complete LAST atomically)
|
||||
self.server.set_auth_success()
|
||||
|
||||
# Redirect to success page
|
||||
self._redirect_to_success()
|
||||
|
||||
except Exception as e:
|
||||
print(f"[Auth Server] Error during authentication: {e}")
|
||||
# Mark as failed (sets auth_complete LAST atomically)
|
||||
self.server.set_auth_failure(str(e))
|
||||
self._redirect_to_failure("fail")
|
||||
|
||||
def _redirect_to_success(self):
|
||||
self.send_response(302)
|
||||
self.send_header(
|
||||
"Location", "https://www.speckle.systems/connector-auth/success"
|
||||
)
|
||||
self.end_headers()
|
||||
|
||||
def _redirect_to_failure(self, reason: str):
|
||||
self.send_response(302)
|
||||
self.send_header(
|
||||
"Location", f"https://www.speckle.systems/connector-auth/{reason}"
|
||||
)
|
||||
self.end_headers()
|
||||
|
||||
def _send_error_response(self, code: int, message: str):
|
||||
self.send_response(code)
|
||||
self.send_header("Content-type", "text/html")
|
||||
self.end_headers()
|
||||
self.wfile.write(
|
||||
f"<html><body><h1>{code} {message}</h1></body></html>".encode()
|
||||
)
|
||||
|
||||
|
||||
def _handle_auth_error(e: AuthenticationError) -> None:
|
||||
"""Re-raise AuthenticationError with user-friendly message for network errors."""
|
||||
error_str = str(e)
|
||||
if "Network error" in error_str or "URLError" in error_str:
|
||||
raise AuthenticationError(
|
||||
"Network error while authenticating. Please check your internet connection."
|
||||
) from e
|
||||
raise
|
||||
|
||||
|
||||
def _post_json(
|
||||
url: str,
|
||||
body: Dict[str, Any],
|
||||
auth_token: Optional[str] = None,
|
||||
error_context: str = "Request",
|
||||
) -> Dict[str, Any]:
|
||||
"""Make POST request with JSON body and optional Bearer token."""
|
||||
# Encode body as JSON
|
||||
data = json.dumps(body).encode("utf-8")
|
||||
|
||||
# Build headers
|
||||
headers = {"Content-Type": "application/json", "User-Agent": get_user_agent()}
|
||||
|
||||
# Add Authorization header if token provided
|
||||
if auth_token:
|
||||
headers["Authorization"] = f"Bearer {auth_token}"
|
||||
|
||||
try:
|
||||
request = Request(url, data=data, headers=headers)
|
||||
with urlopen(request, timeout=30) as response:
|
||||
response_data = json.loads(response.read().decode("utf-8"))
|
||||
return response_data
|
||||
|
||||
except HTTPError as e:
|
||||
error_body = e.read().decode("utf-8") if e.fp else "No error details"
|
||||
raise AuthenticationError(f"{error_context} failed: {e.code} {error_body}")
|
||||
except URLError as e:
|
||||
raise AuthenticationError(f"Network error during {error_context}: {e.reason}")
|
||||
except json.JSONDecodeError as e:
|
||||
raise AuthenticationError(f"Invalid JSON response from {error_context}: {e}")
|
||||
|
||||
|
||||
def exchange_access_code_for_tokens(
|
||||
access_code: str, challenge: str, server_url: str
|
||||
) -> Dict[str, str]:
|
||||
"""Exchange access code and challenge for authentication tokens."""
|
||||
if not challenge:
|
||||
raise AuthenticationError("No challenge available")
|
||||
|
||||
# Prepare request body
|
||||
body = {
|
||||
"appId": SPECKLE_APP_ID,
|
||||
"appSecret": SPECKLE_APP_ID,
|
||||
"accessCode": access_code,
|
||||
"challenge": challenge,
|
||||
}
|
||||
|
||||
# Make POST request
|
||||
url = f"{server_url}/auth/token"
|
||||
try:
|
||||
response_data = _post_json(url, body, error_context="token exchange")
|
||||
except AuthenticationError as e:
|
||||
_handle_auth_error(e)
|
||||
|
||||
# Validate response
|
||||
if "token" not in response_data or "refreshToken" not in response_data:
|
||||
raise AuthenticationError("Invalid response from token endpoint")
|
||||
|
||||
return {
|
||||
"token": response_data["token"],
|
||||
"refreshToken": response_data["refreshToken"],
|
||||
}
|
||||
|
||||
|
||||
def get_user_and_server_info(
|
||||
token: str, server_url: str
|
||||
) -> Tuple[Dict[str, Any], Dict[str, Any]]:
|
||||
"""Get user and server information using GraphQL query with auth token."""
|
||||
# Prepare GraphQL query
|
||||
query = """
|
||||
query {
|
||||
activeUser {
|
||||
id
|
||||
name
|
||||
email
|
||||
company
|
||||
avatar
|
||||
}
|
||||
serverInfo {
|
||||
name
|
||||
company
|
||||
adminContact
|
||||
description
|
||||
version
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
body = {"query": query}
|
||||
|
||||
# Make POST request
|
||||
url = f"{server_url}/graphql"
|
||||
try:
|
||||
response_data = _post_json(
|
||||
url, body, auth_token=token, error_context="user info request"
|
||||
)
|
||||
except AuthenticationError as e:
|
||||
_handle_auth_error(e)
|
||||
|
||||
# Validate response
|
||||
if "data" not in response_data:
|
||||
raise AuthenticationError("Invalid GraphQL response")
|
||||
|
||||
data = response_data["data"]
|
||||
|
||||
if "activeUser" not in data or "serverInfo" not in data:
|
||||
raise AuthenticationError("Missing user or server info in response")
|
||||
|
||||
user_info = data["activeUser"]
|
||||
server_info = data["serverInfo"]
|
||||
|
||||
# Ensure server URL is set correctly
|
||||
server_info["url"] = server_url.rstrip("/")
|
||||
|
||||
return user_info, server_info
|
||||
|
||||
|
||||
def save_account_to_storage(
|
||||
token: str,
|
||||
refresh_token: str,
|
||||
user_info: Dict[str, Any],
|
||||
server_info: Dict[str, Any],
|
||||
) -> None:
|
||||
"""Save account to Accounts.db SQLite database for compatibility with specklepy."""
|
||||
try:
|
||||
import sqlite3
|
||||
import hashlib
|
||||
import os
|
||||
from specklepy.core.api.credentials import speckle_path_provider
|
||||
|
||||
# Generate account ID (hash of email + server URL)
|
||||
account_id_string = f"{user_info['email']}-{server_info['url']}"
|
||||
account_id = hashlib.md5(account_id_string.encode()).hexdigest().upper()
|
||||
|
||||
# Construct account object matching the expected format
|
||||
account_data = {
|
||||
"id": account_id,
|
||||
"token": token,
|
||||
"refreshToken": refresh_token,
|
||||
"isDefault": True,
|
||||
"isOnline": True,
|
||||
"serverInfo": {
|
||||
"name": server_info["name"],
|
||||
"company": server_info.get("company"),
|
||||
"version": server_info.get("version"),
|
||||
"description": server_info.get("description"),
|
||||
"url": server_info["url"],
|
||||
},
|
||||
"userInfo": {
|
||||
"id": user_info["id"],
|
||||
"name": user_info["name"],
|
||||
"email": user_info["email"],
|
||||
"company": user_info.get("company"),
|
||||
"avatar": user_info.get("avatar"),
|
||||
},
|
||||
}
|
||||
|
||||
# Get database path
|
||||
speckle_folder = speckle_path_provider.user_speckle_folder_path()
|
||||
db_path = os.path.join(speckle_folder, "Accounts.db")
|
||||
|
||||
# Ensure the Speckle folder exists
|
||||
os.makedirs(speckle_folder, exist_ok=True)
|
||||
|
||||
# Connect to database and save account
|
||||
# Use IMMEDIATE isolation level to acquire write lock immediately,
|
||||
# preventing race conditions in concurrent account additions
|
||||
conn = sqlite3.connect(db_path, isolation_level="IMMEDIATE")
|
||||
try:
|
||||
with conn:
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Create table if it doesn't exist
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS objects (
|
||||
hash TEXT PRIMARY KEY,
|
||||
content TEXT
|
||||
)
|
||||
""")
|
||||
|
||||
# If setting as default, remove default flag from other accounts
|
||||
# Use batch update to make the operation more atomic
|
||||
if account_data["isDefault"]:
|
||||
cursor.execute("SELECT hash, content FROM objects")
|
||||
rows = cursor.fetchall()
|
||||
|
||||
# Build list of updates to execute in batch
|
||||
updates = []
|
||||
for existing_id, existing_content in rows:
|
||||
try:
|
||||
existing_account = json.loads(existing_content)
|
||||
if existing_account.get("isDefault", False):
|
||||
existing_account["isDefault"] = False
|
||||
updates.append(
|
||||
(json.dumps(existing_account), existing_id)
|
||||
)
|
||||
except json.JSONDecodeError:
|
||||
# Skip malformed accounts
|
||||
continue
|
||||
|
||||
# Execute all updates in batch for better atomicity
|
||||
if updates:
|
||||
cursor.executemany(
|
||||
"UPDATE objects SET content = ? WHERE hash = ?", updates
|
||||
)
|
||||
|
||||
# Insert or replace the account
|
||||
cursor.execute(
|
||||
"INSERT OR REPLACE INTO objects (hash, content) VALUES (?, ?)",
|
||||
(account_id, json.dumps(account_data)),
|
||||
)
|
||||
|
||||
conn.commit()
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
print(
|
||||
f"[Auth] Successfully saved account: {user_info['email']} @ {server_info['url']} (ID: {account_id})"
|
||||
)
|
||||
|
||||
# Track account creation event
|
||||
try:
|
||||
from specklepy.logging import metrics
|
||||
|
||||
metrics.track(metrics.HOST_APP, "connector", "account", {"action": "add"})
|
||||
except Exception as e:
|
||||
# Don't fail if metrics tracking fails
|
||||
print(f"[Auth] Failed to track metrics: {e}")
|
||||
|
||||
except Exception as e:
|
||||
raise AuthenticationError(f"Failed to save account: {e}")
|
||||
|
||||
|
||||
class AuthenticationServer:
|
||||
"""Manages local HTTP server for Speckle authentication in a background thread."""
|
||||
|
||||
def __init__(self, port: int = SPECKLE_AUTH_PORT):
|
||||
self.port = port
|
||||
self.server: Optional[ThreadSafeAuthServer] = None
|
||||
self.thread: Optional[threading.Thread] = None
|
||||
self.shutdown_event = threading.Event()
|
||||
|
||||
def start(self) -> bool:
|
||||
"""Start HTTP server in background thread."""
|
||||
try:
|
||||
# Create thread-safe server (state initialized in constructor)
|
||||
self.server = ThreadSafeAuthServer(
|
||||
("127.0.0.1", self.port), SpeckleAuthHandler
|
||||
)
|
||||
|
||||
# Start server in background thread
|
||||
self.thread = threading.Thread(target=self._run_server, daemon=True)
|
||||
self.thread.start()
|
||||
|
||||
print(f"[Auth Server] Started on http://127.0.0.1:{self.port}")
|
||||
return True
|
||||
|
||||
except OSError as e:
|
||||
if e.errno in (errno.EADDRINUSE, 10048): # Address already in use
|
||||
print(f"[Auth Server] Port {self.port} is already in use.")
|
||||
else:
|
||||
print(f"[Auth Server] Failed to start server: {e}")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"[Auth Server] Unexpected error starting server: {e}")
|
||||
return False
|
||||
|
||||
def _run_server(self):
|
||||
try:
|
||||
# Set a timeout so handle_request doesn't block forever
|
||||
self.server.timeout = 0.5
|
||||
|
||||
# Server should handle a maximum of 3 requests:
|
||||
# 1. /auth/add-account (redirect to Speckle)
|
||||
# 2. / callback (from Speckle with access_code)
|
||||
# 3. Maybe a favicon or other browser request
|
||||
# After that or when shutdown is signaled, stop
|
||||
max_requests = 5 # Allow a few extra for browser quirks
|
||||
|
||||
while (
|
||||
not self.shutdown_event.is_set()
|
||||
and self.server.request_count < max_requests
|
||||
):
|
||||
self.server.handle_request()
|
||||
|
||||
# If auth is complete, we can stop serving
|
||||
if self.server.is_complete:
|
||||
print("[Auth Server] Authentication complete, stopping server")
|
||||
break
|
||||
|
||||
except Exception as e:
|
||||
print(f"[Auth Server] Error in server thread: {e}")
|
||||
self.server.set_auth_failure(f"Server thread crashed: {e}")
|
||||
|
||||
def shutdown(self):
|
||||
if self.server:
|
||||
self.shutdown_event.set()
|
||||
try:
|
||||
# Give the server thread a moment to see the shutdown event
|
||||
if self.thread and self.thread.is_alive():
|
||||
self.thread.join(timeout=2.0)
|
||||
|
||||
self.server.server_close()
|
||||
except Exception as e:
|
||||
print(f"[Auth Server] Error during shutdown: {e}")
|
||||
|
||||
self.server = None
|
||||
self.thread = None
|
||||
print("[Auth Server] Shutdown complete")
|
||||
|
||||
def is_complete(self) -> bool:
|
||||
return self.server.is_complete if self.server else False
|
||||
|
||||
def is_successful(self) -> bool:
|
||||
return self.server.is_successful if self.server else False
|
||||
|
||||
def get_error_message(self) -> Optional[str]:
|
||||
return self.server.error_message if self.server else None
|
||||
|
||||
def open_auth_url(self, server_url: str = "https://app.speckle.systems"):
|
||||
"""Open authentication URL in browser to initiate auth flow."""
|
||||
# Trigger the add-account endpoint
|
||||
url = f"http://127.0.0.1:{self.port}/auth/add-account?serverUrl={server_url}"
|
||||
webbrowser.open(url)
|
||||
print("[Auth Server] Opening browser to initiate authentication...")
|
||||
@@ -0,0 +1,26 @@
|
||||
from typing import Iterator, TypeVar, Type
|
||||
from specklepy.objects.base import Base
|
||||
from specklepy.objects.graph_traversal.traversal import TraversalContext
|
||||
|
||||
|
||||
def get_ascendants(context: TraversalContext) -> Iterator[Base]:
|
||||
"""
|
||||
walks up the tree, returning all ascendants, including context
|
||||
"""
|
||||
head = context
|
||||
while head is not None:
|
||||
yield head.current
|
||||
head = head.parent
|
||||
|
||||
|
||||
T = TypeVar("T", bound=Base)
|
||||
|
||||
|
||||
def get_ascendant_of_type(context: TraversalContext, type_cls: Type[T]) -> Iterator[T]:
|
||||
"""
|
||||
walks up the tree, returning all ascendants of the given type,
|
||||
starting with the context, walking up parent nodes
|
||||
"""
|
||||
for ascendant in get_ascendants(context):
|
||||
if isinstance(ascendant, type_cls):
|
||||
yield ascendant
|
||||
@@ -0,0 +1,53 @@
|
||||
from datetime import datetime, timezone
|
||||
import re
|
||||
|
||||
|
||||
def format_relative_time(timestamp) -> str:
|
||||
"""
|
||||
convert UTC timestamp to local timezone and return relative time string
|
||||
"""
|
||||
if not timestamp:
|
||||
return "Unknown"
|
||||
|
||||
# convert to local timezone
|
||||
try:
|
||||
try:
|
||||
dt = datetime.fromisoformat(str(timestamp).replace("Z", "+00:00"))
|
||||
except ValueError:
|
||||
try:
|
||||
ts = float(timestamp)
|
||||
dt = datetime.fromtimestamp(ts / 1000, tz=timezone.utc)
|
||||
except (ValueError, TypeError):
|
||||
return "Invalid timestamp"
|
||||
|
||||
local_dt = dt.astimezone()
|
||||
|
||||
# calculate relative time
|
||||
now = datetime.now(timezone.utc).astimezone()
|
||||
delta = now - local_dt
|
||||
|
||||
if delta.days == 0:
|
||||
if delta.seconds < 3600:
|
||||
minutes = delta.seconds // 60
|
||||
return f"{minutes} minutes ago"
|
||||
else:
|
||||
hours = delta.seconds // 3600
|
||||
return f"{hours} hours ago"
|
||||
else:
|
||||
return f"{delta.days} days ago"
|
||||
except ValueError:
|
||||
return "Invalid timestamp"
|
||||
|
||||
|
||||
def format_role(role: str) -> str:
|
||||
"""
|
||||
This function takes a Speckle role string in the format "prefix:role" and
|
||||
returns just the role part
|
||||
"""
|
||||
split_role = role.split(":")
|
||||
return f"{split_role[1]}"
|
||||
|
||||
|
||||
def strip_non_ascii(text):
|
||||
# Keep English letters, digits, spaces and basic punctuation
|
||||
return re.sub(r"[^a-zA-Z0-9\s.,!?]", "", text)
|
||||
@@ -0,0 +1,428 @@
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
import bpy
|
||||
from bpy.types import Context
|
||||
|
||||
from ..utils.property_groups import speckle_model_card
|
||||
|
||||
|
||||
def find_layer_collection(layer_collection, collection_name):
|
||||
"""
|
||||
Recursively find a layer collection by collection name
|
||||
"""
|
||||
if layer_collection.collection.name == collection_name:
|
||||
return layer_collection
|
||||
for child in layer_collection.children:
|
||||
result = find_layer_collection(child, collection_name)
|
||||
if result:
|
||||
return result
|
||||
return None
|
||||
|
||||
|
||||
def get_object_by_application_id(app_id: str):
|
||||
"""
|
||||
Find a Blender object by its applicationId stored in custom property
|
||||
"""
|
||||
if not app_id:
|
||||
return None
|
||||
|
||||
for obj in bpy.data.objects:
|
||||
if "applicationId" in obj and obj["applicationId"] == app_id:
|
||||
return obj
|
||||
return None
|
||||
|
||||
|
||||
def get_objects_by_application_ids(app_ids: list):
|
||||
"""
|
||||
Find multiple Blender objects by their applicationIds
|
||||
"""
|
||||
if not app_ids:
|
||||
return {}
|
||||
|
||||
result = {}
|
||||
for obj in bpy.data.objects:
|
||||
if "applicationId" in obj and obj["applicationId"] in app_ids:
|
||||
result[obj["applicationId"]] = obj
|
||||
return result
|
||||
|
||||
|
||||
def get_collection_by_application_id(app_id: str):
|
||||
"""
|
||||
Find a Blender collection by its applicationId stored in custom property
|
||||
"""
|
||||
if not app_id:
|
||||
return None
|
||||
|
||||
for collection in bpy.data.collections:
|
||||
if "applicationId" in collection and collection["applicationId"] == app_id:
|
||||
return collection
|
||||
return None
|
||||
|
||||
|
||||
def get_collection_identifier(blender_col: bpy.types.Collection) -> str:
|
||||
"""
|
||||
Get collection identifier: applicationId if exists, fallback to name
|
||||
"""
|
||||
if "applicationId" in blender_col and blender_col["applicationId"]:
|
||||
return blender_col["applicationId"]
|
||||
return blender_col.name
|
||||
|
||||
|
||||
def find_collection_by_identifier(identifier: str):
|
||||
"""
|
||||
Find collection by identifier: try applicationId first, then name
|
||||
"""
|
||||
# first try to find by applicationId
|
||||
collection = get_collection_by_application_id(identifier)
|
||||
if collection:
|
||||
return collection
|
||||
|
||||
# fallback to name-based lookup
|
||||
return bpy.data.collections.get(identifier)
|
||||
|
||||
|
||||
def capture_modifier_data(blender_obj: bpy.types.Object) -> list:
|
||||
"""
|
||||
Capture modifier data from a Blender object as dictionaries
|
||||
"""
|
||||
modifiers_data = []
|
||||
for modifier in blender_obj.modifiers:
|
||||
modifier_data = {
|
||||
"name": modifier.name,
|
||||
"type": modifier.type,
|
||||
"show_viewport": modifier.show_viewport,
|
||||
"show_render": modifier.show_render,
|
||||
"show_in_editmode": modifier.show_in_editmode,
|
||||
"show_on_cage": modifier.show_on_cage,
|
||||
"properties": {},
|
||||
}
|
||||
|
||||
# Capture modifier-specific properties
|
||||
for prop_name in modifier.bl_rna.properties.keys():
|
||||
if prop_name in [
|
||||
"rna_type",
|
||||
"name",
|
||||
"type",
|
||||
"show_viewport",
|
||||
"show_render",
|
||||
"show_in_editmode",
|
||||
"show_on_cage",
|
||||
]:
|
||||
continue
|
||||
try:
|
||||
if hasattr(modifier, prop_name):
|
||||
prop_value = getattr(modifier, prop_name)
|
||||
# Handle different property types
|
||||
if isinstance(prop_value, (int, float, bool, str)):
|
||||
modifier_data["properties"][prop_name] = prop_value
|
||||
elif hasattr(prop_value, "name"): # Object references
|
||||
modifier_data["properties"][prop_name] = prop_value.name
|
||||
elif (
|
||||
hasattr(prop_value, "__len__") and len(prop_value) <= 4
|
||||
): # Vectors/colors
|
||||
modifier_data["properties"][prop_name] = list(prop_value)
|
||||
except (AttributeError, TypeError):
|
||||
continue
|
||||
|
||||
modifiers_data.append(modifier_data)
|
||||
|
||||
return modifiers_data
|
||||
|
||||
|
||||
def has_visibility_modifications(obj: bpy.types.Object) -> bool:
|
||||
"""Check if object has non-default visibility settings"""
|
||||
return obj.hide_viewport or obj.hide_select or obj.hide_render or obj.hide_get()
|
||||
|
||||
|
||||
def has_modifier_modifications(obj: bpy.types.Object) -> bool:
|
||||
"""Check if object has any modifiers applied"""
|
||||
return hasattr(obj, "modifiers") and len(obj.modifiers) > 0
|
||||
|
||||
|
||||
def has_collection_visibility_modifications(layer_col, collection) -> bool:
|
||||
"""Check if collection has non-default visibility settings"""
|
||||
return (
|
||||
layer_col.hide_viewport
|
||||
or collection.hide_select
|
||||
or collection.hide_render
|
||||
or layer_col.exclude
|
||||
)
|
||||
|
||||
|
||||
def collect_objects_with_properties(
|
||||
model_card: speckle_model_card,
|
||||
) -> Dict[str, Dict[str, Any]]:
|
||||
"""
|
||||
Collect objects and collections with their current properties before deletion
|
||||
Only stores data for objects that have been modified from defaults
|
||||
"""
|
||||
collected_data = {"objects": {}, "collections": {}}
|
||||
|
||||
# Collect object properties (only for modified objects)
|
||||
for s_obj in model_card.objects:
|
||||
blender_obj = get_object_by_application_id(s_obj.applicationId)
|
||||
if blender_obj:
|
||||
obj_data = {}
|
||||
|
||||
# Only collect visibility if modified from defaults
|
||||
if has_visibility_modifications(blender_obj):
|
||||
obj_data["visibility"] = {
|
||||
"hide_get": blender_obj.hide_get(),
|
||||
"hide_viewport": blender_obj.hide_viewport,
|
||||
"hide_select": blender_obj.hide_select,
|
||||
"hide_render": blender_obj.hide_render,
|
||||
}
|
||||
|
||||
# Only collect modifiers if object has any
|
||||
if has_modifier_modifications(blender_obj):
|
||||
obj_data["modifiers"] = capture_modifier_data(blender_obj)
|
||||
|
||||
# Only store object data if it has modifications
|
||||
if obj_data:
|
||||
collected_data["objects"][s_obj.applicationId] = obj_data
|
||||
|
||||
# Collect collection properties (only for modified collections)
|
||||
for s_col in model_card.collections:
|
||||
# try to find collection by applicationId first, then fallback to name
|
||||
blender_col = None
|
||||
if s_col.applicationId:
|
||||
blender_col = get_collection_by_application_id(s_col.applicationId)
|
||||
if not blender_col:
|
||||
blender_col = bpy.data.collections.get(s_col.name)
|
||||
|
||||
if blender_col:
|
||||
view_layer = bpy.context.view_layer
|
||||
if view_layer:
|
||||
layer_col = find_layer_collection(
|
||||
view_layer.layer_collection, blender_col.name
|
||||
)
|
||||
if layer_col and has_collection_visibility_modifications(
|
||||
layer_col, blender_col
|
||||
):
|
||||
# use collection identifier as key
|
||||
collection_id = get_collection_identifier(blender_col)
|
||||
collected_data["collections"][collection_id] = {
|
||||
"hide_viewport": layer_col.hide_viewport,
|
||||
"hide_select": layer_col.collection.hide_select,
|
||||
"hide_render": layer_col.collection.hide_render,
|
||||
"exclude_from_view_layer": layer_col.exclude,
|
||||
}
|
||||
|
||||
return collected_data
|
||||
|
||||
|
||||
def transfer_object_properties(
|
||||
new_obj: bpy.types.Object, old_obj_data: Dict[str, Any]
|
||||
) -> None:
|
||||
"""
|
||||
Transfer visibility and modifiers from old object data to new object
|
||||
Handles sparse data gracefully - applies defaults when data is missing
|
||||
"""
|
||||
# Transfer visibility settings (if any were modified)
|
||||
visibility = old_obj_data.get("visibility")
|
||||
if visibility:
|
||||
new_obj.hide_set(visibility.get("hide_get", False))
|
||||
new_obj.hide_viewport = visibility.get("hide_viewport", False)
|
||||
new_obj.hide_select = visibility.get("hide_select", False)
|
||||
new_obj.hide_render = visibility.get("hide_render", False)
|
||||
# If no visibility data, object keeps defaults (all False)
|
||||
|
||||
# Transfer modifiers (if any were present)
|
||||
old_modifiers = old_obj_data.get("modifiers")
|
||||
if old_modifiers and hasattr(new_obj, "modifiers"):
|
||||
# Clear existing modifiers
|
||||
new_obj.modifiers.clear()
|
||||
|
||||
# Transfer each modifier
|
||||
for modifier_data in old_modifiers:
|
||||
recreate_modifier_from_data(new_obj, modifier_data)
|
||||
# If no modifier data, object keeps default (no modifiers)
|
||||
|
||||
|
||||
def transfer_collection_properties(
|
||||
new_col: bpy.types.Collection, old_col_data: Dict[str, Any]
|
||||
) -> None:
|
||||
"""
|
||||
Transfer visibility properties from old collection data to new collection
|
||||
Handles sparse data gracefully - applies defaults when data is missing
|
||||
"""
|
||||
view_layer = bpy.context.view_layer
|
||||
if view_layer:
|
||||
layer_col = find_layer_collection(view_layer.layer_collection, new_col.name)
|
||||
if layer_col:
|
||||
# Only apply properties if collection had modifications
|
||||
# (otherwise it keeps defaults: all False)
|
||||
layer_col.hide_viewport = old_col_data.get("hide_viewport", False)
|
||||
layer_col.collection.hide_select = old_col_data.get("hide_select", False)
|
||||
layer_col.collection.hide_render = old_col_data.get("hide_render", False)
|
||||
layer_col.exclude = old_col_data.get("exclude_from_view_layer", False)
|
||||
|
||||
|
||||
def recreate_modifier_from_data(
|
||||
new_obj: bpy.types.Object, modifier_data: Dict[str, Any]
|
||||
) -> Optional[bpy.types.Modifier]:
|
||||
"""
|
||||
Recreate a modifier from captured data
|
||||
"""
|
||||
try:
|
||||
# Validate modifier data
|
||||
if not modifier_data.get("type") or not modifier_data.get("name"):
|
||||
print(f"Invalid modifier data: {modifier_data}")
|
||||
return None
|
||||
|
||||
# Create new modifier
|
||||
new_modifier = new_obj.modifiers.new(
|
||||
modifier_data["name"], modifier_data["type"]
|
||||
)
|
||||
|
||||
# Set visibility properties
|
||||
new_modifier.show_viewport = modifier_data.get("show_viewport", True)
|
||||
new_modifier.show_render = modifier_data.get("show_render", True)
|
||||
new_modifier.show_in_editmode = modifier_data.get("show_in_editmode", True)
|
||||
new_modifier.show_on_cage = modifier_data.get("show_on_cage", False)
|
||||
|
||||
# Set modifier-specific properties
|
||||
for prop_name, prop_value in modifier_data.get("properties", {}).items():
|
||||
try:
|
||||
if hasattr(new_modifier, prop_name):
|
||||
current_value = getattr(new_modifier, prop_name)
|
||||
# Handle object references
|
||||
if hasattr(current_value, "name") and isinstance(prop_value, str):
|
||||
referenced_obj = bpy.data.objects.get(prop_value)
|
||||
if referenced_obj:
|
||||
setattr(new_modifier, prop_name, referenced_obj)
|
||||
else:
|
||||
setattr(new_modifier, prop_name, prop_value)
|
||||
except (AttributeError, TypeError):
|
||||
continue
|
||||
|
||||
return new_modifier
|
||||
except Exception as e:
|
||||
print(f"Error recreating modifier {modifier_data.get('name', 'unknown')}: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def update_model_card_objects(
|
||||
model_card: speckle_model_card,
|
||||
converted_objects: Dict[str, bpy.types.Object | bpy.types.Collection],
|
||||
old_properties: Optional[Dict[str, Dict[str, Any]]] = None,
|
||||
):
|
||||
"""
|
||||
Update model card with new objects and apply properties from old objects if provided
|
||||
"""
|
||||
# Clear model card objects
|
||||
model_card.objects.clear()
|
||||
model_card.collections.clear()
|
||||
|
||||
# Convert list to dictionary if needed
|
||||
if isinstance(converted_objects, list):
|
||||
converted_objects = {obj.name: obj for obj in converted_objects}
|
||||
|
||||
# Using a set keeps lookup O(1)
|
||||
object_names = set()
|
||||
collection_names = set()
|
||||
|
||||
for obj in converted_objects.values():
|
||||
# Handle collections
|
||||
if isinstance(obj, bpy.types.Collection):
|
||||
if obj.name in collection_names:
|
||||
continue
|
||||
collection_names.add(obj.name)
|
||||
|
||||
s_col = model_card.collections.add()
|
||||
s_col.name = obj.name
|
||||
s_col.applicationId = obj.get("applicationId", "")
|
||||
|
||||
# apply old collection properties if available (use identifier-based lookup)
|
||||
if old_properties:
|
||||
collection_id = get_collection_identifier(obj)
|
||||
if collection_id in old_properties.get("collections", {}):
|
||||
old_col_data = old_properties["collections"][collection_id]
|
||||
transfer_collection_properties(obj, old_col_data)
|
||||
|
||||
# Handle objects
|
||||
elif isinstance(obj, bpy.types.Object):
|
||||
if obj.name in object_names:
|
||||
continue
|
||||
object_names.add(obj.name)
|
||||
|
||||
s_obj = model_card.objects.add()
|
||||
s_obj.name = obj.name
|
||||
s_obj.applicationId = obj.get("applicationId", "")
|
||||
|
||||
# Apply old object properties if available
|
||||
if (
|
||||
old_properties
|
||||
and s_obj.applicationId
|
||||
and s_obj.applicationId in old_properties.get("objects", {})
|
||||
):
|
||||
old_obj_data = old_properties["objects"][s_obj.applicationId]
|
||||
transfer_object_properties(obj, old_obj_data)
|
||||
|
||||
|
||||
def delete_model_card_objects(model_card: speckle_model_card, context: Context) -> None:
|
||||
"""
|
||||
deletes the model card objects
|
||||
"""
|
||||
# Delete objects directly without requiring selection
|
||||
for obj in model_card.objects:
|
||||
blender_obj = get_object_by_application_id(obj.applicationId)
|
||||
if not blender_obj:
|
||||
continue
|
||||
|
||||
# Remove object from all collections first
|
||||
for collection in blender_obj.users_collection:
|
||||
collection.objects.unlink(blender_obj)
|
||||
|
||||
# Delete the object directly
|
||||
bpy.data.objects.remove(blender_obj)
|
||||
|
||||
# delete model card/currently loaded collections
|
||||
for col in model_card.collections:
|
||||
coll = bpy.data.collections.get(col.name)
|
||||
if not coll:
|
||||
continue
|
||||
# unlink from scenes
|
||||
for scene in bpy.data.scenes:
|
||||
if scene.collection.children.get(coll.name):
|
||||
scene.collection.children.unlink(coll)
|
||||
bpy.data.collections.remove(coll)
|
||||
|
||||
|
||||
def select_model_card_objects(model_card, context: Context):
|
||||
# deselect all objects first
|
||||
bpy.ops.object.select_all(action="DESELECT")
|
||||
# select objects in model card
|
||||
for obj in model_card.objects:
|
||||
blender_obj = get_object_by_application_id(obj.applicationId)
|
||||
if not blender_obj:
|
||||
continue
|
||||
if blender_obj.name in context.view_layer.objects:
|
||||
blender_obj.select_set(True)
|
||||
|
||||
selected = context.selected_objects
|
||||
if selected:
|
||||
context.view_layer.objects.active = selected[0]
|
||||
|
||||
|
||||
def zoom_to_selected_objects(context: Context):
|
||||
"""
|
||||
zooms to the selected objects
|
||||
"""
|
||||
bpy.ops.view3d.view_selected()
|
||||
|
||||
|
||||
def model_card_exists(
|
||||
project_id: str, model_id: str, is_publish: bool, context: Context
|
||||
) -> bool:
|
||||
"""
|
||||
checks if a model card exists
|
||||
"""
|
||||
for model_card in context.scene.speckle_state.model_cards:
|
||||
if (
|
||||
model_card.project_id == project_id
|
||||
and model_card.model_id == model_id
|
||||
and model_card.is_publish == is_publish
|
||||
):
|
||||
return True
|
||||
return False
|
||||
@@ -0,0 +1,52 @@
|
||||
from specklepy.core.api.inputs.project_inputs import ProjectModelsFilter
|
||||
from specklepy.core.api.models.current import Model
|
||||
from typing import List, Tuple, Optional
|
||||
from .misc import format_relative_time, strip_non_ascii
|
||||
from .account_manager import _client_cache
|
||||
|
||||
|
||||
def get_models_for_project(
|
||||
account_id: str, project_id: str, search: Optional[str] = None
|
||||
) -> List[Tuple[str, str, str]]:
|
||||
"""
|
||||
fetches models for a given project from the Speckle server
|
||||
"""
|
||||
try:
|
||||
if not account_id or not project_id:
|
||||
print(
|
||||
f"Error: Invalid inputs - account_id: {account_id}, project_id: {project_id}"
|
||||
)
|
||||
return []
|
||||
|
||||
# Get cached client
|
||||
client = _client_cache.get_client(account_id)
|
||||
if not client:
|
||||
print(f"Error: Could not get client for account: {account_id}")
|
||||
return []
|
||||
|
||||
try:
|
||||
client.project.get(project_id)
|
||||
except Exception as e:
|
||||
print(f"Error: Project with ID {project_id} not found: {str(e)}")
|
||||
return []
|
||||
|
||||
filter = ProjectModelsFilter(search=search) if search else None
|
||||
|
||||
models: List[Model] = client.model.get_models(
|
||||
project_id=project_id, models_limit=10, models_filter=filter
|
||||
).items
|
||||
|
||||
return [
|
||||
(
|
||||
strip_non_ascii(model.name),
|
||||
model.id,
|
||||
format_relative_time(model.updated_at),
|
||||
)
|
||||
for model in models
|
||||
]
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error fetching models: {str(e)}")
|
||||
# Clear cache on error to prevent stale clients
|
||||
_client_cache.clear()
|
||||
return []
|
||||
@@ -0,0 +1,131 @@
|
||||
from specklepy.core.api.client import SpeckleClient
|
||||
from specklepy.core.api.resources.current.workspace_resource import WorkspaceResource
|
||||
from specklepy.core.api.inputs.project_inputs import WorksaceProjectsFilter
|
||||
from typing import List, Tuple, Optional
|
||||
from specklepy.core.api.credentials import Account
|
||||
from .misc import format_relative_time, format_role, strip_non_ascii
|
||||
from .account_manager import _client_cache
|
||||
|
||||
|
||||
def get_projects_for_account(
|
||||
account_id: str, workspace_id: str = None, search: Optional[str] = None
|
||||
) -> List[Tuple[str, str, str, str, bool]]:
|
||||
"""
|
||||
fetches projects for a given account from the Speckle server
|
||||
"""
|
||||
try:
|
||||
# Get cached client
|
||||
client = _client_cache.get_client(account_id)
|
||||
if not client:
|
||||
print(f"Error: Could not get client for account: {account_id}")
|
||||
return []
|
||||
|
||||
# Get account for workspace operations that still need it
|
||||
from specklepy.core.api.credentials import get_local_accounts
|
||||
|
||||
account: Optional[Account] = next(
|
||||
(acc for acc in get_local_accounts() if acc.id == account_id), None
|
||||
)
|
||||
if not account:
|
||||
print(f"Error: Could not find account with ID: {account_id}")
|
||||
return []
|
||||
|
||||
try:
|
||||
workspace_resource = WorkspaceResource(
|
||||
account, client.url, client.httpclient, client.server.version()
|
||||
)
|
||||
|
||||
# create filter with search parameter
|
||||
filter = (
|
||||
WorksaceProjectsFilter(search=search, with_project_role_only=False)
|
||||
if search
|
||||
else None
|
||||
)
|
||||
|
||||
projects_with_permissions = (
|
||||
workspace_resource.get_projects_with_permissions(
|
||||
workspace_id=workspace_id, limit=10, filter=filter
|
||||
)
|
||||
)
|
||||
|
||||
result = []
|
||||
for project in projects_with_permissions.items:
|
||||
can_load_permission = False
|
||||
|
||||
if hasattr(project, "permissions") and project.permissions:
|
||||
can_load_permission = (
|
||||
hasattr(project.permissions, "can_load")
|
||||
and project.permissions.can_load
|
||||
and project.permissions.can_load.authorized
|
||||
)
|
||||
|
||||
result.append(
|
||||
(
|
||||
strip_non_ascii(project.name),
|
||||
format_role(getattr(project, "role", ""))
|
||||
if hasattr(project, "role") and project.role
|
||||
else "",
|
||||
format_relative_time(project.updated_at),
|
||||
project.id,
|
||||
can_load_permission,
|
||||
)
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
except Exception as workspace_error:
|
||||
print(
|
||||
f"WorkspaceResource failed, falling back to old method: {workspace_error}"
|
||||
)
|
||||
return _get_projects_with_individual_permissions(
|
||||
client, workspace_id, search
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
import traceback
|
||||
|
||||
error_msg = f"Error: {str(e)}\n"
|
||||
error_msg += f"Traceback:\n{''.join(traceback.format_tb(e.__traceback__))}"
|
||||
print(error_msg)
|
||||
# Clear cache on error to prevent stale clients
|
||||
_client_cache.clear()
|
||||
return []
|
||||
|
||||
|
||||
def _get_projects_with_individual_permissions(
|
||||
client: SpeckleClient,
|
||||
workspace_id: str,
|
||||
search: Optional[str] = None,
|
||||
) -> List[Tuple[str, str, str, str, bool]]:
|
||||
"""
|
||||
Fallback helper function to get projects with permissions using individual API calls
|
||||
"""
|
||||
from specklepy.core.api.inputs.user_inputs import UserProjectsFilter
|
||||
from .account_manager import can_load
|
||||
|
||||
filter = UserProjectsFilter(
|
||||
search=search,
|
||||
workspaceId=workspace_id,
|
||||
personalOnly=False,
|
||||
include_implicit_access=True,
|
||||
)
|
||||
|
||||
projects = client.active_user.get_projects(limit=10, filter=filter).items
|
||||
|
||||
result = []
|
||||
for project in projects:
|
||||
can_load_permission, _ = can_load(client, project)
|
||||
|
||||
result.append(
|
||||
(
|
||||
strip_non_ascii(project.name),
|
||||
format_role(getattr(project, "role", ""))
|
||||
if hasattr(project, "role") and project.role
|
||||
else "",
|
||||
format_relative_time(project.updated_at),
|
||||
project.id,
|
||||
can_load_permission,
|
||||
)
|
||||
)
|
||||
|
||||
return result
|
||||
@@ -0,0 +1,115 @@
|
||||
import bpy
|
||||
|
||||
|
||||
class speckle_project(bpy.types.PropertyGroup):
|
||||
"""
|
||||
PropertyGroup for storing project information
|
||||
"""
|
||||
|
||||
name: bpy.props.StringProperty() # type: ignore
|
||||
role: bpy.props.StringProperty(name="Role") # type: ignore
|
||||
updated: bpy.props.StringProperty(name="Updated") # type: ignore
|
||||
id: bpy.props.StringProperty(name="ID") # type: ignore
|
||||
can_receive: bpy.props.BoolProperty(name="Can Receive", default=False) # type: ignore
|
||||
|
||||
|
||||
class speckle_model(bpy.types.PropertyGroup):
|
||||
"""
|
||||
PropertyGroup for storing model information
|
||||
"""
|
||||
|
||||
name: bpy.props.StringProperty() # type: ignore
|
||||
id: bpy.props.StringProperty(name="ID") # type: ignore
|
||||
updated: bpy.props.StringProperty(name="Updated") # type: ignore
|
||||
|
||||
|
||||
class speckle_version(bpy.types.PropertyGroup):
|
||||
"""
|
||||
PropertyGroup for storing version information
|
||||
"""
|
||||
|
||||
id: bpy.props.StringProperty(name="ID") # type: ignore
|
||||
message: bpy.props.StringProperty(name="Message") # type: ignore
|
||||
updated: bpy.props.StringProperty(name="Updated") # type: ignore
|
||||
source_app: bpy.props.StringProperty(name="Source") # type: ignore
|
||||
|
||||
|
||||
class speckle_object(bpy.types.PropertyGroup):
|
||||
"""
|
||||
PropertyGroup for storing object names and applicationIds
|
||||
"""
|
||||
|
||||
name: bpy.props.StringProperty() # type: ignore
|
||||
applicationId: bpy.props.StringProperty(name="Application ID", default="") # type: ignore
|
||||
|
||||
|
||||
class speckle_collection(bpy.types.PropertyGroup):
|
||||
"""
|
||||
PropertyGroup for storing collections
|
||||
"""
|
||||
|
||||
name: bpy.props.StringProperty() # type: ignore
|
||||
applicationId: bpy.props.StringProperty(name="Application ID", default="") # type: ignore
|
||||
|
||||
|
||||
class speckle_model_card(bpy.types.PropertyGroup):
|
||||
"""
|
||||
represents a Speckle model card in the Blender UI
|
||||
"""
|
||||
|
||||
account_id: bpy.props.StringProperty(
|
||||
name="Account ID", description="ID of the account", default=""
|
||||
) # type: ignore
|
||||
server_url: bpy.props.StringProperty(
|
||||
name="Server URL",
|
||||
description="URL of the Server",
|
||||
default="app.speckle.systems",
|
||||
) # type: ignore
|
||||
project_name: bpy.props.StringProperty(
|
||||
name="Project Name", description="Name of the project", default=""
|
||||
) # type: ignore
|
||||
project_id: bpy.props.StringProperty(
|
||||
name="Project ID", description="ID of the selected project", default=""
|
||||
) # type: ignore
|
||||
model_id: bpy.props.StringProperty(
|
||||
name="Model ID", description="ID of the model", default=""
|
||||
) # type: ignore
|
||||
model_name: bpy.props.StringProperty(
|
||||
name="Model Name", description="Name of the model", default=""
|
||||
) # type: ignore
|
||||
is_publish: bpy.props.BoolProperty(
|
||||
name="Publish/Load",
|
||||
description="If the model is published or loaded",
|
||||
default=False,
|
||||
) # type: ignore
|
||||
selection_summary: bpy.props.StringProperty(
|
||||
name="Selection Summary", description="Summary of the selection", default=""
|
||||
) # type: ignore
|
||||
version_id: bpy.props.StringProperty(
|
||||
name="Version ID", description="ID of the selected version", default=""
|
||||
) # type: ignore
|
||||
load_option: bpy.props.StringProperty(
|
||||
name="Load Option", description="Option of loading the model", default=""
|
||||
) # type: ignore
|
||||
objects: bpy.props.CollectionProperty(type=speckle_object) # type: ignore
|
||||
collections: bpy.props.CollectionProperty(type=speckle_collection) # type: ignore
|
||||
instance_loading_mode: bpy.props.StringProperty(
|
||||
name="Instance Loading Mode",
|
||||
description="Mode of loading instances",
|
||||
default="INSTANCE_PROXIES",
|
||||
) # type: ignore
|
||||
apply_modifiers: bpy.props.BoolProperty(
|
||||
name="Apply Modifiers",
|
||||
description="Apply modifiers to the objects",
|
||||
default=True,
|
||||
) # type: ignore
|
||||
|
||||
def get_model_card_id(self) -> str:
|
||||
if not self.project_id or not self.model_id:
|
||||
raise ValueError(
|
||||
"Project ID and Model ID are required to generate a model card ID."
|
||||
)
|
||||
if self.is_publish:
|
||||
return f"PUBLISH-{self.project_id}-{self.model_id}"
|
||||
else:
|
||||
return f"LOAD-{self.project_id}-{self.model_id}"
|
||||
@@ -0,0 +1,93 @@
|
||||
from specklepy.core.api.client import SpeckleClient
|
||||
from .account_manager import _client_cache
|
||||
from typing import List, Tuple
|
||||
from .misc import format_relative_time
|
||||
from specklepy.core.api.inputs.model_inputs import ModelVersionsFilter
|
||||
from specklepy.core.api.models.current import Version
|
||||
|
||||
|
||||
def get_versions_for_model(
|
||||
account_id: str, project_id: str, model_id: str
|
||||
) -> List[Tuple[str, str, str]]:
|
||||
"""
|
||||
fetches versions for a given model from the Speckle server
|
||||
"""
|
||||
try:
|
||||
# Validate inputs
|
||||
if not account_id or not project_id or not model_id:
|
||||
print(
|
||||
f"Error: Invalid inputs - account_id: {account_id}, project_id: {project_id}, model_id: {model_id}"
|
||||
)
|
||||
return []
|
||||
|
||||
# Get cached client
|
||||
client: SpeckleClient = _client_cache.get_client(account_id)
|
||||
if not client:
|
||||
print(f"Error: Could not get client for account: {account_id}")
|
||||
return []
|
||||
|
||||
filter: ModelVersionsFilter = ModelVersionsFilter(priorityIds=[])
|
||||
|
||||
# Get versions
|
||||
versions: List[Version] = client.version.get_versions(
|
||||
project_id=project_id, model_id=model_id, limit=10, filter=filter
|
||||
)
|
||||
versions_list: List[Tuple[str, str, str]] = []
|
||||
for version in versions.items:
|
||||
if version.referenced_object != "":
|
||||
versions_list.append(
|
||||
(
|
||||
version.id,
|
||||
version.message
|
||||
if version.message is not None
|
||||
else "No message",
|
||||
format_relative_time(version.created_at),
|
||||
)
|
||||
)
|
||||
return versions_list
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error fetching versions: {str(e)}")
|
||||
# Clear cache on error to prevent stale clients
|
||||
_client_cache.clear()
|
||||
return []
|
||||
|
||||
|
||||
def get_latest_version(
|
||||
account_id: str, project_id: str, model_id: str
|
||||
) -> Tuple[str, str, str]:
|
||||
try:
|
||||
# Validate inputs
|
||||
if not account_id or not project_id or not model_id:
|
||||
print(
|
||||
f"Error: Invalid inputs - account_id: {account_id}, project_id: {project_id}, model_id: {model_id}"
|
||||
)
|
||||
return ("", "", "")
|
||||
|
||||
# Get cached client
|
||||
client: SpeckleClient = _client_cache.get_client(account_id)
|
||||
if not client:
|
||||
print(f"Error: Could not get client for account: {account_id}")
|
||||
return ("", "", "")
|
||||
|
||||
# Get versions (limit to 1 since we only need the latest)
|
||||
versions: List[Version] = client.version.get_versions(
|
||||
project_id=project_id, model_id=model_id, limit=1
|
||||
).items
|
||||
|
||||
if not versions:
|
||||
print(f"Error: No versions found for model_id: {model_id}")
|
||||
return ("", "", "")
|
||||
|
||||
latest = versions[0]
|
||||
return (
|
||||
latest.id,
|
||||
latest.message if latest.message is not None else "No message",
|
||||
format_relative_time(latest.created_at),
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error fetching latest version: {str(e)}")
|
||||
# Clear cache on error to prevent stale clients
|
||||
_client_cache.clear()
|
||||
return ("", "", "")
|
||||
@@ -1,313 +0,0 @@
|
||||
import bpy, idprop
|
||||
from mathutils import Matrix
|
||||
|
||||
from .from_speckle import *
|
||||
from .to_speckle import *
|
||||
from .util import *
|
||||
from bpy_speckle.functions import _report, get_scale_length
|
||||
|
||||
from specklepy.objects.geometry import *
|
||||
from specklepy.objects.other import RenderMaterial
|
||||
|
||||
|
||||
FROM_SPECKLE_SCHEMAS = {
|
||||
Mesh: import_mesh,
|
||||
Brep: import_brep,
|
||||
Curve: import_curve,
|
||||
Line: import_curve,
|
||||
Polyline: import_curve,
|
||||
Polycurve: import_curve,
|
||||
Arc: import_curve,
|
||||
}
|
||||
|
||||
|
||||
TO_SPECKLE = {
|
||||
"MESH": export_mesh,
|
||||
"CURVE": export_curve,
|
||||
"EMPTY": export_empty,
|
||||
}
|
||||
|
||||
|
||||
def set_transform(speckle_object, blender_object):
|
||||
transform = None
|
||||
if hasattr(speckle_object, "transform"):
|
||||
transform = speckle_object.transform
|
||||
elif (
|
||||
hasattr(speckle_object, "properties") and speckle_object.properties is not None
|
||||
):
|
||||
transform = speckle_object.properties.get("transform", None)
|
||||
|
||||
if transform and len(transform) == 16:
|
||||
mat = Matrix(
|
||||
[transform[0:4], transform[4:8], transform[8:12], transform[12:16]]
|
||||
)
|
||||
blender_object.matrix_world = mat
|
||||
|
||||
|
||||
def add_blender_material(smesh, blender_object) -> None:
|
||||
"""Add material to a blender object if the corresponding speckle object has a render material"""
|
||||
if blender_object.data is None:
|
||||
return
|
||||
|
||||
if not hasattr(smesh, "renderMaterial") and not hasattr(smesh, "@renderMaterial"):
|
||||
return
|
||||
|
||||
speckle_mat = getattr(smesh, "renderMaterial", None) or smesh["@renderMaterial"]
|
||||
mat_name = getattr(speckle_mat, "name", None) or speckle_mat.__dict__.get("@name")
|
||||
if not mat_name:
|
||||
mat_name = speckle_mat.applicationId or speckle_mat.id or speckle_mat.get_id()
|
||||
blender_mat = bpy.data.materials.get(mat_name)
|
||||
if not blender_mat:
|
||||
blender_mat = bpy.data.materials.new(mat_name)
|
||||
|
||||
# for now, we're not updating these materials. as per tom's suggestion, we should have a toggle
|
||||
# that enables this as the blender mats will prob be much more complex than whatever is coming in
|
||||
blender_mat.use_nodes = True
|
||||
inputs = blender_mat.node_tree.nodes["Principled BSDF"].inputs
|
||||
|
||||
inputs["Base Color"].default_value = to_rgba(speckle_mat.diffuse)
|
||||
inputs["Emission"].default_value = to_rgba(speckle_mat.emissive)
|
||||
inputs["Roughness"].default_value = speckle_mat.roughness
|
||||
inputs["Metallic"].default_value = speckle_mat.metalness
|
||||
inputs["Alpha"].default_value = speckle_mat.opacity
|
||||
|
||||
if speckle_mat.opacity < 1:
|
||||
blender_mat.blend_method = "BLEND"
|
||||
|
||||
blender_object.data.materials.append(blender_mat)
|
||||
|
||||
|
||||
def material_to_speckle(blender_object) -> RenderMaterial:
|
||||
"""Create and return a render material from a blender object"""
|
||||
if not getattr(blender_object.data, "materials", None):
|
||||
return
|
||||
|
||||
blender_mat = blender_object.data.materials[0]
|
||||
speckle_mat = RenderMaterial()
|
||||
speckle_mat.name = blender_mat.name
|
||||
|
||||
if blender_mat.use_nodes is True:
|
||||
inputs = blender_mat.node_tree.nodes["Principled BSDF"].inputs
|
||||
speckle_mat.diffuse = to_argb_int(inputs["Base Color"].default_value)
|
||||
speckle_mat.emissive = to_argb_int(inputs["Emission"].default_value)
|
||||
speckle_mat.roughness = inputs["Roughness"].default_value
|
||||
speckle_mat.metalness = inputs["Metallic"].default_value
|
||||
speckle_mat.opacity = inputs["Alpha"].default_value
|
||||
|
||||
else:
|
||||
speckle_mat.diffuse = to_argb_int(blender_mat.diffuse_color)
|
||||
speckle_mat.metalness = blender_mat.metallic
|
||||
speckle_mat.roughness = blender_mat.roughness
|
||||
|
||||
return speckle_mat
|
||||
|
||||
|
||||
def try_add_property(speckle_object, blender_object, prop, prop_name):
|
||||
if prop in speckle_object.keys() and speckle_object[prop] is not None:
|
||||
blender_object[prop_name] = speckle_object[prop]
|
||||
|
||||
|
||||
# def add_dictionary(prop, blender_object, superkey=None):
|
||||
# for key in prop.keys():
|
||||
# key_name = "{}.{}".format(superkey, key) if superkey else "{}".format(key)
|
||||
# if isinstance(prop[key], dict):
|
||||
# subtype = prop[key].get("type", None)
|
||||
# if subtype and subtype in FROM_SPECKLE.keys():
|
||||
# continue
|
||||
# else:
|
||||
# add_dictionary(prop[key], blender_object, key_name)
|
||||
# elif hasattr(prop[key], "type"):
|
||||
# subtype = prop[key].type
|
||||
# if subtype and subtype in FROM_SPECKLE.keys():
|
||||
# continue
|
||||
# else:
|
||||
# try:
|
||||
# blender_object[key_name] = prop[key]
|
||||
# except KeyError:
|
||||
# pass
|
||||
|
||||
|
||||
def add_custom_properties(speckle_object, blender_object):
|
||||
|
||||
if blender_object is None:
|
||||
return
|
||||
|
||||
blender_object["_speckle_type"] = type(speckle_object).__name__
|
||||
# blender_object['_speckle_name'] = "SpeckleObject"
|
||||
|
||||
ignore = ["_chunkable", "_units", "units"]
|
||||
|
||||
if hasattr(speckle_object, "applicationId"):
|
||||
blender_object["applicationId"] = speckle_object.applicationId
|
||||
|
||||
for key in speckle_object.get_dynamic_member_names():
|
||||
if key in ignore:
|
||||
continue
|
||||
if isinstance(speckle_object[key], (int, str, float, dict)):
|
||||
blender_object[key] = speckle_object[key]
|
||||
|
||||
|
||||
def dict_to_speckle_object(data):
|
||||
if "type" in data.keys() and data["type"] in SCHEMAS.keys():
|
||||
obj = SCHEMAS[data["type"]].parse_obj(data)
|
||||
for key in obj.properties.keys():
|
||||
if isinstance(obj.properties[key], dict):
|
||||
obj.properties[key] = dict_to_speckle_object(obj.properties[key])
|
||||
elif isinstance(obj.properties[key], list):
|
||||
for i in range(len(obj.properties[key])):
|
||||
if isinstance(obj.properties[key][i], dict):
|
||||
obj.properties[key][i] = dict_to_speckle_object(
|
||||
obj.properties[key][i]
|
||||
)
|
||||
return obj
|
||||
else:
|
||||
for key in data.keys():
|
||||
if isinstance(data[key], dict):
|
||||
data[key] = dict_to_speckle_object(data[key])
|
||||
elif isinstance(data[key], list):
|
||||
for i in range(len(data[key])):
|
||||
if isinstance(data[key][i], dict):
|
||||
data[key][i] = dict_to_speckle_object(data[key][i])
|
||||
return data
|
||||
|
||||
|
||||
def from_speckle_object(speckle_object, scale, name=None):
|
||||
speckle_name = (
|
||||
name
|
||||
or getattr(speckle_object, "name", None)
|
||||
or speckle_object.speckle_type + f" -- {speckle_object.id}"
|
||||
)
|
||||
|
||||
units = getattr(speckle_object, "units", None)
|
||||
if units:
|
||||
scale = get_scale_length(units) / bpy.context.scene.unit_settings.scale_length
|
||||
|
||||
# try native conversion
|
||||
if type(speckle_object) in FROM_SPECKLE_SCHEMAS.keys():
|
||||
print("Got object type: {}".format(type(speckle_object)))
|
||||
|
||||
try:
|
||||
obdata = FROM_SPECKLE_SCHEMAS[type(speckle_object)](
|
||||
speckle_object, scale, speckle_name
|
||||
)
|
||||
except Exception as e: # conversion error
|
||||
_report(f"Error converting {speckle_object} \n{e}")
|
||||
return None
|
||||
|
||||
if speckle_name in bpy.data.objects.keys():
|
||||
blender_object = bpy.data.objects[speckle_name]
|
||||
blender_object.data = obdata
|
||||
blender_object.matrix_world = Matrix()
|
||||
if hasattr(obdata, "materials"):
|
||||
blender_object.data.materials.clear()
|
||||
else:
|
||||
blender_object = bpy.data.objects.new(speckle_name, obdata)
|
||||
|
||||
blender_object.speckle.object_id = str(speckle_object.id)
|
||||
blender_object.speckle.enabled = True
|
||||
|
||||
add_custom_properties(speckle_object, blender_object)
|
||||
add_blender_material(speckle_object, blender_object)
|
||||
# TODO: chat with tom re transforms
|
||||
# set_transform(speckle_object, blender_object)
|
||||
|
||||
return blender_object
|
||||
|
||||
# try display mesh
|
||||
mesh = getattr(
|
||||
speckle_object, "displayMesh", getattr(speckle_object, "displayValue", None)
|
||||
)
|
||||
if mesh:
|
||||
return from_speckle_object(mesh, scale, speckle_name)
|
||||
|
||||
# return none if fail
|
||||
_report("Invalid input: {}".format(speckle_object))
|
||||
return None
|
||||
|
||||
|
||||
def get_speckle_subobjects(attr, scale, name):
|
||||
|
||||
subobjects = []
|
||||
for key in attr.keys():
|
||||
if isinstance(attr[key], dict):
|
||||
subtype = attr[key].get("type", None)
|
||||
if subtype:
|
||||
name = "{}.{}".format(name, key)
|
||||
# print("{} :: {}".format(name, subtype))
|
||||
subobject = from_speckle_object(attr[key], scale, name)
|
||||
add_custom_properties(attr[key], subobject)
|
||||
|
||||
subobjects.append(subobject)
|
||||
props = attr[key].get("properties", None)
|
||||
if props:
|
||||
subobjects.extend(get_speckle_subobjects(props, scale, name))
|
||||
elif hasattr(attr[key], "type"):
|
||||
subtype = attr[key].type
|
||||
if subtype:
|
||||
name = "{}.{}".format(name, key)
|
||||
# print("{} :: {}".format(name, subtype))
|
||||
subobject = from_speckle_object(attr[key], scale, name)
|
||||
add_custom_properties(attr[key], subobject)
|
||||
|
||||
subobjects.append(subobject)
|
||||
props = attr[key].get("properties", None)
|
||||
if props:
|
||||
subobjects.extend(get_speckle_subobjects(props, scale, name))
|
||||
return subobjects
|
||||
|
||||
|
||||
ignored_keys = [
|
||||
"speckle",
|
||||
"_speckle_type",
|
||||
"_speckle_name",
|
||||
"_speckle_transform",
|
||||
"_RNA_UI",
|
||||
"transform",
|
||||
"_units",
|
||||
"_chunkable",
|
||||
]
|
||||
|
||||
|
||||
def get_blender_custom_properties(obj, max_depth=1000):
|
||||
global ignored_keys
|
||||
|
||||
if max_depth < 0:
|
||||
return obj
|
||||
|
||||
if hasattr(obj, "keys"):
|
||||
return {
|
||||
key: get_blender_custom_properties(obj[key], max_depth - 1)
|
||||
for key in obj.keys()
|
||||
if key not in ignored_keys and not key.startswith("_")
|
||||
}
|
||||
|
||||
elif isinstance(obj, (list, tuple, idprop.types.IDPropertyArray)):
|
||||
return [get_blender_custom_properties(o, max_depth - 1) for o in obj]
|
||||
else:
|
||||
return obj
|
||||
|
||||
|
||||
def to_speckle_object(blender_object, scale, desgraph=None):
|
||||
blender_type = blender_object.type
|
||||
speckle_objects = []
|
||||
speckle_material = material_to_speckle(blender_object)
|
||||
|
||||
if blender_type in TO_SPECKLE.keys():
|
||||
if desgraph:
|
||||
blender_object = blender_object.evaluated_get(desgraph)
|
||||
converted = TO_SPECKLE[blender_type](blender_object, blender_object.data, scale)
|
||||
if isinstance(converted, list):
|
||||
speckle_objects.extend([c for c in converted if c != None])
|
||||
|
||||
for so in speckle_objects:
|
||||
so.properties = get_blender_custom_properties(blender_object)
|
||||
so.applicationId = so.properties.pop("applicationId", None)
|
||||
|
||||
if speckle_material:
|
||||
so["renderMaterial"] = speckle_material
|
||||
|
||||
# Set object transform
|
||||
so.properties["transform"] = [y for x in blender_object.matrix_world for y in x]
|
||||
|
||||
# _report(speckle_objects)
|
||||
return speckle_objects
|
||||
@@ -1,3 +0,0 @@
|
||||
from .mesh import import_mesh
|
||||
from .curve import import_curve
|
||||
from .brep import import_brep
|
||||
@@ -1,24 +0,0 @@
|
||||
import bpy
|
||||
from .mesh import to_bmesh
|
||||
from bpy_speckle.util import find_key_case_insensitive
|
||||
|
||||
|
||||
def import_brep(speckle_brep, scale, name=None):
|
||||
if not name:
|
||||
name = speckle_brep.geometryHash or speckle_brep.id
|
||||
|
||||
display = getattr(
|
||||
speckle_brep, "displayMesh", getattr(speckle_brep, "displayValue", None)
|
||||
)
|
||||
if display:
|
||||
if name in bpy.data.meshes.keys():
|
||||
mesh = bpy.data.meshes[name]
|
||||
else:
|
||||
mesh = bpy.data.meshes.new(name=name)
|
||||
|
||||
to_bmesh(display, mesh, name, scale)
|
||||
# add_custom_properties(speckle_brep[dvKey], mesh)
|
||||
else:
|
||||
mesh = None
|
||||
|
||||
return mesh
|
||||
@@ -1,237 +0,0 @@
|
||||
import bpy, math
|
||||
from bpy_speckle.util import find_key_case_insensitive
|
||||
import mathutils
|
||||
from specklepy.objects.geometry import *
|
||||
|
||||
CONVERT = {}
|
||||
|
||||
|
||||
def import_line(scurve, bcurve, scale):
|
||||
line = bcurve.splines.new("POLY")
|
||||
line.points.add(1)
|
||||
|
||||
line.points[0].co = (
|
||||
float(scurve.start.x) * scale,
|
||||
float(scurve.start.y) * scale,
|
||||
float(scurve.start.z) * scale,
|
||||
1,
|
||||
)
|
||||
|
||||
if scurve.end:
|
||||
|
||||
line.points[1].co = (
|
||||
float(scurve.end.x) * scale,
|
||||
float(scurve.end.y) * scale,
|
||||
float(scurve.end.z) * scale,
|
||||
1,
|
||||
)
|
||||
|
||||
return line
|
||||
|
||||
|
||||
CONVERT[Line] = import_line
|
||||
|
||||
|
||||
def import_polyline(scurve, bcurve, scale):
|
||||
|
||||
# value = find_key_case_insensitive(scurve, "value")
|
||||
value = scurve.value
|
||||
|
||||
if value:
|
||||
N = int(len(value) / 3)
|
||||
|
||||
polyline = bcurve.splines.new("POLY")
|
||||
|
||||
if hasattr(scurve, "closed"):
|
||||
polyline.use_cyclic_u = scurve.closed
|
||||
|
||||
# if "closed" in scurve.keys():
|
||||
# polyline.use_cyclic_u = scurve["closed"]
|
||||
|
||||
polyline.points.add(N - 1)
|
||||
for i in range(N):
|
||||
polyline.points[i].co = (
|
||||
float(value[i * 3]) * scale,
|
||||
float(value[i * 3 + 1]) * scale,
|
||||
float(value[i * 3 + 2]) * scale,
|
||||
1,
|
||||
)
|
||||
|
||||
return polyline
|
||||
|
||||
|
||||
CONVERT[Polyline] = import_polyline
|
||||
|
||||
|
||||
def import_nurbs_curve(scurve, bcurve, scale):
|
||||
|
||||
# points = find_key_case_insensitive(scurve, "points")
|
||||
points = scurve.points
|
||||
|
||||
if points:
|
||||
N = int(len(points) / 3)
|
||||
|
||||
nurbs = bcurve.splines.new("NURBS")
|
||||
|
||||
if hasattr(scurve, "closed"):
|
||||
nurbs.use_cyclic_u = scurve.closed != 0
|
||||
|
||||
nurbs.points.add(N - 1)
|
||||
for i in range(N):
|
||||
nurbs.points[i].co = (
|
||||
float(points[i * 3]) * scale,
|
||||
float(points[i * 3 + 1]) * scale,
|
||||
float(points[i * 3 + 2]) * scale,
|
||||
1,
|
||||
)
|
||||
|
||||
if len(scurve.weights) == len(nurbs.points):
|
||||
for i, w in enumerate(scurve.weights):
|
||||
nurbs.points[i].weight = w
|
||||
|
||||
# TODO: anaylize curve knots to decide if use_endpoint_u or use_bezier_u should be enabled
|
||||
# nurbs.use_endpoint_u = True
|
||||
nurbs.order_u = scurve.degree + 1
|
||||
|
||||
return nurbs
|
||||
|
||||
|
||||
CONVERT[Curve] = import_nurbs_curve
|
||||
|
||||
|
||||
def import_arc(rcurve, bcurve, scale):
|
||||
"""
|
||||
Convert Arc object
|
||||
TODO: improve Blender representation of arc
|
||||
"""
|
||||
plane = rcurve.plane
|
||||
if not plane:
|
||||
return
|
||||
|
||||
origin = plane.origin
|
||||
normal = mathutils.Vector([plane.normal.x, plane.normal.y, plane.normal.z])
|
||||
|
||||
xaxis = plane.xdir
|
||||
yaxis = plane.ydir
|
||||
|
||||
radius = rcurve.radius * scale
|
||||
startAngle = rcurve.startAngle
|
||||
endAngle = rcurve.endAngle
|
||||
|
||||
startQuat = mathutils.Quaternion(normal, startAngle)
|
||||
endQuat = mathutils.Quaternion(normal, endAngle)
|
||||
|
||||
"""
|
||||
Get start and end vectors, centre point, angles, etc.
|
||||
"""
|
||||
|
||||
r1 = mathutils.Vector([plane.xdir.x, plane.xdir.y, plane.xdir.z])
|
||||
r1.rotate(startQuat)
|
||||
|
||||
r2 = mathutils.Vector([plane.xdir.x, plane.xdir.y, plane.xdir.z])
|
||||
r2.rotate(endQuat)
|
||||
|
||||
c = mathutils.Vector([plane.origin.x, plane.origin.y, plane.origin.z]) * scale
|
||||
|
||||
spt = c + r1 * radius
|
||||
ept = c + r2 * radius
|
||||
|
||||
angle = endAngle - startAngle
|
||||
|
||||
t1 = normal.cross(r1)
|
||||
|
||||
"""
|
||||
Initialize arc data and calculate subdivisions
|
||||
"""
|
||||
arc = bcurve.splines.new("NURBS")
|
||||
|
||||
arc.use_cyclic_u = False
|
||||
|
||||
Ndiv = max(int(math.floor(angle / 0.3)), 2)
|
||||
step = angle / float(Ndiv)
|
||||
stepQuat = mathutils.Quaternion(normal, step)
|
||||
tan = math.tan(step / 2) * radius
|
||||
|
||||
arc.points.add(Ndiv + 1)
|
||||
|
||||
"""
|
||||
Set start and end points
|
||||
"""
|
||||
arc.points[0].co = (spt.x, spt.y, spt.z, 1)
|
||||
arc.points[Ndiv + 1].co = (ept.x, ept.y, ept.z, 1)
|
||||
|
||||
"""
|
||||
Set intermediate points
|
||||
"""
|
||||
for i in range(Ndiv):
|
||||
t1 = normal.cross(r1)
|
||||
pt = c + r1 * radius + t1 * tan
|
||||
arc.points[i + 1].co = (pt.x, pt.y, pt.z, 1)
|
||||
r1.rotate(stepQuat)
|
||||
|
||||
"""
|
||||
Set curve settings
|
||||
"""
|
||||
|
||||
arc.use_endpoint_u = True
|
||||
arc.order_u = 3
|
||||
|
||||
return arc
|
||||
|
||||
|
||||
CONVERT[Arc] = import_arc
|
||||
|
||||
|
||||
def import_null(speckle_object, bcurve, scale):
|
||||
"""
|
||||
Handle unsupported types
|
||||
"""
|
||||
print("Failed to convert type", speckle_object["type"])
|
||||
return None
|
||||
|
||||
|
||||
def import_polycurve(scurve, bcurve, scale):
|
||||
"""
|
||||
Convert Polycurve object
|
||||
"""
|
||||
segments = scurve.segments
|
||||
|
||||
curves = []
|
||||
|
||||
for seg in segments:
|
||||
speckle_type = type(seg)
|
||||
|
||||
if speckle_type in CONVERT.keys():
|
||||
# segcurve = SCHEMAS[speckle_type].parse_obj(seg)
|
||||
curves.append(CONVERT[speckle_type](seg, bcurve, scale))
|
||||
else:
|
||||
print("Unsupported curve type: {}".format(speckle_type))
|
||||
|
||||
return curves
|
||||
|
||||
|
||||
CONVERT[Polycurve] = import_polycurve
|
||||
|
||||
|
||||
def import_curve(speckle_curve, scale, name=None):
|
||||
"""
|
||||
Convert Curve object
|
||||
"""
|
||||
if not name:
|
||||
name = speckle_curve.geometryHash or speckle_curve.id or "SpeckleCurve"
|
||||
|
||||
if name in bpy.data.curves.keys():
|
||||
curve_data = bpy.data.curves[name]
|
||||
else:
|
||||
curve_data = bpy.data.curves.new(name, type="CURVE")
|
||||
|
||||
curve_data.dimensions = "3D"
|
||||
curve_data.resolution_u = 12
|
||||
|
||||
if type(speckle_curve) not in CONVERT.keys():
|
||||
print("Unsupported curve type: {}".format(speckle_curve.type))
|
||||
return None
|
||||
|
||||
CONVERT[type(speckle_curve)](speckle_curve, curve_data, scale)
|
||||
|
||||
return curve_data
|
||||
@@ -1,145 +0,0 @@
|
||||
import bpy, bmesh, struct
|
||||
import base64
|
||||
from bpy_speckle.functions import _report
|
||||
|
||||
|
||||
def add_vertices(smesh, bmesh, scale=1.0):
|
||||
sverts = smesh.vertices
|
||||
|
||||
if sverts and len(sverts) > 0:
|
||||
for i in range(0, len(sverts), 3):
|
||||
bmesh.verts.new(
|
||||
(
|
||||
float(sverts[i]) * scale,
|
||||
float(sverts[i + 1]) * scale,
|
||||
float(sverts[i + 2]) * scale,
|
||||
)
|
||||
)
|
||||
|
||||
bmesh.verts.ensure_lookup_table()
|
||||
|
||||
|
||||
def add_faces(smesh, bmesh, smooth=False):
|
||||
sfaces = smesh.faces
|
||||
|
||||
if sfaces and len(sfaces) > 0:
|
||||
i = 0
|
||||
while i < len(sfaces):
|
||||
n = sfaces[i]
|
||||
if n < 3:
|
||||
n += 3 # 0 -> 3, 1 -> 4
|
||||
|
||||
i += 1
|
||||
try:
|
||||
f = bmesh.faces.new([bmesh.verts[int(x)] for x in sfaces[i : i + n]])
|
||||
f.smooth = smooth
|
||||
except Exception as e:
|
||||
_report(f"Failed to create face for mesh {smesh.id} \n{e}")
|
||||
i += n
|
||||
|
||||
bmesh.faces.ensure_lookup_table()
|
||||
bmesh.verts.index_update()
|
||||
|
||||
|
||||
def add_colors(smesh, bmesh):
|
||||
|
||||
scolors = smesh.colors
|
||||
|
||||
if scolors:
|
||||
colors = []
|
||||
if len(scolors) > 0:
|
||||
|
||||
for i in range(len(scolors)):
|
||||
col = int(scolors[i])
|
||||
(a, r, g, b) = [
|
||||
int(x) for x in struct.unpack("!BBBB", struct.pack("!i", col))
|
||||
]
|
||||
colors.append(
|
||||
(
|
||||
float(r) / 255.0,
|
||||
float(g) / 255.0,
|
||||
float(b) / 255.0,
|
||||
float(a) / 255.0,
|
||||
)
|
||||
)
|
||||
|
||||
# Make vertex colors
|
||||
if len(scolors) == len(bmesh.verts):
|
||||
color_layer = bmesh.loops.layers.color.new("Col")
|
||||
|
||||
for face in bmesh.faces:
|
||||
for loop in face.loops:
|
||||
loop[color_layer] = colors[loop.vert.index]
|
||||
|
||||
|
||||
def add_uv_coords(smesh, bmesh):
|
||||
if not hasattr(smesh, "properties"):
|
||||
return
|
||||
|
||||
sprops = smesh.properties
|
||||
if sprops:
|
||||
texKey = ""
|
||||
if "texture_coordinates" in sprops.keys():
|
||||
texKey = "texture_coordinates"
|
||||
elif "TextureCoordinates" in sprops.keys():
|
||||
texKey = "TextureCoordinates"
|
||||
|
||||
if texKey != "":
|
||||
|
||||
try:
|
||||
decoded = base64.b64decode(sprops[texKey]).decode("utf-8")
|
||||
s_uvs = decoded.split()
|
||||
uv = []
|
||||
|
||||
if int(len(s_uvs) / 2) == len(bmesh.verts):
|
||||
for i in range(0, len(s_uvs), 2):
|
||||
uv.append((float(s_uvs[i]), float(s_uvs[i + 1])))
|
||||
else:
|
||||
print(len(s_uvs) * 2)
|
||||
print(len(bmesh.verts))
|
||||
print("Failed to match UV coordinates to vert data.")
|
||||
|
||||
# Make UVs
|
||||
uv_layer = bmesh.loops.layers.uv.verify()
|
||||
|
||||
for f in bmesh.faces:
|
||||
for l in f.loops:
|
||||
luv = l[uv_layer]
|
||||
luv.uv = uv[l.vert.index]
|
||||
except:
|
||||
print("Failed to decode texture coordinates.")
|
||||
raise
|
||||
|
||||
del smesh.properties[texKey]
|
||||
|
||||
|
||||
def to_bmesh(speckle_mesh, blender_mesh, name="SpeckleMesh", scale=1.0):
|
||||
bm = bmesh.new()
|
||||
|
||||
add_vertices(speckle_mesh, bm, scale)
|
||||
add_faces(speckle_mesh, bm)
|
||||
add_colors(speckle_mesh, bm)
|
||||
add_uv_coords(speckle_mesh, bm)
|
||||
|
||||
bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
|
||||
bm.to_mesh(blender_mesh)
|
||||
bm.free()
|
||||
|
||||
return blender_mesh
|
||||
|
||||
|
||||
def import_mesh(speckle_mesh, scale=1.0, name=None):
|
||||
"""
|
||||
Convert Mesh object
|
||||
"""
|
||||
if not name:
|
||||
name = speckle_mesh.geometryHash or speckle_mesh.id
|
||||
|
||||
if name in bpy.data.meshes.keys():
|
||||
mesh = bpy.data.meshes[name]
|
||||
else:
|
||||
mesh = bpy.data.meshes.new(name=name)
|
||||
|
||||
to_bmesh(speckle_mesh, mesh, name, scale)
|
||||
|
||||
return mesh
|
||||
@@ -1,6 +0,0 @@
|
||||
import bpy, bmesh, struct
|
||||
import base64
|
||||
|
||||
|
||||
def import_plane(speckle_plane, scale=1.0, name=None):
|
||||
return None
|
||||
@@ -1,3 +0,0 @@
|
||||
from .mesh import export_mesh
|
||||
from .curve import export_curve, export_ngons_as_polylines
|
||||
from .empty import export_empty
|
||||
@@ -1,224 +0,0 @@
|
||||
import bpy, bmesh, struct
|
||||
from specklepy.objects.geometry import Curve, Interval, Box, Polyline
|
||||
from bpy_speckle.convert.to_speckle.mesh import export_mesh
|
||||
|
||||
UNITS = "m"
|
||||
|
||||
|
||||
def bezier_to_speckle(matrix, spline, scale, name=None):
|
||||
degree = 3
|
||||
closed = spline.use_cyclic_u
|
||||
|
||||
points = []
|
||||
for i, bp in enumerate(spline.bezier_points):
|
||||
if i > 0:
|
||||
points.append(tuple(matrix @ bp.handle_left * scale))
|
||||
points.append(tuple(matrix @ bp.co * scale))
|
||||
if i < len(spline.bezier_points) - 1:
|
||||
points.append(tuple(matrix @ bp.handle_right * scale))
|
||||
|
||||
if closed:
|
||||
points.append(tuple(matrix @ spline.bezier_points[-1].handle_right * scale))
|
||||
points.append(tuple(matrix @ spline.bezier_points[0].handle_left * scale))
|
||||
points.append(tuple(matrix @ spline.bezier_points[0].co * scale))
|
||||
|
||||
num_points = len(points)
|
||||
|
||||
knot_count = num_points + degree - 1
|
||||
knots = [0] * knot_count
|
||||
|
||||
for i in range(1, len(knots)):
|
||||
knots[i] = i // 3
|
||||
|
||||
length = spline.calc_length()
|
||||
domain = Interval(start=0, end=length, totalChildrenCount=0)
|
||||
return Curve(
|
||||
name=name,
|
||||
degree=degree,
|
||||
closed=spline.use_cyclic_u,
|
||||
periodic=spline.use_cyclic_u,
|
||||
points=list(sum(points, ())), # magic (flatten list of tuples)
|
||||
weights=[1] * num_points,
|
||||
knots=knots,
|
||||
rational=False,
|
||||
area=0,
|
||||
volume=0,
|
||||
length=length,
|
||||
domain=domain,
|
||||
units=UNITS,
|
||||
bbox=Box(area=0.0, volume=0.0),
|
||||
)
|
||||
|
||||
|
||||
def nurbs_to_speckle(matrix, spline, scale, name=None):
|
||||
knots = makeknots(spline)
|
||||
# print("knots: {}".format(knots))
|
||||
points = [tuple(matrix @ pt.co.xyz * scale) for pt in spline.points]
|
||||
degree = spline.order_u - 1
|
||||
|
||||
length = spline.calc_length()
|
||||
domain = Interval(start=0, end=length, totalChildrenCount=0)
|
||||
|
||||
return Curve(
|
||||
name=name,
|
||||
degree=degree,
|
||||
closed=spline.use_cyclic_u,
|
||||
periodic=spline.use_cyclic_u,
|
||||
points=list(sum(points, ())), # magic (flatten list of tuples)
|
||||
weights=[pt.weight for pt in spline.points],
|
||||
knots=knots,
|
||||
rational=False,
|
||||
area=0,
|
||||
volume=0,
|
||||
length=length,
|
||||
domain=domain,
|
||||
units=UNITS,
|
||||
bbox=Box(area=0.0, volume=0.0),
|
||||
)
|
||||
|
||||
|
||||
def poly_to_speckle(matrix, spline, scale, name=None):
|
||||
points = [tuple(matrix @ pt.co.xyz * scale) for pt in spline.points]
|
||||
|
||||
length = spline.calc_length()
|
||||
domain = Interval(start=0, end=length, totalChildrenCount=0)
|
||||
return Polyline(
|
||||
name=name,
|
||||
closed=spline.use_cyclic_u,
|
||||
value=list(sum(points, ())), # magic (flatten list of tuples)
|
||||
length=length,
|
||||
domain=domain,
|
||||
bbox=Box(area=0.0, volume=0.0),
|
||||
area=0,
|
||||
units=UNITS,
|
||||
)
|
||||
|
||||
|
||||
def export_curve(blender_object, data, scale=1.0):
|
||||
UNITS = "m" if bpy.context.scene.unit_settings.system == "METRIC" else "ft"
|
||||
|
||||
if blender_object.type != "CURVE":
|
||||
return None
|
||||
|
||||
blender_object = blender_object.evaluated_get(bpy.context.view_layer.depsgraph)
|
||||
|
||||
mat = blender_object.matrix_world
|
||||
|
||||
curves = []
|
||||
|
||||
if data.bevel_mode == "OBJECT" and data.bevel_object != None:
|
||||
mesh = export_mesh(blender_object, blender_object.to_mesh(), scale)
|
||||
curves.extend(mesh)
|
||||
|
||||
for spline in data.splines:
|
||||
if spline.type == "BEZIER":
|
||||
curves.append(bezier_to_speckle(mat, spline, scale, blender_object.name))
|
||||
|
||||
elif spline.type == "NURBS":
|
||||
curves.append(nurbs_to_speckle(mat, spline, scale, blender_object.name))
|
||||
|
||||
elif spline.type == "POLY":
|
||||
curves.append(poly_to_speckle(mat, spline, scale, blender_object.name))
|
||||
|
||||
return curves
|
||||
|
||||
|
||||
def export_ngons_as_polylines(blender_object, data, scale=1.0):
|
||||
UNITS = "m" if bpy.context.scene.unit_settings.system == "METRIC" else "ft"
|
||||
|
||||
if blender_object.type != "MESH":
|
||||
return None
|
||||
|
||||
mat = blender_object.matrix_world
|
||||
|
||||
verts = data.vertices
|
||||
polylines = []
|
||||
for i, poly in enumerate(data.polygons):
|
||||
value = []
|
||||
for v in poly.vertices:
|
||||
value.extend(mat @ verts[v].co * scale)
|
||||
|
||||
domain = Interval(start=0, end=1)
|
||||
poly = Polyline(
|
||||
name="{}_{}".format(blender_object.name, i),
|
||||
closed=True,
|
||||
value=value, # magic (flatten list of tuples)
|
||||
length=0,
|
||||
domain=domain,
|
||||
bbox=Box(area=0.0, volume=0.0),
|
||||
area=0,
|
||||
units=UNITS,
|
||||
)
|
||||
|
||||
polylines.append(poly)
|
||||
|
||||
return polylines
|
||||
|
||||
|
||||
"""
|
||||
Python implementation of Blender's NURBS curve generation
|
||||
from: https://blender.stackexchange.com/a/34276
|
||||
"""
|
||||
|
||||
|
||||
def macro_knotsu(nu):
|
||||
return nu.order_u + nu.point_count_u + (nu.order_u - 1 if nu.use_cyclic_u else 0)
|
||||
|
||||
|
||||
def macro_segmentsu(nu):
|
||||
return nu.point_count_u if nu.use_cyclic_u else nu.point_count_u - 1
|
||||
|
||||
|
||||
def makeknots(nu):
|
||||
knots = [0.0] * (4 + macro_knotsu(nu))
|
||||
flag = nu.use_endpoint_u + (nu.use_bezier_u << 1)
|
||||
if nu.use_cyclic_u:
|
||||
calcknots(knots, nu.point_count_u, nu.order_u, 0)
|
||||
makecyclicknots(knots, nu.point_count_u, nu.order_u)
|
||||
else:
|
||||
calcknots(knots, nu.point_count_u, nu.order_u, flag)
|
||||
return knots
|
||||
|
||||
|
||||
def calcknots(knots, pnts, order, flag):
|
||||
pnts_order = pnts + order
|
||||
if flag == 1:
|
||||
k = 0.0
|
||||
for a in range(1, pnts_order + 1):
|
||||
knots[a - 1] = k
|
||||
if a >= order and a <= pnts:
|
||||
k += 1.0
|
||||
elif flag == 2:
|
||||
if order == 4:
|
||||
k = 0.34
|
||||
for a in range(pnts_order):
|
||||
knots[a] = math.floor(k)
|
||||
k += 1.0 / 3.0
|
||||
elif order == 3:
|
||||
k = 0.6
|
||||
for a in range(pnts_order):
|
||||
if a >= order and a <= pnts:
|
||||
k += 0.5
|
||||
knots[a] = math.floor(k)
|
||||
else:
|
||||
for a in range(pnts_order):
|
||||
knots[a] = a
|
||||
|
||||
|
||||
def makecyclicknots(knots, pnts, order):
|
||||
order2 = order - 1
|
||||
|
||||
if order > 2:
|
||||
b = pnts + order2
|
||||
for a in range(1, order2):
|
||||
if knots[b] != knots[b - a]:
|
||||
break
|
||||
|
||||
if a == order2:
|
||||
knots[pnts + order - 2] += 1.0
|
||||
|
||||
b = order
|
||||
c = pnts + order + order2
|
||||
for a in range(pnts + order2, c):
|
||||
knots[a] = knots[a - 1] + (knots[b] - knots[b - 1])
|
||||
b -= 1
|
||||
@@ -1,5 +0,0 @@
|
||||
import bpy, bmesh, struct
|
||||
|
||||
|
||||
def export_default(blender_object, scale=1.0):
|
||||
return None
|
||||
@@ -1,5 +0,0 @@
|
||||
import bpy, bmesh, struct
|
||||
|
||||
|
||||
def export_empty(blender_object, data, scale=1.0):
|
||||
return None
|
||||
@@ -1,44 +0,0 @@
|
||||
import bpy, bmesh, struct
|
||||
|
||||
import base64, hashlib
|
||||
from time import strftime, gmtime
|
||||
|
||||
from specklepy.objects.geometry import Mesh, Interval, Box
|
||||
|
||||
|
||||
def export_mesh(blender_object, data, scale=1.0):
|
||||
if data.loop_triangles is None or len(data.loop_triangles) < 1:
|
||||
data.calc_loop_triangles()
|
||||
|
||||
mat = blender_object.matrix_world
|
||||
|
||||
verts = [tuple(mat @ x.co * scale) for x in data.vertices]
|
||||
|
||||
faces = [p.vertices for p in data.polygons]
|
||||
unit_system = bpy.context.scene.unit_settings.system
|
||||
|
||||
sm = Mesh(
|
||||
name=blender_object.name,
|
||||
vertices=list(sum(verts, ())),
|
||||
faces=[],
|
||||
colors=[],
|
||||
textureCoordinates=[],
|
||||
units="m" if unit_system == "METRIC" else "ft",
|
||||
bbox=Box(area=0.0, volume=0.0),
|
||||
)
|
||||
|
||||
if data.uv_layers.active:
|
||||
for vt in data.uv_layers.active.data:
|
||||
sm.textureCoordinates.extend([vt.uv.x, vt.uv.y])
|
||||
|
||||
for f in faces:
|
||||
n = len(f)
|
||||
if n == 3:
|
||||
sm.faces.append(0)
|
||||
elif n == 4:
|
||||
sm.faces.append(1)
|
||||
else:
|
||||
sm.faces.append(n)
|
||||
sm.faces.extend(f)
|
||||
|
||||
return [sm]
|
||||
@@ -1,19 +0,0 @@
|
||||
from typing import Tuple
|
||||
|
||||
|
||||
def to_rgba(argb_int: int) -> Tuple[float]:
|
||||
"""Converts the int representation of a colour into a percent RGBA tuple"""
|
||||
alpha = ((argb_int >> 24) & 255) / 255
|
||||
red = ((argb_int >> 16) & 255) / 255
|
||||
green = ((argb_int >> 8) & 255) / 255
|
||||
blue = (argb_int & 255) / 255
|
||||
|
||||
return (red, green, blue, alpha)
|
||||
|
||||
|
||||
def to_argb_int(diffuse_colour) -> int:
|
||||
"""Converts an RGBA array to an ARGB integer"""
|
||||
diffuse_colour = diffuse_colour[-1:] + diffuse_colour[0:3]
|
||||
diffuse_colour = [int(val * 255) for val in diffuse_colour]
|
||||
|
||||
return int.from_bytes(diffuse_colour, byteorder="big", signed=True)
|
||||
@@ -0,0 +1,3 @@
|
||||
from ..converter.to_native import * # noqa: F403
|
||||
from ..converter.to_speckle import * # noqa: F403
|
||||
from ..converter.utils import * # noqa: F403
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,6 @@
|
||||
from .to_speckle import convert_to_speckle # noqa: F401
|
||||
from .material_to_speckle import ( # noqa: F401
|
||||
blender_material_to_speckle,
|
||||
create_render_material_proxies,
|
||||
add_render_material_proxies_to_base,
|
||||
)
|
||||
@@ -0,0 +1,272 @@
|
||||
from bpy.types import Object
|
||||
from typing import Union, Optional, Tuple, List
|
||||
from specklepy.objects.geometry import Polyline, Curve
|
||||
from specklepy.objects.primitive import Interval
|
||||
from specklepy.objects.base import Base
|
||||
from mathutils import Matrix
|
||||
from mathutils.geometry import interpolate_bezier
|
||||
from .utils import nurb_make_curve, make_knots
|
||||
|
||||
|
||||
def curve_to_speckle(
|
||||
blender_obj: Object, scale_factor: float = 1.0
|
||||
) -> Union[Base, None]:
|
||||
assert blender_obj.type == "CURVE", "Object must be a curve"
|
||||
assert blender_obj.data is not None, "Curve data cannot be None"
|
||||
|
||||
curve_data = blender_obj.data
|
||||
matrix = blender_obj.matrix_world
|
||||
units = "m" # TODO: Use the unit system from the scene
|
||||
|
||||
base = Base()
|
||||
curves = []
|
||||
|
||||
for spline in curve_data.splines:
|
||||
if spline.type == "BEZIER":
|
||||
curves.append(
|
||||
bezier_to_speckle(matrix, spline, blender_obj.name, scale_factor, units)
|
||||
)
|
||||
elif spline.type == "NURBS":
|
||||
curves.append(
|
||||
nurbs_to_speckle(matrix, spline, blender_obj.name, scale_factor, units)
|
||||
)
|
||||
|
||||
if curves:
|
||||
base["@elements"] = curves
|
||||
base["name"] = blender_obj.name
|
||||
return base
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def bezier_to_speckle(
|
||||
matrix: Matrix,
|
||||
spline,
|
||||
name: Optional[str] = None,
|
||||
scale_factor: float = 1.0,
|
||||
units: str = "m",
|
||||
) -> Curve:
|
||||
degree = 3
|
||||
closed = spline.use_cyclic_u
|
||||
points: List[Tuple[float, float, float]] = []
|
||||
|
||||
for i, bp in enumerate(spline.bezier_points):
|
||||
if i > 0:
|
||||
transformed_point = matrix @ bp.handle_left * scale_factor
|
||||
points.append(
|
||||
(transformed_point.x, transformed_point.y, transformed_point.z)
|
||||
)
|
||||
|
||||
transformed_point = matrix @ bp.co * scale_factor
|
||||
points.append((transformed_point.x, transformed_point.y, transformed_point.z))
|
||||
|
||||
if i < len(spline.bezier_points) - 1:
|
||||
transformed_point = matrix @ bp.handle_right * scale_factor
|
||||
points.append(
|
||||
(transformed_point.x, transformed_point.y, transformed_point.z)
|
||||
)
|
||||
|
||||
if closed:
|
||||
transformed_point = (
|
||||
matrix @ spline.bezier_points[-1].handle_right * scale_factor
|
||||
)
|
||||
points.append((transformed_point.x, transformed_point.y, transformed_point.z))
|
||||
|
||||
transformed_point = matrix @ spline.bezier_points[0].handle_left * scale_factor
|
||||
points.append((transformed_point.x, transformed_point.y, transformed_point.z))
|
||||
|
||||
transformed_point = matrix @ spline.bezier_points[0].co * scale_factor
|
||||
points.append((transformed_point.x, transformed_point.y, transformed_point.z))
|
||||
|
||||
num_points = len(points)
|
||||
|
||||
flattened_points = []
|
||||
for point in points:
|
||||
flattened_points.extend(point)
|
||||
|
||||
knot_count = num_points + degree - 1
|
||||
knots = [0] * knot_count
|
||||
|
||||
for i in range(1, len(knots)):
|
||||
knots[i] = i // 3
|
||||
|
||||
length = spline.calc_length()
|
||||
|
||||
domain = Interval(start=0, end=length)
|
||||
display_value = bezier_to_speckle_polyline(
|
||||
matrix, spline, length, scale_factor, units
|
||||
)
|
||||
|
||||
curve = Curve(
|
||||
degree=degree,
|
||||
periodic=not spline.use_endpoint_u,
|
||||
rational=True,
|
||||
points=flattened_points,
|
||||
weights=[1] * num_points,
|
||||
knots=knots,
|
||||
closed=spline.use_cyclic_u,
|
||||
displayValue=display_value,
|
||||
units=units,
|
||||
bbox=None,
|
||||
)
|
||||
|
||||
curve.__dict__["_length"] = length
|
||||
curve.__dict__["_area"] = 0.0
|
||||
|
||||
curve["domain"] = domain
|
||||
|
||||
if name:
|
||||
curve["name"] = name
|
||||
|
||||
return curve
|
||||
|
||||
|
||||
def bezier_to_speckle_polyline(
|
||||
matrix: Matrix,
|
||||
spline,
|
||||
length: Optional[float] = None,
|
||||
scale_factor: float = 1.0,
|
||||
units: str = "m",
|
||||
) -> Optional[Polyline]:
|
||||
segments = len(spline.bezier_points)
|
||||
if segments < 2:
|
||||
return None
|
||||
|
||||
resolution = spline.resolution_u + 1
|
||||
points: List[float] = []
|
||||
|
||||
if not spline.use_cyclic_u:
|
||||
segments -= 1
|
||||
|
||||
for i in range(segments):
|
||||
inext = (i + 1) % len(spline.bezier_points)
|
||||
|
||||
knot1 = spline.bezier_points[i].co
|
||||
handle1 = spline.bezier_points[i].handle_right
|
||||
handle2 = spline.bezier_points[inext].handle_left
|
||||
knot2 = spline.bezier_points[inext].co
|
||||
|
||||
sampled_points = interpolate_bezier(knot1, handle1, handle2, knot2, resolution)
|
||||
for p in sampled_points:
|
||||
scaled_point = matrix @ p * scale_factor
|
||||
points.append(scaled_point.x)
|
||||
points.append(scaled_point.y)
|
||||
points.append(scaled_point.z)
|
||||
|
||||
length = length or spline.calc_length()
|
||||
|
||||
polyline = Polyline(value=points, units=units)
|
||||
|
||||
polyline["domain"] = {"start": 0, "end": length}
|
||||
polyline["closed"] = spline.use_cyclic_u
|
||||
|
||||
return polyline
|
||||
|
||||
|
||||
def nurbs_to_speckle(
|
||||
matrix: Matrix,
|
||||
spline,
|
||||
name: Optional[str] = None,
|
||||
scale_factor: float = 1.0,
|
||||
units: str = "m",
|
||||
) -> Curve:
|
||||
degree = spline.order_u - 1
|
||||
knots = make_knots(spline)
|
||||
|
||||
length = spline.calc_length()
|
||||
domain = Interval(start=0, end=length)
|
||||
|
||||
weights = [pt.weight for pt in spline.points]
|
||||
first_weight = weights[0] if weights else 1.0
|
||||
is_rational = any(abs(w - first_weight) > 1e-9 for w in weights)
|
||||
|
||||
points = []
|
||||
for pt in spline.points:
|
||||
transformed_point = matrix @ pt.co.xyz * scale_factor
|
||||
points.append((transformed_point.x, transformed_point.y, transformed_point.z))
|
||||
|
||||
flattened_points = []
|
||||
for point in points:
|
||||
flattened_points.extend(point)
|
||||
|
||||
if spline.use_cyclic_u:
|
||||
for i in range(0, degree * 3, 3):
|
||||
flattened_points.append(flattened_points[i + 0])
|
||||
flattened_points.append(flattened_points[i + 1])
|
||||
flattened_points.append(flattened_points[i + 2])
|
||||
|
||||
for i in range(0, degree):
|
||||
weights.append(weights[i])
|
||||
|
||||
resolution_multiplier = (
|
||||
4 if (spline.use_cyclic_u and spline.point_count_u <= 16) else 1
|
||||
)
|
||||
display_value = nurbs_to_speckle_polyline(
|
||||
matrix, spline, length, scale_factor, units, resolution_multiplier
|
||||
)
|
||||
|
||||
curve = Curve(
|
||||
degree=degree,
|
||||
periodic=not spline.use_endpoint_u,
|
||||
rational=is_rational,
|
||||
points=flattened_points,
|
||||
weights=weights,
|
||||
knots=knots,
|
||||
closed=spline.use_cyclic_u,
|
||||
displayValue=display_value,
|
||||
units=units,
|
||||
bbox=None,
|
||||
)
|
||||
|
||||
curve.__dict__["_length"] = length
|
||||
|
||||
curve["domain"] = domain
|
||||
|
||||
if name:
|
||||
curve["name"] = name
|
||||
|
||||
return curve
|
||||
|
||||
|
||||
def nurbs_to_speckle_polyline(
|
||||
matrix: Matrix,
|
||||
spline,
|
||||
length: Optional[float] = None,
|
||||
scale_factor: float = 1.0,
|
||||
units: str = "m",
|
||||
resolution_multiplier: int = 1,
|
||||
) -> Polyline:
|
||||
from mathutils import Vector
|
||||
|
||||
points: List[float] = []
|
||||
|
||||
resolution = spline.resolution_u * resolution_multiplier
|
||||
|
||||
sampled_points = nurb_make_curve(spline, resolution)
|
||||
|
||||
for i in range(0, len(sampled_points), 3):
|
||||
point_vector = Vector(
|
||||
(sampled_points[i], sampled_points[i + 1], sampled_points[i + 2])
|
||||
)
|
||||
transformed_point = matrix @ point_vector * scale_factor
|
||||
|
||||
points.append(transformed_point.x)
|
||||
points.append(transformed_point.y)
|
||||
points.append(transformed_point.z)
|
||||
|
||||
length = length or spline.calc_length()
|
||||
|
||||
polyline = Polyline(value=points, units=units)
|
||||
|
||||
polyline["domain"] = {"start": 0, "end": length}
|
||||
polyline["closed"] = spline.use_cyclic_u
|
||||
|
||||
# Set length property if needed
|
||||
if hasattr(polyline, "length") or hasattr(polyline, "_length"):
|
||||
polyline.__dict__["_length"] = length
|
||||
|
||||
# Set area property if needed
|
||||
if hasattr(polyline, "area") or hasattr(polyline, "_area"):
|
||||
polyline.__dict__["_area"] = 0
|
||||
|
||||
return polyline
|
||||
@@ -0,0 +1,257 @@
|
||||
from typing import Dict, List, Set
|
||||
import bpy
|
||||
from bpy.types import Material, Object
|
||||
from specklepy.objects.base import Base
|
||||
from specklepy.objects.other import RenderMaterial
|
||||
from specklepy.objects.proxies import RenderMaterialProxy
|
||||
from ..utils import to_argb_int
|
||||
|
||||
from .utils import get_submesh_id, get_unique_id
|
||||
|
||||
|
||||
def blender_material_to_speckle(material: Material) -> RenderMaterial:
|
||||
"""
|
||||
convert a Blender material to a Speckle RenderMaterial
|
||||
"""
|
||||
diffuse = -1 # default white
|
||||
opacity = 1.0
|
||||
emissive = -16777216 # default black
|
||||
metalness = 0.0
|
||||
roughness = 1.0
|
||||
|
||||
# extract material properties if using nodes
|
||||
if material.use_nodes and material.node_tree:
|
||||
output_node = None
|
||||
for node in material.node_tree.nodes:
|
||||
if node.type == "OUTPUT_MATERIAL":
|
||||
output_node = node
|
||||
break
|
||||
|
||||
# find the main shader node connected to output
|
||||
main_shader = None
|
||||
if output_node and output_node.inputs["Surface"].is_linked:
|
||||
main_shader = output_node.inputs["Surface"].links[0].from_node
|
||||
|
||||
# handle different shader types
|
||||
# we're supporting: principled, diffuse, emmision and glass - for now
|
||||
if main_shader:
|
||||
if main_shader.type == "BSDF_PRINCIPLED":
|
||||
diffuse, opacity, metalness, roughness, emissive = (
|
||||
_extract_principled_properties(main_shader)
|
||||
)
|
||||
|
||||
elif main_shader.type == "BSDF_DIFFUSE":
|
||||
color_input = main_shader.inputs.get("Color")
|
||||
if color_input:
|
||||
if color_input.is_linked:
|
||||
rgba = _get_color_from_connected_node(
|
||||
color_input.links[0].from_node
|
||||
)
|
||||
else:
|
||||
rgba = list(color_input.default_value)
|
||||
diffuse = to_argb_int(rgba)
|
||||
roughness = 1.0
|
||||
|
||||
elif main_shader.type == "EMISSION":
|
||||
color_input = main_shader.inputs.get("Color")
|
||||
strength_input = main_shader.inputs.get("Strength")
|
||||
if color_input and strength_input:
|
||||
if color_input.is_linked:
|
||||
rgba = _get_color_from_connected_node(
|
||||
color_input.links[0].from_node
|
||||
)
|
||||
else:
|
||||
rgba = list(color_input.default_value)
|
||||
|
||||
strength = (
|
||||
float(strength_input.default_value)
|
||||
if not strength_input.is_linked
|
||||
else 1.0
|
||||
)
|
||||
|
||||
if strength > 0:
|
||||
emission_rgba = [c * strength for c in rgba[:3]] + [rgba[3]]
|
||||
emission_rgba = [min(1.0, max(0.0, c)) for c in emission_rgba]
|
||||
emissive = to_argb_int(emission_rgba)
|
||||
diffuse = to_argb_int(rgba)
|
||||
|
||||
elif main_shader.type == "BSDF_GLASS":
|
||||
color_input = main_shader.inputs.get("Color")
|
||||
if color_input:
|
||||
if color_input.is_linked:
|
||||
rgba = _get_color_from_connected_node(
|
||||
color_input.links[0].from_node
|
||||
)
|
||||
else:
|
||||
rgba = list(color_input.default_value)
|
||||
diffuse = to_argb_int(rgba)
|
||||
roughness_input = main_shader.inputs.get("Roughness")
|
||||
if roughness_input:
|
||||
roughness = (
|
||||
float(roughness_input.default_value)
|
||||
if not roughness_input.is_linked
|
||||
else 0.0
|
||||
)
|
||||
opacity = 0.5
|
||||
|
||||
else:
|
||||
# fallback to legacy material properties
|
||||
if hasattr(material, "diffuse_color"):
|
||||
rgba = list(material.diffuse_color) + [1.0]
|
||||
diffuse = to_argb_int(rgba)
|
||||
|
||||
if hasattr(material, "metallic"):
|
||||
metalness = float(material.metallic)
|
||||
|
||||
if hasattr(material, "roughness"):
|
||||
roughness = float(material.roughness)
|
||||
|
||||
render_material = RenderMaterial(
|
||||
name=material.name,
|
||||
diffuse=diffuse,
|
||||
opacity=opacity,
|
||||
emissive=emissive,
|
||||
metalness=metalness,
|
||||
roughness=roughness,
|
||||
)
|
||||
|
||||
return render_material
|
||||
|
||||
|
||||
def _extract_principled_properties(principled_node):
|
||||
diffuse = -1
|
||||
opacity = 1.0
|
||||
metalness = 0.0
|
||||
roughness = 1.0
|
||||
emissive = -16777216
|
||||
|
||||
base_color_input = principled_node.inputs.get("Base Color")
|
||||
if base_color_input:
|
||||
if base_color_input.is_linked:
|
||||
rgba = _get_color_from_connected_node(base_color_input.links[0].from_node)
|
||||
else:
|
||||
rgba = list(base_color_input.default_value)
|
||||
diffuse = to_argb_int(rgba)
|
||||
|
||||
# Alpha/Opacity
|
||||
alpha_input = principled_node.inputs.get("Alpha")
|
||||
if alpha_input and not alpha_input.is_linked:
|
||||
opacity = float(alpha_input.default_value)
|
||||
|
||||
# Metallic
|
||||
metallic_input = principled_node.inputs.get("Metallic")
|
||||
if metallic_input and not metallic_input.is_linked:
|
||||
metalness = float(metallic_input.default_value)
|
||||
|
||||
# Roughness
|
||||
roughness_input = principled_node.inputs.get("Roughness")
|
||||
if roughness_input and not roughness_input.is_linked:
|
||||
roughness = float(roughness_input.default_value)
|
||||
|
||||
# Emission - try different possible input names for different versions
|
||||
emission_color_input = principled_node.inputs.get(
|
||||
"Emission Color"
|
||||
) or principled_node.inputs.get("Emission")
|
||||
|
||||
emission_strength_input = principled_node.inputs.get("Emission Strength")
|
||||
|
||||
if emission_color_input:
|
||||
if emission_color_input.is_linked:
|
||||
emission_rgba = _get_color_from_connected_node(
|
||||
emission_color_input.links[0].from_node
|
||||
)
|
||||
else:
|
||||
emission_rgba = list(emission_color_input.default_value)
|
||||
|
||||
emission_strength = 1.0
|
||||
if emission_strength_input and not emission_strength_input.is_linked:
|
||||
emission_strength = float(emission_strength_input.default_value)
|
||||
|
||||
if emission_strength > 0 and any(
|
||||
c > 0.01 for c in emission_rgba[:3]
|
||||
): # Check if color is not black
|
||||
final_emission_rgba = [c * emission_strength for c in emission_rgba[:3]] + [
|
||||
emission_rgba[3]
|
||||
]
|
||||
final_emission_rgba = [min(1.0, max(0.0, c)) for c in final_emission_rgba]
|
||||
emissive = to_argb_int(final_emission_rgba)
|
||||
|
||||
return diffuse, opacity, metalness, roughness, emissive
|
||||
|
||||
|
||||
def _get_color_from_connected_node(node):
|
||||
if node.type == "RGB":
|
||||
rgba = list(node.outputs["Color"].default_value)
|
||||
return rgba
|
||||
elif node.type == "VALTORGB":
|
||||
if node.color_ramp.elements:
|
||||
rgba = list(node.color_ramp.elements[0].color)
|
||||
return rgba
|
||||
elif hasattr(node, "color"):
|
||||
rgba = list(node.color) + [1.0]
|
||||
return rgba
|
||||
|
||||
# fallback to white
|
||||
return [1.0, 1.0, 1.0, 1.0]
|
||||
|
||||
|
||||
def collect_material_assignments(objects: List[Object]) -> Dict[str, Set[str]]:
|
||||
"""
|
||||
collect material assignments for objects, creating unique applicationIds
|
||||
for each material slot use a unique id
|
||||
"""
|
||||
material_assignments: Dict[str, Set[str]] = {}
|
||||
|
||||
for obj in objects:
|
||||
if not obj or not hasattr(obj, "data") or not obj.data:
|
||||
continue
|
||||
|
||||
# check if object has materials
|
||||
if hasattr(obj.data, "materials") and obj.data.materials:
|
||||
for material_index, material_slot in enumerate(obj.data.materials):
|
||||
if material_slot:
|
||||
material_name = material_slot.name
|
||||
|
||||
# set unique ID for submeshes
|
||||
application_id = get_submesh_id(obj, material_index)
|
||||
|
||||
if material_name not in material_assignments:
|
||||
material_assignments[material_name] = set()
|
||||
|
||||
material_assignments[material_name].add(application_id)
|
||||
|
||||
return material_assignments
|
||||
|
||||
|
||||
def create_render_material_proxies(objects: List[Object]) -> List[RenderMaterialProxy]:
|
||||
material_assignments = collect_material_assignments(objects)
|
||||
|
||||
if not material_assignments:
|
||||
return []
|
||||
|
||||
proxies = []
|
||||
|
||||
for material_name, object_ids in material_assignments.items():
|
||||
blender_material = bpy.data.materials.get(material_name)
|
||||
if not blender_material:
|
||||
continue
|
||||
|
||||
speckle_material = blender_material_to_speckle(blender_material)
|
||||
|
||||
proxy = RenderMaterialProxy(objects=list(object_ids), value=speckle_material)
|
||||
|
||||
proxy.applicationId = get_unique_id(blender_material)
|
||||
|
||||
proxies.append(proxy)
|
||||
|
||||
return proxies
|
||||
|
||||
|
||||
def add_render_material_proxies_to_base(base: Base, objects: List[Object]) -> None:
|
||||
"""
|
||||
add render material proxies to the base object.
|
||||
"""
|
||||
proxies = create_render_material_proxies(objects)
|
||||
|
||||
if proxies:
|
||||
base.renderMaterialProxies = proxies
|
||||
@@ -0,0 +1,132 @@
|
||||
from typing import Dict, List, cast
|
||||
|
||||
import bpy
|
||||
from bpy.types import Mesh as BMesh
|
||||
from bpy.types import MeshPolygon, Object
|
||||
from mathutils import Matrix as MMatrix
|
||||
from mathutils import Vector as MVector
|
||||
|
||||
from specklepy.objects.base import Base
|
||||
from specklepy.objects.geometry.mesh import Mesh
|
||||
from .utils import get_submesh_id
|
||||
|
||||
|
||||
def mesh_to_speckle(
|
||||
blender_object: Object, data: bpy.types.Mesh, units_scale: float, units: str
|
||||
) -> Base:
|
||||
"""
|
||||
convert a Blender mesh object
|
||||
"""
|
||||
meshes = mesh_to_speckle_meshes(blender_object, data, units_scale, units)
|
||||
return meshes
|
||||
|
||||
|
||||
def mesh_to_speckle_meshes(
|
||||
blender_object: Object, data: bpy.types.Mesh, units_scale: float, units: str
|
||||
) -> List[Mesh]:
|
||||
"""
|
||||
convert a Blender mesh to a list of Speckle meshes
|
||||
each face corner (loop) gets its own vertex
|
||||
"""
|
||||
assert isinstance(data, BMesh), "Data must be a Blender mesh"
|
||||
assert units_scale > 0, "Units scale must be positive"
|
||||
|
||||
submesh_data: Dict[int, List[MeshPolygon]] = {}
|
||||
for p in data.polygons:
|
||||
if p.material_index not in submesh_data:
|
||||
submesh_data[p.material_index] = []
|
||||
submesh_data[p.material_index].append(p)
|
||||
|
||||
transform = cast(MMatrix, blender_object.matrix_world)
|
||||
normal_transform = transform.to_3x3().inverted().transposed()
|
||||
|
||||
submeshes = []
|
||||
|
||||
# sort material indices to ensure consistent ordering
|
||||
for material_index in sorted(submesh_data.keys()):
|
||||
mesh_area = 0
|
||||
m_verts: List[float] = []
|
||||
m_faces: List[int] = []
|
||||
m_texcoords: List[float] = []
|
||||
m_normals: List[float] = []
|
||||
|
||||
vertex_counter = 0
|
||||
|
||||
for face in submesh_data[material_index]:
|
||||
mesh_area += face.area
|
||||
|
||||
loop_indices = face.loop_indices
|
||||
m_faces.append(len(loop_indices))
|
||||
|
||||
for loop_index in loop_indices:
|
||||
loop = data.loops[loop_index]
|
||||
|
||||
vertex = data.vertices[loop.vertex_index]
|
||||
transformed_vertex = transform @ vertex.co * units_scale
|
||||
|
||||
m_verts.extend(
|
||||
[transformed_vertex.x, transformed_vertex.y, transformed_vertex.z]
|
||||
)
|
||||
|
||||
# get and transform the loop normal
|
||||
# try to get split normal, fallback to face normal if not available
|
||||
try:
|
||||
if hasattr(loop, "normal") and len(loop.normal) > 0:
|
||||
# Use split normal from loop
|
||||
loop_normal = normal_transform @ loop.normal
|
||||
else:
|
||||
# Fallback to face normal
|
||||
loop_normal = normal_transform @ face.normal
|
||||
except: # noqa: E722
|
||||
# Final fallback: use face normal
|
||||
loop_normal = normal_transform @ face.normal
|
||||
|
||||
loop_normal.normalize()
|
||||
m_normals.extend([loop_normal.x, loop_normal.y, loop_normal.z])
|
||||
|
||||
# add UV coordinates if available
|
||||
if data.uv_layers.active:
|
||||
uv_data = data.uv_layers.active.data[loop_index]
|
||||
uv = cast(MVector, uv_data.uv)
|
||||
m_texcoords.extend([uv.x, uv.y])
|
||||
|
||||
m_faces.append(vertex_counter)
|
||||
vertex_counter += 1
|
||||
|
||||
speckle_mesh = Mesh(
|
||||
vertices=m_verts,
|
||||
faces=m_faces,
|
||||
colors=[],
|
||||
textureCoordinates=m_texcoords,
|
||||
vertexNormals=m_normals,
|
||||
units=units,
|
||||
)
|
||||
|
||||
if len(m_verts) > 0:
|
||||
speckle_mesh.area = mesh_area
|
||||
|
||||
speckle_mesh.applicationId = get_submesh_id(blender_object, material_index)
|
||||
|
||||
submeshes.append(speckle_mesh)
|
||||
|
||||
return submeshes
|
||||
|
||||
|
||||
def is_closed_mesh(faces: List[int]) -> bool:
|
||||
"""
|
||||
check if a mesh is closed by verifying that each edge is shared by exactly 2 faces.
|
||||
"""
|
||||
edge_counts = {}
|
||||
|
||||
i = 0
|
||||
while i < len(faces):
|
||||
vertex_count = faces[i]
|
||||
for j in range(vertex_count):
|
||||
v1 = faces[i + 1 + j]
|
||||
v2 = faces[i + 1 + ((j + 1) % vertex_count)]
|
||||
edge = tuple(sorted([v1, v2]))
|
||||
edge_counts[edge] = edge_counts.get(edge, 0) + 1
|
||||
|
||||
i += vertex_count + 1
|
||||
|
||||
return all(count == 2 for count in edge_counts.values())
|
||||
@@ -0,0 +1,17 @@
|
||||
from bpy.types import Object
|
||||
from specklepy.objects.geometry import Point
|
||||
|
||||
|
||||
def point_to_speckle(blender_object: Object, scale_factor: float = 1.0) -> Point:
|
||||
assert blender_object.type == "EMPTY", "Object must be an empty."
|
||||
|
||||
location = blender_object.location
|
||||
|
||||
speckle_point = Point(
|
||||
x=location.x * scale_factor,
|
||||
y=location.y * scale_factor,
|
||||
z=location.z * scale_factor,
|
||||
units="", # TODO: implement units in object level
|
||||
)
|
||||
|
||||
return speckle_point
|
||||
@@ -0,0 +1,85 @@
|
||||
from bpy.types import Object
|
||||
from typing import Optional
|
||||
from specklepy.objects.data_objects import BlenderObject
|
||||
from .curve_to_speckle import curve_to_speckle
|
||||
from .mesh_to_speckle import mesh_to_speckle_meshes
|
||||
from .utils import get_object_id, get_curve_element_id
|
||||
|
||||
|
||||
def convert_to_speckle(
|
||||
blender_object: Object,
|
||||
scale_factor: float = 1.0,
|
||||
units: str = "m",
|
||||
apply_modifiers: bool = True,
|
||||
) -> Optional[BlenderObject]:
|
||||
display_value = []
|
||||
properties = {}
|
||||
|
||||
if blender_object.type == "CURVE":
|
||||
# handle curve modifiers apply_modifiers is True
|
||||
if apply_modifiers and blender_object.modifiers:
|
||||
import bpy
|
||||
|
||||
# Convert curve with modifiers to mesh
|
||||
depsgraph = bpy.context.evaluated_depsgraph_get()
|
||||
evaluated_obj = blender_object.evaluated_get(depsgraph)
|
||||
evaluated_mesh = evaluated_obj.to_mesh()
|
||||
|
||||
if evaluated_mesh:
|
||||
meshes = mesh_to_speckle_meshes(
|
||||
blender_object, evaluated_mesh, scale_factor, units
|
||||
)
|
||||
blender_object.to_mesh_clear()
|
||||
if meshes:
|
||||
display_value = meshes
|
||||
else:
|
||||
# curve conversion without modifiers
|
||||
curve_result = curve_to_speckle(blender_object, scale_factor)
|
||||
if curve_result and hasattr(curve_result, "@elements"):
|
||||
display_value = curve_result["@elements"]
|
||||
for i, element in enumerate(display_value):
|
||||
if hasattr(element, "applicationId"):
|
||||
element.applicationId = get_curve_element_id(blender_object, i)
|
||||
elif curve_result:
|
||||
if hasattr(curve_result, "applicationId"):
|
||||
curve_result.applicationId = get_curve_element_id(blender_object, 0)
|
||||
display_value = [curve_result]
|
||||
|
||||
elif blender_object.type == "MESH":
|
||||
# get mesh data - apply modifiers if requested
|
||||
mesh_data = blender_object.data
|
||||
if apply_modifiers and blender_object.modifiers:
|
||||
import bpy
|
||||
|
||||
# use evaluated object to get mesh with modifiers applied
|
||||
depsgraph = bpy.context.evaluated_depsgraph_get()
|
||||
evaluated_obj = blender_object.evaluated_get(depsgraph)
|
||||
evaluated_mesh = evaluated_obj.to_mesh()
|
||||
mesh_data = evaluated_mesh
|
||||
|
||||
meshes = mesh_to_speckle_meshes(blender_object, mesh_data, scale_factor, units)
|
||||
|
||||
if (
|
||||
apply_modifiers
|
||||
and blender_object.modifiers
|
||||
and mesh_data != blender_object.data
|
||||
):
|
||||
blender_object.to_mesh_clear()
|
||||
|
||||
if meshes:
|
||||
display_value = meshes
|
||||
|
||||
if not display_value:
|
||||
return None
|
||||
|
||||
if not isinstance(display_value, list):
|
||||
display_value = [display_value]
|
||||
|
||||
return BlenderObject(
|
||||
name=blender_object.name,
|
||||
type=blender_object.type,
|
||||
displayValue=display_value,
|
||||
applicationId=get_object_id(blender_object),
|
||||
properties=properties,
|
||||
units=units,
|
||||
)
|
||||
@@ -0,0 +1,242 @@
|
||||
import bpy
|
||||
from bpy.types import ID, Object
|
||||
import math
|
||||
from typing import Tuple, Optional
|
||||
|
||||
OBJECT_NAME_SPECKLE_SEPARATOR = " -- "
|
||||
SPECKLE_ID_LENGTH = 32
|
||||
_QUICK_TEST_NAME_LENGTH = SPECKLE_ID_LENGTH + len(OBJECT_NAME_SPECKLE_SEPARATOR)
|
||||
|
||||
|
||||
def to_speckle_name(blender_object: bpy.types.ID) -> str:
|
||||
does_name_contain_id = (
|
||||
len(blender_object.name) > _QUICK_TEST_NAME_LENGTH
|
||||
and OBJECT_NAME_SPECKLE_SEPARATOR in blender_object.name
|
||||
)
|
||||
if does_name_contain_id:
|
||||
return blender_object.name.rsplit(OBJECT_NAME_SPECKLE_SEPARATOR, 1)[0]
|
||||
else:
|
||||
return blender_object.name
|
||||
|
||||
|
||||
"""
|
||||
Python implementation of Blender's NURBS curve generation for to Speckle conversion
|
||||
from: https://blender.stackexchange.com/a/34276
|
||||
based on https://projects.blender.org/blender/blender/src/branch/main/source/blender/blenkernel/intern/curve.cc (check old version)
|
||||
"""
|
||||
|
||||
|
||||
def macro_knotsu(nu: bpy.types.Spline) -> int:
|
||||
return nu.order_u + nu.point_count_u + (nu.order_u - 1 if nu.use_cyclic_u else 0)
|
||||
|
||||
|
||||
def macro_segmentsu(nu: bpy.types.Spline) -> int:
|
||||
return nu.point_count_u if nu.use_cyclic_u else nu.point_count_u - 1
|
||||
|
||||
|
||||
def make_knots(nu: bpy.types.Spline) -> list[float]:
|
||||
knots = [0.0] * macro_knotsu(nu)
|
||||
flag = nu.use_endpoint_u + (nu.use_bezier_u << 1)
|
||||
if nu.use_cyclic_u:
|
||||
calc_knots(knots, nu.point_count_u, nu.order_u, 0)
|
||||
else:
|
||||
calc_knots(knots, nu.point_count_u, nu.order_u, flag)
|
||||
return knots
|
||||
|
||||
|
||||
def calc_knots(knots: list[float], point_count: int, order: int, flag: int) -> None:
|
||||
pts_order = point_count + order
|
||||
if flag == 1: # CU_NURB_ENDPOINT
|
||||
k = 0.0
|
||||
for a in range(1, pts_order + 1):
|
||||
knots[a - 1] = k
|
||||
if a >= order and a <= point_count:
|
||||
k += 1.0
|
||||
elif flag == 2: # CU_NURB_BEZIER
|
||||
if order == 4:
|
||||
k = 0.34
|
||||
for a in range(pts_order):
|
||||
knots[a] = math.floor(k)
|
||||
k += 1.0 / 3.0
|
||||
elif order == 3:
|
||||
k = 0.6
|
||||
for a in range(pts_order):
|
||||
if a >= order and a <= point_count:
|
||||
k += 0.5
|
||||
knots[a] = math.floor(k)
|
||||
else:
|
||||
for a in range(1, len(knots) - 1):
|
||||
knots[a] = a - 1
|
||||
|
||||
knots[-1] = knots[-2]
|
||||
|
||||
|
||||
def basis_nurb(
|
||||
t: float,
|
||||
order: int,
|
||||
point_count: int,
|
||||
knots: list[float],
|
||||
basis: list[float],
|
||||
start: int,
|
||||
end: int,
|
||||
) -> Tuple[int, int]:
|
||||
i1 = i2 = 0
|
||||
orderpluspnts = order + point_count
|
||||
opp2 = orderpluspnts - 1
|
||||
|
||||
# this is for float inaccuracy
|
||||
if t < knots[0]:
|
||||
t = knots[0]
|
||||
elif t > knots[opp2]:
|
||||
t = knots[opp2]
|
||||
|
||||
# this part is order '1'
|
||||
o2 = order + 1
|
||||
for i in range(opp2):
|
||||
if knots[i] != knots[i + 1] and t >= knots[i] and t <= knots[i + 1]:
|
||||
basis[i] = 1.0
|
||||
i1 = i - o2
|
||||
if i1 < 0:
|
||||
i1 = 0
|
||||
i2 = i
|
||||
i += 1
|
||||
while i < opp2:
|
||||
basis[i] = 0.0
|
||||
i += 1
|
||||
break
|
||||
|
||||
else:
|
||||
basis[i] = 0.0
|
||||
|
||||
basis[i] = 0.0 # type: ignore
|
||||
|
||||
# this is order 2, 3, ...
|
||||
for j in range(2, order + 1):
|
||||
if i2 + j >= orderpluspnts:
|
||||
i2 = opp2 - j
|
||||
|
||||
for i in range(i1, i2 + 1):
|
||||
if basis[i] != 0.0:
|
||||
d = ((t - knots[i]) * basis[i]) / (knots[i + j - 1] - knots[i])
|
||||
else:
|
||||
d = 0.0
|
||||
|
||||
if basis[i + 1] != 0.0:
|
||||
e = ((knots[i + j] - t) * basis[i + 1]) / (knots[i + j] - knots[i + 1])
|
||||
else:
|
||||
e = 0.0
|
||||
|
||||
basis[i] = d + e
|
||||
|
||||
start = 1000
|
||||
end = 0
|
||||
|
||||
for i in range(i1, i2 + 1):
|
||||
if basis[i] > 0.0:
|
||||
end = i
|
||||
if start == 1000:
|
||||
start = i
|
||||
|
||||
return start, end
|
||||
|
||||
|
||||
def nurb_make_curve(nu: bpy.types.Spline, resolu: int, stride: int = 3) -> list[float]:
|
||||
""" "BKE_nurb_makeCurve"""
|
||||
EPS = 1e-6
|
||||
coord_index = istart = iend = 0
|
||||
|
||||
coord_array = [0.0] * (3 * nu.resolution_u * macro_segmentsu(nu))
|
||||
sum_array = [0] * nu.point_count_u
|
||||
basisu = [0.0] * macro_knotsu(nu)
|
||||
knots = make_knots(nu)
|
||||
|
||||
resolu = resolu * macro_segmentsu(nu)
|
||||
ustart = knots[nu.order_u - 1]
|
||||
uend = (
|
||||
knots[nu.point_count_u + nu.order_u - 1]
|
||||
if nu.use_cyclic_u
|
||||
else knots[nu.point_count_u]
|
||||
)
|
||||
ustep = (uend - ustart) / (resolu - (0 if nu.use_cyclic_u else 1))
|
||||
cycl = nu.order_u - 1 if nu.use_cyclic_u else 0
|
||||
|
||||
u = ustart
|
||||
while resolu:
|
||||
resolu -= 1
|
||||
istart, iend = basis_nurb(
|
||||
u, nu.order_u, nu.point_count_u + cycl, knots, basisu, istart, iend
|
||||
)
|
||||
|
||||
# /* calc sum */
|
||||
sumdiv = 0.0
|
||||
sum_index = 0
|
||||
pt_index = istart - 1
|
||||
for i in range(istart, iend + 1):
|
||||
if i >= nu.point_count_u:
|
||||
pt_index = i - nu.point_count_u
|
||||
else:
|
||||
pt_index += 1
|
||||
|
||||
sum_array[sum_index] = basisu[i] * nu.points[pt_index].co[3] # type: ignore
|
||||
sumdiv += sum_array[sum_index]
|
||||
sum_index += 1
|
||||
|
||||
if (sumdiv != 0.0) and (sumdiv < 1.0 - EPS or sumdiv > 1.0 + EPS):
|
||||
sum_index = 0
|
||||
for i in range(istart, iend + 1):
|
||||
sum_array[sum_index] /= sumdiv # type: ignore
|
||||
sum_index += 1
|
||||
|
||||
coord_array[coord_index : coord_index + 3] = (0.0, 0.0, 0.0)
|
||||
|
||||
sum_index = 0
|
||||
pt_index = istart - 1
|
||||
for i in range(istart, iend + 1):
|
||||
if i >= nu.point_count_u:
|
||||
pt_index = i - nu.point_count_u
|
||||
else:
|
||||
pt_index += 1
|
||||
|
||||
if sum_array[sum_index] != 0.0:
|
||||
for j in range(3):
|
||||
coord_array[coord_index + j] += (
|
||||
sum_array[sum_index] * nu.points[pt_index].co[j]
|
||||
)
|
||||
sum_index += 1
|
||||
|
||||
coord_index += stride
|
||||
u += ustep
|
||||
|
||||
return coord_array
|
||||
|
||||
|
||||
def get_unique_id(native_object: ID, suffix: Optional[str] = None) -> str:
|
||||
base_id = f"{type(native_object).__name__}:{native_object.name_full}"
|
||||
|
||||
if suffix:
|
||||
return f"{base_id}:{suffix}"
|
||||
|
||||
return base_id
|
||||
|
||||
|
||||
def get_submesh_id(blender_object: Object, material_index: int) -> str:
|
||||
mesh_data = blender_object.data
|
||||
if not mesh_data:
|
||||
return f"Mesh:{blender_object.name_full}_mat{material_index}"
|
||||
|
||||
return f"Mesh:{mesh_data.name_full}_mat{material_index}"
|
||||
|
||||
|
||||
def get_curve_element_id(blender_object: Object, curve_index: int = 0) -> str:
|
||||
curve_data = blender_object.data
|
||||
if not curve_data:
|
||||
return f"Curve:{blender_object.name_full}_curve{curve_index}"
|
||||
|
||||
if curve_index == 0:
|
||||
return f"Curve:{curve_data.name_full}"
|
||||
|
||||
return f"Curve:{curve_data.name_full}_curve{curve_index}"
|
||||
|
||||
|
||||
def get_object_id(blender_object: Object) -> str:
|
||||
return get_unique_id(blender_object)
|
||||
@@ -0,0 +1,226 @@
|
||||
from typing import Tuple, List, Optional, Dict
|
||||
import bpy
|
||||
import mathutils
|
||||
from specklepy.objects import Base
|
||||
from specklepy.objects.graph_traversal.default_traversal import (
|
||||
create_default_traversal_function,
|
||||
)
|
||||
from specklepy.core.api.client import SpeckleClient
|
||||
|
||||
|
||||
def to_rgba(argb_int: int) -> Tuple[float, float, float, float]:
|
||||
"""
|
||||
converts the int representation of a colour into a RGBA tuple
|
||||
"""
|
||||
alpha = ((argb_int >> 24) & 255) / 255
|
||||
red = ((argb_int >> 16) & 255) / 255
|
||||
green = ((argb_int >> 8) & 255) / 255
|
||||
blue = (argb_int & 255) / 255
|
||||
return (red, green, blue, alpha)
|
||||
|
||||
|
||||
def to_argb_int(rgba_color: List[float]) -> int:
|
||||
"""
|
||||
converts an RGBA array to an ARGB integer
|
||||
"""
|
||||
argb_color = rgba_color[-1:] + rgba_color[:3]
|
||||
int_color = [int(val * 255) for val in argb_color]
|
||||
return int.from_bytes(int_color, byteorder="big", signed=True)
|
||||
|
||||
|
||||
def create_material_from_proxy(
|
||||
render_material, material_name: str
|
||||
) -> bpy.types.Material:
|
||||
"""
|
||||
creates a Blender material from a Speckle RenderMaterial
|
||||
"""
|
||||
if material_name in bpy.data.materials:
|
||||
return bpy.data.materials[material_name]
|
||||
|
||||
# create new material
|
||||
material = bpy.data.materials.new(name=material_name)
|
||||
material.use_nodes = True
|
||||
node_tree = material.node_tree
|
||||
nodes = node_tree.nodes
|
||||
|
||||
for node in nodes:
|
||||
nodes.remove(node)
|
||||
|
||||
bsdf = nodes.new(type="ShaderNodeBsdfPrincipled")
|
||||
output = nodes.new(type="ShaderNodeOutputMaterial")
|
||||
|
||||
node_tree.links.new(bsdf.outputs["BSDF"], output.inputs["Surface"])
|
||||
|
||||
if hasattr(render_material, "diffuse"):
|
||||
diffuse_rgba = to_rgba(render_material.diffuse)
|
||||
bsdf.inputs["Base Color"].default_value = (
|
||||
diffuse_rgba[0],
|
||||
diffuse_rgba[1],
|
||||
diffuse_rgba[2],
|
||||
1.0,
|
||||
)
|
||||
|
||||
if hasattr(render_material, "opacity"):
|
||||
opacity = float(render_material.opacity)
|
||||
if opacity < 1.0:
|
||||
material.blend_method = "BLEND"
|
||||
bsdf.inputs["Alpha"].default_value = opacity
|
||||
|
||||
if hasattr(render_material, "metalness"):
|
||||
metalness = float(render_material.metalness)
|
||||
bsdf.inputs["Metallic"].default_value = metalness
|
||||
|
||||
if hasattr(render_material, "roughness"):
|
||||
roughness = float(render_material.roughness)
|
||||
bsdf.inputs["Roughness"].default_value = roughness
|
||||
|
||||
if (
|
||||
hasattr(render_material, "emissive") and render_material.emissive != -16777216
|
||||
): # default black
|
||||
emissive_rgba = to_rgba(render_material.emissive)
|
||||
# only add emission if it's not black (default)
|
||||
if any(val > 0.01 for val in emissive_rgba[:3]):
|
||||
bsdf.inputs["Emission Color"].default_value = (
|
||||
emissive_rgba[0],
|
||||
emissive_rgba[1],
|
||||
emissive_rgba[2],
|
||||
1.0,
|
||||
)
|
||||
bsdf.inputs["Emission Strength"].default_value = 1.0
|
||||
|
||||
# set viewport display color
|
||||
if hasattr(render_material, "diffuse") and hasattr(render_material, "opacity"):
|
||||
material.diffuse_color = (
|
||||
diffuse_rgba[0],
|
||||
diffuse_rgba[1],
|
||||
diffuse_rgba[2],
|
||||
opacity,
|
||||
)
|
||||
|
||||
return material
|
||||
|
||||
|
||||
def transform_matrix(transform: List[float]) -> mathutils.Matrix:
|
||||
"""
|
||||
converts a speckle transform array to a 4x4 matrix (blender needs it)
|
||||
"""
|
||||
|
||||
if len(transform) != 16:
|
||||
raise ValueError(f"Expected transform with 16 values, got {len(transform)}")
|
||||
|
||||
return mathutils.Matrix(
|
||||
(
|
||||
(transform[0], transform[4], transform[8], transform[12]),
|
||||
(transform[1], transform[5], transform[9], transform[13]),
|
||||
(transform[2], transform[6], transform[10], transform[14]),
|
||||
(transform[3], transform[7], transform[11], transform[15]),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def build_object_id_map(root_object: Base) -> Dict[str, Base]:
|
||||
"""
|
||||
Builds a dictionary mapping object IDs (both id and applicationId) to objects.
|
||||
"""
|
||||
id_map = {}
|
||||
traversal_function = create_default_traversal_function()
|
||||
|
||||
for traversal_item in traversal_function.traverse(root_object):
|
||||
obj = traversal_item.current
|
||||
|
||||
if hasattr(obj, "id") and obj.id:
|
||||
id_map[obj.id] = obj
|
||||
|
||||
if hasattr(obj, "applicationId") and obj.applicationId:
|
||||
id_map[obj.applicationId] = obj
|
||||
|
||||
return id_map
|
||||
|
||||
|
||||
def find_object_by_id(root_object: Base, target_id: str) -> Optional[Base]:
|
||||
"""
|
||||
finds an object using traversal, checking both id and applicationId
|
||||
"""
|
||||
if hasattr(root_object, "__closure") and root_object.__closure:
|
||||
if target_id in root_object.__closure:
|
||||
if hasattr(root_object, "elements"):
|
||||
for element in root_object.elements:
|
||||
if hasattr(element, "id") and element.id == target_id:
|
||||
return element
|
||||
if (
|
||||
hasattr(element, "referencedId")
|
||||
and element.referencedId == target_id
|
||||
):
|
||||
return find_object_by_id(root_object, element.referencedId)
|
||||
|
||||
if hasattr(root_object, "@elements"):
|
||||
for element in root_object["@elements"]:
|
||||
if hasattr(element, "id") and element.id == target_id:
|
||||
return element
|
||||
if (
|
||||
hasattr(element, "referencedId")
|
||||
and element.referencedId == target_id
|
||||
):
|
||||
return find_object_by_id(root_object, element.referencedId)
|
||||
|
||||
traversal_function = create_default_traversal_function()
|
||||
|
||||
for traversal_item in traversal_function.traverse(root_object):
|
||||
obj = traversal_item.current
|
||||
|
||||
if not hasattr(obj, "id"):
|
||||
continue
|
||||
|
||||
if obj.id == target_id:
|
||||
return obj
|
||||
|
||||
if hasattr(obj, "applicationId"):
|
||||
app_id = obj.applicationId
|
||||
if app_id == target_id:
|
||||
return obj
|
||||
|
||||
def deep_search(search_obj):
|
||||
if hasattr(search_obj, "id") and search_obj.id == target_id:
|
||||
return search_obj
|
||||
|
||||
elements_attrs = ["elements", "@elements"]
|
||||
for attr in elements_attrs:
|
||||
if hasattr(search_obj, attr):
|
||||
elements = getattr(search_obj, attr)
|
||||
if elements and isinstance(elements, list):
|
||||
for element in elements:
|
||||
if hasattr(element, "id") and element.id == target_id:
|
||||
return element
|
||||
if (
|
||||
hasattr(element, "referencedId")
|
||||
and element.referencedId == target_id
|
||||
):
|
||||
ref_obj = find_object_by_id(
|
||||
root_object, element.referencedId
|
||||
)
|
||||
if ref_obj:
|
||||
return ref_obj
|
||||
result = deep_search(element)
|
||||
if result:
|
||||
return result
|
||||
return None
|
||||
|
||||
return deep_search(root_object)
|
||||
|
||||
|
||||
def get_project_workspace_id(client: SpeckleClient, project_id: str) -> Optional[str]:
|
||||
workspace_id = None
|
||||
server_version = client.project.server_version or client.server.version()
|
||||
|
||||
# Local yarn builds of server will report a server version if "dev"
|
||||
# We'll assume that local builds are up-to-date with the latest features
|
||||
if server_version[0] == "dev":
|
||||
maj = 999
|
||||
min = 999
|
||||
else:
|
||||
maj = server_version[0]
|
||||
min = server_version[1]
|
||||
|
||||
if maj > 2 or (maj == 2 and min > 20):
|
||||
workspace_id = client.project.get(project_id).workspace_id
|
||||
return workspace_id
|
||||
@@ -1,104 +0,0 @@
|
||||
from specklepy.api.client import SpeckleClient
|
||||
from bpy_speckle.clients import speckle_clients
|
||||
|
||||
"""
|
||||
Speckle functions
|
||||
"""
|
||||
|
||||
unit_scale = {
|
||||
"meters": 1.0,
|
||||
"centimeters": 0.01,
|
||||
"millimeters": 0.001,
|
||||
"inches": 0.0254,
|
||||
"feet": 0.3048,
|
||||
"kilometers": 1000.0,
|
||||
"mm": 0.001,
|
||||
"cm": 0.01,
|
||||
"m": 1.0,
|
||||
"km": 1000.0,
|
||||
"in": 0.0254,
|
||||
"ft": 0.3048,
|
||||
"yd": 0.9144,
|
||||
"mi": 1609.340,
|
||||
}
|
||||
|
||||
"""
|
||||
Utility functions
|
||||
"""
|
||||
|
||||
|
||||
def _report(msg):
|
||||
"""
|
||||
Function for printing messages to the console
|
||||
"""
|
||||
print("SpeckleBlender: {}".format(msg))
|
||||
|
||||
|
||||
def get_scale_length(units):
|
||||
if units.lower() in unit_scale.keys():
|
||||
return unit_scale[units]
|
||||
_report("Units <{}> are not supported.".format(units))
|
||||
return 1.0
|
||||
|
||||
|
||||
"""
|
||||
Client, user, and stream functions
|
||||
"""
|
||||
|
||||
|
||||
def _check_speckle_client_user_stream(scene):
|
||||
"""
|
||||
Verify that there is a valid user and stream
|
||||
"""
|
||||
speckle = scene.speckle
|
||||
|
||||
user = (
|
||||
speckle.users[int(speckle.active_user)]
|
||||
if len(speckle.users) > int(speckle.active_user)
|
||||
else None
|
||||
)
|
||||
|
||||
if user is None:
|
||||
print("No users loaded.")
|
||||
|
||||
stream = (
|
||||
user.streams[user.active_stream]
|
||||
if len(user.streams) > user.active_stream
|
||||
else None
|
||||
)
|
||||
|
||||
if stream is None:
|
||||
print("Account contains no streams.")
|
||||
|
||||
return (user, stream)
|
||||
|
||||
|
||||
def _create_stream(user, stream_name, units="Millimeters"):
|
||||
"""
|
||||
Create a new stream
|
||||
"""
|
||||
|
||||
# TODO: double-check, but this should not be accessible through the UI if
|
||||
# there aren't any active users anyway
|
||||
# user = context.scene.speckle.users[int(context.scene.speckle.active_user)]
|
||||
client = speckle_clients[int(bpy.context.scene.speckle.active_user)]
|
||||
return client.stream.create(
|
||||
name=stream_name, description="This is a Blender stream.", is_public=True
|
||||
)
|
||||
|
||||
# TODO: Update stream with properties such as units, etc.
|
||||
|
||||
|
||||
def _delete_stream(client, user, stream):
|
||||
"""
|
||||
Delete the active stream
|
||||
TODO: probably doesn't need to be a separate function and can be
|
||||
folded into the operator
|
||||
"""
|
||||
|
||||
user = context.scene.speckle.users[int(context.scene.speckle.active_user)]
|
||||
client = speckle_clients[int(context.scene.speckle.active_user)]
|
||||
|
||||
if stream:
|
||||
res = client.streams.delete(stream.id)
|
||||
_report(res["message"])
|
||||
@@ -1,104 +0,0 @@
|
||||
import os, sys, bpy
|
||||
import ctypes, sys
|
||||
|
||||
import os, sys
|
||||
|
||||
|
||||
def modules_path():
|
||||
# set up addons/modules under the user
|
||||
# script path. Here we'll install the
|
||||
# dependencies
|
||||
modulespath = os.path.normpath(
|
||||
os.path.join(bpy.utils.script_path_user(), "addons", "modules")
|
||||
)
|
||||
if not os.path.exists(modulespath):
|
||||
os.makedirs(modulespath)
|
||||
|
||||
# set user modules path at beginning of paths for earlier hit
|
||||
if sys.path[1] != modulespath:
|
||||
sys.path.insert(1, modulespath)
|
||||
|
||||
return modulespath
|
||||
|
||||
|
||||
def install_dependencies():
|
||||
import sys
|
||||
import os
|
||||
|
||||
try:
|
||||
try:
|
||||
import pip
|
||||
except:
|
||||
print("Installing pip... "),
|
||||
from subprocess import run as sprun
|
||||
|
||||
res = sprun([bpy.app.binary_path_python, "-m", "ensurepip"])
|
||||
|
||||
if res.returncode == 0:
|
||||
import pip
|
||||
else:
|
||||
raise Exception("Failed to install pip.")
|
||||
|
||||
modulespath = modules_path()
|
||||
|
||||
if not os.path.exists(modulespath):
|
||||
os.makedirs(modulespath)
|
||||
|
||||
print("Installing speckle to {}... ".format(modulespath)),
|
||||
from subprocess import run as sprun
|
||||
|
||||
res = sprun(
|
||||
[
|
||||
bpy.app.binary_path_python,
|
||||
"-m",
|
||||
"pip",
|
||||
"install",
|
||||
"-q",
|
||||
"-t",
|
||||
"{}".format(modulespath),
|
||||
"--no-deps",
|
||||
"pydantic",
|
||||
]
|
||||
)
|
||||
res = sprun(
|
||||
[
|
||||
bpy.app.binary_path_python,
|
||||
"-m",
|
||||
"pip",
|
||||
"install",
|
||||
"-q",
|
||||
"-t",
|
||||
"{}".format(modulespath),
|
||||
"specklepy",
|
||||
]
|
||||
)
|
||||
|
||||
except:
|
||||
raise Exception(
|
||||
"Failed to install dependencies. Please make sure you have pip installed."
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
import specklepy
|
||||
except:
|
||||
print("Failed to load speckle.")
|
||||
from sys import platform
|
||||
|
||||
if platform == "win32":
|
||||
if ctypes.windll.shell32.IsUserAnAdmin():
|
||||
install_dependencies()
|
||||
import specklepy
|
||||
else:
|
||||
ctypes.windll.shell32.ShellExecuteW(
|
||||
None, "runas", sys.executable, __file__, None, 1
|
||||
)
|
||||
|
||||
else:
|
||||
print(
|
||||
"Platform {} cannot automatically install dependencies.".format(
|
||||
platform
|
||||
)
|
||||
)
|
||||
raise
|
||||
@@ -0,0 +1,227 @@
|
||||
"""
|
||||
Provides uniform and consistent path helpers for `specklepy`
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
from importlib import import_module, invalidate_caches
|
||||
|
||||
_user_data_env_var = "SPECKLE_USERDATA_PATH"
|
||||
|
||||
|
||||
def _path() -> Optional[Path]:
|
||||
"""Read the user data path override setting."""
|
||||
path_override = os.environ.get(_user_data_env_var)
|
||||
if path_override:
|
||||
return Path(path_override)
|
||||
return None
|
||||
|
||||
|
||||
_application_name = "Speckle"
|
||||
|
||||
|
||||
def override_application_name(application_name: str) -> None:
|
||||
"""Override the global Speckle application name."""
|
||||
global _application_name
|
||||
_application_name = application_name
|
||||
|
||||
|
||||
def override_application_data_path(path: Optional[str]) -> None:
|
||||
"""
|
||||
Override the global Speckle application data path.
|
||||
|
||||
If the value of path is `None` the environment variable gets deleted.
|
||||
"""
|
||||
if path:
|
||||
os.environ[_user_data_env_var] = path
|
||||
else:
|
||||
os.environ.pop(_user_data_env_var, None)
|
||||
|
||||
|
||||
def _ensure_folder_exists(base_path: Path, folder_name: str) -> Path:
|
||||
path = base_path.joinpath(folder_name)
|
||||
path.mkdir(exist_ok=True, parents=True)
|
||||
return path
|
||||
|
||||
|
||||
def user_application_data_path() -> Path:
|
||||
"""Get the platform specific user configuration folder path"""
|
||||
path_override = _path()
|
||||
if path_override:
|
||||
return path_override
|
||||
|
||||
try:
|
||||
if sys.platform.startswith("win"):
|
||||
app_data_path = os.getenv("APPDATA")
|
||||
if not app_data_path:
|
||||
raise Exception("Cannot get appdata path from environment.")
|
||||
return Path(app_data_path)
|
||||
else:
|
||||
# try getting the standard XDG_DATA_HOME value
|
||||
# as that is used as an override
|
||||
app_data_path = os.getenv("XDG_DATA_HOME")
|
||||
if app_data_path:
|
||||
return Path(app_data_path)
|
||||
else:
|
||||
return _ensure_folder_exists(Path.home(), ".config")
|
||||
except Exception as ex:
|
||||
raise Exception("Failed to initialize user application data path.", ex)
|
||||
|
||||
|
||||
def user_speckle_folder_path() -> Path:
|
||||
"""Get the folder where the user's Speckle data should be stored."""
|
||||
return _ensure_folder_exists(user_application_data_path(), _application_name)
|
||||
|
||||
|
||||
def user_speckle_connector_installation_path(host_application: str) -> Path:
|
||||
"""
|
||||
Gets a connector specific installation folder.
|
||||
|
||||
In this folder we can put our connector installation and all python packages.
|
||||
"""
|
||||
return _ensure_folder_exists(
|
||||
_ensure_folder_exists(user_speckle_folder_path(), "connector_installations"),
|
||||
host_application,
|
||||
)
|
||||
|
||||
|
||||
print("Starting module dependency installation")
|
||||
print(sys.executable)
|
||||
|
||||
PYTHON_PATH = sys.executable
|
||||
|
||||
|
||||
def connector_installation_path(host_application: str) -> Path:
|
||||
connector_installation_path = user_speckle_connector_installation_path(
|
||||
host_application
|
||||
)
|
||||
connector_installation_path.mkdir(exist_ok=True, parents=True)
|
||||
|
||||
# set user modules path at beginning of paths for earlier hit
|
||||
if sys.path[0] != connector_installation_path:
|
||||
sys.path.insert(0, str(connector_installation_path))
|
||||
|
||||
print(f"Using connector installation path {connector_installation_path}")
|
||||
return connector_installation_path
|
||||
|
||||
|
||||
def is_pip_available() -> bool:
|
||||
try:
|
||||
import_module("pip") # noqa F401
|
||||
return True
|
||||
except ImportError:
|
||||
return False
|
||||
|
||||
|
||||
def ensure_pip() -> None:
|
||||
print("Installing pip... ")
|
||||
|
||||
from subprocess import run
|
||||
|
||||
completed_process = run([PYTHON_PATH, "-m", "ensurepip"])
|
||||
|
||||
if completed_process.returncode == 0:
|
||||
print("Successfully installed pip")
|
||||
else:
|
||||
raise Exception(
|
||||
f"Failed to install pip, got {completed_process.returncode} return code"
|
||||
)
|
||||
|
||||
|
||||
def get_requirements_path() -> Path:
|
||||
# we assume that a requirements.txt exists next to the __init__.py file
|
||||
path = Path(Path(__file__).parent, "requirements.txt")
|
||||
return path
|
||||
|
||||
|
||||
def install_requirements(host_application: str) -> None:
|
||||
# set up addons/modules under the user
|
||||
# script path. Here we'll install the
|
||||
# dependencies
|
||||
path = connector_installation_path(host_application)
|
||||
|
||||
from subprocess import run
|
||||
|
||||
def debugger_is_active() -> bool:
|
||||
"""Return if the debugger is currently active"""
|
||||
return hasattr(sys, "gettrace") and sys.gettrace() is not None
|
||||
|
||||
requirements_path = get_requirements_path()
|
||||
|
||||
is_debug = debugger_is_active()
|
||||
|
||||
if not is_debug and not requirements_path.exists():
|
||||
print("Skipped installing dependencies")
|
||||
return
|
||||
|
||||
print(f"Installing Speckle dependencies to {path}")
|
||||
completed_process = run(
|
||||
[
|
||||
PYTHON_PATH,
|
||||
"-m",
|
||||
"pip",
|
||||
"-q",
|
||||
"--disable-pip-version-check",
|
||||
"install",
|
||||
"--prefer-binary",
|
||||
"--ignore-installed",
|
||||
"--no-compile",
|
||||
"--no-deps",
|
||||
"-t",
|
||||
str(path),
|
||||
"-r",
|
||||
str(requirements_path),
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
|
||||
if completed_process.returncode != 0:
|
||||
print(completed_process.stdout)
|
||||
print(completed_process.stderr)
|
||||
m = f"Failed to install dependencies through pip, got {completed_process.returncode} return code"
|
||||
print(m)
|
||||
raise Exception(m)
|
||||
|
||||
print("Successfully installed dependencies")
|
||||
|
||||
if not is_debug:
|
||||
requirements_path.unlink()
|
||||
|
||||
|
||||
def install_dependencies(host_application: str) -> None:
|
||||
if not is_pip_available():
|
||||
ensure_pip()
|
||||
|
||||
install_requirements(host_application)
|
||||
|
||||
|
||||
def _import_dependencies() -> None:
|
||||
import_module("specklepy")
|
||||
# the code above doesn't work for now, it fails on importing graphql-core
|
||||
# despite that, the connector seams to be working as expected
|
||||
# But it would be nice to make this solution work
|
||||
# it would ensure that all dependencies are fully loaded
|
||||
# requirements = get_requirements_path().read_text()
|
||||
# reqs = [
|
||||
# req.split(" ; ")[0].split("==")[0].split("[")[0].replace("-", "_")
|
||||
# for req in requirements.split("\n")
|
||||
# if req and not req.startswith(" ")
|
||||
# ]
|
||||
# for req in reqs:
|
||||
# print(req)
|
||||
# import_module("specklepy")
|
||||
|
||||
|
||||
def ensure_dependencies(host_application: str) -> None:
|
||||
try:
|
||||
install_dependencies(host_application)
|
||||
invalidate_caches()
|
||||
_import_dependencies()
|
||||
print("Successfully found dependencies")
|
||||
except ImportError:
|
||||
raise Exception(
|
||||
f"Cannot automatically ensure Speckle dependencies. Please try restarting the host application {host_application}!"
|
||||
)
|
||||
@@ -1,65 +0,0 @@
|
||||
from .users import LoadUsers, LoadUserStreams
|
||||
from .object import (
|
||||
UpdateObject,
|
||||
ResetObject,
|
||||
DeleteObject,
|
||||
UploadObject,
|
||||
UploadNgonsAsPolylines,
|
||||
SelectIfSameCustomProperty,
|
||||
SelectIfHasCustomProperty,
|
||||
)
|
||||
from .streams import (
|
||||
ReceiveStreamObjects,
|
||||
SendStreamObjects,
|
||||
ViewStreamDataApi,
|
||||
DeleteStream,
|
||||
SelectOrphanObjects,
|
||||
)
|
||||
from .streams import (
|
||||
UpdateGlobal,
|
||||
AddStreamFromURL,
|
||||
CreateStream,
|
||||
CopyStreamId,
|
||||
CopyCommitId,
|
||||
CopyBranchName,
|
||||
)
|
||||
from .commit import DeleteCommit
|
||||
from .misc import OpenSpeckleGuide, OpenSpeckleTutorials, OpenSpeckleForum
|
||||
|
||||
operator_classes = [
|
||||
LoadUsers,
|
||||
ReceiveStreamObjects,
|
||||
SendStreamObjects,
|
||||
LoadUserStreams,
|
||||
CopyStreamId,
|
||||
CopyCommitId,
|
||||
CopyBranchName,
|
||||
]
|
||||
|
||||
operator_classes.extend([DeleteCommit])
|
||||
|
||||
operator_classes.extend(
|
||||
[
|
||||
UpdateObject,
|
||||
ResetObject,
|
||||
DeleteObject,
|
||||
UploadObject,
|
||||
UploadNgonsAsPolylines,
|
||||
SelectIfSameCustomProperty,
|
||||
SelectIfHasCustomProperty,
|
||||
]
|
||||
)
|
||||
|
||||
operator_classes.extend(
|
||||
[
|
||||
ViewStreamDataApi,
|
||||
DeleteStream,
|
||||
SelectOrphanObjects,
|
||||
UpdateGlobal,
|
||||
AddStreamFromURL,
|
||||
CreateStream,
|
||||
OpenSpeckleGuide,
|
||||
OpenSpeckleTutorials,
|
||||
OpenSpeckleForum,
|
||||
]
|
||||
)
|
||||
@@ -1,85 +0,0 @@
|
||||
"""
|
||||
Commit operators
|
||||
"""
|
||||
import bpy, os
|
||||
from bpy.props import (
|
||||
StringProperty,
|
||||
BoolProperty,
|
||||
FloatProperty,
|
||||
CollectionProperty,
|
||||
EnumProperty,
|
||||
)
|
||||
|
||||
from bpy_speckle.functions import (
|
||||
_check_speckle_client_user_stream,
|
||||
_create_stream,
|
||||
get_scale_length,
|
||||
_report,
|
||||
)
|
||||
|
||||
from bpy_speckle.convert import from_speckle_object
|
||||
from bpy_speckle.clients import speckle_clients
|
||||
|
||||
|
||||
class DeleteCommit(bpy.types.Operator):
|
||||
"""
|
||||
Delete stream
|
||||
"""
|
||||
|
||||
bl_idname = "speckle.delete_commit"
|
||||
bl_label = "Delete commit"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Delete active commit permanently"
|
||||
|
||||
are_you_sure: BoolProperty(
|
||||
name="Confirm", default=False,
|
||||
)
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
col = layout.column()
|
||||
col.prop(self, "are_you_sure")
|
||||
|
||||
def invoke(self, context, event):
|
||||
wm = context.window_manager
|
||||
if len(context.scene.speckle.users) > 0:
|
||||
return wm.invoke_props_dialog(self)
|
||||
|
||||
return {"CANCELLED"}
|
||||
|
||||
def execute(self, context):
|
||||
|
||||
if not self.are_you_sure:
|
||||
return {"CANCELLED"}
|
||||
|
||||
self.are_you_sure = False
|
||||
|
||||
speckle = context.scene.speckle
|
||||
|
||||
check = _check_speckle_client_user_stream(context.scene)
|
||||
if check is None:
|
||||
return {"CANCELLED"}
|
||||
|
||||
user, stream = check
|
||||
client = speckle_clients[int(context.scene.speckle.active_user)]
|
||||
|
||||
stream = user.streams[user.active_stream]
|
||||
if len(stream.branches) < 1:
|
||||
return {"CANCELLED"}
|
||||
else:
|
||||
branch = stream.branches[int(stream.branch)]
|
||||
if len(branch.commits) < 1:
|
||||
return {"CANCELLED"}
|
||||
else:
|
||||
commit = branch.commits[int(branch.commit)]
|
||||
|
||||
deleted = client.commit.delete(stream_id=stream.id, commit_id=commit.id)
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
bpy.ops.speckle.load_user_streams()
|
||||
context.view_layer.update()
|
||||
|
||||
if context.area:
|
||||
context.area.tag_redraw()
|
||||
return {"FINISHED"}
|
||||
@@ -1,35 +0,0 @@
|
||||
import bpy
|
||||
import webbrowser
|
||||
|
||||
|
||||
class OpenSpeckleGuide(bpy.types.Operator):
|
||||
bl_idname = "speckle.open_speckle_guide"
|
||||
bl_label = "Speckle Guide"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Browse the documentation on the Speckle Guide"
|
||||
|
||||
def execute(self, context):
|
||||
webbrowser.open("https://speckle.guide/user/blender.html")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class OpenSpeckleTutorials(bpy.types.Operator):
|
||||
bl_idname = "speckle.open_speckle_tutorials"
|
||||
bl_label = "Tutorials Portal"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Visit our tutorials portal for learning resources"
|
||||
|
||||
def execute(self, context):
|
||||
webbrowser.open("https://speckle.systems/tutorials/")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class OpenSpeckleForum(bpy.types.Operator):
|
||||
bl_idname = "speckle.open_speckle_forum"
|
||||
bl_label = "Community Forum"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Ask questions and join the discussion on our community forum"
|
||||
|
||||
def execute(self, context):
|
||||
webbrowser.open("https://speckle.community/")
|
||||
return {"FINISHED"}
|
||||
@@ -1,384 +0,0 @@
|
||||
"""
|
||||
Object operators
|
||||
"""
|
||||
|
||||
import bpy, bmesh, os
|
||||
from bpy.props import (
|
||||
StringProperty,
|
||||
BoolProperty,
|
||||
FloatProperty,
|
||||
CollectionProperty,
|
||||
EnumProperty,
|
||||
)
|
||||
|
||||
from specklepy.api.client import SpeckleClient
|
||||
from bpy_speckle.convert import to_speckle_object
|
||||
from bpy_speckle.convert.to_speckle import export_ngons_as_polylines
|
||||
|
||||
from bpy_speckle.functions import get_scale_length, _report
|
||||
from bpy_speckle.clients import speckle_clients
|
||||
|
||||
|
||||
class UpdateObject(bpy.types.Operator):
|
||||
"""
|
||||
Update local (receive) or remote (send) object depending on
|
||||
the update direction. If sending, updates the object on the
|
||||
server in-place.
|
||||
"""
|
||||
|
||||
bl_idname = "speckle.update_object"
|
||||
bl_label = "Update Object"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
client = None
|
||||
|
||||
def execute(self, context):
|
||||
user = context.scene.speckle.users[int(context.scene.speckle.active_user)]
|
||||
client = speckle_clients[int(context.scene.speckle.active_user)]
|
||||
stream = user.streams[user.active_stream]
|
||||
|
||||
active = context.active_object
|
||||
_report(active)
|
||||
|
||||
if active is not None and active.speckle.enabled:
|
||||
if active.speckle.send_or_receive == "send" and active.speckle.stream_id:
|
||||
sstream = client.streams.get(active.speckle.stream_id)
|
||||
# res = client.StreamGetAsync(active.speckle.stream_id)['resource']
|
||||
# res = client.streams.get(active.speckle.stream_id)
|
||||
|
||||
if sstream is None:
|
||||
_report("Getting stream failed.")
|
||||
return {"CANCELLED"}
|
||||
|
||||
stream_units = "Meters"
|
||||
if sstream.baseProperties:
|
||||
stream_units = sstream.baseProperties.units
|
||||
|
||||
scale = context.scene.unit_settings.scale_length / get_scale_length(
|
||||
stream_units
|
||||
)
|
||||
|
||||
sm = to_speckle_object(active, scale)
|
||||
|
||||
_report("Updating object {}".format(sm["_id"]))
|
||||
client.objects.update(active.speckle.object_id, sm)
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
return {"CANCELLED"}
|
||||
return {"CANCELLED"}
|
||||
|
||||
|
||||
class ResetObject(bpy.types.Operator):
|
||||
"""
|
||||
Reset Speckle object settings
|
||||
"""
|
||||
|
||||
bl_idname = "speckle.reset_object"
|
||||
bl_label = "Reset Object"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
|
||||
context.object.speckle.send_or_receive = "send"
|
||||
context.object.speckle.stream_id = ""
|
||||
context.object.speckle.object_id = ""
|
||||
context.object.speckle.enabled = False
|
||||
context.view_layer.update()
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class DeleteObject(bpy.types.Operator):
|
||||
"""
|
||||
Delete object from the server and update relevant stream
|
||||
"""
|
||||
|
||||
bl_idname = "speckle.delete_object"
|
||||
bl_label = "Delete Object"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
|
||||
user = context.scene.speckle.users[int(context.scene.speckle.active_user)]
|
||||
client = speckle_clients[int(context.scene.speckle.active_user)]
|
||||
active = context.object
|
||||
if active.speckle.enabled:
|
||||
res = client.StreamGetAsync(active.speckle.stream_id)
|
||||
existing = [
|
||||
x
|
||||
for x in res["resource"]["objects"]
|
||||
if x["_id"] == active.speckle.object_id
|
||||
]
|
||||
if existing is None:
|
||||
return {"CANCELLED"}
|
||||
# print("Existing: %s" % SpeckleResource.to_json_pretty(existing))
|
||||
new_objects = [
|
||||
x
|
||||
for x in res["resource"]["objects"]
|
||||
if x["_id"] != active.speckle.object_id
|
||||
]
|
||||
# print (SpeckleResource.to_json_pretty(new_objects))
|
||||
|
||||
res = client.GetLayers(active.speckle.stream_id)
|
||||
new_layers = res["resource"]["layers"]
|
||||
new_layers[-1]["objectCount"] = new_layers[-1]["objectCount"] - 1
|
||||
new_layers[-1]["topology"] = "0-%s" % new_layers[-1]["objectCount"]
|
||||
|
||||
res = client.StreamUpdateAsync(
|
||||
{"objects": new_objects, "layers": new_layers}, active.speckle.stream_id
|
||||
)
|
||||
res = client.ObjectDeleteAsync(active.speckle.object_id)
|
||||
|
||||
active.speckle.send_or_receive = "send"
|
||||
active.speckle.stream_id = ""
|
||||
active.speckle.object_id = ""
|
||||
active.speckle.enabled = False
|
||||
context.view_layer.update()
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class UploadNgonsAsPolylines(bpy.types.Operator):
|
||||
"""
|
||||
Upload mesh ngon faces as polyline outlines
|
||||
TODO: move to another category of specialized operators and fix to work with API 2.0
|
||||
"""
|
||||
|
||||
bl_idname = "speckle.upload_ngons_as_polylines"
|
||||
bl_label = "Upload Ngons As Polylines"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
clear_stream: BoolProperty(
|
||||
name="Clear stream",
|
||||
default=False,
|
||||
)
|
||||
|
||||
def execute(self, context):
|
||||
active = context.active_object
|
||||
if active is not None and active.type == "MESH":
|
||||
|
||||
user = context.scene.speckle.users[int(context.scene.speckle.active_user)]
|
||||
client = speckle_clients[int(context.scene.speckle.active_user)]
|
||||
stream = user.streams[user.active_stream]
|
||||
|
||||
scale = context.scene.unit_settings.scale_length / get_scale_length(
|
||||
stream.units
|
||||
)
|
||||
|
||||
sp = export_ngons_as_polylines(active, scale)
|
||||
|
||||
if sp is None:
|
||||
return {"CANCELLED"}
|
||||
|
||||
placeholders = []
|
||||
for polyline in sp:
|
||||
|
||||
res = client.objects.create([polyline])
|
||||
print(res)
|
||||
|
||||
if res is None:
|
||||
_report(client.me)
|
||||
continue
|
||||
placeholders.extend(res)
|
||||
|
||||
# polyline['_id'] = res['_id']
|
||||
# placeholders.append({'type':'Placeholder', '_id':res['_id']})
|
||||
|
||||
if not placeholders:
|
||||
return {"CANCELLED"}
|
||||
|
||||
# Get list of existing objects in stream and append new object to list
|
||||
_report("Fetching stream...")
|
||||
sstream = client.streams.get(stream.id)
|
||||
|
||||
if self.clear_stream:
|
||||
_report("Clearing stream...")
|
||||
sstream.objects = placeholders
|
||||
N = 0
|
||||
else:
|
||||
sstream.objects.extend(placeholders)
|
||||
|
||||
N = sstream.layers[-1].objectCount
|
||||
if self.clear_stream:
|
||||
N = 0
|
||||
sstream.layers[-1].objectCount = N + len(placeholders)
|
||||
sstream.layers[-1].topology = "0-%s" % (N + len(placeholders))
|
||||
|
||||
res = client.streams.update(sstream.id, sstream)
|
||||
|
||||
# Update view layer
|
||||
context.view_layer.update()
|
||||
_report("Done.")
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context, event):
|
||||
wm = context.window_manager
|
||||
return wm.invoke_props_dialog(self)
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.prop(self, "clear_stream")
|
||||
|
||||
|
||||
class UploadObject(bpy.types.Operator):
|
||||
"""
|
||||
DEPRECATED
|
||||
Upload an individual object
|
||||
"""
|
||||
|
||||
bl_idname = "speckle.upload_object"
|
||||
bl_label = "Upload Object"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
|
||||
active = context.active_object
|
||||
if active is not None:
|
||||
user = context.scene.speckle.users[int(context.scene.speckle.active_user)]
|
||||
client = speckle_clients[int(context.scene.speckle.active_user)]
|
||||
stream = user.streams[user.active_stream]
|
||||
|
||||
scale = context.scene.unit_settings.scale_length / get_scale_length(
|
||||
stream.units
|
||||
)
|
||||
|
||||
sm = to_speckle_object(active, scale)
|
||||
|
||||
placeholders = client.objects.create([sm])
|
||||
if placeholders is None:
|
||||
return {"CANCELLED"}
|
||||
|
||||
sstream = client.streams.get(stream.id)
|
||||
sstream.objects.extend(placeholders)
|
||||
|
||||
N = sstream.layers[-1].objectCount
|
||||
sstream.layers[-1].objectCount = N + 1
|
||||
sstream.layers[-1].topology = "0-%s" % (N + 1)
|
||||
|
||||
_report("Updating stream %s" % stream.id)
|
||||
|
||||
res = client.streams.update(stream["id"], sstream)
|
||||
|
||||
_report(res)
|
||||
|
||||
active.speckle.enabled = True
|
||||
active.speckle.object_id = sm.id
|
||||
active.speckle.stream_id = stream.id
|
||||
active.speckle.send_or_receive = "send"
|
||||
|
||||
context.view_layer.update()
|
||||
_report("Done.")
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
def get_custom_speckle_props(self, context):
|
||||
ignore = ["speckle", "cycles", "cycles_visibility"]
|
||||
|
||||
active = context.active_object
|
||||
if not active:
|
||||
return []
|
||||
|
||||
return [(x, "{}".format(x), "") for x in active.keys()]
|
||||
|
||||
|
||||
class SelectIfSameCustomProperty(bpy.types.Operator):
|
||||
"""
|
||||
Select scene objects if they have the same custom property
|
||||
value as the active object
|
||||
"""
|
||||
|
||||
bl_idname = "speckle.select_if_same_custom_props"
|
||||
bl_label = "Select Identical Custom Props"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
custom_prop: EnumProperty(
|
||||
name="Custom properties",
|
||||
description="Available streams associated with user.",
|
||||
items=get_custom_speckle_props,
|
||||
)
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
col = layout.column()
|
||||
col.prop(self, "custom_prop")
|
||||
|
||||
def invoke(self, context, event):
|
||||
wm = context.window_manager
|
||||
return wm.invoke_props_dialog(self)
|
||||
|
||||
def execute(self, context):
|
||||
|
||||
active = context.active_object
|
||||
if not active:
|
||||
return {"CANCELLED"}
|
||||
|
||||
if self.custom_prop not in active.keys():
|
||||
return {"CANCELLED"}
|
||||
|
||||
value = active[self.custom_prop]
|
||||
|
||||
_report(
|
||||
"Looking for '{}' property with a value of '{}'.".format(
|
||||
self.custom_prop, value
|
||||
)
|
||||
)
|
||||
|
||||
for obj in bpy.data.objects:
|
||||
|
||||
if self.custom_prop in obj.keys() and obj[self.custom_prop] == value:
|
||||
obj.select_set(True)
|
||||
else:
|
||||
obj.select_set(False)
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class SelectIfHasCustomProperty(bpy.types.Operator):
|
||||
"""
|
||||
Select scene objects if they have the same custom property
|
||||
as the active object, regardless of the value
|
||||
"""
|
||||
|
||||
bl_idname = "speckle.select_if_has_custom_props"
|
||||
bl_label = "Select Same Custom Prop"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
custom_prop: EnumProperty(
|
||||
name="Custom properties",
|
||||
description="Custom properties yo",
|
||||
items=get_custom_speckle_props,
|
||||
)
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
col = layout.column()
|
||||
col.prop(self, "custom_prop")
|
||||
|
||||
def invoke(self, context, event):
|
||||
wm = context.window_manager
|
||||
return wm.invoke_props_dialog(self)
|
||||
|
||||
def execute(self, context):
|
||||
|
||||
active = context.active_object
|
||||
if not active:
|
||||
return {"CANCELLED"}
|
||||
|
||||
if self.custom_prop not in active.keys():
|
||||
return {"CANCELLED"}
|
||||
|
||||
value = active[self.custom_prop]
|
||||
|
||||
_report("Looking for '{}' property.".format(self.custom_prop))
|
||||
|
||||
for obj in bpy.data.objects:
|
||||
|
||||
if self.custom_prop in obj.keys():
|
||||
obj.select_set(True)
|
||||
else:
|
||||
obj.select_set(False)
|
||||
|
||||
return {"FINISHED"}
|
||||
@@ -1,814 +0,0 @@
|
||||
"""
|
||||
Stream operators
|
||||
"""
|
||||
from itertools import chain
|
||||
from typing import Dict
|
||||
import bpy, bmesh, os
|
||||
from specklepy.api.models import Commit
|
||||
import webbrowser
|
||||
from bpy.props import (
|
||||
StringProperty,
|
||||
BoolProperty,
|
||||
FloatProperty,
|
||||
CollectionProperty,
|
||||
EnumProperty,
|
||||
)
|
||||
from bpy_speckle.functions import (
|
||||
_check_speckle_client_user_stream,
|
||||
_create_stream,
|
||||
get_scale_length,
|
||||
_report,
|
||||
)
|
||||
from bpy_speckle.convert import to_speckle_object, get_speckle_subobjects
|
||||
from bpy_speckle.convert.to_speckle import export_ngons_as_polylines
|
||||
|
||||
from bpy_speckle.convert import from_speckle_object
|
||||
from bpy_speckle.clients import speckle_clients
|
||||
from bpy_speckle.operators.users import add_user_stream
|
||||
|
||||
from specklepy.api import operations
|
||||
from specklepy.api.credentials import StreamWrapper
|
||||
from specklepy.api.resources.stream import Stream
|
||||
from specklepy.transports.server import ServerTransport
|
||||
from specklepy.objects import Base
|
||||
from specklepy.objects.geometry import *
|
||||
from specklepy.logging.exceptions import SpeckleException
|
||||
|
||||
|
||||
def get_objects_collections(base) -> Dict:
|
||||
"""Create collections based on the dynamic members on a root commit object"""
|
||||
collections = {}
|
||||
for name in base.get_dynamic_member_names():
|
||||
value = base[name]
|
||||
if isinstance(value, list):
|
||||
col = create_collection(name)
|
||||
collections[name] = get_objects_nested_lists(value, col)
|
||||
if isinstance(value, Base):
|
||||
col = create_collection(name)
|
||||
collections[name] = get_objects_collections_recursive(value, col)
|
||||
|
||||
return collections
|
||||
|
||||
|
||||
def get_objects_nested_lists(items, parent_col=None) -> List:
|
||||
"""For handling the weird nested lists that come from Grasshopper"""
|
||||
objects = []
|
||||
|
||||
if isinstance(items[0], list):
|
||||
items = list(chain.from_iterable(items))
|
||||
objects.extend(get_objects_nested_lists(items, parent_col))
|
||||
else:
|
||||
objects = [
|
||||
get_objects_collections_recursive(item, parent_col)
|
||||
for item in items
|
||||
if isinstance(item, Base)
|
||||
]
|
||||
|
||||
return objects
|
||||
|
||||
|
||||
def get_objects_collections_recursive(base, parent_col=None) -> List:
|
||||
"""Recursively create collections based on the dynamic members on nested `Base` objects within the root commit object"""
|
||||
# if it's a convertable (registered) class and not just a plain `Base`, return the object itself
|
||||
object_type = Base.get_registered_type(base.speckle_type)
|
||||
if (
|
||||
(object_type and object_type != Base)
|
||||
or hasattr(base, "displayMesh")
|
||||
or hasattr(base, "displayValue")
|
||||
):
|
||||
return [base]
|
||||
|
||||
# if it's an unknown type, try to drill further down to find convertable objects
|
||||
objects = []
|
||||
|
||||
for name in base.get_dynamic_member_names():
|
||||
value = base[name]
|
||||
if isinstance(value, list):
|
||||
for item in value:
|
||||
if isinstance(item, Base):
|
||||
objects.append(item)
|
||||
if isinstance(value, Base):
|
||||
col = parent_col.children.get(name)
|
||||
if not parent_col.children.get(name):
|
||||
col = create_collection(name)
|
||||
parent_col.children.link(col)
|
||||
objects.append({name: get_objects_collections_recursive(value, col)})
|
||||
|
||||
return objects
|
||||
|
||||
|
||||
def bases_to_native(context, collections, scale, stream_id, func=None):
|
||||
for col_name, objects in collections.items():
|
||||
col = bpy.data.collections[col_name]
|
||||
existing = get_existing_collection_objs(col)
|
||||
if isinstance(objects, dict):
|
||||
bases_to_native(context, objects, scale, stream_id)
|
||||
elif isinstance(objects, list):
|
||||
for obj in objects:
|
||||
if isinstance(obj, dict):
|
||||
bases_to_native(context, obj, scale, stream_id, func)
|
||||
elif isinstance(obj, list):
|
||||
for item in obj:
|
||||
if isinstance(item, dict):
|
||||
bases_to_native(context, item, scale, stream_id, func)
|
||||
elif isinstance(item, Base):
|
||||
base_to_native(
|
||||
context, item, scale, stream_id, col, existing, func
|
||||
)
|
||||
elif isinstance(obj, Base):
|
||||
base_to_native(context, obj, scale, stream_id, col, existing, func)
|
||||
else:
|
||||
_report(
|
||||
f"Something went wrong when receiving collection: {col_name}"
|
||||
)
|
||||
|
||||
bpy.context.view_layer.update()
|
||||
|
||||
if context.area:
|
||||
context.area.tag_redraw()
|
||||
|
||||
|
||||
def base_to_native(context, base, scale, stream_id, col, existing, func=None):
|
||||
new_objects = [from_speckle_object(base, scale)]
|
||||
|
||||
if hasattr(base, "properties") and base.properties is not None:
|
||||
new_objects.extend(get_speckle_subobjects(base.properties, scale, base.id))
|
||||
elif isinstance(base, dict) and "properties" in base.keys():
|
||||
new_objects.extend(
|
||||
get_speckle_subobjects(base["properties"], scale, base["id"])
|
||||
)
|
||||
|
||||
"""
|
||||
Set object Speckle settings
|
||||
"""
|
||||
for new_object in new_objects:
|
||||
if new_object is None:
|
||||
continue
|
||||
|
||||
"""
|
||||
Run injected function
|
||||
"""
|
||||
if func:
|
||||
new_object = func(context.scene, new_object)
|
||||
|
||||
if (
|
||||
new_object is None
|
||||
): # Make sure that the injected function returned an object
|
||||
new_obj = new_object
|
||||
_report("Script '{}' returned None.".format(func.__module__))
|
||||
continue
|
||||
|
||||
new_object.speckle.stream_id = stream_id
|
||||
new_object.speckle.send_or_receive = "receive"
|
||||
|
||||
if new_object.speckle.object_id in existing.keys():
|
||||
name = existing[new_object.speckle.object_id].name
|
||||
existing[new_object.speckle.object_id].name = name + "__deleted"
|
||||
new_object.name = name
|
||||
col.objects.unlink(existing[new_object.speckle.object_id])
|
||||
|
||||
if new_object.name not in col.objects:
|
||||
col.objects.link(new_object)
|
||||
|
||||
|
||||
def create_collection(name, clear_collection=True):
|
||||
if name in bpy.data.collections:
|
||||
col = bpy.data.collections[name]
|
||||
if clear_collection:
|
||||
for obj in col.objects:
|
||||
col.objects.unlink(obj)
|
||||
else:
|
||||
col = bpy.data.collections.new(name)
|
||||
|
||||
return col
|
||||
|
||||
|
||||
def create_child_collections(parent_col, children_names):
|
||||
for name in children_names:
|
||||
col = create_collection(name)
|
||||
parent_col.children.link(col)
|
||||
|
||||
|
||||
def get_existing_collection_objs(col):
|
||||
return {
|
||||
obj.speckle.object_id: obj for obj in col.objects if obj.speckle.object_id != ""
|
||||
}
|
||||
|
||||
|
||||
def get_collection_parents(collection, names):
|
||||
for parent in bpy.data.collections:
|
||||
if collection.name in parent.children.keys():
|
||||
# TODO: this should be rethought to make it clear when this is an IFC delim so we know to replace it
|
||||
# with `/` again on receive
|
||||
names.append(parent.name.replace("/", "::").replace(".", "::"))
|
||||
get_collection_parents(parent, names)
|
||||
|
||||
|
||||
def get_collection_hierarchy(collection):
|
||||
if not collection:
|
||||
return []
|
||||
names = [collection.name.replace("/", "::").replace(".", "::")]
|
||||
get_collection_parents(collection, names)
|
||||
|
||||
return names
|
||||
|
||||
|
||||
def create_nested_hierarchy(base, hierarchy, objects):
|
||||
child = base
|
||||
|
||||
while hierarchy:
|
||||
name = hierarchy.pop()
|
||||
if not hasattr(child, name):
|
||||
child[name] = Base()
|
||||
child.add_detachable_attrs({name})
|
||||
child = child[name]
|
||||
|
||||
# TODO: what do we call this attribute?
|
||||
if not hasattr(child, "objects"):
|
||||
child["objects"] = []
|
||||
child["objects"].extend(objects)
|
||||
|
||||
return base
|
||||
|
||||
|
||||
class ReceiveStreamObjects(bpy.types.Operator):
|
||||
"""
|
||||
Receive stream objects
|
||||
"""
|
||||
|
||||
bl_idname = "speckle.receive_stream_objects"
|
||||
bl_label = "Download Stream Objects"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Receive objects from active stream"
|
||||
|
||||
def execute(self, context):
|
||||
bpy.context.view_layer.objects.active = None
|
||||
|
||||
check = _check_speckle_client_user_stream(context.scene)
|
||||
if check is None:
|
||||
return {"CANCELLED"}
|
||||
|
||||
user, bstream = check
|
||||
|
||||
client = speckle_clients[int(context.scene.speckle.active_user)]
|
||||
|
||||
stream = client.stream.get(id=bstream.id)
|
||||
if stream.branches.totalCount < 1:
|
||||
return {"CANCELLED"}
|
||||
|
||||
if not stream.branches:
|
||||
return {"CANCELLED"}
|
||||
|
||||
branch = stream.branches.items[int(bstream.branch)]
|
||||
|
||||
bbranch = bstream.branches[int(bstream.branch)]
|
||||
|
||||
if branch.commits.totalCount < 1:
|
||||
print("No commits found. Probably an empty stream.")
|
||||
return {"CANCELLED"}
|
||||
|
||||
commit = branch.commits.items[int(bbranch.commit)]
|
||||
|
||||
transport = ServerTransport(stream.id, client)
|
||||
stream_data = operations.receive(commit.referencedObject, transport)
|
||||
client.commit.received(
|
||||
bstream.id,
|
||||
commit.id,
|
||||
source_application="blender",
|
||||
message="received commit from Speckle Blender",
|
||||
)
|
||||
|
||||
"""
|
||||
Create or get Collection for stream objects
|
||||
"""
|
||||
collections = get_objects_collections(stream_data)
|
||||
|
||||
if not collections:
|
||||
return {"CANCELLED"}
|
||||
|
||||
name = "{} [ {} @ {} ]".format(stream.name, branch.name, commit.id)
|
||||
col = create_collection(name)
|
||||
col.speckle.stream_id = stream.id
|
||||
col.speckle.name = stream.name
|
||||
col.speckle.units = stream_data.units
|
||||
if col.name not in bpy.context.scene.collection.children:
|
||||
bpy.context.scene.collection.children.link(col)
|
||||
|
||||
for child_col in collections.keys():
|
||||
try:
|
||||
col.children.link(bpy.data.collections[child_col])
|
||||
except:
|
||||
pass
|
||||
"""
|
||||
Set conversion scale from stream units
|
||||
"""
|
||||
scale = (
|
||||
get_scale_length(stream_data.units)
|
||||
/ context.scene.unit_settings.scale_length
|
||||
)
|
||||
|
||||
"""
|
||||
Get script from text editor for injection
|
||||
"""
|
||||
func = None
|
||||
if context.scene.speckle.receive_script in bpy.data.texts:
|
||||
mod = bpy.data.texts[context.scene.speckle.receive_script].as_module()
|
||||
if hasattr(mod, "execute"):
|
||||
func = mod.execute
|
||||
|
||||
"""
|
||||
Iterate through retrieved resources
|
||||
"""
|
||||
bases_to_native(context, collections, scale, stream.id, func)
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class SendStreamObjects(bpy.types.Operator):
|
||||
"""
|
||||
Send stream objects
|
||||
"""
|
||||
|
||||
bl_idname = "speckle.send_stream_objects"
|
||||
bl_label = "Send stream objects"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Send selected objects to active stream"
|
||||
|
||||
apply_modifiers: BoolProperty(name="Apply modifiers", default=True)
|
||||
commit_message: StringProperty(
|
||||
name="Message",
|
||||
default="Pushed elements from Blender.",
|
||||
)
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
col = layout.column()
|
||||
col.prop(self, "commit_message")
|
||||
col.prop(self, "apply_modifiers")
|
||||
|
||||
def invoke(self, context, event):
|
||||
wm = context.window_manager
|
||||
if len(context.scene.speckle.users) > 0:
|
||||
N = len(context.selected_objects)
|
||||
if N == 1:
|
||||
self.commit_message = "Pushed {} element from Blender.".format(N)
|
||||
else:
|
||||
self.commit_message = "Pushed {} elements from Blender.".format(N)
|
||||
return wm.invoke_props_dialog(self)
|
||||
|
||||
return {"CANCELLED"}
|
||||
|
||||
def execute(self, context):
|
||||
|
||||
selected = context.selected_objects
|
||||
|
||||
if len(selected) < 1:
|
||||
return {"CANCELLED"}
|
||||
|
||||
check = _check_speckle_client_user_stream(context.scene)
|
||||
if check is None:
|
||||
return {"CANCELLED"}
|
||||
|
||||
user, bstream = check
|
||||
stream = user.streams[user.active_stream]
|
||||
branch = stream.branches[int(stream.branch)]
|
||||
|
||||
client = speckle_clients[int(context.scene.speckle.active_user)]
|
||||
|
||||
scale = context.scene.unit_settings.scale_length / get_scale_length(
|
||||
stream.units.lower()
|
||||
)
|
||||
|
||||
"""
|
||||
Get script from text editor for injection
|
||||
"""
|
||||
func = None
|
||||
if context.scene.speckle.send_script in bpy.data.texts:
|
||||
mod = bpy.data.texts[context.scene.speckle.send_script].as_module()
|
||||
if hasattr(mod, "execute"):
|
||||
func = mod.execute
|
||||
|
||||
export = {}
|
||||
|
||||
for obj in selected:
|
||||
|
||||
# if obj.type != 'MESH':
|
||||
# continue
|
||||
|
||||
new_object = obj
|
||||
|
||||
"""
|
||||
Run injected function
|
||||
"""
|
||||
if func:
|
||||
new_object = func(context.scene, obj)
|
||||
|
||||
if (
|
||||
new_object is None
|
||||
): # Make sure that the injected function returned an object
|
||||
new_obj = obj
|
||||
_report("Script '{}' returned None.".format(func.__module__))
|
||||
continue
|
||||
|
||||
_report("Converting {}".format(obj.name))
|
||||
|
||||
ngons = obj.get("speckle_ngons_as_polylines", False)
|
||||
|
||||
if ngons:
|
||||
converted = export_ngons_as_polylines(obj, scale)
|
||||
else:
|
||||
converted = to_speckle_object(
|
||||
obj,
|
||||
scale,
|
||||
bpy.context.evaluated_depsgraph_get()
|
||||
if self.apply_modifiers
|
||||
else None,
|
||||
)
|
||||
|
||||
if not converted:
|
||||
continue
|
||||
|
||||
collection_name = obj.users_collection[0].name
|
||||
if not export.get(collection_name):
|
||||
export[collection_name] = []
|
||||
|
||||
export[collection_name].extend(converted)
|
||||
|
||||
base = Base()
|
||||
for name, objects in export.items():
|
||||
collection = bpy.data.collections.get(name)
|
||||
hierarchy = get_collection_hierarchy(collection)
|
||||
create_nested_hierarchy(base, hierarchy, objects)
|
||||
|
||||
transport = ServerTransport(stream.id, client)
|
||||
|
||||
obj_id = operations.send(
|
||||
base,
|
||||
[transport],
|
||||
)
|
||||
client.commit.create(
|
||||
stream.id,
|
||||
obj_id,
|
||||
branch.name,
|
||||
message=self.commit_message,
|
||||
source_application="blender",
|
||||
)
|
||||
|
||||
bpy.ops.speckle.load_user_streams()
|
||||
|
||||
context.view_layer.update()
|
||||
|
||||
if context.area:
|
||||
context.area.tag_redraw()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class ViewStreamDataApi(bpy.types.Operator):
|
||||
bl_idname = "speckle.view_stream_data_api"
|
||||
bl_label = "Open Stream in Web"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "View the stream in the web browser"
|
||||
|
||||
def execute(self, context):
|
||||
|
||||
if len(context.scene.speckle.users) > 0:
|
||||
user = context.scene.speckle.users[int(context.scene.speckle.active_user)]
|
||||
if len(user.streams) > 0:
|
||||
stream = user.streams[user.active_stream]
|
||||
|
||||
webbrowser.open("%s/streams/%s" % (user.server_url, stream.id), new=2)
|
||||
return {"FINISHED"}
|
||||
return {"CANCELLED"}
|
||||
|
||||
|
||||
class AddStreamFromURL(bpy.types.Operator):
|
||||
"""
|
||||
Add / select a stream using its url
|
||||
"""
|
||||
|
||||
bl_idname = "speckle.add_stream_from_url"
|
||||
bl_label = "Add stream from URL"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Add an existing stream by providing its URL"
|
||||
stream_url: StringProperty(
|
||||
name="Stream URL", default="https://speckle.xyz/streams/3073b96e86"
|
||||
)
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
col = layout.column()
|
||||
col.prop(self, "stream_url")
|
||||
|
||||
def invoke(self, context, event):
|
||||
wm = context.window_manager
|
||||
if len(context.scene.speckle.users) > 0:
|
||||
return wm.invoke_props_dialog(self)
|
||||
|
||||
return {"CANCELLED"}
|
||||
|
||||
def execute(self, context):
|
||||
speckle = context.scene.speckle
|
||||
|
||||
wrapper = StreamWrapper(self.stream_url)
|
||||
user_index = next(
|
||||
(i for i, u in enumerate(speckle.users) if wrapper.host in u.server_url),
|
||||
None,
|
||||
)
|
||||
if user_index is None:
|
||||
return {"CANCELLED"}
|
||||
speckle.active_user = str(user_index)
|
||||
user = speckle.users[user_index]
|
||||
|
||||
client = speckle_clients[user_index]
|
||||
stream = client.stream.get(wrapper.stream_id)
|
||||
if not isinstance(stream, Stream):
|
||||
raise SpeckleException("Could not get the requested stream")
|
||||
|
||||
index, b_stream = next(
|
||||
((i, s) for i, s in enumerate(user.streams) if s.id == stream.id),
|
||||
(None, None),
|
||||
)
|
||||
|
||||
if index is None:
|
||||
add_user_stream(user, stream)
|
||||
user.active_stream, b_stream = next(
|
||||
(i, s) for i, s in enumerate(user.streams) if s.id == stream.id
|
||||
)
|
||||
else:
|
||||
user.active_stream = index
|
||||
|
||||
if wrapper.branch_name:
|
||||
b_index = b_stream.branches.find(wrapper.branch_name)
|
||||
b_stream.branch = str(b_index if b_index != -1 else 0)
|
||||
elif wrapper.commit_id:
|
||||
commit = client.commit.get(wrapper.stream_id, wrapper.commit_id)
|
||||
if isinstance(commit, Commit):
|
||||
b_index = b_stream.branches.find(commit.branchName)
|
||||
if b_index == -1:
|
||||
b_index = 0
|
||||
b_stream.branch = str(b_index)
|
||||
c_index = b_stream.branches[b_index].commits.find(commit.id)
|
||||
b_stream.branches[b_index].commit = str(c_index if c_index != -1 else 0)
|
||||
|
||||
# Update view layer
|
||||
context.view_layer.update()
|
||||
|
||||
if context.area:
|
||||
context.area.tag_redraw()
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class CreateStream(bpy.types.Operator):
|
||||
"""
|
||||
Create new stream
|
||||
"""
|
||||
|
||||
bl_idname = "speckle.create_stream"
|
||||
bl_label = "Create stream"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Create new stream"
|
||||
|
||||
stream_name: StringProperty(name="Stream name", default="SpeckleStream")
|
||||
stream_description: StringProperty(
|
||||
name="Stream description", default="This is a Blender stream."
|
||||
)
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
col = layout.column()
|
||||
col.prop(self, "stream_name")
|
||||
col.prop(self, "stream_description")
|
||||
|
||||
def invoke(self, context, event):
|
||||
wm = context.window_manager
|
||||
if len(context.scene.speckle.users) > 0:
|
||||
return wm.invoke_props_dialog(self)
|
||||
|
||||
return {"CANCELLED"}
|
||||
|
||||
def execute(self, context):
|
||||
|
||||
check = _check_speckle_client_user_stream(context.scene)
|
||||
if check is None:
|
||||
return {"CANCELLED"}
|
||||
|
||||
user, bstream = check
|
||||
|
||||
client = speckle_clients[int(context.scene.speckle.active_user)]
|
||||
|
||||
client.stream.create(
|
||||
name=self.stream_name, description=self.stream_description, is_public=True
|
||||
)
|
||||
|
||||
bpy.ops.speckle.load_user_streams()
|
||||
user.active_stream = user.streams.find(self.stream_name)
|
||||
|
||||
# Update view layer
|
||||
context.view_layer.update()
|
||||
|
||||
if context.area:
|
||||
context.area.tag_redraw()
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class DeleteStream(bpy.types.Operator):
|
||||
"""
|
||||
Delete stream
|
||||
"""
|
||||
|
||||
bl_idname = "speckle.delete_stream"
|
||||
bl_label = "Delete stream"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Delete selected stream permanently"
|
||||
|
||||
are_you_sure: BoolProperty(
|
||||
name="Confirm",
|
||||
default=False,
|
||||
)
|
||||
|
||||
delete_collection: BoolProperty(name="Delete collection", default=False)
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
col = layout.column()
|
||||
col.prop(self, "are_you_sure")
|
||||
col.prop(self, "delete_collection")
|
||||
|
||||
def invoke(self, context, event):
|
||||
wm = context.window_manager
|
||||
if len(context.scene.speckle.users) > 0:
|
||||
return wm.invoke_props_dialog(self)
|
||||
|
||||
return {"CANCELLED"}
|
||||
|
||||
def execute(self, context):
|
||||
|
||||
if not self.are_you_sure:
|
||||
return {"CANCELLED"}
|
||||
|
||||
self.are_you_sure = False
|
||||
|
||||
check = _check_speckle_client_user_stream(context.scene)
|
||||
if check is None:
|
||||
return {"CANCELLED"}
|
||||
|
||||
user, stream = check
|
||||
client = speckle_clients[int(context.scene.speckle.active_user)]
|
||||
|
||||
client.stream.delete(id=stream.id)
|
||||
|
||||
if self.delete_collection:
|
||||
col_name = "SpeckleStream_{}_{}".format(stream.name, stream.id)
|
||||
if col_name in bpy.data.collections:
|
||||
collection = bpy.data.collections[col_name]
|
||||
bpy.data.collections.remove(collection)
|
||||
|
||||
bpy.ops.speckle.load_user_streams()
|
||||
context.view_layer.update()
|
||||
|
||||
if context.area:
|
||||
context.area.tag_redraw()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class SelectOrphanObjects(bpy.types.Operator):
|
||||
"""
|
||||
Select Speckle objects that don't belong to any stream
|
||||
"""
|
||||
|
||||
bl_idname = "speckle.select_orphans"
|
||||
bl_label = "Select orphaned objects"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Select Speckle objects that don't belong to any stream"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
|
||||
def execute(self, context):
|
||||
|
||||
for o in context.scene.objects:
|
||||
if (
|
||||
o.speckle.stream_id
|
||||
and o.speckle.stream_id not in context.scene["speckle_streams"]
|
||||
):
|
||||
o.select = True
|
||||
else:
|
||||
o.select = False
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class UpdateGlobal(bpy.types.Operator):
|
||||
"""
|
||||
DEPRECATED
|
||||
Update all Speckle objects
|
||||
"""
|
||||
|
||||
bl_idname = "speckle.update_global"
|
||||
bl_label = "Update Global"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Update all Speckle objects"
|
||||
|
||||
client = None
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
row = layout.row()
|
||||
label = row.label(text="Update everything.")
|
||||
|
||||
def execute(self, context):
|
||||
|
||||
client = context.scene.speckle.client
|
||||
|
||||
profiles = client.load_local_profiles()
|
||||
if len(profiles) < 1:
|
||||
raise ValueError("No profiles found.")
|
||||
client.use_existing_profile(sorted(profiles.keys())[0])
|
||||
context.scene.speckle.user = sorted(profiles.keys())[0]
|
||||
|
||||
for obj in context.scene.objects:
|
||||
if obj.speckle.enabled:
|
||||
UpdateObject(context.scene.speckle_client, obj)
|
||||
|
||||
context.scene.update()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class CopyStreamId(bpy.types.Operator):
|
||||
"""
|
||||
Copy stream ID to clipboard
|
||||
"""
|
||||
|
||||
bl_idname = "speckle.stream_copy_id"
|
||||
bl_label = "Copy stream ID"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Copy stream ID to clipboard"
|
||||
|
||||
def execute(self, context):
|
||||
speckle = context.scene.speckle
|
||||
|
||||
if len(speckle.users) < 1:
|
||||
return {"CANCELLED"}
|
||||
user = speckle.users[int(speckle.active_user)]
|
||||
if len(user.streams) < 1:
|
||||
return {"CANCELLED"}
|
||||
stream = user.streams[user.active_stream]
|
||||
bpy.context.window_manager.clipboard = stream.id
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class CopyCommitId(bpy.types.Operator):
|
||||
"""
|
||||
Copy commit ID to clipboard
|
||||
"""
|
||||
|
||||
bl_idname = "speckle.commit_copy_id"
|
||||
bl_label = "Copy commit ID"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Copy commit ID to clipboard"
|
||||
|
||||
def execute(self, context):
|
||||
speckle = context.scene.speckle
|
||||
|
||||
if len(speckle.users) < 1:
|
||||
return {"CANCELLED"}
|
||||
user = speckle.users[int(speckle.active_user)]
|
||||
if len(user.streams) < 1:
|
||||
return {"CANCELLED"}
|
||||
stream = user.streams[user.active_stream]
|
||||
if len(stream.branches) < 1:
|
||||
return {"CANCELLED"}
|
||||
branch = stream.branches[int(stream.branch)]
|
||||
if len(branch.commits) < 1:
|
||||
return {"CANCELLED"}
|
||||
commit = branch.commits[int(branch.commit)]
|
||||
bpy.context.window_manager.clipboard = commit.id
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class CopyBranchName(bpy.types.Operator):
|
||||
"""
|
||||
Copy branch name to clipboard
|
||||
"""
|
||||
|
||||
bl_idname = "speckle.branch_copy_name"
|
||||
bl_label = "Copy branch name"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Copy branch name to clipboard"
|
||||
|
||||
def execute(self, context):
|
||||
speckle = context.scene.speckle
|
||||
|
||||
if len(speckle.users) < 1:
|
||||
return {"CANCELLED"}
|
||||
user = speckle.users[int(speckle.active_user)]
|
||||
if len(user.streams) < 1:
|
||||
return {"CANCELLED"}
|
||||
stream = user.streams[user.active_stream]
|
||||
if len(stream.branches) < 1:
|
||||
return {"CANCELLED"}
|
||||
branch = stream.branches[int(stream.branch)]
|
||||
bpy.context.window_manager.clipboard = branch.name
|
||||
return {"FINISHED"}
|
||||
@@ -1,145 +0,0 @@
|
||||
"""
|
||||
User account operators
|
||||
"""
|
||||
|
||||
from typing import cast
|
||||
import bpy, bmesh, os
|
||||
from bpy.props import (
|
||||
StringProperty,
|
||||
BoolProperty,
|
||||
FloatProperty,
|
||||
CollectionProperty,
|
||||
EnumProperty,
|
||||
)
|
||||
from bpy_speckle.properties.scene import SpeckleUserObject
|
||||
|
||||
from bpy_speckle.functions import _report
|
||||
from bpy_speckle.clients import speckle_clients
|
||||
|
||||
from specklepy.api.client import SpeckleClient
|
||||
from specklepy.api.credentials import get_default_account, get_local_accounts
|
||||
|
||||
|
||||
class LoadUsers(bpy.types.Operator):
|
||||
"""
|
||||
Load all users from local user database
|
||||
"""
|
||||
|
||||
bl_idname = "speckle.users_load"
|
||||
bl_label = "Load users"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
|
||||
_report("Loading users...")
|
||||
|
||||
users = context.scene.speckle.users
|
||||
|
||||
context.scene.speckle.users.clear()
|
||||
speckle_clients.clear()
|
||||
|
||||
profiles = get_local_accounts()
|
||||
|
||||
for profile in profiles:
|
||||
user = users.add()
|
||||
user.server_name = profile.serverInfo.name or "Speckle Server"
|
||||
user.server_url = profile.serverInfo.url
|
||||
user.name = profile.userInfo.name
|
||||
user.email = profile.userInfo.email
|
||||
user.company = profile.userInfo.company or ""
|
||||
user.authToken = profile.token
|
||||
try:
|
||||
client = SpeckleClient(
|
||||
host=profile.serverInfo.url,
|
||||
use_ssl="https" in profile.serverInfo.url,
|
||||
)
|
||||
client.authenticate(user.authToken)
|
||||
speckle_clients.append(client)
|
||||
except Exception as ex:
|
||||
_report(ex)
|
||||
users.remove(len(users) - 1)
|
||||
if profile.isDefault:
|
||||
context.scene.speckle.active_user = str(len(users) - 1)
|
||||
|
||||
context.scene.speckle.active_user_index = int(context.scene.speckle.active_user)
|
||||
bpy.ops.speckle.load_user_streams()
|
||||
bpy.context.view_layer.update()
|
||||
|
||||
if context.area:
|
||||
context.area.tag_redraw()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
def add_user_stream(user, stream):
|
||||
s = user.streams.add()
|
||||
s.name = stream.name
|
||||
s.id = stream.id
|
||||
s.description = stream.description
|
||||
|
||||
if not stream.branches:
|
||||
return
|
||||
|
||||
# branches = [branch for branch in stream.branches.items if branch.name != "globals"]
|
||||
for b in stream.branches.items:
|
||||
branch = s.branches.add()
|
||||
branch.name = b.name
|
||||
|
||||
if not b.commits:
|
||||
continue
|
||||
|
||||
for c in b.commits.items:
|
||||
commit = branch.commits.add()
|
||||
commit.id = commit.name = c.id
|
||||
commit.message = c.message
|
||||
commit.author_name = c.authorName
|
||||
commit.author_id = c.authorId
|
||||
commit.created_at = c.createdAt
|
||||
commit.source_application = str(c.sourceApplication)
|
||||
|
||||
if hasattr(s, "baseProperties"):
|
||||
s.units = stream.baseProperties.units
|
||||
else:
|
||||
s.units = "Meters"
|
||||
|
||||
|
||||
class LoadUserStreams(bpy.types.Operator):
|
||||
"""
|
||||
Load all available streams for active user user
|
||||
"""
|
||||
|
||||
bl_idname = "speckle.load_user_streams"
|
||||
bl_label = "Load user streams"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "(Re)load all available user streams"
|
||||
|
||||
def execute(self, context):
|
||||
speckle = context.scene.speckle
|
||||
|
||||
if len(speckle.users) > 0:
|
||||
user = speckle.users[int(context.scene.speckle.active_user)]
|
||||
client = speckle_clients[int(context.scene.speckle.active_user)]
|
||||
|
||||
try:
|
||||
streams = client.stream.list(stream_limit=20)
|
||||
except Exception as e:
|
||||
_report("Failed to retrieve streams: {}".format(e))
|
||||
return
|
||||
if not streams:
|
||||
_report("Failed to retrieve streams.")
|
||||
return
|
||||
|
||||
user.streams.clear()
|
||||
|
||||
default_units = "Meters"
|
||||
|
||||
for s in streams:
|
||||
sstream = client.stream.get(id=s.id)
|
||||
add_user_stream(user, sstream)
|
||||
|
||||
bpy.context.view_layer.update()
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
if context.area:
|
||||
context.area.tag_redraw()
|
||||
return {"CANCELLED"}
|
||||
@@ -1,23 +0,0 @@
|
||||
from .scene import (
|
||||
SpeckleSceneSettings,
|
||||
SpeckleSceneObject,
|
||||
SpeckleUserObject,
|
||||
SpeckleStreamObject,
|
||||
SpeckleBranchObject,
|
||||
SpeckleCommitObject,
|
||||
)
|
||||
from .object import SpeckleObjectSettings
|
||||
from .collection import SpeckleCollectionSettings
|
||||
from .addon import SpeckleAddonPreferences
|
||||
|
||||
property_classes = [
|
||||
SpeckleSceneObject,
|
||||
SpeckleCommitObject,
|
||||
SpeckleBranchObject,
|
||||
SpeckleStreamObject,
|
||||
SpeckleUserObject,
|
||||
SpeckleSceneSettings,
|
||||
SpeckleObjectSettings,
|
||||
SpeckleCollectionSettings,
|
||||
SpeckleAddonPreferences,
|
||||
]
|
||||
@@ -1,19 +0,0 @@
|
||||
"""
|
||||
Addon properties
|
||||
"""
|
||||
|
||||
import bpy
|
||||
from bpy.props import BoolProperty
|
||||
|
||||
|
||||
class SpeckleAddonPreferences(bpy.types.AddonPreferences):
|
||||
"""
|
||||
Add-on preferences
|
||||
TODO: add any preferences that might be relevant here
|
||||
"""
|
||||
|
||||
bl_idname = __package__
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.label(text="SpeckleBlender preferences")
|
||||
@@ -1,21 +0,0 @@
|
||||
"""
|
||||
Collection properties
|
||||
"""
|
||||
|
||||
import bpy
|
||||
from bpy.props import StringProperty, BoolProperty, EnumProperty
|
||||
|
||||
|
||||
class SpeckleCollectionSettings(bpy.types.PropertyGroup):
|
||||
enabled: bpy.props.BoolProperty(default=False, name="Enabled")
|
||||
|
||||
send_or_receive: bpy.props.EnumProperty(
|
||||
name="Mode",
|
||||
items=(
|
||||
("send", "Send", "Send data to Speckle server."),
|
||||
("receive", "Receive", "Receive data from Speckle server."),
|
||||
),
|
||||
)
|
||||
stream_id: bpy.props.StringProperty(default="")
|
||||
name: bpy.props.StringProperty(default="")
|
||||
units: bpy.props.StringProperty(default="")
|
||||
@@ -1,20 +0,0 @@
|
||||
"""
|
||||
Object properties
|
||||
"""
|
||||
|
||||
import bpy
|
||||
from bpy.props import StringProperty, BoolProperty, EnumProperty
|
||||
|
||||
|
||||
class SpeckleObjectSettings(bpy.types.PropertyGroup):
|
||||
enabled: bpy.props.BoolProperty(default=False, name="Enabled")
|
||||
|
||||
send_or_receive: bpy.props.EnumProperty(
|
||||
name="Mode",
|
||||
items=(
|
||||
("send", "Send", "Send data to Speckle server."),
|
||||
("receive", "Receive", "Receive data from Speckle server."),
|
||||
),
|
||||
)
|
||||
stream_id: bpy.props.StringProperty(default="")
|
||||
object_id: bpy.props.StringProperty(default="")
|
||||
@@ -1,135 +0,0 @@
|
||||
"""
|
||||
Scene properties
|
||||
"""
|
||||
|
||||
import bpy
|
||||
from bpy.props import (
|
||||
StringProperty,
|
||||
BoolProperty,
|
||||
FloatProperty,
|
||||
CollectionProperty,
|
||||
EnumProperty,
|
||||
IntProperty,
|
||||
PointerProperty,
|
||||
)
|
||||
from specklepy.api.client import SpeckleClient
|
||||
|
||||
|
||||
class SpeckleSceneObject(bpy.types.PropertyGroup):
|
||||
name: bpy.props.StringProperty(default="")
|
||||
|
||||
|
||||
class SpeckleCommitObject(bpy.types.PropertyGroup):
|
||||
id: StringProperty(default="abc")
|
||||
message: StringProperty(default="A simple commit")
|
||||
author_name: StringProperty(default="Author name")
|
||||
author_id: StringProperty(default="Author ID")
|
||||
created_at: StringProperty(default="Today")
|
||||
source_application: StringProperty(default="Unknown")
|
||||
|
||||
|
||||
class SpeckleBranchObject(bpy.types.PropertyGroup):
|
||||
def get_commits(self, context):
|
||||
if self.commits != None and len(self.commits) > 0:
|
||||
return [
|
||||
(str(i), commit.id, commit.message, i)
|
||||
for i, commit in enumerate(self.commits)
|
||||
]
|
||||
return [("0", "<none>", "<none>", 0)]
|
||||
|
||||
name: StringProperty(default="main")
|
||||
commits: CollectionProperty(type=SpeckleCommitObject)
|
||||
commit: EnumProperty(
|
||||
name="Commit",
|
||||
description="Active commit",
|
||||
items=get_commits,
|
||||
)
|
||||
|
||||
|
||||
class SpeckleStreamObject(bpy.types.PropertyGroup):
|
||||
def get_branches(self, context):
|
||||
if self.branches:
|
||||
return [
|
||||
(str(i), branch.name, branch.name, i)
|
||||
for i, branch in enumerate(self.branches)
|
||||
if branch.name != "globals"
|
||||
]
|
||||
return [("0", "<none>", "<none>", 0)]
|
||||
|
||||
name: StringProperty(default="SpeckleStream")
|
||||
description: StringProperty(default="No description provided.")
|
||||
id: StringProperty(default="")
|
||||
units: StringProperty(default="Meters")
|
||||
query: StringProperty(default="")
|
||||
branches: CollectionProperty(type=SpeckleBranchObject)
|
||||
branch: EnumProperty(
|
||||
name="Branch",
|
||||
description="Active branch",
|
||||
items=get_branches,
|
||||
)
|
||||
|
||||
|
||||
class SpeckleUserObject(bpy.types.PropertyGroup):
|
||||
server_name: StringProperty(default="SpeckleXYZ")
|
||||
server_url: StringProperty(default="https://speckle.xyz")
|
||||
name: StringProperty(default="Speckle User")
|
||||
email: StringProperty(default="user@speckle.xyz")
|
||||
company: StringProperty(default="SpeckleSystems")
|
||||
authToken: StringProperty(default="")
|
||||
streams: CollectionProperty(type=SpeckleStreamObject)
|
||||
active_stream: IntProperty(default=0)
|
||||
|
||||
|
||||
class SpeckleSceneSettings(bpy.types.PropertyGroup):
|
||||
def get_scripts(self, context):
|
||||
seq = [("<none>", "<none>", "<none>")]
|
||||
seq.extend([(t.name, t.name, t.name) for t in bpy.data.texts])
|
||||
return seq
|
||||
|
||||
streams: EnumProperty(
|
||||
name="Available streams",
|
||||
description="Available streams associated with user.",
|
||||
items=[],
|
||||
)
|
||||
|
||||
users: CollectionProperty(type=SpeckleUserObject)
|
||||
|
||||
def get_users(self, context):
|
||||
return [
|
||||
(str(i), "{} ({})".format(user.email, user.server_name), user.server_url, i)
|
||||
for i, user in enumerate(self.users)
|
||||
]
|
||||
|
||||
def set_user(self, context):
|
||||
bpy.ops.speckle.load_user_streams()
|
||||
|
||||
active_user: EnumProperty(
|
||||
items=get_users,
|
||||
name="User",
|
||||
description="Select user",
|
||||
update=set_user,
|
||||
get=None,
|
||||
set=None,
|
||||
)
|
||||
|
||||
objects: CollectionProperty(type=SpeckleSceneObject)
|
||||
|
||||
scale: FloatProperty(default=0.001)
|
||||
|
||||
user: StringProperty(
|
||||
name="User",
|
||||
description="Current user.",
|
||||
default="Speckle User",
|
||||
)
|
||||
|
||||
receive_script: EnumProperty(
|
||||
name="Receive script",
|
||||
description="Script to run when receiving stream objects.",
|
||||
items=get_scripts,
|
||||
)
|
||||
|
||||
send_script: EnumProperty(
|
||||
name="Send script",
|
||||
description="Script to run when sending stream objects.",
|
||||
items=get_scripts,
|
||||
)
|
||||
@@ -1,18 +0,0 @@
|
||||
from .object import OBJECT_PT_speckle
|
||||
from .view3d import (
|
||||
VIEW3D_UL_SpeckleUsers,
|
||||
VIEW3D_UL_SpeckleStreams,
|
||||
VIEW3D_PT_SpeckleUser,
|
||||
VIEW3D_PT_SpeckleStreams,
|
||||
VIEW3D_PT_SpeckleActiveStream,
|
||||
VIEW3D_PT_SpeckleHelp,
|
||||
)
|
||||
|
||||
ui_classes = [
|
||||
VIEW3D_PT_SpeckleUser,
|
||||
VIEW3D_PT_SpeckleStreams,
|
||||
VIEW3D_PT_SpeckleActiveStream,
|
||||
VIEW3D_UL_SpeckleUsers,
|
||||
VIEW3D_UL_SpeckleStreams,
|
||||
VIEW3D_PT_SpeckleHelp,
|
||||
]
|
||||
@@ -1,35 +0,0 @@
|
||||
"""
|
||||
Object UI elements
|
||||
"""
|
||||
|
||||
import bpy
|
||||
from bpy.props import (
|
||||
StringProperty,
|
||||
BoolProperty,
|
||||
FloatProperty,
|
||||
CollectionProperty,
|
||||
EnumProperty,
|
||||
)
|
||||
|
||||
|
||||
class OBJECT_PT_speckle(bpy.types.Panel):
|
||||
bl_space_type = "PROPERTIES"
|
||||
# bl_idname = 'OBJECT_PT_speckle'
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "object"
|
||||
bl_label = "Speckle"
|
||||
|
||||
def draw_header(self, context):
|
||||
self.layout.prop(context.object.speckle, "enabled", text="")
|
||||
|
||||
def draw(self, context):
|
||||
ob = context.object
|
||||
layout = self.layout
|
||||
layout.active = ob.speckle.enabled
|
||||
col = layout.column()
|
||||
col.prop(ob.speckle, "send_or_receive", expand=True)
|
||||
col.prop(ob.speckle, "stream_id", text="Stream ID")
|
||||
col.prop(ob.speckle, "object_id", text="Object ID")
|
||||
col.operator("speckle.update_object", text="Update")
|
||||
col.operator("speckle.reset_object", text="Reset")
|
||||
col.operator("speckle.delete_object", text="Delete")
|
||||
@@ -1,277 +0,0 @@
|
||||
"""
|
||||
Speckle UI elements for the 3d viewport
|
||||
"""
|
||||
|
||||
|
||||
import bpy
|
||||
from bpy.props import (
|
||||
StringProperty,
|
||||
BoolProperty,
|
||||
FloatProperty,
|
||||
CollectionProperty,
|
||||
EnumProperty,
|
||||
)
|
||||
|
||||
import datetime
|
||||
|
||||
"""
|
||||
Compatibility
|
||||
TODO: evaluate if we should still support Blender <2.80
|
||||
"""
|
||||
|
||||
Region = "TOOLS" if bpy.app.version < (2, 80, 0) else "UI"
|
||||
|
||||
|
||||
def wrap(width, text):
|
||||
"""
|
||||
Split strings into width for
|
||||
wrapping
|
||||
"""
|
||||
lines = []
|
||||
|
||||
arr = text.split()
|
||||
lengthSum = 0
|
||||
|
||||
line = []
|
||||
for var in arr:
|
||||
lengthSum += len(var) + 1
|
||||
if lengthSum <= width:
|
||||
line.append(var)
|
||||
else:
|
||||
lines.append(" ".join(line))
|
||||
line = [var]
|
||||
lengthSum = len(var)
|
||||
|
||||
lines.append(" ".join(line))
|
||||
|
||||
return lines
|
||||
|
||||
|
||||
def get_available_users(self, context):
|
||||
"""
|
||||
Function to populate users list
|
||||
"""
|
||||
return [(a, a, a.name) for a in context.scene.speckle.users]
|
||||
|
||||
|
||||
class VIEW3D_UL_SpeckleUsers(bpy.types.UIList):
|
||||
"""
|
||||
Speckle user list
|
||||
"""
|
||||
|
||||
def draw_item(self, context, layout, data, user, active_data, active_propname):
|
||||
if self.layout_type in {"DEFAULT", "COMPACT"}:
|
||||
if user:
|
||||
# layout.prop(user, "name", text=user.name, emboss=False, icon_value=0)
|
||||
layout.label(
|
||||
text=user.name + " (" + user.email + ")",
|
||||
translate=False,
|
||||
icon_value=0,
|
||||
)
|
||||
else:
|
||||
layout.label(text="", translate=False, icon_value=0)
|
||||
|
||||
elif self.layout_type in {"GRID"}:
|
||||
layout.alignment = "CENTER"
|
||||
layout.label(text="Users", icon_value=0)
|
||||
|
||||
|
||||
class VIEW3D_UL_SpeckleStreams(bpy.types.UIList):
|
||||
"""
|
||||
Speckle stream list
|
||||
"""
|
||||
|
||||
def draw_item(self, context, layout, data, stream, active_data, active_propname):
|
||||
if self.layout_type in {"DEFAULT", "COMPACT"}:
|
||||
if stream:
|
||||
# layout.prop(user, "name", text=user.name, emboss=False, icon_value=0)
|
||||
layout.label(
|
||||
text="{} ({})".format(stream.name, stream.id),
|
||||
translate=False,
|
||||
icon_value=0,
|
||||
)
|
||||
else:
|
||||
layout.label(text=" ", translate=False, icon_value=0)
|
||||
|
||||
elif self.layout_type in {"GRID"}:
|
||||
layout.alignment = "CENTER"
|
||||
layout.label(text="Streams", icon_value=0)
|
||||
|
||||
|
||||
class VIEW3D_PT_SpeckleUser(bpy.types.Panel):
|
||||
"""
|
||||
Speckle Users UI panel in the 3d viewport
|
||||
"""
|
||||
|
||||
bl_space_type = "VIEW_3D"
|
||||
bl_region_type = Region
|
||||
bl_category = "Speckle"
|
||||
bl_context = "objectmode"
|
||||
bl_label = "User"
|
||||
|
||||
def draw(self, context):
|
||||
speckle = context.scene.speckle
|
||||
|
||||
layout = self.layout
|
||||
col = layout.column()
|
||||
|
||||
if len(speckle.users) < 1:
|
||||
col.label(text="No users found.")
|
||||
else:
|
||||
# col.label(text="User")
|
||||
col.prop(speckle, "active_user", text="")
|
||||
user = speckle.users[int(speckle.active_user)]
|
||||
col.label(text="{} ({})".format(user.server_name, user.server_url))
|
||||
col.label(text="{} ({})".format(user.name, user.email))
|
||||
|
||||
|
||||
class VIEW3D_PT_SpeckleStreams(bpy.types.Panel):
|
||||
"""
|
||||
Speckle Streams UI panel in the 3d viewport
|
||||
"""
|
||||
|
||||
bl_space_type = "VIEW_3D"
|
||||
bl_region_type = Region
|
||||
bl_category = "Speckle"
|
||||
bl_context = "objectmode"
|
||||
bl_label = "Streams"
|
||||
|
||||
def draw(self, context):
|
||||
speckle = context.scene.speckle
|
||||
col = self.layout.column()
|
||||
|
||||
if len(speckle.users) < 1:
|
||||
col.label(text="No stream data.")
|
||||
else:
|
||||
user = speckle.users[int(speckle.active_user)]
|
||||
col.template_list(
|
||||
"VIEW3D_UL_SpeckleStreams", "", user, "streams", user, "active_stream"
|
||||
)
|
||||
row = col.row(align=True)
|
||||
row.operator("speckle.add_stream_from_url", text="", icon="URL")
|
||||
row.operator("speckle.create_stream", text="", icon="ADD")
|
||||
row.operator("speckle.delete_stream", text="", icon="REMOVE")
|
||||
row.operator("speckle.load_user_streams", text="", icon="FILE_REFRESH")
|
||||
|
||||
|
||||
class VIEW3D_PT_SpeckleActiveStream(bpy.types.Panel):
|
||||
"""
|
||||
Speckle Active Streams UI panel in the 3d viewport
|
||||
"""
|
||||
|
||||
bl_space_type = "VIEW_3D"
|
||||
bl_region_type = Region
|
||||
bl_category = "Speckle"
|
||||
bl_context = "objectmode"
|
||||
bl_label = "Active stream"
|
||||
|
||||
def draw(self, context):
|
||||
speckle = context.scene.speckle
|
||||
col = self.layout.column()
|
||||
|
||||
if len(speckle.users) < 1:
|
||||
col.label(text="No stream data.")
|
||||
else:
|
||||
user = speckle.users[int(speckle.active_user)]
|
||||
if len(user.streams) < 1:
|
||||
col.label(text="No active stream.")
|
||||
else:
|
||||
stream = user.streams[user.active_stream]
|
||||
# user.active_stream = min(user.active_stream, len(user.streams) - 1)
|
||||
row = col.row()
|
||||
row.label(text="{} ({})".format(stream.name, stream.id))
|
||||
row.operator("speckle.stream_copy_id", text="", icon="COPY_ID")
|
||||
col.separator()
|
||||
|
||||
row = col.row()
|
||||
row.prop(stream, "branch", text="")
|
||||
row.operator("speckle.branch_copy_name", text="", icon="COPY_ID")
|
||||
|
||||
if len(stream.branches) > 0:
|
||||
branch = stream.branches[int(stream.branch)]
|
||||
|
||||
row = col.row()
|
||||
row.prop(branch, "commit", text="")
|
||||
row.operator("speckle.commit_copy_id", text="", icon="COPY_ID")
|
||||
|
||||
if len(branch.commits) > 0:
|
||||
commit = branch.commits[int(branch.commit)]
|
||||
area = col.box()
|
||||
area.separator()
|
||||
|
||||
lines = wrap(32, commit.message)
|
||||
for line in lines:
|
||||
row = area.row(align=True)
|
||||
row.alignment = "EXPAND"
|
||||
row.scale_y = 0.4
|
||||
row.label(text=line)
|
||||
area.separator()
|
||||
|
||||
dt = datetime.datetime.strptime(
|
||||
commit.created_at, "%Y-%m-%dT%H:%M:%S.%fZ"
|
||||
)
|
||||
col.label(text="{}".format(dt.ctime()))
|
||||
col.label(
|
||||
text="{} ({})".format(commit.author_name, commit.author_id)
|
||||
)
|
||||
col.label(text=commit.source_application)
|
||||
else:
|
||||
col.label(text="No branches found!")
|
||||
|
||||
col.separator()
|
||||
|
||||
area = col.box()
|
||||
row = area.row()
|
||||
subcol = row.column()
|
||||
subcol.operator("speckle.receive_stream_objects", text="Receive")
|
||||
subcol.prop(speckle, "receive_script", text="")
|
||||
subcol = row.column()
|
||||
subcol.operator("speckle.send_stream_objects", text="Send")
|
||||
subcol.prop(speckle, "send_script", text="")
|
||||
area.prop(stream, "query", text="Filter")
|
||||
|
||||
col.separator()
|
||||
|
||||
row = col.row(align=True)
|
||||
subcol = row.column()
|
||||
subcol.label(text="Units:")
|
||||
subcol = row.column()
|
||||
subcol.label(text=stream.units)
|
||||
|
||||
col.label(text="Description:")
|
||||
area = col.box()
|
||||
area.separator()
|
||||
|
||||
lines = wrap(32, stream.description)
|
||||
|
||||
for line in lines:
|
||||
row = area.row(align=True)
|
||||
row.alignment = "EXPAND"
|
||||
row.scale_y = 0.4
|
||||
row.label(text=line)
|
||||
|
||||
area.separator()
|
||||
col.separator()
|
||||
col.operator("speckle.view_stream_data_api", text="Open Stream in Web")
|
||||
|
||||
|
||||
class VIEW3D_PT_SpeckleHelp(bpy.types.Panel):
|
||||
"""
|
||||
Speckle Help UI panel in the 3d viewport
|
||||
"""
|
||||
|
||||
bl_space_type = "VIEW_3D"
|
||||
bl_region_type = Region
|
||||
bl_category = "Speckle"
|
||||
bl_context = "objectmode"
|
||||
bl_label = "Help"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
col = layout.column()
|
||||
|
||||
col.operator("speckle.open_speckle_guide")
|
||||
col.separator()
|
||||
col.operator("speckle.open_speckle_tutorials")
|
||||
col.separator()
|
||||
col.operator("speckle.open_speckle_forum")
|
||||
@@ -1,54 +0,0 @@
|
||||
def find_key_case_insensitive(data, key, default=None):
|
||||
value = data.get(key)
|
||||
if value:
|
||||
return value
|
||||
|
||||
"""
|
||||
Necessary to find keys where the first character
|
||||
is capitalized
|
||||
"""
|
||||
value = data.get(key[0].upper() + key[1:])
|
||||
if value:
|
||||
return value
|
||||
|
||||
value = data.get(key.upper())
|
||||
if value:
|
||||
return value
|
||||
|
||||
return default
|
||||
|
||||
|
||||
def get_iddata(base, uuid, name, obdata):
|
||||
"""
|
||||
This is taken from the import_3dm add-on:
|
||||
https://github.com/jesterKing/import_3dm
|
||||
# Copyright (c) 2018-2019 Nathan Letwory, Joel Putnam,
|
||||
Tom Svilans
|
||||
|
||||
Get an iddata. If an object with given uuid is found in
|
||||
this .blend use that. Otherwise new up one with base.new,
|
||||
potentially with obdata if that is set
|
||||
"""
|
||||
founditem = None
|
||||
if uuid is not None:
|
||||
for item in base:
|
||||
if item.get("speckle_id", None) == str(uuid):
|
||||
founditem = item
|
||||
break
|
||||
elif name:
|
||||
for item in base:
|
||||
if item.get("name", None) == name:
|
||||
founditem = item
|
||||
break
|
||||
if founditem:
|
||||
theitem = founditem
|
||||
theitem["name"] = name
|
||||
if obdata:
|
||||
theitem.data = obdata
|
||||
else:
|
||||
if obdata:
|
||||
theitem = base.new(name=name, object_data=obdata)
|
||||
else:
|
||||
theitem = base.new(name=name)
|
||||
tag_data(theitem, uuid, name)
|
||||
return theitem
|
||||
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env pwsh
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
uv pip compile pyproject.toml --output-file bpy_speckle/requirements.txt --generate-hashes
|
||||
Executable
+4
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e -o pipefail
|
||||
|
||||
uv pip compile pyproject.toml --output-file bpy_speckle/requirements.txt --generate-hashes
|
||||
+31
-44
@@ -2,64 +2,51 @@ import re
|
||||
import sys
|
||||
|
||||
|
||||
def patch_connector(tag):
|
||||
"""Patches the connector version within the connector init file"""
|
||||
bpy_file = "bpy_speckle/__init__.py"
|
||||
tag = tag.split(".")
|
||||
def patch_addon(simple_version: str):
|
||||
"""Patches the __init__.py bl_info version within the connector init file"""
|
||||
FILE_PATH = "bpy_speckle/__init__.py"
|
||||
version = simple_version.split(".")
|
||||
|
||||
with open(bpy_file, "r") as file:
|
||||
with open(FILE_PATH, "r") as file:
|
||||
lines = file.readlines()
|
||||
|
||||
for (index, line) in enumerate(lines):
|
||||
for index, line in enumerate(lines):
|
||||
if '"version":' in line:
|
||||
lines[index] = f' "version": ({tag[0]}, {tag[1]}, {tag[2]}),\n'
|
||||
print(f"Patched connector version number in {bpy_file}")
|
||||
lines[index] = (
|
||||
f' "version": ({version[0]}, {version[1]}, {version[2]}),\n'
|
||||
)
|
||||
|
||||
with open(FILE_PATH, "w") as file:
|
||||
file.writelines(lines)
|
||||
|
||||
|
||||
def patch_manifest(simple_version: str):
|
||||
"""Patches the connector version within the connector init file"""
|
||||
FILE_PATH = "bpy_speckle/blender_manifest.toml"
|
||||
version = simple_version.split(".")
|
||||
|
||||
with open(FILE_PATH, "r") as file:
|
||||
lines = file.readlines()
|
||||
|
||||
for index, line in enumerate(lines):
|
||||
if line.startswith("version ="):
|
||||
lines[index] = f'version = "{version[0]}.{version[1]}.{version[2]}"\n'
|
||||
print(f"Patched connector version number in {FILE_PATH}")
|
||||
break
|
||||
|
||||
with open(bpy_file, "w") as file:
|
||||
with open(FILE_PATH, "w") as file:
|
||||
file.writelines(lines)
|
||||
|
||||
|
||||
def patch_installer(tag):
|
||||
"""Patches the installer with the correct connector version and specklepy version"""
|
||||
iss_file = "speckle-sharp-ci-tools/blender.iss"
|
||||
|
||||
py_tag = get_specklepy_version()
|
||||
with open(iss_file, "r") as file:
|
||||
lines = file.readlines()
|
||||
lines.insert(11, f'#define SpecklepyVersion "{py_tag}"\n')
|
||||
lines.insert(11, f'#define AppVersion "{tag}"\n')
|
||||
|
||||
with open(iss_file, "w") as file:
|
||||
file.writelines(lines)
|
||||
print(f"Patched installer with connector v{tag} and specklepy v{py_tag}")
|
||||
|
||||
|
||||
def get_specklepy_version():
|
||||
"""Get version of specklepy to install from the pyproject.toml"""
|
||||
version = "2.3.3"
|
||||
with open("pyproject.toml", "r") as f:
|
||||
lines = [line for line in f if line.startswith("specklepy = ")]
|
||||
if not lines:
|
||||
raise Exception("Could not find specklepy in pyproject.toml")
|
||||
match = re.search(r"[0-9]+(\.[0-9]+)*", lines[0])
|
||||
if match:
|
||||
version = match[0]
|
||||
return version
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print(get_specklepy_version())
|
||||
return
|
||||
|
||||
tag = sys.argv[1]
|
||||
if not re.match(r"[0-9]+(\.[0-9]+)*$", tag):
|
||||
if not re.match(r"([0-9]+)\.([0-9]+)\.([0-9]+)", tag):
|
||||
raise ValueError(f"Invalid tag provided: {tag}")
|
||||
|
||||
print(f"Patching version: {tag}")
|
||||
patch_connector(tag)
|
||||
patch_installer(tag)
|
||||
simple_version = tag.split("-")[0]
|
||||
patch_addon(simple_version)
|
||||
patch_manifest(simple_version)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Generated
-824
@@ -1,824 +0,0 @@
|
||||
[[package]]
|
||||
name = "aiohttp"
|
||||
version = "3.7.4.post0"
|
||||
description = "Async http client/server framework (asyncio)"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[package.dependencies]
|
||||
async-timeout = ">=3.0,<4.0"
|
||||
attrs = ">=17.3.0"
|
||||
chardet = ">=2.0,<5.0"
|
||||
multidict = ">=4.5,<7.0"
|
||||
typing-extensions = ">=3.6.5"
|
||||
yarl = ">=1.0,<2.0"
|
||||
|
||||
[package.extras]
|
||||
speedups = ["aiodns", "brotlipy", "cchardet"]
|
||||
|
||||
[[package]]
|
||||
name = "appdirs"
|
||||
version = "1.4.4"
|
||||
description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "async-timeout"
|
||||
version = "3.0.1"
|
||||
description = "Timeout context manager for asyncio programs"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.5.3"
|
||||
|
||||
[[package]]
|
||||
name = "attrs"
|
||||
version = "21.2.0"
|
||||
description = "Classes Without Boilerplate"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||
|
||||
[package.extras]
|
||||
dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit"]
|
||||
docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"]
|
||||
tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface"]
|
||||
tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins"]
|
||||
|
||||
[[package]]
|
||||
name = "bpy"
|
||||
version = "2.82.1"
|
||||
description = "Blender as a python module"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7, <3.8"
|
||||
|
||||
[[package]]
|
||||
name = "bpy-build"
|
||||
version = "2.1.0"
|
||||
description = "Find Blender sources in version control, create build scripts"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.4.0"
|
||||
|
||||
[package.dependencies]
|
||||
cmake = ">=3.13.5"
|
||||
cmake-generators = "*"
|
||||
distro = "*"
|
||||
GitPython = "*"
|
||||
svn = "*"
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2021.10.8"
|
||||
description = "Python package for providing Mozilla's CA Bundle."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "chardet"
|
||||
version = "4.0.0"
|
||||
description = "Universal encoding detector for Python 2 and 3"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||
|
||||
[[package]]
|
||||
name = "charset-normalizer"
|
||||
version = "2.0.8"
|
||||
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.5.0"
|
||||
|
||||
[package.extras]
|
||||
unicode_backport = ["unicodedata2"]
|
||||
|
||||
[[package]]
|
||||
name = "cmake"
|
||||
version = "3.22.0"
|
||||
description = "CMake is an open-source, cross-platform family of tools designed to build, test and package software"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[package.extras]
|
||||
test = ["codecov (>=2.0.5)", "coverage (>=4.2)", "flake8 (>=3.0.4)", "path.py (>=11.5.0)", "pytest (>=3.0.3)", "pytest-cov (>=2.4.0)", "pytest-runner (>=2.9)", "pytest-virtualenv (>=1.7.0)", "scikit-build (>=0.10.0)", "setuptools (>=28.0.0)", "virtualenv (>=15.0.3)", "wheel"]
|
||||
|
||||
[[package]]
|
||||
name = "cmake-generators"
|
||||
version = "1.0.9"
|
||||
description = "Query cmake-generators"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.4.0"
|
||||
|
||||
[[package]]
|
||||
name = "devtools"
|
||||
version = "0.6.1"
|
||||
description = "Python's missing debug print command and other development tools."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[package.extras]
|
||||
pygments = ["Pygments (>=2.2.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "distro"
|
||||
version = "1.6.0"
|
||||
description = "Distro - an OS platform information API"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "gitdb"
|
||||
version = "4.0.9"
|
||||
description = "Git Object Database"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[package.dependencies]
|
||||
smmap = ">=3.0.1,<6"
|
||||
|
||||
[[package]]
|
||||
name = "gitpython"
|
||||
version = "3.1.24"
|
||||
description = "GitPython is a python library used to interact with Git repositories"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[package.dependencies]
|
||||
gitdb = ">=4.0.1,<5"
|
||||
typing-extensions = {version = ">=3.7.4.3", markers = "python_version < \"3.10\""}
|
||||
|
||||
[[package]]
|
||||
name = "gql"
|
||||
version = "3.0.0b1"
|
||||
description = "GraphQL client for Python"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[package.dependencies]
|
||||
aiohttp = {version = ">=3.7.1,<3.8.0", optional = true, markers = "extra == \"all\""}
|
||||
graphql-core = ">=3.1.5,<3.2"
|
||||
requests = {version = ">=2.26,<3", optional = true, markers = "extra == \"all\""}
|
||||
requests_toolbelt = {version = ">=0.9.1,<1", optional = true, markers = "extra == \"all\""}
|
||||
urllib3 = {version = ">=1.26", optional = true, markers = "extra == \"all\""}
|
||||
websockets = {version = ">=9,<10", optional = true, markers = "extra == \"all\""}
|
||||
yarl = ">=1.6,<2.0"
|
||||
|
||||
[package.extras]
|
||||
aiohttp = ["aiohttp (>=3.7.1,<3.8.0)"]
|
||||
all = ["aiohttp (>=3.7.1,<3.8.0)", "requests (>=2.26,<3)", "requests_toolbelt (>=0.9.1,<1)", "urllib3 (>=1.26)", "websockets (>=9,<10)"]
|
||||
dev = ["aiohttp (>=3.7.1,<3.8.0)", "requests (>=2.26,<3)", "requests_toolbelt (>=0.9.1,<1)", "urllib3 (>=1.26)", "websockets (>=9,<10)", "black (==19.10b0)", "check-manifest (>=0.42,<1)", "flake8 (==3.8.1)", "isort (==4.3.21)", "mypy (==0.770)", "sphinx (>=3.0.0,<4)", "sphinx_rtd_theme (>=0.4,<1)", "sphinx-argparse (==0.2.5)", "parse (==1.15.0)", "pytest (==5.4.2)", "pytest-asyncio (==0.11.0)", "pytest-cov (==2.8.1)", "mock (==4.0.2)", "vcrpy (==4.0.2)", "aiofiles"]
|
||||
requests = ["requests (>=2.26,<3)", "requests_toolbelt (>=0.9.1,<1)", "urllib3 (>=1.26)"]
|
||||
test = ["aiohttp (>=3.7.1,<3.8.0)", "requests (>=2.26,<3)", "requests_toolbelt (>=0.9.1,<1)", "urllib3 (>=1.26)", "websockets (>=9,<10)", "parse (==1.15.0)", "pytest (==5.4.2)", "pytest-asyncio (==0.11.0)", "pytest-cov (==2.8.1)", "mock (==4.0.2)", "vcrpy (==4.0.2)", "aiofiles"]
|
||||
test_no_transport = ["parse (==1.15.0)", "pytest (==5.4.2)", "pytest-asyncio (==0.11.0)", "pytest-cov (==2.8.1)", "mock (==4.0.2)", "vcrpy (==4.0.2)", "aiofiles"]
|
||||
websockets = ["websockets (>=9,<10)"]
|
||||
|
||||
[[package]]
|
||||
name = "graphql-core"
|
||||
version = "3.1.6"
|
||||
description = "GraphQL implementation for Python, a port of GraphQL.js, the JavaScript reference implementation for GraphQL."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.6,<4"
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.3"
|
||||
description = "Internationalized Domain Names in Applications (IDNA)"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.5"
|
||||
|
||||
[[package]]
|
||||
name = "multidict"
|
||||
version = "5.2.0"
|
||||
description = "multidict implementation"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[[package]]
|
||||
name = "nose"
|
||||
version = "1.3.7"
|
||||
description = "nose extends unittest to make testing easier"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "numpy"
|
||||
version = "1.21.4"
|
||||
description = "NumPy is the fundamental package for array computing with Python."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7,<3.11"
|
||||
|
||||
[[package]]
|
||||
name = "pydantic"
|
||||
version = "1.8.2"
|
||||
description = "Data validation and settings management using python 3.6 type hinting"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.6.1"
|
||||
|
||||
[package.dependencies]
|
||||
typing-extensions = ">=3.7.4.3"
|
||||
|
||||
[package.extras]
|
||||
dotenv = ["python-dotenv (>=0.10.4)"]
|
||||
email = ["email-validator (>=1.0.3)"]
|
||||
|
||||
[[package]]
|
||||
name = "python-dateutil"
|
||||
version = "2.8.2"
|
||||
description = "Extensions to the standard Python datetime module"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
|
||||
|
||||
[package.dependencies]
|
||||
six = ">=1.5"
|
||||
|
||||
[[package]]
|
||||
name = "requests"
|
||||
version = "2.26.0"
|
||||
description = "Python HTTP for Humans."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
|
||||
|
||||
[package.dependencies]
|
||||
certifi = ">=2017.4.17"
|
||||
charset-normalizer = {version = ">=2.0.0,<2.1.0", markers = "python_version >= \"3\""}
|
||||
idna = {version = ">=2.5,<4", markers = "python_version >= \"3\""}
|
||||
urllib3 = ">=1.21.1,<1.27"
|
||||
|
||||
[package.extras]
|
||||
socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"]
|
||||
use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"]
|
||||
|
||||
[[package]]
|
||||
name = "requests-toolbelt"
|
||||
version = "0.9.1"
|
||||
description = "A utility belt for advanced users of python-requests"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[package.dependencies]
|
||||
requests = ">=2.0.1,<3.0.0"
|
||||
|
||||
[[package]]
|
||||
name = "six"
|
||||
version = "1.16.0"
|
||||
description = "Python 2 and 3 compatibility utilities"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
|
||||
|
||||
[[package]]
|
||||
name = "smmap"
|
||||
version = "5.0.0"
|
||||
description = "A pure Python implementation of a sliding window memory map manager"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[[package]]
|
||||
name = "specklepy"
|
||||
version = "2.4.2"
|
||||
description = "The Python SDK for Speckle 2.0"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.6.5,<4.0.0"
|
||||
|
||||
[package.dependencies]
|
||||
appdirs = ">=1.4.4,<2.0.0"
|
||||
gql = {version = ">=3.0.0b1", extras = ["all"]}
|
||||
pydantic = ">=1.8.2,<2.0.0"
|
||||
ujson = ">=4.3.0,<5.0.0"
|
||||
|
||||
[[package]]
|
||||
name = "svn"
|
||||
version = "1.0.1"
|
||||
description = "Intuitive Subversion wrapper."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[package.dependencies]
|
||||
nose = "*"
|
||||
python-dateutil = ">=2.2"
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.0.0"
|
||||
description = "Backported and Experimental Type Hints for Python 3.6+"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[[package]]
|
||||
name = "ujson"
|
||||
version = "4.3.0"
|
||||
description = "Ultra fast JSON encoder and decoder for Python"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[[package]]
|
||||
name = "urllib3"
|
||||
version = "1.26.7"
|
||||
description = "HTTP library with thread-safe connection pooling, file post, and more."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4"
|
||||
|
||||
[package.extras]
|
||||
brotli = ["brotlipy (>=0.6.0)"]
|
||||
secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"]
|
||||
socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "websockets"
|
||||
version = "9.1"
|
||||
description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.6.1"
|
||||
|
||||
[[package]]
|
||||
name = "yarl"
|
||||
version = "1.7.2"
|
||||
description = "Yet another URL library"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[package.dependencies]
|
||||
idna = ">=2.0"
|
||||
multidict = ">=4.0"
|
||||
typing-extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""}
|
||||
|
||||
[metadata]
|
||||
lock-version = "1.1"
|
||||
python-versions = ">=3.7,<3.8"
|
||||
content-hash = "1459bac4b94b1801e6c6a0daebc87550e8cd6605ecfeb877aa0be1055c1d9380"
|
||||
|
||||
[metadata.files]
|
||||
aiohttp = [
|
||||
{file = "aiohttp-3.7.4.post0-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:3cf75f7cdc2397ed4442594b935a11ed5569961333d49b7539ea741be2cc79d5"},
|
||||
{file = "aiohttp-3.7.4.post0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:4b302b45040890cea949ad092479e01ba25911a15e648429c7c5aae9650c67a8"},
|
||||
{file = "aiohttp-3.7.4.post0-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:fe60131d21b31fd1a14bd43e6bb88256f69dfc3188b3a89d736d6c71ed43ec95"},
|
||||
{file = "aiohttp-3.7.4.post0-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:393f389841e8f2dfc86f774ad22f00923fdee66d238af89b70ea314c4aefd290"},
|
||||
{file = "aiohttp-3.7.4.post0-cp36-cp36m-manylinux2014_ppc64le.whl", hash = "sha256:c6e9dcb4cb338d91a73f178d866d051efe7c62a7166653a91e7d9fb18274058f"},
|
||||
{file = "aiohttp-3.7.4.post0-cp36-cp36m-manylinux2014_s390x.whl", hash = "sha256:5df68496d19f849921f05f14f31bd6ef53ad4b00245da3195048c69934521809"},
|
||||
{file = "aiohttp-3.7.4.post0-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:0563c1b3826945eecd62186f3f5c7d31abb7391fedc893b7e2b26303b5a9f3fe"},
|
||||
{file = "aiohttp-3.7.4.post0-cp36-cp36m-win32.whl", hash = "sha256:3d78619672183be860b96ed96f533046ec97ca067fd46ac1f6a09cd9b7484287"},
|
||||
{file = "aiohttp-3.7.4.post0-cp36-cp36m-win_amd64.whl", hash = "sha256:f705e12750171c0ab4ef2a3c76b9a4024a62c4103e3a55dd6f99265b9bc6fcfc"},
|
||||
{file = "aiohttp-3.7.4.post0-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:230a8f7e24298dea47659251abc0fd8b3c4e38a664c59d4b89cca7f6c09c9e87"},
|
||||
{file = "aiohttp-3.7.4.post0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:2e19413bf84934d651344783c9f5e22dee452e251cfd220ebadbed2d9931dbf0"},
|
||||
{file = "aiohttp-3.7.4.post0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:e4b2b334e68b18ac9817d828ba44d8fcb391f6acb398bcc5062b14b2cbeac970"},
|
||||
{file = "aiohttp-3.7.4.post0-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:d012ad7911653a906425d8473a1465caa9f8dea7fcf07b6d870397b774ea7c0f"},
|
||||
{file = "aiohttp-3.7.4.post0-cp37-cp37m-manylinux2014_ppc64le.whl", hash = "sha256:40eced07f07a9e60e825554a31f923e8d3997cfc7fb31dbc1328c70826e04cde"},
|
||||
{file = "aiohttp-3.7.4.post0-cp37-cp37m-manylinux2014_s390x.whl", hash = "sha256:209b4a8ee987eccc91e2bd3ac36adee0e53a5970b8ac52c273f7f8fd4872c94c"},
|
||||
{file = "aiohttp-3.7.4.post0-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:14762875b22d0055f05d12abc7f7d61d5fd4fe4642ce1a249abdf8c700bf1fd8"},
|
||||
{file = "aiohttp-3.7.4.post0-cp37-cp37m-win32.whl", hash = "sha256:7615dab56bb07bff74bc865307aeb89a8bfd9941d2ef9d817b9436da3a0ea54f"},
|
||||
{file = "aiohttp-3.7.4.post0-cp37-cp37m-win_amd64.whl", hash = "sha256:d9e13b33afd39ddeb377eff2c1c4f00544e191e1d1dee5b6c51ddee8ea6f0cf5"},
|
||||
{file = "aiohttp-3.7.4.post0-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:547da6cacac20666422d4882cfcd51298d45f7ccb60a04ec27424d2f36ba3eaf"},
|
||||
{file = "aiohttp-3.7.4.post0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:af9aa9ef5ba1fd5b8c948bb11f44891968ab30356d65fd0cc6707d989cd521df"},
|
||||
{file = "aiohttp-3.7.4.post0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:64322071e046020e8797117b3658b9c2f80e3267daec409b350b6a7a05041213"},
|
||||
{file = "aiohttp-3.7.4.post0-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:bb437315738aa441251214dad17428cafda9cdc9729499f1d6001748e1d432f4"},
|
||||
{file = "aiohttp-3.7.4.post0-cp38-cp38-manylinux2014_ppc64le.whl", hash = "sha256:e54962802d4b8b18b6207d4a927032826af39395a3bd9196a5af43fc4e60b009"},
|
||||
{file = "aiohttp-3.7.4.post0-cp38-cp38-manylinux2014_s390x.whl", hash = "sha256:a00bb73540af068ca7390e636c01cbc4f644961896fa9363154ff43fd37af2f5"},
|
||||
{file = "aiohttp-3.7.4.post0-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:79ebfc238612123a713a457d92afb4096e2148be17df6c50fb9bf7a81c2f8013"},
|
||||
{file = "aiohttp-3.7.4.post0-cp38-cp38-win32.whl", hash = "sha256:515dfef7f869a0feb2afee66b957cc7bbe9ad0cdee45aec7fdc623f4ecd4fb16"},
|
||||
{file = "aiohttp-3.7.4.post0-cp38-cp38-win_amd64.whl", hash = "sha256:114b281e4d68302a324dd33abb04778e8557d88947875cbf4e842c2c01a030c5"},
|
||||
{file = "aiohttp-3.7.4.post0-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:7b18b97cf8ee5452fa5f4e3af95d01d84d86d32c5e2bfa260cf041749d66360b"},
|
||||
{file = "aiohttp-3.7.4.post0-cp39-cp39-manylinux1_i686.whl", hash = "sha256:15492a6368d985b76a2a5fdd2166cddfea5d24e69eefed4630cbaae5c81d89bd"},
|
||||
{file = "aiohttp-3.7.4.post0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:bdb230b4943891321e06fc7def63c7aace16095be7d9cf3b1e01be2f10fba439"},
|
||||
{file = "aiohttp-3.7.4.post0-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:cffe3ab27871bc3ea47df5d8f7013945712c46a3cc5a95b6bee15887f1675c22"},
|
||||
{file = "aiohttp-3.7.4.post0-cp39-cp39-manylinux2014_ppc64le.whl", hash = "sha256:f881853d2643a29e643609da57b96d5f9c9b93f62429dcc1cbb413c7d07f0e1a"},
|
||||
{file = "aiohttp-3.7.4.post0-cp39-cp39-manylinux2014_s390x.whl", hash = "sha256:a5ca29ee66f8343ed336816c553e82d6cade48a3ad702b9ffa6125d187e2dedb"},
|
||||
{file = "aiohttp-3.7.4.post0-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:17c073de315745a1510393a96e680d20af8e67e324f70b42accbd4cb3315c9fb"},
|
||||
{file = "aiohttp-3.7.4.post0-cp39-cp39-win32.whl", hash = "sha256:932bb1ea39a54e9ea27fc9232163059a0b8855256f4052e776357ad9add6f1c9"},
|
||||
{file = "aiohttp-3.7.4.post0-cp39-cp39-win_amd64.whl", hash = "sha256:02f46fc0e3c5ac58b80d4d56eb0a7c7d97fcef69ace9326289fb9f1955e65cfe"},
|
||||
{file = "aiohttp-3.7.4.post0.tar.gz", hash = "sha256:493d3299ebe5f5a7c66b9819eacdcfbbaaf1a8e84911ddffcdc48888497afecf"},
|
||||
]
|
||||
appdirs = [
|
||||
{file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"},
|
||||
{file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"},
|
||||
]
|
||||
async-timeout = [
|
||||
{file = "async-timeout-3.0.1.tar.gz", hash = "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f"},
|
||||
{file = "async_timeout-3.0.1-py3-none-any.whl", hash = "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3"},
|
||||
]
|
||||
attrs = [
|
||||
{file = "attrs-21.2.0-py2.py3-none-any.whl", hash = "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1"},
|
||||
{file = "attrs-21.2.0.tar.gz", hash = "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"},
|
||||
]
|
||||
bpy = [
|
||||
{file = "bpy-2.82.1-cp37-cp37m-win_amd64.whl", hash = "sha256:ccbd1b7a62ffe67978723e91ff4df733c1a6b7c35a83bc0364b012ce4390167a"},
|
||||
{file = "bpy-2.82.1.tar.gz", hash = "sha256:5f84452552724ae32faca9d001741e982f9cd61640c39975c6a6b734b29f5c35"},
|
||||
]
|
||||
bpy-build = [
|
||||
{file = "bpy-build-2.1.0.tar.gz", hash = "sha256:8b6c621ec51718bdc3c973e47060bfd8df175416d32c1700b677e994e1b8e53c"},
|
||||
{file = "bpy_build-2.1.0-py3-none-any.whl", hash = "sha256:29a72b4b83e7612db8e8208f19854250aad0c4310f883998d9eae01fe645dbc4"},
|
||||
]
|
||||
certifi = [
|
||||
{file = "certifi-2021.10.8-py2.py3-none-any.whl", hash = "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"},
|
||||
{file = "certifi-2021.10.8.tar.gz", hash = "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872"},
|
||||
]
|
||||
chardet = [
|
||||
{file = "chardet-4.0.0-py2.py3-none-any.whl", hash = "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"},
|
||||
{file = "chardet-4.0.0.tar.gz", hash = "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa"},
|
||||
]
|
||||
charset-normalizer = [
|
||||
{file = "charset-normalizer-2.0.8.tar.gz", hash = "sha256:735e240d9a8506778cd7a453d97e817e536bb1fc29f4f6961ce297b9c7a917b0"},
|
||||
{file = "charset_normalizer-2.0.8-py3-none-any.whl", hash = "sha256:83fcdeb225499d6344c8f7f34684c2981270beacc32ede2e669e94f7fa544405"},
|
||||
]
|
||||
cmake = [
|
||||
{file = "cmake-3.22.0-py2.py3-none-macosx_10_9_universal2.macosx_10_9_x86_64.macosx_11_0_arm64.macosx_11_0_universal2.whl", hash = "sha256:99b53d7c4aeea6ec6c61929c642b20265f56d3471a0c38858ea4894d1554683a"},
|
||||
{file = "cmake-3.22.0-py2.py3-none-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:282dbb00f1c0aeb58243b490cb1e033ac159b9c13154c78afb08f18126472660"},
|
||||
{file = "cmake-3.22.0-py2.py3-none-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4f9f672a556921c92033dc084425990a8823d23ff468a24d154729f4418710ab"},
|
||||
{file = "cmake-3.22.0-py2.py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e45e4499572fe3d76123c1d18d5065a5babad3ed5020465fdf88749205ee8dc2"},
|
||||
{file = "cmake-3.22.0-py2.py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7d7f6b9a2d31e7a94b989cc672f242a0286a5f6a2bbc0057e6e9455794f7c127"},
|
||||
{file = "cmake-3.22.0-py2.py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d9904ca81ac2a3dc4c27886c8411e90ca8b0f447f6b16a7caf545078a9979730"},
|
||||
{file = "cmake-3.22.0-py2.py3-none-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cda7e807c7c5a0a4f2df8e3f10a9871a292d0114a1f055b8c4c599911ab8f506"},
|
||||
{file = "cmake-3.22.0-py2.py3-none-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:57e44238d36f4ffffe6e31b064687ce03fae6dc46ec4766b5d82638a2c0c8be4"},
|
||||
{file = "cmake-3.22.0-py2.py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:0aa6bb09eef0c927669d3c9f48d54f28968da7d2c916e693e626814b2e0b3865"},
|
||||
{file = "cmake-3.22.0-py2.py3-none-musllinux_1_1_i686.whl", hash = "sha256:72a13c7d59cd4af9d62da3bd19b4b509e353b7fcebef9887f73d23bbfd82a7b5"},
|
||||
{file = "cmake-3.22.0-py2.py3-none-musllinux_1_1_ppc64le.whl", hash = "sha256:fc62764c07d49607aefa79be05023f1a7698614d03e2f28b5116a977841423b1"},
|
||||
{file = "cmake-3.22.0-py2.py3-none-musllinux_1_1_s390x.whl", hash = "sha256:78b2a8e23cbcd054d63be38b88c5920b48d68d0d620a7ad3389577e370e7a91a"},
|
||||
{file = "cmake-3.22.0-py2.py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:1b24893566cf60350a4601c28f3111c55b48e9da1104f591170748404e8d27b8"},
|
||||
{file = "cmake-3.22.0-py2.py3-none-win32.whl", hash = "sha256:8affabaa556acbf3ef863d453b35193f208618e2f2e7955a9eeaf431bd9ebd4c"},
|
||||
{file = "cmake-3.22.0-py2.py3-none-win_amd64.whl", hash = "sha256:c268288cb088c3de4afc73e10830f64494db2ea78c457f287cb8ccafdffd1eb9"},
|
||||
{file = "cmake-3.22.0.tar.gz", hash = "sha256:5a3ccf93dea99791db93cd8babb52e4b70f92d2b380705ee35570cdd5b7a8e8b"},
|
||||
]
|
||||
cmake-generators = [
|
||||
{file = "cmake-generators-1.0.9.tar.gz", hash = "sha256:2b03bdcd69345c03ffa56aa025b3163bd26b1465561c365a79cf1c6a31672ebf"},
|
||||
{file = "cmake_generators-1.0.9-py3-none-any.whl", hash = "sha256:23428a5b7c6b1f54071dcf88f962fdf958f5c344d9df22330541378c83bf386b"},
|
||||
]
|
||||
devtools = [
|
||||
{file = "devtools-0.6.1-py3-none-any.whl", hash = "sha256:7334183972a8d04e81d08b7f62126abca9b6f4de51d825c5fdcb9c88f252601a"},
|
||||
{file = "devtools-0.6.1.tar.gz", hash = "sha256:a054307594d35d28fae8df7629967363e851ae0ac7b2152640a8a401c39d42d7"},
|
||||
]
|
||||
distro = [
|
||||
{file = "distro-1.6.0-py2.py3-none-any.whl", hash = "sha256:c8713330ab31a034623a9515663ed87696700b55f04556b97c39cd261aa70dc7"},
|
||||
{file = "distro-1.6.0.tar.gz", hash = "sha256:83f5e5a09f9c5f68f60173de572930effbcc0287bb84fdc4426cb4168c088424"},
|
||||
]
|
||||
gitdb = [
|
||||
{file = "gitdb-4.0.9-py3-none-any.whl", hash = "sha256:8033ad4e853066ba6ca92050b9df2f89301b8fc8bf7e9324d412a63f8bf1a8fd"},
|
||||
{file = "gitdb-4.0.9.tar.gz", hash = "sha256:bac2fd45c0a1c9cf619e63a90d62bdc63892ef92387424b855792a6cabe789aa"},
|
||||
]
|
||||
gitpython = [
|
||||
{file = "GitPython-3.1.24-py3-none-any.whl", hash = "sha256:dc0a7f2f697657acc8d7f89033e8b1ea94dd90356b2983bca89dc8d2ab3cc647"},
|
||||
{file = "GitPython-3.1.24.tar.gz", hash = "sha256:df83fdf5e684fef7c6ee2c02fc68a5ceb7e7e759d08b694088d0cacb4eba59e5"},
|
||||
]
|
||||
gql = [
|
||||
{file = "gql-3.0.0b1.tar.gz", hash = "sha256:a08195e73ed08785e1aef71a540e514f80dbfa711b9effba31ba15fcb687f1fd"},
|
||||
]
|
||||
graphql-core = [
|
||||
{file = "graphql-core-3.1.6.tar.gz", hash = "sha256:e65975b6a13878f9113a1fa5320760585b522d139944e005936b1b8358d0651a"},
|
||||
{file = "graphql_core-3.1.6-py3-none-any.whl", hash = "sha256:c78d09596d347e1cffd266c5384abfedf43ed1eae08729773bebb3d527fe5a14"},
|
||||
]
|
||||
idna = [
|
||||
{file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"},
|
||||
{file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"},
|
||||
]
|
||||
multidict = [
|
||||
{file = "multidict-5.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3822c5894c72e3b35aae9909bef66ec83e44522faf767c0ad39e0e2de11d3b55"},
|
||||
{file = "multidict-5.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:28e6d883acd8674887d7edc896b91751dc2d8e87fbdca8359591a13872799e4e"},
|
||||
{file = "multidict-5.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b61f85101ef08cbbc37846ac0e43f027f7844f3fade9b7f6dd087178caedeee7"},
|
||||
{file = "multidict-5.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d9b668c065968c5979fe6b6fa6760bb6ab9aeb94b75b73c0a9c1acf6393ac3bf"},
|
||||
{file = "multidict-5.2.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:517d75522b7b18a3385726b54a081afd425d4f41144a5399e5abd97ccafdf36b"},
|
||||
{file = "multidict-5.2.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1b4ac3ba7a97b35a5ccf34f41b5a8642a01d1e55454b699e5e8e7a99b5a3acf5"},
|
||||
{file = "multidict-5.2.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:df23c83398715b26ab09574217ca21e14694917a0c857e356fd39e1c64f8283f"},
|
||||
{file = "multidict-5.2.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e58a9b5cc96e014ddf93c2227cbdeca94b56a7eb77300205d6e4001805391747"},
|
||||
{file = "multidict-5.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:f76440e480c3b2ca7f843ff8a48dc82446b86ed4930552d736c0bac507498a52"},
|
||||
{file = "multidict-5.2.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:cfde464ca4af42a629648c0b0d79b8f295cf5b695412451716531d6916461628"},
|
||||
{file = "multidict-5.2.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:0fed465af2e0eb6357ba95795d003ac0bdb546305cc2366b1fc8f0ad67cc3fda"},
|
||||
{file = "multidict-5.2.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:b70913cbf2e14275013be98a06ef4b412329fe7b4f83d64eb70dce8269ed1e1a"},
|
||||
{file = "multidict-5.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a5635bcf1b75f0f6ef3c8a1ad07b500104a971e38d3683167b9454cb6465ac86"},
|
||||
{file = "multidict-5.2.0-cp310-cp310-win32.whl", hash = "sha256:77f0fb7200cc7dedda7a60912f2059086e29ff67cefbc58d2506638c1a9132d7"},
|
||||
{file = "multidict-5.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:9416cf11bcd73c861267e88aea71e9fcc35302b3943e45e1dbb4317f91a4b34f"},
|
||||
{file = "multidict-5.2.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:fd77c8f3cba815aa69cb97ee2b2ef385c7c12ada9c734b0f3b32e26bb88bbf1d"},
|
||||
{file = "multidict-5.2.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:98ec9aea6223adf46999f22e2c0ab6cf33f5914be604a404f658386a8f1fba37"},
|
||||
{file = "multidict-5.2.0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e5283c0a00f48e8cafcecadebfa0ed1dac8b39e295c7248c44c665c16dc1138b"},
|
||||
{file = "multidict-5.2.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5f79c19c6420962eb17c7e48878a03053b7ccd7b69f389d5831c0a4a7f1ac0a1"},
|
||||
{file = "multidict-5.2.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:e4a67f1080123de76e4e97a18d10350df6a7182e243312426d508712e99988d4"},
|
||||
{file = "multidict-5.2.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:94b117e27efd8e08b4046c57461d5a114d26b40824995a2eb58372b94f9fca02"},
|
||||
{file = "multidict-5.2.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:2e77282fd1d677c313ffcaddfec236bf23f273c4fba7cdf198108f5940ae10f5"},
|
||||
{file = "multidict-5.2.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:116347c63ba049c1ea56e157fa8aa6edaf5e92925c9b64f3da7769bdfa012858"},
|
||||
{file = "multidict-5.2.0-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:dc3a866cf6c13d59a01878cd806f219340f3e82eed514485e094321f24900677"},
|
||||
{file = "multidict-5.2.0-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:ac42181292099d91217a82e3fa3ce0e0ddf3a74fd891b7c2b347a7f5aa0edded"},
|
||||
{file = "multidict-5.2.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:f0bb0973f42ffcb5e3537548e0767079420aefd94ba990b61cf7bb8d47f4916d"},
|
||||
{file = "multidict-5.2.0-cp36-cp36m-win32.whl", hash = "sha256:ea21d4d5104b4f840b91d9dc8cbc832aba9612121eaba503e54eaab1ad140eb9"},
|
||||
{file = "multidict-5.2.0-cp36-cp36m-win_amd64.whl", hash = "sha256:e6453f3cbeb78440747096f239d282cc57a2997a16b5197c9bc839099e1633d0"},
|
||||
{file = "multidict-5.2.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d3def943bfd5f1c47d51fd324df1e806d8da1f8e105cc7f1c76a1daf0f7e17b0"},
|
||||
{file = "multidict-5.2.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35591729668a303a02b06e8dba0eb8140c4a1bfd4c4b3209a436a02a5ac1de11"},
|
||||
{file = "multidict-5.2.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8cacda0b679ebc25624d5de66c705bc53dcc7c6f02a7fb0f3ca5e227d80422"},
|
||||
{file = "multidict-5.2.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:baf1856fab8212bf35230c019cde7c641887e3fc08cadd39d32a421a30151ea3"},
|
||||
{file = "multidict-5.2.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a43616aec0f0d53c411582c451f5d3e1123a68cc7b3475d6f7d97a626f8ff90d"},
|
||||
{file = "multidict-5.2.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:25cbd39a9029b409167aa0a20d8a17f502d43f2efebfe9e3ac019fe6796c59ac"},
|
||||
{file = "multidict-5.2.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:0a2cbcfbea6dc776782a444db819c8b78afe4db597211298dd8b2222f73e9cd0"},
|
||||
{file = "multidict-5.2.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:3d2d7d1fff8e09d99354c04c3fd5b560fb04639fd45926b34e27cfdec678a704"},
|
||||
{file = "multidict-5.2.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:a37e9a68349f6abe24130846e2f1d2e38f7ddab30b81b754e5a1fde32f782b23"},
|
||||
{file = "multidict-5.2.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:637c1896497ff19e1ee27c1c2c2ddaa9f2d134bbb5e0c52254361ea20486418d"},
|
||||
{file = "multidict-5.2.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:9815765f9dcda04921ba467957be543423e5ec6a1136135d84f2ae092c50d87b"},
|
||||
{file = "multidict-5.2.0-cp37-cp37m-win32.whl", hash = "sha256:8b911d74acdc1fe2941e59b4f1a278a330e9c34c6c8ca1ee21264c51ec9b67ef"},
|
||||
{file = "multidict-5.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:380b868f55f63d048a25931a1632818f90e4be71d2081c2338fcf656d299949a"},
|
||||
{file = "multidict-5.2.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e7d81ce5744757d2f05fc41896e3b2ae0458464b14b5a2c1e87a6a9d69aefaa8"},
|
||||
{file = "multidict-5.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2d1d55cdf706ddc62822d394d1df53573d32a7a07d4f099470d3cb9323b721b6"},
|
||||
{file = "multidict-5.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a4771d0d0ac9d9fe9e24e33bed482a13dfc1256d008d101485fe460359476065"},
|
||||
{file = "multidict-5.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da7d57ea65744d249427793c042094c4016789eb2562576fb831870f9c878d9e"},
|
||||
{file = "multidict-5.2.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cdd68778f96216596218b4e8882944d24a634d984ee1a5a049b300377878fa7c"},
|
||||
{file = "multidict-5.2.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ecc99bce8ee42dcad15848c7885197d26841cb24fa2ee6e89d23b8993c871c64"},
|
||||
{file = "multidict-5.2.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:067150fad08e6f2dd91a650c7a49ba65085303fcc3decbd64a57dc13a2733031"},
|
||||
{file = "multidict-5.2.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:78c106b2b506b4d895ddc801ff509f941119394b89c9115580014127414e6c2d"},
|
||||
{file = "multidict-5.2.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e6c4fa1ec16e01e292315ba76eb1d012c025b99d22896bd14a66628b245e3e01"},
|
||||
{file = "multidict-5.2.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:b227345e4186809d31f22087d0265655114af7cda442ecaf72246275865bebe4"},
|
||||
{file = "multidict-5.2.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:06560fbdcf22c9387100979e65b26fba0816c162b888cb65b845d3def7a54c9b"},
|
||||
{file = "multidict-5.2.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:7878b61c867fb2df7a95e44b316f88d5a3742390c99dfba6c557a21b30180cac"},
|
||||
{file = "multidict-5.2.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:246145bff76cc4b19310f0ad28bd0769b940c2a49fc601b86bfd150cbd72bb22"},
|
||||
{file = "multidict-5.2.0-cp38-cp38-win32.whl", hash = "sha256:c30ac9f562106cd9e8071c23949a067b10211917fdcb75b4718cf5775356a940"},
|
||||
{file = "multidict-5.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:f19001e790013ed580abfde2a4465388950728861b52f0da73e8e8a9418533c0"},
|
||||
{file = "multidict-5.2.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c1ff762e2ee126e6f1258650ac641e2b8e1f3d927a925aafcfde943b77a36d24"},
|
||||
{file = "multidict-5.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bd6c9c50bf2ad3f0448edaa1a3b55b2e6866ef8feca5d8dbec10ec7c94371d21"},
|
||||
{file = "multidict-5.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fc66d4016f6e50ed36fb39cd287a3878ffcebfa90008535c62e0e90a7ab713ae"},
|
||||
{file = "multidict-5.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9acb76d5f3dd9421874923da2ed1e76041cb51b9337fd7f507edde1d86535d6"},
|
||||
{file = "multidict-5.2.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dfc924a7e946dd3c6360e50e8f750d51e3ef5395c95dc054bc9eab0f70df4f9c"},
|
||||
{file = "multidict-5.2.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:32fdba7333eb2351fee2596b756d730d62b5827d5e1ab2f84e6cbb287cc67fe0"},
|
||||
{file = "multidict-5.2.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:b9aad49466b8d828b96b9e3630006234879c8d3e2b0a9d99219b3121bc5cdb17"},
|
||||
{file = "multidict-5.2.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:93de39267c4c676c9ebb2057e98a8138bade0d806aad4d864322eee0803140a0"},
|
||||
{file = "multidict-5.2.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f9bef5cff994ca3026fcc90680e326d1a19df9841c5e3d224076407cc21471a1"},
|
||||
{file = "multidict-5.2.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:5f841c4f14331fd1e36cbf3336ed7be2cb2a8f110ce40ea253e5573387db7621"},
|
||||
{file = "multidict-5.2.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:38ba256ee9b310da6a1a0f013ef4e422fca30a685bcbec86a969bd520504e341"},
|
||||
{file = "multidict-5.2.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:3bc3b1621b979621cee9f7b09f024ec76ec03cc365e638126a056317470bde1b"},
|
||||
{file = "multidict-5.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6ee908c070020d682e9b42c8f621e8bb10c767d04416e2ebe44e37d0f44d9ad5"},
|
||||
{file = "multidict-5.2.0-cp39-cp39-win32.whl", hash = "sha256:1c7976cd1c157fa7ba5456ae5d31ccdf1479680dc9b8d8aa28afabc370df42b8"},
|
||||
{file = "multidict-5.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:c9631c642e08b9fff1c6255487e62971d8b8e821808ddd013d8ac058087591ac"},
|
||||
{file = "multidict-5.2.0.tar.gz", hash = "sha256:0dd1c93edb444b33ba2274b66f63def8a327d607c6c790772f448a53b6ea59ce"},
|
||||
]
|
||||
nose = [
|
||||
{file = "nose-1.3.7-py2-none-any.whl", hash = "sha256:dadcddc0aefbf99eea214e0f1232b94f2fa9bd98fa8353711dacb112bfcbbb2a"},
|
||||
{file = "nose-1.3.7-py3-none-any.whl", hash = "sha256:9ff7c6cc443f8c51994b34a667bbcf45afd6d945be7477b52e97516fd17c53ac"},
|
||||
{file = "nose-1.3.7.tar.gz", hash = "sha256:f1bffef9cbc82628f6e7d7b40d7e255aefaa1adb6a1b1d26c69a8b79e6208a98"},
|
||||
]
|
||||
numpy = [
|
||||
{file = "numpy-1.21.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8890b3360f345e8360133bc078d2dacc2843b6ee6059b568781b15b97acbe39f"},
|
||||
{file = "numpy-1.21.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:69077388c5a4b997442b843dbdc3a85b420fb693ec8e33020bb24d647c164fa5"},
|
||||
{file = "numpy-1.21.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e89717274b41ebd568cd7943fc9418eeb49b1785b66031bc8a7f6300463c5898"},
|
||||
{file = "numpy-1.21.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b78ecfa070460104934e2caf51694ccd00f37d5e5dbe76f021b1b0b0d221823"},
|
||||
{file = "numpy-1.21.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:615d4e328af7204c13ae3d4df7615a13ff60a49cb0d9106fde07f541207883ca"},
|
||||
{file = "numpy-1.21.4-cp310-cp310-win_amd64.whl", hash = "sha256:1403b4e2181fc72664737d848b60e65150f272fe5a1c1cbc16145ed43884065a"},
|
||||
{file = "numpy-1.21.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:74b85a17528ca60cf98381a5e779fc0264b4a88b46025e6bcbe9621f46bb3e63"},
|
||||
{file = "numpy-1.21.4-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:92aafa03da8658609f59f18722b88f0a73a249101169e28415b4fa148caf7e41"},
|
||||
{file = "numpy-1.21.4-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5d95668e727c75b3f5088ec7700e260f90ec83f488e4c0aaccb941148b2cd377"},
|
||||
{file = "numpy-1.21.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f5162ec777ba7138906c9c274353ece5603646c6965570d82905546579573f73"},
|
||||
{file = "numpy-1.21.4-cp37-cp37m-win32.whl", hash = "sha256:81225e58ef5fce7f1d80399575576fc5febec79a8a2742e8ef86d7b03beef49f"},
|
||||
{file = "numpy-1.21.4-cp37-cp37m-win_amd64.whl", hash = "sha256:32fe5b12061f6446adcbb32cf4060a14741f9c21e15aaee59a207b6ce6423469"},
|
||||
{file = "numpy-1.21.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:c449eb870616a7b62e097982c622d2577b3dbc800aaf8689254ec6e0197cbf1e"},
|
||||
{file = "numpy-1.21.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2e4ed57f45f0aa38beca2a03b6532e70e548faf2debbeb3291cfc9b315d9be8f"},
|
||||
{file = "numpy-1.21.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1247ef28387b7bb7f21caf2dbe4767f4f4175df44d30604d42ad9bd701ebb31f"},
|
||||
{file = "numpy-1.21.4-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:34f3456f530ae8b44231c63082c8899fe9c983fd9b108c997c4b1c8c2d435333"},
|
||||
{file = "numpy-1.21.4-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4c9c23158b87ed0e70d9a50c67e5c0b3f75bcf2581a8e34668d4e9d7474d76c6"},
|
||||
{file = "numpy-1.21.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4799be6a2d7d3c33699a6f77201836ac975b2e1b98c2a07f66a38f499cb50ce"},
|
||||
{file = "numpy-1.21.4-cp38-cp38-win32.whl", hash = "sha256:bc988afcea53e6156546e5b2885b7efab089570783d9d82caf1cfd323b0bb3dd"},
|
||||
{file = "numpy-1.21.4-cp38-cp38-win_amd64.whl", hash = "sha256:170b2a0805c6891ca78c1d96ee72e4c3ed1ae0a992c75444b6ab20ff038ba2cd"},
|
||||
{file = "numpy-1.21.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:fde96af889262e85aa033f8ee1d3241e32bf36228318a61f1ace579df4e8170d"},
|
||||
{file = "numpy-1.21.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c885bfc07f77e8fee3dc879152ba993732601f1f11de248d4f357f0ffea6a6d4"},
|
||||
{file = "numpy-1.21.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9e6f5f50d1eff2f2f752b3089a118aee1ea0da63d56c44f3865681009b0af162"},
|
||||
{file = "numpy-1.21.4-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ad010846cdffe7ec27e3f933397f8a8d6c801a48634f419e3d075db27acf5880"},
|
||||
{file = "numpy-1.21.4-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c74c699b122918a6c4611285cc2cad4a3aafdb135c22a16ec483340ef97d573c"},
|
||||
{file = "numpy-1.21.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9864424631775b0c052f3bd98bc2712d131b3e2cd95d1c0c68b91709170890b0"},
|
||||
{file = "numpy-1.21.4-cp39-cp39-win32.whl", hash = "sha256:b1e2312f5b8843a3e4e8224b2b48fe16119617b8fc0a54df8f50098721b5bed2"},
|
||||
{file = "numpy-1.21.4-cp39-cp39-win_amd64.whl", hash = "sha256:e3c3e990274444031482a31280bf48674441e0a5b55ddb168f3a6db3e0c38ec8"},
|
||||
{file = "numpy-1.21.4-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a3deb31bc84f2b42584b8c4001c85d1934dbfb4030827110bc36bfd11509b7bf"},
|
||||
{file = "numpy-1.21.4.zip", hash = "sha256:e6c76a87633aa3fa16614b61ccedfae45b91df2767cf097aa9c933932a7ed1e0"},
|
||||
]
|
||||
pydantic = [
|
||||
{file = "pydantic-1.8.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:05ddfd37c1720c392f4e0d43c484217b7521558302e7069ce8d318438d297739"},
|
||||
{file = "pydantic-1.8.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:a7c6002203fe2c5a1b5cbb141bb85060cbff88c2d78eccbc72d97eb7022c43e4"},
|
||||
{file = "pydantic-1.8.2-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:589eb6cd6361e8ac341db97602eb7f354551482368a37f4fd086c0733548308e"},
|
||||
{file = "pydantic-1.8.2-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:10e5622224245941efc193ad1d159887872776df7a8fd592ed746aa25d071840"},
|
||||
{file = "pydantic-1.8.2-cp36-cp36m-win_amd64.whl", hash = "sha256:99a9fc39470010c45c161a1dc584997f1feb13f689ecf645f59bb4ba623e586b"},
|
||||
{file = "pydantic-1.8.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a83db7205f60c6a86f2c44a61791d993dff4b73135df1973ecd9eed5ea0bda20"},
|
||||
{file = "pydantic-1.8.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:41b542c0b3c42dc17da70554bc6f38cbc30d7066d2c2815a94499b5684582ecb"},
|
||||
{file = "pydantic-1.8.2-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:ea5cb40a3b23b3265f6325727ddfc45141b08ed665458be8c6285e7b85bd73a1"},
|
||||
{file = "pydantic-1.8.2-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:18b5ea242dd3e62dbf89b2b0ec9ba6c7b5abaf6af85b95a97b00279f65845a23"},
|
||||
{file = "pydantic-1.8.2-cp37-cp37m-win_amd64.whl", hash = "sha256:234a6c19f1c14e25e362cb05c68afb7f183eb931dd3cd4605eafff055ebbf287"},
|
||||
{file = "pydantic-1.8.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:021ea0e4133e8c824775a0cfe098677acf6fa5a3cbf9206a376eed3fc09302cd"},
|
||||
{file = "pydantic-1.8.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e710876437bc07bd414ff453ac8ec63d219e7690128d925c6e82889d674bb505"},
|
||||
{file = "pydantic-1.8.2-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:ac8eed4ca3bd3aadc58a13c2aa93cd8a884bcf21cb019f8cfecaae3b6ce3746e"},
|
||||
{file = "pydantic-1.8.2-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:4a03cbbe743e9c7247ceae6f0d8898f7a64bb65800a45cbdc52d65e370570820"},
|
||||
{file = "pydantic-1.8.2-cp38-cp38-win_amd64.whl", hash = "sha256:8621559dcf5afacf0069ed194278f35c255dc1a1385c28b32dd6c110fd6531b3"},
|
||||
{file = "pydantic-1.8.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8b223557f9510cf0bfd8b01316bf6dd281cf41826607eada99662f5e4963f316"},
|
||||
{file = "pydantic-1.8.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:244ad78eeb388a43b0c927e74d3af78008e944074b7d0f4f696ddd5b2af43c62"},
|
||||
{file = "pydantic-1.8.2-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:05ef5246a7ffd2ce12a619cbb29f3307b7c4509307b1b49f456657b43529dc6f"},
|
||||
{file = "pydantic-1.8.2-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:54cd5121383f4a461ff7644c7ca20c0419d58052db70d8791eacbbe31528916b"},
|
||||
{file = "pydantic-1.8.2-cp39-cp39-win_amd64.whl", hash = "sha256:4be75bebf676a5f0f87937c6ddb061fa39cbea067240d98e298508c1bda6f3f3"},
|
||||
{file = "pydantic-1.8.2-py3-none-any.whl", hash = "sha256:fec866a0b59f372b7e776f2d7308511784dace622e0992a0b59ea3ccee0ae833"},
|
||||
{file = "pydantic-1.8.2.tar.gz", hash = "sha256:26464e57ccaafe72b7ad156fdaa4e9b9ef051f69e175dbbb463283000c05ab7b"},
|
||||
]
|
||||
python-dateutil = [
|
||||
{file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"},
|
||||
{file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"},
|
||||
]
|
||||
requests = [
|
||||
{file = "requests-2.26.0-py2.py3-none-any.whl", hash = "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24"},
|
||||
{file = "requests-2.26.0.tar.gz", hash = "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"},
|
||||
]
|
||||
requests-toolbelt = [
|
||||
{file = "requests-toolbelt-0.9.1.tar.gz", hash = "sha256:968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0"},
|
||||
{file = "requests_toolbelt-0.9.1-py2.py3-none-any.whl", hash = "sha256:380606e1d10dc85c3bd47bf5a6095f815ec007be7a8b69c878507068df059e6f"},
|
||||
]
|
||||
six = [
|
||||
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
|
||||
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
|
||||
]
|
||||
smmap = [
|
||||
{file = "smmap-5.0.0-py3-none-any.whl", hash = "sha256:2aba19d6a040e78d8b09de5c57e96207b09ed71d8e55ce0959eeee6c8e190d94"},
|
||||
{file = "smmap-5.0.0.tar.gz", hash = "sha256:c840e62059cd3be204b0c9c9f74be2c09d5648eddd4580d9314c3ecde0b30936"},
|
||||
]
|
||||
specklepy = [
|
||||
{file = "specklepy-2.4.2-py3-none-any.whl", hash = "sha256:8906f69f4c3e7ba950873eb905b6e752c85d6aa2b2decb905165c6d401aebe20"},
|
||||
{file = "specklepy-2.4.2.tar.gz", hash = "sha256:49fd6d8e1f2c6e190a0b6e85a08098984a8616eb46933b2516fcc769995452a6"},
|
||||
]
|
||||
svn = [
|
||||
{file = "svn-1.0.1.tar.gz", hash = "sha256:55f81f07853cc1d66d4800b0cfe2d3376ff02361a2b2361459dc22a0fab95247"},
|
||||
]
|
||||
typing-extensions = [
|
||||
{file = "typing_extensions-4.0.0-py3-none-any.whl", hash = "sha256:829704698b22e13ec9eaf959122315eabb370b0884400e9818334d8b677023d9"},
|
||||
{file = "typing_extensions-4.0.0.tar.gz", hash = "sha256:2cdf80e4e04866a9b3689a51869016d36db0814d84b8d8a568d22781d45d27ed"},
|
||||
]
|
||||
ujson = [
|
||||
{file = "ujson-4.3.0-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:3609e0514f6f721c6c9818b9374ec91b994e59fb193af2f924ca3f2f32009f1c"},
|
||||
{file = "ujson-4.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:de42986e2602b6a0baca452ff50e9cbe66faf256761295d5d07ae3f6757b487d"},
|
||||
{file = "ujson-4.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:843fd8b3246b2b20bbae48b2334d26507c9531b2b014533adfc6132e3ec8e60c"},
|
||||
{file = "ujson-4.3.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5d1083a0dcb39b43cfcd948f09e480c23eb4af66d7d08f6b36951f4c629c3bd1"},
|
||||
{file = "ujson-4.3.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:01d12df8eb25afb939a003284b5b5adca9788c1176c445641e5980fa892562ac"},
|
||||
{file = "ujson-4.3.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b0b9cde57eebaac26de040f8ebf0541e06fe9bcf7e42872dc036d2ced7d99ccf"},
|
||||
{file = "ujson-4.3.0-cp310-cp310-win32.whl", hash = "sha256:3d8eaab72ad8129c12ed90ebf310230bd014b6bbf99145ebf2bc890238e0254f"},
|
||||
{file = "ujson-4.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:85f28c38952b8a94183ab15ec6c6e89c117d00ceeae5d754ef1a33e01e28b845"},
|
||||
{file = "ujson-4.3.0-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:8a0d9dde58937976cd06cd776411b77b0e5d38db0a3c1be28ee8bb428ff5a42b"},
|
||||
{file = "ujson-4.3.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9f4a34386785a33600ac7442fec34c3d8b2d7e5309cfc94bc7c9ba93f12640c2"},
|
||||
{file = "ujson-4.3.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8e2a52fbeee55db306b9306892f5cde7e78c56069c1212abf176d1886fff60a"},
|
||||
{file = "ujson-4.3.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9c5330692122b999997911252466a7d17e4e428d7d9a8db0b99ba81b8b9c010c"},
|
||||
{file = "ujson-4.3.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:9baa160ba1d3f712a356e77718251c9d9eee43ed548debdcc9d75b06a75b3e82"},
|
||||
{file = "ujson-4.3.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:a6c32356145d95a0403b5895d60c36798a48af13b8863e43ad7457a0361afad0"},
|
||||
{file = "ujson-4.3.0-cp36-cp36m-win32.whl", hash = "sha256:b72fadeea5727204674c9f77166da7feaafdf70f1ed50bb15bf321f7c39c7194"},
|
||||
{file = "ujson-4.3.0-cp36-cp36m-win_amd64.whl", hash = "sha256:1601354caaab0697a9b24815a31611ad013d29cf957d545fc1cd59835b82e3c1"},
|
||||
{file = "ujson-4.3.0-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:b80a35bad8fad1772f992bae8086b0cde788cd3b37f35d0d4506c93e6edad645"},
|
||||
{file = "ujson-4.3.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a318df321d7adc3de876b29640cca8de1ad4d4e4fe7c4a76d64d9d6f1676304"},
|
||||
{file = "ujson-4.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc9a508efb829bf0542be9b2578d8da08f0ab1fa712e086ebb777d6ec9e6d8d2"},
|
||||
{file = "ujson-4.3.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:43d2403451d7bd27b6a600f89d4bd2cf6e1b3494254509d8b5ef3c8e94ae4d8e"},
|
||||
{file = "ujson-4.3.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:fd0901db652a58f46550074596227dbddb7a02d2de744d3cd2358101f78037bb"},
|
||||
{file = "ujson-4.3.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:00fd67952b1a8a46cf5b0a51b3838187332d13d2e8d178423c5a5405c21d9e7c"},
|
||||
{file = "ujson-4.3.0-cp37-cp37m-win32.whl", hash = "sha256:b0e9510e867c72a87db2d16377c2bef912f29afd8381d1fdae332b9b7f697efa"},
|
||||
{file = "ujson-4.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:294e907f134fb5d83e0a4439cf4040d74da77157938b4db5730cd174621dcf8b"},
|
||||
{file = "ujson-4.3.0-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:087cd977f4f63f885a49607244e7e157801a22aadcc075a262d3c3633138573c"},
|
||||
{file = "ujson-4.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f35dcf6d2a67e913a7135809006bd000d55ad5b5834b5dbe5b82dcf8db1ac05"},
|
||||
{file = "ujson-4.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f158fdb08e022f2f16f0fba317a80558b0cebc7e2c84ae783e5f75616d5c90d5"},
|
||||
{file = "ujson-4.3.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2a06006dad34c8cfaa734bd6458452e46702b368da53b56e7732351082aa0420"},
|
||||
{file = "ujson-4.3.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6df94e675b05ecf4e7a57883a73b916ffcb5872d7b1298ac5cef8ac1cbce73c6"},
|
||||
{file = "ujson-4.3.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:47af81df5d575e36d4be9396db94f35c8f62de3077a405f9af94f9756255cef5"},
|
||||
{file = "ujson-4.3.0-cp38-cp38-win32.whl", hash = "sha256:e46c1462761db518fae51ab0d89a6256aeac148a795f7244d9084c459b477af5"},
|
||||
{file = "ujson-4.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:bf199015910fcfa19b6e12881abeb462498791b2ab0111ff8b17095d0477e9d4"},
|
||||
{file = "ujson-4.3.0-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:32ee97ec37af31b35ca4395732d883bf74fb70309d38485f7fb9a5cc3332c53e"},
|
||||
{file = "ujson-4.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f211c7c0c9377cbf4650aa990118d0c2cce3c5fad476c39ecd35b6714ba4463"},
|
||||
{file = "ujson-4.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c81159d3f1bcb5729ba019e63e78ee6c91b556e1ac0e67c7579768720fd3c4e"},
|
||||
{file = "ujson-4.3.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b850029d64008e970cae04ada69aa33e1cd412106a1efde221269c1cda1b40cc"},
|
||||
{file = "ujson-4.3.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:327ec982bb89abe779fe463e1013c47aae6ed53b76600af7cb1e8b8cb0ee9f85"},
|
||||
{file = "ujson-4.3.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:103cbabe4e6fd70c957219519e37d65be612d7c74d91ef19022a2c8f8c5e4e82"},
|
||||
{file = "ujson-4.3.0-cp39-cp39-win32.whl", hash = "sha256:7b0a63865ec2978ebafb0906bf982eb52bea26fc98e2ae5e59b9d204afe2d762"},
|
||||
{file = "ujson-4.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:18040475d997d93a6851d8bee474fba2ec94869e8f826dddd66cdae4aa3fdb92"},
|
||||
{file = "ujson-4.3.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df481d4e13ca34d870d1fdf387742867edff3f78a1eea1bbcd72ea2fa68d9a6e"},
|
||||
{file = "ujson-4.3.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e7e73ec5ba1b42c2027773f69b70eff28df132907aa98b28166c39d3ea45e85b"},
|
||||
{file = "ujson-4.3.0-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b270088e472f1d65a0a0aab3190010b9ac1a5b2969d39bf2b53c0fbf339bc87a"},
|
||||
{file = "ujson-4.3.0.tar.gz", hash = "sha256:baee56eca35cb5fbe02c28bd9c0936be41a96fa5c0812d9d4b7edeb5c3d568a0"},
|
||||
]
|
||||
urllib3 = [
|
||||
{file = "urllib3-1.26.7-py2.py3-none-any.whl", hash = "sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844"},
|
||||
{file = "urllib3-1.26.7.tar.gz", hash = "sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece"},
|
||||
]
|
||||
websockets = [
|
||||
{file = "websockets-9.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d144b350045c53c8ff09aa1cfa955012dd32f00c7e0862c199edcabb1a8b32da"},
|
||||
{file = "websockets-9.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:b4ad84b156cf50529b8ac5cc1638c2cf8680490e3fccb6121316c8c02620a2e4"},
|
||||
{file = "websockets-9.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:2cf04601633a4ec176b9cc3d3e73789c037641001dbfaf7c411f89cd3e04fcaf"},
|
||||
{file = "websockets-9.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:5c8f0d82ea2468282e08b0cf5307f3ad022290ed50c45d5cb7767957ca782880"},
|
||||
{file = "websockets-9.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:caa68c95bc1776d3521f81eeb4d5b9438be92514ec2a79fececda814099c8314"},
|
||||
{file = "websockets-9.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:d2c2d9b24d3c65b5a02cac12cbb4e4194e590314519ed49db2f67ef561c3cf58"},
|
||||
{file = "websockets-9.1-cp36-cp36m-win32.whl", hash = "sha256:f31722f1c033c198aa4a39a01905951c00bd1c74f922e8afc1b1c62adbcdd56a"},
|
||||
{file = "websockets-9.1-cp36-cp36m-win_amd64.whl", hash = "sha256:3ddff38894c7857c476feb3538dd847514379d6dc844961dc99f04b0384b1b1b"},
|
||||
{file = "websockets-9.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:51d04df04ed9d08077d10ccbe21e6805791b78eac49d16d30a1f1fe2e44ba0af"},
|
||||
{file = "websockets-9.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:f68c352a68e5fdf1e97288d5cec9296664c590c25932a8476224124aaf90dbcd"},
|
||||
{file = "websockets-9.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:b43b13e5622c5a53ab12f3272e6f42f1ce37cd5b6684b2676cb365403295cd40"},
|
||||
{file = "websockets-9.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:9147868bb0cc01e6846606cd65cbf9c58598f187b96d14dd1ca17338b08793bb"},
|
||||
{file = "websockets-9.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:836d14eb53b500fd92bd5db2fc5894f7c72b634f9c2a28f546f75967503d8e25"},
|
||||
{file = "websockets-9.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:48c222feb3ced18f3dc61168ca18952a22fb88e5eb8902d2bf1b50faefdc34a2"},
|
||||
{file = "websockets-9.1-cp37-cp37m-win32.whl", hash = "sha256:900589e19200be76dd7cbaa95e9771605b5ce3f62512d039fb3bc5da9014912a"},
|
||||
{file = "websockets-9.1-cp37-cp37m-win_amd64.whl", hash = "sha256:ab5ee15d3462198c794c49ccd31773d8a2b8c17d622aa184f669d2b98c2f0857"},
|
||||
{file = "websockets-9.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:85e701a6c316b7067f1e8675c638036a796fe5116783a4c932e7eb8e305a3ffe"},
|
||||
{file = "websockets-9.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:b2e71c4670ebe1067fa8632f0d081e47254ee2d3d409de54168b43b0ba9147e0"},
|
||||
{file = "websockets-9.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:230a3506df6b5f446fed2398e58dcaafdff12d67fe1397dff196411a9e820d02"},
|
||||
{file = "websockets-9.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:7df3596838b2a0c07c6f6d67752c53859a54993d4f062689fdf547cb56d0f84f"},
|
||||
{file = "websockets-9.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:826ccf85d4514609219725ba4a7abd569228c2c9f1968e8be05be366f68291ec"},
|
||||
{file = "websockets-9.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:0dd4eb8e0bbf365d6f652711ce21b8fd2b596f873d32aabb0fbb53ec604418cc"},
|
||||
{file = "websockets-9.1-cp38-cp38-win32.whl", hash = "sha256:1d0971cc7251aeff955aa742ec541ee8aaea4bb2ebf0245748fbec62f744a37e"},
|
||||
{file = "websockets-9.1-cp38-cp38-win_amd64.whl", hash = "sha256:7189e51955f9268b2bdd6cc537e0faa06f8fffda7fb386e5922c6391de51b077"},
|
||||
{file = "websockets-9.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e9e5fd6dbdf95d99bc03732ded1fc8ef22ebbc05999ac7e0c7bf57fe6e4e5ae2"},
|
||||
{file = "websockets-9.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:9e7fdc775fe7403dbd8bc883ba59576a6232eac96dacb56512daacf7af5d618d"},
|
||||
{file = "websockets-9.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:597c28f3aa7a09e8c070a86b03107094ee5cdafcc0d55f2f2eac92faac8dc67d"},
|
||||
{file = "websockets-9.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:ad893d889bc700a5835e0a95a3e4f2c39e91577ab232a3dc03c262a0f8fc4b5c"},
|
||||
{file = "websockets-9.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:1d6b4fddb12ab9adf87b843cd4316c4bd602db8d5efd2fb83147f0458fe85135"},
|
||||
{file = "websockets-9.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:ebf459a1c069f9866d8569439c06193c586e72c9330db1390af7c6a0a32c4afd"},
|
||||
{file = "websockets-9.1-cp39-cp39-win32.whl", hash = "sha256:be5fd35e99970518547edc906efab29afd392319f020c3c58b0e1a158e16ed20"},
|
||||
{file = "websockets-9.1-cp39-cp39-win_amd64.whl", hash = "sha256:85db8090ba94e22d964498a47fdd933b8875a1add6ebc514c7ac8703eb97bbf0"},
|
||||
{file = "websockets-9.1.tar.gz", hash = "sha256:276d2339ebf0df4f45df453923ebd2270b87900eda5dfd4a6b0cfa15f82111c3"},
|
||||
]
|
||||
yarl = [
|
||||
{file = "yarl-1.7.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f2a8508f7350512434e41065684076f640ecce176d262a7d54f0da41d99c5a95"},
|
||||
{file = "yarl-1.7.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:da6df107b9ccfe52d3a48165e48d72db0eca3e3029b5b8cb4fe6ee3cb870ba8b"},
|
||||
{file = "yarl-1.7.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a1d0894f238763717bdcfea74558c94e3bc34aeacd3351d769460c1a586a8b05"},
|
||||
{file = "yarl-1.7.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dfe4b95b7e00c6635a72e2d00b478e8a28bfb122dc76349a06e20792eb53a523"},
|
||||
{file = "yarl-1.7.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c145ab54702334c42237a6c6c4cc08703b6aa9b94e2f227ceb3d477d20c36c63"},
|
||||
{file = "yarl-1.7.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1ca56f002eaf7998b5fcf73b2421790da9d2586331805f38acd9997743114e98"},
|
||||
{file = "yarl-1.7.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1d3d5ad8ea96bd6d643d80c7b8d5977b4e2fb1bab6c9da7322616fd26203d125"},
|
||||
{file = "yarl-1.7.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:167ab7f64e409e9bdd99333fe8c67b5574a1f0495dcfd905bc7454e766729b9e"},
|
||||
{file = "yarl-1.7.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:95a1873b6c0dd1c437fb3bb4a4aaa699a48c218ac7ca1e74b0bee0ab16c7d60d"},
|
||||
{file = "yarl-1.7.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6152224d0a1eb254f97df3997d79dadd8bb2c1a02ef283dbb34b97d4f8492d23"},
|
||||
{file = "yarl-1.7.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:5bb7d54b8f61ba6eee541fba4b83d22b8a046b4ef4d8eb7f15a7e35db2e1e245"},
|
||||
{file = "yarl-1.7.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:9c1f083e7e71b2dd01f7cd7434a5f88c15213194df38bc29b388ccdf1492b739"},
|
||||
{file = "yarl-1.7.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f44477ae29025d8ea87ec308539f95963ffdc31a82f42ca9deecf2d505242e72"},
|
||||
{file = "yarl-1.7.2-cp310-cp310-win32.whl", hash = "sha256:cff3ba513db55cc6a35076f32c4cdc27032bd075c9faef31fec749e64b45d26c"},
|
||||
{file = "yarl-1.7.2-cp310-cp310-win_amd64.whl", hash = "sha256:c9c6d927e098c2d360695f2e9d38870b2e92e0919be07dbe339aefa32a090265"},
|
||||
{file = "yarl-1.7.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:9b4c77d92d56a4c5027572752aa35082e40c561eec776048330d2907aead891d"},
|
||||
{file = "yarl-1.7.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c01a89a44bb672c38f42b49cdb0ad667b116d731b3f4c896f72302ff77d71656"},
|
||||
{file = "yarl-1.7.2-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c19324a1c5399b602f3b6e7db9478e5b1adf5cf58901996fc973fe4fccd73eed"},
|
||||
{file = "yarl-1.7.2-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3abddf0b8e41445426d29f955b24aeecc83fa1072be1be4e0d194134a7d9baee"},
|
||||
{file = "yarl-1.7.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6a1a9fe17621af43e9b9fcea8bd088ba682c8192d744b386ee3c47b56eaabb2c"},
|
||||
{file = "yarl-1.7.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8b0915ee85150963a9504c10de4e4729ae700af11df0dc5550e6587ed7891e92"},
|
||||
{file = "yarl-1.7.2-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:29e0656d5497733dcddc21797da5a2ab990c0cb9719f1f969e58a4abac66234d"},
|
||||
{file = "yarl-1.7.2-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:bf19725fec28452474d9887a128e98dd67eee7b7d52e932e6949c532d820dc3b"},
|
||||
{file = "yarl-1.7.2-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:d6f3d62e16c10e88d2168ba2d065aa374e3c538998ed04996cd373ff2036d64c"},
|
||||
{file = "yarl-1.7.2-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:ac10bbac36cd89eac19f4e51c032ba6b412b3892b685076f4acd2de18ca990aa"},
|
||||
{file = "yarl-1.7.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:aa32aaa97d8b2ed4e54dc65d241a0da1c627454950f7d7b1f95b13985afd6c5d"},
|
||||
{file = "yarl-1.7.2-cp36-cp36m-win32.whl", hash = "sha256:87f6e082bce21464857ba58b569370e7b547d239ca22248be68ea5d6b51464a1"},
|
||||
{file = "yarl-1.7.2-cp36-cp36m-win_amd64.whl", hash = "sha256:ac35ccde589ab6a1870a484ed136d49a26bcd06b6a1c6397b1967ca13ceb3913"},
|
||||
{file = "yarl-1.7.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a467a431a0817a292121c13cbe637348b546e6ef47ca14a790aa2fa8cc93df63"},
|
||||
{file = "yarl-1.7.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ab0c3274d0a846840bf6c27d2c60ba771a12e4d7586bf550eefc2df0b56b3b4"},
|
||||
{file = "yarl-1.7.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d260d4dc495c05d6600264a197d9d6f7fc9347f21d2594926202fd08cf89a8ba"},
|
||||
{file = "yarl-1.7.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fc4dd8b01a8112809e6b636b00f487846956402834a7fd59d46d4f4267181c41"},
|
||||
{file = "yarl-1.7.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c1164a2eac148d85bbdd23e07dfcc930f2e633220f3eb3c3e2a25f6148c2819e"},
|
||||
{file = "yarl-1.7.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:67e94028817defe5e705079b10a8438b8cb56e7115fa01640e9c0bb3edf67332"},
|
||||
{file = "yarl-1.7.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:89ccbf58e6a0ab89d487c92a490cb5660d06c3a47ca08872859672f9c511fc52"},
|
||||
{file = "yarl-1.7.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:8cce6f9fa3df25f55521fbb5c7e4a736683148bcc0c75b21863789e5185f9185"},
|
||||
{file = "yarl-1.7.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:211fcd65c58bf250fb994b53bc45a442ddc9f441f6fec53e65de8cba48ded986"},
|
||||
{file = "yarl-1.7.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c10ea1e80a697cf7d80d1ed414b5cb8f1eec07d618f54637067ae3c0334133c4"},
|
||||
{file = "yarl-1.7.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:52690eb521d690ab041c3919666bea13ab9fbff80d615ec16fa81a297131276b"},
|
||||
{file = "yarl-1.7.2-cp37-cp37m-win32.whl", hash = "sha256:695ba021a9e04418507fa930d5f0704edbce47076bdcfeeaba1c83683e5649d1"},
|
||||
{file = "yarl-1.7.2-cp37-cp37m-win_amd64.whl", hash = "sha256:c17965ff3706beedafd458c452bf15bac693ecd146a60a06a214614dc097a271"},
|
||||
{file = "yarl-1.7.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:fce78593346c014d0d986b7ebc80d782b7f5e19843ca798ed62f8e3ba8728576"},
|
||||
{file = "yarl-1.7.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c2a1ac41a6aa980db03d098a5531f13985edcb451bcd9d00670b03129922cd0d"},
|
||||
{file = "yarl-1.7.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:39d5493c5ecd75c8093fa7700a2fb5c94fe28c839c8e40144b7ab7ccba6938c8"},
|
||||
{file = "yarl-1.7.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1eb6480ef366d75b54c68164094a6a560c247370a68c02dddb11f20c4c6d3c9d"},
|
||||
{file = "yarl-1.7.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ba63585a89c9885f18331a55d25fe81dc2d82b71311ff8bd378fc8004202ff6"},
|
||||
{file = "yarl-1.7.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e39378894ee6ae9f555ae2de332d513a5763276a9265f8e7cbaeb1b1ee74623a"},
|
||||
{file = "yarl-1.7.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c0910c6b6c31359d2f6184828888c983d54d09d581a4a23547a35f1d0b9484b1"},
|
||||
{file = "yarl-1.7.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6feca8b6bfb9eef6ee057628e71e1734caf520a907b6ec0d62839e8293e945c0"},
|
||||
{file = "yarl-1.7.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8300401dc88cad23f5b4e4c1226f44a5aa696436a4026e456fe0e5d2f7f486e6"},
|
||||
{file = "yarl-1.7.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:788713c2896f426a4e166b11f4ec538b5736294ebf7d5f654ae445fd44270832"},
|
||||
{file = "yarl-1.7.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:fd547ec596d90c8676e369dd8a581a21227fe9b4ad37d0dc7feb4ccf544c2d59"},
|
||||
{file = "yarl-1.7.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:737e401cd0c493f7e3dd4db72aca11cfe069531c9761b8ea474926936b3c57c8"},
|
||||
{file = "yarl-1.7.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:baf81561f2972fb895e7844882898bda1eef4b07b5b385bcd308d2098f1a767b"},
|
||||
{file = "yarl-1.7.2-cp38-cp38-win32.whl", hash = "sha256:ede3b46cdb719c794427dcce9d8beb4abe8b9aa1e97526cc20de9bd6583ad1ef"},
|
||||
{file = "yarl-1.7.2-cp38-cp38-win_amd64.whl", hash = "sha256:cc8b7a7254c0fc3187d43d6cb54b5032d2365efd1df0cd1749c0c4df5f0ad45f"},
|
||||
{file = "yarl-1.7.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:580c1f15500e137a8c37053e4cbf6058944d4c114701fa59944607505c2fe3a0"},
|
||||
{file = "yarl-1.7.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3ec1d9a0d7780416e657f1e405ba35ec1ba453a4f1511eb8b9fbab81cb8b3ce1"},
|
||||
{file = "yarl-1.7.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3bf8cfe8856708ede6a73907bf0501f2dc4e104085e070a41f5d88e7faf237f3"},
|
||||
{file = "yarl-1.7.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1be4bbb3d27a4e9aa5f3df2ab61e3701ce8fcbd3e9846dbce7c033a7e8136746"},
|
||||
{file = "yarl-1.7.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:534b047277a9a19d858cde163aba93f3e1677d5acd92f7d10ace419d478540de"},
|
||||
{file = "yarl-1.7.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6ddcd80d79c96eb19c354d9dca95291589c5954099836b7c8d29278a7ec0bda"},
|
||||
{file = "yarl-1.7.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:9bfcd43c65fbb339dc7086b5315750efa42a34eefad0256ba114cd8ad3896f4b"},
|
||||
{file = "yarl-1.7.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f64394bd7ceef1237cc604b5a89bf748c95982a84bcd3c4bbeb40f685c810794"},
|
||||
{file = "yarl-1.7.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:044daf3012e43d4b3538562da94a88fb12a6490652dbc29fb19adfa02cf72eac"},
|
||||
{file = "yarl-1.7.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:368bcf400247318382cc150aaa632582d0780b28ee6053cd80268c7e72796dec"},
|
||||
{file = "yarl-1.7.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:bab827163113177aee910adb1f48ff7af31ee0289f434f7e22d10baf624a6dfe"},
|
||||
{file = "yarl-1.7.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:0cba38120db72123db7c58322fa69e3c0efa933040ffb586c3a87c063ec7cae8"},
|
||||
{file = "yarl-1.7.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:59218fef177296451b23214c91ea3aba7858b4ae3306dde120224cfe0f7a6ee8"},
|
||||
{file = "yarl-1.7.2-cp39-cp39-win32.whl", hash = "sha256:1edc172dcca3f11b38a9d5c7505c83c1913c0addc99cd28e993efeaafdfaa18d"},
|
||||
{file = "yarl-1.7.2-cp39-cp39-win_amd64.whl", hash = "sha256:797c2c412b04403d2da075fb93c123df35239cd7b4cc4e0cd9e5839b73f52c58"},
|
||||
{file = "yarl-1.7.2.tar.gz", hash = "sha256:45399b46d60c253327a460e99856752009fcee5f5d3c80b2f7c0cae1c38d56dd"},
|
||||
]
|
||||
+13
-17
@@ -1,20 +1,16 @@
|
||||
[tool.poetry]
|
||||
[project]
|
||||
name = "speckle-blender"
|
||||
version = "2.0.0"
|
||||
description = "the Speckle 2.0 connector for Blender!"
|
||||
authors = ["izzy lyseggen <izzy.lyseggen@gmail.com>"]
|
||||
version = "3.0.0"
|
||||
description = "Next-Gen Speckle connector for Blender!"
|
||||
requires-python = ">=3.11.9, <4.0.0"
|
||||
license = "Apache-2.0"
|
||||
dependencies = [
|
||||
"specklepy>=3.2.4",
|
||||
]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = ">=3.7,<3.8"
|
||||
specklepy = "2.4.2"
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
devtools = "^0.6.1"
|
||||
numpy = "^1.20.2"
|
||||
bpy = "^2.82.1"
|
||||
bpy-build = "^2.1.0"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core>=1.0.0"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"fake-bpy-module-latest>=20260126",
|
||||
"ruff==0.14.14",
|
||||
"pre-commit>=4.0.1",
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user