Compare commits

..

5 Commits

Author SHA1 Message Date
Mucahit Bilal GOKER 040ea64f86 Merge branch 'v3-dev' into bilal/preserve-uv-maps 2025-07-22 12:44:20 +03:00
Mucahit Bilal GOKER bd154c1575 track by application ID instead of name 2025-07-13 19:19:21 +03:00
Mucahit Bilal GOKER bbc20e2353 Merge branch 'bilal/cnx-2065-keep-track-of-loaded-object-properties' into bilal/preserve-uv-maps 2025-07-13 09:12:52 +03:00
Mucahit Bilal GOKER 716347b497 Merge branch 'v3-dev' into bilal/preserve-uv-maps 2025-07-13 09:11:03 +03:00
Mucahit Bilal GOKER 26927ca6f4 uv map preservation first pass 2025-07-06 21:04:14 +03:00
25 changed files with 558 additions and 431 deletions
+1 -1
View File
@@ -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
View File
@@ -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
+4 -10
View File
@@ -1,10 +1,9 @@
from typing import Dict, List, Optional, Tuple
import bpy
from specklepy.core.api.credentials import get_local_accounts
from typing import List, Tuple, Optional, Dict
from specklepy.core.api.credentials import Account
from specklepy.core.api.client import SpeckleClient
from specklepy.core.api.credentials import Account, get_local_accounts
from specklepy.core.api.wrapper import StreamWrapper
from .misc import strip_non_ascii
@@ -24,11 +23,7 @@ class SpeckleClientCache:
if not account:
raise ValueError(f"No account found for ID: {account_id}")
assert account.serverInfo.url
client = SpeckleClient(
host=account.serverInfo.url,
use_ssl=account.serverInfo.url.startswith("https"),
)
client = SpeckleClient(host=account.serverInfo.url)
client.authenticate_with_account(account)
self._clients[account_id] = client
return client
@@ -184,7 +179,6 @@ def get_project_from_url(
try:
wrapper = StreamWrapper(url)
account = wrapper.get_account()
assert account.id
client = _client_cache.get_client(account.id)
# get the stream_id (project_id) from the wrapper
+1 -3
View File
@@ -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)
+324 -253
View File
@@ -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}")
+11 -5
View File
@@ -1,10 +1,8 @@
from typing import List, Optional, Tuple
from specklepy.core.api.inputs.project_inputs import ProjectModelsFilter
from specklepy.core.api.models.current import Model
from .account_manager import _client_cache
from typing import List, Tuple, Optional
from .misc import format_relative_time, strip_non_ascii
from .account_manager import _client_cache
def get_models_for_project(
@@ -20,9 +18,17 @@ def get_models_for_project(
)
return []
# Get cached client
client = _client_cache.get_client(account_id)
if not client:
print(f"Error: Could not get client for account: {account_id}")
return []
client.project.get(project_id)
try:
client.project.get(project_id)
except Exception as e:
print(f"Error: Project with ID {project_id} not found: {str(e)}")
return []
filter = ProjectModelsFilter(search=search) if search else None
+10 -15
View File
@@ -1,16 +1,14 @@
from typing import List, Optional, Tuple
from specklepy.core.api.client import SpeckleClient
from specklepy.core.api.credentials import Account
from specklepy.core.api.inputs.project_inputs import WorksaceProjectsFilter
from specklepy.core.api.resources.current.workspace_resource import WorkspaceResource
from .account_manager import _client_cache
from specklepy.core.api.inputs.project_inputs import WorksaceProjectsFilter
from typing import List, Tuple, Optional
from specklepy.core.api.credentials import Account
from .misc import format_relative_time, format_role, strip_non_ascii
from .account_manager import _client_cache
def get_projects_for_account(
account_id: str, workspace_id: str, search: Optional[str] = None
account_id: str, workspace_id: str = None, search: Optional[str] = None
) -> List[Tuple[str, str, str, str, bool]]:
"""
fetches projects for a given account from the Speckle server
@@ -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,
)
+14 -3
View File
@@ -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):
+14 -6
View File
@@ -1,10 +1,9 @@
from typing import List, Tuple
from specklepy.core.api.client import SpeckleClient
from specklepy.core.api.models.current import Version
from .account_manager import _client_cache
from typing import List, Tuple
from .misc import format_relative_time
from specklepy.core.api.inputs.model_inputs import ModelVersionsFilter
from specklepy.core.api.models.current import Version
def get_versions_for_model(
@@ -21,11 +20,17 @@ def get_versions_for_model(
)
return []
# Get cached client
client: SpeckleClient = _client_cache.get_client(account_id)
if not client:
print(f"Error: Could not get client for account: {account_id}")
return []
filter: ModelVersionsFilter = ModelVersionsFilter(priorityIds=[])
# Get versions
versions = client.version.get_versions(
project_id=project_id, model_id=model_id, limit=10
versions: List[Version] = client.version.get_versions(
project_id=project_id, model_id=model_id, limit=10, filter=filter
)
versions_list: List[Tuple[str, str, str]] = []
for version in versions.items:
@@ -61,6 +66,9 @@ def get_latest_version(
# Get cached client
client: SpeckleClient = _client_cache.get_client(account_id)
if not client:
print(f"Error: Could not get client for account: {account_id}")
return ("", "", "")
# Get versions (limit to 1 since we only need the latest)
versions: List[Version] = client.version.get_versions(
+2 -2
View File
@@ -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 -2
View File
@@ -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
)
+10 -12
View File
@@ -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
+19
View File
@@ -5,6 +5,7 @@ from specklepy.objects import Base
from specklepy.objects.graph_traversal.default_traversal import (
create_default_traversal_function,
)
from specklepy.core.api.client import SpeckleClient
def to_rgba(argb_int: int) -> Tuple[float, float, float, float]:
@@ -186,3 +187,21 @@ def find_object_by_id(root_object: Base, target_id: str) -> Optional[Base]:
return None
return deep_search(root_object)
def get_project_workspace_id(client: SpeckleClient, project_id: str) -> Optional[str]:
workspace_id = None
server_version = client.project.server_version or client.server.version()
# Local yarn builds of server will report a server version if "dev"
# We'll assume that local builds are up-to-date with the latest features
if server_version[0] == "dev":
maj = 999
min = 999
else:
maj = server_version[0]
min = server_version[1]
if maj > 2 or (maj == 2 and min > 20):
workspace_id = client.project.get(project_id).workspace_id
return workspace_id
+4 -9
View File
@@ -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
View File
@@ -5,7 +5,7 @@ description = "Next-Gen Speckle connector for Blender!"
requires-python = ">=3.11.9, <4.0.0"
license = "Apache-2.0"
dependencies = [
"specklepy>=3.0.3",
"specklepy>=3.0.1",
]
[dependency-groups]