Jedd/cxpla 94 track workspace id in specklepy connector metrics (#209)

* workspace tracking + black

* works
This commit is contained in:
Jedd Morgan
2024-11-05 13:09:38 +00:00
committed by GitHub
parent f7c4fc3665
commit b8b7c0bdf5
28 changed files with 1829 additions and 1368 deletions
+9 -8
View File
@@ -1,15 +1,16 @@
import bpy import bpy
from bpy_speckle.installer import ensure_dependencies from bpy_speckle.installer import ensure_dependencies
ensure_dependencies(f"Blender {bpy.app.version[0]}.{bpy.app.version[1]}") ensure_dependencies(f"Blender {bpy.app.version[0]}.{bpy.app.version[1]}")
from bpy.app.handlers import persistent
from specklepy.logging import metrics 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_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 = { bl_info = {
"name": "SpeckleBlender 2.0", "name": "SpeckleBlender 2.0",
@@ -24,7 +25,6 @@ bl_info = {
} }
""" """
Import SpeckleBlender classes Import SpeckleBlender classes
""" """
@@ -34,16 +34,18 @@ Add load handler to initialize Speckle when
loading a Blender file loading a Blender file
""" """
@persistent @persistent
def load_handler(dummy): def load_handler(dummy):
pass pass
# Calling users_load is an expensive operation, one that force users to wait a good 10s every time blender loads. # 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. # 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 # Instead, we shall just reset the user selection to an uninitiailised state
bpy.ops.speckle.users_reset() bpy.ops.speckle.users_reset()
""" """
Permanent handle on callbacks Permanent handle on callbacks
""" """
@@ -93,7 +95,6 @@ def register():
def unregister(): def unregister():
bpy.app.handlers.load_post.remove(load_handler) 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 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 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" ELEMENTS = "elements"
def _id(native_object: ID) -> str: def _id(native_object: ID) -> str:
#NOTE: to avoid naming collisions, we prefix collections and objects differently # NOTE: to avoid naming collisions, we prefix collections and objects differently
return f"{type(native_object).__name__}:{native_object.name_full}" return f"{type(native_object).__name__}:{native_object.name_full}"
def _try_id(native_object: Optional[Union[Collection, Object]]) -> Optional[str]: def _try_id(native_object: Optional[Union[Collection, Object]]) -> Optional[str]:
return _id(native_object) if native_object else None return _id(native_object) if native_object else None
def convert_collection_to_speckle(col: Collection) -> SCollection: 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) converted_collection.applicationId = _id(col)
color_tag = col.color_tag color_tag = col.color_tag
@@ -27,9 +34,9 @@ def convert_collection_to_speckle(col: Collection) -> SCollection:
return converted_collection return converted_collection
@define(slots=True) @define(slots=True)
class BlenderCommitObjectBuilder(CommitObjectBuilder[Object]): class BlenderCommitObjectBuilder(CommitObjectBuilder[Object]):
_collections: Dict[str, SCollection] _collections: Dict[str, SCollection]
def __init__(self) -> None: def __init__(self) -> None:
@@ -37,35 +44,41 @@ class BlenderCommitObjectBuilder(CommitObjectBuilder[Object]):
self._collections = {} self._collections = {}
def include_object(self, conversion_result: Base, native_object: Object) -> None: def include_object(self, conversion_result: Base, native_object: Object) -> None:
# Set the Child -> Parent relationships # Set the Child -> Parent relationships
parent = native_object.parent parent = native_object.parent
parent_collections = native_object.users_collection 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_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) app_id = _id(native_object)
conversion_result.applicationId = app_id conversion_result.applicationId = app_id
self.converted[app_id] = conversion_result self.converted[app_id] = conversion_result
# in order or priority, direct parent, direct parent collection, root # 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: # if parent_collection:
# self._include_collection(parent_collection) # self._include_collection(parent_collection)
def ensure_collection(self, col: Collection) -> SCollection: def ensure_collection(self, col: Collection) -> SCollection:
id = _id(col) id = _id(col)
if id in self._collections: 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: 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 # 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)) self.set_relationship(_id(c), (id, ELEMENTS), (ROOT, ELEMENTS))
# Set Child -> Parent relationship # Set Child -> Parent relationship
# parent = self.find_collection_parent(col) # 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) converted_collection = convert_collection_to_speckle(col)
self.converted[id] = converted_collection self.converted[id] = converted_collection
@@ -74,7 +87,7 @@ class BlenderCommitObjectBuilder(CommitObjectBuilder[Object]):
return converted_collection return converted_collection
def build_commit_object(self, root_commit_object: Base) -> None: 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 # Create all collections
root_col = self.ensure_collection(bpy.context.scene.collection) 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) self.apply_relationships(objects_to_build, root_commit_object)
assert(isinstance(root_commit_object, SCollection)) assert isinstance(root_commit_object, SCollection)
# Kill unused collections # 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 elements = col.elements
if not elements: return True if not elements:
return True
should_remove_this_col = True should_remove_this_col = True
i = 0 i = 0
while i < len(elements): while i < len(elements):
c = elements[i] c = elements[i]
if not isinstance(c, SCollection): if not isinstance(c, SCollection):
# col has objects (c) # col has objects (c)
should_remove_this_col = False should_remove_this_col = False
i += 1 i += 1
@@ -113,8 +129,10 @@ class BlenderCommitObjectBuilder(CommitObjectBuilder[Object]):
should_remove_this_col = False should_remove_this_col = False
i += 1 i += 1
continue continue
return should_remove_this_col return should_remove_this_col
if should_remove_unuseful_collection(root_commit_object): 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 .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 from specklepy.core.api.client import SpeckleClient
speckle_clients: list[SpeckleClient] = [] speckle_clients: list[SpeckleClient] = []
+2 -2
View File
@@ -10,7 +10,7 @@ IGNORED_PROPERTY_KEYS = {
"vertices", "vertices",
"renderMaterial", "renderMaterial",
"textureCoordinates", "textureCoordinates",
"totalChildrenCount" "totalChildrenCount",
} }
DISPLAY_VALUE_PROPERTY_ALIASES = {"displayValue", "@displayValue"} DISPLAY_VALUE_PROPERTY_ALIASES = {"displayValue", "@displayValue"}
@@ -19,4 +19,4 @@ ELEMENTS_PROPERTY_ALIASES = {"elements", "@elements"}
OBJECT_NAME_MAX_LENGTH = 62 OBJECT_NAME_MAX_LENGTH = 62
SPECKLE_ID_LENGTH = 32 SPECKLE_ID_LENGTH = 32
OBJECT_NAME_SPECKLE_SEPARATOR = " -- " OBJECT_NAME_SPECKLE_SEPARATOR = " -- "
OBJECT_NAME_NUMERAL_SEPARATOR = '.' OBJECT_NAME_NUMERAL_SEPARATOR = "."
+333 -194
View File
@@ -1,66 +1,75 @@
import math import math
from typing import Any, Dict, Iterable, List, Optional, Sequence, Union, Collection, cast from typing import Any, Collection, Dict, Iterable, List, Optional, Union
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 .util import ( import bmesh
add_to_hierarchy, import bpy
get_render_material, from bpy.types import Collection as BCollection
get_vertex_color_material, from bpy.types import Object
render_material_to_native, from mathutils import Matrix as MMatrix
add_custom_properties, from mathutils import Quaternion as MQuaternion
add_vertices, from mathutils import Vector as MVector
add_faces, from specklepy.objects.base import Base
add_colors, from specklepy.objects.geometry import (Arc, Circle, Curve, Ellipse, Line,
add_uv_coords, 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) SUPPORTED_CURVES = (Line, Polyline, Curve, Arc, Polycurve, Ellipse, Circle)
CAN_CONVERT_TO_NATIVE = ( CAN_CONVERT_TO_NATIVE = (
Mesh, Mesh,
*SUPPORTED_CURVES, *SUPPORTED_CURVES,
Instance, Instance,
) )
def _has_native_conversion(speckle_object: Base) -> bool: 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 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: def can_convert_to_native(speckle_object: Base) -> bool:
if _has_native_conversion(speckle_object) or _has_fallback_conversion(
if(_has_native_conversion(speckle_object) or _has_fallback_conversion(speckle_object)): speckle_object
):
return True return True
return False 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): def set_convert_instances_as(value: str):
global convert_instances_as global convert_instances_as
convert_instances_as = value convert_instances_as = value
#TODO: Check usages handle exceptions # TODO: Check usages handle exceptions
def convert_to_native(speckle_object: Base) -> Object: def convert_to_native(speckle_object: Base) -> Object:
speckle_type = type(speckle_object) speckle_type = type(speckle_object)
object_name = _generate_object_name(speckle_object) object_name = _generate_object_name(speckle_object)
@@ -71,9 +80,13 @@ def convert_to_native(speckle_object: Base) -> Object:
# convert elements/breps # convert elements/breps
if not _has_native_conversion(speckle_object): 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: 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 # convert supported geometry
elif isinstance(speckle_object, Mesh): elif isinstance(speckle_object, Mesh):
@@ -81,24 +94,25 @@ def convert_to_native(speckle_object: Base) -> Object:
elif speckle_type in SUPPORTED_CURVES: elif speckle_type in SUPPORTED_CURVES:
converted = icurve_to_native(speckle_object, object_name, scale) converted = icurve_to_native(speckle_object, object_name, scale)
elif "View" in speckle_object.speckle_type: 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): elif isinstance(speckle_object, Instance):
if convert_instances_as == "linked_duplicates": 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": elif convert_instances_as == "collection_instance":
converted = instance_to_native_collection_instance(speckle_object, scale) converted = instance_to_native_collection_instance(speckle_object, scale)
else: 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) converted = instance_to_native_collection_instance(speckle_object, scale)
else: else:
raise Exception(f"Unsupported type {speckle_type}") raise Exception(f"Unsupported type {speckle_type}")
if not isinstance(converted, Object): if not isinstance(converted, Object):
converted = create_new_object(converted, object_name) converted = create_new_object(converted, object_name)
converted.speckle.object_id = str(speckle_object.id) # type: ignore converted.speckle.object_id = str(speckle_object.id) # type: ignore
converted.speckle.enabled = True # type: ignore converted.speckle.enabled = True # type: ignore
add_custom_properties(speckle_object, converted) add_custom_properties(speckle_object, converted)
for c in children: for c in children:
@@ -107,15 +121,30 @@ def convert_to_native(speckle_object: Base) -> Object:
return converted 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]: def elements_to_native(
(_, elements) = _members_to_native(speckle_object, name, scale, ELEMENTS_PROPERTY_ALIASES, False) 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 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 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) display = getattr(speckle_object, alias, None)
count = 0 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: def separate(value: Any) -> bool:
nonlocal meshes, others, count, MAX_DEPTH 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) others.append(value)
elif isinstance(value, list): elif isinstance(value, list):
count += 1 count += 1
if(count > MAX_DEPTH): if count > MAX_DEPTH:
return True return True
for x in value: for x in value:
separate(x) separate(x)
return False return False
did_halt = separate(display) did_halt = separate(display)
if did_halt: 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] = [] children: list[Object] = []
mesh = None mesh = None
if meshes: 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: for item in others:
try: try:
@@ -172,14 +205,13 @@ def _members_to_native(speckle_object: Base, name: str, scale: float, members: I
return (mesh, children) return (mesh, children)
def view_to_native(speckle_view, name: str, scale: float) -> bpy.types.Object: def view_to_native(speckle_view, name: str, scale: float) -> bpy.types.Object:
native_cam: bpy.types.Camera native_cam: bpy.types.Camera
if name in bpy.data.cameras.keys(): if name in bpy.data.cameras.keys():
native_cam = bpy.data.cameras[name] native_cam = bpy.data.cameras[name]
else: else:
native_cam = bpy.data.cameras.new(name=name) 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"): if not hasattr(speckle_view, "origin"):
raise ConversionSkippedException("2D views not supported") 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) cam_obj = create_new_object(native_cam, name)
scale_factor = get_scale_factor(speckle_view, scale) scale_factor = get_scale_factor(speckle_view, scale)
tx = (speckle_view.origin.x * scale_factor) tx = speckle_view.origin.x * scale_factor
ty = (speckle_view.origin.y * scale_factor) ty = speckle_view.origin.y * scale_factor
tz = (speckle_view.origin.z * scale_factor) tz = speckle_view.origin.z * scale_factor
forward = MVector((speckle_view.forwardDirection.x, speckle_view.forwardDirection.y, speckle_view.forwardDirection.z)) forward = MVector(
up = MVector((speckle_view.upDirection.x, speckle_view.upDirection.y, speckle_view.upDirection.z)) (
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() right = forward.cross(up).normalized()
cam_obj.matrix_world = MMatrix(( cam_obj.matrix_world = MMatrix(
(right.x, up.x, -forward.x, tx), (
(right.y, up.y, -forward.y, ty), (right.x, up.x, -forward.x, tx),
(right.z, up.z, -forward.z, tz), (right.y, up.y, -forward.y, ty),
(0, 0, 0, 1 ) (right.z, up.z, -forward.z, tz),
)) (0, 0, 0, 1),
)
)
return cam_obj return cam_obj
def mesh_to_native(speckle_mesh: Mesh, name: str, scale: float) -> bpy.types.Mesh: def mesh_to_native(speckle_mesh: Mesh, name: str, scale: float) -> bpy.types.Mesh:
return meshes_to_native(speckle_mesh, [speckle_mesh], name, scale) return meshes_to_native(speckle_mesh, [speckle_mesh], name, scale)
def meshes_to_native(
def meshes_to_native(element: Base, meshes: Collection[Mesh], name: str, scale: float) -> bpy.types.Mesh: element: Base, meshes: Collection[Mesh], name: str, scale: float
) -> bpy.types.Mesh:
if name in bpy.data.meshes.keys(): if name in bpy.data.meshes.keys():
return bpy.data.meshes[name] return bpy.data.meshes[name]
blender_mesh = bpy.data.meshes.new(name=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 # Second pass, add face data
offset = 0 offset = 0
for i, mesh in enumerate(meshes): for i, mesh in enumerate(meshes):
if not mesh.vertices: continue if not mesh.vertices:
continue
add_faces(mesh, bm, offset, i) 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) add_colors(mesh, bm)
except Exception as ex: except Exception as ex:
_report(f"Skipping converting vertex colors for {name}: {ex}") _report(f"Skipping converting vertex colors for {name}: {ex}")
try: try:
add_uv_coords(mesh, bm) add_uv_coords(mesh, bm)
except Exception as ex: except Exception as ex:
_report(f"Skipping converting uv coordinates for {name}: {ex}") _report(f"Skipping converting uv coordinates for {name}: {ex}")
bm.to_mesh(blender_mesh) bm.to_mesh(blender_mesh)
bm.free() bm.free()
return blender_mesh return blender_mesh
@@ -269,8 +318,12 @@ def meshes_to_native(element: Base, meshes: Collection[Mesh], name: str, scale:
Curves 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 = blender_curve.splines.new("POLY")
line.points.add(1) line.points.add(1)
@@ -292,8 +345,11 @@ def line_to_native(speckle_curve: Line, blender_curve: bpy.types.Curve, scale: f
return [line] return [line]
def polyline_to_native(scurve: Polyline, bcurve: bpy.types.Curve, scale: float) -> List[bpy.types.Spline]: def polyline_to_native(
if not (value := scurve.value): return [] scurve: Polyline, bcurve: bpy.types.Curve, scale: float
) -> List[bpy.types.Spline]:
if not (value := scurve.value):
return []
N = len(value) // 3 N = len(value) // 3
polyline = bcurve.splines.new("POLY") polyline = bcurve.splines.new("POLY")
@@ -311,22 +367,27 @@ def polyline_to_native(scurve: Polyline, bcurve: bpy.types.Curve, scale: float)
) )
return [polyline] return [polyline]
def nurbs_to_native(scurve: Curve, bcurve: bpy.types.Curve, scale: float) -> List[bpy.types.Spline]: def nurbs_to_native(
if not (points := scurve.points): return [] scurve: Curve, bcurve: bpy.types.Curve, scale: float
if not scurve.degree: raise Exception("curve is missing degree") ) -> List[bpy.types.Spline]:
if not scurve.weights: raise Exception("curve is missing weights") 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 # Closed curves from rhino will have n + degree points. We ignore the extras
num_points = len(points) // 3 - scurve.degree if (scurve.closed) else ( num_points = (
len(points) // 3) len(points) // 3 - scurve.degree if (scurve.closed) else (len(points) // 3)
)
nurbs = bcurve.splines.new("NURBS") nurbs = bcurve.splines.new("NURBS")
nurbs.use_cyclic_u = scurve.closed or False nurbs.use_cyclic_u = scurve.closed or False
nurbs.use_endpoint_u = not scurve.periodic nurbs.use_endpoint_u = not scurve.periodic
nurbs.points.add(num_points - 1) nurbs.points.add(num_points - 1)
use_weights = len(scurve.weights) >= num_points use_weights = len(scurve.weights) >= num_points
for i in range(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, float(points[i * 3 + 2]) * scale,
1, 1,
) )
nurbs.points[i].weight = scurve.weights[i] if use_weights else 1 nurbs.points[i].weight = scurve.weights[i] if use_weights else 1
nurbs.order_u = scurve.degree + 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] 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 # TODO: improve Blender representation of arc - check autocad test stream
if not rcurve.radius: raise Exception("curve is missing radius") if not rcurve.radius:
if not rcurve.startAngle: raise Exception("curve is missing startAngle") raise Exception("curve is missing radius")
if not rcurve.endAngle: raise Exception("curve is missing endAngle") if not rcurve.startAngle:
raise Exception("curve is missing startAngle")
if not rcurve.endAngle:
raise Exception("curve is missing endAngle")
plane = rcurve.plane plane = rcurve.plane
if not plane: if not plane:
@@ -360,8 +426,8 @@ def arc_to_native(rcurve: Arc, bcurve: bpy.types.Curve, scale: float) -> Optiona
startAngle = rcurve.startAngle startAngle = rcurve.startAngle
endAngle = rcurve.endAngle endAngle = rcurve.endAngle
startQuat = MQuaternion(normal, startAngle) # type: ignore startQuat = MQuaternion(normal, startAngle) # type: ignore
endQuat = MQuaternion(normal, endAngle) # type: ignore endQuat = MQuaternion(normal, endAngle) # type: ignore
# Get start and end vectors, centre point, angles, etc. # Get start and end vectors, centre point, angles, etc.
r1 = MVector([plane.xdir.x, plane.xdir.y, plane.xdir.z]) 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) Ndiv = max(int(math.floor(angle / 0.3)), 2)
step = angle / float(Ndiv) step = angle / float(Ndiv)
stepQuat = MQuaternion(normal, step) # type: ignore stepQuat = MQuaternion(normal, step) # type: ignore
tan = math.tan(step / 2) * radius tan = math.tan(step / 2) * radius
arc.points.add(Ndiv + 1) arc.points.add(Ndiv + 1)
@@ -409,11 +475,14 @@ def arc_to_native(rcurve: Arc, bcurve: bpy.types.Curve, scale: float) -> Optiona
return arc 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 Convert Polycurve object
""" """
if not scurve.segments: raise Exception("curve is missing segments") if not scurve.segments:
raise Exception("curve is missing segments")
curves = [] curves = []
@@ -426,46 +495,52 @@ def polycurve_to_native(scurve: Polycurve, bcurve: bpy.types.Curve, scale: float
_report(f"Unsupported curve type: {speckle_type}") _report(f"Unsupported curve type: {speckle_type}")
return curves 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 radX: float
radY: float radY: float
if isinstance(ellipse, Ellipse): if isinstance(ellipse, Ellipse):
if not ellipse.firstRadius: raise Exception("curve is missing firstRadius") if not ellipse.firstRadius:
if not ellipse.secondRadius: raise Exception("curve is missing secondRadius") raise Exception("curve is missing firstRadius")
if not ellipse.secondRadius:
raise Exception("curve is missing secondRadius")
radX = ellipse.firstRadius * units_scale radX = ellipse.firstRadius * units_scale
radY = ellipse.secondRadius * units_scale radY = ellipse.secondRadius * units_scale
else: 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 radX = ellipse.radius * units_scale
radY = 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 = [ right_handles = [
(+radX, +radY * D, 0.0), (+radX, +radY * D, 0.0),
(-radX * D, +radY, 0.0), (-radX * D, +radY, 0.0),
(-radX, -radY * D, 0.0), (-radX, -radY * D, 0.0),
(+radX * D, -radY, 0.0), (+radX * D, -radY, 0.0),
] ]
left_handles = [ left_handles = [
(+radX, -radY * D, 0.0), (+radX, -radY * D, 0.0),
(+radX * D, +radY, 0.0), (+radX * D, +radY, 0.0),
(-radX, +radY * D, 0.0), (-radX, +radY * D, 0.0),
(-radX * D, -radY, 0.0), (-radX * D, -radY, 0.0),
] ]
points = [ points = [
(+radX, 0.0, 0.0), (+radX, 0.0, 0.0),
(0.0, +radY, 0.0), (0.0, +radY, 0.0),
(-radX, 0.0, 0.0), (-radX, 0.0, 0.0),
(0.0, -radY, 0.0), (0.0, -radY, 0.0),
] ]
transform = plane_to_native_transform(ellipse.plane, units_scale) 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) spline.bezier_points.add(len(points) - 1)
for i in range(len(points)): for i in range(len(points)):
spline.bezier_points[i].co = transform @ MVector(points[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_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].handle_right = transform @ MVector(right_handles[i]) # type: ignore
spline.use_cyclic_u = True spline.use_cyclic_u = True
#TODO support trims? # TODO support trims?
return [spline] 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 # polycurves
if isinstance(speckle_curve, Polycurve): if isinstance(speckle_curve, Polycurve):
return polycurve_to_native(speckle_curve, blender_curve, scale) 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): elif isinstance(speckle_curve, Ellipse) or isinstance(speckle_curve, Circle):
splines = ellipse_to_native(speckle_curve, blender_curve, scale) splines = ellipse_to_native(speckle_curve, blender_curve, scale)
else: 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 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") else bpy.data.curves.new(name, type="CURVE")
) )
blender_curve.dimensions = "3D" 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) 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 Transforms and Instances
""" """
def transform_to_native(transform: Transform, scale: float) -> MMatrix: def transform_to_native(transform: Transform, scale: float) -> MMatrix:
mat = MMatrix( mat = MMatrix(
[ [
@@ -543,30 +625,34 @@ def transform_to_native(transform: Transform, scale: float) -> MMatrix:
mat[i][3] *= scale mat[i][3] *= scale
return mat 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) scale_factor = get_scale_factor(plane, fallback_scale)
tx = (plane.origin.x * scale_factor) tx = plane.origin.x * scale_factor
ty = (plane.origin.y * scale_factor) ty = plane.origin.y * scale_factor
tz = (plane.origin.z * scale_factor) tz = plane.origin.z * scale_factor
return MMatrix(
return MMatrix(( (
(plane.xdir.x, plane.ydir.x, plane.normal.x, tx), (plane.xdir.x, plane.ydir.x, plane.normal.x, tx),
(plane.xdir.y, plane.ydir.y, plane.normal.y, ty), (plane.xdir.y, plane.ydir.y, plane.normal.y, ty),
(plane.xdir.z, plane.ydir.z, plane.normal.z, tz), (plane.xdir.z, plane.ydir.z, plane.normal.z, tz),
(0, 0, 0, 1 ) (0, 0, 0, 1),
)) )
)
""" """
Instances / Blocks Instances / Blocks
""" """
def _get_instance_name(instance: Instance) -> str: 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 = ( name_prefix = (
_get_friendly_object_name(instance) _get_friendly_object_name(instance)
or _get_friendly_object_name(instance.definition) or _get_friendly_object_name(instance.definition)
or _simplified_speckle_type(instance.speckle_type) or _simplified_speckle_type(instance.speckle_type)
) )
return f"{name_prefix}{OBJECT_NAME_SPECKLE_SEPARATOR}{instance.id}" 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) 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.definition:
if not instance.transform: raise Exception("Instance is missing a transform") raise Exception("Instance is missing a definition")
if not instance.transform:
raise Exception("Instance is missing a transform")
definition = instance.definition 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) name = _get_instance_name(instance)
native_instance: Optional[Object] = None native_instance: Optional[Object] = None
converted_objects: Dict[str, Union[Object, BCollection]] = {} converted_objects: Dict[str, Union[Object, BCollection]] = {}
traversal_root: Base = definition traversal_root: Base = definition
if not can_convert_to_native(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. # 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. # 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 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") traversal_root = Base(elements=definition, id="__ROOT")
#Convert definition + "elements" on definition # Convert definition + "elements" on definition
_deep_conversion(traversal_root, converted_objects, False) _deep_conversion(traversal_root, converted_objects, False)
if not native_instance: 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") raise Exception("Definition was not converted")
converted = converted_objects[definition.id] converted = converted_objects[definition.id]
if not isinstance(converted, Object): if not isinstance(converted, Object):
raise Exception("Definition was not converted to an Object") raise Exception("Definition was not converted to an Object")
native_instance = converted native_instance = converted
instance_transform = transform_to_native(instance.transform, scale) 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 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 Convert an Instance as a transformed Object with the `instance_collection` property
set to be the `instance.Definition` converted as a collection 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 The definition collection won't be linked to the current scene
Any Elements on the instance object will also be converted (and spacially transformed) 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.definition:
if not instance.transform: raise Exception("Instance is missing a transform") raise Exception("Instance is missing a definition")
if not instance.transform:
raise Exception("Instance is missing a transform")
name = _get_instance_name(instance) 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) 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 # hide the instance axes so they don't clutter the viewport
native_instance.empty_display_size = 0 native_instance.empty_display_size = 0
native_instance.instance_collection = collection_def native_instance.instance_collection = collection_def
native_instance.instance_type = "COLLECTION" native_instance.instance_type = "COLLECTION"
native_instance.matrix_world = instance_transform 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) 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 native_def["applicationId"] = definition.applicationId
converted_objects = {} 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") dummyRoot = Base(elements=definition, id="__ROOT")
_deep_conversion(dummyRoot, converted_objects, True) _deep_conversion(dummyRoot, converted_objects, True)
return native_def 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) traversal_func = get_default_traversal_func(can_convert_to_native)
for item in traversal_func.traverse(root): for item in traversal_func.traverse(root):
current: Base = item.current current: Base = item.current
if can_convert_to_native(current) or isinstance(current, SCollection): if can_convert_to_native(current) or isinstance(current, SCollection):
try: 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_data_type: str
converted: Union[Object, BCollection, None] converted: Union[Object, BCollection, None]
if isinstance(current, SCollection): if isinstance(current, SCollection):
if(current.collectionType == "Scene Collection"): raise ConversionSkippedException() if current.collectionType == "Scene Collection":
raise ConversionSkippedException()
converted = collection_to_native(current) converted = collection_to_native(current)
converted_data_type = "COLLECTION" converted_data_type = "COLLECTION"
else: else:
converted = convert_to_native(current) 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: if converted is None:
raise Exception("Conversion returned None") raise Exception("Conversion returned None")
converted_objects[current.id] = converted converted_objects[current.id] = converted
add_to_hierarchy(converted, item, converted_objects, preserve_transform) 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: 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: 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 def collection_to_native(collection: SCollection) -> BCollection:
ret = get_or_create_collection(name) 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) color = getattr(collection, "colorTag", None)
if color: if color:
@@ -710,8 +831,9 @@ def collection_to_native(collection: SCollection) -> BCollection:
return ret return ret
def get_or_create_collection(name: str, clear_collection: bool = True) -> BCollection: 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)) # existing = cast(Optional[BCollection], bpy.data.collections.get(name))
# if existing: # if existing:
# if clear_collection: # if clear_collection:
@@ -721,20 +843,20 @@ def get_or_create_collection(name: str, clear_collection: bool = True) -> BColle
# else: # else:
new_collection = create_new_collection(name) 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": if name == "Rooms":
new_collection.hide_viewport = True new_collection.hide_viewport = True
new_collection.hide_render = True new_collection.hide_render = True
return new_collection return new_collection
""" """
Object Naming and Creation 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 Creates a new blender collection with a unique name
If the desired_name is already taken 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) blender_collection = bpy.data.collections.new(name)
return blender_collection 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, Creates a new blender object with a unique name,
If the desired_name is already taken 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) blender_object = bpy.data.objects.new(name, obj_data)
return blender_object 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 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. # We could use a binary/galloping search, and/or cache the name -> index within a receive.
if name in taken_names: 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 _make_unique_name(desired_name, taken_names, counter + 1)
return name return name
def _get_friendly_object_name(speckle_object: Base) -> Optional[str]: def _get_friendly_object_name(speckle_object: Base) -> Optional[str]:
return (getattr(speckle_object, "name", None) return (
getattr(speckle_object, "name", None)
or getattr(speckle_object, "Name", None) or getattr(speckle_object, "Name", None)
or _get_revit_family_name(speckle_object) or _get_revit_family_name(speckle_object)
) )
def _get_revit_family_name(speckle_object: Base) -> Optional[str]: def _get_revit_family_name(speckle_object: Base) -> Optional[str]:
family = getattr(speckle_object, "family", None) family = getattr(speckle_object, "family", None)
@@ -786,18 +920,23 @@ def _get_revit_family_name(speckle_object: Base) -> Optional[str]:
else: else:
return None return None
# Blender object names must not exceed 62 characters # Blender object names must not exceed 62 characters
# We need to ensure the complete ID is included in the name (to prevent identity collisions) # 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 # So we if the name is too long, we need to truncate
def _truncate_object_name(name: str) -> str: def _truncate_object_name(name: str) -> str:
MAX_NAME_LENGTH = (
MAX_NAME_LENGTH = OBJECT_NAME_MAX_LENGTH - SPECKLE_ID_LENGTH - len(OBJECT_NAME_SPECKLE_SEPARATOR) OBJECT_NAME_MAX_LENGTH - SPECKLE_ID_LENGTH - len(OBJECT_NAME_SPECKLE_SEPARATOR)
)
return name[:MAX_NAME_LENGTH] return name[:MAX_NAME_LENGTH]
def _simplified_speckle_type(speckle_type: str) -> str: 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: def _generate_object_name(speckle_object: Base) -> str:
prefix: str prefix: str
@@ -814,4 +953,4 @@ def get_scale_factor(speckle_object: Base, fallback: float = 1.0) -> float:
scale = fallback scale = fallback
if units := getattr(speckle_object, "units", None): if units := getattr(speckle_object, "units", None):
scale = get_scale_length(units) / bpy.context.scene.unit_settings.scale_length 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 from typing import Dict, Iterable, List, Optional, Tuple, Union, cast
import bpy import bpy
from bpy.types import ( from bpy.types import Camera as NCamera
Depsgraph, from bpy.types import Curve as NCurve
MeshPolygon, from bpy.types import Depsgraph
Object, from bpy.types import Mesh as NMesh
Curve as NCurve, from bpy.types import MeshPolygon, Object
Mesh as NMesh,
Camera as NCamera,
)
from deprecated import deprecated from deprecated import deprecated
from mathutils import Matrix as MMatrix
from mathutils import Vector as MVector
from mathutils.geometry import interpolate_bezier from mathutils.geometry import interpolate_bezier
from mathutils import (
Matrix as MMatrix,
Vector as MVector,
)
from specklepy.objects import Base from specklepy.objects import Base
from specklepy.objects.other import BlockInstance, BlockDefinition, RenderMaterial, Transform from specklepy.objects.geometry import (Box, Curve, Interval, Mesh, Point,
from specklepy.objects.geometry import ( Polyline, Vector)
Mesh, Curve, Interval, Box, Point, Vector, Polyline, 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.blender_commit_object_builder import \
from bpy_speckle.convert.util import ( BlenderCommitObjectBuilder
ConversionSkippedException, from bpy_speckle.convert.constants import (OBJECT_NAME_SPECKLE_SEPARATOR,
get_blender_custom_properties, SPECKLE_ID_LENGTH)
make_knots, from bpy_speckle.convert.util import (ConversionSkippedException,
nurb_make_curve, get_blender_custom_properties,
to_argb_int, make_knots, nurb_make_curve, to_argb_int)
)
from bpy_speckle.functions import _report from bpy_speckle.functions import _report
Units: str = "m" # The desired final units to send
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
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") 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) Converts supported 1 blender objects to 1 speckle object (potentially with children)
:param raw_blender_object: the blender object (unevaluated by a Depsgraph) to convert :param 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 global Units, UnitsScale
Units = units Units = units
UnitsScale = units_scale UnitsScale = units_scale
blender_type = raw_blender_object.type blender_type = raw_blender_object.type
if blender_type not in CAN_CONVERT_TO_SPECKLE: 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, ( blender_object = cast(
raw_blender_object.evaluated_get(depsgraph) Object,
if depsgraph (
else raw_blender_object raw_blender_object.evaluated_get(depsgraph)
)) if depsgraph
else raw_blender_object
),
)
converted: Optional[Base] = None converted: Optional[Base] = None
if blender_type == "MESH": 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": elif blender_type == "EMPTY":
converted = empty_to_speckle(blender_object) converted = empty_to_speckle(blender_object)
elif blender_type == "CAMERA": 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": elif blender_type == "FONT" or "SURFACE" or "META":
converted = anything_to_speckle_mesh(blender_object) converted = anything_to_speckle_mesh(blender_object)
if not converted: if not converted:
raise Exception("Conversion returned None") 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 # 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( converted["properties"]["transform"] = transform_to_speckle(
blender_object.matrix_world blender_object.matrix_world
) )
return converted return converted
def mesh_to_speckle(blender_object: Object, data: bpy.types.Mesh) -> Base: def mesh_to_speckle(blender_object: Object, data: bpy.types.Mesh) -> Base:
b = Base() b = Base()
b["name"] = to_speckle_name(blender_object) b["name"] = to_speckle_name(blender_object)
b["@displayValue"] = mesh_to_speckle_meshes(blender_object, data) b["@displayValue"] = mesh_to_speckle_meshes(blender_object, data)
return b 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 # Categorise polygons by material index
submesh_data: Dict[int, List[MeshPolygon]] = {} 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) submesh_data[p.material_index].append(p)
transform = cast(MMatrix, blender_object.matrix_world) 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 # Create Speckle meshes for each material
submeshes = [] submeshes = []
@@ -109,8 +118,8 @@ def mesh_to_speckle_meshes(blender_object: Object, data: bpy.types.Mesh) -> List
for i in submesh_data: for i in submesh_data:
index_mapping: Dict[int, int] = {} 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 mesh_area = 0
m_verts: List[float] = [] m_verts: List[float] = []
m_faces: List[int] = [] 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[0])
m_verts.append(vert[1]) m_verts.append(vert[1])
m_verts.append(vert[2]) m_verts.append(vert[2])
if data.uv_layers.active: if data.uv_layers.active:
vt = data.uv_layers.active.data[index_counter] vt = data.uv_layers.active.data[index_counter]
uv = cast(MVector, vt.uv) uv = cast(MVector, vt.uv)
@@ -143,43 +152,46 @@ def mesh_to_speckle_meshes(blender_object: Object, data: bpy.types.Mesh) -> List
colors=[], colors=[],
textureCoordinates=m_texcoords, textureCoordinates=m_texcoords,
units=Units, units=Units,
area = mesh_area, area=mesh_area,
bbox=Box(area=0.0, volume=0.0), bbox=Box(area=0.0, volume=0.0),
) )
if i < len(data.materials): if i < len(data.materials):
material = data.materials[i] material = data.materials[i]
if material is not None: if material is not None:
speckle_mesh["renderMaterial"] = material_to_speckle(material) speckle_mesh["renderMaterial"] = material_to_speckle(material)
submeshes.append(speckle_mesh) submeshes.append(speckle_mesh)
return submeshes 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 degree = 3
closed = spline.use_cyclic_u closed = spline.use_cyclic_u
points: List[Tuple[MVector]] = [] points: List[Tuple[MVector]] = []
for i, bp in enumerate(spline.bezier_points): for i, bp in enumerate(spline.bezier_points):
if i > 0: if i > 0:
points.append(tuple(matrix @ bp.handle_left * UnitsScale)) # type: ignore points.append(tuple(matrix @ bp.handle_left * UnitsScale)) # type: ignore
points.append(tuple(matrix @ bp.co * UnitsScale)) # type: ignore points.append(tuple(matrix @ bp.co * UnitsScale)) # type: ignore
if i < len(spline.bezier_points) - 1: 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: if closed:
points.extend( points.extend(
( (
tuple(matrix @ spline.bezier_points[-1].handle_right * 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].handle_left * UnitsScale), # type: ignore
tuple(matrix @ spline.bezier_points[0].co * UnitsScale), # type: ignore tuple(matrix @ spline.bezier_points[0].co * UnitsScale), # type: ignore
) )
) )
num_points = len(points) num_points = len(points)
flattened_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 knot_count = num_points + degree - 1
knots = [0] * knot_count knots = [0] * knot_count
@@ -193,7 +205,7 @@ def bezier_to_speckle(matrix: MMatrix, spline: bpy.types.Spline, name: Optional[
name=name, name=name,
degree=degree, degree=degree,
closed=spline.use_cyclic_u, closed=spline.use_cyclic_u,
periodic= not spline.use_endpoint_u, periodic=not spline.use_endpoint_u,
points=flattened_points, points=flattened_points,
weights=[1] * num_points, weights=[1] * num_points,
knots=knots, knots=knots,
@@ -204,12 +216,13 @@ def bezier_to_speckle(matrix: MMatrix, spline: bpy.types.Spline, name: Optional[
domain=domain, domain=domain,
units=Units, units=Units,
bbox=Box(area=0.0, volume=0.0), 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 degree = spline.order_u - 1
knots = make_knots(spline) 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] weights = [pt.weight for pt in spline.points]
is_rational = all(w == weights[0] for w in weights) 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 = [] flattened_points = []
for row in points: flattened_points.extend(row) for row in points:
flattened_points.extend(row)
if spline.use_cyclic_u: if spline.use_cyclic_u:
for i in range(0, degree * 3, 3): 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 + 0])
flattened_points.append(flattened_points[i + 1]) flattened_points.append(flattened_points[i + 1])
flattened_points.append(flattened_points[i + 2]) flattened_points.append(flattened_points[i + 2])
for i in range(0, degree): for i in range(0, degree):
weights.append(weights[i]) weights.append(weights[i])
@@ -238,11 +252,11 @@ def nurbs_to_speckle(matrix: MMatrix, spline: bpy.types.Spline, name: Optional[s
name=name, name=name,
degree=degree, degree=degree,
closed=spline.use_cyclic_u, closed=spline.use_cyclic_u,
periodic= not spline.use_endpoint_u, periodic=not spline.use_endpoint_u,
points=flattened_points, points=flattened_points,
weights=weights, weights=weights,
knots=knots, knots=knots,
rational=is_rational, rational=is_rational,
area=0, area=0,
volume=0, volume=0,
length=length, 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), 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 Samples a nurbs curve with resolution_u creating a polyline
""" """
points: List[float] = [] points: List[float] = []
sampled_points = nurb_make_curve(spline, spline.resolution_u, 3) sampled_points = nurb_make_curve(spline, spline.resolution_u, 3)
for i in range(0, len(sampled_points), 3): for i in range(0, len(sampled_points), 3):
scaled_point = cast(Vector, matrix @ MVector(( scaled_point = cast(
sampled_points[i + 0], Vector,
sampled_points[i + 1], matrix
sampled_points[i + 2])) * UnitsScale) @ MVector(
(sampled_points[i + 0], sampled_points[i + 1], sampled_points[i + 2])
)
* UnitsScale,
)
points.append(scaled_point.x) points.append(scaled_point.x)
points.append(scaled_point.y) points.append(scaled_point.y)
points.append(scaled_point.z) points.append(scaled_point.z)
length = length or spline.calc_length() length = length or spline.calc_length()
domain = Interval(start=0, end=length, totalChildrenCount=0) 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) # 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]: 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 Samples a Bézier curve with resolution_u creating a polyline
""" """
segments = len(spline.bezier_points) segments = len(spline.bezier_points)
if segments < 2: return None if segments < 2:
return None
R = spline.resolution_u + 1 R = spline.resolution_u + 1
points = [] points = []
if not spline.use_cyclic_u: if not spline.use_cyclic_u:
segments -= 1 segments -= 1
points: List[float] = [] points: List[float] = []
for i in range(segments): for i in range(segments):
inext = (i + 1) % len(spline.bezier_points) 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() length = length or spline.calc_length()
domain = Interval(start=0, end=length, totalChildrenCount=0) 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) _QUICK_TEST_NAME_LENGTH = SPECKLE_ID_LENGTH + len(OBJECT_NAME_SPECKLE_SEPARATOR)
def to_speckle_name(blender_object: bpy.types.ID) -> str: 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: if does_name_contain_id:
return blender_object.name.rsplit(OBJECT_NAME_SPECKLE_SEPARATOR, 1)[0] return blender_object.name.rsplit(OBJECT_NAME_SPECKLE_SEPARATOR, 1)[0]
else: else:
return blender_object.name 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 = [] flattened_points = []
for row in points: flattened_points.extend(row) for row in points:
flattened_points.extend(row)
length = spline.calc_length() length = spline.calc_length()
domain = Interval(start=0, end=length, totalChildrenCount=0) 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 b["@elements"] = curves
return b 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) matrix = cast(MMatrix, blender_object.matrix_world)
meshes: List[Mesh] = [] meshes: List[Mesh] = []
curves: List[Base] = [] curves: List[Base] = []
#TODO: Could we support this better? # TODO: Could we support this better?
if data.bevel_mode == "OBJECT" and data.bevel_object != None: if data.bevel_mode == "OBJECT" and data.bevel_object is not None:
meshes = mesh_to_speckle_meshes(blender_object, blender_object.to_mesh()) meshes = mesh_to_speckle_meshes(blender_object, blender_object.to_mesh())
for spline in data.splines: for spline in data.splines:
if spline.type == "BEZIER": 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": 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": 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) 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()) mesh = mesh_to_speckle(blender_object, blender_object.to_mesh())
blender_object.to_mesh_clear() blender_object.to_mesh_clear()
return mesh return mesh
@deprecated @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" UNITS = "m" if bpy.context.scene.unit_settings.system == "METRIC" else "ft"
if blender_object.type != "MESH": 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): for i, poly in enumerate(data.polygons):
value = [] value = []
for v in poly.vertices: 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) domain = Interval(start=0, end=1)
poly = Polyline( poly = Polyline(
@@ -418,65 +469,75 @@ def material_to_speckle(blender_mat: bpy.types.Material) -> RenderMaterial:
if blender_mat.use_nodes: if blender_mat.use_nodes:
if blender_mat.node_tree.nodes.get("Principled BSDF"): if blender_mat.node_tree.nodes.get("Principled BSDF"):
inputs = blender_mat.node_tree.nodes["Principled BSDF"].inputs inputs = blender_mat.node_tree.nodes["Principled BSDF"].inputs
emission_color = "Emission" if "Emission" in inputs else "Emission Color" # 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.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.emissive = to_argb_int(inputs[emission_color].default_value) # type: ignore
speckle_mat.roughness = inputs["Roughness"].default_value # type: ignore speckle_mat.roughness = inputs["Roughness"].default_value # type: ignore
speckle_mat.metalness = inputs["Metallic"].default_value # type: ignore speckle_mat.metalness = inputs["Metallic"].default_value # type: ignore
speckle_mat.opacity = inputs["Alpha"].default_value # type: ignore speckle_mat.opacity = inputs["Alpha"].default_value # type: ignore
return speckle_mat return speckle_mat
elif blender_mat.node_tree.nodes.get("Diffuse BSDF"): elif blender_mat.node_tree.nodes.get("Diffuse BSDF"):
inputs = blender_mat.node_tree.nodes["Diffuse BSDF"].inputs inputs = blender_mat.node_tree.nodes["Diffuse BSDF"].inputs
speckle_mat.diffuse = to_argb_int(inputs["Color"].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 speckle_mat.roughness = inputs["Roughness"].default_value # type: ignore
return speckle_mat return speckle_mat
#TODO: Support more shaders # TODO: Support more shaders
# fallback to standard material props # 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.metalness = blender_mat.metallic
speckle_mat.roughness = blender_mat.roughness speckle_mat.roughness = blender_mat.roughness
return speckle_mat return speckle_mat
def camera_to_speckle_view(blender_object: Object, data: NCamera) -> Base: 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") raise Exception(f"Cameras of type {data.type} are not currently supported")
matrix = cast(MMatrix, blender_object.matrix_world) matrix = cast(MMatrix, blender_object.matrix_world)
up = cast(MVector, matrix.col[1].xyz) up = cast(MVector, matrix.col[1].xyz)
forwards = cast(MVector, -matrix.col[2].xyz) forwards = cast(MVector, -matrix.col[2].xyz)
translation = matrix.translation 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.name = to_speckle_name(blender_object)
view.origin = vector_to_speckle_point(translation) view.origin = vector_to_speckle_point(translation)
view.upDirection = vector_to_speckle(up) view.upDirection = vector_to_speckle(up)
view.forwardDirection = vector_to_speckle(forwards) 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.units = Units
view.isOrthogonal = False view.isOrthogonal = False
return view return view
def vector_to_speckle_point(xyz: MVector) -> Point: def vector_to_speckle_point(xyz: MVector) -> Point:
return Point( return Point(
x = xyz.x * UnitsScale, x=xyz.x * UnitsScale,
y = xyz.y * UnitsScale, y=xyz.y * UnitsScale,
z = xyz.z * UnitsScale, z=xyz.z * UnitsScale,
units = Units, units=Units,
) )
def vector_to_speckle(xyz: MVector) -> Vector: def vector_to_speckle(xyz: MVector) -> Vector:
return Vector( return Vector(
x = xyz.x * UnitsScale, x=xyz.x * UnitsScale,
y = xyz.y * UnitsScale, y=xyz.y * UnitsScale,
z = xyz.z * UnitsScale, z=xyz.z * UnitsScale,
units = Units, 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] value = [y for x in iterable_transform for y in x]
# scale the translation # scale the translation
for i in (3, 7, 11): 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) c = convert_to_speckle(geo, UnitsScale, Units, None)
geometryBuilder.include_object(c, geo) geometryBuilder.include_object(c, geo)
except ConversionSkippedException as ex: 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: 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() dummyRoot = Base()
geometryBuilder.apply_relationships(geometryBuilder.converted.values(), dummyRoot) 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: def block_instance_to_speckle(blender_instance: Object) -> BlockInstance:
return BlockInstance( return BlockInstance(
blockDefinition=block_def_to_speckle( blockDefinition=block_def_to_speckle(blender_instance.instance_collection),
blender_instance.instance_collection
),
transform=transform_to_speckle(blender_instance.matrix_world), transform=transform_to_speckle(blender_instance.matrix_world),
name=to_speckle_name(blender_instance), name=to_speckle_name(blender_instance),
units=Units, 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]: def empty_to_speckle(blender_object: Object) -> Union[BlockInstance, Base]:
# probably an instance collection (block) so let's try it # 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 # Empty -> Block
return block_instance_to_speckle(blender_object) return block_instance_to_speckle(blender_object)
else: else:
# Empty -> Point # Empty -> Point
wrapper = Base() 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 return wrapper
def matrix_to_speckle_point(matrix: MMatrix, units_scale: float = 1.0) -> Point: def matrix_to_speckle_point(matrix: MMatrix, units_scale: float = 1.0) -> Point:
transformed_pos = cast(MVector, matrix @ MVector((0,0,0)) * units_scale) transformed_pos = cast(MVector, matrix @ MVector((0, 0, 0)) * units_scale)
return Point(x = transformed_pos.x, return Point(x=transformed_pos.x, y=transformed_pos.y, z=transformed_pos.z)
y = transformed_pos.y,
z = transformed_pos.z)
+108 -60
View File
@@ -1,20 +1,24 @@
import math import math
from typing import Any, Dict, Optional, Tuple, Union, cast 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.base import Base
from specklepy.objects.geometry import Mesh from specklepy.objects.geometry import Mesh
from specklepy.objects.graph_traversal.traversal import TraversalContext
from specklepy.objects.other import RenderMaterial from specklepy.objects.other import RenderMaterial
from bpy_speckle.convert.constants import IGNORED_PROPERTY_KEYS from bpy_speckle.convert.constants import IGNORED_PROPERTY_KEYS
from bpy_speckle.functions import _report 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): class ConversionSkippedException(Exception):
pass pass
def to_rgba(argb_int: int) -> Tuple[float, float, float, float]: def to_rgba(argb_int: int) -> Tuple[float, float, float, float]:
"""Converts the int representation of a colour into a percent RGBA tuple""" """Converts the int representation of a colour into a percent RGBA tuple"""
alpha = ((argb_int >> 24) & 255) / 255 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) return int.from_bytes(int_color, byteorder="big", signed=True)
def set_custom_property(key: str, value: Any, blender_object: Object) -> None: def set_custom_property(key: str, value: Any, blender_object: Object) -> None:
try: try:
#Expected c types: float, int, string, float[], int[] # Expected c types: float, int, string, float[], int[]
blender_object[key] = value blender_object[key] = value
except (OverflowError, TypeError) as ex: 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: except Exception as ex:
#TODO: Log this as it's unexpected!!! # TODO: Log this as it's unexpected!!!
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}"
)
def add_custom_properties(speckle_object: Base, blender_object: Object): def add_custom_properties(speckle_object: Base, blender_object: Object):
if blender_object is None: 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) app_id = getattr(speckle_object, "applicationId", None)
if app_id: if app_id:
blender_object["applicationId"] = speckle_object.applicationId 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: for key in keys:
val = getattr(speckle_object, key, None) val = getattr(speckle_object, key, None)
if val is 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)] items = [item for item in val if not isinstance(item, Base)]
if items: if items:
set_custom_property(key, items, blender_object) set_custom_property(key, items, blender_object)
elif isinstance(val,dict): elif isinstance(val, dict):
for (k,v) in val.items(): for k, v in val.items():
if not isinstance(v, Base): if not isinstance(v, Base):
set_custom_property(k, v, blender_object) set_custom_property(k, v, blender_object)
def render_material_to_native(speckle_mat: RenderMaterial) -> Material: def render_material_to_native(speckle_mat: RenderMaterial) -> Material:
mat_name = speckle_mat.name mat_name = speckle_mat.name
if not mat_name: if not mat_name:
mat_name = speckle_mat.applicationId or speckle_mat.id or speckle_mat.get_id() 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 blender_mat.use_nodes = True
inputs = blender_mat.node_tree.nodes["Principled BSDF"].inputs inputs = blender_mat.node_tree.nodes["Principled BSDF"].inputs
inputs["Base Color"].default_value = to_rgba(speckle_mat.diffuse) # type: ignore inputs["Base Color"].default_value = to_rgba(speckle_mat.diffuse) # type: ignore
inputs["Roughness"].default_value = speckle_mat.roughness # type: ignore inputs["Roughness"].default_value = speckle_mat.roughness # type: ignore
inputs["Metallic"].default_value = speckle_mat.metalness # type: ignore inputs["Metallic"].default_value = speckle_mat.metalness # type: ignore
inputs["Alpha"].default_value = speckle_mat.opacity # type: ignore inputs["Alpha"].default_value = speckle_mat.opacity # type: ignore
# Blender >=4.0 use "Emission Color" # Blender >=4.0 use "Emission Color"
emission_color = "Emission" if "Emission" in inputs else "Emission Color" # 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 inputs[emission_color].default_value = to_rgba(speckle_mat.emissive) # type: ignore
if speckle_mat.opacity < 1.0: if speckle_mat.opacity < 1.0:
blender_mat.blend_method = "BLEND" blender_mat.blend_method = "BLEND"
return blender_mat return blender_mat
_vertex_color_material: Optional[Material] = None _vertex_color_material: Optional[Material] = None
def get_vertex_color_material() -> Material: def get_vertex_color_material() -> Material:
global _vertex_color_material global _vertex_color_material
#see https://stackoverflow.com/a/69807985 # see https://stackoverflow.com/a/69807985
if not _vertex_color_material: if not _vertex_color_material:
_vertex_color_material = bpy.data.materials.new("Vertex Color Material") _vertex_color_material = bpy.data.materials.new("Vertex Color Material")
_vertex_color_material.use_nodes = True _vertex_color_material.use_nodes = True
nodes = _vertex_color_material.node_tree.nodes nodes = _vertex_color_material.node_tree.nodes
principled_bsdf_node = cast(Node, nodes.get("Principled BSDF")) principled_bsdf_node = cast(Node, nodes.get("Principled BSDF"))
if not "VERTEX_COLOR" in [node.type for node in nodes]: if "VERTEX_COLOR" not in [node.type for node in nodes]:
vertex_color_node = cast(ShaderNodeVertexColor, nodes.new(type = "ShaderNodeVertexColor")) vertex_color_node = cast(
ShaderNodeVertexColor, nodes.new(type="ShaderNodeVertexColor")
)
else: else:
vertex_color_node = cast(ShaderNodeVertexColor, nodes.get("Vertex Color")) vertex_color_node = cast(ShaderNodeVertexColor, nodes.get("Vertex Color"))
vertex_color_node.layer_name = "Col" vertex_color_node.layer_name = "Col"
links = _vertex_color_material.node_tree.links 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 return _vertex_color_material
def get_render_material(speckle_object: Base) -> Optional[RenderMaterial]: def get_render_material(speckle_object: Base) -> Optional[RenderMaterial]:
"""Trys to get a RenderMaterial on given speckle_object""" """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 speckle_mat
return None return None
def add_vertices(speckle_mesh: Mesh, blender_mesh: BMesh, scale=1.0): 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(
def add_faces(speckle_mesh: Mesh, blender_mesh: BMesh, indexOffset: int, materialIndex: int = 0, smooth:bool = True): speckle_mesh: Mesh,
blender_mesh: BMesh,
indexOffset: int,
materialIndex: int = 0,
smooth: bool = True,
):
sfaces = speckle_mesh.faces sfaces = speckle_mesh.faces
if sfaces and len(sfaces) > 0: if sfaces and len(sfaces) > 0:
i = 0 i = 0
while i < len(sfaces): 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): def add_colors(speckle_mesh: Mesh, blender_mesh: BMesh):
scolors = speckle_mesh.colors scolors = speckle_mesh.colors
if scolors: if scolors:
colors = [] colors = []
if len(scolors) > 0: if len(scolors) > 0:
for i in range(len(scolors)): for i in range(len(scolors)):
argb = int(scolors[i]) argb = int(scolors[i])
(a, r, g, b) = argb_split(argb) (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: for loop in face.loops:
loop[color_layer] = colors[loop.vert.index] loop[color_layer] = colors[loop.vert.index]
def argb_split(argb: int) -> Tuple[int, int, int, int]: def argb_split(argb: int) -> Tuple[int, int, int, int]:
alpha = (argb >> 24) & 0xFF alpha = (argb >> 24) & 0xFF
red = (argb >> 16) & 0xFF red = (argb >> 16) & 0xFF
@@ -213,6 +234,7 @@ def argb_split(argb: int) -> Tuple[int, int, int, int]:
return (alpha, red, green, blue) return (alpha, red, green, blue)
def add_uv_coords(speckle_mesh: Mesh, blender_mesh: BMesh): def add_uv_coords(speckle_mesh: Mesh, blender_mesh: BMesh):
s_uvs = speckle_mesh.textureCoordinates s_uvs = speckle_mesh.textureCoordinates
if not s_uvs: 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): if len(s_uvs) // 2 == len(blender_mesh.verts):
uv.extend( uv.extend(
(float(s_uvs[i]), float(s_uvs[i + 1])) (float(s_uvs[i]), float(s_uvs[i + 1])) for i in range(0, len(s_uvs), 2)
for i in range(0, len(s_uvs), 2)
) )
else: else:
_report( _report(
@@ -235,9 +256,9 @@ def add_uv_coords(speckle_mesh: Mesh, blender_mesh: BMesh):
uv_layer = blender_mesh.loops.layers.uv.verify() uv_layer = blender_mesh.loops.layers.uv.verify()
for f in blender_mesh.faces: for f in blender_mesh.faces:
for l in f.loops: for loop in f.loops:
luv = l[uv_layer] luv = loop[uv_layer]
luv.uv = uv[l.vert.index] luv.uv = uv[loop.vert.index]
except: except:
_report("Failed to decode texture coordinates.") _report("Failed to decode texture coordinates.")
raise raise
@@ -246,8 +267,7 @@ def add_uv_coords(speckle_mesh: Mesh, blender_mesh: BMesh):
ignored_keys = { ignored_keys = {
"id", "id",
"speckle", "speckle",
"speckle_type" "speckle_type" "_speckle_type",
"_speckle_type",
"_speckle_name", "_speckle_name",
"_speckle_transform", "_speckle_transform",
"_RNA_UI", "_RNA_UI",
@@ -257,6 +277,7 @@ ignored_keys = {
"_chunkable", "_chunkable",
} }
def get_blender_custom_properties(obj, max_depth: int = 63): 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""" """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: 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)): 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 return obj
""" """
Python implementation of Blender's NURBS curve generation for to Speckle conversion Python implementation of Blender's NURBS curve generation for to Speckle conversion
from: https://blender.stackexchange.com/a/34276 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) based on https://projects.blender.org/blender/blender/src/branch/main/source/blender/blenkernel/intern/curve.cc (check old version)
""" """
def macro_knotsu(nu: bpy.types.Spline) -> int: def macro_knotsu(nu: bpy.types.Spline) -> int:
return nu.order_u + nu.point_count_u + (nu.order_u - 1 if nu.use_cyclic_u else 0) return nu.order_u + nu.point_count_u + (nu.order_u - 1 if nu.use_cyclic_u else 0)
def macro_segmentsu(nu: bpy.types.Spline) -> int: def macro_segmentsu(nu: bpy.types.Spline) -> int:
return nu.point_count_u if nu.use_cyclic_u else nu.point_count_u - 1 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]: def make_knots(nu: bpy.types.Spline) -> list[float]:
knots = [0.0] * macro_knotsu(nu) knots = [0.0] * macro_knotsu(nu)
flag = nu.use_endpoint_u + (nu.use_bezier_u << 1) 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: def calc_knots(knots: list[float], point_count: int, order: int, flag: int) -> None:
pts_order = point_count + order pts_order = point_count + order
if flag == 1: # CU_NURB_ENDPOINT if flag == 1: # CU_NURB_ENDPOINT
k = 0.0 k = 0.0
for a in range(1, pts_order + 1): for a in range(1, pts_order + 1):
knots[a - 1] = k knots[a - 1] = k
if a >= order and a <= point_count: if a >= order and a <= point_count:
k += 1.0 k += 1.0
elif flag == 2: # CU_NURB_BEZIER elif flag == 2: # CU_NURB_BEZIER
if order == 4: if order == 4:
k = 0.34 k = 0.34
for a in range(pts_order): 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] 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 i1 = i2 = 0
orderpluspnts = order + point_count orderpluspnts = order + point_count
opp2 = orderpluspnts - 1 opp2 = orderpluspnts - 1
# this is for float inaccuracy # this is for float inaccuracy
if t < knots[0]: if t < knots[0]:
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: else:
basis[i] = 0.0 basis[i] = 0.0
basis[i] = 0.0 #type: ignore basis[i] = 0.0 # type: ignore
# this is order 2, 3, ... # this is order 2, 3, ...
for j in range(2, order + 1): for j in range(2, order + 1):
if i2 + j >= orderpluspnts: if i2 + j >= orderpluspnts:
i2 = opp2 - j i2 = opp2 - j
@@ -384,8 +417,9 @@ def basis_nurb(t: float, order: int, point_count: int, knots: list[float], basis
return start, end return start, end
def nurb_make_curve(nu: bpy.types.Spline, resolu: int, stride: int = 3) -> list[float]: def nurb_make_curve(nu: bpy.types.Spline, resolu: int, stride: int = 3) -> list[float]:
""""BKE_nurb_makeCurve""" """ "BKE_nurb_makeCurve"""
EPS = 1e-6 EPS = 1e-6
coord_index = istart = iend = 0 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) resolu = resolu * macro_segmentsu(nu)
ustart = knots[nu.order_u - 1] ustart = knots[nu.order_u - 1]
uend = knots[nu.point_count_u + nu.order_u - 1] if nu.use_cyclic_u else \ uend = (
knots[nu.point_count_u] knots[nu.point_count_u + nu.order_u - 1]
ustep = (uend - ustart) / (resolu - (0 if nu.use_cyclic_u else 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 cycl = nu.order_u - 1 if nu.use_cyclic_u else 0
u = ustart u = ustart
while resolu: while resolu:
resolu -= 1 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 sumdiv = 0.0
sum_index = 0 sum_index = 0
pt_index = istart - 1 pt_index = istart - 1
@@ -416,17 +455,17 @@ def nurb_make_curve(nu: bpy.types.Spline, resolu: int, stride: int = 3) -> list[
else: else:
pt_index += 1 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] sumdiv += sum_array[sum_index]
sum_index += 1 sum_index += 1
if (sumdiv != 0.0) and (sumdiv < 1.0 - EPS or sumdiv > 1.0 + EPS): if (sumdiv != 0.0) and (sumdiv < 1.0 - EPS or sumdiv > 1.0 + EPS):
sum_index = 0 sum_index = 0
for i in range(istart, iend + 1): for i in range(istart, iend + 1):
sum_array[sum_index] /= sumdiv #type: ignore sum_array[sum_index] /= sumdiv # type: ignore
sum_index += 1 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 sum_index = 0
pt_index = istart - 1 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: if sum_array[sum_index] != 0.0:
for j in range(3): 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 sum_index += 1
coord_index += stride coord_index += stride
@@ -446,14 +487,21 @@ def nurb_make_curve(nu: bpy.types.Spline, resolu: int, stride: int = 3) -> list[
return coord_array return coord_array
def link_object_to_collection_nested(obj: Object, col: BCollection): 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) col.objects.link(obj)
for child in obj.children: for child in obj.children:
link_object_to_collection_nested(child, col) 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 nextParent = traversalContext.parent
# Traverse up the tree to find a direct parent object, and a containing collection # 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): if isinstance(c, BCollection):
parent_collection = c parent_collection = c
break break
else: #isinstance(c, Object): else: # isinstance(c, Object):
parent_object = parent_object or c parent_object = parent_object or c
nextParent = nextParent.parent 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: def set_parent(child: Object, parent: Object, preserve_transform: bool = False) -> None:
if preserve_transform : if preserve_transform:
previous = child.matrix_world.copy() # type: ignore previous = child.matrix_world.copy() # type: ignore
child.parent = parent child.parent = parent
child.matrix_world = previous child.matrix_world = previous
else: else:
+22 -16
View File
@@ -1,9 +1,12 @@
from typing import Callable 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.base import Base
from specklepy.objects.units import get_scale_factor_to_meters, get_units_from_string 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: 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)) 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 Traversal func for traversing a speckle commit object
""" """
ignore_rule = TraversalRule( ignore_rule = TraversalRule(
[ [
lambda o: "Objects.Structural.Results" in o.speckle_type, #Sadly, this one is necessary to avoid double conversion... lambda o: "Objects.Structural.Results"
lambda o: "Objects.BuiltElements.Revit.Parameter" in o.speckle_type, #This one is just for traversal performance of revit commits in o.speckle_type, # Sadly, this one is necessary to avoid double conversion...
], lambda o: "Objects.BuiltElements.Revit.Parameter"
lambda _: [], in o.speckle_type, # This one is just for traversal performance of revit commits
],
lambda _: [],
) )
convertible_rule = TraversalRule( convertible_rule = TraversalRule(
[can_convert_to_native], [can_convert_to_native],
lambda _: ELEMENTS_PROPERTY_ALIASES, lambda _: ELEMENTS_PROPERTY_ALIASES,
) )
default_rule = TraversalRule( default_rule = TraversalRule(
[lambda _: True], [lambda _: True],
lambda o: o.get_member_names(), #TODO: avoid deprecated members lambda o: o.get_member_names(), # TODO: avoid deprecated members
) )
return GraphTraversal([ignore_rule, convertible_rule, default_rule]) 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 os
import sys import sys
from importlib import import_module, invalidate_caches
from pathlib import Path from pathlib import Path
from typing import Optional from typing import Optional
from importlib import import_module, invalidate_caches
_user_data_env_var = "SPECKLE_USERDATA_PATH" _user_data_env_var = "SPECKLE_USERDATA_PATH"
@@ -55,9 +55,7 @@ def user_application_data_path() -> Path:
if sys.platform.startswith("win"): if sys.platform.startswith("win"):
app_data_path = os.getenv("APPDATA") app_data_path = os.getenv("APPDATA")
if not app_data_path: if not app_data_path:
raise Exception( raise Exception("Cannot get appdata path from environment.")
"Cannot get appdata path from environment."
)
return Path(app_data_path) return Path(app_data_path)
else: else:
# try getting the standard XDG_DATA_HOME value # try getting the standard XDG_DATA_HOME value
@@ -68,9 +66,7 @@ def user_application_data_path() -> Path:
else: else:
return _ensure_folder_exists(Path.home(), ".config") return _ensure_folder_exists(Path.home(), ".config")
except Exception as ex: except Exception as ex:
raise Exception( raise Exception("Failed to initialize user application data path.", ex)
"Failed to initialize user application data path.", ex
)
def user_speckle_folder_path() -> Path: 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("Starting module dependency installation")
print(sys.executable) print(sys.executable)
PYTHON_PATH = sys.executable PYTHON_PATH = sys.executable
def connector_installation_path(host_application: str) -> Path: 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) connector_installation_path.mkdir(exist_ok=True, parents=True)
# set user modules path at beginning of paths for earlier hit # 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 return connector_installation_path
def is_pip_available() -> bool: def is_pip_available() -> bool:
try: try:
import_module("pip") # noqa F401 import_module("pip") # noqa F401
@@ -132,7 +124,9 @@ def ensure_pip() -> None:
if completed_process.returncode == 0: if completed_process.returncode == 0:
print("Successfully installed pip") print("Successfully installed pip")
else: 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: def get_requirements_path() -> Path:
@@ -151,12 +145,12 @@ def install_requirements(host_application: str) -> None:
def debugger_is_active() -> bool: def debugger_is_active() -> bool:
"""Return if the debugger is currently active""" """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() requirements_path = get_requirements_path()
is_debug = debugger_is_active() is_debug = debugger_is_active()
if not is_debug and not requirements_path.exists(): if not is_debug and not requirements_path.exists():
print("Skipped installing dependencies") print("Skipped installing dependencies")
return 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" m = f"Failed to install dependencies through pip, got {completed_process.returncode} return code"
print(m) print(m)
raise Exception(m) raise Exception(m)
print("Successfully installed dependencies") print("Successfully installed dependencies")
if not is_debug: 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 # the code above doesn't work for now, it fails on importing graphql-core
# despite that, the connector seams to be working as expected # despite that, the connector seams to be working as expected
# But it would be nice to make this solution work # 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() # requirements = get_requirements_path().read_text()
# reqs = [ # reqs = [
# req.split(" ; ")[0].split("==")[0].split("[")[0].replace("-", "_") # req.split(" ; ")[0].split("==")[0].split("[")[0].replace("-", "_")
@@ -216,6 +210,7 @@ def _import_dependencies() -> None:
# print(req) # print(req)
# import_module("specklepy") # import_module("specklepy")
def ensure_dependencies(host_application: str) -> None: def ensure_dependencies(host_application: str) -> None:
try: try:
install_dependencies(host_application) install_dependencies(host_application)
@@ -223,6 +218,6 @@ def ensure_dependencies(host_application: str) -> None:
_import_dependencies() _import_dependencies()
print("Successfully found dependencies") print("Successfully found dependencies")
except ImportError: 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 .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 = [ operator_classes = [
LoadUsers, LoadUsers,
+10 -10
View File
@@ -3,10 +3,11 @@ Commit operators
""" """
import bpy import bpy
from bpy.props import BoolProperty from bpy.props import BoolProperty
from specklepy.logging import metrics
from bpy_speckle.clients import speckle_clients from bpy_speckle.clients import speckle_clients
from bpy_speckle.functions import _report from bpy_speckle.functions import _report
from bpy_speckle.properties.scene import get_speckle from bpy_speckle.properties.scene import get_speckle
from specklepy.logging import metrics
class DeleteCommit(bpy.types.Operator): class DeleteCommit(bpy.types.Operator):
@@ -23,7 +24,7 @@ class DeleteCommit(bpy.types.Operator):
are_you_sure: BoolProperty( are_you_sure: BoolProperty(
name="Confirm", name="Confirm",
default=False, default=False,
) # type: ignore ) # type: ignore
def draw(self, context): def draw(self, context):
layout = self.layout layout = self.layout
@@ -48,7 +49,7 @@ class DeleteCommit(bpy.types.Operator):
return {"FINISHED"} return {"FINISHED"}
@staticmethod @staticmethod
def delete_commit(context: bpy.types.Context) -> None: def delete_commit(context: bpy.types.Context) -> None:
speckle = get_speckle(context) speckle = get_speckle(context)
(_, stream, branch, commit) = speckle.validate_commit_selection() (_, stream, branch, commit) = speckle.validate_commit_selection()
@@ -59,14 +60,13 @@ class DeleteCommit(bpy.types.Operator):
metrics.track( metrics.track(
"Connector Action", "Connector Action",
client.account, client.account,
custom_props={ custom_props={"name": "delete_commit"},
"name": "delete_commit"
},
) )
if not deleted: if not deleted:
raise Exception("Delete operation failed") 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 import webbrowser
from specklepy.logging import metrics
import bpy
from specklepy.logging import metrics
class OpenSpeckleGuide(bpy.types.Operator): class OpenSpeckleGuide(bpy.types.Operator):
@@ -12,15 +11,13 @@ class OpenSpeckleGuide(bpy.types.Operator):
bl_label = "Speckle Docs" bl_label = "Speckle Docs"
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
bl_description = f"Browse the documentation on the Speckle Guide ({_guide_url})" bl_description = f"Browse the documentation on the Speckle Guide ({_guide_url})"
def execute(self, context): def execute(self, context):
webbrowser.open(self._guide_url) webbrowser.open(self._guide_url)
metrics.track( metrics.track(
"Connector Action", "Connector Action",
None, None,
custom_props={ custom_props={"name": "OpenSpeckleGuide"},
"name": "OpenSpeckleGuide"
},
) )
return {"FINISHED"} return {"FINISHED"}
@@ -31,16 +28,16 @@ class OpenSpeckleTutorials(bpy.types.Operator):
bl_idname = "speckle.open_speckle_tutorials" bl_idname = "speckle.open_speckle_tutorials"
bl_label = "Tutorials Portal" bl_label = "Tutorials Portal"
bl_options = {"REGISTER", "UNDO"} 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): def execute(self, context):
webbrowser.open(self._tutorials_url) webbrowser.open(self._tutorials_url)
metrics.track( metrics.track(
"Connector Action", "Connector Action",
None, None,
custom_props={ custom_props={"name": "OpenSpeckleTutorials"},
"name": "OpenSpeckleTutorials"
},
) )
return {"FINISHED"} return {"FINISHED"}
@@ -51,15 +48,15 @@ class OpenSpeckleForum(bpy.types.Operator):
bl_idname = "speckle.open_speckle_forum" bl_idname = "speckle.open_speckle_forum"
bl_label = "Community Forum" bl_label = "Community Forum"
bl_options = {"REGISTER", "UNDO"} 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): def execute(self, context):
webbrowser.open(self._forum_url) webbrowser.open(self._forum_url)
metrics.track( metrics.track(
"Connector Action", "Connector Action",
None, None,
custom_props={ custom_props={"name": "OpenSpeckleForum"},
"name": "OpenSpeckleForum"
},
) )
return {"FINISHED"} return {"FINISHED"}
+24 -44
View File
@@ -5,14 +5,14 @@ Object operators
import bpy import bpy
from bpy.props import BoolProperty, EnumProperty from bpy.props import BoolProperty, EnumProperty
from deprecated import deprecated 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 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 @deprecated
class UpdateObject(bpy.types.Operator): class UpdateObject(bpy.types.Operator):
""" """
@@ -28,7 +28,6 @@ class UpdateObject(bpy.types.Operator):
client = None client = None
def execute(self, context): def execute(self, context):
user = context.scene.speckle.users[int(context.scene.speckle.active_user)]
client = speckle_clients[int(context.scene.speckle.active_user)] client = speckle_clients[int(context.scene.speckle.active_user)]
active = context.active_object active = context.active_object
@@ -59,16 +58,15 @@ class UpdateObject(bpy.types.Operator):
metrics.track( metrics.track(
"Connector Action", "Connector Action",
None, None,
custom_props={ custom_props={"name": "UpdateObject"},
"name": "UpdateObject"
},
) )
return {"FINISHED"} return {"FINISHED"}
return {"CANCELLED"} return {"CANCELLED"}
return {"CANCELLED"} return {"CANCELLED"}
@deprecated @deprecated
class ResetObject(bpy.types.Operator): class ResetObject(bpy.types.Operator):
""" """
@@ -80,7 +78,6 @@ class ResetObject(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
def execute(self, context): def execute(self, context):
context.object.speckle.send_or_receive = "send" context.object.speckle.send_or_receive = "send"
context.object.speckle.stream_id = "" context.object.speckle.stream_id = ""
context.object.speckle.object_id = "" context.object.speckle.object_id = ""
@@ -89,14 +86,13 @@ class ResetObject(bpy.types.Operator):
metrics.track( metrics.track(
"Connector Action", "Connector Action",
None, None,
custom_props={ custom_props={"name": "ResetObject"},
"name": "ResetObject"
},
) )
return {"FINISHED"} return {"FINISHED"}
@deprecated @deprecated
class DeleteObject(bpy.types.Operator): class DeleteObject(bpy.types.Operator):
""" """
@@ -143,14 +139,13 @@ class DeleteObject(bpy.types.Operator):
metrics.track( metrics.track(
"Connector Action", "Connector Action",
None, None,
custom_props={ custom_props={"name": "DeleteObject"},
"name": "DeleteObject"
},
) )
return {"FINISHED"} return {"FINISHED"}
@deprecated @deprecated
class UploadNgonsAsPolylines(bpy.types.Operator): class UploadNgonsAsPolylines(bpy.types.Operator):
""" """
@@ -170,7 +165,6 @@ class UploadNgonsAsPolylines(bpy.types.Operator):
def execute(self, context): def execute(self, context):
active = context.active_object active = context.active_object
if active is not None and active.type == "MESH": if active is not None and active.type == "MESH":
user = context.scene.speckle.users[int(context.scene.speckle.active_user)] user = context.scene.speckle.users[int(context.scene.speckle.active_user)]
client = speckle_clients[int(context.scene.speckle.active_user)] client = speckle_clients[int(context.scene.speckle.active_user)]
stream = user.streams[user.active_stream] stream = user.streams[user.active_stream]
@@ -187,7 +181,6 @@ class UploadNgonsAsPolylines(bpy.types.Operator):
placeholders = [] placeholders = []
for polyline in sp: for polyline in sp:
res = client.objects.create([polyline]) res = client.objects.create([polyline])
if res is None: if res is None:
@@ -223,10 +216,8 @@ class UploadNgonsAsPolylines(bpy.types.Operator):
metrics.track( metrics.track(
"Connector Action", "Connector Action",
None, None,
custom_props={ custom_props={"name": "UploadNgonsAsPolylines"},
"name": "UploadNgonsAsPolylines"
},
) )
return {"FINISHED"} return {"FINISHED"}
@@ -240,14 +231,13 @@ class UploadNgonsAsPolylines(bpy.types.Operator):
def get_custom_speckle_props(self, context): def get_custom_speckle_props(self, context):
ignore = ["speckle", "cycles", "cycles_visibility"]
active = context.active_object active = context.active_object
if not active: if not active:
return [] return []
return [(x, "{}".format(x), "") for x in active.keys()] return [(x, "{}".format(x), "") for x in active.keys()]
@deprecated @deprecated
class SelectIfSameCustomProperty(bpy.types.Operator): class SelectIfSameCustomProperty(bpy.types.Operator):
""" """
@@ -275,7 +265,6 @@ class SelectIfSameCustomProperty(bpy.types.Operator):
return wm.invoke_props_dialog(self) return wm.invoke_props_dialog(self)
def execute(self, context): def execute(self, context):
active = context.active_object active = context.active_object
if not active: if not active:
return {"CANCELLED"} return {"CANCELLED"}
@@ -292,7 +281,6 @@ class SelectIfSameCustomProperty(bpy.types.Operator):
) )
for obj in bpy.data.objects: for obj in bpy.data.objects:
if self.custom_prop in obj.keys() and obj[self.custom_prop] == value: if self.custom_prop in obj.keys() and obj[self.custom_prop] == value:
obj.select_set(True) obj.select_set(True)
else: else:
@@ -300,14 +288,13 @@ class SelectIfSameCustomProperty(bpy.types.Operator):
metrics.track( metrics.track(
"Connector Action", "Connector Action",
None, None,
custom_props={ custom_props={"name": "SelectIfSameCustomProperty"},
"name": "SelectIfSameCustomProperty"
},
) )
return {"FINISHED"} return {"FINISHED"}
@deprecated @deprecated
class SelectIfHasCustomProperty(bpy.types.Operator): class SelectIfHasCustomProperty(bpy.types.Operator):
""" """
@@ -335,7 +322,6 @@ class SelectIfHasCustomProperty(bpy.types.Operator):
return wm.invoke_props_dialog(self) return wm.invoke_props_dialog(self)
def execute(self, context): def execute(self, context):
active = context.active_object active = context.active_object
if not active: if not active:
return {"CANCELLED"} return {"CANCELLED"}
@@ -343,12 +329,9 @@ class SelectIfHasCustomProperty(bpy.types.Operator):
if self.custom_prop not in active.keys(): if self.custom_prop not in active.keys():
return {"CANCELLED"} return {"CANCELLED"}
value = active[self.custom_prop]
_report("Looking for '{}' property.".format(self.custom_prop)) _report("Looking for '{}' property.".format(self.custom_prop))
for obj in bpy.data.objects: for obj in bpy.data.objects:
if self.custom_prop in obj.keys(): if self.custom_prop in obj.keys():
obj.select_set(True) obj.select_set(True)
else: else:
@@ -356,11 +339,8 @@ class SelectIfHasCustomProperty(bpy.types.Operator):
metrics.track( metrics.track(
"Connector Action", "Connector Action",
None, None,
custom_props={ custom_props={"name": "SelectIfHasCustomProperty"},
"name": "SelectIfHasCustomProperty"
},
) )
return {"FINISHED"} return {"FINISHED"}
+210 -185
View File
@@ -1,84 +1,101 @@
""" """
Stream operators Stream operators
""" """
import webbrowser
from math import radians from math import radians
from typing import Callable, Dict, Optional, Tuple, Union, cast from typing import Callable, Dict, Optional, Tuple, Union, cast
import webbrowser
import bpy import bpy
from bpy.props import ( from bpy.props import BoolProperty, EnumProperty, StringProperty
StringProperty, from bpy.types import Collection, Context, Object
BoolProperty,
EnumProperty,
)
from bpy.types import (
Context,
Object,
Collection
)
from deprecated import deprecated from deprecated import deprecated
from bpy_speckle.blender_commit_object_builder import BlenderCommitObjectBuilder from specklepy.core.api import host_applications, operations
from bpy_speckle.convert.to_native import ( from specklepy.core.api.client import SpeckleClient
can_convert_to_native, from specklepy.core.api.models import Commit, Stream
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.wrapper import StreamWrapper from specklepy.core.api.wrapper import StreamWrapper
from specklepy.core.api.resources.stream import Stream from specklepy.logging import metrics
from specklepy.transports.server import ServerTransport from specklepy.logging.exceptions import SpeckleException
from specklepy.objects import Base from specklepy.objects import Base
from specklepy.objects.other import Collection as SCollection from specklepy.objects.other import Collection as SCollection
from specklepy.logging.exceptions import SpeckleException from specklepy.transports.server import ServerTransport
from specklepy.logging import metrics
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]] 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" Fetches the injected callback functions from user specified "Receive Script"
""" """
objectCallback: ObjectCallback = None objectCallback: ObjectCallback = None
receiveCompleteCallback: ReceiveCompleteCallback = None receiveCompleteCallback: ReceiveCompleteCallback = None
if speckle.receive_script in bpy.data.texts: if speckle.receive_script in bpy.data.texts:
mod = bpy.data.texts[speckle.receive_script].as_module() mod = bpy.data.texts[speckle.receive_script].as_module()
if hasattr(mod, "execute_for_each"): if hasattr(mod, "execute_for_each"):
objectCallback = mod.execute_for_each #type: ignore objectCallback = mod.execute_for_each # type: ignore
elif hasattr(mod, "execute"): elif hasattr(mod, "execute"):
objectCallback = lambda c, o, _ : mod.execute(c.scene, o) #type: ignore objectCallback = lambda c, o, _: mod.execute(c.scene, o) # type: ignore
if hasattr(mod, "execute_for_all"): if hasattr(mod, "execute_for_all"):
receiveCompleteCallback = mod.execute_for_all #type: ignore receiveCompleteCallback = mod.execute_for_all # type: ignore
return (objectCallback, receiveCompleteCallback) return (objectCallback, receiveCompleteCallback)
#RECEIVE_MODES = [#TODO: modes
# RECEIVE_MODES = [#TODO: modes
# ("create", "Create", "Add new geometry, without removing any existing objects"), # ("create", "Create", "Add new geometry, without removing any existing objects"),
# ("replace", "Replace", "Replace objects from previous receive operations from the same stream"), # ("replace", "Replace", "Replace objects from previous receive operations from the same stream"),
# #("update","Update", "") #TODO: update mode! # #("update","Update", "") #TODO: update mode!
#] # ]
INSTANCES_SETTINGS = [ 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): class ReceiveStreamObjects(bpy.types.Operator):
""" """
Receive objects from selected model version Receive objects from selected model version
@@ -89,64 +106,63 @@ class ReceiveStreamObjects(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
bl_description = "Receive objects from selected model version" 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_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_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): def draw(self, context):
layout = self.layout layout = self.layout
col = layout.column() col = layout.column()
col.prop(self, "clean_meshes") col.prop(self, "clean_meshes")
#col.prop(self, "receive_mode") # col.prop(self, "receive_mode")
col.prop(self, "receive_instances_as") col.prop(self, "receive_instances_as")
def invoke(self, context, event): def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self) return context.window_manager.invoke_props_dialog(self)
@staticmethod @staticmethod
def clean_converted_meshes(context: bpy.types.Context, convertedObjects: dict[str, Object]): def clean_converted_meshes(
context: bpy.types.Context, convertedObjects: dict[str, Object]
bpy.ops.object.select_all(action='DESELECT') ):
bpy.ops.object.select_all(action="DESELECT")
active = None active = None
for obj in convertedObjects.values(): for obj in convertedObjects.values():
if obj.type != 'MESH': if obj.type != "MESH":
continue continue
obj.select_set(True, view_layer=context.scene.view_layers[0]) obj.select_set(True, view_layer=context.scene.view_layers[0])
active = obj active = obj
if active == None: if active is None:
return return
context.view_layer.objects.active = active context.view_layer.objects.active = active
bpy.ops.object.mode_set(mode='EDIT') bpy.ops.object.mode_set(mode="EDIT")
bpy.ops.mesh.select_all(action='SELECT') bpy.ops.mesh.select_all(action="SELECT")
bpy.ops.mesh.dissolve_limited(angle_limit=radians(0.1)) bpy.ops.mesh.dissolve_limited(angle_limit=radians(0.1))
# Reset state to previous (not quite sure if this is 100% necessary) # Reset state to previous (not quite sure if this is 100% necessary)
bpy.ops.object.mode_set(mode='OBJECT') bpy.ops.object.mode_set(mode="OBJECT")
bpy.ops.object.select_all(action='DESELECT') bpy.ops.object.select_all(action="DESELECT")
bpy.context.view_layer.objects.active = None # type: ignore bpy.context.view_layer.objects.active = None # type: ignore
def execute(self, context): def execute(self, context):
self.receive(context) self.receive(context)
return {"FINISHED"} return {"FINISHED"}
def receive(self, context: Context) -> None: 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) speckle = get_speckle(context)
(user, stream, branch, commit) = speckle.validate_commit_selection() (user, stream, branch, commit) = speckle.validate_commit_selection()
client = speckle_clients[int(speckle.active_user)] client = speckle_clients[int(speckle.active_user)]
transport = ServerTransport(stream.id, client) transport = ServerTransport(stream.id, client)
# Fetch commit data # Fetch commit data
commit_object = operations.receive(commit.referenced_object, transport) commit_object = operations.receive(commit.referenced_object, transport)
client.commit.received( client.commit.received(
@@ -158,20 +174,24 @@ class ReceiveStreamObjects(bpy.types.Operator):
metrics.track( metrics.track(
metrics.RECEIVE, metrics.RECEIVE,
getattr(transport, "account", None), getattr(transport, "account", None),
custom_props={ 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, "sourceHostAppVersion": commit.source_application,
"isMultiplayer": commit.author_id != user.id, "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 # Convert received data
context.window_manager.progress_begin(0, commit_object.totalChildrenCount or 1) 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) traversalFunc = get_default_traversal_func(can_convert_to_native)
converted_objects: Dict[str, Union[Object, Collection]] = {} 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 # ensure commit object has a name if not already
if not commit_object.name: 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): for item in traversalFunc.traverse(commit_object):
current: Base = item.current current: Base = item.current
if can_convert_to_native(current) or isinstance(current, SCollection): 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: if not current or not current.id:
raise Exception(f"{current} was an invalid Speckle object") raise Exception(f"{current} was an invalid Speckle object")
#Convert the object! # Convert the object!
converted_data_type: str converted_data_type: str
converted: Union[Object, Collection, None] converted: Union[Object, Collection, None]
if isinstance(current, SCollection): if isinstance(current, SCollection):
if(current.collectionType == "Scene Collection"): raise ConversionSkippedException() if current.collectionType == "Scene Collection":
raise ConversionSkippedException()
converted = collection_to_native(current) converted = collection_to_native(current)
converted_data_type = "COLLECTION" converted_data_type = "COLLECTION"
else: else:
converted = convert_to_native(current) converted = convert_to_native(current)
converted_data_type = "COLLECTION_INSTANCE" if converted.instance_collection else str(converted.type) converted_data_type = (
"COLLECTION_INSTANCE"
#Run the user specified callback function (AKA receive script) if converted.instance_collection
else str(converted.type)
)
# Run the user specified callback function (AKA receive script)
if object_converted_callback: if object_converted_callback:
converted = object_converted_callback(context, converted, current) converted = object_converted_callback(
context, converted, current
)
if converted is None: if converted is None:
raise Exception("Conversion returned None") raise Exception("Conversion returned None")
converted_objects[current.id] = converted converted_objects[current.id] = converted
add_to_hierarchy(converted, item, converted_objects, True) 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: 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: 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 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() context.window_manager.progress_end()
if self.clean_meshes: 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) self.clean_converted_meshes(context, objects)
if on_complete_callback: if on_complete_callback:
on_complete_callback(context, converted_objects) on_complete_callback(context, converted_objects)
class SendStreamObjects(bpy.types.Operator): class SendStreamObjects(bpy.types.Operator):
""" """
Send selected objects to selected model Send selected objects to selected model
@@ -253,11 +287,11 @@ class SendStreamObjects(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
bl_description = "Send selected objects to selected model" 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( commit_message: StringProperty(
name="Message", name="Message",
default="Sent elements from Blender.", default="Sent elements from Blender.",
) # type: ignore ) # type: ignore
def draw(self, context): def draw(self, context):
layout = self.layout layout = self.layout
@@ -271,7 +305,7 @@ class SendStreamObjects(bpy.types.Operator):
if len(speckle.users) <= 0: if len(speckle.users) <= 0:
_report("No user accounts") _report("No user accounts")
return {"CANCELLED"} return {"CANCELLED"}
N = len(context.selected_objects) N = len(context.selected_objects)
if N == 1: if N == 1:
self.commit_message = f"Sent {N} element from Blender." 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." self.commit_message = f"Sent {N} elements from Blender."
return wm.invoke_props_dialog(self) return wm.invoke_props_dialog(self)
def execute(self, context): def execute(self, context):
self.send(context) self.send(context)
return {"FINISHED"} return {"FINISHED"}
def send(self, context: Context) -> None: def send(self, context: Context) -> None:
selected = context.selected_objects selected = context.selected_objects
if len(selected) < 1: if len(selected) < 1:
raise Exception("No objects are selected, sending canceled") 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: if speckle.send_script in bpy.data.texts:
mod = bpy.data.texts[speckle.send_script].as_module() mod = bpy.data.texts[speckle.send_script].as_module()
if hasattr(mod, "execute"): if hasattr(mod, "execute"):
func = mod.execute #type: ignore func = mod.execute # type: ignore
num_converted = 0 num_converted = 0
context.window_manager.progress_begin(0, max(len(selected), 1)) 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() commit_builder = BlenderCommitObjectBuilder()
for obj in selected: for obj in selected:
@@ -319,27 +353,26 @@ class SendStreamObjects(bpy.types.Operator):
if func: if func:
new_object = func(context.scene, obj) new_object = func(context.scene, obj)
if (new_object is None): if new_object is None:
raise ConversionSkippedException(f"Script '{func.__module__}' returned None.") raise ConversionSkippedException(
f"Script '{func.__module__}' returned None."
)
converted = convert_to_speckle( converted = convert_to_speckle(obj, units_scale, units, depsgraph)
obj,
units_scale,
units,
depsgraph
)
if not converted: if not converted:
raise Exception("Converter returned None") raise Exception("Converter returned None")
commit_builder.include_object(converted, obj) 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: except ConversionSkippedException as ex:
_report(f"Skipped converting '{obj.name_full}': '{ex}'") _report(f"Skipped converting '{obj.name_full}': '{ex}'")
except Exception as ex: except Exception as ex:
_report(f"Failed to converted '{obj.name_full}': '{ex}'") _report(f"Failed to converted '{obj.name_full}': '{ex}'")
num_converted += 1 num_converted += 1
context.window_manager.progress_update(num_converted) context.window_manager.progress_update(num_converted)
@@ -350,14 +383,15 @@ class SendStreamObjects(bpy.types.Operator):
metrics.track( metrics.track(
metrics.SEND, metrics.SEND,
client.account, client.account,
custom_props={ custom_props={
"branches": len(stream.branches), "branches": len(stream.branches),
#"collaborators": 0, #TODO: # "collaborators": 0, #TODO:
"isMain": branch.name == "main", "isMain": branch.name == "main",
"workspace_id": get_project_workspace_id(stream.id),
}, },
) )
_report(f"Sending data to {stream.name}") _report(f"Sending data to {stream.name}")
transport = ServerTransport(stream.id, client) transport = ServerTransport(stream.id, client)
OBJECT_ID = operations.send( OBJECT_ID = operations.send(
@@ -374,7 +408,9 @@ class SendStreamObjects(bpy.types.Operator):
) )
if client.account.serverInfo.frontend2: 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: else:
sent_url = f"{user.server_url}/streams/{stream.id}/commits/{COMMIT_ID}" 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_stream_id = stream.id
selection_state.selected_user_id = user.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() context.view_layer.update()
if context.area: if context.area:
context.area.tag_redraw() context.area.tag_redraw()
class ViewStreamDataApi(bpy.types.Operator): class ViewStreamDataApi(bpy.types.Operator):
bl_idname = "speckle.view_stream_data_api" bl_idname = "speckle.view_stream_data_api"
bl_label = "Open Model in Web" bl_label = "Open Model in Web"
@@ -409,21 +444,18 @@ class ViewStreamDataApi(bpy.types.Operator):
url = self._get_url_from_selection(speckle) url = self._get_url_from_selection(speckle)
_report(f"Opening {url} in web browser") _report(f"Opening {url} in web browser")
if not webbrowser.open(url, new=2): if not webbrowser.open(url, new=2):
raise Exception(f"Failed to open model in browser ({url})") raise Exception(f"Failed to open model in browser ({url})")
metrics.track( metrics.track(
"Connector Action", "Connector Action",
None, None,
custom_props={ custom_props={"name": "view_stream_data_api"},
"name": "view_stream_data_api"
},
) )
@staticmethod @staticmethod
def _get_url_from_selection(speckleScene : SpeckleSceneSettings) -> str: def _get_url_from_selection(speckleScene: SpeckleSceneSettings) -> str:
client = speckle_clients[int(speckleScene.active_user)] client = speckle_clients[int(speckleScene.active_user)]
(user, stream) = speckleScene.validate_stream_selection() (user, stream) = speckleScene.validate_stream_selection()
branch = stream.get_active_branch() branch = stream.get_active_branch()
@@ -443,7 +475,8 @@ class ViewStreamDataApi(bpy.types.Operator):
server_url += f"branches/{branch.name}" server_url += f"branches/{branch.name}"
return server_url return server_url
class AddStreamFromURL(bpy.types.Operator): class AddStreamFromURL(bpy.types.Operator):
""" """
Add / select an existing project by providing its URL 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_label = "Add Project From URL"
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
bl_description = "Add / select an existing project by providing its URL" bl_description = "Add / select an existing project by providing its URL"
stream_url: StringProperty( stream_url: StringProperty(name="Project URL", default="") # type: ignore
name="Project URL", default=""
) # type: ignore
def draw(self, context): def draw(self, context):
layout = self.layout layout = self.layout
@@ -475,21 +506,28 @@ class AddStreamFromURL(bpy.types.Operator):
return {"FINISHED"} return {"FINISHED"}
@staticmethod @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( 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), (None, None),
) )
if index is not None: if index is not None:
assert(b_stream) assert b_stream
return (index, b_stream) return (index, b_stream)
add_user_stream(user, stream) add_user_stream(user, stream)
return next( 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: def add_stream_from_url(self, context: Context) -> None:
speckle = get_speckle(context) speckle = get_speckle(context)
@@ -500,15 +538,23 @@ class AddStreamFromURL(bpy.types.Operator):
None, None,
) )
if user_index is 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) speckle.active_user = str(user_index)
user = cast(SpeckleUserObject, speckle.users[user_index]) user = cast(SpeckleUserObject, speckle.users[user_index])
client = speckle_clients[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): 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) (index, b_stream) = self._get_or_add_stream(user, stream)
user.active_stream = index user.active_stream = index
@@ -533,13 +579,11 @@ class AddStreamFromURL(bpy.types.Operator):
if context.area: if context.area:
context.area.tag_redraw() context.area.tag_redraw()
metrics.track( metrics.track(
"Connector Action", "Connector Action",
client.account, client.account,
custom_props={ custom_props={"name": "add_stream_from_url"},
"name": "add_stream_from_url"
},
) )
@@ -553,10 +597,10 @@ class CreateStream(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
bl_description = "Create a new Speckle project using the selected user account" 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( stream_description: StringProperty(
name="Project description", default="My new project" name="Project description", default="My new project"
) # type: ignore ) # type: ignore
def draw(self, context): def draw(self, context):
layout = self.layout layout = self.layout
@@ -575,7 +619,7 @@ class CreateStream(bpy.types.Operator):
def execute(self, context): def execute(self, context):
self.create_stream(context) self.create_stream(context)
return {"FINISHED"} return {"FINISHED"}
def create_stream(self, context: Context) -> None: def create_stream(self, context: Context) -> None:
speckle = get_speckle(context) speckle = get_speckle(context)
@@ -584,9 +628,7 @@ class CreateStream(bpy.types.Operator):
client = speckle_clients[int(speckle.active_user)] client = speckle_clients[int(speckle.active_user)]
client.stream.create( client.stream.create(
name=self.stream_name, name=self.stream_name, description=self.stream_description, is_public=True
description=self.stream_description,
is_public=True
) )
bpy.ops.speckle.load_user_streams() bpy.ops.speckle.load_user_streams()
@@ -597,13 +639,11 @@ class CreateStream(bpy.types.Operator):
if context.area: if context.area:
context.area.tag_redraw() context.area.tag_redraw()
metrics.track( metrics.track(
"Connector Action", "Connector Action",
client.account, client.account,
custom_props={ custom_props={"name": "create_stream"},
"name": "create_stream"
},
) )
@@ -622,9 +662,9 @@ class DeleteStream(bpy.types.Operator):
name="Confirm", name="Confirm",
description="⚠ This action will delete your entire stream permanently ⚠", description="⚠ This action will delete your entire stream permanently ⚠",
default=False, 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): def draw(self, context):
layout = self.layout layout = self.layout
@@ -673,12 +713,11 @@ class DeleteStream(bpy.types.Operator):
metrics.track( metrics.track(
"Connector Action", "Connector Action",
client.account, client.account,
custom_props={ custom_props={"name": "delete_stream"},
"name": "delete_stream"
},
) )
@deprecated @deprecated
class SelectOrphanObjects(bpy.types.Operator): class SelectOrphanObjects(bpy.types.Operator):
""" """
@@ -694,7 +733,6 @@ class SelectOrphanObjects(bpy.types.Operator):
layout = self.layout layout = self.layout
def execute(self, context): def execute(self, context):
for o in context.scene.objects: for o in context.scene.objects:
if ( if (
o.speckle.stream_id o.speckle.stream_id
@@ -705,14 +743,13 @@ class SelectOrphanObjects(bpy.types.Operator):
o.select = False o.select = False
metrics.track( metrics.track(
"Connector Action", "Connector Action",
custom_props={ custom_props={"name": "SelectOrphanObjects"},
"name": "SelectOrphanObjects"
},
) )
return {"FINISHED"} return {"FINISHED"}
class CopyStreamId(bpy.types.Operator): class CopyStreamId(bpy.types.Operator):
""" """
Copy the selected project id to clipboard Copy the selected project id to clipboard
@@ -726,7 +763,7 @@ class CopyStreamId(bpy.types.Operator):
def execute(self, context): def execute(self, context):
self.copy_stream_id(context) self.copy_stream_id(context)
return {"FINISHED"} return {"FINISHED"}
def copy_stream_id(self, context) -> None: def copy_stream_id(self, context) -> None:
speckle = get_speckle(context) speckle = get_speckle(context)
@@ -735,11 +772,10 @@ class CopyStreamId(bpy.types.Operator):
metrics.track( metrics.track(
"Connector Action", "Connector Action",
custom_props={ custom_props={"name": "copy_stream_id"},
"name": "copy_stream_id"
},
) )
class CopyCommitId(bpy.types.Operator): class CopyCommitId(bpy.types.Operator):
""" """
Copy the selected version id to clipboard Copy the selected version id to clipboard
@@ -754,7 +790,6 @@ class CopyCommitId(bpy.types.Operator):
self.copy_commit_id(context) self.copy_commit_id(context)
return {"FINISHED"} return {"FINISHED"}
def copy_commit_id(self, context) -> None: def copy_commit_id(self, context) -> None:
speckle = get_speckle(context) speckle = get_speckle(context)
@@ -763,13 +798,10 @@ class CopyCommitId(bpy.types.Operator):
metrics.track( metrics.track(
"Connector Action", "Connector Action",
custom_props={ custom_props={"name": "copy_commit_id"},
"name": "copy_commit_id"
},
) )
class CopyModelId(bpy.types.Operator): class CopyModelId(bpy.types.Operator):
""" """
Copy model id to clipboard Copy model id to clipboard
@@ -784,7 +816,6 @@ class CopyModelId(bpy.types.Operator):
self.copy_model_id(context) self.copy_model_id(context)
return {"FINISHED"} return {"FINISHED"}
def copy_model_id(self, context) -> None: def copy_model_id(self, context) -> None:
speckle = get_speckle(context) speckle = get_speckle(context)
@@ -794,11 +825,10 @@ class CopyModelId(bpy.types.Operator):
metrics.track( metrics.track(
"Connector Action", "Connector Action",
custom_props={ custom_props={"name": "copy_branch_id"},
"name": "copy_branch_id"
},
) )
@deprecated @deprecated
class CopyBranchName(bpy.types.Operator): class CopyBranchName(bpy.types.Operator):
""" """
@@ -814,7 +844,6 @@ class CopyBranchName(bpy.types.Operator):
self.copy_branch_id(context) self.copy_branch_id(context)
return {"FINISHED"} return {"FINISHED"}
def copy_branch_id(self, context) -> None: def copy_branch_id(self, context) -> None:
speckle = get_speckle(context) speckle = get_speckle(context)
@@ -824,11 +853,10 @@ class CopyBranchName(bpy.types.Operator):
metrics.track( metrics.track(
"Connector Action", "Connector Action",
custom_props={ custom_props={"name": "copy_branch_id"},
"name": "copy_branch_id"
},
) )
@deprecated @deprecated
class SelectOrphanObjects(bpy.types.Operator): 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" bl_description = "Select Speckle objects that don't belong to any stream"
def draw(self, context): def draw(self, context):
layout = self.layout pass
def execute(self, context): def execute(self, context):
for o in context.scene.objects: for o in context.scene.objects:
if ( if (
o.speckle.stream_id o.speckle.stream_id
@@ -855,10 +882,8 @@ class SelectOrphanObjects(bpy.types.Operator):
o.select = False o.select = False
metrics.track( metrics.track(
"Connector Action", "Connector Action",
custom_props={ custom_props={"name": "SelectOrphanObjects"},
"name": "SelectOrphanObjects"
},
) )
return {"FINISHED"} return {"FINISHED"}
+39 -30
View File
@@ -1,17 +1,23 @@
""" """
User account operators User account operators
""" """
from typing import List, cast from typing import cast
import bpy import bpy
from bpy.types import Context 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.client import SpeckleClient
from specklepy.core.api.credentials import Account, get_local_accounts
from specklepy.core.api.models import Stream from specklepy.core.api.models import Stream
from specklepy.core.api.credentials import get_local_accounts, Account
from specklepy.logging import metrics 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): class ResetUsers(bpy.types.Operator):
""" """
Reset loaded users Reset loaded users
@@ -26,10 +32,8 @@ class ResetUsers(bpy.types.Operator):
metrics.track( metrics.track(
"Connector Action", "Connector Action",
None, None,
custom_props={ custom_props={"name": "ResetUsers"},
"name": "ResetUsers"
},
) )
bpy.context.view_layer.update() bpy.context.view_layer.update()
@@ -44,6 +48,7 @@ class ResetUsers(bpy.types.Operator):
speckle.users.clear() speckle.users.clear()
speckle_clients.clear() speckle_clients.clear()
class LoadUsers(bpy.types.Operator): class LoadUsers(bpy.types.Operator):
""" """
Loads all user accounts from the credentials in the local database. 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" bl_description = "Loads all user accounts from the credentials in the local database.\nSee docs to add accounts via Manager"
def execute(self, context): def execute(self, context):
_report("Loading users...") _report("Loading users...")
speckle = get_speckle(context) speckle = get_speckle(context)
@@ -69,20 +73,24 @@ class LoadUsers(bpy.types.Operator):
metrics.track( metrics.track(
"Connector Action", "Connector Action",
None, None,
custom_props={ custom_props={
"name": "LoadUsers", "name": "LoadUsers",
}, },
) )
if not profiles: 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: for profile in profiles:
try: try:
add_user_account(profile, speckle) add_user_account(profile, speckle)
except Exception as ex: 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) users_list.remove(len(users_list) - 1)
continue continue
@@ -100,11 +108,16 @@ class LoadUsers(bpy.types.Operator):
context.area.tag_redraw() context.area.tag_redraw()
if not users_list: 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"} 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""" """Creates a new new SpeckleUserObject for the provided user Account and adds it to the SpeckleSceneSettings"""
users_list = speckle.users users_list = speckle.users
@@ -118,7 +131,7 @@ def add_user_account(account: Account, speckle: SpeckleSceneSettings) -> Speckle
user.email = account.userInfo.email user.email = account.userInfo.email
user.company = account.userInfo.company or "" user.company = account.userInfo.company or ""
assert(URL) assert URL
client = SpeckleClient( client = SpeckleClient(
host=URL, host=URL,
use_ssl="https" in URL, use_ssl="https" in URL,
@@ -136,7 +149,7 @@ def add_user_stream(user: SpeckleUserObject, stream: Stream):
s.description = stream.description s.description = stream.description
_report(f"Adding stream {s.id} - {s.name}") _report(f"Adding stream {s.id} - {s.name}")
if stream.branches: if stream.branches:
s.load_stream_branches(stream) s.load_stream_branches(stream)
@@ -159,7 +172,6 @@ class LoadUserStreams(bpy.types.Operator):
self.load_user_stream(context) self.load_user_stream(context)
return {"FINISHED"} return {"FINISHED"}
def load_user_stream(self, context: Context) -> None: def load_user_stream(self, context: Context) -> None:
speckle = get_speckle(context) speckle = get_speckle(context)
@@ -169,13 +181,12 @@ class LoadUserStreams(bpy.types.Operator):
try: try:
streams = client.stream.list(stream_limit=self.stream_limit) streams = client.stream.list(stream_limit=self.stream_limit)
except Exception as ex: except Exception as ex:
raise Exception(f"Failed to retrieve projects") from ex raise Exception("Failed to retrieve projects") from ex
if not streams: if not streams:
_report("Zero projects found") _report("Zero projects found")
return return
active_stream_id = None active_stream_id = None
if active_stream := user.get_active_stream(): if active_stream := user.get_active_stream():
active_stream_id = active_stream.id active_stream_id = active_stream.id
@@ -185,10 +196,12 @@ class LoadUserStreams(bpy.types.Operator):
user.streams.clear() user.streams.clear()
for i, s in enumerate(streams): 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 load_branches = s.id == active_stream_id if active_stream_id else i == 0
if load_branches: 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) add_user_stream(user, sstream)
else: else:
add_user_stream(user, s) add_user_stream(user, s)
@@ -199,13 +212,9 @@ class LoadUserStreams(bpy.types.Operator):
if context.area: if context.area:
context.area.tag_redraw() context.area.tag_redraw()
metrics.track( metrics.track(
"Connector Action", "Connector Action",
client.account, client.account,
custom_props={ custom_props={"name": "LoadUserStreams"},
"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 .addon import SpeckleAddonPreferences
from .collection import SpeckleCollectionSettings
from .object import SpeckleObjectSettings
from .scene import (SpeckleBranchObject, SpeckleCommitObject,
SpeckleSceneObject, SpeckleSceneSettings,
SpeckleStreamObject, SpeckleUserObject)
property_classes = [ property_classes = [
SpeckleSceneObject, SpeckleSceneObject,
+4 -4
View File
@@ -5,7 +5,7 @@ import bpy
class SpeckleCollectionSettings(bpy.types.PropertyGroup): 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( send_or_receive: bpy.props.EnumProperty(
name="Mode", name="Mode",
@@ -13,6 +13,6 @@ class SpeckleCollectionSettings(bpy.types.PropertyGroup):
("send", "Send", "Send data to Speckle server."), ("send", "Send", "Send data to Speckle server."),
("receive", "Receive", "Receive data from Speckle server."), ("receive", "Receive", "Receive data from Speckle server."),
), ),
) # type: ignore ) # type: ignore
stream_id: bpy.props.StringProperty(default="") # type: ignore stream_id: bpy.props.StringProperty(default="") # type: ignore
name: 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."), ("send", "Send", "Send data to Speckle server."),
("receive", "Receive", "Receive data from Speckle server."), ("receive", "Receive", "Receive data from Speckle server."),
), ),
) # type: ignore ) # type: ignore
stream_id: bpy.props.StringProperty(default="") # type: ignore stream_id: bpy.props.StringProperty(default="") # type: ignore
object_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 Scene properties
""" """
from typing import Iterable, Optional, Tuple, Union, cast
from dataclasses import dataclass from dataclasses import dataclass
import bpy from typing import Iterable, Optional, Tuple, Union, cast
from bpy.props import (
StringProperty,
FloatProperty,
CollectionProperty,
EnumProperty,
IntProperty,
)
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 specklepy.core.api.models import Stream
from bpy_speckle.clients import speckle_clients
class SpeckleSceneObject(bpy.types.PropertyGroup): class SpeckleSceneObject(bpy.types.PropertyGroup):
name: bpy.props.StringProperty(default="") # type: ignore name: bpy.props.StringProperty(default="") # type: ignore
class SpeckleCommitObject(bpy.types.PropertyGroup): class SpeckleCommitObject(bpy.types.PropertyGroup):
id: StringProperty(default="") # type: ignore id: StringProperty(default="") # type: ignore
message: StringProperty(default="") # type: ignore message: StringProperty(default="") # type: ignore
author_name: StringProperty(default="") # type: ignore author_name: StringProperty(default="") # type: ignore
author_id: StringProperty(default="") # type: ignore author_id: StringProperty(default="") # type: ignore
created_at: StringProperty(default="") # type: ignore created_at: StringProperty(default="") # type: ignore
source_application: StringProperty(default="") # type: ignore source_application: StringProperty(default="") # type: ignore
referenced_object: StringProperty(default="") # type: ignore referenced_object: StringProperty(default="") # type: ignore
class SpeckleBranchObject(bpy.types.PropertyGroup): class SpeckleBranchObject(bpy.types.PropertyGroup):
def get_commits(self, context): 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) COMMITS = cast(Iterable[SpeckleCommitObject], self.commits)
return [ return [
(str(i), commit.id, commit.message, i) (str(i), commit.id, commit.message, i)
for i, commit in enumerate(COMMITS) for i, commit in enumerate(COMMITS)
] ]
return [("0", "<none>", "<none>", 0)] return [("0", "<none>", "<none>", 0)]
def commit_update_hook(self, context: bpy.types.Context): 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 selection_state.selected_branch_id = self.id
# print(f"commit_update_hook: {selection_state.selected_commit_id=}, {selection_state.selected_branch_id=}") # print(f"commit_update_hook: {selection_state.selected_commit_id=}, {selection_state.selected_branch_id=}")
name: StringProperty(default="main") # type: ignore name: StringProperty(default="main") # type: ignore
id: StringProperty(default="") # type: ignore id: StringProperty(default="") # type: ignore
description: StringProperty(default="") # type: ignore description: StringProperty(default="") # type: ignore
commits: CollectionProperty(type=SpeckleCommitObject) # type: ignore commits: CollectionProperty(type=SpeckleCommitObject) # type: ignore
commit: EnumProperty( commit: EnumProperty(
name="Version", name="Version",
description="Selected model version", description="Selected model version",
items=get_commits, items=get_commits,
update=commit_update_hook, update=commit_update_hook,
) # type: ignore ) # type: ignore
def get_active_commit(self) -> Optional[SpeckleCommitObject]: def get_active_commit(self) -> Optional[SpeckleCommitObject]:
selected_index = int(self.commit) 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 self.commits[selected_index]
return None return None
class SpeckleStreamObject(bpy.types.PropertyGroup): class SpeckleStreamObject(bpy.types.PropertyGroup):
def load_stream_branches(self, sstream: Stream): def load_stream_branches(self, sstream: Stream):
self.branches.clear() self.branches.clear()
@@ -80,7 +80,11 @@ class SpeckleStreamObject(bpy.types.PropertyGroup):
commit.message = c.message or "" commit.message = c.message or ""
commit.author_name = c.authorName commit.author_name = c.authorName
commit.author_id = c.authorId 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.source_application = str(c.sourceApplication)
commit.referenced_object = c.referencedObject commit.referenced_object = c.referencedObject
@@ -93,57 +97,66 @@ class SpeckleStreamObject(bpy.types.PropertyGroup):
if branch.name != "globals" if branch.name != "globals"
] ]
return [("0", "<none>", "<none>", 0)] return [("0", "<none>", "<none>", 0)]
def branch_update_hook(self, context: bpy.types.Context): 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=}") # print(f"branch_update_hook: {selection_state.selected_branch_id=}, {selection_state.selected_stream_id=}")
name: StringProperty(default="") # type: ignore name: StringProperty(default="") # type: ignore
description: StringProperty(default="") # type: ignore description: StringProperty(default="") # type: ignore
id: StringProperty(default="") # type: ignore id: StringProperty(default="") # type: ignore
branches: CollectionProperty(type=SpeckleBranchObject) # type: ignore branches: CollectionProperty(type=SpeckleBranchObject) # type: ignore
branch: EnumProperty( branch: EnumProperty(
name="Model", name="Model",
description="Selected Model", description="Selected Model",
items=get_branches, items=get_branches,
update=branch_update_hook, update=branch_update_hook,
) # type: ignore ) # type: ignore
def get_active_branch(self) -> Optional[SpeckleBranchObject]: def get_active_branch(self) -> Optional[SpeckleBranchObject]:
selected_index = int(self.branch) 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 self.branches[selected_index]
return None return None
class SpeckleUserObject(bpy.types.PropertyGroup): 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 speckle = context.scene.speckle
client = speckle_clients[int(speckle.active_user)] 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) stream.load_stream_branches(sstream)
def stream_update_hook(self, context: bpy.types.Context): def stream_update_hook(self, context: bpy.types.Context):
stream = SelectionState.get_item_by_index(self.streams, self.active_stream) stream = SelectionState.get_item_by_index(self.streams, self.active_stream)
selection_state.selected_stream_id = stream.id selection_state.selected_stream_id = stream.id
# print(f"stream_update_hook: {selection_state.selected_stream_id=}, {selection_state.selected_user_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) self.fetch_stream_branches(context, stream)
server_name: StringProperty(default="SpeckleXYZ") # type: ignore server_name: StringProperty(default="SpeckleXYZ") # type: ignore
server_url: StringProperty(default="https://speckle.xyz") # type: ignore server_url: StringProperty(default="https://speckle.xyz") # type: ignore
id: StringProperty(default="") # type: ignore id: StringProperty(default="") # type: ignore
name: StringProperty(default="Speckle User") # type: ignore name: StringProperty(default="Speckle User") # type: ignore
email: StringProperty(default="user@speckle.xyz") # type: ignore email: StringProperty(default="user@speckle.xyz") # type: ignore
company: StringProperty(default="SpeckleSystems") # type: ignore company: StringProperty(default="SpeckleSystems") # type: ignore
streams: CollectionProperty(type=SpeckleStreamObject) # type: ignore streams: CollectionProperty(type=SpeckleStreamObject) # type: ignore
active_stream: IntProperty(default=0, update=stream_update_hook) # type: ignore active_stream: IntProperty(default=0, update=stream_update_hook) # type: ignore
def get_active_stream(self) -> Optional[SpeckleStreamObject]: def get_active_stream(self) -> Optional[SpeckleStreamObject]:
selected_index = int(self.active_stream) 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 self.streams[selected_index]
return None return None
class SpeckleSceneSettings(bpy.types.PropertyGroup): class SpeckleSceneSettings(bpy.types.PropertyGroup):
def get_scripts(self, context): def get_scripts(self, context):
@@ -156,9 +169,9 @@ class SpeckleSceneSettings(bpy.types.PropertyGroup):
name="Available streams", name="Available streams",
description="Available streams associated with user.", description="Available streams associated with user.",
items=[], items=[],
) # type: ignore ) # type: ignore
users: CollectionProperty(type=SpeckleUserObject) # type: ignore users: CollectionProperty(type=SpeckleUserObject) # type: ignore
def get_users(self, context): def get_users(self, context):
USERS = cast(Iterable[SpeckleUserObject], self.users) USERS = cast(Iterable[SpeckleUserObject], self.users)
@@ -168,8 +181,10 @@ class SpeckleSceneSettings(bpy.types.PropertyGroup):
] ]
def user_update_hook(self, context): def user_update_hook(self, context):
bpy.ops.speckle.load_user_streams() # type: ignore bpy.ops.speckle.load_user_streams() # type: ignore
selection_state.selected_user_id = SelectionState.get_item_id_by_index(self.users, self.active_user) selection_state.selected_user_id = SelectionState.get_item_id_by_index(
self.users, self.active_user
)
active_user: EnumProperty( active_user: EnumProperty(
items=get_users, items=get_users,
@@ -178,29 +193,29 @@ class SpeckleSceneSettings(bpy.types.PropertyGroup):
update=user_update_hook, update=user_update_hook,
get=None, get=None,
set=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( user: StringProperty(
name="User", name="User",
description="Current user", description="Current user",
default="Speckle User", default="Speckle User",
) # type: ignore ) # type: ignore
receive_script: EnumProperty( receive_script: EnumProperty(
name="Receive script", name="Receive script",
description="Custom py script to execute when receiving objects. See docs for function signature.", description="Custom py script to execute when receiving objects. See docs for function signature.",
items=get_scripts, items=get_scripts,
) # type: ignore ) # type: ignore
send_script: EnumProperty( send_script: EnumProperty(
name="Send script", name="Send script",
description="Custom py script to execute when sending objects. See docs for function signature", description="Custom py script to execute when sending objects. See docs for function signature",
items=get_scripts, items=get_scripts,
) # type: ignore ) # type: ignore
def get_active_user(self) -> Optional[SpeckleUserObject]: def get_active_user(self) -> Optional[SpeckleUserObject]:
if self.active_user is None: if self.active_user is None:
@@ -209,15 +224,16 @@ class SpeckleSceneSettings(bpy.types.PropertyGroup):
if 0 <= selected_index < len(self.users): if 0 <= selected_index < len(self.users):
return self.users[selected_index] return self.users[selected_index]
return None return None
def validate_user_selection(self) -> SpeckleUserObject: def validate_user_selection(self) -> SpeckleUserObject:
user = self.get_active_user() user = self.get_active_user()
if not user: if not user:
raise SelectionException("No user account selected/found") raise SelectionException("No user account selected/found")
return user return user
def validate_stream_selection(self) -> Tuple[SpeckleUserObject, SpeckleStreamObject]: def validate_stream_selection(
self,
) -> Tuple[SpeckleUserObject, SpeckleStreamObject]:
user = self.validate_user_selection() user = self.validate_user_selection()
stream = user.get_active_stream() stream = user.get_active_stream()
@@ -225,80 +241,109 @@ class SpeckleSceneSettings(bpy.types.PropertyGroup):
raise SelectionException("No project selected/found") raise SelectionException("No project selected/found")
return (user, stream) 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() (user, stream) = self.validate_stream_selection()
branch = stream.get_active_branch() branch = stream.get_active_branch()
if not branch: if not branch:
raise SelectionException("No model selected/found") raise SelectionException("No model selected/found")
return (user, stream, branch) 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() (user, stream, branch) = self.validate_branch_selection()
commit = branch.get_active_commit() commit = branch.get_active_commit()
if commit is None: if commit is None:
raise SelectionException("No model version selected/found") raise SelectionException("No model version selected/found")
return (user, stream, branch, commit) return (user, stream, branch, commit)
class SelectionException(Exception): class SelectionException(Exception):
pass pass
def get_speckle(context: bpy.types.Context) -> SpeckleSceneSettings: def get_speckle(context: bpy.types.Context) -> SpeckleSceneSettings:
""" """
Gets the speckle scene object Gets the speckle scene object
""" """
return context.scene.speckle #type: ignore return context.scene.speckle # type: ignore
@dataclass @dataclass
class SelectionState: class SelectionState:
selected_user_id : Optional[str] = None selected_user_id: Optional[str] = None
selected_stream_id : Optional[str] = None selected_stream_id: Optional[str] = None
selected_branch_id : Optional[str] = None selected_branch_id: Optional[str] = None
selected_commit_id : Optional[str] = None selected_commit_id: Optional[str] = None
@staticmethod @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): if item := SelectionState.get_item_by_index(collection, index):
return item.id return item.id
return None return None
@staticmethod @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() items = collection.values()
i = int(index) i = int(index)
if 0 <= i <= len(items): if 0 <= i <= len(items):
return items[i] return items[i]
return None return None
@staticmethod @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): for index, item in enumerate(collection):
if item.id == id: if item.id == id:
return str(index) return str(index)
return None return None
selection_state = SelectionState() selection_state = SelectionState()
def restore_selection_state(speckle: SpeckleSceneSettings) -> None: def restore_selection_state(speckle: SpeckleSceneSettings) -> None:
# Restore branch selection state # 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() (active_user, active_stream) = speckle.validate_stream_selection()
# print(f"restore_selection_state: {active_user.id=}, {active_stream.id=}") # 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=}") # print(f"restore_selection_state: {selection_state.selected_user_id=}, {selection_state.selected_stream_id=}, {selection_state.selected_branch_id=}, {selection_state.selected_commit_id=}")
is_same_user = active_user.id == selection_state.selected_user_id is_same_user = active_user.id == selection_state.selected_user_id
if is_same_user: if is_same_user:
active_user.active_stream = int(SelectionState.get_item_index_by_id(active_user.streams, selection_state.selected_stream_id)) active_user.active_stream = int(
active_stream = SelectionState.get_item_by_index(active_user.streams, active_user.active_stream) SelectionState.get_item_index_by_id(
if branch := SelectionState.get_item_index_by_id(active_stream.branches, selection_state.selected_branch_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 active_stream.branch = branch
# Restore commit selection state # Restore commit selection state
if selection_state.selected_commit_id != None: if selection_state.selected_commit_id is not None:
(active_user, active_stream, active_branch) = speckle.validate_branch_selection() (
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: {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=}") # 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 is_same_branch = active_branch.id == selection_state.selected_branch_id
if is_same_user and is_same_stream and is_same_branch: 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): if commit := SelectionState.get_item_index_by_id(
active_branch.commit = commit 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 .object import OBJECT_PT_speckle
from .view3d import ( from .view3d import (VIEW3D_PT_SpeckleActiveStream, VIEW3D_PT_SpeckleHelp,
VIEW3D_UL_SpeckleUsers, VIEW3D_PT_SpeckleStreams, VIEW3D_PT_SpeckleUser,
VIEW3D_UL_SpeckleStreams, VIEW3D_UL_SpeckleStreams, VIEW3D_UL_SpeckleUsers)
VIEW3D_PT_SpeckleUser,
VIEW3D_PT_SpeckleStreams,
VIEW3D_PT_SpeckleActiveStream,
VIEW3D_PT_SpeckleHelp,
)
ui_classes = [ ui_classes = [
VIEW3D_PT_SpeckleUser, VIEW3D_PT_SpeckleUser,
+1 -7
View File
@@ -3,15 +3,9 @@ Object UI elements
""" """
import bpy import bpy
from bpy.props import (
StringProperty,
BoolProperty,
FloatProperty,
CollectionProperty,
EnumProperty,
)
from deprecated import deprecated from deprecated import deprecated
@deprecated @deprecated
class OBJECT_PT_speckle(bpy.types.Panel): class OBJECT_PT_speckle(bpy.types.Panel):
bl_space_type = "PROPERTIES" 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 from datetime import datetime
import bpy
from bpy_speckle.properties.scene import get_speckle from bpy_speckle.properties.scene import get_speckle
Region = "TOOLS" if bpy.app.version < (2, 80, 0) else "UI" 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") col.operator("speckle.users_load", text="", icon="FILE_REFRESH")
class VIEW3D_PT_SpeckleStreams(bpy.types.Panel): class VIEW3D_PT_SpeckleStreams(bpy.types.Panel):
""" """
Speckle projects UI panel in the 3d viewport Speckle projects UI panel in the 3d viewport
@@ -161,7 +162,7 @@ class VIEW3D_PT_SpeckleActiveStream(bpy.types.Panel):
col.label(text="No projects") col.label(text="No projects")
else: else:
user = speckle.validate_user_selection() user = speckle.validate_user_selection()
#user = speckle.users[int(speckle.active_user)] # user = speckle.users[int(speckle.active_user)]
if len(user.streams) < 1: if len(user.streams) < 1:
col.label(text="No active project") col.label(text="No active project")
else: 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: This is taken from the import_3dm add-on:
https://github.com/jesterKing/import_3dm https://github.com/jesterKing/import_3dm
# Copyright (c) 2018-2019 Nathan Letwory, Joel Putnam, # Copyright (c) 2018-2019 Nathan Letwory, Joel Putnam,
Tom Svilans Tom Svilans
Get an iddata. If an object with given uuid is found in 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""" """Patches the installer with the correct connector version and specklepy version"""
tag = tag.replace("\n", "") tag = tag.replace("\n", "")
iss_file = "speckle-sharp-ci-tools/blender.iss" 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 = iss_path.read_text().split("\n")
lines.insert(12, f'#define AppVersion "{tag.split("-")[0]}"') lines.insert(12, f'#define AppVersion "{tag.split("-")[0]}"')
lines.insert(13, f'#define AppInfoVersion "{tag}"') lines.insert(13, f'#define AppInfoVersion "{tag}"')
@@ -17,4 +17,4 @@ def patch_installer(tag: str):
if __name__ == "__main__": if __name__ == "__main__":
tag = sys.argv[1] tag = sys.argv[1]
patch_installer(tag) patch_installer(tag)
+3 -1
View File
@@ -1,6 +1,7 @@
import re import re
import sys import sys
def patch_connector(tag): def patch_connector(tag):
"""Patches the connector version within the connector init file""" """Patches the connector version within the connector init file"""
bpy_file = "bpy_speckle/__init__.py" bpy_file = "bpy_speckle/__init__.py"
@@ -9,7 +10,7 @@ def patch_connector(tag):
with open(bpy_file, "r") as file: with open(bpy_file, "r") as file:
lines = file.readlines() lines = file.readlines()
for (index, line) in enumerate(lines): for index, line in enumerate(lines):
if '"version":' in line: if '"version":' in line:
lines[index] = f' "version": ({tag[0]}, {tag[1]}, {tag[2]}),\n' lines[index] = f' "version": ({tag[0]}, {tag[1]}, {tag[2]}),\n'
print(f"Patched connector version number in {bpy_file}") print(f"Patched connector version number in {bpy_file}")
@@ -18,6 +19,7 @@ def patch_connector(tag):
with open(bpy_file, "w") as file: with open(bpy_file, "w") as file:
file.writelines(lines) file.writelines(lines)
def main(): def main():
tag = sys.argv[1] tag = sys.argv[1]
if not re.match(r"([0-9]+)\.([0-9]+)\.([0-9]+)", tag): 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] [tool.poetry.dependencies]
python = ">=3.8, <4.0.0" python = ">=3.8, <4.0.0"
specklepy = "^2.19.1" specklepy = "^2.20.0"
attrs = "^23.1.0" attrs = "^23.1.0"
# [tool.poetry.group.local_specklepy.dependencies] # [tool.poetry.group.local_specklepy.dependencies]
@@ -15,7 +15,7 @@ attrs = "^23.1.0"
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]
fake-bpy-module-latest = "^20240524" fake-bpy-module-latest = "^20240524"
black = "23.11.0" black = "24.3.0"
pylint = "^2.15.7" pylint = "^2.15.7"
ruff = "^0.4.4" ruff = "^0.4.4"