Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 95f4d051d6 | |||
| c79ad8e87d | |||
| 9797dfbfc0 | |||
| 63b00a6257 | |||
| 36091845a6 | |||
| 89e1855e2c | |||
| b7f5725282 | |||
| dc8c8cedf4 | |||
| 31e8b838dd | |||
| baf7f32c2a | |||
| ad1d58bd4c | |||
| ec86688750 | |||
| 84098f4c42 | |||
| 77f9d73698 | |||
| 812e8dd2f3 |
@@ -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&style=flat-square&logo=discourse&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&logo=read-the-docs&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&style=flat-square&logo=discourse&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&logo=read-the-docs&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&circle-token=76eabd350ea243575cbb258b746ed3f471f7ac29" alt="Speckle-Next"></a> </p>
|
||||
|
||||
# About Speckle
|
||||
@@ -25,20 +25,19 @@ What is Speckle? Check our ](https://speckle.xyz) ⇒ creating an account at our public server
|
||||
- [](https://marketplace.digitalocean.com/apps/speckle-server?refcode=947a2b5d7dc1) ⇒ deploying an instance in 1 click
|
||||
- [](https://app.speckle.systems) ⇒ creating an account at our public server
|
||||
|
||||
### Resources
|
||||
|
||||
- [](https://speckle.community) for help, feature requests or just to hang with other speckle enthusiasts, check out our community forum!
|
||||
- [](https://speckle.systems) our tutorials portal is full of resources to get you started using Speckle
|
||||
- [](https://speckle.guide/user/blender.html) reference on almost any end-user and developer functionality
|
||||
- [](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
|
||||
|
||||
@@ -1,25 +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,
|
||||
build_object_id_map,
|
||||
)
|
||||
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(
|
||||
@@ -33,6 +37,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,26 +52,47 @@ 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
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
# 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
|
||||
@@ -78,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)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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", "")
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
from typing import Tuple, List, Optional
|
||||
from typing import Tuple, List, Optional, Dict
|
||||
import bpy
|
||||
import mathutils
|
||||
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]:
|
||||
@@ -117,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
|
||||
@@ -186,3 +206,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
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user