Compare commits

...

24 Commits

Author SHA1 Message Date
Jedd Morgan a3d4881578 Merge pull request #175 from specklesystems/jrm/properties/depth
fix(custom_properties): Set max depth of properties to 64 to align with newtonsoft limit
2023-10-08 18:45:50 +01:00
JR-Morgan 1af158a5e0 poetry lock 2023-10-08 18:44:18 +01:00
JR-Morgan 47857a9db0 Aligned json depth cap with newtonsofts reader at 64 levels of depth 2023-10-08 18:40:43 +01:00
Jedd Morgan 3b026e6027 Merge pull request #173 from specklesystems/jrm/fix-py-indent-error
Update users.py
2023-09-22 13:03:32 +01:00
Jedd Morgan d572609f75 Update users.py 2023-09-22 13:03:06 +01:00
Jedd Morgan 37032cc7aa Merge pull request #172 from specklesystems/jrm/deps/specklepy216
better error message when user has no valid accounts
2023-09-12 12:35:03 +01:00
Jedd Morgan 6027325878 better error message when user has no valid accounts 2023-09-12 11:59:39 +01:00
Jedd Morgan 5ddb2aa052 Merge pull request #171 from specklesystems/jrm/deps/specklepy216
fix(receive)!: Fixed issue with collection name conflicts
2023-09-12 10:50:15 +01:00
Jedd Morgan 67a18821cc fix(receive)!: Collections will no longer update 2023-09-12 10:36:22 +01:00
Jedd Morgan 2688a69286 Merge pull request #169 from specklesystems/jrm/deps/specklepy216
Jrm/deps/specklepy216
2023-09-08 16:04:08 +01:00
Jedd Morgan 56216a6137 bump specklepy 2023-09-08 15:39:25 +01:00
Jedd Morgan 319cbf8960 Specklepy 2.16 2023-09-08 13:41:14 +01:00
Jedd Morgan d7ac6c0b95 Updated to new core metrics 2023-09-04 19:51:39 +01:00
Jedd Morgan 201ca5f26e Update blender_commit_object_builder.py 2023-07-22 01:02:23 +01:00
Jedd Morgan 89528437b1 Merge pull request #167 from specklesystems/vertex-color-uint
Fixed issue with uint32 vertex colours not parsing
2023-07-17 21:51:18 +01:00
Jedd Morgan 91bde24fe9 Fixed issue with uint32 vertex colors not parsing 2023-07-17 21:21:28 +01:00
Jedd Morgan 991b0f9ff1 Merge pull request #166 from specklesystems/2.15-deps-update
Fixed issue with hosted elements on nested instances not receiving or receiving twice
2023-07-06 15:20:58 +01:00
Jedd Morgan ee1715ff8a fixed circular import 2023-07-06 15:12:34 +01:00
Jedd Morgan 70ee09b9bb Fixed issue with non-convertable revit definitions 2023-07-06 13:57:44 +01:00
Jedd Morgan 83dd62d03f deps update 2023-07-06 13:16:50 +01:00
Jedd Morgan 94cc0ac3f7 fix(instance): Fixed issues with hosted and nested instances 2023-07-06 13:14:20 +01:00
Jedd Morgan 36cb94d3d7 fix(converter): ToSpeckle instances 2023-07-03 16:59:53 +01:00
Jedd Morgan c60baf78c5 deps: updated to specklepy 2.15.0 2023-06-27 16:09:04 +01:00
Jedd Morgan d72cfd3522 feat(view): Added support for receiving/sending view objects 2023-05-31 01:12:17 +01:00
19 changed files with 1444 additions and 1482 deletions
+8 -38
View File
@@ -1,9 +1,9 @@
from typing import Deque, Dict, List, Optional, Set, Tuple, Union
from typing import Dict, Optional, Tuple, Union
import bpy
from bpy.types import Object, Collection, ID
from specklepy.objects.base import Base
from bpy_speckle.functions import _report
from bpy_speckle.specklepy_extras.commit_object_builder import CommitObjectBuilder, ROOT
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
@@ -20,6 +20,11 @@ def _try_id(natvive_object: Optional[Union[Collection, Object]]) -> Optional[str
def convert_collection_to_speckle(col: Collection) -> SCollection:
convered_collection = SCollection(name = col.name_full, collectionType = "Blender Collection", elements = [])
convered_collection.applicationId = _id(col)
color_tag = col.color_tag
if color_tag and color_tag != "NONE":
convered_collection["colorTag"] = col.color_tag
return convered_collection
@define(slots=True)
@@ -68,27 +73,13 @@ class BlenderCommitObjectBuilder(CommitObjectBuilder[Object]):
return convered_collection
# def find_collection_parent(self, col: Collection) -> Optional[Collection]:
# for p in bpy.data.collections:
# if col.name in p.children.keys():
# return p
# return None
#TODO: I've started an approach that will not work
# Goal #1 get all collections sending
# Sync with Claire, ask how we handle this in Rhino with partial selection of layers (proably how I'm expecting it works, but good to double check)
# Goal #2 Figure out how to send collections
# - all collections
# - all collections that contain a child collection that has geometry...
# - only collections explicitly selected
def build_commit_object(self, root_commit_object: Base) -> None:
assert(root_commit_object.applicationId in self.converted)
# Create all collections
root_col = self.ensure_collection(bpy.context.scene.collection)
root_col.collectionType = "Scene Collection"
for col in bpy.context.scene.collection.children_recursive:
for col in bpy.context.scene.collection.children_recursive: #type: ignore
self.ensure_collection(col)
objects_to_build = set(self.converted.values())
@@ -127,24 +118,3 @@ class BlenderCommitObjectBuilder(CommitObjectBuilder[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
# def build_commit_object(self, root_commit_object: Base) -> None:
# convertedObjects = [x for x in self.converted.values()] # Converted objects, but no collections!
# for (id, col) in self._collections.items():
# self.converted[id] = col
# # Apply relationships for all non-collection objects
# self.apply_relationships(convertedObjects, root_commit_object)
# # Remove empty collections
# for (id, col) in self._collections.items(): #TODO: XXX: How to ensure empty collections are avoided! Potentially need to traverse from root object down...
# if not col.elements:
# self.converted.pop(id)
# self.apply_relationships(convertedObjects, root_commit_object)
# return
+1 -1
View File
@@ -1,7 +1,7 @@
"""
Permanent handle on all user clients
"""
from specklepy.api.client import SpeckleClient
from specklepy.core.api.client import SpeckleClient
speckle_clients: list[SpeckleClient] = []
-29
View File
@@ -1,29 +0,0 @@
from typing import Union
from bpy_speckle.convert.to_native import convert_to_native
from specklepy.objects.base import Base
def get_speckle_subobjects(attr: Union[dict, Base], scale: float, name: str) -> list:
subobjects = []
keys = attr.keys() if isinstance(attr, dict) else attr.get_dynamic_member_names()
for key in keys:
if isinstance(attr[key], dict):
subtype = attr[key].get("type", None)
if subtype:
name = f"{name}.{key}"
subobject = convert_to_native(attr[key], name)
subobjects.append(subobject)
props = attr[key].get("properties", None)
if props:
subobjects.extend(get_speckle_subobjects(props, scale, name))
elif hasattr(attr[key], "type"):
subtype = attr[key].type
if subtype:
name = "{}.{}".format(name, key)
subobject = convert_to_native(attr[key], name)
subobjects.append(subobject)
props = attr[key].get("properties", None)
if props:
subobjects.extend(get_speckle_subobjects(props, scale, name))
return subobjects
+22
View File
@@ -0,0 +1,22 @@
IGNORED_PROPERTY_KEYS = {
"id",
"elements",
"displayMesh",
"displayValue",
"speckle_type",
"parameters",
"faces",
"colors",
"vertices",
"renderMaterial",
"textureCoordinates",
"totalChildrenCount"
}
DISPLAY_VALUE_PROPERTY_ALIASES = {"displayValue", "@displayValue"}
ELEMENTS_PROPERTY_ALIASES = {"elements", "@elements"}
OBJECT_NAME_MAX_LENGTH = 62
SPECKLE_ID_LENGTH = 32
OBJECT_NAME_SPECKLE_SEPARATOR = " -- "
OBJECT_NAME_NUMERAL_SEPARATOR = '.'
+242 -121
View File
@@ -1,6 +1,8 @@
import math
from typing import Tuple, Union, Collection
from bpy_speckle.functions import get_scale_length, _report
from typing import Any, Dict, Iterable, List, Optional, Sequence, Union, Collection, cast
from bpy_speckle.convert.constants import DISPLAY_VALUE_PROPERTY_ALIASES, ELEMENTS_PROPERTY_ALIASES, OBJECT_NAME_MAX_LENGTH, OBJECT_NAME_NUMERAL_SEPARATOR, OBJECT_NAME_SPECKLE_SEPARATOR, SPECKLE_ID_LENGTH
from bpy_speckle.functions import get_default_traversal_func, get_scale_length, _report
from bpy_speckle.convert.util import ConversionSkippedException
from mathutils import (
Matrix as MMatrix,
Vector as MVector,
@@ -8,15 +10,19 @@ from mathutils import (
)
import bpy, bmesh
from specklepy.objects.other import (
Collection as SCollection,
Instance,
Transform,
BlockDefinition,
)
from specklepy.objects.geometry import *
from bpy.types import Object
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 (
add_to_hierarchy,
get_render_material,
link_object_to_collection_nested,
get_vertex_color_material,
render_material_to_native,
add_custom_properties,
add_vertices,
@@ -34,35 +40,18 @@ CAN_CONVERT_TO_NATIVE = (
)
def _has_native_convesion(speckle_object: Base) -> bool:
return any(isinstance(speckle_object, t) for t in CAN_CONVERT_TO_NATIVE) #or "View" in speckle_object.speckle_type #hack
def _has_native_conversion(speckle_object: Base) -> bool:
return any(isinstance(speckle_object, t) for t in CAN_CONVERT_TO_NATIVE) or "View" in speckle_object.speckle_type #hack
def _has_fallback_conversion(speckle_object: Base) -> bool:
return any(getattr(speckle_object, alias, None) for alias in DISPLAY_VALUE_PROPERTY_ALIASES)
def can_convert_to_native(speckle_object: Base) -> bool:
if(_has_native_convesion(speckle_object) or _has_fallback_conversion(speckle_object)):
if(_has_native_conversion(speckle_object) or _has_fallback_conversion(speckle_object)):
return True
return False
def create_new_object(obj_data: Optional[bpy.types.ID], desired_name: str, counter: int = 0) -> bpy.types.Object:
"""
Creates a new blender object with a unique name,
if the desired_name is already taken
we'll append a number, with the format .xxx to the desired_name to ensure the name is unique.
"""
name = desired_name if counter == 0 else f"{desired_name[:OBJECT_NAME_MAX_LENGTH - 4]}.{counter:03d}" # format counter as name.xxx, truncate to ensure we don't exceed the object name max length
#TODO: This is very slow, and gets slower the more objects you receive with the same name...
# We could use a binary/galloping search, and/or cache the name -> index within a receive.
if name in bpy.data.objects.keys():
#Object already exists, increment counter and try again!
return create_new_object(obj_data, desired_name, counter + 1)
blender_object = bpy.data.objects.new(name, obj_data)
return blender_object
convert_instances_as: str #HACK: This is hacky, we need a better way to pass settings down to the converter
def set_convert_instances_as(value: str):
global convert_instances_as
@@ -81,7 +70,7 @@ def convert_to_native(speckle_object: Base) -> Object:
children: list[Object] = []
# convert elements/breps
if not _has_native_convesion(speckle_object):
if not _has_native_conversion(speckle_object):
(converted, children) = display_value_to_native(speckle_object, object_name, scale)
if not converted and not children:
raise Exception(f"Zero geometry converted from displayValues for {speckle_object}")
@@ -91,11 +80,11 @@ def convert_to_native(speckle_object: Base) -> Object:
converted = mesh_to_native(speckle_object, object_name, scale)
elif speckle_type in SUPPORTED_CURVES:
converted = icurve_to_native(speckle_object, object_name, scale)
# elif "View" in speckle_object.speckle_type:
# return view_to_native(speckle_object, object_name, scale)
elif "View" in speckle_object.speckle_type:
return view_to_native(speckle_object, object_name, scale)
elif isinstance(speckle_object, Instance):
if convert_instances_as == "linked_duplicates":
(converted, children) = instance_to_native_object(speckle_object, scale)
converted = instance_to_native_object(speckle_object, scale)
elif convert_instances_as == "collection_instance":
converted = instance_to_native_collection_instance(speckle_object, scale)
else:
@@ -108,19 +97,16 @@ def convert_to_native(speckle_object: Base) -> Object:
if not isinstance(converted, Object):
converted = create_new_object(converted, object_name)
converted.speckle.object_id = str(speckle_object.id)
converted.speckle.enabled = True
converted.speckle.object_id = str(speckle_object.id) # type: ignore
converted.speckle.enabled = True # type: ignore
add_custom_properties(speckle_object, converted)
for child in children:
child.parent = converted
for c in children:
c.parent = converted
return converted
DISPLAY_VALUE_PROPERTY_ALIASES = ["displayValue", "@displayValue"]
ELEMENTS_PROPERTY_ALIASES = ["elements", "@elements"]
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)
@@ -129,7 +115,7 @@ def elements_to_native(speckle_object: Base, name: str, scale: float) -> list[bp
(_, elements) = _members_to_native(speckle_object, name, scale, ELEMENTS_PROPERTY_ALIASES, False)
return elements
def _members_to_native(speckle_object: Base, name: str, scale: float, members: List[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
@@ -147,8 +133,8 @@ def _members_to_native(speckle_object: Base, name: str, scale: float, members: L
display = getattr(speckle_object, alias, None)
count = 0
MAX_DEPTH = 255 # some large value, to prevent infinite reccursion
def seperate(value: Any) -> bool:
MAX_DEPTH = 255 # some large value, to prevent infinite recursion
def separate(value: Any) -> bool:
nonlocal meshes, others, count, MAX_DEPTH
if combineMeshes and isinstance(value, Mesh):
@@ -160,11 +146,11 @@ def _members_to_native(speckle_object: Base, name: str, scale: float, members: L
if(count > MAX_DEPTH):
return True
for x in value:
seperate(x)
separate(x)
return False
did_halt = seperate(display)
did_halt = separate(display)
if did_halt:
_report(f"Traversal of {speckle_object.speckle_type} {speckle_object.id} halted after traversal depth exceeds MAX_DEPTH={MAX_DEPTH}. Are there circular references object structure?")
@@ -195,6 +181,9 @@ def view_to_native(speckle_view, name: str, scale: float) -> bpy.types.Object:
native_cam = bpy.data.cameras.new(name=name)
native_cam.lens = 18 # 90° horizontal fov
if not hasattr(speckle_view, "origin"):
raise ConversionSkippedException("2D views not supported")
cam_obj = create_new_object(native_cam, name)
scale_factor = get_scale_factor(speckle_view, scale)
@@ -217,10 +206,11 @@ def view_to_native(speckle_view, name: str, scale: float) -> bpy.types.Object:
def mesh_to_native(speckle_mesh: Mesh, name: str, scale: float) -> bpy.types.Mesh:
return meshes_to_native(speckle_mesh, [speckle_mesh], name, scale)
def meshes_to_native(element: Base, meshes: Collection[Mesh], name: str, scale: float) -> bpy.types.Mesh:
if name in bpy.data.meshes.keys():
return bpy.data.meshes[name]
blender_mesh = bpy.data.meshes.new(name=name)
fallback_material = get_render_material(element)
@@ -237,12 +227,20 @@ def meshes_to_native(element: Base, meshes: Collection[Mesh], name: str, scale:
# Second pass, add face data
offset = 0
for i, mesh in enumerate(meshes):
if not mesh.vertices: continue
add_faces(mesh, bm, offset, i)
render_material = get_render_material(mesh) or fallback_material
if render_material is not None:
native_material = render_material_to_native(render_material)
blender_mesh.materials.append(native_material)
try:
render_material = get_render_material(mesh) or fallback_material
if render_material is not None:
native_material = render_material_to_native(render_material)
blender_mesh.materials.append(native_material)
elif mesh.colors:
native_material = get_vertex_color_material()
blender_mesh.materials.append(native_material)
except Exception as ex:
_report(f"Failed converting render material for {name}: {ex}")
offset += len(mesh.vertices) // 3
@@ -251,8 +249,15 @@ def meshes_to_native(element: Base, meshes: Collection[Mesh], name: str, scale:
# Third pass, add vertex instance data
for mesh in meshes:
add_colors(mesh, bm)
add_uv_coords(mesh, bm)
try:
add_colors(mesh, bm)
except Exception as ex:
_report(f"Skipping converting vertex colors for {name}: {ex}")
try:
add_uv_coords(mesh, bm)
except Exception as ex:
_report(f"Skipping converting uv coordinates for {name}: {ex}")
bm.to_mesh(blender_mesh)
bm.free()
@@ -264,7 +269,7 @@ def meshes_to_native(element: Base, meshes: Collection[Mesh], name: str, scale:
Curves
"""
def line_to_native(speckle_curve: Line, blender_curve: bpy.types.Curve, scale: float) -> list[bpy.types.Spline]:
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")
@@ -287,14 +292,14 @@ def line_to_native(speckle_curve: Line, blender_curve: bpy.types.Curve, scale: f
return [line]
def polyline_to_native(scurve: Polyline, bcurve: bpy.types.Curve, scale: float) -> list[bpy.types.Spline]:
def polyline_to_native(scurve: Polyline, bcurve: bpy.types.Curve, scale: float) -> List[bpy.types.Spline]:
if not (value := scurve.value): return []
N = len(value) // 3
polyline = bcurve.splines.new("POLY")
if hasattr(scurve, "closed"):
polyline.use_cyclic_u = scurve.closed
polyline.use_cyclic_u = scurve.closed or False
polyline.points.add(N - 1)
for i in range(N):
@@ -309,15 +314,17 @@ def polyline_to_native(scurve: Polyline, bcurve: bpy.types.Curve, scale: float)
def nurbs_to_native(scurve: Curve, bcurve: bpy.types.Curve, scale: float) -> list[bpy.types.Spline]:
def nurbs_to_native(scurve: Curve, bcurve: bpy.types.Curve, scale: float) -> List[bpy.types.Spline]:
if not (points := scurve.points): return []
if not scurve.degree: raise Exception("curve is missing degree")
if not scurve.weights: raise Exception("curve is missing weights")
# Closed curves from rhino will have n + degree points. We ignore the extras
num_points = len(points) // 3 - scurve.degree if (scurve.closed) else (
len(points) // 3)
nurbs = bcurve.splines.new("NURBS")
nurbs.use_cyclic_u = scurve.closed
nurbs.use_cyclic_u = scurve.closed or False
nurbs.use_endpoint_u = not scurve.periodic
nurbs.points.add(num_points - 1)
@@ -339,6 +346,9 @@ def nurbs_to_native(scurve: Curve, bcurve: bpy.types.Curve, scale: float) -> lis
def arc_to_native(rcurve: Arc, bcurve: bpy.types.Curve, scale: float) -> Optional[bpy.types.Spline]:
# TODO: improve Blender representation of arc - check autocad test stream
if not rcurve.radius: raise Exception("curve is missing radius")
if not rcurve.startAngle: raise Exception("curve is missing startAngle")
if not rcurve.endAngle: raise Exception("curve is missing endAngle")
plane = rcurve.plane
if not plane:
@@ -350,8 +360,8 @@ def arc_to_native(rcurve: Arc, bcurve: bpy.types.Curve, scale: float) -> Optiona
startAngle = rcurve.startAngle
endAngle = rcurve.endAngle
startQuat = MQuaternion(normal, startAngle)
endQuat = MQuaternion(normal, endAngle)
startQuat = MQuaternion(normal, startAngle) # type: ignore
endQuat = MQuaternion(normal, endAngle) # type: ignore
# Get start and end vectors, centre point, angles, etc.
r1 = MVector([plane.xdir.x, plane.xdir.y, plane.xdir.z])
@@ -376,7 +386,7 @@ def arc_to_native(rcurve: Arc, bcurve: bpy.types.Curve, scale: float) -> Optiona
Ndiv = max(int(math.floor(angle / 0.3)), 2)
step = angle / float(Ndiv)
stepQuat = MQuaternion(normal, step)
stepQuat = MQuaternion(normal, step) # type: ignore
tan = math.tan(step / 2) * radius
arc.points.add(Ndiv + 1)
@@ -403,11 +413,11 @@ def polycurve_to_native(scurve: Polycurve, bcurve: bpy.types.Curve, scale: float
"""
Convert Polycurve object
"""
segments = scurve.segments
if not scurve.segments: raise Exception("curve is missing segments")
curves = []
for seg in segments:
for seg in scurve.segments:
speckle_type = type(seg)
if speckle_type in SUPPORTED_CURVES:
@@ -417,14 +427,20 @@ def polycurve_to_native(scurve: Polycurve, bcurve: bpy.types.Curve, scale: float
return curves
def ellipse_to_native(ellipse: Union[Ellipse, Circle], bcurve: bpy.types.Curve, units_scale: float) -> list[bpy.types.Spline]:
def ellipse_to_native(ellipse: Union[Ellipse, Circle], bcurve: bpy.types.Curve, units_scale: float) -> List[bpy.types.Spline]:
if not ellipse.plane: raise Exception("curve is missing plane")
radX: float
radY: float
if isinstance(ellipse, Ellipse):
if not ellipse.firstRadius: raise Exception("curve is missing firstRadius")
if not ellipse.secondRadius: raise Exception("curve is missing secondRadius")
radX = ellipse.firstRadius * units_scale
radY = ellipse.secondRadius * units_scale
else:
if not ellipse.radius: raise Exception("curve is missing radius")
radX = ellipse.radius * units_scale
radY = ellipse.radius * units_scale
@@ -457,9 +473,9 @@ def ellipse_to_native(ellipse: Union[Ellipse, Circle], bcurve: bpy.types.Curve,
spline.bezier_points.add(len(points) - 1)
for i in range(len(points)):
spline.bezier_points[i].co = transform @ MVector(points[i])
spline.bezier_points[i].handle_left = transform @ MVector(left_handles[i])
spline.bezier_points[i].handle_right = transform @ MVector(right_handles[i])
spline.bezier_points[i].co = transform @ MVector(points[i]) # type: ignore
spline.bezier_points[i].handle_left = transform @ MVector(left_handles[i]) # type: ignore
spline.bezier_points[i].handle_right = transform @ MVector(right_handles[i]) # type: ignore
spline.use_cyclic_u = True
@@ -467,26 +483,28 @@ def ellipse_to_native(ellipse: Union[Ellipse, Circle], bcurve: bpy.types.Curve,
return [spline]
def icurve_to_native_spline(speckle_curve: Base, blender_curve: bpy.types.Curve, scale: float) -> list[bpy.types.Spline]:
def icurve_to_native_spline(speckle_curve: Base, blender_curve: bpy.types.Curve, scale: float) -> List[bpy.types.Spline]:
# polycurves
if isinstance(speckle_curve, Polycurve):
return polycurve_to_native(speckle_curve, blender_curve, scale)
splines: List[bpy.types.Spline]
# single curves
if isinstance(speckle_curve, Line):
spline = line_to_native(speckle_curve, blender_curve, scale)
splines = line_to_native(speckle_curve, blender_curve, scale)
elif isinstance(speckle_curve, Curve):
spline = nurbs_to_native(speckle_curve, blender_curve, scale)
splines = nurbs_to_native(speckle_curve, blender_curve, scale)
elif isinstance(speckle_curve, Polyline):
spline = polyline_to_native(speckle_curve, blender_curve, scale)
splines = polyline_to_native(speckle_curve, blender_curve, scale)
elif isinstance(speckle_curve, Arc):
spline = arc_to_native(speckle_curve, blender_curve, scale)
splines = [spline] if spline else []
elif isinstance(speckle_curve, Ellipse) or isinstance(speckle_curve, Circle):
spline = ellipse_to_native(speckle_curve, blender_curve, scale)
splines = ellipse_to_native(speckle_curve, blender_curve, scale)
else:
raise TypeError(f"{speckle_curve} is not a supported curve type. Supported types: {SUPPORTED_CURVES}")
return [spline] if spline is not None else []
return splines
def icurve_to_native(speckle_curve: Base, name: str, scale: float) -> bpy.types.Curve:
@@ -508,7 +526,7 @@ def icurve_to_native(speckle_curve: Base, name: str, scale: float) -> bpy.types.
"""
Transforms and Intances
Transforms and Instances
"""
def transform_to_native(transform: Transform, scale: float) -> MMatrix:
@@ -522,7 +540,7 @@ def transform_to_native(transform: Transform, scale: float) -> MMatrix:
)
# scale the translation
for i in range(3):
mat[i][3] *= scale
mat[i][3] *= scale # type: ignore
return mat
def plane_to_native_transform(plane: Plane, fallback_scale:float = 1) -> MMatrix:
@@ -545,43 +563,59 @@ Instances / Blocks
"""
def _get_instance_name(instance: Instance) -> str:
name_prefix = _get_friendly_object_name(instance) or _get_friendly_object_name(instance.definition) or _simplified_speckle_type(instance.speckle_type)
return f"{name_prefix}{OBJECT_NAME_SEPERATOR}{instance.id}"
if not instance.definition: raise Exception("Instance is missing a definition")
name_prefix = (
_get_friendly_object_name(instance)
or _get_friendly_object_name(instance.definition)
or _simplified_speckle_type(instance.speckle_type)
)
return f"{name_prefix}{OBJECT_NAME_SPECKLE_SEPARATOR}{instance.id}"
def instance_to_native_object(instance: Instance, scale: float) -> Tuple[bpy.types.Object, List[bpy.types.Object]]:
def instance_to_native_object(instance: Instance, scale: float) -> Object:
"""
Converts Instance to a unique object with (potentially) shared data (linked duplicate)
"""
if not instance.definition: raise Exception(f"Instance is missing a definition")
if not instance.transform: raise Exception(f"Instance is missing a transform")
if not instance.definition: raise Exception("Instance is missing a definition")
if not instance.transform: raise Exception("Instance is missing a transform")
definition = instance.definition
if not definition.id: raise Exception("Instance is missing a valid definition")
name = _get_instance_name(instance)
definition = instance.definition
native_instance: Object
native_elements: List[Object] = []
elements_on_instance: List[Object] = []
if isinstance(definition, BlockDefinition): #NOTE: We have to handle BlockDefinitions specially here, since they don't follow normal traversal rules
native_instance = create_new_object(None, name) #Instance will be empty
native_instance: Optional[Object] = None
converted_objects: Dict[str, Union[Object, BCollection]] = {}
traversal_root: Base = definition
if not can_convert_to_native(definition):
# Non-convertible (like all blocks, and some revit instances) will not be converted as part of the deep_traversal.
# so we explicitly convert them as empties.
native_instance = create_new_object(None, name)
native_instance.empty_display_size = 0
for geo in definition.geometry:
native_elements.append(convert_to_native(geo))
else:
native_instance = convert_to_native(instance.definition)
converted_objects["__ROOT"] = native_instance # we create a dummy root to avoid id conflicts, since revit definitions have displayValues, they are convertible
traversal_root = Base(elements=definition, id="__ROOT")
#Convert definition + "elements" on definition
_deep_conversion(traversal_root, converted_objects, False)
if not native_instance:
assert(can_convert_to_native(definition))
if not definition.id in converted_objects:
raise Exception("Definition was not converted")
converted = converted_objects[definition.id]
if not isinstance(converted, Object):
raise Exception("Definition was not converted to an Object")
native_instance = converted
instance_transform = transform_to_native(instance.transform, scale)
instance_transform_inverted = instance_transform.inverted()
native_instance.matrix_world = instance_transform
elements_on_instance = elements_to_native(instance, name, scale)
for c in elements_on_instance:
c.matrix_world = instance_transform_inverted @ c.matrix_world #Undo the instance transform on elements
native_elements.extend(elements_on_instance)
return (native_instance, native_elements) #TODO: need to double check that all child objects have custom props attached correctly
return native_instance
def instance_to_native_collection_instance(instance: Instance, scale: float) -> bpy.types.Object:
"""
@@ -591,33 +625,25 @@ def instance_to_native_collection_instance(instance: Instance, scale: float) ->
The definition collection won't be linked to the current scene
Any Elements on the instance object will also be converted (and spacially transformed)
"""
if not instance.definition: raise Exception(f"Instance is missing a definition")
if not instance.transform: raise Exception(f"Instance is missing a transform")
if not instance.definition: raise Exception("Instance is missing a definition")
if not instance.transform: raise Exception("Instance is missing a transform")
name = _get_instance_name(instance)
# Get/Convert definition collection
collection_def = _instance_definition_to_native(instance.definition)
# Convert elements as children of collection instance object
elements = elements_to_native(instance, name, scale)
instance_transform = transform_to_native(instance.transform, scale)
instance_transform_inverted = instance_transform.inverted()
native_instance = bpy.data.objects.new(name, None)
native_instance = create_new_object(None, name)
#add_custom_properties(instance, native_instance)
# hide the instance axes so they don't clutter the viewport
native_instance.empty_display_size = 0
native_instance.instance_collection = collection_def
native_instance.instance_type = "COLLECTION"
native_instance.matrix_world =instance_transform
for c in elements:
c.matrix_world = instance_transform_inverted @ c.matrix_world #Undo the instance transform on elements
c.parent = native_instance #TODO: need to double check that all child objects have custom props attached correctly
native_instance.matrix_world = instance_transform
return native_instance
def _instance_definition_to_native(definition: Union[Base, BlockDefinition]) -> bpy.types.Collection:
@@ -629,25 +655,122 @@ def _instance_definition_to_native(definition: Union[Base, BlockDefinition]) ->
if native_def:
return native_def
native_def = bpy.data.collections.new(name)
native_def = create_new_collection(name)
native_def["applicationId"] = definition.applicationId
#TODO could maybe replace BlockDefinition awareness with a single traverse member call
geometry = definition.geometry if isinstance(definition, BlockDefinition) else [definition]
for geo in geometry:
if not geo: continue
converted = convert_to_native(geo) #NOTE: we assume the last item is the root converted item
link_object_to_collection_nested(converted, native_def)
converted_objects = {}
converted_objects["__ROOT"] = native_def # we create a dummy root to avoid id conflicts, since revit definitions have displayValues, they are convertible
dummyRoot = Base(elements=definition, id="__ROOT")
_deep_conversion(dummyRoot, converted_objects, True)
return native_def
def _deep_conversion(root: Base, converted_objects: Dict[str, Union[Object, BCollection]], preserve_transform: bool):
traversal_func = get_default_traversal_func(can_convert_to_native)
for item in traversal_func.traverse(root):
current: Base = item.current
if can_convert_to_native(current) or isinstance(current, SCollection):
try:
if not current or not current.id: raise Exception(f"{current} was an invalid speckle object")
#Convert the object!
converted_data_type: str
converted: Union[Object, BCollection, None]
if isinstance(current, SCollection):
if(current.collectionType == "Scene Collection"): raise ConversionSkippedException()
converted = collection_to_native(current)
converted_data_type = "COLLECTION"
else:
converted = convert_to_native(current)
converted_data_type = "COLLECTION_INSTANCE" if converted.instance_collection else str(converted.type)
if converted is None:
raise Exception("Conversion returned None")
converted_objects[current.id] = converted
add_to_hierarchy(converted, item, converted_objects, preserve_transform)
_report(f"Successfully converted {type(current).__name__} {current.id} as '{converted_data_type}'")
except ConversionSkippedException as ex:
_report(f"Skipped converting {type(current).__name__} {current.id}: {ex}")
except Exception as ex:
_report(f"Failed to converted {type(current).__name__} {current.id}: {ex}")
def collection_to_native(collection: SCollection) -> BCollection:
name = collection.name or f"{collection.collectionType} -- {collection.applicationId or collection.id}" #TODO: consider consolidating name formatting with Rhino
ret = get_or_create_collection(name)
color = getattr(collection, "colorTag", None)
if color:
ret.color_tag = color
return ret
def get_or_create_collection(name: str, clear_collection: bool = True) -> BCollection:
#Disabled for now, since update mode needs rescoping.
# existing = cast(Optional[BCollection], bpy.data.collections.get(name))
# if existing:
# if clear_collection:
# for obj in existing.objects:
# existing.objects.unlink(obj)
# return existing
# else:
new_collection = create_new_collection(name)
#NOTE: We want to not render revit "Rooms" collections by default.
if name == "Rooms":
new_collection.hide_viewport = True
new_collection.hide_render = True
return new_collection
"""
Object Naming
Object Naming and Creation
"""
def create_new_collection( desired_name: str) -> bpy.types.Collection:
"""
Creates a new blender collection with a unique name
If the desired_name is already taken
we'll append a number, with the format .xxx to the desired_name to ensure the name is unique.
"""
name = _make_unique_name(desired_name, bpy.data.collections.keys())
blender_collection = bpy.data.collections.new(name)
return blender_collection
def create_new_object(obj_data: Optional[bpy.types.ID], desired_name: str) -> bpy.types.Object:
"""
Creates a new blender object with a unique name,
If the desired_name is already taken
we'll append a number, with the format .xxx to the desired_name to ensure the name is unique.
"""
name = _make_unique_name(desired_name, bpy.data.objects.keys())
blender_object = bpy.data.objects.new(name, obj_data)
return blender_object
def _make_unique_name( desired_name: str, taken_names: Collection[str], counter: int = 0) -> str:
"""
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
#TODO: This is very slow, and gets slower the more objects you receive with the same name...
# We could use a binary/galloping search, and/or cache the name -> index within a receive.
if name in taken_names:
#Name already taken, increment counter and try again!
return _make_unique_name(desired_name, taken_names, counter + 1)
return name
def _get_friendly_object_name(speckle_object: Base) -> Optional[str]:
return (getattr(speckle_object, "name", None)
or getattr(speckle_object, "Name", None)
@@ -658,13 +781,11 @@ def _get_friendly_object_name(speckle_object: Base) -> Optional[str]:
# Blender object names must not exceed 62 characters
# We need to ensure the complete ID is included in the name (to prevent identity collisions)
# So we if the name is too long, we need to truncate
OBJECT_NAME_MAX_LENGTH = 62
SPECKLE_ID_LENGTH = 32
OBJECT_NAME_SEPERATOR = " -- "
def _truncate_object_name(name: str) -> str:
MAX_NAME_LENGTH = OBJECT_NAME_MAX_LENGTH - SPECKLE_ID_LENGTH - len(OBJECT_NAME_SEPERATOR)
MAX_NAME_LENGTH = OBJECT_NAME_MAX_LENGTH - SPECKLE_ID_LENGTH - len(OBJECT_NAME_SPECKLE_SEPARATOR)
return name[:MAX_NAME_LENGTH]
@@ -680,7 +801,7 @@ def _generate_object_name(speckle_object: Base) -> str:
else:
prefix = _simplified_speckle_type(speckle_object.speckle_type)
return f"{prefix}{OBJECT_NAME_SEPERATOR}{speckle_object.id}"
return f"{prefix}{OBJECT_NAME_SPECKLE_SEPARATOR}{speckle_object.id}"
def get_scale_factor(speckle_object: Base, fallback: float = 1.0) -> float:
+85 -58
View File
@@ -6,6 +6,7 @@ from bpy.types import (
Object,
Curve as NCurve,
Mesh as NMesh,
Camera as NCamera,
)
from deprecated import deprecated
from mathutils.geometry import interpolate_bezier
@@ -16,10 +17,12 @@ from mathutils import (
from specklepy.objects import Base
from specklepy.objects.other import BlockInstance, BlockDefinition, RenderMaterial, Transform
from specklepy.objects.geometry import (
Mesh, Curve, Interval, Box, Point, Polyline
Mesh, Curve, Interval, Box, Point, Vector, Polyline,
)
from bpy_speckle.convert.to_native import OBJECT_NAME_SEPERATOR, SPECKLE_ID_LENGTH
from bpy_speckle.blender_commit_object_builder import BlenderCommitObjectBuilder
from bpy_speckle.convert.constants import OBJECT_NAME_SPECKLE_SEPARATOR, SPECKLE_ID_LENGTH
from bpy_speckle.convert.util import (
ConversionSkippedException,
get_blender_custom_properties,
make_knots,
nurb_make_curve,
@@ -27,13 +30,11 @@ from bpy_speckle.convert.util import (
)
from bpy_speckle.functions import _report
class ConversionSkippedException(Exception):
pass
Units: str = "m" # The desired final units to send
UnitsScale: float = 1 # The scale factor conversions need to apply to position data to get to the desired units
CAN_CONVERT_TO_SPECKLE = ("MESH", "CURVE", "EMPTY")
CAN_CONVERT_TO_SPECKLE = ("MESH", "CURVE", "EMPTY", "CAMERA")
def convert_to_speckle(raw_blender_object: Object, units_scale: float, units: str, depsgraph: Optional[Depsgraph]) -> Base:
@@ -66,7 +67,8 @@ def convert_to_speckle(raw_blender_object: Object, units_scale: float, units: st
converted = curve_to_speckle(blender_object, cast(NCurve, blender_object.data))
elif blender_type == "EMPTY":
converted = empty_to_speckle(blender_object)
elif blender_type == "CAMERA":
converted = camera_to_speckle_view(blender_object, cast(NCamera, blender_object.data))
if not converted:
raise Exception("Conversion returned None")
@@ -97,7 +99,7 @@ def mesh_to_speckle_meshes(blender_object: Object, data: bpy.types.Mesh) -> List
submesh_data[p.material_index].append(p)
transform = cast(MMatrix, blender_object.matrix_world)
scaled_vertices = [tuple(transform @ x.co * UnitsScale) for x in data.vertices]
scaled_vertices = [tuple(transform @ x.co * UnitsScale) for x in data.vertices] # type: ignore
# Create Speckle meshes for each material
submeshes = []
@@ -105,7 +107,7 @@ def mesh_to_speckle_meshes(blender_object: Object, data: bpy.types.Mesh) -> List
for i in submesh_data:
index_mapping: Dict[int, int] = {}
#Loop through each polygon, and map indicies to their new index in m_verts
#Loop through each polygon, and map indices to their new index in m_verts
mesh_area = 0
m_verts: List[float] = []
@@ -158,24 +160,24 @@ def bezier_to_speckle(matrix: MMatrix, spline: bpy.types.Spline, name: Optional[
points: List[Tuple[MVector]] = []
for i, bp in enumerate(spline.bezier_points):
if i > 0:
points.append(tuple(matrix @ bp.handle_left * UnitsScale))
points.append(tuple(matrix @ bp.co * UnitsScale))
points.append(tuple(matrix @ bp.handle_left * UnitsScale)) # type: ignore
points.append(tuple(matrix @ bp.co * UnitsScale)) # type: ignore
if i < len(spline.bezier_points) - 1:
points.append(tuple(matrix @ bp.handle_right * UnitsScale))
points.append(tuple(matrix @ bp.handle_right * UnitsScale)) # type: ignore
if closed:
points.extend(
(
tuple(matrix @ spline.bezier_points[-1].handle_right * UnitsScale),
tuple(matrix @ spline.bezier_points[0].handle_left * UnitsScale),
tuple(matrix @ spline.bezier_points[0].co * UnitsScale),
tuple(matrix @ spline.bezier_points[-1].handle_right * UnitsScale), # type: ignore
tuple(matrix @ spline.bezier_points[0].handle_left * UnitsScale), # type: ignore
tuple(matrix @ spline.bezier_points[0].co * UnitsScale), # type: ignore
)
)
num_points = len(points)
flattend_points = []
for row in points: flattend_points.extend(row)
flattened_points = []
for row in points: flattened_points.extend(row)
knot_count = num_points + degree - 1
knots = [0] * knot_count
@@ -190,7 +192,7 @@ def bezier_to_speckle(matrix: MMatrix, spline: bpy.types.Spline, name: Optional[
degree=degree,
closed=spline.use_cyclic_u,
periodic= not spline.use_endpoint_u,
points=flattend_points,
points=flattened_points,
weights=[1] * num_points,
knots=knots,
rational=True,
@@ -215,17 +217,17 @@ def nurbs_to_speckle(matrix: MMatrix, spline: bpy.types.Spline, name: Optional[s
weights = [pt.weight for pt in spline.points]
is_rational = all(w == weights[0] for w in weights)
points = [tuple(matrix @ pt.co.xyz * UnitsScale) for pt in spline.points]
points = [tuple(matrix @ pt.co.xyz * UnitsScale) for pt in spline.points] # type: ignore
flattend_points = []
for row in points: flattend_points.extend(row)
flattened_points = []
for row in points: flattened_points.extend(row)
if spline.use_cyclic_u:
for i in range(0, degree * 3, 3):
# Rhino expects n + degree number of points (for closed curves). So we need to add an extra point for each degree
flattend_points.append(flattend_points[i + 0])
flattend_points.append(flattend_points[i + 1])
flattend_points.append(flattend_points[i + 2])
flattened_points.append(flattened_points[i + 0])
flattened_points.append(flattened_points[i + 1])
flattened_points.append(flattened_points[i + 2])
for i in range(0, degree):
weights.append(weights[i])
@@ -235,7 +237,7 @@ def nurbs_to_speckle(matrix: MMatrix, spline: bpy.types.Spline, name: Optional[s
degree=degree,
closed=spline.use_cyclic_u,
periodic= not spline.use_endpoint_u,
points=flattend_points,
points=flattened_points,
weights=weights,
knots=knots,
rational=is_rational,
@@ -252,13 +254,13 @@ def nurbs_to_speckle_polyline(matrix: MMatrix, spline: bpy.types.Spline, length:
"""
Samples a nurbs curve with resolution_u creating a polyline
"""
points = []
points: List[float] = []
sampled_points = nurb_make_curve(spline, spline.resolution_u, 3)
for i in range(0, len(sampled_points), 3):
scaled_point = matrix @ MVector((
scaled_point = cast(Vector, matrix @ MVector((
sampled_points[i + 0],
sampled_points[i + 1],
sampled_points[i + 2])) * UnitsScale
sampled_points[i + 2])) * UnitsScale)
points.append(scaled_point.x)
points.append(scaled_point.y)
@@ -303,27 +305,27 @@ def bezier_to_speckle_polyline(matrix: MMatrix, spline: bpy.types.Spline, length
domain = Interval(start=0, end=length, totalChildrenCount=0)
return Polyline(value=points, closed = spline.use_cyclic_u, domain=domain, area=0, len=length)
_QUICK_TEST_NAME_LENGTH = SPECKLE_ID_LENGTH + len(OBJECT_NAME_SEPERATOR)
_QUICK_TEST_NAME_LENGTH = SPECKLE_ID_LENGTH + len(OBJECT_NAME_SPECKLE_SEPARATOR)
def to_speckle_name(blender_object: bpy.types.ID) -> str:
does_name_contain_id = len(blender_object.name) > _QUICK_TEST_NAME_LENGTH and OBJECT_NAME_SEPERATOR in blender_object.name
does_name_contain_id = len(blender_object.name) > _QUICK_TEST_NAME_LENGTH and OBJECT_NAME_SPECKLE_SEPARATOR in blender_object.name
if does_name_contain_id:
return blender_object.name.rsplit(OBJECT_NAME_SEPERATOR, 1)[0]
return blender_object.name.rsplit(OBJECT_NAME_SPECKLE_SEPARATOR, 1)[0]
else:
return blender_object.name
def poly_to_speckle(matrix: MMatrix, spline: bpy.types.Spline, name: Optional[str] = None) -> Polyline:
points = [tuple(matrix @ pt.co.xyz * UnitsScale) for pt in spline.points]
points = [tuple(matrix @ pt.co.xyz * UnitsScale) for pt in spline.points] # type: ignore
flattend_points = []
for row in points: flattend_points.extend(row)
flattened_points = []
for row in points: flattened_points.extend(row)
length = spline.calc_length()
domain = Interval(start=0, end=length, totalChildrenCount=0)
return Polyline(
name=name,
closed=bool(spline.use_cyclic_u),
value=list(flattend_points),
value=list(flattened_points),
length=length,
domain=domain,
bbox=Box(area=0.0, volume=0.0),
@@ -382,7 +384,7 @@ def ngons_to_speckle_polylines(blender_object: Object, data: bpy.types.Mesh) ->
for i, poly in enumerate(data.polygons):
value = []
for v in poly.vertices:
value.extend(mat @ verts[v].co * UnitsScale)
value.extend(mat @ verts[v].co * UnitsScale) # type: ignore
domain = Interval(start=0, end=1)
poly = Polyline(
@@ -408,41 +410,63 @@ def material_to_speckle(blender_mat: bpy.types.Material) -> RenderMaterial:
if blender_mat.use_nodes:
if blender_mat.node_tree.nodes.get("Principled BSDF"):
inputs = blender_mat.node_tree.nodes["Principled BSDF"].inputs
speckle_mat.diffuse = to_argb_int(inputs["Base Color"].default_value)
speckle_mat.emissive = to_argb_int(inputs["Emission"].default_value)
speckle_mat.roughness = inputs["Roughness"].default_value
speckle_mat.metalness = inputs["Metallic"].default_value
speckle_mat.opacity = inputs["Alpha"].default_value
speckle_mat.diffuse = to_argb_int(inputs["Base Color"].default_value) # type: ignore
speckle_mat.emissive = to_argb_int(inputs["Emission"].default_value) # type: ignore
speckle_mat.roughness = inputs["Roughness"].default_value # type: ignore
speckle_mat.metalness = inputs["Metallic"].default_value # type: ignore
speckle_mat.opacity = inputs["Alpha"].default_value # type: ignore
return speckle_mat
elif blender_mat.node_tree.nodes.get("Diffuse BSDF"):
inputs = blender_mat.node_tree.nodes["Diffuse BSDF"].inputs
speckle_mat.diffuse = to_argb_int(inputs["Color"].default_value)
speckle_mat.roughness = inputs["Roughness"].default_value
speckle_mat.diffuse = to_argb_int(inputs["Color"].default_value) # type: ignore
speckle_mat.roughness = inputs["Roughness"].default_value # type: ignore
return speckle_mat
#TODO: Support more shaders
# fallback to standard material props
speckle_mat.diffuse = to_argb_int(blender_mat.diffuse_color)
speckle_mat.diffuse = to_argb_int(blender_mat.diffuse_color) # type: ignore
speckle_mat.metalness = blender_mat.metallic
speckle_mat.roughness = blender_mat.roughness
return speckle_mat
@deprecated
def material_to_speckle_old(blender_object: Object) -> Optional[RenderMaterial]:
"""Create and return a render material from a blender object"""
if not getattr(blender_object.data, "materials", None):
return None
def camera_to_speckle_view(blender_object: Object, data: NCamera) -> Base:
if data.type != 'PERSP':
raise Exception(f"Cameras of type {data.type} are not currently supported")
matrix = cast(MMatrix, blender_object.matrix_world)
up = matrix.col[1].xyz # type: ignore
forwards = -matrix.col[2].xyz # type: ignore
translation = matrix.translation
blender_mat: bpy.types.Material = blender_object.data.materials[0]
if not blender_mat:
return None
view = Base.of_type("Objects.BuiltElements.View:Objects.BuiltElements.View3D") #HACK: views are not in specklepy yet!
view.name = to_speckle_name(blender_object)
view.origin = vector_to_speckle_point(translation)
view.upDirection = vector_to_speckle(up)
view.forwardDirection = vector_to_speckle(forwards)
view.target = vector_to_speckle_point(forwards) #TODO: do these need to be scaled?
view.units = Units
view.isOrthogonal = False
return view
return material_to_speckle(blender_mat)
def vector_to_speckle_point(xyz: MVector) -> Point:
return Point(
x = xyz.x * UnitsScale,
y = xyz.y * UnitsScale,
z = xyz.z * UnitsScale,
units = Units,
)
def vector_to_speckle(xyz: MVector) -> Vector:
return Vector(
x = xyz.x * UnitsScale,
y = xyz.y * UnitsScale,
z = xyz.z * UnitsScale,
units = Units,
)
def transform_to_speckle(blender_transform: Union[Iterable[Iterable[float]], MMatrix]) -> Transform:
iterable_transform = cast(Iterable[Iterable[float]], blender_transform) #NOTE: Matrix are itterable, even if type hinting says they are not
iterable_transform = cast(Iterable[Iterable[float]], blender_transform) #NOTE: Matrix are iterable, even if type hinting says they are not
value = [y for x in iterable_transform for y in x]
# scale the translation
for i in (3, 7, 11):
@@ -452,20 +476,23 @@ def transform_to_speckle(blender_transform: Union[Iterable[Iterable[float]], MMa
def block_def_to_speckle(blender_definition: bpy.types.Collection) -> BlockDefinition:
geometry = []
geometryBuilder = BlenderCommitObjectBuilder()
for geo in blender_definition.objects:
try:
#TODO: right now, geometry will be a flat list of objects. Eventually we will want to preseve the parent relationship
geometry.append(convert_to_speckle(geo, UnitsScale, Units, None))
c = convert_to_speckle(geo, UnitsScale, Units, None)
geometryBuilder.include_object(c, geo)
except ConversionSkippedException as ex:
_report(f"Skipped converting '{geo.name_full}' inside collection instance: '{ex}")
except Exception as ex:
_report(f"Failed to converted '{geo.name_full}' inside collection instance: '{ex}'")
dummyRoot = Base()
geometryBuilder.apply_relationships(geometryBuilder.converted.values(), dummyRoot)
block_def = BlockDefinition(
units=Units,
name=to_speckle_name(blender_definition),
geometry=geometry,
geometry=dummyRoot["@elements"],
basePoint=Point(units=Units),
)
# blender_props = get_blender_custom_properties(blender_definition)
@@ -494,7 +521,7 @@ def empty_to_speckle(blender_object: Object) -> Union[BlockInstance, Base]:
wrapper = Base()
wrapper["@displayValue"] = matrix_to_speckle_point(cast(MMatrix, blender_object.matrix_world))
return wrapper
#TODO: we could do a Empty -> Point conversion here. However, the viewer (and likly other apps) don't support a pont with "elements"
#TODO: we could do a Empty -> Point conversion here. However, the viewer (and likely other apps) don't support a pont with "elements"
#return matrix_to_speckle_point(cast(MMatrix, blender_object.matrix_world))
+107 -48
View File
@@ -1,29 +1,19 @@
import math
from typing import Any, Optional, Tuple
from typing import Any, Dict, Optional, Tuple, Union, cast
from bmesh.types import BMesh
import bpy, struct, idprop
import bpy, idprop
from specklepy.objects.base import Base
from specklepy.objects.geometry import Circle, Mesh, Ellipse
from specklepy.objects.geometry import Mesh
from specklepy.objects.other import RenderMaterial
from bpy_speckle.convert.constants import IGNORED_PROPERTY_KEYS
from bpy_speckle.functions import _report
from bpy.types import Material, Object
from bpy.types import Material, Object, Collection as BCollection, Node, ShaderNodeVertexColor
IGNORED_PROPERTY_KEYS = {
"id",
"elements",
"displayMesh",
"displayValue",
"speckle_type",
"parameters",
"faces",
"colors",
"vertices",
"renderMaterial",
"textureCoordinates",
"totalChildrenCount"
}
from specklepy.objects.graph_traversal.traversal import TraversalContext
class ConversionSkippedException(Exception):
pass
def to_rgba(argb_int: int) -> Tuple[float, float, float, float]:
"""Converts the int representation of a colour into a percent RGBA tuple"""
@@ -97,29 +87,53 @@ def render_material_to_native(speckle_mat: RenderMaterial) -> Material:
blender_mat.use_nodes = True
inputs = blender_mat.node_tree.nodes["Principled BSDF"].inputs
inputs["Base Color"].default_value = to_rgba(speckle_mat.diffuse)
inputs["Emission"].default_value = to_rgba(speckle_mat.emissive)
inputs["Roughness"].default_value = speckle_mat.roughness
inputs["Metallic"].default_value = speckle_mat.metalness
inputs["Alpha"].default_value = speckle_mat.opacity
inputs["Base Color"].default_value = to_rgba(speckle_mat.diffuse) # type: ignore
inputs["Emission"].default_value = to_rgba(speckle_mat.emissive) # type: ignore
inputs["Roughness"].default_value = speckle_mat.roughness # type: ignore
inputs["Metallic"].default_value = speckle_mat.metalness # type: ignore
inputs["Alpha"].default_value = speckle_mat.opacity # type: ignore
if speckle_mat.opacity < 1.0:
blender_mat.blend_method = "BLEND"
return blender_mat
_vertex_color_material: Optional[Material] = None
def get_vertex_color_material() -> Material:
global _vertex_color_material
#see https://stackoverflow.com/a/69807985
if not _vertex_color_material:
_vertex_color_material = bpy.data.materials.new("Vertex Color Material")
_vertex_color_material.use_nodes = True
nodes = _vertex_color_material.node_tree.nodes
principled_bsdf_node = cast(Node, nodes.get("Principled BSDF"))
if not "VERTEX_COLOR" in [node.type for node in nodes]:
vertex_color_node = cast(ShaderNodeVertexColor, nodes.new(type = "ShaderNodeVertexColor"))
else:
vertex_color_node = cast(ShaderNodeVertexColor, nodes.get("Vertex Color"))
vertex_color_node.layer_name = "Col"
links = _vertex_color_material.node_tree.links
link = links.new(vertex_color_node.outputs[0], principled_bsdf_node.inputs[0])
return _vertex_color_material
def get_render_material(speckle_object: Base) -> Optional[RenderMaterial]:
"""Trys to get a RenderMaterial on given speckle_object and convert it to a blender material"""
"""Trys to get a RenderMaterial on given speckle_object"""
speckle_mat = getattr(
speckle_object,
"renderMaterial",
getattr(speckle_object, "@renderMaterial", None),
)
if not isinstance(speckle_mat, RenderMaterial):
return None
return speckle_mat
if isinstance(speckle_mat, RenderMaterial):
return speckle_mat
return None
@@ -151,7 +165,7 @@ def add_faces(speckle_mesh: Mesh, blender_mesh: BMesh, indexOffset: int, materia
i += 1
try:
f = blender_mesh.faces.new(
[blender_mesh.verts[x + indexOffset] for x in sfaces[i : i + n]]
[blender_mesh.verts[x + indexOffset] for x in sfaces[i : i + n]] # type: ignore
)
f.material_index = materialIndex
f.smooth = smooth
@@ -169,10 +183,8 @@ def add_colors(speckle_mesh: Mesh, blender_mesh: BMesh):
if len(scolors) > 0:
for i in range(len(scolors)):
col = int(scolors[i])
(a, r, g, b) = [
int(x) for x in struct.unpack("!BBBB", struct.pack("!i", col))
]
argb = int(scolors[i])
(a, r, g, b) = argb_split(argb)
colors.append(
(
float(r) / 255.0,
@@ -183,13 +195,20 @@ def add_colors(speckle_mesh: Mesh, blender_mesh: BMesh):
)
# Make vertex colors
if len(scolors) == len(blender_mesh.verts):
if len(scolors) == len(blender_mesh.verts): # type: ignore
color_layer = blender_mesh.loops.layers.color.new("Col")
for face in blender_mesh.faces:
for face in blender_mesh.faces: # type: ignore
for loop in face.loops:
loop[color_layer] = colors[loop.vert.index]
def argb_split(argb: int) -> Tuple[int, int, int, int]:
alpha = (argb >> 24) & 0xFF
red = (argb >> 16) & 0xFF
green = (argb >> 8) & 0xFF
blue = argb & 0xFF
return (alpha, red, green, blue)
def add_uv_coords(speckle_mesh: Mesh, blender_mesh: BMesh):
s_uvs = speckle_mesh.textureCoordinates
@@ -198,21 +217,21 @@ def add_uv_coords(speckle_mesh: Mesh, blender_mesh: BMesh):
try:
uv = []
if len(s_uvs) // 2 == len(blender_mesh.verts):
if len(s_uvs) // 2 == len(blender_mesh.verts): # type: ignore
uv.extend(
(float(s_uvs[i]), float(s_uvs[i + 1]))
for i in range(0, len(s_uvs), 2)
)
else:
_report(
f"Failed to match UV coordinates to vert data. Blender mesh verts: {len(blender_mesh.verts)}, Speckle UVs: {len(s_uvs) // 2}"
f"Failed to match UV coordinates to vert data. Blender mesh verts: {len(blender_mesh.verts)}, Speckle UVs: {len(s_uvs) // 2}" # type: ignore
)
return
# Make UVs
uv_layer = blender_mesh.loops.layers.uv.verify()
for f in blender_mesh.faces:
for f in blender_mesh.faces: # type: ignore
for l in f.loops:
luv = l[uv_layer]
luv.uv = uv[l.vert.index]
@@ -235,8 +254,9 @@ ignored_keys = {
"_chunkable",
}
def get_blender_custom_properties(obj, max_depth: int = 200):
if max_depth < 0:
def get_blender_custom_properties(obj, max_depth: int = 63):
"""Recursivly 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:
return obj
if hasattr(obj, "keys"):
@@ -248,7 +268,7 @@ def get_blender_custom_properties(obj, max_depth: int = 200):
}
if isinstance(obj, (list, tuple, idprop.types.IDPropertyArray)):
return [get_blender_custom_properties(o, max_depth - 1) for o in obj]
return [get_blender_custom_properties(o, max_depth - 1) for o in obj] # type: ignore
return obj
@@ -304,7 +324,7 @@ def basis_nurb(t: float, order: int, point_count: int, knots: list[float], basis
i1 = i2 = 0
orderpluspnts = order + point_count
opp2 = orderpluspnts - 1
# this is for float inaccuracy
if t < knots[0]:
t = knots[0]
@@ -329,7 +349,7 @@ def basis_nurb(t: float, order: int, point_count: int, knots: list[float], basis
else:
basis[i] = 0.0
basis[i] = 0.0
basis[i] = 0.0 #type: ignore
# this is order 2, 3, ...
for j in range(2, order + 1):
@@ -393,14 +413,14 @@ def nurb_make_curve(nu: bpy.types.Spline, resolu: int, stride: int = 3) -> list[
else:
pt_index += 1
sum_array[sum_index] = basisu[i] * nu.points[pt_index].co[3]
sum_array[sum_index] = basisu[i] * nu.points[pt_index].co[3] #type: ignore
sumdiv += sum_array[sum_index]
sum_index += 1
if (sumdiv != 0.0) and (sumdiv < 1.0 - EPS or sumdiv > 1.0 + EPS):
sum_index = 0
for i in range(istart, iend + 1):
sum_array[sum_index] /= sumdiv
sum_array[sum_index] /= sumdiv #type: ignore
sum_index += 1
coord_array[coord_index: coord_index + 3] = (0.0, 0.0, 0.0)
@@ -423,9 +443,48 @@ def nurb_make_curve(nu: bpy.types.Spline, resolu: int, stride: int = 3) -> list[
return coord_array
def link_object_to_collection_nested(obj: bpy.types.Object, col: bpy.types.Collection):
if obj.name not in col.objects:
def link_object_to_collection_nested(obj: Object, col: BCollection):
if obj.name not in col.objects: #type: ignore
col.objects.link(obj)
for child in obj.children:
link_object_to_collection_nested(child, col)
for child in obj.children: #type: ignore
link_object_to_collection_nested(child, col)
def add_to_hierarchy(converted: Union[Object, BCollection], traversalContext : 'TraversalContext', converted_objects: Dict[str, Union[Object, BCollection]], preserve_transform: bool) -> None:
nextParent = traversalContext.parent
# Traverse up the tree to find a direct parent object, and a containing collection
parent_collection: Optional[BCollection] = None
parent_object: Optional[Object] = None
while nextParent:
if nextParent.current.id in converted_objects:
c = converted_objects[nextParent.current.id]
if isinstance(c, BCollection):
parent_collection = c
break
else: #isinstance(c, Object):
parent_object = parent_object or c
nextParent = nextParent.parent
# If no containing collection is found, fall back to the scene collection
if not parent_collection:
parent_collection = bpy.context.scene.collection
if isinstance(converted, Object):
if parent_object:
set_parent(converted, parent_object, preserve_transform)
link_object_to_collection_nested(converted, parent_collection)
elif converted.name not in parent_collection.children.keys():
parent_collection.children.link(converted)
def set_parent(child: Object, parent: Object, preserve_transform: bool = False) -> None:
if preserve_transform :
previous = child.matrix_world.copy() # type: ignore
child.parent = parent
child.matrix_world = previous
else:
child.parent = parent
+20 -56
View File
@@ -1,38 +1,12 @@
from typing import Callable, Set
import bpy
from typing import Callable
from specklepy.objects.base import Base
from bpy_speckle.properties.scene import SpeckleSceneSettings
from bpy_speckle.convert.constants import ELEMENTS_PROPERTY_ALIASES
from bpy_speckle.specklepy_extras.traversal import GraphTraversal, TraversalRule
"""
Speckle functions
"""
unit_scale = {
"meters": 1.0,
"centimeters": 0.01,
"millimeters": 0.001,
"inches": 0.0254,
"feet": 0.3048,
"kilometers": 1000.0,
"mm": 0.001,
"cm": 0.01,
"m": 1.0,
"km": 1000.0,
"in": 0.0254,
"ft": 0.3048,
"yd": 0.9144,
"mi": 1609.340,
}
"""
Utility functions
"""
from specklepy.objects.graph_traversal.traversal import GraphTraversal, TraversalRule
from specklepy.objects.units import get_scale_factor_to_meters, get_units_from_string
def _report(msg):
def _report(msg: object) -> None:
"""
Function for printing messages to the console
"""
@@ -40,42 +14,32 @@ def _report(msg):
def get_scale_length(units: str) -> float:
if units.lower() in unit_scale.keys():
return unit_scale[units]
_report("Units <{}> are not supported.".format(units))
return 1.0
"""Returns a scalar to convert distance values from one unit system to meters"""
return get_scale_factor_to_meters(get_units_from_string(units))
"""
Client, user, and stream functions
"""
elements_aliases: Set[str] = {"elements", "@elements"}
ignore_props: Set[str] = {"@blockDefinition", "displayValue", "@displayValue", "units", "id", "applicationId"}
def get_default_traversal_func(can_convert_to_native: Callable[[Base], bool]) -> GraphTraversal:
"""
Traversal func for traversing a speckle commit object
"""
convertable_rule = TraversalRule(
[can_convert_to_native],
lambda _: [i for i in elements_aliases if i not in ignore_props],
)
ignore_result_rule = TraversalRule(
[lambda o: "Objects.Structural.Results" in o.speckle_type, #Sadly, this one is nessasary to avoid double conversion...
lambda o: "Objects.BuiltElements.Revit.Parameter" in o.speckle_type], #This one is just for traversal performance of revit commits
ignore_rule = TraversalRule(
[
lambda o: "Objects.Structural.Results" in o.speckle_type, #Sadly, this one is necessary to avoid double conversion...
lambda o: "Objects.BuiltElements.Revit.Parameter" in o.speckle_type, #This one is just for traversal performance of revit commits
],
lambda _: [],
)
convertible_rule = TraversalRule(
[can_convert_to_native],
lambda _: ELEMENTS_PROPERTY_ALIASES,
)
default_rule = TraversalRule(
[lambda _: True],
lambda o: o.get_member_names(), #TODO: avoid deprecated members
)
return GraphTraversal([convertable_rule, ignore_result_rule, default_rule])
def get_speckle(context: bpy.types.Context) -> 'SpeckleSceneSettings':
return context.scene.speckle #type: ignore
return GraphTraversal([ignore_rule, convertible_rule, default_rule])
-2
View File
@@ -15,7 +15,6 @@ from .streams import (
SelectOrphanObjects,
)
from .streams import (
UpdateGlobal,
AddStreamFromURL,
CreateStream,
CopyStreamId,
@@ -54,7 +53,6 @@ operator_classes.extend(
ViewStreamDataApi,
DeleteStream,
SelectOrphanObjects,
UpdateGlobal,
AddStreamFromURL,
CreateStream,
OpenSpeckleGuide,
+11 -3
View File
@@ -1,12 +1,11 @@
"""
Commit operators
"""
from typing import cast
import bpy
from bpy.props import BoolProperty
from bpy_speckle.functions import _report, get_speckle
from bpy_speckle.clients import speckle_clients
from bpy_speckle.properties.scene import SpeckleSceneSettings
from bpy_speckle.properties.scene import get_speckle
from specklepy.logging import metrics
class DeleteCommit(bpy.types.Operator):
@@ -60,6 +59,15 @@ class DeleteCommit(bpy.types.Operator):
client = speckle_clients[int(speckle.active_user)]
deleted = client.commit.delete(stream_id=stream.id, commit_id=commit.id)
metrics.track(
"Connector Action",
client.account,
custom_props={
"name": "delete_commit"
},
)
if not deleted:
raise Exception("Delete operation failed")
+22
View File
@@ -1,5 +1,6 @@
import bpy
import webbrowser
from specklepy.logging import metrics
class OpenSpeckleGuide(bpy.types.Operator):
@@ -10,6 +11,13 @@ class OpenSpeckleGuide(bpy.types.Operator):
def execute(self, context):
webbrowser.open("https://speckle.guide/user/blender.html")
metrics.track(
"Connector Action",
None,
custom_props={
"name": "OpenSpeckleGuide"
},
)
return {"FINISHED"}
@@ -21,6 +29,13 @@ class OpenSpeckleTutorials(bpy.types.Operator):
def execute(self, context):
webbrowser.open("https://speckle.systems/tutorials/")
metrics.track(
"Connector Action",
None,
custom_props={
"name": "OpenSpeckleTutorials"
},
)
return {"FINISHED"}
@@ -32,4 +47,11 @@ class OpenSpeckleForum(bpy.types.Operator):
def execute(self, context):
webbrowser.open("https://speckle.community/")
metrics.track(
"Connector Action",
None,
custom_props={
"name": "OpenSpeckleForum"
},
)
return {"FINISHED"}
+48 -1
View File
@@ -11,7 +11,7 @@ from bpy_speckle.convert.to_speckle import (
)
from bpy_speckle.functions import get_scale_length, _report
from bpy_speckle.clients import speckle_clients
from specklepy.logging import metrics
class UpdateObject(bpy.types.Operator):
"""
@@ -56,6 +56,13 @@ class UpdateObject(bpy.types.Operator):
_report("Updating object {}".format(sm["_id"]))
client.objects.update(active.speckle.object_id, sm)
metrics.track(
"Connector Action",
None,
custom_props={
"name": "UpdateObject"
},
)
return {"FINISHED"}
return {"CANCELLED"}
@@ -79,6 +86,14 @@ class ResetObject(bpy.types.Operator):
context.object.speckle.enabled = False
context.view_layer.update()
metrics.track(
"Connector Action",
None,
custom_props={
"name": "ResetObject"
},
)
return {"FINISHED"}
@@ -125,6 +140,14 @@ class DeleteObject(bpy.types.Operator):
active.speckle.enabled = False
context.view_layer.update()
metrics.track(
"Connector Action",
None,
custom_props={
"name": "DeleteObject"
},
)
return {"FINISHED"}
@deprecated
@@ -197,6 +220,13 @@ class UploadNgonsAsPolylines(bpy.types.Operator):
context.view_layer.update()
_report("Done.")
metrics.track(
"Connector Action",
None,
custom_props={
"name": "UploadNgonsAsPolylines"
},
)
return {"FINISHED"}
def invoke(self, context, event):
@@ -267,6 +297,14 @@ class SelectIfSameCustomProperty(bpy.types.Operator):
else:
obj.select_set(False)
metrics.track(
"Connector Action",
None,
custom_props={
"name": "SelectIfSameCustomProperty"
},
)
return {"FINISHED"}
@@ -315,4 +353,13 @@ class SelectIfHasCustomProperty(bpy.types.Operator):
else:
obj.select_set(False)
metrics.track(
"Connector Action",
None,
custom_props={
"name": "SelectIfHasCustomProperty"
},
)
return {"FINISHED"}
+93 -312
View File
@@ -1,10 +1,8 @@
"""
Stream operators
"""
from itertools import chain
from math import radians
from deprecated import deprecated
from typing import Any, Callable, Dict, Iterable, List, Optional, Union, cast
from typing import Callable, Dict, Optional, Union, cast
import webbrowser
import bpy
from bpy.props import (
@@ -20,99 +18,32 @@ from bpy.types import (
from bpy_speckle.blender_commit_object_builder import BlenderCommitObjectBuilder
from bpy_speckle.convert.to_native import (
can_convert_to_native,
collection_to_native,
convert_to_native,
set_convert_instances_as,
)
from bpy_speckle.convert.to_speckle import (
ConversionSkippedException,
convert_to_speckle,
)
from bpy_speckle.functions import (
get_default_traversal_func,
_report,
get_scale_length,
get_speckle,
)
from bpy_speckle.clients import speckle_clients
from bpy_speckle.operators.users import add_user_stream
from bpy_speckle.properties.scene import SpeckleSceneSettings, SpeckleUserObject
from bpy_speckle.convert.util import link_object_to_collection_nested
from specklepy.api.models import Commit
from specklepy.api import operations, host_applications
from specklepy.api.wrapper import StreamWrapper
from specklepy.api.resources.stream import Stream
from bpy_speckle.properties.scene import SpeckleSceneSettings, SpeckleUserObject, get_speckle
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.resources.stream import Stream
from specklepy.transports.server import ServerTransport
from specklepy.objects import Base
from specklepy.objects.other import Collection as SCollection
from specklepy.logging.exceptions import SpeckleException
from specklepy.logging import metrics
from bpy_speckle.specklepy_extras.traversal import TraversalContext
@deprecated
def get_objects_collections(base: Base) -> Dict[str, list]:
"""Create collections based on the dynamic members on a root commit object"""
collections = {}
for name in base.get_dynamic_member_names():
value = base[name]
if isinstance(value, list):
col = get_or_create_collection(name)
collections[name] = get_objects_nested_lists(value, col)
if isinstance(value, Base):
col = get_or_create_collection(name)
collections[name] = get_objects_collections_recursive(value, col)
return collections
@deprecated
def get_objects_nested_lists(items: list, parent_col: Optional[bpy.types.Collection] = None) -> List:
"""For handling the weird nested lists that come from Grasshopper"""
objects = []
if not items:
return objects
if isinstance(items[0], list):
items = list(chain.from_iterable(items))
objects.extend(get_objects_nested_lists(items, parent_col))
else:
objects = [
get_objects_collections_recursive(item, parent_col)
for item in items
if isinstance(item, Base)
]
return objects
@deprecated
def get_objects_collections_recursive(base: Base, parent_col: Optional[bpy.types.Collection] = None) -> List:
"""Recursively create collections based on the dynamic members on nested `Base` objects within the root commit object"""
# if it's a convertable (registered) class and not just a plain `Base`, return the object itself
if can_convert_to_native(base):
return [base]
# if it's an unknown type, try to drill further down to find convertable objects
objects = []
for name in base.get_dynamic_member_names():
value = base[name]
if isinstance(value, list):
objects.extend(item for item in value if isinstance(item, Base))
if isinstance(value, Base):
col = parent_col.children.get(name)
if not col:
col = get_or_create_collection(name)
try:
parent_col.children.link(col)
except:
_report(
f"Problem linking collection {col.name} to parent {parent_col.name}; skipping"
)
objects.append({name: get_objects_collections_recursive(value, col)})
return objects
ObjectCallback = Optional[Callable[[bpy.types.Context, Object, Base], Object]]
ReceiveCompleteCallback = Optional[Callable[[bpy.types.Context, Dict[str, Union[Object, Collection]]], None]]
@@ -136,192 +67,6 @@ def get_receive_funcs(speckle: SpeckleSceneSettings) -> tuple[ObjectCallback, Re
return (objectCallback, receiveCompleteCallback)
@deprecated
def bases_to_native(context: bpy.types.Context, collections: Dict[str, list], scale: float, stream_id: str, func: ObjectCallback = None):
for col_name, objects in collections.items():
col = bpy.data.collections[col_name]
existing = get_existing_collection_objs(col)
if isinstance(objects, dict):
bases_to_native(context, objects, scale, stream_id)
elif isinstance(objects, list):
for obj in objects:
if isinstance(obj, dict):
bases_to_native(context, obj, scale, stream_id, func)
elif isinstance(obj, list): #FIXME: wtf are these nested if statement, can this not be a recursive call?
for item in obj:
if isinstance(item, dict):
bases_to_native(context, item, scale, stream_id, func)
elif isinstance(item, Base):
base_to_native(
context, item, scale, stream_id, col, existing, func
)
elif isinstance(obj, Base):
base_to_native(context, obj, scale, stream_id, col, existing, func)
else:
_report(
f"Something went wrong when receiving collection: {col_name}" #FIXME: undescript report message
)
bpy.context.view_layer.update()
if context.area:
context.area.tag_redraw()
@deprecated
def base_to_native(context: bpy.types.Context,
base: Base,
scale: float,
stream_id: str,
col: bpy.types.Collection,
existing: Dict[str, Object],
func: ObjectCallback = None
):
new_objects = convert_to_native(base)
#NOTE: this code is ancient, and in testing does nothing, so we are removing it.
# if hasattr(base, "properties") and base.properties is not None:
# new_objects.extend(get_speckle_subobjects(base.properties, scale, base.id))
# elif isinstance(base, dict) and "properties" in base.keys():
# new_objects.extend(
# get_speckle_subobjects(base["properties"], scale, base["id"])
# )
"""
Set object Speckle settings
"""
for new_object in new_objects:
if new_object is None:
continue
"""
Run injected function
"""
if func:
new_object = func(context, new_object, base) #this base object isn't always the right one for hosted elements! #TODO: may be it now, need to double check!
if (
new_object is None
): # If the injected function returned None, then we should ignore this object.
_report(f"Script '{func.__module__}' returned None.")
continue
new_object.speckle.stream_id = stream_id
new_object.speckle.send_or_receive = "receive"
if new_object.speckle.object_id in existing.keys():
name = existing[new_object.speckle.object_id].name
existing[new_object.speckle.object_id].name = f"{name}__deleted"
new_object.name = name
col.objects.unlink(existing[new_object.speckle.object_id])
link_object_to_collection_nested(new_object, col)
#if new_object.name not in col.objects:
#col.objects.link(new_object)
def _add_to_heirarchy(converted: Union[Object, Collection], traversalContext : TraversalContext, converted_objects: Dict[str, Union[Object, Collection]]) -> None:
nextParent = traversalContext.parent
# Traverse up the tree to find a direct parent object, and a containing collection
parent_collection: Optional[Collection] = None
parent_object: Optional[Object] = None
while nextParent:
if nextParent.current.id in converted_objects:
c = converted_objects[nextParent.current.id]
if isinstance(c, Collection):
parent_collection = c
break
else: #isinstance(c, Object):
parent_object = parent_object or c
nextParent = nextParent.parent
# If no containing collection is found, fall back to the scene collection
if not parent_collection:
parent_collection = bpy.context.scene.collection
if isinstance(converted, Object):
if parent_object:
converted.parent = parent_object
link_object_to_collection_nested(converted, parent_collection)
elif converted.name not in parent_collection.children.keys():
parent_collection.children.link(converted)
def collection_to_native(collection: SCollection) -> Collection:
name = collection.name or f"{collection.collectionType} -- {collection.applicationId or collection.id}" #TODO: consider consolidating name formatting with Rhino
return get_or_create_collection(name)
def get_or_create_collection(name: str, clear_collection: bool = True) -> Collection:
existing = cast(Collection, bpy.data.collections.get(name))
if existing:
if clear_collection:
for obj in existing.objects:
existing.objects.unlink(obj)
return existing
else:
new_collection = bpy.data.collections.new(name)
#NOTE: We want to not render revit "Rooms" collections by default.
if name == "Rooms":
new_collection.hide_viewport = True
new_collection.hide_render = True
return new_collection
def create_child_collections(parent_col: bpy.types.Collection, children_names: Iterable[str]) -> None:
for name in children_names:
col = get_or_create_collection(name)
parent_col.children.link(col)
@deprecated
def get_existing_collection_objs(col: bpy.types.Collection) -> Dict[str, bpy.types.Object]:
return {
obj.speckle.object_id: obj for obj in col.objects if obj.speckle.object_id != ""
}
def get_collection_parents(collection: bpy.types.Collection, names: list[str]) -> None:
for parent in bpy.data.collections:
if collection.name in parent.children.keys():
# TODO: this should be rethought to make it clear when this is an IFC delim so we know to replace it
# with `/` again on receive
names.append(parent.name.replace("/", "::").replace(".", "::"))
get_collection_parents(parent, names)
def get_collection_hierarchy(collection: Optional[bpy.types.Collection]) -> list[str]:
if not collection:
return []
names = [collection.name.replace("/", "::").replace(".", "::")]
get_collection_parents(collection, names)
return names
def create_nested_hierarchy(base: Base, hierarchy: List[str], objects: Any):
child = base
while hierarchy:
name = hierarchy.pop()
if not hasattr(child, name):
child[name] = Base()
child.add_detachable_attrs({name})
child = child[name]
if not hasattr(child, "@elements"):
child["@elements"] = []
child["@elements"].extend(objects)
return base
#RECEIVE_MODES = [#TODO: modes
# ("create", "Create", "Add new geometry, without removing any existing objects"),
# ("replace", "Replace", "Replace objects from previous receive operations from the same stream"),
@@ -405,6 +150,15 @@ class ReceiveStreamObjects(bpy.types.Operator):
client = speckle_clients[int(speckle.active_user)]
transport = ServerTransport(stream.id, client)
# Fetch commit data
commit_object = operations.receive(commit.referenced_object, transport)
client.commit.received(
stream.id,
commit.id,
source_application="blender",
message="received commit from Speckle Blender",
)
metrics.track(
metrics.RECEIVE,
@@ -413,17 +167,10 @@ class ReceiveStreamObjects(bpy.types.Operator):
"sourceHostApp": host_applications.get_host_app_from_string(commit.source_application).slug,
"sourceHostAppVersion": commit.source_application,
"isMultiplayer": commit.author_id != user.id,
#"connector_version": "unknown", #TODO
},
)
# Fetch commit data
commit_object = operations._untracked_receive(commit.referenced_object, transport)
client.commit.received(
stream.id,
commit.id,
source_application="blender",
message="received commit from Speckle Blender",
)
# Convert received data
context.window_manager.progress_begin(0, commit_object.totalChildrenCount or 1)
@@ -435,8 +182,8 @@ class ReceiveStreamObjects(bpy.types.Operator):
converted_count: int = 0
(object_converted_callback, on_complete_callback) = get_receive_funcs(speckle)
# older commits won't will have a non-collection root object
# for the sake of consistant behaviour, we will wrap anynon-collection commit objects in a collection
# older commits will have a non-collection root object
# for the sake of consistant behaviour, we will wrap any non-collection commit objects in a collection
if not isinstance(commit_object, SCollection):
dummy_commit_object = SCollection()
dummy_commit_object.elements = [commit_object]
@@ -451,9 +198,10 @@ class ReceiveStreamObjects(bpy.types.Operator):
for item in traversalFunc.traverse(commit_object):
current: Base = item.current
if can_convert_to_native(current) or isinstance(current, SCollection):
try:
if not current or not current.id: raise Exception("{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!
converted_data_type: str
@@ -475,7 +223,7 @@ class ReceiveStreamObjects(bpy.types.Operator):
converted_objects[current.id] = converted
_add_to_heirarchy(converted, item, converted_objects)
add_to_hierarchy(converted, item, converted_objects, True)
_report(f"Successfully converted {type(current).__name__} {current.id} as '{converted_data_type}'")
except ConversionSkippedException as ex:
@@ -604,6 +352,16 @@ class SendStreamObjects(bpy.types.Operator):
commit_object = commit_builder.ensure_collection(context.scene.collection)
commit_builder.build_commit_object(commit_object)
metrics.track(
metrics.SEND,
client.account,
custom_props={
"branches": len(stream.branches),
#"collaborators": 0, #TODO:
"isMain": branch.name == "main",
},
)
_report(f"Sending data to {stream.name}")
transport = ServerTransport(stream.id, client)
OBJECT_ID = operations.send(
@@ -649,6 +407,14 @@ class ViewStreamDataApi(bpy.types.Operator):
if not webbrowser.open("%s/streams/%s" % (user.server_url, stream.id), new=2):
raise Exception("Failed to open stream in browser")
metrics.track(
"Connector Action",
None,
custom_props={
"name": "view_stream_data_api"
},
)
class AddStreamFromURL(bpy.types.Operator):
@@ -735,6 +501,14 @@ class AddStreamFromURL(bpy.types.Operator):
if context.area:
context.area.tag_redraw()
metrics.track(
"Connector Action",
client.account,
custom_props={
"name": "add_stream_from_url"
},
)
class CreateStream(bpy.types.Operator):
@@ -795,6 +569,14 @@ class CreateStream(bpy.types.Operator):
if context.area:
context.area.tag_redraw()
metrics.track(
"Connector Action",
client.account,
custom_props={
"name": "create_stream"
},
)
class DeleteStream(bpy.types.Operator):
@@ -861,6 +643,14 @@ class DeleteStream(bpy.types.Operator):
if context.area:
context.area.tag_redraw()
metrics.track(
"Connector Action",
client.account,
custom_props={
"name": "delete_stream"
},
)
class SelectOrphanObjects(bpy.types.Operator):
"""
@@ -886,45 +676,15 @@ class SelectOrphanObjects(bpy.types.Operator):
else:
o.select = False
metrics.track(
"Connector Action",
custom_props={
"name": "SelectOrphanObjects"
},
)
return {"FINISHED"}
@deprecated
class UpdateGlobal(bpy.types.Operator):
"""
DEPRECATED
Update all Speckle objects
"""
bl_idname = "speckle.update_global"
bl_label = "Update Global"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Update all Speckle objects"
client = None
def draw(self, context):
layout = self.layout
row = layout.row()
label = row.label(text="Update everything.")
def execute(self, context):
client = context.scene.speckle.client
profiles = client.load_local_profiles()
if len(profiles) < 1:
raise ValueError("No profiles found.")
client.use_existing_profile(sorted(profiles.keys())[0])
context.scene.speckle.user = sorted(profiles.keys())[0]
for obj in context.scene.objects:
if obj.speckle.enabled:
UpdateObject(context.scene.speckle_client, obj)
context.scene.update()
return {"FINISHED"}
class CopyStreamId(bpy.types.Operator):
"""
Copy stream ID to clipboard
@@ -949,6 +709,12 @@ class CopyStreamId(bpy.types.Operator):
(_, stream) = speckle.validate_stream_selection()
bpy.context.window_manager.clipboard = stream.id
metrics.track(
"Connector Action",
custom_props={
"name": "copy_stream_id"
},
)
class CopyCommitId(bpy.types.Operator):
"""
@@ -974,6 +740,14 @@ class CopyCommitId(bpy.types.Operator):
(_, _, _, commit) = speckle.validate_commit_selection()
bpy.context.window_manager.clipboard = commit.id
metrics.track(
"Connector Action",
custom_props={
"name": "copy_commit_id"
},
)
class CopyBranchName(bpy.types.Operator):
"""
@@ -999,3 +773,10 @@ class CopyBranchName(bpy.types.Operator):
(_, _, branch) = speckle.validate_branch_selection()
bpy.context.window_manager.clipboard = branch.name
metrics.track(
"Connector Action",
custom_props={
"name": "copy_branch_id"
},
)
+56 -19
View File
@@ -4,12 +4,13 @@ User account operators
from typing import cast
import bpy
from bpy.types import Context
from bpy_speckle.functions import _report, get_speckle
from bpy_speckle.functions import _report
from bpy_speckle.clients import speckle_clients
from bpy_speckle.properties.scene import SpeckleCommitObject, SpeckleSceneSettings, SpeckleUserObject
from specklepy.api.client import SpeckleClient
from specklepy.api.models import Stream
from specklepy.api.credentials import get_local_accounts
from bpy_speckle.properties.scene import SpeckleCommitObject, SpeckleSceneSettings, SpeckleUserObject, get_speckle
from specklepy.core.api.client import SpeckleClient
from specklepy.core.api.models import Stream
from specklepy.core.api.credentials import get_local_accounts
from specklepy.logging import metrics
class ResetUsers(bpy.types.Operator):
"""
@@ -23,6 +24,14 @@ class ResetUsers(bpy.types.Operator):
def execute(self, context):
self.reset_ui(context)
metrics.track(
"Connector Action",
None,
custom_props={
"name": "ResetUsers"
},
)
bpy.context.view_layer.update()
if context.area:
context.area.tag_redraw()
@@ -56,6 +65,17 @@ class LoadUsers(bpy.types.Operator):
profiles = get_local_accounts()
active_user_index = 0
metrics.track(
"Connector Action",
None,
custom_props={
"name": "LoadUsers",
},
)
if not profiles:
raise Exception("Zero accounts were found, please add one through Speckle Manager or a local account")
for profile in profiles:
user = users.add()
user.server_name = profile.serverInfo.name or "Speckle Server"
@@ -74,16 +94,24 @@ class LoadUsers(bpy.types.Operator):
client.authenticate_with_account(profile)
speckle_clients.append(client)
except Exception as ex:
_report(ex)
_report(f"Failed to authenticate user {user.email} with server {user.server_url}: {ex}")
users.remove(len(users) - 1)
if profile.isDefault:
active_user_index = len(users) - 1
speckle.active_user = str(active_user_index)
_report(f"Authenticated {len(users)}/{len(profiles)} accounts")
if active_user_index < len(users):
speckle.active_user = str(active_user_index)
bpy.context.view_layer.update()
if context.area:
context.area.tag_redraw()
if not users:
raise Exception("Zero valid user accounts were found, please ensure account is valid and the server is running")
return {"FINISHED"}
@@ -122,7 +150,7 @@ def add_user_stream(user: SpeckleUserObject, stream: Stream):
class LoadUserStreams(bpy.types.Operator):
"""
Load all available streams for active user user
Load all available streams for active user
"""
bl_idname = "speckle.load_user_streams"
@@ -130,41 +158,50 @@ class LoadUserStreams(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
bl_description = "(Re)load all available user streams"
stream_limit: int = 20
branch_limit: int = 20
def execute(self, context):
try:
self.add_stream_from_url(context)
self.load_user_stream(context)
return {"FINISHED"}
except Exception as ex:
_report(f"{self.bl_idname} failed: {ex}")
return {"CANCELLED"}
def add_stream_from_url(self, context: Context) -> None:
def load_user_stream(self, context: Context) -> None:
speckle = get_speckle(context)
user = speckle.validate_user_selection()
client = speckle_clients[int(speckle.active_user)]
try:
streams = client.stream.list(stream_limit=20)
except Exception as e:
_report(f"Failed to retrieve streams: {e}")
return
streams = client.stream.list(stream_limit=self.stream_limit)
except Exception as ex:
raise Exception(f"Failed to retrieve streams") from ex
if not streams:
_report("Failed to retrieve streams.")
raise Exception("Zero streams found")
return
user.streams.clear()
default_units = "Meters"
for s in streams:
assert(s.id)
sstream = client.stream.get(id=s.id, branch_limit=20)
sstream = client.stream.get(id=s.id, branch_limit=self.branch_limit)
add_user_stream(user, sstream)
bpy.context.view_layer.update()
if context.area:
context.area.tag_redraw()
metrics.track(
"Connector Action",
client.account,
custom_props={
"name": "LoadUserStreams"
},
)
+4 -2
View File
@@ -13,7 +13,6 @@ from bpy.props import (
PointerProperty,
)
class SpeckleSceneObject(bpy.types.PropertyGroup):
name: bpy.props.StringProperty(default="")
@@ -191,4 +190,7 @@ class SpeckleSceneSettings(bpy.types.PropertyGroup):
return (user, stream, branch, commit)
class SelectionException(Exception):
pass
pass
def get_speckle(context: bpy.types.Context) -> SpeckleSceneSettings:
return context.scene.speckle #type: ignore
@@ -1,83 +0,0 @@
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Any, Collection, Dict, Generic, Iterable, List, Optional, Tuple, TypeVar
from attrs import define
from specklepy.objects.base import Base
ROOT: str = "__Root"
T = TypeVar('T')
PARENT_INFO = Tuple[Optional[str], str]
@define(slots=True)
class CommitObjectBuilder(ABC, Generic[T]):
converted: Dict[str, Base]
_parent_infos: Dict[str, Collection[PARENT_INFO]]
def __init__(self) -> None:
self.converted = {}
self._parent_infos = {}
@abstractmethod
def include_object(self, conversion_result: Base, native_object: T) -> None:
pass
def build_commit_object(self, root_commit_object: Base) -> None:
self.apply_relationships(self.converted.values(), root_commit_object)
def set_relationship(self, app_id: Optional[str], *parent_info : PARENT_INFO) -> None:
if not app_id:
return
self._parent_infos[app_id] = parent_info
def apply_relationships(self, to_add: Iterable[Base], root_commit_object: Base) -> None:
for c in to_add:
try:
self.apply_relationship(c, root_commit_object)
except Exception as ex:
print(f"Failed to add object {type(c)} to commit object: {ex}")
def apply_relationship(self, current: Base, root_commit_object: Base):
if not current.applicationId: raise Exception(f"Expected applicationId to have been set")
parents = self._parent_infos[current.applicationId]
for (parent_id, prop_name) in parents:
if not parent_id: continue
parent: Optional[Base]
if parent_id == ROOT:
parent = root_commit_object
else:
parent = self.converted[parent_id] if parent_id in self.converted else None
if not parent: continue
try:
elements = get_detached_prop(parent, prop_name)
if not isinstance(elements, list):
elements = []
set_detached_prop(parent, prop_name, elements)
elements.append(current)
return
except Exception as ex:
# A parent was found, but it was invalid (Likely because of a type mismatch on a `elements` property)
print(f"Failed to add object {type(current)} to a converted parent; {ex}")
raise Exception(f"Could not find a valid parent for object of type {type(current)}. Checked {len(parents)} potential parent, and non were converted!")
def get_detached_prop(speckle_object: Base, prop_name: str) -> Optional[Any]:
detached_prop_name = get_detached_prop_name(speckle_object, prop_name)
return getattr(speckle_object, detached_prop_name, None)
def set_detached_prop(speckle_object: Base, prop_name: str, value: Optional[Any]) -> None:
detached_prop_name = get_detached_prop_name(speckle_object, prop_name)
setattr(speckle_object, detached_prop_name, value)
def get_detached_prop_name(speckle_object: Base, prop_name: str) -> str:
return prop_name if hasattr(speckle_object, prop_name) else f"@{prop_name}"
-121
View File
@@ -1,121 +0,0 @@
from typing import Any, Callable, Collection, Iterable, Iterator, List, Optional, Set
from attrs import define
from typing_extensions import Protocol, final
from specklepy.objects import Base
class ITraversalRule(Protocol):
def get_members_to_traverse(self, o: Base) -> Set[str]:
"""Get the members to traverse."""
pass
def does_rule_hold(self, o: Base) -> bool:
"""Make sure the rule still holds."""
pass
@final
class DefaultRule:
def get_members_to_traverse(self, _) -> Set[str]:
return set()
def does_rule_hold(self, _) -> bool:
return True
# we're creating a local protected "singleton"
_default_rule = DefaultRule()
@final
@define(slots=True, frozen=True)
class TraversalContext:
current: Base
member_name: Optional[str] = None
parent: Optional["TraversalContext"] = None
@final
@define(slots=True, frozen=True)
class GraphTraversal:
_rules: List[ITraversalRule]
def traverse(self, root: Base) -> Iterator[TraversalContext]:
stack: List[TraversalContext] = []
stack.append(TraversalContext(root))
while len(stack) > 0:
head = stack.pop()
yield head
current = head.current
active_rule = self._get_active_rule_or_default_rule(current)
members_to_traverse = active_rule.get_members_to_traverse(current)
for child_prop in members_to_traverse:
try:
if child_prop in {"speckle_type", "units", "applicationId"}: continue #debug: to avoid noisy exceptions, explicitly avoid checking ones we know will fail, this is not exhaustive
value = current[child_prop]
self._traverse_member_to_stack(
stack, value, child_prop, head
)
except KeyError as ex:
# Unset application ids, and class variables like SpeckleType will throw when __getitem__ is called
pass
@staticmethod
def _traverse_member_to_stack(
stack: List[TraversalContext],
value: Any,
member_name: Optional[str] = None,
parent: Optional[TraversalContext] = None,
):
if isinstance(value, Base):
stack.append(TraversalContext(value, member_name, parent))
elif isinstance(value, list):
for obj in value:
GraphTraversal._traverse_member_to_stack(stack, obj, member_name, parent)
elif isinstance(value, dict):
for obj in value.values():
GraphTraversal._traverse_member_to_stack(stack, obj, member_name, parent)
@staticmethod
def traverse_member(value: Optional[Any]) -> Iterator[Base]:
if isinstance(value, Base):
yield value
elif isinstance(value, list):
for obj in value:
for o in GraphTraversal.traverse_member(obj):
yield o
elif isinstance(value, dict):
for obj in value.values():
for o in GraphTraversal.traverse_member(obj):
yield o
def _get_active_rule_or_default_rule(self, o: Base) -> ITraversalRule:
return self._get_active_rule(o) or _default_rule
def _get_active_rule(self, o: Base) -> Optional[ITraversalRule]:
for rule in self._rules:
if rule.does_rule_hold(o):
return rule
return None
@final
@define(slots=True, frozen=True)
class TraversalRule:
_conditions: Collection[Callable[[Base], bool]]
_members_to_traverse: Callable[[Base], Iterable[str]]
def get_members_to_traverse(self, o: Base) -> Set[str]:
return set(self._members_to_traverse(o))
def does_rule_hold(self, o: Base) -> bool:
for condition in self._conditions:
if condition(o):
return True
return False
Generated
+723 -585
View File
File diff suppressed because it is too large Load Diff
+2 -3
View File
@@ -7,18 +7,17 @@ license = "Apache-2.0"
[tool.poetry.dependencies]
python = ">=3.8, <4.0.0"
specklepy = "^2.14.0"
specklepy = "^2.16.2"
attrs = "^23.1.0"
# [tool.poetry.group.local_specklepy.dependencies]
# specklepy = {path = "../specklepy", develop = true}
[tool.poetry.group.dev.dependencies]
numpy = "^1.23.5"
fake-bpy-module-latest = "^20230117"
black = "^22.10.0"
pylint = "^2.15.7"
ruff = "^0.0.166"
ruff = "^0.0.187"
[build-system]
requires = ["poetry-core>=1.0.0"]