Compare commits
223 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a26618a4f7 | |||
| eaf370407d | |||
| a2b50fe5a1 | |||
| 7e62f76841 | |||
| fc804f16d3 | |||
| 6c7da24595 | |||
| b284d39328 | |||
| 907185c9bb | |||
| a189a2e1c0 | |||
| 1fad926275 | |||
| 99c147fe2f | |||
| e2adf710b3 | |||
| 9509344533 | |||
| 6fabc6cae6 | |||
| c39298687d | |||
| bcdddbf930 | |||
| b5684e34f6 | |||
| 2203fe98f8 | |||
| bbfdf2863b | |||
| f25f6cb16c | |||
| 9e4e533ba8 | |||
| 8db12ca9b9 | |||
| 366c864247 | |||
| 52136d3ef6 | |||
| fe764d7f0c | |||
| 669dd67521 | |||
| f74b2c37f0 | |||
| ebb4e32fff | |||
| 25903baf83 | |||
| cb6d6d7ad8 | |||
| fd2687aa3c | |||
| f5c65068de | |||
| 235b49d8c6 | |||
| a1ec137c67 | |||
| b95f621272 | |||
| a1fcdad0e3 | |||
| 584e543964 | |||
| ef20c5240c | |||
| 9fe12a018a | |||
| 8411c01f1b | |||
| 63b82a30cb | |||
| 0b6e39cf38 | |||
| b7efcec517 | |||
| 0ab0096aac | |||
| cbd8fc99bb | |||
| 2a9287e762 | |||
| 98c70f237c | |||
| 048047cf05 | |||
| 6118215cae | |||
| 8e06432fe6 | |||
| cb8620ff8a | |||
| e0eddea8ab | |||
| 8497d0c195 | |||
| 1ce394c08f | |||
| aabbf87dda | |||
| 7e31787d37 | |||
| f1259587fd | |||
| 7f913e3af0 | |||
| 5433c34a4d | |||
| 2c52e93660 | |||
| 3c3b24cf98 | |||
| 3012b0ebcb | |||
| 35b96eaa4e | |||
| c229bb2414 | |||
| 318bd086c0 | |||
| e3eb29daa4 | |||
| 3359c8f275 | |||
| cfc5007d00 | |||
| 0a630457be | |||
| fa124c2312 | |||
| 977994e141 | |||
| d9cbc80ee7 | |||
| 45038fad79 | |||
| cda621d735 | |||
| 2d052d1379 | |||
| 46ce2bc0df | |||
| 80e2216aa0 | |||
| 10d69aa44b | |||
| b1481bd259 | |||
| e19fc9ef4e | |||
| 448eb856b2 | |||
| 06a7d416c7 | |||
| 0753436899 | |||
| 40e0a49b11 | |||
| b17b9c9de4 | |||
| f035111ffa | |||
| fa073f754f | |||
| 83919375f9 | |||
| defb11bc89 | |||
| 548b3ad352 | |||
| d2deecf099 | |||
| fbda0110cd | |||
| ba9ad9ac07 | |||
| d13db2b44a | |||
| 2c127c85f3 | |||
| cc099c0ff1 | |||
| 448ab70c3b | |||
| 020eba2727 | |||
| d8117e2c30 | |||
| 57a6d88e6b | |||
| 74cb3e5f85 | |||
| d0aeecc863 | |||
| 2803308c5e | |||
| 326f04f67d | |||
| 68972ba8f9 | |||
| 73a028b56f | |||
| 33890ef0ee | |||
| 53fe676ab6 | |||
| f027c7eca4 | |||
| ea2b6dfb0e | |||
| 83610cec38 | |||
| 2ed0685b10 | |||
| 877e616188 | |||
| 2e3e258a9a | |||
| 1e1c790eb4 | |||
| 2148fe8dee | |||
| 41c87a8661 | |||
| 96c9add526 | |||
| f425316e60 | |||
| abf363894e | |||
| 8fc8b97b7a | |||
| 64b4175585 | |||
| 4eefda3305 | |||
| f7275140d5 | |||
| 17a36f4fc2 | |||
| ba931e8205 | |||
| 572925cfbb | |||
| 03f94d6371 | |||
| c220337aec | |||
| 1b67304cfc | |||
| 25a1ec1cd1 | |||
| 8296e48c28 | |||
| ca81ac6fd6 | |||
| 88212b94b6 | |||
| 3375a04007 | |||
| c62538fc8f | |||
| fd83e80bbd | |||
| 0a2c1f9f32 | |||
| 2975737377 | |||
| 72dea53eee | |||
| a1aa267a30 | |||
| df30b65fdb | |||
| 6176abb4a3 | |||
| 5c2d2d195f | |||
| 7a784a4a8d | |||
| 2c8f2c96a9 | |||
| bc7400cb36 | |||
| 421c68a143 | |||
| 409adda784 | |||
| f5f1c6a8d0 | |||
| 1eca91a030 | |||
| 6b872f11d9 | |||
| fd3742b800 | |||
| 870174a969 | |||
| 2c6dfcb340 | |||
| 4d3eacbe52 | |||
| df7d631b54 | |||
| 07ceb07058 | |||
| 112bc56ded | |||
| 88e2744e93 | |||
| ad1c201c79 | |||
| e18569f738 | |||
| f7e563f500 | |||
| 7e56c21395 | |||
| 30b701ee27 | |||
| 3cdbc09fc0 | |||
| 656e41f03c | |||
| 7da26e44b6 | |||
| 830ba842e0 | |||
| d38f990e75 | |||
| c01836806c | |||
| bf416dd228 | |||
| a2e2c489b4 | |||
| c40c2d7955 | |||
| b45aa0d6c1 | |||
| 667616b1fb | |||
| 87cb69090a | |||
| 4a8a0ca6c7 | |||
| 3c0d1eba65 | |||
| 14aaf4f064 | |||
| 10bf3e3af5 | |||
| 936d573510 | |||
| 1f886202ec | |||
| 479e5b5b98 | |||
| bed1ecf2cc | |||
| a6cbce977f | |||
| 376a90c5cf | |||
| b0f8dbd63e | |||
| 89a8232a47 | |||
| 985f23b015 | |||
| d15a480e64 | |||
| b88af9ac08 | |||
| b649e29bdc | |||
| bf907519a8 | |||
| aa8cab2f27 | |||
| 5157dbcfde | |||
| d03f11c57d | |||
| b1deb1bb1c | |||
| bdd2c5d0ea | |||
| 6783d82e48 | |||
| f1db69fce5 | |||
| d09908412d | |||
| e84163a600 | |||
| 03a77759a1 | |||
| 47258cb7a6 | |||
| b216f95187 | |||
| 695135b655 | |||
| 5f7e47221c | |||
| 38ee1c6ef9 | |||
| d8b3c49dee | |||
| dceec1b061 | |||
| 2071c3f3b4 | |||
| d45419ba5e | |||
| 2c640b7272 | |||
| c00635d093 | |||
| a07d4f0a8e | |||
| 61e721716f | |||
| 91d12b5a6c | |||
| f331846138 | |||
| d350887860 | |||
| 8ad607a8e0 | |||
| 13f242e47f | |||
| 9f55acd02d |
+274
-89
@@ -1,122 +1,307 @@
|
||||
version: 2.1
|
||||
|
||||
orbs:
|
||||
python: circleci/python@1.3.2
|
||||
# Using windows for builds
|
||||
win: circleci/windows@2.4.0
|
||||
# Upload artifacts to s3
|
||||
aws-s3: circleci/aws-s3@2.0.0
|
||||
win: circleci/windows@5.0.0
|
||||
|
||||
jobs:
|
||||
build-connector: # Reusable job for basic connectors
|
||||
package-connector:
|
||||
docker:
|
||||
- image: cimg/python:3.11.0
|
||||
steps:
|
||||
- checkout
|
||||
- run:
|
||||
name: Setup SEMVER value
|
||||
command: |
|
||||
SEMVER=$(if [ "${CIRCLE_TAG}" ]; then echo $CIRCLE_TAG; else echo "0.0.0"; fi;)
|
||||
echo $SEMVER > ./SEMVER
|
||||
python3 patch_version.py $SEMVER
|
||||
- run:
|
||||
name: install dependencies
|
||||
command: poetry install --only main
|
||||
- run:
|
||||
name: export package dependencies
|
||||
command: ./export_dependencies.sh
|
||||
- persist_to_workspace:
|
||||
root: ./
|
||||
paths:
|
||||
- bpy_speckle
|
||||
- patch_installer.py
|
||||
- SEMVER
|
||||
|
||||
build-connector-zip:
|
||||
docker:
|
||||
- image: cimg/python:3.11.0
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: ./
|
||||
- run: &restore_semver
|
||||
name: Restore Semver
|
||||
command: SEMVER=$(cat ./SEMVER) && echo $SEMVER
|
||||
- run:
|
||||
name: Package to Zip
|
||||
command: zip -r bpy_speckle.zip bpy_speckle
|
||||
- persist_to_workspace:
|
||||
root: ./
|
||||
paths:
|
||||
- bpy_speckle.zip
|
||||
|
||||
get-ci-tools: # Clones our ci tools and persists them to the workspace
|
||||
docker:
|
||||
- image: cimg/base:2021.01
|
||||
steps:
|
||||
- add_ssh_keys:
|
||||
fingerprints:
|
||||
- "77:64:03:93:c5:f3:1d:a6:fd:bd:fb:d1:05:56:ca:e9"
|
||||
- run:
|
||||
name: I know Github as a host
|
||||
command: |
|
||||
mkdir ~/.ssh
|
||||
touch ~/.ssh/known_hosts
|
||||
ssh-keyscan github.com >> ~/.ssh/known_hosts
|
||||
- run:
|
||||
name: Clone
|
||||
command: git clone git@github.com:specklesystems/speckle-sharp-ci-tools.git speckle-sharp-ci-tools
|
||||
- run:
|
||||
command: cd speckle-sharp-ci-tools
|
||||
- persist_to_workspace:
|
||||
root: ./
|
||||
paths:
|
||||
- speckle-sharp-ci-tools
|
||||
|
||||
build-installer-win:
|
||||
executor:
|
||||
name: win/default # comes with python 3.7.3
|
||||
name: win/default
|
||||
shell: cmd.exe
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: ./
|
||||
- run:
|
||||
name: Patch installer
|
||||
shell: powershell.exe
|
||||
command: python patch_installer.py (Get-Content -Raw SEMVER)
|
||||
- run:
|
||||
name: Create Innosetup signing cert
|
||||
shell: powershell.exe
|
||||
command: |
|
||||
echo $env:PFX_B64 > "speckle-sharp-ci-tools\SignTool\AEC Systems Ltd.txt"
|
||||
certutil -decode "speckle-sharp-ci-tools\SignTool\AEC Systems Ltd.txt" "speckle-sharp-ci-tools\SignTool\AEC Systems Ltd.pfx"
|
||||
- run:
|
||||
name: Installer
|
||||
shell: cmd.exe #does not work in powershell
|
||||
command: speckle-sharp-ci-tools\InnoSetup\ISCC.exe speckle-sharp-ci-tools\blender.iss /Sbyparam=$p
|
||||
- persist_to_workspace:
|
||||
root: ./
|
||||
paths:
|
||||
- speckle-sharp-ci-tools/Installers/blender/blender-*.exe
|
||||
|
||||
build-installer-mac:
|
||||
macos:
|
||||
xcode: 12.5.1
|
||||
parameters:
|
||||
runtime:
|
||||
type: string
|
||||
slug:
|
||||
type: string
|
||||
default: ""
|
||||
installer_path:
|
||||
type: string
|
||||
default: speckle-sharp-ci-tools/Mac/SpeckleBlenderInstall
|
||||
steps:
|
||||
- checkout
|
||||
- attach_workspace:
|
||||
at: ./
|
||||
- run:
|
||||
name: upgrade pip and install specklepy
|
||||
command: python -m pip install --upgrade pip & python -m pip install --target=.\modules specklepy
|
||||
name: Install mono
|
||||
command: |
|
||||
HOMEBREW_NO_AUTO_UPDATE=1 brew install mono
|
||||
# Compress build files
|
||||
- run:
|
||||
name: Patch
|
||||
shell: powershell.exe
|
||||
command:
|
||||
| # If no tag, use 0.0.0.1 and don't make any YML (for testing only!)
|
||||
$tag = if([string]::IsNullOrEmpty($env:CIRCLE_TAG)) { "0.0.1" } else { $env:CIRCLE_TAG }
|
||||
$semver = $tag.replace("-beta","")
|
||||
$version = "$($semver).$($env:CIRCLE_BUILD_NUM)"
|
||||
$channel = "latest"
|
||||
if($tag -like "*-beta") { $channel = "beta" }
|
||||
# only create the yml if we have a tag
|
||||
New-Item -Force "speckle-sharp-ci-tools/Installers/blender/$channel.yml" -ItemType File -Value "version: $version"
|
||||
echo $version
|
||||
speckle-sharp-ci-tools\InnoSetup\ISCC.exe speckle-sharp-ci-tools\blender.iss /dAppVersion=$version
|
||||
- persist_to_workspace:
|
||||
root: ./
|
||||
paths:
|
||||
- speckle-sharp-ci-tools/Installers
|
||||
|
||||
get-ci-tools: # Clones our ci tools and persists them to the workspace
|
||||
docker:
|
||||
- image: cimg/base:2021.01
|
||||
steps:
|
||||
- run: # Could not get ssh to work, so using a personal token
|
||||
name: Clone
|
||||
command: git clone https://$GITHUB_TOKEN@github.com/specklesystems/speckle-sharp-ci-tools.git speckle-sharp-ci-tools
|
||||
- persist_to_workspace:
|
||||
root: ./
|
||||
paths:
|
||||
- speckle-sharp-ci-tools
|
||||
- persist_to_workspace:
|
||||
root: ./
|
||||
paths:
|
||||
- speckle-sharp-ci-tools
|
||||
name: Install dotnet
|
||||
command: curl -sSL https://dot.net/v1/dotnet-install.sh | bash /dev/stdin
|
||||
- run: *restore_semver
|
||||
- run:
|
||||
name: Copy connector files to installer
|
||||
command: |
|
||||
mkdir -p <<parameters.installer_path >>/.installationFiles/
|
||||
cp bpy_speckle.zip << parameters.installer_path >>/.installationFiles
|
||||
- run:
|
||||
name: Build Mac installer
|
||||
command: ~/.dotnet/dotnet publish << parameters.installer_path >>/SpeckleBlenderInstall.sln -r << parameters.runtime >> -c Release
|
||||
- run:
|
||||
name: Zip installer
|
||||
command: |
|
||||
SEMVER=$(cat ./SEMVER)
|
||||
echo $SEMVER
|
||||
mkdir -p speckle-sharp-ci-tools/Installers/blender
|
||||
(cd <<parameters.installer_path>>/bin/Release/net6.0/<< parameters.runtime >>/publish/ && zip -r - ./) > << parameters.slug >>-${SEMVER}.zip
|
||||
cp << parameters.slug >>-${SEMVER}.zip speckle-sharp-ci-tools/Installers/blender/
|
||||
|
||||
deploy: # Uploads all installers found to S3
|
||||
- persist_to_workspace:
|
||||
root: ./
|
||||
paths:
|
||||
- speckle-sharp-ci-tools/Installers/blender/<< parameters.slug >>*.zip
|
||||
|
||||
build-installer-manual:
|
||||
docker:
|
||||
- image: cimg/base:2021.01
|
||||
parameters:
|
||||
slug:
|
||||
type: string
|
||||
default: bpy_speckle
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: ./
|
||||
- run: *restore_semver
|
||||
- run:
|
||||
name: List contents
|
||||
command: ls -R speckle-sharp-ci-tools/Installers
|
||||
- aws-s3/copy:
|
||||
arguments: "--recursive --endpoint=https://$SPACES_REGION.digitaloceanspaces.com --acl public-read"
|
||||
aws-access-key-id: SPACES_KEY
|
||||
aws-region: SPACES_REGION
|
||||
aws-secret-access-key: SPACES_SECRET
|
||||
from: '"speckle-sharp-ci-tools/Installers/"'
|
||||
to: s3://speckle-releases/installers/
|
||||
name: Copy zip with semver
|
||||
command: |
|
||||
SEMVER=$(cat ./SEMVER)
|
||||
mkdir -p speckle-sharp-ci-tools/Installers/blender
|
||||
cp bpy_speckle.zip speckle-sharp-ci-tools/Installers/blender/<< parameters.slug >>-${SEMVER}.zip
|
||||
- persist_to_workspace:
|
||||
root: ./
|
||||
paths:
|
||||
- speckle-sharp-ci-tools/Installers/blender/<< parameters.slug >>*.zip
|
||||
|
||||
deploy-connector:
|
||||
docker:
|
||||
- image: mcr.microsoft.com/dotnet/sdk:6.0
|
||||
parameters:
|
||||
file_slug:
|
||||
type: string
|
||||
os:
|
||||
type: string
|
||||
extension:
|
||||
type: string
|
||||
arch:
|
||||
type: string
|
||||
default: Any
|
||||
steps:
|
||||
- checkout
|
||||
- attach_workspace:
|
||||
at: ./
|
||||
- run:
|
||||
name: Install Manager Feed CLI
|
||||
command: dotnet tool install --global Speckle.Manager.Feed
|
||||
- run: *restore_semver
|
||||
- run:
|
||||
name: Upload new version
|
||||
# this is where the installer gets the semver baked into the file name
|
||||
command: |
|
||||
SEMVER=$(cat ./SEMVER)
|
||||
echo $SEMVER
|
||||
/root/.dotnet/tools/Speckle.Manager.Feed deploy \
|
||||
-s blender \
|
||||
-v ${SEMVER} \
|
||||
-u https://releases.speckle.dev/installers/blender/<< parameters.file_slug >>-${SEMVER}.<< parameters.extension >> \
|
||||
-o << parameters.os >> \
|
||||
-a << parameters.arch >> \
|
||||
-f speckle-sharp-ci-tools/Installers/blender/<< parameters.file_slug >>-${SEMVER}.<< parameters.extension >>
|
||||
|
||||
workflows:
|
||||
build:
|
||||
build: # build the installers, but don't persist to workspace for deployment
|
||||
jobs:
|
||||
- get-ci-tools:
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
- main
|
||||
- /ci\/.*/
|
||||
- package-connector:
|
||||
filters: &build_filters
|
||||
tags:
|
||||
only: /([0-9]+)\.([0-9]+)\.([0-9]+)(?:-\w+)?(?:\.[0-9]+)?/
|
||||
|
||||
- build-connector-zip:
|
||||
requires:
|
||||
- package-connector
|
||||
filters: *build_filters
|
||||
|
||||
- build-connector:
|
||||
slug: blender
|
||||
requires:
|
||||
- get-ci-tools
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
- main
|
||||
- /ci\/.*/
|
||||
deploy:
|
||||
jobs:
|
||||
- get-ci-tools:
|
||||
filters:
|
||||
tags:
|
||||
only: /[0-9]+(\.[0-9]+)*/
|
||||
filters: *build_filters
|
||||
|
||||
- build-installer-win:
|
||||
context: innosetup
|
||||
name: Windows Installer Build
|
||||
requires:
|
||||
- package-connector
|
||||
- get-ci-tools
|
||||
filters: *build_filters
|
||||
|
||||
- deploy-connector:
|
||||
context: do-spaces-speckle-releases
|
||||
name: deploy-windows
|
||||
file_slug: blender
|
||||
os: WIN
|
||||
arch: Any
|
||||
extension: exe
|
||||
requires:
|
||||
- Manual Installer Build
|
||||
- Windows Installer Build
|
||||
- Mac Intel Build
|
||||
- Mac ARM Build
|
||||
filters: &deploy_filters
|
||||
branches:
|
||||
ignore: /.*/ # For testing only! /ci\/.*/
|
||||
- build-connector:
|
||||
slug: blender
|
||||
ignore: /.*/
|
||||
tags:
|
||||
only: /([0-9]+)\.([0-9]+)\.([0-9]+)(?:-\w+)?(?:\.[0-9]+)?/
|
||||
|
||||
- build-installer-mac:
|
||||
name: Mac ARM Build
|
||||
slug: blender-mac-arm
|
||||
runtime: osx-arm64
|
||||
requires:
|
||||
- get-ci-tools
|
||||
filters:
|
||||
tags:
|
||||
only: /[0-9]+(\.[0-9]+)*/
|
||||
branches:
|
||||
ignore: /.*/ # For testing only! /ci\/.*/
|
||||
- deploy:
|
||||
- build-connector-zip
|
||||
filters: *build_filters
|
||||
|
||||
- deploy-connector:
|
||||
context: do-spaces-speckle-releases
|
||||
name: deploy-mac-arm
|
||||
file_slug: blender-mac-arm
|
||||
os: OSX
|
||||
arch: Arm
|
||||
extension: zip
|
||||
requires:
|
||||
- Manual Installer Build
|
||||
- Windows Installer Build
|
||||
- Mac Intel Build
|
||||
- Mac ARM Build
|
||||
filters: *deploy_filters
|
||||
|
||||
- build-installer-mac:
|
||||
name: Mac Intel Build
|
||||
slug: blender-mac-intel
|
||||
runtime: osx-x64
|
||||
requires:
|
||||
- get-ci-tools
|
||||
- build-connector
|
||||
filters:
|
||||
tags:
|
||||
only: /[0-9]+(\.[0-9]+)*/
|
||||
branches:
|
||||
ignore: /.*/ # For testing only! /ci\/.*/
|
||||
- build-connector-zip
|
||||
filters: *build_filters
|
||||
|
||||
- deploy-connector:
|
||||
context: do-spaces-speckle-releases
|
||||
name: deploy-mac-intel
|
||||
file_slug: blender-mac-intel
|
||||
os: OSX
|
||||
arch: Intel
|
||||
extension: zip
|
||||
requires:
|
||||
- Manual Installer Build
|
||||
- Windows Installer Build
|
||||
- Mac Intel Build
|
||||
- Mac ARM Build
|
||||
filters: *deploy_filters
|
||||
|
||||
- build-installer-manual:
|
||||
name: Manual Installer Build
|
||||
requires:
|
||||
- get-ci-tools
|
||||
- build-connector-zip
|
||||
filters: *build_filters
|
||||
|
||||
- deploy-connector:
|
||||
context: do-spaces-speckle-releases
|
||||
name: deploy-manual
|
||||
file_slug: bpy_speckle
|
||||
os: Any
|
||||
arch: Any
|
||||
extension: zip
|
||||
requires:
|
||||
- Manual Installer Build
|
||||
- Windows Installer Build
|
||||
- Mac Intel Build
|
||||
- Mac ARM Build
|
||||
filters: *deploy_filters
|
||||
@@ -0,0 +1,12 @@
|
||||
name: Update issue Status
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [closed]
|
||||
|
||||
jobs:
|
||||
update_issue:
|
||||
uses: specklesystems/github-actions/.github/workflows/project-add-issue.yml@main
|
||||
secrets: inherit
|
||||
with:
|
||||
issue-id: ${{ github.event.issue.node_id }}
|
||||
@@ -0,0 +1,12 @@
|
||||
name: Move new issues into Project
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
jobs:
|
||||
track_issue:
|
||||
uses: specklesystems/github-actions/.github/workflows/project-add-issue.yml@main
|
||||
secrets: inherit
|
||||
with:
|
||||
issue-id: ${{ github.event.issue.node_id }}
|
||||
+5
-1
@@ -5,8 +5,12 @@ __pycache__/
|
||||
|
||||
# editor
|
||||
.vscode
|
||||
.idea
|
||||
|
||||
# dev
|
||||
.venv
|
||||
Installers/
|
||||
modules/
|
||||
modules/
|
||||
.tool-versions
|
||||
requirements.txt
|
||||
SEMVER
|
||||
@@ -1,23 +1,51 @@
|
||||
# SpeckleBlender 2.0
|
||||
Speckle add-on for Blender 2.92
|
||||
<h1 align="center">
|
||||
<img src="https://user-images.githubusercontent.com/2679513/131189167-18ea5fe1-c578-47f6-9785-3748178e4312.png" width="150px"/><br/>
|
||||
Speckle | Blender
|
||||
</h1>
|
||||
<h3 align="center">
|
||||
Connector for Blender
|
||||
</h3>
|
||||
<p align="center"><b>Speckle</b> is the data infrastructure for the AEC industry.</p><br/>
|
||||
|
||||
[](https://twitter.com/SpeckleSystems) [](https://discourse.speckle.works) [](https://speckle.systems) [](https://speckle.guide/dev/)
|
||||
<p align="center"><a href="https://twitter.com/SpeckleSystems"><img src="https://img.shields.io/twitter/follow/SpeckleSystems?style=social" alt="Twitter Follow"></a> <a href="https://speckle.community"><img src="https://img.shields.io/discourse/users?server=https%3A%2F%2Fspeckle.community&style=flat-square&logo=discourse&logoColor=white" alt="Community forum users"></a> <a href="https://speckle.systems"><img src="https://img.shields.io/badge/https://-speckle.systems-royalblue?style=flat-square" alt="website"></a> <a href="https://speckle.guide/dev/"><img src="https://img.shields.io/badge/docs-speckle.guide-orange?style=flat-square&logo=read-the-docs&logoColor=white" alt="docs"></a></p>
|
||||
<p align="center"><a href="https://github.com/specklesystems/speckle-blender/"><img src="https://circleci.com/gh/specklesystems/speckle-blender.svg?style=svg&circle-token=76eabd350ea243575cbb258b746ed3f471f7ac29" alt="Speckle-Next"></a> </p>
|
||||
|
||||
## Introduction
|
||||
# About Speckle
|
||||
|
||||
What is Speckle? Check our 
|
||||
|
||||
### Features
|
||||
|
||||
- **Object-based:** say goodbye to files! Speckle is the first object based platform for the AEC industry
|
||||
- **Version control:** Speckle is the Git & Hub for geometry and BIM data
|
||||
- **Collaboration:** share your designs collaborate with others
|
||||
- **3D Viewer:** see your CAD and BIM models online, share and embed them anywhere
|
||||
- **Interoperability:** get your CAD and BIM models into other software without exporting or importing
|
||||
- **Real time:** get real time updates and notifications and changes
|
||||
- **GraphQL API:** get what you need anywhere you want it
|
||||
- **Webhooks:** the base for a automation and next-gen pipelines
|
||||
- **Built for developers:** we are building Speckle with developers in mind and got tools for every stack
|
||||
- **Built for the AEC industry:** Speckle connectors are plugins for the most common software used in the industry such as Revit, Rhino, Grasshopper, AutoCAD, Civil 3D, Excel, Unreal Engine, Unity, QGIS, Blender and more!
|
||||
|
||||
### Try Speckle now!
|
||||
|
||||
Give Speckle a try in no time by:
|
||||
|
||||
- [](https://speckle.xyz) ⇒ creating an account at our public server
|
||||
- [](https://marketplace.digitalocean.com/apps/speckle-server?refcode=947a2b5d7dc1) ⇒ deploying an instance in 1 click
|
||||
|
||||
### Resources
|
||||
|
||||
- [](https://speckle.community) for help, feature requests or just to hang with other speckle enthusiasts, check out our community forum!
|
||||
- [](https://speckle.systems) our tutorials portal is full of resources to get you started using Speckle
|
||||
- [](https://speckle.guide/user/blender.html) reference on almost any end-user and developer functionality
|
||||
|
||||
|
||||
# Repo structure
|
||||
|
||||
The Speckle UI can be found in the 3d viewport toolbar (N), under the Speckle tab.
|
||||
<!--
|
||||
This repo holds Speckle's:
|
||||
|
||||
- Default [Code of Conduct](.github/CODE_OF_CONDUCT.md),
|
||||
- Default [Contribution Guidelines](.github/CONTRIBUTING.md),
|
||||
- README template (you're reading it now),
|
||||
- Default [Issue Template](.github/ISSUE_TEMPLATE/ISSUE_TEMPLATE.md),
|
||||
- Default [Pull Request Template](.github/PULL_REQUEST_TEMPLATE/PR_TEMPLATE.md),
|
||||
- OSS License (Apache 2.0)
|
||||
|
||||
Either copy paste the parts that are useful in existing repos, or use this as a base when creating a new repository.
|
||||
-->
|
||||
Head to the [**📚 documentation**](https://speckle.guide/user/blender.html) for more information.
|
||||
|
||||
## Disclaimer
|
||||
This code is WIP and as such should be used with extreme caution on non-sensitive projects.
|
||||
@@ -45,7 +73,7 @@ This code is WIP and as such should be used with extreme caution on non-sensitiv
|
||||
## Custom properties
|
||||
|
||||
- **SpeckleBlender** will look for a `texture_coordinates` property and use that to create a UV layer for the imported object. These texture coordinates are a space-separated list of floats (`[u v u v u v etc...]`) that is encoded as a base64 blob. This is subject to change as **SpeckleBlender** develops.
|
||||
- If a `material` property is found, **SpeckleBlender** will create a material named using the sub-property `material.name`. If a material with that name already exists in Blender, **SpeckleBlender** will just assign that existing material to the object. This allows geometry to be updated without having to re-assign and re-create materials.
|
||||
- If a `renderMaterial` property is found, **SpeckleBlender** will create a material named using the sub-property `renderMaterial.name`. If a material with that name already exists in Blender, **SpeckleBlender** will just assign that existing material to the object. This allows geometry to be updated without having to re-assign and re-create materials.
|
||||
- Vertex colors are supported. The `colors` list from Speckle meshes is translated to a vertex color layer.
|
||||
- Speckle properties will be imported as custom properties on Blender objects. Nested dictionaries are expanded to individual properties by flattening their key hierarchy. I.e. `propA:{'propB': {'propC':10, 'propD':'foobar'}}` is flattened to `propA.propB.propC = 10` and `propA.propB.propD = "foobar"`.
|
||||
|
||||
@@ -63,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!
|
||||
|
||||
+18
-44
@@ -1,25 +1,15 @@
|
||||
# MIT License
|
||||
import bpy
|
||||
from bpy_speckle.installer import ensure_dependencies
|
||||
|
||||
# Copyright (c) 2018-2021 Tom Svilans
|
||||
ensure_dependencies(f"Blender {bpy.app.version[0]}.{bpy.app.version[1]}")
|
||||
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
from specklepy.logging import metrics
|
||||
|
||||
from bpy_speckle.ui import *
|
||||
from bpy_speckle.properties import *
|
||||
from bpy_speckle.operators import *
|
||||
from bpy_speckle.callbacks import *
|
||||
from bpy.app.handlers import persistent
|
||||
|
||||
bl_info = {
|
||||
"name": "SpeckleBlender 2.0",
|
||||
@@ -33,44 +23,26 @@ bl_info = {
|
||||
"category": "Scene",
|
||||
}
|
||||
|
||||
import bpy
|
||||
|
||||
"""
|
||||
Import PySpeckle and attempt install if not found
|
||||
"""
|
||||
|
||||
try:
|
||||
import specklepy
|
||||
except ModuleNotFoundError as error:
|
||||
print("Speckle not found.")
|
||||
# TODO: Implement automatic installation of speckle and dependencies
|
||||
# to the local Blender module folder
|
||||
|
||||
# from .install_dependencies import install_dependencies
|
||||
# install_dependencies()
|
||||
|
||||
"""
|
||||
Import SpeckleBlender classes
|
||||
"""
|
||||
|
||||
from specklepy.api.client import SpeckleClient # , SpeckleCache
|
||||
|
||||
from bpy_speckle.ui import *
|
||||
from bpy_speckle.properties import *
|
||||
from bpy_speckle.operators import *
|
||||
from bpy_speckle.callbacks import *
|
||||
from bpy.app.handlers import persistent
|
||||
|
||||
"""
|
||||
Add load handler to initialize Speckle when
|
||||
loading a Blender file
|
||||
"""
|
||||
|
||||
|
||||
@persistent
|
||||
def load_handler(dummy):
|
||||
bpy.ops.speckle.users_load()
|
||||
|
||||
pass
|
||||
# Calling users_load is an expensive operation, one that force users to wait a good 10s every time blender loads.
|
||||
# Until we can do this non-blocking, we will make the user hit the refresh button each time.
|
||||
#bpy.ops.speckle.users_load()
|
||||
|
||||
# Instead, we shall just reset the user selection to an uninitiailised state
|
||||
bpy.ops.speckle.users_reset()
|
||||
|
||||
"""
|
||||
Permanent handle on callbacks
|
||||
@@ -94,6 +66,8 @@ def register():
|
||||
for cls in speckle_classes:
|
||||
register_class(cls)
|
||||
|
||||
metrics.set_host_app("blender", f"blender {bpy.app.version_string}")
|
||||
|
||||
"""
|
||||
Register all new properties
|
||||
"""
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
from typing import Deque, Dict, List, Optional, Set, Tuple, Union
|
||||
import bpy
|
||||
from bpy.types import Object, Collection, ID
|
||||
from specklepy.objects.base import Base
|
||||
from bpy_speckle.functions import _report
|
||||
from bpy_speckle.specklepy_extras.commit_object_builder import CommitObjectBuilder, ROOT
|
||||
from specklepy.objects import Base
|
||||
from specklepy.objects.other import Collection as SCollection
|
||||
from attrs import define
|
||||
|
||||
ELEMENTS = "elements"
|
||||
|
||||
def _id(natvive_object: ID) -> str:
|
||||
#NOTE: to avoid naming collisions, we prefix collections and objects differently
|
||||
return f"{type(natvive_object).__name__}:{natvive_object.name_full}"
|
||||
|
||||
def _try_id(natvive_object: Optional[Union[Collection, Object]]) -> Optional[str]:
|
||||
return _id(natvive_object) if natvive_object else None
|
||||
|
||||
def convert_collection_to_speckle(col: Collection) -> SCollection:
|
||||
convered_collection = SCollection(name = col.name_full, collectionType = "Blender Collection", elements = [])
|
||||
convered_collection.applicationId = _id(col)
|
||||
return convered_collection
|
||||
|
||||
@define(slots=True)
|
||||
class BlenderCommitObjectBuilder(CommitObjectBuilder[Object]):
|
||||
|
||||
_collections: Dict[str, SCollection]
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self._collections = {}
|
||||
|
||||
def include_object(self, conversion_result: Base, native_object: Object) -> None:
|
||||
|
||||
# Set the Child -> Parent relationships
|
||||
parent = native_object.parent
|
||||
|
||||
parent_collections: Tuple[Collection] = native_object.users_collection # type: ignore
|
||||
parent_collection = parent_collections[0] if len(parent_collections) > 0 else None #NOTE: we don't support objects appearing in more than one collection, for now, we will just take the zeroth one
|
||||
|
||||
app_id = _id(native_object)
|
||||
conversion_result.applicationId = app_id
|
||||
self.converted[app_id] = conversion_result
|
||||
|
||||
# in order or priority, direct parent, direct parent collection, root
|
||||
self.set_relationship(app_id, (_try_id(parent), ELEMENTS), (_try_id(parent_collection), ELEMENTS), (ROOT, ELEMENTS))
|
||||
# if parent_collection:
|
||||
# self._include_collection(parent_collection)
|
||||
|
||||
def ensure_collection(self, col: Collection) -> SCollection:
|
||||
id = _id(col)
|
||||
if id in self._collections:
|
||||
return self._collections[id] # collection already converted!
|
||||
|
||||
# Set the Parent -> Children relationships
|
||||
for c in col.children:
|
||||
#NOTE: There's no falling back to the grandparent, if the direct parent collection wasn't converted, then we we fallback to the root
|
||||
self.set_relationship(_id(c), (id, ELEMENTS), (ROOT, ELEMENTS))
|
||||
|
||||
# Set Child -> Parent relationship
|
||||
# parent = self.find_collection_parent(col)
|
||||
# self.set_relationship(id, (_try_builder_id(parent), ELEMENTS), (ROOT, ELEMENTS))
|
||||
|
||||
convered_collection = convert_collection_to_speckle(col)
|
||||
self.converted[id] = convered_collection
|
||||
self._collections[id] = convered_collection
|
||||
|
||||
return convered_collection
|
||||
|
||||
# def find_collection_parent(self, col: Collection) -> Optional[Collection]:
|
||||
# for p in bpy.data.collections:
|
||||
# if col.name in p.children.keys():
|
||||
# return p
|
||||
# return None
|
||||
|
||||
#TODO: I've started an approach that will not work
|
||||
# Goal #1 get all collections sending
|
||||
# Sync with Claire, ask how we handle this in Rhino with partial selection of layers (proably how I'm expecting it works, but good to double check)
|
||||
# Goal #2 Figure out how to send collections
|
||||
# - all collections
|
||||
# - all collections that contain a child collection that has geometry...
|
||||
# - only collections explicitly selected
|
||||
|
||||
def build_commit_object(self, root_commit_object: Base) -> None:
|
||||
assert(root_commit_object.applicationId in self.converted)
|
||||
|
||||
# Create all collections
|
||||
root_col = self.ensure_collection(bpy.context.scene.collection)
|
||||
root_col.collectionType = "Scene Collection"
|
||||
for col in bpy.context.scene.collection.children_recursive:
|
||||
self.ensure_collection(col)
|
||||
|
||||
objects_to_build = set(self.converted.values())
|
||||
objects_to_build.remove(root_commit_object)
|
||||
|
||||
self.apply_relationships(objects_to_build, root_commit_object)
|
||||
|
||||
assert(isinstance(root_commit_object, SCollection))
|
||||
# Kill unused collections
|
||||
|
||||
def should_remove_unuseful_collection(col: SCollection) -> bool: #TODO: this maybe could be optimised
|
||||
elements = col.elements
|
||||
if not elements: return True
|
||||
|
||||
should_remove_this_col = True
|
||||
|
||||
i = 0
|
||||
while i < len(elements):
|
||||
c = elements[i]
|
||||
if not isinstance(c, SCollection):
|
||||
# col has objects (c)
|
||||
should_remove_this_col = False
|
||||
i += 1
|
||||
continue
|
||||
|
||||
if should_remove_unuseful_collection(c):
|
||||
# c is not useful, kill it
|
||||
del elements[i]
|
||||
else:
|
||||
# col has a child (c) with objects
|
||||
should_remove_this_col = False
|
||||
i += 1
|
||||
continue
|
||||
|
||||
return should_remove_this_col
|
||||
|
||||
if should_remove_unuseful_collection(root_commit_object):
|
||||
_report("WARNING: Only empty collections have been converted!") #TODO: consider raising exception here, to halt the send operation
|
||||
|
||||
|
||||
|
||||
|
||||
# def build_commit_object(self, root_commit_object: Base) -> None:
|
||||
|
||||
# convertedObjects = [x for x in self.converted.values()] # Converted objects, but no collections!
|
||||
# for (id, col) in self._collections.items():
|
||||
# self.converted[id] = col
|
||||
|
||||
# # Apply relationships for all non-collection objects
|
||||
# self.apply_relationships(convertedObjects, root_commit_object)
|
||||
|
||||
# # Remove empty collections
|
||||
# for (id, col) in self._collections.items(): #TODO: XXX: How to ensure empty collections are avoided! Potentially need to traverse from root object down...
|
||||
# if not col.elements:
|
||||
# self.converted.pop(id)
|
||||
|
||||
# self.apply_relationships(convertedObjects, root_commit_object)
|
||||
|
||||
# return
|
||||
@@ -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')
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
"""
|
||||
Permanent handle on all user clients
|
||||
"""
|
||||
speckle_clients = []
|
||||
from specklepy.api.client import SpeckleClient
|
||||
|
||||
|
||||
speckle_clients: list[SpeckleClient] = []
|
||||
|
||||
@@ -1,239 +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"):
|
||||
return
|
||||
|
||||
speckle_mat = smesh.renderMaterial
|
||||
mat_name = getattr(speckle_mat, "name", None)
|
||||
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
|
||||
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)
|
||||
@@ -243,67 +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):
|
||||
blender_type = blender_object.type
|
||||
speckle_objects = []
|
||||
speckle_material = material_to_speckle(blender_object)
|
||||
|
||||
if blender_type in TO_SPECKLE.keys():
|
||||
converted = TO_SPECKLE[blender_type](blender_object, blender_object.data, scale)
|
||||
if isinstance(converted, list):
|
||||
speckle_objects.extend([c for c in converted if c != None])
|
||||
|
||||
for so in speckle_objects:
|
||||
so.properties = get_blender_custom_properties(blender_object)
|
||||
so.applicationId = so.properties.pop("applicationId", None)
|
||||
|
||||
if speckle_material:
|
||||
so["renderMaterial"] = speckle_material
|
||||
|
||||
# Set object transform
|
||||
so.properties["transform"] = [y for x in blender_object.matrix_world for y in x]
|
||||
|
||||
# _report(speckle_objects)
|
||||
return speckle_objects
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
from .mesh import import_mesh
|
||||
from .curve import import_curve
|
||||
from .brep import import_brep
|
||||
@@ -1,24 +0,0 @@
|
||||
import bpy
|
||||
from .mesh import to_bmesh
|
||||
from bpy_speckle.util import find_key_case_insensitive
|
||||
|
||||
|
||||
def import_brep(speckle_brep, scale, name=None):
|
||||
if not name:
|
||||
name = speckle_brep.geometryHash or speckle_brep.id
|
||||
|
||||
display = getattr(
|
||||
speckle_brep, "displayMesh", getattr(speckle_brep, "displayValue", None)
|
||||
)
|
||||
if display:
|
||||
if name in bpy.data.meshes.keys():
|
||||
mesh = bpy.data.meshes[name]
|
||||
else:
|
||||
mesh = bpy.data.meshes.new(name=name)
|
||||
|
||||
to_bmesh(display, mesh, name, scale)
|
||||
# add_custom_properties(speckle_brep[dvKey], mesh)
|
||||
else:
|
||||
mesh = None
|
||||
|
||||
return mesh
|
||||
@@ -1,237 +0,0 @@
|
||||
import bpy, math
|
||||
from bpy_speckle.util import find_key_case_insensitive
|
||||
import mathutils
|
||||
from specklepy.objects.geometry import *
|
||||
|
||||
CONVERT = {}
|
||||
|
||||
|
||||
def import_line(scurve, bcurve, scale):
|
||||
line = bcurve.splines.new("POLY")
|
||||
line.points.add(1)
|
||||
|
||||
line.points[0].co = (
|
||||
float(scurve.start.x) * scale,
|
||||
float(scurve.start.y) * scale,
|
||||
float(scurve.start.z) * scale,
|
||||
1,
|
||||
)
|
||||
|
||||
if scurve.end:
|
||||
|
||||
line.points[1].co = (
|
||||
float(scurve.end.x) * scale,
|
||||
float(scurve.end.y) * scale,
|
||||
float(scurve.end.z) * scale,
|
||||
1,
|
||||
)
|
||||
|
||||
return line
|
||||
|
||||
|
||||
CONVERT[Line] = import_line
|
||||
|
||||
|
||||
def import_polyline(scurve, bcurve, scale):
|
||||
|
||||
# value = find_key_case_insensitive(scurve, "value")
|
||||
value = scurve.value
|
||||
|
||||
if value:
|
||||
N = int(len(value) / 3)
|
||||
|
||||
polyline = bcurve.splines.new("POLY")
|
||||
|
||||
if hasattr(scurve, "closed"):
|
||||
polyline.use_cyclic_u = scurve.closed
|
||||
|
||||
# if "closed" in scurve.keys():
|
||||
# polyline.use_cyclic_u = scurve["closed"]
|
||||
|
||||
polyline.points.add(N - 1)
|
||||
for i in range(N):
|
||||
polyline.points[i].co = (
|
||||
float(value[i * 3]) * scale,
|
||||
float(value[i * 3 + 1]) * scale,
|
||||
float(value[i * 3 + 2]) * scale,
|
||||
1,
|
||||
)
|
||||
|
||||
return polyline
|
||||
|
||||
|
||||
CONVERT[Polyline] = import_polyline
|
||||
|
||||
|
||||
def import_nurbs_curve(scurve, bcurve, scale):
|
||||
|
||||
# points = find_key_case_insensitive(scurve, "points")
|
||||
points = scurve.points
|
||||
|
||||
if points:
|
||||
N = int(len(points) / 3)
|
||||
|
||||
nurbs = bcurve.splines.new("NURBS")
|
||||
|
||||
if hasattr(scurve, "closed"):
|
||||
nurbs.use_cyclic_u = scurve.closed != 0
|
||||
|
||||
nurbs.points.add(N - 1)
|
||||
for i in range(N):
|
||||
nurbs.points[i].co = (
|
||||
float(points[i * 3]) * scale,
|
||||
float(points[i * 3 + 1]) * scale,
|
||||
float(points[i * 3 + 2]) * scale,
|
||||
1,
|
||||
)
|
||||
|
||||
if len(scurve.weights) == len(nurbs.points):
|
||||
for i, w in enumerate(scurve.weights):
|
||||
nurbs.points[i].weight = w
|
||||
|
||||
# TODO: anaylize curve knots to decide if use_endpoint_u or use_bezier_u should be enabled
|
||||
# nurbs.use_endpoint_u = True
|
||||
nurbs.order_u = scurve.degree + 1
|
||||
|
||||
return nurbs
|
||||
|
||||
|
||||
CONVERT[Curve] = import_nurbs_curve
|
||||
|
||||
|
||||
def import_arc(rcurve, bcurve, scale):
|
||||
"""
|
||||
Convert Arc object
|
||||
TODO: improve Blender representation of arc
|
||||
"""
|
||||
plane = rcurve.plane
|
||||
if not plane:
|
||||
return
|
||||
|
||||
origin = plane.origin
|
||||
normal = mathutils.Vector([plane.normal.x, plane.normal.y, plane.normal.z])
|
||||
|
||||
xaxis = plane.xdir
|
||||
yaxis = plane.ydir
|
||||
|
||||
radius = rcurve.radius * scale
|
||||
startAngle = rcurve.startAngle
|
||||
endAngle = rcurve.endAngle
|
||||
|
||||
startQuat = mathutils.Quaternion(normal, startAngle)
|
||||
endQuat = mathutils.Quaternion(normal, endAngle)
|
||||
|
||||
"""
|
||||
Get start and end vectors, centre point, angles, etc.
|
||||
"""
|
||||
|
||||
r1 = mathutils.Vector([plane.xdir.x, plane.xdir.y, plane.xdir.z])
|
||||
r1.rotate(startQuat)
|
||||
|
||||
r2 = mathutils.Vector([plane.xdir.x, plane.xdir.y, plane.xdir.z])
|
||||
r2.rotate(endQuat)
|
||||
|
||||
c = mathutils.Vector([plane.origin.x, plane.origin.y, plane.origin.z]) * scale
|
||||
|
||||
spt = c + r1 * radius
|
||||
ept = c + r2 * radius
|
||||
|
||||
angle = endAngle - startAngle
|
||||
|
||||
t1 = normal.cross(r1)
|
||||
|
||||
"""
|
||||
Initialize arc data and calculate subdivisions
|
||||
"""
|
||||
arc = bcurve.splines.new("NURBS")
|
||||
|
||||
arc.use_cyclic_u = False
|
||||
|
||||
Ndiv = max(int(math.floor(angle / 0.3)), 2)
|
||||
step = angle / float(Ndiv)
|
||||
stepQuat = mathutils.Quaternion(normal, step)
|
||||
tan = math.tan(step / 2) * radius
|
||||
|
||||
arc.points.add(Ndiv + 1)
|
||||
|
||||
"""
|
||||
Set start and end points
|
||||
"""
|
||||
arc.points[0].co = (spt.x, spt.y, spt.z, 1)
|
||||
arc.points[Ndiv + 1].co = (ept.x, ept.y, ept.z, 1)
|
||||
|
||||
"""
|
||||
Set intermediate points
|
||||
"""
|
||||
for i in range(Ndiv):
|
||||
t1 = normal.cross(r1)
|
||||
pt = c + r1 * radius + t1 * tan
|
||||
arc.points[i + 1].co = (pt.x, pt.y, pt.z, 1)
|
||||
r1.rotate(stepQuat)
|
||||
|
||||
"""
|
||||
Set curve settings
|
||||
"""
|
||||
|
||||
arc.use_endpoint_u = True
|
||||
arc.order_u = 3
|
||||
|
||||
return arc
|
||||
|
||||
|
||||
CONVERT[Arc] = import_arc
|
||||
|
||||
|
||||
def import_null(speckle_object, bcurve, scale):
|
||||
"""
|
||||
Handle unsupported types
|
||||
"""
|
||||
print("Failed to convert type", speckle_object["type"])
|
||||
return None
|
||||
|
||||
|
||||
def import_polycurve(scurve, bcurve, scale):
|
||||
"""
|
||||
Convert Polycurve object
|
||||
"""
|
||||
segments = scurve.segments
|
||||
|
||||
curves = []
|
||||
|
||||
for seg in segments:
|
||||
speckle_type = type(seg)
|
||||
|
||||
if speckle_type in CONVERT.keys():
|
||||
# segcurve = SCHEMAS[speckle_type].parse_obj(seg)
|
||||
curves.append(CONVERT[speckle_type](seg, bcurve, scale))
|
||||
else:
|
||||
print("Unsupported curve type: {}".format(speckle_type))
|
||||
|
||||
return curves
|
||||
|
||||
|
||||
CONVERT[Polycurve] = import_polycurve
|
||||
|
||||
|
||||
def import_curve(speckle_curve, scale, name=None):
|
||||
"""
|
||||
Convert Curve object
|
||||
"""
|
||||
if not name:
|
||||
name = speckle_curve.geometryHash or speckle_curve.id or "SpeckleCurve"
|
||||
|
||||
if name in bpy.data.curves.keys():
|
||||
curve_data = bpy.data.curves[name]
|
||||
else:
|
||||
curve_data = bpy.data.curves.new(name, type="CURVE")
|
||||
|
||||
curve_data.dimensions = "3D"
|
||||
curve_data.resolution_u = 12
|
||||
|
||||
if type(speckle_curve) not in CONVERT.keys():
|
||||
print("Unsupported curve type: {}".format(speckle_curve.type))
|
||||
return None
|
||||
|
||||
CONVERT[type(speckle_curve)](speckle_curve, curve_data, scale)
|
||||
|
||||
return curve_data
|
||||
@@ -1,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() and False:
|
||||
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
|
||||
@@ -0,0 +1,690 @@
|
||||
import math
|
||||
from typing import Tuple, Union, Collection
|
||||
from bpy_speckle.functions import get_scale_length, _report
|
||||
from mathutils import (
|
||||
Matrix as MMatrix,
|
||||
Vector as MVector,
|
||||
Quaternion as MQuaternion,
|
||||
)
|
||||
import bpy, bmesh
|
||||
from specklepy.objects.other import (
|
||||
Instance,
|
||||
Transform,
|
||||
BlockDefinition,
|
||||
)
|
||||
from specklepy.objects.geometry import *
|
||||
from bpy.types import Object
|
||||
from .util import (
|
||||
get_render_material,
|
||||
link_object_to_collection_nested,
|
||||
render_material_to_native,
|
||||
add_custom_properties,
|
||||
add_vertices,
|
||||
add_faces,
|
||||
add_colors,
|
||||
add_uv_coords,
|
||||
)
|
||||
|
||||
SUPPORTED_CURVES = (Line, Polyline, Curve, Arc, Polycurve, Ellipse, Circle)
|
||||
CAN_CONVERT_TO_NATIVE = (
|
||||
|
||||
Mesh,
|
||||
*SUPPORTED_CURVES,
|
||||
Instance,
|
||||
)
|
||||
|
||||
|
||||
def _has_native_convesion(speckle_object: Base) -> bool:
|
||||
return any(isinstance(speckle_object, t) for t in CAN_CONVERT_TO_NATIVE) #or "View" in speckle_object.speckle_type #hack
|
||||
|
||||
def _has_fallback_conversion(speckle_object: Base) -> bool:
|
||||
return any(getattr(speckle_object, alias, None) for alias in DISPLAY_VALUE_PROPERTY_ALIASES)
|
||||
|
||||
def can_convert_to_native(speckle_object: Base) -> bool:
|
||||
|
||||
if(_has_native_convesion(speckle_object) or _has_fallback_conversion(speckle_object)):
|
||||
return True
|
||||
return False
|
||||
|
||||
def create_new_object(obj_data: Optional[bpy.types.ID], desired_name: str, counter: int = 0) -> bpy.types.Object:
|
||||
"""
|
||||
Creates a new blender object with a unique name,
|
||||
if the desired_name is already taken
|
||||
we'll append a number, with the format .xxx to the desired_name to ensure the name is unique.
|
||||
"""
|
||||
name = desired_name if counter == 0 else f"{desired_name[:OBJECT_NAME_MAX_LENGTH - 4]}.{counter:03d}" # format counter as name.xxx, truncate to ensure we don't exceed the object name max length
|
||||
|
||||
#TODO: This is very slow, and gets slower the more objects you receive with the same name...
|
||||
# We could use a binary/galloping search, and/or cache the name -> index within a receive.
|
||||
if name in bpy.data.objects.keys():
|
||||
#Object already exists, increment counter and try again!
|
||||
return create_new_object(obj_data, desired_name, counter + 1)
|
||||
|
||||
blender_object = bpy.data.objects.new(name, obj_data)
|
||||
return blender_object
|
||||
|
||||
convert_instances_as: str #HACK: This is hacky, we need a better way to pass settings down to the converter
|
||||
def set_convert_instances_as(value: str):
|
||||
global convert_instances_as
|
||||
convert_instances_as = value
|
||||
|
||||
#TODO: Check usages handle exceptions
|
||||
|
||||
def convert_to_native(speckle_object: Base) -> Object:
|
||||
|
||||
speckle_type = type(speckle_object)
|
||||
|
||||
object_name = _generate_object_name(speckle_object)
|
||||
scale = get_scale_factor(speckle_object)
|
||||
|
||||
converted: Union[bpy.types.ID, bpy.types.Object, None] = None
|
||||
children: list[Object] = []
|
||||
|
||||
# convert elements/breps
|
||||
if not _has_native_convesion(speckle_object):
|
||||
(converted, children) = display_value_to_native(speckle_object, object_name, scale)
|
||||
if not converted and not children:
|
||||
raise Exception(f"Zero geometry converted from displayValues for {speckle_object}")
|
||||
|
||||
# convert supported geometry
|
||||
elif isinstance(speckle_object, Mesh):
|
||||
converted = mesh_to_native(speckle_object, object_name, scale)
|
||||
elif speckle_type in SUPPORTED_CURVES:
|
||||
converted = icurve_to_native(speckle_object, object_name, scale)
|
||||
# elif "View" in speckle_object.speckle_type:
|
||||
# return view_to_native(speckle_object, object_name, scale)
|
||||
elif isinstance(speckle_object, Instance):
|
||||
if convert_instances_as == "linked_duplicates":
|
||||
(converted, children) = instance_to_native_object(speckle_object, scale)
|
||||
elif convert_instances_as == "collection_instance":
|
||||
converted = instance_to_native_collection_instance(speckle_object, scale)
|
||||
else:
|
||||
_report(f"convert_instances_as = '{convert_instances_as}' is not implemented, Instances will be converted as collection instances!")
|
||||
converted = instance_to_native_collection_instance(speckle_object, scale)
|
||||
else:
|
||||
raise Exception(f"Unsupported type {speckle_type}")
|
||||
|
||||
|
||||
if not isinstance(converted, Object):
|
||||
converted = create_new_object(converted, object_name)
|
||||
|
||||
converted.speckle.object_id = str(speckle_object.id)
|
||||
converted.speckle.enabled = True
|
||||
add_custom_properties(speckle_object, converted)
|
||||
|
||||
for child in children:
|
||||
child.parent = converted
|
||||
|
||||
return converted
|
||||
|
||||
|
||||
DISPLAY_VALUE_PROPERTY_ALIASES = ["displayValue", "@displayValue"]
|
||||
ELEMENTS_PROPERTY_ALIASES = ["elements", "@elements"]
|
||||
|
||||
|
||||
def display_value_to_native(speckle_object: Base, name: str, scale: float) -> tuple[Optional[bpy.types.Mesh], list[bpy.types.Object]]:
|
||||
return _members_to_native(speckle_object, name, scale, DISPLAY_VALUE_PROPERTY_ALIASES, True)
|
||||
|
||||
def elements_to_native(speckle_object: Base, name: str, scale: float) -> list[bpy.types.Object]:
|
||||
(_, elements) = _members_to_native(speckle_object, name, scale, ELEMENTS_PROPERTY_ALIASES, False)
|
||||
return elements
|
||||
|
||||
def _members_to_native(speckle_object: Base, name: str, scale: float, members: List[str], combineMeshes: bool) -> tuple[Optional[bpy.types.Mesh], list[bpy.types.Object]]:
|
||||
"""
|
||||
Converts a given speckle_object by converting specified members
|
||||
|
||||
if combineMeshes == True
|
||||
Converts mesh members as one mesh
|
||||
Converts non-mesh members as child Objects
|
||||
if combineMeshes == False
|
||||
Converts all members as child objects (first item of the returned tuple will be None)
|
||||
:returns: converted mesh, and any other converted child objects (may happen if members contained non-meshes)
|
||||
"""
|
||||
meshes: list[Mesh] = []
|
||||
others: list[Base] = []
|
||||
|
||||
for alias in members:
|
||||
display = getattr(speckle_object, alias, None)
|
||||
|
||||
count = 0
|
||||
MAX_DEPTH = 255 # some large value, to prevent infinite reccursion
|
||||
def seperate(value: Any) -> bool:
|
||||
nonlocal meshes, others, count, MAX_DEPTH
|
||||
|
||||
if combineMeshes and isinstance(value, Mesh):
|
||||
meshes.append(value)
|
||||
elif isinstance(value, Base):
|
||||
others.append(value)
|
||||
elif isinstance(value, list):
|
||||
count += 1
|
||||
if(count > MAX_DEPTH):
|
||||
return True
|
||||
for x in value:
|
||||
seperate(x)
|
||||
|
||||
return False
|
||||
|
||||
did_halt = seperate(display)
|
||||
|
||||
if did_halt:
|
||||
_report(f"Traversal of {speckle_object.speckle_type} {speckle_object.id} halted after traversal depth exceeds MAX_DEPTH={MAX_DEPTH}. Are there circular references object structure?")
|
||||
|
||||
|
||||
children: list[Object] = []
|
||||
mesh = None
|
||||
|
||||
if meshes:
|
||||
mesh = meshes_to_native(speckle_object, meshes, name, scale) #TODO: reconsider passing scale around...
|
||||
|
||||
for item in others:
|
||||
try:
|
||||
blender_object = convert_to_native(item)
|
||||
children.append(blender_object)
|
||||
except Exception as ex:
|
||||
_report(f"Failed to convert display value {item}: {ex}")
|
||||
|
||||
return (mesh, children)
|
||||
|
||||
|
||||
|
||||
def view_to_native(speckle_view, name: str, scale: float) -> bpy.types.Object:
|
||||
native_cam: bpy.types.Camera
|
||||
if name in bpy.data.cameras.keys():
|
||||
native_cam = bpy.data.cameras[name]
|
||||
else:
|
||||
native_cam = bpy.data.cameras.new(name=name)
|
||||
native_cam.lens = 18 # 90° horizontal fov
|
||||
|
||||
cam_obj = create_new_object(native_cam, name)
|
||||
|
||||
scale_factor = get_scale_factor(speckle_view, scale)
|
||||
tx = (speckle_view.origin.x * scale_factor)
|
||||
ty = (speckle_view.origin.y * scale_factor)
|
||||
tz = (speckle_view.origin.z * scale_factor)
|
||||
|
||||
forward = MVector((speckle_view.forwardDirection.x, speckle_view.forwardDirection.y, speckle_view.forwardDirection.z))
|
||||
up = MVector((speckle_view.upDirection.x, speckle_view.upDirection.y, speckle_view.upDirection.z))
|
||||
right = forward.cross(up).normalized()
|
||||
|
||||
cam_obj.matrix_world = MMatrix((
|
||||
(right.x, up.x, -forward.x, tx),
|
||||
(right.y, up.y, -forward.y, ty),
|
||||
(right.z, up.z, -forward.z, tz),
|
||||
(0, 0, 0, 1 )
|
||||
))
|
||||
return cam_obj
|
||||
|
||||
def mesh_to_native(speckle_mesh: Mesh, name: str, scale: float) -> bpy.types.Mesh:
|
||||
return meshes_to_native(speckle_mesh, [speckle_mesh], name, scale)
|
||||
|
||||
def meshes_to_native(element: Base, meshes: Collection[Mesh], name: str, scale: float) -> bpy.types.Mesh:
|
||||
if name in bpy.data.meshes.keys():
|
||||
return bpy.data.meshes[name]
|
||||
|
||||
blender_mesh = bpy.data.meshes.new(name=name)
|
||||
|
||||
fallback_material = get_render_material(element)
|
||||
|
||||
bm = bmesh.new()
|
||||
|
||||
# First pass, add vertex data
|
||||
for mesh in meshes:
|
||||
scale = get_scale_factor(mesh, scale)
|
||||
add_vertices(mesh, bm, scale)
|
||||
|
||||
bm.verts.ensure_lookup_table()
|
||||
|
||||
# Second pass, add face data
|
||||
offset = 0
|
||||
for i, mesh in enumerate(meshes):
|
||||
add_faces(mesh, bm, offset, i)
|
||||
|
||||
render_material = get_render_material(mesh) or fallback_material
|
||||
if render_material is not None:
|
||||
native_material = render_material_to_native(render_material)
|
||||
blender_mesh.materials.append(native_material)
|
||||
|
||||
offset += len(mesh.vertices) // 3
|
||||
|
||||
bm.faces.ensure_lookup_table()
|
||||
bm.verts.index_update()
|
||||
|
||||
# Third pass, add vertex instance data
|
||||
for mesh in meshes:
|
||||
add_colors(mesh, bm)
|
||||
add_uv_coords(mesh, bm)
|
||||
|
||||
bm.to_mesh(blender_mesh)
|
||||
bm.free()
|
||||
|
||||
return blender_mesh
|
||||
|
||||
|
||||
"""
|
||||
Curves
|
||||
"""
|
||||
|
||||
def line_to_native(speckle_curve: Line, blender_curve: bpy.types.Curve, scale: float) -> list[bpy.types.Spline]:
|
||||
if not speckle_curve.end: return []
|
||||
|
||||
line = blender_curve.splines.new("POLY")
|
||||
line.points.add(1)
|
||||
|
||||
line.points[0].co = (
|
||||
float(speckle_curve.start.x) * scale,
|
||||
float(speckle_curve.start.y) * scale,
|
||||
float(speckle_curve.start.z) * scale,
|
||||
1,
|
||||
)
|
||||
|
||||
line.points[1].co = (
|
||||
float(speckle_curve.end.x) * scale,
|
||||
float(speckle_curve.end.y) * scale,
|
||||
float(speckle_curve.end.z) * scale,
|
||||
1,
|
||||
)
|
||||
|
||||
return [line]
|
||||
|
||||
|
||||
def polyline_to_native(scurve: Polyline, bcurve: bpy.types.Curve, scale: float) -> list[bpy.types.Spline]:
|
||||
if not (value := scurve.value): return []
|
||||
N = len(value) // 3
|
||||
|
||||
polyline = bcurve.splines.new("POLY")
|
||||
|
||||
if hasattr(scurve, "closed"):
|
||||
polyline.use_cyclic_u = scurve.closed
|
||||
|
||||
polyline.points.add(N - 1)
|
||||
for i in range(N):
|
||||
polyline.points[i].co = (
|
||||
float(value[i * 3]) * scale,
|
||||
float(value[i * 3 + 1]) * scale,
|
||||
float(value[i * 3 + 2]) * scale,
|
||||
1,
|
||||
)
|
||||
|
||||
return [polyline]
|
||||
|
||||
|
||||
|
||||
def nurbs_to_native(scurve: Curve, bcurve: bpy.types.Curve, scale: float) -> list[bpy.types.Spline]:
|
||||
if not (points := scurve.points): return []
|
||||
|
||||
# Closed curves from rhino will have n + degree points. We ignore the extras
|
||||
num_points = len(points) // 3 - scurve.degree if (scurve.closed) else (
|
||||
len(points) // 3)
|
||||
|
||||
nurbs = bcurve.splines.new("NURBS")
|
||||
nurbs.use_cyclic_u = scurve.closed
|
||||
nurbs.use_endpoint_u = not scurve.periodic
|
||||
|
||||
nurbs.points.add(num_points - 1)
|
||||
use_weights = len(scurve.weights) >= num_points
|
||||
for i in range(num_points):
|
||||
nurbs.points[i].co = (
|
||||
float(points[i * 3]) * scale,
|
||||
float(points[i * 3 + 1]) * scale,
|
||||
float(points[i * 3 + 2]) * scale,
|
||||
1,
|
||||
)
|
||||
|
||||
nurbs.points[i].weight = scurve.weights[i] if use_weights else 1
|
||||
|
||||
nurbs.order_u = scurve.degree + 1
|
||||
|
||||
return [nurbs]
|
||||
|
||||
|
||||
def arc_to_native(rcurve: Arc, bcurve: bpy.types.Curve, scale: float) -> Optional[bpy.types.Spline]:
|
||||
# TODO: improve Blender representation of arc - check autocad test stream
|
||||
|
||||
plane = rcurve.plane
|
||||
if not plane:
|
||||
return None
|
||||
|
||||
normal = MVector([plane.normal.x, plane.normal.y, plane.normal.z])
|
||||
|
||||
radius = rcurve.radius * scale
|
||||
startAngle = rcurve.startAngle
|
||||
endAngle = rcurve.endAngle
|
||||
|
||||
startQuat = MQuaternion(normal, startAngle)
|
||||
endQuat = MQuaternion(normal, endAngle)
|
||||
|
||||
# Get start and end vectors, centre point, angles, etc.
|
||||
r1 = MVector([plane.xdir.x, plane.xdir.y, plane.xdir.z])
|
||||
r1.rotate(startQuat)
|
||||
|
||||
r2 = MVector([plane.xdir.x, plane.xdir.y, plane.xdir.z])
|
||||
r2.rotate(endQuat)
|
||||
|
||||
c = MVector([plane.origin.x, plane.origin.y, plane.origin.z]) * scale
|
||||
|
||||
spt = c + r1 * radius
|
||||
ept = c + r2 * radius
|
||||
|
||||
angle = endAngle - startAngle
|
||||
|
||||
t1 = normal.cross(r1)
|
||||
|
||||
# Initialize arc data and calculate subdivisions
|
||||
arc = bcurve.splines.new("NURBS")
|
||||
|
||||
arc.use_cyclic_u = False
|
||||
|
||||
Ndiv = max(int(math.floor(angle / 0.3)), 2)
|
||||
step = angle / float(Ndiv)
|
||||
stepQuat = MQuaternion(normal, step)
|
||||
tan = math.tan(step / 2) * radius
|
||||
|
||||
arc.points.add(Ndiv + 1)
|
||||
|
||||
# Set start and end points
|
||||
arc.points[0].co = (spt.x, spt.y, spt.z, 1)
|
||||
arc.points[Ndiv + 1].co = (ept.x, ept.y, ept.z, 1)
|
||||
|
||||
# Set intermediate points
|
||||
for i in range(Ndiv):
|
||||
t1 = normal.cross(r1)
|
||||
pt = c + r1 * radius + t1 * tan
|
||||
arc.points[i + 1].co = (pt.x, pt.y, pt.z, 1)
|
||||
r1.rotate(stepQuat)
|
||||
|
||||
# Set curve settings
|
||||
arc.use_endpoint_u = True
|
||||
arc.order_u = 3
|
||||
|
||||
return arc
|
||||
|
||||
|
||||
def polycurve_to_native(scurve: Polycurve, bcurve: bpy.types.Curve, scale: float) -> list[bpy.types.Spline]:
|
||||
"""
|
||||
Convert Polycurve object
|
||||
"""
|
||||
segments = scurve.segments
|
||||
|
||||
curves = []
|
||||
|
||||
for seg in segments:
|
||||
speckle_type = type(seg)
|
||||
|
||||
if speckle_type in SUPPORTED_CURVES:
|
||||
curves.append(icurve_to_native_spline(seg, bcurve, scale))
|
||||
else:
|
||||
_report(f"Unsupported curve type: {speckle_type}")
|
||||
|
||||
return curves
|
||||
|
||||
def ellipse_to_native(ellipse: Union[Ellipse, Circle], bcurve: bpy.types.Curve, units_scale: float) -> list[bpy.types.Spline]:
|
||||
|
||||
radX: float
|
||||
radY: float
|
||||
if isinstance(ellipse, Ellipse):
|
||||
radX = ellipse.firstRadius * units_scale
|
||||
radY = ellipse.secondRadius * units_scale
|
||||
else:
|
||||
radX = ellipse.radius * units_scale
|
||||
radY = ellipse.radius * units_scale
|
||||
|
||||
|
||||
D = 0.5522847498307936 # (4/3)*tan(pi/8)
|
||||
|
||||
right_handles = [
|
||||
(+radX, +radY * D, 0.0),
|
||||
(-radX * D, +radY, 0.0),
|
||||
(-radX, -radY * D, 0.0),
|
||||
(+radX * D, -radY, 0.0),
|
||||
]
|
||||
|
||||
left_handles = [
|
||||
(+radX, -radY * D, 0.0),
|
||||
(+radX * D, +radY, 0.0),
|
||||
(-radX, +radY * D, 0.0),
|
||||
(-radX * D, -radY, 0.0),
|
||||
]
|
||||
|
||||
points = [
|
||||
(+radX, 0.0, 0.0),
|
||||
(0.0, +radY, 0.0),
|
||||
(-radX, 0.0, 0.0),
|
||||
(0.0, -radY, 0.0),
|
||||
]
|
||||
transform = plane_to_native_transform(ellipse.plane, units_scale)
|
||||
|
||||
spline = bcurve.splines.new("BEZIER")
|
||||
spline.bezier_points.add(len(points) - 1)
|
||||
|
||||
for i in range(len(points)):
|
||||
spline.bezier_points[i].co = transform @ MVector(points[i])
|
||||
spline.bezier_points[i].handle_left = transform @ MVector(left_handles[i])
|
||||
spline.bezier_points[i].handle_right = transform @ MVector(right_handles[i])
|
||||
|
||||
spline.use_cyclic_u = True
|
||||
|
||||
#TODO support trims?
|
||||
return [spline]
|
||||
|
||||
|
||||
def icurve_to_native_spline(speckle_curve: Base, blender_curve: bpy.types.Curve, scale: float) -> list[bpy.types.Spline]:
|
||||
# polycurves
|
||||
if isinstance(speckle_curve, Polycurve):
|
||||
return polycurve_to_native(speckle_curve, blender_curve, scale)
|
||||
|
||||
# single curves
|
||||
if isinstance(speckle_curve, Line):
|
||||
spline = line_to_native(speckle_curve, blender_curve, scale)
|
||||
elif isinstance(speckle_curve, Curve):
|
||||
spline = nurbs_to_native(speckle_curve, blender_curve, scale)
|
||||
elif isinstance(speckle_curve, Polyline):
|
||||
spline = polyline_to_native(speckle_curve, blender_curve, scale)
|
||||
elif isinstance(speckle_curve, Arc):
|
||||
spline = arc_to_native(speckle_curve, blender_curve, scale)
|
||||
elif isinstance(speckle_curve, Ellipse) or isinstance(speckle_curve, Circle):
|
||||
spline = ellipse_to_native(speckle_curve, blender_curve, scale)
|
||||
else:
|
||||
raise TypeError(f"{speckle_curve} is not a supported curve type. Supported types: {SUPPORTED_CURVES}")
|
||||
|
||||
return [spline] if spline is not None else []
|
||||
|
||||
|
||||
def icurve_to_native(speckle_curve: Base, name: str, scale: float) -> bpy.types.Curve:
|
||||
curve_type = type(speckle_curve)
|
||||
if curve_type not in SUPPORTED_CURVES:
|
||||
raise Exception(f"Unsupported curve type: {curve_type}")
|
||||
|
||||
blender_curve = (
|
||||
bpy.data.curves[name]
|
||||
if name in bpy.data.curves.keys()
|
||||
else bpy.data.curves.new(name, type="CURVE")
|
||||
)
|
||||
blender_curve.dimensions = "3D"
|
||||
blender_curve.resolution_u = 12 #TODO: We could maybe decern the resolution from the ployline displayValue
|
||||
|
||||
icurve_to_native_spline(speckle_curve, blender_curve, scale)
|
||||
|
||||
return blender_curve
|
||||
|
||||
|
||||
"""
|
||||
Transforms and Intances
|
||||
"""
|
||||
|
||||
def transform_to_native(transform: Transform, scale: float) -> MMatrix:
|
||||
mat = MMatrix(
|
||||
[
|
||||
transform.value[:4],
|
||||
transform.value[4:8],
|
||||
transform.value[8:12],
|
||||
transform.value[12:16],
|
||||
]
|
||||
)
|
||||
# scale the translation
|
||||
for i in range(3):
|
||||
mat[i][3] *= scale
|
||||
return mat
|
||||
|
||||
def plane_to_native_transform(plane: Plane, fallback_scale:float = 1) -> MMatrix:
|
||||
scale_factor = get_scale_factor(plane, fallback_scale)
|
||||
tx = (plane.origin.x * scale_factor)
|
||||
ty = (plane.origin.y * scale_factor)
|
||||
tz = (plane.origin.z * scale_factor)
|
||||
|
||||
|
||||
return MMatrix((
|
||||
(plane.xdir.x, plane.ydir.x, plane.normal.x, tx),
|
||||
(plane.xdir.y, plane.ydir.y, plane.normal.y, ty),
|
||||
(plane.xdir.z, plane.ydir.z, plane.normal.z, tz),
|
||||
(0, 0, 0, 1 )
|
||||
))
|
||||
|
||||
|
||||
"""
|
||||
Instances / Blocks
|
||||
"""
|
||||
|
||||
def _get_instance_name(instance: Instance) -> str:
|
||||
name_prefix = _get_friendly_object_name(instance) or _get_friendly_object_name(instance.definition) or _simplified_speckle_type(instance.speckle_type)
|
||||
return f"{name_prefix}{OBJECT_NAME_SEPERATOR}{instance.id}"
|
||||
|
||||
|
||||
def instance_to_native_object(instance: Instance, scale: float) -> Tuple[bpy.types.Object, List[bpy.types.Object]]:
|
||||
"""
|
||||
Converts Instance to a unique object with (potentially) shared data (linked duplicate)
|
||||
"""
|
||||
if not instance.definition: raise Exception(f"Instance is missing a definition")
|
||||
if not instance.transform: raise Exception(f"Instance is missing a transform")
|
||||
|
||||
name = _get_instance_name(instance)
|
||||
definition = instance.definition
|
||||
|
||||
native_instance: Object
|
||||
native_elements: List[Object] = []
|
||||
elements_on_instance: List[Object] = []
|
||||
|
||||
if isinstance(definition, BlockDefinition): #NOTE: We have to handle BlockDefinitions specially here, since they don't follow normal traversal rules
|
||||
native_instance = create_new_object(None, name) #Instance will be empty
|
||||
native_instance.empty_display_size = 0
|
||||
for geo in definition.geometry:
|
||||
native_elements.append(convert_to_native(geo))
|
||||
else:
|
||||
native_instance = convert_to_native(instance.definition)
|
||||
|
||||
instance_transform = transform_to_native(instance.transform, scale)
|
||||
instance_transform_inverted = instance_transform.inverted()
|
||||
native_instance.matrix_world = instance_transform
|
||||
|
||||
elements_on_instance = elements_to_native(instance, name, scale)
|
||||
for c in elements_on_instance:
|
||||
c.matrix_world = instance_transform_inverted @ c.matrix_world #Undo the instance transform on elements
|
||||
|
||||
native_elements.extend(elements_on_instance)
|
||||
|
||||
return (native_instance, native_elements) #TODO: need to double check that all child objects have custom props attached correctly
|
||||
|
||||
def instance_to_native_collection_instance(instance: Instance, scale: float) -> bpy.types.Object:
|
||||
"""
|
||||
Convert an Instance as a transformed Object with the `instance_collection` property
|
||||
set to be the `instance.Definition` converted as a collection
|
||||
|
||||
The definition collection won't be linked to the current scene
|
||||
Any Elements on the instance object will also be converted (and spacially transformed)
|
||||
"""
|
||||
if not instance.definition: raise Exception(f"Instance is missing a definition")
|
||||
if not instance.transform: raise Exception(f"Instance is missing a transform")
|
||||
|
||||
name = _get_instance_name(instance)
|
||||
|
||||
# Get/Convert definition collection
|
||||
collection_def = _instance_definition_to_native(instance.definition)
|
||||
|
||||
# Convert elements as children of collection instance object
|
||||
elements = elements_to_native(instance, name, scale)
|
||||
|
||||
instance_transform = transform_to_native(instance.transform, scale)
|
||||
instance_transform_inverted = instance_transform.inverted()
|
||||
|
||||
native_instance = bpy.data.objects.new(name, None)
|
||||
|
||||
#add_custom_properties(instance, native_instance)
|
||||
# hide the instance axes so they don't clutter the viewport
|
||||
native_instance.empty_display_size = 0
|
||||
native_instance.instance_collection = collection_def
|
||||
native_instance.instance_type = "COLLECTION"
|
||||
native_instance.matrix_world =instance_transform
|
||||
|
||||
for c in elements:
|
||||
c.matrix_world = instance_transform_inverted @ c.matrix_world #Undo the instance transform on elements
|
||||
c.parent = native_instance #TODO: need to double check that all child objects have custom props attached correctly
|
||||
|
||||
return native_instance
|
||||
|
||||
def _instance_definition_to_native(definition: Union[Base, BlockDefinition]) -> bpy.types.Collection:
|
||||
"""
|
||||
Converts a geometry carrying Base as a collection (does not link it to the scene)
|
||||
"""
|
||||
name = _generate_object_name(definition)
|
||||
native_def = bpy.data.collections.get(name)
|
||||
if native_def:
|
||||
return native_def
|
||||
|
||||
native_def = bpy.data.collections.new(name)
|
||||
native_def["applicationId"] = definition.applicationId
|
||||
|
||||
#TODO could maybe replace BlockDefinition awareness with a single traverse member call
|
||||
geometry = definition.geometry if isinstance(definition, BlockDefinition) else [definition]
|
||||
|
||||
for geo in geometry:
|
||||
if not geo: continue
|
||||
converted = convert_to_native(geo) #NOTE: we assume the last item is the root converted item
|
||||
link_object_to_collection_nested(converted, native_def)
|
||||
|
||||
|
||||
return native_def
|
||||
|
||||
|
||||
"""
|
||||
Object Naming
|
||||
"""
|
||||
|
||||
def _get_friendly_object_name(speckle_object: Base) -> Optional[str]:
|
||||
return (getattr(speckle_object, "name", None)
|
||||
or getattr(speckle_object, "Name", None)
|
||||
or getattr(speckle_object, "family", None)
|
||||
)
|
||||
|
||||
|
||||
# Blender object names must not exceed 62 characters
|
||||
# We need to ensure the complete ID is included in the name (to prevent identity collisions)
|
||||
# So we if the name is too long, we need to truncate
|
||||
OBJECT_NAME_MAX_LENGTH = 62
|
||||
SPECKLE_ID_LENGTH = 32
|
||||
OBJECT_NAME_SEPERATOR = " -- "
|
||||
|
||||
def _truncate_object_name(name: str) -> str:
|
||||
|
||||
MAX_NAME_LENGTH = OBJECT_NAME_MAX_LENGTH - SPECKLE_ID_LENGTH - len(OBJECT_NAME_SEPERATOR)
|
||||
|
||||
return name[:MAX_NAME_LENGTH]
|
||||
|
||||
|
||||
def _simplified_speckle_type(speckle_type: str) -> str:
|
||||
return(speckle_type.rsplit('.')[-1]) #Take only the most specific object type name (without namespace)
|
||||
|
||||
def _generate_object_name(speckle_object: Base) -> str:
|
||||
prefix: str
|
||||
name = _get_friendly_object_name(speckle_object)
|
||||
if name:
|
||||
prefix = _truncate_object_name(name)
|
||||
else:
|
||||
prefix = _simplified_speckle_type(speckle_object.speckle_type)
|
||||
|
||||
return f"{prefix}{OBJECT_NAME_SEPERATOR}{speckle_object.id}"
|
||||
|
||||
|
||||
def get_scale_factor(speckle_object: Base, fallback: float = 1.0) -> float:
|
||||
scale = fallback
|
||||
if units := getattr(speckle_object, "units", None):
|
||||
scale = get_scale_length(units) / bpy.context.scene.unit_settings.scale_length
|
||||
return scale
|
||||
@@ -0,0 +1,505 @@
|
||||
from typing import Dict, Iterable, List, Optional, Tuple, Union, cast
|
||||
import bpy
|
||||
from bpy.types import (
|
||||
Depsgraph,
|
||||
MeshPolygon,
|
||||
Object,
|
||||
Curve as NCurve,
|
||||
Mesh as NMesh,
|
||||
)
|
||||
from deprecated import deprecated
|
||||
from mathutils.geometry import interpolate_bezier
|
||||
from mathutils import (
|
||||
Matrix as MMatrix,
|
||||
Vector as MVector,
|
||||
)
|
||||
from specklepy.objects import Base
|
||||
from specklepy.objects.other import BlockInstance, BlockDefinition, RenderMaterial, Transform
|
||||
from specklepy.objects.geometry import (
|
||||
Mesh, Curve, Interval, Box, Point, Polyline
|
||||
)
|
||||
from bpy_speckle.convert.to_native import OBJECT_NAME_SEPERATOR, SPECKLE_ID_LENGTH
|
||||
from bpy_speckle.convert.util import (
|
||||
get_blender_custom_properties,
|
||||
make_knots,
|
||||
nurb_make_curve,
|
||||
to_argb_int,
|
||||
)
|
||||
from bpy_speckle.functions import _report
|
||||
|
||||
class ConversionSkippedException(Exception):
|
||||
pass
|
||||
|
||||
Units: str = "m" # The desired final units to send
|
||||
UnitsScale: float = 1 # The scale factor conversions need to apply to position data to get to the desired units
|
||||
|
||||
CAN_CONVERT_TO_SPECKLE = ("MESH", "CURVE", "EMPTY")
|
||||
|
||||
|
||||
def convert_to_speckle(raw_blender_object: Object, units_scale: float, units: str, depsgraph: Optional[Depsgraph]) -> Base:
|
||||
"""
|
||||
Converts supported 1 blender objects to 1 speckle object (potentially with children)
|
||||
:param raw_blender_object: the blender object (unevaluated by a Depsgraph) to convert
|
||||
:param units_scale: The scale factor conversions need to apply to position data to get to the desired units
|
||||
:param units: The desired final units to send
|
||||
:param depsgraph: Optional depsgraph if provided will evaluate modifiers on geometry data
|
||||
:return: The Converted blender object
|
||||
"""
|
||||
global Units, UnitsScale
|
||||
Units = units
|
||||
UnitsScale = units_scale
|
||||
|
||||
blender_type = raw_blender_object.type
|
||||
if blender_type not in CAN_CONVERT_TO_SPECKLE:
|
||||
raise ConversionSkippedException(f"Objects of type {blender_type} are not supported")
|
||||
|
||||
blender_object = cast(Object, (
|
||||
raw_blender_object.evaluated_get(depsgraph)
|
||||
if depsgraph
|
||||
else raw_blender_object
|
||||
))
|
||||
|
||||
converted: Optional[Base] = None
|
||||
if blender_type == "MESH":
|
||||
converted = mesh_to_speckle(blender_object, cast(NMesh, blender_object.data))
|
||||
elif blender_type == "CURVE":
|
||||
converted = curve_to_speckle(blender_object, cast(NCurve, blender_object.data))
|
||||
elif blender_type == "EMPTY":
|
||||
converted = empty_to_speckle(blender_object)
|
||||
|
||||
if not converted:
|
||||
raise Exception("Conversion returned None")
|
||||
|
||||
converted["properties"] = get_blender_custom_properties(raw_blender_object) #NOTE: Depsgraph copies don't have custom properties so we use the raw version
|
||||
|
||||
# Set object transform #TODO: this could be deprecated once we add proper geometry instancing support
|
||||
if blender_type != "EMPTY":
|
||||
converted["properties"]["transform"] = transform_to_speckle(
|
||||
blender_object.matrix_world
|
||||
)
|
||||
|
||||
return converted
|
||||
|
||||
def mesh_to_speckle(blender_object: Object, data: bpy.types.Mesh) -> Base:
|
||||
b = Base()
|
||||
b["name"] = to_speckle_name(blender_object)
|
||||
b["@displayValue"] = mesh_to_speckle_meshes(blender_object, data)
|
||||
return b
|
||||
|
||||
def mesh_to_speckle_meshes(blender_object: Object, data: bpy.types.Mesh) -> List[Mesh]:
|
||||
|
||||
# Categorise polygons by material index
|
||||
submesh_data: Dict[int, List[MeshPolygon]] = {}
|
||||
|
||||
for p in data.polygons:
|
||||
if p.material_index not in submesh_data:
|
||||
submesh_data[p.material_index] = []
|
||||
submesh_data[p.material_index].append(p)
|
||||
|
||||
transform = cast(MMatrix, blender_object.matrix_world)
|
||||
scaled_vertices = [tuple(transform @ x.co * UnitsScale) for x in data.vertices]
|
||||
|
||||
# Create Speckle meshes for each material
|
||||
submeshes = []
|
||||
index_counter = 0
|
||||
for i in submesh_data:
|
||||
index_mapping: Dict[int, int] = {}
|
||||
|
||||
#Loop through each polygon, and map indicies to their new index in m_verts
|
||||
|
||||
mesh_area = 0
|
||||
m_verts: List[float] = []
|
||||
m_faces: List[int] = []
|
||||
m_texcoords: List[float] = []
|
||||
for face in submesh_data[i]:
|
||||
u_indices = face.vertices
|
||||
m_faces.append(len(u_indices))
|
||||
|
||||
mesh_area += face.area
|
||||
for u_index in u_indices:
|
||||
if u_index not in index_mapping:
|
||||
# Create mapping between index in blender mesh, and new index in speckle submesh
|
||||
index_mapping[u_index] = len(m_verts) // 3
|
||||
vert = scaled_vertices[u_index]
|
||||
m_verts.append(vert[0])
|
||||
m_verts.append(vert[1])
|
||||
m_verts.append(vert[2])
|
||||
|
||||
if data.uv_layers.active:
|
||||
vt = data.uv_layers.active.data[index_counter]
|
||||
uv = cast(MVector, vt.uv)
|
||||
m_texcoords.extend([uv.x, uv.y])
|
||||
|
||||
m_faces.append(index_mapping[u_index])
|
||||
index_counter += 1
|
||||
|
||||
speckle_mesh = Mesh(
|
||||
vertices=m_verts,
|
||||
faces=m_faces,
|
||||
colors=[],
|
||||
textureCoordinates=m_texcoords,
|
||||
units=Units,
|
||||
area = mesh_area,
|
||||
bbox=Box(area=0.0, volume=0.0),
|
||||
)
|
||||
|
||||
if i < len(data.materials):
|
||||
material = data.materials[i]
|
||||
if material is not None:
|
||||
speckle_mesh["renderMaterial"] = material_to_speckle(material)
|
||||
submeshes.append(speckle_mesh)
|
||||
|
||||
return submeshes
|
||||
|
||||
|
||||
def bezier_to_speckle(matrix: MMatrix, spline: bpy.types.Spline, name: Optional[str] = None) -> Curve:
|
||||
degree = 3
|
||||
closed = spline.use_cyclic_u
|
||||
points: List[Tuple[MVector]] = []
|
||||
for i, bp in enumerate(spline.bezier_points):
|
||||
if i > 0:
|
||||
points.append(tuple(matrix @ bp.handle_left * UnitsScale))
|
||||
points.append(tuple(matrix @ bp.co * UnitsScale))
|
||||
if i < len(spline.bezier_points) - 1:
|
||||
points.append(tuple(matrix @ bp.handle_right * UnitsScale))
|
||||
|
||||
if closed:
|
||||
points.extend(
|
||||
(
|
||||
tuple(matrix @ spline.bezier_points[-1].handle_right * UnitsScale),
|
||||
tuple(matrix @ spline.bezier_points[0].handle_left * UnitsScale),
|
||||
tuple(matrix @ spline.bezier_points[0].co * UnitsScale),
|
||||
)
|
||||
)
|
||||
|
||||
num_points = len(points)
|
||||
|
||||
flattend_points = []
|
||||
for row in points: flattend_points.extend(row)
|
||||
|
||||
knot_count = num_points + degree - 1
|
||||
knots = [0] * knot_count
|
||||
|
||||
for i in range(1, len(knots)):
|
||||
knots[i] = i // 3
|
||||
|
||||
length = spline.calc_length()
|
||||
domain = Interval(start=0, end=length, totalChildrenCount=0)
|
||||
return Curve(
|
||||
name=name,
|
||||
degree=degree,
|
||||
closed=spline.use_cyclic_u,
|
||||
periodic= not spline.use_endpoint_u,
|
||||
points=flattend_points,
|
||||
weights=[1] * num_points,
|
||||
knots=knots,
|
||||
rational=True,
|
||||
area=0,
|
||||
volume=0,
|
||||
length=length,
|
||||
domain=domain,
|
||||
units=Units,
|
||||
bbox=Box(area=0.0, volume=0.0),
|
||||
displayValue = bezier_to_speckle_polyline(matrix, spline, length),
|
||||
)
|
||||
|
||||
|
||||
def nurbs_to_speckle(matrix: MMatrix, spline: bpy.types.Spline, name: Optional[str] = None) -> Curve:
|
||||
|
||||
degree = spline.order_u - 1
|
||||
knots = make_knots(spline)
|
||||
|
||||
length = spline.calc_length()
|
||||
domain = Interval(start=0, end=length, totalChildrenCount=0)
|
||||
|
||||
weights = [pt.weight for pt in spline.points]
|
||||
is_rational = all(w == weights[0] for w in weights)
|
||||
|
||||
points = [tuple(matrix @ pt.co.xyz * UnitsScale) for pt in spline.points]
|
||||
|
||||
flattend_points = []
|
||||
for row in points: flattend_points.extend(row)
|
||||
|
||||
if spline.use_cyclic_u:
|
||||
for i in range(0, degree * 3, 3):
|
||||
# Rhino expects n + degree number of points (for closed curves). So we need to add an extra point for each degree
|
||||
flattend_points.append(flattend_points[i + 0])
|
||||
flattend_points.append(flattend_points[i + 1])
|
||||
flattend_points.append(flattend_points[i + 2])
|
||||
|
||||
for i in range(0, degree):
|
||||
weights.append(weights[i])
|
||||
|
||||
return Curve(
|
||||
name=name,
|
||||
degree=degree,
|
||||
closed=spline.use_cyclic_u,
|
||||
periodic= not spline.use_endpoint_u,
|
||||
points=flattend_points,
|
||||
weights=weights,
|
||||
knots=knots,
|
||||
rational=is_rational,
|
||||
area=0,
|
||||
volume=0,
|
||||
length=length,
|
||||
domain=domain,
|
||||
units=Units,
|
||||
bbox=Box(area=0.0, volume=0.0),
|
||||
displayValue=nurbs_to_speckle_polyline(matrix, spline, length),
|
||||
)
|
||||
|
||||
def nurbs_to_speckle_polyline(matrix: MMatrix, spline: bpy.types.Spline, length: Optional[float] = None) -> Polyline:
|
||||
"""
|
||||
Samples a nurbs curve with resolution_u creating a polyline
|
||||
"""
|
||||
points = []
|
||||
sampled_points = nurb_make_curve(spline, spline.resolution_u, 3)
|
||||
for i in range(0, len(sampled_points), 3):
|
||||
scaled_point = matrix @ MVector((
|
||||
sampled_points[i + 0],
|
||||
sampled_points[i + 1],
|
||||
sampled_points[i + 2])) * UnitsScale
|
||||
|
||||
points.append(scaled_point.x)
|
||||
points.append(scaled_point.y)
|
||||
points.append(scaled_point.z)
|
||||
|
||||
length = length or spline.calc_length()
|
||||
domain = Interval(start=0, end=length, totalChildrenCount=0)
|
||||
return Polyline(value=points, closed = spline.use_cyclic_u, domain=domain, area=0, len=length)
|
||||
|
||||
|
||||
#Inspired by https://blender.stackexchange.com/a/689 (CC BY-SA 3.0)
|
||||
def bezier_to_speckle_polyline(matrix: MMatrix, spline: bpy.types.Spline, length: Optional[float] = None) -> Optional[Polyline]:
|
||||
"""
|
||||
Samples a Bézier curve with resolution_u creating a polyline
|
||||
"""
|
||||
segments = len(spline.bezier_points)
|
||||
if segments < 2: return None
|
||||
|
||||
R = spline.resolution_u + 1
|
||||
|
||||
points = []
|
||||
if not spline.use_cyclic_u:
|
||||
segments -= 1
|
||||
|
||||
points: List[float] = []
|
||||
for i in range(segments):
|
||||
inext = (i + 1) % len(spline.bezier_points)
|
||||
|
||||
knot1 = spline.bezier_points[i].co
|
||||
handle1 = spline.bezier_points[i].handle_right
|
||||
handle2 = spline.bezier_points[inext].handle_left
|
||||
knot2 = spline.bezier_points[inext].co
|
||||
|
||||
_points = interpolate_bezier(knot1, handle1, handle2, knot2, R)
|
||||
for p in _points:
|
||||
scaled_point = matrix @ p * UnitsScale
|
||||
points.append(scaled_point.x)
|
||||
points.append(scaled_point.y)
|
||||
points.append(scaled_point.z)
|
||||
|
||||
length = length or spline.calc_length()
|
||||
domain = Interval(start=0, end=length, totalChildrenCount=0)
|
||||
return Polyline(value=points, closed = spline.use_cyclic_u, domain=domain, area=0, len=length)
|
||||
|
||||
_QUICK_TEST_NAME_LENGTH = SPECKLE_ID_LENGTH + len(OBJECT_NAME_SEPERATOR)
|
||||
|
||||
def to_speckle_name(blender_object: bpy.types.ID) -> str:
|
||||
does_name_contain_id = len(blender_object.name) > _QUICK_TEST_NAME_LENGTH and OBJECT_NAME_SEPERATOR in blender_object.name
|
||||
if does_name_contain_id:
|
||||
return blender_object.name.rsplit(OBJECT_NAME_SEPERATOR, 1)[0]
|
||||
else:
|
||||
return blender_object.name
|
||||
|
||||
def poly_to_speckle(matrix: MMatrix, spline: bpy.types.Spline, name: Optional[str] = None) -> Polyline:
|
||||
points = [tuple(matrix @ pt.co.xyz * UnitsScale) for pt in spline.points]
|
||||
|
||||
flattend_points = []
|
||||
for row in points: flattend_points.extend(row)
|
||||
|
||||
length = spline.calc_length()
|
||||
domain = Interval(start=0, end=length, totalChildrenCount=0)
|
||||
return Polyline(
|
||||
name=name,
|
||||
closed=bool(spline.use_cyclic_u),
|
||||
value=list(flattend_points),
|
||||
length=length,
|
||||
domain=domain,
|
||||
bbox=Box(area=0.0, volume=0.0),
|
||||
area=0,
|
||||
units=Units,
|
||||
)
|
||||
|
||||
|
||||
def curve_to_speckle(blender_object: Object, data: bpy.types.Curve) -> Base:
|
||||
b = Base()
|
||||
(meshes, curves) = curve_to_speckle_geometry(blender_object, data)
|
||||
if meshes:
|
||||
b["@displayValue"] = meshes
|
||||
|
||||
b["name"] = to_speckle_name(blender_object)
|
||||
b["@elements"] = curves
|
||||
return b
|
||||
|
||||
def curve_to_speckle_geometry(blender_object: Object, data: bpy.types.Curve) -> Tuple[List[Mesh], List[Base]]:
|
||||
assert(blender_object.type == "CURVE")
|
||||
|
||||
blender_object = cast(Object, blender_object.evaluated_get(bpy.context.view_layer.depsgraph))
|
||||
|
||||
matrix = cast(MMatrix, blender_object.matrix_world)
|
||||
|
||||
meshes: List[Mesh] = []
|
||||
curves: List[Base] = []
|
||||
|
||||
#TODO: Could we support this better?
|
||||
if data.bevel_mode == "OBJECT" and data.bevel_object != None:
|
||||
meshes = mesh_to_speckle_meshes(blender_object, blender_object.to_mesh())
|
||||
|
||||
for spline in data.splines:
|
||||
if spline.type == "BEZIER":
|
||||
curves.append(bezier_to_speckle(matrix, spline, to_speckle_name(blender_object)))
|
||||
|
||||
elif spline.type == "NURBS":
|
||||
curves.append(nurbs_to_speckle(matrix, spline, to_speckle_name(blender_object)))
|
||||
|
||||
elif spline.type == "POLY":
|
||||
curves.append(poly_to_speckle(matrix, spline, to_speckle_name(blender_object)))
|
||||
|
||||
return (meshes, curves)
|
||||
|
||||
@deprecated
|
||||
def ngons_to_speckle_polylines(blender_object: Object, data: bpy.types.Mesh) -> 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 * UnitsScale)
|
||||
|
||||
domain = Interval(start=0, end=1)
|
||||
poly = Polyline(
|
||||
name="{}_{}".format(blender_object.name, i),
|
||||
closed=True,
|
||||
value=value,
|
||||
length=0,
|
||||
domain=domain,
|
||||
bbox=Box(area=0.0, volume=0.0),
|
||||
area=0,
|
||||
units=UNITS,
|
||||
)
|
||||
|
||||
polylines.append(poly)
|
||||
|
||||
return polylines
|
||||
|
||||
|
||||
def material_to_speckle(blender_mat: bpy.types.Material) -> RenderMaterial:
|
||||
speckle_mat = RenderMaterial()
|
||||
speckle_mat.name = blender_mat.name
|
||||
|
||||
if blender_mat.use_nodes:
|
||||
if 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
|
||||
return speckle_mat
|
||||
elif blender_mat.node_tree.nodes.get("Diffuse BSDF"):
|
||||
inputs = blender_mat.node_tree.nodes["Diffuse BSDF"].inputs
|
||||
speckle_mat.diffuse = to_argb_int(inputs["Color"].default_value)
|
||||
speckle_mat.roughness = inputs["Roughness"].default_value
|
||||
return speckle_mat
|
||||
#TODO: Support more shaders
|
||||
|
||||
# fallback to standard material props
|
||||
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
|
||||
|
||||
@deprecated
|
||||
def material_to_speckle_old(blender_object: Object) -> Optional[RenderMaterial]:
|
||||
"""Create and return a render material from a blender object"""
|
||||
if not getattr(blender_object.data, "materials", None):
|
||||
return None
|
||||
|
||||
blender_mat: bpy.types.Material = blender_object.data.materials[0]
|
||||
if not blender_mat:
|
||||
return None
|
||||
|
||||
return material_to_speckle(blender_mat)
|
||||
|
||||
|
||||
def transform_to_speckle(blender_transform: Union[Iterable[Iterable[float]], MMatrix]) -> Transform:
|
||||
iterable_transform = cast(Iterable[Iterable[float]], blender_transform) #NOTE: Matrix are itterable, even if type hinting says they are not
|
||||
value = [y for x in iterable_transform for y in x]
|
||||
# scale the translation
|
||||
for i in (3, 7, 11):
|
||||
value[i] *= UnitsScale
|
||||
|
||||
return Transform(value=value, units=Units)
|
||||
|
||||
|
||||
def block_def_to_speckle(blender_definition: bpy.types.Collection) -> BlockDefinition:
|
||||
geometry = []
|
||||
for geo in blender_definition.objects:
|
||||
try:
|
||||
#TODO: right now, geometry will be a flat list of objects. Eventually we will want to preseve the parent relationship
|
||||
geometry.append(convert_to_speckle(geo, UnitsScale, Units, None))
|
||||
except ConversionSkippedException as ex:
|
||||
_report(f"Skipped converting '{geo.name_full}' inside collection instance: '{ex}")
|
||||
except Exception as ex:
|
||||
_report(f"Failed to converted '{geo.name_full}' inside collection instance: '{ex}'")
|
||||
|
||||
block_def = BlockDefinition(
|
||||
units=Units,
|
||||
name=to_speckle_name(blender_definition),
|
||||
geometry=geometry,
|
||||
basePoint=Point(units=Units),
|
||||
)
|
||||
# blender_props = get_blender_custom_properties(blender_definition)
|
||||
# block_def.applicationId = blender_props.pop("applicationId", None) #TODO: remove?
|
||||
return block_def
|
||||
|
||||
|
||||
def block_instance_to_speckle(blender_instance: Object) -> BlockInstance:
|
||||
return BlockInstance(
|
||||
blockDefinition=block_def_to_speckle(
|
||||
blender_instance.instance_collection
|
||||
),
|
||||
transform=transform_to_speckle(blender_instance.matrix_world),
|
||||
name=to_speckle_name(blender_instance),
|
||||
units=Units,
|
||||
)
|
||||
|
||||
|
||||
def empty_to_speckle(blender_object: Object) -> Union[BlockInstance, Base]:
|
||||
# probably an instance collection (block) so let's try it
|
||||
|
||||
if blender_object.instance_collection and blender_object.instance_type == "COLLECTION":
|
||||
return block_instance_to_speckle(blender_object)
|
||||
else:
|
||||
#raise ConversionSkippedException("Sending non-collection instance empties are not currently supported")
|
||||
wrapper = Base()
|
||||
wrapper["@displayValue"] = matrix_to_speckle_point(cast(MMatrix, blender_object.matrix_world))
|
||||
return wrapper
|
||||
#TODO: we could do a Empty -> Point conversion here. However, the viewer (and likly other apps) don't support a pont with "elements"
|
||||
#return matrix_to_speckle_point(cast(MMatrix, blender_object.matrix_world))
|
||||
|
||||
|
||||
def matrix_to_speckle_point(matrix: MMatrix, units_scale: float = 1.0) -> Point:
|
||||
transformed_pos = cast(MVector, matrix @ MVector((0,0,0)) * units_scale)
|
||||
return Point(x = transformed_pos.x,
|
||||
y = transformed_pos.y,
|
||||
z = transformed_pos.z)
|
||||
@@ -1,3 +0,0 @@
|
||||
from .mesh import export_mesh
|
||||
from .curve import export_curve, export_ngons_as_polylines
|
||||
from .empty import export_empty
|
||||
@@ -1,224 +0,0 @@
|
||||
import bpy, bmesh, struct
|
||||
from specklepy.objects.geometry import Curve, Interval, Box, Polyline
|
||||
from bpy_speckle.convert.to_speckle.mesh import export_mesh
|
||||
|
||||
UNITS = "m"
|
||||
|
||||
|
||||
def bezier_to_speckle(matrix, spline, scale, name=None):
|
||||
degree = 3
|
||||
closed = spline.use_cyclic_u
|
||||
|
||||
points = []
|
||||
for i, bp in enumerate(spline.bezier_points):
|
||||
if i > 0:
|
||||
points.append(tuple(matrix @ bp.handle_left * scale))
|
||||
points.append(tuple(matrix @ bp.co * scale))
|
||||
if i < len(spline.bezier_points) - 1:
|
||||
points.append(tuple(matrix @ bp.handle_right * scale))
|
||||
|
||||
if closed:
|
||||
points.append(tuple(matrix @ spline.bezier_points[-1].handle_right * scale))
|
||||
points.append(tuple(matrix @ spline.bezier_points[0].handle_left * scale))
|
||||
points.append(tuple(matrix @ spline.bezier_points[0].co * scale))
|
||||
|
||||
num_points = len(points)
|
||||
|
||||
knot_count = num_points + degree - 1
|
||||
knots = [0] * knot_count
|
||||
|
||||
for i in range(1, len(knots)):
|
||||
knots[i] = i // 3
|
||||
|
||||
length = spline.calc_length()
|
||||
domain = Interval(start=0, end=length, totalChildrenCount=0)
|
||||
return Curve(
|
||||
name=name,
|
||||
degree=degree,
|
||||
closed=spline.use_cyclic_u,
|
||||
periodic=spline.use_cyclic_u,
|
||||
points=list(sum(points, ())), # magic (flatten list of tuples)
|
||||
weights=[1] * num_points,
|
||||
knots=knots,
|
||||
rational=False,
|
||||
area=0,
|
||||
volume=0,
|
||||
length=length,
|
||||
domain=domain,
|
||||
units=UNITS,
|
||||
bbox=Box(area=0.0, volume=0.0),
|
||||
)
|
||||
|
||||
|
||||
def nurbs_to_speckle(matrix, spline, scale, name=None):
|
||||
knots = makeknots(spline)
|
||||
# print("knots: {}".format(knots))
|
||||
points = [tuple(matrix @ pt.co.xyz * scale) for pt in spline.points]
|
||||
degree = spline.order_u - 1
|
||||
|
||||
length = spline.calc_length()
|
||||
domain = Interval(start=0, end=length, totalChildrenCount=0)
|
||||
|
||||
return Curve(
|
||||
name=name,
|
||||
degree=degree,
|
||||
closed=spline.use_cyclic_u,
|
||||
periodic=spline.use_cyclic_u,
|
||||
points=list(sum(points, ())), # magic (flatten list of tuples)
|
||||
weights=[pt.weight for pt in spline.points],
|
||||
knots=knots,
|
||||
rational=False,
|
||||
area=0,
|
||||
volume=0,
|
||||
length=length,
|
||||
domain=domain,
|
||||
units=UNITS,
|
||||
bbox=Box(area=0.0, volume=0.0),
|
||||
)
|
||||
|
||||
|
||||
def poly_to_speckle(matrix, spline, scale, name=None):
|
||||
points = [tuple(matrix @ pt.co.xyz * scale) for pt in spline.points]
|
||||
|
||||
length = spline.calc_length()
|
||||
domain = Interval(start=0, end=length, totalChildrenCount=0)
|
||||
return Polyline(
|
||||
name=name,
|
||||
closed=spline.use_cyclic_u,
|
||||
value=list(sum(points, ())), # magic (flatten list of tuples)
|
||||
length=length,
|
||||
domain=domain,
|
||||
bbox=Box(area=0.0, volume=0.0),
|
||||
area=0,
|
||||
units=UNITS,
|
||||
)
|
||||
|
||||
|
||||
def export_curve(blender_object, data, scale=1.0):
|
||||
UNITS = "m" if bpy.context.scene.unit_settings.system == "METRIC" else "ft"
|
||||
|
||||
if blender_object.type != "CURVE":
|
||||
return None
|
||||
|
||||
blender_object = blender_object.evaluated_get(bpy.context.view_layer.depsgraph)
|
||||
|
||||
mat = blender_object.matrix_world
|
||||
|
||||
curves = []
|
||||
|
||||
if data.bevel_mode == "OBJECT" and data.bevel_object != None:
|
||||
mesh = export_mesh(blender_object, blender_object.to_mesh(), scale)
|
||||
curves.extend(mesh)
|
||||
|
||||
for spline in data.splines:
|
||||
if spline.type == "BEZIER":
|
||||
curves.append(bezier_to_speckle(mat, spline, scale, blender_object.name))
|
||||
|
||||
elif spline.type == "NURBS":
|
||||
curves.append(nurbs_to_speckle(mat, spline, scale, blender_object.name))
|
||||
|
||||
elif spline.type == "POLY":
|
||||
curves.append(poly_to_speckle(mat, spline, scale, blender_object.name))
|
||||
|
||||
return curves
|
||||
|
||||
|
||||
def export_ngons_as_polylines(blender_object, data, scale=1.0):
|
||||
UNITS = "m" if bpy.context.scene.unit_settings.system == "METRIC" else "ft"
|
||||
|
||||
if blender_object.type != "MESH":
|
||||
return None
|
||||
|
||||
mat = blender_object.matrix_world
|
||||
|
||||
verts = data.vertices
|
||||
polylines = []
|
||||
for i, poly in enumerate(data.polygons):
|
||||
value = []
|
||||
for v in poly.vertices:
|
||||
value.extend(mat @ verts[v].co * scale)
|
||||
|
||||
domain = Interval(start=0, end=1)
|
||||
poly = Polyline(
|
||||
name="{}_{}".format(blender_object.name, i),
|
||||
closed=True,
|
||||
value=value, # magic (flatten list of tuples)
|
||||
length=0,
|
||||
domain=domain,
|
||||
bbox=Box(area=0.0, volume=0.0),
|
||||
area=0,
|
||||
units=UNITS,
|
||||
)
|
||||
|
||||
polylines.append(poly)
|
||||
|
||||
return polylines
|
||||
|
||||
|
||||
"""
|
||||
Python implementation of Blender's NURBS curve generation
|
||||
from: https://blender.stackexchange.com/a/34276
|
||||
"""
|
||||
|
||||
|
||||
def macro_knotsu(nu):
|
||||
return nu.order_u + nu.point_count_u + (nu.order_u - 1 if nu.use_cyclic_u else 0)
|
||||
|
||||
|
||||
def macro_segmentsu(nu):
|
||||
return nu.point_count_u if nu.use_cyclic_u else nu.point_count_u - 1
|
||||
|
||||
|
||||
def makeknots(nu):
|
||||
knots = [0.0] * (4 + macro_knotsu(nu))
|
||||
flag = nu.use_endpoint_u + (nu.use_bezier_u << 1)
|
||||
if nu.use_cyclic_u:
|
||||
calcknots(knots, nu.point_count_u, nu.order_u, 0)
|
||||
makecyclicknots(knots, nu.point_count_u, nu.order_u)
|
||||
else:
|
||||
calcknots(knots, nu.point_count_u, nu.order_u, flag)
|
||||
return knots
|
||||
|
||||
|
||||
def calcknots(knots, pnts, order, flag):
|
||||
pnts_order = pnts + order
|
||||
if flag == 1:
|
||||
k = 0.0
|
||||
for a in range(1, pnts_order + 1):
|
||||
knots[a - 1] = k
|
||||
if a >= order and a <= pnts:
|
||||
k += 1.0
|
||||
elif flag == 2:
|
||||
if order == 4:
|
||||
k = 0.34
|
||||
for a in range(pnts_order):
|
||||
knots[a] = math.floor(k)
|
||||
k += 1.0 / 3.0
|
||||
elif order == 3:
|
||||
k = 0.6
|
||||
for a in range(pnts_order):
|
||||
if a >= order and a <= pnts:
|
||||
k += 0.5
|
||||
knots[a] = math.floor(k)
|
||||
else:
|
||||
for a in range(pnts_order):
|
||||
knots[a] = a
|
||||
|
||||
|
||||
def makecyclicknots(knots, pnts, order):
|
||||
order2 = order - 1
|
||||
|
||||
if order > 2:
|
||||
b = pnts + order2
|
||||
for a in range(1, order2):
|
||||
if knots[b] != knots[b - a]:
|
||||
break
|
||||
|
||||
if a == order2:
|
||||
knots[pnts + order - 2] += 1.0
|
||||
|
||||
b = order
|
||||
c = pnts + order + order2
|
||||
for a in range(pnts + order2, c):
|
||||
knots[a] = knots[a - 1] + (knots[b] - knots[b - 1])
|
||||
b -= 1
|
||||
@@ -1,5 +0,0 @@
|
||||
import bpy, bmesh, struct
|
||||
|
||||
|
||||
def export_default(blender_object, scale=1.0):
|
||||
return None
|
||||
@@ -1,5 +0,0 @@
|
||||
import bpy, bmesh, struct
|
||||
|
||||
|
||||
def export_empty(blender_object, data, scale=1.0):
|
||||
return None
|
||||
@@ -1,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]
|
||||
+418
-6
@@ -1,7 +1,31 @@
|
||||
from typing import Tuple
|
||||
import math
|
||||
from typing import Any, Optional, Tuple
|
||||
from bmesh.types import BMesh
|
||||
import bpy, struct, idprop
|
||||
|
||||
from specklepy.objects.base import Base
|
||||
from specklepy.objects.geometry import Circle, Mesh, Ellipse
|
||||
from specklepy.objects.other import RenderMaterial
|
||||
from bpy_speckle.functions import _report
|
||||
from bpy.types import Material, Object
|
||||
|
||||
IGNORED_PROPERTY_KEYS = {
|
||||
"id",
|
||||
"elements",
|
||||
"displayMesh",
|
||||
"displayValue",
|
||||
"speckle_type",
|
||||
"parameters",
|
||||
"faces",
|
||||
"colors",
|
||||
"vertices",
|
||||
"renderMaterial",
|
||||
"textureCoordinates",
|
||||
"totalChildrenCount"
|
||||
}
|
||||
|
||||
|
||||
def to_rgba(argb_int: int) -> Tuple[float]:
|
||||
def to_rgba(argb_int: int) -> Tuple[float, float, float, float]:
|
||||
"""Converts the int representation of a colour into a percent RGBA tuple"""
|
||||
alpha = ((argb_int >> 24) & 255) / 255
|
||||
red = ((argb_int >> 16) & 255) / 255
|
||||
@@ -11,9 +35,397 @@ def to_rgba(argb_int: int) -> Tuple[float]:
|
||||
return (red, green, blue, alpha)
|
||||
|
||||
|
||||
def to_argb_int(diffuse_colour) -> int:
|
||||
def to_argb_int(rgba_color: list[float]) -> int:
|
||||
"""Converts an RGBA array to an ARGB integer"""
|
||||
diffuse_colour = diffuse_colour[-1:] + diffuse_colour[0:3]
|
||||
diffuse_colour = [int(val * 255) for val in diffuse_colour]
|
||||
argb_color = rgba_color[-1:] + rgba_color[:3]
|
||||
int_color = [int(val * 255) for val in argb_color]
|
||||
|
||||
return int.from_bytes(diffuse_colour, byteorder="big", signed=True)
|
||||
return int.from_bytes(int_color, byteorder="big", signed=True)
|
||||
|
||||
def set_custom_property(key: str, value: Any, blender_object: Object) -> None:
|
||||
try:
|
||||
#Expected c types: float, int, string, float[], int[]
|
||||
blender_object[key] = value
|
||||
except (OverflowError, TypeError) as ex:
|
||||
print(f"Skipping setting property ({key}={value}) on {blender_object.name_full}, Reason: {ex}")
|
||||
except Exception as ex:
|
||||
#TODO: Log this as it's unexpected!!!
|
||||
print(f"Skipping setting property ({key}={value}) on {blender_object.name_full}, Reason: {ex}")
|
||||
|
||||
def add_custom_properties(speckle_object: Base, blender_object: Object):
|
||||
if blender_object is None:
|
||||
return
|
||||
|
||||
blender_object["_speckle_type"] = type(speckle_object).__name__
|
||||
|
||||
app_id = getattr(speckle_object, "applicationId", None)
|
||||
if app_id:
|
||||
blender_object["applicationId"] = speckle_object.applicationId
|
||||
keys = speckle_object.get_dynamic_member_names() if "Geometry" in speckle_object.speckle_type else (set(speckle_object.get_member_names()) - IGNORED_PROPERTY_KEYS)
|
||||
for key in keys:
|
||||
val = getattr(speckle_object, key, None)
|
||||
if val is None:
|
||||
continue
|
||||
|
||||
if isinstance(val, (int, str, float)):
|
||||
set_custom_property(key, val, blender_object)
|
||||
elif key == "properties" and isinstance(val, Base):
|
||||
val["applicationId"] = None
|
||||
add_custom_properties(val, blender_object)
|
||||
elif isinstance(val, list):
|
||||
items = [item for item in val if not isinstance(item, Base)]
|
||||
if items:
|
||||
set_custom_property(key, items, blender_object)
|
||||
elif isinstance(val,dict):
|
||||
for (k,v) in val.items():
|
||||
if not isinstance(v, Base):
|
||||
set_custom_property(k, v, blender_object)
|
||||
|
||||
|
||||
def render_material_to_native(speckle_mat: RenderMaterial) -> Material:
|
||||
|
||||
mat_name = speckle_mat.name
|
||||
if not mat_name:
|
||||
mat_name = speckle_mat.applicationId or speckle_mat.id or speckle_mat.get_id()
|
||||
|
||||
blender_mat = bpy.data.materials.get(mat_name)
|
||||
if blender_mat is None:
|
||||
blender_mat = bpy.data.materials.new(mat_name)
|
||||
|
||||
# for now, we're not updating these materials. as per tom's suggestion, we should have a toggle
|
||||
# that enables this as the blender mats will prob be much more complex than whatever is coming in
|
||||
blender_mat.use_nodes = True
|
||||
inputs = blender_mat.node_tree.nodes["Principled BSDF"].inputs
|
||||
|
||||
inputs["Base Color"].default_value = to_rgba(speckle_mat.diffuse)
|
||||
inputs["Emission"].default_value = to_rgba(speckle_mat.emissive)
|
||||
inputs["Roughness"].default_value = speckle_mat.roughness
|
||||
inputs["Metallic"].default_value = speckle_mat.metalness
|
||||
inputs["Alpha"].default_value = speckle_mat.opacity
|
||||
|
||||
if speckle_mat.opacity < 1.0:
|
||||
blender_mat.blend_method = "BLEND"
|
||||
|
||||
return blender_mat
|
||||
|
||||
def get_render_material(speckle_object: Base) -> Optional[RenderMaterial]:
|
||||
"""Trys to get a RenderMaterial on given speckle_object and convert it to a blender material"""
|
||||
|
||||
speckle_mat = getattr(
|
||||
speckle_object,
|
||||
"renderMaterial",
|
||||
getattr(speckle_object, "@renderMaterial", None),
|
||||
)
|
||||
if not isinstance(speckle_mat, RenderMaterial):
|
||||
return None
|
||||
|
||||
return speckle_mat
|
||||
|
||||
|
||||
|
||||
def add_vertices(speckle_mesh: Mesh, blender_mesh: BMesh, scale=1.0):
|
||||
sverts = speckle_mesh.vertices
|
||||
|
||||
if sverts and len(sverts) > 0:
|
||||
for i in range(0, len(sverts), 3):
|
||||
blender_mesh.verts.new(
|
||||
(
|
||||
float(sverts[i]) * scale,
|
||||
float(sverts[i + 1]) * scale,
|
||||
float(sverts[i + 2]) * scale,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
|
||||
def add_faces(speckle_mesh: Mesh, blender_mesh: BMesh, indexOffset: int, materialIndex: int = 0, smooth:bool = True):
|
||||
sfaces = speckle_mesh.faces
|
||||
|
||||
if sfaces and len(sfaces) > 0:
|
||||
i = 0
|
||||
while i < len(sfaces):
|
||||
n = sfaces[i]
|
||||
if n < 3:
|
||||
n += 3 # 0 -> 3, 1 -> 4
|
||||
|
||||
i += 1
|
||||
try:
|
||||
f = blender_mesh.faces.new(
|
||||
[blender_mesh.verts[x + indexOffset] for x in sfaces[i : i + n]]
|
||||
)
|
||||
f.material_index = materialIndex
|
||||
f.smooth = smooth
|
||||
except Exception as e:
|
||||
_report(f"Failed to create face for mesh {speckle_mesh.id} \n{e}")
|
||||
i += n
|
||||
|
||||
|
||||
def add_colors(speckle_mesh: Mesh, blender_mesh: BMesh):
|
||||
|
||||
scolors = speckle_mesh.colors
|
||||
|
||||
if scolors:
|
||||
colors = []
|
||||
if len(scolors) > 0:
|
||||
|
||||
for i in range(len(scolors)):
|
||||
col = int(scolors[i])
|
||||
(a, r, g, b) = [
|
||||
int(x) for x in struct.unpack("!BBBB", struct.pack("!i", col))
|
||||
]
|
||||
colors.append(
|
||||
(
|
||||
float(r) / 255.0,
|
||||
float(g) / 255.0,
|
||||
float(b) / 255.0,
|
||||
float(a) / 255.0,
|
||||
)
|
||||
)
|
||||
|
||||
# Make vertex colors
|
||||
if len(scolors) == len(blender_mesh.verts):
|
||||
color_layer = blender_mesh.loops.layers.color.new("Col")
|
||||
|
||||
for face in blender_mesh.faces:
|
||||
for loop in face.loops:
|
||||
loop[color_layer] = colors[loop.vert.index]
|
||||
|
||||
|
||||
def add_uv_coords(speckle_mesh: Mesh, blender_mesh: BMesh):
|
||||
s_uvs = speckle_mesh.textureCoordinates
|
||||
if not s_uvs:
|
||||
return
|
||||
try:
|
||||
uv = []
|
||||
|
||||
if len(s_uvs) // 2 == len(blender_mesh.verts):
|
||||
uv.extend(
|
||||
(float(s_uvs[i]), float(s_uvs[i + 1]))
|
||||
for i in range(0, len(s_uvs), 2)
|
||||
)
|
||||
else:
|
||||
_report(
|
||||
f"Failed to match UV coordinates to vert data. Blender mesh verts: {len(blender_mesh.verts)}, Speckle UVs: {len(s_uvs) // 2}"
|
||||
)
|
||||
return
|
||||
|
||||
# Make UVs
|
||||
uv_layer = blender_mesh.loops.layers.uv.verify()
|
||||
|
||||
for f in blender_mesh.faces:
|
||||
for l in f.loops:
|
||||
luv = l[uv_layer]
|
||||
luv.uv = uv[l.vert.index]
|
||||
except:
|
||||
_report("Failed to decode texture coordinates.")
|
||||
raise
|
||||
|
||||
|
||||
ignored_keys = {
|
||||
"id",
|
||||
"speckle",
|
||||
"speckle_type"
|
||||
"_speckle_type",
|
||||
"_speckle_name",
|
||||
"_speckle_transform",
|
||||
"_RNA_UI",
|
||||
"elements",
|
||||
"transform",
|
||||
"_units",
|
||||
"_chunkable",
|
||||
}
|
||||
|
||||
def get_blender_custom_properties(obj, max_depth: int = 200):
|
||||
if max_depth < 0:
|
||||
return obj
|
||||
|
||||
if hasattr(obj, "keys"):
|
||||
keys = set(obj.keys()) - ignored_keys
|
||||
return {
|
||||
key: get_blender_custom_properties(obj[key], max_depth - 1)
|
||||
for key in keys
|
||||
if not key.startswith("_")
|
||||
}
|
||||
|
||||
if isinstance(obj, (list, tuple, idprop.types.IDPropertyArray)):
|
||||
return [get_blender_custom_properties(o, max_depth - 1) for o in obj]
|
||||
|
||||
return obj
|
||||
|
||||
"""
|
||||
Python implementation of Blender's NURBS curve generation for to Speckle conversion
|
||||
from: https://blender.stackexchange.com/a/34276
|
||||
based on https://projects.blender.org/blender/blender/src/branch/main/source/blender/blenkernel/intern/curve.cc (check old version)
|
||||
"""
|
||||
|
||||
def macro_knotsu(nu: bpy.types.Spline) -> int:
|
||||
return nu.order_u + nu.point_count_u + (nu.order_u - 1 if nu.use_cyclic_u else 0)
|
||||
|
||||
def macro_segmentsu(nu: bpy.types.Spline) -> int:
|
||||
return nu.point_count_u if nu.use_cyclic_u else nu.point_count_u - 1
|
||||
|
||||
def make_knots(nu: bpy.types.Spline) -> list[float]:
|
||||
knots = [0.0] * macro_knotsu(nu)
|
||||
flag = nu.use_endpoint_u + (nu.use_bezier_u << 1)
|
||||
if nu.use_cyclic_u:
|
||||
calc_knots(knots, nu.point_count_u, nu.order_u, 0)
|
||||
else:
|
||||
calc_knots(knots, nu.point_count_u, nu.order_u, flag)
|
||||
return knots
|
||||
|
||||
|
||||
def calc_knots(knots: list[float], point_count: int, order: int, flag: int) -> None:
|
||||
pts_order = point_count + order
|
||||
if flag == 1: # CU_NURB_ENDPOINT
|
||||
k = 0.0
|
||||
for a in range(1, pts_order + 1):
|
||||
knots[a - 1] = k
|
||||
if a >= order and a <= point_count:
|
||||
k += 1.0
|
||||
elif flag == 2: # CU_NURB_BEZIER
|
||||
if order == 4:
|
||||
k = 0.34
|
||||
for a in range(pts_order):
|
||||
knots[a] = math.floor(k)
|
||||
k += 1.0 / 3.0
|
||||
elif order == 3:
|
||||
k = 0.6
|
||||
for a in range(pts_order):
|
||||
if a >= order and a <= point_count:
|
||||
k += 0.5
|
||||
knots[a] = math.floor(k)
|
||||
else:
|
||||
for a in range(1, len(knots) - 1):
|
||||
knots[a] = a - 1
|
||||
|
||||
knots[-1] = knots[-2]
|
||||
|
||||
def basis_nurb(t: float, order: int, point_count: int, knots: list[float], basis: list[float], start: int, end: int) -> Tuple[int, int]:
|
||||
i1 = i2 = 0
|
||||
orderpluspnts = order + point_count
|
||||
opp2 = orderpluspnts - 1
|
||||
|
||||
# this is for float inaccuracy
|
||||
if t < knots[0]:
|
||||
t = knots[0]
|
||||
elif t > knots[opp2]:
|
||||
t = knots[opp2]
|
||||
|
||||
# this part is order '1'
|
||||
o2 = order + 1
|
||||
for i in range(opp2):
|
||||
if knots[i] != knots[i + 1] and t >= knots[i] and t <= knots[i + 1]:
|
||||
basis[i] = 1.0
|
||||
i1 = i - o2
|
||||
if i1 < 0:
|
||||
i1 = 0
|
||||
i2 = i
|
||||
i += 1
|
||||
while i < opp2:
|
||||
basis[i] = 0.0
|
||||
i += 1
|
||||
break
|
||||
|
||||
else:
|
||||
basis[i] = 0.0
|
||||
|
||||
basis[i] = 0.0
|
||||
|
||||
# this is order 2, 3, ...
|
||||
for j in range(2, order + 1):
|
||||
|
||||
if i2 + j >= orderpluspnts:
|
||||
i2 = opp2 - j
|
||||
|
||||
for i in range(i1, i2 + 1):
|
||||
if basis[i] != 0.0:
|
||||
d = ((t - knots[i]) * basis[i]) / (knots[i + j - 1] - knots[i])
|
||||
else:
|
||||
d = 0.0
|
||||
|
||||
if basis[i + 1] != 0.0:
|
||||
e = ((knots[i + j] - t) * basis[i + 1]) / (knots[i + j] - knots[i + 1])
|
||||
else:
|
||||
e = 0.0
|
||||
|
||||
basis[i] = d + e
|
||||
|
||||
start = 1000
|
||||
end = 0
|
||||
|
||||
for i in range(i1, i2 + 1):
|
||||
if basis[i] > 0.0:
|
||||
end = i
|
||||
if start == 1000:
|
||||
start = i
|
||||
|
||||
return start, end
|
||||
|
||||
def nurb_make_curve(nu: bpy.types.Spline, resolu: int, stride: int = 3) -> list[float]:
|
||||
""""BKE_nurb_makeCurve"""
|
||||
EPS = 1e-6
|
||||
coord_index = istart = iend = 0
|
||||
|
||||
coord_array = [0.0] * (3 * nu.resolution_u * macro_segmentsu(nu))
|
||||
sum_array = [0] * nu.point_count_u
|
||||
basisu = [0.0] * macro_knotsu(nu)
|
||||
knots = make_knots(nu)
|
||||
|
||||
resolu = resolu * macro_segmentsu(nu)
|
||||
ustart = knots[nu.order_u - 1]
|
||||
uend = knots[nu.point_count_u + nu.order_u - 1] if nu.use_cyclic_u else \
|
||||
knots[nu.point_count_u]
|
||||
ustep = (uend - ustart) / (resolu - (0 if nu.use_cyclic_u else 1))
|
||||
cycl = nu.order_u - 1 if nu.use_cyclic_u else 0
|
||||
|
||||
u = ustart
|
||||
while resolu:
|
||||
resolu -= 1
|
||||
istart, iend = basis_nurb(u, nu.order_u, nu.point_count_u + cycl, knots, basisu, istart, iend)
|
||||
|
||||
#/* calc sum */
|
||||
sumdiv = 0.0
|
||||
sum_index = 0
|
||||
pt_index = istart - 1
|
||||
for i in range(istart, iend + 1):
|
||||
if i >= nu.point_count_u:
|
||||
pt_index = i - nu.point_count_u
|
||||
else:
|
||||
pt_index += 1
|
||||
|
||||
sum_array[sum_index] = basisu[i] * nu.points[pt_index].co[3]
|
||||
sumdiv += sum_array[sum_index]
|
||||
sum_index += 1
|
||||
|
||||
if (sumdiv != 0.0) and (sumdiv < 1.0 - EPS or sumdiv > 1.0 + EPS):
|
||||
sum_index = 0
|
||||
for i in range(istart, iend + 1):
|
||||
sum_array[sum_index] /= sumdiv
|
||||
sum_index += 1
|
||||
|
||||
coord_array[coord_index: coord_index + 3] = (0.0, 0.0, 0.0)
|
||||
|
||||
sum_index = 0
|
||||
pt_index = istart - 1
|
||||
for i in range(istart, iend + 1):
|
||||
if i >= nu.point_count_u:
|
||||
pt_index = i - nu.point_count_u
|
||||
else:
|
||||
pt_index += 1
|
||||
|
||||
if sum_array[sum_index] != 0.0:
|
||||
for j in range(3):
|
||||
coord_array[coord_index + j] += sum_array[sum_index] * nu.points[pt_index].co[j]
|
||||
sum_index += 1
|
||||
|
||||
coord_index += stride
|
||||
u += ustep
|
||||
|
||||
return coord_array
|
||||
|
||||
def link_object_to_collection_nested(obj: bpy.types.Object, col: bpy.types.Collection):
|
||||
if obj.name not in col.objects:
|
||||
col.objects.link(obj)
|
||||
|
||||
for child in obj.children:
|
||||
link_object_to_collection_nested(child, col)
|
||||
+25
-48
@@ -1,5 +1,10 @@
|
||||
from specklepy.api.client import SpeckleClient
|
||||
from bpy_speckle.clients import speckle_clients
|
||||
from typing import Callable, Set
|
||||
|
||||
import bpy
|
||||
from specklepy.objects.base import Base
|
||||
from bpy_speckle.properties.scene import SpeckleSceneSettings
|
||||
|
||||
from bpy_speckle.specklepy_extras.traversal import GraphTraversal, TraversalRule
|
||||
|
||||
"""
|
||||
Speckle functions
|
||||
@@ -34,7 +39,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))
|
||||
@@ -45,60 +50,32 @@ def get_scale_length(units):
|
||||
Client, user, and stream functions
|
||||
"""
|
||||
|
||||
elements_aliases: Set[str] = {"elements", "@elements"}
|
||||
ignore_props: Set[str] = {"@blockDefinition", "displayValue", "@displayValue", "units", "id", "applicationId"}
|
||||
|
||||
def _check_speckle_client_user_stream(scene):
|
||||
def get_default_traversal_func(can_convert_to_native: Callable[[Base], bool]) -> GraphTraversal:
|
||||
"""
|
||||
Verify that there is a valid user and stream
|
||||
Traversal func for traversing a speckle commit object
|
||||
"""
|
||||
speckle = scene.speckle
|
||||
|
||||
user = (
|
||||
speckle.users[int(speckle.active_user)]
|
||||
if len(speckle.users) > int(speckle.active_user)
|
||||
else None
|
||||
convertable_rule = TraversalRule(
|
||||
[can_convert_to_native],
|
||||
lambda _: [i for i in elements_aliases if i not in ignore_props],
|
||||
)
|
||||
|
||||
if user is None:
|
||||
print("No users loaded.")
|
||||
|
||||
stream = (
|
||||
user.streams[user.active_stream]
|
||||
if len(user.streams) > user.active_stream
|
||||
else None
|
||||
ignore_result_rule = TraversalRule(
|
||||
[lambda o: "Objects.Structural.Results" in o.speckle_type, #Sadly, this one is nessasary to avoid double conversion...
|
||||
lambda o: "Objects.BuiltElements.Revit.Parameter" in o.speckle_type], #This one is just for traversal performance of revit commits
|
||||
lambda _: [],
|
||||
)
|
||||
|
||||
if stream is None:
|
||||
print("Account contains no streams.")
|
||||
|
||||
return (user, stream)
|
||||
|
||||
|
||||
def _create_stream(user, stream_name, units="Millimeters"):
|
||||
"""
|
||||
Create a new stream
|
||||
"""
|
||||
|
||||
# TODO: double-check, but this should not be accessible through the UI if
|
||||
# there aren't any active users anyway
|
||||
# user = context.scene.speckle.users[int(context.scene.speckle.active_user)]
|
||||
client = speckle_clients[int(bpy.context.scene.speckle.active_user)]
|
||||
return client.stream.create(
|
||||
name=stream_name, description="This is a Blender stream.", is_public=True
|
||||
default_rule = TraversalRule(
|
||||
[lambda _: True],
|
||||
lambda o: o.get_member_names(), #TODO: avoid deprecated members
|
||||
)
|
||||
|
||||
# TODO: Update stream with properties such as units, etc.
|
||||
return GraphTraversal([convertable_rule, ignore_result_rule, default_rule])
|
||||
|
||||
|
||||
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"])
|
||||
def get_speckle(context: bpy.types.Context) -> 'SpeckleSceneSettings':
|
||||
return context.scene.speckle #type: ignore
|
||||
@@ -1,104 +0,0 @@
|
||||
import os, sys, bpy
|
||||
import ctypes, sys
|
||||
|
||||
import os, sys
|
||||
|
||||
|
||||
def modules_path():
|
||||
# set up addons/modules under the user
|
||||
# script path. Here we'll install the
|
||||
# dependencies
|
||||
modulespath = os.path.normpath(
|
||||
os.path.join(bpy.utils.script_path_user(), "addons", "modules")
|
||||
)
|
||||
if not os.path.exists(modulespath):
|
||||
os.makedirs(modulespath)
|
||||
|
||||
# set user modules path at beginning of paths for earlier hit
|
||||
if sys.path[1] != modulespath:
|
||||
sys.path.insert(1, modulespath)
|
||||
|
||||
return modulespath
|
||||
|
||||
|
||||
def install_dependencies():
|
||||
import sys
|
||||
import os
|
||||
|
||||
try:
|
||||
try:
|
||||
import pip
|
||||
except:
|
||||
print("Installing pip... "),
|
||||
from subprocess import run as sprun
|
||||
|
||||
res = sprun([bpy.app.binary_path_python, "-m", "ensurepip"])
|
||||
|
||||
if res.returncode == 0:
|
||||
import pip
|
||||
else:
|
||||
raise Exception("Failed to install pip.")
|
||||
|
||||
modulespath = modules_path()
|
||||
|
||||
if not os.path.exists(modulespath):
|
||||
os.makedirs(modulespath)
|
||||
|
||||
print("Installing speckle to {}... ".format(modulespath)),
|
||||
from subprocess import run as sprun
|
||||
|
||||
res = sprun(
|
||||
[
|
||||
bpy.app.binary_path_python,
|
||||
"-m",
|
||||
"pip",
|
||||
"install",
|
||||
"-q",
|
||||
"-t",
|
||||
"{}".format(modulespath),
|
||||
"--no-deps",
|
||||
"pydantic",
|
||||
]
|
||||
)
|
||||
res = sprun(
|
||||
[
|
||||
bpy.app.binary_path_python,
|
||||
"-m",
|
||||
"pip",
|
||||
"install",
|
||||
"-q",
|
||||
"-t",
|
||||
"{}".format(modulespath),
|
||||
"specklepy",
|
||||
]
|
||||
)
|
||||
|
||||
except:
|
||||
raise Exception(
|
||||
"Failed to install dependencies. Please make sure you have pip installed."
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
import specklepy
|
||||
except:
|
||||
print("Failed to load speckle.")
|
||||
from sys import platform
|
||||
|
||||
if platform == "win32":
|
||||
if ctypes.windll.shell32.IsUserAnAdmin():
|
||||
install_dependencies()
|
||||
import specklepy
|
||||
else:
|
||||
ctypes.windll.shell32.ShellExecuteW(
|
||||
None, "runas", sys.executable, __file__, None, 1
|
||||
)
|
||||
|
||||
else:
|
||||
print(
|
||||
"Platform {} cannot automatically install dependencies.".format(
|
||||
platform
|
||||
)
|
||||
)
|
||||
raise
|
||||
@@ -0,0 +1,207 @@
|
||||
"""
|
||||
Provides uniform and consistent path helpers for `specklepy`
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
from importlib import import_module, invalidate_caches
|
||||
|
||||
_user_data_env_var = "SPECKLE_USERDATA_PATH"
|
||||
|
||||
|
||||
def _path() -> Optional[Path]:
|
||||
"""Read the user data path override setting."""
|
||||
path_override = os.environ.get(_user_data_env_var)
|
||||
if path_override:
|
||||
return Path(path_override)
|
||||
return None
|
||||
|
||||
|
||||
_application_name = "Speckle"
|
||||
|
||||
|
||||
def override_application_name(application_name: str) -> None:
|
||||
"""Override the global Speckle application name."""
|
||||
global _application_name
|
||||
_application_name = application_name
|
||||
|
||||
|
||||
def override_application_data_path(path: Optional[str]) -> None:
|
||||
"""
|
||||
Override the global Speckle application data path.
|
||||
|
||||
If the value of path is `None` the environment variable gets deleted.
|
||||
"""
|
||||
if path:
|
||||
os.environ[_user_data_env_var] = path
|
||||
else:
|
||||
os.environ.pop(_user_data_env_var, None)
|
||||
|
||||
|
||||
def _ensure_folder_exists(base_path: Path, folder_name: str) -> Path:
|
||||
path = base_path.joinpath(folder_name)
|
||||
path.mkdir(exist_ok=True, parents=True)
|
||||
return path
|
||||
|
||||
|
||||
def user_application_data_path() -> Path:
|
||||
"""Get the platform specific user configuration folder path"""
|
||||
path_override = _path()
|
||||
if path_override:
|
||||
return path_override
|
||||
|
||||
try:
|
||||
if sys.platform.startswith("win"):
|
||||
app_data_path = os.getenv("APPDATA")
|
||||
if not app_data_path:
|
||||
raise Exception(
|
||||
"Cannot get appdata path from environment."
|
||||
)
|
||||
return Path(app_data_path)
|
||||
else:
|
||||
# try getting the standard XDG_DATA_HOME value
|
||||
# as that is used as an override
|
||||
app_data_path = os.getenv("XDG_DATA_HOME")
|
||||
if app_data_path:
|
||||
return Path(app_data_path)
|
||||
else:
|
||||
return _ensure_folder_exists(Path.home(), ".config")
|
||||
except Exception as ex:
|
||||
raise Exception(
|
||||
"Failed to initialize user application data path.", ex
|
||||
)
|
||||
|
||||
|
||||
def user_speckle_folder_path() -> Path:
|
||||
"""Get the folder where the user's Speckle data should be stored."""
|
||||
return _ensure_folder_exists(user_application_data_path(), _application_name)
|
||||
|
||||
|
||||
def user_speckle_connector_installation_path(host_application: str) -> Path:
|
||||
"""
|
||||
Gets a connector specific installation folder.
|
||||
|
||||
In this folder we can put our connector installation and all python packages.
|
||||
"""
|
||||
return _ensure_folder_exists(
|
||||
_ensure_folder_exists(user_speckle_folder_path(), "connector_installations"),
|
||||
host_application,
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
print("Starting module dependency installation")
|
||||
print(sys.executable)
|
||||
|
||||
PYTHON_PATH = sys.executable
|
||||
|
||||
|
||||
|
||||
def connector_installation_path(host_application: str) -> Path:
|
||||
connector_installation_path = user_speckle_connector_installation_path(host_application)
|
||||
connector_installation_path.mkdir(exist_ok=True, parents=True)
|
||||
|
||||
# set user modules path at beginning of paths for earlier hit
|
||||
if sys.path[0] != connector_installation_path:
|
||||
sys.path.insert(0, str(connector_installation_path))
|
||||
|
||||
print(f"Using connector installation path {connector_installation_path}")
|
||||
return connector_installation_path
|
||||
|
||||
|
||||
|
||||
def is_pip_available() -> bool:
|
||||
try:
|
||||
import_module("pip") # noqa F401
|
||||
return True
|
||||
except ImportError:
|
||||
return False
|
||||
|
||||
|
||||
def ensure_pip() -> None:
|
||||
print("Installing pip... ")
|
||||
|
||||
from subprocess import run
|
||||
|
||||
completed_process = run([PYTHON_PATH, "-m", "ensurepip"])
|
||||
|
||||
if completed_process.returncode == 0:
|
||||
print("Successfully installed pip")
|
||||
else:
|
||||
raise Exception(f"Failed to install pip, got {completed_process.returncode} return code")
|
||||
|
||||
|
||||
def get_requirements_path() -> Path:
|
||||
# we assume that a requirements.txt exists next to the __init__.py file
|
||||
path = Path(Path(__file__).parent, "requirements.txt")
|
||||
assert path.exists()
|
||||
return path
|
||||
|
||||
|
||||
def install_requirements(host_application: str) -> None:
|
||||
# set up addons/modules under the user
|
||||
# script path. Here we'll install the
|
||||
# dependencies
|
||||
path = connector_installation_path(host_application)
|
||||
print(f"Installing Speckle dependencies to {path}")
|
||||
|
||||
from subprocess import run
|
||||
|
||||
completed_process = run(
|
||||
[
|
||||
PYTHON_PATH,
|
||||
"-m",
|
||||
"pip",
|
||||
"install",
|
||||
"-t",
|
||||
str(path),
|
||||
"-r",
|
||||
str(get_requirements_path()),
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
|
||||
if completed_process.returncode != 0:
|
||||
m = f"Failed to install dependenices through pip, got {completed_process.returncode} return code"
|
||||
print(m)
|
||||
raise Exception(m)
|
||||
|
||||
|
||||
def install_dependencies(host_application: str) -> None:
|
||||
if not is_pip_available():
|
||||
ensure_pip()
|
||||
|
||||
install_requirements(host_application)
|
||||
|
||||
|
||||
def _import_dependencies() -> None:
|
||||
import_module("specklepy")
|
||||
# the code above doesn't work for now, it fails on importing graphql-core
|
||||
# despite that, the connector seams to be working as expected
|
||||
# But it would be nice to make this solution work
|
||||
# it would ensure that all dependencies are fully loaded
|
||||
# requirements = get_requirements_path().read_text()
|
||||
# reqs = [
|
||||
# req.split(" ; ")[0].split("==")[0].split("[")[0].replace("-", "_")
|
||||
# for req in requirements.split("\n")
|
||||
# if req and not req.startswith(" ")
|
||||
# ]
|
||||
# for req in reqs:
|
||||
# print(req)
|
||||
# import_module("specklepy")
|
||||
|
||||
def ensure_dependencies(host_application: str) -> None:
|
||||
try:
|
||||
install_dependencies(host_application)
|
||||
invalidate_caches()
|
||||
_import_dependencies()
|
||||
print("Successfully found dependencies")
|
||||
except ImportError:
|
||||
raise Exception(f"Cannot automatically ensure Speckle dependencies. Please try restarting the host application {host_application}!")
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
from .users import LoadUsers, LoadUserStreams
|
||||
from .users import LoadUsers, LoadUserStreams, ResetUsers
|
||||
from .object import (
|
||||
UpdateObject,
|
||||
ResetObject,
|
||||
DeleteObject,
|
||||
UploadObject,
|
||||
UploadNgonsAsPolylines,
|
||||
SelectIfSameCustomProperty,
|
||||
SelectIfHasCustomProperty,
|
||||
@@ -17,15 +16,18 @@ from .streams import (
|
||||
)
|
||||
from .streams import (
|
||||
UpdateGlobal,
|
||||
AddStreamFromURL,
|
||||
CreateStream,
|
||||
CopyStreamId,
|
||||
CopyCommitId,
|
||||
CopyBranchName,
|
||||
)
|
||||
from .commit import DeleteCommit
|
||||
from .misc import OpenSpeckleGuide, OpenSpeckleTutorials, OpenSpeckleForum
|
||||
|
||||
operator_classes = [
|
||||
LoadUsers,
|
||||
ResetUsers,
|
||||
ReceiveStreamObjects,
|
||||
SendStreamObjects,
|
||||
LoadUserStreams,
|
||||
@@ -41,7 +43,6 @@ operator_classes.extend(
|
||||
UpdateObject,
|
||||
ResetObject,
|
||||
DeleteObject,
|
||||
UploadObject,
|
||||
UploadNgonsAsPolylines,
|
||||
SelectIfSameCustomProperty,
|
||||
SelectIfHasCustomProperty,
|
||||
@@ -49,5 +50,15 @@ operator_classes.extend(
|
||||
)
|
||||
|
||||
operator_classes.extend(
|
||||
[ViewStreamDataApi, DeleteStream, SelectOrphanObjects, UpdateGlobal, CreateStream,]
|
||||
[
|
||||
ViewStreamDataApi,
|
||||
DeleteStream,
|
||||
SelectOrphanObjects,
|
||||
UpdateGlobal,
|
||||
AddStreamFromURL,
|
||||
CreateStream,
|
||||
OpenSpeckleGuide,
|
||||
OpenSpeckleTutorials,
|
||||
OpenSpeckleForum,
|
||||
]
|
||||
)
|
||||
|
||||
@@ -1,29 +1,18 @@
|
||||
"""
|
||||
Commit operators
|
||||
"""
|
||||
import bpy, os
|
||||
from bpy.props import (
|
||||
StringProperty,
|
||||
BoolProperty,
|
||||
FloatProperty,
|
||||
CollectionProperty,
|
||||
EnumProperty,
|
||||
)
|
||||
|
||||
from bpy_speckle.functions import (
|
||||
_check_speckle_client_user_stream,
|
||||
_create_stream,
|
||||
get_scale_length,
|
||||
_report,
|
||||
)
|
||||
|
||||
from bpy_speckle.convert import from_speckle_object
|
||||
from typing import cast
|
||||
import bpy
|
||||
from bpy.props import BoolProperty
|
||||
from bpy_speckle.functions import _report, get_speckle
|
||||
from bpy_speckle.clients import speckle_clients
|
||||
from bpy_speckle.properties.scene import SpeckleSceneSettings
|
||||
|
||||
|
||||
class DeleteCommit(bpy.types.Operator):
|
||||
"""
|
||||
Delete stream
|
||||
Deletes the selected commit from the selected stream.
|
||||
To execute from code, call: `bpy.ops.speckle.delete_commit(are_you_sure=True)`
|
||||
"""
|
||||
|
||||
bl_idname = "speckle.delete_commit"
|
||||
@@ -32,7 +21,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):
|
||||
@@ -41,45 +31,37 @@ class DeleteCommit(bpy.types.Operator):
|
||||
col.prop(self, "are_you_sure")
|
||||
|
||||
def invoke(self, context, event):
|
||||
speckle = get_speckle(context)
|
||||
wm = context.window_manager
|
||||
if len(context.scene.speckle.users) > 0:
|
||||
if len(speckle.users) > 0:
|
||||
return wm.invoke_props_dialog(self)
|
||||
|
||||
return {"CANCELLED"}
|
||||
|
||||
def execute(self, context):
|
||||
try:
|
||||
self.delete_commit(context)
|
||||
return {"FINISHED"}
|
||||
except Exception as ex:
|
||||
print(f"{self.bl_idname}: failed: {ex}")
|
||||
return {"CANCELLED"}
|
||||
|
||||
def delete_commit(self, context: bpy.types.Context) -> None:
|
||||
|
||||
if not self.are_you_sure:
|
||||
return {"CANCELLED"}
|
||||
raise Exception("Cancelled by user")
|
||||
|
||||
self.are_you_sure = False
|
||||
|
||||
speckle = context.scene.speckle
|
||||
speckle = get_speckle(context)
|
||||
|
||||
check = _check_speckle_client_user_stream(context.scene)
|
||||
if check is None:
|
||||
return {"CANCELLED"}
|
||||
(_, stream, _, commit) = speckle.validate_commit_selection()
|
||||
|
||||
user, stream = check
|
||||
client = speckle_clients[int(context.scene.speckle.active_user)]
|
||||
client = speckle_clients[int(speckle.active_user)]
|
||||
|
||||
stream = user.streams[user.active_stream]
|
||||
if len(stream.branches) < 1:
|
||||
return {"CANCELLED"}
|
||||
else:
|
||||
branch = stream.branches[int(stream.branch)]
|
||||
if len(branch.commits) < 1:
|
||||
return {"CANCELLED"}
|
||||
else:
|
||||
commit = branch.commits[int(branch.commit)]
|
||||
deleted = client.commit.delete(stream_id=stream.id, commit_id=commit.id)
|
||||
if not deleted:
|
||||
raise Exception("Delete operation failed")
|
||||
|
||||
deleted = client.commit.delete(stream_id=stream.id, commit_id=commit.id)
|
||||
print(f"{self.bl_idname}: succeeded - commit {commit.id} ({commit.message}) has been deleted from stream {stream.id}")
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
bpy.ops.speckle.load_user_streams()
|
||||
context.view_layer.update()
|
||||
|
||||
if context.area:
|
||||
context.area.tag_redraw()
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import bpy
|
||||
import webbrowser
|
||||
|
||||
|
||||
class OpenSpeckleGuide(bpy.types.Operator):
|
||||
bl_idname = "speckle.open_speckle_guide"
|
||||
bl_label = "Speckle Guide"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Browse the documentation on the Speckle Guide"
|
||||
|
||||
def execute(self, context):
|
||||
webbrowser.open("https://speckle.guide/user/blender.html")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class OpenSpeckleTutorials(bpy.types.Operator):
|
||||
bl_idname = "speckle.open_speckle_tutorials"
|
||||
bl_label = "Tutorials Portal"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Visit our tutorials portal for learning resources"
|
||||
|
||||
def execute(self, context):
|
||||
webbrowser.open("https://speckle.systems/tutorials/")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class OpenSpeckleForum(bpy.types.Operator):
|
||||
bl_idname = "speckle.open_speckle_forum"
|
||||
bl_label = "Community Forum"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Ask questions and join the discussion on our community forum"
|
||||
|
||||
def execute(self, context):
|
||||
webbrowser.open("https://speckle.community/")
|
||||
return {"FINISHED"}
|
||||
@@ -2,19 +2,13 @@
|
||||
Object operators
|
||||
"""
|
||||
|
||||
import bpy, bmesh, os
|
||||
from bpy.props import (
|
||||
StringProperty,
|
||||
BoolProperty,
|
||||
FloatProperty,
|
||||
CollectionProperty,
|
||||
EnumProperty,
|
||||
import bpy
|
||||
from bpy.props import BoolProperty, EnumProperty
|
||||
from deprecated import deprecated
|
||||
from bpy_speckle.convert.to_speckle import (
|
||||
convert_to_speckle,
|
||||
ngons_to_speckle_polylines,
|
||||
)
|
||||
|
||||
from specklepy.api.client import SpeckleClient
|
||||
from bpy_speckle.convert import to_speckle_object
|
||||
from bpy_speckle.convert.to_speckle import export_ngons_as_polylines
|
||||
|
||||
from bpy_speckle.functions import get_scale_length, _report
|
||||
from bpy_speckle.clients import speckle_clients
|
||||
|
||||
@@ -35,7 +29,6 @@ class UpdateObject(bpy.types.Operator):
|
||||
def execute(self, context):
|
||||
user = context.scene.speckle.users[int(context.scene.speckle.active_user)]
|
||||
client = speckle_clients[int(context.scene.speckle.active_user)]
|
||||
stream = user.streams[user.active_stream]
|
||||
|
||||
active = context.active_object
|
||||
_report(active)
|
||||
@@ -58,7 +51,7 @@ class UpdateObject(bpy.types.Operator):
|
||||
stream_units
|
||||
)
|
||||
|
||||
sm = to_speckle_object(active, scale)
|
||||
sm = convert_to_speckle(active, scale)
|
||||
|
||||
_report("Updating object {}".format(sm["_id"]))
|
||||
client.objects.update(active.speckle.object_id, sm)
|
||||
@@ -99,8 +92,6 @@ class DeleteObject(bpy.types.Operator):
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
|
||||
user = context.scene.speckle.users[int(context.scene.speckle.active_user)]
|
||||
client = speckle_clients[int(context.scene.speckle.active_user)]
|
||||
active = context.object
|
||||
if active.speckle.enabled:
|
||||
@@ -112,13 +103,11 @@ class DeleteObject(bpy.types.Operator):
|
||||
]
|
||||
if existing is None:
|
||||
return {"CANCELLED"}
|
||||
# print("Existing: %s" % SpeckleResource.to_json_pretty(existing))
|
||||
new_objects = [
|
||||
x
|
||||
for x in res["resource"]["objects"]
|
||||
if x["_id"] != active.speckle.object_id
|
||||
]
|
||||
# print (SpeckleResource.to_json_pretty(new_objects))
|
||||
|
||||
res = client.GetLayers(active.speckle.stream_id)
|
||||
new_layers = res["resource"]["layers"]
|
||||
@@ -138,7 +127,7 @@ class DeleteObject(bpy.types.Operator):
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@deprecated
|
||||
class UploadNgonsAsPolylines(bpy.types.Operator):
|
||||
"""
|
||||
Upload mesh ngon faces as polyline outlines
|
||||
@@ -162,11 +151,12 @@ class UploadNgonsAsPolylines(bpy.types.Operator):
|
||||
client = speckle_clients[int(context.scene.speckle.active_user)]
|
||||
stream = user.streams[user.active_stream]
|
||||
|
||||
scale = context.scene.unit_settings.scale_length / get_scale_length(
|
||||
stream.units
|
||||
)
|
||||
# scale = context.scene.unit_settings.scale_length / get_scale_length(
|
||||
# stream.units
|
||||
# )
|
||||
scale = 1.0
|
||||
|
||||
sp = export_ngons_as_polylines(active, scale)
|
||||
sp = ngons_to_speckle_polylines(active, scale)
|
||||
|
||||
if sp is None:
|
||||
return {"CANCELLED"}
|
||||
@@ -175,16 +165,12 @@ class UploadNgonsAsPolylines(bpy.types.Operator):
|
||||
for polyline in sp:
|
||||
|
||||
res = client.objects.create([polyline])
|
||||
print(res)
|
||||
|
||||
if res is None:
|
||||
_report(client.me)
|
||||
continue
|
||||
placeholders.extend(res)
|
||||
|
||||
# polyline['_id'] = res['_id']
|
||||
# placeholders.append({'type':'Placeholder', '_id':res['_id']})
|
||||
|
||||
if not placeholders:
|
||||
return {"CANCELLED"}
|
||||
|
||||
@@ -222,58 +208,6 @@ class UploadNgonsAsPolylines(bpy.types.Operator):
|
||||
layout.prop(self, "clear_stream")
|
||||
|
||||
|
||||
class UploadObject(bpy.types.Operator):
|
||||
"""
|
||||
DEPRECATED
|
||||
Upload an individual object
|
||||
"""
|
||||
|
||||
bl_idname = "speckle.upload_object"
|
||||
bl_label = "Upload Object"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
|
||||
active = context.active_object
|
||||
if active is not None:
|
||||
user = context.scene.speckle.users[int(context.scene.speckle.active_user)]
|
||||
client = speckle_clients[int(context.scene.speckle.active_user)]
|
||||
stream = user.streams[user.active_stream]
|
||||
|
||||
scale = context.scene.unit_settings.scale_length / get_scale_length(
|
||||
stream.units
|
||||
)
|
||||
|
||||
sm = to_speckle_object(active, scale)
|
||||
|
||||
placeholders = client.objects.create([sm])
|
||||
if placeholders is None:
|
||||
return {"CANCELLED"}
|
||||
|
||||
sstream = client.streams.get(stream.id)
|
||||
sstream.objects.extend(placeholders)
|
||||
|
||||
N = sstream.layers[-1].objectCount
|
||||
sstream.layers[-1].objectCount = N + 1
|
||||
sstream.layers[-1].topology = "0-%s" % (N + 1)
|
||||
|
||||
_report("Updating stream %s" % stream.id)
|
||||
|
||||
res = client.streams.update(stream["id"], sstream)
|
||||
|
||||
_report(res)
|
||||
|
||||
active.speckle.enabled = True
|
||||
active.speckle.object_id = sm.id
|
||||
active.speckle.stream_id = stream.id
|
||||
active.speckle.send_or_receive = "send"
|
||||
|
||||
context.view_layer.update()
|
||||
_report("Done.")
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
def get_custom_speckle_props(self, context):
|
||||
ignore = ["speckle", "cycles", "cycles_visibility"]
|
||||
|
||||
|
||||
+547
-263
File diff suppressed because it is too large
Load Diff
+110
-77
@@ -1,24 +1,39 @@
|
||||
"""
|
||||
User account operators
|
||||
"""
|
||||
|
||||
from typing import cast
|
||||
import bpy, bmesh, os
|
||||
from bpy.props import (
|
||||
StringProperty,
|
||||
BoolProperty,
|
||||
FloatProperty,
|
||||
CollectionProperty,
|
||||
EnumProperty,
|
||||
)
|
||||
from bpy_speckle.properties.scene import SpeckleUserObject
|
||||
|
||||
from bpy_speckle.functions import _report
|
||||
import bpy
|
||||
from bpy.types import Context
|
||||
from bpy_speckle.functions import _report, get_speckle
|
||||
from bpy_speckle.clients import speckle_clients
|
||||
|
||||
from bpy_speckle.properties.scene import SpeckleCommitObject, SpeckleSceneSettings, SpeckleUserObject
|
||||
from specklepy.api.client import SpeckleClient
|
||||
from specklepy.api.credentials import get_default_account, get_local_accounts
|
||||
from specklepy.api.models import Stream
|
||||
from specklepy.api.credentials import get_local_accounts
|
||||
|
||||
class ResetUsers(bpy.types.Operator):
|
||||
"""
|
||||
Reset loaded users
|
||||
"""
|
||||
|
||||
bl_idname = "speckle.users_reset"
|
||||
bl_label = "Reset users"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
self.reset_ui(context)
|
||||
|
||||
bpy.context.view_layer.update()
|
||||
if context.area:
|
||||
context.area.tag_redraw()
|
||||
return {"FINISHED"}
|
||||
|
||||
@staticmethod
|
||||
def reset_ui(context: Context):
|
||||
speckle = get_speckle(context)
|
||||
|
||||
speckle.users.clear()
|
||||
speckle_clients.clear()
|
||||
|
||||
class LoadUsers(bpy.types.Operator):
|
||||
"""
|
||||
@@ -33,31 +48,38 @@ class LoadUsers(bpy.types.Operator):
|
||||
|
||||
_report("Loading users...")
|
||||
|
||||
users = context.scene.speckle.users
|
||||
speckle = cast(SpeckleSceneSettings, context.scene.speckle) #type: ignore
|
||||
users = speckle.users
|
||||
|
||||
context.scene.speckle.users.clear()
|
||||
speckle_clients.clear()
|
||||
ResetUsers.reset_ui(context)
|
||||
|
||||
profiles = get_local_accounts()
|
||||
active_user_index = 0
|
||||
|
||||
for profile in profiles:
|
||||
user = users.add()
|
||||
user.server_name = profile.serverInfo.name or "Speckle Server"
|
||||
user.server_url = profile.serverInfo.url
|
||||
user.id = profile.userInfo.id
|
||||
user.name = profile.userInfo.name
|
||||
user.email = profile.userInfo.email
|
||||
user.company = profile.userInfo.company or ""
|
||||
user.authToken = profile.token
|
||||
try:
|
||||
client = SpeckleClient(host=profile.serverInfo.url, use_ssl=True)
|
||||
client.authenticate(user.authToken)
|
||||
url = profile.serverInfo.url
|
||||
assert(url)
|
||||
client = SpeckleClient(
|
||||
host=url,
|
||||
use_ssl="https" in url,
|
||||
)
|
||||
client.authenticate_with_account(profile)
|
||||
speckle_clients.append(client)
|
||||
except Exception as ex:
|
||||
_report(ex)
|
||||
users.remove(len(users) - 1)
|
||||
if profile.isDefault:
|
||||
active_user_index = len(users) - 1
|
||||
|
||||
context.scene.speckle.active_user_index = int(context.scene.speckle.active_user)
|
||||
bpy.ops.speckle.load_user_streams()
|
||||
speckle.active_user = str(active_user_index)
|
||||
bpy.context.view_layer.update()
|
||||
|
||||
if context.area:
|
||||
@@ -65,6 +87,39 @@ class LoadUsers(bpy.types.Operator):
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
def add_user_stream(user: SpeckleUserObject, stream: Stream):
|
||||
s = user.streams.add()
|
||||
s.name = stream.name
|
||||
s.id = stream.id
|
||||
s.description = stream.description
|
||||
|
||||
if not stream.branches:
|
||||
return
|
||||
|
||||
# branches = [branch for branch in stream.branches.items if branch.name != "globals"]
|
||||
for b in stream.branches.items:
|
||||
branch = s.branches.add()
|
||||
branch.name = b.name
|
||||
|
||||
if not b.commits:
|
||||
continue
|
||||
|
||||
for c in b.commits.items:
|
||||
commit: SpeckleCommitObject = branch.commits.add()
|
||||
commit.id = commit.name = c.id
|
||||
commit.message = c.message or ""
|
||||
commit.author_name = c.authorName
|
||||
commit.author_id = c.authorId
|
||||
commit.created_at = c.createdAt.strftime("%Y-%m-%d %H:%M:%S.%f%Z") if c.createdAt else ""
|
||||
commit.source_application = str(c.sourceApplication)
|
||||
commit.referenced_object = c.referencedObject
|
||||
|
||||
if hasattr(s, "baseProperties"):
|
||||
s.units = stream.baseProperties.units # type: ignore
|
||||
else:
|
||||
s.units = "Meters"
|
||||
|
||||
|
||||
class LoadUserStreams(bpy.types.Operator):
|
||||
"""
|
||||
Load all available streams for active user user
|
||||
@@ -76,62 +131,40 @@ class LoadUserStreams(bpy.types.Operator):
|
||||
bl_description = "(Re)load all available user streams"
|
||||
|
||||
def execute(self, context):
|
||||
speckle = context.scene.speckle
|
||||
|
||||
if len(speckle.users) > 0:
|
||||
user = speckle.users[int(context.scene.speckle.active_user)]
|
||||
client = speckle_clients[int(context.scene.speckle.active_user)]
|
||||
|
||||
try:
|
||||
streams = client.stream.list(stream_limit=20)
|
||||
except Exception as e:
|
||||
_report("Failed to retrieve streams: {}".format(e))
|
||||
return
|
||||
if not streams:
|
||||
_report("Failed to retrieve streams.")
|
||||
return
|
||||
|
||||
user.streams.clear()
|
||||
|
||||
streams = sorted(streams, key=lambda x: x.name, reverse=False)
|
||||
default_units = "Meters"
|
||||
|
||||
for s in streams:
|
||||
stream = user.streams.add()
|
||||
stream.name = s.name
|
||||
stream.id = s.id
|
||||
stream.description = s.description
|
||||
|
||||
sstream = client.stream.get(id=s.id)
|
||||
|
||||
if not sstream.branches:
|
||||
continue
|
||||
|
||||
for b in sstream.branches.items:
|
||||
branch = stream.branches.add()
|
||||
branch.name = b.name
|
||||
|
||||
if not b.commits:
|
||||
continue
|
||||
|
||||
for c in b.commits.items:
|
||||
commit = branch.commits.add()
|
||||
commit.id = c.id
|
||||
commit.message = c.message
|
||||
commit.author_name = c.authorName
|
||||
commit.author_id = c.authorId
|
||||
commit.created_at = c.createdAt
|
||||
commit.source_application = str(c.sourceApplication)
|
||||
|
||||
if hasattr(s, "baseProperties"):
|
||||
stream.units = s.baseProperties.units
|
||||
else:
|
||||
stream.units = default_units
|
||||
|
||||
bpy.context.view_layer.update()
|
||||
|
||||
try:
|
||||
self.add_stream_from_url(context)
|
||||
return {"FINISHED"}
|
||||
except Exception as ex:
|
||||
_report(f"{self.bl_idname} failed: {ex}")
|
||||
return {"CANCELLED"}
|
||||
|
||||
def add_stream_from_url(self, context: Context) -> None:
|
||||
speckle = get_speckle(context)
|
||||
|
||||
user = speckle.validate_user_selection()
|
||||
|
||||
client = speckle_clients[int(speckle.active_user)]
|
||||
|
||||
try:
|
||||
streams = client.stream.list(stream_limit=20)
|
||||
except Exception as e:
|
||||
_report(f"Failed to retrieve streams: {e}")
|
||||
return
|
||||
if not streams:
|
||||
_report("Failed to retrieve streams.")
|
||||
return
|
||||
|
||||
user.streams.clear()
|
||||
|
||||
default_units = "Meters"
|
||||
|
||||
for s in streams:
|
||||
assert(s.id)
|
||||
sstream = client.stream.get(id=s.id, branch_limit=20)
|
||||
add_user_stream(user, sstream)
|
||||
|
||||
bpy.context.view_layer.update()
|
||||
|
||||
if context.area:
|
||||
context.area.tag_redraw()
|
||||
return {"CANCELLED"}
|
||||
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
"""
|
||||
Addon properties
|
||||
"""
|
||||
|
||||
import bpy
|
||||
from bpy.props import BoolProperty
|
||||
|
||||
|
||||
class SpeckleAddonPreferences(bpy.types.AddonPreferences):
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
"""
|
||||
Collection properties
|
||||
"""
|
||||
|
||||
import bpy
|
||||
from bpy.props import StringProperty, BoolProperty, EnumProperty
|
||||
|
||||
|
||||
class SpeckleCollectionSettings(bpy.types.PropertyGroup):
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
"""
|
||||
Object properties
|
||||
"""
|
||||
|
||||
import bpy
|
||||
from bpy.props import StringProperty, BoolProperty, EnumProperty
|
||||
|
||||
|
||||
class SpeckleObjectSettings(bpy.types.PropertyGroup):
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""
|
||||
Scene properties
|
||||
"""
|
||||
|
||||
from typing import Optional, Tuple
|
||||
import bpy
|
||||
from bpy.props import (
|
||||
StringProperty,
|
||||
@@ -12,7 +12,6 @@ from bpy.props import (
|
||||
IntProperty,
|
||||
PointerProperty,
|
||||
)
|
||||
from specklepy.api.client import SpeckleClient
|
||||
|
||||
|
||||
class SpeckleSceneObject(bpy.types.PropertyGroup):
|
||||
@@ -20,12 +19,13 @@ class SpeckleSceneObject(bpy.types.PropertyGroup):
|
||||
|
||||
|
||||
class SpeckleCommitObject(bpy.types.PropertyGroup):
|
||||
id: StringProperty(default="abc")
|
||||
message: StringProperty(default="A simple commit")
|
||||
author_name: StringProperty(default="Author name")
|
||||
author_id: StringProperty(default="Author ID")
|
||||
created_at: StringProperty(default="Today")
|
||||
source_application: StringProperty(default="Unknown")
|
||||
id: StringProperty(default="")
|
||||
message: StringProperty(default="")
|
||||
author_name: StringProperty(default="")
|
||||
author_id: StringProperty(default="")
|
||||
created_at: StringProperty(default="")
|
||||
source_application: StringProperty(default="")
|
||||
referenced_object: StringProperty(default="")
|
||||
|
||||
|
||||
class SpeckleBranchObject(bpy.types.PropertyGroup):
|
||||
@@ -44,14 +44,21 @@ class SpeckleBranchObject(bpy.types.PropertyGroup):
|
||||
description="Active commit",
|
||||
items=get_commits,
|
||||
)
|
||||
|
||||
def get_active_commit(self) -> Optional[SpeckleCommitObject]:
|
||||
selected_index = int(self.commit)
|
||||
if 0 <= selected_index < len(self.commits):
|
||||
return self.commits[selected_index]
|
||||
return None
|
||||
|
||||
|
||||
class SpeckleStreamObject(bpy.types.PropertyGroup):
|
||||
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)]
|
||||
|
||||
@@ -67,23 +74,35 @@ class SpeckleStreamObject(bpy.types.PropertyGroup):
|
||||
items=get_branches,
|
||||
)
|
||||
|
||||
def get_active_branch(self) -> Optional[SpeckleBranchObject]:
|
||||
selected_index = int(self.branch)
|
||||
if 0 <= selected_index < len(self.branches):
|
||||
return self.branches[selected_index]
|
||||
return None
|
||||
|
||||
|
||||
class SpeckleUserObject(bpy.types.PropertyGroup):
|
||||
server_name: StringProperty(default="SpeckleXYZ")
|
||||
server_url: StringProperty(default="https://speckle.xyz")
|
||||
id: StringProperty(default="")
|
||||
name: StringProperty(default="Speckle User")
|
||||
email: StringProperty(default="user@speckle.xyz")
|
||||
company: StringProperty(default="SpeckleSystems")
|
||||
authToken: StringProperty(default="")
|
||||
streams: CollectionProperty(type=SpeckleStreamObject)
|
||||
active_stream: IntProperty(default=0)
|
||||
|
||||
def get_active_stream(self) -> Optional[SpeckleStreamObject]:
|
||||
selected_index = int(self.active_stream)
|
||||
if 0 <= selected_index < len(self.streams):
|
||||
return self.streams[selected_index]
|
||||
return None
|
||||
|
||||
class SpeckleSceneSettings(bpy.types.PropertyGroup):
|
||||
def get_scripts(self, context):
|
||||
seq = [("<none>", "<none>", "<none>")]
|
||||
seq.extend([(t.name, t.name, t.name) for t in bpy.data.texts])
|
||||
return seq
|
||||
return [
|
||||
("<none>", "<none>", "<none>"),
|
||||
*[(t.name, t.name, t.name) for t in bpy.data.texts],
|
||||
]
|
||||
|
||||
streams: EnumProperty(
|
||||
name="Available streams",
|
||||
@@ -104,8 +123,8 @@ class SpeckleSceneSettings(bpy.types.PropertyGroup):
|
||||
|
||||
active_user: EnumProperty(
|
||||
items=get_users,
|
||||
name="User",
|
||||
description="Select user",
|
||||
name="Account",
|
||||
description="Select account",
|
||||
update=set_user,
|
||||
get=None,
|
||||
set=None,
|
||||
@@ -132,3 +151,44 @@ class SpeckleSceneSettings(bpy.types.PropertyGroup):
|
||||
description="Script to run when sending stream objects.",
|
||||
items=get_scripts,
|
||||
)
|
||||
|
||||
def get_active_user(self) -> Optional[SpeckleUserObject]:
|
||||
selected_index = int(self.active_user)
|
||||
if 0 <= selected_index < len(self.users):
|
||||
return self.users[selected_index]
|
||||
return None
|
||||
|
||||
|
||||
def validate_user_selection(self) -> SpeckleUserObject:
|
||||
user = self.get_active_user()
|
||||
if not user:
|
||||
raise SelectionException("No user selected/found")
|
||||
return user
|
||||
|
||||
def validate_stream_selection(self) -> Tuple[SpeckleUserObject, SpeckleStreamObject]:
|
||||
user = self.validate_user_selection()
|
||||
|
||||
stream = user.get_active_stream()
|
||||
if not stream:
|
||||
raise SelectionException("No stream selected/found")
|
||||
|
||||
return (user, stream)
|
||||
|
||||
def validate_branch_selection(self) -> Tuple[SpeckleUserObject, SpeckleStreamObject, SpeckleBranchObject]:
|
||||
(user, stream) = self.validate_stream_selection()
|
||||
|
||||
branch = stream.get_active_branch()
|
||||
if not branch:
|
||||
raise SelectionException("No branch selected/found")
|
||||
return (user, stream, branch)
|
||||
|
||||
def validate_commit_selection(self) ->Tuple[SpeckleUserObject, SpeckleStreamObject, SpeckleBranchObject, SpeckleCommitObject]:
|
||||
(user, stream, branch) = self.validate_branch_selection()
|
||||
commit = branch.get_active_commit()
|
||||
if commit is None:
|
||||
raise SelectionException("No commit selected/found")
|
||||
|
||||
return (user, stream, branch, commit)
|
||||
|
||||
class SelectionException(Exception):
|
||||
pass
|
||||
@@ -0,0 +1,83 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Collection, Dict, Generic, Iterable, List, Optional, Tuple, TypeVar
|
||||
from attrs import define
|
||||
from specklepy.objects.base import Base
|
||||
|
||||
ROOT: str = "__Root"
|
||||
|
||||
T = TypeVar('T')
|
||||
PARENT_INFO = Tuple[Optional[str], str]
|
||||
|
||||
@define(slots=True)
|
||||
class CommitObjectBuilder(ABC, Generic[T]):
|
||||
|
||||
converted: Dict[str, Base]
|
||||
_parent_infos: Dict[str, Collection[PARENT_INFO]]
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.converted = {}
|
||||
self._parent_infos = {}
|
||||
|
||||
@abstractmethod
|
||||
def include_object(self, conversion_result: Base, native_object: T) -> None:
|
||||
pass
|
||||
|
||||
def build_commit_object(self, root_commit_object: Base) -> None:
|
||||
self.apply_relationships(self.converted.values(), root_commit_object)
|
||||
|
||||
def set_relationship(self, app_id: Optional[str], *parent_info : PARENT_INFO) -> None:
|
||||
|
||||
if not app_id:
|
||||
return
|
||||
|
||||
self._parent_infos[app_id] = parent_info
|
||||
|
||||
def apply_relationships(self, to_add: Iterable[Base], root_commit_object: Base) -> None:
|
||||
for c in to_add:
|
||||
try:
|
||||
self.apply_relationship(c, root_commit_object)
|
||||
except Exception as ex:
|
||||
print(f"Failed to add object {type(c)} to commit object: {ex}")
|
||||
|
||||
def apply_relationship(self, current: Base, root_commit_object: Base):
|
||||
if not current.applicationId: raise Exception(f"Expected applicationId to have been set")
|
||||
|
||||
parents = self._parent_infos[current.applicationId]
|
||||
|
||||
for (parent_id, prop_name) in parents:
|
||||
if not parent_id: continue
|
||||
|
||||
parent: Optional[Base]
|
||||
if parent_id == ROOT:
|
||||
parent = root_commit_object
|
||||
else:
|
||||
parent = self.converted[parent_id] if parent_id in self.converted else None
|
||||
|
||||
if not parent: continue
|
||||
|
||||
try:
|
||||
elements = get_detached_prop(parent, prop_name)
|
||||
if not isinstance(elements, list):
|
||||
elements = []
|
||||
set_detached_prop(parent, prop_name, elements)
|
||||
|
||||
elements.append(current)
|
||||
return
|
||||
except Exception as ex:
|
||||
# A parent was found, but it was invalid (Likely because of a type mismatch on a `elements` property)
|
||||
print(f"Failed to add object {type(current)} to a converted parent; {ex}")
|
||||
|
||||
raise Exception(f"Could not find a valid parent for object of type {type(current)}. Checked {len(parents)} potential parent, and non were converted!")
|
||||
|
||||
|
||||
def get_detached_prop(speckle_object: Base, prop_name: str) -> Optional[Any]:
|
||||
detached_prop_name = get_detached_prop_name(speckle_object, prop_name)
|
||||
return getattr(speckle_object, detached_prop_name, None)
|
||||
|
||||
def set_detached_prop(speckle_object: Base, prop_name: str, value: Optional[Any]) -> None:
|
||||
detached_prop_name = get_detached_prop_name(speckle_object, prop_name)
|
||||
setattr(speckle_object, detached_prop_name, value)
|
||||
|
||||
def get_detached_prop_name(speckle_object: Base, prop_name: str) -> str:
|
||||
return prop_name if hasattr(speckle_object, prop_name) else f"@{prop_name}"
|
||||
@@ -0,0 +1,121 @@
|
||||
from typing import Any, Callable, Collection, Iterable, Iterator, List, Optional, Set
|
||||
|
||||
from attrs import define
|
||||
from typing_extensions import Protocol, final
|
||||
|
||||
from specklepy.objects import Base
|
||||
|
||||
|
||||
class ITraversalRule(Protocol):
|
||||
def get_members_to_traverse(self, o: Base) -> Set[str]:
|
||||
"""Get the members to traverse."""
|
||||
pass
|
||||
|
||||
def does_rule_hold(self, o: Base) -> bool:
|
||||
"""Make sure the rule still holds."""
|
||||
pass
|
||||
|
||||
|
||||
@final
|
||||
class DefaultRule:
|
||||
def get_members_to_traverse(self, _) -> Set[str]:
|
||||
return set()
|
||||
|
||||
def does_rule_hold(self, _) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
# we're creating a local protected "singleton"
|
||||
_default_rule = DefaultRule()
|
||||
|
||||
|
||||
@final
|
||||
@define(slots=True, frozen=True)
|
||||
class TraversalContext:
|
||||
current: Base
|
||||
member_name: Optional[str] = None
|
||||
parent: Optional["TraversalContext"] = None
|
||||
|
||||
|
||||
@final
|
||||
@define(slots=True, frozen=True)
|
||||
class GraphTraversal:
|
||||
|
||||
_rules: List[ITraversalRule]
|
||||
|
||||
def traverse(self, root: Base) -> Iterator[TraversalContext]:
|
||||
stack: List[TraversalContext] = []
|
||||
|
||||
stack.append(TraversalContext(root))
|
||||
|
||||
while len(stack) > 0:
|
||||
head = stack.pop()
|
||||
yield head
|
||||
|
||||
current = head.current
|
||||
active_rule = self._get_active_rule_or_default_rule(current)
|
||||
members_to_traverse = active_rule.get_members_to_traverse(current)
|
||||
for child_prop in members_to_traverse:
|
||||
try:
|
||||
if child_prop in {"speckle_type", "units", "applicationId"}: continue #debug: to avoid noisy exceptions, explicitly avoid checking ones we know will fail, this is not exhaustive
|
||||
value = current[child_prop]
|
||||
self._traverse_member_to_stack(
|
||||
stack, value, child_prop, head
|
||||
)
|
||||
except KeyError as ex:
|
||||
# Unset application ids, and class variables like SpeckleType will throw when __getitem__ is called
|
||||
pass
|
||||
@staticmethod
|
||||
def _traverse_member_to_stack(
|
||||
stack: List[TraversalContext],
|
||||
value: Any,
|
||||
member_name: Optional[str] = None,
|
||||
parent: Optional[TraversalContext] = None,
|
||||
):
|
||||
if isinstance(value, Base):
|
||||
stack.append(TraversalContext(value, member_name, parent))
|
||||
elif isinstance(value, list):
|
||||
for obj in value:
|
||||
GraphTraversal._traverse_member_to_stack(stack, obj, member_name, parent)
|
||||
elif isinstance(value, dict):
|
||||
for obj in value.values():
|
||||
GraphTraversal._traverse_member_to_stack(stack, obj, member_name, parent)
|
||||
|
||||
@staticmethod
|
||||
def traverse_member(value: Optional[Any]) -> Iterator[Base]:
|
||||
if isinstance(value, Base):
|
||||
yield value
|
||||
elif isinstance(value, list):
|
||||
for obj in value:
|
||||
for o in GraphTraversal.traverse_member(obj):
|
||||
yield o
|
||||
elif isinstance(value, dict):
|
||||
for obj in value.values():
|
||||
for o in GraphTraversal.traverse_member(obj):
|
||||
yield o
|
||||
|
||||
|
||||
def _get_active_rule_or_default_rule(self, o: Base) -> ITraversalRule:
|
||||
return self._get_active_rule(o) or _default_rule
|
||||
|
||||
def _get_active_rule(self, o: Base) -> Optional[ITraversalRule]:
|
||||
for rule in self._rules:
|
||||
if rule.does_rule_hold(o):
|
||||
return rule
|
||||
return None
|
||||
|
||||
|
||||
@final
|
||||
@define(slots=True, frozen=True)
|
||||
class TraversalRule:
|
||||
_conditions: Collection[Callable[[Base], bool]]
|
||||
_members_to_traverse: Callable[[Base], Iterable[str]]
|
||||
|
||||
def get_members_to_traverse(self, o: Base) -> Set[str]:
|
||||
return set(self._members_to_traverse(o))
|
||||
|
||||
def does_rule_hold(self, o: Base) -> bool:
|
||||
for condition in self._conditions:
|
||||
if condition(o):
|
||||
return True
|
||||
return False
|
||||
@@ -5,6 +5,7 @@ from .view3d import (
|
||||
VIEW3D_PT_SpeckleUser,
|
||||
VIEW3D_PT_SpeckleStreams,
|
||||
VIEW3D_PT_SpeckleActiveStream,
|
||||
VIEW3D_PT_SpeckleHelp,
|
||||
)
|
||||
|
||||
ui_classes = [
|
||||
@@ -13,5 +14,5 @@ ui_classes = [
|
||||
VIEW3D_PT_SpeckleActiveStream,
|
||||
VIEW3D_UL_SpeckleUsers,
|
||||
VIEW3D_UL_SpeckleStreams,
|
||||
# OBJECT_PT_speckle,
|
||||
VIEW3D_PT_SpeckleHelp,
|
||||
]
|
||||
|
||||
+33
-17
@@ -12,7 +12,7 @@ from bpy.props import (
|
||||
EnumProperty,
|
||||
)
|
||||
|
||||
import datetime
|
||||
from datetime import datetime
|
||||
|
||||
"""
|
||||
Compatibility
|
||||
@@ -84,9 +84,8 @@ class VIEW3D_UL_SpeckleStreams(bpy.types.UIList):
|
||||
def draw_item(self, context, layout, data, stream, active_data, active_propname):
|
||||
if self.layout_type in {"DEFAULT", "COMPACT"}:
|
||||
if stream:
|
||||
# layout.prop(user, "name", text=user.name, emboss=False, icon_value=0)
|
||||
layout.label(
|
||||
text="{} ({})".format(stream.name, stream.id),
|
||||
text=f"{stream.name} ({stream.id})",
|
||||
translate=False,
|
||||
icon_value=0,
|
||||
)
|
||||
@@ -116,14 +115,14 @@ class VIEW3D_PT_SpeckleUser(bpy.types.Panel):
|
||||
col = layout.column()
|
||||
|
||||
if len(speckle.users) < 1:
|
||||
col.label(text="No users found.")
|
||||
col.label(text="Refresh to initialise")
|
||||
else:
|
||||
# col.label(text="User")
|
||||
col.prop(speckle, "active_user", text="")
|
||||
user = speckle.users[int(speckle.active_user)]
|
||||
col.label(text="{} ({})".format(user.server_name, user.server_url))
|
||||
col.label(text="{} ({})".format(user.name, user.email))
|
||||
|
||||
|
||||
col.operator("speckle.users_load", text="", icon="FILE_REFRESH")
|
||||
|
||||
class VIEW3D_PT_SpeckleStreams(bpy.types.Panel):
|
||||
"""
|
||||
@@ -144,11 +143,11 @@ class VIEW3D_PT_SpeckleStreams(bpy.types.Panel):
|
||||
col.label(text="No stream data.")
|
||||
else:
|
||||
user = speckle.users[int(speckle.active_user)]
|
||||
# col.label(text="Streams")
|
||||
col.template_list(
|
||||
"VIEW3D_UL_SpeckleStreams", "", user, "streams", user, "active_stream"
|
||||
)
|
||||
row = col.row(align=True)
|
||||
row.operator("speckle.add_stream_from_url", text="", icon="URL")
|
||||
row.operator("speckle.create_stream", text="", icon="ADD")
|
||||
row.operator("speckle.delete_stream", text="", icon="REMOVE")
|
||||
row.operator("speckle.load_user_streams", text="", icon="FILE_REFRESH")
|
||||
@@ -179,7 +178,7 @@ class VIEW3D_PT_SpeckleActiveStream(bpy.types.Panel):
|
||||
stream = user.streams[user.active_stream]
|
||||
# user.active_stream = min(user.active_stream, len(user.streams) - 1)
|
||||
row = col.row()
|
||||
row.label(text="{} ({})".format(stream.name, stream.id))
|
||||
row.label(text=f"{stream.name} ({stream.id})")
|
||||
row.operator("speckle.stream_copy_id", text="", icon="COPY_ID")
|
||||
col.separator()
|
||||
|
||||
@@ -207,13 +206,11 @@ class VIEW3D_PT_SpeckleActiveStream(bpy.types.Panel):
|
||||
row.label(text=line)
|
||||
area.separator()
|
||||
|
||||
dt = datetime.datetime.strptime(
|
||||
commit.created_at, "%Y-%m-%dT%H:%M:%S.%fZ"
|
||||
)
|
||||
col.label(text="{}".format(dt.ctime()))
|
||||
col.label(
|
||||
text="{} ({})".format(commit.author_name, commit.author_id)
|
||||
dt = datetime.strptime(
|
||||
commit.created_at, "%Y-%m-%d %H:%M:%S.%f%Z"
|
||||
)
|
||||
col.label(text=f"{dt.ctime()}")
|
||||
col.label(text=f"{commit.author_name} ({commit.author_id})")
|
||||
col.label(text=commit.source_application)
|
||||
else:
|
||||
col.label(text="No branches found!")
|
||||
@@ -234,9 +231,6 @@ class VIEW3D_PT_SpeckleActiveStream(bpy.types.Panel):
|
||||
|
||||
row = col.row(align=True)
|
||||
subcol = row.column()
|
||||
subcol.label(text="Units:")
|
||||
subcol = row.column()
|
||||
subcol.label(text=stream.units)
|
||||
|
||||
col.label(text="Description:")
|
||||
area = col.box()
|
||||
@@ -253,3 +247,25 @@ class VIEW3D_PT_SpeckleActiveStream(bpy.types.Panel):
|
||||
area.separator()
|
||||
col.separator()
|
||||
col.operator("speckle.view_stream_data_api", text="Open Stream in Web")
|
||||
|
||||
|
||||
class VIEW3D_PT_SpeckleHelp(bpy.types.Panel):
|
||||
"""
|
||||
Speckle Help UI panel in the 3d viewport
|
||||
"""
|
||||
|
||||
bl_space_type = "VIEW_3D"
|
||||
bl_region_type = Region
|
||||
bl_category = "Speckle"
|
||||
bl_context = "objectmode"
|
||||
bl_label = "Help"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
col = layout.column()
|
||||
|
||||
col.operator("speckle.open_speckle_guide")
|
||||
col.separator()
|
||||
col.operator("speckle.open_speckle_tutorials")
|
||||
col.separator()
|
||||
col.operator("speckle.open_speckle_forum")
|
||||
|
||||
Executable
+4
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e -o pipefail
|
||||
|
||||
poetry export --only main -o bpy_speckle/requirements.txt
|
||||
@@ -0,0 +1,20 @@
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def patch_installer(tag: str):
|
||||
"""Patches the installer with the correct connector version and specklepy version"""
|
||||
tag = tag.replace("\n", "")
|
||||
iss_file = "speckle-sharp-ci-tools/blender.iss"
|
||||
iss_path = Path(iss_file)
|
||||
lines = iss_path.read_text().split("\n")
|
||||
lines.insert(12, f'#define AppVersion "{tag.split("-")[0]}"')
|
||||
lines.insert(13, f'#define AppInfoVersion "{tag}"')
|
||||
|
||||
iss_path.write_text("\n".join(lines))
|
||||
print(f"Patched installer with connector v{tag}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
tag = sys.argv[1]
|
||||
patch_installer(tag)
|
||||
@@ -0,0 +1,31 @@
|
||||
import re
|
||||
import sys
|
||||
|
||||
def patch_connector(tag):
|
||||
"""Patches the connector version within the connector init file"""
|
||||
bpy_file = "bpy_speckle/__init__.py"
|
||||
tag = tag.split(".")
|
||||
|
||||
with open(bpy_file, "r") as file:
|
||||
lines = file.readlines()
|
||||
|
||||
for (index, line) in enumerate(lines):
|
||||
if '"version":' in line:
|
||||
lines[index] = f' "version": ({tag[0]}, {tag[1]}, {tag[2]}),\n'
|
||||
print(f"Patched connector version number in {bpy_file}")
|
||||
break
|
||||
|
||||
with open(bpy_file, "w") as file:
|
||||
file.writelines(lines)
|
||||
|
||||
def main():
|
||||
tag = sys.argv[1]
|
||||
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.split("-")[0])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Generated
+917
-434
File diff suppressed because it is too large
Load Diff
+13
-8
@@ -2,18 +2,23 @@
|
||||
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.2.2"
|
||||
python = ">=3.8, <4.0.0"
|
||||
specklepy = "^2.14.0"
|
||||
attrs = "^23.1.0"
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
devtools = "^0.6.1"
|
||||
numpy = "^1.20.2"
|
||||
bpy = "^2.82.1"
|
||||
bpy-build = "^2.1.0"
|
||||
# [tool.poetry.group.local_specklepy.dependencies]
|
||||
# specklepy = {path = "../specklepy", develop = true}
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
numpy = "^1.23.5"
|
||||
fake-bpy-module-latest = "^20230117"
|
||||
black = "^22.10.0"
|
||||
pylint = "^2.15.7"
|
||||
ruff = "^0.0.166"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core>=1.0.0"]
|
||||
|
||||
Reference in New Issue
Block a user