Compare commits

...

15 Commits

Author SHA1 Message Date
Dogukan Karatas 95f4d051d6 Merge pull request #304 from specklesystems/dogukan/cnx-2682-display-value-proxies-in-blender
Release workflow / Build Zip (push) Has been cancelled
Release workflow / deploy-installers (push) Has been cancelled
feat: display value proxy handler
2025-10-20 15:42:56 +02:00
Dogukan Karatas c79ad8e87d fixed coordinate space issue 2025-10-20 11:26:04 +02:00
Dogukan Karatas 9797dfbfc0 Merge branch 'v3-dev' into dogukan/cnx-2682-display-value-proxies-in-blender 2025-10-17 15:32:07 +02:00
Dogukan Karatas 63b00a6257 Merge pull request #305 from specklesystems/jrm/perf-receive
perf(receive): optimise set lookups
2025-10-17 15:31:18 +02:00
Dogukan Karatas 36091845a6 added an object id mapping 2025-10-17 15:05:07 +02:00
Jedd Morgan 89e1855e2c perf(receive): optimise set lookups 2025-10-17 11:44:21 +01:00
Dogukan Karatas b7f5725282 force linked duplicates for proxies 2025-10-17 11:13:19 +02:00
Dogukan Karatas dc8c8cedf4 proxy handler added 2025-10-16 14:17:41 +02:00
Mucahit Bilal GOKER 31e8b838dd Merge pull request #303 from specklesystems/bilal/bump-specklepy
Release workflow / Build Zip (push) Has been cancelled
Release workflow / deploy-installers (push) Has been cancelled
bump specklepy to 3.0.4
2025-09-08 12:32:32 +03:00
bimgeek baf7f32c2a bump specklepy to 3.0.4 2025-09-08 12:27:02 +03:00
Mucahit Bilal GOKER ad1d58bd4c Merge pull request #302 from specklesystems/bilal/null-check-on-active-workspace
null check on active workspace
2025-09-08 12:14:51 +03:00
Mucahit Bilal GOKER ec86688750 null check on active workspace 2025-09-05 16:26:22 +03:00
Mucahit Bilal GOKER 84098f4c42 Merge pull request #301 from specklesystems/bilal/update-docs
replace docs links
2025-08-26 11:38:12 +03:00
bimgeek 77f9d73698 replace xyz with app 2025-08-26 11:33:28 +03:00
bimgeek 812e8dd2f3 replace docs links 2025-08-26 11:30:22 +03:00
8 changed files with 151 additions and 54 deletions
+4 -5
View File
@@ -7,7 +7,7 @@
</h3>
<p align="center"><b>Speckle</b> is the data infrastructure for the AEC industry.</p><br/>
<p align="center"><a href="https://twitter.com/SpeckleSystems"><img src="https://img.shields.io/twitter/follow/SpeckleSystems?style=social" alt="Twitter Follow"></a> <a href="https://speckle.community"><img src="https://img.shields.io/discourse/users?server=https%3A%2F%2Fspeckle.community&amp;style=flat-square&amp;logo=discourse&amp;logoColor=white" alt="Community forum users"></a> <a href="https://speckle.systems"><img src="https://img.shields.io/badge/https://-speckle.systems-royalblue?style=flat-square" alt="website"></a> <a href="https://speckle.guide/dev/"><img src="https://img.shields.io/badge/docs-speckle.guide-orange?style=flat-square&amp;logo=read-the-docs&amp;logoColor=white" alt="docs"></a></p>
<p align="center"><a href="https://twitter.com/SpeckleSystems"><img src="https://img.shields.io/twitter/follow/SpeckleSystems?style=social" alt="Twitter Follow"></a> <a href="https://speckle.community"><img src="https://img.shields.io/discourse/users?server=https%3A%2F%2Fspeckle.community&amp;style=flat-square&amp;logo=discourse&amp;logoColor=white" alt="Community forum users"></a> <a href="https://speckle.systems"><img src="https://img.shields.io/badge/https://-speckle.systems-royalblue?style=flat-square" alt="website"></a> <a href="https://docs.speckle.systems/dev/"><img src="https://img.shields.io/badge/docs-docs.speckle.systems-orange?style=flat-square&amp;logo=read-the-docs&amp;logoColor=white" alt="docs"></a></p>
<p align="center"><a href="https://github.com/specklesystems/speckle-blender/"><img src="https://circleci.com/gh/specklesystems/speckle-blender.svg?style=svg&amp;circle-token=76eabd350ea243575cbb258b746ed3f471f7ac29" alt="Speckle-Next"></a> </p>
# About Speckle
@@ -25,20 +25,19 @@ What is Speckle? Check our ![YouTube Video Views](https://img.shields.io/youtube
- **GraphQL API:** get what you need anywhere you want it
- **Webhooks:** the base for a automation and next-gen pipelines
- **Built for developers:** we are building Speckle with developers in mind and got tools for every stack
- **Built for the AEC industry:** Speckle connectors are plugins for the most common software used in the industry such as Revit, Rhino, Grasshopper, AutoCAD, Civil 3D, Excel, Unreal Engine, Unity, QGIS, Blender and more!
- **Built for the AEC industry:** Speckle connectors are plugins for the most common software used in the industry such as Revit, Rhino, Grasshopper, AutoCAD, Civil 3D, Blender and more!
### Try Speckle now!
Give Speckle a try in no time by:
- [![speckle XYZ](https://img.shields.io/badge/https://-speckle.xyz-0069ff?style=flat-square&logo=hackthebox&logoColor=white)](https://speckle.xyz) ⇒ creating an account at our public server
- [![create a droplet](https://img.shields.io/badge/Create%20a%20Droplet-0069ff?style=flat-square&logo=digitalocean&logoColor=white)](https://marketplace.digitalocean.com/apps/speckle-server?refcode=947a2b5d7dc1) ⇒ deploying an instance in 1 click
- [![speckle XYZ](https://img.shields.io/badge/https://-app.speckle.systems-0069ff?style=flat-square&logo=hackthebox&logoColor=white)](https://app.speckle.systems) ⇒ creating an account at our public server
### Resources
- [![Community forum users](https://img.shields.io/badge/community-forum-green?style=for-the-badge&logo=discourse&logoColor=white)](https://speckle.community) for help, feature requests or just to hang with other speckle enthusiasts, check out our community forum!
- [![website](https://img.shields.io/badge/tutorials-speckle.systems-royalblue?style=for-the-badge&logo=youtube)](https://speckle.systems) our tutorials portal is full of resources to get you started using Speckle
- [![docs](https://img.shields.io/badge/docs-speckle.guide-orange?style=for-the-badge&logo=read-the-docs&logoColor=white)](https://speckle.guide/user/blender.html) reference on almost any end-user and developer functionality
- [![docs](https://img.shields.io/badge/docs-docs.speckle.systems-orange?style=for-the-badge&logo=read-the-docs&logoColor=white)](https://docs.speckle.systems/connectors/blender) reference on almost any end-user and developer functionality
# Blender Connector
@@ -10,7 +10,11 @@ from specklepy.core.api import host_applications
from ..utils.get_ascendants import get_ascendants
from ..utils.account_manager import _client_cache
from ...converter.utils import find_object_by_id, get_project_workspace_id
from ...converter.utils import (
find_object_by_id,
get_project_workspace_id,
build_object_id_map,
)
from ...converter.to_native import (
convert_to_native,
render_material_proxy_to_native,
@@ -78,11 +82,17 @@ def load_operation(
},
)
# Build object ID map once
object_id_map = build_object_id_map(version_data)
# Create material mapping first
material_mapping = render_material_proxy_to_native(version_data)
definition_collections, definition_objects = instance_definition_proxy_to_native(
version_data, material_mapping, instance_loading_mode=instance_loading_mode
version_data,
material_mapping,
instance_loading_mode=instance_loading_mode,
object_id_map=object_id_map,
)
definitions_root_collection = None
@@ -96,7 +106,8 @@ def load_operation(
for definition in find_instance_definitions(version_data).values():
definition_object_ids.update(definition.objects)
for obj_id in definition.objects:
found_obj = find_object_by_id(version_data, obj_id)
# Use ID map
found_obj = object_id_map.get(obj_id)
if found_obj:
if hasattr(found_obj, "id"):
definition_object_ids.add(found_obj.id)
@@ -123,7 +123,11 @@ def update_workspaces_list(context: Context) -> None:
workspace: speckle_workspace = wm.speckle_workspaces.add()
workspace.id = id
workspace.name = name
wm.selected_workspace.id = get_active_workspace(wm.selected_account_id)["id"]
active_workspace = get_active_workspace(wm.selected_account_id)
if active_workspace:
wm.selected_workspace.id = active_workspace["id"]
else:
wm.selected_workspace.id = "personal"
print("Updated Workspaces List!")
@@ -120,10 +120,13 @@ class SPECKLE_OT_project_selection_dialog(bpy.types.Operator):
if wm.selected_account_id == "":
wm.selected_account_id = get_default_account_id()
wm.selected_workspace.id = get_active_workspace(wm.selected_account_id)["id"]
wm.selected_workspace.name = get_active_workspace(wm.selected_account_id)[
"name"
]
active_workspace = get_active_workspace(wm.selected_account_id)
if active_workspace:
wm.selected_workspace.id = active_workspace["id"]
wm.selected_workspace.name = active_workspace["name"]
else:
wm.selected_workspace.id = "personal"
wm.selected_workspace.name = "Personal Projects"
# Fetch projects from server
projects: List[Tuple[str, str, str, str, bool]] = get_projects_for_account(
@@ -1,6 +1,8 @@
from typing import Any, Dict, Optional
import bpy
from bpy.types import Context
from typing import Dict, Any, Optional
from ..utils.property_groups import speckle_model_card
@@ -316,11 +318,17 @@ def update_model_card_objects(
if isinstance(converted_objects, list):
converted_objects = {obj.name: obj for obj in converted_objects}
# Using a set keeps lookup O(1)
object_names = set()
collection_names = set()
for obj in converted_objects.values():
# Handle collections
if isinstance(obj, bpy.types.Collection):
if obj.name in (o.name for o in model_card.collections):
if obj.name in collection_names:
continue
collection_names.add(obj.name)
s_col = model_card.collections.add()
s_col.name = obj.name
s_col.applicationId = obj.get("applicationId", "")
@@ -334,8 +342,10 @@ def update_model_card_objects(
# Handle objects
elif isinstance(obj, bpy.types.Object):
if obj.name in (o.name for o in model_card.objects):
if obj.name in object_names:
continue
object_names.add(obj.name)
s_obj = model_card.objects.add()
s_obj.name = obj.name
s_obj.applicationId = obj.get("applicationId", "")
+87 -36
View File
@@ -159,7 +159,14 @@ def convert_to_native(
else:
# Fallback to display value if direct conversion not supported
mesh, children = display_value_to_native(
speckle_object, object_name, data_block_name, scale, material_mapping
speckle_object,
object_name,
data_block_name,
scale,
material_mapping,
definition_collections,
root_collection,
instance_loading_mode,
)
if mesh:
# Create a mesh object with the object_name (simple name) and mesh data
@@ -176,7 +183,11 @@ def convert_to_native(
# Ensure the converted object has the correct name (especially for DataObjects)
if isinstance(speckle_object, DataObject):
converted_object.name = object_name
data_block_name = converted_object.data.name
if (
hasattr(converted_object, "data")
and converted_object.data is not None
):
data_block_name = converted_object.data.name
# If there are multiple objects, parent remaining ones to the first
for child in children[1:]:
@@ -197,6 +208,9 @@ def display_value_to_native(
data_block_name: str,
scale: float,
material_mapping: Optional[Dict[str, bpy.types.Material]] = None,
definition_collections: Optional[Dict[str, bpy.types.Collection]] = None,
root_collection: Optional[bpy.types.Collection] = None,
instance_loading_mode: str = "INSTANCE_PROXIES",
) -> Tuple[Optional[bpy.types.Mesh], List[Object]]:
"""
fallback conversion mechanism using displayValue if present
@@ -215,6 +229,9 @@ def display_value_to_native(
DISPLAY_VALUE_PROPERTY_ALIASES,
True,
material_mapping,
definition_collections,
root_collection,
instance_loading_mode,
)
# If the parent had an applicationId and we created a mesh, apply the material
@@ -247,6 +264,9 @@ def elements_to_native(
data_block_name: str,
scale: float,
material_mapping: Optional[Dict[str, bpy.types.Material]] = None,
definition_collections: Optional[Dict[str, bpy.types.Collection]] = None,
root_collection: Optional[bpy.types.Collection] = None,
instance_loading_mode: str = "INSTANCE_PROXIES",
) -> List[Object]:
"""
convert elements collection of a speckle object
@@ -259,6 +279,9 @@ def elements_to_native(
ELEMENTS_PROPERTY_ALIASES,
False,
material_mapping,
definition_collections,
root_collection,
instance_loading_mode,
)
return elements
@@ -271,12 +294,16 @@ def _members_to_native(
members: Iterable[str],
combineMeshes: bool,
material_mapping: Optional[Dict[str, bpy.types.Material]] = None,
definition_collections: Optional[Dict[str, bpy.types.Collection]] = None,
root_collection: Optional[bpy.types.Collection] = None,
instance_loading_mode: str = "INSTANCE_PROXIES",
) -> Tuple[Optional[bpy.types.Mesh], List[Object]]:
"""
converts a given speckle_object by converting specified members
"""
meshes: List[Mesh] = []
others: List[Base] = []
instance_proxies: List[InstanceProxy] = []
for alias in members:
display = getattr(speckle_object, alias, None)
@@ -285,10 +312,13 @@ def _members_to_native(
MAX_DEPTH = 255 # some large value, to prevent infinite recursion
def separate(value: Any) -> bool:
nonlocal meshes, others, count, MAX_DEPTH
nonlocal meshes, others, instance_proxies, count, MAX_DEPTH
if combineMeshes and isinstance(value, Mesh):
meshes.append(value)
elif isinstance(value, InstanceProxy):
# Handle InstanceProxy objects separately - they need definition_collections
instance_proxies.append(value)
elif isinstance(value, Base):
others.append(value)
elif isinstance(value, list):
@@ -318,10 +348,28 @@ def _members_to_native(
# Check if the original object is a DataObject
is_data_object = isinstance(speckle_object, DataObject)
# Process InstanceProxy objects - do not add to children list as they are already
for item in instance_proxies:
try:
convert_to_native(
item,
material_mapping,
definition_collections=definition_collections,
root_collection=root_collection,
instance_loading_mode="LINKED_DUPLICATES", # always use Linked Duplicates for displayValue proxies
)
except Exception as ex:
print(f"Failed to convert instance proxy in display value {item}: {ex}")
# Process other objects
for item in others:
try:
blender_object = convert_to_native(
item, material_mapping, instance_loading_mode="INSTANCE_PROXIES"
item,
material_mapping,
definition_collections=definition_collections,
root_collection=root_collection,
instance_loading_mode=instance_loading_mode,
)
if blender_object:
# If the parent is a DataObject, override the name of the converted child
@@ -987,7 +1035,14 @@ def curve_to_native(
):
print("curve_to_native: degree 2 curve, falling back to displayValue")
mesh, children = display_value_to_native(
speckle_curve, object_name, data_block_name, scale
speckle_curve,
object_name,
data_block_name,
scale,
None,
None,
None,
"INSTANCE_PROXIES",
)
if mesh:
curve_obj = bpy.data.objects.new(object_name, mesh)
@@ -1059,7 +1114,14 @@ def polycurve_to_native(
and speckle_polycurve.displayValue
):
mesh, children = display_value_to_native(
speckle_polycurve, object_name, data_block_name, scale
speckle_polycurve,
object_name,
data_block_name,
scale,
None,
None,
None,
"INSTANCE_PROXIES",
)
if mesh:
curve_obj = bpy.data.objects.new(object_name, mesh)
@@ -1211,6 +1273,7 @@ def instance_definition_proxy_to_native(
material_mapping: Dict[str, Any],
processed_definitions: Dict[str, Any] = None,
instance_loading_mode: str = "INSTANCE_PROXIES",
object_id_map: Optional[Dict[str, Base]] = None,
) -> Tuple[Dict[str, bpy.types.Collection], Dict[str, Any]]:
"""
converts instance definition proxies to Blender collections recursively
@@ -1262,7 +1325,8 @@ def instance_definition_proxy_to_native(
# Process objects, including nested instances
if hasattr(definition, "objects") and isinstance(definition.objects, list):
for obj_id in definition.objects:
found_obj = find_object_by_id(root_object, obj_id)
# Use the ID map for lookup
found_obj = object_id_map.get(obj_id) if object_id_map else None
if found_obj:
try:
@@ -1362,7 +1426,8 @@ def instance_proxy_to_linked_duplicates(
print(f"Definition collection not found for instance {speckle_instance.id}")
return None
unit_scale = proxy_scale(speckle_instance)
# Use the scale from the parent context
unit_scale = scale
# convert transformation matrix
matrix = mathutils.Matrix(
@@ -1397,7 +1462,6 @@ def instance_proxy_to_linked_duplicates(
location, rotation, scale_vector = matrix.decompose()
location = location * unit_scale
# create transformation matrix
final_matrix = (
mathutils.Matrix.Translation(location)
@ rotation.to_matrix().to_4x4()
@@ -1409,10 +1473,8 @@ def instance_proxy_to_linked_duplicates(
parent_empty.empty_display_type = "PLAIN_AXES"
parent_empty.empty_display_size = 0.1
parent_empty.matrix_world = final_matrix
# link parent to root collection
root_collection.objects.link(parent_empty)
parent_empty.matrix_world = final_matrix
parent_empty["speckle_id"] = speckle_instance.id
parent_empty["speckle_type"] = speckle_instance.speckle_type
@@ -1422,15 +1484,14 @@ def instance_proxy_to_linked_duplicates(
duplicated_objects = []
for obj in definition_collection.objects:
# create a copy of the object with linked data
duplicate_obj = obj.copy()
duplicate_obj.name = f"{obj.name}_{speckle_instance.id[:8]}"
root_collection.objects.link(duplicate_obj)
# apply the instance transformation directly to each object
duplicate_obj.matrix_world = final_matrix @ obj.matrix_world
duplicate_obj.parent = parent_empty
duplicate_obj.matrix_parent_inverse.identity()
duplicate_obj.matrix_basis = obj.matrix_world
duplicated_objects.append(duplicate_obj)
@@ -1450,7 +1511,8 @@ def instance_proxy_to_native(
print(f"Definition collection not found for instance {speckle_instance.id}")
return None
unit_scale = proxy_scale(speckle_instance)
# Use the scale from the parent context
unit_scale = scale
# convert transformation matrix
matrix = mathutils.Matrix(
@@ -1483,35 +1545,24 @@ def instance_proxy_to_native(
)
location, rotation, scale_vector = matrix.decompose()
location = location * unit_scale
bpy.ops.object.collection_instance_add(
collection=definition_collection.name,
align="WORLD",
location=(0, 0, 0),
rotation=(0, 0, 0),
scale=(1, 1, 1),
)
instance_obj = bpy.context.active_object
instance_name = f"Instance_{speckle_instance.id}"
instance_obj = bpy.data.objects.new(instance_name, None)
instance_obj.instance_type = "COLLECTION"
instance_obj.instance_collection = definition_collection
instance_obj.empty_display_size = 0
instance_name = f"Instance_{speckle_instance.id}"
instance_obj.name = instance_name
if instance_obj.name not in root_collection.objects:
for coll in instance_obj.users_collection:
coll.objects.unlink(instance_obj)
root_collection.objects.link(instance_obj)
# Link to root collection
root_collection.objects.link(instance_obj)
# Store metadata
instance_obj["speckle_id"] = speckle_instance.id
instance_obj["speckle_type"] = speckle_instance.speckle_type
instance_obj["definition_id"] = speckle_instance.definitionId
if hasattr(speckle_instance, "maxDepth"):
instance_obj["max_depth"] = speckle_instance.maxDepth
# Apply transformation
final_matrix = (
mathutils.Matrix.Translation(location)
@ rotation.to_matrix().to_4x4()
+20 -1
View File
@@ -1,4 +1,4 @@
from typing import Tuple, List, Optional
from typing import Tuple, List, Optional, Dict
import bpy
import mathutils
from specklepy.objects import Base
@@ -118,6 +118,25 @@ def transform_matrix(transform: List[float]) -> mathutils.Matrix:
)
def build_object_id_map(root_object: Base) -> Dict[str, Base]:
"""
Builds a dictionary mapping object IDs (both id and applicationId) to objects.
"""
id_map = {}
traversal_function = create_default_traversal_function()
for traversal_item in traversal_function.traverse(root_object):
obj = traversal_item.current
if hasattr(obj, "id") and obj.id:
id_map[obj.id] = obj
if hasattr(obj, "applicationId") and obj.applicationId:
id_map[obj.applicationId] = obj
return id_map
def find_object_by_id(root_object: Base, target_id: str) -> Optional[Base]:
"""
finds an object using traversal, checking both id and applicationId
+1 -1
View File
@@ -5,7 +5,7 @@ description = "Next-Gen Speckle connector for Blender!"
requires-python = ">=3.11.9, <4.0.0"
license = "Apache-2.0"
dependencies = [
"specklepy>=3.0.3",
"specklepy>=3.0.4",
]
[dependency-groups]