Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 040ea64f86 | |||
| bd154c1575 | |||
| bbc20e2353 | |||
| 716347b497 | |||
| 26927ca6f4 |
@@ -34,10 +34,10 @@ bl_info = {
|
||||
|
||||
# UI
|
||||
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 (
|
||||
SPECKLE_OT_project_selection_dialog,
|
||||
SPECKLE_UL_projects_list,
|
||||
speckle_workspace,
|
||||
)
|
||||
from .connector.ui.model_selection_dialog import (
|
||||
SPECKLE_OT_model_selection_dialog,
|
||||
|
||||
@@ -2,37 +2,37 @@ import bpy
|
||||
import webbrowser
|
||||
from bpy.types import Event, Context
|
||||
|
||||
|
||||
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_label = "Add New Account"
|
||||
bl_description = "Add a new account"
|
||||
|
||||
|
||||
server_url: bpy.props.StringProperty( # type: ignore
|
||||
name="Server URL",
|
||||
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]:
|
||||
return context.window_manager.invoke_props_dialog(self)
|
||||
|
||||
|
||||
def draw(self, context: Context):
|
||||
layout = self.layout
|
||||
# Server URL textbox
|
||||
layout.prop(self, "server_url", text="Server URL")
|
||||
|
||||
|
||||
def execute(self, context: Context) -> set[str]:
|
||||
# Logic to handle sign in
|
||||
api_url = "http://localhost:29364"
|
||||
url = f"{api_url}/auth/add-account?serverUrl={self.server_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
|
||||
context.window.screen = context.window.screen
|
||||
context.area.tag_redraw()
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
return {'FINISHED'}
|
||||
@@ -1,5 +1,5 @@
|
||||
import bpy
|
||||
from bpy.types import Context, Event, UILayout
|
||||
from bpy.types import Context, Event, UILayout, WindowManager
|
||||
from ..utils.account_manager import (
|
||||
get_model_details_by_wrapper,
|
||||
get_project_from_url,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -19,14 +19,14 @@ class SPECKLE_OT_create_model(bpy.types.Operator):
|
||||
if not self.model_name.strip():
|
||||
self.report({"ERROR"}, "Model name cannot be empty")
|
||||
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,17 @@ 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
|
||||
)
|
||||
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
|
||||
|
||||
@@ -6,7 +6,9 @@ from ..operations.load_operation import load_operation
|
||||
from ..utils.model_card_utils import (
|
||||
delete_model_card_objects,
|
||||
update_model_card_objects,
|
||||
collect_objects_with_properties,
|
||||
store_visibility_settings,
|
||||
store_uv_mappings,
|
||||
store_modifier_settings,
|
||||
)
|
||||
|
||||
|
||||
@@ -28,7 +30,10 @@ class SPECKLE_OT_load_model_card(bpy.types.Operator):
|
||||
self.report({"ERROR"}, "Model card not found")
|
||||
return {"CANCELLED"}
|
||||
|
||||
old_properties = collect_objects_with_properties(model_card)
|
||||
store_visibility_settings(model_card)
|
||||
store_modifier_settings(model_card)
|
||||
store_uv_mappings(model_card)
|
||||
|
||||
delete_model_card_objects(model_card, context)
|
||||
|
||||
# set wm
|
||||
@@ -50,7 +55,7 @@ class SPECKLE_OT_load_model_card(bpy.types.Operator):
|
||||
context, model_card.instance_loading_mode
|
||||
)
|
||||
# update model card details
|
||||
update_model_card_objects(model_card, converted_objects, old_properties)
|
||||
update_model_card_objects(model_card, converted_objects)
|
||||
model_card.version_id = latest_version_id
|
||||
|
||||
else:
|
||||
@@ -65,7 +70,7 @@ class SPECKLE_OT_load_model_card(bpy.types.Operator):
|
||||
self.report({"ERROR"}, "Load operation failed")
|
||||
return {"CANCELLED"}
|
||||
# update model card details
|
||||
update_model_card_objects(model_card, converted_objects, old_properties)
|
||||
update_model_card_objects(model_card, converted_objects)
|
||||
|
||||
# Clear selected model details from Window Manager
|
||||
wm.selected_account_id = ""
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
from typing import Dict, Union
|
||||
|
||||
import bpy
|
||||
from bpy.types import Context
|
||||
from specklepy.core.api import host_applications, operations
|
||||
from specklepy.logging import metrics
|
||||
from specklepy.transports.server import ServerTransport
|
||||
from specklepy.core.api import operations
|
||||
from specklepy.objects.models.collections.collection import Collection as SCollection
|
||||
from specklepy.objects.graph_traversal.default_traversal import (
|
||||
create_default_traversal_function,
|
||||
)
|
||||
from specklepy.objects.models.collections.collection import Collection as SCollection
|
||||
from specklepy.transports.server import ServerTransport
|
||||
from specklepy.core.api import host_applications
|
||||
|
||||
from ... import bl_info
|
||||
from ..utils.get_ascendants import get_ascendants
|
||||
from ..utils.account_manager import _client_cache
|
||||
from ...converter.utils import find_object_by_id, get_project_workspace_id
|
||||
from ...converter.to_native import (
|
||||
convert_to_native,
|
||||
find_instance_definitions,
|
||||
instance_definition_proxy_to_native,
|
||||
render_material_proxy_to_native,
|
||||
instance_definition_proxy_to_native,
|
||||
find_instance_definitions,
|
||||
)
|
||||
from ...converter.utils import find_object_by_id
|
||||
from ..utils.account_manager import _client_cache
|
||||
from ..utils.get_ascendants import get_ascendants
|
||||
from specklepy.logging import metrics
|
||||
from ... import bl_info
|
||||
from typing import Dict, Union
|
||||
|
||||
|
||||
def load_operation(
|
||||
@@ -33,6 +33,9 @@ def load_operation(
|
||||
|
||||
# get cached client
|
||||
client = _client_cache.get_client(context.window_manager.selected_account_id)
|
||||
if not client:
|
||||
print("No Speckle client found")
|
||||
return {}
|
||||
|
||||
print(f"Using client for account: {context.window_manager.selected_account_id}")
|
||||
|
||||
@@ -45,20 +48,32 @@ def load_operation(
|
||||
|
||||
metrics.set_host_app("blender")
|
||||
|
||||
metrics.track(
|
||||
metrics.RECEIVE,
|
||||
client.account,
|
||||
{
|
||||
"ui": "dui3",
|
||||
"hostAppVersion": ".".join(map(str, bl_info["blender"])),
|
||||
"core_version": ".".join(map(str, bl_info["version"])),
|
||||
"sourceHostApp": host_applications.get_host_app_from_string(
|
||||
version.source_application
|
||||
).slug,
|
||||
"isMultiplayer": version.author_user.id != client.account.userInfo.id,
|
||||
"workspace_id": client.project.get(wm.selected_project_id).workspace_id,
|
||||
},
|
||||
# Get account for metrics tracking
|
||||
from specklepy.core.api.credentials import get_local_accounts
|
||||
account = next(
|
||||
(
|
||||
acc
|
||||
for acc in get_local_accounts()
|
||||
if acc.id == context.window_manager.selected_account_id
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
if account:
|
||||
metrics.track(
|
||||
metrics.RECEIVE,
|
||||
account,
|
||||
{
|
||||
"ui": "dui3",
|
||||
"hostAppVersion": ".".join(map(str, bl_info["blender"])),
|
||||
"core_version": ".".join(map(str, bl_info["version"])),
|
||||
"sourceHostApp": host_applications.get_host_app_from_string(
|
||||
version.source_application
|
||||
).slug,
|
||||
"isMultiplayer": version.author_user.id != account.userInfo.id,
|
||||
"workspace_id": get_project_workspace_id(client, wm.selected_project_id),
|
||||
},
|
||||
)
|
||||
|
||||
# Create material mapping first
|
||||
material_mapping = render_material_proxy_to_native(version_data)
|
||||
@@ -137,7 +152,6 @@ def load_operation(
|
||||
"id": speckle_obj.id,
|
||||
"name": collection_name,
|
||||
"parent_id": parent_id,
|
||||
"applicationId": getattr(speckle_obj, "applicationId", ""),
|
||||
"blender_collection": None,
|
||||
"full_path": [collection_name],
|
||||
}
|
||||
@@ -169,6 +183,8 @@ def load_operation(
|
||||
if speckle_root_id and speckle_root_id in collection_hierarchy:
|
||||
collection_hierarchy[speckle_root_id]["blender_collection"] = root_collection
|
||||
converted_objects[speckle_root_id] = root_collection
|
||||
# Add root collection name as key for UV mapping preservation
|
||||
converted_objects[root_collection.name] = root_collection
|
||||
|
||||
# create collections in depth order (skip the root that's already mapped)
|
||||
for coll_id in sorted_collections:
|
||||
@@ -193,13 +209,13 @@ def load_operation(
|
||||
blender_collection = created_collections[collection_key]
|
||||
else:
|
||||
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)
|
||||
created_collections[collection_key] = blender_collection
|
||||
|
||||
coll_info["blender_collection"] = blender_collection
|
||||
converted_objects[coll_id] = blender_collection
|
||||
# Add collection name as key for UV mapping preservation
|
||||
converted_objects[blender_collection.name] = blender_collection
|
||||
|
||||
conversion_count = 0
|
||||
for traversal_item in traversal_function.traverse(version_data):
|
||||
@@ -249,6 +265,8 @@ def load_operation(
|
||||
converted_objects[speckle_obj.id] = blender_obj
|
||||
if hasattr(speckle_obj, "applicationId"):
|
||||
converted_objects[speckle_obj.applicationId] = blender_obj
|
||||
# Add object name as key for UV mapping preservation
|
||||
converted_objects[blender_obj.name] = blender_obj
|
||||
|
||||
if not isinstance(blender_obj, bpy.types.Collection):
|
||||
try:
|
||||
|
||||
@@ -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,13 +33,13 @@ 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)
|
||||
|
||||
# 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:
|
||||
return False, "No objects could be converted to Speckle format", None
|
||||
@@ -50,28 +50,38 @@ 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)
|
||||
@@ -106,9 +116,7 @@ def build_collection_hierarchy(
|
||||
if not collection_data["objects"] and not collection_data["collections"]:
|
||||
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:
|
||||
return None
|
||||
|
||||
@@ -270,9 +278,7 @@ def convert_selected_objects(
|
||||
speckle_objects.append(None)
|
||||
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)
|
||||
|
||||
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
|
||||
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
|
||||
from ..utils.property_groups import speckle_model
|
||||
|
||||
|
||||
class SPECKLE_UL_models_list(bpy.types.UIList):
|
||||
|
||||
@@ -2,6 +2,10 @@ import bpy
|
||||
from bpy.types import UILayout, Context, PropertyGroup, Event
|
||||
from typing import List, Tuple
|
||||
from ..utils.account_manager import (
|
||||
get_account_enum_items,
|
||||
speckle_account,
|
||||
get_workspaces,
|
||||
speckle_workspace,
|
||||
can_create_project_in_workspace,
|
||||
get_active_workspace,
|
||||
get_default_account_id,
|
||||
@@ -60,6 +64,7 @@ class SPECKLE_OT_project_selection_dialog(bpy.types.Operator):
|
||||
"""
|
||||
wm = context.window_manager
|
||||
|
||||
|
||||
wm.can_create_project_in_workspace = can_create_project_in_workspace(
|
||||
wm.selected_account_id, wm.selected_workspace.id
|
||||
)
|
||||
@@ -121,9 +126,7 @@ class SPECKLE_OT_project_selection_dialog(bpy.types.Operator):
|
||||
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"
|
||||
]
|
||||
wm.selected_workspace.name = get_active_workspace(wm.selected_account_id)["name"]
|
||||
|
||||
# Fetch projects from server
|
||||
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"Model: {model_name}")
|
||||
|
||||
# layout.prop(self, "selection_type")
|
||||
#layout.prop(self, "selection_type")
|
||||
layout.separator()
|
||||
|
||||
selected_objects: List[Object] = context.selected_objects
|
||||
|
||||
@@ -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,7 +1,6 @@
|
||||
from datetime import datetime, timezone
|
||||
import re
|
||||
|
||||
|
||||
def format_relative_time(timestamp) -> str:
|
||||
"""
|
||||
convert UTC timestamp to local timezone and return relative time string
|
||||
@@ -47,7 +46,6 @@ def format_role(role: str) -> str:
|
||||
split_role = role.split(":")
|
||||
return f"{split_role[1]}"
|
||||
|
||||
|
||||
def strip_non_ascii(text):
|
||||
# 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,6 +1,8 @@
|
||||
import bpy
|
||||
import json
|
||||
from bpy.types import Context
|
||||
from typing import Dict, Any, Optional
|
||||
from typing import Dict
|
||||
import json
|
||||
from ..utils.property_groups import speckle_model_card
|
||||
|
||||
|
||||
@@ -44,310 +46,262 @@ def get_objects_by_application_ids(app_ids: list):
|
||||
return result
|
||||
|
||||
|
||||
def get_collection_by_application_id(app_id: str):
|
||||
def store_visibility_settings(model_card: speckle_model_card):
|
||||
"""
|
||||
Find a Blender collection by its applicationId stored in custom property
|
||||
Store current visibility settings of model card objects and collections
|
||||
This is used to restore the visibility settings of the loaded objects after loading a new version
|
||||
"""
|
||||
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 = {}
|
||||
s_obj.hide_get = blender_obj.hide_get()
|
||||
s_obj.hide_viewport = blender_obj.hide_viewport
|
||||
s_obj.hide_select = blender_obj.hide_select
|
||||
s_obj.hide_render = blender_obj.hide_render
|
||||
|
||||
# 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)
|
||||
|
||||
blender_col = bpy.data.collections.get(s_col.name)
|
||||
if blender_col:
|
||||
# For collections, visibility is controlled through the view layer system
|
||||
view_layer = bpy.context.view_layer
|
||||
if view_layer:
|
||||
# Find the layer collection for this collection
|
||||
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
|
||||
if layer_col:
|
||||
s_col.hide_viewport = layer_col.hide_viewport
|
||||
s_col.hide_select = layer_col.collection.hide_select
|
||||
s_col.hide_render = layer_col.collection.hide_render
|
||||
s_col.exclude_from_view_layer = layer_col.exclude
|
||||
else:
|
||||
s_col.hide_viewport = False
|
||||
s_col.hide_select = False
|
||||
s_col.hide_render = False
|
||||
s_col.exclude_from_view_layer = False
|
||||
|
||||
|
||||
def transfer_object_properties(
|
||||
new_obj: bpy.types.Object, old_obj_data: Dict[str, Any]
|
||||
) -> None:
|
||||
def store_uv_mappings(model_card: speckle_model_card):
|
||||
"""
|
||||
Transfer visibility and modifiers from old object data to new object
|
||||
Handles sparse data gracefully - applies defaults when data is missing
|
||||
Store current UV mapping data of model card mesh objects
|
||||
This is used to restore the UV mappings after loading a new version
|
||||
"""
|
||||
# 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)
|
||||
for s_obj in model_card.objects:
|
||||
blender_obj = get_object_by_application_id(s_obj.applicationId)
|
||||
|
||||
# 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()
|
||||
if blender_obj and blender_obj.type == "MESH" and blender_obj.data:
|
||||
mesh = blender_obj.data
|
||||
|
||||
# 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)
|
||||
uv_data = {"active_uv_layer": "", "uv_layers": []}
|
||||
|
||||
# Store active UV layer name
|
||||
if mesh.uv_layers.active:
|
||||
uv_data["active_uv_layer"] = mesh.uv_layers.active.name
|
||||
|
||||
# Store UV data for each UV layer
|
||||
for uv_layer in mesh.uv_layers:
|
||||
# Extract UV coordinates for all loops in this layer
|
||||
uv_coords = []
|
||||
for uv_loop in uv_layer.data:
|
||||
uv_coords.extend([uv_loop.uv.x, uv_loop.uv.y])
|
||||
|
||||
uv_data["uv_layers"].append(
|
||||
{"name": uv_layer.name, "uv_coords": uv_coords}
|
||||
)
|
||||
|
||||
# Serialize complete UV data as JSON string
|
||||
s_obj.uv_data_serialized = json.dumps(uv_data)
|
||||
|
||||
|
||||
def transfer_collection_properties(
|
||||
new_col: bpy.types.Collection, old_col_data: Dict[str, Any]
|
||||
) -> None:
|
||||
def restore_uv_mappings(
|
||||
model_card: speckle_model_card,
|
||||
converted_objects: Dict[str, bpy.types.Object | bpy.types.Collection],
|
||||
):
|
||||
"""
|
||||
Transfer visibility properties from old collection data to new collection
|
||||
Handles sparse data gracefully - applies defaults when data is missing
|
||||
Restore UV mapping data to reloaded mesh objects
|
||||
"""
|
||||
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():
|
||||
# First, collect UV mapping data from property groups before they are cleared
|
||||
uv_mapping_data = {}
|
||||
for s_obj in model_card.objects:
|
||||
if s_obj.uv_data_serialized: # Only process objects that have UV data stored
|
||||
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):
|
||||
uv_data = json.loads(s_obj.uv_data_serialized)
|
||||
uv_mapping_data[s_obj.applicationId] = uv_data
|
||||
except (json.JSONDecodeError, ValueError):
|
||||
# Skip invalid UV data
|
||||
continue
|
||||
|
||||
return new_modifier
|
||||
except Exception as e:
|
||||
print(f"Error recreating modifier {modifier_data.get('name', 'unknown')}: {e}")
|
||||
return None
|
||||
# Now restore UV mappings to the new objects
|
||||
for app_id, uv_data in uv_mapping_data.items():
|
||||
# Find the blender object by applicationId in converted_objects
|
||||
blender_obj = None
|
||||
for obj in converted_objects.values():
|
||||
if isinstance(obj, bpy.types.Object) and obj.get("applicationId") == app_id:
|
||||
blender_obj = obj
|
||||
break
|
||||
|
||||
if blender_obj:
|
||||
# Only process mesh objects
|
||||
if (
|
||||
isinstance(blender_obj, bpy.types.Object)
|
||||
and blender_obj.type == "MESH"
|
||||
and blender_obj.data
|
||||
):
|
||||
mesh = blender_obj.data
|
||||
|
||||
# Restore UV layers
|
||||
for uv_layer_data in uv_data.get("uv_layers", []):
|
||||
layer_name = uv_layer_data["name"]
|
||||
uv_coords = uv_layer_data["uv_coords"]
|
||||
|
||||
# Find or create the UV layer
|
||||
uv_layer = mesh.uv_layers.get(layer_name)
|
||||
if not uv_layer:
|
||||
uv_layer = mesh.uv_layers.new(name=layer_name)
|
||||
|
||||
# Restore UV coordinates
|
||||
expected_coords = len(mesh.loops) * 2 # 2 coords per loop
|
||||
|
||||
if len(uv_coords) == expected_coords:
|
||||
for i, uv_loop in enumerate(uv_layer.data):
|
||||
coord_idx = i * 2
|
||||
if coord_idx + 1 < len(uv_coords):
|
||||
uv_loop.uv = (
|
||||
uv_coords[coord_idx],
|
||||
uv_coords[coord_idx + 1],
|
||||
)
|
||||
|
||||
# Restore active UV layer
|
||||
active_uv_layer = uv_data.get("active_uv_layer", "")
|
||||
if active_uv_layer and mesh.uv_layers.get(active_uv_layer):
|
||||
mesh.uv_layers.active = mesh.uv_layers[active_uv_layer]
|
||||
|
||||
|
||||
def update_model_card_objects(
|
||||
model_card: speckle_model_card,
|
||||
converted_objects: Dict[str, bpy.types.Object | bpy.types.Collection],
|
||||
old_properties: Optional[Dict[str, Dict[str, Any]]] = None,
|
||||
):
|
||||
"""
|
||||
Update model card with new objects and apply properties from old objects if provided
|
||||
"""
|
||||
# Clear model card objects
|
||||
# Restore UV mappings before clearing property groups
|
||||
restore_uv_mappings(model_card, converted_objects)
|
||||
|
||||
# Store visibility settings from property group before clearing
|
||||
visibility_settings = {}
|
||||
for s_obj in model_card.objects:
|
||||
if s_obj.applicationId:
|
||||
visibility_settings[s_obj.applicationId] = {
|
||||
"hide_get": s_obj.hide_get,
|
||||
"hide_viewport": s_obj.hide_viewport,
|
||||
"hide_select": s_obj.hide_select,
|
||||
"hide_render": s_obj.hide_render,
|
||||
}
|
||||
|
||||
# Store modifier settings from property group before clearing
|
||||
modifier_settings = {}
|
||||
for s_obj in model_card.objects:
|
||||
if s_obj.applicationId:
|
||||
modifier_settings[s_obj.applicationId] = s_obj.modifiers
|
||||
|
||||
# Store collection visibility settings from property group before clearing
|
||||
collection_visibility_settings = {}
|
||||
for s_col in model_card.collections:
|
||||
collection_visibility_settings[s_col.name] = {
|
||||
"hide_viewport": s_col.hide_viewport,
|
||||
"hide_select": s_col.hide_select,
|
||||
"hide_render": s_col.hide_render,
|
||||
"exclude_from_view_layer": s_col.exclude_from_view_layer,
|
||||
}
|
||||
|
||||
# clear model card objects
|
||||
model_card.objects.clear()
|
||||
model_card.collections.clear()
|
||||
|
||||
# Convert list to dictionary if needed
|
||||
# if converted_objects is a list, convert it to a dictionary
|
||||
if isinstance(converted_objects, list):
|
||||
converted_objects = {obj.name: obj for obj in converted_objects}
|
||||
|
||||
for obj in converted_objects.values():
|
||||
# Handle collections
|
||||
# if its a collection, add it to collections field of model card
|
||||
if isinstance(obj, bpy.types.Collection):
|
||||
if obj.name in (o.name for o in model_card.collections):
|
||||
continue
|
||||
s_col = model_card.collections.add()
|
||||
s_col.name = obj.name
|
||||
s_col.applicationId = obj.get("applicationId", "")
|
||||
|
||||
# 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)
|
||||
# Restore collection visibility settings if they exist
|
||||
if obj.name in collection_visibility_settings:
|
||||
s_col.hide_viewport = collection_visibility_settings[obj.name][
|
||||
"hide_viewport"
|
||||
]
|
||||
s_col.hide_select = collection_visibility_settings[obj.name][
|
||||
"hide_select"
|
||||
]
|
||||
s_col.hide_render = collection_visibility_settings[obj.name][
|
||||
"hide_render"
|
||||
]
|
||||
s_col.exclude_from_view_layer = collection_visibility_settings[
|
||||
obj.name
|
||||
]["exclude_from_view_layer"]
|
||||
|
||||
# Handle objects
|
||||
elif isinstance(obj, bpy.types.Object):
|
||||
# Apply the visibility settings to the new collection through view layer
|
||||
view_layer = bpy.context.view_layer
|
||||
if view_layer:
|
||||
# Find the layer collection for this collection
|
||||
layer_col = find_layer_collection(
|
||||
view_layer.layer_collection, obj.name
|
||||
)
|
||||
if layer_col:
|
||||
# Apply viewport visibility (controlled by layer collection)
|
||||
layer_col.hide_viewport = collection_visibility_settings[
|
||||
obj.name
|
||||
]["hide_viewport"]
|
||||
# Apply selectability and render visibility (controlled by collection)
|
||||
obj.hide_select = collection_visibility_settings[obj.name][
|
||||
"hide_select"
|
||||
]
|
||||
obj.hide_render = collection_visibility_settings[obj.name][
|
||||
"hide_render"
|
||||
]
|
||||
# Apply view layer exclusion
|
||||
layer_col.exclude = collection_visibility_settings[obj.name][
|
||||
"exclude_from_view_layer"
|
||||
]
|
||||
|
||||
# if its an object, add it to the objects field of model card
|
||||
if isinstance(obj, bpy.types.Object):
|
||||
if obj.name in (o.name for o in model_card.objects):
|
||||
continue
|
||||
s_obj = model_card.objects.add()
|
||||
s_obj.name = obj.name
|
||||
s_obj.applicationId = obj.get("applicationId", "")
|
||||
# Restore visibility settings if they exist
|
||||
if s_obj.applicationId and s_obj.applicationId in visibility_settings:
|
||||
s_obj.hide_get = visibility_settings[s_obj.applicationId]["hide_get"]
|
||||
s_obj.hide_viewport = visibility_settings[s_obj.applicationId][
|
||||
"hide_viewport"
|
||||
]
|
||||
s_obj.hide_select = visibility_settings[s_obj.applicationId][
|
||||
"hide_select"
|
||||
]
|
||||
s_obj.hide_render = visibility_settings[s_obj.applicationId][
|
||||
"hide_render"
|
||||
]
|
||||
|
||||
# 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)
|
||||
# Apply the visibility settings to the new object
|
||||
obj.hide_set(visibility_settings[s_obj.applicationId]["hide_get"])
|
||||
obj.hide_viewport = visibility_settings[s_obj.applicationId][
|
||||
"hide_viewport"
|
||||
]
|
||||
obj.hide_select = visibility_settings[s_obj.applicationId][
|
||||
"hide_select"
|
||||
]
|
||||
obj.hide_render = visibility_settings[s_obj.applicationId][
|
||||
"hide_render"
|
||||
]
|
||||
|
||||
# Restore modifier settings if they exist
|
||||
if s_obj.applicationId and s_obj.applicationId in modifier_settings:
|
||||
s_obj.modifiers = modifier_settings[s_obj.applicationId]
|
||||
restore_modifier_settings(obj, modifier_settings[s_obj.applicationId])
|
||||
|
||||
|
||||
def delete_model_card_objects(model_card: speckle_model_card, context: Context) -> None:
|
||||
@@ -416,3 +370,120 @@ def model_card_exists(
|
||||
):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def serialize_modifier(modifier):
|
||||
"""
|
||||
Serialize a Blender modifier to a dictionary
|
||||
"""
|
||||
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": {},
|
||||
}
|
||||
|
||||
# Store all 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:
|
||||
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):
|
||||
# Skip properties that can't be serialized
|
||||
continue
|
||||
|
||||
return modifier_data
|
||||
|
||||
|
||||
def deserialize_modifier(obj, modifier_data):
|
||||
"""
|
||||
Recreate a modifier from serialized data
|
||||
"""
|
||||
try:
|
||||
modifier = obj.modifiers.new(modifier_data["name"], modifier_data["type"])
|
||||
|
||||
# Set visibility properties
|
||||
modifier.show_viewport = modifier_data.get("show_viewport", True)
|
||||
modifier.show_render = modifier_data.get("show_render", True)
|
||||
modifier.show_in_editmode = modifier_data.get("show_in_editmode", True)
|
||||
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(modifier, prop_name):
|
||||
current_value = getattr(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(modifier, prop_name, referenced_obj)
|
||||
else:
|
||||
setattr(modifier, prop_name, prop_value)
|
||||
except (AttributeError, TypeError):
|
||||
# Skip properties that can't be set
|
||||
continue
|
||||
|
||||
return modifier
|
||||
except Exception as e:
|
||||
print(f"Error deserializing modifier {modifier_data['name']}: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def store_modifier_settings(model_card: speckle_model_card):
|
||||
"""
|
||||
Store current modifier settings of model card objects
|
||||
This is used to restore the modifier settings of the loaded objects after loading a new version
|
||||
"""
|
||||
for s_obj in model_card.objects:
|
||||
blender_obj = get_object_by_application_id(s_obj.applicationId)
|
||||
if blender_obj and hasattr(blender_obj, "modifiers"):
|
||||
modifiers_data = []
|
||||
for modifier in blender_obj.modifiers:
|
||||
modifier_data = serialize_modifier(modifier)
|
||||
modifiers_data.append(modifier_data)
|
||||
|
||||
# Store as JSON string
|
||||
s_obj.modifiers = json.dumps(modifiers_data)
|
||||
|
||||
|
||||
def restore_modifier_settings(blender_obj, modifier_data_json):
|
||||
"""
|
||||
Restore modifier settings to a Blender object
|
||||
"""
|
||||
if not modifier_data_json or not hasattr(blender_obj, "modifiers"):
|
||||
return
|
||||
|
||||
try:
|
||||
modifiers_data = json.loads(modifier_data_json)
|
||||
|
||||
# Clear existing modifiers
|
||||
blender_obj.modifiers.clear()
|
||||
|
||||
# Recreate modifiers
|
||||
for modifier_data in modifiers_data:
|
||||
deserialize_modifier(blender_obj, modifier_data)
|
||||
|
||||
except (json.JSONDecodeError, KeyError, TypeError) as e:
|
||||
print(f"Error restoring modifiers for {blender_obj.name}: {e}")
|
||||
|
||||
@@ -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
|
||||
@@ -21,10 +19,9 @@ def get_projects_for_account(
|
||||
if not client:
|
||||
print(f"Error: Could not get client for account: {account_id}")
|
||||
return []
|
||||
|
||||
|
||||
# Get account for workspace operations that still need it
|
||||
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
|
||||
)
|
||||
@@ -104,13 +101,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 +140,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,
|
||||
)
|
||||
|
||||
|
||||
@@ -36,20 +36,31 @@ class speckle_version(bpy.types.PropertyGroup):
|
||||
|
||||
class speckle_object(bpy.types.PropertyGroup):
|
||||
"""
|
||||
PropertyGroup for storing object names and applicationIds
|
||||
PropertyGroup for storing object names, visibility settings, and UV mapping data
|
||||
"""
|
||||
|
||||
name: bpy.props.StringProperty() # type: ignore
|
||||
applicationId: bpy.props.StringProperty(name="Application ID", default="") # type: ignore
|
||||
hide_get: bpy.props.BoolProperty(name="Hide Get", default=False) # type: ignore
|
||||
hide_viewport: bpy.props.BoolProperty(name="Hide Viewport", default=False) # type: ignore
|
||||
hide_select: bpy.props.BoolProperty(name="Hide Select", default=False) # type: ignore
|
||||
hide_render: bpy.props.BoolProperty(name="Hide Render", default=False) # type: ignore
|
||||
modifiers: bpy.props.StringProperty(name="Modifiers", default="") # type: ignore
|
||||
uv_data_serialized: bpy.props.StringProperty() # type: ignore
|
||||
|
||||
|
||||
class speckle_collection(bpy.types.PropertyGroup):
|
||||
"""
|
||||
PropertyGroup for storing collections
|
||||
PropertyGroup for storing collection information and visibility settings
|
||||
"""
|
||||
|
||||
name: bpy.props.StringProperty() # type: ignore
|
||||
applicationId: bpy.props.StringProperty(name="Application ID", default="") # type: ignore
|
||||
hide_viewport: bpy.props.BoolProperty(name="Hide Viewport", default=False) # type: ignore
|
||||
hide_select: bpy.props.BoolProperty(name="Hide Select", default=False) # type: ignore
|
||||
hide_render: bpy.props.BoolProperty(name="Hide Render", default=False) # type: ignore
|
||||
exclude_from_view_layer: bpy.props.BoolProperty(
|
||||
name="Exclude From View Layer", default=False
|
||||
) # type: ignore
|
||||
|
||||
|
||||
class speckle_model_card(bpy.types.PropertyGroup):
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
from ..converter.to_native import * # noqa: F403
|
||||
from ..converter.to_speckle import * # noqa: F403
|
||||
from ..converter.to_native import * #noqa: F403
|
||||
from ..converter.to_speckle import * #noqa: F403
|
||||
from ..converter.utils import * # noqa: F403
|
||||
|
||||
@@ -2,5 +2,5 @@ from .to_speckle import convert_to_speckle # noqa: F401
|
||||
from .material_to_speckle import ( # noqa: F401
|
||||
blender_material_to_speckle,
|
||||
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
|
||||
if apply_modifiers and blender_object.modifiers:
|
||||
import bpy
|
||||
|
||||
|
||||
# Convert curve with modifiers to mesh
|
||||
depsgraph = bpy.context.evaluated_depsgraph_get()
|
||||
evaluated_obj = blender_object.evaluated_get(depsgraph)
|
||||
evaluated_mesh = evaluated_obj.to_mesh()
|
||||
|
||||
|
||||
if evaluated_mesh:
|
||||
meshes = mesh_to_speckle_meshes(
|
||||
blender_object, evaluated_mesh, scale_factor, units
|
||||
@@ -50,22 +50,20 @@ def convert_to_speckle(
|
||||
mesh_data = blender_object.data
|
||||
if apply_modifiers and blender_object.modifiers:
|
||||
import bpy
|
||||
|
||||
|
||||
# use evaluated object to get mesh with modifiers applied
|
||||
depsgraph = bpy.context.evaluated_depsgraph_get()
|
||||
evaluated_obj = blender_object.evaluated_get(depsgraph)
|
||||
evaluated_mesh = evaluated_obj.to_mesh()
|
||||
mesh_data = evaluated_mesh
|
||||
|
||||
meshes = mesh_to_speckle_meshes(blender_object, mesh_data, scale_factor, units)
|
||||
|
||||
if (
|
||||
apply_modifiers
|
||||
and blender_object.modifiers
|
||||
and mesh_data != blender_object.data
|
||||
):
|
||||
|
||||
meshes = mesh_to_speckle_meshes(
|
||||
blender_object, mesh_data, scale_factor, units
|
||||
)
|
||||
|
||||
if apply_modifiers and blender_object.modifiers and mesh_data != blender_object.data:
|
||||
blender_object.to_mesh_clear()
|
||||
|
||||
|
||||
if meshes:
|
||||
display_value = meshes
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ from specklepy.objects import Base
|
||||
from specklepy.objects.graph_traversal.default_traversal import (
|
||||
create_default_traversal_function,
|
||||
)
|
||||
from specklepy.core.api.client import SpeckleClient
|
||||
|
||||
|
||||
def to_rgba(argb_int: int) -> Tuple[float, float, float, float]:
|
||||
@@ -186,3 +187,21 @@ def find_object_by_id(root_object: Base, target_id: str) -> Optional[Base]:
|
||||
return None
|
||||
|
||||
return deep_search(root_object)
|
||||
|
||||
|
||||
def get_project_workspace_id(client: SpeckleClient, project_id: str) -> Optional[str]:
|
||||
workspace_id = None
|
||||
server_version = client.project.server_version or client.server.version()
|
||||
|
||||
# Local yarn builds of server will report a server version if "dev"
|
||||
# We'll assume that local builds are up-to-date with the latest features
|
||||
if server_version[0] == "dev":
|
||||
maj = 999
|
||||
min = 999
|
||||
else:
|
||||
maj = server_version[0]
|
||||
min = server_version[1]
|
||||
|
||||
if maj > 2 or (maj == 2 and min > 20):
|
||||
workspace_id = client.project.get(project_id).workspace_id
|
||||
return workspace_id
|
||||
|
||||
+4
-9
@@ -1,7 +1,6 @@
|
||||
import re
|
||||
import sys
|
||||
|
||||
|
||||
def patch_addon(simple_version: str):
|
||||
"""Patches the __init__.py bl_info version within the connector init file"""
|
||||
FILE_PATH = "bpy_speckle/__init__.py"
|
||||
@@ -10,16 +9,13 @@ def patch_addon(simple_version: str):
|
||||
with open(FILE_PATH, "r") as file:
|
||||
lines = file.readlines()
|
||||
|
||||
for index, line in enumerate(lines):
|
||||
for (index, line) in enumerate(lines):
|
||||
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:
|
||||
file.writelines(lines)
|
||||
|
||||
|
||||
def patch_manifest(simple_version: str):
|
||||
"""Patches the connector version within the connector init file"""
|
||||
FILE_PATH = "bpy_speckle/blender_manifest.toml"
|
||||
@@ -28,8 +24,8 @@ def patch_manifest(simple_version: str):
|
||||
with open(FILE_PATH, "r") as file:
|
||||
lines = file.readlines()
|
||||
|
||||
for index, line in enumerate(lines):
|
||||
if line.startswith("version ="):
|
||||
for (index, line) in enumerate(lines):
|
||||
if line.startswith('version ='):
|
||||
lines[index] = f'version = "{version[0]}.{version[1]}.{version[2]}",\n'
|
||||
print(f"Patched connector version number in {FILE_PATH}")
|
||||
break
|
||||
@@ -37,7 +33,6 @@ def patch_manifest(simple_version: str):
|
||||
with open(FILE_PATH, "w") as file:
|
||||
file.writelines(lines)
|
||||
|
||||
|
||||
def main():
|
||||
tag = sys.argv[1]
|
||||
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"
|
||||
license = "Apache-2.0"
|
||||
dependencies = [
|
||||
"specklepy>=3.0.3",
|
||||
"specklepy>=3.0.1",
|
||||
]
|
||||
|
||||
[dependency-groups]
|
||||
|
||||
Reference in New Issue
Block a user