Compare commits

...

64 Commits

Author SHA1 Message Date
Jedd Morgan c39298687d Merge pull request #160 from specklesystems/jrm/ismultiplayer
Added `isMultiplayer` property
2023-04-13 14:26:29 +01:00
Jedd Morgan bcdddbf930 Added isMultiplayer property 2023-04-13 14:24:17 +01:00
Jedd Morgan b5684e34f6 Merge pull request #159 from specklesystems/jrm/curve-fix
Removed merge vertices by distance from clean mesh
2023-04-05 12:51:27 +01:00
Jedd Morgan 2203fe98f8 Removed merge vertices by distance from clean mesh 2023-04-05 12:47:03 +01:00
Jedd Morgan bbfdf2863b Merge pull request #158 from specklesystems/jrm/curve-fix
Using new installer.py
2023-04-04 20:22:48 +01:00
Jedd Morgan f25f6cb16c Fixed some misc issues 2023-04-04 20:21:20 +01:00
Jedd Morgan 9e4e533ba8 Using new installer.py 2023-03-28 17:06:40 +01:00
Jedd Morgan 8db12ca9b9 Merge pull request #157 from specklesystems/jrm/curve-fix
Mesh area calc + minor cleanup
2023-03-28 16:47:59 +01:00
Jedd Morgan 366c864247 Mesh area calc + minor cleanup 2023-03-28 16:47:07 +01:00
Jedd Morgan 52136d3ef6 Merge pull request #155 from specklesystems/jrm/instances
Implemented support for new instances
2023-03-21 22:01:46 +00:00
Jedd Morgan fe764d7f0c removed old comment 2023-03-21 21:58:07 +00:00
Jedd Morgan 669dd67521 Added option to receive instances as linked duplicates 2023-03-21 21:45:53 +00:00
Jedd Morgan f74b2c37f0 Implemented support for new instances 2023-03-19 03:08:57 +00:00
Jedd Morgan ebb4e32fff Merge pull request #151 from specklesystems/jrm/curve-fix
Fixes many bugs with curves
2023-03-09 14:52:28 +00:00
Jedd Morgan 25903baf83 Set default mesh smoothing to True 2023-02-21 19:19:31 +00:00
Jedd Morgan cb6d6d7ad8 feat(converter): Added support for Ellipse and Circles 2023-02-21 19:17:14 +00:00
Jedd Morgan fd2687aa3c All non-bezier nurbs curves now work perfectly rhino->blend->rhino 2023-02-20 22:26:11 +00:00
Jedd Morgan f5c65068de feat(converter): Better curve support 2023-02-18 02:23:41 +00:00
Jedd Morgan 235b49d8c6 Merge pull request #150 from specklesystems/hotfix-authtoken
fix(auth): hotfix for serializing authTokens in blender file
2023-02-15 18:00:53 +00:00
Jedd Morgan a1ec137c67 minor cleanup 2023-02-15 16:40:49 +00:00
Jedd Morgan b95f621272 Aligned nurbs knot multiplicities with rhino curves 2023-02-15 16:37:48 +00:00
Jedd Morgan a1fcdad0e3 fix(auth): hotfix for serializing authTokens in blender file 2023-02-15 15:41:36 +00:00
Gergő Jedlicska 584e543964 makes sure to insert a path string into the sys path not a path object (#149) 2023-02-08 12:45:29 +01:00
Jedd Morgan ef20c5240c Merge pull request #148 from specklesystems/jrm/converter/fix
Custom Properties overflow fix
2023-02-05 17:20:17 +00:00
Jedd Morgan 9fe12a018a fix(ToNative): fixed issue with attaching custom properties that exceed int32 max value 2023-02-05 15:30:55 +00:00
JR-Morgan 8411c01f1b Merge branch 'main' of https://github.com/specklesystems/speckle-blender 2023-02-02 15:06:25 +00:00
JR-Morgan 63b82a30cb bumped specklepy 2.12.0 2023-02-02 15:05:59 +00:00
Jedd Morgan 0b6e39cf38 Merge pull request #147 from specklesystems/jrm/converter/multi-mesh
Fixed misalgined uv error message
2023-02-02 13:56:21 +00:00
JR-Morgan b7efcec517 uv experimentation 2023-02-02 13:45:57 +00:00
Jedd Morgan 0ab0096aac Merge pull request #142 from specklesystems/jrm/converter/multi-mesh
Jrm/converter/multi mesh
2023-02-01 14:00:11 +00:00
JR-Morgan cbd8fc99bb Merge remote-tracking branch 'origin' into jrm/converter/multi-mesh 2023-02-01 14:00:00 +00:00
JR-Morgan 2a9287e762 final polish 2023-02-01 13:52:20 +00:00
Jedd Morgan 98c70f237c Merge pull request #146 from specklesystems/gergo/alwaysInstall
fix(installer): make sure to always install specklepy dependencies
2023-02-01 13:10:22 +00:00
Gergő Jedlicska 048047cf05 fix(installer): make sure to always install specklepy dependencies
Co-authored-by: Jedd Morgan <45512892+JR-Morgan@users.noreply.github.com>
2023-01-31 10:57:05 +02:00
Jedd Morgan 6118215cae Experimentation with receive modes 2023-01-26 14:58:53 +00:00
Jedd Morgan 8e06432fe6 relock poetry 2023-01-23 23:50:42 +00:00
Jedd Morgan cb8620ff8a Merge branch 'main' into jrm/converter/multi-mesh
Signed-off-by: Jedd Morgan <45512892+JR-Morgan@users.noreply.github.com>
2023-01-23 23:49:12 +00:00
Jedd Morgan e0eddea8ab Fixed regression of null elements 2023-01-23 19:59:49 +00:00
Jedd Morgan 8497d0c195 Merge pull request #144 from specklesystems/jrm/ifc/fix
fixed IFC commits with empty lists
2023-01-20 17:00:04 +00:00
Jedd Morgan 1ce394c08f fixed issue with IFC commits with empty lists 2023-01-20 15:31:44 +00:00
Jedd Morgan aabbf87dda fix(ci): signing cert for iss 2023-01-16 17:12:39 +00:00
Jedd Morgan 7e31787d37 Updated config.yml 2023-01-16 16:29:13 +00:00
Jedd Morgan f1259587fd Bumped speckle py to 2.11.4 2023-01-16 15:00:10 +00:00
Jedd Morgan 7f913e3af0 Merge pull request #141 from specklesystems/gergo/ciContextUpdate
clone ci tools with ssh, use circleci context for env vars
2023-01-16 13:42:24 +00:00
Jedd Morgan 5433c34a4d Merge pull request #140 from specklesystems/jrm/manual-installer
Manual Installer
2023-01-16 13:39:50 +00:00
Alan Rynne 2c52e93660 ci: Updated github actions to use new actions repo 2023-01-09 20:41:22 +01:00
Gergő Jedlicska 3c3b24cf98 use innosetup context in the windows build 2023-01-06 14:01:56 +01:00
Gergő Jedlicska 3012b0ebcb clone ci tools with ssh, use circleci context for env vars 2023-01-06 12:17:19 +01:00
Jedd Morgan 35b96eaa4e feat(converter): meshes with multiple materials ToSpeckle 2022-12-19 13:42:35 +00:00
Jedd Morgan c229bb2414 materials 2022-12-15 18:04:31 +00:00
Jedd Morgan 318bd086c0 feat(converter): convert displayValues ToNative as a single mesh 2022-12-15 16:00:44 +00:00
Jedd Morgan e3eb29daa4 fiX(ci): fixed mistake in manual installer ci 2022-12-15 14:17:29 +00:00
Jedd Morgan 3359c8f275 feat(ci): Added manual instaler deploy 2022-12-14 17:01:45 +00:00
Gergő Jedlicska cfc5007d00 update tag fiter for numbered alpha and beta 2022-12-14 11:20:00 +01:00
Jedd Morgan 0a630457be Jrm/accounts reload (#138)
* add new installer mechanism

* ci(circleci): add new package connector step

* ci(circleci): install dependencies before packaging

* chore(deps): remove local specklepy dep

* chore(deps): relock again

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

* Zero restart installation

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

* ci(circleci): add new package connector step

* ci(circleci): install dependencies before packaging

* chore(deps): remove local specklepy dep

* chore(deps): relock again

* ci(circleci): rewrite full ci WIP

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

* ci(circleci): updates

* ci(circleci): fix naming

* ci(circleci): update filters

* ci(circleci): update semver location

* ci(circleci): update dependency graph

* getting the specific installer branch

* get proper ci tools

* force remove new lines from installer tags

* fix installer patch pathing

* properly saving packaged zip

* add python to dotnet installer

* add zip again

* only zip the published isntaller

* publish only usefull stuff

* ci(circleci): update details in deploy

* ci(circleci): fix filters

* ci(cirleci): fix mac  build triggers

* make sure semver is set

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

* ci(circleci): fix missing semver in files

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

* ci(circleci): fix persist to workspace pathing

* fix zipping dir

* mkdir for the zip folder

* mkdir -p

* force copy

* copy 101

* mkdir 101

* top level cp operation

* pipe the zip

* no CD

* only parameter

* build for osx 13 for arm

* moving back to a mac build machine

* fixc arm mac runtime

* remove old linux installer build

* allow alpha numbered version

* remove ci tools branch
2022-12-14 10:55:44 +01:00
Jedd Morgan 977994e141 feat(converter): initial refactor of mesh traversal 2022-12-13 19:02:43 +00:00
Jedd Morgan d9cbc80ee7 Zero restart installation 2022-12-09 15:00:44 +00:00
Jedd Morgan 45038fad79 feat(connector): Added reload operation to user account selection 2022-12-09 03:26:21 +00:00
Gergő Jedlicska cda621d735 chore(deps): relock again 2022-12-07 14:46:09 +01:00
Gergő Jedlicska 2d052d1379 chore(deps): remove local specklepy dep 2022-12-07 14:44:20 +01:00
Gergő Jedlicska 46ce2bc0df ci(circleci): install dependencies before packaging 2022-12-07 14:38:06 +01:00
Gergő Jedlicska 80e2216aa0 ci(circleci): add new package connector step 2022-12-07 14:32:48 +01:00
Gergő Jedlicska 10d69aa44b add new installer mechanism 2022-12-07 13:52:22 +01:00
22 changed files with 2105 additions and 1314 deletions
+211 -254
View File
@@ -1,101 +1,110 @@
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
commands:
install-specklepy-windows: # Reusable job for installing `specklepy` for windows machines
parameters:
python-version:
type: string
default: "" # leave blank for blender v2.93
steps:
- when:
condition: << parameters.python-version >>
steps:
- run:
name: Upgrade python version << parameters.python-version >>
shell: powershell.exe
command: |
choco upgrade python --version=<< parameters.python-version >>
refreshenv
python --version
- run:
name: Install specklepy into modules directory
shell: powershell.exe
command: |
$pyarr=(python --version).split(' ')[1].split('.')
$pyver=($pyarr[0..1] -join '.')
echo "using python version:" $pyver
$specklepy=(python patch_version.py)
python -m pip install --target=./modules-$pyver specklepy==$specklepy
win: circleci/windows@5.0.0
jobs:
build-connector-win: # Reusable job for basic connectors
executor:
name: win/default # comes with python 3.7.3
shell: cmd.exe
parameters:
slug:
type: string
default: "blender"
installer:
type: boolean
default: false
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: ./
- install-specklepy-windows
- install-specklepy-windows:
python-version: "3.9.10"
- install-specklepy-windows:
python-version: "3.10.2"
- run: &restore_semver
name: Restore Semver
command: SEMVER=$(cat ./SEMVER) && echo $SEMVER
- run:
name: Patch
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
shell: cmd.exe
steps:
- attach_workspace:
at: ./
- run:
name: Patch installer
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.0" } else { $env:CIRCLE_TAG }
$semver = if($tag.Contains('/')) {$tag.Split("/")[1] } else { $tag }
$ver = if($semver.Contains('-')) {$semver.Split("-")[0] } else { $semver }
$version = "$($ver).$($env:CIRCLE_BUILD_NUM)"
$channel = if($semver.Contains('-')) { "prerelease" } else { "latest" }
New-Item -Force "speckle-sharp-ci-tools/Installers/blender/$channel.yml" -ItemType File -Value "version: $semver"
echo $semver
python patch_version.py $semver
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
- when:
condition: << parameters.installer >>
steps:
- persist_to_workspace:
root: ./
paths:
- speckle-sharp-ci-tools/Installers
- persist_to_workspace:
root: ./
paths:
- speckle-sharp-ci-tools/Installers/blender/blender-*.exe
build-connector-mac:
build-installer-mac:
macos:
xcode: 12.5.1
parameters:
slug:
type: string
default: "blender-mac"
installername:
type: string
default: "SpeckleBlenderInstall"
runtime:
type: string
default: "osx-x64"
installer:
type: boolean
default: false
slug:
type: string
installer_path:
type: string
default: speckle-sharp-ci-tools/Mac/SpeckleBlenderInstall
steps:
- checkout
- attach_workspace:
@@ -108,90 +117,55 @@ jobs:
- run:
name: Install dotnet
command: curl -sSL https://dot.net/v1/dotnet-install.sh | bash /dev/stdin
- run:
name: Build & Patch Version
command: |
TAG=$(if [ "${CIRCLE_TAG}" ]; then echo $CIRCLE_TAG; else echo "0.0.0"; fi;)
SEMVER=$(echo "$TAG" | sed -e 's/[a-zA-Z-]*\///')
VER=$(echo "$SEMVER" | sed -e 's/-beta//')
VERSION=$(echo $VER.$CIRCLE_BUILD_NUM)
CHANNEL=$(if [[ "$VERSION" == *"-"* ]]; then echo $(cut -d "-" -f2 \<\<\< $VERSION); else echo latest; fi)
mkdir -p speckle-sharp-ci-tools/Installers/<< parameters.slug >>
if [ "${CIRCLE_TAG}" ]; then echo "version: $SEMVER" > "speckle-sharp-ci-tools/Installers/<< parameters.slug >>/$CHANNEL.yml"; fi
python3 patch_version.py $SEMVER
# update python and package dependencies
- when:
condition:
and:
# - << parameters.installer >>
- equal: [osx-x64, << parameters.runtime >>]
steps:
- run:
name: Install python 3.10
command: |
brew install python@3.10
brew link --overwrite python@3.10
- run:
name: Package specklepy dependencies for blender 3.1 & 3.2
command: |
python3 --version
python3 -m pip install --target=./modules-intel-3.10 specklepy==$(python3 -m patch_version)
zip -r modules-intel-3.10.zip modules-intel-3.10/
cp modules-intel-3.10.zip speckle-sharp-ci-tools/Mac/<<parameters.installername>>/.installationFiles
- run:
name: Zip Connector files
command: |
zip -r << parameters.slug >>.zip bpy_speckle/
- run: *restore_semver
- run:
name: Copy connector files to installer
command: |
mkdir -p speckle-sharp-ci-tools/Mac/<<parameters.installername>>/.installationFiles/
cp << parameters.slug >>.zip speckle-sharp-ci-tools/Mac/<<parameters.installername>>/.installationFiles
python3 patch_version.py > speckle-sharp-ci-tools/Mac/<<parameters.installername>>/.installationFiles/specklepy_ver.yml
mkdir -p <<parameters.installer_path >>/.installationFiles/
cp bpy_speckle.zip << parameters.installer_path >>/.installationFiles
- run:
name: Build Mac installer
command: ~/.dotnet/dotnet publish speckle-sharp-ci-tools/Mac/<<parameters.installername>>/<<parameters.installername>>.sln -r << parameters.runtime >> -c Release
command: ~/.dotnet/dotnet publish << parameters.installer_path >>/SpeckleBlenderInstall.sln -r << parameters.runtime >> -c Release
- run:
name: Zip installer
command: |
cd speckle-sharp-ci-tools/Mac/<<parameters.installername>>/bin/Release/net6.0/<< parameters.runtime >>/
zip -r << parameters.slug >>.zip ./
- store_artifacts:
path: speckle-sharp-ci-tools/Mac/<<parameters.installername>>/bin/Release/net6.0/<< parameters.runtime >>/<< parameters.slug >>.zip
- run:
name: Copy to installer location
command: |
TAG=$(if [ "${CIRCLE_TAG}" ]; then echo $CIRCLE_TAG; else echo "0.0.0"; fi;)
SEMVER=$(echo "$TAG" | sed -e 's/[a-zA-Z-]*\///')
cp speckle-sharp-ci-tools/Mac/<<parameters.installername>>/bin/Release/net6.0/<< parameters.runtime >>/<< parameters.slug >>.zip speckle-sharp-ci-tools/Installers/<< parameters.slug >>/<< parameters.slug >>-$SEMVER.zip
- when:
condition: << parameters.installer >>
steps:
- persist_to_workspace:
root: ./
paths:
- speckle-sharp-ci-tools/Installers
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/
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
- speckle-sharp-ci-tools/Installers/blender/<< parameters.slug >>*.zip
deploy-connector-new:
build-installer-manual:
docker:
- image: mcr.microsoft.com/dotnet/sdk:6.0
- image: cimg/base:2021.01
parameters:
slug:
type: string
default: bpy_speckle
steps:
- attach_workspace:
at: ./
- run: *restore_semver
- run:
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:
@@ -208,143 +182,126 @@ jobs:
- 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: |
TAG=$(if [ "${CIRCLE_TAG}" ]; then echo $CIRCLE_TAG; else echo "0.0.0"; fi;)
SEMVER=$(echo "$TAG" | sed -e 's/\/[a-zA-Z-]*//')
SEMVER=$(cat ./SEMVER)
echo $SEMVER
/root/.dotnet/tools/Speckle.Manager.Feed deploy \
-s << parameters.slug >> \
-s blender \
-v ${SEMVER} \
-u https://releases.speckle.dev/installers/<< parameters.slug >>/<< parameters.file_slug >>-${SEMVER}.<< parameters.extension >> \
-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/<< parameters.file_slug >>/<< parameters.file_slug >>-${SEMVER}.<< parameters.extension >>
-f speckle-sharp-ci-tools/Installers/blender/<< parameters.file_slug >>-${SEMVER}.<< parameters.extension >>
workflows:
build: # build the installers, but don't persist to workspace for deployment
jobs:
- 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
- get-ci-tools:
filters:
branches:
only:
- main
- /ci\/.*/
- build-connector-win:
name: Windows Build
requires:
- get-ci-tools
filters:
branches:
only:
- main
- /ci\/.*/
- build-connector-mac:
name: Mac ARM Build
slug: blender-mac-arm
runtime: osx-arm64
requires:
- get-ci-tools
- build-connector-mac:
name: Mac Intel Build
slug: blender-mac-intel
runtime: osx-x64
requires:
- get-ci-tools
deploy: # build installers and deploy
jobs:
- get-ci-tools:
filters:
tags:
only: /.*/
branches:
ignore: /.*/
filters: *build_filters
- build-connector-win:
name: Windows Build
slug: blender
installer: true
- build-installer-win:
context: innosetup
name: Windows Installer Build
requires:
- package-connector
- get-ci-tools
filters:
tags:
only: /([0-9]+)\.([0-9]+)\.([0-9]+)(?:-\w+)?$/
branches:
ignore: /.*/
filters: *build_filters
- build-connector-mac:
name: Mac ARM Build
slug: blender-mac-arm
runtime: osx-arm64
installer: true
requires:
- get-ci-tools
filters:
tags:
only: /([0-9]+)\.([0-9]+)\.([0-9]+)(?:-\w+)?$/
branches:
ignore: /.*/
- build-connector-mac:
name: Mac Intel Build
slug: blender-mac-intel
runtime: osx-x64
installer: true
requires:
- get-ci-tools
filters:
tags:
only: /([0-9]+)\.([0-9]+)\.([0-9]+)(?:-\w+)?$/
branches:
ignore: /.*/
- deploy-connector-new:
name: deploy-win
slug: blender
- deploy-connector:
context: do-spaces-speckle-releases
name: deploy-windows
file_slug: blender
os: Win
extension: exe
os: WIN
arch: Any
extension: exe
requires:
- Windows Build
- Mac ARM Build
- Manual Installer Build
- Windows Installer Build
- Mac Intel Build
filters:
tags:
only: /([0-9]+)\.([0-9]+)\.([0-9]+)(?:-\w+)?$/
- Mac ARM Build
filters: &deploy_filters
branches:
ignore: /.*/ # For testing only! /ci\/.*/
ignore: /.*/
tags:
only: /([0-9]+)\.([0-9]+)\.([0-9]+)(?:-\w+)?(?:\.[0-9]+)?/
- deploy-connector-new:
name: deploy-mac-intel
slug: blender
file_slug: blender-mac-intel
os: OSX
arch: Intel
extension: zip
- build-installer-mac:
name: Mac ARM Build
slug: blender-mac-arm
runtime: osx-arm64
requires:
- Windows Build
- Mac ARM Build
- Mac Intel Build
filters:
tags:
only: /([0-9]+)\.([0-9]+)\.([0-9]+)(?:-\w+)?$/
branches:
ignore: /.*/ # For testing only! /ci\/.*/
- deploy-connector-new:
- get-ci-tools
- build-connector-zip
filters: *build_filters
- deploy-connector:
context: do-spaces-speckle-releases
name: deploy-mac-arm
slug: blender
file_slug: blender-mac-arm
os: OSX
arch: Arm
extension: zip
requires:
- Windows Build
- Mac ARM Build
- Manual Installer Build
- Windows Installer Build
- Mac Intel Build
filters:
tags:
only: /([0-9]+)\.([0-9]+)\.([0-9]+)(?:-\w+)?$/
branches:
ignore: /.*/ # For testing only! /ci\/.*/
- 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-zip
filters: *build_filters
- deploy-connector:
context: do-spaces-speckle-releases
name: deploy-mac-intel
file_slug: blender-mac-intel
os: OSX
arch: Intel
extension: zip
requires:
- Manual Installer Build
- Windows Installer Build
- Mac Intel Build
- Mac ARM Build
filters: *deploy_filters
- build-installer-manual:
name: Manual Installer Build
requires:
- get-ci-tools
- build-connector-zip
filters: *build_filters
- deploy-connector:
context: do-spaces-speckle-releases
name: deploy-manual
file_slug: bpy_speckle
os: Any
arch: Any
extension: zip
requires:
- Manual Installer Build
- Windows Installer Build
- Mac Intel Build
- Mac ARM Build
filters: *deploy_filters
+4 -70
View File
@@ -6,73 +6,7 @@ on:
jobs:
update_issue:
runs-on: ubuntu-latest
steps:
- name: Get project data
env:
GITHUB_TOKEN: ${{secrets.GHPROJECT_TOKEN}}
ORGANIZATION: specklesystems
PROJECT_NUMBER: 9
run: |
gh api graphql --header 'GraphQL-Features: projects_next_graphql' -f query='
query($org: String!, $number: Int!) {
organization(login: $org){
projectNext(number: $number) {
id
fields(first:20) {
nodes {
id
name
settings
}
}
}
}
}' -f org=$ORGANIZATION -F number=$PROJECT_NUMBER > project_data.json
echo 'PROJECT_ID='$(jq '.data.organization.projectNext.id' project_data.json) >> $GITHUB_ENV
echo 'STATUS_FIELD_ID='$(jq '.data.organization.projectNext.fields.nodes[] | select(.name== "Status") | .id' project_data.json) >> $GITHUB_ENV
echo "$PROJECT_ID"
echo "$STATUS_FIELD_ID"
echo 'DONE_ID='$(jq '.data.organization.projectNext.fields.nodes[] | select(.name== "Status") | .settings | fromjson | .options[] | select(.name== "Done") | .id' project_data.json) >> $GITHUB_ENV
echo "$DONE_ID"
- name: Add Issue to project #it's already in the project, but we do this to get its node id!
env:
GITHUB_TOKEN: ${{secrets.GHPROJECT_TOKEN}}
ISSUE_ID: ${{ github.event.issue.node_id }}
run: |
item_id="$( gh api graphql --header 'GraphQL-Features: projects_next_graphql' -f query='
mutation($project:ID!, $id:ID!) {
addProjectNextItem(input: {projectId: $project, contentId: $id}) {
projectNextItem {
id
}
}
}' -f project=$PROJECT_ID -f id=$ISSUE_ID --jq '.data.addProjectNextItem.projectNextItem.id')"
echo 'ITEM_ID='$item_id >> $GITHUB_ENV
- name: Update Status
env:
GITHUB_TOKEN: ${{secrets.GHPROJECT_TOKEN}}
ISSUE_ID: ${{ github.event.issue.node_id }}
run: |
gh api graphql --header 'GraphQL-Features: projects_next_graphql' -f query='
mutation($project:ID!, $status:ID!, $id:ID!, $value:String!) {
set_status: updateProjectNextItemField(
input: {
projectId: $project
itemId: $id
fieldId: $status
value: $value
}
) {
projectNextItem {
id
}
}
}' -f project=$PROJECT_ID -f status=$STATUS_FIELD_ID -f id=$ITEM_ID -f value=${{ env.DONE_ID }}
uses: specklesystems/github-actions/.github/workflows/project-add-issue.yml@main
secrets: inherit
with:
issue-id: ${{ github.event.issue.node_id }}
+4 -42
View File
@@ -6,45 +6,7 @@ on:
jobs:
track_issue:
runs-on: ubuntu-latest
steps:
- name: Get project data
env:
GITHUB_TOKEN: ${{secrets.GHPROJECT_TOKEN}}
ORGANIZATION: specklesystems
PROJECT_NUMBER: 9
run: |
gh api graphql --header 'GraphQL-Features: projects_next_graphql' -f query='
query($org: String!, $number: Int!) {
organization(login: $org){
projectNext(number: $number) {
id
fields(first:20) {
nodes {
id
name
settings
}
}
}
}
}' -f org=$ORGANIZATION -F number=$PROJECT_NUMBER > project_data.json
echo 'PROJECT_ID='$(jq '.data.organization.projectNext.id' project_data.json) >> $GITHUB_ENV
echo 'STATUS_FIELD_ID='$(jq '.data.organization.projectNext.fields.nodes[] | select(.name== "Status") | .id' project_data.json) >> $GITHUB_ENV
- name: Add Issue to project
env:
GITHUB_TOKEN: ${{secrets.GHPROJECT_TOKEN}}
ISSUE_ID: ${{ github.event.issue.node_id }}
run: |
item_id="$( gh api graphql --header 'GraphQL-Features: projects_next_graphql' -f query='
mutation($project:ID!, $id:ID!) {
addProjectNextItem(input: {projectId: $project, contentId: $id}) {
projectNextItem {
id
}
}
}' -f project=$PROJECT_ID -f id=$ISSUE_ID --jq '.data.addProjectNextItem.projectNextItem.id')"
echo 'ITEM_ID='$item_id >> $GITHUB_ENV
uses: specklesystems/github-actions/.github/workflows/project-add-issue.yml@main
secrets: inherit
with:
issue-id: ${{ github.event.issue.node_id }}
+2 -1
View File
@@ -12,4 +12,5 @@ __pycache__/
Installers/
modules/
.tool-versions
requirements.txt
requirements.txt
SEMVER
+18 -11
View File
@@ -1,4 +1,15 @@
import bpy
from bpy_speckle.installer import ensure_dependencies
ensure_dependencies(f"Blender {bpy.app.version[0]}.{bpy.app.version[1]}")
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",
@@ -18,15 +29,6 @@ bl_info = {
Import SpeckleBlender classes
"""
from specklepy.api.client import SpeckleClient # , SpeckleCache
from specklepy.logging import metrics
from bpy_speckle.ui import *
from bpy_speckle.properties import *
from bpy_speckle.operators import *
from bpy_speckle.callbacks import *
from bpy.app.handlers import persistent
"""
Add load handler to initialize Speckle when
loading a Blender file
@@ -34,8 +36,13 @@ 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
+477 -208
View File
@@ -1,13 +1,23 @@
import math
from typing import Union
from typing import Tuple, Union, Collection
from bpy_speckle.functions import get_scale_length, _report
import mathutils
import bpy, bmesh, bpy_types
from specklepy.objects.other import *
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 (
add_blender_material,
get_render_material,
link_object_to_collection_nested,
render_material_to_native,
add_custom_properties,
add_vertices,
add_faces,
@@ -15,147 +25,221 @@ from .util import (
add_uv_coords,
)
SUPPORTED_CURVES = (Line, Polyline, Curve, Arc, Polycurve)
SUPPORTED_CURVES = (Line, Polyline, Curve, Arc, Polycurve, Ellipse, Circle)
CAN_CONVERT_TO_NATIVE = (
Mesh,
Brep,
*SUPPORTED_CURVES,
Transform,
BlockDefinition,
BlockInstance,
Instance,
)
def _has_native_convesion(speckle_object: Base) -> bool:
return any(isinstance(speckle_object, t) for t in CAN_CONVERT_TO_NATIVE)
def _has_fallback_conversion(speckle_object: Base) -> bool:
return any(getattr(speckle_object, alias, None) for alias in DISPLAY_VALUE_PROPERTY_ALIASES)
def can_convert_to_native(speckle_object: Base) -> bool:
if type(speckle_object) in CAN_CONVERT_TO_NATIVE:
return True
if getattr(
speckle_object, "displayValue", getattr(speckle_object, "displayMesh", None)
):
if(_has_native_convesion(speckle_object) or _has_fallback_conversion(speckle_object)):
return True
_report(f"Could not convert unsupported Speckle object: {speckle_object}")
return False
def create_new_object(obj_data: Optional[bpy.types.ID], desired_name: str, counter: int = 0) -> bpy.types.Object:
"""
Creates a new blender object with a unique name,
if the desired_name is already taken
we'll append a number, with the format .xxx to the desired_name to ensure the name is unique.
"""
name = desired_name if counter == 0 else f"{desired_name[:OBJECT_NAME_MAX_LENGTH - 4]}.{counter:03d}" # format counter as name.xxx, truncate to ensure we don't exceed the object name max length
#TODO: This is very slow, and gets slower the more objects you receive with the same name...
# We could use a binary/galloping search, and/or cache the name -> index within a receive.
if name in bpy.data.objects.keys():
#Object already exists, increment counter and try again!
return create_new_object(obj_data, desired_name, counter + 1)
blender_object = bpy.data.objects.new(name, obj_data)
return blender_object
convert_instances_as: str #HACK: This is hacky, we need a better way to pass settings down to the converter
def set_convert_instances_as(value: str):
global convert_instances_as
convert_instances_as = value
def convert_to_native(speckle_object: Base) -> list[Object]:
def convert_to_native(speckle_object: Base, name: Optional[str] = None) -> Optional[Union[list, Object]]:
speckle_type = type(speckle_object)
speckle_name = (
name
or getattr(speckle_object, "name", None)
or f"{speckle_object.speckle_type} -- {speckle_object.id}"
)
# convert unsupported types with display values
if speckle_type not in CAN_CONVERT_TO_NATIVE:
elements = getattr(speckle_object, "elements", []) or []
display = getattr(
speckle_object, "displayValue", getattr(speckle_object, "displayMesh", None)
)
if not elements and not display:
_report(f"Could not convert unsupported Speckle object: {speckle_object}")
return None
if isinstance(display, list):
elements.extend(display)
else:
elements.append(display)
# TODO: depreciate the parent type
# add parent type here so we can use it as a blender custom prop
# not making it hidden, so it will get added on send as i think it might be helpful? can reconsider
converted = []
for item in elements:
if not isinstance(item, Base):
continue
item.parent_speckle_type = speckle_object.speckle_type
blender_object = convert_to_native(item)
if isinstance(blender_object, list):
converted.extend(blender_object)
else:
add_custom_properties(speckle_object, blender_object)
converted.append(blender_object)
return converted
try:
# convert breps
if speckle_type is Brep:
meshes = getattr(
speckle_object, "displayValue", getattr(speckle_object, "displayMesh", iter([]))
)
if material := getattr(speckle_object, "renderMaterial", getattr(speckle_object, "@renderMaterial", None),):
for mesh in meshes:
mesh["renderMaterial"] = material
object_name = _generate_object_name(speckle_object)
scale = get_scale_factor(speckle_object)
return [convert_to_native(mesh) for mesh in meshes]
obj_data: Optional[Union[bpy.types.ID, bpy.types.Object]] = None
converted: list[Object] = []
# convert elements/breps
if not _has_native_convesion(speckle_object):
(obj_data, converted) = element_to_native(speckle_object, object_name, scale)
if not obj_data and not converted:
_report(f"Unsupported type {speckle_object.speckle_type}")
scale = 1.0
if units := getattr(speckle_object, "units", None):
scale = get_scale_length(units) / bpy.context.scene.unit_settings.scale_length
# convert supported geometry
if isinstance(speckle_object, Mesh):
obj_data = mesh_to_native(speckle_object, name=speckle_name, scale=scale)
elif isinstance(speckle_object, Mesh):
obj_data = mesh_to_native(speckle_object, object_name, scale)
elif speckle_type in SUPPORTED_CURVES:
obj_data = icurve_to_native(speckle_object, name=speckle_name, scale=scale)
elif isinstance(speckle_object, Transform):
obj_data = transform_to_native(speckle_object, scale=scale)
elif isinstance(speckle_object, BlockDefinition):
obj_data = block_def_to_native(speckle_object, scale=scale)
elif isinstance(speckle_object, BlockInstance): # speckle_type is BlockInstance:
obj_data = block_instance_to_native(speckle_object, scale=scale)
obj_data = icurve_to_native(speckle_object, object_name, scale)
elif isinstance(speckle_object, Instance):
if convert_instances_as == "linked_duplicates":
(obj_data, converted) = instance_to_native_object(speckle_object, scale)
elif convert_instances_as != "collection_instance":
obj_data = instance_to_native_collection_instance(speckle_object, scale)
else:
_report(f"convert_instances_as = '{convert_instances_as}' is not implemented, Instances will be converted as collection instances!")
obj_data = instance_to_native_collection_instance(speckle_object, scale)
else:
_report(f"Unsupported type {speckle_type}")
return None
return []
except Exception as ex: # conversion error
_report(f"Error converting {speckle_object} \n{ex}")
return None
return []
if speckle_name in bpy.data.objects.keys():
blender_object = bpy.data.objects[speckle_name]
blender_object.data = (
obj_data.data if isinstance(obj_data, bpy_types.Object) else obj_data
)
blender_object.matrix_world = (
blender_object.matrix_world
if speckle_type is BlockInstance
else mathutils.Matrix()
)
if hasattr(obj_data, "materials"):
blender_object.data.materials.clear()
else:
blender_object = (
obj_data
if isinstance(obj_data, bpy_types.Object)
else bpy.data.objects.new(speckle_name, obj_data)
)
blender_object = obj_data if isinstance(obj_data, Object) else create_new_object(obj_data, object_name)
blender_object.speckle.object_id = str(speckle_object.id)
blender_object.speckle.enabled = True
add_custom_properties(speckle_object, blender_object)
add_blender_material(speckle_object, blender_object)
return blender_object
for child in converted:
child.parent = blender_object
converted.append(blender_object)
_report(f"Successfully converted {object_name} as {blender_object.type}")
return converted
def mesh_to_native(speckle_mesh: Mesh, name: str, scale=1.0) -> bpy.types.Mesh:
DISPLAY_VALUE_PROPERTY_ALIASES = ["displayValue", "@displayValue", "displayMesh", "@displayMesh", "elements", "@elements"]
def element_to_native(speckle_object: Base, name: str, scale: float, combineMeshes: bool = True) -> tuple[Optional[bpy.types.Mesh], list[bpy.types.Object]]:
"""
Converts a given speckle_object by converting displayValue properties (elements treated the same as displayValues)
if combineMeshes == True
Converts mesh displayValues as one mesh
Converts non-mesh displayValues as child Objects
if combineMeshes == False
Converts all displayValues as child objects (first item of the returned tuple will be None)
"""
meshes: list[Mesh] = []
elements: list[Base] = []
#NOTE: raw Mesh elements will be treated like displayValues, which is not ideal, but no connector sends raw Mesh elements so it's fine
for alias in DISPLAY_VALUE_PROPERTY_ALIASES:
display = getattr(speckle_object, alias, None)
count = 0
MAX_DEPTH = 255 # some large value, to prevent infinite reccursion
def seperate(value: Any) -> bool:
nonlocal meshes, elements, count, MAX_DEPTH
if combineMeshes and isinstance(value, Mesh):
meshes.append(value)
elif isinstance(value, Base):
elements.append(value)
elif isinstance(value, list):
count += 1
if(count > MAX_DEPTH):
return True
for x in value:
seperate(x)
return False
did_halt = seperate(display)
if did_halt:
_report(f"Traversal of {speckle_object.speckle_type} {speckle_object.id} halted after traversal depth exceeds MAX_DEPTH={MAX_DEPTH}. Are there circular references object structure?")
converted: list[Object] = []
mesh = None
if meshes:
mesh = meshes_to_native(speckle_object, meshes, name, scale)
for item in elements:
# add parent type here so we can use it as a blender custom prop
# not making it hidden, so it will get added on send as i think it might be helpful? can reconsider
item.parent_speckle_type = speckle_object.speckle_type #TODO: consider if this is still useful, as we now properly structure object parenting
blender_object = convert_to_native(item)
if isinstance(blender_object, list):
converted.extend(blender_object)
else:
add_custom_properties(speckle_object, blender_object)
converted.append(blender_object)
return (mesh, converted)
def mesh_to_native(speckle_mesh: Mesh, name: str, scale: float) -> bpy.types.Mesh:
return meshes_to_native(speckle_mesh, [speckle_mesh], name, scale)
def meshes_to_native(element: Base, meshes: Collection[Mesh], name: str, scale: float) -> bpy.types.Mesh:
if name in bpy.data.meshes.keys():
blender_mesh = bpy.data.meshes[name]
else:
blender_mesh = bpy.data.meshes.new(name=name)
return bpy.data.meshes[name]
blender_mesh = bpy.data.meshes.new(name=name)
fallback_material = get_render_material(element)
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)
# First pass, add vertex data
for mesh in meshes:
scale = get_scale_factor(mesh, scale)
add_vertices(mesh, bm, scale)
bm.verts.ensure_lookup_table()
# Second pass, add face data
offset = 0
for i, mesh in enumerate(meshes):
add_faces(mesh, bm, offset, i)
render_material = get_render_material(mesh) or fallback_material
if render_material is not None:
native_material = render_material_to_native(render_material)
blender_mesh.materials.append(native_material)
offset += len(mesh.vertices) // 3
bm.faces.ensure_lookup_table()
bm.verts.index_update()
# Third pass, add vertex instance data
for mesh in meshes:
add_colors(mesh, bm)
add_uv_coords(mesh, bm)
bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
bm.to_mesh(blender_mesh)
bm.free()
bm.free()
return blender_mesh
def line_to_native(speckle_curve: Line, blender_curve: bpy.types.Curve, scale: float) -> Optional[bpy.types.Spline]:
"""
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)
@@ -166,95 +250,90 @@ def line_to_native(speckle_curve: Line, blender_curve: bpy.types.Curve, scale: f
1,
)
if speckle_curve.end:
line.points[1].co = (
float(speckle_curve.end.x) * scale,
float(speckle_curve.end.y) * scale,
float(speckle_curve.end.z) * scale,
1,
)
line.points[1].co = (
float(speckle_curve.end.x) * scale,
float(speckle_curve.end.y) * scale,
float(speckle_curve.end.z) * scale,
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 line
return [polyline]
def polyline_to_native(scurve: Polyline, bcurve: bpy.types.Curve, scale: float) -> Optional[bpy.types.Spline]:
if value := scurve.value:
N = len(value) // 3
def nurbs_to_native(scurve: Curve, bcurve: bpy.types.Curve, scale: float) -> list[bpy.types.Spline]:
if not (points := scurve.points): return []
polyline = bcurve.splines.new("POLY")
# 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
if hasattr(scurve, "closed"):
polyline.use_cyclic_u = scurve.closed
nurbs.order_u = scurve.degree + 1
# if "closed" in scurve.keys():
# polyline.use_cyclic_u = scurve["closed"]
polyline.points.add(N - 1)
for i in range(N):
polyline.points[i].co = (
float(value[i * 3]) * scale,
float(value[i * 3 + 1]) * scale,
float(value[i * 3 + 2]) * scale,
1,
)
return polyline
def nurbs_to_native(scurve: Curve, bcurve: bpy.types.Curve, scale: float) -> Optional[bpy.types.Spline]:
if points := scurve.points:
N = len(points) // 3
nurbs = bcurve.splines.new("NURBS")
if hasattr(scurve, "closed"):
nurbs.use_cyclic_u = scurve.closed != 0
nurbs.points.add(N - 1)
for i in range(N):
nurbs.points[i].co = (
float(points[i * 3]) * scale,
float(points[i * 3 + 1]) * scale,
float(points[i * 3 + 2]) * scale,
1,
)
if len(scurve.weights) == len(nurbs.points):
for i, w in enumerate(scurve.weights):
nurbs.points[i].weight = w
# TODO: anaylize curve knots to decide if use_endpoint_u or use_bezier_u should be enabled
# nurbs.use_endpoint_u = True
nurbs.order_u = scurve.degree + 1
return nurbs
return [nurbs]
def arc_to_native(rcurve: Arc, bcurve: bpy.types.Curve, scale: float) -> Optional[bpy.types.Spline]:
# TODO: improve Blender representation of arc
# TODO: improve Blender representation of arc - check autocad test stream
plane = rcurve.plane
if not plane:
return None
normal = mathutils.Vector([plane.normal.x, plane.normal.y, plane.normal.z])
normal = MVector([plane.normal.x, plane.normal.y, plane.normal.z])
radius = rcurve.radius * scale
startAngle = rcurve.startAngle
endAngle = rcurve.endAngle
startQuat = mathutils.Quaternion(normal, startAngle)
endQuat = mathutils.Quaternion(normal, endAngle)
startQuat = MQuaternion(normal, startAngle)
endQuat = MQuaternion(normal, endAngle)
# Get start and end vectors, centre point, angles, etc.
r1 = mathutils.Vector([plane.xdir.x, plane.xdir.y, plane.xdir.z])
r1 = MVector([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 = MVector([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
c = MVector([plane.origin.x, plane.origin.y, plane.origin.z]) * scale
spt = c + r1 * radius
ept = c + r2 * radius
@@ -270,7 +349,7 @@ def arc_to_native(rcurve: Arc, bcurve: bpy.types.Curve, scale: float) -> Optiona
Ndiv = max(int(math.floor(angle / 0.3)), 2)
step = angle / float(Ndiv)
stepQuat = mathutils.Quaternion(normal, step)
stepQuat = MQuaternion(normal, step)
tan = math.tan(step / 2) * radius
arc.points.add(Ndiv + 1)
@@ -293,7 +372,7 @@ def arc_to_native(rcurve: Arc, bcurve: bpy.types.Curve, scale: float) -> Optiona
return arc
def polycurve_to_native(scurve: Polycurve, bcurve: bpy.types.Curve, scale: float):
def polycurve_to_native(scurve: Polycurve, bcurve: bpy.types.Curve, scale: float) -> list[bpy.types.Spline]:
"""
Convert Polycurve object
"""
@@ -305,48 +384,110 @@ def polycurve_to_native(scurve: Polycurve, bcurve: bpy.types.Curve, scale: float
speckle_type = type(seg)
if speckle_type in SUPPORTED_CURVES:
curves.append(icurve_to_native_spline(seg, bcurve, scale=scale))
curves.append(icurve_to_native_spline(seg, bcurve, scale))
else:
_report(f"Unsupported curve type: {speckle_type}")
return curves
def circle_to_native(circle: Circle, bcurve: bpy.types.Curve, units_scale: float) -> list[bpy.types.Spline]:
#HACK: violates typing, but it works...
circle["firstRadius"] = circle.radius
circle["secondRadius"] = circle.radius
return ellipse_to_native(circle, bcurve, units_scale)
def ellipse_to_native(ellipse: Ellipse, bcurve: bpy.types.Curve, units_scale: float) -> list[bpy.types.Spline]:
plane = ellipse.plane
def icurve_to_native_spline(speckle_curve: Base, blender_curve: bpy.types.Curve, scale=1.0):
curve_type = type(speckle_curve)
if curve_type is Line:
return line_to_native(speckle_curve, blender_curve, scale)
if curve_type is Polyline:
return polyline_to_native(speckle_curve, blender_curve, scale)
if curve_type is Curve:
return nurbs_to_native(speckle_curve, blender_curve, scale)
if curve_type is Polycurve:
radX = ellipse.firstRadius * units_scale
radY = ellipse.secondRadius * units_scale
D = 0.5522847498307936 # (4/3)*tan(pi/8)
right_handles = [
(+radX, +radY * D, 0.0),
(-radX * D, +radY, 0.0),
(-radX, -radY * D, 0.0),
(+radX * D, -radY, 0.0),
]
left_handles = [
(+radX, -radY * D, 0.0),
(+radX * D, +radY, 0.0),
(-radX, +radY * D, 0.0),
(-radX * D, -radY, 0.0),
]
points = [
(+radX, 0.0, 0.0),
(0.0, +radY, 0.0),
(-radX, 0.0, 0.0),
(0.0, -radY, 0.0),
]
transform = plane_to_native_transform(plane, units_scale)
spline = bcurve.splines.new("BEZIER")
spline.bezier_points.add(len(points) - 1)
for i in range(len(points)):
spline.bezier_points[i].co = transform @ MVector(points[i])
spline.bezier_points[i].handle_left = transform @ MVector(left_handles[i])
spline.bezier_points[i].handle_right = transform @ MVector(right_handles[i])
spline.use_cyclic_u = True
#TODO support trims?
return [spline]
def icurve_to_native_spline(speckle_curve: Base, blender_curve: bpy.types.Curve, scale: float) -> list[bpy.types.Spline]:
# polycurves
if isinstance(speckle_curve, Polycurve):
return polycurve_to_native(speckle_curve, blender_curve, scale)
if curve_type is Arc:
return arc_to_native(speckle_curve, blender_curve, scale)
# single curves
if isinstance(speckle_curve, Line):
spline = line_to_native(speckle_curve, blender_curve, scale)
elif isinstance(speckle_curve, Curve):
spline = nurbs_to_native(speckle_curve, blender_curve, scale)
elif isinstance(speckle_curve, Polyline):
spline = polyline_to_native(speckle_curve, blender_curve, scale)
elif isinstance(speckle_curve, Arc):
spline = arc_to_native(speckle_curve, blender_curve, scale)
elif isinstance(speckle_curve, Ellipse):
spline = ellipse_to_native(speckle_curve, blender_curve, scale)
elif isinstance(speckle_curve, Circle):
spline = circle_to_native(speckle_curve, blender_curve, scale)
else:
raise TypeError(f"{speckle_curve} is not a supported curve type. Supported types: {SUPPORTED_CURVES}")
return [spline] if spline is not None else []
def icurve_to_native(speckle_curve: Base, name=None, scale=1.0) -> Optional[Curve]:
def icurve_to_native(speckle_curve: Base, name: str, scale: float) -> Optional[bpy.types.Curve]:
curve_type = type(speckle_curve)
if curve_type not in SUPPORTED_CURVES:
_report(f"Unsupported curve type: {curve_type}")
return None
name = name or f"{curve_type} -- {speckle_curve.id}"
blender_curve = (
bpy.data.curves[name]
if name in bpy.data.curves.keys()
else bpy.data.curves.new(name, type="CURVE")
)
blender_curve.dimensions = "3D"
blender_curve.resolution_u = 12
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
def transform_to_native(transform: Transform, scale=1.0) -> mathutils.Matrix:
mat = mathutils.Matrix(
"""
Transforms and Intances
"""
def transform_to_native(transform: Transform, scale: float) -> MMatrix:
mat = MMatrix(
[
transform.value[:4],
transform.value[4:8],
@@ -359,38 +500,166 @@ def transform_to_native(transform: Transform, scale=1.0) -> mathutils.Matrix:
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)
def block_def_to_native(definition: BlockDefinition, scale=1.0) -> bpy.types.Collection:
native_def = bpy.data.collections.get(definition.name)
return MMatrix((
(plane.xdir.x, plane.ydir.x, plane.normal.x, tx),
(plane.xdir.y, plane.ydir.y, plane.normal.y, ty),
(plane.xdir.z, plane.ydir.z, plane.normal.z, tz),
(0, 0, 0, 1 )
))
"""
Instances / Blocks
"""
def _get_instance_name(instance: Instance) -> str:
name_prefix = _get_friendly_object_name(instance) or _get_friendly_object_name(instance.definition) or _simplified_speckle_type(instance.speckle_type)
return f"{name_prefix}{OBJECT_NAME_SEPERATOR}{instance.id}"
def instance_to_native_object(instance: Instance, scale: float) -> Tuple[bpy.types.Object, List[bpy.types.Object]]:
"""
Converts Instance to a unique object with (potentially) shared data (linked duplicate)
"""
if not instance.definition: raise Exception(f"Instance is missing a definition")
if not instance.transform: raise Exception(f"Instance is missing a transform")
name = _get_instance_name(instance)
definition = instance.definition
native_instance: Object
native_elements: List[Object] = []
elements_on_instance: List[Object] = []
if isinstance(definition, BlockDefinition): #NOTE: We have to handle BlockDefinitions specially here, since they don't follow normal traversal rules
native_instance = create_new_object(None, name) #Instance will be empty
native_instance.empty_display_size = 0
for geo in definition.geometry:
native_elements.append(convert_to_native(geo)[-1])
else:
native_instance = convert_to_native(instance.definition)[-1] # Convert assuming that definition is convertable
instance_transform = transform_to_native(instance.transform, scale)
instance_transform_inverted = instance_transform.inverted()
native_instance.matrix_world = instance_transform
(_, elements_on_instance) = element_to_native(instance, name, scale)
for c in elements_on_instance:
c.matrix_world = instance_transform_inverted @ c.matrix_world #Undo the instance transform on elements
native_elements.extend(elements_on_instance)
return (native_instance, native_elements) #TODO: need to double check that all child objects have custom props attached correctly
def instance_to_native_collection_instance(instance: Instance, scale: float) -> bpy.types.Object:
"""
Convert an Instance as a transformed Object with the `instance_collection` property
set to be the `instance.Definition` converted as a collection
The definition collection won't be linked to the current scene
Any Elements on the instance object will also be converted (and spacially transformed)
"""
if not instance.definition: raise Exception(f"Instance is missing a definition")
if not instance.transform: raise Exception(f"Instance is missing a transform")
name = _get_instance_name(instance)
# Get/Convert definition collection
collection_def = _instance_definition_to_native(instance.definition)
# Convert elements as children of collection instance object
(_, elements) = element_to_native(instance, name, scale, False)
instance_transform = transform_to_native(instance.transform, scale)
instance_transform_inverted = instance_transform.inverted()
native_instance = bpy.data.objects.new(name, None)
#add_custom_properties(instance, native_instance)
# hide the instance axes so they don't clutter the viewport
native_instance.empty_display_size = 0
native_instance.instance_collection = collection_def
native_instance.instance_type = "COLLECTION"
native_instance.matrix_world =instance_transform
for c in elements:
c.matrix_world = instance_transform_inverted @ c.matrix_world #Undo the instance transform on elements
c.parent = native_instance #TODO: need to double check that all child objects have custom props attached correctly
return native_instance
def _instance_definition_to_native(definition: Union[Base, BlockDefinition]) -> bpy.types.Collection:
"""
Converts a geometry carrying Base as a collection (does not link it to the scene)
"""
name = _generate_object_name(definition)
native_def = bpy.data.collections.get(name)
if native_def:
return native_def
native_def = bpy.data.collections.new(definition.name)
native_def = bpy.data.collections.new(name)
native_def["applicationId"] = definition.applicationId
for geo in definition.geometry:
if b_obj := convert_to_native(geo):
native_def.objects.link(
b_obj
if isinstance(b_obj, bpy_types.Object)
else bpy.data.objects.new(b_obj.name, b_obj)
)
#TODO could maybe replace BlockDefinition awareness with a single traverse member call
geometry = definition.geometry if isinstance(definition, BlockDefinition) else [definition]
for geo in geometry:
if not geo: continue
converted = convert_to_native(geo)[-1] #NOTE: we assume the last item is the root converted item
link_object_to_collection_nested(converted, native_def)
return native_def
def block_instance_to_native(instance: BlockInstance, scale=1.0) -> bpy.types.Object:
"""
Convert BlockInstance to native
"""
name = f"{getattr(instance, 'name', None) or instance.blockDefinition.name} -- {instance.id}"
native_def = block_def_to_native(instance.blockDefinition, scale)
"""
Object Naming
"""
native_instance = bpy.data.objects.new(name, None)
add_custom_properties(instance, native_instance)
native_instance["name"] = getattr(instance, 'name', None) or instance.blockDefinition.name
# hide the instance axes so they don't clutter the viewport
native_instance.empty_display_size = 0
native_instance.instance_collection = native_def
native_instance.instance_type = "COLLECTION"
native_instance.matrix_world = transform_to_native(instance.transform, scale)
return native_instance
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
+189 -84
View File
@@ -1,12 +1,21 @@
from typing import Optional
from typing import Dict, Iterable, Optional, Tuple
import bpy
from bpy.types import Depsgraph, MeshVertColor, MeshVertex, Object
from specklepy.objects.geometry import Mesh, Curve, Interval, Box, Point, Polyline
from bpy.types import Depsgraph, MeshPolygon, Object
from deprecated import deprecated
from mathutils.geometry import interpolate_bezier
from mathutils import (
Matrix as MMatrix,
Vector as MVector,
)
from specklepy.objects.geometry import (
Mesh, Curve, Interval, Box, Point, Polyline
)
from specklepy.objects.other import *
from bpy_speckle.functions import _report
from bpy_speckle.convert.util import (
get_blender_custom_properties,
make_knots,
nurb_make_curve,
to_argb_int,
)
@@ -15,17 +24,20 @@ UNITS = "m"
CAN_CONVERT_TO_SPECKLE = ("MESH", "CURVE", "EMPTY")
def convert_to_speckle(blender_object: Object, scale: float, units: str, desgraph: Optional[Depsgraph]) -> Optional[list]:
def convert_to_speckle(raw_blender_object: Object, scale: float, units: str, depsgraph: Optional[Depsgraph]) -> Optional[list]:
global UNITS
UNITS = units
blender_type = blender_object.type
blender_type = raw_blender_object.type
if blender_type not in CAN_CONVERT_TO_SPECKLE:
return None
speckle_objects = []
speckle_material = material_to_speckle(blender_object)
if desgraph:
blender_object = blender_object.evaluated_get(desgraph)
blender_object: Object = (
raw_blender_object.evaluated_get(depsgraph)
if depsgraph
else raw_blender_object
)
converted = None
if blender_type == "MESH":
converted = mesh_to_speckle(blender_object, blender_object.data, scale)
@@ -36,75 +48,93 @@ def convert_to_speckle(blender_object: Object, scale: float, units: str, desgrap
if not converted:
return None
speckle_objects = []
if isinstance(converted, list):
speckle_objects.extend([c for c in converted if c != None])
else:
speckle_objects.append(converted)
for so in speckle_objects:
so.properties = get_blender_custom_properties(blender_object)
so.applicationId = so.properties.pop("applicationId", None)
if speckle_material:
so["renderMaterial"] = speckle_material
for so in speckle_objects:
so["properties"] = get_blender_custom_properties(raw_blender_object) #NOTE: Depsgraph copies don't have custom properties so we use the raw version
so["applicationId"] = so.properties.pop("applicationId", None)
# Set object transform
if blender_type != "EMPTY":
so.properties["transform"] = transform_to_speckle(
if blender_type != "EMPTY": #TODO: this could be deprecated once we add proper instancing support
so["properties"]["transform"] = transform_to_speckle(
blender_object.matrix_world
)
return speckle_objects
def mesh_to_speckle(blender_object: Object, data: bpy.types.Mesh, scale: float = 1.0) -> List[Mesh]:
#if data.loop_triangles is None or len(data.loop_triangles) < 1:
# data.calc_loop_triangles()
def mesh_to_speckle(blender_object: Object, data: bpy.types.Mesh, scale=1.0) -> List[Mesh]:
if data.loop_triangles is None or len(data.loop_triangles) < 1:
data.calc_loop_triangles()
# Categorise polygons by material index
submesh_data: Dict[int, List[MeshPolygon]] = {}
for p in data.polygons:
if p.material_index not in submesh_data:
submesh_data[p.material_index] = []
submesh_data[p.material_index].append(p)
transform = blender_object.matrix_world
scaled_vertices = [tuple(transform @ x.co * scale) for x in data.vertices]
# Create Speckle meshes for each material
submeshes = []
index_counter = 0
for i in submesh_data:
index_mapping: Dict[int, int] = {}
#Loop through each polygon, and map indicies to their new index in m_verts
mesh_area = 0
m_verts: List[float] = []
m_faces: List[int] = []
m_texcoords: List[float] = []
for face in submesh_data[i]:
u_indices = face.vertices
m_faces.append(len(u_indices))
mesh_area += face.area
for u_index in u_indices:
if u_index not in index_mapping:
# Create mapping between index in blender mesh, and new index in speckle submesh
index_mapping[u_index] = len(m_verts) // 3
vert = scaled_vertices[u_index]
m_verts.append(vert[0])
m_verts.append(vert[1])
m_verts.append(vert[2])
if data.uv_layers.active:
vt = data.uv_layers.active.data[index_counter]
m_texcoords.extend([vt.uv.x, vt.uv.y])
m_faces.append(index_mapping[u_index])
index_counter += 1
speckle_mesh = Mesh(
vertices=m_verts,
faces=m_faces,
colors=[],
textureCoordinates=m_texcoords,
units=UNITS,
area = mesh_area,
bbox=Box(area=0.0, volume=0.0),
)
if i < len(data.materials):
material = data.materials[i]
if material is not None:
speckle_mesh["renderMaterial"] = material_to_speckle(material)
submeshes.append(speckle_mesh)
return submeshes
mat = blender_object.matrix_world
verts = [tuple(mat @ x.co * scale) for x in data.vertices]
flattend_verts = []
for row in verts: flattend_verts.extend(row)
faces = [p.vertices for p in data.polygons]
unit_system = bpy.context.scene.unit_settings.system
sm = Mesh(
name=blender_object.name,
vertices=flattend_verts,
faces=[],
colors=[],
textureCoordinates=[],
units=UNITS,
bbox=Box(area=0.0, volume=0.0),
)
if data.uv_layers.active:
for vt in data.uv_layers.active.data:
sm.textureCoordinates.extend([vt.uv.x, vt.uv.y])
for f in faces:
n = len(f)
if n == 3:
sm.faces.append(0)
elif n == 4:
sm.faces.append(1)
else:
sm.faces.append(n)
sm.faces.extend(f)
# TODO: figure out how to align vertex colors and vertices consistantly in receiving applications
# we are seeing the same issue as with texture coordinate alignment
#if data.color_attributes.active_color:
# sm.colors = [to_argb_int(x.color) for x in data.color_attributes.active_color.data]
return [sm]
def bezier_to_speckle(matrix: List[float], spline: bpy.types.Spline, scale: float, name: Optional[str] = None) -> Curve:
def bezier_to_speckle(matrix: MMatrix, spline: bpy.types.Spline, scale: float, name: Optional[str] = None) -> Curve:
degree = 3
closed = spline.use_cyclic_u
@@ -142,50 +172,121 @@ def bezier_to_speckle(matrix: List[float], spline: bpy.types.Spline, scale: floa
name=name,
degree=degree,
closed=spline.use_cyclic_u,
periodic=spline.use_cyclic_u,
periodic= not spline.use_endpoint_u,
points=flattend_points,
weights=[1] * num_points,
knots=knots,
rational=False,
rational=True,
area=0,
volume=0,
length=length,
domain=domain,
units=UNITS,
bbox=Box(area=0.0, volume=0.0),
displayValue = bezier_to_speckle_polyline(matrix, spline, scale, length),
)
def nurbs_to_speckle(matrix: List[float], spline: bpy.types.Spline, scale: float, name: Optional[str] = None) -> Curve:
knots = make_knots(spline)
points = [tuple(matrix @ pt.co.xyz * scale) for pt in spline.points]
def nurbs_to_speckle(matrix: MMatrix, spline: bpy.types.Spline, scale: float, name: Optional[str] = None) -> Curve:
degree = spline.order_u - 1
knots = make_knots(spline)
length = spline.calc_length()
domain = Interval(start=0, end=length, totalChildrenCount=0)
weights = [pt.weight for pt in spline.points]
is_rational = all(w == weights[0] for w in weights)
points = [tuple(matrix @ pt.co.xyz * scale) for pt in spline.points]
flattend_points = []
for row in points: flattend_points.extend(row)
if spline.use_cyclic_u:
for i in range(0, degree * 3, 3):
# Rhino expects n + degree number of points (for closed curves). So we need to add an extra point for each degree
flattend_points.append(flattend_points[i + 0])
flattend_points.append(flattend_points[i + 1])
flattend_points.append(flattend_points[i + 2])
for i in range(0, degree):
weights.append(weights[i])
return Curve(
name=name,
degree=degree,
closed=spline.use_cyclic_u,
periodic=spline.use_cyclic_u,
periodic= not spline.use_endpoint_u,
points=flattend_points,
weights=[pt.weight for pt in spline.points],
weights=weights,
knots=knots,
rational=False,
rational=is_rational,
area=0,
volume=0,
length=length,
domain=domain,
units=UNITS,
bbox=Box(area=0.0, volume=0.0),
displayValue=nurbs_to_speckle_polyline(matrix, spline, scale, length),
)
def nurbs_to_speckle_polyline(matrix: MMatrix, spline: bpy.types.Spline, scale: float, length: Optional[float] = None) -> Polyline:
"""
Samples a nurbs curve with resolution_u creating a polyline
"""
points = []
sampled_points = nurb_make_curve(spline, spline.resolution_u, 3)
for i in range(0, len(sampled_points), 3):
scaled_point = matrix @ MVector((
sampled_points[i + 0],
sampled_points[i + 1],
sampled_points[i + 2])) * scale
def poly_to_speckle(matrix: List[float], spline: bpy.types.Spline, scale: float, name: Optional[str] = None) -> Polyline:
points.append(scaled_point.x)
points.append(scaled_point.y)
points.append(scaled_point.z)
length = length or spline.calc_length()
domain = Interval(start=0, end=length, totalChildrenCount=0)
return Polyline(value=points, closed = spline.use_cyclic_u, domain=domain, area=0, len=length)
#Inspired by https://blender.stackexchange.com/a/689 (CC BY-SA 3.0)
def bezier_to_speckle_polyline(matrix: MMatrix, spline: bpy.types.Spline, scale: float, length: Optional[float] = None) -> Optional[Polyline]:
"""
Samples a Bézier curve with resolution_u creating a polyline
"""
segments = len(spline.bezier_points)
if segments < 2: return None
R = spline.resolution_u + 1
points = []
if not spline.use_cyclic_u:
segments -= 1
points: List[float] = []
for i in range(segments):
inext = (i + 1) % len(spline.bezier_points)
knot1 = spline.bezier_points[i].co
handle1 = spline.bezier_points[i].handle_right
handle2 = spline.bezier_points[inext].handle_left
knot2 = spline.bezier_points[inext].co
_points = interpolate_bezier(knot1, handle1, handle2, knot2, R)
for p in _points:
scaled_point = matrix @ p * scale
points.append(scaled_point.x)
points.append(scaled_point.y)
points.append(scaled_point.z)
length = length or spline.calc_length()
domain = Interval(start=0, end=length, totalChildrenCount=0)
return Polyline(value=points, closed = spline.use_cyclic_u, domain=domain, area=0, len=length)
def poly_to_speckle(matrix: MMatrix, spline: bpy.types.Spline, scale: float, name: Optional[str] = None) -> Polyline:
points = [tuple(matrix @ pt.co.xyz * scale) for pt in spline.points]
flattend_points = []
@@ -233,7 +334,7 @@ def icurve_to_speckle(blender_object: Object, data: bpy.types.Curve, scale=1.0)
return curves
@deprecated
def ngons_to_speckle_polylines(blender_object: Object, data: bpy.types.Mesh, scale=1.0) -> Optional[List[Polyline]]:
UNITS = "m" if bpy.context.scene.unit_settings.system == "METRIC" else "ft"
@@ -253,7 +354,7 @@ def ngons_to_speckle_polylines(blender_object: Object, data: bpy.types.Mesh, sca
poly = Polyline(
name="{}_{}".format(blender_object.name, i),
closed=True,
value=value, # magic (flatten list of tuples)
value=value,
length=0,
domain=domain,
bbox=Box(area=0.0, volume=0.0),
@@ -266,15 +367,7 @@ def ngons_to_speckle_polylines(blender_object: Object, data: bpy.types.Mesh, sca
return polylines
def material_to_speckle(blender_object: Object) -> Optional[RenderMaterial]:
"""Create and return a render material from a blender object"""
if not getattr(blender_object.data, "materials", None):
return None
blender_mat: bpy.types.Material = blender_object.data.materials[0]
if not blender_mat:
return None
def material_to_speckle(blender_mat: bpy.types.Material) -> RenderMaterial:
speckle_mat = RenderMaterial()
speckle_mat.name = blender_mat.name
@@ -296,7 +389,19 @@ def material_to_speckle(blender_object: Object) -> Optional[RenderMaterial]:
return speckle_mat
def transform_to_speckle(blender_transform: List[float], scale=1.0) -> Transform:
def material_to_speckle_old(blender_object: Object) -> Optional[RenderMaterial]:
"""Create and return a render material from a blender object"""
if not getattr(blender_object.data, "materials", None):
return None
blender_mat: bpy.types.Material = blender_object.data.materials[0]
if not blender_mat:
return None
return material_to_speckle(blender_mat)
def transform_to_speckle(blender_transform: Iterable[Iterable[float]], scale=1.0) -> Transform:
value = [y for x in blender_transform for y in x]
# scale the translation
for i in (3, 7, 11):
+185 -67
View File
@@ -1,13 +1,13 @@
import math
from typing import Tuple
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 Mesh
from specklepy.serialization.base_object_serializer import BaseObjectSerializer
from specklepy.objects.geometry import Circle, Mesh, Ellipse
from specklepy.objects.other import RenderMaterial
from bpy_speckle.functions import _report
from bpy.types import Object
from bpy.types import Material, Object
IGNORED_PROPERTY_KEYS = {
"id",
@@ -25,7 +25,7 @@ IGNORED_PROPERTY_KEYS = {
}
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
@@ -35,18 +35,27 @@ 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[: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
serializer = BaseObjectSerializer()
blender_object["_speckle_type"] = type(speckle_object).__name__
app_id = getattr(speckle_object, "applicationId", None)
@@ -59,39 +68,28 @@ def add_custom_properties(speckle_object: Base, blender_object: Object):
continue
if isinstance(val, (int, str, float)):
blender_object[key] = val
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:
blender_object[key] = items
set_custom_property(key, items, blender_object)
elif isinstance(val,dict):
for (k,v) in val.items():
if not isinstance(v, Base):
blender_object[k] = v
set_custom_property(k, v, blender_object)
def add_blender_material(speckle_object: Base, blender_object: Object) -> None:
"""Add material to a blender object if the corresponding speckle object has a render material"""
if blender_object.data is None:
return
speckle_mat = getattr(
speckle_object,
"renderMaterial",
getattr(speckle_object, "@renderMaterial", None),
)
if not speckle_mat:
return
mat_name = getattr(speckle_mat, "name", None) or speckle_mat.__dict__.get("@name")
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 not blender_mat:
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
@@ -105,10 +103,24 @@ def add_blender_material(speckle_object: Base, blender_object: Object) -> None:
inputs["Metallic"].default_value = speckle_mat.metalness
inputs["Alpha"].default_value = speckle_mat.opacity
if speckle_mat.opacity < 1:
if speckle_mat.opacity < 1.0:
blender_mat.blend_method = "BLEND"
blender_object.data.materials.append(blender_mat)
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):
@@ -124,12 +136,11 @@ def add_vertices(speckle_mesh: Mesh, blender_mesh: BMesh, scale=1.0):
)
)
blender_mesh.verts.ensure_lookup_table()
def add_faces(speckle_mesh: Mesh, blender_mesh: BMesh, smooth=False):
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):
@@ -140,16 +151,14 @@ def add_faces(speckle_mesh: Mesh, blender_mesh: BMesh, smooth=False):
i += 1
try:
f = blender_mesh.faces.new(
[blender_mesh.verts[int(x)] for x in sfaces[i : i + n]]
[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
blender_mesh.faces.ensure_lookup_table()
blender_mesh.verts.index_update()
def add_colors(speckle_mesh: Mesh, blender_mesh: BMesh):
@@ -196,7 +205,7 @@ def add_uv_coords(speckle_mesh: Mesh, blender_mesh: BMesh):
)
else:
_report(
f"Failed to match UV coordinates to vert data. Blender mesh verts: {len(blender_mesh.verts)}, Speckle UVs * 2: {len(s_uvs) * 2}"
f"Failed to match UV coordinates to vert data. Blender mesh verts: {len(blender_mesh.verts)}, Speckle UVs: {len(s_uvs) // 2}"
)
return
@@ -226,7 +235,7 @@ ignored_keys = {
"_chunkable",
}
def get_blender_custom_properties(obj, max_depth=1000):
def get_blender_custom_properties(obj, max_depth: int = 1000):
if max_depth < 0:
return obj
@@ -243,41 +252,37 @@ def get_blender_custom_properties(obj, max_depth=1000):
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):
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):
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):
knots = [0.0] * (4 + macro_knotsu(nu))
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)
makecyclicknots(knots, nu.point_count_u, nu.order_u)
else:
calc_knots(knots, nu.point_count_u, nu.order_u, flag)
return knots
def calc_knots(knots, point_count, order, flag):
def calc_knots(knots: list[float], point_count: int, order: int, flag: int) -> None:
pts_order = point_count + order
if flag == 1:
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:
elif flag == 2: # CU_NURB_BEZIER
if order == 4:
k = 0.34
for a in range(pts_order):
@@ -290,24 +295,137 @@ def calc_knots(knots, point_count, order, flag):
k += 0.5
knots[a] = math.floor(k)
else:
for a in range(pts_order):
knots[a] = a
for a in range(1, len(knots) - 1):
knots[a] = a - 1
knots[-1] = knots[-2]
def makecyclicknots(knots, point_count, order):
order2 = order - 1
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
if order > 2:
b = point_count + order2
for a in range(1, order2):
if knots[b] != knots[b - a]:
break
# this is for float inaccuracy
if t < knots[0]:
t = knots[0]
elif t > knots[opp2]:
t = knots[opp2]
if a == order2:
knots[point_count + order - 2] += 1.0
# 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
b = order
c = point_count + order + order2
for a in range(point_count + order2, c):
knots[a] = knots[a - 1] + (knots[b] - knots[b - 1])
b -= 1
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)
+207
View File
@@ -0,0 +1,207 @@
"""
Provides uniform and consistent path helpers for `specklepy`
"""
import os
import sys
from pathlib import Path
from typing import Optional
from importlib import import_module, invalidate_caches
_user_data_env_var = "SPECKLE_USERDATA_PATH"
def _path() -> Optional[Path]:
"""Read the user data path override setting."""
path_override = os.environ.get(_user_data_env_var)
if path_override:
return Path(path_override)
return None
_application_name = "Speckle"
def override_application_name(application_name: str) -> None:
"""Override the global Speckle application name."""
global _application_name
_application_name = application_name
def override_application_data_path(path: Optional[str]) -> None:
"""
Override the global Speckle application data path.
If the value of path is `None` the environment variable gets deleted.
"""
if path:
os.environ[_user_data_env_var] = path
else:
os.environ.pop(_user_data_env_var, None)
def _ensure_folder_exists(base_path: Path, folder_name: str) -> Path:
path = base_path.joinpath(folder_name)
path.mkdir(exist_ok=True, parents=True)
return path
def user_application_data_path() -> Path:
"""Get the platform specific user configuration folder path"""
path_override = _path()
if path_override:
return path_override
try:
if sys.platform.startswith("win"):
app_data_path = os.getenv("APPDATA")
if not app_data_path:
raise Exception(
"Cannot get appdata path from environment."
)
return Path(app_data_path)
else:
# try getting the standard XDG_DATA_HOME value
# as that is used as an override
app_data_path = os.getenv("XDG_DATA_HOME")
if app_data_path:
return Path(app_data_path)
else:
return _ensure_folder_exists(Path.home(), ".config")
except Exception as ex:
raise Exception(
"Failed to initialize user application data path.", ex
)
def user_speckle_folder_path() -> Path:
"""Get the folder where the user's Speckle data should be stored."""
return _ensure_folder_exists(user_application_data_path(), _application_name)
def user_speckle_connector_installation_path(host_application: str) -> Path:
"""
Gets a connector specific installation folder.
In this folder we can put our connector installation and all python packages.
"""
return _ensure_folder_exists(
_ensure_folder_exists(user_speckle_folder_path(), "connector_installations"),
host_application,
)
print("Starting module dependency installation")
print(sys.executable)
PYTHON_PATH = sys.executable
def connector_installation_path(host_application: str) -> Path:
connector_installation_path = user_speckle_connector_installation_path(host_application)
connector_installation_path.mkdir(exist_ok=True, parents=True)
# set user modules path at beginning of paths for earlier hit
if sys.path[0] != connector_installation_path:
sys.path.insert(0, str(connector_installation_path))
print(f"Using connector installation path {connector_installation_path}")
return connector_installation_path
def is_pip_available() -> bool:
try:
import_module("pip") # noqa F401
return True
except ImportError:
return False
def ensure_pip() -> None:
print("Installing pip... ")
from subprocess import run
completed_process = run([PYTHON_PATH, "-m", "ensurepip"])
if completed_process.returncode == 0:
print("Successfully installed pip")
else:
raise Exception(f"Failed to install pip, got {completed_process.returncode} return code")
def get_requirements_path() -> Path:
# we assume that a requirements.txt exists next to the __init__.py file
path = Path(Path(__file__).parent, "requirements.txt")
assert path.exists()
return path
def install_requirements(host_application: str) -> None:
# set up addons/modules under the user
# script path. Here we'll install the
# dependencies
path = connector_installation_path(host_application)
print(f"Installing Speckle dependencies to {path}")
from subprocess import run
completed_process = run(
[
PYTHON_PATH,
"-m",
"pip",
"install",
"-t",
str(path),
"-r",
str(get_requirements_path()),
],
capture_output=True,
text=True,
)
if completed_process.returncode != 0:
m = f"Failed to install dependenices through pip, got {completed_process.returncode} return code"
print(m)
raise Exception(m)
def install_dependencies(host_application: str) -> None:
if not is_pip_available():
ensure_pip()
install_requirements(host_application)
def _import_dependencies() -> None:
import_module("specklepy")
# the code above doesn't work for now, it fails on importing graphql-core
# despite that, the connector seams to be working as expected
# But it would be nice to make this solution work
# it would ensure that all dependencies are fully loaded
# requirements = get_requirements_path().read_text()
# reqs = [
# req.split(" ; ")[0].split("==")[0].split("[")[0].replace("-", "_")
# for req in requirements.split("\n")
# if req and not req.startswith(" ")
# ]
# for req in reqs:
# print(req)
# import_module("specklepy")
def ensure_dependencies(host_application: str) -> None:
try:
install_dependencies(host_application)
invalidate_caches()
_import_dependencies()
print("Successfully found dependencies")
except ImportError:
raise Exception(f"Cannot automatically ensure Speckle dependencies. Please try restarting the host application {host_application}!")
+2 -1
View File
@@ -1,4 +1,4 @@
from .users import LoadUsers, LoadUserStreams
from .users import LoadUsers, LoadUserStreams, ResetUsers
from .object import (
UpdateObject,
ResetObject,
@@ -27,6 +27,7 @@ from .misc import OpenSpeckleGuide, OpenSpeckleTutorials, OpenSpeckleForum
operator_classes = [
LoadUsers,
ResetUsers,
ReceiveStreamObjects,
SendStreamObjects,
LoadUserStreams,
+26 -12
View File
@@ -3,13 +3,15 @@ Commit operators
"""
import bpy
from bpy.props import BoolProperty
from bpy_speckle.functions import _check_speckle_client_user_stream
from bpy_speckle.functions import _check_speckle_client_user_stream, _report
from bpy_speckle.clients import speckle_clients
from bpy_speckle.properties.scene import SpeckleSceneSettings
class DeleteCommit(bpy.types.Operator):
"""
Delete stream
Deletes the selected commit from the selected stream.
To execute from code, call: `bpy.ops.speckle.delete_commit(are_you_sure=True)`
"""
bl_idname = "speckle.delete_commit"
@@ -37,27 +39,39 @@ class DeleteCommit(bpy.types.Operator):
def execute(self, context):
if not self.are_you_sure:
_report(f"{self.bl_idname}: cancelled by user")
return {"CANCELLED"}
self.are_you_sure = False
speckle = context.scene.speckle
speckle: SpeckleSceneSettings = context.scene.speckle
check = _check_speckle_client_user_stream(context.scene)
if check is None:
user = speckle.get_active_user()
if user is None:
print(f"{self.bl_idname}: failed - No user selected/found")
return {"CANCELLED"}
user, stream = check
client = speckle_clients[int(context.scene.speckle.active_user)]
stream = user.get_active_stream()
if stream is None:
print(f"{self.bl_idname}: failed - No stream selected/found")
return {"CANCELLED"}
stream = user.streams[user.active_stream]
if len(stream.branches) < 1:
branch = stream.get_active_branch()
if branch is None:
print(f"{self.bl_idname}: failed - No branch selected/found")
return {"CANCELLED"}
branch = stream.branches[int(stream.branch)]
if len(branch.commits) < 1:
commit = branch.get_active_commit()
if commit is None:
print(f"{self.bl_idname}: failed - No commit selected/found")
return {"CANCELLED"}
commit = branch.commits[int(branch.commit)]
client = speckle_clients[int(speckle.active_user)]
deleted = client.commit.delete(stream_id=stream.id, commit_id=commit.id)
if not deleted:
print(f"{self.bl_idname}: failed - Delete operation failed")
return {"CANCELLED"}
print(f"{self.bl_idname}: succeeded - commit {commit.id} ({commit.message}) has been deleted from stream {stream.id}")
return {"FINISHED"}
+2 -2
View File
@@ -4,6 +4,7 @@ Object operators
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,
@@ -28,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)
@@ -127,7 +127,7 @@ class DeleteObject(bpy.types.Operator):
return {"FINISHED"}
@deprecated
class UploadNgonsAsPolylines(bpy.types.Operator):
"""
Upload mesh ngon faces as polyline outlines
+92 -61
View File
@@ -3,16 +3,23 @@ Stream operators
"""
from itertools import chain
from math import radians
from typing import Callable, Dict, Iterable
from typing import Callable, Dict, Iterable, Optional
import bpy
from bpy_speckle.convert.util import link_object_to_collection_nested
from bpy_speckle.properties.scene import SpeckleSceneSettings
from specklepy.api.models import Commit
import webbrowser
from bpy.props import (
StringProperty,
BoolProperty,
EnumProperty,
)
from bpy.types import Context, Object
from bpy_speckle.convert.to_native import can_convert_to_native, convert_to_native
from bpy_speckle.convert.to_native import (
can_convert_to_native,
convert_to_native,
set_convert_instances_as,
)
from bpy_speckle.convert.to_speckle import (
convert_to_speckle,
)
@@ -21,7 +28,6 @@ from bpy_speckle.functions import (
get_scale_length,
_report,
)
from bpy_speckle.convert import get_speckle_subobjects
from bpy_speckle.clients import speckle_clients
from bpy_speckle.operators.users import add_user_stream
@@ -52,6 +58,8 @@ def get_objects_collections(base: Base) -> Dict[str, list]:
def get_objects_nested_lists(items: list, parent_col: Optional[bpy.types.Collection] = None) -> List:
"""For handling the weird nested lists that come from Grasshopper"""
objects = []
if not items:
return objects
if isinstance(items[0], list):
items = list(chain.from_iterable(items))
@@ -77,8 +85,6 @@ def get_objects_collections_recursive(base: Base, parent_col: Optional[bpy.types
for name in base.get_dynamic_member_names():
value = base[name]
if name == "parameters" and "Revit" in base.speckle_type:
continue
if isinstance(value, list):
objects.extend(item for item in value if isinstance(item, Base))
if isinstance(value, Base):
@@ -125,7 +131,7 @@ def get_receive_funcs(context: Context, created_objects: Dict[str, Object]) -> t
nonlocal created_objects
nonlocal objectCallback
progress += 1 #TODO: Progress bar never reaches 100 because func is only called for convertible objects
progress += 1 #NOTE:XXX Progress bar never reaches 100 because func is only called for convertible objects
context.window_manager.progress_update(progress)
created_objects[obj.name] = obj
@@ -146,7 +152,7 @@ def bases_to_native(context: bpy.types.Context, collections: Dict[str, list], sc
for obj in objects:
if isinstance(obj, dict):
bases_to_native(context, obj, scale, stream_id, func)
elif isinstance(obj, list):
elif isinstance(obj, list): #FIXME: wtf are these nested if statement, can this not be a recursive call?
for item in obj:
if isinstance(item, dict):
bases_to_native(context, item, scale, stream_id, func)
@@ -159,7 +165,7 @@ def bases_to_native(context: bpy.types.Context, collections: Dict[str, list], sc
else:
_report(
f"Something went wrong when receiving collection: {col_name}"
f"Something went wrong when receiving collection: {col_name}" #FIXME: undescript report message
)
bpy.context.view_layer.update()
@@ -177,17 +183,17 @@ def base_to_native(context: bpy.types.Context,
existing: Dict[str, Object],
func: ObjectCallback = None
):
new_objects = convert_to_native(base)
if not isinstance(new_objects, list):
new_objects = [new_objects]
if hasattr(base, "properties") and base.properties is not None:
new_objects.extend(get_speckle_subobjects(base.properties, scale, base.id))
elif isinstance(base, dict) and "properties" in base.keys():
new_objects.extend(
get_speckle_subobjects(base["properties"], scale, base["id"])
)
#NOTE: this code is ancient, and in testing does nothing, so we are removing it.
# if hasattr(base, "properties") and base.properties is not None:
# new_objects.extend(get_speckle_subobjects(base.properties, scale, base.id))
# elif isinstance(base, dict) and "properties" in base.keys():
# new_objects.extend(
# get_speckle_subobjects(base["properties"], scale, base["id"])
# )
"""
Set object Speckle settings
"""
@@ -199,11 +205,11 @@ def base_to_native(context: bpy.types.Context,
Run injected function
"""
if func:
new_object = func(context, new_object, base) #this base object isn't the right one for hosted elements!
new_object = func(context, new_object, base) #this base object isn't always the right one for hosted elements! #TODO: may be it now, need to double check!
if (
new_object is None
): # Make sure that the injected function returned an object
): # If the injected function returned None, then we should ignore this object.
_report(f"Script '{func.__module__}' returned None.")
continue
@@ -216,8 +222,9 @@ def base_to_native(context: bpy.types.Context,
new_object.name = name
col.objects.unlink(existing[new_object.speckle.object_id])
if new_object.name not in col.objects:
col.objects.link(new_object)
link_object_to_collection_nested(new_object, col)
#if new_object.name not in col.objects:
#col.objects.link(new_object)
def create_collection(name: str, clear_collection=True) -> bpy.types.Collection:
@@ -244,7 +251,7 @@ def get_existing_collection_objs(col: bpy.types.Collection) -> Dict[str, bpy.typ
}
def get_collection_parents(collection, names):
def get_collection_parents(collection: bpy.types.Collection, names: list[str]) -> None:
for parent in bpy.data.collections:
if collection.name in parent.children.keys():
# TODO: this should be rethought to make it clear when this is an IFC delim so we know to replace it
@@ -253,7 +260,7 @@ def get_collection_parents(collection, names):
get_collection_parents(parent, names)
def get_collection_hierarchy(collection):
def get_collection_hierarchy(collection: Optional[bpy.types.Collection]) -> list[str]:
if not collection:
return []
names = [collection.name.replace("/", "::").replace(".", "::")]
@@ -262,7 +269,7 @@ def get_collection_hierarchy(collection):
return names
def create_nested_hierarchy(base, hierarchy, objects):
def create_nested_hierarchy(base: Base, hierarchy: List[str], objects: Any):
child = base
while hierarchy:
@@ -272,13 +279,23 @@ def create_nested_hierarchy(base, hierarchy, objects):
child.add_detachable_attrs({name})
child = child[name]
# TODO: what do we call this attribute?
if not hasattr(child, "@objects"):
child["@objects"] = []
child["@objects"].extend(objects)
if not hasattr(child, "@elements"):
child["@elements"] = []
child["@elements"].extend(objects)
return base
#RECEIVE_MODES = [#TODO: modes
# ("create", "Create", "Add new geometry, without removing any existing objects"),
# ("replace", "Replace", "Replace objects from previous receive operations from the same stream"),
# #("update","Update", "") #TODO: update mode!
#]
INSTANCES_SETTINGS = [
("collection_instance", "Collection Instace", "Receive Instances as Collection Instances"),
("linked_duplicates", "Linked Duplicates", "Receive Instances as Linked Duplicates"),
]
class ReceiveStreamObjects(bpy.types.Operator):
"""
Receive stream objects
@@ -292,10 +309,16 @@ class ReceiveStreamObjects(bpy.types.Operator):
clean_meshes: BoolProperty(name="Clean Meshes", default=False)
#receive_mode: EnumProperty(items=RECEIVE_MODES, name="Receive Type", default="replace", description="The behaviour of the recieve operation")
receive_instances_as: EnumProperty(items=INSTANCES_SETTINGS, name="Receive Instances As", default="collection_instance", description="How to receive speckle Instances")
def draw(self, context):
layout = self.layout
col = layout.column()
col.prop(self, "clean_meshes")
#col.prop(self, "receive_mode")
col.prop(self, "receive_instances_as")
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self)
@@ -320,7 +343,6 @@ class ReceiveStreamObjects(bpy.types.Operator):
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action='SELECT')
bpy.ops.mesh.remove_doubles()
bpy.ops.mesh.dissolve_limited(angle_limit=radians(0.1))
# Reset state to previous (not quite sure if this is 100% necessary)
@@ -331,30 +353,31 @@ class ReceiveStreamObjects(bpy.types.Operator):
def execute(self, context):
bpy.context.view_layer.objects.active = None
check = _check_speckle_client_user_stream(context.scene)
if check is None:
speckle: SpeckleSceneSettings = context.scene.speckle
#Get UI Selection
user = speckle.get_active_user()
if not user:
print("No user selected/found")
return {"CANCELLED"}
user, bstream = check
client = speckle_clients[int(context.scene.speckle.active_user)]
stream = client.stream.get(id=bstream.id, branch_limit=20)
if stream.branches.totalCount < 1:
stream = user.get_active_stream()
if not stream:
print("No stream selected/found")
return {"CANCELLED"}
if not stream.branches:
branch = stream.get_active_branch()
if not branch:
print("No branch selected/found")
return {"CANCELLED"}
branch = stream.branches.items[int(bstream.branch)]
bbranch = bstream.branches[int(bstream.branch)]
if branch.commits.totalCount < 1:
_report("No commits found. Probably an empty stream.")
commit = branch.get_active_commit()
if commit is None:
print("No commit selected/found")
return {"CANCELLED"}
commit: Commit = branch.commits.items[int(bbranch.commit)]
#Get actual stream data
client = speckle_clients[int(speckle.active_user)]
transport = ServerTransport(stream.id, client)
@@ -362,33 +385,41 @@ class ReceiveStreamObjects(bpy.types.Operator):
metrics.RECEIVE,
getattr(transport, "account", None),
custom_props={
"sourceHostApp": host_applications.get_host_app_from_string(commit.sourceApplication).slug,
"sourceHostAppVersion": commit.sourceApplication
"sourceHostApp": host_applications.get_host_app_from_string(commit.source_application).slug,
"sourceHostAppVersion": commit.source_application,
"isMultiplayer": commit.author_id != user.id,
},
)
stream_data = operations._untracked_receive(commit.referencedObject, transport)
commit_object = operations._untracked_receive(commit.referenced_object, transport)
client.commit.received(
bstream.id,
stream.id,
commit.id,
source_application="blender",
message="received commit from Speckle Blender",
)
context.window_manager.progress_begin(0, stream_data.totalChildrenCount)
context.window_manager.progress_begin(0, commit_object.totalChildrenCount or 1)
set_convert_instances_as(self.receive_instances_as) #HACK: we need a better way to pass settings down to the converter
"""
Create or get Collection for stream objects
"""
collections = get_objects_collections(stream_data)
collections = get_objects_collections(commit_object)
if not collections:
print("Unusual commit structure - did not correctly create collections")
return {"CANCELLED"}
name = "{} [ {} @ {} ]".format(stream.name, branch.name, commit.id)
# name = ""
# if self.receive_mode == "create":
name = "{} [ {} @ {} ]".format(stream.name, branch.name, commit.id) # Matches Rhino "Create" naming
# else:
# name = stream.name # Doesn't quite match rhino's Update layer naming, but is close enough no?
col = create_collection(name)
col.speckle.stream_id = stream.id
col.speckle.units = stream_data.units or "m"
col.speckle.units = commit_object.units or "m"
if col.name not in bpy.context.scene.collection.children:
bpy.context.scene.collection.children.link(col)
@@ -416,6 +447,7 @@ class ReceiveStreamObjects(bpy.types.Operator):
"""
Iterate through retrieved resources
"""
bases_to_native(context, collections, scale, stream.id, func)
context.window_manager.progress_end()
@@ -474,10 +506,11 @@ class SendStreamObjects(bpy.types.Operator):
return {"CANCELLED"}
check = _check_speckle_client_user_stream(context.scene)
if check is None:
user, bstream = check
if user is None:
return {"CANCELLED"}
user, bstream = check
stream = user.streams[user.active_stream]
branch = stream.branches[int(stream.branch)]
@@ -525,11 +558,6 @@ class SendStreamObjects(bpy.types.Operator):
_report("Converting {}".format(obj.name))
# ngons = obj.get("speckle_ngons_as_polylines", False)
# if ngons:
# converted = ngons_to_speckle_polylines(obj, scale)
# else:
converted = convert_to_speckle(
obj,
scale,
@@ -556,17 +584,20 @@ class SendStreamObjects(bpy.types.Operator):
transport = ServerTransport(stream.id, client)
_report(f"Sending to {stream}")
obj_id = operations.send(
base,
[transport],
)
client.commit.create(
commitId = client.commit.create(
stream.id,
obj_id,
branch.name,
message=self.commit_message,
source_application="blender",
)
_report(f"Commit Created {user.server_url}/streams/{stream.id}/commits/{commitId}")
bpy.ops.speckle.load_user_streams()
+35 -9
View File
@@ -4,11 +4,35 @@ User account operators
import bpy
from bpy_speckle.functions import _report
from bpy_speckle.clients import speckle_clients
from bpy_speckle.properties.scene import SpeckleCommitObject, SpeckleSceneSettings
from specklepy.api.client import SpeckleClient
from specklepy.api.models import Stream, User
from specklepy.api.credentials import get_local_accounts
from datetime import datetime
class ResetUsers(bpy.types.Operator):
"""
Reset loaded users
"""
bl_idname = "speckle.users_reset"
bl_label = "Reset users"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
self.reset_ui(context)
bpy.context.view_layer.update()
if context.area:
context.area.tag_redraw()
return {"FINISHED"}
@staticmethod
def reset_ui(context: bpy.types.Context):
speckle: SpeckleSceneSettings = context.scene.speckle
speckle.users.clear()
speckle_clients.clear()
class LoadUsers(bpy.types.Operator):
"""
@@ -23,36 +47,37 @@ class LoadUsers(bpy.types.Operator):
_report("Loading users...")
users = context.scene.speckle.users
speckle: SpeckleSceneSettings = context.scene.speckle
users = speckle.users
context.scene.speckle.users.clear()
speckle_clients.clear()
ResetUsers.reset_ui(context)
profiles = get_local_accounts()
active_user_index = 0
for profile in profiles:
user = users.add()
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="https" in profile.serverInfo.url,
)
client.authenticate(user.authToken)
client.authenticate_with_account(profile)
speckle_clients.append(client)
except Exception as ex:
_report(ex)
users.remove(len(users) - 1)
if profile.isDefault:
context.scene.speckle.active_user = str(len(users) - 1)
active_user_index = len(users) - 1
context.scene.speckle.active_user_index = int(context.scene.speckle.active_user)
bpy.ops.speckle.load_user_streams()
#speckle.active_user_index = int(speckle.active_user) #TODO Wtf is this?
speckle.active_user = str(active_user_index)
bpy.context.view_layer.update()
if context.area:
@@ -78,13 +103,14 @@ def add_user_stream(user: User, stream: Stream):
continue
for c in b.commits.items:
commit = branch.commits.add()
commit: SpeckleCommitObject = branch.commits.add()
commit.id = commit.name = c.id
commit.message = c.message or ""
commit.author_name = c.authorName
commit.author_id = c.authorId
commit.created_at = datetime.strftime(c.createdAt, "%Y-%m-%d %H:%M:%S.%f%Z")
commit.source_application = str(c.sourceApplication)
commit.referenced_object = c.referencedObject
if hasattr(s, "baseProperties"):
s.units = stream.baseProperties.units
+34 -9
View File
@@ -1,6 +1,7 @@
"""
Scene properties
"""
from typing import Optional
import bpy
from bpy.props import (
StringProperty,
@@ -18,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):
@@ -42,6 +44,12 @@ class SpeckleBranchObject(bpy.types.PropertyGroup):
description="Active commit",
items=get_commits,
)
def get_active_commit(self) -> Optional[SpeckleCommitObject]:
selected_index = int(self.commit)
if 0 <= selected_index < len(self.commits):
return self.commits[selected_index]
return None
class SpeckleStreamObject(bpy.types.PropertyGroup):
@@ -66,17 +74,28 @@ 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):
@@ -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,9 @@ class SpeckleSceneSettings(bpy.types.PropertyGroup):
description="Script to run when sending stream objects.",
items=get_scripts,
)
def get_active_user(self) -> Optional[SpeckleUserObject]:
selected_index = int(self.active_user)
if 0 <= selected_index < len(self.users):
return self.users[selected_index]
return None
+3 -2
View File
@@ -115,13 +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.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):
"""
+4
View File
@@ -0,0 +1,4 @@
#!/usr/bin/env bash
set -e -o pipefail
poetry export --only main -o bpy_speckle/requirements.txt
-14
View File
@@ -1,14 +0,0 @@
#!/bin/sh
MODULES_PATH="./bpy_speckle/modules"
# Clean up
rm -rf $MODULES_PATH/*
rm -f requirements.txt
# Export poetry dependencies
poetry lock --no-update
poetry export -f requirements.txt --with dev --without-hashes > requirements.txt
# Install dependencies via pip
python -m pip install -r requirements.txt -t $MODULES_PATH
+20
View File
@@ -0,0 +1,20 @@
import sys
from pathlib import Path
def patch_installer(tag: str):
"""Patches the installer with the correct connector version and specklepy version"""
tag = tag.replace("\n", "")
iss_file = "speckle-sharp-ci-tools/blender.iss"
iss_path = Path(iss_file)
lines = iss_path.read_text().split("\n")
lines.insert(12, f'#define AppVersion "{tag.split("-")[0]}"')
lines.insert(13, f'#define AppInfoVersion "{tag}"')
iss_path.write_text("\n".join(lines))
print(f"Patched installer with connector v{tag}")
if __name__ == "__main__":
tag = sys.argv[1]
patch_installer(tag)
-36
View File
@@ -1,7 +1,6 @@
import re
import sys
def patch_connector(tag):
"""Patches the connector version within the connector init file"""
bpy_file = "bpy_speckle/__init__.py"
@@ -19,48 +18,13 @@ def patch_connector(tag):
with open(bpy_file, "w") as file:
file.writelines(lines)
def patch_installer(tag):
"""Patches the installer with the correct connector version and specklepy version"""
iss_file = "speckle-sharp-ci-tools/blender.iss"
py_tag = get_specklepy_version()
with open(iss_file, "r") as file:
lines = file.readlines()
lines.insert(11, f'#define SpecklepyVersion "{py_tag}"\n')
lines.insert(12, f'#define AppVersion "{tag.split("-")[0]}"\n')
lines.insert(13, f'#define AppInfoVersion "{tag}"\n')
with open(iss_file, "w") as file:
file.writelines(lines)
print(f"Patched installer with connector v{tag} and specklepy v{py_tag}")
def get_specklepy_version():
"""Get version of specklepy to install from the pyproject.toml"""
version = "2.3.3"
with open("pyproject.toml", "r") as f:
lines = [line for line in f if line.startswith("specklepy = ")]
if not lines:
raise Exception("Could not find specklepy in pyproject.toml")
match = re.search(r"[0-9]+(\.[0-9]+)*", lines[0])
if match:
version = match[0]
return version
def main():
if len(sys.argv) < 2:
print(get_specklepy_version())
return
tag = sys.argv[1]
if not re.match(r"([0-9]+)\.([0-9]+)\.([0-9]+)", tag):
raise ValueError(f"Invalid tag provided: {tag}")
print(f"Patching version: {tag}")
patch_connector(tag.split("-")[0])
patch_installer(tag)
if __name__ == "__main__":
Generated
+583 -425
View File
File diff suppressed because it is too large Load Diff
+7 -6
View File
@@ -7,16 +7,17 @@ license = "Apache-2.0"
[tool.poetry.dependencies]
python = ">=3.8, <4.0.0"
specklepy = "^2.9.1"
specklepy = "^2.13.0"
[tool.poetry.dev-dependencies]
# [tool.poetry.group.local_specklepy.dependencies]
# specklepy = {path = "../specklepy", develop = true}
[tool.poetry.group.dev.dependencies]
numpy = "^1.23.5"
fake-bpy-module-latest = "^20221006"
fake-bpy-module-latest = "^20230117"
black = "^22.10.0"
pylint = "^2.15.7"
[tool.poetry.group.local_specklepy.dependencies]
specklepy = {path = "../specklepy", develop = true}
ruff = "^0.0.166"
[build-system]
requires = ["poetry-core>=1.0.0"]