Compare commits

...

724 Commits

Author SHA1 Message Date
Mucahit Bilal GOKER f75afc2b37 fix: Change auth port and app slug (#318)
Release workflow / Build Zip (push) Has been cancelled
Release workflow / deploy-installers (push) Has been cancelled
* change port and app slug

* fix errno check
2026-02-27 09:02:25 +03:00
Mucahit Bilal GOKER 34c922feb1 feat: auth without desktop service (#313)
* yolo

* add restart dialog

* add user agent

* remove restart prompt

* fix silent thread crash

* fix sqlite connection leak

* fix insecure prng

* fix thread-safe authentication

* document magic values

* fix silent auth cleanup

* extract post json helper

* consolidate auth server shutdown

* desktop service fallback

* fix sqlite connection leaks in auth module

* fix sqlite race condition

* simplify port checking

* simplify error message handling

* replace getter setters with properties

* early return in modal logic

* simplify verbose docstrings

* remove redundant port checking logic

* simplify word wrapping

* ruff format check

* uv ruff check

* fix eof
2026-02-20 15:35:45 +03:00
Mucahit Bilal GOKER cee05260c1 fix: remove comma (#317) 2026-02-06 12:16:01 +03:00
Dogukan Karatas add470699b Merge pull request #314 from specklesystems/dogukan/cnx-3036-blender
Release workflow / Build Zip (push) Has been cancelled
Release workflow / deploy-installers (push) Has been cancelled
feat: add model and version permission check
2026-02-02 14:32:18 +01:00
Dogukan Karatas f5bcd805e8 Merge branch 'v3-dev' into dogukan/cnx-3036-blender 2026-02-02 14:30:38 +01:00
Dogukan Karatas cf4bb14240 Merge pull request #316 from specklesystems/dogukan/cnx-3003-blender
feat: add model ingestion support
2026-02-02 14:26:42 +01:00
Dogukan Karatas 1ee1650fef error messages implemented 2026-02-02 13:05:44 +01:00
Dogukan Karatas 70f5f672a6 Merge branch 'v3-dev' into dogukan/cnx-3003-blender 2026-02-02 09:57:46 +01:00
Dogukan Karatas b329ec8c97 Merge pull request #315 from specklesystems/dogukan/cnx-2996-blender
feat: remove personal projects
2026-02-02 09:57:24 +01:00
bimgeek b54cfe16e8 fix result version id 2026-01-30 21:54:17 +03:00
bimgeek 357859827c fix model ingestion inputs 2026-01-30 21:47:38 +03:00
Dogukan Karatas f35457dff8 pass file size 2026-01-30 17:11:06 +01:00
Dogukan Karatas f993c38ea9 selection dialog fix 2026-01-30 16:59:30 +01:00
Dogukan Karatas 624537cc5d implements model ingestion check 2026-01-30 12:33:16 +01:00
Dogukan Karatas ebb7f1b3bf formatted 2026-01-30 09:12:38 +01:00
Dogukan Karatas ac2a95d968 removes personal projects 2026-01-30 09:08:29 +01:00
Dogukan Karatas 2440c44f44 formatted 2026-01-29 17:21:43 +01:00
Dogukan Karatas 33dfa1229c can create version checks 2026-01-29 16:37:35 +01:00
Jedd Morgan ea61bd06b8 feat(receive): Call MarkVersionReceived (#312)
* Enable ruff pre-commit

* bump ruff

* Call `client.version.received`
2026-01-27 09:35:43 +00:00
Jedd Morgan e071aca299 Update readme with local dev instructions (#311) 2026-01-26 15:03:58 +00:00
Jedd Morgan 4a8a980034 chore(ci): enable pre-commit (#309)
* Enable ruff pre-commit

* bump ruff
2026-01-26 15:03:07 +00:00
Dogukan Karatas b05447dc30 Merge pull request #308 from specklesystems/dogukan/ssl-match-localhost
Release workflow / Build Zip (push) Has been cancelled
Release workflow / deploy-installers (push) Has been cancelled
fix: skip ssl use for http servers
2026-01-20 09:44:30 +01:00
Dogukan Karatas 7a36f9ec08 check url 2026-01-20 09:29:14 +01:00
Mucahit Bilal GOKER 80e3971706 Show update button in connector ui (#297)
* bump specklepy

* update button first pass

* clear timer on unregister

* remove unnecessary specklepy import handling

---------

Co-authored-by: Jedd Morgan <45512892+JR-Morgan@users.noreply.github.com>
2026-01-19 14:06:07 +03:00
Dogukan Karatas dc770b7a79 Merge pull request #307 from specklesystems/dogukan/cnx-1776-typeerror-bpy_prop_collection__contains__-expected-a-string
fix: handle null material name
2026-01-08 18:09:41 +01:00
Dogukan Karatas f8e7d391be handle none 2026-01-08 17:59:06 +01:00
Mucahit Bilal GOKER 3092ba3056 separate panel for model cards (#292)
Co-authored-by: Jedd Morgan <45512892+JR-Morgan@users.noreply.github.com>
2026-01-08 16:05:54 +03:00
Mucahit Bilal GOKER 9d10006116 bump specklepy (#306) 2026-01-06 12:41:32 +03:00
Dogukan Karatas 95f4d051d6 Merge pull request #304 from specklesystems/dogukan/cnx-2682-display-value-proxies-in-blender
Release workflow / Build Zip (push) Has been cancelled
Release workflow / deploy-installers (push) Has been cancelled
feat: display value proxy handler
2025-10-20 15:42:56 +02:00
Dogukan Karatas c79ad8e87d fixed coordinate space issue 2025-10-20 11:26:04 +02:00
Dogukan Karatas 9797dfbfc0 Merge branch 'v3-dev' into dogukan/cnx-2682-display-value-proxies-in-blender 2025-10-17 15:32:07 +02:00
Dogukan Karatas 63b00a6257 Merge pull request #305 from specklesystems/jrm/perf-receive
perf(receive): optimise set lookups
2025-10-17 15:31:18 +02:00
Dogukan Karatas 36091845a6 added an object id mapping 2025-10-17 15:05:07 +02:00
Jedd Morgan 89e1855e2c perf(receive): optimise set lookups 2025-10-17 11:44:21 +01:00
Dogukan Karatas b7f5725282 force linked duplicates for proxies 2025-10-17 11:13:19 +02:00
Dogukan Karatas dc8c8cedf4 proxy handler added 2025-10-16 14:17:41 +02:00
Mucahit Bilal GOKER 31e8b838dd Merge pull request #303 from specklesystems/bilal/bump-specklepy
Release workflow / Build Zip (push) Has been cancelled
Release workflow / deploy-installers (push) Has been cancelled
bump specklepy to 3.0.4
2025-09-08 12:32:32 +03:00
bimgeek baf7f32c2a bump specklepy to 3.0.4 2025-09-08 12:27:02 +03:00
Mucahit Bilal GOKER ad1d58bd4c Merge pull request #302 from specklesystems/bilal/null-check-on-active-workspace
null check on active workspace
2025-09-08 12:14:51 +03:00
Mucahit Bilal GOKER ec86688750 null check on active workspace 2025-09-05 16:26:22 +03:00
Mucahit Bilal GOKER 84098f4c42 Merge pull request #301 from specklesystems/bilal/update-docs
replace docs links
2025-08-26 11:38:12 +03:00
bimgeek 77f9d73698 replace xyz with app 2025-08-26 11:33:28 +03:00
bimgeek 812e8dd2f3 replace docs links 2025-08-26 11:30:22 +03:00
Mucahit Bilal GOKER 5c19d9aa16 Merge pull request #300 from specklesystems/bilal/bump-specklepy-3.0.3
Release workflow / Build Zip (push) Has been cancelled
Release workflow / deploy-installers (push) Has been cancelled
bump specklepy to 3.0.3
2025-07-24 21:21:09 +03:00
bimgeek 29d706e1b6 bump specklepy to 3.0.3 2025-07-24 21:18:14 +03:00
Mucahit Bilal GOKER 25d02673a7 Merge pull request #290 from specklesystems/bilal/cnx-2065-keep-track-of-loaded-object-properties
Persistent modifiers, visibility settings and tracking by applicationId
2025-07-22 17:19:53 +03:00
Jedd Morgan 6a4dc62622 small fix 2025-07-22 13:29:25 +01:00
Jedd Morgan bb9a5ea604 Merge branch 'v3-dev' into bilal/cnx-2065-keep-track-of-loaded-object-properties 2025-07-22 13:00:42 +01:00
Jedd Morgan 2e995bd0fa chore(lint): Run Ruff (#298)
* ruff check

* format

* format
2025-07-22 11:48:25 +01:00
Mucahit Bilal GOKER 38e6096ea9 Merge branch 'v3-dev' into bilal/cnx-2065-keep-track-of-loaded-object-properties 2025-07-22 12:43:49 +03:00
Mucahit Bilal GOKER 30e3398cd4 Merge pull request #296 from specklesystems/bilal/cnx-1798-update-bl_info
Update bl_info
2025-07-22 12:43:33 +03:00
bimgeek 3f7e98aff5 shorten description 2025-07-18 15:31:59 +03:00
Mucahit Bilal GOKER 1ad8429928 update bl_info and manifest 2025-07-18 15:24:36 +03:00
Mucahit Bilal GOKER a6c820183b fix: track collections by applicationId 2025-07-15 23:20:11 +03:00
Mucahit Bilal GOKER 56b6c813c0 remove logging of optimization metrics 2025-07-15 22:30:03 +03:00
Mucahit Bilal GOKER 8ab110f7ec only store modified objects and collections 2025-07-15 22:23:44 +03:00
Mucahit Bilal GOKER 11ff018f18 refactor visibility settings and modifiers storage: model cards > temp storage 2025-07-15 22:09:26 +03:00
Mucahit Bilal GOKER 227f63d266 Merge pull request #288 from specklesystems/bilal/cnx-2003-cache-speckle-clients
Bilal/cnx 2003 cache speckle clients
2025-07-15 16:04:58 +03:00
Mucahit Bilal GOKER 9e8aaf4f3b Merge branch 'v3-dev' into bilal/cnx-2003-cache-speckle-clients 2025-07-15 16:04:28 +03:00
Mucahit Bilal GOKER afcb760bbf added typing 2025-07-14 13:42:54 +03:00
Mucahit Bilal GOKER 58283439ab comments cleanup 2025-07-11 15:19:43 +03:00
Mucahit Bilal GOKER 0c29a2ec0a replace selecting by name logic to applicationId 2025-07-11 12:50:31 +03:00
Mucahit Bilal GOKER 4ec62d4168 Revert "rename speckle_application_id to applicationId"
This reverts commit 8d596823ed.
2025-07-11 12:09:15 +03:00
Mucahit Bilal GOKER 8d596823ed rename speckle_application_id to applicationId 2025-07-11 12:06:47 +03:00
Mucahit Bilal GOKER ccd62e3452 remove 8 char limit from ids 2025-07-11 11:45:41 +03:00
Mucahit Bilal GOKER 1bd08497e6 Merge remote-tracking branch 'origin/v3-dev' into bilal/cnx-2065-keep-track-of-loaded-object-properties 2025-07-11 11:38:18 +03:00
Jedd Morgan d23cc5a738 Merge pull request #289 from specklesystems/jrm/requirementstxt
Ensure hashes are included in requirements.txt creaiton
2025-07-10 15:59:24 +01:00
Mucahit Bilal GOKER 3e2ac4b5b6 preserve modifiers 2025-07-06 22:14:28 +03:00
Mucahit Bilal GOKER 928bc15ff1 preserve layer collection visibility settings 2025-07-05 21:05:19 +03:00
Mucahit Bilal GOKER e410e40060 preserve object visibility settings and update object removal function 2025-07-05 19:54:39 +03:00
Mucahit Bilal GOKER d1f2c938b1 clear cache on unregister 2025-06-25 22:22:46 +03:00
Mucahit Bilal GOKER 388ec2bdfd use cached client for managers 2025-06-25 21:45:10 +03:00
Mucahit Bilal GOKER b057c6c0da use cached client for publish and load 2025-06-25 21:44:29 +03:00
Mucahit Bilal GOKER 40089bdbb8 resolve get_active_workspace 2025-06-25 21:44:10 +03:00
Mucahit Bilal GOKER 49dd688219 create project and model - use cached client 2025-06-25 21:43:51 +03:00
Mucahit Bilal GOKER 6993e8cb83 initialize client on startup 2025-06-25 21:41:54 +03:00
Mucahit Bilal GOKER 709015b9d8 Merge branch 'v3-dev' into bilal/cnx-2003-cache-speckle-clients 2025-06-25 16:18:07 +03:00
Dogukan Karatas c5e0dfa36b Merge pull request #287 from specklesystems/bilal/handle-no-account-state
Release workflow / Build Zip (push) Has been cancelled
Release workflow / deploy-installers (push) Has been cancelled
Bilal/handle no account state
2025-06-23 11:54:04 +02:00
Mucahit Bilal GOKER 1f72741b62 update collapse icon 2025-06-23 11:34:43 +03:00
Mucahit Bilal GOKER 0f8f7e02be Added error handling in get_active_workspace to return default workspace if no account is found for the given ID. 2025-06-23 11:14:09 +03:00
Dogukan Karatas f3e188b4f8 Merge pull request #286 from specklesystems/bilal/cnx-2013-implement-workspace-dropdown
Release workflow / Build Zip (push) Has been cancelled
Release workflow / deploy-installers (push) Has been cancelled
Bilal/cnx 2013 implement workspace dropdown
2025-06-17 15:28:12 +02:00
Mucahit Bilal GOKER 346b0210a5 resolve leftover conflicts 2025-06-17 16:00:36 +03:00
Mucahit Bilal GOKER 5d072bbb1d Merge branch 'v3-dev' into bilal/cnx-2013-implement-workspace-dropdown 2025-06-17 15:55:11 +03:00
Mucahit Bilal GOKER fcc8527c9f Merge pull request #285 from specklesystems/bilal/account-selection-button
Bilal/cnx 2012 account selection button
2025-06-17 14:57:15 +03:00
Mucahit Bilal GOKER 2814dabe14 Merge pull request #284 from specklesystems/bilal/fix-model-card-publish-load-buttons
Bilal/cnx 1994 fix model card publish load buttons
2025-06-17 14:49:13 +03:00
Mucahit Bilal GOKER 0f6150b272 hide selection filter dropdown 2025-06-17 14:47:35 +03:00
Mucahit Bilal GOKER e3bc770369 remove unnecessary checks 2025-06-17 14:42:18 +03:00
Mucahit Bilal GOKER 1bb6ce7b63 merge workspace properties into one 2025-06-17 12:18:46 +03:00
Mucahit Bilal GOKER 6bf05f6ffd bind dialog to button in project selection dialog 2025-06-17 11:59:54 +03:00
Mucahit Bilal GOKER 684c868cd0 add selected workspace to init 2025-06-17 11:36:20 +03:00
Mucahit Bilal GOKER 27009a61e9 workspace selection dialog ui 2025-06-17 11:33:52 +03:00
Mucahit Bilal GOKER 27ff32f584 remove duplicate Windowmanager property invokes 2025-06-17 10:30:25 +03:00
Mucahit Bilal GOKER 223ede3ec7 show sign in button when no accounts present 2025-06-17 08:14:38 +03:00
Mucahit Bilal GOKER 3b2e609888 print to self report 2025-06-17 08:02:11 +03:00
Mucahit Bilal GOKER f4caa55298 cleanup + store selected account 2025-06-17 07:51:17 +03:00
Mucahit Bilal GOKER 166d686b9d update workspace and projects list after execute 2025-06-17 07:50:57 +03:00
Mucahit Bilal GOKER f3f43eebd5 add button to project selection dialog 2025-06-17 07:25:51 +03:00
Mucahit Bilal GOKER 3c16b35232 first pass 2025-06-17 06:57:01 +03:00
Mucahit Bilal GOKER 86fb480a15 draw function shouldn't return anything 2025-06-16 14:10:34 +03:00
Mucahit Bilal GOKER abef8f85d9 update misleading descriptions 2025-06-16 14:10:27 +03:00
Mucahit Bilal GOKER 33a7009585 add type hints for model_card_objects 2025-06-16 14:10:22 +03:00
Mucahit Bilal GOKER 0249ecb313 remove double registry 2025-06-16 14:10:11 +03:00
Mucahit Bilal GOKER bbf8a3b45e remove unused time import 2025-06-16 14:09:52 +03:00
Mucahit Bilal GOKER f1eec55633 remove time limits 2025-06-16 14:09:48 +03:00
Mucahit Bilal GOKER f2bc9a9701 client caching first pass 2025-06-16 14:09:42 +03:00
Mucahit Bilal GOKER 1eb662c6a7 add ui mode to model card id 2025-06-16 14:03:53 +03:00
Mucahit Bilal GOKER c08d4c398f remove unnecessary json usage for saving model cards 2025-06-15 00:52:00 +03:00
Mucahit Bilal GOKER a81a44ca1d remove unused search input 2025-06-15 00:26:05 +03:00
Mucahit Bilal GOKER c4c4431ed2 move refresh after load 2025-06-15 00:22:57 +03:00
Mucahit Bilal GOKER fd23d40a5c force refresh after loading a new version 2025-06-15 00:22:12 +03:00
Mucahit Bilal GOKER f2d6e03ad8 gracefully handle missing icon files 2025-06-15 00:20:54 +03:00
Mucahit Bilal GOKER d5c9097afa fix model card state when publish fails 2025-06-15 00:18:43 +03:00
Mucahit Bilal GOKER 90cd0706ba fix return type mismatch on load operation 2025-06-15 00:16:08 +03:00
Mucahit Bilal GOKER 2daf0de073 remove report from panel 2025-06-15 00:14:54 +03:00
Mucahit Bilal GOKER 3653ece109 removed unnecessary registrations 2025-06-15 00:13:45 +03:00
Mucahit Bilal GOKER c29e0bf8ee duplicate registration of speckle_project 2025-06-15 00:11:37 +03:00
Mucahit Bilal GOKER 95a84eda53 clear model_card_id for selection and version buttons in main panel 2025-06-14 21:41:22 +03:00
Mucahit Bilal GOKER 8116402023 add message to load button in model card 2025-06-13 23:55:03 +03:00
Mucahit Bilal GOKER f6238eb29c fix selection button text showing previous selection 2025-06-13 23:37:55 +03:00
Mucahit Bilal GOKER 0a4c9ec380 update select objects buttons icon 2025-06-13 23:31:14 +03:00
Mucahit Bilal GOKER 4b8b1393ba update descriptions 2025-06-13 23:31:02 +03:00
Mucahit Bilal GOKER ee01b28645 update object highlight button 2025-06-13 19:54:50 +03:00
Mucahit Bilal GOKER 06225a8aab update speckle logo 2025-06-13 19:43:59 +03:00
Mucahit Bilal GOKER 1129d169cb fix duplicate collection 2025-06-13 17:09:20 +03:00
Mucahit Bilal GOKER a787bfb8a3 nil safety 2025-06-13 16:36:12 +03:00
Mucahit Bilal GOKER 700ec92ed2 first pass - update loaded version from model card 2025-06-13 16:22:24 +03:00
Mucahit Bilal GOKER 7fbf0a827b fix version selection dialog and remove search 2025-06-13 15:12:39 +03:00
Mucahit Bilal GOKER b6f659e91f cleanup if else in main panel model cards 2025-06-13 14:53:33 +03:00
Mucahit Bilal GOKER e9628cde77 use version message on selection filter dialog 2025-06-13 14:42:58 +03:00
Mucahit Bilal GOKER cd919353f7 remove unused imports 2025-06-13 09:24:22 +03:00
Mucahit Bilal GOKER 6400138a3c publish after selection 2025-06-13 09:24:11 +03:00
Mucahit Bilal GOKER 9b8ddcc706 remove collection name from model card 2025-06-13 08:33:59 +03:00
Mucahit Bilal GOKER ba3e0b275e fix collection get 2025-06-13 08:06:41 +03:00
Mucahit Bilal GOKER 45a68b3c5f update model card selection 2025-06-13 07:54:20 +03:00
Mucahit Bilal GOKER 49379a3e33 create property groups file 2025-06-13 07:43:36 +03:00
Mucahit Bilal GOKER b30ffdc7d1 remove comments 2025-06-13 07:02:41 +03:00
Mucahit Bilal GOKER 2e53a2fea4 check if model card exists - publish 2025-06-13 06:59:28 +03:00
Mucahit Bilal GOKER 9dc51ee1dc update model card objects to accept a list of object names 2025-06-13 06:58:59 +03:00
Mucahit Bilal GOKER 58e7ac5138 Merge remote-tracking branch 'origin/bilal/fix-model-card-publish-load-buttons' into bilal/fix-model-card-publish-load-buttons 2025-06-13 06:33:26 +03:00
Mucahit Bilal GOKER 6bc9bfdc28 check if model card exists - load 2025-06-13 06:33:15 +03:00
Mucahit Bilal GOKER cce1f3b092 check if model card exists 2025-06-13 06:07:50 +03:00
Mucahit Bilal GOKER ebcbe29398 model card utils 2025-06-13 06:04:48 +03:00
Mucahit Bilal GOKER 59b6b21736 rename load latest -> load model card 2025-06-13 06:00:43 +03:00
Mucahit Bilal GOKER 6c9823776a safety checks on publish 2025-06-13 05:34:35 +03:00
Mucahit Bilal GOKER c77e7642b7 first pass - model card publish button 2025-06-12 22:09:48 +03:00
Mucahit Bilal GOKER 9240fce897 Merge remote-tracking branch 'origin/bilal/fix-model-card-publish-load-buttons' into bilal/fix-model-card-publish-load-buttons 2025-06-12 20:02:51 +03:00
Mucahit Bilal GOKER a87b7a830e first pass - load button on model card 2025-06-12 20:02:45 +03:00
Mucahit Bilal GOKER aad7943f1a first pass 2025-06-12 19:55:31 +03:00
Mucahit Bilal GOKER 589b309d91 moved select objects to a separate function 2025-06-12 14:57:40 +03:00
Mucahit Bilal GOKER 95cb7c35de Merge branch 'v3-dev' into bilal/fix-model-card-publish-load-buttons 2025-06-12 14:33:17 +03:00
Mucahit Bilal GOKER aab3f1792a Merge pull request #281 from specklesystems/dogukan/cnx-1930-blender-receive-blocks-as-linked-duplicates
feat: option to load instances as linked duplicates
2025-06-12 14:32:50 +03:00
Mucahit Bilal GOKER 80a6e424e4 Merge pull request #282 from specklesystems/bilal/select-objects-by-name
Bilal/select objects by name
2025-06-12 14:32:36 +03:00
Mucahit Bilal GOKER f08cac36e7 Merge pull request #283 from specklesystems/bilal/fix-no-versions-in-model
fix adding model urls with no versions
2025-06-12 14:32:25 +03:00
Mucahit Bilal GOKER aa01b5ccf4 remove columns 2025-06-12 14:25:48 +03:00
Mucahit Bilal GOKER da7d260542 rename load_latest operator to model_card_load_button 2025-06-12 13:58:57 +03:00
Dogukan Karatas 64e0f65e29 fixed transform 2025-06-12 12:57:22 +02:00
Dogukan Karatas fedf036e2f fixed nested structures 2025-06-12 12:40:08 +02:00
Mucahit Bilal GOKER b9df9b09de fix adding model urls with no versions 2025-06-12 12:14:03 +03:00
Mucahit Bilal GOKER 3afdb916b3 select only objects in the active view layer 2025-06-11 23:38:38 +03:00
Mucahit Bilal GOKER 8d7b0adea4 better error handling 2025-06-11 22:31:22 +03:00
Mucahit Bilal GOKER 7a6d956ccf fix saving model card issue 2025-06-11 22:31:12 +03:00
Mucahit Bilal GOKER d272743642 select published elements 2025-06-11 22:04:21 +03:00
Mucahit Bilal GOKER c3ce77fe4d first pass 2025-06-11 22:00:54 +03:00
Dogukan Karatas 564aa58ebf Merge pull request #280 from specklesystems/dogukan/apply-modifiers
feat: apply modifiers option
2025-06-11 20:42:52 +02:00
Dogukan Karatas 48fe756fcd converts instances as linked duplicates 2025-06-11 20:39:59 +02:00
Dogukan Karatas 8e2f546cd6 adds applying modififers 2025-06-11 20:08:28 +02:00
Dogukan Karatas 3219172fed Merge pull request #276 from specklesystems/dogukan/query-optimizations
feat: projects with permission in a single query
2025-06-11 15:21:09 +02:00
Mucahit Bilal GOKER 098d576a0b redraw after selection
Ensures the UI updates immediately after adding an object
to the selection filter dialog. This provides more responsive feedback to the user.
2025-06-11 16:13:53 +03:00
Mucahit Bilal GOKER 1950c412c2 fail when trying to create a model in personal workspace 2025-06-11 16:08:27 +03:00
Dogukan Karatas b646b2c7b1 Merge branch 'v3-dev' into dogukan/query-optimizations 2025-06-11 14:44:22 +02:00
Dogukan Karatas 19f22296b4 Merge pull request #279 from specklesystems/bilal/cnx-1980-add-version-message-to-blender
set version message
2025-06-11 14:44:00 +02:00
Dogukan Karatas 240fb6cbbe Merge branch 'v3-dev' into bilal/cnx-1980-add-version-message-to-blender 2025-06-11 14:43:39 +02:00
Dogukan Karatas 5ca6a3f2c2 Merge pull request #277 from specklesystems/bilal/create_project_button
Bilal/create project button
2025-06-11 14:43:08 +02:00
Dogukan Karatas 06fef6ca91 Merge branch 'v3-dev' into bilal/create_project_button 2025-06-11 14:40:49 +02:00
Dogukan Karatas dcbe2d7814 Merge pull request #278 from specklesystems/bilal/create_model_button
Bilal/create model button
2025-06-11 14:39:32 +02:00
Dogukan Karatas b66850b994 adds personal projects 2025-06-11 13:41:50 +02:00
Mucahit Bilal GOKER 6a337ccdad default empty string 2025-06-11 14:20:43 +03:00
Mucahit Bilal GOKER 92fd5387ef set version message 2025-06-11 14:14:59 +03:00
Dogukan Karatas 6205afdda5 optimization on workspace id 2025-06-11 13:10:33 +02:00
Dogukan Karatas d060c3b8cf filter updated 2025-06-11 12:57:31 +02:00
Dogukan Karatas 5dd3dd713a search added 2025-06-11 12:40:34 +02:00
Mucahit Bilal GOKER 4fc1053af7 error handling 2025-06-10 23:38:25 +03:00
Mucahit Bilal GOKER 80204faedf first pass 2025-06-10 23:30:52 +03:00
Mucahit Bilal GOKER 06a47a2f9b exception handling 2025-06-10 22:27:34 +03:00
Mucahit Bilal GOKER b65a7a3d4a unused wm 2025-06-10 22:27:16 +03:00
Mucahit Bilal GOKER 7064574387 unused workspace name 2025-06-10 22:27:00 +03:00
Mucahit Bilal GOKER 169ae4d32a first pass 2025-06-10 22:07:36 +03:00
Dogukan Karatas 4d44b41e2f optimized query 2025-06-10 16:29:18 +02:00
Dogukan Karatas a1f835dc77 Merge pull request #272 from specklesystems/dogukan/cnx-1976-conversion-of-collections-in-blender-send
Release workflow / Build Zip (push) Has been cancelled
Release workflow / deploy-installers (push) Has been cancelled
feat: collection conversion
2025-06-06 10:59:08 +02:00
Dogukan Karatas e2172216a5 Merge pull request #275 from specklesystems/jrm/uv-installer
chore(installer): Revert uv implementation of the installer
2025-06-06 10:58:53 +02:00
Jedd Morgan 965c3e9c6e Removed uv from installer 2025-06-05 14:16:09 +01:00
Dogukan Karatas 65e4812ba1 fixes the object selection 2025-06-05 15:16:01 +02:00
Jedd Morgan 87df86f723 Format 2025-06-05 14:15:53 +01:00
Dogukan Karatas fd32371be3 adds ancestor collections 2025-06-05 12:56:26 +02:00
Dogukan Karatas 19c1334bb3 adds collection conversion 2025-06-04 16:38:34 +02:00
Dogukan Karatas 7a36450143 Merge pull request #271 from specklesystems/dogukan/fix-version-import
Release workflow / Build Zip (push) Has been cancelled
Release workflow / deploy-installers (push) Has been cancelled
fix: global import of bl_info
2025-06-04 10:10:44 +02:00
Dogukan Karatas d37fce644b adds global import 2025-06-04 10:07:41 +02:00
Dogukan Karatas 00bcefba56 Merge pull request #269 from specklesystems/dogukan/fixing-applicationIds
Release workflow / Build Zip (push) Has been cancelled
Release workflow / deploy-installers (push) Has been cancelled
fix: unique applicationIds
2025-06-03 18:17:58 +02:00
Dogukan Karatas d53bc064e1 removes the volume calculation 2025-06-03 17:58:11 +02:00
Dogukan Karatas 50affbedf1 sets unique ids 2025-06-03 17:55:09 +02:00
Dogukan Karatas f00aeecead Merge pull request #268 from specklesystems/bilal/send-ui
feat: publish blender models
2025-06-03 17:52:08 +02:00
Dogukan Karatas bbf2ee79be fixes the applicationId 2025-06-03 16:22:11 +02:00
Dogukan Karatas abb1f042d4 Merge pull request #262 from specklesystems/bilal/v3-cnx-1815-blender-connector-legacy-to-next-gen-transition
v3 - prep for next-gen as stable
2025-06-02 14:42:51 +02:00
Dogukan Karatas 04b4e02d05 adds workspace to metrics 2025-06-02 13:02:52 +02:00
Dogukan Karatas 144990db7a adds metrics 2025-06-02 12:35:55 +02:00
Mucahit Bilal GOKER a5592cdd7d Merge branch 'bilal/cnx-1880-implement-send-ui' into bilal/send-ui 2025-05-28 22:44:02 +03:00
Dogukan Karatas 249e3a0a84 fixed model card issue 2025-05-28 20:54:05 +02:00
Dogukan Karatas f869139d2a fix the specklepy version 2025-05-28 19:41:13 +02:00
Dogukan Karatas 3954369db5 Merge branch 'v3-dev' into bilal/send-ui 2025-05-28 19:39:58 +02:00
Dogukan Karatas 7165d99b76 splitting the edges 2025-05-28 15:11:13 +02:00
Dogukan Karatas 8234db872d fixed the displayValue hierarchy 2025-05-27 22:56:14 +02:00
Dogukan Karatas 9acc84387f updates units 2025-05-27 21:47:06 +02:00
Dogukan Karatas 4eb1f2b773 base collection added 2025-05-27 21:04:27 +02:00
Dogukan Karatas 4d60607a92 materials are updated 2025-05-27 20:53:24 +02:00
Dogukan Karatas 09f809fc8f adds render material conversion 2025-05-27 15:34:26 +02:00
Mucahit Bilal GOKER 9f6833c36e show publish button only after selection made 2025-05-24 22:14:29 +03:00
Mucahit Bilal GOKER 1cc8ed804a store user selection in wm 2025-05-24 22:12:39 +03:00
Mucahit Bilal GOKER eb0f036641 publish > selection filter button 2025-05-24 22:09:11 +03:00
Mucahit Bilal GOKER f140c41af6 replace invoke on publish button 2025-05-24 21:59:42 +03:00
Mucahit Bilal GOKER 8a91634f55 placeholder publish button 2025-05-24 21:58:37 +03:00
Mucahit Bilal GOKER b025819307 hide version selector and load button when publish is selected 2025-05-24 21:57:40 +03:00
Mucahit Bilal GOKER 3ec98d3dd2 remove ui mode from speckle state 2025-05-24 21:55:07 +03:00
Mucahit Bilal GOKER ba182d48c4 add ui mode radio button 2025-05-24 21:53:54 +03:00
Dogukan Karatas f35853cff1 adds mesh to speckle 2025-05-21 15:59:03 +02:00
Dogukan Karatas d64ad50d32 adds spline conversion 2025-05-21 12:57:51 +02:00
Dogukan Karatas ab0b012f58 first pass of conversions 2025-05-20 16:51:53 +02:00
Dogukan Karatas dfa8ff2929 Merge pull request #263 from specklesystems/bilal/cnx-1770-set-viewport-display-color-in-addition-to-material-color
Release workflow / Build Zip (push) Has been cancelled
Release workflow / deploy-installers (push) Has been cancelled
Set Viewport Display color of materials
2025-05-15 15:53:17 +02:00
Mucahit Bilal GOKER 14b628cd3d Merge pull request #265 from specklesystems/dogukan/cnx-1814-project-visibility-workspace-for-blender
fix: updates the sdk version
2025-05-15 16:34:09 +03:00
Dogukan Karatas dfc1d4aaf7 updates the sdk version 2025-05-15 15:06:07 +02:00
Mucahit Bilal GOKER 8c39da370f update tagline 2025-05-15 14:22:43 +03:00
Mucahit Bilal GOKER 532eba37a5 remove beta tag 2025-05-15 14:18:29 +03:00
Mucahit Bilal GOKER 39207e5716 update website 2025-05-15 14:17:59 +03:00
Mucahit Bilal GOKER 48ee5c1ffe final touch 2025-05-15 12:51:23 +03:00
Mucahit Bilal GOKER b41eb812ab use opacity too 2025-05-15 12:47:40 +03:00
Mucahit Bilal GOKER fece1a0151 diffuse color as viewport display color 2025-05-15 07:21:45 +03:00
Dogukan Karatas 3a9e03b982 Merge pull request #260 from specklesystems/dogukan/update-readme
Release workflow / Build Zip (push) Has been cancelled
Release workflow / deploy-installers (push) Has been cancelled
update readme for v3-dev
2025-05-13 11:59:28 +02:00
Dogukan Karatas 96f0db5f63 updates the version 2025-05-13 11:54:19 +02:00
Dogukan Karatas 56a43bca06 small update 2025-05-13 11:42:15 +02:00
Dogukan Karatas 5f451f0143 updates readme 2025-05-13 11:41:25 +02:00
Dogukan Karatas 4cda3ff143 Merge pull request #254 from specklesystems/dogukan/update-project-permissions
feat: server-side checks for project permission
2025-05-13 11:28:31 +02:00
Dogukan Karatas 167a691294 Merge pull request #257 from specklesystems/dogukan/cnx-1775-archicad-to-blender-objects-not-named-with-the-name
fix: naming of Archicad objects
2025-05-13 11:28:07 +02:00
Dogukan Karatas 2f320d360e solving stashes 2025-05-13 11:26:38 +02:00
Dogukan Karatas 25ebdb8c3b solving merge conflict 2025-05-12 17:41:24 +02:00
Dogukan Karatas afd52e788f Merge branch 'v3-dev' into dogukan/cnx-1775-archicad-to-blender-objects-not-named-with-the-name 2025-05-12 17:40:25 +02:00
Dogukan Karatas 7997eedb05 updated uv lock 2025-05-12 17:35:03 +02:00
Dogukan Karatas e98aecaa82 Merge branch 'v3-dev' into dogukan/update-project-permissions 2025-05-12 17:32:47 +02:00
Dogukan Karatas 73e44c6b0e instance check for dataobject 2025-05-12 17:00:06 +02:00
Dogukan Karatas c2b7752539 updates the naming of dataobjects 2025-05-12 11:51:58 +02:00
Mucahit Bilal GOKER 050fc61377 Merge pull request #253 from specklesystems/jedd/cnx-1114-add-support-for-vertex-normals
feat(receive): Added support for receiving vertex normals from Rhino
2025-05-12 11:16:24 +03:00
Mucahit Bilal GOKER 3ab4774dc8 Merge pull request #259 from specklesystems/bilal/cnx-1778-sign-in-button-when-no-accounts-available
Bilal/cnx 1778 sign in button when no accounts available
2025-05-12 11:13:03 +03:00
Mucahit Bilal GOKER d9727005cf don't show workspaces or projects when no accounts 2025-05-11 21:52:38 +03:00
Mucahit Bilal GOKER f73fc604d0 no account no workspaces 2025-05-11 21:50:08 +03:00
Mucahit Bilal GOKER 4bd3dcaf61 fix: workspaces error when no accounts found 2025-05-11 21:49:39 +03:00
Mucahit Bilal GOKER a2ecf1d20e Merge pull request #256 from specklesystems/dogukan/cnx-1774-loading-sketchup-models-in-blender
fix: unit conversion for instances
2025-05-09 17:49:21 +03:00
Mucahit Bilal GOKER 74228b13dd Merge pull request #258 from specklesystems/dogukan/fix-polycurve-splines
fix: adds properties to polycurves
2025-05-09 17:44:54 +03:00
Dogukan Karatas 5aa39fefa4 adds properties to polycuve 2025-05-09 16:40:22 +02:00
Dogukan Karatas 0205636b28 update naming of dataobjects 2025-05-09 16:22:21 +02:00
Mucahit Bilal GOKER 9869ed3b63 Merge pull request #255 from specklesystems/dogukan/cnx-1764-blender-curves-with-arcs-and-closed-splines
fix: better arc and curve conversion
2025-05-09 17:17:14 +03:00
Dogukan Karatas 6a29e0c7cd updates unit conversion 2025-05-09 15:48:35 +02:00
Dogukan Karatas 011e1b90c1 fromat 2025-05-09 15:03:53 +02:00
Dogukan Karatas a1189e3878 updates polycurves 2025-05-09 14:52:36 +02:00
Dogukan Karatas 4fb801b44c updates curve conversion 2025-05-09 14:12:19 +02:00
Dogukan Karatas 80eeb2de39 updates arc and curve conversion 2025-05-09 13:51:04 +02:00
Dogukan Karatas 753760fee4 simplifies the authorization 2025-05-08 13:52:46 +02:00
Dogukan Karatas 8fbb01dee4 updates project permissions 2025-05-08 13:35:45 +02:00
Jedd Morgan 41f2b93347 Fix backwards compatibility with meshes pre-normals 2025-05-08 14:13:57 +03:00
Jedd Morgan 98a6f3251d Merge branch 'v3-dev' into jedd/cnx-1114-add-support-for-vertex-normals 2025-05-08 13:54:36 +03:00
Jedd Morgan 8287ed2e57 Added vertex normals 2025-05-08 13:51:32 +03:00
Mucahit Bilal GOKER 3f5cf49971 Merge pull request #252 from specklesystems/dogukan/implicit-access
Release workflow / Build Zip (push) Has been cancelled
Release workflow / deploy-installers (push) Has been cancelled
fix: checks for permissions
2025-05-06 10:45:52 +03:00
Dogukan Karatas efc2c0b07c seperation of ui and sdk 2025-05-06 09:38:00 +02:00
Dogukan Karatas 242625ebe3 checks permissons 2025-05-05 15:19:19 +02:00
Dogukan Karatas 5f5af2b15a disables the projects with no access 2025-05-05 13:35:34 +02:00
Jedd Morgan ff33b82013 First pass adding vertex normal receive 2025-05-02 10:43:16 +03:00
Dogukan Karatas dbe059f507 Merge pull request #251 from specklesystems/dogukan/material-consistency
feat: material consistency
2025-05-01 22:06:57 +02:00
Dogukan Karatas 9d19e52037 adds a material mapping 2025-05-01 21:35:32 +02:00
Dogukan Karatas f0495ab093 Merge pull request #249 from specklesystems/dogukan/get-version-message
fix: none check for version messages
2025-05-01 17:14:40 +02:00
Dogukan Karatas e57dacc6ef updates the version check 2025-05-01 17:11:00 +02:00
Dogukan Karatas 69dbf2f117 updates version manager 2025-05-01 17:00:52 +02:00
Dogukan Karatas 3cf3867877 Merge pull request #247 from specklesystems/bilal/cnx-1650-integrating-workspaces-to-blender-dui
Bilal/cnx 1650 integrating workspaces to blender dui
2025-05-01 11:49:58 +02:00
Dogukan Karatas 3577f3fd6a updates personal account check 2025-05-01 10:58:32 +02:00
Mucahit Bilal GOKER 242af16e81 Merge remote-tracking branch 'origin/bilal/cnx-1650-integrating-workspaces-to-blender-dui' into bilal/cnx-1650-integrating-workspaces-to-blender-dui 2025-05-01 11:19:05 +03:00
Dogukan Karatas 20f36ffe19 Merge pull request #248 from specklesystems/dogukan/cnx-1476-instance-proxies
feat: conversion of instance proxies
2025-05-01 10:17:33 +02:00
Mucahit Bilal GOKER b103d0da09 only show completed workspaces 2025-05-01 11:16:58 +03:00
Mucahit Bilal GOKER 4add9c5d72 bump specklepy version 2025-05-01 11:16:44 +03:00
Mucahit Bilal GOKER aecb15e549 bump specklepy version 2025-04-30 19:56:21 +03:00
Dogukan Karatas bff8140559 cleaning up some checks 2025-04-30 18:44:12 +02:00
Dogukan Karatas fe98f71be2 fixes the scaling issue 2025-04-30 18:03:51 +02:00
Mucahit Bilal GOKER f2008502f3 support self hosters 2025-04-30 18:24:53 +03:00
Dogukan Karatas 2981c1270b fixes scaling weirdly 2025-04-30 17:22:57 +02:00
Mucahit Bilal GOKER 3011921df9 only list valid versions 2025-04-30 17:29:07 +03:00
Mucahit Bilal GOKER 79ecba213a strip non ascii from model selection dialog texts 2025-04-30 17:28:54 +03:00
Mucahit Bilal GOKER 06db6653fb swıtch the order of account text > move server url to the end so it's visible 2025-04-30 17:28:29 +03:00
Mucahit Bilal GOKER 86200091dc strip non ascii chars 2025-04-30 14:45:19 +03:00
Mucahit Bilal GOKER 4b2a678605 update workspaces when account changes 2025-04-30 14:45:09 +03:00
Dogukan Karatas 1ca2c03ec0 check attributes 2025-04-30 13:07:50 +02:00
Mucahit Bilal GOKER 528b81d294 fetch personal projects 2025-04-30 13:46:05 +03:00
Dogukan Karatas d0dd57a731 sorts nested proxies 2025-04-30 11:56:15 +02:00
Mucahit Bilal GOKER 25258fd39f update projects list when workspace changes 2025-04-30 07:29:15 +03:00
Mucahit Bilal GOKER a884f7a6ea filter projects by workspace 2025-04-30 07:24:05 +03:00
Mucahit Bilal GOKER 6e0df0d4e4 added personal projects to workspaces dropdown 2025-04-30 07:15:51 +03:00
Mucahit Bilal GOKER 58ff3e667e initial workspace dropdown 2025-04-30 07:05:50 +03:00
Mucahit Bilal GOKER 3f5c933ee9 Merge branch 'bilal/cnx-1690-get_accounts-is-continously-called-in-the-background' into bilal/cnx-1650-integrating-workspaces-to-blender-dui 2025-04-30 06:17:56 +03:00
Mucahit Bilal GOKER d137c7b991 store accounts in window manager and initialize in invoke 2025-04-29 23:30:18 +03:00
Mucahit Bilal GOKER 5eec02296b move print statement out of list 2025-04-29 21:59:42 +03:00
Mucahit Bilal GOKER 681acd81c8 added logging to update projects function 2025-04-29 18:56:55 +03:00
Dogukan Karatas 09dd819504 some cleanup 2025-04-29 17:32:24 +02:00
Dogukan Karatas 211296c803 unlink instance collection from scene 2025-04-29 17:29:04 +02:00
Mucahit Bilal GOKER 0fd5448342 print account manager 2025-04-29 17:53:50 +03:00
Dogukan Karatas 3002d7a31b adds poc for instance proxy conversion 2025-04-29 16:36:38 +02:00
Mucahit Bilal GOKER 8eee1ede58 specklepy version update 2025-04-29 17:24:23 +03:00
Mucahit Bilal GOKER 75763d0929 show publish button only after selection made 2025-04-25 11:28:26 +03:00
Mucahit Bilal GOKER 2f2a67a569 store user selection in wm 2025-04-25 11:28:14 +03:00
Mucahit Bilal GOKER 7246d239be publish > selection filter button 2025-04-24 23:06:12 +03:00
Mucahit Bilal GOKER 4d1fe83c1e show placeholder publish button 2025-04-24 22:52:30 +03:00
Mucahit Bilal GOKER 1097bba539 hide version selector and load button when publish is selected 2025-04-24 22:48:43 +03:00
Mucahit Bilal GOKER eb25b6d821 remove ui mode from speckle state 2025-04-24 22:46:26 +03:00
Mucahit Bilal GOKER 80515fdc69 add ui mode radio button in the main panel 2025-04-24 22:46:14 +03:00
Jedd Morgan 4a340ef1ae refactor(ci): Update workflow to use new consolidated deployment workflow (#238)
Release workflow / Build Zip (push) Has been cancelled
Release workflow / deploy-installers (push) Has been cancelled
* Update release workflow

* new workflow

* fileversion experiment

* Always use the run number in the file_version

* only capture first 3 numbers in semver

* build file version from tag

* updated workflow-dispatch
2025-04-24 14:05:07 +01:00
Mucahit Bilal GOKER b6a96802a8 Merge pull request #234 from specklesystems/bilal/cnx-1507-load-button-in-the-model-card-should-get-new-version
Bilal/cnx 1507 load button in the model card should get new version
2025-04-23 14:40:23 +03:00
Mucahit Bilal GOKER f2a0ffa9ee Merge branch 'v3-dev' into bilal/cnx-1507-load-button-in-the-model-card-should-get-new-version 2025-04-23 14:35:19 +03:00
Mucahit Bilal GOKER 24479811f7 Merge pull request #242 from specklesystems/bilal/cnx-1601-adding-models-by-url
Bilal/cnx 1601 adding models by url
2025-04-23 14:33:51 +03:00
Mucahit Bilal GOKER 2153db9704 fix: incorrect version id when model url is given 2025-04-22 23:33:54 +03:00
Mucahit Bilal GOKER dbcc820304 fix: load button in the model card loads latest version 2025-04-17 17:28:45 +03:00
Mucahit Bilal GOKER 830632fa1e ruff check: remove unused imports 2025-04-17 17:18:53 +03:00
Mucahit Bilal GOKER 2f57ca96ca added comments to main panel 2025-04-17 17:12:49 +03:00
Mucahit Bilal GOKER 1b9ee91880 select objects by model card collection name 2025-04-17 17:12:01 +03:00
Mucahit Bilal GOKER 8fb7519e7b load button -> set account id and collection name to model cards 2025-04-17 17:11:37 +03:00
Mucahit Bilal GOKER 9dce548a05 add collection name and account id to model cards 2025-04-17 17:10:24 +03:00
Mucahit Bilal GOKER 487253babe remove window manager invokes from dialogs 2025-04-17 17:08:17 +03:00
Mucahit Bilal GOKER f245584428 i forgot to add actual invoke method 2025-04-17 17:06:18 +03:00
Mucahit Bilal GOKER 1ac784d290 invoke window manager properties on initialization 2025-04-17 17:05:58 +03:00
Mucahit Bilal GOKER 314a962014 Merge branch 'v3-dev' into bilal/cnx-1507-load-button-in-the-model-card-should-get-new-version 2025-04-17 13:12:45 +03:00
Mucahit Bilal GOKER 6b07a0fff4 change button icon and remove text label 2025-04-17 12:50:14 +03:00
Mucahit Bilal GOKER d3208de754 error handling for the url 2025-04-17 12:38:44 +03:00
Mucahit Bilal GOKER 4ba19231b7 code cleanup 2025-04-17 11:57:27 +03:00
Mucahit Bilal GOKER a93ac797fc add project by url functionality 2025-04-16 19:31:06 +03:00
Mucahit Bilal GOKER 4569b1e623 ensure wm properties exists 2025-04-16 19:30:48 +03:00
Mucahit Bilal GOKER ae3222683e get model details by wrapper 2025-04-16 19:30:30 +03:00
Mucahit Bilal GOKER 780184c562 add button to project selection dialog 2025-04-16 18:06:09 +03:00
Dogukan Karatas 310b29292d Merge pull request #239 from specklesystems/bilal/ui-refactor
Bilal/UI refactor
2025-04-16 14:20:03 +02:00
Mucahit Bilal GOKER 8593200a48 Merge branch 'v3-dev' into bilal/ui-refactor 2025-04-16 14:57:48 +03:00
Mucahit Bilal GOKER e53ede3349 Merge branch 'bilal/ui-refactor' into v3-dev 2025-04-16 14:54:54 +03:00
Mucahit Bilal GOKER 892396f1e9 fix: resolve merge conflicts with dev 2025-04-16 14:45:23 +03:00
Dogukan Karatas c2ba9bc8f5 Merge pull request #240 from specklesystems/dogukan/cnx-1599-curve-and-polycurve-converters
feat: converter functions for curve, polycurve and point
2025-04-16 13:33:08 +02:00
Mucahit Bilal GOKER 3f8ec1d259 Merge pull request #241 from specklesystems/bilal/cnx-1590-add-desktop-service-auth-flow
Bilal/cnx 1590 add desktop service auth flow
2025-04-16 14:30:44 +03:00
Mucahit Bilal GOKER fd203bbfea added dynamic selection back 2025-04-16 14:16:33 +03:00
Dogukan Karatas 5ac0c64c9e code formatting 2025-04-16 13:09:37 +02:00
Mucahit Bilal GOKER db367d92cb force redraw of main panel on all selection dialogs 2025-04-16 12:29:12 +03:00
Mucahit Bilal GOKER 289ed58812 force redraw of main panel and close popup 2025-04-16 12:23:48 +03:00
Mucahit Bilal GOKER d1a65510ef Remove popup closing code from model card operators 2025-04-16 11:55:28 +03:00
Mucahit Bilal GOKER 4f58f77c92 Change model card settings dialog to popup window 2025-04-16 11:53:24 +03:00
Mucahit Bilal GOKER 10084b30da close popup after clicking any button on model card settings 2025-04-16 11:50:36 +03:00
Mucahit Bilal GOKER 88f1365fa6 close popup after deleting a model card 2025-04-16 11:34:04 +03:00
Dogukan Karatas f767f53c24 updates exception raise 2025-04-15 17:15:13 +02:00
Mucahit Bilal GOKER 9322e4bfe8 fix: resolve accounts property initialization in project selection dialog 2025-04-15 17:56:54 +03:00
Mucahit Bilal GOKER 31a9452ef0 dynamically update accounts list 2025-04-15 17:07:57 +03:00
Mucahit Bilal GOKER c69f70c7ac show only sign in button when no accounts available 2025-04-15 16:36:49 +03:00
Dogukan Karatas 7ff0778c7e adds curve, polycurve and point converter 2025-04-15 15:33:15 +02:00
Mucahit Bilal GOKER 0cd08b5cef fix: server url typo 2025-04-15 16:32:15 +03:00
Mucahit Bilal GOKER 46eed71a56 add account button next to account dropdown 2025-04-15 16:26:46 +03:00
Dogukan Karatas 623270117e Merge pull request #233 from specklesystems/bilal/cnx-1471-remove-model-card-option
Bilal/cnx 1471 remove model card option
2025-04-14 13:27:04 +02:00
Mucahit Bilal GOKER c5d081a765 clear window manager after load 2025-04-11 22:16:12 +03:00
Mucahit Bilal GOKER b2d9aba778 load button adds model cards 2025-04-11 22:16:02 +03:00
Mucahit Bilal GOKER 12b8bb20d5 get latest version by default after model selection 2025-04-11 22:02:47 +03:00
Mucahit Bilal GOKER 6e6214133a remove setting ui state from load button 2025-04-11 22:02:28 +03:00
Mucahit Bilal GOKER 2c35c4c1b2 version -> code reorg 2025-04-11 18:00:20 +03:00
Mucahit Bilal GOKER 80f9c54111 model -> code reorg 2025-04-11 18:00:05 +03:00
Mucahit Bilal GOKER 508cf6790f version button -> show latest is load option is latest 2025-04-11 17:59:49 +03:00
Mucahit Bilal GOKER 1c20949f58 reorg project selection dialog 2025-04-11 17:39:47 +03:00
Mucahit Bilal GOKER 88e17d66b8 model -> get account and project id from wm 2025-04-11 17:32:30 +03:00
Mucahit Bilal GOKER 625dd64f87 remove mouse mixin 2025-04-11 17:31:31 +03:00
Mucahit Bilal GOKER b22d94606c add load operation to load button 2025-04-10 17:46:18 +03:00
Mucahit Bilal GOKER 309a657420 resolve circular dependency 2025-04-10 17:45:59 +03:00
Mucahit Bilal GOKER 95d79391dd remove model card from load operation 2025-04-10 17:27:33 +03:00
Mucahit Bilal GOKER 49ec641f7d version selection dialog -> selected version id & load option 2025-04-10 17:24:14 +03:00
Mucahit Bilal GOKER 3410e1613b model selection dialog -> add selected_model_id and name 2025-04-10 16:54:22 +03:00
Mucahit Bilal GOKER b013855c97 project selection dialog -> set project id and name on execut 2025-04-10 16:49:20 +03:00
Mucahit Bilal GOKER 78f1a4e23e buttons for selecting project/model/version 2025-04-10 16:42:29 +03:00
Mucahit Bilal GOKER bf4afe0180 Merge branch 'v3-dev' into bilal/cnx-1471-remove-model-card-option 2025-04-09 17:56:05 +03:00
Mucahit Bilal GOKER 7714e8ba28 replace model card index approach with id 2025-04-09 17:39:27 +03:00
Mucahit Bilal GOKER 5ff0965ee6 remove confirmation dialog 2025-04-09 17:38:27 +03:00
Jedd Morgan 0add4fabec feat(installers): Add Installer building workflow (#237)
Release workflow / Build Zip (push) Has been cancelled
Release workflow / deploy-installers (push) Has been cancelled
* New workflows for installer

* versioning

* installers test

* echo version

* echo again

* test21

* set version env

* Extensions manifest

* Add old bl_info

* fixed mistake

* removal of workspace

* Test end to end

* test envar cleanup

* main
2025-04-02 12:33:39 +01:00
Dogukan Karatas 8df7559e76 Merge pull request #235 from specklesystems/dogukan/cnx-1423-native-blender-conversion
feat: native speckle to blender conversion
2025-04-01 16:57:14 +02:00
Dogukan Karatas a1f5d0f21f Merge pull request #236 from specklesystems/dogukan/cnx-1483-tekla-colors-not-applied-to-objects
fix: applying materials to objects properly
2025-04-01 16:56:56 +02:00
Jedd Morgan 1fe08136de Updated config.yml 2025-04-01 10:25:47 +01:00
Dogukan Karatas 7ef35cd4e1 updates display_to_native function 2025-03-31 14:30:25 +02:00
Dogukan Karatas f9a75772ed adds ellipse converter 2025-03-28 17:21:28 +01:00
Dogukan Karatas b85f805330 adds arc and circle conversion 2025-03-28 16:12:10 +01:00
Mucahit Bilal GOKER 6af53c56b3 make delete button red 2025-03-28 18:10:46 +03:00
Mucahit Bilal GOKER 100dc9213b delete self.report 2025-03-28 18:10:36 +03:00
Mucahit Bilal GOKER 8068962cdf Merge pull request #232 from specklesystems/bilal/cnx-1469-group-model-cards-by-project
group model cards by project
2025-03-28 17:28:53 +03:00
Mucahit Bilal GOKER aad9246463 adds functional load button in the model card 2025-03-27 19:04:10 +03:00
Mucahit Bilal GOKER 0dbf691f16 adds delete model card button 2025-03-26 16:49:02 +03:00
Mucahit Bilal GOKER 9628ab5bdf group model cards by project 2025-03-26 16:23:18 +03:00
Mucahit Bilal GOKER db195a4285 Merge pull request #230 from specklesystems/dogukan/cnx-1422-rendermaterial-and-colorproxy
feat: conversion of renderMaterialProxies
2025-03-26 15:24:34 +03:00
KatKatKateryna 7d52ac9568 Merge branch 'v3-dev' into dogukan/cnx-1422-rendermaterial-and-colorproxy 2025-03-25 12:38:04 +00:00
KatKatKateryna 8c08bf87cc Merge pull request #231 from specklesystems/updated-time-for-model-search
updated_at time for Model search
2025-03-25 12:37:08 +00:00
KatKatKateryna 1c979f821c small fix 2025-03-25 12:28:54 +00:00
Dogukan Karatas 8589c7b467 adds renderMaterial conversion 2025-03-21 16:10:33 +01:00
Dogukan Karatas 64bf623ee4 adds utility functions 2025-03-21 15:52:49 +01:00
Mucahit Bilal GOKER 36069e2908 Merge pull request #229 from specklesystems/dogukan/fix-scaling
fix: add the scaling function
2025-03-21 15:29:26 +03:00
Mucahit Bilal GOKER 54e129d0b4 Merge pull request #228 from specklesystems/bilal/object-highlight
object highlight logic added
2025-03-21 15:17:20 +03:00
Mucahit Bilal GOKER cffcaf74a1 zoom to selection 2025-03-21 15:08:08 +03:00
Mucahit Bilal GOKER 1bd60a92b6 moved it to blender_operators folder 2025-03-21 15:05:22 +03:00
Mucahit Bilal GOKER 37fc1ff57f object highlight logic added 2025-03-20 23:43:48 +03:00
Dogukan Karatas 9bfc73a53d adds the scale factor 2025-03-20 15:03:28 +01:00
Dogukan Karatas 32f3415680 Merge pull request #227 from specklesystems/bilal/code-reorg
feat: code re-organization
2025-03-20 14:51:35 +01:00
Dogukan Karatas ede16df1b1 operations splitted 2025-03-20 14:15:38 +01:00
Mucahit Bilal GOKER 0f067ce968 code reorg 2025-03-19 21:51:46 +03:00
Mucahit Bilal GOKER 0e03226a55 Merge pull request #225 from specklesystems/bilal/view-model-in-browser
Bilal/view model in browser
2025-03-19 18:36:18 +03:00
Mucahit Bilal GOKER f5b436ea62 Merge branch 'v3-dev' into bilal/view-model-in-browser 2025-03-19 18:36:09 +03:00
Mucahit Bilal GOKER 70de8b7a45 Merge pull request #226 from specklesystems/bilal/load-latest-version-radio-button
Bilal/load latest version radio button
2025-03-19 18:35:18 +03:00
Dogukan Karatas 87dea86ba6 Merge pull request #224 from specklesystems/dogukan/cnx-1090-first-receive-on-blender-dui25
feat: first receive with next-gen blender connector
2025-03-19 16:34:57 +01:00
Dogukan Karatas d8eef2b51c Merge pull request #223 from specklesystems/bilal/hide-publish-button
Bilal/hide publish button
2025-03-19 14:50:12 +01:00
Dogukan Karatas f452562fff hierarchy creation updated 2025-03-19 14:49:17 +01:00
Mucahit Bilal GOKER a559158931 little touches for loading latest 2025-03-18 23:05:42 +03:00
Mucahit Bilal GOKER cc08df5c88 view model and versions in browser 2025-03-18 22:19:33 +03:00
Mucahit Bilal GOKER 4aff5aaca9 add server url to wm 2025-03-18 17:14:53 +03:00
Mucahit Bilal GOKER ea2243a14a add project id and model id to model card setting 2025-03-18 16:26:38 +03:00
Mucahit Bilal GOKER 15f88773d2 add model card id 2025-03-18 16:19:05 +03:00
Mucahit Bilal GOKER e703116e78 load latest version radio button 2025-03-17 22:34:26 +03:00
Dogukan Karatas ae081295d5 creates collection hierarchy 2025-03-14 10:55:50 +01:00
Dogukan Karatas 2ae25f22be new naming logic 2025-03-13 16:47:13 +01:00
Dogukan Karatas 5c2ecc6f97 first receive attempts 2025-03-12 12:41:31 +01:00
Mucahit Bilal GOKER 2ee6636dde comment out publish button 2025-03-06 18:32:14 +03:00
Mucahit Bilal GOKER fdd05f7958 Merge pull request #222 from specklesystems/bilal/cnx-1003-replace-poetry-with-uv
Bilal/cnx 1003 replace poetry with uv
2025-02-18 17:36:55 +03:00
Mucahit Bilal GOKER 62e3e80f65 replace pip with uv in the readme. 2025-02-18 17:23:39 +03:00
Mucahit Bilal GOKER 6953a34645 Merge pull request #221 from specklesystems/bilal/cnx-997-add-docstrings
Bilal/cnx 997 add docstrings
2025-01-29 13:08:34 +03:00
Mucahit Bilal GOKER 8d704b1034 move from poetry to uv for dependency management 2025-01-21 21:10:56 +03:00
Mucahit Bilal GOKER 816ff52669 move UserProjectsFilter to user_inputs 2025-01-21 21:10:42 +03:00
Mucahit Bilal GOKER 105ef9b713 Merge branch 'bilal/dui3' into bilal/cnx-997-add-docstrings 2025-01-21 15:10:31 +03:00
Mucahit Bilal GOKER a38f82a91a Merge pull request #220 from specklesystems/bilal/cnx-996-add-type-hints
Bilal/cnx 996 add type hints
2025-01-21 15:05:01 +03:00
Mucahit Bilal GOKER 1388fb3c5a suppress ruff warnings for init py 2025-01-21 14:23:48 +03:00
Mucahit Bilal GOKER 5d1be43263 move dependencies back at the top. 2025-01-21 14:19:05 +03:00
Mucahit Bilal GOKER 1da76dadf8 ruff warning fixes 2025-01-21 14:06:50 +03:00
Mucahit Bilal GOKER 188efd7ea5 jedds suggested changes 2025-01-21 14:06:42 +03:00
Mucahit Bilal GOKER f5f5c513a6 add docstrings to model_card_settings 2025-01-20 15:06:27 +03:00
Mucahit Bilal GOKER 70e0e1e727 add docstrings to model_card 2025-01-20 15:03:12 +03:00
Mucahit Bilal GOKER 9301186b63 add docstrings to speckle_state 2025-01-20 15:02:16 +03:00
Mucahit Bilal GOKER 0689cf34a1 add docstrings to misc 2025-01-20 14:53:23 +03:00
Mucahit Bilal GOKER 8299ca84af add docstrings to account_manager 2025-01-20 14:51:04 +03:00
Mucahit Bilal GOKER 5f1228091e add docstrings to version_manager 2025-01-20 14:48:33 +03:00
Mucahit Bilal GOKER 7afb2ec18a add docstrings to model_manager 2025-01-20 14:45:51 +03:00
Mucahit Bilal GOKER 758c6f48cd add docstrings to project_manager 2025-01-20 14:40:35 +03:00
Mucahit Bilal GOKER 44ba054e07 add docstrings to main_panel 2025-01-20 14:34:00 +03:00
Mucahit Bilal GOKER 0f2b208b90 adds docstrings to selection_filter_dialog 2025-01-20 14:29:40 +03:00
Mucahit Bilal GOKER accdd00880 add docstrings to version_selection_dialog 2025-01-20 14:09:51 +03:00
Mucahit Bilal GOKER de4ed8e55a remove none returns from project_selection_dialog 2025-01-20 13:56:36 +03:00
Mucahit Bilal GOKER 5bd46de070 added docstrings to model_selection_dialog 2025-01-20 13:45:48 +03:00
Mucahit Bilal GOKER fb23cc3eaf added docstrings to project_selection_dialog 2025-01-20 13:37:59 +03:00
Mucahit Bilal GOKER fafa529df4 added comments to operators 2025-01-20 13:34:04 +03:00
Mucahit Bilal GOKER 5ed98f7acf type hinting to mouse_positing_mixin py 2025-01-17 22:18:52 +03:00
Mucahit Bilal GOKER e542a7d99b add type hinting to speckle_state py 2025-01-17 22:16:42 +03:00
Mucahit Bilal GOKER 9635f04db8 type checking in model_card py 2025-01-17 22:13:03 +03:00
Mucahit Bilal GOKER cd40e32b4e remove spaces 2025-01-17 21:55:32 +03:00
Mucahit Bilal GOKER 8319a73edf type hinting to misc py 2025-01-17 21:55:14 +03:00
Mucahit Bilal GOKER aca7547f6c type checking in icons.py 2025-01-17 21:51:31 +03:00
Mucahit Bilal GOKER c4061182f9 added type checking to model card settings 2025-01-17 21:08:19 +03:00
Mucahit Bilal GOKER d1dcf86357 added type hinting to load.py 2025-01-17 21:03:38 +03:00
Mucahit Bilal GOKER 70d52db17f import typing 2025-01-17 21:03:29 +03:00
Mucahit Bilal GOKER d2392b0d2b type checking in publish py 2025-01-17 18:37:48 +03:00
Mucahit Bilal GOKER 14bf10f1bb added type checking to main panel 2025-01-17 18:28:46 +03:00
Mucahit Bilal GOKER 10d94e5d28 import object to selection filter dialog 2025-01-17 18:15:16 +03:00
Mucahit Bilal GOKER dff6b3101e added type hints to selection filter dialog 2025-01-17 18:14:45 +03:00
Mucahit Bilal GOKER 497b04a70b added type hinting to version manager 2025-01-17 14:58:07 +03:00
Mucahit Bilal GOKER 31137a9fd8 added type checking to model manager 2025-01-17 14:39:35 +03:00
Mucahit Bilal GOKER f3f65a037a type checking account manager 2025-01-17 14:33:28 +03:00
Mucahit Bilal GOKER a680bea021 added type checking to project_manager 2025-01-17 12:48:50 +03:00
Mucahit Bilal GOKER 1138cc12d4 added type hints to version selection dialog 2025-01-17 12:42:25 +03:00
Mucahit Bilal GOKER 13e995db53 added type hints to model selection dialog 2025-01-17 12:42:17 +03:00
Mucahit Bilal GOKER 572eeecb3a added type hints to project selection dialog 2025-01-17 12:42:08 +03:00
Mucahit Bilal GOKER f0d39dc39f add WindowManager and project retrieval functionality to project selection dialog 2025-01-16 12:50:27 +03:00
Mucahit Bilal GOKER 74e84f803d Merge pull request #216 from specklesystems/bilal/cnx-986-fix-account-switching-bug
Bilal/cnx 986 fix account switching bug
2025-01-15 23:08:10 +03:00
Mucahit Bilal GOKER 79f09e5364 Merge pull request #218 from specklesystems/bilal/cnx-892-replace-placeholder-version-dialog
Bilal/cnx 892 replace placeholder version dialog
2025-01-15 23:07:42 +03:00
Mucahit Bilal GOKER 57f671dd60 Merge branch 'bilal/dui3' into bilal/cnx-892-replace-placeholder-version-dialog 2025-01-15 12:02:55 +03:00
Mucahit Bilal GOKER d862ace188 Merge pull request #217 from specklesystems/bilal/cnx-890-replace-placeholder-model-info
Bilal/cnx 890 replace placeholder model info
2025-01-15 12:00:47 +03:00
Mucahit Bilal GOKER fd46280130 Merge branch 'bilal/dui3' into bilal/cnx-890-replace-placeholder-model-info 2025-01-15 11:58:19 +03:00
Mucahit Bilal GOKER 0c68eb1a6a Merge pull request #214 from specklesystems/bilal/cnx-886-replace-placeholder-projects-info
Bilal/cnx 886 replace placeholder projects info
2025-01-13 19:55:42 +03:00
Mucahit Bilal GOKER 11e4860364 Merge branch 'bilal/dui3' into bilal/cnx-886-replace-placeholder-projects-info 2025-01-13 19:53:25 +03:00
Mucahit Bilal GOKER f8474777c0 Merge pull request #213 from specklesystems/bilal/cnx-602-account-binding
Bilal/cnx 602 account manager
2025-01-13 19:48:28 +03:00
Mucahit Bilal GOKER 242476a43a use List and Tuple from typing for compatibility 2025-01-13 19:45:41 +03:00
Mucahit Bilal GOKER 75398aa830 removed blender executable path from workspace file 2025-01-13 19:44:49 +03:00
Mucahit Bilal GOKER 9827c46988 Update search field label and description for pasting urls 2025-01-02 14:25:51 +03:00
Mucahit Bilal GOKER 8c7908b4ef Update project selection dialog to store selected account ID in window manager 2025-01-02 13:37:58 +03:00
Mucahit Bilal GOKER 76aaf2fd41 it gets versions associated with the selected account 2024-12-08 21:54:06 +03:00
Mucahit Bilal GOKER a8b2500c0a first draft 2024-12-08 21:23:00 +03:00
Mucahit Bilal GOKER 3a863cd0dd Added model manager and implemented model fetching from selected project 2024-12-07 22:16:28 +03:00
Mucahit Bilal GOKER a57fbe6e3d hide add project by url button. 2024-12-07 08:03:44 +03:00
Mucahit Bilal GOKER f346a3918c implemented search in project_selection_dialog 2024-12-06 19:44:00 +03:00
Mucahit Bilal GOKER e13a910700 updated specklepy to 2.21.1 2024-12-06 19:43:38 +03:00
Mucahit Bilal GOKER 71320a5acc update projects list when account changes 2024-12-06 16:09:44 +03:00
Mucahit Bilal GOKER 6f409ee228 removed projects from the state and formatted project role 2024-12-06 16:06:22 +03:00
Mucahit Bilal GOKER 44f3c88c81 Merge branch 'bilal/cnx-602-account-binding' into bilal/cnx-886-replace-placeholder-projects-info 2024-12-06 15:37:48 +03:00
Mucahit Bilal GOKER 8b3aaefe8c removes accounts from state.
don't want to bloat the state with useless info. we can always fetch this via specklepy.
2024-12-06 15:11:43 +03:00
Mucahit Bilal GOKER f63345e304 handles no account found case
When no account is added to Manager, dropdown menu in the UI prompts user to add an account from Manager.
2024-12-06 14:46:17 +03:00
Mucahit Bilal GOKER f81752b41e first draft implementation on projects fetching 2024-12-06 14:36:08 +03:00
Mucahit Bilal GOKER 482a3189d8 upgraded specklepy version to 2.21.0 2024-12-06 14:35:55 +03:00
Mucahit Bilal GOKER c8714d0df8 formatted speckle_state 2024-12-05 15:54:33 +03:00
Mucahit Bilal GOKER fe69091c5c added type hints for account manager functions 2024-12-05 15:13:26 +03:00
Mucahit Bilal GOKER 457380bc3c removed unnecesary projects code 2024-12-05 14:59:44 +03:00
Mucahit Bilal GOKER 6613e1a7a6 added account manager 2024-12-05 13:34:49 +03:00
Mucahit Bilal GOKER 027df4f5d9 get rid of todo comment 2024-10-03 17:12:19 +01:00
Mucahit Bilal GOKER 171105f827 moved the account into speckle state 2024-10-03 13:54:07 +01:00
Mucahit Bilal GOKER f2363586aa added speckle state 2024-10-03 12:27:40 +01:00
Mucahit Bilal GOKER 28a7a02ee5 fixed initialization issue 2024-10-02 11:36:15 +01:00
Mucahit Bilal GOKER dce78ceeca added poetry 2024-10-02 09:54:45 +01:00
Mucahit Bilal GOKER a5824702ab Merge branch 'bilal/dui3' of https://github.com/specklesystems/speckle-blender into bilal/dui3 2024-09-29 23:40:17 +03:00
Mucahit Bilal GOKER bb8486c94a keep props dialog at the same place 2024-09-29 23:37:31 +03:00
Mucahit Bilal GOKER d32fc23e14 saving model cards to file (initial implementation) 2024-09-28 14:16:09 +03:00
Mucahit Bilal GOKER 3e85a018fc renamed selection dialog to selection filter dialog 2024-09-25 10:23:47 +03:00
Mucahit Bilal GOKER dd2e222c84 Added Speckle logo in the main panel. 2024-09-24 23:49:44 +03:00
Mucahit Bilal GOKER bcdabb1226 added project by url button. 2024-09-24 22:47:51 +03:00
Mucahit Bilal GOKER 8c1a5b4463 added type hinting 2024-09-24 22:36:42 +03:00
Mucahit Bilal GOKER 4811329d9e Add docstrings to some classes. 2024-09-24 22:26:40 +03:00
Mucahit Bilal GOKER 6c3ab4baef adjusted column widths in uilists 2024-09-24 22:04:20 +03:00
Mucahit Bilal GOKER a7295e7b25 adjusted the layout of buttons in the model card 2024-09-24 21:51:33 +03:00
Mucahit Bilal GOKER fb8fda27c5 added some todo comments to model card settings dialog. 2024-09-24 21:33:14 +03:00
Mucahit Bilal GOKER 32b114274c Added options to model card settings 2024-09-24 21:31:26 +03:00
Mucahit Bilal GOKER 02a9da050f show greeting message only when no model card added. 2024-09-24 21:16:59 +03:00
Mucahit Bilal GOKER c6ba0ff86d model cards 1st pass 2024-09-24 18:03:58 +03:00
Mucahit Bilal GOKER 0d386aa93d adjustments to init py 2024-09-24 17:03:20 +03:00
Mucahit Bilal GOKER d439f65463 Project and Model names visible across dialogs. 2024-09-23 22:47:27 +03:00
Mucahit Bilal GOKER 345bae9463 beautified the selection summary. 2024-09-23 22:05:48 +03:00
Mucahit Bilal GOKER cb1f9c0480 first pass on selection dialog. 2024-09-23 21:46:49 +03:00
Mucahit Bilal GOKER 2be74ce617 Update .gitignore 2024-09-23 16:07:58 +03:00
Mucahit Bilal GOKER 56675ef88d initial commit 2024-09-23 16:02:26 +03:00
Alper S. Soylu 4c381bd809 fix send stream object cases (#207)
* fix send stream object cases

* clean logs

* revert workaround

* revert unnecessary logic

* revert unnecessary logic

* rename set_user

---------

Co-authored-by: Soylu <alper.soylu@siemens.com>
2024-08-05 13:36:06 +01:00
Alper S. Soylu 8c3885ece8 Lazy loading for stream branches & commits (#200)
* only fetch branches on reload

* initial load

* load on stream selection

* refactor get_item_by_index

* fix resetting commit selection on default branch

* refactor load_stream_branches

* clean logs

* explicit return

---------

Co-authored-by: Soylu <alper.soylu@siemens.com>
Co-authored-by: Jedd Morgan <45512892+JR-Morgan@users.noreply.github.com>
2024-07-29 12:15:33 +01:00
Alper S. Soylu 15bd3f5070 fix resetting commit selection on default branch (#206)
Co-authored-by: Soylu <alper.soylu@siemens.com>
2024-07-28 23:22:07 +01:00
Jedd Morgan 6fd4571d34 update macos images (#203) 2024-07-16 19:42:34 +02:00
Jedd Morgan 5081177653 Small tweaks to installer (#202)
* Skip installing dependencies after initial install on production

* remove print

* remove requirements.txt on install

* small tweaks

---------

Co-authored-by: Soylu <alper.soylu@siemens.com>
2024-07-12 12:23:36 +01:00
Alper S. Soylu 0d0ca2c811 Skip installing dependencies after initial install (#199)
* Skip installing dependencies after initial install on production

* remove print

* remove requirements.txt on install

---------

Co-authored-by: Soylu <alper.soylu@siemens.com>
2024-07-12 12:18:40 +01:00
Jedd Morgan 230e27a162 Merge pull request #196 from overengineer/fix/resetting_selected_branch_workaround
Recover branch selection
2024-06-05 14:54:21 +01:00
Jedd Morgan 669fd19c2e Type hint polish 2024-06-05 14:52:32 +01:00
Soylu 139f8ccb33 type hints 2024-06-03 19:10:54 +03:00
Soylu 7f625bd468 check None item 2024-05-30 18:11:29 +03:00
Soylu 4d07ba7637 use object ids 2024-05-30 16:53:53 +03:00
Jedd Morgan 7431b57e0e Merge pull request #198 from specklesystems/jrm/bump-deps
Updated deps
2024-05-27 16:34:39 +01:00
Jedd Morgan 57c19ba3c5 Updated deps 2024-05-27 16:34:07 +01:00
Jedd Morgan 8e3c2ece2f Merge pull request #197 from overengineer/fix/slow_startup
Speedup install dependencies
2024-05-24 16:48:29 +01:00
Soylu cfc58d9456 speedup install dependencies 2024-05-21 19:54:09 +03:00
Soylu e74a6cebb1 boundary check 2024-05-19 23:24:13 +03:00
Soylu 5e01d5a976 Recover branch selection 2024-05-16 18:19:36 +03:00
Jedd Morgan a2f7ab422f Merge pull request #194 from specklesystems/dev
2.19 changes
2024-05-14 17:41:21 +01:00
Jedd Morgan 8c58d9d14c Dev (#192)
* feat(UI): CNX-9070 update connectors to use new fe2 terminology (#186)

* feat: CNX-8705 fe2 ur ls in blender (#182)

* Increased default branch get to 100 limit, and added as mesh conversion for text + surfaces + metaball

* poetry lock

* Upgraded typing module

* FE2 URL support

* Raised exceptions now display to user

* Fixed unused imports

* Updated terminology to fe2

* merge from stash

* comments

* bl_descriptions

* bl_desc

* new urls

* Updated naming of revit elements to include family type (#193)
2024-05-14 17:38:50 +01:00
Jedd Morgan 90e61b6dc1 Updated naming of revit elements to include family type (#193) 2024-05-01 21:14:52 +01:00
Jedd Morgan 5c479e4c0e Merge branch 'main' into dev 2024-04-11 13:32:06 +01:00
Jedd Morgan 97d20ad7b1 Ci tweaks (#191) 2024-03-14 11:09:48 +00:00
Jedd Morgan 2800b84747 Merge pull request #190 from specklesystems/main
BACK MERGE MAIN -> DEV
2024-03-11 16:18:25 +00:00
Jedd Morgan 511d69314e feat(ci): [CNX-9125] Update to digicert-keylocker (#189)
* feat(ci): Update to digicert-keylocker

* removed pem
2024-03-11 17:14:36 +01:00
Jedd Morgan 24e7f02213 feat(UI): CNX-9070 update connectors to use new fe2 terminology (#186)
* feat: CNX-8705 fe2 ur ls in blender (#182)

* Increased default branch get to 100 limit, and added as mesh conversion for text + surfaces + metaball

* poetry lock

* Upgraded typing module

* FE2 URL support

* Raised exceptions now display to user

* Fixed unused imports

* Updated terminology to fe2

* merge from stash

* comments

* bl_descriptions

* bl_desc

* new urls
2024-02-28 18:53:11 +00:00
Jedd Morgan c1d7947085 Merge pull request #188 from specklesystems/main
deprecated delete stream (#187)
2024-02-28 16:46:37 +00:00
Jedd Morgan 21281e5d77 deprecated delete stream (#187) 2024-02-28 16:46:08 +00:00
Jedd Morgan 29bbdc69a2 chore(ci): CNX-9051 Update ci signing (#185)
* feat: CNX-8705 fe2 ur ls in blender (#182)

* Increased default branch get to 100 limit, and added as mesh conversion for text + surfaces + metaball

* poetry lock

* Upgraded typing module

* FE2 URL support

* Raised exceptions now display to user

* Fixed unused imports

* Update ci signing

* Update config.yml

* Bump Deps

* powershell

* More powershell
2024-02-27 11:29:33 +00:00
Jedd Morgan efe6e6a4a0 2.18 Update (#183)
* feat: CNX-8705 fe2 ur ls in blender (#182)

* Increased default branch get to 100 limit, and added as mesh conversion for text + surfaces + metaball

* poetry lock

* Upgraded typing module

* FE2 URL support

* Raised exceptions now display to user

* Fixed unused imports
2024-02-26 17:32:38 +00:00
Jedd Morgan f036109020 Increased default branch get to 100 limit, and added as mesh conversi… (#181)
* Increased default branch get to 100 limit, and added as mesh conversion for text + surfaces + metaball

* poetry lock
2024-02-07 18:05:00 +00:00
Jedd Morgan 86bc2dc590 Merge pull request #180 from specklesystems/jrm/update/2.17
chore(deps): lock deps
2023-11-29 11:57:35 +00:00
Jedd Morgan a34b6ad0c2 lock + readme 2023-11-29 11:56:50 +00:00
Jedd Morgan e436949ef9 Merge pull request #179 from specklesystems/jrm/4.0/material-support
feat(4.X): Added support for Blender 4.X material nodes
2023-11-11 20:18:37 +00:00
Jedd Morgan 6d8f4a4a80 Added support for Blender 4.X BSDF materials 2023-11-11 20:16:49 +00:00
Jedd Morgan dabb65427a Added defaults to converter settings so converter can be used without connector 2023-10-18 15:48:31 +01:00
Jedd Morgan 57ece17e8b Merge pull request #177 from specklesystems/jrm/chore/comments
chore: fixed some mistakes in code comments
2023-10-16 11:43:58 +01:00
Jedd Morgan 4362f737d0 chore: fixed some mistakes in code comments 2023-10-16 11:43:36 +01:00
Jedd Morgan b55df58313 Merge pull request #176 from specklesystems/jrm/blender/automate
Extracted some functions for automate
2023-10-13 13:02:28 +01:00
Jedd Morgan afa6722253 Extracted some functions for automate 2023-10-13 12:56:30 +01:00
Jedd Morgan a3d4881578 Merge pull request #175 from specklesystems/jrm/properties/depth
fix(custom_properties): Set max depth of properties to 64 to align with newtonsoft limit
2023-10-08 18:45:50 +01:00
JR-Morgan 1af158a5e0 poetry lock 2023-10-08 18:44:18 +01:00
JR-Morgan 47857a9db0 Aligned json depth cap with newtonsofts reader at 64 levels of depth 2023-10-08 18:40:43 +01:00
Jedd Morgan 3b026e6027 Merge pull request #173 from specklesystems/jrm/fix-py-indent-error
Update users.py
2023-09-22 13:03:32 +01:00
Jedd Morgan d572609f75 Update users.py 2023-09-22 13:03:06 +01:00
Jedd Morgan 37032cc7aa Merge pull request #172 from specklesystems/jrm/deps/specklepy216
better error message when user has no valid accounts
2023-09-12 12:35:03 +01:00
Jedd Morgan 6027325878 better error message when user has no valid accounts 2023-09-12 11:59:39 +01:00
Jedd Morgan 5ddb2aa052 Merge pull request #171 from specklesystems/jrm/deps/specklepy216
fix(receive)!: Fixed issue with collection name conflicts
2023-09-12 10:50:15 +01:00
Jedd Morgan 67a18821cc fix(receive)!: Collections will no longer update 2023-09-12 10:36:22 +01:00
Jedd Morgan 2688a69286 Merge pull request #169 from specklesystems/jrm/deps/specklepy216
Jrm/deps/specklepy216
2023-09-08 16:04:08 +01:00
Jedd Morgan 56216a6137 bump specklepy 2023-09-08 15:39:25 +01:00
Jedd Morgan 319cbf8960 Specklepy 2.16 2023-09-08 13:41:14 +01:00
Jedd Morgan d7ac6c0b95 Updated to new core metrics 2023-09-04 19:51:39 +01:00
Jedd Morgan 201ca5f26e Update blender_commit_object_builder.py 2023-07-22 01:02:23 +01:00
Jedd Morgan 89528437b1 Merge pull request #167 from specklesystems/vertex-color-uint
Fixed issue with uint32 vertex colours not parsing
2023-07-17 21:51:18 +01:00
Jedd Morgan 91bde24fe9 Fixed issue with uint32 vertex colors not parsing 2023-07-17 21:21:28 +01:00
Jedd Morgan 991b0f9ff1 Merge pull request #166 from specklesystems/2.15-deps-update
Fixed issue with hosted elements on nested instances not receiving or receiving twice
2023-07-06 15:20:58 +01:00
Jedd Morgan ee1715ff8a fixed circular import 2023-07-06 15:12:34 +01:00
Jedd Morgan 70ee09b9bb Fixed issue with non-convertable revit definitions 2023-07-06 13:57:44 +01:00
Jedd Morgan 83dd62d03f deps update 2023-07-06 13:16:50 +01:00
Jedd Morgan 94cc0ac3f7 fix(instance): Fixed issues with hosted and nested instances 2023-07-06 13:14:20 +01:00
Jedd Morgan 36cb94d3d7 fix(converter): ToSpeckle instances 2023-07-03 16:59:53 +01:00
Jedd Morgan c60baf78c5 deps: updated to specklepy 2.15.0 2023-06-27 16:09:04 +01:00
Jedd Morgan d72cfd3522 feat(view): Added support for receiving/sending view objects 2023-05-31 01:12:17 +01:00
Jedd Morgan a26618a4f7 Merge pull request #164 from specklesystems/2.14/bug-fixes
2.14/bug fixes
2023-05-29 13:39:06 +01:00
Jedd Morgan eaf370407d Updated lock 2023-05-29 13:37:49 +01:00
Jedd Morgan a2b50fe5a1 Added support for sending diffuse BSDF shader materials 2023-05-29 13:33:53 +01:00
Jedd Morgan 7e62f76841 fix: fixed bug with imperial units scaling on send 2023-05-29 12:40:54 +01:00
Jedd Morgan fc804f16d3 Fixed bug with circular referenced custom props 2023-05-29 12:40:34 +01:00
Jedd Morgan 6c7da24595 Merge pull request #163 from specklesystems/collections
Collections
2023-05-27 13:44:19 +01:00
Jedd Morgan b284d39328 removed normal re-calculation 2023-05-27 13:43:42 +01:00
Jedd Morgan 907185c9bb Object naming tweaks 2023-05-26 18:58:42 +01:00
Jedd Morgan a189a2e1c0 Various cleanup and bug fixes 2023-05-26 18:18:10 +01:00
Jedd Morgan 1fad926275 Remove empty collections from send 2023-05-25 21:01:34 +01:00
Jedd Morgan 99c147fe2f Sending collections (all collections regardless of contents) 2023-05-25 17:45:31 +01:00
Jedd Morgan e2adf710b3 commit object builder 2023-05-25 00:22:09 +01:00
Jedd Morgan 9509344533 Added traversal refactor and support for receiving collections 2023-05-18 22:15:35 +01:00
Jedd Morgan 6fabc6cae6 feat(converter): implemented view to native 2023-05-10 17:31:18 +01:00
Jedd Morgan c39298687d Merge pull request #160 from specklesystems/jrm/ismultiplayer
Added `isMultiplayer` property
2023-04-13 14:26:29 +01:00
Jedd Morgan bcdddbf930 Added isMultiplayer property 2023-04-13 14:24:17 +01:00
Jedd Morgan b5684e34f6 Merge pull request #159 from specklesystems/jrm/curve-fix
Removed merge vertices by distance from clean mesh
2023-04-05 12:51:27 +01:00
Jedd Morgan 2203fe98f8 Removed merge vertices by distance from clean mesh 2023-04-05 12:47:03 +01:00
Jedd Morgan bbfdf2863b Merge pull request #158 from specklesystems/jrm/curve-fix
Using new installer.py
2023-04-04 20:22:48 +01:00
Jedd Morgan f25f6cb16c Fixed some misc issues 2023-04-04 20:21:20 +01:00
Jedd Morgan 9e4e533ba8 Using new installer.py 2023-03-28 17:06:40 +01:00
Jedd Morgan 8db12ca9b9 Merge pull request #157 from specklesystems/jrm/curve-fix
Mesh area calc + minor cleanup
2023-03-28 16:47:59 +01:00
Jedd Morgan 366c864247 Mesh area calc + minor cleanup 2023-03-28 16:47:07 +01:00
Jedd Morgan 52136d3ef6 Merge pull request #155 from specklesystems/jrm/instances
Implemented support for new instances
2023-03-21 22:01:46 +00:00
Jedd Morgan fe764d7f0c removed old comment 2023-03-21 21:58:07 +00:00
Jedd Morgan 669dd67521 Added option to receive instances as linked duplicates 2023-03-21 21:45:53 +00:00
Jedd Morgan f74b2c37f0 Implemented support for new instances 2023-03-19 03:08:57 +00:00
Jedd Morgan ebb4e32fff Merge pull request #151 from specklesystems/jrm/curve-fix
Fixes many bugs with curves
2023-03-09 14:52:28 +00:00
Jedd Morgan 25903baf83 Set default mesh smoothing to True 2023-02-21 19:19:31 +00:00
Jedd Morgan cb6d6d7ad8 feat(converter): Added support for Ellipse and Circles 2023-02-21 19:17:14 +00:00
Jedd Morgan fd2687aa3c All non-bezier nurbs curves now work perfectly rhino->blend->rhino 2023-02-20 22:26:11 +00:00
Jedd Morgan f5c65068de feat(converter): Better curve support 2023-02-18 02:23:41 +00:00
Jedd Morgan 235b49d8c6 Merge pull request #150 from specklesystems/hotfix-authtoken
fix(auth): hotfix for serializing authTokens in blender file
2023-02-15 18:00:53 +00:00
Jedd Morgan a1ec137c67 minor cleanup 2023-02-15 16:40:49 +00:00
Jedd Morgan b95f621272 Aligned nurbs knot multiplicities with rhino curves 2023-02-15 16:37:48 +00:00
Jedd Morgan a1fcdad0e3 fix(auth): hotfix for serializing authTokens in blender file 2023-02-15 15:41:36 +00:00
Gergő Jedlicska 584e543964 makes sure to insert a path string into the sys path not a path object (#149) 2023-02-08 12:45:29 +01:00
Jedd Morgan ef20c5240c Merge pull request #148 from specklesystems/jrm/converter/fix
Custom Properties overflow fix
2023-02-05 17:20:17 +00:00
Jedd Morgan 9fe12a018a fix(ToNative): fixed issue with attaching custom properties that exceed int32 max value 2023-02-05 15:30:55 +00:00
JR-Morgan 8411c01f1b Merge branch 'main' of https://github.com/specklesystems/speckle-blender 2023-02-02 15:06:25 +00:00
JR-Morgan 63b82a30cb bumped specklepy 2.12.0 2023-02-02 15:05:59 +00:00
Jedd Morgan 0b6e39cf38 Merge pull request #147 from specklesystems/jrm/converter/multi-mesh
Fixed misalgined uv error message
2023-02-02 13:56:21 +00:00
JR-Morgan b7efcec517 uv experimentation 2023-02-02 13:45:57 +00:00
Jedd Morgan 0ab0096aac Merge pull request #142 from specklesystems/jrm/converter/multi-mesh
Jrm/converter/multi mesh
2023-02-01 14:00:11 +00:00
JR-Morgan cbd8fc99bb Merge remote-tracking branch 'origin' into jrm/converter/multi-mesh 2023-02-01 14:00:00 +00:00
JR-Morgan 2a9287e762 final polish 2023-02-01 13:52:20 +00:00
Jedd Morgan 98c70f237c Merge pull request #146 from specklesystems/gergo/alwaysInstall
fix(installer): make sure to always install specklepy dependencies
2023-02-01 13:10:22 +00:00
Gergő Jedlicska 048047cf05 fix(installer): make sure to always install specklepy dependencies
Co-authored-by: Jedd Morgan <45512892+JR-Morgan@users.noreply.github.com>
2023-01-31 10:57:05 +02:00
Jedd Morgan 6118215cae Experimentation with receive modes 2023-01-26 14:58:53 +00:00
Jedd Morgan 8e06432fe6 relock poetry 2023-01-23 23:50:42 +00:00
Jedd Morgan cb8620ff8a Merge branch 'main' into jrm/converter/multi-mesh
Signed-off-by: Jedd Morgan <45512892+JR-Morgan@users.noreply.github.com>
2023-01-23 23:49:12 +00:00
Jedd Morgan e0eddea8ab Fixed regression of null elements 2023-01-23 19:59:49 +00:00
Jedd Morgan 8497d0c195 Merge pull request #144 from specklesystems/jrm/ifc/fix
fixed IFC commits with empty lists
2023-01-20 17:00:04 +00:00
Jedd Morgan 1ce394c08f fixed issue with IFC commits with empty lists 2023-01-20 15:31:44 +00:00
Jedd Morgan aabbf87dda fix(ci): signing cert for iss 2023-01-16 17:12:39 +00:00
Jedd Morgan 7e31787d37 Updated config.yml 2023-01-16 16:29:13 +00:00
Jedd Morgan f1259587fd Bumped speckle py to 2.11.4 2023-01-16 15:00:10 +00:00
Jedd Morgan 7f913e3af0 Merge pull request #141 from specklesystems/gergo/ciContextUpdate
clone ci tools with ssh, use circleci context for env vars
2023-01-16 13:42:24 +00:00
Jedd Morgan 5433c34a4d Merge pull request #140 from specklesystems/jrm/manual-installer
Manual Installer
2023-01-16 13:39:50 +00:00
Alan Rynne 2c52e93660 ci: Updated github actions to use new actions repo 2023-01-09 20:41:22 +01:00
Gergő Jedlicska 3c3b24cf98 use innosetup context in the windows build 2023-01-06 14:01:56 +01:00
Gergő Jedlicska 3012b0ebcb clone ci tools with ssh, use circleci context for env vars 2023-01-06 12:17:19 +01:00
Jedd Morgan 35b96eaa4e feat(converter): meshes with multiple materials ToSpeckle 2022-12-19 13:42:35 +00:00
Jedd Morgan c229bb2414 materials 2022-12-15 18:04:31 +00:00
Jedd Morgan 318bd086c0 feat(converter): convert displayValues ToNative as a single mesh 2022-12-15 16:00:44 +00:00
Jedd Morgan e3eb29daa4 fiX(ci): fixed mistake in manual installer ci 2022-12-15 14:17:29 +00:00
Jedd Morgan 3359c8f275 feat(ci): Added manual instaler deploy 2022-12-14 17:01:45 +00:00
Gergő Jedlicska cfc5007d00 update tag fiter for numbered alpha and beta 2022-12-14 11:20:00 +01:00
Jedd Morgan 0a630457be Jrm/accounts reload (#138)
* add new installer mechanism

* ci(circleci): add new package connector step

* ci(circleci): install dependencies before packaging

* chore(deps): remove local specklepy dep

* chore(deps): relock again

* feat(connector): Added reload operation to user account selection

* Zero restart installation

Co-authored-by: Gergő Jedlicska <gergo@jedlicska.com>
2022-12-14 11:09:57 +01:00
Gergő Jedlicska fa124c2312 gergo/newInstaller (#136)
* add new installer mechanism

* ci(circleci): add new package connector step

* ci(circleci): install dependencies before packaging

* chore(deps): remove local specklepy dep

* chore(deps): relock again

* ci(circleci): rewrite full ci WIP

* fix(installer): ensure pip call was commented outy

* ci(circleci): updates

* ci(circleci): fix naming

* ci(circleci): update filters

* ci(circleci): update semver location

* ci(circleci): update dependency graph

* getting the specific installer branch

* get proper ci tools

* force remove new lines from installer tags

* fix installer patch pathing

* properly saving packaged zip

* add python to dotnet installer

* add zip again

* only zip the published isntaller

* publish only usefull stuff

* ci(circleci): update details in deploy

* ci(circleci): fix filters

* ci(cirleci): fix mac  build triggers

* make sure semver is set

* ci(circleci): fix env var restore in deploy

* ci(circleci): fix missing semver in files

* ci(circleci): wtf is microsoft doing with env vars in net6 containers?

* ci(circleci): fix persist to workspace pathing

* fix zipping dir

* mkdir for the zip folder

* mkdir -p

* force copy

* copy 101

* mkdir 101

* top level cp operation

* pipe the zip

* no CD

* only parameter

* build for osx 13 for arm

* moving back to a mac build machine

* fixc arm mac runtime

* remove old linux installer build

* allow alpha numbered version

* remove ci tools branch
2022-12-14 10:55:44 +01:00
Jedd Morgan 977994e141 feat(converter): initial refactor of mesh traversal 2022-12-13 19:02:43 +00:00
Jedd Morgan d9cbc80ee7 Zero restart installation 2022-12-09 15:00:44 +00:00
Jedd Morgan 45038fad79 feat(connector): Added reload operation to user account selection 2022-12-09 03:26:21 +00:00
Gergő Jedlicska cda621d735 chore(deps): relock again 2022-12-07 14:46:09 +01:00
Gergő Jedlicska 2d052d1379 chore(deps): remove local specklepy dep 2022-12-07 14:44:20 +01:00
Gergő Jedlicska 46ce2bc0df ci(circleci): install dependencies before packaging 2022-12-07 14:38:06 +01:00
Gergő Jedlicska 80e2216aa0 ci(circleci): add new package connector step 2022-12-07 14:32:48 +01:00
Gergő Jedlicska 10d69aa44b add new installer mechanism 2022-12-07 13:52:22 +01:00
Jedd Morgan b1481bd259 Merge pull request #135 from specklesystems/jrm/2.10-fixes
Jrm/2.10 fixes
2022-11-30 20:40:01 +00:00
Jedd Morgan e19fc9ef4e fix(deployment): potential fix for mac releases 2022-11-30 20:38:43 +00:00
Jedd Morgan 448eb856b2 Bumped dependencies 2022-11-30 20:36:07 +00:00
Jedd Morgan 06a7d416c7 Merge pull request #134 from specklesystems/jrm/2.10-fixes
fix(converter): fixed issue with meshes with very high number of vert…
2022-11-30 20:32:42 +00:00
Jedd Morgan 0753436899 fix(converter): fixed issue with meshes with very high number of verts taking an eternity to send
Changed from using sum to extend list functions
2022-11-29 23:54:28 +00:00
Jedd Morgan 40e0a49b11 Merge pull request #130 from specklesystems/jrm/clean-mesh-fix
fix(receive): Fixed issue with Clean Mesh creating duplicated collection
2022-11-17 16:05:56 +00:00
JR-Morgan b17b9c9de4 fix(receive): Fixed issue with Clean Mesh creating duplicated collection 2022-11-17 14:45:17 +00:00
Jedd Morgan f035111ffa Merge pull request #128 from specklesystems/fix-py39
Fixed issue with connector failing to load on Blender 2.93
2022-11-11 12:09:55 +00:00
JR-Morgan fa073f754f Merge remote-tracking branch 'origin' into fix-py39 2022-11-11 02:23:50 +00:00
Jedd Morgan 83919375f9 Merge pull request #129 from specklesystems/jrm/ci/macos
MacOS CI
2022-11-11 02:22:49 +00:00
JR-Morgan defb11bc89 fix: Fixed issue with connector on Blender 2.93 failing due to accidental use of unsupported python features 2022-11-11 02:13:19 +00:00
JR-Morgan 548b3ad352 CI fix for macos slugs 2022-10-24 21:48:25 +01:00
JR-Morgan d2deecf099 added deploy steps for macos 2022-10-24 21:27:38 +01:00
JR-Morgan fbda0110cd fix(connector): 2.9.0 update 2022-10-11 18:43:36 +01:00
Jedd Morgan ba9ad9ac07 Merge pull request #118 from specklesystems/gergo/workflowMetrics
add workflow tracking to receive operations
2022-10-11 17:56:40 +01:00
Jedd Morgan d13db2b44a Merge pull request #117 from specklesystems/jrm/receive-script-update
Jrm/receive script update
2022-10-11 17:54:33 +01:00
JR-Morgan 2c127c85f3 feat: Updated recieve scripts to use context rather than scene 2022-10-07 16:03:10 +01:00
Gergő Jedlicska cc099c0ff1 add workflow tracking to receive operations 2022-10-07 16:08:27 +02:00
JR-Morgan 448ab70c3b Merge remote-tracking branch 'origin/jrm/clean-mesh' into jrm/receive-script-update 2022-10-04 18:28:57 +01:00
JR-Morgan 020eba2727 Merge remote-tracking branch 'origin/main' into jrm/receive-script-update 2022-10-04 17:53:01 +01:00
JR-Morgan d8117e2c30 feat(converter): Updated Recieve Script callback functions to allow for access to Base object, and for callback on ToNative completion 2022-10-04 17:50:57 +01:00
Matteo Cominetti 57a6d88e6b ci 2022-09-27 10:27:50 +01:00
Matteo Cominetti 74cb3e5f85 ci: change shell 2022-09-27 10:15:58 +01:00
Matteo Cominetti d0aeecc863 Update README.md 2022-09-27 10:08:55 +01:00
Matteo Cominetti 2803308c5e Update README.md 2022-09-27 10:08:06 +01:00
Matteo Cominetti 326f04f67d ci: adds signtool param 2022-09-27 10:07:01 +01:00
JR-Morgan 68972ba8f9 Merge remote-tracking branch 'origin' into jrm/clean-mesh 2022-09-23 13:58:07 +01:00
Alan Rynne 73a028b56f Merge pull request #112 from specklesystems/ci/force-run
ci: Activated both mac runs on build
2022-09-23 14:56:02 +02:00
Alan Rynne 33890ef0ee fix: went too far 2022-09-23 14:28:51 +02:00
Alan Rynne 53fe676ab6 ci: Prettified steps 2022-09-23 14:27:50 +02:00
Alan Rynne f027c7eca4 ci: Activated both mac runs on build 2022-09-23 14:11:19 +02:00
JR-Morgan ea2b6dfb0e feat(connector): Added clean mesh option to recieve geometry with limited disolve + merge verts 2022-09-23 12:36:55 +01:00
JR-Morgan 83610cec38 Added type hinting to codebase 2022-09-21 22:22:07 +01:00
izzy lyseggen 2ed0685b10 fix(ci): soz just me clowning w regex (#108) 2022-08-26 12:09:51 +01:00
izzy lyseggen 877e616188 ci: temp remove mac steps and clean tags (#107) 2022-08-26 12:04:14 +01:00
izzy lyseggen 2e3e258a9a ci: deploy to manager 2 (#106)
* ci: add manager 2 feed

* ci: update manager2 requirements
2022-08-26 11:47:50 +01:00
izzy lyseggen 1e1c790eb4 ci: blender mac pt 2 electric boogaloo (#100)
* ci: build mac installers for both intel & arm

* ci: fix conditional logic?

* ci: final cleanup
2022-06-29 17:04:49 +01:00
izzy lyseggen 2148fe8dee ci: fix deploy job names 2022-06-29 15:07:47 +01:00
izzy lyseggen 41c87a8661 ci: clean up config and add blender 3.2 (#99)
* chore: remove unused stuff

* ci: split out pip install win job

* ci: fix modules path

* ci: try reusable command instead

* ci: fix shell specify syntax

* ci: try only mior ver for upgrade py

* ci: specify full py version

* ci: separate mac and build w tags

* ci: fix branch filter

* ci: rename deploy jobs
2022-06-29 15:05:40 +01:00
izzy lyseggen 96c9add526 ci: update version patch script 2022-06-21 16:20:25 +01:00
izzy lyseggen f425316e60 fix(converter): pass brep materials to displayvals (#96)
* chore: bump py version to >=3.8

no need for old versions, and gives us some nice syntax

* refactor(ui): remove some superficial units stuff

* fix(converter): pass brep materials to displayvals

this only passes the `renderMaterial` and it assumes all items in the brep's `displayValue` are meshes
2022-06-17 12:40:52 +01:00
izzy lyseggen abf363894e fix(ci): relax regex on the script 2022-06-15 14:46:44 +01:00
izzy lyseggen 8fc8b97b7a fix(ci): another patching fix oop 2022-06-15 14:38:04 +01:00
izzylys 64b4175585 fix(ci): patch version script 2022-06-15 12:02:56 +01:00
izzy lyseggen 4eefda3305 ci: tag deploy trigger 2022-06-15 11:47:04 +01:00
izzy lyseggen f7275140d5 ci: initial mac install ci (#95)
* ci: initial mac install ci

* ci: blender 3.1 mac support

* ci: update brew

so this worked on ssh let's see if it's ok now o.o

* ci(mac): retry py3.10 symlink

* ci: fix installer paths

* ci: tags and patch ver

* ci: mac syntax for python3! argh
2022-06-15 11:43:41 +01:00
izzy lyseggen 17a36f4fc2 ci: fix version tags again oops 2022-04-22 16:34:05 +01:00
izzy lyseggen ba931e8205 fix(installer/metrics): updates and cleanup (#92)
* ci: use semver tag instead of full circle tag

* chore: update deps

* chore: update specklepy

* feat(metrics): add blender version
2022-04-22 11:58:47 +01:00
izzy lyseggen 572925cfbb feat(convert): temps custom props patch (#89)
* chore: bump specklepy and fix deps

* feat(convert): wip attached props

* feat(convert): wip custom props
2022-04-04 12:10:57 +01:00
izzy lyseggen 03f94d6371 fix(convert): breps and dates (#87) 2022-03-28 15:03:27 +01:00
izzy lyseggen c220337aec chore: upgrade specklepy and add Blender v3.1 to ci (#86)
* chore(specklepy): upgrade to 2.6.3

* ci: add py 3.10 for blender 3.1
2022-03-28 11:47:56 +01:00
izzy lyseggen 1b67304cfc fix(converter): skip if not Pincipled BSDF (#85) 2022-03-09 11:07:35 +00:00
izzy lyseggen 25a1ec1cd1 ci: fix py 3.9 package install (#83)
* ci: test upgrade py

* ci: how about now?

* ci: we did it! 💃
2022-02-23 16:42:01 +00:00
izzy lyseggen 8296e48c28 Merge pull request #81 from specklesystems/izzy/displayvals
fix(convert): new `displayValue` to native & specklepy update
2022-02-23 11:56:15 +00:00
izzy lyseggen ca81ac6fd6 feat(convert): list displayvals to native 2022-02-23 11:50:33 +00:00
izzy lyseggen 88212b94b6 fix(users): none commit bug 2022-02-23 11:50:21 +00:00
izzy lyseggen 3375a04007 chore: update specklepy 2022-02-23 11:06:56 +00:00
91 changed files with 9620 additions and 4958 deletions
+9 -134
View File
@@ -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
-78
View File
@@ -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 }}
-50
View File
@@ -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
+31
View File
@@ -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
+96
View File
@@ -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
View File
@@ -10,4 +10,8 @@ __pycache__/
# dev
.venv
Installers/
modules/
modules/
.tool-versions
requirements.txt
SEMVER
dui3/
+21
View File
@@ -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
+63 -34
View File
@@ -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&amp;style=flat-square&amp;logo=discourse&amp;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&amp;logo=read-the-docs&amp;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&amp;style=flat-square&amp;logo=discourse&amp;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&amp;logo=read-the-docs&amp;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&amp;circle-token=76eabd350ea243575cbb258b746ed3f471f7ac29" alt="Speckle-Next"></a> </p>
# About Speckle
@@ -25,58 +25,90 @@ What is Speckle? Check our ![YouTube Video Views](https://img.shields.io/youtube
- **GraphQL API:** get what you need anywhere you want it
- **Webhooks:** the base for a automation and next-gen pipelines
- **Built for developers:** we are building Speckle with developers in mind and got tools for every stack
- **Built for the AEC industry:** Speckle connectors are plugins for the most common software used in the industry such as Revit, Rhino, Grasshopper, AutoCAD, Civil 3D, Excel, Unreal Engine, Unity, QGIS, Blender and more!
- **Built for the AEC industry:** Speckle connectors are plugins for the most common software used in the industry such as Revit, Rhino, Grasshopper, AutoCAD, Civil 3D, Blender and more!
### Try Speckle now!
Give Speckle a try in no time by:
- [![speckle XYZ](https://img.shields.io/badge/https://-speckle.xyz-0069ff?style=flat-square&logo=hackthebox&logoColor=white)](https://speckle.xyz) ⇒ creating an account at our public server
- [![create a droplet](https://img.shields.io/badge/Create%20a%20Droplet-0069ff?style=flat-square&logo=digitalocean&logoColor=white)](https://marketplace.digitalocean.com/apps/speckle-server?refcode=947a2b5d7dc1) ⇒ deploying an instance in 1 click
- [![speckle XYZ](https://img.shields.io/badge/https://-app.speckle.systems-0069ff?style=flat-square&logo=hackthebox&logoColor=white)](https://app.speckle.systems) ⇒ creating an account at our public server
### Resources
- [![Community forum users](https://img.shields.io/badge/community-forum-green?style=for-the-badge&logo=discourse&logoColor=white)](https://speckle.community) for help, feature requests or just to hang with other speckle enthusiasts, check out our community forum!
- [![website](https://img.shields.io/badge/tutorials-speckle.systems-royalblue?style=for-the-badge&logo=youtube)](https://speckle.systems) our tutorials portal is full of resources to get you started using Speckle
- [![docs](https://img.shields.io/badge/docs-speckle.guide-orange?style=for-the-badge&logo=read-the-docs&logoColor=white)](https://speckle.guide/user/blender.html) reference on almost any end-user and developer functionality
- [![docs](https://img.shields.io/badge/docs-docs.speckle.systems-orange?style=for-the-badge&logo=read-the-docs&logoColor=white)](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
View File
@@ -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()
+66
View File
@@ -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"]
-2
View File
@@ -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))
-13
View File
@@ -1,13 +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))
-4
View File
@@ -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)
+1
View File
@@ -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!")
+34
View File
@@ -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
+106
View File
@@ -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

+48
View File
@@ -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
+53
View File
@@ -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 ("", "", "")
-27
View File
@@ -1,27 +0,0 @@
from bpy_speckle.convert.to_native import convert_to_native
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 = f"{name}.{key}"
subobject = convert_to_native(attr[key], name)
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)
subobject = convert_to_native(attr[key], name)
subobjects.append(subobject)
props = attr[key].get("properties", None)
if props:
subobjects.extend(get_speckle_subobjects(props, scale, name))
return subobjects
-389
View File
@@ -1,389 +0,0 @@
import math
from bpy_speckle.functions import get_scale_length, _report
import mathutils
import bpy, bmesh, bpy_types
from specklepy.objects.other import *
from specklepy.objects.geometry import *
from .util import (
add_blender_material,
add_custom_properties,
add_vertices,
add_faces,
add_colors,
add_uv_coords,
)
SUPPORTED_CURVES = (Line, Polyline, Curve, Arc, Polycurve)
CAN_CONVERT_TO_NATIVE = (
Mesh,
Brep,
*SUPPORTED_CURVES,
Transform,
BlockDefinition,
BlockInstance,
)
def can_convert_to_native(speckle_object):
if type(speckle_object) in CAN_CONVERT_TO_NATIVE:
return True
display = getattr(
speckle_object, "displayMesh", getattr(speckle_object, "displayValue", None)
)
if display:
return True
_report(f"Could not convert unsupported Speckle object: {speckle_object}")
return False
def convert_to_native(speckle_object, name=None):
speckle_type = type(speckle_object)
speckle_name = (
name
or getattr(speckle_object, "name", None)
or speckle_object.speckle_type + f" -- {speckle_object.id}"
)
if speckle_type not in CAN_CONVERT_TO_NATIVE:
display = getattr(
speckle_object, "displayMesh", getattr(speckle_object, "displayValue", None)
)
if not display:
_report(f"Could not convert unsupported Speckle object: {speckle_object}")
return
# add parent type here so we can use it as a blender custom prop
# not making it hidden, so it will get added on send as i think it might be helpful? can reconsider
if isinstance(display, list):
for item in display:
item.parent_speckle_type = speckle_object.speckle_type
convert_to_native(item)
else:
display.parent_speckle_type = speckle_object.speckle_type
return convert_to_native(display, speckle_name)
units = getattr(speckle_object, "units", None)
if units:
scale = get_scale_length(units) / bpy.context.scene.unit_settings.scale_length
try:
if speckle_type is Mesh:
obj_data = mesh_to_native(speckle_object, name=speckle_name, scale=scale)
elif speckle_type is Brep:
obj_data = brep_to_native(speckle_object, name=speckle_name, scale=scale)
elif speckle_type in SUPPORTED_CURVES:
obj_data = icurve_to_native(speckle_object, name=speckle_name, scale=scale)
elif speckle_type is Transform:
obj_data = transform_to_native(speckle_object, scale=scale)
elif speckle_type is BlockDefinition:
obj_data = block_def_to_native(speckle_object, scale=scale)
elif speckle_type is BlockInstance:
obj_data = block_instance_to_native(speckle_object, scale=scale)
else:
_report(f"Unsupported type {speckle_type}")
return None
except Exception as ex: # conversion error
_report(f"Error converting {speckle_object} \n{ex}")
return None
if speckle_name in bpy.data.objects.keys():
blender_object = bpy.data.objects[speckle_name]
blender_object.data = (
obj_data.data if isinstance(obj_data, bpy_types.Object) else obj_data
)
blender_object.matrix_world = (
blender_object.matrix_world
if speckle_type is BlockInstance
else mathutils.Matrix()
)
if hasattr(obj_data, "materials"):
blender_object.data.materials.clear()
else:
blender_object = (
obj_data
if isinstance(obj_data, bpy_types.Object)
else bpy.data.objects.new(speckle_name, obj_data)
)
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)
return blender_object
def brep_to_native(speckle_brep, name, scale=1.0):
display = getattr(
speckle_brep, "displayMesh", getattr(speckle_brep, "displayValue", None)
)
return mesh_to_native(display, name, scale) if display else None
def mesh_to_native(speckle_mesh, name, scale=1.0):
if name in bpy.data.meshes.keys():
blender_mesh = bpy.data.meshes[name]
else:
blender_mesh = bpy.data.meshes.new(name=name)
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 line_to_native(speckle_curve, blender_curve, scale):
line = blender_curve.splines.new("POLY")
line.points.add(1)
line.points[0].co = (
float(speckle_curve.start.x) * scale,
float(speckle_curve.start.y) * scale,
float(speckle_curve.start.z) * scale,
1,
)
if speckle_curve.end:
line.points[1].co = (
float(speckle_curve.end.x) * scale,
float(speckle_curve.end.y) * scale,
float(speckle_curve.end.z) * scale,
1,
)
return line
def polyline_to_native(scurve, bcurve, scale):
# value = find_key_case_insensitive(scurve, "value")
value = scurve.value
if value:
N = 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
def nurbs_to_native(scurve, bcurve, scale):
# points = find_key_case_insensitive(scurve, "points")
points = scurve.points
if points:
N = 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
def arc_to_native(rcurve, bcurve, scale):
# TODO: improve Blender representation of arc
plane = rcurve.plane
if not plane:
return
normal = mathutils.Vector([plane.normal.x, plane.normal.y, plane.normal.z])
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
def polycurve_to_native(scurve, bcurve, scale):
"""
Convert Polycurve object
"""
segments = scurve.segments
curves = []
for seg in segments:
speckle_type = type(seg)
if speckle_type in SUPPORTED_CURVES:
curves.append(icurve_to_native_spline(seg, bcurve, scale=scale))
else:
_report(f"Unsupported curve type: {speckle_type}")
return curves
def icurve_to_native_spline(speckle_curve, blender_curve, scale=1.0):
curve_type = type(speckle_curve)
if curve_type is Line:
return line_to_native(speckle_curve, blender_curve, scale)
if curve_type is Polyline:
return polyline_to_native(speckle_curve, blender_curve, scale)
if curve_type is Curve:
return nurbs_to_native(speckle_curve, blender_curve, scale)
if curve_type is Polycurve:
return polycurve_to_native(speckle_curve, blender_curve, scale)
if curve_type is Arc:
return arc_to_native(speckle_curve, blender_curve, scale)
def icurve_to_native(speckle_curve, name=None, scale=1.0):
curve_type = type(speckle_curve)
if curve_type not in SUPPORTED_CURVES:
_report(f"Unsupported curve type: {curve_type}")
return None
name = name or f"{curve_type} -- {speckle_curve.id}"
blender_curve = (
bpy.data.curves[name]
if name in bpy.data.curves.keys()
else bpy.data.curves.new(name, type="CURVE")
)
blender_curve.dimensions = "3D"
blender_curve.resolution_u = 12
icurve_to_native_spline(speckle_curve, blender_curve, scale)
return blender_curve
def transform_to_native(transform: Transform, scale=1.0):
mat = mathutils.Matrix(
[
transform.value[:4],
transform.value[4:8],
transform.value[8:12],
transform.value[12:16],
]
)
# scale the translation
for i in range(3):
mat[i][3] *= scale
return mat
def block_def_to_native(definition: BlockDefinition, scale=1.0):
native_def = bpy.data.collections.get(definition.name)
if native_def:
return native_def
native_def = bpy.data.collections.new(definition.name)
native_def["applicationId"] = definition.applicationId
for geo in definition.geometry:
b_obj = convert_to_native(geo)
if b_obj:
native_def.objects.link(
b_obj
if isinstance(b_obj, bpy_types.Object)
else bpy.data.objects.new(b_obj.name, b_obj)
)
return native_def
def block_instance_to_native(instance: BlockInstance, scale=1.0):
"""
Convert BlockInstance to native
"""
name = f"{getattr(instance, 'name', None) or instance.blockDefinition.name} -- {instance.id}"
native_def = block_def_to_native(instance.blockDefinition, scale)
native_instance = bpy.data.objects.new(name, None)
# hide the instance axes so they don't clutter the viewport
native_instance.empty_display_size = 0
native_instance.instance_collection = native_def
native_instance.instance_type = "COLLECTION"
native_instance.matrix_world = transform_to_native(instance.transform, scale)
return native_instance
-312
View File
@@ -1,312 +0,0 @@
import bpy
from specklepy.objects.geometry import Mesh, Curve, Interval, Box, Point, Polyline
from specklepy.objects.other import *
from bpy_speckle.functions import _report
from bpy_speckle.convert.util import (
get_blender_custom_properties,
make_knots,
to_argb_int,
)
UNITS = "m"
CAN_CONVERT_TO_SPECKLE = ("MESH", "CURVE", "EMPTY")
def convert_to_speckle(blender_object, scale, desgraph=None):
blender_type = blender_object.type
if blender_type not in CAN_CONVERT_TO_SPECKLE:
return
speckle_objects = []
speckle_material = material_to_speckle(blender_object)
if desgraph:
blender_object = blender_object.evaluated_get(desgraph)
converted = None
if blender_type == "MESH":
converted = mesh_to_speckle(blender_object, blender_object.data, scale)
elif blender_type == "CURVE":
converted = icurve_to_speckle(blender_object, blender_object.data, scale)
elif blender_type == "EMPTY":
converted = empty_to_speckle(blender_object, scale)
if not converted:
return None
if isinstance(converted, list):
speckle_objects.extend([c for c in converted if c != None])
else:
speckle_objects.append(converted)
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
if blender_type != "EMPTY":
so.properties["transform"] = transform_to_speckle(
blender_object.matrix_world
)
return speckle_objects
def mesh_to_speckle(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]
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 = make_knots(spline)
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 icurve_to_speckle(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 = mesh_to_speckle(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 ngons_to_speckle_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
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 transform_to_speckle(blender_transform, scale=1.0):
units = "m" if bpy.context.scene.unit_settings.system == "METRIC" else "ft"
value = [y for x in blender_transform for y in x]
# scale the translation
for i in (3, 7, 11):
value[i] *= scale
return Transform(value=value, units=units)
def block_def_to_speckle(blender_definition, scale=1.0):
geometry = []
for geo in blender_definition.objects:
geometry.extend(convert_to_speckle(geo, scale))
block_def = BlockDefinition(
units=UNITS,
name=blender_definition.name,
geometry=geometry,
basePoint=Point(units=UNITS),
)
blender_props = get_blender_custom_properties(blender_definition)
block_def.applicationId = blender_props.pop("applicationId", None)
return block_def
def block_instance_to_speckle(blender_instance, scale=1.0):
return BlockInstance(
blockDefinition=block_def_to_speckle(
blender_instance.instance_collection, scale
),
transform=transform_to_speckle(blender_instance.matrix_world),
name=blender_instance.name,
units=UNITS,
)
def empty_to_speckle(blender_object, scale=1.0):
# probably an instance collection (block) so let's try it
try:
geo = blender_object.instance_collection.objects.items()
return block_instance_to_speckle(blender_object, scale)
except AttributeError as err:
_report(
f"No instance collection found in empty. Skipping object {blender_object.name}"
)
return None
-290
View File
@@ -1,290 +0,0 @@
import base64
from typing import Tuple
import bpy, struct, idprop
from specklepy.serialization.base_object_serializer import BaseObjectSerializer
from bpy_speckle.functions import _report
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[:3]
diffuse_colour = [int(val * 255) for val in diffuse_colour]
return int.from_bytes(diffuse_colour, byteorder="big", signed=True)
def add_custom_properties(speckle_object, blender_object):
if blender_object is None:
return
serializer = BaseObjectSerializer()
blender_object["_speckle_type"] = type(speckle_object).__name__
app_id = getattr(speckle_object, "applicationId", None)
if app_id:
blender_object["applicationId"] = speckle_object.applicationId
for key in speckle_object.get_dynamic_member_names():
if isinstance(speckle_object[key], (int, str, float)):
blender_object[key] = speckle_object[key]
elif isinstance(speckle_object[key], (dict, list)):
blender_object[key] = serializer.traverse_value(speckle_object[key])
def add_blender_material(speckle_object, 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
speckle_mat = getattr(
speckle_object,
"renderMaterial",
getattr(speckle_object, "@renderMaterial", None),
)
if not speckle_mat:
return
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 add_vertices(speckle_mesh, blender_mesh, scale=1.0):
sverts = speckle_mesh.vertices
if sverts and len(sverts) > 0:
for i in range(0, len(sverts), 3):
blender_mesh.verts.new(
(
float(sverts[i]) * scale,
float(sverts[i + 1]) * scale,
float(sverts[i + 2]) * scale,
)
)
blender_mesh.verts.ensure_lookup_table()
def add_faces(speckle_mesh, blender_mesh, smooth=False):
sfaces = speckle_mesh.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 = blender_mesh.faces.new(
[blender_mesh.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 {speckle_mesh.id} \n{e}")
i += n
blender_mesh.faces.ensure_lookup_table()
blender_mesh.verts.index_update()
def add_colors(speckle_mesh, blender_mesh):
scolors = speckle_mesh.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(blender_mesh.verts):
color_layer = blender_mesh.loops.layers.color.new("Col")
for face in blender_mesh.faces:
for loop in face.loops:
loop[color_layer] = colors[loop.vert.index]
def add_uv_coords(speckle_mesh, blender_mesh):
if not hasattr(speckle_mesh, "properties"):
return
sprops = speckle_mesh.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 len(s_uvs) // 2 == len(blender_mesh.verts):
for i in range(0, len(s_uvs), 2):
uv.append((float(s_uvs[i]), float(s_uvs[i + 1])))
else:
_report(
f"Failed to match UV coordinates to vert data. Blender mesh verts: {len(blender_mesh.verts)}, Speckle UVs * 2: {len(s_uvs) * 2}"
)
# Make UVs
uv_layer = blender_mesh.loops.layers.uv.verify()
for f in blender_mesh.faces:
for l in f.loops:
luv = l[uv_layer]
luv.uv = uv[l.vert.index]
except:
_report("Failed to decode texture coordinates.")
raise
del speckle_mesh.properties[texKey]
ignored_keys = (
"speckle",
"_speckle_type",
"_speckle_name",
"_speckle_transform",
"_RNA_UI",
"transform",
"_units",
"_chunkable",
)
def get_blender_custom_properties(obj, max_depth=1000):
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
"""
Python implementation of Blender's NURBS curve generation for to Speckle conversion
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 make_knots(nu):
knots = [0.0] * (4 + 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)
makecyclicknots(knots, nu.point_count_u, nu.order_u)
else:
calc_knots(knots, nu.point_count_u, nu.order_u, flag)
return knots
def calc_knots(knots, point_count, order, flag):
pts_order = point_count + order
if flag == 1:
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:
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(pts_order):
knots[a] = a
def makecyclicknots(knots, point_count, order):
order2 = order - 1
if order > 2:
b = point_count + order2
for a in range(1, order2):
if knots[b] != knots[b - a]:
break
if a == order2:
knots[point_count + order - 2] += 1.0
b = order
c = point_count + order + order2
for a in range(point_count + order2, c):
knots[a] = knots[a - 1] + (knots[b] - knots[b - 1])
b -= 1
+3
View File
@@ -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,
)
+242
View File
@@ -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)
+226
View File
@@ -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
-72
View File
@@ -1,72 +0,0 @@
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)
-104
View File
@@ -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
+227
View File
@@ -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}!"
)
-63
View File
@@ -1,63 +0,0 @@
from .users import LoadUsers, LoadUserStreams
from .object import (
UpdateObject,
ResetObject,
DeleteObject,
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,
UploadNgonsAsPolylines,
SelectIfSameCustomProperty,
SelectIfHasCustomProperty,
]
)
operator_classes.extend(
[
ViewStreamDataApi,
DeleteStream,
SelectOrphanObjects,
UpdateGlobal,
AddStreamFromURL,
CreateStream,
OpenSpeckleGuide,
OpenSpeckleTutorials,
OpenSpeckleForum,
]
)
-63
View File
@@ -1,63 +0,0 @@
"""
Commit operators
"""
import bpy
from bpy.props import BoolProperty
from bpy_speckle.functions import _check_speckle_client_user_stream
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"}
branch = stream.branches[int(stream.branch)]
if len(branch.commits) < 1:
return {"CANCELLED"}
commit = branch.commits[int(branch.commit)]
deleted = client.commit.delete(stream_id=stream.id, commit_id=commit.id)
return {"FINISHED"}
-35
View File
@@ -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"}
-317
View File
@@ -1,317 +0,0 @@
"""
Object operators
"""
import bpy
from bpy.props import BoolProperty, EnumProperty
from bpy_speckle.convert.to_speckle import (
convert_to_speckle,
ngons_to_speckle_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 = convert_to_speckle(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):
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"}
new_objects = [
x
for x in res["resource"]["objects"]
if x["_id"] != active.speckle.object_id
]
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 = ngons_to_speckle_polylines(active, scale)
if sp is None:
return {"CANCELLED"}
placeholders = []
for polyline in sp:
res = client.objects.create([polyline])
if res is None:
_report(client.me)
continue
placeholders.extend(res)
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")
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"}
-812
View File
@@ -1,812 +0,0 @@
"""
Stream operators
"""
from itertools import chain
from typing import Dict
import bpy
from specklepy.api.models import Commit
import webbrowser
from bpy.props import (
StringProperty,
BoolProperty,
)
from bpy_speckle.convert.to_native import can_convert_to_native, convert_to_native
from bpy_speckle.convert.to_speckle import (
convert_to_speckle,
ngons_to_speckle_polylines,
)
from bpy_speckle.functions import (
_check_speckle_client_user_stream,
get_scale_length,
_report,
)
from bpy_speckle.convert import get_speckle_subobjects
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.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
if can_convert_to_native(base):
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 col:
col = create_collection(name)
try:
parent_col.children.link(col)
except:
_report(
f"Problem linking collection {col.name} to parent {parent_col.name}; skipping"
)
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 = [convert_to_native(base)]
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:
_report("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 = ngons_to_speckle_polylines(obj, scale)
else:
converted = convert_to_speckle(
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")
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"}
-133
View File
@@ -1,133 +0,0 @@
"""
User account operators
"""
import bpy
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_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"}
-23
View File
@@ -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,
]
-17
View File
@@ -1,17 +0,0 @@
"""
Addon properties
"""
import bpy
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")
-19
View File
@@ -1,19 +0,0 @@
"""
Collection properties
"""
import bpy
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="")
-18
View File
@@ -1,18 +0,0 @@
"""
Object properties
"""
import bpy
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="")
-134
View File
@@ -1,134 +0,0 @@
"""
Scene properties
"""
import bpy
from bpy.props import (
StringProperty,
BoolProperty,
FloatProperty,
CollectionProperty,
EnumProperty,
IntProperty,
PointerProperty,
)
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):
return [
("<none>", "<none>", "<none>"),
*[(t.name, t.name, t.name) for t in bpy.data.texts],
]
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,
)
-18
View File
@@ -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,
]
-35
View File
@@ -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")
-275
View File
@@ -1,275 +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.label(
text=f"{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.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")
-54
View File
@@ -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
+4
View File
@@ -0,0 +1,4 @@
#!/usr/bin/env pwsh
$ErrorActionPreference = "Stop"
uv pip compile pyproject.toml --output-file bpy_speckle/requirements.txt --generate-hashes
+4
View File
@@ -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
View File
@@ -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
-1236
View File
File diff suppressed because it is too large Load Diff
+13 -19
View File
@@ -1,22 +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.5.1"
[tool.poetry.dev-dependencies]
devtools = "^0.6.1"
numpy = "^1.20.2"
bpy = "^2.82.1"
bpy-build = "^2.1.0"
black = "^21.12b0"
pylint = "^2.12.2"
[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",
]
Generated
+1123
View File
File diff suppressed because it is too large Load Diff