Compare commits

...

4 Commits

Author SHA1 Message Date
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
29 changed files with 1838 additions and 1402 deletions
+9 -34
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
+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)
"""
+47 -29
View File
@@ -1,24 +1,31 @@
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 +34,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 +44,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 = 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
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,7 +87,7 @@ 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)
@@ -87,19 +100,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 +129,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
View File
@@ -3,5 +3,4 @@ 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 = "."
+333 -194
View File
@@ -1,66 +1,75 @@
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 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 typing import Any, Collection, Dict, Iterable, List, Optional, Union
from .util import (
add_to_hierarchy,
get_render_material,
get_vertex_color_material,
render_material_to_native,
add_custom_properties,
add_vertices,
add_faces,
add_colors,
add_uv_coords,
)
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 (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)
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 +80,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 +94,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 +121,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 +162,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 +173,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 +205,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 +219,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 +275,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 +302,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 +318,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 +345,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 +367,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 +397,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 +405,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 +426,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 +452,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 +475,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 +495,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 +548,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 +579,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 +597,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 +610,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(
[
@@ -543,30 +625,34 @@ def transform_to_native(transform: Transform, scale: float) -> MMatrix:
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 +662,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 +708,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 +719,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 +733,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 +758,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 +831,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 +843,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 +867,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,26 +881,35 @@ 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 _get_revit_family_name(speckle_object)
)
)
def _get_revit_family_name(speckle_object: Base) -> Optional[str]:
family = getattr(speckle_object, "family", None)
@@ -786,18 +920,23 @@ def _get_revit_family_name(speckle_object: Base) -> Optional[str]:
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
@@ -814,4 +953,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
+196 -130
View File
@@ -1,43 +1,42 @@
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,
)
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.util import (
ConversionSkippedException,
get_blender_custom_properties,
make_knots,
nurb_make_curve,
to_argb_int,
)
from specklepy.objects.geometry import (Box, Curve, Interval, Mesh, Point,
Polyline, Vector)
from specklepy.objects.other import (BlockDefinition, BlockInstance,
RenderMaterial, Transform)
from bpy_speckle.blender_commit_object_builder import \
BlenderCommitObjectBuilder
from bpy_speckle.convert.constants import (OBJECT_NAME_SPECKLE_SEPARATOR,
SPECKLE_ID_LENGTH)
from bpy_speckle.convert.util import (ConversionSkippedException,
get_blender_custom_properties,
make_knots, nurb_make_curve, to_argb_int)
from bpy_speckle.functions import _report
Units: str = "m" # The desired final units to send
UnitsScale: float = 1 # The scale factor conversions need to apply to position data to get to the desired units
Units: 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 +48,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 +72,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 +110,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]
scaled_vertices = [tuple(transform @ x.co * UnitsScale) for x in data.vertices]
# Create Speckle meshes for each material
submeshes = []
@@ -109,8 +118,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 +137,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 +152,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 +205,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 +216,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 +232,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 +244,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 +252,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 +266,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 +331,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 +383,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 +443,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 +469,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 = 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 +553,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 +577,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 +587,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)
+108 -60
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):
@@ -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)
@@ -205,6 +225,7 @@ def add_colors(speckle_mesh: Mesh, blender_mesh: BMesh):
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:
@@ -222,8 +244,7 @@ def add_uv_coords(speckle_mesh: Mesh, blender_mesh: BMesh):
if len(s_uvs) // 2 == len(blender_mesh.verts):
uv.extend(
(float(s_uvs[i]), float(s_uvs[i + 1]))
for i in range(0, len(s_uvs), 2)
(float(s_uvs[i]), float(s_uvs[i + 1])) for i in range(0, len(s_uvs), 2)
)
else:
_report(
@@ -235,9 +256,9 @@ def add_uv_coords(speckle_mesh: Mesh, blender_mesh: BMesh):
uv_layer = blender_mesh.loops.layers.uv.verify()
for f in blender_mesh.faces:
for l in f.loops:
luv = l[uv_layer]
luv.uv = uv[l.vert.index]
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:
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:
+22 -16
View File
@@ -1,9 +1,12 @@
from typing import Callable
from specklepy.objects.base import Base
from bpy_speckle.convert.constants import ELEMENTS_PROPERTY_ALIASES
from specklepy.objects.graph_traversal.traversal import GraphTraversal, TraversalRule
from specklepy.objects.units import get_scale_factor_to_meters, get_units_from_string
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 +21,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])
+18 -23
View File
@@ -3,9 +3,9 @@ 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 +55,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 +66,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 +86,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 +106,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,7 +124,9 @@ 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:
@@ -151,12 +145,12 @@ def install_requirements(host_application: str) -> None:
def debugger_is_active() -> bool:
"""Return if the debugger is currently active"""
return hasattr(sys, 'gettrace') and sys.gettrace() is not None
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
@@ -186,7 +180,7 @@ def install_requirements(host_application: str) -> None:
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:
@@ -205,7 +199,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("-", "_")
@@ -216,6 +210,7 @@ def _import_dependencies() -> None:
# print(req)
# import_module("specklepy")
def ensure_dependencies(host_application: str) -> None:
try:
install_dependencies(host_application)
@@ -223,6 +218,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 -25
View File
@@ -1,29 +1,13 @@
from .users import LoadUsers, LoadUserStreams, ResetUsers
from .object import (
UpdateObject,
ResetObject,
DeleteObject,
UploadNgonsAsPolylines,
SelectIfSameCustomProperty,
SelectIfHasCustomProperty,
)
from .streams import (
ReceiveStreamObjects,
SendStreamObjects,
ViewStreamDataApi,
DeleteStream,
SelectOrphanObjects,
)
from .streams import (
AddStreamFromURL,
CreateStream,
CopyStreamId,
CopyCommitId,
CopyBranchName,
CopyModelId,
)
from .commit import DeleteCommit
from .misc import OpenSpeckleGuide, OpenSpeckleTutorials, OpenSpeckleForum
from .misc import OpenSpeckleForum, OpenSpeckleGuide, OpenSpeckleTutorials
from .object import (DeleteObject, ResetObject, SelectIfHasCustomProperty,
SelectIfSameCustomProperty, UpdateObject,
UploadNgonsAsPolylines)
from .streams import (AddStreamFromURL, CopyBranchName, CopyCommitId,
CopyModelId, CopyStreamId, CreateStream, DeleteStream,
ReceiveStreamObjects, SelectOrphanObjects,
SendStreamObjects, ViewStreamDataApi)
from .users import LoadUsers, LoadUserStreams, ResetUsers
operator_classes = [
LoadUsers,
+10 -10
View File
@@ -3,10 +3,11 @@ Commit operators
"""
import bpy
from bpy.props import BoolProperty
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
from specklepy.logging import metrics
class DeleteCommit(bpy.types.Operator):
@@ -23,7 +24,7 @@ class DeleteCommit(bpy.types.Operator):
are_you_sure: BoolProperty(
name="Confirm",
default=False,
) # type: ignore
) # type: ignore
def draw(self, context):
layout = self.layout
@@ -48,7 +49,7 @@ class DeleteCommit(bpy.types.Operator):
return {"FINISHED"}
@staticmethod
def delete_commit(context: bpy.types.Context) -> None:
def delete_commit(context: bpy.types.Context) -> None:
speckle = get_speckle(context)
(_, stream, branch, commit) = speckle.validate_commit_selection()
@@ -59,14 +60,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"Version {commit.id} ({commit.message}) of model {branch.id} ({branch.name}) has been deleted from project {stream.id} ({stream.name})")
print(
f"Version {commit.id} ({commit.message}) of model {branch.id} ({branch.name}) has been deleted from project {stream.id} ({stream.name})"
)
+16 -19
View File
@@ -1,8 +1,7 @@
import bpy
import webbrowser
from specklepy.logging import metrics
import bpy
from specklepy.logging import metrics
class OpenSpeckleGuide(bpy.types.Operator):
@@ -12,15 +11,13 @@ class OpenSpeckleGuide(bpy.types.Operator):
bl_label = "Speckle Docs"
bl_options = {"REGISTER", "UNDO"}
bl_description = f"Browse the documentation on the Speckle Guide ({_guide_url})"
def execute(self, context):
webbrowser.open(self._guide_url)
metrics.track(
"Connector Action",
None,
custom_props={
"name": "OpenSpeckleGuide"
},
None,
custom_props={"name": "OpenSpeckleGuide"},
)
return {"FINISHED"}
@@ -31,16 +28,16 @@ class OpenSpeckleTutorials(bpy.types.Operator):
bl_idname = "speckle.open_speckle_tutorials"
bl_label = "Tutorials Portal"
bl_options = {"REGISTER", "UNDO"}
bl_description = f"Visit our tutorials portal for learning resources ({_tutorials_url})"
bl_description = (
f"Visit our tutorials portal for learning resources ({_tutorials_url})"
)
def execute(self, context):
webbrowser.open(self._tutorials_url)
metrics.track(
"Connector Action",
None,
custom_props={
"name": "OpenSpeckleTutorials"
},
None,
custom_props={"name": "OpenSpeckleTutorials"},
)
return {"FINISHED"}
@@ -51,15 +48,15 @@ class OpenSpeckleForum(bpy.types.Operator):
bl_idname = "speckle.open_speckle_forum"
bl_label = "Community Forum"
bl_options = {"REGISTER", "UNDO"}
bl_description = f"Ask questions and join the discussion on our community forum ({_forum_url})"
bl_description = (
f"Ask questions and join the discussion on our community forum ({_forum_url})"
)
def execute(self, context):
webbrowser.open(self._forum_url)
metrics.track(
"Connector Action",
None,
custom_props={
"name": "OpenSpeckleForum"
},
None,
custom_props={"name": "OpenSpeckleForum"},
)
return {"FINISHED"}
return {"FINISHED"}
+24 -44
View File
@@ -5,14 +5,14 @@ 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
from bpy_speckle.clients import speckle_clients
from bpy_speckle.convert.to_speckle import (convert_to_speckle,
ngons_to_speckle_polylines)
from bpy_speckle.functions import _report, get_scale_length
@deprecated
class UpdateObject(bpy.types.Operator):
"""
@@ -28,7 +28,6 @@ class UpdateObject(bpy.types.Operator):
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
@@ -59,16 +58,15 @@ class UpdateObject(bpy.types.Operator):
metrics.track(
"Connector Action",
None,
custom_props={
"name": "UpdateObject"
},
None,
custom_props={"name": "UpdateObject"},
)
return {"FINISHED"}
return {"CANCELLED"}
return {"CANCELLED"}
@deprecated
class ResetObject(bpy.types.Operator):
"""
@@ -80,7 +78,6 @@ class ResetObject(bpy.types.Operator):
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 = ""
@@ -89,14 +86,13 @@ class ResetObject(bpy.types.Operator):
metrics.track(
"Connector Action",
None,
custom_props={
"name": "ResetObject"
},
None,
custom_props={"name": "ResetObject"},
)
return {"FINISHED"}
@deprecated
class DeleteObject(bpy.types.Operator):
"""
@@ -143,14 +139,13 @@ class DeleteObject(bpy.types.Operator):
metrics.track(
"Connector Action",
None,
custom_props={
"name": "DeleteObject"
},
None,
custom_props={"name": "DeleteObject"},
)
return {"FINISHED"}
@deprecated
class UploadNgonsAsPolylines(bpy.types.Operator):
"""
@@ -170,7 +165,6 @@ class UploadNgonsAsPolylines(bpy.types.Operator):
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]
@@ -187,7 +181,6 @@ class UploadNgonsAsPolylines(bpy.types.Operator):
placeholders = []
for polyline in sp:
res = client.objects.create([polyline])
if res is None:
@@ -223,10 +216,8 @@ class UploadNgonsAsPolylines(bpy.types.Operator):
metrics.track(
"Connector Action",
None,
custom_props={
"name": "UploadNgonsAsPolylines"
},
None,
custom_props={"name": "UploadNgonsAsPolylines"},
)
return {"FINISHED"}
@@ -240,14 +231,13 @@ class UploadNgonsAsPolylines(bpy.types.Operator):
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()]
@deprecated
class SelectIfSameCustomProperty(bpy.types.Operator):
"""
@@ -275,7 +265,6 @@ class SelectIfSameCustomProperty(bpy.types.Operator):
return wm.invoke_props_dialog(self)
def execute(self, context):
active = context.active_object
if not active:
return {"CANCELLED"}
@@ -292,7 +281,6 @@ class SelectIfSameCustomProperty(bpy.types.Operator):
)
for obj in bpy.data.objects:
if self.custom_prop in obj.keys() and obj[self.custom_prop] == value:
obj.select_set(True)
else:
@@ -300,14 +288,13 @@ class SelectIfSameCustomProperty(bpy.types.Operator):
metrics.track(
"Connector Action",
None,
custom_props={
"name": "SelectIfSameCustomProperty"
},
None,
custom_props={"name": "SelectIfSameCustomProperty"},
)
return {"FINISHED"}
@deprecated
class SelectIfHasCustomProperty(bpy.types.Operator):
"""
@@ -335,7 +322,6 @@ class SelectIfHasCustomProperty(bpy.types.Operator):
return wm.invoke_props_dialog(self)
def execute(self, context):
active = context.active_object
if not active:
return {"CANCELLED"}
@@ -343,12 +329,9 @@ class SelectIfHasCustomProperty(bpy.types.Operator):
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:
@@ -356,11 +339,8 @@ class SelectIfHasCustomProperty(bpy.types.Operator):
metrics.track(
"Connector Action",
None,
custom_props={
"name": "SelectIfHasCustomProperty"
},
None,
custom_props={"name": "SelectIfHasCustomProperty"},
)
return {"FINISHED"}
+210 -185
View File
@@ -1,84 +1,101 @@
"""
Stream operators
"""
import webbrowser
from math import radians
from typing import Callable, Dict, Optional, Tuple, Union, cast
import webbrowser
import bpy
from bpy.props import (
StringProperty,
BoolProperty,
EnumProperty,
)
from bpy.types import (
Context,
Object,
Collection
)
from bpy.props import BoolProperty, EnumProperty, StringProperty
from bpy.types import Collection, Context, Object
from deprecated import deprecated
from bpy_speckle.blender_commit_object_builder import BlenderCommitObjectBuilder
from bpy_speckle.convert.to_native import (
can_convert_to_native,
collection_to_native,
convert_to_native,
set_convert_instances_as,
)
from bpy_speckle.convert.to_speckle import (
convert_to_speckle,
)
from bpy_speckle.functions import (
get_default_traversal_func,
_report,
get_scale_length,
)
from bpy_speckle.clients import speckle_clients
from bpy_speckle.operators.users import LoadUserStreams, add_user_stream
from bpy_speckle.properties.scene import SpeckleSceneSettings, SpeckleStreamObject, SpeckleUserObject, get_speckle, selection_state
from bpy_speckle.convert.util import ConversionSkippedException, add_to_hierarchy
from specklepy.core.api.models import Commit
from specklepy.core.api import operations, host_applications
from specklepy.core.api import host_applications, operations
from specklepy.core.api.client import SpeckleClient
from specklepy.core.api.models import Commit, Stream
from specklepy.core.api.wrapper import StreamWrapper
from specklepy.core.api.resources.stream import Stream
from specklepy.transports.server import ServerTransport
from specklepy.logging import metrics
from specklepy.logging.exceptions import SpeckleException
from specklepy.objects import Base
from specklepy.objects.other import Collection as SCollection
from specklepy.logging.exceptions import SpeckleException
from specklepy.logging import metrics
from specklepy.transports.server import ServerTransport
from bpy_speckle.blender_commit_object_builder import \
BlenderCommitObjectBuilder
from bpy_speckle.clients import speckle_clients
from bpy_speckle.convert.to_native import (can_convert_to_native,
collection_to_native,
convert_to_native,
set_convert_instances_as)
from bpy_speckle.convert.to_speckle import convert_to_speckle
from bpy_speckle.convert.util import (ConversionSkippedException,
add_to_hierarchy)
from bpy_speckle.functions import (_report, get_default_traversal_func,
get_scale_length)
from bpy_speckle.operators.users import LoadUserStreams, add_user_stream
from bpy_speckle.properties.scene import (SpeckleSceneSettings,
SpeckleStreamObject,
SpeckleUserObject, get_speckle,
selection_state)
ObjectCallback = Optional[Callable[[bpy.types.Context, Object, Base], Object]]
ReceiveCompleteCallback = Optional[Callable[[bpy.types.Context, Dict[str, Union[Object, Collection]]], None]]
ReceiveCompleteCallback = Optional[
Callable[[bpy.types.Context, Dict[str, Union[Object, Collection]]], None]
]
def get_receive_funcs(speckle: SpeckleSceneSettings) -> tuple[ObjectCallback, ReceiveCompleteCallback]:
def get_project_workspace_id(client: SpeckleClient, project_id: str) -> Optional[str]:
workspace_id = None
server_version = client.project.server_version or client.server.verison()
maj = server_version[0]
min = server_version[1]
if maj > 2 or (maj == 2 and min > 20):
workspace_id = client.project.get(project_id).workspaceId
return workspace_id
def get_receive_funcs(
speckle: SpeckleSceneSettings,
) -> tuple[ObjectCallback, ReceiveCompleteCallback]:
"""
Fetches the injected callback functions from user specified "Receive Script"
"""
objectCallback: ObjectCallback = None
receiveCompleteCallback: ReceiveCompleteCallback = None
if speckle.receive_script in bpy.data.texts:
mod = bpy.data.texts[speckle.receive_script].as_module()
if hasattr(mod, "execute_for_each"):
objectCallback = mod.execute_for_each #type: ignore
elif hasattr(mod, "execute"):
objectCallback = lambda c, o, _ : mod.execute(c.scene, o) #type: ignore
objectCallback = mod.execute_for_each # type: ignore
elif hasattr(mod, "execute"):
objectCallback = lambda c, o, _: mod.execute(c.scene, o) # type: ignore
if hasattr(mod, "execute_for_all"):
receiveCompleteCallback = mod.execute_for_all #type: ignore
receiveCompleteCallback = mod.execute_for_all # type: ignore
return (objectCallback, receiveCompleteCallback)
#RECEIVE_MODES = [#TODO: modes
# RECEIVE_MODES = [#TODO: modes
# ("create", "Create", "Add new geometry, without removing any existing objects"),
# ("replace", "Replace", "Replace objects from previous receive operations from the same stream"),
# #("update","Update", "") #TODO: update mode!
#]
# ]
INSTANCES_SETTINGS = [
("collection_instance", "Collection Instance", "Receive Instances as Collection Instances"),
("linked_duplicates", "Linked Duplicates", "Receive Instances as Linked Duplicates"),
(
"collection_instance",
"Collection Instance",
"Receive Instances as Collection Instances",
),
(
"linked_duplicates",
"Linked Duplicates",
"Receive Instances as Linked Duplicates",
),
]
class ReceiveStreamObjects(bpy.types.Operator):
"""
Receive objects from selected model version
@@ -89,64 +106,63 @@ class ReceiveStreamObjects(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
bl_description = "Receive objects from selected model version"
clean_meshes: BoolProperty(name="Clean Meshes", default=False) # type: ignore
clean_meshes: BoolProperty(name="Clean Meshes", default=False) # type: ignore
#receive_mode: EnumProperty(items=RECEIVE_MODES, name="Receive Type", default="replace", description="The behaviour of the receive operation")
receive_instances_as: EnumProperty(items=INSTANCES_SETTINGS, name="Receive Instances As", default="collection_instance", description="How to receive speckle Instances") # type: ignore
# receive_mode: EnumProperty(items=RECEIVE_MODES, name="Receive Type", default="replace", description="The behaviour of the receive operation")
receive_instances_as: EnumProperty(items=INSTANCES_SETTINGS, name="Receive Instances As", default="collection_instance", description="How to receive speckle Instances") # type: ignore
def draw(self, context):
layout = self.layout
col = layout.column()
col.prop(self, "clean_meshes")
#col.prop(self, "receive_mode")
# col.prop(self, "receive_mode")
col.prop(self, "receive_instances_as")
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self)
@staticmethod
def clean_converted_meshes(context: bpy.types.Context, convertedObjects: dict[str, Object]):
bpy.ops.object.select_all(action='DESELECT')
def clean_converted_meshes(
context: bpy.types.Context, convertedObjects: dict[str, Object]
):
bpy.ops.object.select_all(action="DESELECT")
active = None
for obj in convertedObjects.values():
if obj.type != 'MESH':
if obj.type != "MESH":
continue
obj.select_set(True, view_layer=context.scene.view_layers[0])
active = obj
if active == None:
if active is None:
return
context.view_layer.objects.active = active
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action='SELECT')
bpy.ops.object.mode_set(mode="EDIT")
bpy.ops.mesh.select_all(action="SELECT")
bpy.ops.mesh.dissolve_limited(angle_limit=radians(0.1))
# Reset state to previous (not quite sure if this is 100% necessary)
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.select_all(action='DESELECT')
bpy.context.view_layer.objects.active = None # type: ignore
bpy.ops.object.mode_set(mode="OBJECT")
bpy.ops.object.select_all(action="DESELECT")
bpy.context.view_layer.objects.active = None # type: ignore
def execute(self, context):
self.receive(context)
return {"FINISHED"}
def receive(self, context: Context) -> None:
bpy.context.view_layer.objects.active = None # type: ignore
bpy.context.view_layer.objects.active = None # type: ignore
speckle = get_speckle(context)
(user, stream, branch, commit) = speckle.validate_commit_selection()
client = speckle_clients[int(speckle.active_user)]
transport = ServerTransport(stream.id, client)
# Fetch commit data
commit_object = operations.receive(commit.referenced_object, transport)
client.commit.received(
@@ -158,20 +174,24 @@ class ReceiveStreamObjects(bpy.types.Operator):
metrics.track(
metrics.RECEIVE,
getattr(transport, "account", None),
getattr(transport, "account", None),
custom_props={
"sourceHostApp": host_applications.get_host_app_from_string(commit.source_application).slug,
"sourceHostApp": host_applications.get_host_app_from_string(
commit.source_application
).slug,
"sourceHostAppVersion": commit.source_application,
"isMultiplayer": commit.author_id != user.id,
#"connector_version": "unknown", #TODO
"workspace_id": get_project_workspace_id(client, stream.id)
# "connector_version": "unknown", #TODO
},
)
# Convert received data
context.window_manager.progress_begin(0, commit_object.totalChildrenCount or 1)
set_convert_instances_as(self.receive_instances_as) #HACK: we need a better way to pass settings down to the converter
set_convert_instances_as(
self.receive_instances_as
) # HACK: we need a better way to pass settings down to the converter
traversalFunc = get_default_traversal_func(can_convert_to_native)
converted_objects: Dict[str, Union[Object, Collection]] = {}
@@ -189,10 +209,9 @@ class ReceiveStreamObjects(bpy.types.Operator):
# ensure commit object has a name if not already
if not commit_object.name:
commit_object.name = f"{stream.name} [ {branch.name} @ {commit.id} ]" # Matches Rhino "Create" naming
commit_object.name = f"{stream.name} [ {branch.name} @ {commit.id} ]" # Matches Rhino "Create" naming
for item in traversalFunc.traverse(commit_object):
current: Base = item.current
if can_convert_to_native(current) or isinstance(current, SCollection):
@@ -200,49 +219,64 @@ class ReceiveStreamObjects(bpy.types.Operator):
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, Collection, 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)
#Run the user specified callback function (AKA receive script)
converted_data_type = (
"COLLECTION_INSTANCE"
if converted.instance_collection
else str(converted.type)
)
# Run the user specified callback function (AKA receive script)
if object_converted_callback:
converted = object_converted_callback(context, converted, current)
converted = object_converted_callback(
context, converted, current
)
if converted is None:
raise Exception("Conversion returned None")
converted_objects[current.id] = converted
add_to_hierarchy(converted, item, converted_objects, True)
_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}"
)
converted_count += 1
context.window_manager.progress_update(converted_count) #NOTE: We don't expect to ever reach 100% since not every object will be traversed
context.window_manager.progress_update(
converted_count
) # NOTE: We don't expect to ever reach 100% since not every object will be traversed
context.window_manager.progress_end()
if self.clean_meshes:
objects = {k: v for k, v in converted_objects.items() if isinstance(v, Object)}
objects = {
k: v for k, v in converted_objects.items() if isinstance(v, Object)
}
self.clean_converted_meshes(context, objects)
if on_complete_callback:
on_complete_callback(context, converted_objects)
class SendStreamObjects(bpy.types.Operator):
"""
Send selected objects to selected model
@@ -253,11 +287,11 @@ class SendStreamObjects(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
bl_description = "Send selected objects to selected model"
apply_modifiers: BoolProperty(name="Apply modifiers", default=True) # type: ignore
apply_modifiers: BoolProperty(name="Apply modifiers", default=True) # type: ignore
commit_message: StringProperty(
name="Message",
default="Sent elements from Blender.",
) # type: ignore
) # type: ignore
def draw(self, context):
layout = self.layout
@@ -271,7 +305,7 @@ class SendStreamObjects(bpy.types.Operator):
if len(speckle.users) <= 0:
_report("No user accounts")
return {"CANCELLED"}
N = len(context.selected_objects)
if N == 1:
self.commit_message = f"Sent {N} element from Blender."
@@ -279,13 +313,11 @@ class SendStreamObjects(bpy.types.Operator):
self.commit_message = f"Sent {N} elements from Blender."
return wm.invoke_props_dialog(self)
def execute(self, context):
self.send(context)
return {"FINISHED"}
def send(self, context: Context) -> None:
selected = context.selected_objects
if len(selected) < 1:
raise Exception("No objects are selected, sending canceled")
@@ -304,12 +336,14 @@ class SendStreamObjects(bpy.types.Operator):
if speckle.send_script in bpy.data.texts:
mod = bpy.data.texts[speckle.send_script].as_module()
if hasattr(mod, "execute"):
func = mod.execute #type: ignore
func = mod.execute # type: ignore
num_converted = 0
context.window_manager.progress_begin(0, max(len(selected), 1))
depsgraph = bpy.context.evaluated_depsgraph_get() if self.apply_modifiers else None
depsgraph = (
bpy.context.evaluated_depsgraph_get() if self.apply_modifiers else None
)
commit_builder = BlenderCommitObjectBuilder()
for obj in selected:
@@ -319,27 +353,26 @@ class SendStreamObjects(bpy.types.Operator):
if func:
new_object = func(context.scene, obj)
if (new_object is None):
raise ConversionSkippedException(f"Script '{func.__module__}' returned None.")
if new_object is None:
raise ConversionSkippedException(
f"Script '{func.__module__}' returned None."
)
converted = convert_to_speckle(
obj,
units_scale,
units,
depsgraph
)
converted = convert_to_speckle(obj, units_scale, units, depsgraph)
if not converted:
raise Exception("Converter returned None")
commit_builder.include_object(converted, obj)
_report(f"Successfully converted '{obj.name_full}' as '{converted.speckle_type}'")
_report(
f"Successfully converted '{obj.name_full}' as '{converted.speckle_type}'"
)
except ConversionSkippedException as ex:
_report(f"Skipped converting '{obj.name_full}': '{ex}'")
except Exception as ex:
_report(f"Failed to converted '{obj.name_full}': '{ex}'")
num_converted += 1
context.window_manager.progress_update(num_converted)
@@ -350,14 +383,15 @@ class SendStreamObjects(bpy.types.Operator):
metrics.track(
metrics.SEND,
client.account,
client.account,
custom_props={
"branches": len(stream.branches),
#"collaborators": 0, #TODO:
# "collaborators": 0, #TODO:
"isMain": branch.name == "main",
"workspace_id": get_project_workspace_id(client, stream.id),
},
)
_report(f"Sending data to {stream.name}")
transport = ServerTransport(stream.id, client)
OBJECT_ID = operations.send(
@@ -374,7 +408,9 @@ class SendStreamObjects(bpy.types.Operator):
)
if client.account.serverInfo.frontend2:
sent_url = f"{user.server_url}/projects/{stream.id}/models/{branch.id}@{COMMIT_ID}"
sent_url = (
f"{user.server_url}/projects/{stream.id}/models/{branch.id}@{COMMIT_ID}"
)
else:
sent_url = f"{user.server_url}/streams/{stream.id}/commits/{COMMIT_ID}"
@@ -385,14 +421,13 @@ class SendStreamObjects(bpy.types.Operator):
selection_state.selected_stream_id = stream.id
selection_state.selected_user_id = user.id
bpy.ops.speckle.load_user_streams() # refresh loaded commits
bpy.ops.speckle.load_user_streams() # refresh loaded commits
context.view_layer.update()
if context.area:
context.area.tag_redraw()
class ViewStreamDataApi(bpy.types.Operator):
bl_idname = "speckle.view_stream_data_api"
bl_label = "Open Model in Web"
@@ -409,21 +444,18 @@ class ViewStreamDataApi(bpy.types.Operator):
url = self._get_url_from_selection(speckle)
_report(f"Opening {url} in web browser")
if not webbrowser.open(url, new=2):
raise Exception(f"Failed to open model in browser ({url})")
metrics.track(
"Connector Action",
None,
custom_props={
"name": "view_stream_data_api"
},
None,
custom_props={"name": "view_stream_data_api"},
)
@staticmethod
def _get_url_from_selection(speckleScene : SpeckleSceneSettings) -> str:
def _get_url_from_selection(speckleScene: SpeckleSceneSettings) -> str:
client = speckle_clients[int(speckleScene.active_user)]
(user, stream) = speckleScene.validate_stream_selection()
branch = stream.get_active_branch()
@@ -443,7 +475,8 @@ class ViewStreamDataApi(bpy.types.Operator):
server_url += f"branches/{branch.name}"
return server_url
class AddStreamFromURL(bpy.types.Operator):
"""
Add / select an existing project by providing its URL
@@ -453,9 +486,7 @@ class AddStreamFromURL(bpy.types.Operator):
bl_label = "Add Project From URL"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Add / select an existing project by providing its URL"
stream_url: StringProperty(
name="Project URL", default=""
) # type: ignore
stream_url: StringProperty(name="Project URL", default="") # type: ignore
def draw(self, context):
layout = self.layout
@@ -475,21 +506,28 @@ class AddStreamFromURL(bpy.types.Operator):
return {"FINISHED"}
@staticmethod
def _get_or_add_stream(user : SpeckleUserObject, stream : Stream) -> Tuple[int, SpeckleStreamObject]:
def _get_or_add_stream(
user: SpeckleUserObject, stream: Stream
) -> Tuple[int, SpeckleStreamObject]:
index, b_stream = next(
((i, cast(SpeckleStreamObject, s)) for i, s in enumerate(user.streams) if s.id == stream.id),
(
(i, cast(SpeckleStreamObject, s))
for i, s in enumerate(user.streams)
if s.id == stream.id
),
(None, None),
)
if index is not None:
assert(b_stream)
assert b_stream
return (index, b_stream)
add_user_stream(user, stream)
return next(
(i, cast(SpeckleStreamObject, s)) for i, s in enumerate(user.streams) if s.id == stream.id
(i, cast(SpeckleStreamObject, s))
for i, s in enumerate(user.streams)
if s.id == stream.id
)
def add_stream_from_url(self, context: Context) -> None:
speckle = get_speckle(context)
@@ -500,15 +538,23 @@ class AddStreamFromURL(bpy.types.Operator):
None,
)
if user_index is None:
raise Exception(f"No user account credentials for {wrapper.host}, have you added your account in Manager?")
raise Exception(
f"No user account credentials for {wrapper.host}, have you added your account in Manager?"
)
speckle.active_user = str(user_index)
user = cast(SpeckleUserObject, speckle.users[user_index])
client = speckle_clients[user_index]
stream = client.stream.get(wrapper.stream_id, branch_limit=LoadUserStreams.branch_limit, commit_limit=LoadUserStreams.commits_limit)
stream = client.stream.get(
wrapper.stream_id,
branch_limit=LoadUserStreams.branch_limit,
commit_limit=LoadUserStreams.commits_limit,
)
if not isinstance(stream, Stream):
raise SpeckleException(f"Could not get the requested project {wrapper.stream_id}")
raise SpeckleException(
f"Could not get the requested project {wrapper.stream_id}"
)
(index, b_stream) = self._get_or_add_stream(user, stream)
user.active_stream = index
@@ -533,13 +579,11 @@ class AddStreamFromURL(bpy.types.Operator):
if context.area:
context.area.tag_redraw()
metrics.track(
"Connector Action",
client.account,
custom_props={
"name": "add_stream_from_url"
},
client.account,
custom_props={"name": "add_stream_from_url"},
)
@@ -553,10 +597,10 @@ class CreateStream(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
bl_description = "Create a new Speckle project using the selected user account"
stream_name: StringProperty(name="Project name") # type: ignore
stream_name: StringProperty(name="Project name") # type: ignore
stream_description: StringProperty(
name="Project description", default="My new project"
) # type: ignore
) # type: ignore
def draw(self, context):
layout = self.layout
@@ -575,7 +619,7 @@ class CreateStream(bpy.types.Operator):
def execute(self, context):
self.create_stream(context)
return {"FINISHED"}
def create_stream(self, context: Context) -> None:
speckle = get_speckle(context)
@@ -584,9 +628,7 @@ class CreateStream(bpy.types.Operator):
client = speckle_clients[int(speckle.active_user)]
client.stream.create(
name=self.stream_name,
description=self.stream_description,
is_public=True
name=self.stream_name, description=self.stream_description, is_public=True
)
bpy.ops.speckle.load_user_streams()
@@ -597,13 +639,11 @@ class CreateStream(bpy.types.Operator):
if context.area:
context.area.tag_redraw()
metrics.track(
"Connector Action",
client.account,
custom_props={
"name": "create_stream"
},
client.account,
custom_props={"name": "create_stream"},
)
@@ -622,9 +662,9 @@ class DeleteStream(bpy.types.Operator):
name="Confirm",
description="⚠ This action will delete your entire stream permanently ⚠",
default=False,
) # type: ignore
) # type: ignore
delete_collection: BoolProperty(name="Delete collection", default=False) # type: ignore
delete_collection: BoolProperty(name="Delete collection", default=False) # type: ignore
def draw(self, context):
layout = self.layout
@@ -673,12 +713,11 @@ class DeleteStream(bpy.types.Operator):
metrics.track(
"Connector Action",
client.account,
custom_props={
"name": "delete_stream"
},
client.account,
custom_props={"name": "delete_stream"},
)
@deprecated
class SelectOrphanObjects(bpy.types.Operator):
"""
@@ -694,7 +733,6 @@ class SelectOrphanObjects(bpy.types.Operator):
layout = self.layout
def execute(self, context):
for o in context.scene.objects:
if (
o.speckle.stream_id
@@ -705,14 +743,13 @@ class SelectOrphanObjects(bpy.types.Operator):
o.select = False
metrics.track(
"Connector Action",
custom_props={
"name": "SelectOrphanObjects"
},
"Connector Action",
custom_props={"name": "SelectOrphanObjects"},
)
return {"FINISHED"}
class CopyStreamId(bpy.types.Operator):
"""
Copy the selected project id to clipboard
@@ -726,7 +763,7 @@ class CopyStreamId(bpy.types.Operator):
def execute(self, context):
self.copy_stream_id(context)
return {"FINISHED"}
def copy_stream_id(self, context) -> None:
speckle = get_speckle(context)
@@ -735,11 +772,10 @@ class CopyStreamId(bpy.types.Operator):
metrics.track(
"Connector Action",
custom_props={
"name": "copy_stream_id"
},
custom_props={"name": "copy_stream_id"},
)
class CopyCommitId(bpy.types.Operator):
"""
Copy the selected version id to clipboard
@@ -754,7 +790,6 @@ class CopyCommitId(bpy.types.Operator):
self.copy_commit_id(context)
return {"FINISHED"}
def copy_commit_id(self, context) -> None:
speckle = get_speckle(context)
@@ -763,13 +798,10 @@ class CopyCommitId(bpy.types.Operator):
metrics.track(
"Connector Action",
custom_props={
"name": "copy_commit_id"
},
custom_props={"name": "copy_commit_id"},
)
class CopyModelId(bpy.types.Operator):
"""
Copy model id to clipboard
@@ -784,7 +816,6 @@ class CopyModelId(bpy.types.Operator):
self.copy_model_id(context)
return {"FINISHED"}
def copy_model_id(self, context) -> None:
speckle = get_speckle(context)
@@ -794,11 +825,10 @@ class CopyModelId(bpy.types.Operator):
metrics.track(
"Connector Action",
custom_props={
"name": "copy_branch_id"
},
custom_props={"name": "copy_branch_id"},
)
@deprecated
class CopyBranchName(bpy.types.Operator):
"""
@@ -814,7 +844,6 @@ class CopyBranchName(bpy.types.Operator):
self.copy_branch_id(context)
return {"FINISHED"}
def copy_branch_id(self, context) -> None:
speckle = get_speckle(context)
@@ -824,11 +853,10 @@ class CopyBranchName(bpy.types.Operator):
metrics.track(
"Connector Action",
custom_props={
"name": "copy_branch_id"
},
custom_props={"name": "copy_branch_id"},
)
@deprecated
class SelectOrphanObjects(bpy.types.Operator):
"""
@@ -841,10 +869,9 @@ class SelectOrphanObjects(bpy.types.Operator):
bl_description = "Select Speckle objects that don't belong to any stream"
def draw(self, context):
layout = self.layout
pass
def execute(self, context):
for o in context.scene.objects:
if (
o.speckle.stream_id
@@ -855,10 +882,8 @@ class SelectOrphanObjects(bpy.types.Operator):
o.select = False
metrics.track(
"Connector Action",
custom_props={
"name": "SelectOrphanObjects"
},
"Connector Action",
custom_props={"name": "SelectOrphanObjects"},
)
return {"FINISHED"}
return {"FINISHED"}
+39 -30
View File
@@ -1,17 +1,23 @@
"""
User account operators
"""
from typing import List, cast
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 SpeckleBranchObject, SpeckleCommitObject, SpeckleSceneSettings, SpeckleStreamObject, SpeckleUserObject, get_speckle, restore_selection_state
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
@@ -26,10 +32,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,6 +48,7 @@ class ResetUsers(bpy.types.Operator):
speckle.users.clear()
speckle_clients.clear()
class LoadUsers(bpy.types.Operator):
"""
Loads all user accounts from the credentials in the local database.
@@ -56,7 +61,6 @@ class LoadUsers(bpy.types.Operator):
bl_description = "Loads all user accounts from the credentials in the local database.\nSee docs to add accounts via Manager"
def execute(self, context):
_report("Loading users...")
speckle = get_speckle(context)
@@ -69,20 +73,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
@@ -100,11 +108,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
@@ -118,7 +131,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,
@@ -136,7 +149,7 @@ def add_user_stream(user: SpeckleUserObject, stream: Stream):
s.description = stream.description
_report(f"Adding stream {s.id} - {s.name}")
if stream.branches:
s.load_stream_branches(stream)
@@ -159,7 +172,6 @@ class LoadUserStreams(bpy.types.Operator):
self.load_user_stream(context)
return {"FINISHED"}
def load_user_stream(self, context: Context) -> None:
speckle = get_speckle(context)
@@ -169,13 +181,12 @@ 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 projects") from ex
raise Exception("Failed to retrieve projects") from ex
if not streams:
_report("Zero projects found")
return
active_stream_id = None
if active_stream := user.get_active_stream():
active_stream_id = active_stream.id
@@ -185,10 +196,12 @@ class LoadUserStreams(bpy.types.Operator):
user.streams.clear()
for i, s in enumerate(streams):
assert(s.id)
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)
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)
@@ -199,13 +212,9 @@ class LoadUserStreams(bpy.types.Operator):
if context.area:
context.area.tag_redraw()
metrics.track(
"Connector Action",
client.account,
custom_props={
"name": "LoadUserStreams"
},
client.account,
custom_props={"name": "LoadUserStreams"},
)
+5 -10
View File
@@ -1,14 +1,9 @@
from .scene import (
SpeckleSceneSettings,
SpeckleSceneObject,
SpeckleUserObject,
SpeckleStreamObject,
SpeckleBranchObject,
SpeckleCommitObject,
)
from .object import SpeckleObjectSettings
from .collection import SpeckleCollectionSettings
from .addon import SpeckleAddonPreferences
from .collection import SpeckleCollectionSettings
from .object import SpeckleObjectSettings
from .scene import (SpeckleBranchObject, SpeckleCommitObject,
SpeckleSceneObject, SpeckleSceneSettings,
SpeckleStreamObject, SpeckleUserObject)
property_classes = [
SpeckleSceneObject,
+4 -4
View File
@@ -5,7 +5,7 @@ import bpy
class SpeckleCollectionSettings(bpy.types.PropertyGroup):
enabled: bpy.props.BoolProperty(default=False, name="Enabled") # type: ignore
enabled: bpy.props.BoolProperty(default=False, name="Enabled") # type: ignore
send_or_receive: bpy.props.EnumProperty(
name="Mode",
@@ -13,6 +13,6 @@ class SpeckleCollectionSettings(bpy.types.PropertyGroup):
("send", "Send", "Send data to Speckle server."),
("receive", "Receive", "Receive data from Speckle server."),
),
) # type: ignore
stream_id: bpy.props.StringProperty(default="") # type: ignore
name: bpy.props.StringProperty(default="") # type: ignore
) # type: ignore
stream_id: bpy.props.StringProperty(default="") # type: ignore
name: bpy.props.StringProperty(default="") # type: ignore
+3 -3
View File
@@ -13,6 +13,6 @@ class SpeckleObjectSettings(bpy.types.PropertyGroup):
("send", "Send", "Send data to Speckle server."),
("receive", "Receive", "Receive data from Speckle server."),
),
) # type: ignore
stream_id: bpy.props.StringProperty(default="") # type: ignore
object_id: bpy.props.StringProperty(default="") # type: ignore
) # type: ignore
stream_id: bpy.props.StringProperty(default="") # type: ignore
object_id: bpy.props.StringProperty(default="") # type: ignore
+137 -90
View File
@@ -1,66 +1,66 @@
"""
Scene properties
"""
from typing import Iterable, Optional, Tuple, Union, cast
from dataclasses import dataclass
import bpy
from bpy.props import (
StringProperty,
FloatProperty,
CollectionProperty,
EnumProperty,
IntProperty,
)
from typing import Iterable, Optional, Tuple, Union, cast
from bpy_speckle.clients import speckle_clients
import bpy
from bpy.props import (CollectionProperty, EnumProperty, FloatProperty,
IntProperty, 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="") # type: ignore
name: bpy.props.StringProperty(default="") # type: ignore
class SpeckleCommitObject(bpy.types.PropertyGroup):
id: StringProperty(default="") # type: ignore
message: StringProperty(default="") # type: ignore
author_name: StringProperty(default="") # type: ignore
author_id: StringProperty(default="") # type: ignore
created_at: StringProperty(default="") # type: ignore
source_application: StringProperty(default="") # type: ignore
referenced_object: StringProperty(default="") # type: ignore
id: StringProperty(default="") # 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(COMMITS)
]
return [("0", "<none>", "<none>", 0)]
def commit_update_hook(self, context: bpy.types.Context):
selection_state.selected_commit_id = SelectionState.get_item_id_by_index(self.commits, self.commit)
selection_state.selected_commit_id = SelectionState.get_item_id_by_index(
self.commits, self.commit
)
selection_state.selected_branch_id = self.id
# print(f"commit_update_hook: {selection_state.selected_commit_id=}, {selection_state.selected_branch_id=}")
name: StringProperty(default="main") # type: ignore
id: StringProperty(default="") # type: ignore
description: StringProperty(default="") # type: ignore
commits: CollectionProperty(type=SpeckleCommitObject) # type: ignore
name: StringProperty(default="main") # type: ignore
id: StringProperty(default="") # type: ignore
description: StringProperty(default="") # type: ignore
commits: CollectionProperty(type=SpeckleCommitObject) # type: ignore
commit: EnumProperty(
name="Version",
description="Selected model version",
items=get_commits,
update=commit_update_hook,
) # type: ignore
) # 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()
@@ -80,7 +80,11 @@ class SpeckleStreamObject(bpy.types.PropertyGroup):
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.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
@@ -93,57 +97,66 @@ class SpeckleStreamObject(bpy.types.PropertyGroup):
if branch.name != "globals"
]
return [("0", "<none>", "<none>", 0)]
def branch_update_hook(self, context: bpy.types.Context):
selection_state.selected_branch_id = SelectionState.get_item_id_by_index(self.branches, self.branch)
selection_state.selected_branch_id = SelectionState.get_item_id_by_index(
self.branches, self.branch
)
# print(f"branch_update_hook: {selection_state.selected_branch_id=}, {selection_state.selected_stream_id=}")
name: StringProperty(default="") # type: ignore
description: StringProperty(default="") # type: ignore
id: StringProperty(default="") # type: ignore
branches: CollectionProperty(type=SpeckleBranchObject) # type: ignore
name: StringProperty(default="") # type: ignore
description: StringProperty(default="") # type: ignore
id: StringProperty(default="") # type: ignore
branches: CollectionProperty(type=SpeckleBranchObject) # type: ignore
branch: EnumProperty(
name="Model",
description="Selected Model",
items=get_branches,
update=branch_update_hook,
) # type: ignore
) # 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):
def fetch_stream_branches(self, context: bpy.types.Context, stream: SpeckleStreamObject):
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
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
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
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):
@@ -156,9 +169,9 @@ class SpeckleSceneSettings(bpy.types.PropertyGroup):
name="Available streams",
description="Available streams associated with user.",
items=[],
) # type: ignore
) # type: ignore
users: CollectionProperty(type=SpeckleUserObject) # type: ignore
users: CollectionProperty(type=SpeckleUserObject) # type: ignore
def get_users(self, context):
USERS = cast(Iterable[SpeckleUserObject], self.users)
@@ -168,8 +181,10 @@ class SpeckleSceneSettings(bpy.types.PropertyGroup):
]
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)
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,
@@ -178,29 +193,29 @@ class SpeckleSceneSettings(bpy.types.PropertyGroup):
update=user_update_hook,
get=None,
set=None,
) # type: ignore
) # type: ignore
objects: CollectionProperty(type=SpeckleSceneObject) # type: ignore
objects: CollectionProperty(type=SpeckleSceneObject) # type: ignore
scale: FloatProperty(default=0.001) # type: ignore
scale: FloatProperty(default=0.001) # type: ignore
user: StringProperty(
name="User",
description="Current user",
default="Speckle User",
) # type: ignore
) # type: ignore
receive_script: EnumProperty(
name="Receive script",
description="Custom py script to execute when receiving objects. See docs for function signature.",
items=get_scripts,
) # type: ignore
) # type: ignore
send_script: EnumProperty(
name="Send script",
description="Custom py script to execute when sending objects. See docs for function signature",
items=get_scripts,
) # type: ignore
) # type: ignore
def get_active_user(self) -> Optional[SpeckleUserObject]:
if self.active_user is None:
@@ -209,15 +224,16 @@ class SpeckleSceneSettings(bpy.types.PropertyGroup):
if 0 <= selected_index < len(self.users):
return self.users[selected_index]
return None
def validate_user_selection(self) -> SpeckleUserObject:
user = self.get_active_user()
if not user:
raise SelectionException("No user account selected/found")
return user
def validate_stream_selection(self) -> Tuple[SpeckleUserObject, SpeckleStreamObject]:
def validate_stream_selection(
self,
) -> Tuple[SpeckleUserObject, SpeckleStreamObject]:
user = self.validate_user_selection()
stream = user.get_active_stream()
@@ -225,80 +241,109 @@ class SpeckleSceneSettings(bpy.types.PropertyGroup):
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 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 model version selected/found")
return (user, stream, branch, commit)
class SelectionException(Exception):
pass
def get_speckle(context: bpy.types.Context) -> SpeckleSceneSettings:
"""
Gets the speckle scene object
"""
return context.scene.speckle #type: ignore
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
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]:
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]:
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]:
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 != None:
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_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 != None:
(active_user, active_stream, active_branch) = speckle.validate_branch_selection()
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=}")
@@ -307,5 +352,7 @@ def restore_selection_state(speckle: SpeckleSceneSettings) -> None:
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
if commit := SelectionState.get_item_index_by_id(
active_branch.commits, selection_state.selected_commit_id
):
active_branch.commit = commit
+3 -8
View File
@@ -1,12 +1,7 @@
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,
)
from .view3d import (VIEW3D_PT_SpeckleActiveStream, VIEW3D_PT_SpeckleHelp,
VIEW3D_PT_SpeckleStreams, VIEW3D_PT_SpeckleUser,
VIEW3D_UL_SpeckleStreams, VIEW3D_UL_SpeckleUsers)
ui_classes = [
VIEW3D_PT_SpeckleUser,
+1 -7
View File
@@ -3,15 +3,9 @@ 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"
+4 -3
View File
@@ -3,10 +3,10 @@ Speckle UI elements for the 3d viewport
"""
import bpy
from datetime import datetime
import bpy
from bpy_speckle.properties.scene import get_speckle
Region = "TOOLS" if bpy.app.version < (2, 80, 0) else "UI"
@@ -114,6 +114,7 @@ class VIEW3D_PT_SpeckleUser(bpy.types.Panel):
col.operator("speckle.users_load", text="", icon="FILE_REFRESH")
class VIEW3D_PT_SpeckleStreams(bpy.types.Panel):
"""
Speckle projects UI panel in the 3d viewport
@@ -161,7 +162,7 @@ class VIEW3D_PT_SpeckleActiveStream(bpy.types.Panel):
col.label(text="No projects")
else:
user = speckle.validate_user_selection()
#user = speckle.users[int(speckle.active_user)]
# user = speckle.users[int(speckle.active_user)]
if len(user.streams) < 1:
col.label(text="No active project")
else:
+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
+620 -460
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -7,7 +7,7 @@ license = "Apache-2.0"
[tool.poetry.dependencies]
python = ">=3.8, <4.0.0"
specklepy = "^2.19.1"
specklepy = "^2.20.0"
attrs = "^23.1.0"
# [tool.poetry.group.local_specklepy.dependencies]
@@ -15,7 +15,7 @@ attrs = "^23.1.0"
[tool.poetry.group.dev.dependencies]
fake-bpy-module-latest = "^20240524"
black = "23.11.0"
black = "24.3.0"
pylint = "^2.15.7"
ruff = "^0.4.4"