Compare commits

...

108 Commits

Author SHA1 Message Date
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
izzy lyseggen 376a90c5cf Merge pull request #59 from specklesystems/izzy/texture-coords
feat(convert): texture coords out and material receive fix
2021-10-14 18:03:52 +02:00
izzy lyseggen b0f8dbd63e fix(ui): diff fix for hiding globals
this is acting kinda weird, but it's taking up too much time for me
and it works as is so whatever
2021-10-14 17:03:19 +01:00
izzy lyseggen 89a8232a47 chore: update specklepy 2021-10-14 16:23:15 +01:00
izzy lyseggen 985f23b015 fix(convert): temp fix for materials w no name/id 2021-10-14 16:22:35 +01:00
izzy lyseggen d15a480e64 feat(uvs): try adding active uv layer 2021-10-14 16:22:35 +01:00
izzy lyseggen b88af9ac08 chore: update specklepy to 2.3.4 2021-10-13 10:37:14 +01:00
36 changed files with 2308 additions and 1987 deletions
+279 -52
View File
@@ -7,42 +7,172 @@ orbs:
# Upload artifacts to s3
aws-s3: circleci/aws-s3@2.0.0
commands:
install-specklepy-windows: # Reusable job for installing `specklepy` for windows machines
parameters:
python-version:
type: string
default: "" # leave blank for blender v2.93
steps:
- when:
condition: << parameters.python-version >>
steps:
- run:
name: Upgrade python version << parameters.python-version >>
shell: powershell.exe
command: |
choco upgrade python --version=<< parameters.python-version >>
refreshenv
python --version
- run:
name: Install specklepy into modules directory
shell: powershell.exe
command: |
$pyarr=(python --version).split(' ')[1].split('.')
$pyver=($pyarr[0..1] -join '.')
echo "using python version:" $pyver
$specklepy=(python patch_version.py)
python -m pip install --target=./modules-$pyver specklepy==$specklepy
jobs:
build-connector: # Reusable job for basic connectors
build-connector-win: # Reusable job for basic connectors
executor:
name: win/default # comes with python 3.7.3
shell: cmd.exe
parameters:
slug:
type: string
default: ""
default: "blender"
installer:
type: boolean
default: false
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
- install-specklepy-windows
- install-specklepy-windows:
python-version: "3.9.10"
- install-specklepy-windows:
python-version: "3.10.2"
- 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
$tag = if([string]::IsNullOrEmpty($env:CIRCLE_TAG)) { "0.0.0" } else { $env:CIRCLE_TAG }
$semver = if($tag.Contains('/')) {$tag.Split("/")[1] } else { $tag }
$ver = if($semver.Contains('-')) {$semver.Split("-")[0] } else { $semver }
$version = "$($ver).$($env:CIRCLE_BUILD_NUM)"
$channel = if($semver.Contains('-')) { "prerelease" } else { "latest" }
New-Item -Force "speckle-sharp-ci-tools/Installers/blender/$channel.yml" -ItemType File -Value "version: $semver"
echo $semver
python patch_version.py $semver
- 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
- when:
condition: << parameters.installer >>
steps:
- persist_to_workspace:
root: ./
paths:
- speckle-sharp-ci-tools/Installers
build-connector-mac:
macos:
xcode: 12.5.1
parameters:
slug:
type: string
default: "blender-mac"
installername:
type: string
default: "SpeckleBlenderInstall"
runtime:
type: string
default: "osx-x64"
installer:
type: boolean
default: false
steps:
- checkout
- attach_workspace:
at: ./
- run:
name: Install mono
command: |
HOMEBREW_NO_AUTO_UPDATE=1 brew install mono
# Compress build files
- run:
name: Install dotnet
command: curl -sSL https://dot.net/v1/dotnet-install.sh | bash /dev/stdin
- run:
name: Build & Patch Version
command: |
TAG=$(if [ "${CIRCLE_TAG}" ]; then echo $CIRCLE_TAG; else echo "0.0.0"; fi;)
SEMVER=$(echo "$TAG" | sed -e 's/[a-zA-Z-]*\///')
VER=$(echo "$SEMVER" | sed -e 's/-beta//')
VERSION=$(echo $VER.$CIRCLE_BUILD_NUM)
CHANNEL=$(if [[ "$VERSION" == *"-"* ]]; then echo $(cut -d "-" -f2 \<\<\< $VERSION); else echo latest; fi)
mkdir -p speckle-sharp-ci-tools/Installers/<< parameters.slug >>
if [ "${CIRCLE_TAG}" ]; then echo "version: $SEMVER" > "speckle-sharp-ci-tools/Installers/<< parameters.slug >>/$CHANNEL.yml"; fi
python3 patch_version.py $SEMVER
# update python and package dependencies
- when:
condition:
and:
# - << parameters.installer >>
- equal: [osx-x64, << parameters.runtime >>]
steps:
- run:
name: Install python 3.10
command: |
brew install python@3.10
brew link --overwrite python@3.10
- run:
name: Package specklepy dependencies for blender 3.1 & 3.2
command: |
python3 --version
python3 -m pip install --target=./modules-intel-3.10 specklepy==$(python3 -m patch_version)
zip -r modules-intel-3.10.zip modules-intel-3.10/
cp modules-intel-3.10.zip speckle-sharp-ci-tools/Mac/<<parameters.installername>>/.installationFiles
- run:
name: Zip Connector files
command: |
zip -r << parameters.slug >>.zip bpy_speckle/
- run:
name: Copy connector files to installer
command: |
mkdir -p speckle-sharp-ci-tools/Mac/<<parameters.installername>>/.installationFiles/
cp << parameters.slug >>.zip speckle-sharp-ci-tools/Mac/<<parameters.installername>>/.installationFiles
python3 patch_version.py > speckle-sharp-ci-tools/Mac/<<parameters.installername>>/.installationFiles/specklepy_ver.yml
- run:
name: Build Mac installer
command: ~/.dotnet/dotnet publish speckle-sharp-ci-tools/Mac/<<parameters.installername>>/<<parameters.installername>>.sln -r << parameters.runtime >> -c Release
- run:
name: Zip installer
command: |
cd speckle-sharp-ci-tools/Mac/<<parameters.installername>>/bin/Release/net6.0/<< parameters.runtime >>/
zip -r << parameters.slug >>.zip ./
- store_artifacts:
path: speckle-sharp-ci-tools/Mac/<<parameters.installername>>/bin/Release/net6.0/<< parameters.runtime >>/<< parameters.slug >>.zip
- run:
name: Copy to installer location
command: |
TAG=$(if [ "${CIRCLE_TAG}" ]; then echo $CIRCLE_TAG; else echo "0.0.0"; fi;)
SEMVER=$(echo "$TAG" | sed -e 's/[a-zA-Z-]*\///')
cp speckle-sharp-ci-tools/Mac/<<parameters.installername>>/bin/Release/net6.0/<< parameters.runtime >>/<< parameters.slug >>.zip speckle-sharp-ci-tools/Installers/<< parameters.slug >>/<< parameters.slug >>-$SEMVER.zip
- when:
condition: << parameters.installer >>
steps:
- 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
@@ -54,30 +184,45 @@ jobs:
root: ./
paths:
- speckle-sharp-ci-tools
- persist_to_workspace:
root: ./
paths:
- speckle-sharp-ci-tools
deploy: # Uploads all installers found to S3
deploy-connector-new:
docker:
- image: cimg/base:2021.01
- image: mcr.microsoft.com/dotnet/sdk:6.0
parameters:
slug:
type: string
file_slug:
type: string
os:
type: string
extension:
type: string
arch:
type: string
default: Any
steps:
- checkout
- attach_workspace:
at: ./
- run:
name: List contents
command: ls -R speckle-sharp-ci-tools/Installers
- aws-s3/copy:
arguments: "--recursive --endpoint=https://$SPACES_REGION.digitaloceanspaces.com --acl public-read"
aws-access-key-id: SPACES_KEY
aws-region: SPACES_REGION
aws-secret-access-key: SPACES_SECRET
from: '"speckle-sharp-ci-tools/Installers/"'
to: s3://speckle-releases/installers/
name: Install Manager Feed CLI
command: dotnet tool install --global Speckle.Manager.Feed
- run:
name: Upload new version
command: |
TAG=$(if [ "${CIRCLE_TAG}" ]; then echo $CIRCLE_TAG; else echo "0.0.0"; fi;)
SEMVER=$(echo "$TAG" | sed -e 's/\/[a-zA-Z-]*//')
/root/.dotnet/tools/Speckle.Manager.Feed deploy \
-s << parameters.slug >> \
-v ${SEMVER} \
-u https://releases.speckle.dev/installers/<< parameters.file_slug >>/<< parameters.file_slug >>-${SEMVER}.<< parameters.extension >> \
-o << parameters.os >> \
-a << parameters.arch >> \
-f speckle-sharp-ci-tools/Installers/<< parameters.file_slug >>/<< 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:
@@ -85,39 +230,121 @@ workflows:
only:
- main
- /ci\/.*/
- build-connector-win:
name: Windows Build
requires:
- get-ci-tools
filters:
branches:
only:
- main
- /ci\/.*/
- build-connector-mac:
name: Mac ARM Build
slug: blender-mac-arm
runtime: osx-arm64
requires:
- get-ci-tools
- build-connector-mac:
name: Mac Intel Build
slug: blender-mac-intel
runtime: osx-x64
requires:
- get-ci-tools
deploy: # build installers and deploy
jobs:
- get-ci-tools:
filters:
tags:
only: /.*/
branches:
ignore: /.*/
- build-connector:
- build-connector-win:
name: Windows Build
slug: blender
installer: true
requires:
- get-ci-tools
filters:
tags:
only: /([0-9]+)\.([0-9]+)\.([0-9]+)(?:-\w+)?$/
branches:
only:
- main
- /ci\/.*/
deploy:
jobs:
- get-ci-tools:
ignore: /.*/
- build-connector-mac:
name: Mac ARM Build
slug: blender-mac-arm
runtime: osx-arm64
installer: true
requires:
- get-ci-tools
filters:
tags:
only: /[0-9]+(\.[0-9]+)*/
only: /([0-9]+)\.([0-9]+)\.([0-9]+)(?:-\w+)?$/
branches:
ignore: /.*/ # For testing only! /ci\/.*/
- build-connector:
ignore: /.*/
- build-connector-mac:
name: Mac Intel Build
slug: blender-mac-intel
runtime: osx-x64
installer: true
requires:
- get-ci-tools
filters:
tags:
only: /([0-9]+)\.([0-9]+)\.([0-9]+)(?:-\w+)?$/
branches:
ignore: /.*/
- deploy-connector-new:
name: deploy-win
slug: blender
file_slug: blender
os: Win
extension: exe
arch: Any
requires:
- get-ci-tools
- Windows Build
- Mac ARM Build
- Mac Intel Build
filters:
tags:
only: /[0-9]+(\.[0-9]+)*/
only: /([0-9]+)\.([0-9]+)\.([0-9]+)(?:-\w+)?$/
branches:
ignore: /.*/ # For testing only! /ci\/.*/
- deploy:
- deploy-connector-new:
name: deploy-mac-intel
slug: blender
file_slug: blender-mac-intel
os: OSX
arch: Intel
extension: zip
requires:
- get-ci-tools
- build-connector
- Windows Build
- Mac ARM Build
- Mac Intel Build
filters:
tags:
only: /[0-9]+(\.[0-9]+)*/
only: /([0-9]+)\.([0-9]+)\.([0-9]+)(?:-\w+)?$/
branches:
ignore: /.*/ # For testing only! /ci\/.*/
- deploy-connector-new:
name: deploy-mac-arm
slug: blender
file_slug: blender-mac-arm
os: OSX
arch: Arm
extension: zip
requires:
- Windows Build
- Mac ARM Build
- Mac Intel Build
filters:
tags:
only: /([0-9]+)\.([0-9]+)\.([0-9]+)(?:-\w+)?$/
branches:
ignore: /.*/ # For testing only! /ci\/.*/
+4 -1
View File
@@ -5,8 +5,11 @@ __pycache__/
# editor
.vscode
.idea
# dev
.venv
Installers/
modules/
modules/
.tool-versions
requirements.txt
+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!
+2 -38
View File
@@ -1,25 +1,4 @@
# MIT License
# Copyright (c) 2018-2021 Tom Svilans
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
import bpy
bl_info = {
"name": "SpeckleBlender 2.0",
@@ -33,21 +12,7 @@ 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
@@ -67,7 +32,6 @@ Add load handler to initialize Speckle when
loading a Blender file
"""
@persistent
def load_handler(dummy):
bpy.ops.speckle.users_load()
@@ -95,7 +59,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 -294
View File
@@ -1,242 +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
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)
@@ -246,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
+396
View File
@@ -0,0 +1,396 @@
import math
from typing import Union
from bpy_speckle.functions import get_scale_length, _report
import mathutils
import bpy, bmesh, bpy_types
from specklepy.objects.other import *
from specklepy.objects.geometry import *
from bpy.types import Object
from .util import (
add_blender_material,
add_custom_properties,
add_vertices,
add_faces,
add_colors,
add_uv_coords,
)
SUPPORTED_CURVES = (Line, Polyline, Curve, Arc, Polycurve)
CAN_CONVERT_TO_NATIVE = (
Mesh,
Brep,
*SUPPORTED_CURVES,
Transform,
BlockDefinition,
BlockInstance,
)
def can_convert_to_native(speckle_object: Base) -> bool:
if type(speckle_object) in CAN_CONVERT_TO_NATIVE:
return True
if getattr(
speckle_object, "displayValue", getattr(speckle_object, "displayMesh", None)
):
return True
_report(f"Could not convert unsupported Speckle object: {speckle_object}")
return False
def convert_to_native(speckle_object: Base, name: Optional[str] = None) -> Optional[Union[list, Object]]:
speckle_type = type(speckle_object)
speckle_name = (
name
or getattr(speckle_object, "name", None)
or f"{speckle_object.speckle_type} -- {speckle_object.id}"
)
# convert unsupported types with display values
if speckle_type not in CAN_CONVERT_TO_NATIVE:
elements = getattr(speckle_object, "elements", []) or []
display = getattr(
speckle_object, "displayValue", getattr(speckle_object, "displayMesh", None)
)
if not elements and not display:
_report(f"Could not convert unsupported Speckle object: {speckle_object}")
return None
if isinstance(display, list):
elements.extend(display)
else:
elements.append(display)
# TODO: depreciate the parent type
# 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
converted = []
for item in elements:
if not isinstance(item, Base):
continue
item.parent_speckle_type = speckle_object.speckle_type
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 converted
try:
# convert breps
if speckle_type is Brep:
meshes = getattr(
speckle_object, "displayValue", getattr(speckle_object, "displayMesh", iter([]))
)
if material := getattr(speckle_object, "renderMaterial", getattr(speckle_object, "@renderMaterial", None),):
for mesh in meshes:
mesh["renderMaterial"] = material
return [convert_to_native(mesh) for mesh in meshes]
scale = 1.0
if units := getattr(speckle_object, "units", None):
scale = get_scale_length(units) / bpy.context.scene.unit_settings.scale_length
# convert supported geometry
if isinstance(speckle_object, Mesh):
obj_data = mesh_to_native(speckle_object, name=speckle_name, scale=scale)
elif speckle_type in SUPPORTED_CURVES:
obj_data = icurve_to_native(speckle_object, name=speckle_name, scale=scale)
elif isinstance(speckle_object, Transform):
obj_data = transform_to_native(speckle_object, scale=scale)
elif isinstance(speckle_object, BlockDefinition):
obj_data = block_def_to_native(speckle_object, scale=scale)
elif isinstance(speckle_object, BlockInstance): # speckle_type is BlockInstance:
obj_data = block_instance_to_native(speckle_object, scale=scale)
else:
_report(f"Unsupported type {speckle_type}")
return None
except Exception as ex: # conversion error
_report(f"Error converting {speckle_object} \n{ex}")
return None
if speckle_name in bpy.data.objects.keys():
blender_object = bpy.data.objects[speckle_name]
blender_object.data = (
obj_data.data if isinstance(obj_data, bpy_types.Object) else obj_data
)
blender_object.matrix_world = (
blender_object.matrix_world
if speckle_type is BlockInstance
else mathutils.Matrix()
)
if hasattr(obj_data, "materials"):
blender_object.data.materials.clear()
else:
blender_object = (
obj_data
if isinstance(obj_data, bpy_types.Object)
else bpy.data.objects.new(speckle_name, obj_data)
)
blender_object.speckle.object_id = str(speckle_object.id)
blender_object.speckle.enabled = True
add_custom_properties(speckle_object, blender_object)
add_blender_material(speckle_object, blender_object)
return blender_object
def mesh_to_native(speckle_mesh: Mesh, name: str, scale=1.0) -> bpy.types.Mesh:
if name in bpy.data.meshes.keys():
blender_mesh = bpy.data.meshes[name]
else:
blender_mesh = bpy.data.meshes.new(name=name)
bm = bmesh.new()
add_vertices(speckle_mesh, bm, scale)
add_faces(speckle_mesh, bm)
add_colors(speckle_mesh, bm)
add_uv_coords(speckle_mesh, bm)
bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
bm.to_mesh(blender_mesh)
bm.free()
return blender_mesh
def line_to_native(speckle_curve: Line, blender_curve: bpy.types.Curve, scale: float) -> Optional[bpy.types.Spline]:
line = blender_curve.splines.new("POLY")
line.points.add(1)
line.points[0].co = (
float(speckle_curve.start.x) * scale,
float(speckle_curve.start.y) * scale,
float(speckle_curve.start.z) * scale,
1,
)
if speckle_curve.end:
line.points[1].co = (
float(speckle_curve.end.x) * scale,
float(speckle_curve.end.y) * scale,
float(speckle_curve.end.z) * scale,
1,
)
return line
def polyline_to_native(scurve: Polyline, bcurve: bpy.types.Curve, scale: float) -> Optional[bpy.types.Spline]:
if value := scurve.value:
N = len(value) // 3
polyline = bcurve.splines.new("POLY")
if hasattr(scurve, "closed"):
polyline.use_cyclic_u = scurve.closed
# if "closed" in scurve.keys():
# polyline.use_cyclic_u = scurve["closed"]
polyline.points.add(N - 1)
for i in range(N):
polyline.points[i].co = (
float(value[i * 3]) * scale,
float(value[i * 3 + 1]) * scale,
float(value[i * 3 + 2]) * scale,
1,
)
return polyline
def nurbs_to_native(scurve: Curve, bcurve: bpy.types.Curve, scale: float) -> Optional[bpy.types.Spline]:
if points := scurve.points:
N = len(points) // 3
nurbs = bcurve.splines.new("NURBS")
if hasattr(scurve, "closed"):
nurbs.use_cyclic_u = scurve.closed != 0
nurbs.points.add(N - 1)
for i in range(N):
nurbs.points[i].co = (
float(points[i * 3]) * scale,
float(points[i * 3 + 1]) * scale,
float(points[i * 3 + 2]) * scale,
1,
)
if len(scurve.weights) == len(nurbs.points):
for i, w in enumerate(scurve.weights):
nurbs.points[i].weight = w
# TODO: anaylize curve knots to decide if use_endpoint_u or use_bezier_u should be enabled
# nurbs.use_endpoint_u = True
nurbs.order_u = scurve.degree + 1
return nurbs
def arc_to_native(rcurve: Arc, bcurve: bpy.types.Curve, scale: float) -> Optional[bpy.types.Spline]:
# TODO: improve Blender representation of arc
plane = rcurve.plane
if not plane:
return None
normal = mathutils.Vector([plane.normal.x, plane.normal.y, plane.normal.z])
radius = rcurve.radius * scale
startAngle = rcurve.startAngle
endAngle = rcurve.endAngle
startQuat = mathutils.Quaternion(normal, startAngle)
endQuat = mathutils.Quaternion(normal, endAngle)
# Get start and end vectors, centre point, angles, etc.
r1 = mathutils.Vector([plane.xdir.x, plane.xdir.y, plane.xdir.z])
r1.rotate(startQuat)
r2 = mathutils.Vector([plane.xdir.x, plane.xdir.y, plane.xdir.z])
r2.rotate(endQuat)
c = mathutils.Vector([plane.origin.x, plane.origin.y, plane.origin.z]) * scale
spt = c + r1 * radius
ept = c + r2 * radius
angle = endAngle - startAngle
t1 = normal.cross(r1)
# Initialize arc data and calculate subdivisions
arc = bcurve.splines.new("NURBS")
arc.use_cyclic_u = False
Ndiv = max(int(math.floor(angle / 0.3)), 2)
step = angle / float(Ndiv)
stepQuat = mathutils.Quaternion(normal, step)
tan = math.tan(step / 2) * radius
arc.points.add(Ndiv + 1)
# Set start and end points
arc.points[0].co = (spt.x, spt.y, spt.z, 1)
arc.points[Ndiv + 1].co = (ept.x, ept.y, ept.z, 1)
# Set intermediate points
for i in range(Ndiv):
t1 = normal.cross(r1)
pt = c + r1 * radius + t1 * tan
arc.points[i + 1].co = (pt.x, pt.y, pt.z, 1)
r1.rotate(stepQuat)
# Set curve settings
arc.use_endpoint_u = True
arc.order_u = 3
return arc
def polycurve_to_native(scurve: Polycurve, bcurve: bpy.types.Curve, scale: float):
"""
Convert Polycurve object
"""
segments = scurve.segments
curves = []
for seg in segments:
speckle_type = type(seg)
if speckle_type in SUPPORTED_CURVES:
curves.append(icurve_to_native_spline(seg, bcurve, scale=scale))
else:
_report(f"Unsupported curve type: {speckle_type}")
return curves
def icurve_to_native_spline(speckle_curve: Base, blender_curve: bpy.types.Curve, scale=1.0):
curve_type = type(speckle_curve)
if curve_type is Line:
return line_to_native(speckle_curve, blender_curve, scale)
if curve_type is Polyline:
return polyline_to_native(speckle_curve, blender_curve, scale)
if curve_type is Curve:
return nurbs_to_native(speckle_curve, blender_curve, scale)
if curve_type is Polycurve:
return polycurve_to_native(speckle_curve, blender_curve, scale)
if curve_type is Arc:
return arc_to_native(speckle_curve, blender_curve, scale)
def icurve_to_native(speckle_curve: Base, name=None, scale=1.0) -> Optional[Curve]:
curve_type = type(speckle_curve)
if curve_type not in SUPPORTED_CURVES:
_report(f"Unsupported curve type: {curve_type}")
return None
name = name or f"{curve_type} -- {speckle_curve.id}"
blender_curve = (
bpy.data.curves[name]
if name in bpy.data.curves.keys()
else bpy.data.curves.new(name, type="CURVE")
)
blender_curve.dimensions = "3D"
blender_curve.resolution_u = 12
icurve_to_native_spline(speckle_curve, blender_curve, scale)
return blender_curve
def transform_to_native(transform: Transform, scale=1.0) -> mathutils.Matrix:
mat = mathutils.Matrix(
[
transform.value[:4],
transform.value[4:8],
transform.value[8:12],
transform.value[12:16],
]
)
# scale the translation
for i in range(3):
mat[i][3] *= scale
return mat
def block_def_to_native(definition: BlockDefinition, scale=1.0) -> bpy.types.Collection:
native_def = bpy.data.collections.get(definition.name)
if native_def:
return native_def
native_def = bpy.data.collections.new(definition.name)
native_def["applicationId"] = definition.applicationId
for geo in definition.geometry:
if b_obj := convert_to_native(geo):
native_def.objects.link(
b_obj
if isinstance(b_obj, bpy_types.Object)
else bpy.data.objects.new(b_obj.name, b_obj)
)
return native_def
def block_instance_to_native(instance: BlockInstance, scale=1.0) -> bpy.types.Object:
"""
Convert BlockInstance to native
"""
name = f"{getattr(instance, 'name', None) or instance.blockDefinition.name} -- {instance.id}"
native_def = block_def_to_native(instance.blockDefinition, scale)
native_instance = bpy.data.objects.new(name, None)
add_custom_properties(instance, native_instance)
native_instance["name"] = getattr(instance, 'name', None) or instance.blockDefinition.name
# hide the instance axes so they don't clutter the viewport
native_instance.empty_display_size = 0
native_instance.instance_collection = native_def
native_instance.instance_type = "COLLECTION"
native_instance.matrix_world = transform_to_native(instance.transform, scale)
return native_instance
+331
View File
@@ -0,0 +1,331 @@
from typing import Optional
import bpy
from bpy.types import Depsgraph, MeshVertColor, MeshVertex, Object
from specklepy.objects.geometry import Mesh, Curve, Interval, Box, Point, Polyline
from specklepy.objects.other import *
from bpy_speckle.functions import _report
from bpy_speckle.convert.util import (
get_blender_custom_properties,
make_knots,
to_argb_int,
)
UNITS = "m"
CAN_CONVERT_TO_SPECKLE = ("MESH", "CURVE", "EMPTY")
def convert_to_speckle(blender_object: Object, scale: float, units: str, desgraph: Optional[Depsgraph]) -> Optional[list]:
global UNITS
UNITS = units
blender_type = blender_object.type
if blender_type not in CAN_CONVERT_TO_SPECKLE:
return None
speckle_objects = []
speckle_material = material_to_speckle(blender_object)
if desgraph:
blender_object = blender_object.evaluated_get(desgraph)
converted = None
if blender_type == "MESH":
converted = mesh_to_speckle(blender_object, blender_object.data, scale)
elif blender_type == "CURVE":
converted = icurve_to_speckle(blender_object, blender_object.data, scale)
elif blender_type == "EMPTY":
converted = empty_to_speckle(blender_object, scale)
if not converted:
return None
if isinstance(converted, list):
speckle_objects.extend([c for c in converted if c != None])
else:
speckle_objects.append(converted)
for so in speckle_objects:
so.properties = get_blender_custom_properties(blender_object)
so.applicationId = so.properties.pop("applicationId", None)
if speckle_material:
so["renderMaterial"] = speckle_material
# Set object transform
if blender_type != "EMPTY":
so.properties["transform"] = transform_to_speckle(
blender_object.matrix_world
)
return speckle_objects
def mesh_to_speckle(blender_object: Object, data: bpy.types.Mesh, scale=1.0) -> List[Mesh]:
if data.loop_triangles is None or len(data.loop_triangles) < 1:
data.calc_loop_triangles()
mat = blender_object.matrix_world
verts = [tuple(mat @ x.co * scale) for x in data.vertices]
faces = [p.vertices for p in data.polygons]
unit_system = bpy.context.scene.unit_settings.system
sm = Mesh(
name=blender_object.name,
vertices=list(sum(verts, ())),
faces=[],
colors=[],
textureCoordinates=[],
units=UNITS,
bbox=Box(area=0.0, volume=0.0),
)
if data.uv_layers.active:
for vt in data.uv_layers.active.data:
sm.textureCoordinates.extend([vt.uv.x, vt.uv.y])
for f in faces:
n = len(f)
if n == 3:
sm.faces.append(0)
elif n == 4:
sm.faces.append(1)
else:
sm.faces.append(n)
sm.faces.extend(f)
# TODO: figure out how to align vertex colors and vertices consistantly in receiving applications
# we are seeing the same issue as with texture coordinate alignment
#if data.color_attributes.active_color:
# sm.colors = [to_argb_int(x.color) for x in data.color_attributes.active_color.data]
return [sm]
def bezier_to_speckle(matrix: List[float], 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)
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: List[float], spline: bpy.types.Spline, scale: float, name: Optional[str] = None) -> Curve:
knots = make_knots(spline)
points = [tuple(matrix @ pt.co.xyz * scale) for pt in spline.points]
degree = spline.order_u - 1
length = spline.calc_length()
domain = Interval(start=0, end=length, totalChildrenCount=0)
return Curve(
name=name,
degree=degree,
closed=spline.use_cyclic_u,
periodic=spline.use_cyclic_u,
points=list(sum(points, ())), # magic (flatten list of tuples)
weights=[pt.weight for pt in spline.points],
knots=knots,
rational=False,
area=0,
volume=0,
length=length,
domain=domain,
units=UNITS,
bbox=Box(area=0.0, volume=0.0),
)
def poly_to_speckle(matrix: List[float], spline: bpy.types.Spline, scale: float, name: Optional[str] = None) -> Polyline:
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=bool(spline.use_cyclic_u),
value=list(sum(points, ())), # magic (flatten list of tuples)
length=length,
domain=domain,
bbox=Box(area=0.0, volume=0.0),
area=0,
units=UNITS,
)
def icurve_to_speckle(blender_object: 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
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, # magic (flatten list of tuples)
length=0,
domain=domain,
bbox=Box(area=0.0, volume=0.0),
area=0,
units=UNITS,
)
polylines.append(poly)
return polylines
def material_to_speckle(blender_object: 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
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 transform_to_speckle(blender_transform: List[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))
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):
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
-40
View File
@@ -1,40 +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=[],
units="m" if unit_system == "METRIC" else "ft",
bbox=Box(area=0.0, volume=0.0),
)
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]
+295 -1
View File
@@ -1,4 +1,28 @@
import math
from typing import Tuple
from bmesh.types import BMesh
import bpy, struct, idprop
from specklepy.objects.base import Base
from specklepy.objects.geometry import Mesh
from specklepy.serialization.base_object_serializer import BaseObjectSerializer
from bpy_speckle.functions import _report
from bpy.types import 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]:
@@ -13,7 +37,277 @@ def to_rgba(argb_int: int) -> Tuple[float]:
def to_argb_int(diffuse_colour) -> int:
"""Converts an RGBA array to an ARGB integer"""
diffuse_colour = diffuse_colour[-1:] + diffuse_colour[0:3]
diffuse_colour = diffuse_colour[-1:] + diffuse_colour[:3]
diffuse_colour = [int(val * 255) for val in diffuse_colour]
return int.from_bytes(diffuse_colour, byteorder="big", signed=True)
def add_custom_properties(speckle_object: Base, blender_object: Object):
if blender_object is None:
return
serializer = BaseObjectSerializer()
blender_object["_speckle_type"] = type(speckle_object).__name__
app_id = getattr(speckle_object, "applicationId", None)
if app_id:
blender_object["applicationId"] = speckle_object.applicationId
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)):
blender_object[key] = val
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:
blender_object[key] = items
elif isinstance(val,dict):
for (k,v) in val.items():
if not isinstance(v, Base):
blender_object[k] = v
def add_blender_material(speckle_object: Base, blender_object: Object) -> None:
"""Add material to a blender object if the corresponding speckle object has a render material"""
if blender_object.data is None:
return
speckle_mat = getattr(
speckle_object,
"renderMaterial",
getattr(speckle_object, "@renderMaterial", None),
)
if not speckle_mat:
return
mat_name = getattr(speckle_mat, "name", None) or speckle_mat.__dict__.get("@name")
if not mat_name:
mat_name = speckle_mat.applicationId or speckle_mat.id or speckle_mat.get_id()
blender_mat = bpy.data.materials.get(mat_name)
if not blender_mat:
blender_mat = bpy.data.materials.new(mat_name)
# for now, we're not updating these materials. as per tom's suggestion, we should have a toggle
# that enables this as the blender mats will prob be much more complex than whatever is coming in
blender_mat.use_nodes = True
inputs = blender_mat.node_tree.nodes["Principled BSDF"].inputs
inputs["Base Color"].default_value = to_rgba(speckle_mat.diffuse)
inputs["Emission"].default_value = to_rgba(speckle_mat.emissive)
inputs["Roughness"].default_value = speckle_mat.roughness
inputs["Metallic"].default_value = speckle_mat.metalness
inputs["Alpha"].default_value = speckle_mat.opacity
if speckle_mat.opacity < 1:
blender_mat.blend_method = "BLEND"
blender_object.data.materials.append(blender_mat)
def add_vertices(speckle_mesh: 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,
)
)
blender_mesh.verts.ensure_lookup_table()
def add_faces(speckle_mesh: Mesh, blender_mesh: BMesh, smooth=False):
sfaces = speckle_mesh.faces
if sfaces and len(sfaces) > 0:
i = 0
while i < len(sfaces):
n = sfaces[i]
if n < 3:
n += 3 # 0 -> 3, 1 -> 4
i += 1
try:
f = blender_mesh.faces.new(
[blender_mesh.verts[int(x)] for x in sfaces[i : i + n]]
)
f.smooth = smooth
except Exception as e:
_report(f"Failed to create face for mesh {speckle_mesh.id} \n{e}")
i += n
blender_mesh.faces.ensure_lookup_table()
blender_mesh.verts.index_update()
def add_colors(speckle_mesh: 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 * 2: {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=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
"""
def macro_knotsu(nu):
return nu.order_u + nu.point_count_u + (nu.order_u - 1 if nu.use_cyclic_u else 0)
def macro_segmentsu(nu):
return nu.point_count_u if nu.use_cyclic_u else nu.point_count_u - 1
def make_knots(nu):
knots = [0.0] * (4 + macro_knotsu(nu))
flag = nu.use_endpoint_u + (nu.use_bezier_u << 1)
if nu.use_cyclic_u:
calc_knots(knots, nu.point_count_u, nu.order_u, 0)
makecyclicknots(knots, nu.point_count_u, nu.order_u)
else:
calc_knots(knots, nu.point_count_u, nu.order_u, flag)
return knots
def calc_knots(knots, point_count, order, flag):
pts_order = point_count + order
if flag == 1:
k = 0.0
for a in range(1, pts_order + 1):
knots[a - 1] = k
if a >= order and a <= point_count:
k += 1.0
elif flag == 2:
if order == 4:
k = 0.34
for a in range(pts_order):
knots[a] = math.floor(k)
k += 1.0 / 3.0
elif order == 3:
k = 0.6
for a in range(pts_order):
if a >= order and a <= point_count:
k += 0.5
knots[a] = math.floor(k)
else:
for a in range(pts_order):
knots[a] = a
def makecyclicknots(knots, point_count, order):
order2 = order - 1
if order > 2:
b = point_count + order2
for a in range(1, order2):
if knots[b] != knots[b - a]:
break
if a == order2:
knots[point_count + order - 2] += 1.0
b = order
c = point_count + order + order2
for a in range(point_count + order2, c):
knots[a] = knots[a - 1] + (knots[b] - knots[b - 1])
b -= 1
+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
-2
View File
@@ -3,7 +3,6 @@ from .object import (
UpdateObject,
ResetObject,
DeleteObject,
UploadObject,
UploadNgonsAsPolylines,
SelectIfSameCustomProperty,
SelectIfHasCustomProperty,
@@ -43,7 +42,6 @@ operator_classes.extend(
UpdateObject,
ResetObject,
DeleteObject,
UploadObject,
UploadNgonsAsPolylines,
SelectIfSameCustomProperty,
SelectIfHasCustomProperty,
+10 -32
View File
@@ -1,23 +1,9 @@
"""
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
from bpy_speckle.clients import speckle_clients
@@ -32,7 +18,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):
@@ -66,20 +53,11 @@ class DeleteCommit(bpy.types.Operator):
stream = user.streams[user.active_stream]
if len(stream.branches) < 1:
return {"CANCELLED"}
else:
branch = stream.branches[int(stream.branch)]
if len(branch.commits) < 1:
return {"CANCELLED"}
else:
commit = branch.commits[int(branch.commit)]
branch = stream.branches[int(stream.branch)]
if len(branch.commits) < 1:
return {"CANCELLED"}
commit = branch.commits[int(branch.commit)]
deleted = client.commit.delete(stream_id=stream.id, commit_id=commit.id)
deleted = client.commit.delete(stream_id=stream.id, commit_id=commit.id)
return {"FINISHED"}
bpy.ops.speckle.load_user_streams()
context.view_layer.update()
if context.area:
context.area.tag_redraw()
return {"FINISHED"}
+11 -77
View File
@@ -2,19 +2,12 @@
Object operators
"""
import bpy, bmesh, os
from bpy.props import (
StringProperty,
BoolProperty,
FloatProperty,
CollectionProperty,
EnumProperty,
import bpy
from bpy.props import BoolProperty, EnumProperty
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
@@ -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"]
@@ -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"]
+191 -71
View File
@@ -2,40 +2,39 @@
Stream operators
"""
from itertools import chain
from typing import Dict
import bpy, bmesh, os
from math import radians
from typing import Callable, Dict, Iterable
import bpy
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
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.convert import get_speckle_subobjects
from bpy_speckle.clients import speckle_clients
from bpy_speckle.operators.users import add_user_stream
from specklepy.api import operations
from specklepy.api.credentials import StreamWrapper
from specklepy.api 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,7 +49,7 @@ 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: bpy.types.Collection = None) -> List:
"""For handling the weird nested lists that come from Grasshopper"""
objects = []
@@ -67,15 +66,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: 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
@@ -83,21 +77,66 @@ def get_objects_collections_recursive(base, parent_col=None) -> List:
for name in base.get_dynamic_member_names():
value = base[name]
if name == "parameters" and "Revit" in base.speckle_type:
continue
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 #TODO: 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)
@@ -117,6 +156,7 @@ 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}"
@@ -128,8 +168,18 @@ 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)]
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)
if not isinstance(new_objects, list):
new_objects = [new_objects]
if hasattr(base, "properties") and base.properties is not None:
new_objects.extend(get_speckle_subobjects(base.properties, scale, base.id))
@@ -149,13 +199,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 the right one for hosted elements!
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__))
_report(f"Script '{func.__module__}' returned None.")
continue
new_object.speckle.stream_id = stream_id
@@ -163,7 +212,7 @@ 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])
@@ -171,7 +220,7 @@ def base_to_native(context, base, scale, stream_id, col, existing, func=None):
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,13 +232,13 @@ 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 != ""
}
@@ -224,13 +273,12 @@ def create_nested_hierarchy(base, hierarchy, objects):
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, "@objects"):
child["@objects"] = []
child["@objects"].extend(objects)
return base
class ReceiveStreamObjects(bpy.types.Operator):
"""
Receive stream objects
@@ -241,6 +289,45 @@ class ReceiveStreamObjects(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
bl_description = "Receive objects from active stream"
clean_meshes: BoolProperty(name="Clean Meshes", default=False)
def draw(self, context):
layout = self.layout
col = layout.column()
col.prop(self, "clean_meshes")
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
@@ -252,7 +339,7 @@ class ReceiveStreamObjects(bpy.types.Operator):
client = speckle_clients[int(context.scene.speckle.active_user)]
stream = client.stream.get(id=bstream.id)
stream = client.stream.get(id=bstream.id, branch_limit=20)
if stream.branches.totalCount < 1:
return {"CANCELLED"}
@@ -264,13 +351,31 @@ class ReceiveStreamObjects(bpy.types.Operator):
bbranch = bstream.branches[int(bstream.branch)]
if branch.commits.totalCount < 1:
print("No commits found. Probably an empty stream.")
_report("No commits found. Probably an empty stream.")
return {"CANCELLED"}
commit = branch.commits.items[int(bbranch.commit)]
commit: Commit = branch.commits.items[int(bbranch.commit)]
transport = ServerTransport(stream.id, client)
metrics.track(
metrics.RECEIVE,
getattr(transport, "account", None),
custom_props={
"sourceHostApp": host_applications.get_host_app_from_string(commit.sourceApplication).slug,
"sourceHostAppVersion": commit.sourceApplication
},
)
stream_data = operations._untracked_receive(commit.referencedObject, transport)
client.commit.received(
bstream.id,
commit.id,
source_application="blender",
message="received commit from Speckle Blender",
)
context.window_manager.progress_begin(0, stream_data.totalChildrenCount)
transport = ServerTransport(client, stream.id)
stream_data = operations.receive(commit.referencedObject, transport)
"""
Create or get Collection for stream objects
@@ -283,8 +388,8 @@ class ReceiveStreamObjects(bpy.types.Operator):
name = "{} [ {} @ {} ]".format(stream.name, branch.name, commit.id)
col = create_collection(name)
col.speckle.stream_id = stream.id
col.speckle.name = stream.name
col.speckle.units = stream_data.units
col.speckle.units = stream_data.units or "m"
if col.name not in bpy.context.scene.collection.children:
bpy.context.scene.collection.children.link(col)
@@ -297,25 +402,34 @@ 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):
@@ -369,9 +483,14 @@ class SendStreamObjects(bpy.types.Operator):
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 +525,19 @@ class SendStreamObjects(bpy.types.Operator):
_report("Converting {}".format(obj.name))
ngons = obj.get("speckle_ngons_as_polylines", False)
# ngons = obj.get("speckle_ngons_as_polylines", False)
if ngons:
converted = export_ngons_as_polylines(obj, scale)
else:
converted = to_speckle_object(
obj,
scale,
bpy.context.evaluated_depsgraph_get()
if self.apply_modifiers
else None,
)
# if ngons:
# converted = ngons_to_speckle_polylines(obj, scale)
# else:
converted = convert_to_speckle(
obj,
scale,
units,
bpy.context.evaluated_depsgraph_get()
if self.apply_modifiers
else None,
)
if not converted:
continue
@@ -434,7 +554,7 @@ 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)
obj_id = operations.send(
base,
@@ -514,7 +634,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 +683,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."
)
+10 -23
View File
@@ -1,23 +1,13 @@
"""
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 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 LoadUsers(bpy.types.Operator):
@@ -70,7 +60,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
@@ -79,10 +69,8 @@ def add_user_stream(user, stream):
if not stream.branches:
return
# branches = [branch for branch in stream.branches.items if branch.name != "globals"]
for b in stream.branches.items:
if b.name == "globals":
continue
branch = s.branches.add()
branch.name = b.name
@@ -92,10 +80,10 @@ def add_user_stream(user, stream):
for c in b.commits.items:
commit = 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)
if hasattr(s, "baseProperties"):
@@ -124,7 +112,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.")
@@ -132,11 +120,10 @@ class LoadUserStreams(bpy.types.Operator):
user.streams.clear()
streams = sorted(streams, key=lambda x: x.name, reverse=False)
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):
+6 -6
View File
@@ -1,7 +1,6 @@
"""
Scene properties
"""
import bpy
from bpy.props import (
StringProperty,
@@ -12,7 +11,6 @@ from bpy.props import (
IntProperty,
PointerProperty,
)
from specklepy.api.client import SpeckleClient
class SpeckleSceneObject(bpy.types.PropertyGroup):
@@ -48,10 +46,11 @@ class SpeckleBranchObject(bpy.types.PropertyGroup):
class SpeckleStreamObject(bpy.types.PropertyGroup):
def get_branches(self, context):
if len(self.branches) > 0:
if self.branches:
return [
(str(i), branch.name, branch.name, i)
for i, branch in enumerate(self.branches)
if branch.name != "globals"
]
return [("0", "<none>", "<none>", 0)]
@@ -81,9 +80,10 @@ class SpeckleUserObject(bpy.types.PropertyGroup):
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",
+7 -14
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,
)
@@ -118,7 +117,6 @@ class VIEW3D_PT_SpeckleUser(bpy.types.Panel):
if len(speckle.users) < 1:
col.label(text="No users found.")
else:
# col.label(text="User")
col.prop(speckle, "active_user", text="")
user = speckle.users[int(speckle.active_user)]
col.label(text="{} ({})".format(user.server_name, user.server_url))
@@ -179,7 +177,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 +205,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 +230,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()
+14
View File
@@ -0,0 +1,14 @@
#!/bin/sh
MODULES_PATH="./bpy_speckle/modules"
# Clean up
rm -rf $MODULES_PATH/*
rm -f requirements.txt
# Export poetry dependencies
poetry lock --no-update
poetry export -f requirements.txt --with dev --without-hashes > requirements.txt
# Install dependencies via pip
python -m pip install -r requirements.txt -t $MODULES_PATH
+5 -3
View File
@@ -28,7 +28,8 @@ def patch_installer(tag):
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')
lines.insert(12, f'#define AppVersion "{tag.split("-")[0]}"\n')
lines.insert(13, f'#define AppInfoVersion "{tag}"\n')
with open(iss_file, "w") as file:
file.writelines(lines)
@@ -50,14 +51,15 @@ def get_specklepy_version():
def main():
if len(sys.argv) < 2:
print(get_specklepy_version())
return
tag = sys.argv[1]
if not re.match(r"[0-9]+(\.[0-9]+)*$", tag):
if not re.match(r"([0-9]+)\.([0-9]+)\.([0-9]+)", tag):
raise ValueError(f"Invalid tag provided: {tag}")
print(f"Patching version: {tag}")
patch_connector(tag)
patch_connector(tag.split("-")[0])
patch_installer(tag)
Generated
+722 -507
View File
File diff suppressed because it is too large Load Diff
+9 -5
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.3"
python = ">=3.8, <4.0.0"
specklepy = "^2.9.0"
[tool.poetry.dev-dependencies]
devtools = "^0.6.1"
numpy = "^1.20.2"
bpy = "^2.82.1"
bpy-build = "^2.1.0"
fake-bpy-module-latest = "^20220401"
black = "^21.12b0"
pylint = "^2.12.2"
[tool.poetry.group.local_specklepy.dependencies]
specklepy = {path = "../specklepy", develop = true}
[build-system]
requires = ["poetry-core>=1.0.0"]