Compare commits

..

14 Commits

Author SHA1 Message Date
Mucahit Bilal GOKER c3976b6962 Merge remote-tracking branch 'origin/v3-dev' into bilal/cnx-1895-load-properties-in-blender 2025-09-21 16:39:49 +03:00
Mucahit Bilal GOKER 6b9420f5f1 instance proxies -> collection instances 2025-09-21 16:26:30 +03:00
Mucahit Bilal GOKER fedc7a8e67 extract properties only for data objects 2025-09-21 16:22:51 +03:00
Mucahit Bilal GOKER 3c09fec185 remove unnecessary boolean check 2025-09-21 16:13:37 +03: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
Mucahit Bilal GOKER 4de853f89a Merge branch 'v3-dev' into bilal/cnx-1895-load-properties-in-blender 2025-07-22 12:44:08 +03:00
Mucahit Bilal GOKER 9a1469afe9 exclude archicad composite structure 2025-07-11 17:47:53 +03:00
Mucahit Bilal GOKER 97d425e59f property extraction and conversion functions 2025-07-11 17:24:42 +03:00
17 changed files with 275 additions and 128 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
@@ -1,7 +1,7 @@
import bpy
from bpy.types import Context, Event, UILayout
from specklepy.core.api.inputs import CreateModelInput
from specklepy.core.api.models import Model
from typing import Tuple
from ..utils.account_manager import _client_cache
@@ -21,12 +21,12 @@ class SPECKLE_OT_create_model(bpy.types.Operator):
return {"CANCELLED"}
try:
model = _create_model(
model_id, model_name = create_model(
wm.selected_account_id, wm.selected_project_id, self.model_name
)
wm.selected_model_id = model.id
wm.selected_model_name = model.name
self.report({"INFO"}, f"Created model: {model.name} -> ID: {model.id}")
wm.selected_model_id = model_id
wm.selected_model_name = model_name
self.report({"INFO"}, f"Created model: {model_name} -> ID: {model_id}")
# Force redraw
context.window.screen = context.window.screen
context.area.tag_redraw()
@@ -51,17 +51,19 @@ def unregister() -> None:
bpy.utils.unregister_class(SPECKLE_OT_create_model)
def _create_model(account_id: str, project_id: str, model_name: str) -> Model:
def create_model(account_id: str, project_id: str, model_name: str) -> Tuple[str, str]:
try:
# Get cached client
client = _client_cache.get_client(account_id)
if not client:
raise ValueError(f"Could not get client for account: {account_id}")
model = client.model.create(
input=CreateModelInput(
name=model_name, description="", project_id=project_id
)
)
return model
return (model.id, model.name)
except Exception as e:
# Clear cache on error to prevent stale clients
_client_cache.clear()
@@ -1,11 +1,10 @@
from typing import Optional
import bpy
from bpy.types import Context, Event, UILayout
from specklepy.core.api.enums import ProjectVisibility
from specklepy.core.api.inputs import ProjectCreateInput
from specklepy.core.api.inputs.project_inputs import WorkspaceProjectCreateInput
from specklepy.core.api.models import Project
from specklepy.core.api.enums import ProjectVisibility
from typing import Tuple, Optional
from ..utils.account_manager import _client_cache
@@ -23,16 +22,16 @@ class SPECKLE_OT_create_project(bpy.types.Operator):
def execute(self, context: Context) -> set[str]:
wm = context.window_manager
project = _create_project(
project_id, project_name = create_project(
wm.selected_account_id,
self.project_name,
None
if wm.selected_workspace.id == "personal"
else wm.selected_workspace.id,
)
wm.selected_project_id = project.id
wm.selected_project_name = project.name
self.report({"INFO"}, f"Created project: {project.name} -> ID: {project.id}")
wm.selected_project_id = project_id
wm.selected_project_name = project_name
self.report({"INFO"}, f"Created project: {project_name} -> ID: {project_id}")
# Force redraw
context.window.screen = context.window.screen
context.area.tag_redraw()
@@ -54,19 +53,20 @@ def unregister() -> None:
bpy.utils.unregister_class(SPECKLE_OT_create_project)
def _create_project(
def create_project(
account_id: str, project_name: str, workspace_id: Optional[str]
) -> Project:
) -> Tuple[str, str]:
try:
# Get cached client
client = _client_cache.get_client(account_id)
if not client:
raise Exception(f"Could not get client for account: {account_id}")
if workspace_id:
project = client.project.create_in_workspace(
input=WorkspaceProjectCreateInput(
name=project_name,
description="",
visibility=ProjectVisibility.PUBLIC,
visibility=ProjectVisibility("PUBLIC"),
workspaceId=workspace_id,
)
)
@@ -75,11 +75,11 @@ def _create_project(
input=ProjectCreateInput(
name=project_name,
description="",
visibility=ProjectVisibility.PUBLIC,
visibility=ProjectVisibility("PUBLIC"),
)
)
return project
return (project.id, project.name)
except Exception as e:
print(f"Failed to create project: {str(e)}")
# Clear cache on error to prevent stale clients
@@ -20,7 +20,7 @@ class SPECKLE_OT_load(bpy.types.Operator):
description="Choose how to load instances",
items=[
(
"INSTANCE_PROXIES",
"COLLECTION_INSTANCES",
"Collection Instances",
"Load objects as collection instances",
),
@@ -30,7 +30,7 @@ class SPECKLE_OT_load(bpy.types.Operator):
"Get objects as linked duplicates",
),
],
default="INSTANCE_PROXIES",
default="COLLECTION_INSTANCES",
)
def draw(self, context: Context) -> None:
@@ -1,29 +1,29 @@
from typing import Dict, Union
import bpy
from bpy.types import Context
from specklepy.core.api import host_applications, operations
from specklepy.logging import metrics
from specklepy.transports.server import ServerTransport
from specklepy.core.api import operations
from specklepy.objects.models.collections.collection import Collection as SCollection
from specklepy.objects.graph_traversal.default_traversal import (
create_default_traversal_function,
)
from specklepy.objects.models.collections.collection import Collection as SCollection
from specklepy.transports.server import ServerTransport
from specklepy.core.api import host_applications
from ... import bl_info
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.to_native import (
convert_to_native,
find_instance_definitions,
instance_definition_proxy_to_native,
render_material_proxy_to_native,
instance_definition_proxy_to_native,
find_instance_definitions,
)
from ...converter.utils import find_object_by_id
from ..utils.account_manager import _client_cache
from ..utils.get_ascendants import get_ascendants
from specklepy.logging import metrics
from ... import bl_info
from typing import Dict, Union
def load_operation(
context: Context, instance_loading_mode: str = "INSTANCE_PROXIES"
context: Context, instance_loading_mode: str = "COLLECTION_INSTANCES"
) -> Dict[str, Union[bpy.types.Collection, bpy.types.Object]]:
"""
load objects from Speckle and maintain hierarchy.
@@ -33,6 +33,9 @@ def load_operation(
# get cached client
client = _client_cache.get_client(context.window_manager.selected_account_id)
if not client:
print("No Speckle client found")
return {}
print(f"Using client for account: {context.window_manager.selected_account_id}")
@@ -45,21 +48,36 @@ def load_operation(
metrics.set_host_app("blender")
metrics.track(
metrics.RECEIVE,
client.account,
{
"ui": "dui3",
"hostAppVersion": ".".join(map(str, bl_info["blender"])),
"core_version": ".".join(map(str, bl_info["version"])),
"sourceHostApp": host_applications.get_host_app_from_string(
version.source_application
).slug,
"isMultiplayer": version.author_user.id != client.account.userInfo.id,
"workspace_id": client.project.get(wm.selected_project_id).workspace_id,
},
# Get account for metrics tracking
from specklepy.core.api.credentials import get_local_accounts
account = next(
(
acc
for acc in get_local_accounts()
if acc.id == context.window_manager.selected_account_id
),
None,
)
if account:
metrics.track(
metrics.RECEIVE,
account,
{
"ui": "dui3",
"hostAppVersion": ".".join(map(str, bl_info["blender"])),
"core_version": ".".join(map(str, bl_info["version"])),
"sourceHostApp": host_applications.get_host_app_from_string(
version.source_application
).slug,
"isMultiplayer": version.author_user.id != account.userInfo.id,
"workspace_id": get_project_workspace_id(
client, wm.selected_project_id
),
},
)
# Create material mapping first
material_mapping = render_material_proxy_to_native(version_data)
@@ -1,22 +1,22 @@
from typing import Dict, List, Optional, Tuple
import bpy
from bpy.types import Collection as BlenderCollection
from bpy.types import Context
from specklepy.core.api import operations
from specklepy.core.api.inputs.version_inputs import CreateVersionInput
from specklepy.logging import metrics
from bpy.types import Context, Collection as BlenderCollection
from typing import List, Optional, Dict, Tuple
from specklepy.objects import Base
from specklepy.objects.models.collections.collection import Collection
from specklepy.objects.models.units import Units
from specklepy.core.api import operations
from specklepy.transports.server import ServerTransport
from specklepy.core.api.inputs.version_inputs import CreateVersionInput
from specklepy.objects.models.units import Units
from ... import bl_info
from ...converter.to_speckle import convert_to_speckle
from ...converter.to_speckle.material_to_speckle import (
add_render_material_proxies_to_base,
)
from ...converter.utils import get_project_workspace_id
from ..utils.account_manager import _client_cache
from specklepy.logging import metrics
from ... import bl_info
def publish_operation(
@@ -33,6 +33,8 @@ def publish_operation(
try:
# get cached client
client = _client_cache.get_client(wm.selected_account_id)
if not client:
return False, "No Speckle client found", None
transport = ServerTransport(stream_id=wm.selected_project_id, client=client)
@@ -50,29 +52,40 @@ def publish_operation(
obj_id = operations.send(root_collection, [transport])
version_input = CreateVersionInput(
object_id=obj_id,
model_id=wm.selected_model_id,
project_id=wm.selected_project_id,
objectId=obj_id,
modelId=wm.selected_model_id,
projectId=wm.selected_project_id,
message=version_message,
source_application="blender",
sourceApplication="blender",
)
version = client.version.create(version_input)
version_id = version.id
# track metrics
metrics.set_host_app("blender")
metrics.track(
metrics.SEND,
client.account,
{
"ui": "dui3",
"hostAppVersion": ".".join(map(str, bl_info["blender"])),
"core_version": ".".join(map(str, bl_info["version"])),
"workspace_id": client.project.get(wm.selected_project_id).workspace_id,
},
# Get account for metrics tracking
from specklepy.core.api.credentials import get_local_accounts
account = next(
(acc for acc in get_local_accounts() if acc.id == wm.selected_account_id),
None,
)
if account:
# track metrics
metrics.set_host_app("blender")
metrics.track(
metrics.SEND,
account,
{
"ui": "dui3",
"hostAppVersion": ".".join(map(str, bl_info["blender"])),
"core_version": ".".join(map(str, bl_info["version"])),
"workspace_id": get_project_workspace_id(
client, wm.selected_project_id
),
},
)
# count total objects for success message
total_objects = count_objects_in_collection(root_collection)
@@ -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!")
@@ -1,6 +1,5 @@
import bpy
from bpy.types import Context, Event, PropertyGroup, UILayout
from bpy.types import UILayout, Context, PropertyGroup, Event
from ..utils.model_manager import get_models_for_project
from ..utils.version_manager import get_latest_version
@@ -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(
+4 -10
View File
@@ -1,10 +1,9 @@
from typing import Dict, List, Optional, Tuple
import bpy
from specklepy.core.api.credentials import get_local_accounts
from typing import List, Tuple, Optional, Dict
from specklepy.core.api.credentials import Account
from specklepy.core.api.client import SpeckleClient
from specklepy.core.api.credentials import Account, get_local_accounts
from specklepy.core.api.wrapper import StreamWrapper
from .misc import strip_non_ascii
@@ -24,11 +23,7 @@ class SpeckleClientCache:
if not account:
raise ValueError(f"No account found for ID: {account_id}")
assert account.serverInfo.url
client = SpeckleClient(
host=account.serverInfo.url,
use_ssl=account.serverInfo.url.startswith("https"),
)
client = SpeckleClient(host=account.serverInfo.url)
client.authenticate_with_account(account)
self._clients[account_id] = client
return client
@@ -184,7 +179,6 @@ def get_project_from_url(
try:
wrapper = StreamWrapper(url)
account = wrapper.get_account()
assert account.id
client = _client_cache.get_client(account.id)
# get the stream_id (project_id) from the wrapper
+11 -5
View File
@@ -1,10 +1,8 @@
from typing import List, Optional, Tuple
from specklepy.core.api.inputs.project_inputs import ProjectModelsFilter
from specklepy.core.api.models.current import Model
from .account_manager import _client_cache
from typing import List, Tuple, Optional
from .misc import format_relative_time, strip_non_ascii
from .account_manager import _client_cache
def get_models_for_project(
@@ -20,9 +18,17 @@ def get_models_for_project(
)
return []
# Get cached client
client = _client_cache.get_client(account_id)
if not client:
print(f"Error: Could not get client for account: {account_id}")
return []
client.project.get(project_id)
try:
client.project.get(project_id)
except Exception as e:
print(f"Error: Project with ID {project_id} not found: {str(e)}")
return []
filter = ProjectModelsFilter(search=search) if search else None
+9 -13
View File
@@ -1,16 +1,14 @@
from typing import List, Optional, Tuple
from specklepy.core.api.client import SpeckleClient
from specklepy.core.api.credentials import Account
from specklepy.core.api.inputs.project_inputs import WorksaceProjectsFilter
from specklepy.core.api.resources.current.workspace_resource import WorkspaceResource
from .account_manager import _client_cache
from specklepy.core.api.inputs.project_inputs import WorksaceProjectsFilter
from typing import List, Tuple, Optional
from specklepy.core.api.credentials import Account
from .misc import format_relative_time, format_role, strip_non_ascii
from .account_manager import _client_cache
def get_projects_for_account(
account_id: str, workspace_id: str, search: Optional[str] = None
account_id: str, workspace_id: str = None, search: Optional[str] = None
) -> List[Tuple[str, str, str, str, bool]]:
"""
fetches projects for a given account from the Speckle server
@@ -104,13 +102,12 @@ def _get_personal_projects_with_permissions(
helper function to get personal projects with permissions using the old method
"""
from specklepy.core.api.inputs.user_inputs import UserProjectsFilter
from .account_manager import can_load
filter = UserProjectsFilter(
search=search,
workspace_id=None,
personal_only=True,
workspaceId=None,
personalOnly=True,
include_implicit_access=True,
)
@@ -144,13 +141,12 @@ def _get_projects_with_individual_permissions(
Fallback helper function to get projects with permissions using individual API calls
"""
from specklepy.core.api.inputs.user_inputs import UserProjectsFilter
from .account_manager import can_load
filter = UserProjectsFilter(
search=search,
workspace_id=workspace_id,
personal_only=False,
workspaceId=workspace_id,
personalOnly=False,
include_implicit_access=True,
)
@@ -96,7 +96,7 @@ class speckle_model_card(bpy.types.PropertyGroup):
instance_loading_mode: bpy.props.StringProperty(
name="Instance Loading Mode",
description="Mode of loading instances",
default="INSTANCE_PROXIES",
default="COLLECTION_INSTANCES",
) # type: ignore
apply_modifiers: bpy.props.BoolProperty(
name="Apply Modifiers",
+14 -6
View File
@@ -1,10 +1,9 @@
from typing import List, Tuple
from specklepy.core.api.client import SpeckleClient
from specklepy.core.api.models.current import Version
from .account_manager import _client_cache
from typing import List, Tuple
from .misc import format_relative_time
from specklepy.core.api.inputs.model_inputs import ModelVersionsFilter
from specklepy.core.api.models.current import Version
def get_versions_for_model(
@@ -21,11 +20,17 @@ def get_versions_for_model(
)
return []
# Get cached client
client: SpeckleClient = _client_cache.get_client(account_id)
if not client:
print(f"Error: Could not get client for account: {account_id}")
return []
filter: ModelVersionsFilter = ModelVersionsFilter(priorityIds=[])
# Get versions
versions = client.version.get_versions(
project_id=project_id, model_id=model_id, limit=10
versions: List[Version] = client.version.get_versions(
project_id=project_id, model_id=model_id, limit=10, filter=filter
)
versions_list: List[Tuple[str, str, str]] = []
for version in versions.items:
@@ -61,6 +66,9 @@ def get_latest_version(
# Get cached client
client: SpeckleClient = _client_cache.get_client(account_id)
if not client:
print(f"Error: Could not get client for account: {account_id}")
return ("", "", "")
# Get versions (limit to 1 since we only need the latest)
versions: List[Version] = client.version.get_versions(
+93 -7
View File
@@ -85,7 +85,7 @@ def convert_to_native(
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",
instance_loading_mode: str = "COLLECTION_INSTANCES",
) -> Optional[Object]:
"""
converts a speckle object to blender object with material support
@@ -114,7 +114,7 @@ def convert_to_native(
converted_object = instance_proxy_to_linked_duplicates(
speckle_object, coll, root_collection, scale
)
else: # INSTANCE_PROXIES (default)
else: # COLLECTION_INSTANCES (default)
converted_object = instance_proxy_to_native(
speckle_object, coll, root_collection, scale
)
@@ -188,9 +188,95 @@ def convert_to_native(
if hasattr(speckle_object, "applicationId"):
converted_object["applicationId"] = speckle_object.applicationId
# Extract and store Speckle properties
speckle_properties = extract_speckle_properties(speckle_object)
for prop_name, prop_value in speckle_properties.items():
try:
converted_object[prop_name] = prop_value
except Exception as e:
# Skip problematic properties
print(f"Warning: Could not set property '{prop_name}': {e}")
return converted_object
def convert_property_value(value: Any) -> Any:
"""Convert property values to appropriate Python types."""
if isinstance(value, str):
# Return string as-is
return value
elif isinstance(value, (int, float)):
# Return numeric values as-is
return value
elif isinstance(value, bool):
# Return boolean as-is
return value
else:
# Convert everything else to string
return str(value)
def _traverse_properties(obj: Any, parent_name: str = "") -> Dict[str, Any]:
"""Recursively traverse properties, collecting name/value pairs with duplicate handling."""
properties = {}
for key, value in obj.items():
# Skip excluded sections
if key == "Material Quantities":
continue
if key == "Composite Structure":
continue
if parent_name == "Type Parameters" and key == "Structure":
continue
if isinstance(value, dict):
# Check if this is a complex property (has name and value)
if "name" in value and "value" in value:
# Extract only name and value, ignore other fields
prop_name = value.get("name", key)
prop_value = convert_property_value(value["value"])
# Handle duplicates by adding parent suffix
final_name = prop_name
if final_name in properties:
final_name = f"{prop_name}_{key}" # Use the dict key as suffix
properties[final_name] = prop_value
else:
# Recurse into nested structure
nested_props = _traverse_properties(value, key)
for nested_name, nested_value in nested_props.items():
# Handle duplicates by adding parent suffix
final_name = nested_name
if final_name in properties:
final_name = f"{nested_name}_{key}"
properties[final_name] = nested_value
else:
# Simple property - store directly with key as name
final_name = key
if final_name in properties:
final_name = f"{key}_{parent_name}" if parent_name else key
properties[final_name] = convert_property_value(value)
return properties
def extract_speckle_properties(speckle_object: Base) -> Dict[str, Any]:
"""Extract properties from Speckle object properties field only."""
if not isinstance(speckle_object, DataObject):
return {}
try:
return _traverse_properties(speckle_object.properties)
except Exception as e:
# Silently handle any extraction errors
print(f"Warning: Failed to extract properties: {e}")
return {}
def display_value_to_native(
speckle_object: Base,
object_name: str,
@@ -321,7 +407,7 @@ def _members_to_native(
for item in others:
try:
blender_object = convert_to_native(
item, material_mapping, instance_loading_mode="INSTANCE_PROXIES"
item, material_mapping, instance_loading_mode="COLLECTION_INSTANCES"
)
if blender_object:
# If the parent is a DataObject, override the name of the converted child
@@ -1210,13 +1296,13 @@ def instance_definition_proxy_to_native(
root_object: Base,
material_mapping: Dict[str, Any],
processed_definitions: Dict[str, Any] = None,
instance_loading_mode: str = "INSTANCE_PROXIES",
instance_loading_mode: str = "COLLECTION_INSTANCES",
) -> Tuple[Dict[str, bpy.types.Collection], Dict[str, Any]]:
"""
converts instance definition proxies to Blender collections recursively
"""
# Validate instance loading mode
assert instance_loading_mode in ["INSTANCE_PROXIES", "LINKED_DUPLICATES"], (
assert instance_loading_mode in ["COLLECTION_INSTANCES", "LINKED_DUPLICATES"], (
f"Invalid instance_loading_mode: {instance_loading_mode}. "
"Must be 'INSTANCE_PROXIES' or 'LINKED_DUPLICATES'"
)
@@ -1285,7 +1371,7 @@ def instance_definition_proxy_to_native(
definition_collection,
scale=1.0,
)
else: # INSTANCE_PROXIES (default)
else: # COLLECTION_INSTANCES (default)
blender_obj = instance_proxy_to_native(
found_obj,
definition_collections[found_obj.definitionId],
@@ -1298,7 +1384,7 @@ def instance_definition_proxy_to_native(
blender_obj = convert_to_native(
found_obj,
material_mapping,
instance_loading_mode="INSTANCE_PROXIES",
instance_loading_mode="COLLECTION_INSTANCES",
)
if blender_obj:
definition_collection.objects.link(blender_obj)
+19
View File
@@ -5,6 +5,7 @@ from specklepy.objects import Base
from specklepy.objects.graph_traversal.default_traversal import (
create_default_traversal_function,
)
from specklepy.core.api.client import SpeckleClient
def to_rgba(argb_int: int) -> Tuple[float, float, float, float]:
@@ -186,3 +187,21 @@ def find_object_by_id(root_object: Base, target_id: str) -> Optional[Base]:
return None
return deep_search(root_object)
def get_project_workspace_id(client: SpeckleClient, project_id: str) -> Optional[str]:
workspace_id = None
server_version = client.project.server_version or client.server.version()
# Local yarn builds of server will report a server version if "dev"
# We'll assume that local builds are up-to-date with the latest features
if server_version[0] == "dev":
maj = 999
min = 999
else:
maj = server_version[0]
min = server_version[1]
if maj > 2 or (maj == 2 and min > 20):
workspace_id = client.project.get(project_id).workspace_id
return workspace_id
+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]