Skip to content

Commit

Permalink
Merge pull request #171 from specklesystems/jrm/deps/specklepy216
Browse files Browse the repository at this point in the history
fix(receive)!: Fixed issue with collection name conflicts
  • Loading branch information
JR-Morgan committed Sep 12, 2023
2 parents 2688a69 + 67a1882 commit 5ddb2aa
Show file tree
Hide file tree
Showing 7 changed files with 103 additions and 119 deletions.
3 changes: 2 additions & 1 deletion bpy_speckle/convert/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,5 @@

OBJECT_NAME_MAX_LENGTH = 62
SPECKLE_ID_LENGTH = 32
OBJECT_NAME_SEPERATOR = " -- "
OBJECT_NAME_SPECKLE_SEPARATOR = " -- "
OBJECT_NAME_NUMERAL_SEPARATOR = '.'
127 changes: 74 additions & 53 deletions bpy_speckle/convert/to_native.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import math
from typing import Any, Dict, Iterable, List, Optional, Union, Collection, cast
from bpy_speckle.convert.constants import DISPLAY_VALUE_PROPERTY_ALIASES, ELEMENTS_PROPERTY_ALIASES, OBJECT_NAME_MAX_LENGTH, OBJECT_NAME_SEPERATOR, SPECKLE_ID_LENGTH
from typing import Any, Dict, Iterable, List, Optional, Sequence, Union, Collection, cast
from bpy_speckle.convert.constants import DISPLAY_VALUE_PROPERTY_ALIASES, ELEMENTS_PROPERTY_ALIASES, OBJECT_NAME_MAX_LENGTH, OBJECT_NAME_NUMERAL_SEPARATOR, OBJECT_NAME_SPECKLE_SEPARATOR, SPECKLE_ID_LENGTH
from bpy_speckle.functions import get_default_traversal_func, get_scale_length, _report
from bpy_speckle.convert.util import ConversionSkippedException
from mathutils import (
Expand All @@ -20,7 +20,7 @@
from bpy.types import Object, Collection as BCollection

from .util import (
add_to_heirarchy,
add_to_hierarchy,
get_render_material,
get_vertex_color_material,
render_material_to_native,
Expand All @@ -40,35 +40,18 @@
)


def _has_native_convesion(speckle_object: Base) -> bool:
def _has_native_conversion(speckle_object: Base) -> bool:
return any(isinstance(speckle_object, t) for t in CAN_CONVERT_TO_NATIVE) or "View" in speckle_object.speckle_type #hack

def _has_fallback_conversion(speckle_object: Base) -> bool:
return any(getattr(speckle_object, alias, None) for alias in DISPLAY_VALUE_PROPERTY_ALIASES)

def can_convert_to_native(speckle_object: Base) -> bool:

if(_has_native_convesion(speckle_object) or _has_fallback_conversion(speckle_object)):
if(_has_native_conversion(speckle_object) or _has_fallback_conversion(speckle_object)):
return True
return False

def create_new_object(obj_data: Optional[bpy.types.ID], desired_name: str, counter: int = 0) -> bpy.types.Object:
"""
Creates a new blender object with a unique name,
if the desired_name is already taken
we'll append a number, with the format .xxx to the desired_name to ensure the name is unique.
"""
name = desired_name if counter == 0 else f"{desired_name[:OBJECT_NAME_MAX_LENGTH - 4]}.{counter:03d}" # format counter as name.xxx, truncate to ensure we don't exceed the object name max length

#TODO: This is very slow, and gets slower the more objects you receive with the same name...
# We could use a binary/galloping search, and/or cache the name -> index within a receive.
if name in bpy.data.objects.keys():
#Object already exists, increment counter and try again!
return create_new_object(obj_data, desired_name, counter + 1)

blender_object = bpy.data.objects.new(name, obj_data)
return blender_object

convert_instances_as: str #HACK: This is hacky, we need a better way to pass settings down to the converter
def set_convert_instances_as(value: str):
global convert_instances_as
Expand All @@ -87,7 +70,7 @@ def convert_to_native(speckle_object: Base) -> Object:
children: list[Object] = []

# convert elements/breps
if not _has_native_convesion(speckle_object):
if not _has_native_conversion(speckle_object):
(converted, children) = display_value_to_native(speckle_object, object_name, scale)
if not converted and not children:
raise Exception(f"Zero geometry converted from displayValues for {speckle_object}")
Expand Down Expand Up @@ -150,8 +133,8 @@ def _members_to_native(speckle_object: Base, name: str, scale: float, members: I
display = getattr(speckle_object, alias, None)

count = 0
MAX_DEPTH = 255 # some large value, to prevent infinite reccursion
def seperate(value: Any) -> bool:
MAX_DEPTH = 255 # some large value, to prevent infinite recursion
def separate(value: Any) -> bool:
nonlocal meshes, others, count, MAX_DEPTH

if combineMeshes and isinstance(value, Mesh):
Expand All @@ -163,11 +146,11 @@ def seperate(value: Any) -> bool:
if(count > MAX_DEPTH):
return True
for x in value:
seperate(x)
separate(x)

return False

did_halt = seperate(display)
did_halt = separate(display)

if did_halt:
_report(f"Traversal of {speckle_object.speckle_type} {speckle_object.id} halted after traversal depth exceeds MAX_DEPTH={MAX_DEPTH}. Are there circular references object structure?")
Expand Down Expand Up @@ -543,7 +526,7 @@ def icurve_to_native(speckle_curve: Base, name: str, scale: float) -> bpy.types.


"""
Transforms and Intances
Transforms and Instances
"""

def transform_to_native(transform: Transform, scale: float) -> MMatrix:
Expand Down Expand Up @@ -586,7 +569,7 @@ def _get_instance_name(instance: Instance) -> str:
or _get_friendly_object_name(instance.definition)
or _simplified_speckle_type(instance.speckle_type)
)
return f"{name_prefix}{OBJECT_NAME_SEPERATOR}{instance.id}"
return f"{name_prefix}{OBJECT_NAME_SPECKLE_SEPARATOR}{instance.id}"


def instance_to_native_object(instance: Instance, scale: float) -> Object:
Expand All @@ -605,12 +588,12 @@ def instance_to_native_object(instance: Instance, scale: float) -> Object:
traversal_root: Base = definition

if not can_convert_to_native(definition):
# Non-convertable (like all blocks, and some revit instances) will not be converted as part of the deep_traversal.
# Non-convertible (like all blocks, and some revit instances) will not be converted as part of the deep_traversal.
# so we explicitly convert them as empties.
native_instance = create_new_object(None, name)
native_instance.empty_display_size = 0

converted_objects["__ROOT"] = native_instance # we create a dummy root to avoid id conflicts, since revit definitions have displayValues, they are convertable
converted_objects["__ROOT"] = native_instance # we create a dummy root to avoid id conflicts, since revit definitions have displayValues, they are convertible
traversal_root = Base(elements=definition, id="__ROOT")

#Convert definition + "elements" on definition
Expand Down Expand Up @@ -652,7 +635,7 @@ def instance_to_native_collection_instance(instance: Instance, scale: float) ->

instance_transform = transform_to_native(instance.transform, scale)

native_instance = bpy.data.objects.new(name, None)
native_instance = create_new_object(None, name)

#add_custom_properties(instance, native_instance)
# hide the instance axes so they don't clutter the viewport
Expand All @@ -672,11 +655,11 @@ def _instance_definition_to_native(definition: Union[Base, BlockDefinition]) ->
if native_def:
return native_def

native_def = bpy.data.collections.new(name)
native_def = create_new_collection(name)
native_def["applicationId"] = definition.applicationId

converted_objects = {}
converted_objects["__ROOT"] = native_def # we create a dummy root to avoid id conflicts, since revit definitions have displayValues, they are convertable
converted_objects["__ROOT"] = native_def # we create a dummy root to avoid id conflicts, since revit definitions have displayValues, they are convertible
dummyRoot = Base(elements=definition, id="__ROOT")

_deep_conversion(dummyRoot, converted_objects, True)
Expand Down Expand Up @@ -709,7 +692,7 @@ def _deep_conversion(root: Base, converted_objects: Dict[str, Union[Object, BCol

converted_objects[current.id] = converted

add_to_heirarchy(converted, item, converted_objects, preserve_transform)
add_to_hierarchy(converted, item, converted_objects, preserve_transform)

_report(f"Successfully converted {type(current).__name__} {current.id} as '{converted_data_type}'")
except ConversionSkippedException as ex:
Expand All @@ -728,28 +711,66 @@ def collection_to_native(collection: SCollection) -> BCollection:
return ret

def get_or_create_collection(name: str, clear_collection: bool = True) -> BCollection:
existing = cast(BCollection, bpy.data.collections.get(name))
if existing:
if clear_collection:
for obj in existing.objects:
existing.objects.unlink(obj)
return existing
else:
new_collection = bpy.data.collections.new(name)

#NOTE: We want to not render revit "Rooms" collections by default.
if name == "Rooms":
new_collection.hide_viewport = True
new_collection.hide_render = True

return new_collection
#Disabled for now, since update mode needs rescoping.
# existing = cast(Optional[BCollection], bpy.data.collections.get(name))
# if existing:
# if clear_collection:
# for obj in existing.objects:
# existing.objects.unlink(obj)
# return existing
# else:
new_collection = create_new_collection(name)

#NOTE: We want to not render revit "Rooms" collections by default.
if name == "Rooms":
new_collection.hide_viewport = True
new_collection.hide_render = True

return new_collection



"""
Object Naming
Object Naming and Creation
"""

def create_new_collection( desired_name: str) -> bpy.types.Collection:
"""
Creates a new blender collection with a unique name
If the desired_name is already taken
we'll append a number, with the format .xxx to the desired_name to ensure the name is unique.
"""
name = _make_unique_name(desired_name, bpy.data.collections.keys())

blender_collection = bpy.data.collections.new(name)
return blender_collection

def create_new_object(obj_data: Optional[bpy.types.ID], desired_name: str) -> bpy.types.Object:
"""
Creates a new blender object with a unique name,
If the desired_name is already taken
we'll append a number, with the format .xxx to the desired_name to ensure the name is unique.
"""
name = _make_unique_name(desired_name, bpy.data.objects.keys())

blender_object = bpy.data.objects.new(name, obj_data)
return blender_object

def _make_unique_name( desired_name: str, taken_names: Collection[str], counter: int = 0) -> str:
"""
Using Blenders default naming (append numeral in .xxx format) to avoid name conflicts with taken names
"""
name = desired_name if counter == 0 else f"{desired_name[:OBJECT_NAME_MAX_LENGTH - 4]}{OBJECT_NAME_NUMERAL_SEPARATOR}{counter:03d}" # format counter as name.xxx, truncate to ensure we don't exceed the object name max length

#TODO: This is very slow, and gets slower the more objects you receive with the same name...
# We could use a binary/galloping search, and/or cache the name -> index within a receive.
if name in taken_names:
#Name already taken, increment counter and try again!
return _make_unique_name(desired_name, taken_names, counter + 1)

return name


def _get_friendly_object_name(speckle_object: Base) -> Optional[str]:
return (getattr(speckle_object, "name", None)
or getattr(speckle_object, "Name", None)
Expand All @@ -764,7 +785,7 @@ def _get_friendly_object_name(speckle_object: Base) -> Optional[str]:

def _truncate_object_name(name: str) -> str:

MAX_NAME_LENGTH = OBJECT_NAME_MAX_LENGTH - SPECKLE_ID_LENGTH - len(OBJECT_NAME_SEPERATOR)
MAX_NAME_LENGTH = OBJECT_NAME_MAX_LENGTH - SPECKLE_ID_LENGTH - len(OBJECT_NAME_SPECKLE_SEPARATOR)

return name[:MAX_NAME_LENGTH]

Expand All @@ -780,7 +801,7 @@ def _generate_object_name(speckle_object: Base) -> str:
else:
prefix = _simplified_speckle_type(speckle_object.speckle_type)

return f"{prefix}{OBJECT_NAME_SEPERATOR}{speckle_object.id}"
return f"{prefix}{OBJECT_NAME_SPECKLE_SEPARATOR}{speckle_object.id}"


def get_scale_factor(speckle_object: Base, fallback: float = 1.0) -> float:
Expand Down
38 changes: 19 additions & 19 deletions bpy_speckle/convert/to_speckle.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
Mesh, Curve, Interval, Box, Point, Vector, Polyline,
)
from bpy_speckle.blender_commit_object_builder import BlenderCommitObjectBuilder
from bpy_speckle.convert.constants import OBJECT_NAME_SEPERATOR, SPECKLE_ID_LENGTH
from bpy_speckle.convert.constants import OBJECT_NAME_SPECKLE_SEPARATOR, SPECKLE_ID_LENGTH
from bpy_speckle.convert.util import (
ConversionSkippedException,
get_blender_custom_properties,
Expand Down Expand Up @@ -107,7 +107,7 @@ def mesh_to_speckle_meshes(blender_object: Object, data: bpy.types.Mesh) -> List
for i in submesh_data:
index_mapping: Dict[int, int] = {}

#Loop through each polygon, and map indicies to their new index in m_verts
#Loop through each polygon, and map indices to their new index in m_verts

mesh_area = 0
m_verts: List[float] = []
Expand Down Expand Up @@ -176,8 +176,8 @@ def bezier_to_speckle(matrix: MMatrix, spline: bpy.types.Spline, name: Optional[

num_points = len(points)

flattend_points = []
for row in points: flattend_points.extend(row)
flattened_points = []
for row in points: flattened_points.extend(row)

knot_count = num_points + degree - 1
knots = [0] * knot_count
Expand All @@ -192,7 +192,7 @@ def bezier_to_speckle(matrix: MMatrix, spline: bpy.types.Spline, name: Optional[
degree=degree,
closed=spline.use_cyclic_u,
periodic= not spline.use_endpoint_u,
points=flattend_points,
points=flattened_points,
weights=[1] * num_points,
knots=knots,
rational=True,
Expand All @@ -219,15 +219,15 @@ def nurbs_to_speckle(matrix: MMatrix, spline: bpy.types.Spline, name: Optional[s

points = [tuple(matrix @ pt.co.xyz * UnitsScale) for pt in spline.points] # type: ignore

flattend_points = []
for row in points: flattend_points.extend(row)
flattened_points = []
for row in points: flattened_points.extend(row)

if spline.use_cyclic_u:
for i in range(0, degree * 3, 3):
# Rhino expects n + degree number of points (for closed curves). So we need to add an extra point for each degree
flattend_points.append(flattend_points[i + 0])
flattend_points.append(flattend_points[i + 1])
flattend_points.append(flattend_points[i + 2])
flattened_points.append(flattened_points[i + 0])
flattened_points.append(flattened_points[i + 1])
flattened_points.append(flattened_points[i + 2])

for i in range(0, degree):
weights.append(weights[i])
Expand All @@ -237,7 +237,7 @@ def nurbs_to_speckle(matrix: MMatrix, spline: bpy.types.Spline, name: Optional[s
degree=degree,
closed=spline.use_cyclic_u,
periodic= not spline.use_endpoint_u,
points=flattend_points,
points=flattened_points,
weights=weights,
knots=knots,
rational=is_rational,
Expand Down Expand Up @@ -305,27 +305,27 @@ def bezier_to_speckle_polyline(matrix: MMatrix, spline: bpy.types.Spline, length
domain = Interval(start=0, end=length, totalChildrenCount=0)
return Polyline(value=points, closed = spline.use_cyclic_u, domain=domain, area=0, len=length)

_QUICK_TEST_NAME_LENGTH = SPECKLE_ID_LENGTH + len(OBJECT_NAME_SEPERATOR)
_QUICK_TEST_NAME_LENGTH = SPECKLE_ID_LENGTH + len(OBJECT_NAME_SPECKLE_SEPARATOR)

def to_speckle_name(blender_object: bpy.types.ID) -> str:
does_name_contain_id = len(blender_object.name) > _QUICK_TEST_NAME_LENGTH and OBJECT_NAME_SEPERATOR in blender_object.name
does_name_contain_id = len(blender_object.name) > _QUICK_TEST_NAME_LENGTH and OBJECT_NAME_SPECKLE_SEPARATOR in blender_object.name
if does_name_contain_id:
return blender_object.name.rsplit(OBJECT_NAME_SEPERATOR, 1)[0]
return blender_object.name.rsplit(OBJECT_NAME_SPECKLE_SEPARATOR, 1)[0]
else:
return blender_object.name

def poly_to_speckle(matrix: MMatrix, spline: bpy.types.Spline, name: Optional[str] = None) -> Polyline:
points = [tuple(matrix @ pt.co.xyz * UnitsScale) for pt in spline.points] # type: ignore

flattend_points = []
for row in points: flattend_points.extend(row)
flattened_points = []
for row in points: flattened_points.extend(row)

length = spline.calc_length()
domain = Interval(start=0, end=length, totalChildrenCount=0)
return Polyline(
name=name,
closed=bool(spline.use_cyclic_u),
value=list(flattend_points),
value=list(flattened_points),
length=length,
domain=domain,
bbox=Box(area=0.0, volume=0.0),
Expand Down Expand Up @@ -466,7 +466,7 @@ def vector_to_speckle(xyz: MVector) -> Vector:
)

def transform_to_speckle(blender_transform: Union[Iterable[Iterable[float]], MMatrix]) -> Transform:
iterable_transform = cast(Iterable[Iterable[float]], blender_transform) #NOTE: Matrix are itterable, even if type hinting says they are not
iterable_transform = cast(Iterable[Iterable[float]], blender_transform) #NOTE: Matrix are iterable, even if type hinting says they are not
value = [y for x in iterable_transform for y in x]
# scale the translation
for i in (3, 7, 11):
Expand Down Expand Up @@ -521,7 +521,7 @@ def empty_to_speckle(blender_object: Object) -> Union[BlockInstance, Base]:
wrapper = Base()
wrapper["@displayValue"] = matrix_to_speckle_point(cast(MMatrix, blender_object.matrix_world))
return wrapper
#TODO: we could do a Empty -> Point conversion here. However, the viewer (and likly other apps) don't support a pont with "elements"
#TODO: we could do a Empty -> Point conversion here. However, the viewer (and likely other apps) don't support a pont with "elements"
#return matrix_to_speckle_point(cast(MMatrix, blender_object.matrix_world))


Expand Down
2 changes: 1 addition & 1 deletion bpy_speckle/convert/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -449,7 +449,7 @@ def link_object_to_collection_nested(obj: Object, col: BCollection):
for child in obj.children: #type: ignore
link_object_to_collection_nested(child, col)

def add_to_heirarchy(converted: Union[Object, BCollection], traversalContext : 'TraversalContext', converted_objects: Dict[str, Union[Object, BCollection]], preserve_transform: bool) -> None:
def add_to_hierarchy(converted: Union[Object, BCollection], traversalContext : 'TraversalContext', converted_objects: Dict[str, Union[Object, BCollection]], preserve_transform: bool) -> None:
nextParent = traversalContext.parent

# Traverse up the tree to find a direct parent object, and a containing collection
Expand Down
Loading

0 comments on commit 5ddb2aa

Please sign in to comment.