Compare commits

...

34 Commits

Author SHA1 Message Date
Jedd Morgan a26618a4f7 Merge pull request #164 from specklesystems/2.14/bug-fixes
2.14/bug fixes
2023-05-29 13:39:06 +01:00
Jedd Morgan eaf370407d Updated lock 2023-05-29 13:37:49 +01:00
Jedd Morgan a2b50fe5a1 Added support for sending diffuse BSDF shader materials 2023-05-29 13:33:53 +01:00
Jedd Morgan 7e62f76841 fix: fixed bug with imperial units scaling on send 2023-05-29 12:40:54 +01:00
Jedd Morgan fc804f16d3 Fixed bug with circular referenced custom props 2023-05-29 12:40:34 +01:00
Jedd Morgan 6c7da24595 Merge pull request #163 from specklesystems/collections
Collections
2023-05-27 13:44:19 +01:00
Jedd Morgan b284d39328 removed normal re-calculation 2023-05-27 13:43:42 +01:00
Jedd Morgan 907185c9bb Object naming tweaks 2023-05-26 18:58:42 +01:00
Jedd Morgan a189a2e1c0 Various cleanup and bug fixes 2023-05-26 18:18:10 +01:00
Jedd Morgan 1fad926275 Remove empty collections from send 2023-05-25 21:01:34 +01:00
Jedd Morgan 99c147fe2f Sending collections (all collections regardless of contents) 2023-05-25 17:45:31 +01:00
Jedd Morgan e2adf710b3 commit object builder 2023-05-25 00:22:09 +01:00
Jedd Morgan 9509344533 Added traversal refactor and support for receiving collections 2023-05-18 22:15:35 +01:00
Jedd Morgan 6fabc6cae6 feat(converter): implemented view to native 2023-05-10 17:31:18 +01:00
Jedd Morgan c39298687d Merge pull request #160 from specklesystems/jrm/ismultiplayer
Added `isMultiplayer` property
2023-04-13 14:26:29 +01:00
Jedd Morgan bcdddbf930 Added isMultiplayer property 2023-04-13 14:24:17 +01:00
Jedd Morgan b5684e34f6 Merge pull request #159 from specklesystems/jrm/curve-fix
Removed merge vertices by distance from clean mesh
2023-04-05 12:51:27 +01:00
Jedd Morgan 2203fe98f8 Removed merge vertices by distance from clean mesh 2023-04-05 12:47:03 +01:00
Jedd Morgan bbfdf2863b Merge pull request #158 from specklesystems/jrm/curve-fix
Using new installer.py
2023-04-04 20:22:48 +01:00
Jedd Morgan f25f6cb16c Fixed some misc issues 2023-04-04 20:21:20 +01:00
Jedd Morgan 9e4e533ba8 Using new installer.py 2023-03-28 17:06:40 +01:00
Jedd Morgan 8db12ca9b9 Merge pull request #157 from specklesystems/jrm/curve-fix
Mesh area calc + minor cleanup
2023-03-28 16:47:59 +01:00
Jedd Morgan 366c864247 Mesh area calc + minor cleanup 2023-03-28 16:47:07 +01:00
Jedd Morgan 52136d3ef6 Merge pull request #155 from specklesystems/jrm/instances
Implemented support for new instances
2023-03-21 22:01:46 +00:00
Jedd Morgan fe764d7f0c removed old comment 2023-03-21 21:58:07 +00:00
Jedd Morgan 669dd67521 Added option to receive instances as linked duplicates 2023-03-21 21:45:53 +00:00
Jedd Morgan f74b2c37f0 Implemented support for new instances 2023-03-19 03:08:57 +00:00
Jedd Morgan ebb4e32fff Merge pull request #151 from specklesystems/jrm/curve-fix
Fixes many bugs with curves
2023-03-09 14:52:28 +00:00
Jedd Morgan 25903baf83 Set default mesh smoothing to True 2023-02-21 19:19:31 +00:00
Jedd Morgan cb6d6d7ad8 feat(converter): Added support for Ellipse and Circles 2023-02-21 19:17:14 +00:00
Jedd Morgan fd2687aa3c All non-bezier nurbs curves now work perfectly rhino->blend->rhino 2023-02-20 22:26:11 +00:00
Jedd Morgan f5c65068de feat(converter): Better curve support 2023-02-18 02:23:41 +00:00
Jedd Morgan a1ec137c67 minor cleanup 2023-02-15 16:40:49 +00:00
Jedd Morgan b95f621272 Aligned nurbs knot multiplicities with rhino curves 2023-02-15 16:37:48 +00:00
17 changed files with 2513 additions and 1419 deletions
+7 -3
View File
@@ -1,7 +1,7 @@
import bpy
from bpy_speckle.installer import ensure_dependencies
ensure_dependencies()
ensure_dependencies(f"Blender {bpy.app.version[0]}.{bpy.app.version[1]}")
from specklepy.logging import metrics
@@ -37,8 +37,12 @@ loading a Blender file
@persistent
def load_handler(dummy):
pass
#bpy.ops.speckle.users_load() #this is an expensive operation, one that forces the user to wait every time blender loads. Until we can do this non-blocking, we will make the user hit the refresh button each time.
# Calling users_load is an expensive operation, one that force users to wait a good 10s every time blender loads.
# Until we can do this non-blocking, we will make the user hit the refresh button each time.
#bpy.ops.speckle.users_load()
# Instead, we shall just reset the user selection to an uninitiailised state
bpy.ops.speckle.users_reset()
"""
Permanent handle on callbacks
@@ -0,0 +1,150 @@
from typing import Deque, Dict, List, Optional, Set, 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 import Base
from specklepy.objects.other import Collection as SCollection
from attrs import define
ELEMENTS = "elements"
def _id(natvive_object: ID) -> str:
#NOTE: to avoid naming collisions, we prefix collections and objects differently
return f"{type(natvive_object).__name__}:{natvive_object.name_full}"
def _try_id(natvive_object: Optional[Union[Collection, Object]]) -> Optional[str]:
return _id(natvive_object) if natvive_object else None
def convert_collection_to_speckle(col: Collection) -> SCollection:
convered_collection = SCollection(name = col.name_full, collectionType = "Blender Collection", elements = [])
convered_collection.applicationId = _id(col)
return convered_collection
@define(slots=True)
class BlenderCommitObjectBuilder(CommitObjectBuilder[Object]):
_collections: Dict[str, SCollection]
def __init__(self) -> None:
super().__init__()
self._collections = {}
def include_object(self, conversion_result: Base, native_object: Object) -> None:
# Set the Child -> Parent relationships
parent = native_object.parent
parent_collections: Tuple[Collection] = native_object.users_collection # type: ignore
parent_collection = parent_collections[0] if len(parent_collections) > 0 else None #NOTE: we don't support objects appearing in more than one collection, for now, we will just take the zeroth one
app_id = _id(native_object)
conversion_result.applicationId = app_id
self.converted[app_id] = conversion_result
# in order or priority, direct parent, direct parent collection, root
self.set_relationship(app_id, (_try_id(parent), ELEMENTS), (_try_id(parent_collection), ELEMENTS), (ROOT, ELEMENTS))
# if parent_collection:
# self._include_collection(parent_collection)
def ensure_collection(self, col: Collection) -> SCollection:
id = _id(col)
if id in self._collections:
return self._collections[id] # collection already converted!
# Set the Parent -> Children relationships
for c in col.children:
#NOTE: There's no falling back to the grandparent, if the direct parent collection wasn't converted, then we we fallback to the root
self.set_relationship(_id(c), (id, ELEMENTS), (ROOT, ELEMENTS))
# Set Child -> Parent relationship
# parent = self.find_collection_parent(col)
# self.set_relationship(id, (_try_builder_id(parent), ELEMENTS), (ROOT, ELEMENTS))
convered_collection = convert_collection_to_speckle(col)
self.converted[id] = convered_collection
self._collections[id] = convered_collection
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:
self.ensure_collection(col)
objects_to_build = set(self.converted.values())
objects_to_build.remove(root_commit_object)
self.apply_relationships(objects_to_build, root_commit_object)
assert(isinstance(root_commit_object, SCollection))
# Kill unused collections
def should_remove_unuseful_collection(col: SCollection) -> bool: #TODO: this maybe could be optimised
elements = col.elements
if not elements: return True
should_remove_this_col = True
i = 0
while i < len(elements):
c = elements[i]
if not isinstance(c, SCollection):
# col has objects (c)
should_remove_this_col = False
i += 1
continue
if should_remove_unuseful_collection(c):
# c is not useful, kill it
del elements[i]
else:
# col has a child (c) with objects
should_remove_this_col = False
i += 1
continue
return should_remove_this_col
if should_remove_unuseful_collection(root_commit_object):
_report("WARNING: Only empty collections have been converted!") #TODO: consider raising exception here, to halt the send operation
# 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
+432 -212
View File
@@ -1,14 +1,22 @@
import math
from typing import Iterable, Union, Collection
from bpy_speckle.convert.to_speckle import transform_to_speckle
from typing import Tuple, Union, Collection
from bpy_speckle.functions import get_scale_length, _report
import mathutils
import bpy, bmesh, bpy_types
from specklepy.objects.other import *
from mathutils import (
Matrix as MMatrix,
Vector as MVector,
Quaternion as MQuaternion,
)
import bpy, bmesh
from specklepy.objects.other import (
Instance,
Transform,
BlockDefinition,
)
from specklepy.objects.geometry import *
from bpy.types import Object
from .util import (
get_render_material,
link_object_to_collection_nested,
render_material_to_native,
add_custom_properties,
add_vertices,
@@ -17,171 +25,203 @@ from .util import (
add_uv_coords,
)
SUPPORTED_CURVES = (Line, Polyline, Curve, Arc, Polycurve)
SUPPORTED_CURVES = (Line, Polyline, Curve, Arc, Polycurve, Ellipse, Circle)
CAN_CONVERT_TO_NATIVE = (
Mesh,
*SUPPORTED_CURVES,
transform_to_speckle,
BlockDefinition,
BlockInstance,
Instance,
)
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_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 type(speckle_object) in CAN_CONVERT_TO_NATIVE:
if(_has_native_convesion(speckle_object) or _has_fallback_conversion(speckle_object)):
return True
for alias in DISPLAY_VALUE_PROPERTY_ALIASES:
if getattr(speckle_object, alias, None):
return True
_report(f"Could not convert unsupported Speckle object: {speckle_object}")
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
convert_instances_as = value
#TODO: Check usages handle exceptions
def convert_to_native(speckle_object: Base) -> Object:
def convert_to_native(speckle_object: Base) -> list[Object]:
speckle_type = type(speckle_object)
speckle_name = generate_object_name(speckle_object)
try:
scale = get_scale_factor(speckle_object)
obj_data: Optional[Union[bpy.types.ID, bpy.types.Object, mathutils.Matrix]] = None
converted: list[Object] = []
object_name = _generate_object_name(speckle_object)
scale = get_scale_factor(speckle_object)
# convert elements/breps
if speckle_type not in CAN_CONVERT_TO_NATIVE:
(obj_data, converted) = display_value_to_native(speckle_object, speckle_name, scale)
converted: Union[bpy.types.ID, bpy.types.Object, None] = None
children: list[Object] = []
# convert supported geometry
elif isinstance(speckle_object, Mesh):
obj_data = mesh_to_native(speckle_object, speckle_name, scale)
elif speckle_type in SUPPORTED_CURVES:
obj_data = icurve_to_native(speckle_object, speckle_name, scale)
elif isinstance(speckle_object, Transform):
obj_data = transform_to_native(speckle_object, scale)
elif isinstance(speckle_object, BlockDefinition):
obj_data = block_def_to_native(speckle_object)
elif isinstance(speckle_object, BlockInstance):
obj_data = block_instance_to_native(speckle_object, scale)
# convert elements/breps
if not _has_native_convesion(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}")
# convert supported geometry
elif isinstance(speckle_object, Mesh):
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 isinstance(speckle_object, Instance):
if convert_instances_as == "linked_duplicates":
(converted, children) = instance_to_native_object(speckle_object, scale)
elif convert_instances_as == "collection_instance":
converted = instance_to_native_collection_instance(speckle_object, scale)
else:
_report(f"Unsupported type {speckle_type}")
return []
except Exception as ex: # conversion error
_report(f"Error converting {speckle_object} \n{ex}")
return []
if speckle_name in bpy.data.objects.keys():
blender_object = bpy.data.objects[speckle_name]
blender_object.data = (
obj_data.data if isinstance(obj_data, Object) else obj_data
)
blender_object.matrix_world = (
blender_object.matrix_world
if speckle_type is BlockInstance
else mathutils.Matrix()
)
if hasattr(obj_data, "materials"):
blender_object.data.materials.clear()
_report(f"convert_instances_as = '{convert_instances_as}' is not implemented, Instances will be converted as collection instances!")
converted = instance_to_native_collection_instance(speckle_object, scale)
else:
blender_object = (
obj_data
if isinstance(obj_data, Object)
else bpy.data.objects.new(speckle_name, obj_data)
)
raise Exception(f"Unsupported type {speckle_type}")
blender_object.speckle.object_id = str(speckle_object.id)
blender_object.speckle.enabled = True
add_custom_properties(speckle_object, blender_object)
for child in converted:
child.parent = blender_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
add_custom_properties(speckle_object, converted)
for child in children:
child.parent = converted
converted.append(blender_object)
return converted
DISPLAY_VALUE_PROPERTY_ALIASES = ["displayValue", "@displayValue"]
ELEMENTS_PROPERTY_ALIASES = ["elements", "@elements"]
def generate_object_name(speckle_object: Base) -> str:
prefix = (getattr(speckle_object, "name", None)
or getattr(speckle_object, "Name", None)
or speckle_object.speckle_type.rsplit(':')[-1])
return f"{prefix} -- {speckle_object.id}"
def get_scale_factor(speckle_object: Base, fallback: float = 1.0) -> float:
scale = fallback
if units := getattr(speckle_object, "units", None):
scale = get_scale_length(units) / bpy.context.scene.unit_settings.scale_length
return scale
DISPLAY_VALUE_PROPERTY_ALIASES = ["displayValue", "@displayValue", "displayMesh", "@displayMesh", "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)
def elements_to_native(speckle_object: Base, name: str, scale: float) -> list[bpy.types.Object]:
(_, elements) = _members_to_native(speckle_object, name, scale, ELEMENTS_PROPERTY_ALIASES, False)
return elements
def _members_to_native(speckle_object: Base, name: str, scale: float, members: List[str], combineMeshes: bool) -> tuple[Optional[bpy.types.Mesh], list[bpy.types.Object]]:
"""
Converts mesh displayValues as one mesh
Converts non-mesh displayValues as child Objects
Converts a given speckle_object by converting specified members
if combineMeshes == True
Converts mesh members as one mesh
Converts non-mesh members as child Objects
if combineMeshes == False
Converts all members as child objects (first item of the returned tuple will be None)
:returns: converted mesh, and any other converted child objects (may happen if members contained non-meshes)
"""
meshes: list[Mesh] = []
elements: list[Base] = []
others: list[Base] = []
#NOTE: raw Mesh elements will be treated like displayValues, which is not ideal, but no connector sends raw Mesh elements so its fine
for alias in DISPLAY_VALUE_PROPERTY_ALIASES:
for alias in members:
display = getattr(speckle_object, alias, None)
count = 0
max_depth = 255
def seperate(value: Any) -> None:
nonlocal meshes, elements, count, max_depth
MAX_DEPTH = 255 # some large value, to prevent infinite reccursion
def seperate(value: Any) -> bool:
nonlocal meshes, others, count, MAX_DEPTH
if isinstance(value, Mesh):
if combineMeshes and isinstance(value, Mesh):
meshes.append(value)
elif isinstance(value, Base):
elements.append(value)
others.append(value)
elif isinstance(value, list):
count += 1
if(count > max_depth):
return
if(count > MAX_DEPTH):
return True
for x in value:
seperate(x)
seperate(display)
return False
did_halt = seperate(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?")
converted: list[Object] = []
children: list[Object] = []
mesh = None
if meshes:
mesh = meshes_to_native(speckle_object, meshes, name, scale)
mesh = meshes_to_native(speckle_object, meshes, name, scale) #TODO: reconsider passing scale around...
# add parent type here so we can use it as a blender custom prop
# not making it hidden, so it will get added on send as i think it might be helpful? can reconsider
for item in elements:
item.parent_speckle_type = speckle_object.speckle_type
blender_object = convert_to_native(item)
if isinstance(blender_object, list):
converted.extend(blender_object)
else:
add_custom_properties(speckle_object, blender_object)
converted.append(blender_object)
for item in others:
try:
blender_object = convert_to_native(item)
children.append(blender_object)
except Exception as ex:
_report(f"Failed to convert display value {item}: {ex}")
if not elements and not meshes:
_report(f"Unsupported type {speckle_object.speckle_type}")
return (mesh, children)
return (mesh, converted)
def view_to_native(speckle_view, name: str, scale: float) -> bpy.types.Object:
native_cam: bpy.types.Camera
if name in bpy.data.cameras.keys():
native_cam = bpy.data.cameras[name]
else:
native_cam = bpy.data.cameras.new(name=name)
native_cam.lens = 18 # 90° horizontal fov
cam_obj = create_new_object(native_cam, name)
scale_factor = get_scale_factor(speckle_view, scale)
tx = (speckle_view.origin.x * scale_factor)
ty = (speckle_view.origin.y * scale_factor)
tz = (speckle_view.origin.z * scale_factor)
forward = MVector((speckle_view.forwardDirection.x, speckle_view.forwardDirection.y, speckle_view.forwardDirection.z))
up = MVector((speckle_view.upDirection.x, speckle_view.upDirection.y, speckle_view.upDirection.z))
right = forward.cross(up).normalized()
cam_obj.matrix_world = MMatrix((
(right.x, up.x, -forward.x, tx),
(right.y, up.y, -forward.y, ty),
(right.z, up.z, -forward.z, tz),
(0, 0, 0, 1 )
))
return cam_obj
def mesh_to_native(speckle_mesh: Mesh, name: str, scale: float) -> bpy.types.Mesh:
return meshes_to_native(speckle_mesh, [speckle_mesh], name, scale)
def meshes_to_native(element: Base, meshes: Collection[Mesh], name: str, scale: float) -> bpy.types.Mesh:
if name in bpy.data.meshes.keys():
blender_mesh = bpy.data.meshes[name]
else:
blender_mesh = bpy.data.meshes.new(name=name)
return bpy.data.meshes[name]
blender_mesh = bpy.data.meshes.new(name=name)
fallback_material = get_render_material(element)
@@ -214,15 +254,19 @@ def meshes_to_native(element: Base, meshes: Collection[Mesh], name: str, scale:
add_colors(mesh, bm)
add_uv_coords(mesh, bm)
bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
bm.to_mesh(blender_mesh)
bm.free()
return blender_mesh
"""
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 []
line = blender_curve.splines.new("POLY")
line.points.add(1)
@@ -233,97 +277,90 @@ def line_to_native(speckle_curve: Line, blender_curve: bpy.types.Curve, scale: f
1,
)
if speckle_curve.end:
line.points[1].co = (
float(speckle_curve.end.x) * scale,
float(speckle_curve.end.y) * scale,
float(speckle_curve.end.z) * scale,
1,
)
line.points[1].co = (
float(speckle_curve.end.x) * scale,
float(speckle_curve.end.y) * scale,
float(speckle_curve.end.z) * scale,
return [line]
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.points.add(N - 1)
for i in range(N):
polyline.points[i].co = (
float(value[i * 3]) * scale,
float(value[i * 3 + 1]) * scale,
float(value[i * 3 + 2]) * scale,
1,
)
return [line]
return []
def polyline_to_native(scurve: Polyline, bcurve: bpy.types.Curve, scale: float) -> list[bpy.types.Spline]:
if value := scurve.value:
N = len(value) // 3
polyline = bcurve.splines.new("POLY")
if hasattr(scurve, "closed"):
polyline.use_cyclic_u = scurve.closed
# if "closed" in scurve.keys():
# polyline.use_cyclic_u = scurve["closed"]
polyline.points.add(N - 1)
for i in range(N):
polyline.points[i].co = (
float(value[i * 3]) * scale,
float(value[i * 3 + 1]) * scale,
float(value[i * 3 + 2]) * scale,
1,
)
return [polyline]
return []
return [polyline]
def nurbs_to_native(scurve: Curve, bcurve: bpy.types.Curve, scale: float) -> list[bpy.types.Spline]:
if points := scurve.points:
N = len(points) // 3
if not (points := scurve.points): return []
# 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_endpoint_u = not scurve.periodic
nurbs.points.add(num_points - 1)
use_weights = len(scurve.weights) >= num_points
for i in range(num_points):
nurbs.points[i].co = (
float(points[i * 3]) * scale,
float(points[i * 3 + 1]) * scale,
float(points[i * 3 + 2]) * scale,
1,
)
nurbs = bcurve.splines.new("NURBS")
nurbs.points[i].weight = scurve.weights[i] if use_weights else 1
if hasattr(scurve, "closed"):
nurbs.use_cyclic_u = scurve.closed != 0
nurbs.order_u = scurve.degree + 1
nurbs.points.add(N - 1)
for i in range(N):
nurbs.points[i].co = (
float(points[i * 3]) * scale,
float(points[i * 3 + 1]) * scale,
float(points[i * 3 + 2]) * scale,
1,
)
if len(scurve.weights) == len(nurbs.points):
for i, w in enumerate(scurve.weights):
nurbs.points[i].weight = w
# TODO: anaylize curve knots to decide if use_endpoint_u or use_bezier_u should be enabled
# nurbs.use_endpoint_u = True
nurbs.order_u = scurve.degree + 1
return [nurbs]
return []
return [nurbs]
def arc_to_native(rcurve: Arc, bcurve: bpy.types.Curve, scale: float) -> Optional[bpy.types.Spline]:
# TODO: improve Blender representation of arc
# TODO: improve Blender representation of arc - check autocad test stream
plane = rcurve.plane
if not plane:
return None
normal = mathutils.Vector([plane.normal.x, plane.normal.y, plane.normal.z])
normal = MVector([plane.normal.x, plane.normal.y, plane.normal.z])
radius = rcurve.radius * scale
startAngle = rcurve.startAngle
endAngle = rcurve.endAngle
startQuat = mathutils.Quaternion(normal, startAngle)
endQuat = mathutils.Quaternion(normal, endAngle)
startQuat = MQuaternion(normal, startAngle)
endQuat = MQuaternion(normal, endAngle)
# Get start and end vectors, centre point, angles, etc.
r1 = mathutils.Vector([plane.xdir.x, plane.xdir.y, plane.xdir.z])
r1 = MVector([plane.xdir.x, plane.xdir.y, plane.xdir.z])
r1.rotate(startQuat)
r2 = mathutils.Vector([plane.xdir.x, plane.xdir.y, plane.xdir.z])
r2 = MVector([plane.xdir.x, plane.xdir.y, plane.xdir.z])
r2.rotate(endQuat)
c = mathutils.Vector([plane.origin.x, plane.origin.y, plane.origin.z]) * scale
c = MVector([plane.origin.x, plane.origin.y, plane.origin.z]) * scale
spt = c + r1 * radius
ept = c + r2 * radius
@@ -339,7 +376,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 = mathutils.Quaternion(normal, step)
stepQuat = MQuaternion(normal, step)
tan = math.tan(step / 2) * radius
arc.points.add(Ndiv + 1)
@@ -379,6 +416,55 @@ def polycurve_to_native(scurve: Polycurve, bcurve: bpy.types.Curve, scale: float
_report(f"Unsupported curve type: {speckle_type}")
return curves
def ellipse_to_native(ellipse: Union[Ellipse, Circle], bcurve: bpy.types.Curve, units_scale: float) -> list[bpy.types.Spline]:
radX: float
radY: float
if isinstance(ellipse, Ellipse):
radX = ellipse.firstRadius * units_scale
radY = ellipse.secondRadius * units_scale
else:
radX = ellipse.radius * units_scale
radY = ellipse.radius * units_scale
D = 0.5522847498307936 # (4/3)*tan(pi/8)
right_handles = [
(+radX, +radY * D, 0.0),
(-radX * D, +radY, 0.0),
(-radX, -radY * D, 0.0),
(+radX * D, -radY, 0.0),
]
left_handles = [
(+radX, -radY * D, 0.0),
(+radX * D, +radY, 0.0),
(-radX, +radY * D, 0.0),
(-radX * D, -radY, 0.0),
]
points = [
(+radX, 0.0, 0.0),
(0.0, +radY, 0.0),
(-radX, 0.0, 0.0),
(0.0, -radY, 0.0),
]
transform = plane_to_native_transform(ellipse.plane, units_scale)
spline = bcurve.splines.new("BEZIER")
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.use_cyclic_u = True
#TODO support trims?
return [spline]
def icurve_to_native_spline(speckle_curve: Base, blender_curve: bpy.types.Curve, scale: float) -> list[bpy.types.Spline]:
@@ -391,36 +477,42 @@ def icurve_to_native_spline(speckle_curve: Base, blender_curve: bpy.types.Curve,
spline = line_to_native(speckle_curve, blender_curve, scale)
elif isinstance(speckle_curve, Curve):
spline = nurbs_to_native(speckle_curve, blender_curve, scale)
elif isinstance(speckle_curve,Polyline):
elif isinstance(speckle_curve, Polyline):
spline = polyline_to_native(speckle_curve, blender_curve, scale)
elif isinstance(speckle_curve, Arc):
spline = arc_to_native(speckle_curve, blender_curve, scale)
spline = arc_to_native(speckle_curve, blender_curve, scale)
elif isinstance(speckle_curve, Ellipse) or isinstance(speckle_curve, Circle):
spline = 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 []
def icurve_to_native(speckle_curve: Base, name: str, scale: float) -> Optional[bpy.types.Curve]:
def icurve_to_native(speckle_curve: Base, name: str, scale: float) -> bpy.types.Curve:
curve_type = type(speckle_curve)
if curve_type not in SUPPORTED_CURVES:
_report(f"Unsupported curve type: {curve_type}")
return None
raise Exception(f"Unsupported curve type: {curve_type}")
blender_curve = (
bpy.data.curves[name]
if name in bpy.data.curves.keys()
else bpy.data.curves.new(name, type="CURVE")
)
blender_curve.dimensions = "3D"
blender_curve.resolution_u = 12
blender_curve.resolution_u = 12 #TODO: We could maybe decern the resolution from the ployline displayValue
icurve_to_native_spline(speckle_curve, blender_curve, scale)
return blender_curve
def transform_to_native(transform: Transform, scale: float) -> mathutils.Matrix:
mat = mathutils.Matrix(
"""
Transforms and Intances
"""
def transform_to_native(transform: Transform, scale: float) -> MMatrix:
mat = MMatrix(
[
transform.value[:4],
transform.value[4:8],
@@ -433,38 +525,166 @@ def transform_to_native(transform: Transform, scale: float) -> mathutils.Matrix:
mat[i][3] *= scale
return mat
def plane_to_native_transform(plane: Plane, fallback_scale:float = 1) -> MMatrix:
scale_factor = get_scale_factor(plane, fallback_scale)
tx = (plane.origin.x * scale_factor)
ty = (plane.origin.y * scale_factor)
tz = (plane.origin.z * scale_factor)
def block_def_to_native(definition: BlockDefinition) -> bpy.types.Collection:
native_def = bpy.data.collections.get(definition.name)
return MMatrix((
(plane.xdir.x, plane.ydir.x, plane.normal.x, tx),
(plane.xdir.y, plane.ydir.y, plane.normal.y, ty),
(plane.xdir.z, plane.ydir.z, plane.normal.z, tz),
(0, 0, 0, 1 )
))
"""
Instances / Blocks
"""
def _get_instance_name(instance: Instance) -> str:
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}"
def instance_to_native_object(instance: Instance, scale: float) -> Tuple[bpy.types.Object, List[bpy.types.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")
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.empty_display_size = 0
for geo in definition.geometry:
native_elements.append(convert_to_native(geo))
else:
native_instance = convert_to_native(instance.definition)
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
def instance_to_native_collection_instance(instance: Instance, scale: float) -> bpy.types.Object:
"""
Convert an Instance as a transformed Object with the `instance_collection` property
set to be the `instance.Definition` converted as a collection
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")
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)
#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
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)
"""
name = _generate_object_name(definition)
native_def = bpy.data.collections.get(name)
if native_def:
return native_def
native_def = bpy.data.collections.new(definition.name)
native_def = bpy.data.collections.new(name)
native_def["applicationId"] = definition.applicationId
for geo in definition.geometry:
if b_obj := convert_to_native(geo):
native_def.objects.link(
b_obj
if isinstance(b_obj, bpy_types.Object)
else bpy.data.objects.new(b_obj.name, b_obj)
)
#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)
return native_def
def block_instance_to_native(instance: BlockInstance, scale: float) -> bpy.types.Object:
"""
Convert BlockInstance to native
"""
name = f"{getattr(instance, 'name', None) or instance.blockDefinition.name} -- {instance.id}"
native_def = block_def_to_native(instance.blockDefinition)
"""
Object Naming
"""
native_instance = bpy.data.objects.new(name, None)
add_custom_properties(instance, native_instance)
native_instance["name"] = getattr(instance, 'name', None) or instance.blockDefinition.name
# hide the instance axes so they don't clutter the viewport
native_instance.empty_display_size = 0
native_instance.instance_collection = native_def
native_instance.instance_type = "COLLECTION"
native_instance.matrix_world = transform_to_native(instance.transform, scale)
return native_instance
def _get_friendly_object_name(speckle_object: Base) -> Optional[str]:
return (getattr(speckle_object, "name", None)
or getattr(speckle_object, "Name", None)
or getattr(speckle_object, "family", None)
)
# Blender object names must not exceed 62 characters
# We need to ensure the complete ID is included in the name (to prevent identity collisions)
# So we if the name is too long, we need to truncate
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)
return name[:MAX_NAME_LENGTH]
def _simplified_speckle_type(speckle_type: str) -> str:
return(speckle_type.rsplit('.')[-1]) #Take only the most specific object type name (without namespace)
def _generate_object_name(speckle_object: Base) -> str:
prefix: str
name = _get_friendly_object_name(speckle_object)
if name:
prefix = _truncate_object_name(name)
else:
prefix = _simplified_speckle_type(speckle_object.speckle_type)
return f"{prefix}{OBJECT_NAME_SEPERATOR}{speckle_object.id}"
def get_scale_factor(speckle_object: Base, fallback: float = 1.0) -> float:
scale = fallback
if units := getattr(speckle_object, "units", None):
scale = get_scale_length(units) / bpy.context.scene.unit_settings.scale_length
return scale
+263 -118
View File
@@ -1,62 +1,92 @@
from typing import Dict, Iterable, Optional, Tuple
from typing import Dict, Iterable, List, Optional, Tuple, Union, cast
import bpy
from bpy.types import Depsgraph, Material, MeshPolygon, Object
from specklepy.objects.geometry import Mesh, Curve, Interval, Box, Point, Polyline
from specklepy.objects.other import *
from bpy_speckle.functions import _report
from bpy.types import (
Depsgraph,
MeshPolygon,
Object,
Curve as NCurve,
Mesh as NMesh,
)
from deprecated import deprecated
from mathutils.geometry import interpolate_bezier
from mathutils import (
Matrix as MMatrix,
Vector as MVector,
)
from specklepy.objects import Base
from specklepy.objects.other import BlockInstance, BlockDefinition, RenderMaterial, Transform
from specklepy.objects.geometry import (
Mesh, Curve, Interval, Box, Point, Polyline
)
from bpy_speckle.convert.to_native import OBJECT_NAME_SEPERATOR, SPECKLE_ID_LENGTH
from bpy_speckle.convert.util import (
get_blender_custom_properties,
make_knots,
nurb_make_curve,
to_argb_int,
)
from bpy_speckle.functions import _report
UNITS = "m"
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")
def convert_to_speckle(blender_object: Object, scale: float, units: str, desgraph: Optional[Depsgraph]) -> Optional[list]:
global UNITS
UNITS = units
blender_type = blender_object.type
def convert_to_speckle(raw_blender_object: Object, units_scale: float, units: str, depsgraph: Optional[Depsgraph]) -> Base:
"""
Converts supported 1 blender objects to 1 speckle object (potentially with children)
:param raw_blender_object: the blender object (unevaluated by a Depsgraph) to convert
:param units_scale: The scale factor conversions need to apply to position data to get to the desired units
:param units: The desired final units to send
:param depsgraph: Optional depsgraph if provided will evaluate modifiers on geometry data
:return: The Converted blender object
"""
global Units, UnitsScale
Units = units
UnitsScale = units_scale
blender_type = raw_blender_object.type
if blender_type not in CAN_CONVERT_TO_SPECKLE:
return None
raise ConversionSkippedException(f"Objects of type {blender_type} are not supported")
speckle_objects = []
# speckle_material = material_to_speckle_old(blender_object) #TODO: What about curves with materials...
if desgraph:
blender_object = blender_object.evaluated_get(desgraph)
converted = None
blender_object = cast(Object, (
raw_blender_object.evaluated_get(depsgraph)
if depsgraph
else raw_blender_object
))
converted: Optional[Base] = None
if blender_type == "MESH":
converted = mesh_to_speckle(blender_object, blender_object.data, scale)
converted = mesh_to_speckle(blender_object, cast(NMesh, blender_object.data))
elif blender_type == "CURVE":
converted = icurve_to_speckle(blender_object, blender_object.data, scale)
converted = curve_to_speckle(blender_object, cast(NCurve, blender_object.data))
elif blender_type == "EMPTY":
converted = empty_to_speckle(blender_object, scale)
converted = empty_to_speckle(blender_object)
if not converted:
return None
raise Exception("Conversion returned None")
if isinstance(converted, list):
speckle_objects.extend([c for c in converted if c != None])
else:
speckle_objects.append(converted)
converted["properties"] = get_blender_custom_properties(raw_blender_object) #NOTE: Depsgraph copies don't have custom properties so we use the raw version
for so in speckle_objects:
so.properties = get_blender_custom_properties(blender_object)
so.applicationId = so.properties.pop("applicationId", None)
# Set object transform #TODO: this could be deprecated once we add proper geometry instancing support
if blender_type != "EMPTY":
converted["properties"]["transform"] = transform_to_speckle(
blender_object.matrix_world
)
return converted
# Set object transform
if blender_type != "EMPTY":
so.properties["transform"] = transform_to_speckle(
blender_object.matrix_world
)
def mesh_to_speckle(blender_object: Object, data: bpy.types.Mesh) -> Base:
b = Base()
b["name"] = to_speckle_name(blender_object)
b["@displayValue"] = mesh_to_speckle_meshes(blender_object, data)
return b
return speckle_objects
def mesh_to_speckle(blender_object: Object, data: bpy.types.Mesh, scale: float = 1.0) -> List[Mesh]:
#if data.loop_triangles is None or len(data.loop_triangles) < 1:
# data.calc_loop_triangles()
def mesh_to_speckle_meshes(blender_object: Object, data: bpy.types.Mesh) -> List[Mesh]:
# Categorise polygons by material index
submesh_data: Dict[int, List[MeshPolygon]] = {}
@@ -66,8 +96,8 @@ def mesh_to_speckle(blender_object: Object, data: bpy.types.Mesh, scale: float =
submesh_data[p.material_index] = []
submesh_data[p.material_index].append(p)
transform = blender_object.matrix_world
scaled_vertices = [tuple(transform @ x.co * scale) for x in data.vertices]
transform = cast(MMatrix, blender_object.matrix_world)
scaled_vertices = [tuple(transform @ x.co * UnitsScale) for x in data.vertices]
# Create Speckle meshes for each material
submeshes = []
@@ -77,12 +107,15 @@ def mesh_to_speckle(blender_object: Object, data: bpy.types.Mesh, scale: float =
#Loop through each polygon, and map indicies to their new index in m_verts
mesh_area = 0
m_verts: List[float] = []
m_faces: List[int] = []
m_texcoords: List[float] = []
for face in submesh_data[i]:
u_indices = face.vertices
m_faces.append(len(u_indices))
mesh_area += face.area
for u_index in u_indices:
if u_index not in index_mapping:
# Create mapping between index in blender mesh, and new index in speckle submesh
@@ -94,7 +127,8 @@ def mesh_to_speckle(blender_object: Object, data: bpy.types.Mesh, scale: float =
if data.uv_layers.active:
vt = data.uv_layers.active.data[index_counter]
m_texcoords.extend([vt.uv.x, vt.uv.y])
uv = cast(MVector, vt.uv)
m_texcoords.extend([uv.x, uv.y])
m_faces.append(index_mapping[u_index])
index_counter += 1
@@ -104,7 +138,8 @@ def mesh_to_speckle(blender_object: Object, data: bpy.types.Mesh, scale: float =
faces=m_faces,
colors=[],
textureCoordinates=m_texcoords,
units=UNITS,
units=Units,
area = mesh_area,
bbox=Box(area=0.0, volume=0.0),
)
@@ -117,24 +152,23 @@ def mesh_to_speckle(blender_object: Object, data: bpy.types.Mesh, scale: float =
return submeshes
def bezier_to_speckle(matrix: List[float], spline: bpy.types.Spline, scale: float, name: Optional[str] = None) -> Curve:
def bezier_to_speckle(matrix: MMatrix, spline: bpy.types.Spline, name: Optional[str] = None) -> Curve:
degree = 3
closed = spline.use_cyclic_u
points = []
points: List[Tuple[MVector]] = []
for i, bp in enumerate(spline.bezier_points):
if i > 0:
points.append(tuple(matrix @ bp.handle_left * scale))
points.append(tuple(matrix @ bp.co * scale))
points.append(tuple(matrix @ bp.handle_left * UnitsScale))
points.append(tuple(matrix @ bp.co * UnitsScale))
if i < len(spline.bezier_points) - 1:
points.append(tuple(matrix @ bp.handle_right * scale))
points.append(tuple(matrix @ bp.handle_right * UnitsScale))
if closed:
points.extend(
(
tuple(matrix @ spline.bezier_points[-1].handle_right * scale),
tuple(matrix @ spline.bezier_points[0].handle_left * scale),
tuple(matrix @ spline.bezier_points[0].co * scale),
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),
)
)
@@ -155,51 +189,131 @@ def bezier_to_speckle(matrix: List[float], spline: bpy.types.Spline, scale: floa
name=name,
degree=degree,
closed=spline.use_cyclic_u,
periodic=spline.use_cyclic_u,
periodic= not spline.use_endpoint_u,
points=flattend_points,
weights=[1] * num_points,
knots=knots,
rational=False,
rational=True,
area=0,
volume=0,
length=length,
domain=domain,
units=UNITS,
units=Units,
bbox=Box(area=0.0, volume=0.0),
displayValue = bezier_to_speckle_polyline(matrix, spline, length),
)
def nurbs_to_speckle(matrix: List[float], spline: bpy.types.Spline, scale: float, name: Optional[str] = None) -> Curve:
knots = make_knots(spline)
points = [tuple(matrix @ pt.co.xyz * scale) for pt in spline.points]
def nurbs_to_speckle(matrix: MMatrix, spline: bpy.types.Spline, name: Optional[str] = None) -> Curve:
degree = spline.order_u - 1
knots = make_knots(spline)
length = spline.calc_length()
domain = Interval(start=0, end=length, totalChildrenCount=0)
weights = [pt.weight for pt in spline.points]
is_rational = all(w == weights[0] for w in weights)
points = [tuple(matrix @ pt.co.xyz * UnitsScale) for pt in spline.points]
flattend_points = []
for row in points: flattend_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])
for i in range(0, degree):
weights.append(weights[i])
return Curve(
name=name,
degree=degree,
closed=spline.use_cyclic_u,
periodic=spline.use_cyclic_u,
periodic= not spline.use_endpoint_u,
points=flattend_points,
weights=[pt.weight for pt in spline.points],
weights=weights,
knots=knots,
rational=False,
rational=is_rational,
area=0,
volume=0,
length=length,
domain=domain,
units=UNITS,
units=Units,
bbox=Box(area=0.0, volume=0.0),
displayValue=nurbs_to_speckle_polyline(matrix, spline, length),
)
def nurbs_to_speckle_polyline(matrix: MMatrix, spline: bpy.types.Spline, length: Optional[float] = None) -> Polyline:
"""
Samples a nurbs curve with resolution_u creating a polyline
"""
points = []
sampled_points = nurb_make_curve(spline, spline.resolution_u, 3)
for i in range(0, len(sampled_points), 3):
scaled_point = matrix @ MVector((
sampled_points[i + 0],
sampled_points[i + 1],
sampled_points[i + 2])) * UnitsScale
def poly_to_speckle(matrix: List[float], spline: bpy.types.Spline, scale: float, name: Optional[str] = None) -> Polyline:
points = [tuple(matrix @ pt.co.xyz * scale) for pt in spline.points]
points.append(scaled_point.x)
points.append(scaled_point.y)
points.append(scaled_point.z)
length = length or spline.calc_length()
domain = Interval(start=0, end=length, totalChildrenCount=0)
return Polyline(value=points, closed = spline.use_cyclic_u, domain=domain, area=0, len=length)
#Inspired by https://blender.stackexchange.com/a/689 (CC BY-SA 3.0)
def bezier_to_speckle_polyline(matrix: MMatrix, spline: bpy.types.Spline, length: Optional[float] = None) -> Optional[Polyline]:
"""
Samples a Bézier curve with resolution_u creating a polyline
"""
segments = len(spline.bezier_points)
if segments < 2: return None
R = spline.resolution_u + 1
points = []
if not spline.use_cyclic_u:
segments -= 1
points: List[float] = []
for i in range(segments):
inext = (i + 1) % len(spline.bezier_points)
knot1 = spline.bezier_points[i].co
handle1 = spline.bezier_points[i].handle_right
handle2 = spline.bezier_points[inext].handle_left
knot2 = spline.bezier_points[inext].co
_points = interpolate_bezier(knot1, handle1, handle2, knot2, R)
for p in _points:
scaled_point = matrix @ p * UnitsScale
points.append(scaled_point.x)
points.append(scaled_point.y)
points.append(scaled_point.z)
length = length or spline.calc_length()
domain = Interval(start=0, end=length, totalChildrenCount=0)
return Polyline(value=points, closed = spline.use_cyclic_u, domain=domain, area=0, len=length)
_QUICK_TEST_NAME_LENGTH = SPECKLE_ID_LENGTH + len(OBJECT_NAME_SEPERATOR)
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
if does_name_contain_id:
return blender_object.name.rsplit(OBJECT_NAME_SEPERATOR, 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]
flattend_points = []
for row in points: flattend_points.extend(row)
@@ -214,40 +328,48 @@ def poly_to_speckle(matrix: List[float], spline: bpy.types.Spline, scale: float,
domain=domain,
bbox=Box(area=0.0, volume=0.0),
area=0,
units=UNITS,
units=Units,
)
def icurve_to_speckle(blender_object: Object, data: bpy.types.Curve, scale=1.0) -> Optional[List[Base]]:
UNITS = "m" if bpy.context.scene.unit_settings.system == "METRIC" else "ft"
def curve_to_speckle(blender_object: Object, data: bpy.types.Curve) -> Base:
b = Base()
(meshes, curves) = curve_to_speckle_geometry(blender_object, data)
if meshes:
b["@displayValue"] = meshes
if blender_object.type != "CURVE":
return None
b["name"] = to_speckle_name(blender_object)
b["@elements"] = curves
return b
blender_object = blender_object.evaluated_get(bpy.context.view_layer.depsgraph)
def curve_to_speckle_geometry(blender_object: Object, data: bpy.types.Curve) -> Tuple[List[Mesh], List[Base]]:
assert(blender_object.type == "CURVE")
mat = blender_object.matrix_world
blender_object = cast(Object, blender_object.evaluated_get(bpy.context.view_layer.depsgraph))
curves = []
matrix = cast(MMatrix, blender_object.matrix_world)
meshes: List[Mesh] = []
curves: List[Base] = []
#TODO: Could we support this better?
if data.bevel_mode == "OBJECT" and data.bevel_object != None:
mesh = mesh_to_speckle(blender_object, blender_object.to_mesh(), scale)
curves.extend(mesh)
meshes = mesh_to_speckle_meshes(blender_object, blender_object.to_mesh())
for spline in data.splines:
if spline.type == "BEZIER":
curves.append(bezier_to_speckle(mat, spline, scale, blender_object.name))
curves.append(bezier_to_speckle(matrix, spline, to_speckle_name(blender_object)))
elif spline.type == "NURBS":
curves.append(nurbs_to_speckle(mat, spline, scale, blender_object.name))
curves.append(nurbs_to_speckle(matrix, spline, to_speckle_name(blender_object)))
elif spline.type == "POLY":
curves.append(poly_to_speckle(mat, spline, scale, blender_object.name))
curves.append(poly_to_speckle(matrix, spline, to_speckle_name(blender_object)))
return curves
return (meshes, curves)
def ngons_to_speckle_polylines(blender_object: Object, data: bpy.types.Mesh, scale=1.0) -> Optional[List[Polyline]]:
@deprecated
def ngons_to_speckle_polylines(blender_object: Object, data: bpy.types.Mesh) -> Optional[List[Polyline]]:
UNITS = "m" if bpy.context.scene.unit_settings.system == "METRIC" else "ft"
if blender_object.type != "MESH":
@@ -260,13 +382,13 @@ def ngons_to_speckle_polylines(blender_object: Object, data: bpy.types.Mesh, sca
for i, poly in enumerate(data.polygons):
value = []
for v in poly.vertices:
value.extend(mat @ verts[v].co * scale)
value.extend(mat @ verts[v].co * UnitsScale)
domain = Interval(start=0, end=1)
poly = Polyline(
name="{}_{}".format(blender_object.name, i),
closed=True,
value=value, # magic (flatten list of tuples)
value=value,
length=0,
domain=domain,
bbox=Box(area=0.0, volume=0.0),
@@ -283,24 +405,30 @@ def material_to_speckle(blender_mat: bpy.types.Material) -> RenderMaterial:
speckle_mat = RenderMaterial()
speckle_mat.name = blender_mat.name
if blender_mat.use_nodes is True and blender_mat.node_tree.nodes.get(
"Principled BSDF"
):
inputs = blender_mat.node_tree.nodes["Principled BSDF"].inputs
speckle_mat.diffuse = to_argb_int(inputs["Base Color"].default_value)
speckle_mat.emissive = to_argb_int(inputs["Emission"].default_value)
speckle_mat.roughness = inputs["Roughness"].default_value
speckle_mat.metalness = inputs["Metallic"].default_value
speckle_mat.opacity = inputs["Alpha"].default_value
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
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
return speckle_mat
#TODO: Support more shaders
else:
speckle_mat.diffuse = to_argb_int(blender_mat.diffuse_color)
speckle_mat.metalness = blender_mat.metallic
speckle_mat.roughness = blender_mat.roughness
# fallback to standard material props
speckle_mat.diffuse = to_argb_int(blender_mat.diffuse_color)
speckle_mat.metalness = blender_mat.metallic
speckle_mat.roughness = blender_mat.roughness
return speckle_mat
@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):
@@ -313,48 +441,65 @@ def material_to_speckle_old(blender_object: Object) -> Optional[RenderMaterial]:
return material_to_speckle(blender_mat)
def transform_to_speckle(blender_transform: Iterable[Iterable[float]], scale=1.0) -> Transform:
value = [y for x in blender_transform for y in x]
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
value = [y for x in iterable_transform for y in x]
# scale the translation
for i in (3, 7, 11):
value[i] *= scale
value[i] *= UnitsScale
return Transform(value=value, units=UNITS)
return Transform(value=value, units=Units)
def block_def_to_speckle(blender_definition: bpy.types.Collection, scale=1.0) -> BlockDefinition:
def block_def_to_speckle(blender_definition: bpy.types.Collection) -> BlockDefinition:
geometry = []
for geo in blender_definition.objects:
geometry.extend(convert_to_speckle(geo, scale, UNITS, None))
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))
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}'")
block_def = BlockDefinition(
units=UNITS,
name=blender_definition.name,
units=Units,
name=to_speckle_name(blender_definition),
geometry=geometry,
basePoint=Point(units=UNITS),
basePoint=Point(units=Units),
)
blender_props = get_blender_custom_properties(blender_definition)
block_def.applicationId = blender_props.pop("applicationId", None)
# blender_props = get_blender_custom_properties(blender_definition)
# block_def.applicationId = blender_props.pop("applicationId", None) #TODO: remove?
return block_def
def block_instance_to_speckle(blender_instance: Object, scale=1.0) -> BlockInstance:
def block_instance_to_speckle(blender_instance: Object) -> BlockInstance:
return BlockInstance(
blockDefinition=block_def_to_speckle(
blender_instance.instance_collection, scale
blender_instance.instance_collection
),
transform=transform_to_speckle(blender_instance.matrix_world),
name=blender_instance.name,
units=UNITS,
name=to_speckle_name(blender_instance),
units=Units,
)
def empty_to_speckle(blender_object: Object, scale=1.0) -> Optional[BlockInstance]:
def empty_to_speckle(blender_object: Object) -> Union[BlockInstance, Base]:
# probably an instance collection (block) so let's try it
try:
geo = blender_object.instance_collection.objects.items()
return block_instance_to_speckle(blender_object, scale)
except AttributeError as err:
_report(
f"No instance collection found in empty. Skipping object {blender_object.name}"
)
return None
if blender_object.instance_collection and blender_object.instance_type == "COLLECTION":
return block_instance_to_speckle(blender_object)
else:
#raise ConversionSkippedException("Sending non-collection instance empties are not currently supported")
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"
#return matrix_to_speckle_point(cast(MMatrix, blender_object.matrix_world))
def matrix_to_speckle_point(matrix: MMatrix, units_scale: float = 1.0) -> Point:
transformed_pos = cast(MVector, matrix @ MVector((0,0,0)) * units_scale)
return Point(x = transformed_pos.x,
y = transformed_pos.y,
z = transformed_pos.z)
+136 -27
View File
@@ -4,7 +4,7 @@ from bmesh.types import BMesh
import bpy, struct, idprop
from specklepy.objects.base import Base
from specklepy.objects.geometry import Mesh
from specklepy.objects.geometry import Circle, Mesh, Ellipse
from specklepy.objects.other import RenderMaterial
from bpy_speckle.functions import _report
from bpy.types import Material, Object
@@ -138,7 +138,7 @@ def add_vertices(speckle_mesh: Mesh, blender_mesh: BMesh, scale=1.0):
def add_faces(speckle_mesh: Mesh, blender_mesh: BMesh, indexOffset: int, materialIndex: int = 0, smooth:bool = False):
def add_faces(speckle_mesh: Mesh, blender_mesh: BMesh, indexOffset: int, materialIndex: int = 0, smooth:bool = True):
sfaces = speckle_mesh.faces
if sfaces and len(sfaces) > 0:
@@ -235,7 +235,7 @@ ignored_keys = {
"_chunkable",
}
def get_blender_custom_properties(obj, max_depth=1000):
def get_blender_custom_properties(obj, max_depth: int = 200):
if max_depth < 0:
return obj
@@ -252,27 +252,23 @@ def get_blender_custom_properties(obj, max_depth=1000):
return obj
"""
Python implementation of Blender's NURBS curve generation for to Speckle conversion
from: https://blender.stackexchange.com/a/34276
based on https://projects.blender.org/blender/blender/src/branch/main/source/blender/blenkernel/intern/curve.cc (check old version)
"""
def macro_knotsu(nu: bpy.types.Spline) -> int:
return nu.order_u + nu.point_count_u + (nu.order_u - 1 if nu.use_cyclic_u else 0)
def macro_segmentsu(nu: bpy.types.Spline) -> int:
return nu.point_count_u if nu.use_cyclic_u else nu.point_count_u - 1
def make_knots(nu: bpy.types.Spline) -> list[float]:
knots = [0.0] * (4 + macro_knotsu(nu))
knots = [0.0] * macro_knotsu(nu)
flag = nu.use_endpoint_u + (nu.use_bezier_u << 1)
if nu.use_cyclic_u:
calc_knots(knots, nu.point_count_u, nu.order_u, 0)
makecyclicknots(knots, nu.point_count_u, nu.order_u)
else:
calc_knots(knots, nu.point_count_u, nu.order_u, flag)
return knots
@@ -280,13 +276,13 @@ def make_knots(nu: bpy.types.Spline) -> list[float]:
def calc_knots(knots: list[float], point_count: int, order: int, flag: int) -> None:
pts_order = point_count + order
if flag == 1:
if flag == 1: # CU_NURB_ENDPOINT
k = 0.0
for a in range(1, pts_order + 1):
knots[a - 1] = k
if a >= order and a <= point_count:
k += 1.0
elif flag == 2:
elif flag == 2: # CU_NURB_BEZIER
if order == 4:
k = 0.34
for a in range(pts_order):
@@ -299,24 +295,137 @@ def calc_knots(knots: list[float], point_count: int, order: int, flag: int) -> N
k += 0.5
knots[a] = math.floor(k)
else:
for a in range(pts_order):
knots[a] = a
for a in range(1, len(knots) - 1):
knots[a] = a - 1
knots[-1] = knots[-2]
def makecyclicknots(knots: list[float], point_count: int, order: int) -> None:
order2 = order - 1
def basis_nurb(t: float, order: int, point_count: int, knots: list[float], basis: list[float], start: int, end: int) -> Tuple[int, int]:
i1 = i2 = 0
orderpluspnts = order + point_count
opp2 = orderpluspnts - 1
if order > 2:
b = point_count + order2
for a in range(1, order2):
if knots[b] != knots[b - a]:
break
# this is for float inaccuracy
if t < knots[0]:
t = knots[0]
elif t > knots[opp2]:
t = knots[opp2]
if a == order2:
knots[point_count + order - 2] += 1.0
# this part is order '1'
o2 = order + 1
for i in range(opp2):
if knots[i] != knots[i + 1] and t >= knots[i] and t <= knots[i + 1]:
basis[i] = 1.0
i1 = i - o2
if i1 < 0:
i1 = 0
i2 = i
i += 1
while i < opp2:
basis[i] = 0.0
i += 1
break
b = order
c = point_count + order + order2
for a in range(point_count + order2, c):
knots[a] = knots[a - 1] + (knots[b] - knots[b - 1])
b -= 1
else:
basis[i] = 0.0
basis[i] = 0.0
# this is order 2, 3, ...
for j in range(2, order + 1):
if i2 + j >= orderpluspnts:
i2 = opp2 - j
for i in range(i1, i2 + 1):
if basis[i] != 0.0:
d = ((t - knots[i]) * basis[i]) / (knots[i + j - 1] - knots[i])
else:
d = 0.0
if basis[i + 1] != 0.0:
e = ((knots[i + j] - t) * basis[i + 1]) / (knots[i + j] - knots[i + 1])
else:
e = 0.0
basis[i] = d + e
start = 1000
end = 0
for i in range(i1, i2 + 1):
if basis[i] > 0.0:
end = i
if start == 1000:
start = i
return start, end
def nurb_make_curve(nu: bpy.types.Spline, resolu: int, stride: int = 3) -> list[float]:
""""BKE_nurb_makeCurve"""
EPS = 1e-6
coord_index = istart = iend = 0
coord_array = [0.0] * (3 * nu.resolution_u * macro_segmentsu(nu))
sum_array = [0] * nu.point_count_u
basisu = [0.0] * macro_knotsu(nu)
knots = make_knots(nu)
resolu = resolu * macro_segmentsu(nu)
ustart = knots[nu.order_u - 1]
uend = knots[nu.point_count_u + nu.order_u - 1] if nu.use_cyclic_u else \
knots[nu.point_count_u]
ustep = (uend - ustart) / (resolu - (0 if nu.use_cyclic_u else 1))
cycl = nu.order_u - 1 if nu.use_cyclic_u else 0
u = ustart
while resolu:
resolu -= 1
istart, iend = basis_nurb(u, nu.order_u, nu.point_count_u + cycl, knots, basisu, istart, iend)
#/* calc sum */
sumdiv = 0.0
sum_index = 0
pt_index = istart - 1
for i in range(istart, iend + 1):
if i >= nu.point_count_u:
pt_index = i - nu.point_count_u
else:
pt_index += 1
sum_array[sum_index] = basisu[i] * nu.points[pt_index].co[3]
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_index += 1
coord_array[coord_index: coord_index + 3] = (0.0, 0.0, 0.0)
sum_index = 0
pt_index = istart - 1
for i in range(istart, iend + 1):
if i >= nu.point_count_u:
pt_index = i - nu.point_count_u
else:
pt_index += 1
if sum_array[sum_index] != 0.0:
for j in range(3):
coord_array[coord_index + j] += sum_array[sum_index] * nu.points[pt_index].co[j]
sum_index += 1
coord_index += stride
u += ustep
return coord_array
def link_object_to_collection_nested(obj: bpy.types.Object, col: bpy.types.Collection):
if obj.name not in col.objects:
col.objects.link(obj)
for child in obj.children:
link_object_to_collection_nested(child, col)
+27 -18
View File
@@ -1,4 +1,10 @@
from bpy_speckle.clients import speckle_clients
from typing import Callable, Set
import bpy
from specklepy.objects.base import Base
from bpy_speckle.properties.scene import SpeckleSceneSettings
from bpy_speckle.specklepy_extras.traversal import GraphTraversal, TraversalRule
"""
Speckle functions
@@ -44,29 +50,32 @@ def get_scale_length(units: str) -> float:
Client, user, and stream functions
"""
elements_aliases: Set[str] = {"elements", "@elements"}
ignore_props: Set[str] = {"@blockDefinition", "displayValue", "@displayValue", "units", "id", "applicationId"}
def _check_speckle_client_user_stream(scene):
def get_default_traversal_func(can_convert_to_native: Callable[[Base], bool]) -> GraphTraversal:
"""
Verify that there is a valid user and stream
Traversal func for traversing a speckle commit object
"""
speckle = scene.speckle
user = (
speckle.users[int(speckle.active_user)]
if len(speckle.users) > int(speckle.active_user)
else None
convertable_rule = TraversalRule(
[can_convert_to_native],
lambda _: [i for i in elements_aliases if i not in ignore_props],
)
if user is None:
print("No users loaded.")
stream = (
user.streams[user.active_stream]
if len(user.streams) > user.active_stream
else None
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
lambda _: [],
)
if stream is None:
print("Account contains no streams.")
default_rule = TraversalRule(
[lambda _: True],
lambda o: o.get_member_names(), #TODO: avoid deprecated members
)
return (user, stream)
return GraphTraversal([convertable_rule, ignore_result_rule, default_rule])
def get_speckle(context: bpy.types.Context) -> 'SpeckleSceneSettings':
return context.scene.speckle #type: ignore
+113 -34
View File
@@ -1,29 +1,118 @@
"""
Provides uniform and consistent path helpers for `specklepy`
"""
import os
import sys
from pathlib import Path
from typing import Optional
from importlib import import_module, invalidate_caches
import bpy
import sys
_user_data_env_var = "SPECKLE_USERDATA_PATH"
print("Starting Speckle Blender installation")
def _path() -> Optional[Path]:
"""Read the user data path override setting."""
path_override = os.environ.get(_user_data_env_var)
if path_override:
return Path(path_override)
return None
_application_name = "Speckle"
def override_application_name(application_name: str) -> None:
"""Override the global Speckle application name."""
global _application_name
_application_name = application_name
def override_application_data_path(path: Optional[str]) -> None:
"""
Override the global Speckle application data path.
If the value of path is `None` the environment variable gets deleted.
"""
if path:
os.environ[_user_data_env_var] = path
else:
os.environ.pop(_user_data_env_var, None)
def _ensure_folder_exists(base_path: Path, folder_name: str) -> Path:
path = base_path.joinpath(folder_name)
path.mkdir(exist_ok=True, parents=True)
return path
def user_application_data_path() -> Path:
"""Get the platform specific user configuration folder path"""
path_override = _path()
if path_override:
return path_override
try:
if sys.platform.startswith("win"):
app_data_path = os.getenv("APPDATA")
if not app_data_path:
raise Exception(
"Cannot get appdata path from environment."
)
return Path(app_data_path)
else:
# try getting the standard XDG_DATA_HOME value
# as that is used as an override
app_data_path = os.getenv("XDG_DATA_HOME")
if app_data_path:
return Path(app_data_path)
else:
return _ensure_folder_exists(Path.home(), ".config")
except Exception as ex:
raise Exception(
"Failed to initialize user application data path.", ex
)
def user_speckle_folder_path() -> Path:
"""Get the folder where the user's Speckle data should be stored."""
return _ensure_folder_exists(user_application_data_path(), _application_name)
def user_speckle_connector_installation_path(host_application: str) -> Path:
"""
Gets a connector specific installation folder.
In this folder we can put our connector installation and all python packages.
"""
return _ensure_folder_exists(
_ensure_folder_exists(user_speckle_folder_path(), "connector_installations"),
host_application,
)
print("Starting module dependency installation")
print(sys.executable)
PYTHON_PATH = sys.executable
def modules_path() -> Path:
modules_path = Path(bpy.utils.script_path_user(), "addons", "modules")
modules_path.mkdir(exist_ok=True, parents=True)
def connector_installation_path(host_application: str) -> Path:
connector_installation_path = user_speckle_connector_installation_path(host_application)
connector_installation_path.mkdir(exist_ok=True, parents=True)
# set user modules path at beginning of paths for earlier hit
if sys.path[1] != modules_path:
sys.path.insert(1, str(modules_path))
if sys.path[0] != connector_installation_path:
sys.path.insert(0, str(connector_installation_path))
return modules_path
print(f"Using connector installation path {connector_installation_path}")
return connector_installation_path
print(f"Found blender modules path {modules_path()}")
def is_pip_available() -> bool:
try:
@@ -34,7 +123,7 @@ def is_pip_available() -> bool:
def ensure_pip() -> None:
print("Installing pip... "),
print("Installing pip... ")
from subprocess import run
@@ -43,7 +132,7 @@ def ensure_pip() -> None:
if completed_process.returncode == 0:
print("Successfully installed pip")
else:
raise Exception("Failed to install pip.")
raise Exception(f"Failed to install pip, got {completed_process.returncode} return code")
def get_requirements_path() -> Path:
@@ -53,11 +142,11 @@ def get_requirements_path() -> Path:
return path
def install_requirements() -> None:
def install_requirements(host_application: str) -> None:
# set up addons/modules under the user
# script path. Here we'll install the
# dependencies
path = modules_path()
path = connector_installation_path(host_application)
print(f"Installing Speckle dependencies to {path}")
from subprocess import run
@@ -78,20 +167,16 @@ def install_requirements() -> None:
)
if completed_process.returncode != 0:
print("Please try manually installing speckle-blender")
raise Exception(
"""
Failed to install speckle-blender.
See console for manual install instruction.
"""
)
m = f"Failed to install dependenices through pip, got {completed_process.returncode} return code"
print(m)
raise Exception(m)
def install_dependencies() -> None:
def install_dependencies(host_application: str) -> None:
if not is_pip_available():
ensure_pip()
install_requirements()
install_requirements(host_application)
def _import_dependencies() -> None:
@@ -110,19 +195,13 @@ def _import_dependencies() -> None:
# print(req)
# import_module("specklepy")
def ensure_dependencies() -> None:
def ensure_dependencies(host_application: str) -> None:
try:
install_dependencies()
install_dependencies(host_application)
invalidate_caches()
_import_dependencies()
print("Found all dependencies, proceed with loading")
print("Successfully found dependencies")
except ImportError:
raise Exception(
"Cannot automatically ensure Speckle dependencies. Please restart Blender!"
)
raise Exception(f"Cannot automatically ensure Speckle dependencies. Please try restarting the host application {host_application}!")
if __name__ == "__main__":
ensure_dependencies()
+2 -1
View File
@@ -1,4 +1,4 @@
from .users import LoadUsers, LoadUserStreams
from .users import LoadUsers, LoadUserStreams, ResetUsers
from .object import (
UpdateObject,
ResetObject,
@@ -27,6 +27,7 @@ from .misc import OpenSpeckleGuide, OpenSpeckleTutorials, OpenSpeckleForum
operator_classes = [
LoadUsers,
ResetUsers,
ReceiveStreamObjects,
SendStreamObjects,
LoadUserStreams,
+23 -19
View File
@@ -1,15 +1,18 @@
"""
Commit operators
"""
from typing import cast
import bpy
from bpy.props import BoolProperty
from bpy_speckle.functions import _check_speckle_client_user_stream
from bpy_speckle.functions import _report, get_speckle
from bpy_speckle.clients import speckle_clients
from bpy_speckle.properties.scene import SpeckleSceneSettings
class DeleteCommit(bpy.types.Operator):
"""
Delete stream
Deletes the selected commit from the selected stream.
To execute from code, call: `bpy.ops.speckle.delete_commit(are_you_sure=True)`
"""
bl_idname = "speckle.delete_commit"
@@ -28,36 +31,37 @@ class DeleteCommit(bpy.types.Operator):
col.prop(self, "are_you_sure")
def invoke(self, context, event):
speckle = get_speckle(context)
wm = context.window_manager
if len(context.scene.speckle.users) > 0:
if len(speckle.users) > 0:
return wm.invoke_props_dialog(self)
return {"CANCELLED"}
def execute(self, context):
try:
self.delete_commit(context)
return {"FINISHED"}
except Exception as ex:
print(f"{self.bl_idname}: failed: {ex}")
return {"CANCELLED"}
def delete_commit(self, context: bpy.types.Context) -> None:
if not self.are_you_sure:
return {"CANCELLED"}
raise Exception("Cancelled by user")
self.are_you_sure = False
speckle = context.scene.speckle
speckle = get_speckle(context)
check = _check_speckle_client_user_stream(context.scene)
if check is None:
return {"CANCELLED"}
(_, stream, _, commit) = speckle.validate_commit_selection()
user, stream = check
client = speckle_clients[int(context.scene.speckle.active_user)]
stream = user.streams[user.active_stream]
if len(stream.branches) < 1:
return {"CANCELLED"}
branch = stream.branches[int(stream.branch)]
if len(branch.commits) < 1:
return {"CANCELLED"}
commit = branch.commits[int(branch.commit)]
client = speckle_clients[int(speckle.active_user)]
deleted = client.commit.delete(stream_id=stream.id, commit_id=commit.id)
if not deleted:
raise Exception("Delete operation failed")
print(f"{self.bl_idname}: succeeded - commit {commit.id} ({commit.message}) has been deleted from stream {stream.id}")
return {"FINISHED"}
+2 -2
View File
@@ -4,6 +4,7 @@ Object operators
import bpy
from bpy.props import BoolProperty, EnumProperty
from deprecated import deprecated
from bpy_speckle.convert.to_speckle import (
convert_to_speckle,
ngons_to_speckle_polylines,
@@ -28,7 +29,6 @@ class UpdateObject(bpy.types.Operator):
def execute(self, context):
user = context.scene.speckle.users[int(context.scene.speckle.active_user)]
client = speckle_clients[int(context.scene.speckle.active_user)]
stream = user.streams[user.active_stream]
active = context.active_object
_report(active)
@@ -127,7 +127,7 @@ class DeleteObject(bpy.types.Operator):
return {"FINISHED"}
@deprecated
class UploadNgonsAsPolylines(bpy.types.Operator):
"""
Upload mesh ngon faces as polyline outlines
+347 -290
View File
@@ -3,52 +3,69 @@ Stream operators
"""
from itertools import chain
from math import radians
from typing import Callable, Dict, Iterable, Optional
import bpy
from specklepy.api.models import Commit
from deprecated import deprecated
from typing import Any, Callable, Dict, Iterable, List, Optional, Union, cast
import webbrowser
import bpy
from bpy.props import (
StringProperty,
BoolProperty,
EnumProperty,
)
from bpy.types import Context, Object
from bpy_speckle.convert.to_native import can_convert_to_native, convert_to_native
from bpy.types import (
Context,
Object,
Collection
)
from bpy_speckle.blender_commit_object_builder import BlenderCommitObjectBuilder
from bpy_speckle.convert.to_native import (
can_convert_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 (
_check_speckle_client_user_stream,
get_scale_length,
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 specklepy.transports.server import ServerTransport
from specklepy.objects.geometry import *
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 = create_collection(name)
col = get_or_create_collection(name)
collections[name] = get_objects_nested_lists(value, col)
if isinstance(value, Base):
col = create_collection(name)
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 = []
@@ -67,7 +84,7 @@ def get_objects_nested_lists(items: list, parent_col: Optional[bpy.types.Collect
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
@@ -84,7 +101,7 @@ def get_objects_collections_recursive(base: Base, parent_col: Optional[bpy.types
if isinstance(value, Base):
col = parent_col.children.get(name)
if not col:
col = create_collection(name)
col = get_or_create_collection(name)
try:
parent_col.children.link(col)
except:
@@ -97,45 +114,29 @@ def get_objects_collections_recursive(base: Base, parent_col: Optional[bpy.types
ObjectCallback = Optional[Callable[[bpy.types.Context, Object, Base], Object]]
ReceiveCompleteCallback = Optional[Callable[[bpy.types.Context, Dict[str, Object]], None]]
ReceiveCompleteCallback = Optional[Callable[[bpy.types.Context, Dict[str, Union[Object, Collection]]], None]]
def get_receive_funcs(context: Context, created_objects: Dict[str, Object]) -> tuple[ObjectCallback, ReceiveCompleteCallback]:
"""
Fetches the injected callback functions from user specified "Receive Script"
"""
def get_receive_funcs(speckle: SpeckleSceneSettings) -> tuple[ObjectCallback, ReceiveCompleteCallback]:
"""
Fetches the injected callback functions from user specified "Receive Script"
"""
objectCallback: ObjectCallback = None
receiveCompleteCallback: ReceiveCompleteCallback = None
if context.scene.speckle.receive_script in bpy.data.texts:
mod = bpy.data.texts[context.scene.speckle.receive_script].as_module()
if hasattr(mod, "execute_for_each"):
objectCallback = mod.execute_for_each
elif hasattr(mod, "execute"):
objectCallback = lambda c, o, _ : mod.execute(c.scene, o)
objectCallback: ObjectCallback = None
receiveCompleteCallback: ReceiveCompleteCallback = None
if speckle.receive_script in bpy.data.texts:
mod = bpy.data.texts[speckle.receive_script].as_module()
if hasattr(mod, "execute_for_each"):
objectCallback = mod.execute_for_each #type: ignore
elif hasattr(mod, "execute"):
objectCallback = lambda c, o, _ : mod.execute(c.scene, o) #type: ignore
if hasattr(mod, "execute_for_all"):
receiveCompleteCallback = mod.execute_for_all
if hasattr(mod, "execute_for_all"):
receiveCompleteCallback = mod.execute_for_all #type: ignore
return (objectCallback, receiveCompleteCallback)
progress = 0
def for_each_object(context: bpy.types.Context, obj: Object, base: Base) -> Object:
nonlocal progress
nonlocal created_objects
nonlocal objectCallback
progress += 1 #NOTE:XXX Progress bar never reaches 100 because func is only called for convertible objects
context.window_manager.progress_update(progress)
created_objects[obj.name] = obj
if objectCallback:
return objectCallback(context, obj, base)
else:
return obj
return (for_each_object, 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]
@@ -168,7 +169,7 @@ def bases_to_native(context: bpy.types.Context, collections: Dict[str, list], sc
context.area.tag_redraw()
@deprecated
def base_to_native(context: bpy.types.Context,
base: Base,
scale: float,
@@ -187,7 +188,7 @@ def base_to_native(context: bpy.types.Context,
# new_objects.extend(
# get_speckle_subobjects(base["properties"], scale, base["id"])
# )
"""
Set object Speckle settings
"""
@@ -216,28 +217,71 @@ def base_to_native(context: bpy.types.Context,
new_object.name = name
col.objects.unlink(existing[new_object.speckle.object_id])
if new_object.name not in col.objects:
col.objects.link(new_object)
link_object_to_collection_nested(new_object, col)
#if new_object.name not in col.objects:
#col.objects.link(new_object)
def create_collection(name: str, clear_collection=True) -> bpy.types.Collection:
if name in bpy.data.collections:
col = bpy.data.collections[name]
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 col.objects:
col.objects.unlink(obj)
for obj in existing.objects:
existing.objects.unlink(obj)
return existing
else:
col = bpy.data.collections.new(name)
new_collection = bpy.data.collections.new(name)
return col
#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]):
def create_child_collections(parent_col: bpy.types.Collection, children_names: Iterable[str]) -> None:
for name in children_names:
col = create_collection(name)
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 != ""
@@ -272,19 +316,23 @@ def create_nested_hierarchy(base: Base, hierarchy: List[str], objects: Any):
child.add_detachable_attrs({name})
child = child[name]
# TODO: what do we call this attribute?
if not hasattr(child, "@objects"):
child["@objects"] = []
child["@objects"].extend(objects)
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"),
# #("update","Update") #TODO: update mode!
# #("update","Update", "") #TODO: update mode!
#]
INSTANCES_SETTINGS = [
("collection_instance", "Collection Instace", "Receive Instances as Collection Instances"),
("linked_duplicates", "Linked Duplicates", "Receive Instances as Linked Duplicates"),
]
class ReceiveStreamObjects(bpy.types.Operator):
"""
Receive stream objects
@@ -299,13 +347,15 @@ class ReceiveStreamObjects(bpy.types.Operator):
clean_meshes: BoolProperty(name="Clean Meshes", default=False)
#receive_mode: EnumProperty(items=RECEIVE_MODES, name="Receive Type", default="replace", description="The behaviour of the recieve operation")
receive_instances_as: EnumProperty(items=INSTANCES_SETTINGS, name="Receive Instances As", default="collection_instance", description="How to receive speckle Instances")
def draw(self, context):
layout = self.layout
col = layout.column()
col.prop(self, "clean_meshes")
#col.prop(self, "receive_mode")
col.prop(self, "receive_instances_as")
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self)
@@ -330,7 +380,6 @@ class ReceiveStreamObjects(bpy.types.Operator):
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action='SELECT')
bpy.ops.mesh.remove_doubles()
bpy.ops.mesh.dissolve_limited(angle_limit=radians(0.1))
# Reset state to previous (not quite sure if this is 100% necessary)
@@ -339,32 +388,21 @@ class ReceiveStreamObjects(bpy.types.Operator):
bpy.context.view_layer.objects.active = None
def execute(self, context):
try:
self.receive(context)
return {"FINISHED"}
except Exception as ex:
_report(f"Failed to receive objects: {type(ex)} {ex}")
return {"CANCELLED"}
def receive(self, context: Context) -> None:
bpy.context.view_layer.objects.active = None
check = _check_speckle_client_user_stream(context.scene)
if check is None:
return {"CANCELLED"}
speckle = get_speckle(context)
(user, stream, branch, commit) = speckle.validate_commit_selection()
user, bstream = check
client = speckle_clients[int(context.scene.speckle.active_user)]
stream = client.stream.get(id=bstream.id, branch_limit=20)
if stream.branches.totalCount < 1:
return {"CANCELLED"}
if not stream.branches:
return {"CANCELLED"}
branch = stream.branches.items[int(bstream.branch)]
bbranch = bstream.branches[int(bstream.branch)]
if branch.commits.totalCount < 1:
_report("No commits found. Probably an empty stream.")
return {"CANCELLED"}
commit: Commit = branch.commits.items[int(bbranch.commit)]
client = speckle_clients[int(speckle.active_user)]
transport = ServerTransport(stream.id, client)
@@ -372,79 +410,91 @@ class ReceiveStreamObjects(bpy.types.Operator):
metrics.RECEIVE,
getattr(transport, "account", None),
custom_props={
"sourceHostApp": host_applications.get_host_app_from_string(commit.sourceApplication).slug,
"sourceHostAppVersion": commit.sourceApplication
"sourceHostApp": host_applications.get_host_app_from_string(commit.source_application).slug,
"sourceHostAppVersion": commit.source_application,
"isMultiplayer": commit.author_id != user.id,
},
)
stream_data = operations._untracked_receive(commit.referencedObject, transport)
# Fetch commit data
commit_object = operations._untracked_receive(commit.referenced_object, transport)
client.commit.received(
bstream.id,
stream.id,
commit.id,
source_application="blender",
message="received commit from Speckle Blender",
)
context.window_manager.progress_begin(0, stream_data.totalChildrenCount)
# Convert received data
context.window_manager.progress_begin(0, commit_object.totalChildrenCount or 1)
set_convert_instances_as(self.receive_instances_as) #HACK: we need a better way to pass settings down to the converter
"""
Create or get Collection for stream objects
"""
collections = get_objects_collections(stream_data)
traversalFunc = get_default_traversal_func(can_convert_to_native)
converted_objects: Dict[str, Union[Object, Collection]] = {}
converted_count: int = 0
(object_converted_callback, on_complete_callback) = get_receive_funcs(speckle)
if not collections:
return {"CANCELLED"}
# 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
if not isinstance(commit_object, SCollection):
dummy_commit_object = SCollection()
dummy_commit_object.elements = [commit_object]
dummy_commit_object.name = getattr(commit_object, "name", None)
dummy_commit_object.id = dummy_commit_object.get_id()
commit_object = dummy_commit_object
# name = ""
# if self.receive_mode == "create":
name = "{} [ {} @ {} ]".format(stream.name, branch.name, commit.id) # Matches Rhino "Create" naming
# else:
# name = stream.name # Doesn't quite match rhino's Update layer naming, but is close enough no?
# ensure commit object has a name if not already
if not commit_object.name:
commit_object.name = "{} [ {} @ {} ]".format(stream.name, branch.name, commit.id) # Matches Rhino "Create" naming
col = create_collection(name)
col.speckle.stream_id = stream.id
col.speckle.units = stream_data.units or "m"
for item in traversalFunc.traverse(commit_object):
if col.name not in bpy.context.scene.collection.children:
bpy.context.scene.collection.children.link(col)
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")
for child_col in collections.keys():
try:
col.children.link(bpy.data.collections[child_col])
except:
pass
"""
Set conversion scale from stream units
"""
scale = (
get_scale_length(col.speckle.units)
/ context.scene.unit_settings.scale_length
)
#Convert the object!
converted_data_type: str
converted: Union[Object, Collection, 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)
#Run the user specified callback function (AKA receive script)
if object_converted_callback:
converted = object_converted_callback(context, converted, current)
if converted is None:
raise Exception("Conversion returned None")
converted_objects[current.id] = converted
"""
Get script from text editor for injection
"""
created_objects = {}
(func, on_complete) = get_receive_funcs(context, created_objects)
_add_to_heirarchy(converted, item, converted_objects)
_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}")
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
"""
Iterate through retrieved resources
"""
bases_to_native(context, collections, scale, stream.id, func)
context.window_manager.progress_end()
if self.clean_meshes:
self.clean_converted_meshes(context, created_objects)
objects = {k: v for k, v in converted_objects.items() if isinstance(v, Object)}
self.clean_converted_meshes(context, objects)
if on_complete:
on_complete(context, created_objects)
return {"FINISHED"}
if on_complete_callback:
on_complete_callback(context, converted_objects)
@@ -472,125 +522,110 @@ class SendStreamObjects(bpy.types.Operator):
def invoke(self, context, event):
wm = context.window_manager
if len(context.scene.speckle.users) > 0:
N = len(context.selected_objects)
if N == 1:
self.commit_message = "Pushed {} element from Blender.".format(N)
else:
self.commit_message = "Pushed {} elements from Blender.".format(N)
return wm.invoke_props_dialog(self)
if len(context.scene.speckle.users) <= 0: return {"CANCELLED"}
N = len(context.selected_objects)
if N == 1:
self.commit_message = f"Pushed {N} element from Blender."
else:
self.commit_message = f"Pushed {N} elements from Blender."
return wm.invoke_props_dialog(self)
return {"CANCELLED"}
def execute(self, context):
try:
self.send(context)
return {"FINISHED"}
except Exception as ex:
_report(f"Send failed: {ex}")
return {"CANCELLED"}
def send(self, context: Context) -> None:
selected = context.selected_objects
if len(selected) < 1:
return {"CANCELLED"}
raise Exception("No objects are selected, sending canceled")
check = _check_speckle_client_user_stream(context.scene)
if check is None:
return {"CANCELLED"}
speckle = get_speckle(context)
(user, stream, branch) = speckle.validate_branch_selection()
user, bstream = check
stream = user.streams[user.active_stream]
branch = stream.branches[int(stream.branch)]
client = speckle_clients[int(context.scene.speckle.active_user)]
# scale = context.scene.unit_settings.scale_length / get_scale_length(
# stream.units.lower()
# )
scale = 1.0
client = speckle_clients[int(speckle.active_user)]
units = "m" if bpy.context.scene.unit_settings.system == "METRIC" else "ft"
"""
Get script from text editor for injection
"""
units_scale = context.scene.unit_settings.scale_length / get_scale_length(units)
# Get script from text editor for injection
func = None
if context.scene.speckle.send_script in bpy.data.texts:
mod = bpy.data.texts[context.scene.speckle.send_script].as_module()
if speckle.send_script in bpy.data.texts:
mod = bpy.data.texts[speckle.send_script].as_module()
if hasattr(mod, "execute"):
func = mod.execute
func = mod.execute #type: ignore
export = {}
num_converted = 0
context.window_manager.progress_begin(0, max(len(selected), 1))
depsgraph = bpy.context.evaluated_depsgraph_get() if self.apply_modifiers else None
commit_builder = BlenderCommitObjectBuilder()
for obj in selected:
try:
# Run injected function
new_object = obj
if func:
new_object = func(context.scene, obj)
# if obj.type != 'MESH':
# continue
if (new_object is None):
raise ConversionSkippedException(f"Script '{func.__module__}' returned None.")
new_object = obj
converted = convert_to_speckle(
obj,
units_scale,
units,
depsgraph
)
"""
Run injected function
"""
if func:
new_object = func(context.scene, obj)
if not converted:
raise Exception("Converter returned None")
if (
new_object is None
): # Make sure that the injected function returned an object
new_obj = obj
_report("Script '{}' returned None.".format(func.__module__))
continue
commit_builder.include_object(converted, obj)
_report("Converting {}".format(obj.name))
_report(f"Successfully converted '{obj.name_full}' as '{converted.speckle_type}'")
except ConversionSkippedException as ex:
_report(f"Skipped converting '{obj.name_full}': '{ex}'")
except Exception as ex:
_report(f"Failed to converted '{obj.name_full}': '{ex}'")
num_converted += 1
context.window_manager.progress_update(num_converted)
# ngons = obj.get("speckle_ngons_as_polylines", False)
context.window_manager.progress_end()
# if ngons:
# converted = ngons_to_speckle_polylines(obj, scale)
# else:
converted = convert_to_speckle(
obj,
scale,
units,
bpy.context.evaluated_depsgraph_get()
if self.apply_modifiers
else None,
)
if not converted:
continue
collection_name = obj.users_collection[0].name
if not export.get(collection_name):
export[collection_name] = []
export[collection_name].extend(converted)
base = Base()
for name, objects in export.items():
collection = bpy.data.collections.get(name)
hierarchy = get_collection_hierarchy(collection)
create_nested_hierarchy(base, hierarchy, objects)
commit_object = commit_builder.ensure_collection(context.scene.collection)
commit_builder.build_commit_object(commit_object)
_report(f"Sending data to {stream.name}")
transport = ServerTransport(stream.id, client)
obj_id = operations.send(
base,
OBJECT_ID = operations.send(
commit_object,
[transport],
)
client.commit.create(
COMMIT_ID = client.commit.create(
stream.id,
obj_id,
OBJECT_ID,
branch.name,
message=self.commit_message,
source_application="blender",
)
_report(f"Commit Created {user.server_url}/streams/{stream.id}/commits/{COMMIT_ID}")
bpy.ops.speckle.load_user_streams()
bpy.ops.speckle.load_user_streams() # refresh loaded commits
context.view_layer.update()
if context.area:
context.area.tag_redraw()
return {"FINISHED"}
class ViewStreamDataApi(bpy.types.Operator):
@@ -600,15 +635,20 @@ class ViewStreamDataApi(bpy.types.Operator):
bl_description = "View the stream in the web browser"
def execute(self, context):
try:
self.view_stream_data_api(context)
return {"FINISHED"}
except Exception as ex:
_report(f"{self.bl_idname} failed: {ex}")
return {"CANCELLED"}
if len(context.scene.speckle.users) > 0:
user = context.scene.speckle.users[int(context.scene.speckle.active_user)]
if len(user.streams) > 0:
stream = user.streams[user.active_stream]
def view_stream_data_api(self, context: Context) -> None:
speckle = get_speckle(context)
webbrowser.open("%s/streams/%s" % (user.server_url, stream.id), new=2)
return {"FINISHED"}
return {"CANCELLED"}
(user, stream) = speckle.validate_stream_selection()
if not webbrowser.open("%s/streams/%s" % (user.server_url, stream.id), new=2):
raise Exception("Failed to open stream in browser")
class AddStreamFromURL(bpy.types.Operator):
@@ -631,13 +671,22 @@ class AddStreamFromURL(bpy.types.Operator):
def invoke(self, context, event):
wm = context.window_manager
if len(context.scene.speckle.users) > 0:
speckle = get_speckle(context)
if len(speckle.users) > 0:
return wm.invoke_props_dialog(self)
return {"CANCELLED"}
def execute(self, context):
speckle = context.scene.speckle
try:
self.add_stream_from_url(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:
speckle = get_speckle(context)
wrapper = StreamWrapper(self.stream_url)
user_index = next(
@@ -645,9 +694,10 @@ class AddStreamFromURL(bpy.types.Operator):
None,
)
if user_index is None:
return {"CANCELLED"}
raise Exception("Unable to find user stream server")
speckle.active_user = str(user_index)
user = speckle.users[user_index]
user = cast(SpeckleUserObject, speckle.users[user_index])
client = speckle_clients[user_index]
stream = client.stream.get(wrapper.stream_id, branch_limit=20)
@@ -686,8 +736,6 @@ class AddStreamFromURL(bpy.types.Operator):
if context.area:
context.area.tag_redraw()
return {"FINISHED"}
class CreateStream(bpy.types.Operator):
"""
@@ -712,23 +760,31 @@ class CreateStream(bpy.types.Operator):
def invoke(self, context, event):
wm = context.window_manager
if len(context.scene.speckle.users) > 0:
speckle = get_speckle(context)
if len(speckle.users) > 0:
return wm.invoke_props_dialog(self)
return {"CANCELLED"}
def execute(self, context):
try:
self.create_stream(context)
return {"FINISHED"}
except Exception as ex:
_report(f"{self.bl_idname} failed: {ex}")
return {"CANCELLED"}
def create_stream(self, context: Context) -> None:
speckle = get_speckle(context)
check = _check_speckle_client_user_stream(context.scene)
if check is None:
return {"CANCELLED"}
user = speckle.validate_user_selection()
user, bstream = check
client = speckle_clients[int(context.scene.speckle.active_user)]
client = speckle_clients[int(speckle.active_user)]
client.stream.create(
name=self.stream_name, description=self.stream_description, is_public=True
name=self.stream_name,
description=self.stream_description,
is_public=True
)
bpy.ops.speckle.load_user_streams()
@@ -740,8 +796,6 @@ class CreateStream(bpy.types.Operator):
if context.area:
context.area.tag_redraw()
return {"FINISHED"}
class DeleteStream(bpy.types.Operator):
"""
@@ -768,24 +822,30 @@ class DeleteStream(bpy.types.Operator):
def invoke(self, context, event):
wm = context.window_manager
if len(context.scene.speckle.users) > 0:
speckle = get_speckle(context)
if len(speckle.users) > 0:
return wm.invoke_props_dialog(self)
return {"CANCELLED"}
def execute(self, context):
try:
self.delete_stream(context)
return {"FINISHED"}
except Exception as ex:
_report(f"{self.bl_idname} failed: {ex}")
return {"CANCELLED"}
def delete_stream(self, context: Context) -> None:
if not self.are_you_sure:
return {"CANCELLED"}
raise Exception("Cancled by user")
self.are_you_sure = False
check = _check_speckle_client_user_stream(context.scene)
if check is None:
return {"CANCELLED"}
speckle = get_speckle(context)
(_, stream) = speckle.validate_stream_selection()
user, stream = check
client = speckle_clients[int(context.scene.speckle.active_user)]
client = speckle_clients[int(speckle.active_user)]
client.stream.delete(id=stream.id)
@@ -800,7 +860,6 @@ class DeleteStream(bpy.types.Operator):
if context.area:
context.area.tag_redraw()
return {"FINISHED"}
class SelectOrphanObjects(bpy.types.Operator):
@@ -829,7 +888,7 @@ class SelectOrphanObjects(bpy.types.Operator):
return {"FINISHED"}
@deprecated
class UpdateGlobal(bpy.types.Operator):
"""
DEPRECATED
@@ -849,7 +908,7 @@ class UpdateGlobal(bpy.types.Operator):
label = row.label(text="Update everything.")
def execute(self, context):
client = context.scene.speckle.client
profiles = client.load_local_profiles()
@@ -877,16 +936,18 @@ class CopyStreamId(bpy.types.Operator):
bl_description = "Copy stream ID to clipboard"
def execute(self, context):
speckle = context.scene.speckle
try:
self.copy_stream_id(context)
return {"FINISHED"}
except Exception as ex:
_report(f"{self.bl_idname} failed: {ex}")
return {"CANCELLED"}
def copy_stream_id(self, context) -> None:
speckle = get_speckle(context)
if len(speckle.users) < 1:
return {"CANCELLED"}
user = speckle.users[int(speckle.active_user)]
if len(user.streams) < 1:
return {"CANCELLED"}
stream = user.streams[user.active_stream]
(_, stream) = speckle.validate_stream_selection()
bpy.context.window_manager.clipboard = stream.id
return {"FINISHED"}
class CopyCommitId(bpy.types.Operator):
@@ -900,22 +961,18 @@ class CopyCommitId(bpy.types.Operator):
bl_description = "Copy commit ID to clipboard"
def execute(self, context):
speckle = context.scene.speckle
try:
self.copy_commit_id(context)
return {"FINISHED"}
except Exception as ex:
_report(f"{self.bl_idname} failed: {ex}")
return {"CANCELLED"}
def copy_commit_id(self, context) -> None:
speckle = get_speckle(context)
if len(speckle.users) < 1:
return {"CANCELLED"}
user = speckle.users[int(speckle.active_user)]
if len(user.streams) < 1:
return {"CANCELLED"}
stream = user.streams[user.active_stream]
if len(stream.branches) < 1:
return {"CANCELLED"}
branch = stream.branches[int(stream.branch)]
if len(branch.commits) < 1:
return {"CANCELLED"}
commit = branch.commits[int(branch.commit)]
(_, _, _, commit) = speckle.validate_commit_selection()
bpy.context.window_manager.clipboard = commit.id
return {"FINISHED"}
class CopyBranchName(bpy.types.Operator):
@@ -929,16 +986,16 @@ class CopyBranchName(bpy.types.Operator):
bl_description = "Copy branch name to clipboard"
def execute(self, context):
speckle = context.scene.speckle
try:
self.copy_branch_id(context)
return {"FINISHED"}
except Exception as ex:
_report(f"{self.bl_idname} failed: {ex}")
return {"CANCELLED"}
def copy_branch_id(self, context) -> None:
speckle = get_speckle(context)
(_, _, branch) = speckle.validate_branch_selection()
if len(speckle.users) < 1:
return {"CANCELLED"}
user = speckle.users[int(speckle.active_user)]
if len(user.streams) < 1:
return {"CANCELLED"}
stream = user.streams[user.active_stream]
if len(stream.branches) < 1:
return {"CANCELLED"}
branch = stream.branches[int(stream.branch)]
bpy.context.window_manager.clipboard = branch.name
return {"FINISHED"}
+73 -40
View File
@@ -1,15 +1,39 @@
"""
User account operators
"""
from typing import cast
import bpy
from bpy_speckle.functions import _report
from bpy.types import Context
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 SpeckleCommitObject, SpeckleSceneSettings, SpeckleUserObject
from specklepy.api.client import SpeckleClient
from specklepy.api.models import Stream, User
from specklepy.api.models import Stream
from specklepy.api.credentials import get_local_accounts
from datetime import datetime
class ResetUsers(bpy.types.Operator):
"""
Reset loaded users
"""
bl_idname = "speckle.users_reset"
bl_label = "Reset users"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
self.reset_ui(context)
bpy.context.view_layer.update()
if context.area:
context.area.tag_redraw()
return {"FINISHED"}
@staticmethod
def reset_ui(context: Context):
speckle = get_speckle(context)
speckle.users.clear()
speckle_clients.clear()
class LoadUsers(bpy.types.Operator):
"""
@@ -24,11 +48,10 @@ class LoadUsers(bpy.types.Operator):
_report("Loading users...")
speckle : SpeckleSceneSettings = context.scene.speckle
speckle = cast(SpeckleSceneSettings, context.scene.speckle) #type: ignore
users = speckle.users
speckle.users.clear()
speckle_clients.clear()
ResetUsers.reset_ui(context)
profiles = get_local_accounts()
active_user_index = 0
@@ -37,13 +60,16 @@ class LoadUsers(bpy.types.Operator):
user = users.add()
user.server_name = profile.serverInfo.name or "Speckle Server"
user.server_url = profile.serverInfo.url
user.id = profile.userInfo.id
user.name = profile.userInfo.name
user.email = profile.userInfo.email
user.company = profile.userInfo.company or ""
try:
url = profile.serverInfo.url
assert(url)
client = SpeckleClient(
host=profile.serverInfo.url,
use_ssl="https" in profile.serverInfo.url,
host=url,
use_ssl="https" in url,
)
client.authenticate_with_account(profile)
speckle_clients.append(client)
@@ -53,7 +79,6 @@ class LoadUsers(bpy.types.Operator):
if profile.isDefault:
active_user_index = len(users) - 1
speckle.active_user_index = int(speckle.active_user)
speckle.active_user = str(active_user_index)
bpy.context.view_layer.update()
@@ -62,7 +87,7 @@ class LoadUsers(bpy.types.Operator):
return {"FINISHED"}
def add_user_stream(user: User, stream: Stream):
def add_user_stream(user: SpeckleUserObject, stream: Stream):
s = user.streams.add()
s.name = stream.name
s.id = stream.id
@@ -80,16 +105,17 @@ def add_user_stream(user: User, stream: Stream):
continue
for c in b.commits.items:
commit = branch.commits.add()
commit: SpeckleCommitObject = branch.commits.add()
commit.id = commit.name = c.id
commit.message = c.message or ""
commit.author_name = c.authorName
commit.author_id = c.authorId
commit.created_at = datetime.strftime(c.createdAt, "%Y-%m-%d %H:%M:%S.%f%Z")
commit.created_at = c.createdAt.strftime("%Y-%m-%d %H:%M:%S.%f%Z") if c.createdAt else ""
commit.source_application = str(c.sourceApplication)
commit.referenced_object = c.referencedObject
if hasattr(s, "baseProperties"):
s.units = stream.baseProperties.units
s.units = stream.baseProperties.units # type: ignore
else:
s.units = "Meters"
@@ -105,33 +131,40 @@ class LoadUserStreams(bpy.types.Operator):
bl_description = "(Re)load all available user streams"
def execute(self, context):
speckle = context.scene.speckle
if len(speckle.users) > 0:
user = speckle.users[int(context.scene.speckle.active_user)]
client = speckle_clients[int(context.scene.speckle.active_user)]
try:
streams = client.stream.list(stream_limit=20)
except Exception as e:
_report(f"Failed to retrieve streams: {e}")
return
if not streams:
_report("Failed to retrieve streams.")
return
user.streams.clear()
default_units = "Meters"
for s in streams:
sstream = client.stream.get(id=s.id, branch_limit=20)
add_user_stream(user, sstream)
bpy.context.view_layer.update()
try:
self.add_stream_from_url(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:
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
if not streams:
_report("Failed to retrieve streams.")
return
user.streams.clear()
default_units = "Meters"
for s in streams:
assert(s.id)
sstream = client.stream.get(id=s.id, branch_limit=20)
add_user_stream(user, sstream)
bpy.context.view_layer.update()
if context.area:
context.area.tag_redraw()
return {"CANCELLED"}
+69 -8
View File
@@ -1,6 +1,7 @@
"""
Scene properties
"""
from typing import Optional, Tuple
import bpy
from bpy.props import (
StringProperty,
@@ -18,12 +19,13 @@ class SpeckleSceneObject(bpy.types.PropertyGroup):
class SpeckleCommitObject(bpy.types.PropertyGroup):
id: StringProperty(default="abc")
message: StringProperty(default="A simple commit")
author_name: StringProperty(default="Author name")
author_id: StringProperty(default="Author ID")
created_at: StringProperty(default="Today")
source_application: StringProperty(default="Unknown")
id: StringProperty(default="")
message: StringProperty(default="")
author_name: StringProperty(default="")
author_id: StringProperty(default="")
created_at: StringProperty(default="")
source_application: StringProperty(default="")
referenced_object: StringProperty(default="")
class SpeckleBranchObject(bpy.types.PropertyGroup):
@@ -42,6 +44,12 @@ class SpeckleBranchObject(bpy.types.PropertyGroup):
description="Active commit",
items=get_commits,
)
def get_active_commit(self) -> Optional[SpeckleCommitObject]:
selected_index = int(self.commit)
if 0 <= selected_index < len(self.commits):
return self.commits[selected_index]
return None
class SpeckleStreamObject(bpy.types.PropertyGroup):
@@ -66,16 +74,28 @@ class SpeckleStreamObject(bpy.types.PropertyGroup):
items=get_branches,
)
def get_active_branch(self) -> Optional[SpeckleBranchObject]:
selected_index = int(self.branch)
if 0 <= selected_index < len(self.branches):
return self.branches[selected_index]
return None
class SpeckleUserObject(bpy.types.PropertyGroup):
server_name: StringProperty(default="SpeckleXYZ")
server_url: StringProperty(default="https://speckle.xyz")
id: StringProperty(default="")
name: StringProperty(default="Speckle User")
email: StringProperty(default="user@speckle.xyz")
company: StringProperty(default="SpeckleSystems")
streams: CollectionProperty(type=SpeckleStreamObject)
active_stream: IntProperty(default=0)
def get_active_stream(self) -> Optional[SpeckleStreamObject]:
selected_index = int(self.active_stream)
if 0 <= selected_index < len(self.streams):
return self.streams[selected_index]
return None
class SpeckleSceneSettings(bpy.types.PropertyGroup):
def get_scripts(self, context):
@@ -103,8 +123,8 @@ class SpeckleSceneSettings(bpy.types.PropertyGroup):
active_user: EnumProperty(
items=get_users,
name="User",
description="Select user",
name="Account",
description="Select account",
update=set_user,
get=None,
set=None,
@@ -131,3 +151,44 @@ class SpeckleSceneSettings(bpy.types.PropertyGroup):
description="Script to run when sending stream objects.",
items=get_scripts,
)
def get_active_user(self) -> Optional[SpeckleUserObject]:
selected_index = int(self.active_user)
if 0 <= selected_index < len(self.users):
return self.users[selected_index]
return None
def validate_user_selection(self) -> SpeckleUserObject:
user = self.get_active_user()
if not user:
raise SelectionException("No user selected/found")
return user
def validate_stream_selection(self) -> Tuple[SpeckleUserObject, SpeckleStreamObject]:
user = self.validate_user_selection()
stream = user.get_active_stream()
if not stream:
raise SelectionException("No stream selected/found")
return (user, stream)
def validate_branch_selection(self) -> Tuple[SpeckleUserObject, SpeckleStreamObject, SpeckleBranchObject]:
(user, stream) = self.validate_stream_selection()
branch = stream.get_active_branch()
if not branch:
raise SelectionException("No branch selected/found")
return (user, stream, branch)
def validate_commit_selection(self) ->Tuple[SpeckleUserObject, SpeckleStreamObject, SpeckleBranchObject, SpeckleCommitObject]:
(user, stream, branch) = self.validate_branch_selection()
commit = branch.get_active_commit()
if commit is None:
raise SelectionException("No commit selected/found")
return (user, stream, branch, commit)
class SelectionException(Exception):
pass
@@ -0,0 +1,83 @@
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
@@ -0,0 +1,121 @@
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
+662 -645
View File
File diff suppressed because it is too large Load Diff
+3 -2
View File
@@ -7,14 +7,15 @@ license = "Apache-2.0"
[tool.poetry.dependencies]
python = ">=3.8, <4.0.0"
specklepy = "^2.12.0"
specklepy = "^2.14.0"
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 = "^20221006"
fake-bpy-module-latest = "^20230117"
black = "^22.10.0"
pylint = "^2.15.7"
ruff = "^0.0.166"