Compare commits
43 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 32dca397a3 | |||
| 5c19d9aa16 | |||
| 29d706e1b6 | |||
| 25d02673a7 | |||
| 6a4dc62622 | |||
| bb9a5ea604 | |||
| 48759746dc | |||
| 4d5fb64893 | |||
| 6a3247aafa | |||
| 2e995bd0fa | |||
| ae0280a630 | |||
| 698b2a79fe | |||
| 38e6096ea9 | |||
| 30e3398cd4 | |||
| 3f7e98aff5 | |||
| 1ad8429928 | |||
| a6c820183b | |||
| 56b6c813c0 | |||
| 8ab110f7ec | |||
| 11ff018f18 | |||
| 227f63d266 | |||
| 9e8aaf4f3b | |||
| afcb760bbf | |||
| 58283439ab | |||
| 0c29a2ec0a | |||
| 4ec62d4168 | |||
| 8d596823ed | |||
| ccd62e3452 | |||
| 1bd08497e6 | |||
| d23cc5a738 | |||
| 3e2ac4b5b6 | |||
| 928bc15ff1 | |||
| e410e40060 | |||
| d1f2c938b1 | |||
| 388ec2bdfd | |||
| b057c6c0da | |||
| 40089bdbb8 | |||
| 49dd688219 | |||
| 6993e8cb83 | |||
| 709015b9d8 | |||
| bbf8a3b45e | |||
| f1eec55633 | |||
| f2bc9a9701 |
+25
-7
@@ -21,24 +21,23 @@ from .installer import ensure_dependencies
|
|||||||
ensure_dependencies(f"Blender {bpy.app.version[0]}.{bpy.app.version[1]}")
|
ensure_dependencies(f"Blender {bpy.app.version[0]}.{bpy.app.version[1]}")
|
||||||
|
|
||||||
bl_info = {
|
bl_info = {
|
||||||
"name": "Speckle Blender ",
|
"name": "Speckle Connector",
|
||||||
"author": "Speckle Systems",
|
"author": "Speckle",
|
||||||
"version": (3, 999, 999),
|
"version": (3, 999, 999),
|
||||||
"blender": (4, 2, 0),
|
"blender": (4, 2, 0),
|
||||||
"location": "3d viewport toolbar (N), under the Speckle tab.",
|
"location": "3d viewport toolbar (N), under the Speckle tab.",
|
||||||
"description": "The Speckle Connector using specklepy 3.x!",
|
"description": "Publish models to and load models from other AEC apps.",
|
||||||
"warning": "This add-on is WIP and should be used with caution",
|
"wiki_url": "https://speckle.systems/connectors/blender",
|
||||||
"wiki_url": "https://github.com/specklesystems/speckle-blender",
|
|
||||||
"category": "Scene",
|
"category": "Scene",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
# UI
|
# UI
|
||||||
from .connector.ui.main_panel import SPECKLE_PT_main_panel
|
from .connector.ui.main_panel import SPECKLE_PT_main_panel
|
||||||
|
from .connector.utils.account_manager import speckle_workspace
|
||||||
from .connector.ui.project_selection_dialog import (
|
from .connector.ui.project_selection_dialog import (
|
||||||
SPECKLE_OT_project_selection_dialog,
|
SPECKLE_OT_project_selection_dialog,
|
||||||
SPECKLE_UL_projects_list,
|
SPECKLE_UL_projects_list,
|
||||||
speckle_workspace,
|
|
||||||
)
|
)
|
||||||
from .connector.ui.model_selection_dialog import (
|
from .connector.ui.model_selection_dialog import (
|
||||||
SPECKLE_OT_model_selection_dialog,
|
SPECKLE_OT_model_selection_dialog,
|
||||||
@@ -81,7 +80,11 @@ from .connector.blender_operators.add_project_by_url import (
|
|||||||
|
|
||||||
from .connector.blender_operators.create_project import SPECKLE_OT_create_project
|
from .connector.blender_operators.create_project import SPECKLE_OT_create_project
|
||||||
from .connector.blender_operators.create_model import SPECKLE_OT_create_model
|
from .connector.blender_operators.create_model import SPECKLE_OT_create_model
|
||||||
from .connector.utils.account_manager import speckle_account
|
from .connector.utils.account_manager import (
|
||||||
|
speckle_account,
|
||||||
|
get_default_account_id,
|
||||||
|
_client_cache,
|
||||||
|
)
|
||||||
|
|
||||||
# States
|
# States
|
||||||
from .connector.states.speckle_state import (
|
from .connector.states.speckle_state import (
|
||||||
@@ -186,10 +189,25 @@ def register():
|
|||||||
|
|
||||||
invoke_window_manager_properties()
|
invoke_window_manager_properties()
|
||||||
|
|
||||||
|
# Pre-warm client cache for default account
|
||||||
|
try:
|
||||||
|
default_account_id = get_default_account_id()
|
||||||
|
if default_account_id:
|
||||||
|
print(
|
||||||
|
f"[Speckle] Pre-warming client for default account: {default_account_id}"
|
||||||
|
)
|
||||||
|
_client_cache.get_client(default_account_id)
|
||||||
|
print(
|
||||||
|
f"[Speckle] Client pre-warming complete for account: {default_account_id}"
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[Speckle] Failed to pre-warm client: {e}")
|
||||||
|
|
||||||
|
|
||||||
def unregister():
|
def unregister():
|
||||||
icons.unload_icons()
|
icons.unload_icons()
|
||||||
unregister_speckle_state() # Unregister SpeckleState
|
unregister_speckle_state() # Unregister SpeckleState
|
||||||
|
_client_cache.clear()
|
||||||
for cls in classes:
|
for cls in classes:
|
||||||
bpy.utils.unregister_class(cls)
|
bpy.utils.unregister_class(cls)
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ maintainer = "Speckle"
|
|||||||
type = "add-on"
|
type = "add-on"
|
||||||
|
|
||||||
# Optional link to documentation, support, source files, etc
|
# Optional link to documentation, support, source files, etc
|
||||||
website = "https://app.speckle.systems/connectors"
|
website = "https://speckle.systems/connectors/blender"
|
||||||
|
|
||||||
# Optional list defined by Blender and server, see:
|
# Optional list defined by Blender and server, see:
|
||||||
# https://docs.blender.org/manual/en/dev/advanced/extensions/tags.html
|
# https://docs.blender.org/manual/en/dev/advanced/extensions/tags.html
|
||||||
@@ -24,13 +24,9 @@ blender_version_min = "4.2.0"
|
|||||||
|
|
||||||
# License conforming to https://spdx.org/licenses/ (use "SPDX: prefix)
|
# License conforming to https://spdx.org/licenses/ (use "SPDX: prefix)
|
||||||
# https://docs.blender.org/manual/en/dev/advanced/extensions/licenses.html
|
# https://docs.blender.org/manual/en/dev/advanced/extensions/licenses.html
|
||||||
license = [
|
license = ["SPDX:Apache-2.0"]
|
||||||
"SPDX:Apache-2.0",
|
|
||||||
]
|
|
||||||
# Optional: required by some licenses.
|
# Optional: required by some licenses.
|
||||||
copyright = [
|
copyright = ["2022-2025 AEC SYSTEMS LTD"]
|
||||||
"2022-2025 AEC SYSTEMS LTD",
|
|
||||||
]
|
|
||||||
|
|
||||||
# Optional list of supported platforms. If omitted, the extension will be available in all operating systems.
|
# Optional list of supported platforms. If omitted, the extension will be available in all operating systems.
|
||||||
# platforms = ["windows-x64", "macos-arm64", "linux-x64"]
|
# platforms = ["windows-x64", "macos-arm64", "linux-x64"]
|
||||||
@@ -67,8 +63,4 @@ clipboard = "Copy and paste URLs and Names (UI)"
|
|||||||
# Optional: build settings.
|
# Optional: build settings.
|
||||||
# https://docs.blender.org/manual/en/dev/advanced/extensions/command_line_arguments.html#command-line-args-extension-build
|
# https://docs.blender.org/manual/en/dev/advanced/extensions/command_line_arguments.html#command-line-args-extension-build
|
||||||
[build]
|
[build]
|
||||||
paths_exclude_pattern = [
|
paths_exclude_pattern = ["__pycache__/", "/.vscode", "*.code-workspace"]
|
||||||
"__pycache__/",
|
|
||||||
"/.vscode",
|
|
||||||
"*.code-workspace",
|
|
||||||
]
|
|
||||||
|
|||||||
@@ -2,37 +2,37 @@ import bpy
|
|||||||
import webbrowser
|
import webbrowser
|
||||||
from bpy.types import Event, Context
|
from bpy.types import Event, Context
|
||||||
|
|
||||||
|
|
||||||
class SPECKLE_OT_add_account(bpy.types.Operator):
|
class SPECKLE_OT_add_account(bpy.types.Operator):
|
||||||
"""Operator for adding a new Speckle account.
|
"""Operator for adding a new Speckle account."""
|
||||||
"""
|
|
||||||
bl_idname = "speckle.add_account"
|
bl_idname = "speckle.add_account"
|
||||||
bl_label = "Add New Account"
|
bl_label = "Add New Account"
|
||||||
bl_description = "Add a new account"
|
bl_description = "Add a new account"
|
||||||
|
|
||||||
server_url: bpy.props.StringProperty( # type: ignore
|
server_url: bpy.props.StringProperty( # type: ignore
|
||||||
name="Server URL",
|
name="Server URL",
|
||||||
description="Speckle server URL to connect to",
|
description="Speckle server URL to connect to",
|
||||||
default="https://app.speckle.systems"
|
default="https://app.speckle.systems",
|
||||||
)
|
)
|
||||||
|
|
||||||
def invoke(self, context: Context, event: Event) -> set[str]:
|
def invoke(self, context: Context, event: Event) -> set[str]:
|
||||||
return context.window_manager.invoke_props_dialog(self)
|
return context.window_manager.invoke_props_dialog(self)
|
||||||
|
|
||||||
def draw(self, context: Context):
|
def draw(self, context: Context):
|
||||||
layout = self.layout
|
layout = self.layout
|
||||||
# Server URL textbox
|
# Server URL textbox
|
||||||
layout.prop(self, "server_url", text="Server URL")
|
layout.prop(self, "server_url", text="Server URL")
|
||||||
|
|
||||||
def execute(self, context: Context) -> set[str]:
|
def execute(self, context: Context) -> set[str]:
|
||||||
# Logic to handle sign in
|
# Logic to handle sign in
|
||||||
api_url = "http://localhost:29364"
|
api_url = "http://localhost:29364"
|
||||||
url = f"{api_url}/auth/add-account?serverUrl={self.server_url}"
|
url = f"{api_url}/auth/add-account?serverUrl={self.server_url}"
|
||||||
webbrowser.open(url)
|
webbrowser.open(url)
|
||||||
self.report({'INFO'}, f"Adding account from {self.server_url}: {url}")
|
self.report({"INFO"}, f"Adding account from {self.server_url}: {url}")
|
||||||
|
|
||||||
# Force redraw
|
# Force redraw
|
||||||
context.window.screen = context.window.screen
|
context.window.screen = context.window.screen
|
||||||
context.area.tag_redraw()
|
context.area.tag_redraw()
|
||||||
|
|
||||||
|
return {"FINISHED"}
|
||||||
return {'FINISHED'}
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import bpy
|
import bpy
|
||||||
from bpy.types import Context, Event, UILayout, WindowManager
|
from bpy.types import Context, Event, UILayout
|
||||||
from ..utils.account_manager import (
|
from ..utils.account_manager import (
|
||||||
get_model_details_by_wrapper,
|
get_model_details_by_wrapper,
|
||||||
get_project_from_url,
|
get_project_from_url,
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import bpy
|
import bpy
|
||||||
from bpy.types import Context, Event, UILayout
|
from bpy.types import Context, Event, UILayout
|
||||||
from specklepy.api.client import SpeckleClient
|
|
||||||
from specklepy.api.credentials import get_local_accounts, Account
|
|
||||||
from specklepy.core.api.inputs import CreateModelInput
|
from specklepy.core.api.inputs import CreateModelInput
|
||||||
from typing import List, Tuple, Optional
|
from specklepy.core.api.models import Model
|
||||||
|
|
||||||
|
from ..utils.account_manager import _client_cache
|
||||||
|
|
||||||
|
|
||||||
class SPECKLE_OT_create_model(bpy.types.Operator):
|
class SPECKLE_OT_create_model(bpy.types.Operator):
|
||||||
@@ -19,14 +19,14 @@ class SPECKLE_OT_create_model(bpy.types.Operator):
|
|||||||
if not self.model_name.strip():
|
if not self.model_name.strip():
|
||||||
self.report({"ERROR"}, "Model name cannot be empty")
|
self.report({"ERROR"}, "Model name cannot be empty")
|
||||||
return {"CANCELLED"}
|
return {"CANCELLED"}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
model_id, model_name = create_model(
|
model = _create_model(
|
||||||
wm.selected_account_id, wm.selected_project_id, self.model_name
|
wm.selected_account_id, wm.selected_project_id, self.model_name
|
||||||
)
|
)
|
||||||
wm.selected_model_id = model_id
|
wm.selected_model_id = model.id
|
||||||
wm.selected_model_name = model_name
|
wm.selected_model_name = model.name
|
||||||
self.report({"INFO"}, f"Created model: {model_name} -> ID: {model_id}")
|
self.report({"INFO"}, f"Created model: {model.name} -> ID: {model.id}")
|
||||||
# Force redraw
|
# Force redraw
|
||||||
context.window.screen = context.window.screen
|
context.window.screen = context.window.screen
|
||||||
context.area.tag_redraw()
|
context.area.tag_redraw()
|
||||||
@@ -51,19 +51,18 @@ def unregister() -> None:
|
|||||||
bpy.utils.unregister_class(SPECKLE_OT_create_model)
|
bpy.utils.unregister_class(SPECKLE_OT_create_model)
|
||||||
|
|
||||||
|
|
||||||
def create_model(account_id: str, project_id: str, model_name: str) -> Tuple[str, str]:
|
def _create_model(account_id: str, project_id: str, model_name: str) -> Model:
|
||||||
accounts: List[Account] = get_local_accounts()
|
try:
|
||||||
account: Optional[Account] = next(
|
# Get cached client
|
||||||
(acc for acc in accounts if acc.id == account_id), None
|
client = _client_cache.get_client(account_id)
|
||||||
)
|
|
||||||
|
|
||||||
if not account:
|
model = client.model.create(
|
||||||
raise ValueError(f"Account with ID {account_id} not found")
|
input=CreateModelInput(
|
||||||
|
name=model_name, description="", project_id=project_id
|
||||||
client = SpeckleClient(host=account.serverInfo.url)
|
)
|
||||||
client.authenticate_with_account(account)
|
)
|
||||||
model = client.model.create(
|
return model
|
||||||
input=CreateModelInput(name=model_name, description="", project_id=project_id)
|
except Exception as e:
|
||||||
)
|
# Clear cache on error to prevent stale clients
|
||||||
# Function is annotated to return Tuple[str, str] but currently returns a list.
|
_client_cache.clear()
|
||||||
return (model.id, model.name)
|
raise e
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
|
from typing import Optional
|
||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
from bpy.types import Context, Event, UILayout
|
from bpy.types import Context, Event, UILayout
|
||||||
|
from specklepy.core.api.enums import ProjectVisibility
|
||||||
from specklepy.api.client import SpeckleClient
|
|
||||||
from specklepy.api.credentials import get_local_accounts, Account
|
|
||||||
from specklepy.core.api.inputs import ProjectCreateInput
|
from specklepy.core.api.inputs import ProjectCreateInput
|
||||||
from specklepy.core.api.inputs.project_inputs import WorkspaceProjectCreateInput
|
from specklepy.core.api.inputs.project_inputs import WorkspaceProjectCreateInput
|
||||||
from specklepy.core.api.enums import ProjectVisibility
|
from specklepy.core.api.models import Project
|
||||||
from typing import List, Tuple, Optional
|
|
||||||
|
from ..utils.account_manager import _client_cache
|
||||||
|
|
||||||
|
|
||||||
class SPECKLE_OT_create_project(bpy.types.Operator):
|
class SPECKLE_OT_create_project(bpy.types.Operator):
|
||||||
@@ -22,16 +23,16 @@ class SPECKLE_OT_create_project(bpy.types.Operator):
|
|||||||
|
|
||||||
def execute(self, context: Context) -> set[str]:
|
def execute(self, context: Context) -> set[str]:
|
||||||
wm = context.window_manager
|
wm = context.window_manager
|
||||||
project_id, project_name = create_project(
|
project = _create_project(
|
||||||
wm.selected_account_id,
|
wm.selected_account_id,
|
||||||
self.project_name,
|
self.project_name,
|
||||||
None
|
None
|
||||||
if wm.selected_workspace.id == "personal"
|
if wm.selected_workspace.id == "personal"
|
||||||
else wm.selected_workspace.id,
|
else wm.selected_workspace.id,
|
||||||
)
|
)
|
||||||
wm.selected_project_id = project_id
|
wm.selected_project_id = project.id
|
||||||
wm.selected_project_name = project_name
|
wm.selected_project_name = project.name
|
||||||
self.report({"INFO"}, f"Created project: {project_name} -> ID: {project_id}")
|
self.report({"INFO"}, f"Created project: {project.name} -> ID: {project.id}")
|
||||||
# Force redraw
|
# Force redraw
|
||||||
context.window.screen = context.window.screen
|
context.window.screen = context.window.screen
|
||||||
context.area.tag_redraw()
|
context.area.tag_redraw()
|
||||||
@@ -53,23 +54,19 @@ def unregister() -> None:
|
|||||||
bpy.utils.unregister_class(SPECKLE_OT_create_project)
|
bpy.utils.unregister_class(SPECKLE_OT_create_project)
|
||||||
|
|
||||||
|
|
||||||
def create_project(
|
def _create_project(
|
||||||
account_id: str, project_name: str, workspace_id: Optional[str]
|
account_id: str, project_name: str, workspace_id: Optional[str]
|
||||||
) -> Tuple[str, str]:
|
) -> Project:
|
||||||
try:
|
try:
|
||||||
accounts: List[Account] = get_local_accounts()
|
# Get cached client
|
||||||
account: Optional[Account] = next(
|
client = _client_cache.get_client(account_id)
|
||||||
(acc for acc in accounts if acc.id == account_id), None
|
|
||||||
)
|
|
||||||
|
|
||||||
client = SpeckleClient(host=account.serverInfo.url)
|
|
||||||
client.authenticate_with_account(account)
|
|
||||||
if workspace_id:
|
if workspace_id:
|
||||||
project = client.project.create_in_workspace(
|
project = client.project.create_in_workspace(
|
||||||
input=WorkspaceProjectCreateInput(
|
input=WorkspaceProjectCreateInput(
|
||||||
name=project_name,
|
name=project_name,
|
||||||
description="",
|
description="",
|
||||||
visibility=ProjectVisibility("PUBLIC"),
|
visibility=ProjectVisibility.PUBLIC,
|
||||||
workspaceId=workspace_id,
|
workspaceId=workspace_id,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -78,11 +75,13 @@ def create_project(
|
|||||||
input=ProjectCreateInput(
|
input=ProjectCreateInput(
|
||||||
name=project_name,
|
name=project_name,
|
||||||
description="",
|
description="",
|
||||||
visibility=ProjectVisibility("PUBLIC"),
|
visibility=ProjectVisibility.PUBLIC,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
return (project.id, project.name)
|
return project
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Failed to create project: {str(e)}")
|
print(f"Failed to create project: {str(e)}")
|
||||||
|
# Clear cache on error to prevent stale clients
|
||||||
|
_client_cache.clear()
|
||||||
raise
|
raise
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from ..operations.load_operation import load_operation
|
|||||||
from ..utils.model_card_utils import (
|
from ..utils.model_card_utils import (
|
||||||
delete_model_card_objects,
|
delete_model_card_objects,
|
||||||
update_model_card_objects,
|
update_model_card_objects,
|
||||||
|
collect_objects_with_properties,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -27,6 +28,7 @@ class SPECKLE_OT_load_model_card(bpy.types.Operator):
|
|||||||
self.report({"ERROR"}, "Model card not found")
|
self.report({"ERROR"}, "Model card not found")
|
||||||
return {"CANCELLED"}
|
return {"CANCELLED"}
|
||||||
|
|
||||||
|
old_properties = collect_objects_with_properties(model_card)
|
||||||
delete_model_card_objects(model_card, context)
|
delete_model_card_objects(model_card, context)
|
||||||
|
|
||||||
# set wm
|
# set wm
|
||||||
@@ -48,7 +50,7 @@ class SPECKLE_OT_load_model_card(bpy.types.Operator):
|
|||||||
context, model_card.instance_loading_mode
|
context, model_card.instance_loading_mode
|
||||||
)
|
)
|
||||||
# update model card details
|
# update model card details
|
||||||
update_model_card_objects(model_card, converted_objects)
|
update_model_card_objects(model_card, converted_objects, old_properties)
|
||||||
model_card.version_id = latest_version_id
|
model_card.version_id = latest_version_id
|
||||||
|
|
||||||
else:
|
else:
|
||||||
@@ -63,7 +65,7 @@ class SPECKLE_OT_load_model_card(bpy.types.Operator):
|
|||||||
self.report({"ERROR"}, "Load operation failed")
|
self.report({"ERROR"}, "Load operation failed")
|
||||||
return {"CANCELLED"}
|
return {"CANCELLED"}
|
||||||
# update model card details
|
# update model card details
|
||||||
update_model_card_objects(model_card, converted_objects)
|
update_model_card_objects(model_card, converted_objects, old_properties)
|
||||||
|
|
||||||
# Clear selected model details from Window Manager
|
# Clear selected model details from Window Manager
|
||||||
wm.selected_account_id = ""
|
wm.selected_account_id = ""
|
||||||
|
|||||||
@@ -1,26 +1,25 @@
|
|||||||
|
from typing import Dict, Union
|
||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
from bpy.types import Context
|
from bpy.types import Context
|
||||||
from specklepy.core.api.credentials import get_local_accounts
|
from specklepy.core.api import host_applications, operations
|
||||||
from specklepy.transports.server import ServerTransport
|
from specklepy.logging import metrics
|
||||||
from specklepy.core.api import operations
|
|
||||||
from specklepy.core.api.client import SpeckleClient
|
|
||||||
from specklepy.objects.models.collections.collection import Collection as SCollection
|
|
||||||
from specklepy.objects.graph_traversal.default_traversal import (
|
from specklepy.objects.graph_traversal.default_traversal import (
|
||||||
create_default_traversal_function,
|
create_default_traversal_function,
|
||||||
)
|
)
|
||||||
from specklepy.core.api import host_applications
|
from specklepy.objects.models.collections.collection import Collection as SCollection
|
||||||
|
from specklepy.transports.server import ServerTransport
|
||||||
|
|
||||||
from ..utils.get_ascendants import get_ascendants
|
from ... import bl_info
|
||||||
from ...converter.utils import find_object_by_id, get_project_workspace_id
|
|
||||||
from ...converter.to_native import (
|
from ...converter.to_native import (
|
||||||
convert_to_native,
|
convert_to_native,
|
||||||
render_material_proxy_to_native,
|
|
||||||
instance_definition_proxy_to_native,
|
|
||||||
find_instance_definitions,
|
find_instance_definitions,
|
||||||
|
instance_definition_proxy_to_native,
|
||||||
|
render_material_proxy_to_native,
|
||||||
)
|
)
|
||||||
from specklepy.logging import metrics
|
from ...converter.utils import find_object_by_id
|
||||||
from ... import bl_info
|
from ..utils.account_manager import _client_cache
|
||||||
from typing import Dict, Union
|
from ..utils.get_ascendants import get_ascendants
|
||||||
|
|
||||||
|
|
||||||
def load_operation(
|
def load_operation(
|
||||||
@@ -32,25 +31,10 @@ def load_operation(
|
|||||||
|
|
||||||
wm = context.window_manager
|
wm = context.window_manager
|
||||||
|
|
||||||
# get account
|
# get cached client
|
||||||
account = next(
|
client = _client_cache.get_client(context.window_manager.selected_account_id)
|
||||||
(
|
|
||||||
acc
|
|
||||||
for acc in get_local_accounts()
|
|
||||||
if acc.id == context.window_manager.selected_account_id
|
|
||||||
),
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
|
|
||||||
if account is None:
|
print(f"Using client for account: {context.window_manager.selected_account_id}")
|
||||||
print("No Speckle account found")
|
|
||||||
return {}
|
|
||||||
|
|
||||||
print(f"Using account: {account.userInfo.email}")
|
|
||||||
|
|
||||||
# receive the data
|
|
||||||
client = SpeckleClient(host=account.serverInfo.url)
|
|
||||||
client.authenticate_with_account(account)
|
|
||||||
|
|
||||||
transport = ServerTransport(stream_id=wm.selected_project_id, client=client)
|
transport = ServerTransport(stream_id=wm.selected_project_id, client=client)
|
||||||
|
|
||||||
@@ -63,7 +47,7 @@ def load_operation(
|
|||||||
|
|
||||||
metrics.track(
|
metrics.track(
|
||||||
metrics.RECEIVE,
|
metrics.RECEIVE,
|
||||||
account,
|
client.account,
|
||||||
{
|
{
|
||||||
"ui": "dui3",
|
"ui": "dui3",
|
||||||
"hostAppVersion": ".".join(map(str, bl_info["blender"])),
|
"hostAppVersion": ".".join(map(str, bl_info["blender"])),
|
||||||
@@ -71,8 +55,8 @@ def load_operation(
|
|||||||
"sourceHostApp": host_applications.get_host_app_from_string(
|
"sourceHostApp": host_applications.get_host_app_from_string(
|
||||||
version.source_application
|
version.source_application
|
||||||
).slug,
|
).slug,
|
||||||
"isMultiplayer": version.author_user.id != account.userInfo.id,
|
"isMultiplayer": version.author_user.id != client.account.userInfo.id,
|
||||||
"workspace_id": get_project_workspace_id(client, wm.selected_project_id),
|
"workspace_id": client.project.get(wm.selected_project_id).workspace_id,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -103,7 +87,7 @@ def load_operation(
|
|||||||
|
|
||||||
traversal_function = create_default_traversal_function()
|
traversal_function = create_default_traversal_function()
|
||||||
|
|
||||||
root_collection_name = f"{wm.selected_model_name} - {wm.selected_version_id[:8]}"
|
root_collection_name = f"{wm.selected_model_name} - {wm.selected_version_id}"
|
||||||
root_collection = bpy.data.collections.new(root_collection_name)
|
root_collection = bpy.data.collections.new(root_collection_name)
|
||||||
context.scene.collection.children.link(root_collection)
|
context.scene.collection.children.link(root_collection)
|
||||||
|
|
||||||
@@ -140,7 +124,7 @@ def load_operation(
|
|||||||
speckle_root_id = speckle_obj.id
|
speckle_root_id = speckle_obj.id
|
||||||
|
|
||||||
collection_name = getattr(
|
collection_name = getattr(
|
||||||
speckle_obj, "name", f"Collection_{speckle_obj.id[:8]}"
|
speckle_obj, "name", f"Collection_{speckle_obj.id}"
|
||||||
)
|
)
|
||||||
|
|
||||||
parent_id = None
|
parent_id = None
|
||||||
@@ -153,6 +137,7 @@ def load_operation(
|
|||||||
"id": speckle_obj.id,
|
"id": speckle_obj.id,
|
||||||
"name": collection_name,
|
"name": collection_name,
|
||||||
"parent_id": parent_id,
|
"parent_id": parent_id,
|
||||||
|
"applicationId": getattr(speckle_obj, "applicationId", ""),
|
||||||
"blender_collection": None,
|
"blender_collection": None,
|
||||||
"full_path": [collection_name],
|
"full_path": [collection_name],
|
||||||
}
|
}
|
||||||
@@ -208,6 +193,8 @@ def load_operation(
|
|||||||
blender_collection = created_collections[collection_key]
|
blender_collection = created_collections[collection_key]
|
||||||
else:
|
else:
|
||||||
blender_collection = bpy.data.collections.new(coll_name)
|
blender_collection = bpy.data.collections.new(coll_name)
|
||||||
|
if coll_info.get("applicationId"):
|
||||||
|
blender_collection["applicationId"] = coll_info["applicationId"]
|
||||||
parent_collection.children.link(blender_collection)
|
parent_collection.children.link(blender_collection)
|
||||||
created_collections[collection_key] = blender_collection
|
created_collections[collection_key] = blender_collection
|
||||||
|
|
||||||
|
|||||||
@@ -1,23 +1,22 @@
|
|||||||
import bpy
|
from typing import Dict, List, Optional, Tuple
|
||||||
from bpy.types import Context, Collection as BlenderCollection
|
|
||||||
from typing import List, Optional, Dict, 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 specklepy.objects import Base
|
from specklepy.objects import Base
|
||||||
from specklepy.objects.models.collections.collection import Collection
|
from specklepy.objects.models.collections.collection import Collection
|
||||||
from specklepy.core.api import operations
|
|
||||||
from specklepy.core.api.client import SpeckleClient
|
|
||||||
from specklepy.transports.server import ServerTransport
|
|
||||||
from specklepy.core.api.inputs.version_inputs import CreateVersionInput
|
|
||||||
from specklepy.core.api.credentials import get_local_accounts
|
|
||||||
from specklepy.objects.models.units import Units
|
from specklepy.objects.models.units import Units
|
||||||
|
from specklepy.transports.server import ServerTransport
|
||||||
|
|
||||||
|
from ... import bl_info
|
||||||
from ...converter.to_speckle import convert_to_speckle
|
from ...converter.to_speckle import convert_to_speckle
|
||||||
from ...converter.to_speckle.material_to_speckle import (
|
from ...converter.to_speckle.material_to_speckle import (
|
||||||
add_render_material_proxies_to_base,
|
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(
|
def publish_operation(
|
||||||
@@ -32,22 +31,15 @@ def publish_operation(
|
|||||||
wm = context.window_manager
|
wm = context.window_manager
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# get account and authenticate
|
# get cached client
|
||||||
account = next(
|
client = _client_cache.get_client(wm.selected_account_id)
|
||||||
(acc for acc in get_local_accounts() if acc.id == wm.selected_account_id),
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
|
|
||||||
if account is None:
|
|
||||||
return False, "No Speckle account found", None
|
|
||||||
|
|
||||||
client = SpeckleClient(host=account.serverInfo.url)
|
|
||||||
client.authenticate_with_account(account)
|
|
||||||
|
|
||||||
transport = ServerTransport(stream_id=wm.selected_project_id, client=client)
|
transport = ServerTransport(stream_id=wm.selected_project_id, client=client)
|
||||||
|
|
||||||
# build collection hierarchy and convert objects
|
# build collection hierarchy and convert objects
|
||||||
root_collection = build_collection_hierarchy(context, objects_to_convert, apply_modifiers)
|
root_collection = build_collection_hierarchy(
|
||||||
|
context, objects_to_convert, apply_modifiers
|
||||||
|
)
|
||||||
|
|
||||||
if not root_collection:
|
if not root_collection:
|
||||||
return False, "No objects could be converted to Speckle format", None
|
return False, "No objects could be converted to Speckle format", None
|
||||||
@@ -58,11 +50,11 @@ def publish_operation(
|
|||||||
obj_id = operations.send(root_collection, [transport])
|
obj_id = operations.send(root_collection, [transport])
|
||||||
|
|
||||||
version_input = CreateVersionInput(
|
version_input = CreateVersionInput(
|
||||||
objectId=obj_id,
|
object_id=obj_id,
|
||||||
modelId=wm.selected_model_id,
|
model_id=wm.selected_model_id,
|
||||||
projectId=wm.selected_project_id,
|
project_id=wm.selected_project_id,
|
||||||
message=version_message,
|
message=version_message,
|
||||||
sourceApplication="blender",
|
source_application="blender",
|
||||||
)
|
)
|
||||||
|
|
||||||
version = client.version.create(version_input)
|
version = client.version.create(version_input)
|
||||||
@@ -72,14 +64,12 @@ def publish_operation(
|
|||||||
metrics.set_host_app("blender")
|
metrics.set_host_app("blender")
|
||||||
metrics.track(
|
metrics.track(
|
||||||
metrics.SEND,
|
metrics.SEND,
|
||||||
account,
|
client.account,
|
||||||
{
|
{
|
||||||
"ui": "dui3",
|
"ui": "dui3",
|
||||||
"hostAppVersion": ".".join(map(str, bl_info["blender"])),
|
"hostAppVersion": ".".join(map(str, bl_info["blender"])),
|
||||||
"core_version": ".".join(map(str, bl_info["version"])),
|
"core_version": ".".join(map(str, bl_info["version"])),
|
||||||
"workspace_id": get_project_workspace_id(
|
"workspace_id": client.project.get(wm.selected_project_id).workspace_id,
|
||||||
client, wm.selected_project_id
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -96,6 +86,8 @@ def publish_operation(
|
|||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
|
# Clear cache on error to prevent stale clients
|
||||||
|
_client_cache.clear()
|
||||||
return False, f"Failed to publish: {str(e)}", None
|
return False, f"Failed to publish: {str(e)}", None
|
||||||
|
|
||||||
|
|
||||||
@@ -114,7 +106,9 @@ def build_collection_hierarchy(
|
|||||||
if not collection_data["objects"] and not collection_data["collections"]:
|
if not collection_data["objects"] and not collection_data["collections"]:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
converted_objects = convert_selected_objects(context, objects_to_convert, apply_modifiers)
|
converted_objects = convert_selected_objects(
|
||||||
|
context, objects_to_convert, apply_modifiers
|
||||||
|
)
|
||||||
if not converted_objects:
|
if not converted_objects:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -276,7 +270,9 @@ def convert_selected_objects(
|
|||||||
speckle_objects.append(None)
|
speckle_objects.append(None)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
speckle_obj = convert_to_speckle(obj, scale_factor, units.value, apply_modifiers)
|
speckle_obj = convert_to_speckle(
|
||||||
|
obj, scale_factor, units.value, apply_modifiers
|
||||||
|
)
|
||||||
speckle_objects.append(speckle_obj)
|
speckle_objects.append(speckle_obj)
|
||||||
|
|
||||||
return speckle_objects
|
return speckle_objects
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
from .main_panel import SPECKLE_PT_main_panel # noqa: F401
|
from .main_panel import SPECKLE_PT_main_panel # noqa: F401
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import bpy
|
import bpy
|
||||||
from bpy.types import UILayout, Context, PropertyGroup, Event
|
from bpy.types import Context, Event, PropertyGroup, UILayout
|
||||||
|
|
||||||
from ..utils.model_manager import get_models_for_project
|
from ..utils.model_manager import get_models_for_project
|
||||||
from ..utils.version_manager import get_latest_version
|
from ..utils.version_manager import get_latest_version
|
||||||
from ..utils.property_groups import speckle_model
|
|
||||||
|
|
||||||
|
|
||||||
class SPECKLE_UL_models_list(bpy.types.UIList):
|
class SPECKLE_UL_models_list(bpy.types.UIList):
|
||||||
|
|||||||
@@ -2,10 +2,6 @@ import bpy
|
|||||||
from bpy.types import UILayout, Context, PropertyGroup, Event
|
from bpy.types import UILayout, Context, PropertyGroup, Event
|
||||||
from typing import List, Tuple
|
from typing import List, Tuple
|
||||||
from ..utils.account_manager import (
|
from ..utils.account_manager import (
|
||||||
get_account_enum_items,
|
|
||||||
speckle_account,
|
|
||||||
get_workspaces,
|
|
||||||
speckle_workspace,
|
|
||||||
can_create_project_in_workspace,
|
can_create_project_in_workspace,
|
||||||
get_active_workspace,
|
get_active_workspace,
|
||||||
get_default_account_id,
|
get_default_account_id,
|
||||||
@@ -64,7 +60,6 @@ class SPECKLE_OT_project_selection_dialog(bpy.types.Operator):
|
|||||||
"""
|
"""
|
||||||
wm = context.window_manager
|
wm = context.window_manager
|
||||||
|
|
||||||
|
|
||||||
wm.can_create_project_in_workspace = can_create_project_in_workspace(
|
wm.can_create_project_in_workspace = can_create_project_in_workspace(
|
||||||
wm.selected_account_id, wm.selected_workspace.id
|
wm.selected_account_id, wm.selected_workspace.id
|
||||||
)
|
)
|
||||||
@@ -126,7 +121,9 @@ class SPECKLE_OT_project_selection_dialog(bpy.types.Operator):
|
|||||||
wm.selected_account_id = get_default_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.id = get_active_workspace(wm.selected_account_id)["id"]
|
||||||
wm.selected_workspace.name = get_active_workspace(wm.selected_account_id)["name"]
|
wm.selected_workspace.name = get_active_workspace(wm.selected_account_id)[
|
||||||
|
"name"
|
||||||
|
]
|
||||||
|
|
||||||
# Fetch projects from server
|
# Fetch projects from server
|
||||||
projects: List[Tuple[str, str, str, str, bool]] = get_projects_for_account(
|
projects: List[Tuple[str, str, str, str, bool]] = get_projects_for_account(
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ class SPECKLE_OT_selection_filter_dialog(Operator):
|
|||||||
layout.label(text=f"Project: {project_name}")
|
layout.label(text=f"Project: {project_name}")
|
||||||
layout.label(text=f"Model: {model_name}")
|
layout.label(text=f"Model: {model_name}")
|
||||||
|
|
||||||
#layout.prop(self, "selection_type")
|
# layout.prop(self, "selection_type")
|
||||||
layout.separator()
|
layout.separator()
|
||||||
|
|
||||||
selected_objects: List[Object] = context.selected_objects
|
selected_objects: List[Object] = context.selected_objects
|
||||||
|
|||||||
@@ -1,13 +1,48 @@
|
|||||||
|
from typing import Dict, List, Optional, Tuple
|
||||||
|
|
||||||
import bpy
|
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.client import SpeckleClient
|
||||||
|
from specklepy.core.api.credentials import Account, get_local_accounts
|
||||||
from specklepy.core.api.wrapper import StreamWrapper
|
from specklepy.core.api.wrapper import StreamWrapper
|
||||||
|
|
||||||
from .misc import strip_non_ascii
|
from .misc import strip_non_ascii
|
||||||
|
|
||||||
|
|
||||||
|
class SpeckleClientCache:
|
||||||
|
def __init__(self):
|
||||||
|
self._clients: Dict[str, SpeckleClient] = {}
|
||||||
|
|
||||||
|
def get_client(self, account_id: str) -> SpeckleClient:
|
||||||
|
# Check cache first
|
||||||
|
if account_id in self._clients:
|
||||||
|
print(f"[Cache HIT] Using cached client for account {account_id}")
|
||||||
|
return self._clients[account_id]
|
||||||
|
|
||||||
|
# Create new client if needed
|
||||||
|
print(f"[Cache MISS] Creating new client for account {account_id}")
|
||||||
|
account = get_account_from_id(account_id)
|
||||||
|
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.authenticate_with_account(account)
|
||||||
|
self._clients[account_id] = client
|
||||||
|
return client
|
||||||
|
|
||||||
|
def clear(self) -> None:
|
||||||
|
"""Clear all cached clients."""
|
||||||
|
print("[Cache] Clearing all cached clients")
|
||||||
|
self._clients.clear()
|
||||||
|
|
||||||
|
|
||||||
|
# Global cache instance
|
||||||
|
_client_cache = SpeckleClientCache()
|
||||||
|
|
||||||
|
|
||||||
class speckle_account(bpy.types.PropertyGroup):
|
class speckle_account(bpy.types.PropertyGroup):
|
||||||
id: bpy.props.StringProperty() # type: ignore
|
id: bpy.props.StringProperty() # type: ignore
|
||||||
user_name: bpy.props.StringProperty() # type: ignore
|
user_name: bpy.props.StringProperty() # type: ignore
|
||||||
@@ -47,37 +82,44 @@ def get_workspaces(account_id: str) -> List[Tuple[str, str]]:
|
|||||||
"""
|
"""
|
||||||
retrieves the workspaces for a given account ID
|
retrieves the workspaces for a given account ID
|
||||||
"""
|
"""
|
||||||
account = next((acc for acc in get_local_accounts() if acc.id == account_id), None)
|
|
||||||
if not account:
|
try:
|
||||||
print("No accounts found > No workspaces!")
|
# Get client from cache
|
||||||
|
client = _client_cache.get_client(account_id)
|
||||||
|
|
||||||
|
workspaces_enabled = client.server.get().workspaces.workspaces_enabled
|
||||||
|
|
||||||
|
if workspaces_enabled:
|
||||||
|
workspaces = client.active_user.get_workspaces().items
|
||||||
|
|
||||||
|
workspace_list = [
|
||||||
|
(ws.id, strip_non_ascii(ws.name))
|
||||||
|
for ws in workspaces
|
||||||
|
if ws.creation_state is None or ws.creation_state.completed
|
||||||
|
]
|
||||||
|
personal_projects_text = "Personal Projects (Legacy)"
|
||||||
|
else:
|
||||||
|
workspace_list = []
|
||||||
|
personal_projects_text = "Personal Projects"
|
||||||
|
|
||||||
|
workspace_list.append(("personal", personal_projects_text))
|
||||||
|
|
||||||
|
if workspaces_enabled:
|
||||||
|
active_workspace = client.active_user.get_active_workspace()
|
||||||
|
default_workspace_id = (
|
||||||
|
active_workspace.id if active_workspace else "personal"
|
||||||
|
)
|
||||||
|
|
||||||
|
result = reorder_tuple(workspace_list, default_workspace_id)
|
||||||
|
else:
|
||||||
|
result = workspace_list
|
||||||
|
|
||||||
|
return result
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error in get_workspaces: {str(e)}")
|
||||||
|
_client_cache.clear() # Clear cache on error
|
||||||
return [("", "")]
|
return [("", "")]
|
||||||
|
|
||||||
client = SpeckleClient(host=account.serverInfo.url)
|
|
||||||
client.authenticate_with_account(account)
|
|
||||||
workspaces_enabled = client.server.get().workspaces.workspaces_enabled
|
|
||||||
|
|
||||||
if workspaces_enabled:
|
|
||||||
workspaces = client.active_user.get_workspaces().items
|
|
||||||
workspace_list = [
|
|
||||||
(ws.id, strip_non_ascii(ws.name))
|
|
||||||
for ws in workspaces
|
|
||||||
if ws.creation_state is None or ws.creation_state.completed
|
|
||||||
]
|
|
||||||
personal_projects_text = "Personal Projects (Legacy)"
|
|
||||||
else:
|
|
||||||
workspace_list = []
|
|
||||||
personal_projects_text = "Personal Projects"
|
|
||||||
|
|
||||||
workspace_list.append(("personal", personal_projects_text))
|
|
||||||
print("Workspaces added")
|
|
||||||
|
|
||||||
if workspaces_enabled:
|
|
||||||
active_workspace = client.active_user.get_active_workspace()
|
|
||||||
default_workspace_id = active_workspace.id if active_workspace else "personal"
|
|
||||||
return reorder_tuple(workspace_list, default_workspace_id)
|
|
||||||
else:
|
|
||||||
return workspace_list
|
|
||||||
|
|
||||||
|
|
||||||
def get_default_account_id() -> Optional[str]:
|
def get_default_account_id() -> Optional[str]:
|
||||||
"""
|
"""
|
||||||
@@ -103,16 +145,16 @@ def get_active_workspace(account_id: str) -> Optional[Dict[str, str]]:
|
|||||||
"""
|
"""
|
||||||
retrieves the ID of the default workspace for a given account ID
|
retrieves the ID of the default workspace for a given account ID
|
||||||
"""
|
"""
|
||||||
account = next((acc for acc in get_local_accounts() if acc.id == account_id), None)
|
try:
|
||||||
if account is None:
|
client = _client_cache.get_client(account_id)
|
||||||
print(f"No account found for ID: {account_id}, returning default workspace.")
|
active_workspace = client.active_user.get_active_workspace()
|
||||||
|
if active_workspace:
|
||||||
|
return {"id": active_workspace.id, "name": active_workspace.name}
|
||||||
return {"id": "personal", "name": "Personal Projects"}
|
return {"id": "personal", "name": "Personal Projects"}
|
||||||
client = SpeckleClient(host=account.serverInfo.url)
|
except Exception as e:
|
||||||
client.authenticate_with_account(account)
|
print(f"Error in get_active_workspace: {str(e)}")
|
||||||
active_workspace = client.active_user.get_active_workspace()
|
_client_cache.clear()
|
||||||
if active_workspace:
|
return None
|
||||||
return {"id": active_workspace.id, "name": active_workspace.name}
|
|
||||||
return {"id": "personal", "name": "Personal Projects"}
|
|
||||||
|
|
||||||
|
|
||||||
def get_account_from_id(account_id: str) -> Optional[Account]:
|
def get_account_from_id(account_id: str) -> Optional[Account]:
|
||||||
@@ -141,8 +183,9 @@ def get_project_from_url(
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
wrapper = StreamWrapper(url)
|
wrapper = StreamWrapper(url)
|
||||||
client = wrapper.get_client()
|
account = wrapper.get_account()
|
||||||
client.authenticate_with_account(wrapper.get_account())
|
assert account.id
|
||||||
|
client = _client_cache.get_client(account.id)
|
||||||
|
|
||||||
# get the stream_id (project_id) from the wrapper
|
# get the stream_id (project_id) from the wrapper
|
||||||
if not wrapper.stream_id:
|
if not wrapper.stream_id:
|
||||||
@@ -246,21 +289,19 @@ def can_create_project_in_workspace(account_id: str, workspace_id: str) -> bool:
|
|||||||
"""
|
"""
|
||||||
Check if the user can create a project in the specified workspace.
|
Check if the user can create a project in the specified workspace.
|
||||||
"""
|
"""
|
||||||
account = get_account_from_id(account_id)
|
try:
|
||||||
if not account:
|
client = _client_cache.get_client(account_id)
|
||||||
print(f"No account found for ID: {account_id}")
|
|
||||||
|
if workspace_id == "personal":
|
||||||
|
return client.active_user.can_create_personal_projects().authorized
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
workspace = client.workspace.get(workspace_id)
|
||||||
|
return workspace.permissions.can_create_project.authorized
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Failed to get workspace: {str(e)}")
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error in can_create_project_in_workspace: {str(e)}")
|
||||||
|
_client_cache.clear() # Clear cache on error
|
||||||
return False
|
return False
|
||||||
client = SpeckleClient(host=account.serverInfo.url)
|
|
||||||
client.authenticate_with_account(account)
|
|
||||||
|
|
||||||
# wrap the workspace request in try/except and return False on any exception to keep the UI responsive.
|
|
||||||
|
|
||||||
if workspace_id == "personal":
|
|
||||||
return client.active_user.can_create_personal_projects().authorized
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
workspace = client.workspace.get(workspace_id)
|
|
||||||
return workspace.permissions.can_create_project.authorized
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Failed to get workspace: {str(e)}")
|
|
||||||
return False
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
import re
|
import re
|
||||||
|
|
||||||
|
|
||||||
def format_relative_time(timestamp) -> str:
|
def format_relative_time(timestamp) -> str:
|
||||||
"""
|
"""
|
||||||
convert UTC timestamp to local timezone and return relative time string
|
convert UTC timestamp to local timezone and return relative time string
|
||||||
@@ -46,6 +47,7 @@ def format_role(role: str) -> str:
|
|||||||
split_role = role.split(":")
|
split_role = role.split(":")
|
||||||
return f"{split_role[1]}"
|
return f"{split_role[1]}"
|
||||||
|
|
||||||
|
|
||||||
def strip_non_ascii(text):
|
def strip_non_ascii(text):
|
||||||
# Keep English letters, digits, spaces and basic punctuation
|
# Keep English letters, digits, spaces and basic punctuation
|
||||||
return re.sub(r'[^a-zA-Z0-9\s.,!?]', '', text)
|
return re.sub(r"[^a-zA-Z0-9\s.,!?]", "", text)
|
||||||
|
|||||||
@@ -1,42 +1,372 @@
|
|||||||
import bpy
|
import bpy
|
||||||
from bpy.types import Context
|
from bpy.types import Context
|
||||||
from typing import Dict
|
from typing import Dict, Any, Optional
|
||||||
from ..utils.property_groups import speckle_model_card
|
from ..utils.property_groups import speckle_model_card
|
||||||
|
|
||||||
|
|
||||||
|
def find_layer_collection(layer_collection, collection_name):
|
||||||
|
"""
|
||||||
|
Recursively find a layer collection by collection name
|
||||||
|
"""
|
||||||
|
if layer_collection.collection.name == collection_name:
|
||||||
|
return layer_collection
|
||||||
|
for child in layer_collection.children:
|
||||||
|
result = find_layer_collection(child, collection_name)
|
||||||
|
if result:
|
||||||
|
return result
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_object_by_application_id(app_id: str):
|
||||||
|
"""
|
||||||
|
Find a Blender object by its applicationId stored in custom property
|
||||||
|
"""
|
||||||
|
if not app_id:
|
||||||
|
return None
|
||||||
|
|
||||||
|
for obj in bpy.data.objects:
|
||||||
|
if "applicationId" in obj and obj["applicationId"] == app_id:
|
||||||
|
return obj
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_objects_by_application_ids(app_ids: list):
|
||||||
|
"""
|
||||||
|
Find multiple Blender objects by their applicationIds
|
||||||
|
"""
|
||||||
|
if not app_ids:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
result = {}
|
||||||
|
for obj in bpy.data.objects:
|
||||||
|
if "applicationId" in obj and obj["applicationId"] in app_ids:
|
||||||
|
result[obj["applicationId"]] = obj
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def get_collection_by_application_id(app_id: str):
|
||||||
|
"""
|
||||||
|
Find a Blender collection by its applicationId stored in custom property
|
||||||
|
"""
|
||||||
|
if not app_id:
|
||||||
|
return None
|
||||||
|
|
||||||
|
for collection in bpy.data.collections:
|
||||||
|
if "applicationId" in collection and collection["applicationId"] == app_id:
|
||||||
|
return collection
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_collection_identifier(blender_col: bpy.types.Collection) -> str:
|
||||||
|
"""
|
||||||
|
Get collection identifier: applicationId if exists, fallback to name
|
||||||
|
"""
|
||||||
|
if "applicationId" in blender_col and blender_col["applicationId"]:
|
||||||
|
return blender_col["applicationId"]
|
||||||
|
return blender_col.name
|
||||||
|
|
||||||
|
|
||||||
|
def find_collection_by_identifier(identifier: str):
|
||||||
|
"""
|
||||||
|
Find collection by identifier: try applicationId first, then name
|
||||||
|
"""
|
||||||
|
# first try to find by applicationId
|
||||||
|
collection = get_collection_by_application_id(identifier)
|
||||||
|
if collection:
|
||||||
|
return collection
|
||||||
|
|
||||||
|
# fallback to name-based lookup
|
||||||
|
return bpy.data.collections.get(identifier)
|
||||||
|
|
||||||
|
|
||||||
|
def capture_modifier_data(blender_obj: bpy.types.Object) -> list:
|
||||||
|
"""
|
||||||
|
Capture modifier data from a Blender object as dictionaries
|
||||||
|
"""
|
||||||
|
modifiers_data = []
|
||||||
|
for modifier in blender_obj.modifiers:
|
||||||
|
modifier_data = {
|
||||||
|
"name": modifier.name,
|
||||||
|
"type": modifier.type,
|
||||||
|
"show_viewport": modifier.show_viewport,
|
||||||
|
"show_render": modifier.show_render,
|
||||||
|
"show_in_editmode": modifier.show_in_editmode,
|
||||||
|
"show_on_cage": modifier.show_on_cage,
|
||||||
|
"properties": {},
|
||||||
|
}
|
||||||
|
|
||||||
|
# Capture modifier-specific properties
|
||||||
|
for prop_name in modifier.bl_rna.properties.keys():
|
||||||
|
if prop_name in [
|
||||||
|
"rna_type",
|
||||||
|
"name",
|
||||||
|
"type",
|
||||||
|
"show_viewport",
|
||||||
|
"show_render",
|
||||||
|
"show_in_editmode",
|
||||||
|
"show_on_cage",
|
||||||
|
]:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
if hasattr(modifier, prop_name):
|
||||||
|
prop_value = getattr(modifier, prop_name)
|
||||||
|
# Handle different property types
|
||||||
|
if isinstance(prop_value, (int, float, bool, str)):
|
||||||
|
modifier_data["properties"][prop_name] = prop_value
|
||||||
|
elif hasattr(prop_value, "name"): # Object references
|
||||||
|
modifier_data["properties"][prop_name] = prop_value.name
|
||||||
|
elif (
|
||||||
|
hasattr(prop_value, "__len__") and len(prop_value) <= 4
|
||||||
|
): # Vectors/colors
|
||||||
|
modifier_data["properties"][prop_name] = list(prop_value)
|
||||||
|
except (AttributeError, TypeError):
|
||||||
|
continue
|
||||||
|
|
||||||
|
modifiers_data.append(modifier_data)
|
||||||
|
|
||||||
|
return modifiers_data
|
||||||
|
|
||||||
|
|
||||||
|
def has_visibility_modifications(obj: bpy.types.Object) -> bool:
|
||||||
|
"""Check if object has non-default visibility settings"""
|
||||||
|
return obj.hide_viewport or obj.hide_select or obj.hide_render or obj.hide_get()
|
||||||
|
|
||||||
|
|
||||||
|
def has_modifier_modifications(obj: bpy.types.Object) -> bool:
|
||||||
|
"""Check if object has any modifiers applied"""
|
||||||
|
return hasattr(obj, "modifiers") and len(obj.modifiers) > 0
|
||||||
|
|
||||||
|
|
||||||
|
def has_collection_visibility_modifications(layer_col, collection) -> bool:
|
||||||
|
"""Check if collection has non-default visibility settings"""
|
||||||
|
return (
|
||||||
|
layer_col.hide_viewport
|
||||||
|
or collection.hide_select
|
||||||
|
or collection.hide_render
|
||||||
|
or layer_col.exclude
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def collect_objects_with_properties(
|
||||||
|
model_card: speckle_model_card,
|
||||||
|
) -> Dict[str, Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
Collect objects and collections with their current properties before deletion
|
||||||
|
Only stores data for objects that have been modified from defaults
|
||||||
|
"""
|
||||||
|
collected_data = {"objects": {}, "collections": {}}
|
||||||
|
|
||||||
|
# Collect object properties (only for modified objects)
|
||||||
|
for s_obj in model_card.objects:
|
||||||
|
blender_obj = get_object_by_application_id(s_obj.applicationId)
|
||||||
|
if blender_obj:
|
||||||
|
obj_data = {}
|
||||||
|
|
||||||
|
# Only collect visibility if modified from defaults
|
||||||
|
if has_visibility_modifications(blender_obj):
|
||||||
|
obj_data["visibility"] = {
|
||||||
|
"hide_get": blender_obj.hide_get(),
|
||||||
|
"hide_viewport": blender_obj.hide_viewport,
|
||||||
|
"hide_select": blender_obj.hide_select,
|
||||||
|
"hide_render": blender_obj.hide_render,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Only collect modifiers if object has any
|
||||||
|
if has_modifier_modifications(blender_obj):
|
||||||
|
obj_data["modifiers"] = capture_modifier_data(blender_obj)
|
||||||
|
|
||||||
|
# Only store object data if it has modifications
|
||||||
|
if obj_data:
|
||||||
|
collected_data["objects"][s_obj.applicationId] = obj_data
|
||||||
|
|
||||||
|
# Collect collection properties (only for modified collections)
|
||||||
|
for s_col in model_card.collections:
|
||||||
|
# try to find collection by applicationId first, then fallback to name
|
||||||
|
blender_col = None
|
||||||
|
if s_col.applicationId:
|
||||||
|
blender_col = get_collection_by_application_id(s_col.applicationId)
|
||||||
|
if not blender_col:
|
||||||
|
blender_col = bpy.data.collections.get(s_col.name)
|
||||||
|
|
||||||
|
if blender_col:
|
||||||
|
view_layer = bpy.context.view_layer
|
||||||
|
if view_layer:
|
||||||
|
layer_col = find_layer_collection(
|
||||||
|
view_layer.layer_collection, blender_col.name
|
||||||
|
)
|
||||||
|
if layer_col and has_collection_visibility_modifications(
|
||||||
|
layer_col, blender_col
|
||||||
|
):
|
||||||
|
# use collection identifier as key
|
||||||
|
collection_id = get_collection_identifier(blender_col)
|
||||||
|
collected_data["collections"][collection_id] = {
|
||||||
|
"hide_viewport": layer_col.hide_viewport,
|
||||||
|
"hide_select": layer_col.collection.hide_select,
|
||||||
|
"hide_render": layer_col.collection.hide_render,
|
||||||
|
"exclude_from_view_layer": layer_col.exclude,
|
||||||
|
}
|
||||||
|
|
||||||
|
return collected_data
|
||||||
|
|
||||||
|
|
||||||
|
def transfer_object_properties(
|
||||||
|
new_obj: bpy.types.Object, old_obj_data: Dict[str, Any]
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Transfer visibility and modifiers from old object data to new object
|
||||||
|
Handles sparse data gracefully - applies defaults when data is missing
|
||||||
|
"""
|
||||||
|
# Transfer visibility settings (if any were modified)
|
||||||
|
visibility = old_obj_data.get("visibility")
|
||||||
|
if visibility:
|
||||||
|
new_obj.hide_set(visibility.get("hide_get", False))
|
||||||
|
new_obj.hide_viewport = visibility.get("hide_viewport", False)
|
||||||
|
new_obj.hide_select = visibility.get("hide_select", False)
|
||||||
|
new_obj.hide_render = visibility.get("hide_render", False)
|
||||||
|
# If no visibility data, object keeps defaults (all False)
|
||||||
|
|
||||||
|
# Transfer modifiers (if any were present)
|
||||||
|
old_modifiers = old_obj_data.get("modifiers")
|
||||||
|
if old_modifiers and hasattr(new_obj, "modifiers"):
|
||||||
|
# Clear existing modifiers
|
||||||
|
new_obj.modifiers.clear()
|
||||||
|
|
||||||
|
# Transfer each modifier
|
||||||
|
for modifier_data in old_modifiers:
|
||||||
|
recreate_modifier_from_data(new_obj, modifier_data)
|
||||||
|
# If no modifier data, object keeps default (no modifiers)
|
||||||
|
|
||||||
|
|
||||||
|
def transfer_collection_properties(
|
||||||
|
new_col: bpy.types.Collection, old_col_data: Dict[str, Any]
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Transfer visibility properties from old collection data to new collection
|
||||||
|
Handles sparse data gracefully - applies defaults when data is missing
|
||||||
|
"""
|
||||||
|
view_layer = bpy.context.view_layer
|
||||||
|
if view_layer:
|
||||||
|
layer_col = find_layer_collection(view_layer.layer_collection, new_col.name)
|
||||||
|
if layer_col:
|
||||||
|
# Only apply properties if collection had modifications
|
||||||
|
# (otherwise it keeps defaults: all False)
|
||||||
|
layer_col.hide_viewport = old_col_data.get("hide_viewport", False)
|
||||||
|
layer_col.collection.hide_select = old_col_data.get("hide_select", False)
|
||||||
|
layer_col.collection.hide_render = old_col_data.get("hide_render", False)
|
||||||
|
layer_col.exclude = old_col_data.get("exclude_from_view_layer", False)
|
||||||
|
|
||||||
|
|
||||||
|
def recreate_modifier_from_data(
|
||||||
|
new_obj: bpy.types.Object, modifier_data: Dict[str, Any]
|
||||||
|
) -> Optional[bpy.types.Modifier]:
|
||||||
|
"""
|
||||||
|
Recreate a modifier from captured data
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Validate modifier data
|
||||||
|
if not modifier_data.get("type") or not modifier_data.get("name"):
|
||||||
|
print(f"Invalid modifier data: {modifier_data}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Create new modifier
|
||||||
|
new_modifier = new_obj.modifiers.new(
|
||||||
|
modifier_data["name"], modifier_data["type"]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Set visibility properties
|
||||||
|
new_modifier.show_viewport = modifier_data.get("show_viewport", True)
|
||||||
|
new_modifier.show_render = modifier_data.get("show_render", True)
|
||||||
|
new_modifier.show_in_editmode = modifier_data.get("show_in_editmode", True)
|
||||||
|
new_modifier.show_on_cage = modifier_data.get("show_on_cage", False)
|
||||||
|
|
||||||
|
# Set modifier-specific properties
|
||||||
|
for prop_name, prop_value in modifier_data.get("properties", {}).items():
|
||||||
|
try:
|
||||||
|
if hasattr(new_modifier, prop_name):
|
||||||
|
current_value = getattr(new_modifier, prop_name)
|
||||||
|
# Handle object references
|
||||||
|
if hasattr(current_value, "name") and isinstance(prop_value, str):
|
||||||
|
referenced_obj = bpy.data.objects.get(prop_value)
|
||||||
|
if referenced_obj:
|
||||||
|
setattr(new_modifier, prop_name, referenced_obj)
|
||||||
|
else:
|
||||||
|
setattr(new_modifier, prop_name, prop_value)
|
||||||
|
except (AttributeError, TypeError):
|
||||||
|
continue
|
||||||
|
|
||||||
|
return new_modifier
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error recreating modifier {modifier_data.get('name', 'unknown')}: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def update_model_card_objects(
|
def update_model_card_objects(
|
||||||
model_card: speckle_model_card,
|
model_card: speckle_model_card,
|
||||||
converted_objects: Dict[str, bpy.types.Object | bpy.types.Collection],
|
converted_objects: Dict[str, bpy.types.Object | bpy.types.Collection],
|
||||||
|
old_properties: Optional[Dict[str, Dict[str, Any]]] = None,
|
||||||
):
|
):
|
||||||
# clear model card objects
|
"""
|
||||||
|
Update model card with new objects and apply properties from old objects if provided
|
||||||
|
"""
|
||||||
|
# Clear model card objects
|
||||||
model_card.objects.clear()
|
model_card.objects.clear()
|
||||||
model_card.collections.clear()
|
model_card.collections.clear()
|
||||||
|
|
||||||
# if converted_objects is a list, convert it to a dictionary
|
# Convert list to dictionary if needed
|
||||||
if isinstance(converted_objects, list):
|
if isinstance(converted_objects, list):
|
||||||
converted_objects = {obj.name: obj for obj in converted_objects}
|
converted_objects = {obj.name: obj for obj in converted_objects}
|
||||||
|
|
||||||
for obj in converted_objects.values():
|
for obj in converted_objects.values():
|
||||||
# if its a collection, add it to collections field of model card
|
# Handle collections
|
||||||
if isinstance(obj, bpy.types.Collection):
|
if isinstance(obj, bpy.types.Collection):
|
||||||
if obj.name in (o.name for o in model_card.collections):
|
if obj.name in (o.name for o in model_card.collections):
|
||||||
continue
|
continue
|
||||||
s_col = model_card.collections.add()
|
s_col = model_card.collections.add()
|
||||||
s_col.name = obj.name
|
s_col.name = obj.name
|
||||||
# if its an object, add it to the objects field of model card
|
s_col.applicationId = obj.get("applicationId", "")
|
||||||
if isinstance(obj, bpy.types.Object):
|
|
||||||
|
# apply old collection properties if available (use identifier-based lookup)
|
||||||
|
if old_properties:
|
||||||
|
collection_id = get_collection_identifier(obj)
|
||||||
|
if collection_id in old_properties.get("collections", {}):
|
||||||
|
old_col_data = old_properties["collections"][collection_id]
|
||||||
|
transfer_collection_properties(obj, old_col_data)
|
||||||
|
|
||||||
|
# Handle objects
|
||||||
|
elif isinstance(obj, bpy.types.Object):
|
||||||
if obj.name in (o.name for o in model_card.objects):
|
if obj.name in (o.name for o in model_card.objects):
|
||||||
continue
|
continue
|
||||||
s_obj = model_card.objects.add()
|
s_obj = model_card.objects.add()
|
||||||
s_obj.name = obj.name
|
s_obj.name = obj.name
|
||||||
|
s_obj.applicationId = obj.get("applicationId", "")
|
||||||
|
|
||||||
|
# Apply old object properties if available
|
||||||
|
if (
|
||||||
|
old_properties
|
||||||
|
and s_obj.applicationId
|
||||||
|
and s_obj.applicationId in old_properties.get("objects", {})
|
||||||
|
):
|
||||||
|
old_obj_data = old_properties["objects"][s_obj.applicationId]
|
||||||
|
transfer_object_properties(obj, old_obj_data)
|
||||||
|
|
||||||
|
|
||||||
def delete_model_card_objects(model_card: speckle_model_card, context: Context) -> None:
|
def delete_model_card_objects(model_card: speckle_model_card, context: Context) -> None:
|
||||||
"""
|
"""
|
||||||
deletes the model card objects
|
deletes the model card objects
|
||||||
"""
|
"""
|
||||||
select_model_card_objects(model_card, context)
|
# Delete objects directly without requiring selection
|
||||||
bpy.ops.object.delete()
|
for obj in model_card.objects:
|
||||||
|
blender_obj = get_object_by_application_id(obj.applicationId)
|
||||||
|
if not blender_obj:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Remove object from all collections first
|
||||||
|
for collection in blender_obj.users_collection:
|
||||||
|
collection.objects.unlink(blender_obj)
|
||||||
|
|
||||||
|
# Delete the object directly
|
||||||
|
bpy.data.objects.remove(blender_obj)
|
||||||
|
|
||||||
# delete model card/currently loaded collections
|
# delete model card/currently loaded collections
|
||||||
for col in model_card.collections:
|
for col in model_card.collections:
|
||||||
coll = bpy.data.collections.get(col.name)
|
coll = bpy.data.collections.get(col.name)
|
||||||
@@ -54,7 +384,7 @@ def select_model_card_objects(model_card, context: Context):
|
|||||||
bpy.ops.object.select_all(action="DESELECT")
|
bpy.ops.object.select_all(action="DESELECT")
|
||||||
# select objects in model card
|
# select objects in model card
|
||||||
for obj in model_card.objects:
|
for obj in model_card.objects:
|
||||||
blender_obj = bpy.data.objects.get(obj.name)
|
blender_obj = get_object_by_application_id(obj.applicationId)
|
||||||
if not blender_obj:
|
if not blender_obj:
|
||||||
continue
|
continue
|
||||||
if blender_obj.name in context.view_layer.objects:
|
if blender_obj.name in context.view_layer.objects:
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
from specklepy.core.api.client import SpeckleClient
|
from typing import List, Optional, Tuple
|
||||||
from specklepy.core.api.credentials import get_local_accounts, Account
|
|
||||||
from specklepy.core.api.inputs.project_inputs import ProjectModelsFilter
|
from specklepy.core.api.inputs.project_inputs import ProjectModelsFilter
|
||||||
from specklepy.core.api.models.current import Model
|
from specklepy.core.api.models.current import Model
|
||||||
from typing import List, Tuple, Optional
|
|
||||||
|
from .account_manager import _client_cache
|
||||||
from .misc import format_relative_time, strip_non_ascii
|
from .misc import format_relative_time, strip_non_ascii
|
||||||
|
|
||||||
|
|
||||||
@@ -19,22 +20,9 @@ def get_models_for_project(
|
|||||||
)
|
)
|
||||||
return []
|
return []
|
||||||
|
|
||||||
# Get the account info
|
client = _client_cache.get_client(account_id)
|
||||||
account: Optional[Account] = next(
|
|
||||||
(acc for acc in get_local_accounts() if acc.id == account_id), None
|
|
||||||
)
|
|
||||||
if not account:
|
|
||||||
print(f"Error: Could not find account with ID: {account_id}")
|
|
||||||
return []
|
|
||||||
|
|
||||||
client = SpeckleClient(host=account.serverInfo.url)
|
client.project.get(project_id)
|
||||||
client.authenticate_with_account(account)
|
|
||||||
|
|
||||||
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
|
filter = ProjectModelsFilter(search=search) if search else None
|
||||||
|
|
||||||
@@ -53,4 +41,6 @@ def get_models_for_project(
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error fetching models: {str(e)}")
|
print(f"Error fetching models: {str(e)}")
|
||||||
|
# Clear cache on error to prevent stale clients
|
||||||
|
_client_cache.clear()
|
||||||
return []
|
return []
|
||||||
|
|||||||
@@ -1,31 +1,39 @@
|
|||||||
|
from typing import List, Optional, Tuple
|
||||||
|
|
||||||
from specklepy.core.api.client import SpeckleClient
|
from specklepy.core.api.client import SpeckleClient
|
||||||
from specklepy.core.api.credentials import get_local_accounts
|
|
||||||
from specklepy.core.api.resources.current.workspace_resource import WorkspaceResource
|
|
||||||
from specklepy.core.api.inputs.project_inputs import WorksaceProjectsFilter
|
|
||||||
from typing import List, Tuple, Optional
|
|
||||||
from specklepy.core.api.credentials import Account
|
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 .misc import format_relative_time, format_role, strip_non_ascii
|
from .misc import format_relative_time, format_role, strip_non_ascii
|
||||||
|
|
||||||
|
|
||||||
def get_projects_for_account(
|
def get_projects_for_account(
|
||||||
account_id: str, workspace_id: str = None, search: Optional[str] = None
|
account_id: str, workspace_id: str, search: Optional[str] = None
|
||||||
) -> List[Tuple[str, str, str, str, bool]]:
|
) -> List[Tuple[str, str, str, str, bool]]:
|
||||||
"""
|
"""
|
||||||
fetches projects for a given account from the Speckle server
|
fetches projects for a given account from the Speckle server
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
accounts: List[Account] = get_local_accounts()
|
# Get cached client
|
||||||
account: Optional[Account] = next(
|
client = _client_cache.get_client(account_id)
|
||||||
(acc for acc in accounts if acc.id == account_id), None
|
if not client:
|
||||||
)
|
print(f"Error: Could not get client for account: {account_id}")
|
||||||
if not account:
|
|
||||||
return []
|
return []
|
||||||
|
|
||||||
client = SpeckleClient(host=account.serverInfo.url)
|
# Get account for workspace operations that still need it
|
||||||
client.authenticate_with_account(account)
|
from specklepy.core.api.credentials import get_local_accounts
|
||||||
|
|
||||||
|
account: Optional[Account] = next(
|
||||||
|
(acc for acc in get_local_accounts() if acc.id == account_id), None
|
||||||
|
)
|
||||||
|
if not account:
|
||||||
|
print(f"Error: Could not find account with ID: {account_id}")
|
||||||
|
return []
|
||||||
|
|
||||||
if workspace_id == "personal":
|
if workspace_id == "personal":
|
||||||
return _get_personal_projects_with_permissions(client, account, search)
|
return _get_personal_projects_with_permissions(client, search)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
workspace_resource = WorkspaceResource(
|
workspace_resource = WorkspaceResource(
|
||||||
@@ -75,7 +83,7 @@ def get_projects_for_account(
|
|||||||
f"WorkspaceResource failed, falling back to old method: {workspace_error}"
|
f"WorkspaceResource failed, falling back to old method: {workspace_error}"
|
||||||
)
|
)
|
||||||
return _get_projects_with_individual_permissions(
|
return _get_projects_with_individual_permissions(
|
||||||
client, account, workspace_id, search
|
client, workspace_id, search
|
||||||
)
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -84,22 +92,25 @@ def get_projects_for_account(
|
|||||||
error_msg = f"Error: {str(e)}\n"
|
error_msg = f"Error: {str(e)}\n"
|
||||||
error_msg += f"Traceback:\n{''.join(traceback.format_tb(e.__traceback__))}"
|
error_msg += f"Traceback:\n{''.join(traceback.format_tb(e.__traceback__))}"
|
||||||
print(error_msg)
|
print(error_msg)
|
||||||
|
# Clear cache on error to prevent stale clients
|
||||||
|
_client_cache.clear()
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
def _get_personal_projects_with_permissions(
|
def _get_personal_projects_with_permissions(
|
||||||
client: SpeckleClient, account: Account, search: Optional[str] = None
|
client: SpeckleClient, search: Optional[str] = None
|
||||||
) -> List[Tuple[str, str, str, str, bool]]:
|
) -> List[Tuple[str, str, str, str, bool]]:
|
||||||
"""
|
"""
|
||||||
helper function to get personal projects with permissions using the old method
|
helper function to get personal projects with permissions using the old method
|
||||||
"""
|
"""
|
||||||
from specklepy.core.api.inputs.user_inputs import UserProjectsFilter
|
from specklepy.core.api.inputs.user_inputs import UserProjectsFilter
|
||||||
|
|
||||||
from .account_manager import can_load
|
from .account_manager import can_load
|
||||||
|
|
||||||
filter = UserProjectsFilter(
|
filter = UserProjectsFilter(
|
||||||
search=search,
|
search=search,
|
||||||
workspaceId=None,
|
workspace_id=None,
|
||||||
personalOnly=True,
|
personal_only=True,
|
||||||
include_implicit_access=True,
|
include_implicit_access=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -126,7 +137,6 @@ def _get_personal_projects_with_permissions(
|
|||||||
|
|
||||||
def _get_projects_with_individual_permissions(
|
def _get_projects_with_individual_permissions(
|
||||||
client: SpeckleClient,
|
client: SpeckleClient,
|
||||||
account: Account,
|
|
||||||
workspace_id: str,
|
workspace_id: str,
|
||||||
search: Optional[str] = None,
|
search: Optional[str] = None,
|
||||||
) -> List[Tuple[str, str, str, str, bool]]:
|
) -> List[Tuple[str, str, str, str, bool]]:
|
||||||
@@ -134,12 +144,13 @@ def _get_projects_with_individual_permissions(
|
|||||||
Fallback helper function to get projects with permissions using individual API calls
|
Fallback helper function to get projects with permissions using individual API calls
|
||||||
"""
|
"""
|
||||||
from specklepy.core.api.inputs.user_inputs import UserProjectsFilter
|
from specklepy.core.api.inputs.user_inputs import UserProjectsFilter
|
||||||
|
|
||||||
from .account_manager import can_load
|
from .account_manager import can_load
|
||||||
|
|
||||||
filter = UserProjectsFilter(
|
filter = UserProjectsFilter(
|
||||||
search=search,
|
search=search,
|
||||||
workspaceId=workspace_id,
|
workspace_id=workspace_id,
|
||||||
personalOnly=False,
|
personal_only=False,
|
||||||
include_implicit_access=True,
|
include_implicit_access=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import bpy
|
import bpy
|
||||||
from typing import Dict, Any
|
|
||||||
|
|
||||||
|
|
||||||
class speckle_project(bpy.types.PropertyGroup):
|
class speckle_project(bpy.types.PropertyGroup):
|
||||||
@@ -37,18 +36,20 @@ class speckle_version(bpy.types.PropertyGroup):
|
|||||||
|
|
||||||
class speckle_object(bpy.types.PropertyGroup):
|
class speckle_object(bpy.types.PropertyGroup):
|
||||||
"""
|
"""
|
||||||
PropertyGroup for storing object names
|
PropertyGroup for storing object names and applicationIds
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name: bpy.props.StringProperty() # type: ignore
|
name: bpy.props.StringProperty() # type: ignore
|
||||||
|
applicationId: bpy.props.StringProperty(name="Application ID", default="") # type: ignore
|
||||||
|
|
||||||
|
|
||||||
class speckle_collection(bpy.types.PropertyGroup):
|
class speckle_collection(bpy.types.PropertyGroup):
|
||||||
"""
|
"""
|
||||||
PropertyGroup for storing collection information
|
PropertyGroup for storing collections
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name: bpy.props.StringProperty() # type: ignore
|
name: bpy.props.StringProperty() # type: ignore
|
||||||
|
applicationId: bpy.props.StringProperty(name="Application ID", default="") # type: ignore
|
||||||
|
|
||||||
|
|
||||||
class speckle_model_card(bpy.types.PropertyGroup):
|
class speckle_model_card(bpy.types.PropertyGroup):
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
|
from typing import List, Tuple
|
||||||
|
|
||||||
from specklepy.core.api.client import SpeckleClient
|
from specklepy.core.api.client import SpeckleClient
|
||||||
from specklepy.core.api.credentials import get_local_accounts, Account
|
|
||||||
from typing import List, Tuple, Optional
|
|
||||||
from .misc import format_relative_time
|
|
||||||
from specklepy.core.api.inputs.model_inputs import ModelVersionsFilter
|
|
||||||
from specklepy.core.api.models.current import Version
|
from specklepy.core.api.models.current import Version
|
||||||
|
|
||||||
|
from .account_manager import _client_cache
|
||||||
|
from .misc import format_relative_time
|
||||||
|
|
||||||
|
|
||||||
def get_versions_for_model(
|
def get_versions_for_model(
|
||||||
account_id: str, project_id: str, model_id: str
|
account_id: str, project_id: str, model_id: str
|
||||||
@@ -20,24 +21,11 @@ def get_versions_for_model(
|
|||||||
)
|
)
|
||||||
return []
|
return []
|
||||||
|
|
||||||
# Get the account info
|
client: SpeckleClient = _client_cache.get_client(account_id)
|
||||||
account: Optional[Account] = next(
|
|
||||||
(acc for acc in get_local_accounts() if acc.id == account_id), None
|
|
||||||
)
|
|
||||||
if not account:
|
|
||||||
print(f"Error: Could not find account with ID: {account_id}")
|
|
||||||
return []
|
|
||||||
|
|
||||||
# Initialize the client
|
|
||||||
client: SpeckleClient = SpeckleClient(host=account.serverInfo.url)
|
|
||||||
# Authenticate
|
|
||||||
client.authenticate_with_account(account)
|
|
||||||
|
|
||||||
filter: ModelVersionsFilter = ModelVersionsFilter(priorityIds=[])
|
|
||||||
|
|
||||||
# Get versions
|
# Get versions
|
||||||
versions: List[Version] = client.version.get_versions(
|
versions = client.version.get_versions(
|
||||||
project_id=project_id, model_id=model_id, limit=10, filter=filter
|
project_id=project_id, model_id=model_id, limit=10
|
||||||
)
|
)
|
||||||
versions_list: List[Tuple[str, str, str]] = []
|
versions_list: List[Tuple[str, str, str]] = []
|
||||||
for version in versions.items:
|
for version in versions.items:
|
||||||
@@ -55,6 +43,8 @@ def get_versions_for_model(
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error fetching versions: {str(e)}")
|
print(f"Error fetching versions: {str(e)}")
|
||||||
|
# Clear cache on error to prevent stale clients
|
||||||
|
_client_cache.clear()
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
@@ -69,18 +59,8 @@ def get_latest_version(
|
|||||||
)
|
)
|
||||||
return ("", "", "")
|
return ("", "", "")
|
||||||
|
|
||||||
# Get the account info
|
# Get cached client
|
||||||
account: Optional[Account] = next(
|
client: SpeckleClient = _client_cache.get_client(account_id)
|
||||||
(acc for acc in get_local_accounts() if acc.id == account_id), None
|
|
||||||
)
|
|
||||||
if not account:
|
|
||||||
print(f"Error: Could not find account with ID: {account_id}")
|
|
||||||
return ("", "", "")
|
|
||||||
|
|
||||||
# Initialize the client
|
|
||||||
client: SpeckleClient = SpeckleClient(host=account.serverInfo.url)
|
|
||||||
# Authenticate
|
|
||||||
client.authenticate_with_account(account)
|
|
||||||
|
|
||||||
# Get versions (limit to 1 since we only need the latest)
|
# Get versions (limit to 1 since we only need the latest)
|
||||||
versions: List[Version] = client.version.get_versions(
|
versions: List[Version] = client.version.get_versions(
|
||||||
@@ -100,4 +80,6 @@ def get_latest_version(
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error fetching latest version: {str(e)}")
|
print(f"Error fetching latest version: {str(e)}")
|
||||||
|
# Clear cache on error to prevent stale clients
|
||||||
|
_client_cache.clear()
|
||||||
return ("", "", "")
|
return ("", "", "")
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
from ..converter.to_native import * #noqa: F403
|
from ..converter.to_native import * # noqa: F403
|
||||||
from ..converter.to_speckle import * #noqa: F403
|
from ..converter.to_speckle import * # noqa: F403
|
||||||
from ..converter.utils import * # noqa: F403
|
from ..converter.utils import * # noqa: F403
|
||||||
|
|||||||
@@ -186,7 +186,7 @@ def convert_to_native(
|
|||||||
# Store Speckle ID in custom property
|
# Store Speckle ID in custom property
|
||||||
converted_object["speckle_id"] = speckle_object.id
|
converted_object["speckle_id"] = speckle_object.id
|
||||||
if hasattr(speckle_object, "applicationId"):
|
if hasattr(speckle_object, "applicationId"):
|
||||||
converted_object["speckle_application_id"] = speckle_object.applicationId
|
converted_object["applicationId"] = speckle_object.applicationId
|
||||||
|
|
||||||
return converted_object
|
return converted_object
|
||||||
|
|
||||||
@@ -320,7 +320,9 @@ def _members_to_native(
|
|||||||
|
|
||||||
for item in others:
|
for item in others:
|
||||||
try:
|
try:
|
||||||
blender_object = convert_to_native(item, material_mapping, instance_loading_mode="INSTANCE_PROXIES")
|
blender_object = convert_to_native(
|
||||||
|
item, material_mapping, instance_loading_mode="INSTANCE_PROXIES"
|
||||||
|
)
|
||||||
if blender_object:
|
if blender_object:
|
||||||
# If the parent is a DataObject, override the name of the converted child
|
# If the parent is a DataObject, override the name of the converted child
|
||||||
if is_data_object:
|
if is_data_object:
|
||||||
@@ -1219,7 +1221,7 @@ def instance_definition_proxy_to_native(
|
|||||||
"Must be 'INSTANCE_PROXIES' or 'LINKED_DUPLICATES'"
|
"Must be 'INSTANCE_PROXIES' or 'LINKED_DUPLICATES'"
|
||||||
)
|
)
|
||||||
assert isinstance(material_mapping, dict), "material_mapping must be a dictionary"
|
assert isinstance(material_mapping, dict), "material_mapping must be a dictionary"
|
||||||
|
|
||||||
processed_definitions = processed_definitions or {}
|
processed_definitions = processed_definitions or {}
|
||||||
definition_collections = {}
|
definition_collections = {}
|
||||||
converted_objects = {}
|
converted_objects = {}
|
||||||
@@ -1240,7 +1242,7 @@ def instance_definition_proxy_to_native(
|
|||||||
sorted_components = sort_instance_components(definitions, [])
|
sorted_components = sort_instance_components(definitions, [])
|
||||||
|
|
||||||
for _, _, def_id, definition in sorted_components:
|
for _, _, def_id, definition in sorted_components:
|
||||||
collection_name = getattr(definition, "name", f"Definition_{def_id[:8]}")
|
collection_name = getattr(definition, "name", f"Definition_{def_id}")
|
||||||
|
|
||||||
if def_id in processed_definitions:
|
if def_id in processed_definitions:
|
||||||
definition_collections[def_id] = processed_definitions[def_id]
|
definition_collections[def_id] = processed_definitions[def_id]
|
||||||
@@ -1272,10 +1274,10 @@ def instance_definition_proxy_to_native(
|
|||||||
nested_def = definitions[found_obj.definitionId]
|
nested_def = definitions[found_obj.definitionId]
|
||||||
max_depth = getattr(nested_def, "maxDepth", 0)
|
max_depth = getattr(nested_def, "maxDepth", 0)
|
||||||
if max_depth > 0: # Only process if max_depth allows
|
if max_depth > 0: # Only process if max_depth allows
|
||||||
assert found_obj.definitionId in definition_collections, (
|
assert (
|
||||||
f"Definition collection not found for nested instance {found_obj.definitionId}"
|
found_obj.definitionId in definition_collections
|
||||||
)
|
), f"Definition collection not found for nested instance {found_obj.definitionId}"
|
||||||
|
|
||||||
if instance_loading_mode == "LINKED_DUPLICATES":
|
if instance_loading_mode == "LINKED_DUPLICATES":
|
||||||
blender_obj = instance_proxy_to_linked_duplicates(
|
blender_obj = instance_proxy_to_linked_duplicates(
|
||||||
found_obj,
|
found_obj,
|
||||||
@@ -1293,7 +1295,11 @@ def instance_definition_proxy_to_native(
|
|||||||
if blender_obj:
|
if blender_obj:
|
||||||
converted_objects[obj_id] = blender_obj
|
converted_objects[obj_id] = blender_obj
|
||||||
else:
|
else:
|
||||||
blender_obj = convert_to_native(found_obj, material_mapping, instance_loading_mode="INSTANCE_PROXIES")
|
blender_obj = convert_to_native(
|
||||||
|
found_obj,
|
||||||
|
material_mapping,
|
||||||
|
instance_loading_mode="INSTANCE_PROXIES",
|
||||||
|
)
|
||||||
if blender_obj:
|
if blender_obj:
|
||||||
definition_collection.objects.link(blender_obj)
|
definition_collection.objects.link(blender_obj)
|
||||||
converted_objects[obj_id] = blender_obj
|
converted_objects[obj_id] = blender_obj
|
||||||
@@ -1398,16 +1404,16 @@ def instance_proxy_to_linked_duplicates(
|
|||||||
@ mathutils.Matrix.Diagonal(scale_vector).to_4x4()
|
@ mathutils.Matrix.Diagonal(scale_vector).to_4x4()
|
||||||
)
|
)
|
||||||
|
|
||||||
instance_name = f"Instance_{speckle_instance.id[:8]}"
|
instance_name = f"Instance_{speckle_instance.id}"
|
||||||
parent_empty = bpy.data.objects.new(instance_name, None)
|
parent_empty = bpy.data.objects.new(instance_name, None)
|
||||||
parent_empty.empty_display_type = 'PLAIN_AXES'
|
parent_empty.empty_display_type = "PLAIN_AXES"
|
||||||
parent_empty.empty_display_size = 0.1
|
parent_empty.empty_display_size = 0.1
|
||||||
|
|
||||||
parent_empty.matrix_world = final_matrix
|
parent_empty.matrix_world = final_matrix
|
||||||
|
|
||||||
# link parent to root collection
|
# link parent to root collection
|
||||||
root_collection.objects.link(parent_empty)
|
root_collection.objects.link(parent_empty)
|
||||||
|
|
||||||
parent_empty["speckle_id"] = speckle_instance.id
|
parent_empty["speckle_id"] = speckle_instance.id
|
||||||
parent_empty["speckle_type"] = speckle_instance.speckle_type
|
parent_empty["speckle_type"] = speckle_instance.speckle_type
|
||||||
parent_empty["definition_id"] = speckle_instance.definitionId
|
parent_empty["definition_id"] = speckle_instance.definitionId
|
||||||
@@ -1418,14 +1424,14 @@ def instance_proxy_to_linked_duplicates(
|
|||||||
for obj in definition_collection.objects:
|
for obj in definition_collection.objects:
|
||||||
# create a copy of the object with linked data
|
# create a copy of the object with linked data
|
||||||
duplicate_obj = obj.copy()
|
duplicate_obj = obj.copy()
|
||||||
|
|
||||||
duplicate_obj.name = f"{obj.name}_{speckle_instance.id[:8]}"
|
duplicate_obj.name = f"{obj.name}_{speckle_instance.id[:8]}"
|
||||||
|
|
||||||
root_collection.objects.link(duplicate_obj)
|
root_collection.objects.link(duplicate_obj)
|
||||||
|
|
||||||
# apply the instance transformation directly to each object
|
# apply the instance transformation directly to each object
|
||||||
duplicate_obj.matrix_world = final_matrix @ obj.matrix_world
|
duplicate_obj.matrix_world = final_matrix @ obj.matrix_world
|
||||||
|
|
||||||
duplicated_objects.append(duplicate_obj)
|
duplicated_objects.append(duplicate_obj)
|
||||||
|
|
||||||
return parent_empty
|
return parent_empty
|
||||||
@@ -1492,7 +1498,7 @@ def instance_proxy_to_native(
|
|||||||
|
|
||||||
instance_obj.empty_display_size = 0
|
instance_obj.empty_display_size = 0
|
||||||
|
|
||||||
instance_name = f"Instance_{speckle_instance.id[:8]}"
|
instance_name = f"Instance_{speckle_instance.id}"
|
||||||
instance_obj.name = instance_name
|
instance_obj.name = instance_name
|
||||||
|
|
||||||
if instance_obj.name not in root_collection.objects:
|
if instance_obj.name not in root_collection.objects:
|
||||||
|
|||||||
@@ -2,5 +2,5 @@ from .to_speckle import convert_to_speckle # noqa: F401
|
|||||||
from .material_to_speckle import ( # noqa: F401
|
from .material_to_speckle import ( # noqa: F401
|
||||||
blender_material_to_speckle,
|
blender_material_to_speckle,
|
||||||
create_render_material_proxies,
|
create_render_material_proxies,
|
||||||
add_render_material_proxies_to_base
|
add_render_material_proxies_to_base,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -19,12 +19,12 @@ def convert_to_speckle(
|
|||||||
# handle curve modifiers apply_modifiers is True
|
# handle curve modifiers apply_modifiers is True
|
||||||
if apply_modifiers and blender_object.modifiers:
|
if apply_modifiers and blender_object.modifiers:
|
||||||
import bpy
|
import bpy
|
||||||
|
|
||||||
# Convert curve with modifiers to mesh
|
# Convert curve with modifiers to mesh
|
||||||
depsgraph = bpy.context.evaluated_depsgraph_get()
|
depsgraph = bpy.context.evaluated_depsgraph_get()
|
||||||
evaluated_obj = blender_object.evaluated_get(depsgraph)
|
evaluated_obj = blender_object.evaluated_get(depsgraph)
|
||||||
evaluated_mesh = evaluated_obj.to_mesh()
|
evaluated_mesh = evaluated_obj.to_mesh()
|
||||||
|
|
||||||
if evaluated_mesh:
|
if evaluated_mesh:
|
||||||
meshes = mesh_to_speckle_meshes(
|
meshes = mesh_to_speckle_meshes(
|
||||||
blender_object, evaluated_mesh, scale_factor, units
|
blender_object, evaluated_mesh, scale_factor, units
|
||||||
@@ -50,20 +50,22 @@ def convert_to_speckle(
|
|||||||
mesh_data = blender_object.data
|
mesh_data = blender_object.data
|
||||||
if apply_modifiers and blender_object.modifiers:
|
if apply_modifiers and blender_object.modifiers:
|
||||||
import bpy
|
import bpy
|
||||||
|
|
||||||
# use evaluated object to get mesh with modifiers applied
|
# use evaluated object to get mesh with modifiers applied
|
||||||
depsgraph = bpy.context.evaluated_depsgraph_get()
|
depsgraph = bpy.context.evaluated_depsgraph_get()
|
||||||
evaluated_obj = blender_object.evaluated_get(depsgraph)
|
evaluated_obj = blender_object.evaluated_get(depsgraph)
|
||||||
evaluated_mesh = evaluated_obj.to_mesh()
|
evaluated_mesh = evaluated_obj.to_mesh()
|
||||||
mesh_data = evaluated_mesh
|
mesh_data = evaluated_mesh
|
||||||
|
|
||||||
meshes = mesh_to_speckle_meshes(
|
meshes = mesh_to_speckle_meshes(blender_object, mesh_data, scale_factor, units)
|
||||||
blender_object, mesh_data, scale_factor, units
|
|
||||||
)
|
if (
|
||||||
|
apply_modifiers
|
||||||
if apply_modifiers and blender_object.modifiers and mesh_data != blender_object.data:
|
and blender_object.modifiers
|
||||||
|
and mesh_data != blender_object.data
|
||||||
|
):
|
||||||
blender_object.to_mesh_clear()
|
blender_object.to_mesh_clear()
|
||||||
|
|
||||||
if meshes:
|
if meshes:
|
||||||
display_value = meshes
|
display_value = meshes
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ from specklepy.objects import Base
|
|||||||
from specklepy.objects.graph_traversal.default_traversal import (
|
from specklepy.objects.graph_traversal.default_traversal import (
|
||||||
create_default_traversal_function,
|
create_default_traversal_function,
|
||||||
)
|
)
|
||||||
from specklepy.core.api.client import SpeckleClient
|
|
||||||
|
|
||||||
|
|
||||||
def to_rgba(argb_int: int) -> Tuple[float, float, float, float]:
|
def to_rgba(argb_int: int) -> Tuple[float, float, float, float]:
|
||||||
@@ -187,21 +186,3 @@ def find_object_by_id(root_object: Base, target_id: str) -> Optional[Base]:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
return deep_search(root_object)
|
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,4 +1,4 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -e -o pipefail
|
set -e -o pipefail
|
||||||
|
|
||||||
uv pip compile pyproject.toml --output-file bpy_speckle/requirements.txt --all-extras
|
uv pip compile pyproject.toml --output-file bpy_speckle/requirements.txt --generate-hashes
|
||||||
|
|||||||
+9
-4
@@ -1,6 +1,7 @@
|
|||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
|
||||||
def patch_addon(simple_version: str):
|
def patch_addon(simple_version: str):
|
||||||
"""Patches the __init__.py bl_info version within the connector init file"""
|
"""Patches the __init__.py bl_info version within the connector init file"""
|
||||||
FILE_PATH = "bpy_speckle/__init__.py"
|
FILE_PATH = "bpy_speckle/__init__.py"
|
||||||
@@ -9,13 +10,16 @@ def patch_addon(simple_version: str):
|
|||||||
with open(FILE_PATH, "r") as file:
|
with open(FILE_PATH, "r") as file:
|
||||||
lines = file.readlines()
|
lines = file.readlines()
|
||||||
|
|
||||||
for (index, line) in enumerate(lines):
|
for index, line in enumerate(lines):
|
||||||
if '"version":' in line:
|
if '"version":' in line:
|
||||||
lines[index] = f' "version": ({version[0]}, {version[1]}, {version[2]}),\n'
|
lines[index] = (
|
||||||
|
f' "version": ({version[0]}, {version[1]}, {version[2]}),\n'
|
||||||
|
)
|
||||||
|
|
||||||
with open(FILE_PATH, "w") as file:
|
with open(FILE_PATH, "w") as file:
|
||||||
file.writelines(lines)
|
file.writelines(lines)
|
||||||
|
|
||||||
|
|
||||||
def patch_manifest(simple_version: str):
|
def patch_manifest(simple_version: str):
|
||||||
"""Patches the connector version within the connector init file"""
|
"""Patches the connector version within the connector init file"""
|
||||||
FILE_PATH = "bpy_speckle/blender_manifest.toml"
|
FILE_PATH = "bpy_speckle/blender_manifest.toml"
|
||||||
@@ -24,8 +28,8 @@ def patch_manifest(simple_version: str):
|
|||||||
with open(FILE_PATH, "r") as file:
|
with open(FILE_PATH, "r") as file:
|
||||||
lines = file.readlines()
|
lines = file.readlines()
|
||||||
|
|
||||||
for (index, line) in enumerate(lines):
|
for index, line in enumerate(lines):
|
||||||
if line.startswith('version ='):
|
if line.startswith("version ="):
|
||||||
lines[index] = f'version = "{version[0]}.{version[1]}.{version[2]}",\n'
|
lines[index] = f'version = "{version[0]}.{version[1]}.{version[2]}",\n'
|
||||||
print(f"Patched connector version number in {FILE_PATH}")
|
print(f"Patched connector version number in {FILE_PATH}")
|
||||||
break
|
break
|
||||||
@@ -33,6 +37,7 @@ def patch_manifest(simple_version: str):
|
|||||||
with open(FILE_PATH, "w") as file:
|
with open(FILE_PATH, "w") as file:
|
||||||
file.writelines(lines)
|
file.writelines(lines)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
tag = sys.argv[1]
|
tag = sys.argv[1]
|
||||||
if not re.match(r"([0-9]+)\.([0-9]+)\.([0-9]+)", tag):
|
if not re.match(r"([0-9]+)\.([0-9]+)\.([0-9]+)", tag):
|
||||||
|
|||||||
+1
-1
@@ -5,7 +5,7 @@ description = "Next-Gen Speckle connector for Blender!"
|
|||||||
requires-python = ">=3.11.9, <4.0.0"
|
requires-python = ">=3.11.9, <4.0.0"
|
||||||
license = "Apache-2.0"
|
license = "Apache-2.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"specklepy>=3.0.1",
|
"specklepy>=3.0.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
[dependency-groups]
|
[dependency-groups]
|
||||||
|
|||||||
Reference in New Issue
Block a user