Compare commits

...

168 Commits

Author SHA1 Message Date
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
izzy lyseggen c62538fc8f Merge pull request #78 from specklesystems/izzy/blocks
feat(convert): big refactor !! and blocks 🧊
2022-01-10 17:41:07 +00:00
izzylys fd83e80bbd refactor: final cleanup 2022-01-10 09:40:44 -08:00
izzylys 0a2c1f9f32 refactor(convert): remove unused methods 2022-01-10 09:26:22 -08:00
izzylys 2975737377 feat(convert): col fixes & can_convert_to_native 2022-01-10 09:18:16 -08:00
izzylys 72dea53eee feat(convert): block to speckle 2022-01-10 09:17:51 -08:00
izzylys a1aa267a30 fix(convert): custom props and cols fixes 2022-01-07 10:17:42 -08:00
izzylys df30b65fdb refactor: deleting code is my love language 2022-01-07 09:09:29 -08:00
izzylys 6176abb4a3 refactor(converter): more cleanup 2022-01-06 13:41:43 -08:00
izzylys 5c2d2d195f refactor(converter): to_speckle refactor!! 2022-01-06 11:42:46 -08:00
izzylys 7a784a4a8d fix(convert): unique names for all instances
add the id to the end of the name to ensure this
2022-01-04 13:09:58 -08:00
izzylys 2c8f2c96a9 feat(convert): BIG to native refactor + blocks 2022-01-04 12:42:14 -08:00
izzylys bc7400cb36 Merge branch 'izzy/blocks' of https://github.com/specklesystems/speckle-blender into izzy/blocks 2022-01-03 10:46:35 -08:00
izzy lyseggen 421c68a143 wip: block nonsense from other pc 2022-01-03 18:37:00 +00:00
izzy lyseggen 409adda784 Merge pull request #77 from specklesystems/izzy/nulls-fix
fix(convert): nulls on receive
2022-01-03 17:11:40 +00:00
izzylys f5f1c6a8d0 fix(convert): handle list display val 2022-01-03 17:09:57 +00:00
izzylys 1eca91a030 chore: bump specklepy 2022-01-03 17:09:29 +00:00
izzy lyseggen 6b872f11d9 Merge pull request #75 from specklesystems/izzy/add-parent-type
feat(convert): add parent type to display meshes
2021-12-09 10:59:28 +00:00
izzy lyseggen fd3742b800 feat(convert): add parent type to display meshes
makes it easier to work w geo from places like revit
2021-12-09 10:58:22 +00:00
izzy lyseggen 870174a969 Merge pull request #73 from specklesystems/ci/fix-install-platform
ci: fix install platform
2021-12-07 16:26:58 +00:00
izzy lyseggen 2c6dfcb340 ci: grab py version to ensure all is working 2021-12-07 16:22:42 +00:00
izzy lyseggen 4d3eacbe52 ci: no need to persist now i think 2021-12-07 16:05:51 +00:00
izzy lyseggen df7d631b54 ci: bump to 3.9.2 2021-12-07 16:05:23 +00:00
izzy lyseggen 07ceb07058 ci: try ths install again! 2021-12-07 16:01:16 +00:00
izzy lyseggen 112bc56ded ci: remove install specklepy step 2021-12-07 15:54:59 +00:00
izzy lyseggen 88e2744e93 ci: test in win exe 2021-12-07 15:53:52 +00:00
izzy lyseggen ad1c201c79 Merge pull request #72 from specklesystems/ci/install-changes
ci: preinstall specklepy
2021-12-07 14:54:26 +00:00
Gergő Jedlicska e18569f738 (ci) wait for install 2021-12-07 15:18:03 +01:00
Gergő Jedlicska f7e563f500 (ci) new glob 2021-12-07 15:15:13 +01:00
Gergő Jedlicska 7e56c21395 (ci) what is attached? 2021-12-07 15:01:47 +01:00
Gergő Jedlicska 30b701ee27 Merge branch 'ci/install-changes' of github.com:specklesystems/speckle-blender into ci/install-changes 2021-12-07 14:48:50 +01:00
Gergő Jedlicska 3cdbc09fc0 (ci) update job order 2021-12-07 14:48:43 +01:00
Gergő Jedlicska 656e41f03c (ci) update job order 2021-12-07 14:41:09 +01:00
Gergő Jedlicska 7da26e44b6 Update config.yml
try with glob pattern
2021-12-07 14:27:30 +01:00
izzy lyseggen 830ba842e0 ci: remove unecessary steps 2021-12-07 13:01:31 +00:00
izzy lyseggen d38f990e75 ci: fix modules path 2021-12-07 12:42:51 +00:00
izzy lyseggen c01836806c ci: persist to workspace 2021-12-07 12:38:20 +00:00
izzy lyseggen bf416dd228 ci: remove pip upgrade step? 2021-12-07 12:28:48 +00:00
izzy lyseggen a2e2c489b4 ci: change branch filter 2021-12-07 12:17:12 +00:00
izzy lyseggen c40c2d7955 ci: add specklepy install step 2021-12-07 12:08:37 +00:00
izzy lyseggen b45aa0d6c1 chore: return specklepy V in patch script w no arg
should allow us to grab the version for manual specklepy install in the ci env
2021-12-07 12:07:27 +00:00
izzy lyseggen 667616b1fb Merge pull request #68 from specklesystems/izzy/bump-deps
chore: bump to specklepy 2.4.2
2021-11-30 11:56:52 +00:00
izzy lyseggen 87cb69090a chore: bump to specklepy 2.4.2
attempting to resolve Plugin activation error due to `typing_extensions` #67
2021-11-30 11:56:10 +00:00
izzy lyseggen 4a8a0ca6c7 Merge pull request #66 from specklesystems/jm/ngons
Ngon Support
2021-11-29 10:54:20 +00:00
izzy lyseggen 3c0d1eba65 style: remove old comment re face error
(was resolved in #35)
2021-11-29 10:52:02 +00:00
JR-Morgan 14aaf4f064 N-gon ToSpeckle should now work! 2021-11-25 16:54:35 +00:00
JR-Morgan 10bf3e3af5 Experimenting with n-gon sending 2021-11-25 13:55:18 +00:00
izzy lyseggen 936d573510 Merge pull request #63 from specklesystems/izzy/read-receipts
feat(streams): add read reciepts + update to specklepy 2.4
2021-11-15 11:17:33 +00:00
izzy lyseggen 1f886202ec feat(streams): add commit received call 2021-11-15 11:14:51 +00:00
izzy lyseggen 479e5b5b98 fix(streams): update stream transport constructor 2021-11-15 11:14:29 +00:00
izzy lyseggen bed1ecf2cc chore: update specklepy to 2.4 2021-11-15 11:02:35 +00:00
izzy lyseggen a6cbce977f ci: remove pip install specklepy step 2021-10-14 17:07:08 +01:00
40 changed files with 3435 additions and 2392 deletions
+274 -90
View File
@@ -1,123 +1,307 @@
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
win: circleci/windows@5.0.0
jobs:
build-connector: # Reusable job for basic connectors
package-connector:
docker:
- image: cimg/python:3.11.0
steps:
- checkout
- run:
name: Setup SEMVER value
command: |
SEMVER=$(if [ "${CIRCLE_TAG}" ]; then echo $CIRCLE_TAG; else echo "0.0.0"; fi;)
echo $SEMVER > ./SEMVER
python3 patch_version.py $SEMVER
- run:
name: install dependencies
command: poetry install --only main
- run:
name: export package dependencies
command: ./export_dependencies.sh
- persist_to_workspace:
root: ./
paths:
- bpy_speckle
- patch_installer.py
- SEMVER
build-connector-zip:
docker:
- image: cimg/python:3.11.0
steps:
- attach_workspace:
at: ./
- run: &restore_semver
name: Restore Semver
command: SEMVER=$(cat ./SEMVER) && echo $SEMVER
- run:
name: Package to Zip
command: zip -r bpy_speckle.zip bpy_speckle
- persist_to_workspace:
root: ./
paths:
- bpy_speckle.zip
get-ci-tools: # Clones our ci tools and persists them to the workspace
docker:
- image: cimg/base:2021.01
steps:
- add_ssh_keys:
fingerprints:
- "77:64:03:93:c5:f3:1d:a6:fd:bd:fb:d1:05:56:ca:e9"
- run:
name: I know Github as a host
command: |
mkdir ~/.ssh
touch ~/.ssh/known_hosts
ssh-keyscan github.com >> ~/.ssh/known_hosts
- run:
name: Clone
command: git clone git@github.com:specklesystems/speckle-sharp-ci-tools.git speckle-sharp-ci-tools
- run:
command: cd speckle-sharp-ci-tools
- persist_to_workspace:
root: ./
paths:
- speckle-sharp-ci-tools
build-installer-win:
executor:
name: win/default # comes with python 3.7.3
name: win/default
shell: cmd.exe
steps:
- attach_workspace:
at: ./
- run:
name: Patch installer
shell: powershell.exe
command: python patch_installer.py (Get-Content -Raw SEMVER)
- run:
name: Create Innosetup signing cert
shell: powershell.exe
command: |
echo $env:PFX_B64 > "speckle-sharp-ci-tools\SignTool\AEC Systems Ltd.txt"
certutil -decode "speckle-sharp-ci-tools\SignTool\AEC Systems Ltd.txt" "speckle-sharp-ci-tools\SignTool\AEC Systems Ltd.pfx"
- run:
name: Installer
shell: cmd.exe #does not work in powershell
command: speckle-sharp-ci-tools\InnoSetup\ISCC.exe speckle-sharp-ci-tools\blender.iss /Sbyparam=$p
- persist_to_workspace:
root: ./
paths:
- speckle-sharp-ci-tools/Installers/blender/blender-*.exe
build-installer-mac:
macos:
xcode: 12.5.1
parameters:
runtime:
type: string
slug:
type: string
default: ""
installer_path:
type: string
default: speckle-sharp-ci-tools/Mac/SpeckleBlenderInstall
steps:
- checkout
- attach_workspace:
at: ./
- run:
name: upgrade pip and install specklepy
command: python -m pip install --upgrade pip & python -m pip install --target=.\modules specklepy
name: Install mono
command: |
HOMEBREW_NO_AUTO_UPDATE=1 brew install mono
# Compress build files
- 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
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
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
- persist_to_workspace:
root: ./
paths:
- speckle-sharp-ci-tools
name: Install dotnet
command: curl -sSL https://dot.net/v1/dotnet-install.sh | bash /dev/stdin
- run: *restore_semver
- run:
name: Copy connector files to installer
command: |
mkdir -p <<parameters.installer_path >>/.installationFiles/
cp bpy_speckle.zip << parameters.installer_path >>/.installationFiles
- run:
name: Build Mac installer
command: ~/.dotnet/dotnet publish << parameters.installer_path >>/SpeckleBlenderInstall.sln -r << parameters.runtime >> -c Release
- run:
name: Zip installer
command: |
SEMVER=$(cat ./SEMVER)
echo $SEMVER
mkdir -p speckle-sharp-ci-tools/Installers/blender
(cd <<parameters.installer_path>>/bin/Release/net6.0/<< parameters.runtime >>/publish/ && zip -r - ./) > << parameters.slug >>-${SEMVER}.zip
cp << parameters.slug >>-${SEMVER}.zip speckle-sharp-ci-tools/Installers/blender/
deploy: # Uploads all installers found to S3
- persist_to_workspace:
root: ./
paths:
- speckle-sharp-ci-tools/Installers/blender/<< parameters.slug >>*.zip
build-installer-manual:
docker:
- image: cimg/base:2021.01
parameters:
slug:
type: string
default: bpy_speckle
steps:
- attach_workspace:
at: ./
- run: *restore_semver
- 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/
name: Copy zip with semver
command: |
SEMVER=$(cat ./SEMVER)
mkdir -p speckle-sharp-ci-tools/Installers/blender
cp bpy_speckle.zip speckle-sharp-ci-tools/Installers/blender/<< parameters.slug >>-${SEMVER}.zip
- persist_to_workspace:
root: ./
paths:
- speckle-sharp-ci-tools/Installers/blender/<< parameters.slug >>*.zip
deploy-connector:
docker:
- image: mcr.microsoft.com/dotnet/sdk:6.0
parameters:
file_slug:
type: string
os:
type: string
extension:
type: string
arch:
type: string
default: Any
steps:
- checkout
- attach_workspace:
at: ./
- run:
name: Install Manager Feed CLI
command: dotnet tool install --global Speckle.Manager.Feed
- run: *restore_semver
- run:
name: Upload new version
# this is where the installer gets the semver baked into the file name
command: |
SEMVER=$(cat ./SEMVER)
echo $SEMVER
/root/.dotnet/tools/Speckle.Manager.Feed deploy \
-s blender \
-v ${SEMVER} \
-u https://releases.speckle.dev/installers/blender/<< parameters.file_slug >>-${SEMVER}.<< parameters.extension >> \
-o << parameters.os >> \
-a << parameters.arch >> \
-f speckle-sharp-ci-tools/Installers/blender/<< parameters.file_slug >>-${SEMVER}.<< parameters.extension >>
workflows:
build:
build: # build the installers, but don't persist to workspace for deployment
jobs:
- get-ci-tools:
filters:
branches:
only:
- main
- /ci\/.*/
- package-connector:
filters: &build_filters
tags:
only: /([0-9]+)\.([0-9]+)\.([0-9]+)(?:-\w+)?(?:\.[0-9]+)?/
- build-connector-zip:
requires:
- package-connector
filters: *build_filters
- build-connector:
slug: blender
requires:
- get-ci-tools
filters:
branches:
only:
- main
- /ci\/.*/
deploy:
jobs:
- get-ci-tools:
filters:
tags:
only: /[0-9]+(\.[0-9]+)*/
filters: *build_filters
- build-installer-win:
context: innosetup
name: Windows Installer Build
requires:
- package-connector
- get-ci-tools
filters: *build_filters
- deploy-connector:
context: do-spaces-speckle-releases
name: deploy-windows
file_slug: blender
os: WIN
arch: Any
extension: exe
requires:
- Manual Installer Build
- Windows Installer Build
- Mac Intel Build
- Mac ARM Build
filters: &deploy_filters
branches:
ignore: /.*/ # For testing only! /ci\/.*/
- build-connector:
slug: blender
ignore: /.*/
tags:
only: /([0-9]+)\.([0-9]+)\.([0-9]+)(?:-\w+)?(?:\.[0-9]+)?/
- build-installer-mac:
name: Mac ARM Build
slug: blender-mac-arm
runtime: osx-arm64
requires:
- get-ci-tools
filters:
tags:
only: /[0-9]+(\.[0-9]+)*/
branches:
ignore: /.*/ # For testing only! /ci\/.*/
- deploy:
- build-connector-zip
filters: *build_filters
- deploy-connector:
context: do-spaces-speckle-releases
name: deploy-mac-arm
file_slug: blender-mac-arm
os: OSX
arch: Arm
extension: zip
requires:
- Manual Installer Build
- Windows Installer Build
- Mac Intel Build
- Mac ARM Build
filters: *deploy_filters
- build-installer-mac:
name: Mac Intel Build
slug: blender-mac-intel
runtime: osx-x64
requires:
- get-ci-tools
- build-connector
filters:
tags:
only: /[0-9]+(\.[0-9]+)*/
branches:
ignore: /.*/ # For testing only! /ci\/.*/
- build-connector-zip
filters: *build_filters
- deploy-connector:
context: do-spaces-speckle-releases
name: deploy-mac-intel
file_slug: blender-mac-intel
os: OSX
arch: Intel
extension: zip
requires:
- Manual Installer Build
- Windows Installer Build
- Mac Intel Build
- Mac ARM Build
filters: *deploy_filters
- build-installer-manual:
name: Manual Installer Build
requires:
- get-ci-tools
- build-connector-zip
filters: *build_filters
- deploy-connector:
context: do-spaces-speckle-releases
name: deploy-manual
file_slug: bpy_speckle
os: Any
arch: Any
extension: zip
requires:
- Manual Installer Build
- Windows Installer Build
- Mac Intel Build
- Mac ARM Build
filters: *deploy_filters
+4 -70
View File
@@ -6,73 +6,7 @@ on:
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 }}
uses: specklesystems/github-actions/.github/workflows/project-add-issue.yml@main
secrets: inherit
with:
issue-id: ${{ github.event.issue.node_id }}
+4 -42
View File
@@ -6,45 +6,7 @@ on:
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
uses: specklesystems/github-actions/.github/workflows/project-add-issue.yml@main
secrets: inherit
with:
issue-id: ${{ github.event.issue.node_id }}
+5 -1
View File
@@ -5,8 +5,12 @@ __pycache__/
# editor
.vscode
.idea
# dev
.venv
Installers/
modules/
modules/
.tool-versions
requirements.txt
SEMVER
+2 -2
View File
@@ -3,7 +3,7 @@
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/>
@@ -91,4 +91,4 @@ The Speckle Community hangs out on [the forum](https://discourse.speckle.works),
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)).
Thanks to [Tom Svilans](http://tomsvilans.com) ([Github](https://github.com/tsvilans)) for the original v1 contribution!
+17 -46
View File
@@ -1,25 +1,15 @@
# MIT License
import bpy
from bpy_speckle.installer import ensure_dependencies
# Copyright (c) 2018-2021 Tom Svilans
ensure_dependencies(f"Blender {bpy.app.version[0]}.{bpy.app.version[1]}")
# 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.
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
bl_info = {
"name": "SpeckleBlender 2.0",
@@ -33,45 +23,26 @@ bl_info = {
"category": "Scene",
}
import bpy
"""
Import PySpeckle and attempt install if not found
"""
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
# from .install_dependencies import install_dependencies
# install_dependencies()
"""
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
"""
@persistent
def load_handler(dummy):
bpy.ops.speckle.users_load()
pass
# Calling users_load is an expensive operation, one that force users to wait a good 10s every time blender loads.
# Until we can do this non-blocking, we will make the user hit the refresh button each time.
#bpy.ops.speckle.users_load()
# Instead, we shall just reset the user selection to an uninitiailised state
bpy.ops.speckle.users_reset()
"""
Permanent handle on callbacks
@@ -95,7 +66,7 @@ def register():
for cls in speckle_classes:
register_class(cls)
metrics.set_host_app("Blender")
metrics.set_host_app("blender", f"blender {bpy.app.version_string}")
"""
Register all new properties
-1
View File
@@ -11,4 +11,3 @@ def scb_on_mesh_edit(context):
edit_obj = bpy.context.edit_object
if edit_obj is not None and edit_obj.is_updated_data is True:
print("Mesh edited: {}".format(edit_obj))
# print('>>> Update')
+4 -1
View File
@@ -1,4 +1,7 @@
"""
Permanent handle on all user clients
"""
speckle_clients = []
from specklepy.api.client import SpeckleClient
speckle_clients: list[SpeckleClient] = []
+9 -293
View File
@@ -1,241 +1,16 @@
import bpy, idprop
from mathutils import Matrix
from .from_speckle import *
from .to_speckle import *
from .util import *
from bpy_speckle.functions import _report, get_scale_length
from specklepy.objects.geometry import *
from specklepy.objects.other import RenderMaterial
FROM_SPECKLE_SCHEMAS = {
Mesh: import_mesh,
Brep: import_brep,
Curve: import_curve,
Line: import_curve,
Polyline: import_curve,
Polycurve: import_curve,
Arc: import_curve,
}
TO_SPECKLE = {
"MESH": export_mesh,
"CURVE": export_curve,
"EMPTY": export_empty,
}
def set_transform(speckle_object, blender_object):
transform = None
if hasattr(speckle_object, "transform"):
transform = speckle_object.transform
elif (
hasattr(speckle_object, "properties") and speckle_object.properties is not None
):
transform = speckle_object.properties.get("transform", None)
if transform and len(transform) == 16:
mat = Matrix(
[transform[0:4], transform[4:8], transform[8:12], transform[12:16]]
)
blender_object.matrix_world = mat
def add_blender_material(smesh, blender_object) -> None:
"""Add material to a blender object if the corresponding speckle object has a render material"""
if blender_object.data is None:
return
if not hasattr(smesh, "renderMaterial") and not hasattr(smesh, "@renderMaterial"):
return
speckle_mat = getattr(smesh, "renderMaterial", None) or smesh["@renderMaterial"]
mat_name = getattr(speckle_mat, "name", None) or speckle_mat.__dict__.get("@name")
if not mat_name:
mat_name = speckle_mat.applicationId or speckle_mat.id or speckle_mat.get_id()
blender_mat = bpy.data.materials.get(mat_name)
if not blender_mat:
blender_mat = bpy.data.materials.new(mat_name)
# for now, we're not updating these materials. as per tom's suggestion, we should have a toggle
# that enables this as the blender mats will prob be much more complex than whatever is coming in
blender_mat.use_nodes = True
inputs = blender_mat.node_tree.nodes["Principled BSDF"].inputs
inputs["Base Color"].default_value = to_rgba(speckle_mat.diffuse)
inputs["Emission"].default_value = to_rgba(speckle_mat.emissive)
inputs["Roughness"].default_value = speckle_mat.roughness
inputs["Metallic"].default_value = speckle_mat.metalness
inputs["Alpha"].default_value = speckle_mat.opacity
if speckle_mat.opacity < 1:
blender_mat.blend_method = "BLEND"
blender_object.data.materials.append(blender_mat)
def material_to_speckle(blender_object) -> RenderMaterial:
"""Create and return a render material from a blender object"""
if not getattr(blender_object.data, "materials", None):
return
blender_mat = blender_object.data.materials[0]
speckle_mat = RenderMaterial()
speckle_mat.name = blender_mat.name
if blender_mat.use_nodes is True:
inputs = blender_mat.node_tree.nodes["Principled BSDF"].inputs
speckle_mat.diffuse = to_argb_int(inputs["Base Color"].default_value)
speckle_mat.emissive = to_argb_int(inputs["Emission"].default_value)
speckle_mat.roughness = inputs["Roughness"].default_value
speckle_mat.metalness = inputs["Metallic"].default_value
speckle_mat.opacity = inputs["Alpha"].default_value
else:
speckle_mat.diffuse = to_argb_int(blender_mat.diffuse_color)
speckle_mat.metalness = blender_mat.metallic
speckle_mat.roughness = blender_mat.roughness
return speckle_mat
def try_add_property(speckle_object, blender_object, prop, prop_name):
if prop in speckle_object.keys() and speckle_object[prop] is not None:
blender_object[prop_name] = speckle_object[prop]
# def add_dictionary(prop, blender_object, superkey=None):
# for key in prop.keys():
# key_name = "{}.{}".format(superkey, key) if superkey else "{}".format(key)
# if isinstance(prop[key], dict):
# subtype = prop[key].get("type", None)
# if subtype and subtype in FROM_SPECKLE.keys():
# continue
# else:
# add_dictionary(prop[key], blender_object, key_name)
# elif hasattr(prop[key], "type"):
# subtype = prop[key].type
# if subtype and subtype in FROM_SPECKLE.keys():
# continue
# else:
# try:
# blender_object[key_name] = prop[key]
# except KeyError:
# pass
def add_custom_properties(speckle_object, blender_object):
if blender_object is None:
return
blender_object["_speckle_type"] = type(speckle_object).__name__
# blender_object['_speckle_name'] = "SpeckleObject"
ignore = ["_chunkable", "_units", "units"]
if hasattr(speckle_object, "applicationId"):
blender_object["applicationId"] = speckle_object.applicationId
for key in speckle_object.get_dynamic_member_names():
if key in ignore:
continue
if isinstance(speckle_object[key], (int, str, float, dict)):
blender_object[key] = speckle_object[key]
def dict_to_speckle_object(data):
if "type" in data.keys() and data["type"] in SCHEMAS.keys():
obj = SCHEMAS[data["type"]].parse_obj(data)
for key in obj.properties.keys():
if isinstance(obj.properties[key], dict):
obj.properties[key] = dict_to_speckle_object(obj.properties[key])
elif isinstance(obj.properties[key], list):
for i in range(len(obj.properties[key])):
if isinstance(obj.properties[key][i], dict):
obj.properties[key][i] = dict_to_speckle_object(
obj.properties[key][i]
)
return obj
else:
for key in data.keys():
if isinstance(data[key], dict):
data[key] = dict_to_speckle_object(data[key])
elif isinstance(data[key], list):
for i in range(len(data[key])):
if isinstance(data[key][i], dict):
data[key][i] = dict_to_speckle_object(data[key][i])
return data
def from_speckle_object(speckle_object, scale, name=None):
speckle_name = (
name
or getattr(speckle_object, "name", None)
or speckle_object.speckle_type + f" -- {speckle_object.id}"
)
units = getattr(speckle_object, "units", None)
if units:
scale = get_scale_length(units) / bpy.context.scene.unit_settings.scale_length
# try native conversion
if type(speckle_object) in FROM_SPECKLE_SCHEMAS.keys():
print("Got object type: {}".format(type(speckle_object)))
try:
obdata = FROM_SPECKLE_SCHEMAS[type(speckle_object)](
speckle_object, scale, speckle_name
)
except Exception as e: # conversion error
_report(f"Error converting {speckle_object} \n{e}")
return None
if speckle_name in bpy.data.objects.keys():
blender_object = bpy.data.objects[speckle_name]
blender_object.data = obdata
blender_object.matrix_world = Matrix()
if hasattr(obdata, "materials"):
blender_object.data.materials.clear()
else:
blender_object = bpy.data.objects.new(speckle_name, obdata)
blender_object.speckle.object_id = str(speckle_object.id)
blender_object.speckle.enabled = True
add_custom_properties(speckle_object, blender_object)
add_blender_material(speckle_object, blender_object)
# TODO: chat with tom re transforms
# set_transform(speckle_object, blender_object)
return blender_object
# try display mesh
mesh = getattr(
speckle_object, "displayMesh", getattr(speckle_object, "displayValue", None)
)
if mesh:
return from_speckle_object(mesh, scale, speckle_name)
# return none if fail
_report("Invalid input: {}".format(speckle_object))
return None
def get_speckle_subobjects(attr, scale, name):
from typing import Union
from bpy_speckle.convert.to_native import convert_to_native
from specklepy.objects.base import Base
def get_speckle_subobjects(attr: Union[dict, Base], scale: float, name: str) -> list:
subobjects = []
for key in attr.keys():
keys = attr.keys() if isinstance(attr, dict) else attr.get_dynamic_member_names()
for key in keys:
if isinstance(attr[key], dict):
subtype = attr[key].get("type", None)
if subtype:
name = "{}.{}".format(name, key)
# print("{} :: {}".format(name, subtype))
subobject = from_speckle_object(attr[key], scale, name)
add_custom_properties(attr[key], subobject)
name = f"{name}.{key}"
subobject = convert_to_native(attr[key], name)
subobjects.append(subobject)
props = attr[key].get("properties", None)
@@ -245,69 +20,10 @@ def get_speckle_subobjects(attr, scale, name):
subtype = attr[key].type
if subtype:
name = "{}.{}".format(name, key)
# print("{} :: {}".format(name, subtype))
subobject = from_speckle_object(attr[key], scale, name)
add_custom_properties(attr[key], subobject)
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
ignored_keys = [
"speckle",
"_speckle_type",
"_speckle_name",
"_speckle_transform",
"_RNA_UI",
"transform",
"_units",
"_chunkable",
]
def get_blender_custom_properties(obj, max_depth=1000):
global ignored_keys
if max_depth < 0:
return obj
if hasattr(obj, "keys"):
return {
key: get_blender_custom_properties(obj[key], max_depth - 1)
for key in obj.keys()
if key not in ignored_keys and not key.startswith("_")
}
elif isinstance(obj, (list, tuple, idprop.types.IDPropertyArray)):
return [get_blender_custom_properties(o, max_depth - 1) for o in obj]
else:
return obj
def to_speckle_object(blender_object, scale, desgraph=None):
blender_type = blender_object.type
speckle_objects = []
speckle_material = material_to_speckle(blender_object)
if blender_type in TO_SPECKLE.keys():
if desgraph:
blender_object = blender_object.evaluated_get(desgraph)
converted = TO_SPECKLE[blender_type](blender_object, blender_object.data, scale)
if isinstance(converted, list):
speckle_objects.extend([c for c in converted if c != None])
for so in speckle_objects:
so.properties = get_blender_custom_properties(blender_object)
so.applicationId = so.properties.pop("applicationId", None)
if speckle_material:
so["renderMaterial"] = speckle_material
# Set object transform
so.properties["transform"] = [y for x in blender_object.matrix_world for y in x]
# _report(speckle_objects)
return speckle_objects
@@ -1,3 +0,0 @@
from .mesh import import_mesh
from .curve import import_curve
from .brep import import_brep
-24
View File
@@ -1,24 +0,0 @@
import bpy
from .mesh import to_bmesh
from bpy_speckle.util import find_key_case_insensitive
def import_brep(speckle_brep, scale, name=None):
if not name:
name = speckle_brep.geometryHash or speckle_brep.id
display = getattr(
speckle_brep, "displayMesh", getattr(speckle_brep, "displayValue", None)
)
if display:
if name in bpy.data.meshes.keys():
mesh = bpy.data.meshes[name]
else:
mesh = bpy.data.meshes.new(name=name)
to_bmesh(display, mesh, name, scale)
# add_custom_properties(speckle_brep[dvKey], mesh)
else:
mesh = None
return mesh
-237
View File
@@ -1,237 +0,0 @@
import bpy, math
from bpy_speckle.util import find_key_case_insensitive
import mathutils
from specklepy.objects.geometry import *
CONVERT = {}
def import_line(scurve, bcurve, scale):
line = bcurve.splines.new("POLY")
line.points.add(1)
line.points[0].co = (
float(scurve.start.x) * scale,
float(scurve.start.y) * scale,
float(scurve.start.z) * scale,
1,
)
if scurve.end:
line.points[1].co = (
float(scurve.end.x) * scale,
float(scurve.end.y) * scale,
float(scurve.end.z) * scale,
1,
)
return line
CONVERT[Line] = import_line
def import_polyline(scurve, bcurve, scale):
# value = find_key_case_insensitive(scurve, "value")
value = scurve.value
if value:
N = int(len(value) / 3)
polyline = bcurve.splines.new("POLY")
if hasattr(scurve, "closed"):
polyline.use_cyclic_u = scurve.closed
# if "closed" in scurve.keys():
# polyline.use_cyclic_u = scurve["closed"]
polyline.points.add(N - 1)
for i in range(N):
polyline.points[i].co = (
float(value[i * 3]) * scale,
float(value[i * 3 + 1]) * scale,
float(value[i * 3 + 2]) * scale,
1,
)
return polyline
CONVERT[Polyline] = import_polyline
def import_nurbs_curve(scurve, bcurve, scale):
# points = find_key_case_insensitive(scurve, "points")
points = scurve.points
if points:
N = int(len(points) / 3)
nurbs = bcurve.splines.new("NURBS")
if hasattr(scurve, "closed"):
nurbs.use_cyclic_u = scurve.closed != 0
nurbs.points.add(N - 1)
for i in range(N):
nurbs.points[i].co = (
float(points[i * 3]) * scale,
float(points[i * 3 + 1]) * scale,
float(points[i * 3 + 2]) * scale,
1,
)
if len(scurve.weights) == len(nurbs.points):
for i, w in enumerate(scurve.weights):
nurbs.points[i].weight = w
# TODO: anaylize curve knots to decide if use_endpoint_u or use_bezier_u should be enabled
# nurbs.use_endpoint_u = True
nurbs.order_u = scurve.degree + 1
return nurbs
CONVERT[Curve] = import_nurbs_curve
def import_arc(rcurve, bcurve, scale):
"""
Convert Arc object
TODO: improve Blender representation of arc
"""
plane = rcurve.plane
if not plane:
return
origin = plane.origin
normal = mathutils.Vector([plane.normal.x, plane.normal.y, plane.normal.z])
xaxis = plane.xdir
yaxis = plane.ydir
radius = rcurve.radius * scale
startAngle = rcurve.startAngle
endAngle = rcurve.endAngle
startQuat = mathutils.Quaternion(normal, startAngle)
endQuat = mathutils.Quaternion(normal, endAngle)
"""
Get start and end vectors, centre point, angles, etc.
"""
r1 = mathutils.Vector([plane.xdir.x, plane.xdir.y, plane.xdir.z])
r1.rotate(startQuat)
r2 = mathutils.Vector([plane.xdir.x, plane.xdir.y, plane.xdir.z])
r2.rotate(endQuat)
c = mathutils.Vector([plane.origin.x, plane.origin.y, plane.origin.z]) * scale
spt = c + r1 * radius
ept = c + r2 * radius
angle = endAngle - startAngle
t1 = normal.cross(r1)
"""
Initialize arc data and calculate subdivisions
"""
arc = bcurve.splines.new("NURBS")
arc.use_cyclic_u = False
Ndiv = max(int(math.floor(angle / 0.3)), 2)
step = angle / float(Ndiv)
stepQuat = mathutils.Quaternion(normal, step)
tan = math.tan(step / 2) * radius
arc.points.add(Ndiv + 1)
"""
Set start and end points
"""
arc.points[0].co = (spt.x, spt.y, spt.z, 1)
arc.points[Ndiv + 1].co = (ept.x, ept.y, ept.z, 1)
"""
Set intermediate points
"""
for i in range(Ndiv):
t1 = normal.cross(r1)
pt = c + r1 * radius + t1 * tan
arc.points[i + 1].co = (pt.x, pt.y, pt.z, 1)
r1.rotate(stepQuat)
"""
Set curve settings
"""
arc.use_endpoint_u = True
arc.order_u = 3
return arc
CONVERT[Arc] = import_arc
def import_null(speckle_object, bcurve, scale):
"""
Handle unsupported types
"""
print("Failed to convert type", speckle_object["type"])
return None
def import_polycurve(scurve, bcurve, scale):
"""
Convert Polycurve object
"""
segments = scurve.segments
curves = []
for seg in segments:
speckle_type = type(seg)
if speckle_type in CONVERT.keys():
# segcurve = SCHEMAS[speckle_type].parse_obj(seg)
curves.append(CONVERT[speckle_type](seg, bcurve, scale))
else:
print("Unsupported curve type: {}".format(speckle_type))
return curves
CONVERT[Polycurve] = import_polycurve
def import_curve(speckle_curve, scale, name=None):
"""
Convert Curve object
"""
if not name:
name = speckle_curve.geometryHash or speckle_curve.id or "SpeckleCurve"
if name in bpy.data.curves.keys():
curve_data = bpy.data.curves[name]
else:
curve_data = bpy.data.curves.new(name, type="CURVE")
curve_data.dimensions = "3D"
curve_data.resolution_u = 12
if type(speckle_curve) not in CONVERT.keys():
print("Unsupported curve type: {}".format(speckle_curve.type))
return None
CONVERT[type(speckle_curve)](speckle_curve, curve_data, scale)
return curve_data
-167
View File
@@ -1,167 +0,0 @@
import bpy, bmesh, struct
import base64
from bpy_speckle.functions import _report
def add_vertices(smesh, bmesh, scale=1.0):
sverts = smesh.vertices
if sverts and len(sverts) > 0:
for i in range(0, len(sverts), 3):
bmesh.verts.new(
(
float(sverts[i]) * scale,
float(sverts[i + 1]) * scale,
float(sverts[i + 2]) * scale,
)
)
bmesh.verts.ensure_lookup_table()
def add_faces(smesh, bmesh, smooth=False):
sfaces = smesh.faces
if sfaces and len(sfaces) > 0:
i = 0
# TODO: why does `faces.new()` seem to fail so often?
while i < len(sfaces):
if sfaces[i] == 0:
i += 1
try:
f = bmesh.faces.new(
(
bmesh.verts[int(sfaces[i])],
bmesh.verts[int(sfaces[i + 1])],
bmesh.verts[int(sfaces[i + 2])],
)
)
f.smooth = smooth
except Exception as e:
_report(f"Failed to create face for mesh {smesh.id} \n{e}")
i += 3
elif sfaces[i] == 1:
i += 1
try:
f = bmesh.faces.new(
(
bmesh.verts[int(sfaces[i])],
bmesh.verts[int(sfaces[i + 1])],
bmesh.verts[int(sfaces[i + 2])],
bmesh.verts[int(sfaces[i + 3])],
)
)
f.smooth = smooth
except Exception as e:
_report(f"Failed to create face for mesh {smesh.id} \n{e}")
i += 4
else:
print("Invalid face length.\n" + str(sfaces[i]))
break
bmesh.faces.ensure_lookup_table()
bmesh.verts.index_update()
def add_colors(smesh, bmesh):
scolors = smesh.colors
if scolors:
colors = []
if len(scolors) > 0:
for i in range(len(scolors)):
col = int(scolors[i])
(a, r, g, b) = [
int(x) for x in struct.unpack("!BBBB", struct.pack("!i", col))
]
colors.append(
(
float(r) / 255.0,
float(g) / 255.0,
float(b) / 255.0,
float(a) / 255.0,
)
)
# Make vertex colors
if len(scolors) == len(bmesh.verts):
color_layer = bmesh.loops.layers.color.new("Col")
for face in bmesh.faces:
for loop in face.loops:
loop[color_layer] = colors[loop.vert.index]
def add_uv_coords(smesh, bmesh):
if not hasattr(smesh, "properties"):
return
sprops = smesh.properties
if sprops:
texKey = ""
if "texture_coordinates" in sprops.keys():
texKey = "texture_coordinates"
elif "TextureCoordinates" in sprops.keys():
texKey = "TextureCoordinates"
if texKey != "":
try:
decoded = base64.b64decode(sprops[texKey]).decode("utf-8")
s_uvs = decoded.split()
uv = []
if int(len(s_uvs) / 2) == len(bmesh.verts):
for i in range(0, len(s_uvs), 2):
uv.append((float(s_uvs[i]), float(s_uvs[i + 1])))
else:
print(len(s_uvs) * 2)
print(len(bmesh.verts))
print("Failed to match UV coordinates to vert data.")
# Make UVs
uv_layer = bmesh.loops.layers.uv.verify()
for f in bmesh.faces:
for l in f.loops:
luv = l[uv_layer]
luv.uv = uv[l.vert.index]
except:
print("Failed to decode texture coordinates.")
raise
del smesh.properties[texKey]
def to_bmesh(speckle_mesh, blender_mesh, name="SpeckleMesh", scale=1.0):
bm = bmesh.new()
add_vertices(speckle_mesh, bm, scale)
add_faces(speckle_mesh, bm)
add_colors(speckle_mesh, bm)
add_uv_coords(speckle_mesh, bm)
bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
bm.to_mesh(blender_mesh)
bm.free()
return blender_mesh
def import_mesh(speckle_mesh, scale=1.0, name=None):
"""
Convert Mesh object
"""
if not name:
name = speckle_mesh.geometryHash or speckle_mesh.id
if name in bpy.data.meshes.keys():
mesh = bpy.data.meshes[name]
else:
mesh = bpy.data.meshes.new(name=name)
to_bmesh(speckle_mesh, mesh, name, scale)
return mesh
@@ -1,6 +0,0 @@
import bpy, bmesh, struct
import base64
def import_plane(speckle_plane, scale=1.0, name=None):
return None
+665
View File
@@ -0,0 +1,665 @@
import math
from typing import Tuple, Union, Collection
from bpy_speckle.functions import get_scale_length, _report
from mathutils import (
Matrix as MMatrix,
Vector as MVector,
Quaternion as MQuaternion,
)
import bpy, bmesh
from specklepy.objects.other import (
Instance,
Transform,
BlockDefinition,
)
from specklepy.objects.geometry import *
from bpy.types import Object
from .util import (
get_render_material,
link_object_to_collection_nested,
render_material_to_native,
add_custom_properties,
add_vertices,
add_faces,
add_colors,
add_uv_coords,
)
SUPPORTED_CURVES = (Line, Polyline, Curve, Arc, Polycurve, Ellipse, Circle)
CAN_CONVERT_TO_NATIVE = (
Mesh,
*SUPPORTED_CURVES,
Instance,
)
def _has_native_convesion(speckle_object: Base) -> bool:
return any(isinstance(speckle_object, t) for t in CAN_CONVERT_TO_NATIVE)
def _has_fallback_conversion(speckle_object: Base) -> bool:
return any(getattr(speckle_object, alias, None) for alias in DISPLAY_VALUE_PROPERTY_ALIASES)
def can_convert_to_native(speckle_object: Base) -> bool:
if(_has_native_convesion(speckle_object) or _has_fallback_conversion(speckle_object)):
return True
_report(f"Could not convert unsupported Speckle object: {speckle_object}")
return False
def create_new_object(obj_data: Optional[bpy.types.ID], desired_name: str, counter: int = 0) -> bpy.types.Object:
"""
Creates a new blender object with a unique name,
if the desired_name is already taken
we'll append a number, with the format .xxx to the desired_name to ensure the name is unique.
"""
name = desired_name if counter == 0 else f"{desired_name[:OBJECT_NAME_MAX_LENGTH - 4]}.{counter:03d}" # format counter as name.xxx, truncate to ensure we don't exceed the object name max length
#TODO: This is very slow, and gets slower the more objects you receive with the same name...
# We could use a binary/galloping search, and/or cache the name -> index within a receive.
if name in bpy.data.objects.keys():
#Object already exists, increment counter and try again!
return create_new_object(obj_data, desired_name, counter + 1)
blender_object = bpy.data.objects.new(name, obj_data)
return blender_object
convert_instances_as: str #HACK: This is hacky, we need a better way to pass settings down to the converter
def set_convert_instances_as(value: str):
global convert_instances_as
convert_instances_as = value
def convert_to_native(speckle_object: Base) -> list[Object]:
speckle_type = type(speckle_object)
try:
object_name = _generate_object_name(speckle_object)
scale = get_scale_factor(speckle_object)
obj_data: Optional[Union[bpy.types.ID, bpy.types.Object]] = None
converted: list[Object] = []
# convert elements/breps
if not _has_native_convesion(speckle_object):
(obj_data, converted) = element_to_native(speckle_object, object_name, scale)
if not obj_data and not converted:
_report(f"Unsupported type {speckle_object.speckle_type}")
# convert supported geometry
elif isinstance(speckle_object, Mesh):
obj_data = mesh_to_native(speckle_object, object_name, scale)
elif speckle_type in SUPPORTED_CURVES:
obj_data = icurve_to_native(speckle_object, object_name, scale)
elif isinstance(speckle_object, Instance):
if convert_instances_as == "linked_duplicates":
(obj_data, converted) = instance_to_native_object(speckle_object, scale)
elif convert_instances_as != "collection_instance":
obj_data = instance_to_native_collection_instance(speckle_object, scale)
else:
_report(f"convert_instances_as = '{convert_instances_as}' is not implemented, Instances will be converted as collection instances!")
obj_data = instance_to_native_collection_instance(speckle_object, scale)
else:
_report(f"Unsupported type {speckle_type}")
return []
except Exception as ex: # conversion error
_report(f"Error converting {speckle_object} \n{ex}")
return []
blender_object = obj_data if isinstance(obj_data, Object) else create_new_object(obj_data, object_name)
blender_object.speckle.object_id = str(speckle_object.id)
blender_object.speckle.enabled = True
add_custom_properties(speckle_object, blender_object)
for child in converted:
child.parent = blender_object
converted.append(blender_object)
_report(f"Successfully converted {object_name} as {blender_object.type}")
return converted
DISPLAY_VALUE_PROPERTY_ALIASES = ["displayValue", "@displayValue", "displayMesh", "@displayMesh", "elements", "@elements"]
def element_to_native(speckle_object: Base, name: str, scale: float, combineMeshes: bool = True) -> tuple[Optional[bpy.types.Mesh], list[bpy.types.Object]]:
"""
Converts a given speckle_object by converting displayValue properties (elements treated the same as displayValues)
if combineMeshes == True
Converts mesh displayValues as one mesh
Converts non-mesh displayValues as child Objects
if combineMeshes == False
Converts all displayValues as child objects (first item of the returned tuple will be None)
"""
meshes: list[Mesh] = []
elements: list[Base] = []
#NOTE: raw Mesh elements will be treated like displayValues, which is not ideal, but no connector sends raw Mesh elements so it's fine
for alias in DISPLAY_VALUE_PROPERTY_ALIASES:
display = getattr(speckle_object, alias, None)
count = 0
MAX_DEPTH = 255 # some large value, to prevent infinite reccursion
def seperate(value: Any) -> bool:
nonlocal meshes, elements, count, MAX_DEPTH
if combineMeshes and isinstance(value, Mesh):
meshes.append(value)
elif isinstance(value, Base):
elements.append(value)
elif isinstance(value, list):
count += 1
if(count > MAX_DEPTH):
return True
for x in value:
seperate(x)
return False
did_halt = seperate(display)
if did_halt:
_report(f"Traversal of {speckle_object.speckle_type} {speckle_object.id} halted after traversal depth exceeds MAX_DEPTH={MAX_DEPTH}. Are there circular references object structure?")
converted: list[Object] = []
mesh = None
if meshes:
mesh = meshes_to_native(speckle_object, meshes, name, scale)
for item in elements:
# 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
item.parent_speckle_type = speckle_object.speckle_type #TODO: consider if this is still useful, as we now properly structure object parenting
blender_object = convert_to_native(item)
if isinstance(blender_object, list):
converted.extend(blender_object)
else:
add_custom_properties(speckle_object, blender_object)
converted.append(blender_object)
return (mesh, converted)
def mesh_to_native(speckle_mesh: Mesh, name: str, scale: float) -> bpy.types.Mesh:
return meshes_to_native(speckle_mesh, [speckle_mesh], name, scale)
def meshes_to_native(element: Base, meshes: Collection[Mesh], name: str, scale: float) -> bpy.types.Mesh:
if name in bpy.data.meshes.keys():
return bpy.data.meshes[name]
blender_mesh = bpy.data.meshes.new(name=name)
fallback_material = get_render_material(element)
bm = bmesh.new()
# First pass, add vertex data
for mesh in meshes:
scale = get_scale_factor(mesh, scale)
add_vertices(mesh, bm, scale)
bm.verts.ensure_lookup_table()
# Second pass, add face data
offset = 0
for i, mesh in enumerate(meshes):
add_faces(mesh, bm, offset, i)
render_material = get_render_material(mesh) or fallback_material
if render_material is not None:
native_material = render_material_to_native(render_material)
blender_mesh.materials.append(native_material)
offset += len(mesh.vertices) // 3
bm.faces.ensure_lookup_table()
bm.verts.index_update()
# Third pass, add vertex instance data
for mesh in meshes:
add_colors(mesh, bm)
add_uv_coords(mesh, bm)
bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
bm.to_mesh(blender_mesh)
bm.free()
return blender_mesh
"""
Curves
"""
def line_to_native(speckle_curve: Line, blender_curve: bpy.types.Curve, scale: float) -> list[bpy.types.Spline]:
if not speckle_curve.end: return []
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,
)
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: Polyline, bcurve: bpy.types.Curve, scale: float) -> list[bpy.types.Spline]:
if not (value := scurve.value): return []
N = len(value) // 3
polyline = bcurve.splines.new("POLY")
if hasattr(scurve, "closed"):
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: Curve, bcurve: bpy.types.Curve, scale: float) -> list[bpy.types.Spline]:
if not (points := scurve.points): return []
# Closed curves from rhino will have n + degree points. We ignore the extras
num_points = len(points) // 3 - scurve.degree if (scurve.closed) else (
len(points) // 3)
nurbs = bcurve.splines.new("NURBS")
nurbs.use_cyclic_u = scurve.closed
nurbs.use_endpoint_u = not scurve.periodic
nurbs.points.add(num_points - 1)
use_weights = len(scurve.weights) >= num_points
for i in range(num_points):
nurbs.points[i].co = (
float(points[i * 3]) * scale,
float(points[i * 3 + 1]) * scale,
float(points[i * 3 + 2]) * scale,
1,
)
nurbs.points[i].weight = scurve.weights[i] if use_weights else 1
nurbs.order_u = scurve.degree + 1
return [nurbs]
def arc_to_native(rcurve: Arc, bcurve: bpy.types.Curve, scale: float) -> Optional[bpy.types.Spline]:
# TODO: improve Blender representation of arc - check autocad test stream
plane = rcurve.plane
if not plane:
return None
normal = MVector([plane.normal.x, plane.normal.y, plane.normal.z])
radius = rcurve.radius * scale
startAngle = rcurve.startAngle
endAngle = rcurve.endAngle
startQuat = MQuaternion(normal, startAngle)
endQuat = MQuaternion(normal, endAngle)
# Get start and end vectors, centre point, angles, etc.
r1 = MVector([plane.xdir.x, plane.xdir.y, plane.xdir.z])
r1.rotate(startQuat)
r2 = MVector([plane.xdir.x, plane.xdir.y, plane.xdir.z])
r2.rotate(endQuat)
c = MVector([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 = MQuaternion(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: Polycurve, bcurve: bpy.types.Curve, scale: float) -> list[bpy.types.Spline]:
"""
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))
else:
_report(f"Unsupported curve type: {speckle_type}")
return curves
def circle_to_native(circle: Circle, bcurve: bpy.types.Curve, units_scale: float) -> list[bpy.types.Spline]:
#HACK: violates typing, but it works...
circle["firstRadius"] = circle.radius
circle["secondRadius"] = circle.radius
return ellipse_to_native(circle, bcurve, units_scale)
def ellipse_to_native(ellipse: Ellipse, bcurve: bpy.types.Curve, units_scale: float) -> list[bpy.types.Spline]:
plane = ellipse.plane
radX = ellipse.firstRadius * units_scale
radY = ellipse.secondRadius * units_scale
D = 0.5522847498307936 # (4/3)*tan(pi/8)
right_handles = [
(+radX, +radY * D, 0.0),
(-radX * D, +radY, 0.0),
(-radX, -radY * D, 0.0),
(+radX * D, -radY, 0.0),
]
left_handles = [
(+radX, -radY * D, 0.0),
(+radX * D, +radY, 0.0),
(-radX, +radY * D, 0.0),
(-radX * D, -radY, 0.0),
]
points = [
(+radX, 0.0, 0.0),
(0.0, +radY, 0.0),
(-radX, 0.0, 0.0),
(0.0, -radY, 0.0),
]
transform = plane_to_native_transform(plane, units_scale)
spline = bcurve.splines.new("BEZIER")
spline.bezier_points.add(len(points) - 1)
for i in range(len(points)):
spline.bezier_points[i].co = transform @ MVector(points[i])
spline.bezier_points[i].handle_left = transform @ MVector(left_handles[i])
spline.bezier_points[i].handle_right = transform @ MVector(right_handles[i])
spline.use_cyclic_u = True
#TODO support trims?
return [spline]
def icurve_to_native_spline(speckle_curve: Base, blender_curve: bpy.types.Curve, scale: float) -> list[bpy.types.Spline]:
# polycurves
if isinstance(speckle_curve, Polycurve):
return polycurve_to_native(speckle_curve, blender_curve, scale)
# single curves
if isinstance(speckle_curve, Line):
spline = line_to_native(speckle_curve, blender_curve, scale)
elif isinstance(speckle_curve, Curve):
spline = nurbs_to_native(speckle_curve, blender_curve, scale)
elif isinstance(speckle_curve, Polyline):
spline = polyline_to_native(speckle_curve, blender_curve, scale)
elif isinstance(speckle_curve, Arc):
spline = arc_to_native(speckle_curve, blender_curve, scale)
elif isinstance(speckle_curve, Ellipse):
spline = ellipse_to_native(speckle_curve, blender_curve, scale)
elif isinstance(speckle_curve, Circle):
spline = circle_to_native(speckle_curve, blender_curve, scale)
else:
raise TypeError(f"{speckle_curve} is not a supported curve type. Supported types: {SUPPORTED_CURVES}")
return [spline] if spline is not None else []
def icurve_to_native(speckle_curve: Base, name: str, scale: float) -> Optional[bpy.types.Curve]:
curve_type = type(speckle_curve)
if curve_type not in SUPPORTED_CURVES:
_report(f"Unsupported curve type: {curve_type}")
return None
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 #TODO: We could maybe decern the resolution from the ployline displayValue
icurve_to_native_spline(speckle_curve, blender_curve, scale)
return blender_curve
"""
Transforms and Intances
"""
def transform_to_native(transform: Transform, scale: float) -> MMatrix:
mat = MMatrix(
[
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 plane_to_native_transform(plane: Plane, fallback_scale:float = 1) -> MMatrix:
scale_factor = get_scale_factor(plane, fallback_scale)
tx = (plane.origin.x * scale_factor)
ty = (plane.origin.y * scale_factor)
tz = (plane.origin.z * scale_factor)
return MMatrix((
(plane.xdir.x, plane.ydir.x, plane.normal.x, tx),
(plane.xdir.y, plane.ydir.y, plane.normal.y, ty),
(plane.xdir.z, plane.ydir.z, plane.normal.z, tz),
(0, 0, 0, 1 )
))
"""
Instances / Blocks
"""
def _get_instance_name(instance: Instance) -> str:
name_prefix = _get_friendly_object_name(instance) or _get_friendly_object_name(instance.definition) or _simplified_speckle_type(instance.speckle_type)
return f"{name_prefix}{OBJECT_NAME_SEPERATOR}{instance.id}"
def instance_to_native_object(instance: Instance, scale: float) -> Tuple[bpy.types.Object, List[bpy.types.Object]]:
"""
Converts Instance to a unique object with (potentially) shared data (linked duplicate)
"""
if not instance.definition: raise Exception(f"Instance is missing a definition")
if not instance.transform: raise Exception(f"Instance is missing a transform")
name = _get_instance_name(instance)
definition = instance.definition
native_instance: Object
native_elements: List[Object] = []
elements_on_instance: List[Object] = []
if isinstance(definition, BlockDefinition): #NOTE: We have to handle BlockDefinitions specially here, since they don't follow normal traversal rules
native_instance = create_new_object(None, name) #Instance will be empty
native_instance.empty_display_size = 0
for geo in definition.geometry:
native_elements.append(convert_to_native(geo)[-1])
else:
native_instance = convert_to_native(instance.definition)[-1] # Convert assuming that definition is convertable
instance_transform = transform_to_native(instance.transform, scale)
instance_transform_inverted = instance_transform.inverted()
native_instance.matrix_world = instance_transform
(_, elements_on_instance) = element_to_native(instance, name, scale)
for c in elements_on_instance:
c.matrix_world = instance_transform_inverted @ c.matrix_world #Undo the instance transform on elements
native_elements.extend(elements_on_instance)
return (native_instance, native_elements) #TODO: need to double check that all child objects have custom props attached correctly
def instance_to_native_collection_instance(instance: Instance, scale: float) -> bpy.types.Object:
"""
Convert an Instance as a transformed Object with the `instance_collection` property
set to be the `instance.Definition` converted as a collection
The definition collection won't be linked to the current scene
Any Elements on the instance object will also be converted (and spacially transformed)
"""
if not instance.definition: raise Exception(f"Instance is missing a definition")
if not instance.transform: raise Exception(f"Instance is missing a transform")
name = _get_instance_name(instance)
# Get/Convert definition collection
collection_def = _instance_definition_to_native(instance.definition)
# Convert elements as children of collection instance object
(_, elements) = element_to_native(instance, name, scale, False)
instance_transform = transform_to_native(instance.transform, scale)
instance_transform_inverted = instance_transform.inverted()
native_instance = bpy.data.objects.new(name, None)
#add_custom_properties(instance, native_instance)
# hide the instance axes so they don't clutter the viewport
native_instance.empty_display_size = 0
native_instance.instance_collection = collection_def
native_instance.instance_type = "COLLECTION"
native_instance.matrix_world =instance_transform
for c in elements:
c.matrix_world = instance_transform_inverted @ c.matrix_world #Undo the instance transform on elements
c.parent = native_instance #TODO: need to double check that all child objects have custom props attached correctly
return native_instance
def _instance_definition_to_native(definition: Union[Base, BlockDefinition]) -> bpy.types.Collection:
"""
Converts a geometry carrying Base as a collection (does not link it to the scene)
"""
name = _generate_object_name(definition)
native_def = bpy.data.collections.get(name)
if native_def:
return native_def
native_def = bpy.data.collections.new(name)
native_def["applicationId"] = definition.applicationId
#TODO could maybe replace BlockDefinition awareness with a single traverse member call
geometry = definition.geometry if isinstance(definition, BlockDefinition) else [definition]
for geo in geometry:
if not geo: continue
converted = convert_to_native(geo)[-1] #NOTE: we assume the last item is the root converted item
link_object_to_collection_nested(converted, native_def)
return native_def
"""
Object Naming
"""
def _get_friendly_object_name(speckle_object: Base) -> Optional[str]:
return (getattr(speckle_object, "name", None)
or getattr(speckle_object, "Name", None)
or getattr(speckle_object, "family", None)
)
# Blender object names must not exceed 62 characters
# We need to ensure the complete ID is included in the name (to prevent identity collisions)
# So we if the name is too long, we need to truncate
OBJECT_NAME_MAX_LENGTH = 62
SPECKLE_ID_LENGTH = 32
OBJECT_NAME_SEPERATOR = " -- "
def _truncate_object_name(name: str) -> str:
MAX_NAME_LENGTH = OBJECT_NAME_MAX_LENGTH - SPECKLE_ID_LENGTH - len(OBJECT_NAME_SEPERATOR)
return name[:MAX_NAME_LENGTH]
def _simplified_speckle_type(speckle_type: str) -> str:
return(speckle_type.rsplit('.')[-1]) #Take only the most specific object type name (without namespace)
def _generate_object_name(speckle_object: Base) -> str:
prefix: str
name = _get_friendly_object_name(speckle_object)
if name:
prefix = _truncate_object_name(name)
else:
prefix = _simplified_speckle_type(speckle_object.speckle_type)
return f"{prefix}{OBJECT_NAME_SEPERATOR}{speckle_object.id}"
def get_scale_factor(speckle_object: Base, fallback: float = 1.0) -> float:
scale = fallback
if units := getattr(speckle_object, "units", None):
scale = get_scale_length(units) / bpy.context.scene.unit_settings.scale_length
return scale
+448
View File
@@ -0,0 +1,448 @@
from typing import Dict, Iterable, Optional, Tuple
import bpy
from bpy.types import Depsgraph, MeshPolygon, Object
from deprecated import deprecated
from mathutils.geometry import interpolate_bezier
from mathutils import (
Matrix as MMatrix,
Vector as MVector,
)
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,
nurb_make_curve,
to_argb_int,
)
UNITS = "m"
CAN_CONVERT_TO_SPECKLE = ("MESH", "CURVE", "EMPTY")
def convert_to_speckle(raw_blender_object: Object, scale: float, units: str, depsgraph: Optional[Depsgraph]) -> Optional[list]:
global UNITS
UNITS = units
blender_type = raw_blender_object.type
if blender_type not in CAN_CONVERT_TO_SPECKLE:
return None
blender_object: Object = (
raw_blender_object.evaluated_get(depsgraph)
if depsgraph
else raw_blender_object
)
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
speckle_objects = []
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(raw_blender_object) #NOTE: Depsgraph copies don't have custom properties so we use the raw version
so["applicationId"] = so.properties.pop("applicationId", None)
# Set object transform
if blender_type != "EMPTY": #TODO: this could be deprecated once we add proper instancing support
so["properties"]["transform"] = transform_to_speckle(
blender_object.matrix_world
)
return speckle_objects
def mesh_to_speckle(blender_object: Object, data: bpy.types.Mesh, scale: float = 1.0) -> List[Mesh]:
#if data.loop_triangles is None or len(data.loop_triangles) < 1:
# data.calc_loop_triangles()
# Categorise polygons by material index
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 = blender_object.matrix_world
scaled_vertices = [tuple(transform @ x.co * scale) for x in data.vertices]
# Create Speckle meshes for each material
submeshes = []
index_counter = 0
for i in submesh_data:
index_mapping: Dict[int, int] = {}
#Loop through each polygon, and map indicies to their new index in m_verts
mesh_area = 0
m_verts: List[float] = []
m_faces: List[int] = []
m_texcoords: List[float] = []
for face in submesh_data[i]:
u_indices = face.vertices
m_faces.append(len(u_indices))
mesh_area += face.area
for u_index in u_indices:
if u_index not in index_mapping:
# Create mapping between index in blender mesh, and new index in speckle submesh
index_mapping[u_index] = len(m_verts) // 3
vert = scaled_vertices[u_index]
m_verts.append(vert[0])
m_verts.append(vert[1])
m_verts.append(vert[2])
if data.uv_layers.active:
vt = data.uv_layers.active.data[index_counter]
m_texcoords.extend([vt.uv.x, vt.uv.y])
m_faces.append(index_mapping[u_index])
index_counter += 1
speckle_mesh = Mesh(
vertices=m_verts,
faces=m_faces,
colors=[],
textureCoordinates=m_texcoords,
units=UNITS,
area = mesh_area,
bbox=Box(area=0.0, volume=0.0),
)
if i < len(data.materials):
material = data.materials[i]
if material is not None:
speckle_mesh["renderMaterial"] = material_to_speckle(material)
submeshes.append(speckle_mesh)
return submeshes
def bezier_to_speckle(matrix: MMatrix, spline: bpy.types.Spline, scale: float, name: Optional[str] = None) -> Curve:
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.extend(
(
tuple(matrix @ spline.bezier_points[-1].handle_right * scale),
tuple(matrix @ spline.bezier_points[0].handle_left * scale),
tuple(matrix @ spline.bezier_points[0].co * scale),
)
)
num_points = len(points)
flattend_points = []
for row in points: flattend_points.extend(row)
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= not spline.use_endpoint_u,
points=flattend_points,
weights=[1] * num_points,
knots=knots,
rational=True,
area=0,
volume=0,
length=length,
domain=domain,
units=UNITS,
bbox=Box(area=0.0, volume=0.0),
displayValue = bezier_to_speckle_polyline(matrix, spline, scale, length),
)
def nurbs_to_speckle(matrix: MMatrix, spline: bpy.types.Spline, scale: float, name: Optional[str] = None) -> Curve:
degree = spline.order_u - 1
knots = make_knots(spline)
length = spline.calc_length()
domain = Interval(start=0, end=length, totalChildrenCount=0)
weights = [pt.weight for pt in spline.points]
is_rational = all(w == weights[0] for w in weights)
points = [tuple(matrix @ pt.co.xyz * scale) for pt in spline.points]
flattend_points = []
for row in points: flattend_points.extend(row)
if spline.use_cyclic_u:
for i in range(0, degree * 3, 3):
# Rhino expects n + degree number of points (for closed curves). So we need to add an extra point for each degree
flattend_points.append(flattend_points[i + 0])
flattend_points.append(flattend_points[i + 1])
flattend_points.append(flattend_points[i + 2])
for i in range(0, degree):
weights.append(weights[i])
return Curve(
name=name,
degree=degree,
closed=spline.use_cyclic_u,
periodic= not spline.use_endpoint_u,
points=flattend_points,
weights=weights,
knots=knots,
rational=is_rational,
area=0,
volume=0,
length=length,
domain=domain,
units=UNITS,
bbox=Box(area=0.0, volume=0.0),
displayValue=nurbs_to_speckle_polyline(matrix, spline, scale, length),
)
def nurbs_to_speckle_polyline(matrix: MMatrix, spline: bpy.types.Spline, scale: float, length: Optional[float] = None) -> Polyline:
"""
Samples a nurbs curve with resolution_u creating a polyline
"""
points = []
sampled_points = nurb_make_curve(spline, spline.resolution_u, 3)
for i in range(0, len(sampled_points), 3):
scaled_point = matrix @ MVector((
sampled_points[i + 0],
sampled_points[i + 1],
sampled_points[i + 2])) * scale
points.append(scaled_point.x)
points.append(scaled_point.y)
points.append(scaled_point.z)
length = length or spline.calc_length()
domain = Interval(start=0, end=length, totalChildrenCount=0)
return Polyline(value=points, closed = spline.use_cyclic_u, domain=domain, area=0, len=length)
#Inspired by https://blender.stackexchange.com/a/689 (CC BY-SA 3.0)
def bezier_to_speckle_polyline(matrix: MMatrix, spline: bpy.types.Spline, scale: float, length: Optional[float] = None) -> Optional[Polyline]:
"""
Samples a Bézier curve with resolution_u creating a polyline
"""
segments = len(spline.bezier_points)
if segments < 2: return None
R = spline.resolution_u + 1
points = []
if not spline.use_cyclic_u:
segments -= 1
points: List[float] = []
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
_points = interpolate_bezier(knot1, handle1, handle2, knot2, R)
for p in _points:
scaled_point = matrix @ p * scale
points.append(scaled_point.x)
points.append(scaled_point.y)
points.append(scaled_point.z)
length = length or spline.calc_length()
domain = Interval(start=0, end=length, totalChildrenCount=0)
return Polyline(value=points, closed = spline.use_cyclic_u, domain=domain, area=0, len=length)
def poly_to_speckle(matrix: MMatrix, spline: bpy.types.Spline, scale: float, name: Optional[str] = None) -> Polyline:
points = [tuple(matrix @ pt.co.xyz * scale) for pt in spline.points]
flattend_points = []
for row in points: flattend_points.extend(row)
length = spline.calc_length()
domain = Interval(start=0, end=length, totalChildrenCount=0)
return Polyline(
name=name,
closed=bool(spline.use_cyclic_u),
value=list(flattend_points),
length=length,
domain=domain,
bbox=Box(area=0.0, volume=0.0),
area=0,
units=UNITS,
)
def icurve_to_speckle(blender_object: Object, data: bpy.types.Curve, scale=1.0) -> Optional[List[Base]]:
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
@deprecated
def ngons_to_speckle_polylines(blender_object: Object, data: bpy.types.Mesh, scale=1.0) -> Optional[List[Polyline]]:
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,
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_mat: bpy.types.Material) -> RenderMaterial:
speckle_mat = RenderMaterial()
speckle_mat.name = blender_mat.name
if blender_mat.use_nodes is True and blender_mat.node_tree.nodes.get(
"Principled BSDF"
):
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 material_to_speckle_old(blender_object: Object) -> Optional[RenderMaterial]:
"""Create and return a render material from a blender object"""
if not getattr(blender_object.data, "materials", None):
return None
blender_mat: bpy.types.Material = blender_object.data.materials[0]
if not blender_mat:
return None
return material_to_speckle(blender_mat)
def transform_to_speckle(blender_transform: Iterable[Iterable[float]], scale=1.0) -> Transform:
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: bpy.types.Collection, scale=1.0) -> BlockDefinition:
geometry = []
for geo in blender_definition.objects:
geometry.extend(convert_to_speckle(geo, scale, UNITS, None))
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: Object, scale=1.0) -> BlockInstance:
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: Object, scale=1.0) -> Optional[BlockInstance]:
# 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
@@ -1,3 +0,0 @@
from .mesh import export_mesh
from .curve import export_curve, export_ngons_as_polylines
from .empty import export_empty
-224
View File
@@ -1,224 +0,0 @@
import bpy, bmesh, struct
from specklepy.objects.geometry import Curve, Interval, Box, Polyline
from bpy_speckle.convert.to_speckle.mesh import export_mesh
UNITS = "m"
def bezier_to_speckle(matrix, spline, scale, name=None):
degree = 3
closed = spline.use_cyclic_u
points = []
for i, bp in enumerate(spline.bezier_points):
if i > 0:
points.append(tuple(matrix @ bp.handle_left * scale))
points.append(tuple(matrix @ bp.co * scale))
if i < len(spline.bezier_points) - 1:
points.append(tuple(matrix @ bp.handle_right * scale))
if closed:
points.append(tuple(matrix @ spline.bezier_points[-1].handle_right * scale))
points.append(tuple(matrix @ spline.bezier_points[0].handle_left * scale))
points.append(tuple(matrix @ spline.bezier_points[0].co * scale))
num_points = len(points)
knot_count = num_points + degree - 1
knots = [0] * knot_count
for i in range(1, len(knots)):
knots[i] = i // 3
length = spline.calc_length()
domain = Interval(start=0, end=length, totalChildrenCount=0)
return Curve(
name=name,
degree=degree,
closed=spline.use_cyclic_u,
periodic=spline.use_cyclic_u,
points=list(sum(points, ())), # magic (flatten list of tuples)
weights=[1] * num_points,
knots=knots,
rational=False,
area=0,
volume=0,
length=length,
domain=domain,
units=UNITS,
bbox=Box(area=0.0, volume=0.0),
)
def nurbs_to_speckle(matrix, spline, scale, name=None):
knots = makeknots(spline)
# print("knots: {}".format(knots))
points = [tuple(matrix @ pt.co.xyz * scale) for pt in spline.points]
degree = spline.order_u - 1
length = spline.calc_length()
domain = Interval(start=0, end=length, totalChildrenCount=0)
return Curve(
name=name,
degree=degree,
closed=spline.use_cyclic_u,
periodic=spline.use_cyclic_u,
points=list(sum(points, ())), # magic (flatten list of tuples)
weights=[pt.weight for pt in spline.points],
knots=knots,
rational=False,
area=0,
volume=0,
length=length,
domain=domain,
units=UNITS,
bbox=Box(area=0.0, volume=0.0),
)
def poly_to_speckle(matrix, spline, scale, name=None):
points = [tuple(matrix @ pt.co.xyz * scale) for pt in spline.points]
length = spline.calc_length()
domain = Interval(start=0, end=length, totalChildrenCount=0)
return Polyline(
name=name,
closed=spline.use_cyclic_u,
value=list(sum(points, ())), # magic (flatten list of tuples)
length=length,
domain=domain,
bbox=Box(area=0.0, volume=0.0),
area=0,
units=UNITS,
)
def export_curve(blender_object, data, scale=1.0):
UNITS = "m" if bpy.context.scene.unit_settings.system == "METRIC" else "ft"
if blender_object.type != "CURVE":
return None
blender_object = blender_object.evaluated_get(bpy.context.view_layer.depsgraph)
mat = blender_object.matrix_world
curves = []
if data.bevel_mode == "OBJECT" and data.bevel_object != None:
mesh = export_mesh(blender_object, blender_object.to_mesh(), scale)
curves.extend(mesh)
for spline in data.splines:
if spline.type == "BEZIER":
curves.append(bezier_to_speckle(mat, spline, scale, blender_object.name))
elif spline.type == "NURBS":
curves.append(nurbs_to_speckle(mat, spline, scale, blender_object.name))
elif spline.type == "POLY":
curves.append(poly_to_speckle(mat, spline, scale, blender_object.name))
return curves
def export_ngons_as_polylines(blender_object, data, scale=1.0):
UNITS = "m" if bpy.context.scene.unit_settings.system == "METRIC" else "ft"
if blender_object.type != "MESH":
return None
mat = blender_object.matrix_world
verts = data.vertices
polylines = []
for i, poly in enumerate(data.polygons):
value = []
for v in poly.vertices:
value.extend(mat @ verts[v].co * scale)
domain = Interval(start=0, end=1)
poly = Polyline(
name="{}_{}".format(blender_object.name, i),
closed=True,
value=value, # magic (flatten list of tuples)
length=0,
domain=domain,
bbox=Box(area=0.0, volume=0.0),
area=0,
units=UNITS,
)
polylines.append(poly)
return polylines
"""
Python implementation of Blender's NURBS curve generation
from: https://blender.stackexchange.com/a/34276
"""
def macro_knotsu(nu):
return nu.order_u + nu.point_count_u + (nu.order_u - 1 if nu.use_cyclic_u else 0)
def macro_segmentsu(nu):
return nu.point_count_u if nu.use_cyclic_u else nu.point_count_u - 1
def makeknots(nu):
knots = [0.0] * (4 + macro_knotsu(nu))
flag = nu.use_endpoint_u + (nu.use_bezier_u << 1)
if nu.use_cyclic_u:
calcknots(knots, nu.point_count_u, nu.order_u, 0)
makecyclicknots(knots, nu.point_count_u, nu.order_u)
else:
calcknots(knots, nu.point_count_u, nu.order_u, flag)
return knots
def calcknots(knots, pnts, order, flag):
pnts_order = pnts + order
if flag == 1:
k = 0.0
for a in range(1, pnts_order + 1):
knots[a - 1] = k
if a >= order and a <= pnts:
k += 1.0
elif flag == 2:
if order == 4:
k = 0.34
for a in range(pnts_order):
knots[a] = math.floor(k)
k += 1.0 / 3.0
elif order == 3:
k = 0.6
for a in range(pnts_order):
if a >= order and a <= pnts:
k += 0.5
knots[a] = math.floor(k)
else:
for a in range(pnts_order):
knots[a] = a
def makecyclicknots(knots, pnts, order):
order2 = order - 1
if order > 2:
b = pnts + order2
for a in range(1, order2):
if knots[b] != knots[b - a]:
break
if a == order2:
knots[pnts + order - 2] += 1.0
b = order
c = pnts + order + order2
for a in range(pnts + order2, c):
knots[a] = knots[a - 1] + (knots[b] - knots[b - 1])
b -= 1
@@ -1,5 +0,0 @@
import bpy, bmesh, struct
def export_default(blender_object, scale=1.0):
return None
-5
View File
@@ -1,5 +0,0 @@
import bpy, bmesh, struct
def export_empty(blender_object, data, scale=1.0):
return None
-45
View File
@@ -1,45 +0,0 @@
import bpy, bmesh, struct
import base64, hashlib
from time import strftime, gmtime
from specklepy.objects.geometry import Mesh, Interval, Box
def export_mesh(blender_object, data, scale=1.0):
if data.loop_triangles is None or len(data.loop_triangles) < 1:
data.calc_loop_triangles()
mat = blender_object.matrix_world
verts = [tuple(mat @ x.co * scale) for x in data.vertices]
# TODO: add n-gon support, using tessfaces for now
# faces = [x.vertices for x in data.loop_triangles]
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:
if len(f) == 3:
sm.faces.append(0)
elif len(f) == 4:
sm.faces.append(1)
else:
continue
sm.faces.extend(f)
return [sm]
+418 -6
View File
@@ -1,7 +1,31 @@
from typing import Tuple
import math
from typing import Any, Optional, Tuple
from bmesh.types import BMesh
import bpy, struct, idprop
from specklepy.objects.base import Base
from specklepy.objects.geometry import Circle, Mesh, Ellipse
from specklepy.objects.other import RenderMaterial
from bpy_speckle.functions import _report
from bpy.types import Material, Object
IGNORED_PROPERTY_KEYS = {
"id",
"elements",
"displayMesh",
"displayValue",
"speckle_type",
"parameters",
"faces",
"colors",
"vertices",
"renderMaterial",
"textureCoordinates",
"totalChildrenCount"
}
def to_rgba(argb_int: int) -> Tuple[float]:
def to_rgba(argb_int: int) -> Tuple[float, float, float, 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
@@ -11,9 +35,397 @@ def to_rgba(argb_int: int) -> Tuple[float]:
return (red, green, blue, alpha)
def to_argb_int(diffuse_colour) -> int:
def to_argb_int(rgba_color: list[float]) -> int:
"""Converts an RGBA array to an ARGB integer"""
diffuse_colour = diffuse_colour[-1:] + diffuse_colour[0:3]
diffuse_colour = [int(val * 255) for val in diffuse_colour]
argb_color = rgba_color[-1:] + rgba_color[:3]
int_color = [int(val * 255) for val in argb_color]
return int.from_bytes(diffuse_colour, byteorder="big", signed=True)
return int.from_bytes(int_color, byteorder="big", signed=True)
def set_custom_property(key: str, value: Any, blender_object: Object) -> None:
try:
#Expected c types: float, int, string, float[], int[]
blender_object[key] = value
except (OverflowError, TypeError) as ex:
print(f"Skipping setting property ({key}={value}) on {blender_object.name_full}, Reason: {ex}")
except Exception as ex:
#TODO: Log this as it's unexpected!!!
print(f"Skipping setting property ({key}={value}) on {blender_object.name_full}, Reason: {ex}")
def add_custom_properties(speckle_object: Base, blender_object: Object):
if blender_object is None:
return
blender_object["_speckle_type"] = type(speckle_object).__name__
app_id = getattr(speckle_object, "applicationId", None)
if app_id:
blender_object["applicationId"] = speckle_object.applicationId
keys = speckle_object.get_dynamic_member_names() if "Geometry" in speckle_object.speckle_type else (set(speckle_object.get_member_names()) - IGNORED_PROPERTY_KEYS)
for key in keys:
val = getattr(speckle_object, key, None)
if val is None:
continue
if isinstance(val, (int, str, float)):
set_custom_property(key, val, blender_object)
elif key == "properties" and isinstance(val, Base):
val["applicationId"] = None
add_custom_properties(val, blender_object)
elif isinstance(val, list):
items = [item for item in val if not isinstance(item, Base)]
if items:
set_custom_property(key, items, blender_object)
elif isinstance(val,dict):
for (k,v) in val.items():
if not isinstance(v, Base):
set_custom_property(k, v, blender_object)
def render_material_to_native(speckle_mat: RenderMaterial) -> Material:
mat_name = speckle_mat.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 blender_mat is None:
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.0:
blender_mat.blend_method = "BLEND"
return blender_mat
def get_render_material(speckle_object: Base) -> Optional[RenderMaterial]:
"""Trys to get a RenderMaterial on given speckle_object and convert it to a blender material"""
speckle_mat = getattr(
speckle_object,
"renderMaterial",
getattr(speckle_object, "@renderMaterial", None),
)
if not isinstance(speckle_mat, RenderMaterial):
return None
return speckle_mat
def add_vertices(speckle_mesh: Mesh, blender_mesh: BMesh, 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,
)
)
def add_faces(speckle_mesh: Mesh, blender_mesh: BMesh, indexOffset: int, materialIndex: int = 0, smooth:bool = True):
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[x + indexOffset] for x in sfaces[i : i + n]]
)
f.material_index = materialIndex
f.smooth = smooth
except Exception as e:
_report(f"Failed to create face for mesh {speckle_mesh.id} \n{e}")
i += n
def add_colors(speckle_mesh: Mesh, blender_mesh: BMesh):
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: Mesh, blender_mesh: BMesh):
s_uvs = speckle_mesh.textureCoordinates
if not s_uvs:
return
try:
uv = []
if len(s_uvs) // 2 == len(blender_mesh.verts):
uv.extend(
(float(s_uvs[i]), float(s_uvs[i + 1]))
for i in range(0, len(s_uvs), 2)
)
else:
_report(
f"Failed to match UV coordinates to vert data. Blender mesh verts: {len(blender_mesh.verts)}, Speckle UVs: {len(s_uvs) // 2}"
)
return
# 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
ignored_keys = {
"id",
"speckle",
"speckle_type"
"_speckle_type",
"_speckle_name",
"_speckle_transform",
"_RNA_UI",
"elements",
"transform",
"_units",
"_chunkable",
}
def get_blender_custom_properties(obj, max_depth: int = 1000):
if max_depth < 0:
return obj
if hasattr(obj, "keys"):
keys = set(obj.keys()) - ignored_keys
return {
key: get_blender_custom_properties(obj[key], max_depth - 1)
for key in keys
if not key.startswith("_")
}
if isinstance(obj, (list, tuple, idprop.types.IDPropertyArray)):
return [get_blender_custom_properties(o, max_depth - 1) for o in obj]
return obj
"""
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
# 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]
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
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 link_object_to_collection_nested(obj: bpy.types.Object, col: bpy.types.Collection):
if obj.name not in col.objects:
col.objects.link(obj)
for child in obj.children:
link_object_to_collection_nested(child, col)
+1 -33
View File
@@ -1,4 +1,3 @@
from specklepy.api.client import SpeckleClient
from bpy_speckle.clients import speckle_clients
"""
@@ -34,7 +33,7 @@ def _report(msg):
print("SpeckleBlender: {}".format(msg))
def get_scale_length(units):
def get_scale_length(units: str) -> float:
if units.lower() in unit_scale.keys():
return unit_scale[units]
_report("Units <{}> are not supported.".format(units))
@@ -71,34 +70,3 @@ def _check_speckle_client_user_stream(scene):
print("Account contains no streams.")
return (user, stream)
def _create_stream(user, stream_name, units="Millimeters"):
"""
Create a new stream
"""
# TODO: double-check, but this should not be accessible through the UI if
# there aren't any active users anyway
# user = context.scene.speckle.users[int(context.scene.speckle.active_user)]
client = speckle_clients[int(bpy.context.scene.speckle.active_user)]
return client.stream.create(
name=stream_name, description="This is a Blender stream.", is_public=True
)
# TODO: Update stream with properties such as units, etc.
def _delete_stream(client, user, stream):
"""
Delete the active stream
TODO: probably doesn't need to be a separate function and can be
folded into the operator
"""
user = context.scene.speckle.users[int(context.scene.speckle.active_user)]
client = speckle_clients[int(context.scene.speckle.active_user)]
if stream:
res = client.streams.delete(stream.id)
_report(res["message"])
-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
+207
View File
@@ -0,0 +1,207 @@
"""
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")
assert path.exists()
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)
print(f"Installing Speckle dependencies to {path}")
from subprocess import run
completed_process = run(
[
PYTHON_PATH,
"-m",
"pip",
"install",
"-t",
str(path),
"-r",
str(get_requirements_path()),
],
capture_output=True,
text=True,
)
if completed_process.returncode != 0:
m = f"Failed to install dependenices through pip, got {completed_process.returncode} return code"
print(m)
raise Exception(m)
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}!")
+2 -3
View File
@@ -1,9 +1,8 @@
from .users import LoadUsers, LoadUserStreams
from .users import LoadUsers, LoadUserStreams, ResetUsers
from .object import (
UpdateObject,
ResetObject,
DeleteObject,
UploadObject,
UploadNgonsAsPolylines,
SelectIfSameCustomProperty,
SelectIfHasCustomProperty,
@@ -28,6 +27,7 @@ from .misc import OpenSpeckleGuide, OpenSpeckleTutorials, OpenSpeckleForum
operator_classes = [
LoadUsers,
ResetUsers,
ReceiveStreamObjects,
SendStreamObjects,
LoadUserStreams,
@@ -43,7 +43,6 @@ operator_classes.extend(
UpdateObject,
ResetObject,
DeleteObject,
UploadObject,
UploadNgonsAsPolylines,
SelectIfSameCustomProperty,
SelectIfHasCustomProperty,
+33 -41
View File
@@ -1,29 +1,17 @@
"""
Commit operators
"""
import bpy, os
from bpy.props import (
StringProperty,
BoolProperty,
FloatProperty,
CollectionProperty,
EnumProperty,
)
from bpy_speckle.functions import (
_check_speckle_client_user_stream,
_create_stream,
get_scale_length,
_report,
)
from bpy_speckle.convert import from_speckle_object
import bpy
from bpy.props import BoolProperty
from bpy_speckle.functions import _check_speckle_client_user_stream, _report
from bpy_speckle.clients import speckle_clients
from bpy_speckle.properties.scene import SpeckleSceneSettings
class DeleteCommit(bpy.types.Operator):
"""
Delete stream
Deletes the selected commit from the selected stream.
To execute from code, call: `bpy.ops.speckle.delete_commit(are_you_sure=True)`
"""
bl_idname = "speckle.delete_commit"
@@ -32,7 +20,8 @@ class DeleteCommit(bpy.types.Operator):
bl_description = "Delete active commit permanently"
are_you_sure: BoolProperty(
name="Confirm", default=False,
name="Confirm",
default=False,
)
def draw(self, context):
@@ -50,36 +39,39 @@ class DeleteCommit(bpy.types.Operator):
def execute(self, context):
if not self.are_you_sure:
_report(f"{self.bl_idname}: cancelled by user")
return {"CANCELLED"}
self.are_you_sure = False
speckle = context.scene.speckle
speckle: SpeckleSceneSettings = context.scene.speckle
check = _check_speckle_client_user_stream(context.scene)
if check is None:
user = speckle.get_active_user()
if user is None:
print(f"{self.bl_idname}: failed - No user selected/found")
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:
stream = user.get_active_stream()
if stream is None:
print(f"{self.bl_idname}: failed - No stream selected/found")
return {"CANCELLED"}
else:
branch = stream.branches[int(stream.branch)]
if len(branch.commits) < 1:
return {"CANCELLED"}
else:
commit = branch.commits[int(branch.commit)]
deleted = client.commit.delete(stream_id=stream.id, commit_id=commit.id)
branch = stream.get_active_branch()
if branch is None:
print(f"{self.bl_idname}: failed - No branch selected/found")
return {"CANCELLED"}
return {"FINISHED"}
bpy.ops.speckle.load_user_streams()
context.view_layer.update()
if context.area:
context.area.tag_redraw()
commit = branch.get_active_commit()
if commit is None:
print(f"{self.bl_idname}: failed - No commit selected/found")
return {"CANCELLED"}
client = speckle_clients[int(speckle.active_user)]
deleted = client.commit.delete(stream_id=stream.id, commit_id=commit.id)
if not deleted:
print(f"{self.bl_idname}: failed - Delete operation failed")
return {"CANCELLED"}
print(f"{self.bl_idname}: succeeded - commit {commit.id} ({commit.message}) has been deleted from stream {stream.id}")
return {"FINISHED"}
+13 -79
View File
@@ -2,19 +2,13 @@
Object operators
"""
import bpy, bmesh, os
from bpy.props import (
StringProperty,
BoolProperty,
FloatProperty,
CollectionProperty,
EnumProperty,
import bpy
from bpy.props import BoolProperty, EnumProperty
from deprecated import deprecated
from bpy_speckle.convert.to_speckle import (
convert_to_speckle,
ngons_to_speckle_polylines,
)
from specklepy.api.client import SpeckleClient
from bpy_speckle.convert import to_speckle_object
from bpy_speckle.convert.to_speckle import export_ngons_as_polylines
from bpy_speckle.functions import get_scale_length, _report
from bpy_speckle.clients import speckle_clients
@@ -35,7 +29,6 @@ class UpdateObject(bpy.types.Operator):
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)
@@ -58,7 +51,7 @@ class UpdateObject(bpy.types.Operator):
stream_units
)
sm = to_speckle_object(active, scale)
sm = convert_to_speckle(active, scale)
_report("Updating object {}".format(sm["_id"]))
client.objects.update(active.speckle.object_id, sm)
@@ -99,8 +92,6 @@ class DeleteObject(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
user = context.scene.speckle.users[int(context.scene.speckle.active_user)]
client = speckle_clients[int(context.scene.speckle.active_user)]
active = context.object
if active.speckle.enabled:
@@ -112,13 +103,11 @@ class DeleteObject(bpy.types.Operator):
]
if existing is None:
return {"CANCELLED"}
# print("Existing: %s" % SpeckleResource.to_json_pretty(existing))
new_objects = [
x
for x in res["resource"]["objects"]
if x["_id"] != active.speckle.object_id
]
# print (SpeckleResource.to_json_pretty(new_objects))
res = client.GetLayers(active.speckle.stream_id)
new_layers = res["resource"]["layers"]
@@ -138,7 +127,7 @@ class DeleteObject(bpy.types.Operator):
return {"FINISHED"}
@deprecated
class UploadNgonsAsPolylines(bpy.types.Operator):
"""
Upload mesh ngon faces as polyline outlines
@@ -162,11 +151,12 @@ class UploadNgonsAsPolylines(bpy.types.Operator):
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
)
# scale = context.scene.unit_settings.scale_length / get_scale_length(
# stream.units
# )
scale = 1.0
sp = export_ngons_as_polylines(active, scale)
sp = ngons_to_speckle_polylines(active, scale)
if sp is None:
return {"CANCELLED"}
@@ -175,16 +165,12 @@ class UploadNgonsAsPolylines(bpy.types.Operator):
for polyline in sp:
res = client.objects.create([polyline])
print(res)
if res is None:
_report(client.me)
continue
placeholders.extend(res)
# polyline['_id'] = res['_id']
# placeholders.append({'type':'Placeholder', '_id':res['_id']})
if not placeholders:
return {"CANCELLED"}
@@ -222,58 +208,6 @@ class UploadNgonsAsPolylines(bpy.types.Operator):
layout.prop(self, "clear_stream")
class UploadObject(bpy.types.Operator):
"""
DEPRECATED
Upload an individual object
"""
bl_idname = "speckle.upload_object"
bl_label = "Upload Object"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
active = context.active_object
if active is not None:
user = context.scene.speckle.users[int(context.scene.speckle.active_user)]
client = speckle_clients[int(context.scene.speckle.active_user)]
stream = user.streams[user.active_stream]
scale = context.scene.unit_settings.scale_length / get_scale_length(
stream.units
)
sm = to_speckle_object(active, scale)
placeholders = client.objects.create([sm])
if placeholders is None:
return {"CANCELLED"}
sstream = client.streams.get(stream.id)
sstream.objects.extend(placeholders)
N = sstream.layers[-1].objectCount
sstream.layers[-1].objectCount = N + 1
sstream.layers[-1].topology = "0-%s" % (N + 1)
_report("Updating stream %s" % stream.id)
res = client.streams.update(stream["id"], sstream)
_report(res)
active.speckle.enabled = True
active.speckle.object_id = sm.id
active.speckle.stream_id = stream.id
active.speckle.send_or_receive = "send"
context.view_layer.update()
_report("Done.")
return {"FINISHED"}
def get_custom_speckle_props(self, context):
ignore = ["speckle", "cycles", "cycles_visibility"]
+254 -103
View File
@@ -2,40 +2,45 @@
Stream operators
"""
from itertools import chain
from typing import Dict
import bpy, bmesh, os
from math import radians
from typing import Callable, Dict, Iterable, Optional
import bpy
from bpy_speckle.convert.util import link_object_to_collection_nested
from bpy_speckle.properties.scene import SpeckleSceneSettings
from specklepy.api.models import Commit
import webbrowser
from bpy.props import (
StringProperty,
BoolProperty,
FloatProperty,
CollectionProperty,
EnumProperty,
)
from bpy.types import Context, Object
from bpy_speckle.convert.to_native import (
can_convert_to_native,
convert_to_native,
set_convert_instances_as,
)
from bpy_speckle.convert.to_speckle import (
convert_to_speckle,
)
from bpy_speckle.functions import (
_check_speckle_client_user_stream,
_create_stream,
get_scale_length,
_report,
)
from bpy_speckle.convert import to_speckle_object, get_speckle_subobjects
from bpy_speckle.convert.to_speckle import export_ngons_as_polylines
from bpy_speckle.convert import from_speckle_object
from bpy_speckle.clients import speckle_clients
from bpy_speckle.operators.users import add_user_stream
from specklepy.api import operations
from specklepy.api.credentials import StreamWrapper
from specklepy.api import operations, host_applications
from specklepy.api.wrapper import StreamWrapper
from specklepy.api.resources.stream import Stream
from specklepy.transports.server import ServerTransport
from specklepy.objects import Base
from specklepy.objects.geometry import *
from specklepy.logging.exceptions import SpeckleException
from specklepy.logging import metrics
def get_objects_collections(base) -> Dict:
def get_objects_collections(base: Base) -> Dict[str, list]:
"""Create collections based on the dynamic members on a root commit object"""
collections = {}
for name in base.get_dynamic_member_names():
@@ -50,9 +55,11 @@ def get_objects_collections(base) -> Dict:
return collections
def get_objects_nested_lists(items, parent_col=None) -> List:
def get_objects_nested_lists(items: list, parent_col: Optional[bpy.types.Collection] = None) -> List:
"""For handling the weird nested lists that come from Grasshopper"""
objects = []
if not items:
return objects
if isinstance(items[0], list):
items = list(chain.from_iterable(items))
@@ -67,15 +74,10 @@ def get_objects_nested_lists(items, parent_col=None) -> List:
return objects
def get_objects_collections_recursive(base, parent_col=None) -> List:
def get_objects_collections_recursive(base: Base, parent_col: Optional[bpy.types.Collection] = None) -> List:
"""Recursively create collections based on the dynamic members on nested `Base` objects within the root commit object"""
# if it's a convertable (registered) class and not just a plain `Base`, return the object itself
object_type = Base.get_registered_type(base.speckle_type)
if (
(object_type and object_type != Base)
or hasattr(base, "displayMesh")
or hasattr(base, "displayValue")
):
if can_convert_to_native(base):
return [base]
# if it's an unknown type, try to drill further down to find convertable objects
@@ -84,20 +86,63 @@ def get_objects_collections_recursive(base, parent_col=None) -> List:
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)
objects.extend(item for item in value if isinstance(item, Base))
if isinstance(value, Base):
col = parent_col.children.get(name)
if not parent_col.children.get(name):
if not col:
col = create_collection(name)
parent_col.children.link(col)
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):
ObjectCallback = Optional[Callable[[bpy.types.Context, Object, Base], Object]]
ReceiveCompleteCallback = Optional[Callable[[bpy.types.Context, Dict[str, Object]], None]]
def get_receive_funcs(context: Context, created_objects: Dict[str, Object]) -> tuple[ObjectCallback, ReceiveCompleteCallback]:
"""
Fetches the injected callback functions from user specified "Receive Script"
"""
objectCallback: ObjectCallback = None
receiveCompleteCallback: ReceiveCompleteCallback = 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_for_each"):
objectCallback = mod.execute_for_each
elif hasattr(mod, "execute"):
objectCallback = lambda c, o, _ : mod.execute(c.scene, o)
if hasattr(mod, "execute_for_all"):
receiveCompleteCallback = mod.execute_for_all
progress = 0
def for_each_object(context: bpy.types.Context, obj: Object, base: Base) -> Object:
nonlocal progress
nonlocal created_objects
nonlocal objectCallback
progress += 1 #NOTE:XXX Progress bar never reaches 100 because func is only called for convertible objects
context.window_manager.progress_update(progress)
created_objects[obj.name] = obj
if objectCallback:
return objectCallback(context, obj, base)
else:
return obj
return (for_each_object, receiveCompleteCallback)
def bases_to_native(context: bpy.types.Context, collections: Dict[str, list], scale: float, stream_id: str, func: ObjectCallback = None):
for col_name, objects in collections.items():
col = bpy.data.collections[col_name]
existing = get_existing_collection_objs(col)
@@ -107,7 +152,7 @@ def bases_to_native(context, collections, scale, stream_id, func=None):
for obj in objects:
if isinstance(obj, dict):
bases_to_native(context, obj, scale, stream_id, func)
elif isinstance(obj, list):
elif isinstance(obj, list): #FIXME: wtf are these nested if statement, can this not be a recursive call?
for item in obj:
if isinstance(item, dict):
bases_to_native(context, item, scale, stream_id, func)
@@ -117,9 +162,10 @@ def bases_to_native(context, collections, scale, stream_id, func=None):
)
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}"
f"Something went wrong when receiving collection: {col_name}" #FIXME: undescript report message
)
bpy.context.view_layer.update()
@@ -128,16 +174,26 @@ def bases_to_native(context, collections, scale, stream_id, func=None):
context.area.tag_redraw()
def base_to_native(context, base, scale, stream_id, col, existing, func=None):
new_objects = [from_speckle_object(base, scale)]
if hasattr(base, "properties") and base.properties is not None:
new_objects.extend(get_speckle_subobjects(base.properties, scale, base.id))
elif isinstance(base, dict) and "properties" in base.keys():
new_objects.extend(
get_speckle_subobjects(base["properties"], scale, base["id"])
)
def base_to_native(context: bpy.types.Context,
base: Base,
scale: float,
stream_id: str,
col: bpy.types.Collection,
existing: Dict[str, Object],
func: ObjectCallback = None
):
new_objects = convert_to_native(base)
#NOTE: this code is ancient, and in testing does nothing, so we are removing it.
# 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
"""
@@ -149,13 +205,12 @@ def base_to_native(context, base, scale, stream_id, col, existing, func=None):
Run injected function
"""
if func:
new_object = func(context.scene, new_object)
new_object = func(context, new_object, base) #this base object isn't always the right one for hosted elements! #TODO: may be it now, need to double check!
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__))
): # If the injected function returned None, then we should ignore this object.
_report(f"Script '{func.__module__}' returned None.")
continue
new_object.speckle.stream_id = stream_id
@@ -163,15 +218,16 @@ def base_to_native(context, base, scale, stream_id, col, existing, func=None):
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"
existing[new_object.speckle.object_id].name = f"{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)
link_object_to_collection_nested(new_object, col)
#if new_object.name not in col.objects:
#col.objects.link(new_object)
def create_collection(name, clear_collection=True):
def create_collection(name: str, clear_collection=True) -> bpy.types.Collection:
if name in bpy.data.collections:
col = bpy.data.collections[name]
if clear_collection:
@@ -183,19 +239,19 @@ def create_collection(name, clear_collection=True):
return col
def create_child_collections(parent_col, children_names):
def create_child_collections(parent_col: bpy.types.Collection, children_names: Iterable[str]):
for name in children_names:
col = create_collection(name)
parent_col.children.link(col)
def get_existing_collection_objs(col):
def get_existing_collection_objs(col: bpy.types.Collection) -> Dict[str, bpy.types.Object]:
return {
obj.speckle.object_id: obj for obj in col.objects if obj.speckle.object_id != ""
}
def get_collection_parents(collection, names):
def get_collection_parents(collection: bpy.types.Collection, names: list[str]) -> None:
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
@@ -204,7 +260,7 @@ def get_collection_parents(collection, names):
get_collection_parents(parent, names)
def get_collection_hierarchy(collection):
def get_collection_hierarchy(collection: Optional[bpy.types.Collection]) -> list[str]:
if not collection:
return []
names = [collection.name.replace("/", "::").replace(".", "::")]
@@ -213,7 +269,7 @@ def get_collection_hierarchy(collection):
return names
def create_nested_hierarchy(base, hierarchy, objects):
def create_nested_hierarchy(base: Base, hierarchy: List[str], objects: Any):
child = base
while hierarchy:
@@ -223,13 +279,22 @@ def create_nested_hierarchy(base, hierarchy, objects):
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)
if not hasattr(child, "@elements"):
child["@elements"] = []
child["@elements"].extend(objects)
return base
#RECEIVE_MODES = [#TODO: modes
# ("create", "Create", "Add new geometry, without removing any existing objects"),
# ("replace", "Replace", "Replace objects from previous receive operations from the same stream"),
# #("update","Update", "") #TODO: update mode!
#]
INSTANCES_SETTINGS = [
("collection_instance", "Collection Instace", "Receive Instances as Collection Instances"),
("linked_duplicates", "Linked Duplicates", "Receive Instances as Linked Duplicates"),
]
class ReceiveStreamObjects(bpy.types.Operator):
"""
@@ -241,50 +306,121 @@ class ReceiveStreamObjects(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
bl_description = "Receive objects from active stream"
clean_meshes: BoolProperty(name="Clean Meshes", default=False)
#receive_mode: EnumProperty(items=RECEIVE_MODES, name="Receive Type", default="replace", description="The behaviour of the recieve operation")
receive_instances_as: EnumProperty(items=INSTANCES_SETTINGS, name="Receive Instances As", default="collection_instance", description="How to receive speckle Instances")
def draw(self, context):
layout = self.layout
col = layout.column()
col.prop(self, "clean_meshes")
#col.prop(self, "receive_mode")
col.prop(self, "receive_instances_as")
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self)
@staticmethod
def clean_converted_meshes(context: bpy.types.Context, convertedObjects: dict[str, Object]):
bpy.ops.object.select_all(action='DESELECT')
active = None
for obj in convertedObjects.values():
if obj.type != 'MESH':
continue
obj.select_set(True, view_layer=context.scene.view_layers[0])
active = obj
if active == None:
return
context.view_layer.objects.active = active
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action='SELECT')
bpy.ops.mesh.remove_doubles()
bpy.ops.mesh.dissolve_limited(angle_limit=radians(0.1))
# Reset state to previous (not quite sure if this is 100% necessary)
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.select_all(action='DESELECT')
bpy.context.view_layer.objects.active = None
def execute(self, context):
bpy.context.view_layer.objects.active = None
check = _check_speckle_client_user_stream(context.scene)
if check is None:
speckle: SpeckleSceneSettings = context.scene.speckle
#Get UI Selection
user = speckle.get_active_user()
if not user:
print("No user selected/found")
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:
stream = user.get_active_stream()
if not stream:
print("No stream selected/found")
return {"CANCELLED"}
if not stream.branches:
branch = stream.get_active_branch()
if not branch:
print("No branch selected/found")
return {"CANCELLED"}
branch = stream.branches.items[int(bstream.branch)]
bbranch = bstream.branches[int(bstream.branch)]
if branch.commits.totalCount < 1:
print("No commits found. Probably an empty stream.")
commit = branch.get_active_commit()
if commit is None:
print("No commit selected/found")
return {"CANCELLED"}
commit = branch.commits.items[int(bbranch.commit)]
#Get actual stream data
client = speckle_clients[int(speckle.active_user)]
transport = ServerTransport(client, stream.id)
stream_data = operations.receive(commit.referencedObject, transport)
transport = ServerTransport(stream.id, client)
metrics.track(
metrics.RECEIVE,
getattr(transport, "account", None),
custom_props={
"sourceHostApp": host_applications.get_host_app_from_string(commit.source_application).slug,
"sourceHostAppVersion": commit.source_application
},
)
commit_object = operations._untracked_receive(commit.referenced_object, transport)
client.commit.received(
stream.id,
commit.id,
source_application="blender",
message="received commit from Speckle Blender",
)
context.window_manager.progress_begin(0, commit_object.totalChildrenCount or 1)
set_convert_instances_as(self.receive_instances_as) #HACK: we need a better way to pass settings down to the converter
"""
Create or get Collection for stream objects
"""
collections = get_objects_collections(stream_data)
collections = get_objects_collections(commit_object)
if not collections:
print("Unusual commit structure - did not correctly create collections")
return {"CANCELLED"}
name = "{} [ {} @ {} ]".format(stream.name, branch.name, commit.id)
# name = ""
# if self.receive_mode == "create":
name = "{} [ {} @ {} ]".format(stream.name, branch.name, commit.id) # Matches Rhino "Create" naming
# else:
# name = stream.name # Doesn't quite match rhino's Update layer naming, but is close enough no?
col = create_collection(name)
col.speckle.stream_id = stream.id
col.speckle.name = stream.name
col.speckle.units = stream_data.units
col.speckle.units = commit_object.units or "m"
if col.name not in bpy.context.scene.collection.children:
bpy.context.scene.collection.children.link(col)
@@ -297,25 +433,35 @@ class ReceiveStreamObjects(bpy.types.Operator):
Set conversion scale from stream units
"""
scale = (
get_scale_length(stream_data.units)
get_scale_length(col.speckle.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
created_objects = {}
(func, on_complete) = get_receive_funcs(context, created_objects)
"""
Iterate through retrieved resources
"""
bases_to_native(context, collections, scale, stream.id, func)
context.window_manager.progress_end()
if self.clean_meshes:
self.clean_converted_meshes(context, created_objects)
if on_complete:
on_complete(context, created_objects)
return {"FINISHED"}
class SendStreamObjects(bpy.types.Operator):
@@ -360,18 +506,24 @@ class SendStreamObjects(bpy.types.Operator):
return {"CANCELLED"}
check = _check_speckle_client_user_stream(context.scene)
if check is None:
user, bstream = check
if user 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()
)
# scale = context.scene.unit_settings.scale_length / get_scale_length(
# stream.units.lower()
# )
scale = 1.0
units = "m" if bpy.context.scene.unit_settings.system == "METRIC" else "ft"
"""
Get script from text editor for injection
@@ -406,18 +558,14 @@ class SendStreamObjects(bpy.types.Operator):
_report("Converting {}".format(obj.name))
ngons = obj.get("speckle_ngons_as_polylines", False)
if ngons:
converted = export_ngons_as_polylines(obj, scale)
else:
converted = to_speckle_object(
obj,
scale,
bpy.context.evaluated_depsgraph_get()
if self.apply_modifiers
else None,
)
converted = convert_to_speckle(
obj,
scale,
units,
bpy.context.evaluated_depsgraph_get()
if self.apply_modifiers
else None,
)
if not converted:
continue
@@ -434,19 +582,22 @@ class SendStreamObjects(bpy.types.Operator):
hierarchy = get_collection_hierarchy(collection)
create_nested_hierarchy(base, hierarchy, objects)
transport = ServerTransport(client, stream.id)
transport = ServerTransport(stream.id, client)
_report(f"Sending to {stream}")
obj_id = operations.send(
base,
[transport],
)
client.commit.create(
commitId = client.commit.create(
stream.id,
obj_id,
branch.name,
message=self.commit_message,
source_application="blender",
)
_report(f"Commit Created {user.server_url}/streams/{stream.id}/commits/{commitId}")
bpy.ops.speckle.load_user_streams()
@@ -514,7 +665,7 @@ class AddStreamFromURL(bpy.types.Operator):
user = speckle.users[user_index]
client = speckle_clients[user_index]
stream = client.stream.get(wrapper.stream_id)
stream = client.stream.get(wrapper.stream_id, branch_limit=20)
if not isinstance(stream, Stream):
raise SpeckleException("Could not get the requested stream")
@@ -563,7 +714,7 @@ class CreateStream(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
bl_description = "Create new stream"
stream_name: StringProperty(name="Stream name", default="SpeckleStream")
stream_name: StringProperty(name="Stream name")
stream_description: StringProperty(
name="Stream description", default="This is a Blender stream."
)
+43 -28
View File
@@ -1,24 +1,38 @@
"""
User account operators
"""
from typing import cast
import bpy, bmesh, os
from bpy.props import (
StringProperty,
BoolProperty,
FloatProperty,
CollectionProperty,
EnumProperty,
)
from bpy_speckle.properties.scene import SpeckleUserObject
import bpy
from bpy_speckle.functions import _report
from bpy_speckle.clients import speckle_clients
from bpy_speckle.properties.scene import SpeckleCommitObject, SpeckleSceneSettings
from specklepy.api.client import SpeckleClient
from specklepy.api.credentials import get_default_account, get_local_accounts
from specklepy.api.models import Stream, User
from specklepy.api.credentials import get_local_accounts
from datetime import datetime
class ResetUsers(bpy.types.Operator):
"""
Reset loaded users
"""
bl_idname = "speckle.users_reset"
bl_label = "Reset users"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
self.reset_ui(context)
bpy.context.view_layer.update()
if context.area:
context.area.tag_redraw()
return {"FINISHED"}
@staticmethod
def reset_ui(context: bpy.types.Context):
speckle: SpeckleSceneSettings = context.scene.speckle
speckle.users.clear()
speckle_clients.clear()
class LoadUsers(bpy.types.Operator):
"""
@@ -33,12 +47,13 @@ class LoadUsers(bpy.types.Operator):
_report("Loading users...")
users = context.scene.speckle.users
speckle: SpeckleSceneSettings = context.scene.speckle
users = speckle.users
context.scene.speckle.users.clear()
speckle_clients.clear()
ResetUsers.reset_ui(context)
profiles = get_local_accounts()
active_user_index = 0
for profile in profiles:
user = users.add()
@@ -47,22 +62,21 @@ class LoadUsers(bpy.types.Operator):
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)
client.authenticate_with_account(profile)
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)
active_user_index = len(users) - 1
context.scene.speckle.active_user_index = int(context.scene.speckle.active_user)
bpy.ops.speckle.load_user_streams()
#speckle.active_user_index = int(speckle.active_user) #TODO Wtf is this?
speckle.active_user = str(active_user_index)
bpy.context.view_layer.update()
if context.area:
@@ -70,7 +84,7 @@ class LoadUsers(bpy.types.Operator):
return {"FINISHED"}
def add_user_stream(user, stream):
def add_user_stream(user: User, stream: Stream):
s = user.streams.add()
s.name = stream.name
s.id = stream.id
@@ -88,13 +102,14 @@ def add_user_stream(user, stream):
continue
for c in b.commits.items:
commit = branch.commits.add()
commit: SpeckleCommitObject = branch.commits.add()
commit.id = commit.name = c.id
commit.message = c.message
commit.message = c.message or ""
commit.author_name = c.authorName
commit.author_id = c.authorId
commit.created_at = c.createdAt
commit.created_at = datetime.strftime(c.createdAt, "%Y-%m-%d %H:%M:%S.%f%Z")
commit.source_application = str(c.sourceApplication)
commit.referenced_object = c.referencedObject
if hasattr(s, "baseProperties"):
s.units = stream.baseProperties.units
@@ -122,7 +137,7 @@ class LoadUserStreams(bpy.types.Operator):
try:
streams = client.stream.list(stream_limit=20)
except Exception as e:
_report("Failed to retrieve streams: {}".format(e))
_report(f"Failed to retrieve streams: {e}")
return
if not streams:
_report("Failed to retrieve streams.")
@@ -133,7 +148,7 @@ class LoadUserStreams(bpy.types.Operator):
default_units = "Meters"
for s in streams:
sstream = client.stream.get(id=s.id)
sstream = client.stream.get(id=s.id, branch_limit=20)
add_user_stream(user, sstream)
bpy.context.view_layer.update()
-2
View File
@@ -1,9 +1,7 @@
"""
Addon properties
"""
import bpy
from bpy.props import BoolProperty
class SpeckleAddonPreferences(bpy.types.AddonPreferences):
-2
View File
@@ -1,9 +1,7 @@
"""
Collection properties
"""
import bpy
from bpy.props import StringProperty, BoolProperty, EnumProperty
class SpeckleCollectionSettings(bpy.types.PropertyGroup):
-2
View File
@@ -1,9 +1,7 @@
"""
Object properties
"""
import bpy
from bpy.props import StringProperty, BoolProperty, EnumProperty
class SpeckleObjectSettings(bpy.types.PropertyGroup):
+37 -14
View File
@@ -1,7 +1,7 @@
"""
Scene properties
"""
from typing import Optional
import bpy
from bpy.props import (
StringProperty,
@@ -12,7 +12,6 @@ from bpy.props import (
IntProperty,
PointerProperty,
)
from specklepy.api.client import SpeckleClient
class SpeckleSceneObject(bpy.types.PropertyGroup):
@@ -20,12 +19,13 @@ class SpeckleSceneObject(bpy.types.PropertyGroup):
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")
id: StringProperty(default="")
message: StringProperty(default="")
author_name: StringProperty(default="")
author_id: StringProperty(default="")
created_at: StringProperty(default="")
source_application: StringProperty(default="")
referenced_object: StringProperty(default="")
class SpeckleBranchObject(bpy.types.PropertyGroup):
@@ -44,6 +44,12 @@ class SpeckleBranchObject(bpy.types.PropertyGroup):
description="Active commit",
items=get_commits,
)
def get_active_commit(self) -> Optional[SpeckleCommitObject]:
selected_index = int(self.commit)
if 0 <= selected_index < len(self.commits):
return self.commits[selected_index]
return None
class SpeckleStreamObject(bpy.types.PropertyGroup):
@@ -68,6 +74,12 @@ class SpeckleStreamObject(bpy.types.PropertyGroup):
items=get_branches,
)
def get_active_branch(self) -> Optional[SpeckleBranchObject]:
selected_index = int(self.branch)
if 0 <= selected_index < len(self.branches):
return self.branches[selected_index]
return None
class SpeckleUserObject(bpy.types.PropertyGroup):
server_name: StringProperty(default="SpeckleXYZ")
@@ -75,16 +87,21 @@ class SpeckleUserObject(bpy.types.PropertyGroup):
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)
def get_active_stream(self) -> Optional[SpeckleStreamObject]:
selected_index = int(self.active_stream)
if 0 <= selected_index < len(self.streams):
return self.streams[selected_index]
return None
class SpeckleSceneSettings(bpy.types.PropertyGroup):
def get_scripts(self, context):
seq = [("<none>", "<none>", "<none>")]
seq.extend([(t.name, t.name, t.name) for t in bpy.data.texts])
return seq
return [
("<none>", "<none>", "<none>"),
*[(t.name, t.name, t.name) for t in bpy.data.texts],
]
streams: EnumProperty(
name="Available streams",
@@ -105,8 +122,8 @@ class SpeckleSceneSettings(bpy.types.PropertyGroup):
active_user: EnumProperty(
items=get_users,
name="User",
description="Select user",
name="Account",
description="Select account",
update=set_user,
get=None,
set=None,
@@ -133,3 +150,9 @@ class SpeckleSceneSettings(bpy.types.PropertyGroup):
description="Script to run when sending stream objects.",
items=get_scripts,
)
def get_active_user(self) -> Optional[SpeckleUserObject]:
selected_index = int(self.active_user)
if 0 <= selected_index < len(self.users):
return self.users[selected_index]
return None
+10 -16
View File
@@ -12,7 +12,7 @@ from bpy.props import (
EnumProperty,
)
import datetime
from datetime import datetime
"""
Compatibility
@@ -84,9 +84,8 @@ class VIEW3D_UL_SpeckleStreams(bpy.types.UIList):
def draw_item(self, context, layout, data, stream, active_data, active_propname):
if self.layout_type in {"DEFAULT", "COMPACT"}:
if stream:
# layout.prop(user, "name", text=user.name, emboss=False, icon_value=0)
layout.label(
text="{} ({})".format(stream.name, stream.id),
text=f"{stream.name} ({stream.id})",
translate=False,
icon_value=0,
)
@@ -116,14 +115,14 @@ class VIEW3D_PT_SpeckleUser(bpy.types.Panel):
col = layout.column()
if len(speckle.users) < 1:
col.label(text="No users found.")
col.label(text="Refresh to initialise")
else:
# col.label(text="User")
col.prop(speckle, "active_user", text="")
user = speckle.users[int(speckle.active_user)]
col.label(text="{} ({})".format(user.server_name, user.server_url))
col.label(text="{} ({})".format(user.name, user.email))
col.operator("speckle.users_load", text="", icon="FILE_REFRESH")
class VIEW3D_PT_SpeckleStreams(bpy.types.Panel):
"""
@@ -179,7 +178,7 @@ class VIEW3D_PT_SpeckleActiveStream(bpy.types.Panel):
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.label(text=f"{stream.name} ({stream.id})")
row.operator("speckle.stream_copy_id", text="", icon="COPY_ID")
col.separator()
@@ -207,13 +206,11 @@ class VIEW3D_PT_SpeckleActiveStream(bpy.types.Panel):
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)
dt = datetime.strptime(
commit.created_at, "%Y-%m-%d %H:%M:%S.%f%Z"
)
col.label(text=f"{dt.ctime()}")
col.label(text=f"{commit.author_name} ({commit.author_id})")
col.label(text=commit.source_application)
else:
col.label(text="No branches found!")
@@ -234,9 +231,6 @@ class VIEW3D_PT_SpeckleActiveStream(bpy.types.Panel):
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()
+4
View File
@@ -0,0 +1,4 @@
#!/usr/bin/env bash
set -e -o pipefail
poetry export --only main -o bpy_speckle/requirements.txt
+20
View File
@@ -0,0 +1,20 @@
import sys
from pathlib import Path
def patch_installer(tag: str):
"""Patches the installer with the correct connector version and specklepy version"""
tag = tag.replace("\n", "")
iss_file = "speckle-sharp-ci-tools/blender.iss"
iss_path = Path(iss_file)
lines = iss_path.read_text().split("\n")
lines.insert(12, f'#define AppVersion "{tag.split("-")[0]}"')
lines.insert(13, f'#define AppInfoVersion "{tag}"')
iss_path.write_text("\n".join(lines))
print(f"Patched installer with connector v{tag}")
if __name__ == "__main__":
tag = sys.argv[1]
patch_installer(tag)
+2 -36
View File
@@ -1,7 +1,6 @@
import re
import sys
def patch_connector(tag):
"""Patches the connector version within the connector init file"""
bpy_file = "bpy_speckle/__init__.py"
@@ -19,46 +18,13 @@ def patch_connector(tag):
with open(bpy_file, "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:
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)
patch_connector(tag.split("-")[0])
if __name__ == "__main__":
Generated
+947 -650
View File
File diff suppressed because it is too large Load Diff
+12 -8
View File
@@ -2,18 +2,22 @@
name = "speckle-blender"
version = "2.0.0"
description = "the Speckle 2.0 connector for Blender!"
authors = ["izzy lyseggen <izzy.lyseggen@gmail.com>"]
authors = ["izzy lyseggen <izzy.lyseggen@gmail.com>", "Gergő Jedlicska <gergo@jedlicska.com>"]
license = "Apache-2.0"
[tool.poetry.dependencies]
python = ">=3.7,<3.8"
specklepy = "2.3.5"
python = ">=3.8, <4.0.0"
specklepy = "^2.13.0"
[tool.poetry.dev-dependencies]
devtools = "^0.6.1"
numpy = "^1.20.2"
bpy = "^2.82.1"
bpy-build = "^2.1.0"
# [tool.poetry.group.local_specklepy.dependencies]
# specklepy = {path = "../specklepy", develop = true}
[tool.poetry.group.dev.dependencies]
numpy = "^1.23.5"
fake-bpy-module-latest = "^20230117"
black = "^22.10.0"
pylint = "^2.15.7"
ruff = "^0.0.166"
[build-system]
requires = ["poetry-core>=1.0.0"]