Compare commits

..

35 Commits

Author SHA1 Message Date
Gergő Jedlicska 13421a23a0 build for osx 13 for arm 2022-12-13 16:48:23 +01:00
Gergő Jedlicska 4328c5b8cb only parameter 2022-12-13 13:12:49 +01:00
Gergő Jedlicska 4d44a49988 no CD 2022-12-13 13:11:36 +01:00
Gergő Jedlicska c174101176 pipe the zip 2022-12-13 13:11:18 +01:00
Gergő Jedlicska 62566b1bab top level cp operation 2022-12-13 12:59:27 +01:00
Gergő Jedlicska 88ec39caa4 mkdir 101 2022-12-13 12:52:37 +01:00
Gergő Jedlicska d65920a774 copy 101 2022-12-13 12:42:26 +01:00
Gergő Jedlicska c133689fea force copy 2022-12-13 12:37:52 +01:00
Gergő Jedlicska 6aaf4a5a99 mkdir -p 2022-12-13 12:33:42 +01:00
Gergő Jedlicska 66ec9d5af1 mkdir for the zip folder 2022-12-13 12:31:29 +01:00
Gergő Jedlicska 3cabadf552 fix zipping dir 2022-12-13 12:28:01 +01:00
Gergő Jedlicska c15bb7f045 ci(circleci): fix persist to workspace pathing 2022-12-13 12:21:26 +01:00
Gergő Jedlicska b0aa9cb48f ci(circleci): wtf is microsoft doing with env vars in net6 containers? 2022-12-13 12:05:58 +01:00
Gergő Jedlicska 7bbc286ae2 ci(circleci): fix missing semver in files 2022-12-13 11:48:56 +01:00
Gergő Jedlicska 99b130290f ci(circleci): fix env var restore in deploy 2022-12-13 11:38:51 +01:00
Gergő Jedlicska a0f782af5f make sure semver is set 2022-12-13 11:26:49 +01:00
Gergő Jedlicska f48a5c9582 ci(cirleci): fix mac build triggers 2022-12-13 11:21:12 +01:00
Gergő Jedlicska a9875ffbaa ci(circleci): fix filters 2022-12-13 11:18:13 +01:00
Gergő Jedlicska cee9d83e1d ci(circleci): update details in deploy 2022-12-13 11:09:56 +01:00
Gergő Jedlicska f76b6cf59b publish only usefull stuff 2022-12-12 18:10:52 +01:00
Gergő Jedlicska 8cd9d03806 only zip the published isntaller 2022-12-12 18:08:23 +01:00
Gergő Jedlicska 28ace1f591 add zip again 2022-12-12 18:04:26 +01:00
Gergő Jedlicska beebe10dff add python to dotnet installer 2022-12-12 17:58:33 +01:00
Gergő Jedlicska 1f964bf939 properly saving packaged zip 2022-12-12 17:52:40 +01:00
Gergő Jedlicska 997b94dab9 fix installer patch pathing 2022-12-12 17:46:48 +01:00
Gergő Jedlicska ea962952e5 force remove new lines from installer tags 2022-12-12 17:45:33 +01:00
Gergő Jedlicska 5aff85b586 get proper ci tools 2022-12-12 17:37:28 +01:00
Gergő Jedlicska e1553ed389 getting the specific installer branch 2022-12-12 17:36:20 +01:00
Gergő Jedlicska 88235402f1 ci(circleci): update dependency graph 2022-12-12 17:28:14 +01:00
Gergő Jedlicska a1eea0a6fc ci(circleci): update semver location 2022-12-12 17:26:34 +01:00
Gergő Jedlicska 80496be394 ci(circleci): update filters 2022-12-12 17:21:01 +01:00
Gergő Jedlicska ac63cbe17d ci(circleci): fix naming 2022-12-12 17:18:15 +01:00
Gergő Jedlicska de0c97cdbd ci(circleci): updates 2022-12-12 17:16:51 +01:00
Gergő Jedlicska 14e8445cf2 fix(installer): ensure pip call was commented outy 2022-12-12 16:09:45 +01:00
Gergő Jedlicska ddf908b3f9 ci(circleci): rewrite full ci WIP 2022-12-09 13:58:48 +01:00
34 changed files with 2844 additions and 4502 deletions
+33 -124
View File
@@ -1,12 +1,12 @@
version: 2.1
orbs:
win: circleci/windows@5.0.0
win: circleci/windows@2.4.0
jobs:
package-connector:
docker:
- image: cimg/python:3.11.12
- image: cimg/python:3.11.0
steps:
- checkout
- run:
@@ -30,7 +30,7 @@ jobs:
build-connector-zip:
docker:
- image: cimg/python:3.11.12
- image: cimg/python:3.11.0
steps:
- attach_workspace:
at: ./
@@ -44,25 +44,16 @@ jobs:
root: ./
paths:
- bpy_speckle.zip
get-ci-tools: # Clones our ci tools and persists them to the workspace
docker:
- image: cimg/base:2025.04
- 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:
- run: # Could not get ssh to work, so using a personal token
name: Clone
command: git clone git@github.com:specklesystems/speckle-sharp-ci-tools.git speckle-sharp-ci-tools
command: git clone https://$GITHUB_TOKEN@github.com/specklesystems/speckle-sharp-ci-tools.git speckle-sharp-ci-tools
- run:
command: cd speckle-sharp-ci-tools
command: cd speckle-sharp-ci-tools && git checkout blender/installer-changes
- persist_to_workspace:
root: ./
paths:
@@ -71,53 +62,26 @@ jobs:
build-installer-win:
executor:
name: win/default
environment:
SSM: 'C:\Program Files\DigiCert\DigiCert One Signing Manager Tools'
shell: cmd.exe
steps:
- attach_workspace:
at: ./
- run:
name: Patch installer
shell: powershell.exe
command: python patch_installer.py (Get-Content -Raw SEMVER)
- unless: # Build installers unsigned on non-tagged builds
condition: << pipeline.git.tag >>
steps:
- run:
name: Build Installer
command: speckle-sharp-ci-tools\InnoSetup\ISCC.exe speckle-sharp-ci-tools\blender.iss /Sbyparam=$p
shell: cmd.exe #does not work in powershell
- when: # Setup certificates and build installers signed for tagged builds
condition: << pipeline.git.tag >>
steps:
- run:
name: "Digicert Signing Manager Setup"
command: |
cd C:\
curl.exe -X GET https://one.digicert.com/signingmanager/api-ui/v1/releases/smtools-windows-x64.msi/download -H "x-api-key:$env:SM_API_KEY" -o smtools-windows-x64.msi
msiexec.exe /i smtools-windows-x64.msi /quiet /qn | Wait-Process
- run:
name: Create Auth & OV Signing Cert
command: |
cd C:\
echo $env:SM_CLIENT_CERT_FILE_B64 > certificate.txt
certutil -decode certificate.txt certificate.p12
- run:
name: Sync Certs
command: |
& $env:SSM\smctl.exe windows certsync
- run:
name: Build Installer
command: speckle-sharp-ci-tools\InnoSetup\ISCC.exe speckle-sharp-ci-tools\blender.iss /Sbyparam=$p /DSIGN_INSTALLER /DCODE_SIGNING_CERT_FINGERPRINT=%SM_CODE_SIGNING_CERT_SHA1_HASH%
shell: cmd.exe #does not work in powershell
- run:
name: Installer
shell: cmd.exe #does not work in powershell
command: speckle-sharp-ci-tools\InnoSetup\ISCC.exe speckle-sharp-ci-tools\blender.iss /Sbyparam=$p
- persist_to_workspace:
root: ./
paths:
- speckle-sharp-ci-tools/Installers/blender/blender-*.exe
build-installer-mac:
macos:
xcode: 13.4.1
resource_class: macos.m1.medium.gen1
build-dotnet-installer:
docker:
- image: mcr.microsoft.com/dotnet/sdk:6.0
parameters:
runtime:
type: string
@@ -127,29 +91,23 @@ jobs:
type: string
default: speckle-sharp-ci-tools/Mac/SpeckleBlenderInstall
steps:
- checkout
- attach_workspace:
at: ./
- run:
name: Exit if External PR
command: if [ "$CIRCLE_PR_REPONAME" ]; then circleci-agent step halt; fi
- run:
name: Install mono
command: |
HOMEBREW_NO_AUTO_UPDATE=1 brew install mono
# Compress build files
- run:
name: Install dotnet
command: curl -sSL https://dot.net/v1/dotnet-install.sh | bash /dev/stdin
- run: *restore_semver
- run:
name: ensure zip
command: apt update && apt install -y zip
- run:
name: Copy connector files to installer
command: |
mkdir -p <<parameters.installer_path >>/.installationFiles/
mkdir -p << parameters.installer_path >>/.installationFiles
cp bpy_speckle.zip << parameters.installer_path >>/.installationFiles
- run:
name: Build Mac installer
command: ~/.dotnet/dotnet publish << parameters.installer_path >>/SpeckleBlenderInstall.sln -r << parameters.runtime >> -c Release
name: Build installer
command: dotnet publish <<parameters.installer_path>>/SpeckleBlenderInstall.sln -r << parameters.runtime >> -c Release
# cd <<parameters.installer_path>>/bin/Release/net6.0/<< parameters.runtime >>/publish/
# zip -r << parameters.slug >>-${SEMVER}.zip <<parameters.installer_path>>/bin/Release/net6.0/<< parameters.runtime >>/publish/.
# cd ${CIRCLE_WORKING_DIRECTORY}
- run:
name: Zip installer
command: |
@@ -164,28 +122,6 @@ jobs:
paths:
- speckle-sharp-ci-tools/Installers/blender/<< parameters.slug >>*.zip
build-installer-manual:
docker:
- image: cimg/base:2025.04
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
@@ -227,18 +163,18 @@ workflows:
- package-connector:
filters: &build_filters
tags:
only: /([0-9]+)\.([0-9]+)\.([0-9]+)(?:-\w+)?(?:\.[0-9]+)?/
only: /([0-9]+)\.([0-9]+)\.([0-9]+)(?:-\w+)?$/
- build-connector-zip:
requires:
- package-connector
filters: *build_filters
- get-ci-tools:
filters: *build_filters
- build-installer-win:
context: digicert-keylocker
name: Windows Installer Build
requires:
- package-connector
@@ -246,14 +182,12 @@ workflows:
filters: *build_filters
- deploy-connector:
context: do-spaces-speckle-releases
name: deploy-windows
file_slug: blender
os: WIN
arch: Any
extension: exe
requires:
- Manual Installer Build
- Windows Installer Build
- Mac Intel Build
- Mac ARM Build
@@ -261,32 +195,30 @@ workflows:
branches:
ignore: /.*/
tags:
only: /([0-9]+)\.([0-9]+)\.([0-9]+)(?:-\w+)?(?:\.[0-9]+)?/
only: /([0-9]+)\.([0-9]+)\.([0-9]+)(?:-\w+)?$/
- build-installer-mac:
- build-dotnet-installer:
name: Mac ARM Build
slug: blender-mac-arm
runtime: osx-arm64
runtime: osx.13-arm64
requires:
- get-ci-tools
- build-connector-zip
filters: *build_filters
- deploy-connector:
context: do-spaces-speckle-releases
name: deploy-mac-arm
file_slug: blender-mac-arm
os: OSX
arch: Arm
extension: zip
requires:
- Manual Installer Build
- Windows Installer Build
- Mac Intel Build
- Mac ARM Build
filters: *deploy_filters
- build-installer-mac:
- build-dotnet-installer:
name: Mac Intel Build
slug: blender-mac-intel
runtime: osx-x64
@@ -296,35 +228,12 @@ workflows:
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
+70 -4
View File
@@ -6,7 +6,73 @@ on:
jobs:
update_issue:
uses: specklesystems/github-actions/.github/workflows/project-add-issue.yml@main
secrets: inherit
with:
issue-id: ${{ github.event.issue.node_id }}
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 }}
+42 -4
View File
@@ -6,7 +6,45 @@ on:
jobs:
track_issue:
uses: specklesystems/github-actions/.github/workflows/project-add-issue.yml@main
secrets: inherit
with:
issue-id: ${{ github.event.issue.node_id }}
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
+54 -44
View File
@@ -1,72 +1,82 @@
<<h1 align="center">
<h1 align="center">
<img src="https://user-images.githubusercontent.com/2679513/131189167-18ea5fe1-c578-47f6-9785-3748178e4312.png" width="150px"/><br/>
Speckle | Blender
</h1>
<h3 align="center">
Connector for Blender
</h3>
<p align="center"><b>Speckle</b> is the data infrastructure for the AEC industry.</p><br/>
<p align="center"><a href="https://twitter.com/SpeckleSystems"><img src="https://img.shields.io/twitter/follow/SpeckleSystems?style=social" alt="Twitter Follow"></a> <a href="https://speckle.community"><img src="https://img.shields.io/discourse/users?server=https%3A%2F%2Fspeckle.community&amp;style=flat-square&amp;logo=discourse&amp;logoColor=white" alt="Community forum users"></a> <a href="https://speckle.systems"><img src="https://img.shields.io/badge/https://-speckle.systems-royalblue?style=flat-square" alt="website"></a> <a href="https://speckle.guide/dev/"><img src="https://img.shields.io/badge/docs-speckle.guide-orange?style=flat-square&amp;logo=read-the-docs&amp;logoColor=white" alt="docs"></a></p>
<p align="center"><a href="https://github.com/specklesystems/speckle-blender/"><img src="https://circleci.com/gh/specklesystems/speckle-blender.svg?style=svg&amp;circle-token=76eabd350ea243575cbb258b746ed3f471f7ac29" alt="Speckle-Next"></a> </p>
> Speckle is the first AEC data hub that connects with your favorite AEC tools. Speckle exists to overcome the challenges of working in a fragmented industry where communication, creative workflows, and the exchange of data are often hindered by siloed software and processes. It is here to make the industry better.
# About Speckle
<h3 align="center">
Speckle Connector for Blender
</h3>
What is Speckle? Check our ![YouTube Video Views](https://img.shields.io/youtube/views/B9humiSpHzM?label=Speckle%20in%201%20minute%20video&style=social)
> [!WARNING]
> This is a legacy repo! A new next generation connector will be coming soon. In the meantime, check out our active next generation repos here 👇<br/>
> [`speckle-sharp-connectors`](https://github.com/specklesystems/speckle-sharp-connectors): our .NET next generation connectors and desktop UI<br/>
> [`speckle-sharp-sdk`](https://github.com/specklesystems/speckle-sharp-sdk): our .NET SDK, Tests, and Objects
### Features
# Blender Connector
- **Object-based:** say goodbye to files! Speckle is the first object based platform for the AEC industry
- **Version control:** Speckle is the Git & Hub for geometry and BIM data
- **Collaboration:** share your designs collaborate with others
- **3D Viewer:** see your CAD and BIM models online, share and embed them anywhere
- **Interoperability:** get your CAD and BIM models into other software without exporting or importing
- **Real time:** get real time updates and notifications and changes
- **GraphQL API:** get what you need anywhere you want it
- **Webhooks:** the base for a automation and next-gen pipelines
- **Built for developers:** we are building Speckle with developers in mind and got tools for every stack
- **Built for the AEC industry:** Speckle connectors are plugins for the most common software used in the industry such as Revit, Rhino, Grasshopper, AutoCAD, Civil 3D, Excel, Unreal Engine, Unity, QGIS, Blender and more!
### Try Speckle now!
Give Speckle a try in no time by:
- [![speckle XYZ](https://img.shields.io/badge/https://-speckle.xyz-0069ff?style=flat-square&logo=hackthebox&logoColor=white)](https://speckle.xyz) ⇒ creating an account at our public server
- [![create a droplet](https://img.shields.io/badge/Create%20a%20Droplet-0069ff?style=flat-square&logo=digitalocean&logoColor=white)](https://marketplace.digitalocean.com/apps/speckle-server?refcode=947a2b5d7dc1) ⇒ deploying an instance in 1 click
### Resources
- [![Community forum users](https://img.shields.io/badge/community-forum-green?style=for-the-badge&logo=discourse&logoColor=white)](https://speckle.community) for help, feature requests or just to hang with other speckle enthusiasts, check out our community forum!
- [![website](https://img.shields.io/badge/tutorials-speckle.systems-royalblue?style=for-the-badge&logo=youtube)](https://speckle.systems) our tutorials portal is full of resources to get you started using Speckle
- [![docs](https://img.shields.io/badge/docs-speckle.guide-orange?style=for-the-badge&logo=read-the-docs&logoColor=white)](https://speckle.guide/user/blender.html) reference on almost any end-user and developer functionality
# Repo structure
The Speckle UI can be found in the 3d viewport toolbar (N), under the Speckle tab.
Head to the [**📚 documentation**](https://speckle.guide/user/blender.html) for more information.
## Disclaimer
This code is WIP and as such should be used with extreme caution on non-sensitive projects.
## Installation
We officially support Blender 3.3 and newer, on Windows and Mac.
Please follow our installation instructions on our [connector docs](https://speckle.guide/user/blender.html#installation)
1. Place `bpy_speckle` folder in your `addons` folder. On Windows this is typically `%APPDATA%/Blender Foundation/Blender/2.80/scripts/addons`.
2. Go to `Edit->Preferences` (Ctrl + Alt + U)
3. Go to the `Add-ons` tab
4. Find and enable `SpeckleBlender 2.0` in the `Scene` category. <!-- **If enabling for the first time, expect the UI to freeze for bit while it silently installs all the dependencies.** -->
5. The Speckle UI can be found in the 3d viewport toolbar (N), under the `Speckle` tab.
## Usage
Once enabled in `Preferences -> Addons`,
The Speckle connector UI can be found in the 3d viewport toolbar (N), under the `Speckle` tab.
- Available user accounts are automatically detected and made available. To add user accounts use **Speckle Manager**.
- Select the user from the dropdown list in the `Users` panel. This will populate the `Projects` list with available projects for the selected user account.
- Select a model and version from the dropdown menus.
- Click on `Receive` to download and convert the objects from the selected model version. The objects will be linked into a Blender Collection, named `<PROJECT_NAME> [ <MODEL_NAME> @ <VERSION_ID> ]`.
- Click on `Open Model in Web` to view the model in your web browser.
- Select the user from the dropdown list in the `Users` panel. This will populate the `Streams` list with available streams for the selected user.
- Select a branch and commit from the dropdown menus.
- Click on `Receive` to download the objects from the selected stream, branch, and commit. The stream objects will be loaded into a Blender Collection, named `<STREAM_NAME> [ <STREAM_BRANCH> @ <BRANCH_COMMIT> ]`. <!-- You can filter the stream by entering a query into the `Filter` field (i.e. `properties.weight>10` or `type="Mesh"`). -->
- Click on `Open Stream in Web` to view the stream in your web browser.
## Supported Elements
## Caveats
The Blender Connector is still a work in progress and, as such, data sent from the Blender connector is a highly lossy exchange. Our connectors are ever evolving to facilitate more and more Speckle usecases. We welcome feedback, requests, edge cases, and contributions!
- Mesh objects are supported. Breps are imported as meshes using their `displayValue` data.
- Curves have limited support: `Polylines` are supported; `NurbsCurves` are supported, though they are not guaranteed to look the same; `Lines` are supported; `Arcs` are not supported, though they are very roughly approximated; `PolyCurves` are supported for linear / polyline segments and very approximate arc segments. These conversions are a point of focus for further development.
The full matrix of supported Blender and Speckle types [can be found here](https://speckle.guide/user/support-tables.html#blender)
## Additional Features
## Custom properties
- **SpeckleBlender** will look for a `texture_coordinates` property and use that to create a UV layer for the imported object. These texture coordinates are a space-separated list of floats (`[u v u v u v etc...]`) that is encoded as a base64 blob. This is subject to change as **SpeckleBlender** develops.
- If a `renderMaterial` property is found, **SpeckleBlender** will create a material named using the sub-property `renderMaterial.name`. If a material with that name already exists in Blender, **SpeckleBlender** will just assign that existing material to the object. This allows geometry to be updated without having to re-assign and re-create materials.
- Vertex colors are supported. The `colors` list from Speckle meshes is translated to a vertex color layer.
- Speckle properties will be imported as custom properties on Blender objects. Nested dictionaries are expanded to individual properties by flattening their key hierarchy. I.e. `propA:{'propB': {'propC':10, 'propD':'foobar'}}` is flattened to `propA.propB.propC = 10` and `propA.propB.propD = "foobar"`.
- If a `renderMaterial` property is found, **SpeckleBlender** will create a material named using the sub-property `renderMaterial.name`. If a material with that name already exists in Blender, **SpeckleBlender** will just assign that existing material to the object. This allows geometry to be updated without having to re-assign and re-create materials.
- Receiving vertex colors is supported. The `colors` list from Speckle meshes is translated to a vertex color layer.
- Receive/Send scripts. Allow injecting a custom python function to the receive/send process to automate any blender operations
## Dependency Installation and Compatibility with Other Blender Addons
Upon first launch of the addon, the Speckle connector installs its SpecklePy dependencies in `%appdata%/Speckle/connector_installations` on Windows and `~/.config/Speckle/connector_installations` on Mac.
This is done through our [`installer.py`](https://github.com/specklesystems/speckle-blender/blob/main/bpy_speckle/installer.py). Through pip, we install the correct version of each dependency for your blender python version, host OS, and system architecture.
As such, an internet connection is required for first launch of the connector.
Other blender addons may require dependencies that conflict with specklepy. In these cases, one or both addons may fail to load.
If you suspect you're seeing a conflict, Please uninstall other third party addons one at a time to identify which addon is conflicting.
If you find an addon that conflicts, please try using a different version of that addon (newer or older).
If you can't find a version of an addon that works, please let us know on [our forums](https://speckle.community/) the name of the addon, the versions you've tried, the version of the Speckle connector you've tried, and your OS (win/mac/linux).
## Contributing
+11 -17
View File
@@ -1,30 +1,30 @@
import bpy
from bpy_speckle.installer import ensure_dependencies
ensure_dependencies(f"Blender {bpy.app.version[0]}.{bpy.app.version[1]}")
ensure_dependencies()
from bpy.app.handlers import persistent
from specklepy.logging import metrics
from bpy_speckle.callbacks import *
from bpy_speckle.operators import *
from bpy_speckle.properties import *
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 (Legacy)",
"name": "SpeckleBlender 2.0",
"author": "Speckle Systems",
"version": (0, 2, 0),
"blender": (2, 92, 0),
"location": "3d viewport toolbar (N), under the Speckle tab.",
"description": "(Legacy) Speckle Connector using specklepy 2.0!",
"warning": "This is a legacy add-on. Download the new connector from https://app.speckle.systems/connectors",
"description": "The Speckle Connector using specklepy 2.0!",
"warning": "This add-on is WIP and should be used with caution",
"wiki_url": "https://github.com/specklesystems/speckle-blender",
"category": "Scene",
}
"""
Import SpeckleBlender classes
"""
@@ -34,16 +34,9 @@ Add load handler to initialize Speckle when
loading a Blender file
"""
@persistent
def load_handler(dummy):
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()
bpy.ops.speckle.users_load()
"""
@@ -95,6 +88,7 @@ def register():
def unregister():
bpy.app.handlers.load_post.remove(load_handler)
"""
@@ -1,140 +0,0 @@
from typing import Dict, Optional, Union
import bpy
from attrs import define
from bpy.types import ID, Collection, Object
from specklepy.objects.base import Base
from specklepy.objects.graph_traversal.commit_object_builder import (
ROOT,
CommitObjectBuilder,
)
from specklepy.objects.other import Collection as SCollection
from bpy_speckle.functions import _report
ELEMENTS = "elements"
def _id(native_object: ID) -> str:
# NOTE: to avoid naming collisions, we prefix collections and objects differently
return f"{type(native_object).__name__}:{native_object.name_full}"
def _try_id(native_object: Optional[Union[Collection, Object]]) -> Optional[str]:
return _id(native_object) if native_object else None
def convert_collection_to_speckle(col: Collection) -> SCollection:
converted_collection = SCollection(
name=col.name_full, collectionType="Blender Collection", elements=[]
)
converted_collection.applicationId = _id(col)
color_tag = col.color_tag
if color_tag and color_tag != "NONE":
converted_collection["colorTag"] = col.color_tag
return converted_collection
@define(slots=True)
class BlenderCommitObjectBuilder(CommitObjectBuilder[Object]):
_collections: Dict[str, SCollection]
def __init__(self) -> None:
super().__init__()
self._collections = {}
def include_object(self, conversion_result: Base, native_object: Object) -> None:
# Set the Child -> Parent relationships
parent = native_object.parent
parent_collections = native_object.users_collection
parent_collection = (
parent_collections[0] if len(parent_collections) > 0 else None
) # NOTE: we don't support objects appearing in more than one collection, for now, we will just take the zeroth one
app_id = _id(native_object)
conversion_result.applicationId = app_id
self.converted[app_id] = conversion_result
# in order or priority, direct parent, direct parent collection, root
self.set_relationship(
app_id,
(_try_id(parent), ELEMENTS),
(_try_id(parent_collection), ELEMENTS),
(ROOT, ELEMENTS),
)
# if parent_collection:
# self._include_collection(parent_collection)
def ensure_collection(self, col: Collection) -> SCollection:
id = _id(col)
if id in self._collections:
return self._collections[id] # collection already converted!
# Set the Parent -> Children relationships
for c in col.children:
# NOTE: There's no falling back to the grandparent, if the direct parent collection wasn't converted, then we we fallback to the root
self.set_relationship(_id(c), (id, ELEMENTS), (ROOT, ELEMENTS))
# Set Child -> Parent relationship
# parent = self.find_collection_parent(col)
# self.set_relationship(id, (_try_builder_id(parent), ELEMENTS), (ROOT, ELEMENTS))
converted_collection = convert_collection_to_speckle(col)
self.converted[id] = converted_collection
self._collections[id] = converted_collection
return converted_collection
def build_commit_object(self, root_commit_object: Base) -> None:
assert root_commit_object.applicationId in self.converted
# Create all collections
root_col = self.ensure_collection(bpy.context.scene.collection)
root_col.collectionType = "Scene Collection"
for col in bpy.context.scene.collection.children_recursive:
self.ensure_collection(col)
objects_to_build = set(self.converted.values())
objects_to_build.remove(root_commit_object)
self.apply_relationships(objects_to_build, root_commit_object)
assert isinstance(root_commit_object, SCollection)
# Kill unused collections
def should_remove_unuseful_collection(
col: SCollection,
) -> bool: # TODO: this maybe could be optimised
elements = col.elements
if not elements:
return True
should_remove_this_col = True
i = 0
while i < len(elements):
c = elements[i]
if not isinstance(c, SCollection):
# col has objects (c)
should_remove_this_col = False
i += 1
continue
if should_remove_unuseful_collection(c):
# c is not useful, kill it
del elements[i]
else:
# col has a child (c) with objects
should_remove_this_col = False
i += 1
continue
return should_remove_this_col
if should_remove_unuseful_collection(root_commit_object):
_report(
"WARNING: Only empty collections have been converted!"
) # TODO: consider raising exception here, to halt the send operation
+1 -1
View File
@@ -1,2 +1,2 @@
from .draw_speckle_info import draw_speckle_info
from .on_mesh_edit import scb_on_mesh_edit
from .draw_speckle_info import draw_speckle_info
+1 -1
View File
@@ -1,7 +1,7 @@
"""
Permanent handle on all user clients
"""
from specklepy.api.client import SpeckleClient
from specklepy.core.api.client import SpeckleClient
speckle_clients: list[SpeckleClient] = []
+29
View File
@@ -0,0 +1,29 @@
from typing import Union
from bpy_speckle.convert.to_native import convert_to_native
from specklepy.objects.base import Base
def get_speckle_subobjects(attr: Union[dict, Base], scale: float, name: str) -> list:
subobjects = []
keys = attr.keys() if isinstance(attr, dict) else attr.get_dynamic_member_names()
for key in keys:
if isinstance(attr[key], dict):
subtype = attr[key].get("type", None)
if subtype:
name = f"{name}.{key}"
subobject = convert_to_native(attr[key], name)
subobjects.append(subobject)
props = attr[key].get("properties", None)
if props:
subobjects.extend(get_speckle_subobjects(props, scale, name))
elif hasattr(attr[key], "type"):
subtype = attr[key].type
if subtype:
name = "{}.{}".format(name, key)
subobject = convert_to_native(attr[key], name)
subobjects.append(subobject)
props = attr[key].get("properties", None)
if props:
subobjects.extend(get_speckle_subobjects(props, scale, name))
return subobjects
-22
View File
@@ -1,22 +0,0 @@
IGNORED_PROPERTY_KEYS = {
"id",
"elements",
"displayMesh",
"displayValue",
"speckle_type",
"parameters",
"faces",
"colors",
"vertices",
"renderMaterial",
"textureCoordinates",
"totalChildrenCount",
}
DISPLAY_VALUE_PROPERTY_ALIASES = {"displayValue", "@displayValue"}
ELEMENTS_PROPERTY_ALIASES = {"elements", "@elements"}
OBJECT_NAME_MAX_LENGTH = 62
SPECKLE_ID_LENGTH = 32
OBJECT_NAME_SPECKLE_SEPARATOR = " -- "
OBJECT_NAME_NUMERAL_SEPARATOR = "."
File diff suppressed because it is too large Load Diff
+185 -467
View File
@@ -1,215 +1,134 @@
from typing import Dict, Iterable, List, Optional, Tuple, Union, cast
from typing import Optional
import bpy
from bpy.types import Camera as NCamera
from bpy.types import Curve as NCurve
from bpy.types import Depsgraph
from bpy.types import Mesh as NMesh
from bpy.types import MeshPolygon, Object
from deprecated import deprecated
from mathutils import Matrix as MMatrix
from mathutils import Vector as MVector
from mathutils.geometry import interpolate_bezier
from specklepy.objects import Base
from specklepy.objects.geometry import (
Box,
Curve,
Interval,
Mesh,
Point,
Polyline,
Vector,
)
from specklepy.objects.other import (
BlockDefinition,
BlockInstance,
RenderMaterial,
Transform,
)
from bpy_speckle.blender_commit_object_builder import BlenderCommitObjectBuilder
from bpy_speckle.convert.constants import (
OBJECT_NAME_SPECKLE_SEPARATOR,
SPECKLE_ID_LENGTH,
)
from bpy.types import Depsgraph, MeshVertColor, MeshVertex, Object
from specklepy.objects.geometry import Mesh, Curve, Interval, Box, Point, Polyline
from specklepy.objects.other import *
from bpy_speckle.functions import _report
from bpy_speckle.convert.util import (
ConversionSkippedException,
get_blender_custom_properties,
make_knots,
nurb_make_curve,
to_argb_int,
)
from bpy_speckle.functions import _report
Units: str = "m" # The desired final units to send
UnitsScale: float = (
1 # The scale factor conversions need to apply to position data to get to the desired units
)
UNITS = "m"
CAN_CONVERT_TO_SPECKLE = ("MESH", "CURVE", "EMPTY", "CAMERA", "FONT", "SURFACE", "META")
CAN_CONVERT_TO_SPECKLE = ("MESH", "CURVE", "EMPTY")
def convert_to_speckle(
raw_blender_object: Object,
units_scale: float,
units: str,
depsgraph: Optional[Depsgraph],
) -> Base:
"""
Converts supported 1 blender objects to 1 speckle object (potentially with children)
:param raw_blender_object: the blender object (unevaluated by a Depsgraph) to convert
:param units_scale: The scale factor conversions need to apply to position data to get to the desired units
:param units: The desired final units to send
:param depsgraph: Optional depsgraph if provided will evaluate modifiers on geometry data
:return: The Converted blender object
"""
global Units, UnitsScale
Units = units
UnitsScale = units_scale
blender_type = raw_blender_object.type
def convert_to_speckle(blender_object: Object, scale: float, units: str, desgraph: Optional[Depsgraph]) -> Optional[list]:
global UNITS
UNITS = units
blender_type = blender_object.type
if blender_type not in CAN_CONVERT_TO_SPECKLE:
raise ConversionSkippedException(
f"Objects of type {blender_type} are not supported"
)
return None
blender_object = cast(
Object,
(
raw_blender_object.evaluated_get(depsgraph)
if depsgraph
else raw_blender_object
),
speckle_objects = []
speckle_material = material_to_speckle(blender_object)
if desgraph:
blender_object = blender_object.evaluated_get(desgraph)
converted = None
if blender_type == "MESH":
converted = mesh_to_speckle(blender_object, blender_object.data, scale)
elif blender_type == "CURVE":
converted = icurve_to_speckle(blender_object, blender_object.data, scale)
elif blender_type == "EMPTY":
converted = empty_to_speckle(blender_object, scale)
if not converted:
return None
if isinstance(converted, list):
speckle_objects.extend([c for c in converted if c != None])
else:
speckle_objects.append(converted)
for so in speckle_objects:
so.properties = get_blender_custom_properties(blender_object)
so.applicationId = so.properties.pop("applicationId", None)
if speckle_material:
so["renderMaterial"] = speckle_material
# Set object transform
if blender_type != "EMPTY":
so.properties["transform"] = transform_to_speckle(
blender_object.matrix_world
)
return speckle_objects
def mesh_to_speckle(blender_object: Object, data: bpy.types.Mesh, scale=1.0) -> List[Mesh]:
if data.loop_triangles is None or len(data.loop_triangles) < 1:
data.calc_loop_triangles()
mat = blender_object.matrix_world
verts = [tuple(mat @ x.co * scale) for x in data.vertices]
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),
)
converted: Optional[Base] = None
if blender_type == "MESH":
converted = mesh_to_speckle(blender_object, cast(NMesh, blender_object.data))
elif blender_type == "CURVE":
converted = curve_to_speckle(blender_object, cast(NCurve, blender_object.data))
elif blender_type == "EMPTY":
converted = empty_to_speckle(blender_object)
elif blender_type == "CAMERA":
converted = camera_to_speckle_view(
blender_object, cast(NCamera, blender_object.data)
)
elif blender_type == "FONT" or "SURFACE" or "META":
converted = anything_to_speckle_mesh(blender_object)
if not converted:
raise Exception("Conversion returned None")
if data.uv_layers.active:
for vt in data.uv_layers.active.data:
sm.textureCoordinates.extend([vt.uv.x, vt.uv.y])
converted["properties"] = get_blender_custom_properties(
raw_blender_object
) # NOTE: Depsgraph copies don't have custom properties so we use the raw version
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)
# Set object transform #TODO: this could be deprecated once we add proper geometry instancing support
if blender_type != "EMPTY":
converted["properties"]["transform"] = transform_to_speckle(
blender_object.matrix_world
)
# 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 converted
return [sm]
def mesh_to_speckle(blender_object: Object, data: bpy.types.Mesh) -> Base:
b = Base()
b["name"] = to_speckle_name(blender_object)
b["@displayValue"] = mesh_to_speckle_meshes(blender_object, data)
return b
def mesh_to_speckle_meshes(blender_object: Object, data: bpy.types.Mesh) -> List[Mesh]:
# Categorise polygons by material index
submesh_data: Dict[int, List[MeshPolygon]] = {}
for p in data.polygons:
if p.material_index not in submesh_data:
submesh_data[p.material_index] = []
submesh_data[p.material_index].append(p)
transform = cast(MMatrix, blender_object.matrix_world)
scaled_vertices = [tuple(transform @ x.co * UnitsScale) for x in data.vertices]
# Create Speckle meshes for each material
submeshes = []
index_counter = 0
for i in submesh_data:
index_mapping: Dict[int, int] = {}
# Loop through each polygon, and map indices to their new index in m_verts
mesh_area = 0
m_verts: List[float] = []
m_faces: List[int] = []
m_texcoords: List[float] = []
for face in submesh_data[i]:
u_indices = face.vertices
m_faces.append(len(u_indices))
mesh_area += face.area
for u_index in u_indices:
if u_index not in index_mapping:
# Create mapping between index in blender mesh, and new index in speckle submesh
index_mapping[u_index] = len(m_verts) // 3
vert = scaled_vertices[u_index]
m_verts.append(vert[0])
m_verts.append(vert[1])
m_verts.append(vert[2])
if data.uv_layers.active:
vt = data.uv_layers.active.data[index_counter]
uv = cast(MVector, vt.uv)
m_texcoords.extend([uv.x, uv.y])
m_faces.append(index_mapping[u_index])
index_counter += 1
speckle_mesh = Mesh(
vertices=m_verts,
faces=m_faces,
colors=[],
textureCoordinates=m_texcoords,
units=Units,
area=mesh_area,
bbox=Box(area=0.0, volume=0.0),
)
if i < len(data.materials):
material = data.materials[i]
if material is not None:
speckle_mesh["renderMaterial"] = material_to_speckle(material)
submeshes.append(speckle_mesh)
return submeshes
def bezier_to_speckle(
matrix: MMatrix, spline: bpy.types.Spline, name: Optional[str] = None
) -> Curve:
def bezier_to_speckle(matrix: List[float], spline: bpy.types.Spline, scale: float, name: Optional[str] = None) -> Curve:
degree = 3
closed = spline.use_cyclic_u
points: List[Tuple[MVector]] = []
points = []
for i, bp in enumerate(spline.bezier_points):
if i > 0:
points.append(tuple(matrix @ bp.handle_left * UnitsScale)) # type: ignore
points.append(tuple(matrix @ bp.co * UnitsScale)) # type: ignore
points.append(tuple(matrix @ bp.handle_left * scale))
points.append(tuple(matrix @ bp.co * scale))
if i < len(spline.bezier_points) - 1:
points.append(tuple(matrix @ bp.handle_right * UnitsScale)) # type: ignore
points.append(tuple(matrix @ bp.handle_right * scale))
if closed:
points.extend(
(
tuple(matrix @ spline.bezier_points[-1].handle_right * UnitsScale), # type: ignore
tuple(matrix @ spline.bezier_points[0].handle_left * UnitsScale), # type: ignore
tuple(matrix @ spline.bezier_points[0].co * UnitsScale), # type: ignore
tuple(matrix @ spline.bezier_points[-1].handle_right * scale),
tuple(matrix @ spline.bezier_points[0].handle_left * scale),
tuple(matrix @ spline.bezier_points[0].co * scale),
)
)
num_points = len(points)
flattened_points = []
for row in points:
flattened_points.extend(row)
flattend_points = []
for row in points: flattend_points.extend(row)
knot_count = num_points + degree - 1
knots = [0] * knot_count
@@ -223,232 +142,99 @@ def bezier_to_speckle(
name=name,
degree=degree,
closed=spline.use_cyclic_u,
periodic=not spline.use_endpoint_u,
points=flattened_points,
periodic=spline.use_cyclic_u,
points=flattend_points,
weights=[1] * num_points,
knots=knots,
rational=True,
rational=False,
area=0,
volume=0,
length=length,
domain=domain,
units=Units,
units=UNITS,
bbox=Box(area=0.0, volume=0.0),
displayValue=bezier_to_speckle_polyline(matrix, spline, length),
)
def nurbs_to_speckle(
matrix: MMatrix, spline: bpy.types.Spline, name: Optional[str] = None
) -> Curve:
degree = spline.order_u - 1
def nurbs_to_speckle(matrix: List[float], spline: bpy.types.Spline, scale: float, name: Optional[str] = None) -> Curve:
knots = make_knots(spline)
points = [tuple(matrix @ pt.co.xyz * scale) for pt in spline.points]
degree = spline.order_u - 1
length = spline.calc_length()
domain = Interval(start=0, end=length, totalChildrenCount=0)
weights = [pt.weight for pt in spline.points]
is_rational = all(w == weights[0] for w in weights)
points = [tuple(matrix @ pt.co.xyz * UnitsScale) for pt in spline.points] # type: ignore
flattened_points = []
for row in points:
flattened_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
flattened_points.append(flattened_points[i + 0])
flattened_points.append(flattened_points[i + 1])
flattened_points.append(flattened_points[i + 2])
for i in range(0, degree):
weights.append(weights[i])
flattend_points = []
for row in points: flattend_points.extend(row)
return Curve(
name=name,
degree=degree,
closed=spline.use_cyclic_u,
periodic=not spline.use_endpoint_u,
points=flattened_points,
weights=weights,
periodic=spline.use_cyclic_u,
points=flattend_points,
weights=[pt.weight for pt in spline.points],
knots=knots,
rational=is_rational,
rational=False,
area=0,
volume=0,
length=length,
domain=domain,
units=Units,
units=UNITS,
bbox=Box(area=0.0, volume=0.0),
displayValue=nurbs_to_speckle_polyline(matrix, spline, length),
)
def nurbs_to_speckle_polyline(
matrix: MMatrix, spline: bpy.types.Spline, length: Optional[float] = None
) -> Polyline:
"""
Samples a nurbs curve with resolution_u creating a polyline
"""
points: List[float] = []
sampled_points = nurb_make_curve(spline, spline.resolution_u, 3)
for i in range(0, len(sampled_points), 3):
scaled_point = cast(
Vector,
matrix
@ MVector(
(sampled_points[i + 0], sampled_points[i + 1], sampled_points[i + 2])
)
* UnitsScale,
)
def poly_to_speckle(matrix: List[float], spline: bpy.types.Spline, scale: float, name: Optional[str] = None) -> Polyline:
points = [tuple(matrix @ pt.co.xyz * scale) for pt in spline.points]
points.append(scaled_point.x)
points.append(scaled_point.y)
points.append(scaled_point.z)
length = length or spline.calc_length()
domain = Interval(start=0, end=length, totalChildrenCount=0)
return Polyline(
value=points, closed=spline.use_cyclic_u, domain=domain, area=0, len=length
)
# Inspired by https://blender.stackexchange.com/a/689 (CC BY-SA 3.0)
def bezier_to_speckle_polyline(
matrix: MMatrix, spline: bpy.types.Spline, length: Optional[float] = None
) -> Optional[Polyline]:
"""
Samples a Bézier curve with resolution_u creating a polyline
"""
segments = len(spline.bezier_points)
if segments < 2:
return None
R = spline.resolution_u + 1
points = []
if not spline.use_cyclic_u:
segments -= 1
points: List[float] = []
for i in range(segments):
inext = (i + 1) % len(spline.bezier_points)
knot1 = spline.bezier_points[i].co
handle1 = spline.bezier_points[i].handle_right
handle2 = spline.bezier_points[inext].handle_left
knot2 = spline.bezier_points[inext].co
_points = interpolate_bezier(knot1, handle1, handle2, knot2, R)
for p in _points:
scaled_point = matrix @ p * UnitsScale
points.append(scaled_point.x)
points.append(scaled_point.y)
points.append(scaled_point.z)
length = length or spline.calc_length()
domain = Interval(start=0, end=length, totalChildrenCount=0)
return Polyline(
value=points, closed=spline.use_cyclic_u, domain=domain, area=0, len=length
)
_QUICK_TEST_NAME_LENGTH = SPECKLE_ID_LENGTH + len(OBJECT_NAME_SPECKLE_SEPARATOR)
def to_speckle_name(blender_object: bpy.types.ID) -> str:
does_name_contain_id = (
len(blender_object.name) > _QUICK_TEST_NAME_LENGTH
and OBJECT_NAME_SPECKLE_SEPARATOR in blender_object.name
)
if does_name_contain_id:
return blender_object.name.rsplit(OBJECT_NAME_SPECKLE_SEPARATOR, 1)[0]
else:
return blender_object.name
def poly_to_speckle(
matrix: MMatrix, spline: bpy.types.Spline, name: Optional[str] = None
) -> Polyline:
points = [tuple(matrix @ pt.co.xyz * UnitsScale) for pt in spline.points] # type: ignore
flattened_points = []
for row in points:
flattened_points.extend(row)
flattend_points = []
for row in points: flattend_points.extend(row)
length = spline.calc_length()
domain = Interval(start=0, end=length, totalChildrenCount=0)
return Polyline(
name=name,
closed=bool(spline.use_cyclic_u),
value=list(flattened_points),
value=list(flattend_points),
length=length,
domain=domain,
bbox=Box(area=0.0, volume=0.0),
area=0,
units=Units,
units=UNITS,
)
def curve_to_speckle(blender_object: Object, data: bpy.types.Curve) -> Base:
b = Base()
(meshes, curves) = curve_to_speckle_geometry(blender_object, data)
if meshes:
b["@displayValue"] = meshes
def icurve_to_speckle(blender_object: Object, data: bpy.types.Curve, scale=1.0) -> Optional[List[Base]]:
UNITS = "m" if bpy.context.scene.unit_settings.system == "METRIC" else "ft"
b["name"] = to_speckle_name(blender_object)
b["@elements"] = curves
return b
if blender_object.type != "CURVE":
return None
blender_object = blender_object.evaluated_get(bpy.context.view_layer.depsgraph)
def curve_to_speckle_geometry(
blender_object: Object, data: bpy.types.Curve
) -> Tuple[List[Mesh], List[Base]]:
assert blender_object.type == "CURVE"
mat = blender_object.matrix_world
blender_object = cast(
Object, blender_object.evaluated_get(bpy.context.view_layer.depsgraph)
)
curves = []
matrix = cast(MMatrix, blender_object.matrix_world)
meshes: List[Mesh] = []
curves: List[Base] = []
# TODO: Could we support this better?
if data.bevel_mode == "OBJECT" and data.bevel_object is not None:
meshes = mesh_to_speckle_meshes(blender_object, blender_object.to_mesh())
if data.bevel_mode == "OBJECT" and data.bevel_object != None:
mesh = mesh_to_speckle(blender_object, blender_object.to_mesh(), scale)
curves.extend(mesh)
for spline in data.splines:
if spline.type == "BEZIER":
curves.append(
bezier_to_speckle(matrix, spline, to_speckle_name(blender_object))
)
curves.append(bezier_to_speckle(mat, spline, scale, blender_object.name))
elif spline.type == "NURBS":
curves.append(
nurbs_to_speckle(matrix, spline, to_speckle_name(blender_object))
)
curves.append(nurbs_to_speckle(mat, spline, scale, blender_object.name))
elif spline.type == "POLY":
curves.append(
poly_to_speckle(matrix, spline, to_speckle_name(blender_object))
)
curves.append(poly_to_speckle(mat, spline, scale, blender_object.name))
return (meshes, curves)
return curves
def anything_to_speckle_mesh(blender_object: Object) -> Base:
mesh = mesh_to_speckle(blender_object, blender_object.to_mesh())
blender_object.to_mesh_clear()
return mesh
@deprecated
def ngons_to_speckle_polylines(
blender_object: Object, data: bpy.types.Mesh
) -> Optional[List[Polyline]]:
def ngons_to_speckle_polylines(blender_object: Object, data: bpy.types.Mesh, scale=1.0) -> Optional[List[Polyline]]:
UNITS = "m" if bpy.context.scene.unit_settings.system == "METRIC" else "ft"
if blender_object.type != "MESH":
@@ -461,13 +247,13 @@ def ngons_to_speckle_polylines(
for i, poly in enumerate(data.polygons):
value = []
for v in poly.vertices:
value.extend(mat @ verts[v].co * UnitsScale) # type: ignore
value.extend(mat @ verts[v].co * scale)
domain = Interval(start=0, end=1)
poly = Polyline(
name="{}_{}".format(blender_object.name, i),
closed=True,
value=value,
value=value, # magic (flatten list of tuples)
length=0,
domain=domain,
bbox=Box(area=0.0, volume=0.0),
@@ -480,146 +266,78 @@ def ngons_to_speckle_polylines(
return polylines
def material_to_speckle(blender_mat: bpy.types.Material) -> RenderMaterial:
def material_to_speckle(blender_object: Object) -> Optional[RenderMaterial]:
"""Create and return a render material from a blender object"""
if not getattr(blender_object.data, "materials", None):
return None
blender_mat: bpy.types.Material = blender_object.data.materials[0]
if not blender_mat:
return None
speckle_mat = RenderMaterial()
speckle_mat.name = blender_mat.name
if blender_mat.use_nodes:
if blender_mat.node_tree.nodes.get("Principled BSDF"):
inputs = blender_mat.node_tree.nodes["Principled BSDF"].inputs
emission_color = "Emission" if "Emission" in inputs else "Emission Color" # type: ignore
if blender_mat.use_nodes is True and blender_mat.node_tree.nodes.get(
"Principled BSDF"
):
inputs = blender_mat.node_tree.nodes["Principled BSDF"].inputs
speckle_mat.diffuse = to_argb_int(inputs["Base Color"].default_value)
speckle_mat.emissive = to_argb_int(inputs["Emission"].default_value)
speckle_mat.roughness = inputs["Roughness"].default_value
speckle_mat.metalness = inputs["Metallic"].default_value
speckle_mat.opacity = inputs["Alpha"].default_value
speckle_mat.diffuse = to_argb_int(inputs["Base Color"].default_value) # type: ignore
speckle_mat.emissive = to_argb_int(inputs[emission_color].default_value) # type: ignore
speckle_mat.roughness = inputs["Roughness"].default_value # type: ignore
speckle_mat.metalness = inputs["Metallic"].default_value # type: ignore
speckle_mat.opacity = inputs["Alpha"].default_value # type: ignore
return speckle_mat
elif blender_mat.node_tree.nodes.get("Diffuse BSDF"):
inputs = blender_mat.node_tree.nodes["Diffuse BSDF"].inputs
speckle_mat.diffuse = to_argb_int(inputs["Color"].default_value) # type: ignore
speckle_mat.roughness = inputs["Roughness"].default_value # type: ignore
return speckle_mat
# TODO: Support more shaders
# fallback to standard material props
speckle_mat.diffuse = to_argb_int(blender_mat.diffuse_color) # type: ignore
speckle_mat.metalness = blender_mat.metallic
speckle_mat.roughness = blender_mat.roughness
else:
speckle_mat.diffuse = to_argb_int(blender_mat.diffuse_color)
speckle_mat.metalness = blender_mat.metallic
speckle_mat.roughness = blender_mat.roughness
return speckle_mat
def camera_to_speckle_view(blender_object: Object, data: NCamera) -> Base:
if data.type != "PERSP":
raise Exception(f"Cameras of type {data.type} are not currently supported")
matrix = cast(MMatrix, blender_object.matrix_world)
up = cast(MVector, matrix.col[1].xyz)
forwards = cast(MVector, -matrix.col[2].xyz)
translation = matrix.translation
view = Base.of_type(
"Objects.BuiltElements.View:Objects.BuiltElements.View3D"
) # HACK: views are not in specklepy yet!
view.name = to_speckle_name(blender_object)
view.origin = vector_to_speckle_point(translation)
view.upDirection = vector_to_speckle(up)
view.forwardDirection = vector_to_speckle(forwards)
view.target = vector_to_speckle_point(forwards) # TODO: do these need to be scaled?
view.units = Units
view.isOrthogonal = False
return view
def vector_to_speckle_point(xyz: MVector) -> Point:
return Point(
x=xyz.x * UnitsScale,
y=xyz.y * UnitsScale,
z=xyz.z * UnitsScale,
units=Units,
)
def vector_to_speckle(xyz: MVector) -> Vector:
return Vector(
x=xyz.x * UnitsScale,
y=xyz.y * UnitsScale,
z=xyz.z * UnitsScale,
units=Units,
)
def transform_to_speckle(
blender_transform: Union[Iterable[Iterable[float]], MMatrix]
) -> Transform:
iterable_transform = cast(
Iterable[Iterable[float]], blender_transform
) # NOTE: Matrix are iterable, even if type hinting says they are not
value = [y for x in iterable_transform for y in x]
def transform_to_speckle(blender_transform: List[float], scale=1.0) -> Transform:
value = [y for x in blender_transform for y in x]
# scale the translation
for i in (3, 7, 11):
value[i] *= UnitsScale
value[i] *= scale
return Transform(value=value, units=Units)
return Transform(value=value, units=UNITS)
def block_def_to_speckle(blender_definition: bpy.types.Collection) -> BlockDefinition:
geometryBuilder = BlenderCommitObjectBuilder()
def block_def_to_speckle(blender_definition: bpy.types.Collection, scale=1.0) -> BlockDefinition:
geometry = []
for geo in blender_definition.objects:
try:
c = convert_to_speckle(geo, UnitsScale, Units, None)
geometryBuilder.include_object(c, geo)
except ConversionSkippedException as ex:
_report(
f"Skipped converting '{geo.name_full}' inside collection instance: '{ex}"
)
except Exception as ex:
_report(
f"Failed to converted '{geo.name_full}' inside collection instance: '{ex}'"
)
dummyRoot = Base()
geometryBuilder.apply_relationships(geometryBuilder.converted.values(), dummyRoot)
geometry.extend(convert_to_speckle(geo, scale, UNITS, None))
block_def = BlockDefinition(
units=Units,
name=to_speckle_name(blender_definition),
geometry=dummyRoot["@elements"],
basePoint=Point(units=Units),
units=UNITS,
name=blender_definition.name,
geometry=geometry,
basePoint=Point(units=UNITS),
)
# blender_props = get_blender_custom_properties(blender_definition)
# block_def.applicationId = blender_props.pop("applicationId", None) #TODO: remove?
blender_props = get_blender_custom_properties(blender_definition)
block_def.applicationId = blender_props.pop("applicationId", None)
return block_def
def block_instance_to_speckle(blender_instance: Object) -> BlockInstance:
def block_instance_to_speckle(blender_instance: Object, scale=1.0) -> BlockInstance:
return BlockInstance(
blockDefinition=block_def_to_speckle(blender_instance.instance_collection),
blockDefinition=block_def_to_speckle(
blender_instance.instance_collection, scale
),
transform=transform_to_speckle(blender_instance.matrix_world),
name=to_speckle_name(blender_instance),
units=Units,
name=blender_instance.name,
units=UNITS,
)
def empty_to_speckle(blender_object: Object) -> Union[BlockInstance, Base]:
def empty_to_speckle(blender_object: Object, scale=1.0) -> Optional[BlockInstance]:
# probably an instance collection (block) so let's try it
if (
blender_object.instance_collection
and blender_object.instance_type == "COLLECTION"
):
# Empty -> Block
return block_instance_to_speckle(blender_object)
else:
# Empty -> Point
wrapper = Base()
wrapper["@displayValue"] = matrix_to_speckle_point(
cast(MMatrix, blender_object.matrix_world)
try:
geo = blender_object.instance_collection.objects.items()
return block_instance_to_speckle(blender_object, scale)
except AttributeError as err:
_report(
f"No instance collection found in empty. Skipping object {blender_object.name}"
)
return wrapper
def matrix_to_speckle_point(matrix: MMatrix, units_scale: float = 1.0) -> Point:
transformed_pos = cast(MVector, matrix @ MVector((0, 0, 0)) * units_scale)
return Point(x=transformed_pos.x, y=transformed_pos.y, z=transformed_pos.z)
return None
+102 -330
View File
@@ -1,25 +1,31 @@
import math
from typing import Any, Dict, Optional, Tuple, Union, cast
import bpy
import idprop
from typing import Tuple
from bmesh.types import BMesh
from bpy.types import Collection as BCollection
from bpy.types import Material, Node, Object, ShaderNodeVertexColor
import bpy, struct, idprop
from specklepy.objects.base import Base
from specklepy.objects.geometry import Mesh
from specklepy.objects.graph_traversal.traversal import TraversalContext
from specklepy.objects.other import RenderMaterial
from bpy_speckle.convert.constants import IGNORED_PROPERTY_KEYS
from specklepy.serialization.base_object_serializer import BaseObjectSerializer
from bpy_speckle.functions import _report
from bpy.types import Object
IGNORED_PROPERTY_KEYS = {
"id",
"elements",
"displayMesh",
"displayValue",
"speckle_type",
"parameters",
"faces",
"colors",
"vertices",
"renderMaterial",
"textureCoordinates",
"totalChildrenCount"
}
class ConversionSkippedException(Exception):
pass
def to_rgba(argb_int: int) -> Tuple[float, float, float, float]:
def to_rgba(argb_int: int) -> Tuple[float]:
"""Converts the int representation of a colour into a percent RGBA tuple"""
alpha = ((argb_int >> 24) & 255) / 255
red = ((argb_int >> 16) & 255) / 255
@@ -29,70 +35,63 @@ def to_rgba(argb_int: int) -> Tuple[float, float, float, float]:
return (red, green, blue, alpha)
def to_argb_int(rgba_color: list[float]) -> int:
def to_argb_int(diffuse_colour) -> int:
"""Converts an RGBA array to an ARGB integer"""
argb_color = rgba_color[-1:] + rgba_color[:3]
int_color = [int(val * 255) for val in argb_color]
return int.from_bytes(int_color, byteorder="big", signed=True)
def 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}"
)
diffuse_colour = diffuse_colour[-1:] + diffuse_colour[:3]
diffuse_colour = [int(val * 255) for val in diffuse_colour]
return int.from_bytes(diffuse_colour, byteorder="big", signed=True)
def add_custom_properties(speckle_object: Base, blender_object: Object):
if blender_object is None:
return
serializer = BaseObjectSerializer()
blender_object["_speckle_type"] = type(speckle_object).__name__
app_id = getattr(speckle_object, "applicationId", None)
if app_id:
blender_object["applicationId"] = speckle_object.applicationId
keys = (
speckle_object.get_dynamic_member_names()
if "Geometry" in speckle_object.speckle_type
else (set(speckle_object.get_member_names()) - IGNORED_PROPERTY_KEYS)
)
keys = speckle_object.get_dynamic_member_names() if "Geometry" in speckle_object.speckle_type else (set(speckle_object.get_member_names()) - IGNORED_PROPERTY_KEYS)
for key in keys:
val = getattr(speckle_object, key, None)
if val is None:
continue
if isinstance(val, (int, str, float)):
set_custom_property(key, val, blender_object)
blender_object[key] = val
elif key == "properties" and isinstance(val, Base):
val["applicationId"] = None
add_custom_properties(val, blender_object)
elif isinstance(val, list):
items = [item for item in val if not isinstance(item, Base)]
if items:
set_custom_property(key, items, blender_object)
elif isinstance(val, dict):
for k, v in val.items():
blender_object[key] = items
elif isinstance(val,dict):
for (k,v) in val.items():
if not isinstance(v, Base):
set_custom_property(k, v, blender_object)
blender_object[k] = v
def render_material_to_native(speckle_mat: RenderMaterial) -> Material:
mat_name = speckle_mat.name
def add_blender_material(speckle_object: Base, blender_object: Object) -> None:
"""Add material to a blender object if the corresponding speckle object has a render material"""
if blender_object.data is None:
return
speckle_mat = getattr(
speckle_object,
"renderMaterial",
getattr(speckle_object, "@renderMaterial", None),
)
if not speckle_mat:
return
mat_name = getattr(speckle_mat, "name", None) or speckle_mat.__dict__.get("@name")
if not mat_name:
mat_name = speckle_mat.applicationId or speckle_mat.id or speckle_mat.get_id()
blender_mat = bpy.data.materials.get(mat_name)
if blender_mat is None:
if not blender_mat:
blender_mat = bpy.data.materials.new(mat_name)
# for now, we're not updating these materials. as per tom's suggestion, we should have a toggle
@@ -100,61 +99,16 @@ def render_material_to_native(speckle_mat: RenderMaterial) -> Material:
blender_mat.use_nodes = True
inputs = blender_mat.node_tree.nodes["Principled BSDF"].inputs
inputs["Base Color"].default_value = to_rgba(speckle_mat.diffuse) # type: ignore
inputs["Roughness"].default_value = speckle_mat.roughness # type: ignore
inputs["Metallic"].default_value = speckle_mat.metalness # type: ignore
inputs["Alpha"].default_value = speckle_mat.opacity # type: ignore
inputs["Base Color"].default_value = to_rgba(speckle_mat.diffuse)
inputs["Emission"].default_value = to_rgba(speckle_mat.emissive)
inputs["Roughness"].default_value = speckle_mat.roughness
inputs["Metallic"].default_value = speckle_mat.metalness
inputs["Alpha"].default_value = speckle_mat.opacity
# Blender >=4.0 use "Emission Color"
emission_color = "Emission" if "Emission" in inputs else "Emission Color" # type: ignore
inputs[emission_color].default_value = to_rgba(speckle_mat.emissive) # type: ignore
if speckle_mat.opacity < 1.0:
if speckle_mat.opacity < 1:
blender_mat.blend_method = "BLEND"
return blender_mat
_vertex_color_material: Optional[Material] = None
def get_vertex_color_material() -> Material:
global _vertex_color_material
# see https://stackoverflow.com/a/69807985
if not _vertex_color_material:
_vertex_color_material = bpy.data.materials.new("Vertex Color Material")
_vertex_color_material.use_nodes = True
nodes = _vertex_color_material.node_tree.nodes
principled_bsdf_node = cast(Node, nodes.get("Principled BSDF"))
if "VERTEX_COLOR" not in [node.type for node in nodes]:
vertex_color_node = cast(
ShaderNodeVertexColor, nodes.new(type="ShaderNodeVertexColor")
)
else:
vertex_color_node = cast(ShaderNodeVertexColor, nodes.get("Vertex Color"))
vertex_color_node.layer_name = "Col"
links = _vertex_color_material.node_tree.links
_ = links.new(vertex_color_node.outputs[0], principled_bsdf_node.inputs[0])
return _vertex_color_material
def get_render_material(speckle_object: Base) -> Optional[RenderMaterial]:
"""Trys to get a RenderMaterial on given speckle_object"""
speckle_mat = getattr(
speckle_object,
"renderMaterial",
getattr(speckle_object, "@renderMaterial", None),
)
if isinstance(speckle_mat, RenderMaterial):
return speckle_mat
return None
blender_object.data.materials.append(blender_mat)
def add_vertices(speckle_mesh: Mesh, blender_mesh: BMesh, scale=1.0):
@@ -170,14 +124,10 @@ 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,
indexOffset: int,
materialIndex: int = 0,
smooth: bool = True,
):
def add_faces(speckle_mesh: Mesh, blender_mesh: BMesh, smooth=False):
sfaces = speckle_mesh.faces
if sfaces and len(sfaces) > 0:
@@ -190,24 +140,30 @@ def add_faces(
i += 1
try:
f = blender_mesh.faces.new(
[blender_mesh.verts[x + indexOffset] for x in sfaces[i : i + n]]
[blender_mesh.verts[int(x)] 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):
scolors = speckle_mesh.colors
if scolors:
colors = []
if len(scolors) > 0:
for i in range(len(scolors)):
argb = int(scolors[i])
(a, r, g, b) = argb_split(argb)
col = int(scolors[i])
(a, r, g, b) = [
int(x) for x in struct.unpack("!BBBB", struct.pack("!i", col))
]
colors.append(
(
float(r) / 255.0,
@@ -226,15 +182,6 @@ def add_colors(speckle_mesh: Mesh, blender_mesh: BMesh):
loop[color_layer] = colors[loop.vert.index]
def argb_split(argb: int) -> Tuple[int, int, int, int]:
alpha = (argb >> 24) & 0xFF
red = (argb >> 16) & 0xFF
green = (argb >> 8) & 0xFF
blue = argb & 0xFF
return (alpha, red, green, blue)
def add_uv_coords(speckle_mesh: Mesh, blender_mesh: BMesh):
s_uvs = speckle_mesh.textureCoordinates
if not s_uvs:
@@ -244,11 +191,12 @@ def add_uv_coords(speckle_mesh: Mesh, blender_mesh: BMesh):
if len(s_uvs) // 2 == len(blender_mesh.verts):
uv.extend(
(float(s_uvs[i]), float(s_uvs[i + 1])) for i in range(0, len(s_uvs), 2)
(float(s_uvs[i]), float(s_uvs[i + 1]))
for i in range(0, len(s_uvs), 2)
)
else:
_report(
f"Failed to match UV coordinates to vert data. Blender mesh verts: {len(blender_mesh.verts)}, Speckle UVs: {len(s_uvs) // 2}"
f"Failed to match UV coordinates to vert data. Blender mesh verts: {len(blender_mesh.verts)}, Speckle UVs * 2: {len(s_uvs) * 2}"
)
return
@@ -256,9 +204,9 @@ def add_uv_coords(speckle_mesh: Mesh, blender_mesh: BMesh):
uv_layer = blender_mesh.loops.layers.uv.verify()
for f in blender_mesh.faces:
for loop in f.loops:
luv = loop[uv_layer]
luv.uv = uv[loop.vert.index]
for l in f.loops:
luv = l[uv_layer]
luv.uv = uv[l.vert.index]
except:
_report("Failed to decode texture coordinates.")
raise
@@ -267,7 +215,8 @@ def add_uv_coords(speckle_mesh: Mesh, blender_mesh: BMesh):
ignored_keys = {
"id",
"speckle",
"speckle_type" "_speckle_type",
"speckle_type"
"_speckle_type",
"_speckle_name",
"_speckle_transform",
"_RNA_UI",
@@ -277,10 +226,8 @@ ignored_keys = {
"_chunkable",
}
def get_blender_custom_properties(obj, max_depth: int = 63):
"""Recursively grabs custom properties on blender objects. Max depth is determined by the max allowed by Newtonsoft.NET, don't exceed unless you know what you're doing"""
if max_depth <= 0:
def get_blender_custom_properties(obj, max_depth=1000):
if max_depth < 0:
return obj
if hasattr(obj, "keys"):
@@ -292,45 +239,45 @@ def get_blender_custom_properties(obj, max_depth: int = 63):
}
if isinstance(obj, (list, tuple, idprop.types.IDPropertyArray)):
return [get_blender_custom_properties(o, max_depth - 1) for o in obj] # type: ignore
return [get_blender_custom_properties(o, max_depth - 1) for o in obj]
return obj
"""
Python implementation of Blender's NURBS curve generation for to Speckle conversion
from: https://blender.stackexchange.com/a/34276
based on https://projects.blender.org/blender/blender/src/branch/main/source/blender/blenkernel/intern/curve.cc (check old version)
"""
def macro_knotsu(nu: bpy.types.Spline) -> int:
def macro_knotsu(nu):
return nu.order_u + nu.point_count_u + (nu.order_u - 1 if nu.use_cyclic_u else 0)
def macro_segmentsu(nu: bpy.types.Spline) -> int:
def macro_segmentsu(nu):
return nu.point_count_u if nu.use_cyclic_u else nu.point_count_u - 1
def make_knots(nu: bpy.types.Spline) -> list[float]:
knots = [0.0] * macro_knotsu(nu)
def make_knots(nu):
knots = [0.0] * (4 + macro_knotsu(nu))
flag = nu.use_endpoint_u + (nu.use_bezier_u << 1)
if nu.use_cyclic_u:
calc_knots(knots, nu.point_count_u, nu.order_u, 0)
makecyclicknots(knots, nu.point_count_u, nu.order_u)
else:
calc_knots(knots, nu.point_count_u, nu.order_u, flag)
return knots
def calc_knots(knots: list[float], point_count: int, order: int, flag: int) -> None:
def calc_knots(knots, point_count, order, flag):
pts_order = point_count + order
if flag == 1: # CU_NURB_ENDPOINT
if flag == 1:
k = 0.0
for a in range(1, pts_order + 1):
knots[a - 1] = k
if a >= order and a <= point_count:
k += 1.0
elif flag == 2: # CU_NURB_BEZIER
elif flag == 2:
if order == 4:
k = 0.34
for a in range(pts_order):
@@ -343,199 +290,24 @@ def calc_knots(knots: list[float], point_count: int, order: int, flag: int) -> N
k += 0.5
knots[a] = math.floor(k)
else:
for a in range(1, len(knots) - 1):
knots[a] = a - 1
knots[-1] = knots[-2]
for a in range(pts_order):
knots[a] = a
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
def makecyclicknots(knots, point_count, order):
order2 = order - 1
# this is for float inaccuracy
if t < knots[0]:
t = knots[0]
elif t > knots[opp2]:
t = knots[opp2]
# this part is order '1'
o2 = order + 1
for i in range(opp2):
if knots[i] != knots[i + 1] and t >= knots[i] and t <= knots[i + 1]:
basis[i] = 1.0
i1 = i - o2
if i1 < 0:
i1 = 0
i2 = i
i += 1
while i < opp2:
basis[i] = 0.0
i += 1
break
else:
basis[i] = 0.0
basis[i] = 0.0 # type: ignore
# this is order 2, 3, ...
for j in range(2, order + 1):
if i2 + j >= orderpluspnts:
i2 = opp2 - j
for i in range(i1, i2 + 1):
if basis[i] != 0.0:
d = ((t - knots[i]) * basis[i]) / (knots[i + j - 1] - knots[i])
else:
d = 0.0
if basis[i + 1] != 0.0:
e = ((knots[i + j] - t) * basis[i + 1]) / (knots[i + j] - knots[i + 1])
else:
e = 0.0
basis[i] = d + e
start = 1000
end = 0
for i in range(i1, i2 + 1):
if basis[i] > 0.0:
end = i
if start == 1000:
start = i
return start, end
def nurb_make_curve(nu: bpy.types.Spline, resolu: int, stride: int = 3) -> list[float]:
""" "BKE_nurb_makeCurve"""
EPS = 1e-6
coord_index = istart = iend = 0
coord_array = [0.0] * (3 * nu.resolution_u * macro_segmentsu(nu))
sum_array = [0] * nu.point_count_u
basisu = [0.0] * macro_knotsu(nu)
knots = make_knots(nu)
resolu = resolu * macro_segmentsu(nu)
ustart = knots[nu.order_u - 1]
uend = (
knots[nu.point_count_u + nu.order_u - 1]
if nu.use_cyclic_u
else knots[nu.point_count_u]
)
ustep = (uend - ustart) / (resolu - (0 if nu.use_cyclic_u else 1))
cycl = nu.order_u - 1 if nu.use_cyclic_u else 0
u = ustart
while resolu:
resolu -= 1
istart, iend = basis_nurb(
u, nu.order_u, nu.point_count_u + cycl, knots, basisu, istart, iend
)
# /* calc sum */
sumdiv = 0.0
sum_index = 0
pt_index = istart - 1
for i in range(istart, iend + 1):
if i >= nu.point_count_u:
pt_index = i - nu.point_count_u
else:
pt_index += 1
sum_array[sum_index] = basisu[i] * nu.points[pt_index].co[3] # type: ignore
sumdiv += sum_array[sum_index]
sum_index += 1
if (sumdiv != 0.0) and (sumdiv < 1.0 - EPS or sumdiv > 1.0 + EPS):
sum_index = 0
for i in range(istart, iend + 1):
sum_array[sum_index] /= sumdiv # type: ignore
sum_index += 1
coord_array[coord_index : coord_index + 3] = (0.0, 0.0, 0.0)
sum_index = 0
pt_index = istart - 1
for i in range(istart, iend + 1):
if i >= nu.point_count_u:
pt_index = i - nu.point_count_u
else:
pt_index += 1
if sum_array[sum_index] != 0.0:
for j in range(3):
coord_array[coord_index + j] += (
sum_array[sum_index] * nu.points[pt_index].co[j]
)
sum_index += 1
coord_index += stride
u += ustep
return coord_array
def link_object_to_collection_nested(obj: Object, col: BCollection):
if obj.name not in col.objects: # type: ignore
col.objects.link(obj)
for child in obj.children:
link_object_to_collection_nested(child, col)
def add_to_hierarchy(
converted: Union[Object, BCollection],
traversalContext: "TraversalContext",
converted_objects: Dict[str, Union[Object, BCollection]],
preserve_transform: bool,
) -> None:
nextParent = traversalContext.parent
# Traverse up the tree to find a direct parent object, and a containing collection
parent_collection: Optional[BCollection] = None
parent_object: Optional[Object] = None
while nextParent:
if nextParent.current.id in converted_objects:
c = converted_objects[nextParent.current.id]
if isinstance(c, BCollection):
parent_collection = c
if order > 2:
b = point_count + order2
for a in range(1, order2):
if knots[b] != knots[b - a]:
break
else: # isinstance(c, Object):
parent_object = parent_object or c
nextParent = nextParent.parent
if a == order2:
knots[point_count + order - 2] += 1.0
# If no containing collection is found, fall back to the scene collection
if not parent_collection:
parent_collection = bpy.context.scene.collection
if isinstance(converted, Object):
if parent_object:
set_parent(converted, parent_object, preserve_transform)
link_object_to_collection_nested(converted, parent_collection)
elif converted.name not in parent_collection.children.keys():
parent_collection.children.link(converted)
def set_parent(child: Object, parent: Object, preserve_transform: bool = False) -> None:
if preserve_transform:
previous = child.matrix_world.copy() # type: ignore
child.parent = parent
child.matrix_world = previous
else:
child.parent = parent
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
+51 -28
View File
@@ -1,13 +1,32 @@
from typing import Callable
from bpy_speckle.clients import speckle_clients
from specklepy.objects.base import Base
from specklepy.objects.graph_traversal.traversal import GraphTraversal, TraversalRule
from specklepy.objects.units import get_scale_factor_to_meters, get_units_from_string
"""
Speckle functions
"""
from bpy_speckle.convert.constants import ELEMENTS_PROPERTY_ALIASES
unit_scale = {
"meters": 1.0,
"centimeters": 0.01,
"millimeters": 0.001,
"inches": 0.0254,
"feet": 0.3048,
"kilometers": 1000.0,
"mm": 0.001,
"cm": 0.01,
"m": 1.0,
"km": 1000.0,
"in": 0.0254,
"ft": 0.3048,
"yd": 0.9144,
"mi": 1609.340,
}
"""
Utility functions
"""
def _report(msg: object) -> None:
def _report(msg):
"""
Function for printing messages to the console
"""
@@ -15,35 +34,39 @@ def _report(msg: object) -> None:
def get_scale_length(units: str) -> float:
"""Returns a scalar to convert distance values from one unit system to meters"""
return get_scale_factor_to_meters(get_units_from_string(units))
if units.lower() in unit_scale.keys():
return unit_scale[units]
_report("Units <{}> are not supported.".format(units))
return 1.0
def get_default_traversal_func(
can_convert_to_native: Callable[[Base], bool]
) -> GraphTraversal:
"""
Client, user, and stream functions
"""
def _check_speckle_client_user_stream(scene):
"""
Traversal func for traversing a speckle commit object
Verify that there is a valid user and stream
"""
speckle = scene.speckle
ignore_rule = TraversalRule(
[
lambda o: "Objects.Structural.Results"
in o.speckle_type, # Sadly, this one is necessary to avoid double conversion...
lambda o: "Objects.BuiltElements.Revit.Parameter"
in o.speckle_type, # This one is just for traversal performance of revit commits
],
lambda _: [],
user = (
speckle.users[int(speckle.active_user)]
if len(speckle.users) > int(speckle.active_user)
else None
)
convertible_rule = TraversalRule(
[can_convert_to_native],
lambda _: ELEMENTS_PROPERTY_ALIASES,
if user is None:
print("No users loaded.")
stream = (
user.streams[user.active_stream]
if len(user.streams) > user.active_stream
else None
)
default_rule = TraversalRule(
[lambda _: True],
lambda o: o.get_member_names(), # TODO: avoid deprecated members
)
if stream is None:
print("Account contains no streams.")
return GraphTraversal([ignore_rule, convertible_rule, default_rule])
return (user, stream)
+40 -140
View File
@@ -1,110 +1,28 @@
"""
Provides uniform and consistent path helpers for `specklepy`
"""
import os
import sys
from importlib import import_module, invalidate_caches
from pathlib import Path
from typing import Optional
from importlib import import_module
_user_data_env_var = "SPECKLE_USERDATA_PATH"
import bpy
import sys
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("Starting Speckle Blender 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)
def modules_path() -> Path:
modules_path = Path(bpy.utils.script_path_user(), "addons", "modules")
modules_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))
if sys.path[1] != modules_path:
sys.path.insert(1, modules_path)
print(f"Using connector installation path {connector_installation_path}")
return connector_installation_path
return modules_path
print(f"Found blender modules path {modules_path()}")
def is_pip_available() -> bool:
@@ -116,7 +34,7 @@ def is_pip_available() -> bool:
def ensure_pip() -> None:
print("Installing pip... ")
print("Installing pip... "),
from subprocess import run
@@ -125,75 +43,55 @@ def ensure_pip() -> None:
if completed_process.returncode == 0:
print("Successfully installed pip")
else:
raise Exception(
f"Failed to install pip, got {completed_process.returncode} return code"
)
raise Exception("Failed to install pip.")
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:
def install_requirements() -> None:
# set up addons/modules under the user
# script path. Here we'll install the
# dependencies
path = connector_installation_path(host_application)
path = modules_path()
print(f"Installing Speckle dependencies to {path}")
from subprocess import run
def debugger_is_active() -> bool:
"""Return if the debugger is currently active"""
return hasattr(sys, "gettrace") and sys.gettrace() is not None
requirements_path = get_requirements_path()
is_debug = debugger_is_active()
if not is_debug and not requirements_path.exists():
print("Skipped installing dependencies")
return
print(f"Installing Speckle dependencies to {path}")
completed_process = run(
[
PYTHON_PATH,
"-m",
"pip",
"-q",
"--disable-pip-version-check",
"install",
"--prefer-binary",
"--ignore-installed",
"--no-compile",
"--no-deps",
"-t",
str(path),
"-r",
str(requirements_path),
str(get_requirements_path()),
],
capture_output=True,
text=True,
)
if completed_process.returncode != 0:
m = f"Failed to install dependencies through pip, got {completed_process.returncode} return code"
print(m)
raise Exception(m)
print("Successfully installed dependencies")
if not is_debug:
requirements_path.unlink()
print("Please try manually installing speckle-blender")
raise Exception(
"""
Failed to install speckle-blender.
See console for manual install instruction.
"""
)
def install_dependencies(host_application: str) -> None:
def install_dependencies() -> None:
if not is_pip_available():
ensure_pip()
install_requirements(host_application)
install_requirements()
def _import_dependencies() -> None:
@@ -201,7 +99,7 @@ def _import_dependencies() -> None:
# 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
# it would ensure that all dependencies are fully loaded
# requirements = get_requirements_path().read_text()
# reqs = [
# req.split(" ; ")[0].split("==")[0].split("[")[0].replace("-", "_")
@@ -213,13 +111,15 @@ def _import_dependencies() -> None:
# import_module("specklepy")
def ensure_dependencies(host_application: str) -> None:
def ensure_dependencies() -> None:
try:
install_dependencies(host_application)
invalidate_caches()
_import_dependencies()
print("Successfully found dependencies")
print("Found all dependencies, proceed with loading")
except ImportError:
raise Exception(
f"Cannot automatically ensure Speckle dependencies. Please try restarting the host application {host_application}!"
)
print("Failed to load all dependencies, trying to install them...")
install_dependencies()
raise Exception("Please restart Blender.")
if __name__ == "__main__":
ensure_dependencies()
+36 -10
View File
@@ -1,33 +1,59 @@
from .commit import DeleteCommit
from .misc import OpenSpeckleForum, OpenSpeckleGuide, OpenSpeckleTutorials
from .users import LoadUsers, LoadUserStreams
from .object import (
UpdateObject,
ResetObject,
DeleteObject,
UploadNgonsAsPolylines,
SelectIfSameCustomProperty,
SelectIfHasCustomProperty,
)
from .streams import (
AddStreamFromURL,
CopyCommitId,
CopyModelId,
CopyStreamId,
CreateStream,
ReceiveStreamObjects,
SendStreamObjects,
ViewStreamDataApi,
DeleteStream,
SelectOrphanObjects,
)
from .users import LoadUsers, LoadUserStreams, ResetUsers
from .streams import (
UpdateGlobal,
AddStreamFromURL,
CreateStream,
CopyStreamId,
CopyCommitId,
CopyBranchName,
)
from .commit import DeleteCommit
from .misc import OpenSpeckleGuide, OpenSpeckleTutorials, OpenSpeckleForum
operator_classes = [
LoadUsers,
ResetUsers,
ReceiveStreamObjects,
SendStreamObjects,
LoadUserStreams,
CopyStreamId,
CopyCommitId,
CopyModelId,
CopyBranchName,
]
operator_classes.extend([DeleteCommit])
operator_classes.extend(
[
UpdateObject,
ResetObject,
DeleteObject,
UploadNgonsAsPolylines,
SelectIfSameCustomProperty,
SelectIfHasCustomProperty,
]
)
operator_classes.extend(
[
ViewStreamDataApi,
DeleteStream,
SelectOrphanObjects,
UpdateGlobal,
AddStreamFromURL,
CreateStream,
OpenSpeckleGuide,
+22 -32
View File
@@ -1,31 +1,26 @@
"""
Commit operators
"""
import bpy
from bpy.props import BoolProperty
from specklepy.logging import metrics
from bpy_speckle.functions import _check_speckle_client_user_stream
from bpy_speckle.clients import speckle_clients
from bpy_speckle.functions import _report
from bpy_speckle.properties.scene import get_speckle
class DeleteCommit(bpy.types.Operator):
"""
Permanently deletes the selected version from the selected model.
To execute from code, call: `bpy.ops.speckle.delete_commit(are_you_sure=True)`
Delete stream
"""
bl_idname = "speckle.delete_commit"
bl_label = "Delete Version"
bl_label = "Delete commit"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Permanently Deletes the selected version from the selected model"
bl_description = "Delete active commit permanently"
are_you_sure: BoolProperty(
name="Confirm",
default=False,
) # type: ignore
)
def draw(self, context):
layout = self.layout
@@ -33,41 +28,36 @@ class DeleteCommit(bpy.types.Operator):
col.prop(self, "are_you_sure")
def invoke(self, context, event):
speckle = get_speckle(context)
wm = context.window_manager
if len(speckle.users) > 0:
if len(context.scene.speckle.users) > 0:
return wm.invoke_props_dialog(self)
return {"CANCELLED"}
def execute(self, context):
if not self.are_you_sure:
_report("Cancelled by user")
return {"CANCELLED"}
self.are_you_sure = False
self.delete_commit(context)
return {"FINISHED"}
speckle = context.scene.speckle
@staticmethod
def delete_commit(context: bpy.types.Context) -> None:
speckle = get_speckle(context)
check = _check_speckle_client_user_stream(context.scene)
if check is None:
return {"CANCELLED"}
(_, stream, branch, commit) = speckle.validate_commit_selection()
user, stream = check
client = speckle_clients[int(context.scene.speckle.active_user)]
client = speckle_clients[int(speckle.active_user)]
stream = user.streams[user.active_stream]
if len(stream.branches) < 1:
return {"CANCELLED"}
branch = stream.branches[int(stream.branch)]
if len(branch.commits) < 1:
return {"CANCELLED"}
commit = branch.commits[int(branch.commit)]
deleted = client.commit.delete(stream_id=stream.id, commit_id=commit.id)
metrics.track(
"Connector Action",
client.account,
custom_props={"name": "delete_commit"},
)
if not deleted:
raise Exception("Delete operation failed")
print(
f"Version {commit.id} ({commit.message}) of model {branch.id} ({branch.name}) has been deleted from project {stream.id} ({stream.name})"
)
return {"FINISHED"}
+9 -36
View File
@@ -1,62 +1,35 @@
import webbrowser
import bpy
from specklepy.logging import metrics
import webbrowser
class OpenSpeckleGuide(bpy.types.Operator):
_guide_url = "https://speckle.guide/user/blender.html"
bl_idname = "speckle.open_speckle_guide"
bl_label = "Speckle Docs"
bl_label = "Speckle Guide"
bl_options = {"REGISTER", "UNDO"}
bl_description = f"Browse the documentation on the Speckle Guide ({_guide_url})"
bl_description = "Browse the documentation on the Speckle Guide"
def execute(self, context):
webbrowser.open(self._guide_url)
metrics.track(
"Connector Action",
None,
custom_props={"name": "OpenSpeckleGuide"},
)
webbrowser.open("https://speckle.guide/user/blender.html")
return {"FINISHED"}
class OpenSpeckleTutorials(bpy.types.Operator):
_tutorials_url = "https://speckle.systems/tutorials/"
bl_idname = "speckle.open_speckle_tutorials"
bl_label = "Tutorials Portal"
bl_options = {"REGISTER", "UNDO"}
bl_description = (
f"Visit our tutorials portal for learning resources ({_tutorials_url})"
)
bl_description = "Visit our tutorials portal for learning resources"
def execute(self, context):
webbrowser.open(self._tutorials_url)
metrics.track(
"Connector Action",
None,
custom_props={"name": "OpenSpeckleTutorials"},
)
webbrowser.open("https://speckle.systems/tutorials/")
return {"FINISHED"}
class OpenSpeckleForum(bpy.types.Operator):
_forum_url = "https://speckle.community/"
bl_idname = "speckle.open_speckle_forum"
bl_label = "Community Forum"
bl_options = {"REGISTER", "UNDO"}
bl_description = (
f"Ask questions and join the discussion on our community forum ({_forum_url})"
)
bl_description = "Ask questions and join the discussion on our community forum"
def execute(self, context):
webbrowser.open(self._forum_url)
metrics.track(
"Connector Action",
None,
custom_props={"name": "OpenSpeckleForum"},
)
return {"FINISHED"}
webbrowser.open("https://speckle.community/")
return {"FINISHED"}
+318
View File
@@ -0,0 +1,318 @@
"""
Object operators
"""
import bpy
from bpy.props import BoolProperty, EnumProperty
from bpy_speckle.convert.to_speckle import (
convert_to_speckle,
ngons_to_speckle_polylines,
)
from bpy_speckle.functions import get_scale_length, _report
from bpy_speckle.clients import speckle_clients
class UpdateObject(bpy.types.Operator):
"""
Update local (receive) or remote (send) object depending on
the update direction. If sending, updates the object on the
server in-place.
"""
bl_idname = "speckle.update_object"
bl_label = "Update Object"
bl_options = {"REGISTER", "UNDO"}
client = None
def execute(self, context):
user = context.scene.speckle.users[int(context.scene.speckle.active_user)]
client = speckle_clients[int(context.scene.speckle.active_user)]
stream = user.streams[user.active_stream]
active = context.active_object
_report(active)
if active is not None and active.speckle.enabled:
if active.speckle.send_or_receive == "send" and active.speckle.stream_id:
sstream = client.streams.get(active.speckle.stream_id)
# res = client.StreamGetAsync(active.speckle.stream_id)['resource']
# res = client.streams.get(active.speckle.stream_id)
if sstream is None:
_report("Getting stream failed.")
return {"CANCELLED"}
stream_units = "Meters"
if sstream.baseProperties:
stream_units = sstream.baseProperties.units
scale = context.scene.unit_settings.scale_length / get_scale_length(
stream_units
)
sm = convert_to_speckle(active, scale)
_report("Updating object {}".format(sm["_id"]))
client.objects.update(active.speckle.object_id, sm)
return {"FINISHED"}
return {"CANCELLED"}
return {"CANCELLED"}
class ResetObject(bpy.types.Operator):
"""
Reset Speckle object settings
"""
bl_idname = "speckle.reset_object"
bl_label = "Reset Object"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
context.object.speckle.send_or_receive = "send"
context.object.speckle.stream_id = ""
context.object.speckle.object_id = ""
context.object.speckle.enabled = False
context.view_layer.update()
return {"FINISHED"}
class DeleteObject(bpy.types.Operator):
"""
Delete object from the server and update relevant stream
"""
bl_idname = "speckle.delete_object"
bl_label = "Delete Object"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
client = speckle_clients[int(context.scene.speckle.active_user)]
active = context.object
if active.speckle.enabled:
res = client.StreamGetAsync(active.speckle.stream_id)
existing = [
x
for x in res["resource"]["objects"]
if x["_id"] == active.speckle.object_id
]
if existing is None:
return {"CANCELLED"}
new_objects = [
x
for x in res["resource"]["objects"]
if x["_id"] != active.speckle.object_id
]
res = client.GetLayers(active.speckle.stream_id)
new_layers = res["resource"]["layers"]
new_layers[-1]["objectCount"] = new_layers[-1]["objectCount"] - 1
new_layers[-1]["topology"] = "0-%s" % new_layers[-1]["objectCount"]
res = client.StreamUpdateAsync(
{"objects": new_objects, "layers": new_layers}, active.speckle.stream_id
)
res = client.ObjectDeleteAsync(active.speckle.object_id)
active.speckle.send_or_receive = "send"
active.speckle.stream_id = ""
active.speckle.object_id = ""
active.speckle.enabled = False
context.view_layer.update()
return {"FINISHED"}
class UploadNgonsAsPolylines(bpy.types.Operator):
"""
Upload mesh ngon faces as polyline outlines
TODO: move to another category of specialized operators and fix to work with API 2.0
"""
bl_idname = "speckle.upload_ngons_as_polylines"
bl_label = "Upload Ngons As Polylines"
bl_options = {"REGISTER", "UNDO"}
clear_stream: BoolProperty(
name="Clear stream",
default=False,
)
def execute(self, context):
active = context.active_object
if active is not None and active.type == "MESH":
user = context.scene.speckle.users[int(context.scene.speckle.active_user)]
client = speckle_clients[int(context.scene.speckle.active_user)]
stream = user.streams[user.active_stream]
# scale = context.scene.unit_settings.scale_length / get_scale_length(
# stream.units
# )
scale = 1.0
sp = ngons_to_speckle_polylines(active, scale)
if sp is None:
return {"CANCELLED"}
placeholders = []
for polyline in sp:
res = client.objects.create([polyline])
if res is None:
_report(client.me)
continue
placeholders.extend(res)
if not placeholders:
return {"CANCELLED"}
# Get list of existing objects in stream and append new object to list
_report("Fetching stream...")
sstream = client.streams.get(stream.id)
if self.clear_stream:
_report("Clearing stream...")
sstream.objects = placeholders
N = 0
else:
sstream.objects.extend(placeholders)
N = sstream.layers[-1].objectCount
if self.clear_stream:
N = 0
sstream.layers[-1].objectCount = N + len(placeholders)
sstream.layers[-1].topology = "0-%s" % (N + len(placeholders))
res = client.streams.update(sstream.id, sstream)
# Update view layer
context.view_layer.update()
_report("Done.")
return {"FINISHED"}
def invoke(self, context, event):
wm = context.window_manager
return wm.invoke_props_dialog(self)
def draw(self, context):
layout = self.layout
layout.prop(self, "clear_stream")
def get_custom_speckle_props(self, context):
ignore = ["speckle", "cycles", "cycles_visibility"]
active = context.active_object
if not active:
return []
return [(x, "{}".format(x), "") for x in active.keys()]
class SelectIfSameCustomProperty(bpy.types.Operator):
"""
Select scene objects if they have the same custom property
value as the active object
"""
bl_idname = "speckle.select_if_same_custom_props"
bl_label = "Select Identical Custom Props"
bl_options = {"REGISTER", "UNDO"}
custom_prop: EnumProperty(
name="Custom properties",
description="Available streams associated with user.",
items=get_custom_speckle_props,
)
def draw(self, context):
layout = self.layout
col = layout.column()
col.prop(self, "custom_prop")
def invoke(self, context, event):
wm = context.window_manager
return wm.invoke_props_dialog(self)
def execute(self, context):
active = context.active_object
if not active:
return {"CANCELLED"}
if self.custom_prop not in active.keys():
return {"CANCELLED"}
value = active[self.custom_prop]
_report(
"Looking for '{}' property with a value of '{}'.".format(
self.custom_prop, value
)
)
for obj in bpy.data.objects:
if self.custom_prop in obj.keys() and obj[self.custom_prop] == value:
obj.select_set(True)
else:
obj.select_set(False)
return {"FINISHED"}
class SelectIfHasCustomProperty(bpy.types.Operator):
"""
Select scene objects if they have the same custom property
as the active object, regardless of the value
"""
bl_idname = "speckle.select_if_has_custom_props"
bl_label = "Select Same Custom Prop"
bl_options = {"REGISTER", "UNDO"}
custom_prop: EnumProperty(
name="Custom properties",
description="Custom properties yo",
items=get_custom_speckle_props,
)
def draw(self, context):
layout = self.layout
col = layout.column()
col.prop(self, "custom_prop")
def invoke(self, context, event):
wm = context.window_manager
return wm.invoke_props_dialog(self)
def execute(self, context):
active = context.active_object
if not active:
return {"CANCELLED"}
if self.custom_prop not in active.keys():
return {"CANCELLED"}
value = active[self.custom_prop]
_report("Looking for '{}' property.".format(self.custom_prop))
for obj in bpy.data.objects:
if self.custom_prop in obj.keys():
obj.select_set(True)
else:
obj.select_set(False)
return {"FINISHED"}
File diff suppressed because it is too large Load Diff
+76 -165
View File
@@ -1,224 +1,135 @@
"""
User account operators
"""
from typing import cast
import bpy
from bpy.types import Context
from specklepy.core.api.client import SpeckleClient
from specklepy.core.api.credentials import Account, get_local_accounts
from specklepy.core.api.models import Stream
from specklepy.logging import metrics
from bpy_speckle.clients import speckle_clients
from bpy_speckle.functions import _report
from bpy_speckle.properties.scene import (
SpeckleSceneSettings,
SpeckleStreamObject,
SpeckleUserObject,
get_speckle,
restore_selection_state,
)
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)
metrics.track(
"Connector Action",
None,
custom_props={"name": "ResetUsers"},
)
bpy.context.view_layer.update()
if context.area:
context.area.tag_redraw()
return {"FINISHED"}
@staticmethod
def reset_ui(context: Context):
speckle = get_speckle(context)
speckle.users.clear()
speckle_clients.clear()
from bpy_speckle.clients import speckle_clients
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 LoadUsers(bpy.types.Operator):
"""
Loads all user accounts from the credentials in the local database.
See docs to add accounts via Manager
Load all users from local user database
"""
bl_idname = "speckle.users_load"
bl_label = "Load Users"
bl_label = "Load users"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Loads all user accounts from the credentials in the local database.\nSee docs to add accounts via Manager"
def execute(self, context):
_report("Loading users...")
speckle = get_speckle(context)
users_list = speckle.users
users = context.scene.speckle.users
ResetUsers.reset_ui(context)
context.scene.speckle.users.clear()
speckle_clients.clear()
profiles = get_local_accounts()
active_user_index = 0
metrics.track(
"Connector Action",
None,
custom_props={
"name": "LoadUsers",
},
)
if not profiles:
raise Exception(
"Zero accounts were found, please add one through Speckle Manager or a local account"
)
for profile in profiles:
user = users.add()
user.server_name = profile.serverInfo.name or "Speckle Server"
user.server_url = profile.serverInfo.url
user.name = profile.userInfo.name
user.email = profile.userInfo.email
user.company = profile.userInfo.company or ""
user.authToken = profile.token
try:
add_user_account(profile, speckle)
except Exception as ex:
_report(
f"Failed to authenticate user account {profile.userInfo.email} with server {profile.serverInfo.url}: {ex}"
client = SpeckleClient(
host=profile.serverInfo.url,
use_ssl="https" in profile.serverInfo.url,
)
users_list.remove(len(users_list) - 1)
continue
client.authenticate(user.authToken)
speckle_clients.append(client)
except Exception as ex:
_report(ex)
users.remove(len(users) - 1)
if profile.isDefault:
active_user_index = len(users_list) - 1
_report(f"Authenticated {len(users_list)}/{len(profiles)} accounts")
if active_user_index < len(users_list):
speckle.active_user = str(active_user_index)
context.scene.speckle.active_user = str(len(users) - 1)
context.scene.speckle.active_user_index = int(context.scene.speckle.active_user)
bpy.ops.speckle.load_user_streams()
bpy.context.view_layer.update()
if context.area:
context.area.tag_redraw()
if not users_list:
raise Exception(
"Zero valid user accounts were found, please ensure account is valid and the server is running"
)
return {"FINISHED"}
def add_user_account(
account: Account, speckle: SpeckleSceneSettings
) -> SpeckleUserObject:
"""Creates a new new SpeckleUserObject for the provided user Account and adds it to the SpeckleSceneSettings"""
users_list = speckle.users
URL = account.serverInfo.url
user = cast(SpeckleUserObject, users_list.add())
user.server_name = account.serverInfo.name or "Speckle Server"
user.server_url = URL
user.id = account.userInfo.id
user.name = account.userInfo.name
user.email = account.userInfo.email
user.company = account.userInfo.company or ""
assert URL
client = SpeckleClient(
host=URL,
use_ssl="https" in URL,
)
client.authenticate_with_account(account)
speckle_clients.append(client)
return user
def add_user_stream(user: SpeckleUserObject, stream: Stream):
"""Adds the provided Stream (with branch & commits) to the SpeckleUserObject"""
s = cast(SpeckleStreamObject, user.streams.add())
def add_user_stream(user: User, stream: Stream):
s = user.streams.add()
s.name = stream.name
s.id = stream.id
s.description = stream.description
_report(f"Adding stream {s.id} - {s.name}")
if not stream.branches:
return
if stream.branches:
s.load_stream_branches(stream)
# branches = [branch for branch in stream.branches.items if branch.name != "globals"]
for b in stream.branches.items:
branch = s.branches.add()
branch.name = b.name
if not b.commits:
continue
for c in b.commits.items:
commit = branch.commits.add()
commit.id = commit.name = c.id
commit.message = c.message 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)
if hasattr(s, "baseProperties"):
s.units = stream.baseProperties.units
else:
s.units = "Meters"
class LoadUserStreams(bpy.types.Operator):
"""
(Re)Load all available projects for active user
Load all available streams for active user user
"""
bl_idname = "speckle.load_user_streams"
bl_label = "Load User's Projects"
bl_label = "Load user streams"
bl_options = {"REGISTER", "UNDO"}
bl_description = "(Re)Load all available projects for active user"
stream_limit: int = 20
branch_limit: int = 100
commits_limit: int = 10
bl_description = "(Re)load all available user streams"
def execute(self, context):
self.load_user_stream(context)
return {"FINISHED"}
speckle = context.scene.speckle
def load_user_stream(self, context: Context) -> None:
speckle = get_speckle(context)
if len(speckle.users) > 0:
user = speckle.users[int(context.scene.speckle.active_user)]
client = speckle_clients[int(context.scene.speckle.active_user)]
user = speckle.validate_user_selection()
try:
streams = client.stream.list(stream_limit=20)
except Exception as e:
_report(f"Failed to retrieve streams: {e}")
return
if not streams:
_report("Failed to retrieve streams.")
return
client = speckle_clients[int(speckle.active_user)]
try:
streams = client.stream.list(stream_limit=self.stream_limit)
except Exception as ex:
raise Exception("Failed to retrieve projects") from ex
user.streams.clear()
if not streams:
_report("Zero projects found")
return
default_units = "Meters"
active_stream_id = None
if active_stream := user.get_active_stream():
active_stream_id = active_stream.id
elif len(user.streams) > 0:
active_stream_id = user.streams[0].id
user.streams.clear()
for i, s in enumerate(streams):
assert s.id
load_branches = s.id == active_stream_id if active_stream_id else i == 0
if load_branches:
sstream = client.stream.get(
id=s.id, branch_limit=self.branch_limit, commit_limit=10
)
for s in streams:
sstream = client.stream.get(id=s.id, branch_limit=20)
add_user_stream(user, sstream)
else:
add_user_stream(user, s)
restore_selection_state(speckle)
bpy.context.view_layer.update()
bpy.context.view_layer.update()
return {"FINISHED"}
if context.area:
context.area.tag_redraw()
metrics.track(
"Connector Action",
client.account,
custom_props={"name": "LoadUserStreams"},
)
return {"CANCELLED"}
+7 -7
View File
@@ -1,14 +1,14 @@
from .addon import SpeckleAddonPreferences
from .collection import SpeckleCollectionSettings
from .object import SpeckleObjectSettings
from .scene import (
SpeckleSceneSettings,
SpeckleSceneObject,
SpeckleUserObject,
SpeckleStreamObject,
SpeckleBranchObject,
SpeckleCommitObject,
SpeckleSceneObject,
SpeckleSceneSettings,
SpeckleStreamObject,
SpeckleUserObject,
)
from .object import SpeckleObjectSettings
from .collection import SpeckleCollectionSettings
from .addon import SpeckleAddonPreferences
property_classes = [
SpeckleSceneObject,
-1
View File
@@ -1,7 +1,6 @@
"""
Addon properties
"""
import bpy
+5 -5
View File
@@ -1,12 +1,11 @@
"""
Collection properties
"""
import bpy
class SpeckleCollectionSettings(bpy.types.PropertyGroup):
enabled: bpy.props.BoolProperty(default=False, name="Enabled") # type: ignore
enabled: bpy.props.BoolProperty(default=False, name="Enabled")
send_or_receive: bpy.props.EnumProperty(
name="Mode",
@@ -14,6 +13,7 @@ class SpeckleCollectionSettings(bpy.types.PropertyGroup):
("send", "Send", "Send data to Speckle server."),
("receive", "Receive", "Receive data from Speckle server."),
),
) # type: ignore
stream_id: bpy.props.StringProperty(default="") # type: ignore
name: bpy.props.StringProperty(default="") # type: ignore
)
stream_id: bpy.props.StringProperty(default="")
name: bpy.props.StringProperty(default="")
units: bpy.props.StringProperty(default="")
+3 -4
View File
@@ -1,7 +1,6 @@
"""
Object properties
"""
import bpy
@@ -14,6 +13,6 @@ class SpeckleObjectSettings(bpy.types.PropertyGroup):
("send", "Send", "Send data to Speckle server."),
("receive", "Receive", "Receive data from Speckle server."),
),
) # type: ignore
stream_id: bpy.props.StringProperty(default="") # type: ignore
object_id: bpy.props.StringProperty(default="") # type: ignore
)
stream_id: bpy.props.StringProperty(default="")
object_id: bpy.props.StringProperty(default="")
+55 -285
View File
@@ -1,167 +1,81 @@
"""
Scene properties
"""
from dataclasses import dataclass
from typing import Iterable, Optional, Tuple, Union, cast
import bpy
from bpy.props import (
StringProperty,
BoolProperty,
FloatProperty,
CollectionProperty,
EnumProperty,
FloatProperty,
IntProperty,
StringProperty,
PointerProperty,
)
from specklepy.core.api.models import Stream
from bpy_speckle.clients import speckle_clients
class SpeckleSceneObject(bpy.types.PropertyGroup):
name: bpy.props.StringProperty(default="") # type: ignore
name: bpy.props.StringProperty(default="")
class SpeckleCommitObject(bpy.types.PropertyGroup):
id: StringProperty(default="") # type: ignore
message: StringProperty(default="") # type: ignore
author_name: StringProperty(default="") # type: ignore
author_id: StringProperty(default="") # type: ignore
created_at: StringProperty(default="") # type: ignore
source_application: StringProperty(default="") # type: ignore
referenced_object: StringProperty(default="") # type: ignore
id: StringProperty(default="abc")
message: StringProperty(default="A simple commit")
author_name: StringProperty(default="Author name")
author_id: StringProperty(default="Author ID")
created_at: StringProperty(default="Today")
source_application: StringProperty(default="Unknown")
class SpeckleBranchObject(bpy.types.PropertyGroup):
def get_commits(self, context):
if self.commits is not None and len(self.commits) > 0:
COMMITS = cast(Iterable[SpeckleCommitObject], self.commits)
if self.commits != None and len(self.commits) > 0:
return [
(str(i), commit.id, commit.message, i)
for i, commit in enumerate(COMMITS)
for i, commit in enumerate(self.commits)
]
return [("0", "<none>", "<none>", 0)]
def commit_update_hook(self, context: bpy.types.Context):
selection_state.selected_commit_id = SelectionState.get_item_id_by_index(
self.commits, self.commit
)
selection_state.selected_branch_id = self.id
# print(f"commit_update_hook: {selection_state.selected_commit_id=}, {selection_state.selected_branch_id=}")
name: StringProperty(default="main") # type: ignore
id: StringProperty(default="") # type: ignore
description: StringProperty(default="") # type: ignore
commits: CollectionProperty(type=SpeckleCommitObject) # type: ignore
name: StringProperty(default="main")
commits: CollectionProperty(type=SpeckleCommitObject)
commit: EnumProperty(
name="Version",
description="Selected model version",
name="Commit",
description="Active commit",
items=get_commits,
update=commit_update_hook,
) # type: ignore
def get_active_commit(self) -> Optional[SpeckleCommitObject]:
selected_index = int(self.commit)
if 0 <= selected_index < len(self.commits):
return self.commits[selected_index]
return None
)
class SpeckleStreamObject(bpy.types.PropertyGroup):
def load_stream_branches(self, sstream: Stream):
self.branches.clear()
# branches = [branch for branch in stream.branches.items if branch.name != "globals"]
for b in sstream.branches.items:
branch = cast(SpeckleBranchObject, self.branches.add())
branch.name = b.name
branch.id = b.id
branch.description = b.description or ""
if not b.commits:
continue
for c in b.commits.items:
commit: SpeckleCommitObject = branch.commits.add()
commit.id = commit.name = c.id
commit.message = c.message or ""
commit.author_name = c.authorName
commit.author_id = c.authorId
commit.created_at = (
c.createdAt.strftime("%Y-%m-%d %H:%M:%S.%f%Z")
if c.createdAt
else ""
)
commit.source_application = str(c.sourceApplication)
commit.referenced_object = c.referencedObject
def get_branches(self, context):
if self.branches:
BRANCHES = cast(Iterable[SpeckleBranchObject], self.branches)
return [
(str(i), branch.name, branch.description, i)
for i, branch in enumerate(BRANCHES)
(str(i), branch.name, branch.name, i)
for i, branch in enumerate(self.branches)
if branch.name != "globals"
]
return [("0", "<none>", "<none>", 0)]
def branch_update_hook(self, context: bpy.types.Context):
selection_state.selected_branch_id = SelectionState.get_item_id_by_index(
self.branches, self.branch
)
# print(f"branch_update_hook: {selection_state.selected_branch_id=}, {selection_state.selected_stream_id=}")
name: StringProperty(default="") # type: ignore
description: StringProperty(default="") # type: ignore
id: StringProperty(default="") # type: ignore
branches: CollectionProperty(type=SpeckleBranchObject) # type: ignore
name: StringProperty(default="SpeckleStream")
description: StringProperty(default="No description provided.")
id: StringProperty(default="")
units: StringProperty(default="Meters")
query: StringProperty(default="")
branches: CollectionProperty(type=SpeckleBranchObject)
branch: EnumProperty(
name="Model",
description="Selected Model",
name="Branch",
description="Active branch",
items=get_branches,
update=branch_update_hook,
) # type: ignore
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):
def fetch_stream_branches(
self, context: bpy.types.Context, stream: SpeckleStreamObject
):
speckle = context.scene.speckle
client = speckle_clients[int(speckle.active_user)]
sstream = client.stream.get(
id=stream.id, branch_limit=100, commit_limit=10
) # TODO: refactor magic numbers
stream.load_stream_branches(sstream)
def stream_update_hook(self, context: bpy.types.Context):
stream = SelectionState.get_item_by_index(self.streams, self.active_stream)
selection_state.selected_stream_id = stream.id
# print(f"stream_update_hook: {selection_state.selected_stream_id=}, {selection_state.selected_user_id=}")
if (
len(stream.branches) == 0
): # do not reload on selection, same as the old behavior
self.fetch_stream_branches(context, stream)
server_name: StringProperty(default="SpeckleXYZ") # type: ignore
server_url: StringProperty(default="https://speckle.xyz") # type: ignore
id: StringProperty(default="") # type: ignore
name: StringProperty(default="Speckle User") # type: ignore
email: StringProperty(default="user@speckle.xyz") # type: ignore
company: StringProperty(default="SpeckleSystems") # type: ignore
streams: CollectionProperty(type=SpeckleStreamObject) # type: ignore
active_stream: IntProperty(default=0, update=stream_update_hook) # type: ignore
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
server_name: StringProperty(default="SpeckleXYZ")
server_url: StringProperty(default="https://speckle.xyz")
name: StringProperty(default="Speckle User")
email: StringProperty(default="user@speckle.xyz")
company: StringProperty(default="SpeckleSystems")
authToken: StringProperty(default="")
streams: CollectionProperty(type=SpeckleStreamObject)
active_stream: IntProperty(default=0)
class SpeckleSceneSettings(bpy.types.PropertyGroup):
@@ -175,190 +89,46 @@ class SpeckleSceneSettings(bpy.types.PropertyGroup):
name="Available streams",
description="Available streams associated with user.",
items=[],
) # type: ignore
)
users: CollectionProperty(type=SpeckleUserObject) # type: ignore
users: CollectionProperty(type=SpeckleUserObject)
def get_users(self, context):
USERS = cast(Iterable[SpeckleUserObject], self.users)
return [
(str(i), f"{user.email} ({user.server_name})", user.server_url, i)
for i, user in enumerate(USERS)
(str(i), "{} ({})".format(user.email, user.server_name), user.server_url, i)
for i, user in enumerate(self.users)
]
def user_update_hook(self, context):
bpy.ops.speckle.load_user_streams() # type: ignore
selection_state.selected_user_id = SelectionState.get_item_id_by_index(
self.users, self.active_user
)
def set_user(self, context):
bpy.ops.speckle.load_user_streams()
active_user: EnumProperty(
items=get_users,
name="Account",
description="Select account",
update=user_update_hook,
name="User",
description="Select user",
update=set_user,
get=None,
set=None,
) # type: ignore
)
objects: CollectionProperty(type=SpeckleSceneObject) # type: ignore
objects: CollectionProperty(type=SpeckleSceneObject)
scale: FloatProperty(default=0.001) # type: ignore
scale: FloatProperty(default=0.001)
user: StringProperty(
name="User",
description="Current user",
description="Current user.",
default="Speckle User",
) # type: ignore
)
receive_script: EnumProperty(
name="Receive script",
description="Custom py script to execute when receiving objects. See docs for function signature.",
description="Script to run when receiving stream objects.",
items=get_scripts,
) # type: ignore
)
send_script: EnumProperty(
name="Send script",
description="Custom py script to execute when sending objects. See docs for function signature",
description="Script to run when sending stream objects.",
items=get_scripts,
) # type: ignore
def get_active_user(self) -> Optional[SpeckleUserObject]:
if self.active_user is None:
return None
selected_index = int(self.active_user)
if 0 <= selected_index < len(self.users):
return self.users[selected_index]
return None
def validate_user_selection(self) -> SpeckleUserObject:
user = self.get_active_user()
if not user:
raise SelectionException("No user account selected/found")
return user
def validate_stream_selection(
self,
) -> Tuple[SpeckleUserObject, SpeckleStreamObject]:
user = self.validate_user_selection()
stream = user.get_active_stream()
if not stream:
raise SelectionException("No project selected/found")
return (user, stream)
def validate_branch_selection(
self,
) -> Tuple[SpeckleUserObject, SpeckleStreamObject, SpeckleBranchObject]:
(user, stream) = self.validate_stream_selection()
branch = stream.get_active_branch()
if not branch:
raise SelectionException("No model selected/found")
return (user, stream, branch)
def validate_commit_selection(
self,
) -> Tuple[
SpeckleUserObject, SpeckleStreamObject, SpeckleBranchObject, SpeckleCommitObject
]:
(user, stream, branch) = self.validate_branch_selection()
commit = branch.get_active_commit()
if commit is None:
raise SelectionException("No model version selected/found")
return (user, stream, branch, commit)
class SelectionException(Exception):
pass
def get_speckle(context: bpy.types.Context) -> SpeckleSceneSettings:
"""
Gets the speckle scene object
"""
return context.scene.speckle # type: ignore
@dataclass
class SelectionState:
selected_user_id: Optional[str] = None
selected_stream_id: Optional[str] = None
selected_branch_id: Optional[str] = None
selected_commit_id: Optional[str] = None
@staticmethod
def get_item_id_by_index(
collection: bpy.types.PropertyGroup, index: Union[str, int]
) -> Optional[str]:
if item := SelectionState.get_item_by_index(collection, index):
return item.id
return None
@staticmethod
def get_item_by_index(
collection: bpy.types.PropertyGroup, index: Union[str, int]
) -> Optional[bpy.types.PropertyGroup]:
items = collection.values()
i = int(index)
if 0 <= i <= len(items):
return items[i]
return None
@staticmethod
def get_item_index_by_id(
collection: Iterable[SpeckleCommitObject], id: Optional[str]
) -> Optional[str]:
for index, item in enumerate(collection):
if item.id == id:
return str(index)
return None
selection_state = SelectionState()
def restore_selection_state(speckle: SpeckleSceneSettings) -> None:
# Restore branch selection state
if selection_state.selected_branch_id is not None:
(active_user, active_stream) = speckle.validate_stream_selection()
# print(f"restore_selection_state: {active_user.id=}, {active_stream.id=}")
# print(f"restore_selection_state: {selection_state.selected_user_id=}, {selection_state.selected_stream_id=}, {selection_state.selected_branch_id=}, {selection_state.selected_commit_id=}")
is_same_user = active_user.id == selection_state.selected_user_id
if is_same_user:
active_user.active_stream = int(
SelectionState.get_item_index_by_id(
active_user.streams, selection_state.selected_stream_id
)
)
active_stream = SelectionState.get_item_by_index(
active_user.streams, active_user.active_stream
)
if branch := SelectionState.get_item_index_by_id(
active_stream.branches, selection_state.selected_branch_id
):
active_stream.branch = branch
# Restore commit selection state
if selection_state.selected_commit_id is not None:
(
active_user,
active_stream,
active_branch,
) = speckle.validate_branch_selection()
# print(f"restore_selection_state: {active_user.id=}, {active_stream.id=}, {active_branch.id=}")
# print(f"restore_selection_state: {selection_state.selected_user_id=}, {selection_state.selected_stream_id=}, {selection_state.selected_branch_id=}, {selection_state.selected_commit_id=}")
is_same_user = active_user.id == selection_state.selected_user_id
is_same_stream = active_stream.id == selection_state.selected_stream_id
is_same_branch = active_branch.id == selection_state.selected_branch_id
if is_same_user and is_same_stream and is_same_branch:
if commit := SelectionState.get_item_index_by_id(
active_branch.commits, selection_state.selected_commit_id
):
active_branch.commit = commit
)
+4 -4
View File
@@ -1,11 +1,11 @@
from .object import OBJECT_PT_speckle
from .view3d import (
VIEW3D_UL_SpeckleUsers,
VIEW3D_UL_SpeckleStreams,
VIEW3D_PT_SpeckleUser,
VIEW3D_PT_SpeckleStreams,
VIEW3D_PT_SpeckleActiveStream,
VIEW3D_PT_SpeckleHelp,
VIEW3D_PT_SpeckleStreams,
VIEW3D_PT_SpeckleUser,
VIEW3D_UL_SpeckleStreams,
VIEW3D_UL_SpeckleUsers,
)
ui_classes = [
+9 -4
View File
@@ -3,16 +3,21 @@ Object UI elements
"""
import bpy
from deprecated import deprecated
from bpy.props import (
StringProperty,
BoolProperty,
FloatProperty,
CollectionProperty,
EnumProperty,
)
@deprecated
class OBJECT_PT_speckle(bpy.types.Panel):
bl_space_type = "PROPERTIES"
# bl_idname = 'OBJECT_PT_speckle'
bl_region_type = "WINDOW"
bl_context = "object"
bl_label = "Speckle (Legacy)"
bl_label = "Speckle"
def draw_header(self, context):
self.layout.prop(context.object.speckle, "enabled", text="")
@@ -23,7 +28,7 @@ class OBJECT_PT_speckle(bpy.types.Panel):
layout.active = ob.speckle.enabled
col = layout.column()
col.prop(ob.speckle, "send_or_receive", expand=True)
col.prop(ob.speckle, "stream_id", text="Project ID")
col.prop(ob.speckle, "stream_id", text="Stream ID")
col.prop(ob.speckle, "object_id", text="Object ID")
col.operator("speckle.update_object", text="Update")
col.operator("speckle.reset_object", text="Reset")
+41 -38
View File
@@ -2,11 +2,22 @@
Speckle UI elements for the 3d viewport
"""
from datetime import datetime
import bpy
from bpy.props import (
StringProperty,
BoolProperty,
FloatProperty,
CollectionProperty,
EnumProperty,
)
from bpy_speckle.properties.scene import get_speckle
from datetime import datetime
"""
Compatibility
TODO: evaluate if we should still support Blender <2.80
"""
Region = "TOOLS" if bpy.app.version < (2, 80, 0) else "UI"
@@ -67,7 +78,7 @@ class VIEW3D_UL_SpeckleUsers(bpy.types.UIList):
class VIEW3D_UL_SpeckleStreams(bpy.types.UIList):
"""
Speckle projects list
Speckle stream list
"""
def draw_item(self, context, layout, data, stream, active_data, active_propname):
@@ -83,7 +94,7 @@ class VIEW3D_UL_SpeckleStreams(bpy.types.UIList):
elif self.layout_type in {"GRID"}:
layout.alignment = "CENTER"
layout.label(text="Projects", icon_value=0)
layout.label(text="Streams", icon_value=0)
class VIEW3D_PT_SpeckleUser(bpy.types.Panel):
@@ -93,51 +104,42 @@ class VIEW3D_PT_SpeckleUser(bpy.types.Panel):
bl_space_type = "VIEW_3D"
bl_region_type = Region
bl_category = "Speckle (Legacy)"
bl_category = "Speckle"
bl_context = "objectmode"
bl_label = "User Account"
bl_label = "User"
def draw(self, context):
speckle = get_speckle(context)
speckle = context.scene.speckle
layout = self.layout
# Draw a box with the text "Warning: This is a legacy add-on. Download the new connector from https://app.speckle.systems/connectors"
box = layout.box()
box.label(text="Warning: This is a legacy add-on.", icon="ERROR")
box.label(text="Download the new connector from")
box.label(text="https://app.speckle.systems/connectors")
layout.separator()
col = layout.column()
if len(speckle.users) < 1:
col.label(text="Refresh to initialise")
col.label(text="No users found.")
else:
col.prop(speckle, "active_user", text="")
user = speckle.users[int(speckle.active_user)]
col.label(text=f"{user.server_name} ({user.server_url})")
col.label(text=f"{user.name} ({user.email})")
col.operator("speckle.users_load", text="", icon="FILE_REFRESH")
col.label(text="{} ({})".format(user.server_name, user.server_url))
col.label(text="{} ({})".format(user.name, user.email))
class VIEW3D_PT_SpeckleStreams(bpy.types.Panel):
"""
Speckle projects UI panel in the 3d viewport
Speckle Streams UI panel in the 3d viewport
"""
bl_space_type = "VIEW_3D"
bl_region_type = Region
bl_category = "Speckle (Legacy)"
bl_category = "Speckle"
bl_context = "objectmode"
bl_label = "Projects"
bl_label = "Streams"
def draw(self, context):
speckle = get_speckle(context)
speckle = context.scene.speckle
col = self.layout.column()
if len(speckle.users) < 1:
col.label(text="No Projects")
col.label(text="No stream data.")
else:
user = speckle.users[int(speckle.active_user)]
col.template_list(
@@ -146,31 +148,31 @@ class VIEW3D_PT_SpeckleStreams(bpy.types.Panel):
row = col.row(align=True)
row.operator("speckle.add_stream_from_url", text="", icon="URL")
row.operator("speckle.create_stream", text="", icon="ADD")
row.operator("speckle.delete_stream", text="", icon="REMOVE")
row.operator("speckle.load_user_streams", text="", icon="FILE_REFRESH")
class VIEW3D_PT_SpeckleActiveStream(bpy.types.Panel):
"""
Speckle Active Projects UI panel in the 3d viewport
Speckle Active Streams UI panel in the 3d viewport
"""
bl_space_type = "VIEW_3D"
bl_region_type = Region
bl_category = "Speckle (Legacy)"
bl_category = "Speckle"
bl_context = "objectmode"
bl_label = "Active Project"
bl_label = "Active stream"
def draw(self, context):
speckle = get_speckle(context)
speckle = context.scene.speckle
col = self.layout.column()
if len(speckle.users) < 1:
col.label(text="No projects")
col.label(text="No stream data.")
else:
user = speckle.validate_user_selection()
# user = speckle.users[int(speckle.active_user)]
user = speckle.users[int(speckle.active_user)]
if len(user.streams) < 1:
col.label(text="No active project")
col.label(text="No active stream.")
else:
stream = user.streams[user.active_stream]
# user.active_stream = min(user.active_stream, len(user.streams) - 1)
@@ -180,14 +182,14 @@ class VIEW3D_PT_SpeckleActiveStream(bpy.types.Panel):
col.separator()
row = col.row()
row.prop(stream, "branch", text="Model")
row.operator("speckle.model_copy_id", text="", icon="COPY_ID")
row.prop(stream, "branch", text="")
row.operator("speckle.branch_copy_name", text="", icon="COPY_ID")
if len(stream.branches) > 0:
branch = stream.branches[int(stream.branch)]
row = col.row()
row.prop(branch, "commit", text="Version")
row.prop(branch, "commit", text="")
row.operator("speckle.commit_copy_id", text="", icon="COPY_ID")
if len(branch.commits) > 0:
@@ -210,7 +212,7 @@ class VIEW3D_PT_SpeckleActiveStream(bpy.types.Panel):
col.label(text=f"{commit.author_name} ({commit.author_id})")
col.label(text=commit.source_application)
else:
col.label(text="No models found!")
col.label(text="No branches found!")
col.separator()
@@ -222,6 +224,7 @@ class VIEW3D_PT_SpeckleActiveStream(bpy.types.Panel):
subcol = row.column()
subcol.operator("speckle.send_stream_objects", text="Send")
subcol.prop(speckle, "send_script", text="")
area.prop(stream, "query", text="Filter")
col.separator()
@@ -242,7 +245,7 @@ class VIEW3D_PT_SpeckleActiveStream(bpy.types.Panel):
area.separator()
col.separator()
col.operator("speckle.view_stream_data_api", text="Open Model in Web")
col.operator("speckle.view_stream_data_api", text="Open Stream in Web")
class VIEW3D_PT_SpeckleHelp(bpy.types.Panel):
@@ -252,7 +255,7 @@ class VIEW3D_PT_SpeckleHelp(bpy.types.Panel):
bl_space_type = "VIEW_3D"
bl_region_type = Region
bl_category = "Speckle (Legacy)"
bl_category = "Speckle"
bl_context = "objectmode"
bl_label = "Help"
+1 -1
View File
@@ -22,7 +22,7 @@ def get_iddata(base, uuid, name, obdata):
"""
This is taken from the import_3dm add-on:
https://github.com/jesterKing/import_3dm
# Copyright (c) 2018-2019 Nathan Letwory, Joel Putnam,
# Copyright (c) 2018-2019 Nathan Letwory, Joel Putnam,
Tom Svilans
Get an iddata. If an object with given uuid is found in
+2 -2
View File
@@ -6,7 +6,7 @@ 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)
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}"')
@@ -17,4 +17,4 @@ def patch_installer(tag: str):
if __name__ == "__main__":
tag = sys.argv[1]
patch_installer(tag)
patch_installer(tag)
+1 -3
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"
@@ -10,7 +9,7 @@ def patch_connector(tag):
with open(bpy_file, "r") as file:
lines = file.readlines()
for index, line in enumerate(lines):
for (index, line) in enumerate(lines):
if '"version":' in line:
lines[index] = f' "version": ({tag[0]}, {tag[1]}, {tag[2]}),\n'
print(f"Patched connector version number in {bpy_file}")
@@ -19,7 +18,6 @@ def patch_connector(tag):
with open(bpy_file, "w") as file:
file.writelines(lines)
def main():
tag = sys.argv[1]
if not re.match(r"([0-9]+)\.([0-9]+)\.([0-9]+)", tag):
Generated
+747 -1280
View File
File diff suppressed because it is too large Load Diff
+7 -33
View File
@@ -4,47 +4,21 @@ version = "2.0.0"
description = "the Speckle 2.0 connector for Blender!"
authors = ["izzy lyseggen <izzy.lyseggen@gmail.com>", "Gergő Jedlicska <gergo@jedlicska.com>"]
license = "Apache-2.0"
package-mode = false
[tool.poetry.requires-plugins]
poetry-plugin-export = ">=1.8"
[tool.poetry.dependencies]
python = ">=3.9.0, <4.0.0"
specklepy = "^2.23.0"
python = ">=3.8, <4.0.0"
specklepy = "^2.9.1"
# [tool.poetry.group.local_specklepy.dependencies]
# specklepy = {path = "../specklepy", develop = true}
[tool.poetry.group.dev.dependencies]
fake-bpy-module-latest = "^20241010"
black = "24.10.0"
isort = "^5.13.2"
pylint = "^3.3.2"
ruff = "^0.8.2"
numpy = "^1.23.5"
fake-bpy-module-latest = "^20221006"
black = "^22.10.0"
pylint = "^2.15.7"
ruff = "^0.0.166"
[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
[tool.black]
exclude = '''
/(
\.eggs
| \.git
| \.hg
| \.mypy_cache
| \.tox
| \.venv
| _build
| buck-out
| build
| dist
)/
'''
include = '\.pyi?$'
line-length = 88
target-version = ["py39", "py310", "py311", "py312", "py313"]
[tool.isort]
profile = "black"