Compare commits

...

38 Commits

Author SHA1 Message Date
Jedd Morgan ffd4da1ed9 fix(CI): Fix broken signing (#244)
* Poetry update

* Updated runner

* bump poetry again

* require export

* Fixed Digicerts broken sync certs
2025-04-23 11:35:23 +01:00
Jedd Morgan 26ba5c921b Poetry update (#243)
* Poetry update

* Updated runner

* bump poetry again

* require export
2025-04-22 18:26:01 +01:00
Jedd Morgan a3aaf4471c Bumped Dependencies (#215)
* Updated dependencies

* black + isort
2024-12-11 14:12:41 +00:00
Jedd Morgan 4995f02351 Fixed issue with local server builds (#212) 2024-11-22 11:57:55 +00:00
Jedd Morgan a456cdf38b bump speckle py (#211) 2024-11-18 12:45:43 +00:00
Jedd Morgan 7d9ef2d418 hotfix for previous PR mistakenly calling the get_project_workspace_id function with incorrect args (#210)
* workspace tracking + black

* works

* fixed issue
2024-11-05 17:56:34 +00:00
Jedd Morgan b8b7c0bdf5 Jedd/cxpla 94 track workspace id in specklepy connector metrics (#209)
* workspace tracking + black

* works
2024-11-05 13:09:38 +00:00
Claire Kuang f7c4fc3665 Merge pull request #208 from specklesystems/claire/cnx-699-update-github-links-to-point-to-v3
Update README.md to align with main github page
2024-11-01 18:46:20 +00:00
Claire Kuang cb9ba23c0c Update README.md to align with main github page 2024-11-01 18:45:25 +00:00
Alper S. Soylu 4c381bd809 fix send stream object cases (#207)
* fix send stream object cases

* clean logs

* revert workaround

* revert unnecessary logic

* revert unnecessary logic

* rename set_user

---------

Co-authored-by: Soylu <alper.soylu@siemens.com>
2024-08-05 13:36:06 +01:00
Alper S. Soylu 8c3885ece8 Lazy loading for stream branches & commits (#200)
* only fetch branches on reload

* initial load

* load on stream selection

* refactor get_item_by_index

* fix resetting commit selection on default branch

* refactor load_stream_branches

* clean logs

* explicit return

---------

Co-authored-by: Soylu <alper.soylu@siemens.com>
Co-authored-by: Jedd Morgan <45512892+JR-Morgan@users.noreply.github.com>
2024-07-29 12:15:33 +01:00
Alper S. Soylu 15bd3f5070 fix resetting commit selection on default branch (#206)
Co-authored-by: Soylu <alper.soylu@siemens.com>
2024-07-28 23:22:07 +01:00
Jedd Morgan 6fd4571d34 update macos images (#203) 2024-07-16 19:42:34 +02:00
Jedd Morgan 5081177653 Small tweaks to installer (#202)
* Skip installing dependencies after initial install on production

* remove print

* remove requirements.txt on install

* small tweaks

---------

Co-authored-by: Soylu <alper.soylu@siemens.com>
2024-07-12 12:23:36 +01:00
Alper S. Soylu 0d0ca2c811 Skip installing dependencies after initial install (#199)
* Skip installing dependencies after initial install on production

* remove print

* remove requirements.txt on install

---------

Co-authored-by: Soylu <alper.soylu@siemens.com>
2024-07-12 12:18:40 +01:00
Jedd Morgan 230e27a162 Merge pull request #196 from overengineer/fix/resetting_selected_branch_workaround
Recover branch selection
2024-06-05 14:54:21 +01:00
Jedd Morgan 669fd19c2e Type hint polish 2024-06-05 14:52:32 +01:00
Soylu 139f8ccb33 type hints 2024-06-03 19:10:54 +03:00
Soylu 7f625bd468 check None item 2024-05-30 18:11:29 +03:00
Soylu 4d07ba7637 use object ids 2024-05-30 16:53:53 +03:00
Jedd Morgan 7431b57e0e Merge pull request #198 from specklesystems/jrm/bump-deps
Updated deps
2024-05-27 16:34:39 +01:00
Jedd Morgan 57c19ba3c5 Updated deps 2024-05-27 16:34:07 +01:00
Jedd Morgan 8e3c2ece2f Merge pull request #197 from overengineer/fix/slow_startup
Speedup install dependencies
2024-05-24 16:48:29 +01:00
Soylu cfc58d9456 speedup install dependencies 2024-05-21 19:54:09 +03:00
Soylu e74a6cebb1 boundary check 2024-05-19 23:24:13 +03:00
Soylu 5e01d5a976 Recover branch selection 2024-05-16 18:19:36 +03:00
Jedd Morgan a2f7ab422f Merge pull request #194 from specklesystems/dev
2.19 changes
2024-05-14 17:41:21 +01:00
Jedd Morgan 8c58d9d14c Dev (#192)
* feat(UI): CNX-9070 update connectors to use new fe2 terminology (#186)

* feat: CNX-8705 fe2 ur ls in blender (#182)

* Increased default branch get to 100 limit, and added as mesh conversion for text + surfaces + metaball

* poetry lock

* Upgraded typing module

* FE2 URL support

* Raised exceptions now display to user

* Fixed unused imports

* Updated terminology to fe2

* merge from stash

* comments

* bl_descriptions

* bl_desc

* new urls

* Updated naming of revit elements to include family type (#193)
2024-05-14 17:38:50 +01:00
Jedd Morgan 90e61b6dc1 Updated naming of revit elements to include family type (#193) 2024-05-01 21:14:52 +01:00
Jedd Morgan 5c479e4c0e Merge branch 'main' into dev 2024-04-11 13:32:06 +01:00
Jedd Morgan 97d20ad7b1 Ci tweaks (#191) 2024-03-14 11:09:48 +00:00
Jedd Morgan 2800b84747 Merge pull request #190 from specklesystems/main
BACK MERGE MAIN -> DEV
2024-03-11 16:18:25 +00:00
Jedd Morgan 511d69314e feat(ci): [CNX-9125] Update to digicert-keylocker (#189)
* feat(ci): Update to digicert-keylocker

* removed pem
2024-03-11 17:14:36 +01:00
Jedd Morgan 24e7f02213 feat(UI): CNX-9070 update connectors to use new fe2 terminology (#186)
* feat: CNX-8705 fe2 ur ls in blender (#182)

* Increased default branch get to 100 limit, and added as mesh conversion for text + surfaces + metaball

* poetry lock

* Upgraded typing module

* FE2 URL support

* Raised exceptions now display to user

* Fixed unused imports

* Updated terminology to fe2

* merge from stash

* comments

* bl_descriptions

* bl_desc

* new urls
2024-02-28 18:53:11 +00:00
Jedd Morgan c1d7947085 Merge pull request #188 from specklesystems/main
deprecated delete stream (#187)
2024-02-28 16:46:37 +00:00
Jedd Morgan 21281e5d77 deprecated delete stream (#187) 2024-02-28 16:46:08 +00:00
Jedd Morgan 29bbdc69a2 chore(ci): CNX-9051 Update ci signing (#185)
* feat: CNX-8705 fe2 ur ls in blender (#182)

* Increased default branch get to 100 limit, and added as mesh conversion for text + surfaces + metaball

* poetry lock

* Upgraded typing module

* FE2 URL support

* Raised exceptions now display to user

* Fixed unused imports

* Update ci signing

* Update config.yml

* Bump Deps

* powershell

* More powershell
2024-02-27 11:29:33 +00:00
Jedd Morgan efe6e6a4a0 2.18 Update (#183)
* feat: CNX-8705 fe2 ur ls in blender (#182)

* Increased default branch get to 100 limit, and added as mesh conversion for text + surfaces + metaball

* poetry lock

* Upgraded typing module

* FE2 URL support

* Raised exceptions now display to user

* Fixed unused imports
2024-02-26 17:32:38 +00:00
31 changed files with 2584 additions and 2250 deletions
+45 -21
View File
@@ -6,7 +6,7 @@ orbs:
jobs:
package-connector:
docker:
- image: cimg/python:3.11.0
- image: cimg/python:3.11.12
steps:
- checkout
- run:
@@ -30,7 +30,7 @@ jobs:
build-connector-zip:
docker:
- image: cimg/python:3.11.0
- image: cimg/python:3.11.12
steps:
- attach_workspace:
at: ./
@@ -44,10 +44,10 @@ jobs:
root: ./
paths:
- bpy_speckle.zip
get-ci-tools: # Clones our ci tools and persists them to the workspace
docker:
- image: cimg/base:2021.01
- image: cimg/base:2025.04
steps:
- add_ssh_keys:
fingerprints:
@@ -71,24 +71,44 @@ jobs:
build-installer-win:
executor:
name: win/default
shell: cmd.exe
environment:
SSM: 'C:\Program Files\DigiCert\DigiCert One Signing Manager Tools'
steps:
- attach_workspace:
at: ./
- run:
name: Patch installer
shell: powershell.exe
command: python patch_installer.py (Get-Content -Raw SEMVER)
- run:
name: Create Innosetup signing cert
shell: powershell.exe
command: |
echo $env:PFX_B64 > "speckle-sharp-ci-tools\SignTool\AEC Systems Ltd.txt"
certutil -decode "speckle-sharp-ci-tools\SignTool\AEC Systems Ltd.txt" "speckle-sharp-ci-tools\SignTool\AEC Systems Ltd.pfx"
- run:
name: Installer
shell: cmd.exe #does not work in powershell
command: speckle-sharp-ci-tools\InnoSetup\ISCC.exe speckle-sharp-ci-tools\blender.iss /Sbyparam=$p
- 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
- persist_to_workspace:
root: ./
paths:
@@ -96,7 +116,8 @@ jobs:
build-installer-mac:
macos:
xcode: 12.5.1
xcode: 13.4.1
resource_class: macos.m1.medium.gen1
parameters:
runtime:
type: string
@@ -109,6 +130,9 @@ jobs:
- 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: |
@@ -142,7 +166,7 @@ jobs:
build-installer-manual:
docker:
- image: cimg/base:2021.01
- image: cimg/base:2025.04
parameters:
slug:
type: string
@@ -204,7 +228,7 @@ workflows:
filters: &build_filters
tags:
only: /([0-9]+)\.([0-9]+)\.([0-9]+)(?:-\w+)?(?:\.[0-9]+)?/
- build-connector-zip:
requires:
- package-connector
@@ -214,7 +238,7 @@ workflows:
filters: *build_filters
- build-installer-win:
context: innosetup
context: digicert-keylocker
name: Windows Installer Build
requires:
- package-connector
@@ -304,4 +328,4 @@ workflows:
- Windows Installer Build
- Mac Intel Build
- Mac ARM Build
filters: *deploy_filters
filters: *deploy_filters
+15 -41
View File
@@ -1,45 +1,20 @@
<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>
# About Speckle
> 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.
What is Speckle? Check our ![YouTube Video Views](https://img.shields.io/youtube/views/B9humiSpHzM?label=Speckle%20in%201%20minute%20video&style=social)
### Features
- **Object-based:** say goodbye to files! Speckle is the first object based platform for the AEC industry
- **Version control:** Speckle is the Git & Hub for geometry and BIM data
- **Collaboration:** share your designs collaborate with others
- **3D Viewer:** see your CAD and BIM models online, share and embed them anywhere
- **Interoperability:** get your CAD and BIM models into other software without exporting or importing
- **Real time:** get real time updates and notifications and changes
- **GraphQL API:** get what you need anywhere you want it
- **Webhooks:** the base for a automation and next-gen pipelines
- **Built for developers:** we are building Speckle with developers in mind and got tools for every stack
- **Built for the AEC industry:** Speckle connectors are plugins for the most common software used in the industry such as Revit, Rhino, Grasshopper, AutoCAD, Civil 3D, Excel, Unreal Engine, Unity, QGIS, Blender and more!
### Try Speckle now!
Give Speckle a try in no time by:
- [![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
<h3 align="center">
Speckle Connector for Blender
</h3>
> [!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
# Blender Connector
@@ -49,8 +24,7 @@ Head to the [**📚 documentation**](https://speckle.guide/user/blender.html) fo
## Installation
Currently, we are supporting all Blender 3.X versions on Windows and Mac.
We have experimental support for Blender 4.0 and greater.
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)
@@ -59,10 +33,10 @@ 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 `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.
- 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.
## Supported Elements
@@ -77,7 +51,7 @@ The full matrix of supported Blender and Speckle types [can be found here](https
- 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 are supported. The `colors` list from Speckle meshes is translated to a vertex color layer.
- 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
+9 -8
View File
@@ -1,15 +1,16 @@
import bpy
from bpy_speckle.installer import ensure_dependencies
ensure_dependencies(f"Blender {bpy.app.version[0]}.{bpy.app.version[1]}")
from bpy.app.handlers import persistent
from specklepy.logging import metrics
from bpy_speckle.ui import *
from bpy_speckle.properties import *
from bpy_speckle.operators import *
from bpy_speckle.callbacks import *
from bpy.app.handlers import persistent
from bpy_speckle.operators import *
from bpy_speckle.properties import *
from bpy_speckle.ui import *
bl_info = {
"name": "SpeckleBlender 2.0",
@@ -24,7 +25,6 @@ bl_info = {
}
"""
Import SpeckleBlender classes
"""
@@ -34,16 +34,18 @@ 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()
# bpy.ops.speckle.users_load()
# Instead, we shall just reset the user selection to an uninitiailised state
bpy.ops.speckle.users_reset()
"""
Permanent handle on callbacks
"""
@@ -93,7 +95,6 @@ def register():
def unregister():
bpy.app.handlers.load_post.remove(load_handler)
"""
+50 -30
View File
@@ -1,24 +1,33 @@
from typing import Dict, Optional, Tuple, Union
from typing import Dict, Optional, Union
import bpy
from bpy.types import Object, Collection, ID
from specklepy.objects.base import Base
from bpy_speckle.functions import _report
from specklepy.objects.graph_traversal.commit_object_builder import CommitObjectBuilder, ROOT
from specklepy.objects import Base
from specklepy.objects.other import Collection as SCollection
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}"
# 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 = SCollection(
name=col.name_full, collectionType="Blender Collection", elements=[]
)
converted_collection.applicationId = _id(col)
color_tag = col.color_tag
@@ -27,9 +36,9 @@ def convert_collection_to_speckle(col: Collection) -> SCollection:
return converted_collection
@define(slots=True)
class BlenderCommitObjectBuilder(CommitObjectBuilder[Object]):
_collections: Dict[str, SCollection]
def __init__(self) -> None:
@@ -37,35 +46,41 @@ class BlenderCommitObjectBuilder(CommitObjectBuilder[Object]):
self._collections = {}
def include_object(self, conversion_result: Base, native_object: Object) -> None:
# Set the Child -> Parent relationships
parent = native_object.parent
parent_collections: Tuple[Collection] = native_object.users_collection # type: ignore
parent_collection = parent_collections[0] if len(parent_collections) > 0 else None #NOTE: we don't support objects appearing in more than one collection, for now, we will just take the zeroth one
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))
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!
return self._collections[id] # collection already converted!
# Set the Parent -> Children relationships
# 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))
# 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))
# self.set_relationship(id, (_try_builder_id(parent), ELEMENTS), (ROOT, ELEMENTS))
converted_collection = convert_collection_to_speckle(col)
self.converted[id] = converted_collection
@@ -74,12 +89,12 @@ class BlenderCommitObjectBuilder(CommitObjectBuilder[Object]):
return converted_collection
def build_commit_object(self, root_commit_object: Base) -> None:
assert(root_commit_object.applicationId in self.converted)
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: #type: ignore
for col in bpy.context.scene.collection.children_recursive:
self.ensure_collection(col)
objects_to_build = set(self.converted.values())
@@ -87,19 +102,22 @@ class BlenderCommitObjectBuilder(CommitObjectBuilder[Object]):
self.apply_relationships(objects_to_build, root_commit_object)
assert(isinstance(root_commit_object, SCollection))
assert isinstance(root_commit_object, SCollection)
# Kill unused collections
def should_remove_unuseful_collection(col: SCollection) -> bool: #TODO: this maybe could be optimised
def should_remove_unuseful_collection(
col: SCollection,
) -> bool: # TODO: this maybe could be optimised
elements = col.elements
if not elements: return True
if not elements:
return True
should_remove_this_col = True
i = 0
while i < len(elements):
c = elements[i]
if not isinstance(c, SCollection):
if not isinstance(c, SCollection):
# col has objects (c)
should_remove_this_col = False
i += 1
@@ -113,8 +131,10 @@ class BlenderCommitObjectBuilder(CommitObjectBuilder[Object]):
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
_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 .on_mesh_edit import scb_on_mesh_edit
from .draw_speckle_info import draw_speckle_info
from .on_mesh_edit import scb_on_mesh_edit
+1 -1
View File
@@ -1,7 +1,7 @@
"""
Permanent handle on all user clients
"""
from specklepy.core.api.client import SpeckleClient
speckle_clients: list[SpeckleClient] = []
+2 -2
View File
@@ -10,7 +10,7 @@ IGNORED_PROPERTY_KEYS = {
"vertices",
"renderMaterial",
"textureCoordinates",
"totalChildrenCount"
"totalChildrenCount",
}
DISPLAY_VALUE_PROPERTY_ALIASES = {"displayValue", "@displayValue"}
@@ -19,4 +19,4 @@ ELEMENTS_PROPERTY_ALIASES = {"elements", "@elements"}
OBJECT_NAME_MAX_LENGTH = 62
SPECKLE_ID_LENGTH = 32
OBJECT_NAME_SPECKLE_SEPARATOR = " -- "
OBJECT_NAME_NUMERAL_SEPARATOR = '.'
OBJECT_NAME_NUMERAL_SEPARATOR = "."
+355 -191
View File
@@ -1,66 +1,94 @@
import math
from typing import Any, Dict, Iterable, List, Optional, Sequence, Union, Collection, cast
from bpy_speckle.convert.constants import DISPLAY_VALUE_PROPERTY_ALIASES, ELEMENTS_PROPERTY_ALIASES, OBJECT_NAME_MAX_LENGTH, OBJECT_NAME_NUMERAL_SEPARATOR, OBJECT_NAME_SPECKLE_SEPARATOR, SPECKLE_ID_LENGTH
from bpy_speckle.functions import get_default_traversal_func, get_scale_length, _report
from bpy_speckle.convert.util import ConversionSkippedException
from mathutils import (
Matrix as MMatrix,
Vector as MVector,
Quaternion as MQuaternion,
)
import bpy, bmesh
from specklepy.objects.other import (
Collection as SCollection,
Instance,
Transform,
BlockDefinition,
)
from typing import Any, Collection, Dict, Iterable, List, Optional, Union
import bmesh
import bpy
from bpy.types import Collection as BCollection
from bpy.types import Object
from mathutils import Matrix as MMatrix
from mathutils import Quaternion as MQuaternion
from mathutils import Vector as MVector
from specklepy.objects.base import Base
from specklepy.objects.geometry import Mesh, Line, Polyline, Curve, Arc, Polycurve, Ellipse, Circle, Plane
from bpy.types import Object, Collection as BCollection
from specklepy.objects.geometry import (
Arc,
Circle,
Curve,
Ellipse,
Line,
Mesh,
Plane,
Polycurve,
Polyline,
)
from specklepy.objects.other import BlockDefinition
from specklepy.objects.other import Collection as SCollection
from specklepy.objects.other import Instance, Transform
from bpy_speckle.convert.constants import (
DISPLAY_VALUE_PROPERTY_ALIASES,
ELEMENTS_PROPERTY_ALIASES,
OBJECT_NAME_MAX_LENGTH,
OBJECT_NAME_NUMERAL_SEPARATOR,
OBJECT_NAME_SPECKLE_SEPARATOR,
SPECKLE_ID_LENGTH,
)
from bpy_speckle.convert.util import ConversionSkippedException
from bpy_speckle.functions import _report, get_default_traversal_func, get_scale_length
from .util import (
add_colors,
add_custom_properties,
add_faces,
add_to_hierarchy,
add_uv_coords,
add_vertices,
get_render_material,
get_vertex_color_material,
render_material_to_native,
add_custom_properties,
add_vertices,
add_faces,
add_colors,
add_uv_coords,
)
SUPPORTED_CURVES = (Line, Polyline, Curve, Arc, Polycurve, Ellipse, Circle)
CAN_CONVERT_TO_NATIVE = (
Mesh,
*SUPPORTED_CURVES,
Instance,
)
def _has_native_conversion(speckle_object: Base) -> bool:
return any(isinstance(speckle_object, t) for t in CAN_CONVERT_TO_NATIVE) or "View" in speckle_object.speckle_type #hack
def _has_native_conversion(speckle_object: Base) -> bool:
return (
any(isinstance(speckle_object, t) for t in CAN_CONVERT_TO_NATIVE)
or "View" in speckle_object.speckle_type
) # hack
def _has_fallback_conversion(speckle_object: Base) -> bool:
return any(
getattr(speckle_object, alias, None) for alias in DISPLAY_VALUE_PROPERTY_ALIASES
)
def _has_fallback_conversion(speckle_object: Base) -> bool:
return any(getattr(speckle_object, alias, None) for alias in DISPLAY_VALUE_PROPERTY_ALIASES)
def can_convert_to_native(speckle_object: Base) -> bool:
if(_has_native_conversion(speckle_object) or _has_fallback_conversion(speckle_object)):
if _has_native_conversion(speckle_object) or _has_fallback_conversion(
speckle_object
):
return True
return False
convert_instances_as: str = "" #HACK: This is hacky, we need a better way to pass settings down to the converter
convert_instances_as: str = (
"" # HACK: This is hacky, we need a better way to pass settings down to the converter
)
def set_convert_instances_as(value: str):
global convert_instances_as
convert_instances_as = value
#TODO: Check usages handle exceptions
# TODO: Check usages handle exceptions
def convert_to_native(speckle_object: Base) -> Object:
speckle_type = type(speckle_object)
object_name = _generate_object_name(speckle_object)
@@ -71,9 +99,13 @@ def convert_to_native(speckle_object: Base) -> Object:
# convert elements/breps
if not _has_native_conversion(speckle_object):
(converted, children) = display_value_to_native(speckle_object, object_name, scale)
(converted, children) = display_value_to_native(
speckle_object, object_name, scale
)
if not converted and not children:
raise Exception(f"Zero geometry converted from displayValues for {speckle_object}")
raise Exception(
f"Zero geometry converted from displayValues for {speckle_object}"
)
# convert supported geometry
elif isinstance(speckle_object, Mesh):
@@ -81,24 +113,25 @@ def convert_to_native(speckle_object: Base) -> Object:
elif speckle_type in SUPPORTED_CURVES:
converted = icurve_to_native(speckle_object, object_name, scale)
elif "View" in speckle_object.speckle_type:
return view_to_native(speckle_object, object_name, scale)
return view_to_native(speckle_object, object_name, scale)
elif isinstance(speckle_object, Instance):
if convert_instances_as == "linked_duplicates":
converted = instance_to_native_object(speckle_object, scale)
converted = instance_to_native_object(speckle_object, scale)
elif convert_instances_as == "collection_instance":
converted = instance_to_native_collection_instance(speckle_object, scale)
else:
_report(f"convert_instances_as = '{convert_instances_as}' is not implemented, Instances will be converted as collection instances!")
_report(
f"convert_instances_as = '{convert_instances_as}' is not implemented, Instances will be converted as collection instances!"
)
converted = instance_to_native_collection_instance(speckle_object, scale)
else:
raise Exception(f"Unsupported type {speckle_type}")
if not isinstance(converted, Object):
converted = create_new_object(converted, object_name)
converted.speckle.object_id = str(speckle_object.id) # type: ignore
converted.speckle.enabled = True # type: ignore
converted.speckle.object_id = str(speckle_object.id) # type: ignore
converted.speckle.enabled = True # type: ignore
add_custom_properties(speckle_object, converted)
for c in children:
@@ -107,15 +140,30 @@ def convert_to_native(speckle_object: Base) -> Object:
return converted
def display_value_to_native(
speckle_object: Base, name: str, scale: float
) -> tuple[Optional[bpy.types.Mesh], list[bpy.types.Object]]:
return _members_to_native(
speckle_object, name, scale, DISPLAY_VALUE_PROPERTY_ALIASES, True
)
def display_value_to_native(speckle_object: Base, name: str, scale: float) -> tuple[Optional[bpy.types.Mesh], list[bpy.types.Object]]:
return _members_to_native(speckle_object, name, scale, DISPLAY_VALUE_PROPERTY_ALIASES, True)
def elements_to_native(speckle_object: Base, name: str, scale: float) -> list[bpy.types.Object]:
(_, elements) = _members_to_native(speckle_object, name, scale, ELEMENTS_PROPERTY_ALIASES, False)
def elements_to_native(
speckle_object: Base, name: str, scale: float
) -> list[bpy.types.Object]:
(_, elements) = _members_to_native(
speckle_object, name, scale, ELEMENTS_PROPERTY_ALIASES, False
)
return elements
def _members_to_native(speckle_object: Base, name: str, scale: float, members: Iterable[str], combineMeshes: bool) -> tuple[Optional[bpy.types.Mesh], list[bpy.types.Object]]:
def _members_to_native(
speckle_object: Base,
name: str,
scale: float,
members: Iterable[str],
combineMeshes: bool,
) -> tuple[Optional[bpy.types.Mesh], list[bpy.types.Object]]:
"""
Converts a given speckle_object by converting specified members
@@ -133,7 +181,8 @@ def _members_to_native(speckle_object: Base, name: str, scale: float, members: I
display = getattr(speckle_object, alias, None)
count = 0
MAX_DEPTH = 255 # some large value, to prevent infinite recursion
MAX_DEPTH = 255 # some large value, to prevent infinite recursion
def separate(value: Any) -> bool:
nonlocal meshes, others, count, MAX_DEPTH
@@ -143,24 +192,27 @@ def _members_to_native(speckle_object: Base, name: str, scale: float, members: I
others.append(value)
elif isinstance(value, list):
count += 1
if(count > MAX_DEPTH):
if count > MAX_DEPTH:
return True
for x in value:
separate(x)
separate(x)
return False
did_halt = separate(display)
if did_halt:
_report(f"Traversal of {speckle_object.speckle_type} {speckle_object.id} halted after traversal depth exceeds MAX_DEPTH={MAX_DEPTH}. Are there circular references object structure?")
_report(
f"Traversal of {speckle_object.speckle_type} {speckle_object.id} halted after traversal depth exceeds MAX_DEPTH={MAX_DEPTH}. Are there circular references object structure?"
)
children: list[Object] = []
mesh = None
if meshes:
mesh = meshes_to_native(speckle_object, meshes, name, scale) #TODO: reconsider passing scale around...
mesh = meshes_to_native(
speckle_object, meshes, name, scale
) # TODO: reconsider passing scale around...
for item in others:
try:
@@ -172,14 +224,13 @@ def _members_to_native(speckle_object: Base, name: str, scale: float, members: I
return (mesh, children)
def view_to_native(speckle_view, name: str, scale: float) -> bpy.types.Object:
native_cam: bpy.types.Camera
if name in bpy.data.cameras.keys():
native_cam = bpy.data.cameras[name]
native_cam = bpy.data.cameras[name]
else:
native_cam = bpy.data.cameras.new(name=name)
native_cam.lens = 18 # 90° horizontal fov
native_cam.lens = 18 # 90° horizontal fov
if not hasattr(speckle_view, "origin"):
raise ConversionSkippedException("2D views not supported")
@@ -187,28 +238,44 @@ def view_to_native(speckle_view, name: str, scale: float) -> bpy.types.Object:
cam_obj = create_new_object(native_cam, name)
scale_factor = get_scale_factor(speckle_view, scale)
tx = (speckle_view.origin.x * scale_factor)
ty = (speckle_view.origin.y * scale_factor)
tz = (speckle_view.origin.z * scale_factor)
tx = speckle_view.origin.x * scale_factor
ty = speckle_view.origin.y * scale_factor
tz = speckle_view.origin.z * scale_factor
forward = MVector((speckle_view.forwardDirection.x, speckle_view.forwardDirection.y, speckle_view.forwardDirection.z))
up = MVector((speckle_view.upDirection.x, speckle_view.upDirection.y, speckle_view.upDirection.z))
forward = MVector(
(
speckle_view.forwardDirection.x,
speckle_view.forwardDirection.y,
speckle_view.forwardDirection.z,
)
)
up = MVector(
(
speckle_view.upDirection.x,
speckle_view.upDirection.y,
speckle_view.upDirection.z,
)
)
right = forward.cross(up).normalized()
cam_obj.matrix_world = MMatrix((
(right.x, up.x, -forward.x, tx),
(right.y, up.y, -forward.y, ty),
(right.z, up.z, -forward.z, tz),
(0, 0, 0, 1 )
))
cam_obj.matrix_world = MMatrix(
(
(right.x, up.x, -forward.x, tx),
(right.y, up.y, -forward.y, ty),
(right.z, up.z, -forward.z, tz),
(0, 0, 0, 1),
)
)
return cam_obj
def mesh_to_native(speckle_mesh: Mesh, name: str, scale: float) -> bpy.types.Mesh:
return meshes_to_native(speckle_mesh, [speckle_mesh], name, scale)
def meshes_to_native(element: Base, meshes: Collection[Mesh], name: str, scale: float) -> bpy.types.Mesh:
def meshes_to_native(
element: Base, meshes: Collection[Mesh], name: str, scale: float
) -> bpy.types.Mesh:
if name in bpy.data.meshes.keys():
return bpy.data.meshes[name]
blender_mesh = bpy.data.meshes.new(name=name)
@@ -227,7 +294,8 @@ def meshes_to_native(element: Base, meshes: Collection[Mesh], name: str, scale:
# Second pass, add face data
offset = 0
for i, mesh in enumerate(meshes):
if not mesh.vertices: continue
if not mesh.vertices:
continue
add_faces(mesh, bm, offset, i)
@@ -253,14 +321,14 @@ def meshes_to_native(element: Base, meshes: Collection[Mesh], name: str, scale:
add_colors(mesh, bm)
except Exception as ex:
_report(f"Skipping converting vertex colors for {name}: {ex}")
try:
add_uv_coords(mesh, bm)
except Exception as ex:
_report(f"Skipping converting uv coordinates for {name}: {ex}")
bm.to_mesh(blender_mesh)
bm.free()
bm.free()
return blender_mesh
@@ -269,8 +337,12 @@ def meshes_to_native(element: Base, meshes: Collection[Mesh], name: str, scale:
Curves
"""
def line_to_native(speckle_curve: Line, blender_curve: bpy.types.Curve, scale: float) -> List[bpy.types.Spline]:
if not speckle_curve.end: return []
def line_to_native(
speckle_curve: Line, blender_curve: bpy.types.Curve, scale: float
) -> List[bpy.types.Spline]:
if not speckle_curve.end:
return []
line = blender_curve.splines.new("POLY")
line.points.add(1)
@@ -292,8 +364,11 @@ def line_to_native(speckle_curve: Line, blender_curve: bpy.types.Curve, scale: f
return [line]
def polyline_to_native(scurve: Polyline, bcurve: bpy.types.Curve, scale: float) -> List[bpy.types.Spline]:
if not (value := scurve.value): return []
def polyline_to_native(
scurve: Polyline, bcurve: bpy.types.Curve, scale: float
) -> List[bpy.types.Spline]:
if not (value := scurve.value):
return []
N = len(value) // 3
polyline = bcurve.splines.new("POLY")
@@ -311,22 +386,27 @@ def polyline_to_native(scurve: Polyline, bcurve: bpy.types.Curve, scale: float)
)
return [polyline]
def nurbs_to_native(scurve: Curve, bcurve: bpy.types.Curve, scale: float) -> List[bpy.types.Spline]:
if not (points := scurve.points): return []
if not scurve.degree: raise Exception("curve is missing degree")
if not scurve.weights: raise Exception("curve is missing weights")
def nurbs_to_native(
scurve: Curve, bcurve: bpy.types.Curve, scale: float
) -> List[bpy.types.Spline]:
if not (points := scurve.points):
return []
if not scurve.degree:
raise Exception("curve is missing degree")
if not scurve.weights:
raise Exception("curve is missing weights")
# Closed curves from rhino will have n + degree points. We ignore the extras
num_points = len(points) // 3 - scurve.degree if (scurve.closed) else (
len(points) // 3)
num_points = (
len(points) // 3 - scurve.degree if (scurve.closed) else (len(points) // 3)
)
nurbs = bcurve.splines.new("NURBS")
nurbs.use_cyclic_u = scurve.closed or False
nurbs.use_endpoint_u = not scurve.periodic
nurbs.points.add(num_points - 1)
use_weights = len(scurve.weights) >= num_points
for i in range(num_points):
@@ -336,7 +416,7 @@ def nurbs_to_native(scurve: Curve, bcurve: bpy.types.Curve, scale: float) -> Lis
float(points[i * 3 + 2]) * scale,
1,
)
nurbs.points[i].weight = scurve.weights[i] if use_weights else 1
nurbs.order_u = scurve.degree + 1
@@ -344,11 +424,16 @@ def nurbs_to_native(scurve: Curve, bcurve: bpy.types.Curve, scale: float) -> Lis
return [nurbs]
def arc_to_native(rcurve: Arc, bcurve: bpy.types.Curve, scale: float) -> Optional[bpy.types.Spline]:
def arc_to_native(
rcurve: Arc, bcurve: bpy.types.Curve, scale: float
) -> Optional[bpy.types.Spline]:
# TODO: improve Blender representation of arc - check autocad test stream
if not rcurve.radius: raise Exception("curve is missing radius")
if not rcurve.startAngle: raise Exception("curve is missing startAngle")
if not rcurve.endAngle: raise Exception("curve is missing endAngle")
if not rcurve.radius:
raise Exception("curve is missing radius")
if not rcurve.startAngle:
raise Exception("curve is missing startAngle")
if not rcurve.endAngle:
raise Exception("curve is missing endAngle")
plane = rcurve.plane
if not plane:
@@ -360,8 +445,8 @@ def arc_to_native(rcurve: Arc, bcurve: bpy.types.Curve, scale: float) -> Optiona
startAngle = rcurve.startAngle
endAngle = rcurve.endAngle
startQuat = MQuaternion(normal, startAngle) # type: ignore
endQuat = MQuaternion(normal, endAngle) # type: ignore
startQuat = MQuaternion(normal, startAngle) # type: ignore
endQuat = MQuaternion(normal, endAngle) # type: ignore
# Get start and end vectors, centre point, angles, etc.
r1 = MVector([plane.xdir.x, plane.xdir.y, plane.xdir.z])
@@ -386,7 +471,7 @@ def arc_to_native(rcurve: Arc, bcurve: bpy.types.Curve, scale: float) -> Optiona
Ndiv = max(int(math.floor(angle / 0.3)), 2)
step = angle / float(Ndiv)
stepQuat = MQuaternion(normal, step) # type: ignore
stepQuat = MQuaternion(normal, step) # type: ignore
tan = math.tan(step / 2) * radius
arc.points.add(Ndiv + 1)
@@ -409,11 +494,14 @@ def arc_to_native(rcurve: Arc, bcurve: bpy.types.Curve, scale: float) -> Optiona
return arc
def polycurve_to_native(scurve: Polycurve, bcurve: bpy.types.Curve, scale: float) -> list[bpy.types.Spline]:
def polycurve_to_native(
scurve: Polycurve, bcurve: bpy.types.Curve, scale: float
) -> list[bpy.types.Spline]:
"""
Convert Polycurve object
"""
if not scurve.segments: raise Exception("curve is missing segments")
if not scurve.segments:
raise Exception("curve is missing segments")
curves = []
@@ -426,46 +514,52 @@ def polycurve_to_native(scurve: Polycurve, bcurve: bpy.types.Curve, scale: float
_report(f"Unsupported curve type: {speckle_type}")
return curves
def ellipse_to_native(ellipse: Union[Ellipse, Circle], bcurve: bpy.types.Curve, units_scale: float) -> List[bpy.types.Spline]:
if not ellipse.plane: raise Exception("curve is missing plane")
def ellipse_to_native(
ellipse: Union[Ellipse, Circle], bcurve: bpy.types.Curve, units_scale: float
) -> List[bpy.types.Spline]:
if not ellipse.plane:
raise Exception("curve is missing plane")
radX: float
radY: float
if isinstance(ellipse, Ellipse):
if not ellipse.firstRadius: raise Exception("curve is missing firstRadius")
if not ellipse.secondRadius: raise Exception("curve is missing secondRadius")
if not ellipse.firstRadius:
raise Exception("curve is missing firstRadius")
if not ellipse.secondRadius:
raise Exception("curve is missing secondRadius")
radX = ellipse.firstRadius * units_scale
radY = ellipse.secondRadius * units_scale
else:
if not ellipse.radius: raise Exception("curve is missing radius")
if not ellipse.radius:
raise Exception("curve is missing radius")
radX = ellipse.radius * units_scale
radY = ellipse.radius * units_scale
D = 0.5522847498307936 # (4/3)*tan(pi/8)
D = 0.5522847498307936 # (4/3)*tan(pi/8)
right_handles = [
(+radX, +radY * D, 0.0),
(-radX * D, +radY, 0.0),
(-radX, -radY * D, 0.0),
(+radX * D, -radY, 0.0),
(+radX, +radY * D, 0.0),
(-radX * D, +radY, 0.0),
(-radX, -radY * D, 0.0),
(+radX * D, -radY, 0.0),
]
left_handles = [
(+radX, -radY * D, 0.0),
(+radX * D, +radY, 0.0),
(-radX, +radY * D, 0.0),
(-radX * D, -radY, 0.0),
(+radX, -radY * D, 0.0),
(+radX * D, +radY, 0.0),
(-radX, +radY * D, 0.0),
(-radX * D, -radY, 0.0),
]
points = [
(+radX, 0.0, 0.0),
(0.0, +radY, 0.0),
(-radX, 0.0, 0.0),
(0.0, -radY, 0.0),
(+radX, 0.0, 0.0),
(0.0, +radY, 0.0),
(-radX, 0.0, 0.0),
(0.0, -radY, 0.0),
]
transform = plane_to_native_transform(ellipse.plane, units_scale)
@@ -473,17 +567,19 @@ def ellipse_to_native(ellipse: Union[Ellipse, Circle], bcurve: bpy.types.Curve,
spline.bezier_points.add(len(points) - 1)
for i in range(len(points)):
spline.bezier_points[i].co = transform @ MVector(points[i]) # type: ignore
spline.bezier_points[i].handle_left = transform @ MVector(left_handles[i]) # type: ignore
spline.bezier_points[i].handle_right = transform @ MVector(right_handles[i]) # type: ignore
spline.bezier_points[i].co = transform @ MVector(points[i]) # type: ignore
spline.bezier_points[i].handle_left = transform @ MVector(left_handles[i]) # type: ignore
spline.bezier_points[i].handle_right = transform @ MVector(right_handles[i]) # type: ignore
spline.use_cyclic_u = True
#TODO support trims?
# TODO support trims?
return [spline]
def icurve_to_native_spline(speckle_curve: Base, blender_curve: bpy.types.Curve, scale: float) -> List[bpy.types.Spline]:
def icurve_to_native_spline(
speckle_curve: Base, blender_curve: bpy.types.Curve, scale: float
) -> List[bpy.types.Spline]:
# polycurves
if isinstance(speckle_curve, Polycurve):
return polycurve_to_native(speckle_curve, blender_curve, scale)
@@ -502,7 +598,9 @@ def icurve_to_native_spline(speckle_curve: Base, blender_curve: bpy.types.Curve,
elif isinstance(speckle_curve, Ellipse) or isinstance(speckle_curve, Circle):
splines = ellipse_to_native(speckle_curve, blender_curve, scale)
else:
raise TypeError(f"{speckle_curve} is not a supported curve type. Supported types: {SUPPORTED_CURVES}")
raise TypeError(
f"{speckle_curve} is not a supported curve type. Supported types: {SUPPORTED_CURVES}"
)
return splines
@@ -518,7 +616,9 @@ def icurve_to_native(speckle_curve: Base, name: str, scale: float) -> bpy.types.
else bpy.data.curves.new(name, type="CURVE")
)
blender_curve.dimensions = "3D"
blender_curve.resolution_u = 12 #TODO: We could maybe decern the resolution from the polyline displayValue
blender_curve.resolution_u = (
12 # TODO: We could maybe decern the resolution from the polyline displayValue
)
icurve_to_native_spline(speckle_curve, blender_curve, scale)
@@ -529,6 +629,7 @@ def icurve_to_native(speckle_curve: Base, name: str, scale: float) -> bpy.types.
Transforms and Instances
"""
def transform_to_native(transform: Transform, scale: float) -> MMatrix:
mat = MMatrix(
[
@@ -540,33 +641,37 @@ def transform_to_native(transform: Transform, scale: float) -> MMatrix:
)
# scale the translation
for i in range(3):
mat[i][3] *= scale # type: ignore
mat[i][3] *= scale
return mat
def plane_to_native_transform(plane: Plane, fallback_scale:float = 1) -> MMatrix:
def plane_to_native_transform(plane: Plane, fallback_scale: float = 1) -> MMatrix:
scale_factor = get_scale_factor(plane, fallback_scale)
tx = (plane.origin.x * scale_factor)
ty = (plane.origin.y * scale_factor)
tz = (plane.origin.z * scale_factor)
tx = plane.origin.x * scale_factor
ty = plane.origin.y * scale_factor
tz = plane.origin.z * scale_factor
return MMatrix((
(plane.xdir.x, plane.ydir.x, plane.normal.x, tx),
(plane.xdir.y, plane.ydir.y, plane.normal.y, ty),
(plane.xdir.z, plane.ydir.z, plane.normal.z, tz),
(0, 0, 0, 1 )
))
return MMatrix(
(
(plane.xdir.x, plane.ydir.x, plane.normal.x, tx),
(plane.xdir.y, plane.ydir.y, plane.normal.y, ty),
(plane.xdir.z, plane.ydir.z, plane.normal.z, tz),
(0, 0, 0, 1),
)
)
"""
Instances / Blocks
"""
def _get_instance_name(instance: Instance) -> str:
if not instance.definition: raise Exception("Instance is missing a definition")
if not instance.definition:
raise Exception("Instance is missing a definition")
name_prefix = (
_get_friendly_object_name(instance)
or _get_friendly_object_name(instance.definition)
_get_friendly_object_name(instance)
or _get_friendly_object_name(instance.definition)
or _simplified_speckle_type(instance.speckle_type)
)
return f"{name_prefix}{OBJECT_NAME_SPECKLE_SEPARATOR}{instance.id}"
@@ -576,40 +681,45 @@ def instance_to_native_object(instance: Instance, scale: float) -> Object:
"""
Converts Instance to a unique object with (potentially) shared data (linked duplicate)
"""
if not instance.definition: raise Exception("Instance is missing a definition")
if not instance.transform: raise Exception("Instance is missing a transform")
if not instance.definition:
raise Exception("Instance is missing a definition")
if not instance.transform:
raise Exception("Instance is missing a transform")
definition = instance.definition
if not definition.id: raise Exception("Instance is missing a valid definition")
if not definition.id:
raise Exception("Instance is missing a valid definition")
name = _get_instance_name(instance)
native_instance: Optional[Object] = None
converted_objects: Dict[str, Union[Object, BCollection]] = {}
traversal_root: Base = definition
if not can_convert_to_native(definition):
# Non-convertible (like all blocks, and some revit instances) will not be converted as part of the deep_traversal.
# so we explicitly convert them as empties.
native_instance = create_new_object(None, name)
native_instance = create_new_object(None, name)
native_instance.empty_display_size = 0
converted_objects["__ROOT"] = native_instance # we create a dummy root to avoid id conflicts, since revit definitions have displayValues, they are convertible
converted_objects["__ROOT"] = (
native_instance # we create a dummy root to avoid id conflicts, since revit definitions have displayValues, they are convertible
)
traversal_root = Base(elements=definition, id="__ROOT")
#Convert definition + "elements" on definition
# Convert definition + "elements" on definition
_deep_conversion(traversal_root, converted_objects, False)
if not native_instance:
assert(can_convert_to_native(definition))
assert can_convert_to_native(definition)
if not definition.id in converted_objects:
if definition.id not in converted_objects:
raise Exception("Definition was not converted")
converted = converted_objects[definition.id]
if not isinstance(converted, Object):
raise Exception("Definition was not converted to an Object")
native_instance = converted
instance_transform = transform_to_native(instance.transform, scale)
@@ -617,7 +727,10 @@ def instance_to_native_object(instance: Instance, scale: float) -> Object:
return native_instance
def instance_to_native_collection_instance(instance: Instance, scale: float) -> bpy.types.Object:
def instance_to_native_collection_instance(
instance: Instance, scale: float
) -> bpy.types.Object:
"""
Convert an Instance as a transformed Object with the `instance_collection` property
set to be the `instance.Definition` converted as a collection
@@ -625,8 +738,10 @@ def instance_to_native_collection_instance(instance: Instance, scale: float) ->
The definition collection won't be linked to the current scene
Any Elements on the instance object will also be converted (and spacially transformed)
"""
if not instance.definition: raise Exception("Instance is missing a definition")
if not instance.transform: raise Exception("Instance is missing a transform")
if not instance.definition:
raise Exception("Instance is missing a definition")
if not instance.transform:
raise Exception("Instance is missing a transform")
name = _get_instance_name(instance)
@@ -637,16 +752,19 @@ def instance_to_native_collection_instance(instance: Instance, scale: float) ->
native_instance = create_new_object(None, name)
#add_custom_properties(instance, native_instance)
# add_custom_properties(instance, native_instance)
# hide the instance axes so they don't clutter the viewport
native_instance.empty_display_size = 0
native_instance.instance_collection = collection_def
native_instance.instance_type = "COLLECTION"
native_instance.matrix_world = instance_transform
return native_instance
def _instance_definition_to_native(definition: Union[Base, BlockDefinition]) -> bpy.types.Collection:
return native_instance
def _instance_definition_to_native(
definition: Union[Base, BlockDefinition]
) -> bpy.types.Collection:
"""
Converts a geometry carrying Base as a collection (does not link it to the scene)
"""
@@ -659,50 +777,72 @@ def _instance_definition_to_native(definition: Union[Base, BlockDefinition]) ->
native_def["applicationId"] = definition.applicationId
converted_objects = {}
converted_objects["__ROOT"] = native_def # we create a dummy root to avoid id conflicts, since revit definitions have displayValues, they are convertible
converted_objects["__ROOT"] = (
native_def # we create a dummy root to avoid id conflicts, since revit definitions have displayValues, they are convertible
)
dummyRoot = Base(elements=definition, id="__ROOT")
_deep_conversion(dummyRoot, converted_objects, True)
return native_def
def _deep_conversion(root: Base, converted_objects: Dict[str, Union[Object, BCollection]], preserve_transform: bool):
def _deep_conversion(
root: Base,
converted_objects: Dict[str, Union[Object, BCollection]],
preserve_transform: bool,
):
traversal_func = get_default_traversal_func(can_convert_to_native)
for item in traversal_func.traverse(root):
current: Base = item.current
if can_convert_to_native(current) or isinstance(current, SCollection):
try:
if not current or not current.id: raise Exception(f"{current} was an invalid speckle object")
if not current or not current.id:
raise Exception(f"{current} was an invalid speckle object")
#Convert the object!
# Convert the object!
converted_data_type: str
converted: Union[Object, BCollection, None]
if isinstance(current, SCollection):
if(current.collectionType == "Scene Collection"): raise ConversionSkippedException()
if current.collectionType == "Scene Collection":
raise ConversionSkippedException()
converted = collection_to_native(current)
converted_data_type = "COLLECTION"
else:
converted = convert_to_native(current)
converted_data_type = "COLLECTION_INSTANCE" if converted.instance_collection else str(converted.type)
converted_data_type = (
"COLLECTION_INSTANCE"
if converted.instance_collection
else str(converted.type)
)
if converted is None:
raise Exception("Conversion returned None")
converted_objects[current.id] = converted
add_to_hierarchy(converted, item, converted_objects, preserve_transform)
_report(f"Successfully converted {type(current).__name__} {current.id} as '{converted_data_type}'")
_report(
f"Successfully converted {type(current).__name__} {current.id} as '{converted_data_type}'"
)
except ConversionSkippedException as ex:
_report(f"Skipped converting {type(current).__name__} {current.id}: {ex}")
_report(
f"Skipped converting {type(current).__name__} {current.id}: {ex}"
)
except Exception as ex:
_report(f"Failed to converted {type(current).__name__} {current.id}: {ex}")
_report(
f"Failed to converted {type(current).__name__} {current.id}: {ex}"
)
def collection_to_native(collection: SCollection) -> BCollection:
name = collection.name or f"{collection.collectionType} -- {collection.applicationId or collection.id}" #TODO: consider consolidating name formatting with Rhino
ret = get_or_create_collection(name)
def collection_to_native(collection: SCollection) -> BCollection:
name = (
collection.name
or f"{collection.collectionType} -- {collection.applicationId or collection.id}"
) # TODO: consider consolidating name formatting with Rhino
ret = get_or_create_collection(name)
color = getattr(collection, "colorTag", None)
if color:
@@ -710,8 +850,9 @@ def collection_to_native(collection: SCollection) -> BCollection:
return ret
def get_or_create_collection(name: str, clear_collection: bool = True) -> BCollection:
#Disabled for now, since update mode needs rescoping.
# Disabled for now, since update mode needs rescoping.
# existing = cast(Optional[BCollection], bpy.data.collections.get(name))
# if existing:
# if clear_collection:
@@ -721,20 +862,20 @@ def get_or_create_collection(name: str, clear_collection: bool = True) -> BColle
# else:
new_collection = create_new_collection(name)
#NOTE: We want to not render revit "Rooms" collections by default.
# NOTE: We want to not render revit "Rooms" collections by default.
if name == "Rooms":
new_collection.hide_viewport = True
new_collection.hide_render = True
return new_collection
"""
Object Naming and Creation
"""
def create_new_collection( desired_name: str) -> bpy.types.Collection:
def create_new_collection(desired_name: str) -> bpy.types.Collection:
"""
Creates a new blender collection with a unique name
If the desired_name is already taken
@@ -745,7 +886,10 @@ def create_new_collection( desired_name: str) -> bpy.types.Collection:
blender_collection = bpy.data.collections.new(name)
return blender_collection
def create_new_object(obj_data: Optional[bpy.types.ID], desired_name: str) -> bpy.types.Object:
def create_new_object(
obj_data: Optional[bpy.types.ID], desired_name: str
) -> bpy.types.Object:
"""
Creates a new blender object with a unique name,
If the desired_name is already taken
@@ -756,42 +900,62 @@ def create_new_object(obj_data: Optional[bpy.types.ID], desired_name: str) -> bp
blender_object = bpy.data.objects.new(name, obj_data)
return blender_object
def _make_unique_name( desired_name: str, taken_names: Collection[str], counter: int = 0) -> str:
def _make_unique_name(
desired_name: str, taken_names: Collection[str], counter: int = 0
) -> str:
"""
Using Blenders default naming (append numeral in .xxx format) to avoid name conflicts with taken names
"""
name = desired_name if counter == 0 else f"{desired_name[:OBJECT_NAME_MAX_LENGTH - 4]}{OBJECT_NAME_NUMERAL_SEPARATOR}{counter:03d}" # format counter as name.xxx, truncate to ensure we don't exceed the object name max length
name = (
desired_name
if counter == 0
else f"{desired_name[:OBJECT_NAME_MAX_LENGTH - 4]}{OBJECT_NAME_NUMERAL_SEPARATOR}{counter:03d}"
) # format counter as name.xxx, truncate to ensure we don't exceed the object name max length
#TODO: This is very slow, and gets slower the more objects you receive with the same name...
# TODO: This is very slow, and gets slower the more objects you receive with the same name...
# We could use a binary/galloping search, and/or cache the name -> index within a receive.
if name in taken_names:
#Name already taken, increment counter and try again!
# Name already taken, increment counter and try again!
return _make_unique_name(desired_name, taken_names, counter + 1)
return name
def _get_friendly_object_name(speckle_object: Base) -> Optional[str]:
return (getattr(speckle_object, "name", None)
return (
getattr(speckle_object, "name", None)
or getattr(speckle_object, "Name", None)
or getattr(speckle_object, "family", None)
)
or _get_revit_family_name(speckle_object)
)
def _get_revit_family_name(speckle_object: Base) -> Optional[str]:
family = getattr(speckle_object, "family", None)
family_type = getattr(speckle_object, "type", None)
if family and family_type:
return f"{family_type}-{family}"
else:
return None
# Blender object names must not exceed 62 characters
# We need to ensure the complete ID is included in the name (to prevent identity collisions)
# So we if the name is too long, we need to truncate
def _truncate_object_name(name: str) -> str:
MAX_NAME_LENGTH = OBJECT_NAME_MAX_LENGTH - SPECKLE_ID_LENGTH - len(OBJECT_NAME_SPECKLE_SEPARATOR)
MAX_NAME_LENGTH = (
OBJECT_NAME_MAX_LENGTH - SPECKLE_ID_LENGTH - len(OBJECT_NAME_SPECKLE_SEPARATOR)
)
return name[:MAX_NAME_LENGTH]
def _simplified_speckle_type(speckle_type: str) -> str:
return(speckle_type.rsplit('.')[-1]) #Take only the most specific object type name (without namespace)
return speckle_type.rsplit(".")[
-1
] # Take only the most specific object type name (without namespace)
def _generate_object_name(speckle_object: Base) -> str:
prefix: str
@@ -808,4 +972,4 @@ def get_scale_factor(speckle_object: Base, fallback: float = 1.0) -> float:
scale = fallback
if units := getattr(speckle_object, "units", None):
scale = get_scale_length(units) / bpy.context.scene.unit_settings.scale_length
return scale
return scale
+206 -122
View File
@@ -1,26 +1,37 @@
from typing import Dict, Iterable, List, Optional, Tuple, Union, cast
import bpy
from bpy.types import (
Depsgraph,
MeshPolygon,
Object,
Curve as NCurve,
Mesh as NMesh,
Camera as NCamera,
)
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 mathutils import (
Matrix as MMatrix,
Vector as MVector,
)
from specklepy.objects import Base
from specklepy.objects.other import BlockInstance, BlockDefinition, RenderMaterial, Transform
from specklepy.objects.geometry import (
Mesh, Curve, Interval, Box, Point, Vector, Polyline,
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_speckle.convert.constants import (
OBJECT_NAME_SPECKLE_SEPARATOR,
SPECKLE_ID_LENGTH,
)
from bpy_speckle.convert.util import (
ConversionSkippedException,
get_blender_custom_properties,
@@ -30,14 +41,20 @@ from bpy_speckle.convert.util import (
)
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: str = "m" # The desired final units to send
UnitsScale: float = (
1 # The scale factor conversions need to apply to position data to get to the desired units
)
CAN_CONVERT_TO_SPECKLE = ("MESH", "CURVE", "EMPTY", "CAMERA", "FONT", "SURFACE", "META")
def convert_to_speckle(raw_blender_object: Object, units_scale: float, units: str, depsgraph: Optional[Depsgraph]) -> Base:
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
@@ -49,16 +66,21 @@ def convert_to_speckle(raw_blender_object: Object, units_scale: float, units: st
global Units, UnitsScale
Units = units
UnitsScale = units_scale
blender_type = raw_blender_object.type
if blender_type not in CAN_CONVERT_TO_SPECKLE:
raise ConversionSkippedException(f"Objects of type {blender_type} are not supported")
raise ConversionSkippedException(
f"Objects of type {blender_type} are not supported"
)
blender_object = cast(Object, (
raw_blender_object.evaluated_get(depsgraph)
if depsgraph
else raw_blender_object
))
blender_object = cast(
Object,
(
raw_blender_object.evaluated_get(depsgraph)
if depsgraph
else raw_blender_object
),
)
converted: Optional[Base] = None
if blender_type == "MESH":
@@ -68,30 +90,35 @@ def convert_to_speckle(raw_blender_object: Object, units_scale: float, units: st
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))
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")
converted["properties"] = get_blender_custom_properties(raw_blender_object) #NOTE: Depsgraph copies don't have custom properties so we use the raw version
converted["properties"] = get_blender_custom_properties(
raw_blender_object
) # NOTE: Depsgraph copies don't have custom properties so we use the raw version
# Set object transform #TODO: this could be deprecated once we add proper geometry instancing support
if blender_type != "EMPTY":
if blender_type != "EMPTY":
converted["properties"]["transform"] = transform_to_speckle(
blender_object.matrix_world
)
return converted
def mesh_to_speckle(blender_object: Object, data: bpy.types.Mesh) -> Base:
b = Base()
b["name"] = to_speckle_name(blender_object)
b["@displayValue"] = mesh_to_speckle_meshes(blender_object, data)
return b
def mesh_to_speckle_meshes(blender_object: Object, data: bpy.types.Mesh) -> List[Mesh]:
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]] = {}
@@ -101,7 +128,7 @@ def mesh_to_speckle_meshes(blender_object: Object, data: bpy.types.Mesh) -> List
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] # type: ignore
scaled_vertices = [tuple(transform @ x.co * UnitsScale) for x in data.vertices]
# Create Speckle meshes for each material
submeshes = []
@@ -109,8 +136,8 @@ def mesh_to_speckle_meshes(blender_object: Object, data: bpy.types.Mesh) -> List
for i in submesh_data:
index_mapping: Dict[int, int] = {}
#Loop through each polygon, and map indices to their new index in m_verts
# 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] = []
@@ -128,7 +155,7 @@ def mesh_to_speckle_meshes(blender_object: Object, data: bpy.types.Mesh) -> List
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)
@@ -143,43 +170,46 @@ def mesh_to_speckle_meshes(blender_object: Object, data: bpy.types.Mesh) -> List
colors=[],
textureCoordinates=m_texcoords,
units=Units,
area = mesh_area,
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)
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: MMatrix, spline: bpy.types.Spline, name: Optional[str] = None
) -> Curve:
degree = 3
closed = spline.use_cyclic_u
points: List[Tuple[MVector]] = []
for i, bp in enumerate(spline.bezier_points):
if i > 0:
points.append(tuple(matrix @ bp.handle_left * UnitsScale)) # type: ignore
points.append(tuple(matrix @ bp.co * UnitsScale)) # type: ignore
points.append(tuple(matrix @ bp.handle_left * UnitsScale)) # type: ignore
points.append(tuple(matrix @ bp.co * UnitsScale)) # type: ignore
if i < len(spline.bezier_points) - 1:
points.append(tuple(matrix @ bp.handle_right * UnitsScale)) # type: ignore
points.append(tuple(matrix @ bp.handle_right * UnitsScale)) # type: ignore
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 * UnitsScale), # type: ignore
tuple(matrix @ spline.bezier_points[0].handle_left * UnitsScale), # type: ignore
tuple(matrix @ spline.bezier_points[0].co * UnitsScale), # type: ignore
)
)
num_points = len(points)
flattened_points = []
for row in points: flattened_points.extend(row)
for row in points:
flattened_points.extend(row)
knot_count = num_points + degree - 1
knots = [0] * knot_count
@@ -193,7 +223,7 @@ def bezier_to_speckle(matrix: MMatrix, spline: bpy.types.Spline, name: Optional[
name=name,
degree=degree,
closed=spline.use_cyclic_u,
periodic= not spline.use_endpoint_u,
periodic=not spline.use_endpoint_u,
points=flattened_points,
weights=[1] * num_points,
knots=knots,
@@ -204,12 +234,13 @@ def bezier_to_speckle(matrix: MMatrix, spline: bpy.types.Spline, name: Optional[
domain=domain,
units=Units,
bbox=Box(area=0.0, volume=0.0),
displayValue = bezier_to_speckle_polyline(matrix, spline, length),
displayValue=bezier_to_speckle_polyline(matrix, spline, length),
)
def nurbs_to_speckle(matrix: MMatrix, spline: bpy.types.Spline, name: Optional[str] = None) -> Curve:
def nurbs_to_speckle(
matrix: MMatrix, spline: bpy.types.Spline, name: Optional[str] = None
) -> Curve:
degree = spline.order_u - 1
knots = make_knots(spline)
@@ -219,10 +250,11 @@ def nurbs_to_speckle(matrix: MMatrix, spline: bpy.types.Spline, name: Optional[s
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
points = [tuple(matrix @ pt.co.xyz * UnitsScale) for pt in spline.points] # type: ignore
flattened_points = []
for row in points: flattened_points.extend(row)
for row in points:
flattened_points.extend(row)
if spline.use_cyclic_u:
for i in range(0, degree * 3, 3):
@@ -230,7 +262,7 @@ def nurbs_to_speckle(matrix: MMatrix, spline: bpy.types.Spline, name: Optional[s
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])
@@ -238,11 +270,11 @@ def nurbs_to_speckle(matrix: MMatrix, spline: bpy.types.Spline, name: Optional[s
name=name,
degree=degree,
closed=spline.use_cyclic_u,
periodic= not spline.use_endpoint_u,
periodic=not spline.use_endpoint_u,
points=flattened_points,
weights=weights,
knots=knots,
rational=is_rational,
rational=is_rational,
area=0,
volume=0,
length=length,
@@ -252,41 +284,53 @@ def nurbs_to_speckle(matrix: MMatrix, spline: bpy.types.Spline, name: Optional[s
displayValue=nurbs_to_speckle_polyline(matrix, spline, length),
)
def nurbs_to_speckle_polyline(matrix: MMatrix, spline: bpy.types.Spline, length: Optional[float] = None) -> Polyline:
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)
scaled_point = cast(
Vector,
matrix
@ MVector(
(sampled_points[i + 0], sampled_points[i + 1], sampled_points[i + 2])
)
* UnitsScale,
)
points.append(scaled_point.x)
points.append(scaled_point.y)
points.append(scaled_point.z)
length = length or spline.calc_length()
domain = Interval(start=0, end=length, totalChildrenCount=0)
return Polyline(value=points, closed = spline.use_cyclic_u, domain=domain, area=0, len=length)
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]:
# 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
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)
@@ -305,22 +349,33 @@ def bezier_to_speckle_polyline(matrix: MMatrix, spline: bpy.types.Spline, length
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)
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
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
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)
for row in points:
flattened_points.extend(row)
length = spline.calc_length()
domain = Interval(start=0, end=length, totalChildrenCount=0)
@@ -346,40 +401,54 @@ def curve_to_speckle(blender_object: Object, data: bpy.types.Curve) -> Base:
b["@elements"] = curves
return b
def curve_to_speckle_geometry(blender_object: Object, data: bpy.types.Curve) -> Tuple[List[Mesh], List[Base]]:
assert(blender_object.type == "CURVE")
blender_object = cast(Object, blender_object.evaluated_get(bpy.context.view_layer.depsgraph))
def curve_to_speckle_geometry(
blender_object: Object, data: bpy.types.Curve
) -> Tuple[List[Mesh], List[Base]]:
assert blender_object.type == "CURVE"
blender_object = cast(
Object, blender_object.evaluated_get(bpy.context.view_layer.depsgraph)
)
matrix = cast(MMatrix, blender_object.matrix_world)
meshes: List[Mesh] = []
curves: List[Base] = []
#TODO: Could we support this better?
if data.bevel_mode == "OBJECT" and data.bevel_object != None:
# 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())
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(matrix, spline, to_speckle_name(blender_object))
)
elif spline.type == "NURBS":
curves.append(nurbs_to_speckle(matrix, spline, to_speckle_name(blender_object)))
curves.append(
nurbs_to_speckle(matrix, spline, to_speckle_name(blender_object))
)
elif spline.type == "POLY":
curves.append(poly_to_speckle(matrix, spline, to_speckle_name(blender_object)))
curves.append(
poly_to_speckle(matrix, spline, to_speckle_name(blender_object))
)
return (meshes, curves)
def anything_to_speckle_mesh(blender_object: Object) -> Base:
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
) -> Optional[List[Polyline]]:
UNITS = "m" if bpy.context.scene.unit_settings.system == "METRIC" else "ft"
if blender_object.type != "MESH":
@@ -392,7 +461,7 @@ def ngons_to_speckle_polylines(blender_object: Object, data: bpy.types.Mesh) ->
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 * UnitsScale) # type: ignore
domain = Interval(start=0, end=1)
poly = Polyline(
@@ -418,65 +487,75 @@ def material_to_speckle(blender_mat: bpy.types.Material) -> RenderMaterial:
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
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
emission_color = "Emission" if "Emission" in inputs else "Emission Color" # type: ignore
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
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
# TODO: Support more shaders
# fallback to standard material props
speckle_mat.diffuse = to_argb_int(blender_mat.diffuse_color) # type: ignore
speckle_mat.diffuse = to_argb_int(blender_mat.diffuse_color) # type: ignore
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':
if data.type != "PERSP":
raise Exception(f"Cameras of type {data.type} are not currently supported")
matrix = cast(MMatrix, blender_object.matrix_world)
up = matrix.col[1].xyz # type: ignore
forwards = -matrix.col[2].xyz # type: ignore
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 = 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.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,
)
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,
)
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
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]
# scale the translation
for i in (3, 7, 11):
@@ -492,9 +571,13 @@ def block_def_to_speckle(blender_definition: bpy.types.Collection) -> BlockDefin
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}")
_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}'")
_report(
f"Failed to converted '{geo.name_full}' inside collection instance: '{ex}'"
)
dummyRoot = Base()
geometryBuilder.apply_relationships(geometryBuilder.converted.values(), dummyRoot)
@@ -512,9 +595,7 @@ def block_def_to_speckle(blender_definition: bpy.types.Collection) -> BlockDefin
def block_instance_to_speckle(blender_instance: Object) -> BlockInstance:
return BlockInstance(
blockDefinition=block_def_to_speckle(
blender_instance.instance_collection
),
blockDefinition=block_def_to_speckle(blender_instance.instance_collection),
transform=transform_to_speckle(blender_instance.matrix_world),
name=to_speckle_name(blender_instance),
units=Units,
@@ -524,18 +605,21 @@ def block_instance_to_speckle(blender_instance: Object) -> BlockInstance:
def empty_to_speckle(blender_object: Object) -> Union[BlockInstance, Base]:
# probably an instance collection (block) so let's try it
if blender_object.instance_collection and blender_object.instance_type == "COLLECTION":
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))
wrapper["@displayValue"] = matrix_to_speckle_point(
cast(MMatrix, blender_object.matrix_world)
)
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)
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)
+115 -67
View File
@@ -1,20 +1,24 @@
import math
from typing import Any, Dict, Optional, Tuple, Union, cast
from bmesh.types import BMesh
import bpy, idprop
import bpy
import idprop
from bmesh.types import BMesh
from bpy.types import Collection as BCollection
from bpy.types import Material, Node, Object, ShaderNodeVertexColor
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 bpy_speckle.functions import _report
from bpy.types import Material, Object, Collection as BCollection, Node, ShaderNodeVertexColor, NodeInputs
from specklepy.objects.graph_traversal.traversal import TraversalContext
class ConversionSkippedException(Exception):
pass
def to_rgba(argb_int: int) -> Tuple[float, float, float, float]:
"""Converts the int representation of a colour into a percent RGBA tuple"""
alpha = ((argb_int >> 24) & 255) / 255
@@ -32,15 +36,21 @@ def to_argb_int(rgba_color: list[float]) -> int:
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[]
# 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}")
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}")
# TODO: Log this as it's unexpected!!!
print(
f"Skipping setting property ({key}={value}) on {blender_object.name_full}, Reason: {ex}"
)
def add_custom_properties(speckle_object: Base, blender_object: Object):
if blender_object is None:
@@ -51,7 +61,11 @@ def add_custom_properties(speckle_object: Base, blender_object: Object):
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:
@@ -66,14 +80,13 @@ def add_custom_properties(speckle_object: Base, blender_object: Object):
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():
elif isinstance(val, dict):
for k, v in val.items():
if not isinstance(v, Base):
set_custom_property(k, v, blender_object)
def render_material_to_native(speckle_mat: RenderMaterial) -> Material:
mat_name = speckle_mat.name
if not mat_name:
mat_name = speckle_mat.applicationId or speckle_mat.id or speckle_mat.get_id()
@@ -87,43 +100,48 @@ 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) # 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
# 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
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:
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
# 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 not "VERTEX_COLOR" in [node.type for node in nodes]:
vertex_color_node = cast(ShaderNodeVertexColor, nodes.new(type = "ShaderNodeVertexColor"))
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
link = links.new(vertex_color_node.outputs[0], principled_bsdf_node.inputs[0])
_ = 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"""
@@ -137,7 +155,6 @@ def get_render_material(speckle_object: Base) -> Optional[RenderMaterial]:
return speckle_mat
return None
def add_vertices(speckle_mesh: Mesh, blender_mesh: BMesh, scale=1.0):
@@ -154,10 +171,15 @@ def add_vertices(speckle_mesh: Mesh, blender_mesh: BMesh, scale=1.0):
)
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,
indexOffset: int,
materialIndex: int = 0,
smooth: bool = True,
):
sfaces = speckle_mesh.faces
if sfaces and len(sfaces) > 0:
i = 0
while i < len(sfaces):
@@ -168,7 +190,7 @@ def add_faces(speckle_mesh: Mesh, blender_mesh: BMesh, indexOffset: int, materia
i += 1
try:
f = blender_mesh.faces.new(
[blender_mesh.verts[x + indexOffset] for x in sfaces[i : i + n]] # type: ignore
[blender_mesh.verts[x + indexOffset] for x in sfaces[i : i + n]]
)
f.material_index = materialIndex
f.smooth = smooth
@@ -178,13 +200,11 @@ def add_faces(speckle_mesh: Mesh, blender_mesh: BMesh, indexOffset: int, materia
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)
@@ -198,13 +218,14 @@ def add_colors(speckle_mesh: Mesh, blender_mesh: BMesh):
)
# Make vertex colors
if len(scolors) == len(blender_mesh.verts): # type: ignore
if len(scolors) == len(blender_mesh.verts):
color_layer = blender_mesh.loops.layers.color.new("Col")
for face in blender_mesh.faces: # type: ignore
for face in blender_mesh.faces:
for loop in face.loops:
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
@@ -213,6 +234,7 @@ def argb_split(argb: int) -> Tuple[int, int, int, int]:
return (alpha, red, green, blue)
def add_uv_coords(speckle_mesh: Mesh, blender_mesh: BMesh):
s_uvs = speckle_mesh.textureCoordinates
if not s_uvs:
@@ -220,24 +242,23 @@ def add_uv_coords(speckle_mesh: Mesh, blender_mesh: BMesh):
try:
uv = []
if len(s_uvs) // 2 == len(blender_mesh.verts): # type: ignore
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}" # type: ignore
f"Failed to match UV coordinates to vert data. Blender mesh verts: {len(blender_mesh.verts)}, Speckle UVs: {len(s_uvs) // 2}"
)
return
# Make UVs
uv_layer = blender_mesh.loops.layers.uv.verify()
for f in blender_mesh.faces: # type: ignore
for l in f.loops:
luv = l[uv_layer]
luv.uv = uv[l.vert.index]
for f in blender_mesh.faces:
for loop in f.loops:
luv = loop[uv_layer]
luv.uv = uv[loop.vert.index]
except:
_report("Failed to decode texture coordinates.")
raise
@@ -246,8 +267,7 @@ 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",
@@ -257,6 +277,7 @@ 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:
@@ -271,22 +292,26 @@ 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] # type: ignore
return obj
"""
Python implementation of Blender's NURBS curve generation for to Speckle conversion
from: https://blender.stackexchange.com/a/34276
based on https://projects.blender.org/blender/blender/src/branch/main/source/blender/blenkernel/intern/curve.cc (check old version)
"""
def macro_knotsu(nu: bpy.types.Spline) -> int:
return nu.order_u + nu.point_count_u + (nu.order_u - 1 if nu.use_cyclic_u else 0)
def macro_segmentsu(nu: bpy.types.Spline) -> int:
return nu.point_count_u if nu.use_cyclic_u else nu.point_count_u - 1
def make_knots(nu: bpy.types.Spline) -> list[float]:
knots = [0.0] * macro_knotsu(nu)
flag = nu.use_endpoint_u + (nu.use_bezier_u << 1)
@@ -299,13 +324,13 @@ def make_knots(nu: bpy.types.Spline) -> list[float]:
def calc_knots(knots: list[float], point_count: int, order: int, flag: int) -> None:
pts_order = point_count + order
if flag == 1: # CU_NURB_ENDPOINT
if flag == 1: # CU_NURB_ENDPOINT
k = 0.0
for a in range(1, pts_order + 1):
knots[a - 1] = k
if a >= order and a <= point_count:
k += 1.0
elif flag == 2: # CU_NURB_BEZIER
elif flag == 2: # CU_NURB_BEZIER
if order == 4:
k = 0.34
for a in range(pts_order):
@@ -323,11 +348,20 @@ def calc_knots(knots: list[float], point_count: int, order: int, flag: int) -> N
knots[-1] = knots[-2]
def basis_nurb(t: float, order: int, point_count: int, knots: list[float], basis: list[float], start: int, end: int) -> Tuple[int, int]:
def basis_nurb(
t: float,
order: int,
point_count: int,
knots: list[float],
basis: list[float],
start: int,
end: int,
) -> Tuple[int, int]:
i1 = i2 = 0
orderpluspnts = order + point_count
opp2 = orderpluspnts - 1
# this is for float inaccuracy
if t < knots[0]:
t = knots[0]
@@ -352,11 +386,10 @@ def basis_nurb(t: float, order: int, point_count: int, knots: list[float], basis
else:
basis[i] = 0.0
basis[i] = 0.0 #type: ignore
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
@@ -384,8 +417,9 @@ def basis_nurb(t: float, order: int, point_count: int, knots: list[float], basis
return start, end
def nurb_make_curve(nu: bpy.types.Spline, resolu: int, stride: int = 3) -> list[float]:
""""BKE_nurb_makeCurve"""
""" "BKE_nurb_makeCurve"""
EPS = 1e-6
coord_index = istart = iend = 0
@@ -396,17 +430,22 @@ def nurb_make_curve(nu: bpy.types.Spline, resolu: int, stride: int = 3) -> list[
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))
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)
istart, iend = basis_nurb(
u, nu.order_u, nu.point_count_u + cycl, knots, basisu, istart, iend
)
#/* calc sum */
# /* calc sum */
sumdiv = 0.0
sum_index = 0
pt_index = istart - 1
@@ -416,17 +455,17 @@ def nurb_make_curve(nu: bpy.types.Spline, resolu: int, stride: int = 3) -> list[
else:
pt_index += 1
sum_array[sum_index] = basisu[i] * nu.points[pt_index].co[3] #type: ignore
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_array[sum_index] /= sumdiv # type: ignore
sum_index += 1
coord_array[coord_index: coord_index + 3] = (0.0, 0.0, 0.0)
coord_array[coord_index : coord_index + 3] = (0.0, 0.0, 0.0)
sum_index = 0
pt_index = istart - 1
@@ -438,7 +477,9 @@ def nurb_make_curve(nu: bpy.types.Spline, resolu: int, stride: int = 3) -> list[
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]
coord_array[coord_index + j] += (
sum_array[sum_index] * nu.points[pt_index].co[j]
)
sum_index += 1
coord_index += stride
@@ -446,14 +487,21 @@ def nurb_make_curve(nu: bpy.types.Spline, resolu: int, stride: int = 3) -> list[
return coord_array
def link_object_to_collection_nested(obj: Object, col: BCollection):
if obj.name not in col.objects: #type: ignore
if obj.name not in col.objects: # type: ignore
col.objects.link(obj)
for child in obj.children: #type: ignore
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:
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
@@ -467,7 +515,7 @@ def add_to_hierarchy(converted: Union[Object, BCollection], traversalContext : '
if isinstance(c, BCollection):
parent_collection = c
break
else: #isinstance(c, Object):
else: # isinstance(c, Object):
parent_object = parent_object or c
nextParent = nextParent.parent
@@ -485,8 +533,8 @@ def add_to_hierarchy(converted: Union[Object, BCollection], traversalContext : '
def set_parent(child: Object, parent: Object, preserve_transform: bool = False) -> None:
if preserve_transform :
previous = child.matrix_world.copy() # type: ignore
if preserve_transform:
previous = child.matrix_world.copy() # type: ignore
child.parent = parent
child.matrix_world = previous
else:
+18 -14
View File
@@ -1,10 +1,11 @@
from typing import Callable
from specklepy.objects.base import Base
from bpy_speckle.convert.constants import ELEMENTS_PROPERTY_ALIASES
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
from bpy_speckle.convert.constants import ELEMENTS_PROPERTY_ALIASES
def _report(msg: object) -> None:
"""
@@ -18,28 +19,31 @@ def get_scale_length(units: str) -> float:
return get_scale_factor_to_meters(get_units_from_string(units))
def get_default_traversal_func(can_convert_to_native: Callable[[Base], bool]) -> GraphTraversal:
def get_default_traversal_func(
can_convert_to_native: Callable[[Base], bool]
) -> GraphTraversal:
"""
Traversal func for traversing a speckle commit object
"""
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 _: [],
[
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 _: [],
)
convertible_rule = TraversalRule(
[can_convert_to_native],
lambda _: ELEMENTS_PROPERTY_ALIASES,
[can_convert_to_native],
lambda _: ELEMENTS_PROPERTY_ALIASES,
)
default_rule = TraversalRule(
[lambda _: True],
lambda o: o.get_member_names(), #TODO: avoid deprecated members
[lambda _: True],
lambda o: o.get_member_names(), # TODO: avoid deprecated members
)
return GraphTraversal([ignore_rule, convertible_rule, default_rule])
+40 -23
View File
@@ -1,11 +1,12 @@
"""
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, invalidate_caches
_user_data_env_var = "SPECKLE_USERDATA_PATH"
@@ -55,9 +56,7 @@ def user_application_data_path() -> Path:
if sys.platform.startswith("win"):
app_data_path = os.getenv("APPDATA")
if not app_data_path:
raise Exception(
"Cannot get appdata path from environment."
)
raise Exception("Cannot get appdata path from environment.")
return Path(app_data_path)
else:
# try getting the standard XDG_DATA_HOME value
@@ -68,9 +67,7 @@ def user_application_data_path() -> Path:
else:
return _ensure_folder_exists(Path.home(), ".config")
except Exception as ex:
raise Exception(
"Failed to initialize user application data path.", ex
)
raise Exception("Failed to initialize user application data path.", ex)
def user_speckle_folder_path() -> Path:
@@ -90,19 +87,16 @@ def user_speckle_connector_installation_path(host_application: str) -> Path:
)
print("Starting module dependency installation")
print(sys.executable)
PYTHON_PATH = sys.executable
def connector_installation_path(host_application: str) -> Path:
connector_installation_path = user_speckle_connector_installation_path(host_application)
connector_installation_path = user_speckle_connector_installation_path(
host_application
)
connector_installation_path.mkdir(exist_ok=True, parents=True)
# set user modules path at beginning of paths for earlier hit
@@ -113,7 +107,6 @@ def connector_installation_path(host_application: str) -> Path:
return connector_installation_path
def is_pip_available() -> bool:
try:
import_module("pip") # noqa F401
@@ -132,13 +125,14 @@ 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(
f"Failed to install pip, got {completed_process.returncode} return code"
)
def get_requirements_path() -> Path:
# we assume that a requirements.txt exists next to the __init__.py file
path = Path(Path(__file__).parent, "requirements.txt")
assert path.exists()
return path
@@ -147,30 +141,52 @@ def install_requirements(host_application: str) -> None:
# script path. Here we'll install the
# dependencies
path = connector_installation_path(host_application)
print(f"Installing Speckle dependencies to {path}")
from subprocess import run
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",
"-t",
str(path),
"-r",
str(get_requirements_path()),
str(requirements_path),
],
capture_output=True,
text=True,
)
if completed_process.returncode != 0:
m = f"Failed to install dependenices through pip, got {completed_process.returncode} return code"
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()
def install_dependencies(host_application: str) -> None:
if not is_pip_available():
@@ -184,7 +200,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("-", "_")
@@ -195,6 +211,7 @@ def _import_dependencies() -> None:
# print(req)
# import_module("specklepy")
def ensure_dependencies(host_application: str) -> None:
try:
install_dependencies(host_application)
@@ -202,6 +219,6 @@ def ensure_dependencies(host_application: str) -> None:
_import_dependencies()
print("Successfully found dependencies")
except ImportError:
raise Exception(f"Cannot automatically ensure Speckle dependencies. Please try restarting the host application {host_application}!")
raise Exception(
f"Cannot automatically ensure Speckle dependencies. Please try restarting the host application {host_application}!"
)
+9 -34
View File
@@ -1,28 +1,16 @@
from .users import LoadUsers, LoadUserStreams, ResetUsers
from .object import (
UpdateObject,
ResetObject,
DeleteObject,
UploadNgonsAsPolylines,
SelectIfSameCustomProperty,
SelectIfHasCustomProperty,
)
from .commit import DeleteCommit
from .misc import OpenSpeckleForum, OpenSpeckleGuide, OpenSpeckleTutorials
from .streams import (
AddStreamFromURL,
CopyCommitId,
CopyModelId,
CopyStreamId,
CreateStream,
ReceiveStreamObjects,
SendStreamObjects,
ViewStreamDataApi,
DeleteStream,
SelectOrphanObjects,
)
from .streams import (
AddStreamFromURL,
CreateStream,
CopyStreamId,
CopyCommitId,
CopyBranchName,
)
from .commit import DeleteCommit
from .misc import OpenSpeckleGuide, OpenSpeckleTutorials, OpenSpeckleForum
from .users import LoadUsers, LoadUserStreams, ResetUsers
operator_classes = [
LoadUsers,
@@ -32,27 +20,14 @@ operator_classes = [
LoadUserStreams,
CopyStreamId,
CopyCommitId,
CopyBranchName,
CopyModelId,
]
operator_classes.extend([DeleteCommit])
operator_classes.extend(
[
UpdateObject,
ResetObject,
DeleteObject,
UploadNgonsAsPolylines,
SelectIfSameCustomProperty,
SelectIfHasCustomProperty,
]
)
operator_classes.extend(
[
ViewStreamDataApi,
DeleteStream,
SelectOrphanObjects,
AddStreamFromURL,
CreateStream,
OpenSpeckleGuide,
+23 -25
View File
@@ -1,28 +1,31 @@
"""
Commit operators
"""
import bpy
from bpy.props import BoolProperty
from bpy_speckle.clients import speckle_clients
from bpy_speckle.properties.scene import get_speckle
from specklepy.logging import metrics
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):
"""
Deletes the selected commit from the selected stream.
Permanently deletes the selected version from the selected model.
To execute from code, call: `bpy.ops.speckle.delete_commit(are_you_sure=True)`
"""
bl_idname = "speckle.delete_commit"
bl_label = "Delete commit"
bl_label = "Delete Version"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Delete active commit permanently"
bl_description = "Permanently Deletes the selected version from the selected model"
are_you_sure: BoolProperty(
name="Confirm",
default=False,
)
) # type: ignore
def draw(self, context):
layout = self.layout
@@ -38,23 +41,19 @@ class DeleteCommit(bpy.types.Operator):
return {"CANCELLED"}
def execute(self, context):
try:
self.delete_commit(context)
return {"FINISHED"}
except Exception as ex:
print(f"{self.bl_idname}: failed: {ex}")
return {"CANCELLED"}
def delete_commit(self, context: bpy.types.Context) -> None:
if not self.are_you_sure:
raise Exception("Cancelled by user")
_report("Cancelled by user")
return {"CANCELLED"}
self.are_you_sure = False
self.delete_commit(context)
return {"FINISHED"}
@staticmethod
def delete_commit(context: bpy.types.Context) -> None:
speckle = get_speckle(context)
(_, stream, _, commit) = speckle.validate_commit_selection()
(_, stream, branch, commit) = speckle.validate_commit_selection()
client = speckle_clients[int(speckle.active_user)]
@@ -62,14 +61,13 @@ class DeleteCommit(bpy.types.Operator):
metrics.track(
"Connector Action",
client.account,
custom_props={
"name": "delete_commit"
},
client.account,
custom_props={"name": "delete_commit"},
)
if not deleted:
raise Exception("Delete operation failed")
print(f"{self.bl_idname}: succeeded - commit {commit.id} ({commit.message}) has been deleted from stream {stream.id}")
print(
f"Version {commit.id} ({commit.message}) of model {branch.id} ({branch.name}) has been deleted from project {stream.id} ({stream.name})"
)
+27 -22
View File
@@ -1,57 +1,62 @@
import bpy
import webbrowser
from specklepy.logging import metrics
import bpy
from specklepy.logging import metrics
class OpenSpeckleGuide(bpy.types.Operator):
_guide_url = "https://speckle.guide/user/blender.html"
bl_idname = "speckle.open_speckle_guide"
bl_label = "Speckle Guide"
bl_label = "Speckle Docs"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Browse the documentation on the Speckle Guide"
bl_description = f"Browse the documentation on the Speckle Guide ({_guide_url})"
def execute(self, context):
webbrowser.open("https://speckle.guide/user/blender.html")
webbrowser.open(self._guide_url)
metrics.track(
"Connector Action",
None,
custom_props={
"name": "OpenSpeckleGuide"
},
None,
custom_props={"name": "OpenSpeckleGuide"},
)
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 = "Visit our tutorials portal for learning resources"
bl_description = (
f"Visit our tutorials portal for learning resources ({_tutorials_url})"
)
def execute(self, context):
webbrowser.open("https://speckle.systems/tutorials/")
webbrowser.open(self._tutorials_url)
metrics.track(
"Connector Action",
None,
custom_props={
"name": "OpenSpeckleTutorials"
},
None,
custom_props={"name": "OpenSpeckleTutorials"},
)
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 = "Ask questions and join the discussion on our community forum"
bl_description = (
f"Ask questions and join the discussion on our community forum ({_forum_url})"
)
def execute(self, context):
webbrowser.open("https://speckle.community/")
webbrowser.open(self._forum_url)
metrics.track(
"Connector Action",
None,
custom_props={
"name": "OpenSpeckleForum"
},
None,
custom_props={"name": "OpenSpeckleForum"},
)
return {"FINISHED"}
return {"FINISHED"}
-365
View File
@@ -1,365 +0,0 @@
"""
Object operators
"""
import bpy
from bpy.props import BoolProperty, EnumProperty
from deprecated import deprecated
from bpy_speckle.convert.to_speckle import (
convert_to_speckle,
ngons_to_speckle_polylines,
)
from bpy_speckle.functions import get_scale_length, _report
from bpy_speckle.clients import speckle_clients
from specklepy.logging import metrics
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)]
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)
metrics.track(
"Connector Action",
None,
custom_props={
"name": "UpdateObject"
},
)
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()
metrics.track(
"Connector Action",
None,
custom_props={
"name": "ResetObject"
},
)
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()
metrics.track(
"Connector Action",
None,
custom_props={
"name": "DeleteObject"
},
)
return {"FINISHED"}
@deprecated
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.")
metrics.track(
"Connector Action",
None,
custom_props={
"name": "UploadNgonsAsPolylines"
},
)
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)
metrics.track(
"Connector Action",
None,
custom_props={
"name": "SelectIfSameCustomProperty"
},
)
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)
metrics.track(
"Connector Action",
None,
custom_props={
"name": "SelectIfHasCustomProperty"
},
)
return {"FINISHED"}
File diff suppressed because it is too large Load Diff
+72 -67
View File
@@ -1,24 +1,34 @@
"""
User account operators
"""
from typing import cast
import bpy
from bpy.types import Context
from bpy_speckle.functions import _report
from bpy_speckle.clients import speckle_clients
from bpy_speckle.properties.scene import SpeckleCommitObject, SpeckleSceneSettings, SpeckleStreamObject, SpeckleUserObject, get_speckle
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.core.api.credentials import get_local_accounts, Account
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_label = "Reset Users"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
@@ -26,10 +36,8 @@ class ResetUsers(bpy.types.Operator):
metrics.track(
"Connector Action",
None,
custom_props={
"name": "ResetUsers"
},
None,
custom_props={"name": "ResetUsers"},
)
bpy.context.view_layer.update()
@@ -44,20 +52,22 @@ class ResetUsers(bpy.types.Operator):
speckle.users.clear()
speckle_clients.clear()
class LoadUsers(bpy.types.Operator):
"""
Load all users from local user database
Loads all user accounts from the credentials in the local database.
See docs to add accounts via Manager
"""
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 = cast(SpeckleSceneSettings, context.scene.speckle) #type: ignore
speckle = get_speckle(context)
users_list = speckle.users
ResetUsers.reset_ui(context)
@@ -67,20 +77,24 @@ class LoadUsers(bpy.types.Operator):
metrics.track(
"Connector Action",
None,
None,
custom_props={
"name": "LoadUsers",
},
)
if not profiles:
raise Exception("Zero accounts were found, please add one through Speckle Manager or a local account")
raise Exception(
"Zero accounts were found, please add one through Speckle Manager or a local account"
)
for profile in profiles:
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}")
_report(
f"Failed to authenticate user account {profile.userInfo.email} with server {profile.serverInfo.url}: {ex}"
)
users_list.remove(len(users_list) - 1)
continue
@@ -98,11 +112,16 @@ class LoadUsers(bpy.types.Operator):
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")
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:
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
@@ -116,7 +135,7 @@ def add_user_account(account: Account, speckle: SpeckleSceneSettings) -> Speckle
user.email = account.userInfo.email
user.company = account.userInfo.company or ""
assert(URL)
assert URL
client = SpeckleClient(
host=URL,
use_ssl="https" in URL,
@@ -133,54 +152,30 @@ def add_user_stream(user: SpeckleUserObject, stream: Stream):
s.id = stream.id
s.description = stream.description
if not stream.branches:
return
_report(f"Adding stream {s.id} - {s.name}")
# branches = [branch for branch in stream.branches.items if branch.name != "globals"]
for b in stream.branches.items:
branch = s.branches.add()
branch.name = b.name
if not b.commits:
continue
for c in b.commits.items:
commit: SpeckleCommitObject = branch.commits.add()
commit.id = commit.name = c.id
commit.message = c.message or ""
commit.author_name = c.authorName
commit.author_id = c.authorId
commit.created_at = c.createdAt.strftime("%Y-%m-%d %H:%M:%S.%f%Z") if c.createdAt else ""
commit.source_application = str(c.sourceApplication)
commit.referenced_object = c.referencedObject
if hasattr(s, "baseProperties"):
s.units = stream.baseProperties.units # type: ignore
else:
s.units = "Meters"
if stream.branches:
s.load_stream_branches(stream)
class LoadUserStreams(bpy.types.Operator):
"""
Load all available streams for active user
(Re)Load all available projects for active user
"""
bl_idname = "speckle.load_user_streams"
bl_label = "Load user streams"
bl_label = "Load User's Projects"
bl_options = {"REGISTER", "UNDO"}
bl_description = "(Re)load all available user streams"
bl_description = "(Re)Load all available projects for active user"
stream_limit: int = 20
branch_limit: int = 100
commits_limit: int = 10
def execute(self, context):
try:
self.load_user_stream(context)
return {"FINISHED"}
except Exception as ex:
_report(f"{self.bl_idname} failed: {ex}")
return {"CANCELLED"}
self.load_user_stream(context)
return {"FINISHED"}
def load_user_stream(self, context: Context) -> None:
speckle = get_speckle(context)
@@ -190,30 +185,40 @@ class LoadUserStreams(bpy.types.Operator):
try:
streams = client.stream.list(stream_limit=self.stream_limit)
except Exception as ex:
raise Exception(f"Failed to retrieve streams") from ex
raise Exception("Failed to retrieve projects") from ex
if not streams:
raise Exception("Zero streams found")
_report("Zero projects found")
return
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 s in streams:
assert(s.id)
sstream = client.stream.get(id=s.id, branch_limit=self.branch_limit)
add_user_stream(user, sstream)
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
)
add_user_stream(user, sstream)
else:
add_user_stream(user, s)
restore_selection_state(speckle)
bpy.context.view_layer.update()
if context.area:
context.area.tag_redraw()
metrics.track(
"Connector Action",
client.account,
custom_props={
"name": "LoadUserStreams"
},
client.account,
custom_props={"name": "LoadUserStreams"},
)
+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,6 +1,7 @@
"""
Addon properties
"""
import bpy
+5 -5
View File
@@ -1,11 +1,12 @@
"""
Collection properties
"""
import bpy
class SpeckleCollectionSettings(bpy.types.PropertyGroup):
enabled: bpy.props.BoolProperty(default=False, name="Enabled")
enabled: bpy.props.BoolProperty(default=False, name="Enabled") # type: ignore
send_or_receive: bpy.props.EnumProperty(
name="Mode",
@@ -13,7 +14,6 @@ class SpeckleCollectionSettings(bpy.types.PropertyGroup):
("send", "Send", "Send data to Speckle server."),
("receive", "Receive", "Receive data from Speckle server."),
),
)
stream_id: bpy.props.StringProperty(default="")
name: bpy.props.StringProperty(default="")
units: bpy.props.StringProperty(default="")
) # type: ignore
stream_id: bpy.props.StringProperty(default="") # type: ignore
name: bpy.props.StringProperty(default="") # type: ignore
+4 -3
View File
@@ -1,6 +1,7 @@
"""
Object properties
"""
import bpy
@@ -13,6 +14,6 @@ class SpeckleObjectSettings(bpy.types.PropertyGroup):
("send", "Send", "Send data to Speckle server."),
("receive", "Receive", "Receive data from Speckle server."),
),
)
stream_id: bpy.props.StringProperty(default="")
object_id: bpy.props.StringProperty(default="")
) # type: ignore
stream_id: bpy.props.StringProperty(default="") # type: ignore
object_id: bpy.props.StringProperty(default="") # type: ignore
+241 -73
View File
@@ -1,101 +1,169 @@
"""
Scene properties
"""
from typing import Optional, Tuple
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,
PointerProperty,
StringProperty,
)
from specklepy.core.api.models import Stream
from bpy_speckle.clients import speckle_clients
class SpeckleSceneObject(bpy.types.PropertyGroup):
name: bpy.props.StringProperty(default="")
name: bpy.props.StringProperty(default="") # type: ignore
class SpeckleCommitObject(bpy.types.PropertyGroup):
id: StringProperty(default="")
message: StringProperty(default="")
author_name: StringProperty(default="")
author_id: StringProperty(default="")
created_at: StringProperty(default="")
source_application: StringProperty(default="")
referenced_object: StringProperty(default="")
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
class SpeckleBranchObject(bpy.types.PropertyGroup):
def get_commits(self, context):
if self.commits != None and len(self.commits) > 0:
if self.commits is not None and len(self.commits) > 0:
COMMITS = cast(Iterable[SpeckleCommitObject], self.commits)
return [
(str(i), commit.id, commit.message, i)
for i, commit in enumerate(self.commits)
for i, commit in enumerate(COMMITS)
]
return [("0", "<none>", "<none>", 0)]
name: StringProperty(default="main")
commits: CollectionProperty(type=SpeckleCommitObject)
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
commit: EnumProperty(
name="Commit",
description="Active commit",
name="Version",
description="Selected model version",
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):
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.name, i)
for i, branch in enumerate(self.branches)
(str(i), branch.name, branch.description, i)
for i, branch in enumerate(BRANCHES)
if branch.name != "globals"
]
return [("0", "<none>", "<none>", 0)]
name: StringProperty(default="SpeckleStream")
description: StringProperty(default="No description provided.")
id: StringProperty(default="")
units: StringProperty(default="Meters")
query: StringProperty(default="")
branches: CollectionProperty(type=SpeckleBranchObject)
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
branch: EnumProperty(
name="Branch",
description="Active branch",
name="Model",
description="Selected Model",
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):
if 0 <= selected_index < len(self.branches):
return self.branches[selected_index]
return None
class SpeckleUserObject(bpy.types.PropertyGroup):
server_name: StringProperty(default="SpeckleXYZ")
server_url: StringProperty(default="https://speckle.xyz")
id: StringProperty(default="")
name: StringProperty(default="Speckle User")
email: StringProperty(default="user@speckle.xyz")
company: StringProperty(default="SpeckleSystems")
streams: CollectionProperty(type=SpeckleStreamObject)
active_stream: IntProperty(default=0)
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):
if 0 <= selected_index < len(self.streams):
return self.streams[selected_index]
return None
class SpeckleSceneSettings(bpy.types.PropertyGroup):
def get_scripts(self, context):
return [
@@ -107,90 +175,190 @@ class SpeckleSceneSettings(bpy.types.PropertyGroup):
name="Available streams",
description="Available streams associated with user.",
items=[],
)
) # type: ignore
users: CollectionProperty(type=SpeckleUserObject)
users: CollectionProperty(type=SpeckleUserObject) # type: ignore
def get_users(self, context):
USERS = cast(Iterable[SpeckleUserObject], self.users)
return [
(str(i), "{} ({})".format(user.email, user.server_name), user.server_url, i)
for i, user in enumerate(self.users)
(str(i), f"{user.email} ({user.server_name})", user.server_url, i)
for i, user in enumerate(USERS)
]
def set_user(self, context):
bpy.ops.speckle.load_user_streams()
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
)
active_user: EnumProperty(
items=get_users,
name="Account",
description="Select account",
update=set_user,
update=user_update_hook,
get=None,
set=None,
)
) # type: ignore
objects: CollectionProperty(type=SpeckleSceneObject)
objects: CollectionProperty(type=SpeckleSceneObject) # type: ignore
scale: FloatProperty(default=0.001)
scale: FloatProperty(default=0.001) # type: ignore
user: StringProperty(
name="User",
description="Current user.",
description="Current user",
default="Speckle User",
)
) # type: ignore
receive_script: EnumProperty(
name="Receive script",
description="Script to run when receiving stream objects.",
description="Custom py script to execute when receiving objects. See docs for function signature.",
items=get_scripts,
)
) # type: ignore
send_script: EnumProperty(
name="Send script",
description="Script to run when sending stream objects.",
description="Custom py script to execute when sending objects. See docs for function signature",
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 selected/found")
raise SelectionException("No user account selected/found")
return user
def validate_stream_selection(self) -> Tuple[SpeckleUserObject, SpeckleStreamObject]:
def validate_stream_selection(
self,
) -> Tuple[SpeckleUserObject, SpeckleStreamObject]:
user = self.validate_user_selection()
stream = user.get_active_stream()
if not stream:
raise SelectionException("No stream selected/found")
raise SelectionException("No project selected/found")
return (user, stream)
def validate_branch_selection(self) -> Tuple[SpeckleUserObject, SpeckleStreamObject, SpeckleBranchObject]:
def validate_branch_selection(
self,
) -> Tuple[SpeckleUserObject, SpeckleStreamObject, SpeckleBranchObject]:
(user, stream) = self.validate_stream_selection()
branch = stream.get_active_branch()
if not branch:
raise SelectionException("No branch selected/found")
raise SelectionException("No model selected/found")
return (user, stream, branch)
def validate_commit_selection(self) ->Tuple[SpeckleUserObject, SpeckleStreamObject, SpeckleBranchObject, SpeckleCommitObject]:
def validate_commit_selection(
self,
) -> Tuple[
SpeckleUserObject, SpeckleStreamObject, SpeckleBranchObject, SpeckleCommitObject
]:
(user, stream, branch) = self.validate_branch_selection()
commit = branch.get_active_commit()
if commit is None:
raise SelectionException("No commit selected/found")
raise SelectionException("No model version selected/found")
return (user, stream, branch, commit)
class SelectionException(Exception):
pass
def get_speckle(context: bpy.types.Context) -> SpeckleSceneSettings:
return context.scene.speckle #type: ignore
"""
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 = [
+3 -8
View File
@@ -3,15 +3,10 @@ Object UI elements
"""
import bpy
from bpy.props import (
StringProperty,
BoolProperty,
FloatProperty,
CollectionProperty,
EnumProperty,
)
from deprecated import deprecated
@deprecated
class OBJECT_PT_speckle(bpy.types.Panel):
bl_space_type = "PROPERTIES"
# bl_idname = 'OBJECT_PT_speckle'
@@ -28,7 +23,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="Stream ID")
col.prop(ob.speckle, "stream_id", text="Project ID")
col.prop(ob.speckle, "object_id", text="Object ID")
col.operator("speckle.update_object", text="Update")
col.operator("speckle.reset_object", text="Reset")
+27 -38
View File
@@ -2,22 +2,11 @@
Speckle UI elements for the 3d viewport
"""
import bpy
from bpy.props import (
StringProperty,
BoolProperty,
FloatProperty,
CollectionProperty,
EnumProperty,
)
from datetime import datetime
"""
Compatibility
TODO: evaluate if we should still support Blender <2.80
"""
import bpy
from bpy_speckle.properties.scene import get_speckle
Region = "TOOLS" if bpy.app.version < (2, 80, 0) else "UI"
@@ -78,7 +67,7 @@ class VIEW3D_UL_SpeckleUsers(bpy.types.UIList):
class VIEW3D_UL_SpeckleStreams(bpy.types.UIList):
"""
Speckle stream list
Speckle projects list
"""
def draw_item(self, context, layout, data, stream, active_data, active_propname):
@@ -94,7 +83,7 @@ class VIEW3D_UL_SpeckleStreams(bpy.types.UIList):
elif self.layout_type in {"GRID"}:
layout.alignment = "CENTER"
layout.label(text="Streams", icon_value=0)
layout.label(text="Projects", icon_value=0)
class VIEW3D_PT_SpeckleUser(bpy.types.Panel):
@@ -106,10 +95,10 @@ class VIEW3D_PT_SpeckleUser(bpy.types.Panel):
bl_region_type = Region
bl_category = "Speckle"
bl_context = "objectmode"
bl_label = "User"
bl_label = "User Account"
def draw(self, context):
speckle = context.scene.speckle
speckle = get_speckle(context)
layout = self.layout
col = layout.column()
@@ -119,28 +108,29 @@ class VIEW3D_PT_SpeckleUser(bpy.types.Panel):
else:
col.prop(speckle, "active_user", text="")
user = speckle.users[int(speckle.active_user)]
col.label(text="{} ({})".format(user.server_name, user.server_url))
col.label(text="{} ({})".format(user.name, user.email))
col.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")
class VIEW3D_PT_SpeckleStreams(bpy.types.Panel):
"""
Speckle Streams UI panel in the 3d viewport
Speckle projects UI panel in the 3d viewport
"""
bl_space_type = "VIEW_3D"
bl_region_type = Region
bl_category = "Speckle"
bl_context = "objectmode"
bl_label = "Streams"
bl_label = "Projects"
def draw(self, context):
speckle = context.scene.speckle
speckle = get_speckle(context)
col = self.layout.column()
if len(speckle.users) < 1:
col.label(text="No stream data.")
col.label(text="No Projects")
else:
user = speckle.users[int(speckle.active_user)]
col.template_list(
@@ -149,31 +139,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 Streams UI panel in the 3d viewport
Speckle Active Projects UI panel in the 3d viewport
"""
bl_space_type = "VIEW_3D"
bl_region_type = Region
bl_category = "Speckle"
bl_context = "objectmode"
bl_label = "Active stream"
bl_label = "Active Project"
def draw(self, context):
speckle = context.scene.speckle
speckle = get_speckle(context)
col = self.layout.column()
if len(speckle.users) < 1:
col.label(text="No stream data.")
col.label(text="No projects")
else:
user = speckle.users[int(speckle.active_user)]
user = speckle.validate_user_selection()
# user = speckle.users[int(speckle.active_user)]
if len(user.streams) < 1:
col.label(text="No active stream.")
col.label(text="No active project")
else:
stream = user.streams[user.active_stream]
# user.active_stream = min(user.active_stream, len(user.streams) - 1)
@@ -183,14 +173,14 @@ class VIEW3D_PT_SpeckleActiveStream(bpy.types.Panel):
col.separator()
row = col.row()
row.prop(stream, "branch", text="")
row.operator("speckle.branch_copy_name", text="", icon="COPY_ID")
row.prop(stream, "branch", text="Model")
row.operator("speckle.model_copy_id", text="", icon="COPY_ID")
if len(stream.branches) > 0:
branch = stream.branches[int(stream.branch)]
row = col.row()
row.prop(branch, "commit", text="")
row.prop(branch, "commit", text="Version")
row.operator("speckle.commit_copy_id", text="", icon="COPY_ID")
if len(branch.commits) > 0:
@@ -213,7 +203,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 branches found!")
col.label(text="No models found!")
col.separator()
@@ -225,7 +215,6 @@ 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()
@@ -246,7 +235,7 @@ class VIEW3D_PT_SpeckleActiveStream(bpy.types.Panel):
area.separator()
col.separator()
col.operator("speckle.view_stream_data_api", text="Open Stream in Web")
col.operator("speckle.view_stream_data_api", text="Open Model in Web")
class VIEW3D_PT_SpeckleHelp(bpy.types.Panel):
+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)
+3 -1
View File
@@ -1,6 +1,7 @@
import re
import sys
def patch_connector(tag):
"""Patches the connector version within the connector init file"""
bpy_file = "bpy_speckle/__init__.py"
@@ -9,7 +10,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}")
@@ -18,6 +19,7 @@ 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
+958 -715
View File
File diff suppressed because it is too large Load Diff
+33 -7
View File
@@ -4,21 +4,47 @@ 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.8, <4.0.0"
specklepy = "^2.17.17"
attrs = "^23.1.0"
python = ">=3.9.0, <4.0.0"
specklepy = "^2.20.2"
# [tool.poetry.group.local_specklepy.dependencies]
# specklepy = {path = "../specklepy", develop = true}
[tool.poetry.group.dev.dependencies]
fake-bpy-module-latest = "^20230117"
black = "^22.10.0"
pylint = "^2.15.7"
ruff = "^0.0.187"
fake-bpy-module-latest = "^20241010"
black = "24.10.0"
isort = "^5.13.2"
pylint = "^3.3.2"
ruff = "^0.8.2"
[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"