Compare commits

...

60 Commits

Author SHA1 Message Date
Jedd Morgan c70c9823d8 Merge pull request #250 from specklesystems/v3-dev
Release workflow / Build Zip (push) Has been cancelled
Release workflow / deploy-installers (push) Has been cancelled
dev to installer
2025-05-01 18:16:09 +03:00
Dogukan Karatas f0495ab093 Merge pull request #249 from specklesystems/dogukan/get-version-message
fix: none check for version messages
2025-05-01 17:14:40 +02:00
Dogukan Karatas e57dacc6ef updates the version check 2025-05-01 17:11:00 +02:00
Dogukan Karatas 69dbf2f117 updates version manager 2025-05-01 17:00:52 +02:00
Jedd Morgan e7bf842508 use sectigo timestamp
Release workflow / Build Zip (push) Has been cancelled
Release workflow / deploy-installers (push) Has been cancelled
2025-05-01 16:03:29 +03:00
Dogukan Karatas 3cf3867877 Merge pull request #247 from specklesystems/bilal/cnx-1650-integrating-workspaces-to-blender-dui
Bilal/cnx 1650 integrating workspaces to blender dui
2025-05-01 11:49:58 +02:00
Dogukan Karatas 3577f3fd6a updates personal account check 2025-05-01 10:58:32 +02:00
Mucahit Bilal GOKER 242af16e81 Merge remote-tracking branch 'origin/bilal/cnx-1650-integrating-workspaces-to-blender-dui' into bilal/cnx-1650-integrating-workspaces-to-blender-dui 2025-05-01 11:19:05 +03:00
Dogukan Karatas 20f36ffe19 Merge pull request #248 from specklesystems/dogukan/cnx-1476-instance-proxies
feat: conversion of instance proxies
2025-05-01 10:17:33 +02:00
Mucahit Bilal GOKER b103d0da09 only show completed workspaces 2025-05-01 11:16:58 +03:00
Mucahit Bilal GOKER 4add9c5d72 bump specklepy version 2025-05-01 11:16:44 +03:00
Mucahit Bilal GOKER aecb15e549 bump specklepy version 2025-04-30 19:56:21 +03:00
Dogukan Karatas bff8140559 cleaning up some checks 2025-04-30 18:44:12 +02:00
Dogukan Karatas fe98f71be2 fixes the scaling issue 2025-04-30 18:03:51 +02:00
Mucahit Bilal GOKER f2008502f3 support self hosters 2025-04-30 18:24:53 +03:00
Dogukan Karatas 2981c1270b fixes scaling weirdly 2025-04-30 17:22:57 +02:00
Mucahit Bilal GOKER 3011921df9 only list valid versions 2025-04-30 17:29:07 +03:00
Mucahit Bilal GOKER 79ecba213a strip non ascii from model selection dialog texts 2025-04-30 17:28:54 +03:00
Mucahit Bilal GOKER 06db6653fb swıtch the order of account text > move server url to the end so it's visible 2025-04-30 17:28:29 +03:00
Mucahit Bilal GOKER 86200091dc strip non ascii chars 2025-04-30 14:45:19 +03:00
Mucahit Bilal GOKER 4b2a678605 update workspaces when account changes 2025-04-30 14:45:09 +03:00
Dogukan Karatas 1ca2c03ec0 check attributes 2025-04-30 13:07:50 +02:00
Mucahit Bilal GOKER 528b81d294 fetch personal projects 2025-04-30 13:46:05 +03:00
Dogukan Karatas d0dd57a731 sorts nested proxies 2025-04-30 11:56:15 +02:00
Mucahit Bilal GOKER 25258fd39f update projects list when workspace changes 2025-04-30 07:29:15 +03:00
Mucahit Bilal GOKER a884f7a6ea filter projects by workspace 2025-04-30 07:24:05 +03:00
Mucahit Bilal GOKER 6e0df0d4e4 added personal projects to workspaces dropdown 2025-04-30 07:15:51 +03:00
Mucahit Bilal GOKER 58ff3e667e initial workspace dropdown 2025-04-30 07:05:50 +03:00
Mucahit Bilal GOKER 3f5c933ee9 Merge branch 'bilal/cnx-1690-get_accounts-is-continously-called-in-the-background' into bilal/cnx-1650-integrating-workspaces-to-blender-dui 2025-04-30 06:17:56 +03:00
Mucahit Bilal GOKER d137c7b991 store accounts in window manager and initialize in invoke 2025-04-29 23:30:18 +03:00
Mucahit Bilal GOKER 5eec02296b move print statement out of list 2025-04-29 21:59:42 +03:00
Mucahit Bilal GOKER 681acd81c8 added logging to update projects function 2025-04-29 18:56:55 +03:00
Dogukan Karatas 09dd819504 some cleanup 2025-04-29 17:32:24 +02:00
Dogukan Karatas 211296c803 unlink instance collection from scene 2025-04-29 17:29:04 +02:00
Mucahit Bilal GOKER 0fd5448342 print account manager 2025-04-29 17:53:50 +03:00
Dogukan Karatas 3002d7a31b adds poc for instance proxy conversion 2025-04-29 16:36:38 +02:00
Mucahit Bilal GOKER 8eee1ede58 specklepy version update 2025-04-29 17:24:23 +03:00
Jedd Morgan 4a340ef1ae refactor(ci): Update workflow to use new consolidated deployment workflow (#238)
Release workflow / Build Zip (push) Has been cancelled
Release workflow / deploy-installers (push) Has been cancelled
* Update release workflow

* new workflow

* fileversion experiment

* Always use the run number in the file_version

* only capture first 3 numbers in semver

* build file version from tag

* updated workflow-dispatch
2025-04-24 14:05:07 +01:00
Mucahit Bilal GOKER b6a96802a8 Merge pull request #234 from specklesystems/bilal/cnx-1507-load-button-in-the-model-card-should-get-new-version
Bilal/cnx 1507 load button in the model card should get new version
2025-04-23 14:40:23 +03:00
Mucahit Bilal GOKER f2a0ffa9ee Merge branch 'v3-dev' into bilal/cnx-1507-load-button-in-the-model-card-should-get-new-version 2025-04-23 14:35:19 +03:00
Mucahit Bilal GOKER 24479811f7 Merge pull request #242 from specklesystems/bilal/cnx-1601-adding-models-by-url
Bilal/cnx 1601 adding models by url
2025-04-23 14:33:51 +03:00
Mucahit Bilal GOKER 2153db9704 fix: incorrect version id when model url is given 2025-04-22 23:33:54 +03:00
Mucahit Bilal GOKER dbcc820304 fix: load button in the model card loads latest version 2025-04-17 17:28:45 +03:00
Mucahit Bilal GOKER 830632fa1e ruff check: remove unused imports 2025-04-17 17:18:53 +03:00
Mucahit Bilal GOKER 2f57ca96ca added comments to main panel 2025-04-17 17:12:49 +03:00
Mucahit Bilal GOKER 1b9ee91880 select objects by model card collection name 2025-04-17 17:12:01 +03:00
Mucahit Bilal GOKER 8fb7519e7b load button -> set account id and collection name to model cards 2025-04-17 17:11:37 +03:00
Mucahit Bilal GOKER 9dce548a05 add collection name and account id to model cards 2025-04-17 17:10:24 +03:00
Mucahit Bilal GOKER 487253babe remove window manager invokes from dialogs 2025-04-17 17:08:17 +03:00
Mucahit Bilal GOKER f245584428 i forgot to add actual invoke method 2025-04-17 17:06:18 +03:00
Mucahit Bilal GOKER 1ac784d290 invoke window manager properties on initialization 2025-04-17 17:05:58 +03:00
Mucahit Bilal GOKER 314a962014 Merge branch 'v3-dev' into bilal/cnx-1507-load-button-in-the-model-card-should-get-new-version 2025-04-17 13:12:45 +03:00
Mucahit Bilal GOKER 6b07a0fff4 change button icon and remove text label 2025-04-17 12:50:14 +03:00
Mucahit Bilal GOKER d3208de754 error handling for the url 2025-04-17 12:38:44 +03:00
Mucahit Bilal GOKER 4ba19231b7 code cleanup 2025-04-17 11:57:27 +03:00
Mucahit Bilal GOKER a93ac797fc add project by url functionality 2025-04-16 19:31:06 +03:00
Mucahit Bilal GOKER 4569b1e623 ensure wm properties exists 2025-04-16 19:30:48 +03:00
Mucahit Bilal GOKER ae3222683e get model details by wrapper 2025-04-16 19:30:30 +03:00
Mucahit Bilal GOKER 780184c562 add button to project selection dialog 2025-04-16 18:06:09 +03:00
Mucahit Bilal GOKER aad9246463 adds functional load button in the model card 2025-03-27 19:04:10 +03:00
23 changed files with 937 additions and 197 deletions
+16 -11
View File
@@ -26,14 +26,14 @@ jobs:
- id: set-version
name: Set version to output
run:
| # Processing the ref manually atm.. likely we'll want to use Adam's fancy logic at some point instead
run: |
TAG=${{ github.ref_name }}
if [[ "${{ github.ref }}" != refs/tags/* ]]; then
TAG="3.0.99.${{ github.run_number }}"
TAG="v3.0.99.${{ github.run_number }}"
fi
SEMVER=$(echo "$TAG" | sed -E 's/\/[a-zA-Z-]+//')
FILE_VERSION=$(echo "$TAG" | sed -E 's/^v([0-9]+)\.([0-9]+)\.([0-9]+)(-[a-zA-Z]+\.([0-9]+))?/\1.\2.\3.\5/' | sed 's/\.$/.0/')
SEMVER="${TAG#v}"
FILE_VERSION=$(echo "$TAG" | sed -E 's/^v([0-9]+\.[0-9]+\.[0-9]+).*/\1/')
FILE_VERSION="$FILE_VERSION.${{ github.run_number }}"
echo "semver=$SEMVER" >> "$GITHUB_OUTPUT"
echo "fileVersion=$FILE_VERSION" >> "$GITHUB_OUTPUT"
@@ -69,17 +69,22 @@ jobs:
runs-on: ubuntu-latest
needs: build
env:
IS_TAG_BUILD: ${{ github.ref_type == 'tag' }}
IS_RELEASE_BRANCH: ${{ startsWith(github.ref_name, 'installer-test/') || github.ref_name == 'main'}}
IS_PUBLIC_RELEASE: ${{ github.ref_type == 'tag' }}
steps:
- name: 🔫 Trigger Build Installer(s)
uses: ALEEF02/workflow-dispatch@v3.0.0
uses: the-actions-org/workflow-dispatch@v4.0.0
with:
workflow: Build Blender Installer
workflow: Build Installers
repo: specklesystems/connector-installers
token: ${{ secrets.CONNECTORS_GH_TOKEN }}
inputs: '{ "run_id": "${{ github.run_id }}", "semver": "${{ needs.build.outputs.semver }}", "file_version": "${{ needs.build.outputs.fileVersion }}","public_release": ${{ env.IS_TAG_BUILD }}, "store_artifacts": ${{ env.IS_RELEASE_BRANCH }} }'
ref: main
inputs: '{
"run_id": "${{ github.run_id }}",
"semver": "${{ needs.build.outputs.semver }}",
"file_version": "${{ needs.build.outputs.fileVersion }}",
"repo": "${{ github.repository }}",
"is_public_release": ${{ env.IS_PUBLIC_RELEASE }}
}'
ref: timestamp-sectigo
wait-for-completion: true
wait-for-completion-interval: 10s
wait-for-completion-timeout: 10m
+42 -3
View File
@@ -12,6 +12,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# ruff: noqa
import bpy
from bpy.types import WindowManager
from .connector.ui import icons
import json
@@ -34,7 +35,7 @@ bl_info = {
# UI
from .connector.ui.main_panel import SPECKLE_PT_main_panel
from .connector.ui.project_selection_dialog import SPECKLE_OT_project_selection_dialog, speckle_project, SPECKLE_UL_projects_list, SPECKLE_OT_add_project_by_url
from .connector.ui.project_selection_dialog import SPECKLE_OT_project_selection_dialog, speckle_project, SPECKLE_UL_projects_list, speckle_workspace
from .connector.ui.model_selection_dialog import SPECKLE_OT_model_selection_dialog, speckle_model, SPECKLE_UL_models_list
from .connector.ui.version_selection_dialog import SPECKLE_OT_version_selection_dialog, speckle_version, SPECKLE_UL_versions_list
from .connector.ui.selection_filter_dialog import SPECKLE_OT_selection_filter_dialog
@@ -45,9 +46,42 @@ from .connector.blender_operators.load_button import SPECKLE_OT_load
from .connector.blender_operators.model_card_settings import SPECKLE_OT_model_card_settings, SPECKLE_OT_view_in_browser, SPECKLE_OT_view_model_versions, SPECKLE_OT_delete_model_card
from .connector.blender_operators.select_objects import SPECKLE_OT_select_objects
from .connector.blender_operators.add_account_button import SPECKLE_OT_add_account
from .connector.blender_operators.load_latest_button import SPECKLE_OT_load_latest
from .connector.blender_operators.add_project_by_url import SPECKLE_OT_add_project_by_url
from .connector.utils.account_manager import speckle_account
# States
from .connector.states.speckle_state import register as register_speckle_state, unregister as unregister_speckle_state
def invoke_window_manager_properties():
# Accounts
WindowManager.speckle_accounts = bpy.props.CollectionProperty(
type = speckle_account
)
WindowManager.selected_account_id = bpy.props.StringProperty()
# Workspaces
WindowManager.speckle_workspaces = bpy.props.CollectionProperty(
type = speckle_workspace
)
WindowManager.selected_workspace_id = bpy.props.StringProperty()
# Projects
WindowManager.speckle_projects = bpy.props.CollectionProperty(
type=speckle_project
)
WindowManager.selected_project_id = bpy.props.StringProperty()
WindowManager.selected_project_name = bpy.props.StringProperty()
# Models
WindowManager.speckle_models = bpy.props.CollectionProperty(
type=speckle_model
)
WindowManager.selected_model_id = bpy.props.StringProperty()
WindowManager.selected_model_name = bpy.props.StringProperty()
# Versions
WindowManager.speckle_versions = bpy.props.CollectionProperty(
type=speckle_version
)
WindowManager.selected_version_id = bpy.props.StringProperty()
WindowManager.selected_version_load_option = bpy.props.StringProperty()
def save_model_cards(scene):
model_cards_data = [card.to_dict() for card in scene.speckle_state.model_cards]
scene["speckle_model_cards_data"] = json.dumps(model_cards_data)
@@ -66,13 +100,16 @@ classes = (
SPECKLE_PT_main_panel,
SPECKLE_OT_publish,
SPECKLE_OT_load,
SPECKLE_OT_project_selection_dialog, speckle_project, SPECKLE_UL_projects_list, SPECKLE_OT_add_project_by_url,
SPECKLE_OT_project_selection_dialog, speckle_project, SPECKLE_UL_projects_list, speckle_workspace,
SPECKLE_OT_model_selection_dialog, speckle_model, SPECKLE_UL_models_list,
SPECKLE_OT_version_selection_dialog, speckle_version, SPECKLE_UL_versions_list,
SPECKLE_OT_selection_filter_dialog,
speckle_model_card, SPECKLE_OT_model_card_settings, SPECKLE_OT_view_in_browser, SPECKLE_OT_view_model_versions, SPECKLE_OT_delete_model_card,
SPECKLE_OT_select_objects,
SPECKLE_OT_add_account)
SPECKLE_OT_add_account,
SPECKLE_OT_load_latest,
SPECKLE_OT_add_project_by_url,
speckle_account)
@bpy.app.handlers.persistent
def load_handler(dummy):
@@ -93,6 +130,8 @@ def register():
bpy.app.handlers.load_post.append(load_handler)
bpy.app.handlers.save_post.append(save_handler)
invoke_window_manager_properties()
def unregister():
icons.unload_icons()
unregister_speckle_state() # Unregister SpeckleState
@@ -1,8 +1,9 @@
from ..blender_operators.load_button import SPECKLE_OT_load # noqa: F401
from ..blender_operators.load_latest_button import SPECKLE_OT_load_latest # noqa: F401
from ..blender_operators.publish_button import SPECKLE_OT_publish # noqa: F401
from ..blender_operators.model_card_settings import (
SPECKLE_OT_model_card_settings,
SPECKLE_OT_view_in_browser,
SPECKLE_OT_view_model_versions,
SPECKLE_OT_delete_model_card
) # noqa: F401
SPECKLE_OT_model_card_settings, #noqa: F401
SPECKLE_OT_view_in_browser, #noqa: F401
SPECKLE_OT_view_model_versions, #noqa: F401
SPECKLE_OT_delete_model_card #noqa: F401
)
@@ -1,7 +1,6 @@
import bpy
import webbrowser
from typing import Set
from bpy.types import Event, Context, UILayout
from bpy.types import Event, Context
class SPECKLE_OT_add_account(bpy.types.Operator):
"""Operator for adding a new Speckle account.
@@ -0,0 +1,109 @@
import bpy
from bpy.types import Context, Event, UILayout, WindowManager
from specklepy.api.wrapper import StreamWrapper
from typing import Tuple
from ...connector.utils.version_manager import get_latest_version
class SPECKLE_OT_add_project_by_url(bpy.types.Operator):
"""
operator for adding a Speckle project by URL
"""
bl_idname = "speckle.add_project_by_url"
bl_label = "Add Project by URL"
bl_description = "Add a project from a URL"
url: bpy.props.StringProperty( # type: ignore
name="Project URL", description="Enter the Speckle project URL", default=""
)
def execute(self, context: Context) -> set[str]:
# TODO: Implement logic to add project using the URL
self.report({"INFO"}, f"Adding project from URL: {self.url}")
wm = context.window_manager
try:
wrapper = StreamWrapper(self.url)
except Exception as e:
self.report({"ERROR"}, f"Failed to process URL: {str(e)}")
return {"CANCELLED"}
# Get model details from the wrapper
account_id, project_id, project_name, model_id, model_name, version_id, load_option = get_model_details_by_wrapper(wrapper)
wm.selected_account_id = account_id
if project_id:
wm.selected_project_id = project_id
wm.selected_project_name = project_name
if model_id:
wm.selected_model_id = model_id
wm.selected_model_name = model_name
if version_id:
wm.selected_version_id = version_id
wm.selected_version_id = version_id
wm.selected_version_load_option = load_option
context.window.screen = context.window.screen
context.area.tag_redraw()
return {"FINISHED"}
def invoke(self, context: Context, event: Event) -> set[str]:
# Ensure all required properties exist in WindowManager
if not hasattr(WindowManager, "selected_account_id"):
WindowManager.selected_account_id = bpy.props.StringProperty()
if not hasattr(WindowManager, "selected_project_id"):
WindowManager.selected_project_id = bpy.props.StringProperty(
name="Selected Project ID"
)
if not hasattr(WindowManager, "selected_project_name"):
WindowManager.selected_project_name = bpy.props.StringProperty(
name="Selected Project Name"
)
if not hasattr(WindowManager, "selected_model_id"):
WindowManager.selected_model_id = bpy.props.StringProperty(
name="Selected Model ID"
)
if not hasattr(WindowManager, "selected_model_name"):
WindowManager.selected_model_name = bpy.props.StringProperty(
name="Selected Model Name"
)
if not hasattr(WindowManager, "selected_version_id"):
WindowManager.selected_version_id = bpy.props.StringProperty(
name="Selected Version ID"
)
if not hasattr(WindowManager, "selected_version_load_option"):
WindowManager.selected_version_load_option = bpy.props.StringProperty(
name="Selected Version Load Option"
)
return context.window_manager.invoke_props_dialog(self)
def draw(self, context: Context) -> None:
layout: UILayout = self.layout
layout.prop(self, "url", text="")
def register() -> None:
bpy.utils.register_class(SPECKLE_OT_add_project_by_url)
def unregister() -> None:
bpy.utils.unregister_class(SPECKLE_OT_add_project_by_url)
def get_model_details_by_wrapper(wrapper: StreamWrapper) -> Tuple[str, str, str, str, str, str]:
client = wrapper.get_client()
client.authenticate_with_account(wrapper.get_account())
account_id, project_id, project_name, model_id, model_name, version_id, load_option = "", "", "", "", "", "", ""
account_id = wrapper.get_account().id
if wrapper.stream_id:
project_id = wrapper.stream_id
project_name = client.project.get(project_id).name
if wrapper.model_id:
model_id = wrapper.model_id
model = client.model.get(model_id, project_id)
model_name = model.name
load_option = "LATEST" if not wrapper.commit_id else "SPECIFIC"
version_id = wrapper.commit_id if wrapper.commit_id else client.version.get_versions(wrapper.model_id, wrapper.stream_id, limit = 1).items[0].id
return (account_id, project_id, project_name, model_id, model_name, version_id, load_option)
@@ -16,6 +16,7 @@ class SPECKLE_OT_load(bpy.types.Operator):
def execute(self, context: Context) -> Set[str]:
wm = context.window_manager
model_card = context.scene.speckle_state.model_cards.add()
model_card.account_id = wm.selected_account_id
model_card.server_url = get_server_url_by_account_id(wm.selected_account_id)
model_card.project_id = wm.selected_project_id
model_card.project_name = wm.selected_project_name
@@ -24,11 +25,13 @@ class SPECKLE_OT_load(bpy.types.Operator):
model_card.is_publish = False
model_card.load_option = wm.selected_version_load_option
model_card.version_id = wm.selected_version_id
model_card.collection_name = f"{wm.selected_model_name} - {wm.selected_version_id[:8]}"
# Load selected model version
load_operation(context)
# Clear selected model details from Window Manager
wm.selected_account_id = ""
wm.selected_project_id = ""
wm.selected_project_name = ""
wm.selected_model_id = ""
@@ -0,0 +1,81 @@
import bpy
from typing import Set
from bpy.types import Context
from ..utils.version_manager import get_latest_version
from ..operations.load_operation import load_operation
class SPECKLE_OT_load_latest(bpy.types.Operator):
bl_idname = "speckle.load_latest"
bl_label = "Load Latest from Speckle"
bl_description = "Load the latest version from Speckle"
model_card_id: bpy.props.StringProperty(name="Model Card ID", default="") # type: ignore
def execute(self, context: Context) -> Set[str]:
wm = context.window_manager
# Get the model card
model_card = context.scene.speckle_state.get_model_card_by_id(self.model_card_id)
# Check if load_option is set to "LATEST"
if model_card.load_option != "LATEST":
# Do nothing if load_option is not "LATEST"
return {"FINISHED"}
# Get the latest version from Speckle
latest_version_id, message, timestamp = get_latest_version(
model_card.account_id,
model_card.project_id,
model_card.model_id
)
# Throw error if latest version is not found
if not latest_version_id:
self.report({"ERROR"}, "Failed to get latest version")
return {"CANCELLED"}
# Check if the collection exists and delete it if it does
collection = bpy.data.collections.get(model_card.collection_name)
# Update the model card with the latest version ID
original_version_id = model_card.version_id
if latest_version_id == original_version_id:
self.report({"INFO"}, "Latest version is already loaded")
return {"FINISHED"}
if collection:
# Remove the collection
bpy.data.collections.remove(collection)
self.report({"INFO"}, f"Deleted existing collection: {model_card.collection_name}")
# overwrite version id of the model card stored in the doc
model_card.version_id = latest_version_id
# overwrite version id store in wm
# Set Window Manager properties
wm.selected_account_id = model_card.account_id
wm.selected_project_id = model_card.project_id
wm.selected_model_name = model_card.model_name
wm.selected_version_id = latest_version_id
# Load the latest version
try:
load_operation(context)
self.report(
{"INFO"},
f"Loaded latest version: {latest_version_id[:8]} (was: {original_version_id[:8]})"
)
# update collection name in model card
model_card.collection_name = f"{model_card.model_name} - {latest_version_id[:8]}"
except Exception as e:
# Restore the original version ID if loading fails
model_card.version_id = original_version_id
self.report({"ERROR"}, f"Failed to load latest version: {str(e)}")
return {"CANCELLED"}
# Clear selected model details from Window Manager
wm.selected_account_id = ""
wm.selected_project_id = ""
wm.selected_version_id = ""
wm.selected_model_name = ""
return {"FINISHED"}
@@ -24,7 +24,7 @@ class SPECKLE_OT_select_objects(Operator):
self.report({"ERROR"}, "Model card not found")
return {"CANCELLED"}
collection_name = f"{model_card.model_name} - {model_card.version_id[:8]}"
collection_name = model_card.collection_name
collection = bpy.data.collections.get(collection_name)
if not collection:
@@ -10,18 +10,22 @@ from specklepy.objects.graph_traversal.default_traversal import (
)
from ..utils.get_ascendants import get_ascendants
from ...converter.to_native import convert_to_native, render_material_proxy_to_native
from ...converter.utils import find_object_by_id
from ...converter.to_native import (
convert_to_native,
render_material_proxy_to_native,
instance_definition_proxy_to_native,
find_instance_definitions,
)
def load_operation(context: Context) -> None:
"""
load objects from Speckle and maintain hierarchy.
"""
wm = context.window_manager
# get account
# to discuss: this looks redundant, we need to cache it somehow
account = next(
(
acc
@@ -35,6 +39,8 @@ def load_operation(context: Context) -> None:
print("No Speckle account found")
return
print(f"Using account: {account.userInfo.email}")
# receive the data
client = SpeckleClient(host=account.serverInfo.url)
client.authenticate_with_account(account)
@@ -46,8 +52,30 @@ def load_operation(context: Context) -> None:
version_data = operations.receive(obj_id, transport)
# Create material mapping first
material_mapping = render_material_proxy_to_native(version_data)
print(f"Created material mapping for {len(material_mapping)} objects")
definition_collections, definition_objects = instance_definition_proxy_to_native(
version_data, material_mapping
)
definitions_root_collection = None
if definition_collections:
definitions_root_collection = bpy.data.collections.new("InstanceDefinitions")
for collection in definition_collections.values():
definitions_root_collection.children.link(collection)
definition_object_ids = set()
for definition in find_instance_definitions(version_data).values():
definition_object_ids.update(definition.objects)
for obj_id in definition.objects:
found_obj = find_object_by_id(version_data, obj_id)
if found_obj:
if hasattr(found_obj, "id"):
definition_object_ids.add(found_obj.id)
if hasattr(found_obj, "applicationId"):
definition_object_ids.add(found_obj.applicationId)
traversal_function = create_default_traversal_function()
@@ -57,8 +85,8 @@ def load_operation(context: Context) -> None:
context.window_manager.progress_begin(0, 100)
# dictionary to track converted objects by Speckle ID
converted_objects = {}
converted_objects = definition_objects.copy()
created_collections = {}
created_collections[root_collection_name] = root_collection
@@ -70,7 +98,11 @@ def load_operation(context: Context) -> None:
for traversal_item in traversal_function.traverse(version_data):
speckle_obj = traversal_item.current
if not hasattr(speckle_obj, "id"):
# Skip objects that are part of instance definitions
if speckle_obj.id in definition_object_ids or (
hasattr(speckle_obj, "applicationId")
and speckle_obj.applicationId in definition_object_ids
):
continue
all_objects[speckle_obj.id] = speckle_obj
@@ -101,19 +133,13 @@ def load_operation(context: Context) -> None:
"full_path": [collection_name],
}
# build full path hierarchy
if parent_id in collection_hierarchy:
collection_hierarchy[speckle_obj.id]["full_path"] = (
collection_hierarchy[parent_id]["full_path"] + [collection_name]
)
else:
if hasattr(speckle_obj, "id"):
parent_id = None
for parent in parent_ascendants:
if isinstance(parent, SCollection) and hasattr(parent, "id"):
parent_id = parent.id
break
pass
def get_collection_depth(coll_id):
parent_id = collection_hierarchy[coll_id]["parent_id"]
@@ -153,10 +179,9 @@ def load_operation(context: Context) -> None:
if parent_info["blender_collection"]:
parent_collection = parent_info["blender_collection"]
# create or find the collection
if collection_key in created_collections:
print(f"Collection already exists: {coll_name}")
blender_collection = created_collections[collection_key]
else:
blender_collection = bpy.data.collections.new(coll_name)
parent_collection.children.link(blender_collection)
@@ -172,18 +197,21 @@ def load_operation(context: Context) -> None:
if isinstance(speckle_obj, SCollection):
continue
if hasattr(speckle_obj, "id") and speckle_obj.id in converted_objects:
if not hasattr(speckle_obj, "id"):
print("Skipping object without ID")
continue
# Skip objects that are part of instance definitions
if speckle_obj.id in definition_object_ids or (
hasattr(speckle_obj, "applicationId")
and speckle_obj.applicationId in definition_object_ids
):
continue
if speckle_obj.id in converted_objects:
continue
try:
blender_obj = convert_to_native(speckle_obj, material_mapping)
if blender_obj is None:
print(f"No converter found for: {speckle_obj.speckle_type}")
continue
if hasattr(speckle_obj, "id"):
converted_objects[speckle_obj.id] = blender_obj
target_collection = root_collection
ascendants = list(get_ascendants(traversal_item))
@@ -196,17 +224,32 @@ def load_operation(context: Context) -> None:
target_collection = coll_info["blender_collection"]
break
try:
already_linked = False
for coll in bpy.data.collections:
if blender_obj.name in coll.objects:
already_linked = True
blender_obj = convert_to_native(
speckle_obj,
material_mapping,
definition_collections=definition_collections,
root_collection=target_collection,
)
if not already_linked:
target_collection.objects.link(blender_obj)
if blender_obj is None:
continue
except RuntimeError as e:
print(f"Error linking object to collection: {e}")
converted_objects[speckle_obj.id] = blender_obj
if hasattr(speckle_obj, "applicationId"):
converted_objects[speckle_obj.applicationId] = blender_obj
if not isinstance(blender_obj, bpy.types.Collection):
try:
already_linked = False
for coll in bpy.data.collections:
if blender_obj.name in coll.objects:
already_linked = True
if not already_linked:
target_collection.objects.link(blender_obj)
except RuntimeError as e:
print(f"Error linking object to collection: {e}")
except Exception as e:
print(f"Error converting {speckle_obj.speckle_type}: {str(e)}")
@@ -224,4 +267,4 @@ def load_operation(context: Context) -> None:
if area.type == "OUTLINER":
area.tag_redraw()
print(f"Load process completed. Imported {len(converted_objects)} objects.")
print(f"\nLoad process completed. Imported {len(converted_objects)} objects.")
+7 -1
View File
@@ -99,12 +99,18 @@ class SPECKLE_PT_main_panel(bpy.types.Panel):
box: UILayout = project_box.box()
row: UILayout = box.row()
icon: str = "EXPORT" if model_card.is_publish else "IMPORT"
row.operator("speckle.publish", text="", icon=icon)
# Load latest button in the model card
row.operator("speckle.load_latest", text="", icon=icon).model_card_id = model_card.get_model_card_id()
row.label(text=f"{model_card.model_name}")
# Select button in the model card
select_op = row.operator(
"speckle.select_objects", text="", icon="RESTRICT_SELECT_OFF"
)
select_op.model_card_id = model_card.get_model_card_id()
# Settings button in the model card
row.operator(
"speckle.model_card_settings", text="", icon="PREFERENCES"
).model_card_id = model_card.get_model_card_id()
+10
View File
@@ -7,6 +7,9 @@ class speckle_model_card(bpy.types.PropertyGroup):
represents a Speckle model card in the Blender UI
"""
account_id: bpy.props.StringProperty(
name="Account ID", description="ID of the account", default=""
) # type: ignore
server_url: bpy.props.StringProperty(
name="Server URL",
description="URL of the Server",
@@ -38,6 +41,9 @@ class speckle_model_card(bpy.types.PropertyGroup):
load_option: bpy.props.StringProperty(
name="Version ID", description="ID of the selected version", default=""
) # type: ignore
collection_name: bpy.props.StringProperty(
name="Collection Name", description="Name of the collection", default=""
) # type: ignore
def get_model_card_id(self) -> str:
if not self.project_id or not self.model_id:
@@ -51,6 +57,7 @@ class speckle_model_card(bpy.types.PropertyGroup):
converts the model card to a dictionary representation
"""
return {
"account_id": self.account_id,
"server_url": self.server_url,
"project_name": self.project_name,
"project_id": self.project_id,
@@ -59,6 +66,7 @@ class speckle_model_card(bpy.types.PropertyGroup):
"is_publish": self.is_publish,
"selection_summary": self.selection_summary,
"version_id": self.version_id,
"collection_name": self.collection_name,
}
@classmethod
@@ -67,6 +75,7 @@ class speckle_model_card(bpy.types.PropertyGroup):
creates a new model card instance from a dictionary
"""
item = cls()
item.account_id = data["account_id"]
item.server_url = data["server_url"]
item.project_name = data["project_name"]
item.project_id = data["project_id"]
@@ -75,3 +84,4 @@ class speckle_model_card(bpy.types.PropertyGroup):
item.is_publish = data["is_publish"]
item.selection_summary = data["selection_summary"]
item.version_id = data["version_id"]
item.collection_name = data["collection_name"]
@@ -1,5 +1,5 @@
import bpy
from bpy.types import UILayout, Context, PropertyGroup, Event, WindowManager
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
@@ -101,31 +101,6 @@ class SPECKLE_OT_model_selection_dialog(bpy.types.Operator):
return {"FINISHED"}
def invoke(self, context: Context, event: Event) -> set[str]:
if not hasattr(WindowManager, "speckle_models"):
WindowManager.speckle_models = bpy.props.CollectionProperty(
type=speckle_model
)
if not hasattr(WindowManager, "selected_model_id"):
WindowManager.selected_model_id = bpy.props.StringProperty(
name="Selected Model ID"
)
if not hasattr(WindowManager, "selected_model_name"):
WindowManager.selected_model_name = bpy.props.StringProperty(
name="Selected Model Name"
)
# Loading Latest version will be the default behaviour
if not hasattr(WindowManager, "selected_version_id"):
WindowManager.selected_version_id = bpy.props.StringProperty(
name="Selected Version ID"
)
if not hasattr(WindowManager, "selected_version_load_option"):
WindowManager.selected_version_load_option = bpy.props.StringProperty(
name="Selected Version Load Option"
)
self.update_models_list(context)
return context.window_manager.invoke_props_dialog(self)
@@ -157,9 +132,6 @@ def register() -> None:
def unregister() -> None:
if hasattr(WindowManager, "speckle_models"):
del WindowManager.speckle_models
bpy.utils.unregister_class(SPECKLE_OT_model_selection_dialog)
bpy.utils.unregister_class(SPECKLE_UL_models_list)
bpy.utils.unregister_class(speckle_model)
@@ -1,13 +1,37 @@
import bpy
from bpy.types import UILayout, Context, PropertyGroup, Event, WindowManager
from bpy.types import UILayout, Context, PropertyGroup, Event
from typing import List, Tuple
from ..utils.account_manager import get_account_enum_items, get_default_account_id
from ..utils.account_manager import get_account_enum_items, speckle_account, get_workspaces, speckle_workspace, get_account_from_id
from ..utils.project_manager import get_projects_for_account
def get_accounts_callback(self, context):
"""Callback to dynamically fetch account enum items.
"""
return get_account_enum_items()
wm = context.window_manager
return [
(
account.id,
f"{account.user_name} - {account.user_email} - {account.server_url}",
""
)
for account in wm.speckle_accounts
]
def get_workspaces_callback(self, context):
"""
Callback to dynamically fetch workspace enum items.
"""
wm = context.window_manager
return [
(
workspace.id,
workspace.name,
"",
"WORKSPACE",
i
)
for i, workspace in enumerate(wm.speckle_workspaces)
]
class speckle_project(bpy.types.PropertyGroup):
"""
@@ -58,20 +82,23 @@ class SPECKLE_OT_project_selection_dialog(bpy.types.Operator):
bl_idname = "speckle.project_selection_dialog"
bl_label = "Select Project"
def update_projects_list(self, context: Context) -> None:
"""
updates the list of projects based on the selected account and search query
"""
def update_workspaces_and_projects_list(self, context: Context) -> None:
wm = context.window_manager
wm.selected_account_id = self.accounts
wm.speckle_workspaces.clear()
workspaces = get_workspaces(self.accounts)
for id, name in workspaces:
workspace: speckle_workspace = wm.speckle_workspaces.add()
workspace.id = id
workspace.name = name
print("Updated Workspaces List!")
wm.speckle_projects.clear()
# get projects for the selected account, using search if provided
search = self.search_query if self.search_query.strip() else None
projects: List[Tuple[str, str, str, str]] = get_projects_for_account(
self.accounts, search=search
self.accounts, search=search, workspace_id=self.workspaces
)
for name, role, updated, id in projects:
@@ -80,9 +107,36 @@ class SPECKLE_OT_project_selection_dialog(bpy.types.Operator):
project.role = role
project.updated = updated
project.id = id
print("Updated Projects List!")
return None
def update_projects_list(self, context: Context) -> None:
"""
updates the list of projects based on the selected account and search query
"""
wm = context.window_manager
wm.selected_account_id = self.accounts
wm.selected_workspace_id = self.workspaces
wm.speckle_projects.clear()
# get projects for the selected account, using search if provided
search = self.search_query if self.search_query.strip() else None
projects: List[Tuple[str, str, str, str]] = get_projects_for_account(
self.accounts, search=search, workspace_id=self.workspaces
)
for name, role, updated, id in projects:
project: speckle_project = wm.speckle_projects.add()
project.name = name
project.role = role
project.updated = updated
project.id = id
print("Updated Projects List!")
return None
search_query: bpy.props.StringProperty( # type: ignore
name="Search or Paste a URL",
description="Search a project or paste a URL to add a project",
@@ -94,6 +148,13 @@ class SPECKLE_OT_project_selection_dialog(bpy.types.Operator):
name="Account",
description="Selected account to filter projects by",
items=get_accounts_callback,
update=update_workspaces_and_projects_list,
)
workspaces: bpy.props.EnumProperty( # type: ignore
name="Workspace",
description="Selected workspace to filter projects by",
items=get_workspaces_callback,
update=update_projects_list
)
@@ -115,31 +176,33 @@ class SPECKLE_OT_project_selection_dialog(bpy.types.Operator):
def invoke(self, context: Context, event: Event) -> set[str]:
wm = context.window_manager
if not hasattr(WindowManager, "speckle_projects"):
WindowManager.speckle_projects = bpy.props.CollectionProperty(
type=speckle_project
)
if not hasattr(WindowManager, "selected_project_id"):
WindowManager.selected_project_id = bpy.props.StringProperty(
name="Selected Project ID"
)
if not hasattr(WindowManager, "selected_project_name"):
WindowManager.selected_project_name = bpy.props.StringProperty(
name="Selected Project Name"
)
if not hasattr(WindowManager, "selected_account_id"):
WindowManager.selected_account_id = bpy.props.StringProperty()
# Clear existing accounts and projects
wm.speckle_accounts.clear()
wm.speckle_projects.clear()
wm.speckle_workspaces.clear()
# Fetch accounts
for id, user_name, server_url, user_email in get_account_enum_items():
account: speckle_account = wm.speckle_accounts.add()
account.id = id
account.user_name = user_name
account.server_url = server_url
account.user_email = user_email
selected_account_id = self.accounts
wm.selected_account_id = selected_account_id
# Fetch workspaces from server
for id, name in get_workspaces(selected_account_id):
workspace: speckle_workspace = wm.speckle_workspaces.add()
workspace.id = id
workspace.name = name
selected_workspace_id = self.workspaces
wm.selected_workspace_id = selected_workspace_id
# Fetch projects from server
projects: List[Tuple[str, str, str, str]] = get_projects_for_account(
selected_account_id
selected_account_id, workspace_id=selected_workspace_id
)
for name, role, updated, id in projects:
@@ -163,11 +226,15 @@ class SPECKLE_OT_project_selection_dialog(bpy.types.Operator):
add_account_button_icon = 'WORLD' if wm.selected_account_id == "NO_ACCOUNTS" else 'ADD'
row.operator("speckle.add_account", icon=add_account_button_icon, text=add_account_button_text)
# Workspace selection
row = layout.row()
if wm.selected_workspace_id != "NO_WORKSPACES":
row.prop(self, "workspaces", text="")
# Search field
row = layout.row(align=True)
row.prop(self, "search_query", icon="VIEWZOOM", text="")
# TODO: Add a button for adding a project by URL
# row.operator("speckle.add_project_by_url", icon='URL', text="")
row.operator("speckle.add_project_by_url", icon='LINKED', text="")
layout.template_list(
"SPECKLE_UL_projects_list",
@@ -180,44 +247,15 @@ class SPECKLE_OT_project_selection_dialog(bpy.types.Operator):
layout.separator()
class SPECKLE_OT_add_project_by_url(bpy.types.Operator):
"""
operator for adding a Speckle project by URL
"""
bl_idname = "speckle.add_project_by_url"
bl_label = "Add Project by URL"
bl_description = "Add a project from a URL"
url: bpy.props.StringProperty( # type: ignore
name="Project URL", description="Enter the Speckle project URL", default=""
)
def execute(self, context: Context) -> set[str]:
# TODO: Implement logic to add project using the URL
self.report({"INFO"}, f"Adding project from URL: {self.url}")
return {"FINISHED"}
def invoke(self, context: Context, event: Event) -> set[str]:
return context.window_manager.invoke_props_dialog(self)
def draw(self, context: Context) -> None:
layout: UILayout = self.layout
layout.prop(self, "url")
def register() -> None:
bpy.utils.register_class(speckle_project)
bpy.utils.register_class(SPECKLE_UL_projects_list)
bpy.utils.register_class(SPECKLE_OT_project_selection_dialog)
bpy.utils.register_class(SPECKLE_OT_add_project_by_url)
def unregister() -> None:
if hasattr(WindowManager, "speckle_projects"):
del WindowManager.speckle_projects
bpy.utils.unregister_class(SPECKLE_OT_add_project_by_url)
bpy.utils.unregister_class(SPECKLE_OT_project_selection_dialog)
bpy.utils.unregister_class(SPECKLE_UL_projects_list)
bpy.utils.unregister_class(speckle_project)
@@ -1,5 +1,5 @@
import bpy
from bpy.types import WindowManager, UILayout, Context, PropertyGroup, Event
from bpy.types import UILayout, Context, PropertyGroup, Event
from ..utils.version_manager import get_versions_for_model, get_latest_version
@@ -124,20 +124,6 @@ class SPECKLE_OT_version_selection_dialog(bpy.types.Operator):
return {"FINISHED"}
def invoke(self, context: Context, event: Event) -> set[str]:
if not hasattr(WindowManager, "speckle_versions"):
WindowManager.speckle_versions = bpy.props.CollectionProperty(
type=speckle_version
)
if not hasattr(WindowManager, "selected_version_id"):
WindowManager.selected_version_id = bpy.props.StringProperty(
name="Selected Version ID"
)
if not hasattr(WindowManager, "selected_version_load_option"):
WindowManager.selected_version_load_option = bpy.props.StringProperty(
name="Selected Version Load Option"
)
self.update_versions_list(context)
return context.window_manager.invoke_props_dialog(self)
+95 -15
View File
@@ -1,37 +1,85 @@
import bpy
from specklepy.api.credentials import get_local_accounts
from typing import List, Tuple, Optional
from specklepy.core.api.credentials import Account
from specklepy.api.client import SpeckleClient
from .misc import strip_non_ascii
def get_account_enum_items() -> List[Tuple[str, str, str]]:
class speckle_account(bpy.types.PropertyGroup):
id: bpy.props.StringProperty() # type: ignore
user_name: bpy.props.StringProperty() # type: ignore
server_url: bpy.props.StringProperty() # type: ignore
user_email: bpy.props.StringProperty() # type: ignore
class speckle_workspace(bpy.types.PropertyGroup):
"""
retrieves a list of Speckle accounts formatted for Blender enum properties
PropertyGroup for storing workspace information
"""
id: bpy.props.StringProperty(name="ID") # type: ignore
name: bpy.props.StringProperty() # type: ignore
def get_account_enum_items() -> List[Tuple[str, str, str, str]]:
accounts: List[Account] = get_local_accounts()
if not accounts:
return [
print("No accounts found!")
return [("NO_ACCOUNTS", "No accounts found!", "", "")]
print("Accounts added")
speckle_accounts = []
for acc in accounts:
speckle_accounts.append(
(
"NO_ACCOUNTS",
"No accounts found! Please add an account from Manager for Speckle.",
"",
acc.id,
strip_non_ascii(acc.userInfo.name),
acc.serverInfo.url,
acc.userInfo.email,
)
]
return [
(
acc.id,
f"{acc.userInfo.name} - {acc.serverInfo.url} - {acc.userInfo.email}",
"",
)
for acc in accounts
]
return speckle_accounts
def get_workspaces(account_id: str) -> List[Tuple[str, str]]:
"""
retrieves the workspaces for a given account ID
"""
account = next((acc for acc in get_local_accounts() if acc.id == account_id), None)
client = SpeckleClient(host=account.serverInfo.url)
client.authenticate_with_account(account)
workspaces_enabled = client.server.get().workspaces.workspaces_enabled
if workspaces_enabled:
workspaces = client.active_user.get_workspaces().items
workspace_list = [
(ws.id, strip_non_ascii(ws.name))
for ws in workspaces
if ws.creation_state == None or ws.creation_state.completed
]
personal_projects_text = "Personal Projects (Legacy)"
else:
workspace_list = []
personal_projects_text = "Personal Projects"
# Append Personal Projects do workspace dropdown
if client.active_user.can_create_personal_projects().authorized:
workspace_list.append(("personal", personal_projects_text))
print("Workspaces added")
return (
reorder_tuple(workspace_list, get_default_workspace_id(account_id))
if workspaces_enabled
else workspace_list
)
def get_default_account_id() -> Optional[str]:
"""
retrieves the ID of the default Speckle account
"""
return next((acc.id for acc in get_local_accounts() if acc.isDefault), "NO_ACCOUNTS")
return next(
(acc.id for acc in get_local_accounts() if acc.isDefault), "NO_ACCOUNTS"
)
def get_server_url_by_account_id(account_id: str) -> Optional[str]:
@@ -43,3 +91,35 @@ def get_server_url_by_account_id(account_id: str) -> Optional[str]:
if acc.id == account_id:
return acc.serverInfo.url
return None
def get_default_workspace_id(account_id: str) -> Optional[str]:
"""
retrieves the ID of the default workspace for a given account ID
"""
account = next((acc for acc in get_local_accounts() if acc.id == account_id), None)
client = SpeckleClient(host=account.serverInfo.url)
client.authenticate_with_account(account)
return (
client.active_user.get_active_workspace().id
if client.active_user.get_active_workspace()
else "personal"
)
def get_account_from_id(account_id: str) -> Optional[Account]:
return next((acc for acc in get_local_accounts() if acc.id == account_id), None)
def reorder_tuple(tuple_list, target_id):
for i, (id, value) in enumerate(tuple_list):
if id == target_id:
# Remove the tuple from its current position
target_tuple = tuple_list.pop(i)
# Insert it at the beginning of the list
tuple_list.insert(0, target_tuple)
return tuple_list
# If the target_id wasn't found
print(f"Tuple with ID {target_id} not found in the list")
return tuple_list
+5 -1
View File
@@ -1,5 +1,5 @@
from datetime import datetime, timezone
import re
def format_relative_time(timestamp) -> str:
"""
@@ -45,3 +45,7 @@ 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)
+2 -2
View File
@@ -3,7 +3,7 @@ from specklepy.api.credentials import get_local_accounts, Account
from specklepy.core.api.inputs.project_inputs import ProjectModelsFilter
from specklepy.core.api.models.current import Model
from typing import List, Tuple, Optional
from .misc import format_relative_time
from .misc import format_relative_time, strip_non_ascii
def get_models_for_project(
@@ -43,7 +43,7 @@ def get_models_for_project(
).items
return [
(model.name, model.id, format_relative_time(model.updated_at))
(strip_non_ascii(model.name), model.id, format_relative_time(model.updated_at))
for model in models
]
@@ -3,11 +3,10 @@ from specklepy.api.credentials import get_local_accounts
from specklepy.core.api.inputs.user_inputs import UserProjectsFilter
from typing import List, Tuple, Optional
from specklepy.core.api.credentials import Account
from .misc import format_relative_time, format_role
from .misc import format_relative_time, format_role, strip_non_ascii
def get_projects_for_account(
account_id: str, search: Optional[str] = None
account_id: str, workspace_id: str = None, search: Optional[str] = None
) -> List[Tuple[str, str, str, str]]:
"""
fetches projects for a given account from the Speckle server
@@ -24,13 +23,15 @@ def get_projects_for_account(
client = SpeckleClient(host=account.serverInfo.url)
client.authenticate_with_account(account)
filter = UserProjectsFilter(search=search) if search else None
personal_only = workspace_id == "personal"
workspace_id = None if personal_only else workspace_id
filter = UserProjectsFilter(search=search, workspaceId=workspace_id, personalOnly=personal_only)
projects = client.active_user.get_projects(limit=10, filter=filter).items
return [
(
project.name,
strip_non_ascii(project.name),
format_role(project.role),
format_relative_time(project.updated_at),
project.id,
@@ -1,7 +1,7 @@
from specklepy.api.client import SpeckleClient
from specklepy.api.credentials import get_local_accounts, Account
from typing import List, Tuple, Optional
from .misc import format_relative_time
from .misc import format_relative_time, strip_non_ascii
from specklepy.core.api.inputs.model_inputs import ModelVersionsFilter
from specklepy.core.api.models.current import Version
@@ -38,15 +38,16 @@ def get_versions_for_model(
# Get versions
versions: List[Version] = client.version.get_versions(
project_id=project_id, model_id=model_id, limit=10, filter=filter
).items
)
return [
(
version.id,
version.message or "No message",
version.message if version.message is not None else "No message",
format_relative_time(version.created_at),
)
for version in versions
if version.referenced_object is not None
]
except Exception as e:
@@ -90,7 +91,7 @@ def get_latest_version(
latest = versions[0]
return (
latest.id,
latest.message or "No message",
latest.message if latest.message is not None else "No message",
format_relative_time(latest.created_at),
)
+2 -2
View File
@@ -1,2 +1,2 @@
from ..converter.to_native import *
from ..converter.utils import *
from ..converter.to_native import * #noqa: F403
from ..converter.utils import * # noqa: F403
+274 -4
View File
@@ -11,13 +11,15 @@ from specklepy.objects.geometry import (
Polycurve,
Point,
)
from specklepy.objects.proxies import InstanceProxy
from specklepy.objects.models.units import (
get_units_from_string,
get_scale_factor_to_meters,
)
import bpy
from bpy.types import Object
from ..converter.utils import create_material_from_proxy
import mathutils
from ..converter.utils import create_material_from_proxy, find_object_by_id
# Display value property aliases to check for
DISPLAY_VALUE_PROPERTY_ALIASES = [
@@ -88,6 +90,8 @@ def generate_unique_name(speckle_object: Base) -> Tuple[str, str]:
def convert_to_native(
speckle_object: Base,
material_mapping: Optional[Dict[str, bpy.types.Material]] = None,
definition_collections: Optional[Dict[str, bpy.types.Collection]] = None,
root_collection: Optional[bpy.types.Collection] = None,
) -> Optional[Object]:
"""
converts a speckle object to blender object with material support
@@ -103,7 +107,16 @@ def convert_to_native(
material_mapping = {}
# Try direct conversion based on object type
if isinstance(speckle_object, Line):
if isinstance(speckle_object, InstanceProxy):
if definition_collections:
for def_id, coll in definition_collections.items():
if def_id == speckle_object.definitionId:
converted_object = instance_proxy_to_native(
speckle_object, coll, root_collection, scale
)
else:
print("No InstanceDefinitionProxy is found.")
elif isinstance(speckle_object, Line):
converted_object = line_to_native(
speckle_object, object_name, data_block_name, scale
)
@@ -390,10 +403,15 @@ def mesh_to_native(
mesh_obj = bpy.data.objects.new(object_name, mesh)
if len(speckle_mesh.colors) > 0:
# Add this hasattr check before accessing colors
if hasattr(speckle_mesh, "colors") and len(speckle_mesh.colors) > 0:
add_vertex_colors(mesh, speckle_mesh.colors)
if len(speckle_mesh.textureCoordinates) > 0:
# Add this hasattr check before accessing textureCoordinates
if (
hasattr(speckle_mesh, "textureCoordinates")
and len(speckle_mesh.textureCoordinates) > 0
):
add_texture_coordinates(mesh, speckle_mesh.textureCoordinates)
if material_mapping and hasattr(speckle_mesh, "applicationId"):
@@ -1122,3 +1140,255 @@ def point_to_native(
)
return point_obj
def find_instance_definitions(root_object: Base) -> Dict[str, Base]:
"""
finds all instance definitions in the root object
"""
definitions = {}
definitions_attr_names = [
"instanceDefinitionProxies",
"@instanceDefinitionProxies",
"instanceDefinitions",
"@instanceDefinitions",
]
for attr_name in definitions_attr_names:
if hasattr(root_object, attr_name):
attr_value = getattr(root_object, attr_name)
if isinstance(attr_value, list):
for definition in attr_value:
if hasattr(definition, "applicationId"):
definitions[definition.applicationId] = definition
if not definitions:
print("No instanceDefinitionProxy founded!")
return definitions
def sort_instance_components(definitions, instances):
"""
sort instance components by max depth and type (definitions first)
"""
components = []
# Add definitions with their max_depth
for def_id, definition in definitions.items():
max_depth = getattr(definition, "maxDepth", 0)
components.append((max_depth, 0, def_id, definition))
for instance in instances:
if hasattr(instance, "definitionId") and instance.definitionId in definitions:
definition = definitions[instance.definitionId]
max_depth = getattr(definition, "maxDepth", 0)
components.append((max_depth, 1, instance.id, instance))
components.sort(key=lambda x: (-x[0], x[1]))
return components
def instance_definition_proxy_to_native(
root_object: Base,
material_mapping: Dict[str, Any],
processed_definitions: Dict[str, Any] = None,
) -> Tuple[Dict[str, bpy.types.Collection], Dict[str, Any]]:
"""
converts instance definition proxies to Blender collections recursively
"""
processed_definitions = processed_definitions or {}
definition_collections = {}
converted_objects = {}
definitions = find_instance_definitions(root_object)
if not definitions:
print("No definitions found!")
return definition_collections, converted_objects
existing_definitions = bpy.data.collections.get("InstanceDefinitions")
if existing_definitions:
for coll in existing_definitions.children:
for obj in coll.objects:
bpy.data.objects.remove(obj, do_unlink=True)
bpy.data.collections.remove(coll, do_unlink=True)
bpy.data.collections.remove(existing_definitions, do_unlink=True)
sorted_components = sort_instance_components(definitions, [])
for _, _, def_id, definition in sorted_components:
collection_name = getattr(definition, "name", f"Definition_{def_id[:8]}")
if def_id in processed_definitions:
definition_collections[def_id] = processed_definitions[def_id]
continue
definition_collection = bpy.data.collections.new(collection_name)
definition_collections[def_id] = definition_collection
# Store metadata
definition_collection["speckle_id"] = def_id
definition_collection["speckle_type"] = getattr(
definition, "speckle_type", "InstanceDefinitionProxy"
)
if hasattr(definition, "maxDepth"):
definition_collection["max_depth"] = definition.maxDepth
# Process objects, including nested instances
if hasattr(definition, "objects") and isinstance(definition.objects, list):
for obj_id in definition.objects:
found_obj = find_object_by_id(root_object, obj_id)
if found_obj:
try:
# Handle nested instance proxies
if (
isinstance(found_obj, InstanceProxy)
and found_obj.definitionId in definitions
):
nested_def = definitions[found_obj.definitionId]
max_depth = getattr(nested_def, "maxDepth", 0)
if max_depth > 0: # Only process if max_depth allows
blender_obj = instance_proxy_to_native(
found_obj,
definition_collections[found_obj.definitionId],
definition_collection,
scale=1.0,
)
if blender_obj:
converted_objects[obj_id] = blender_obj
else:
blender_obj = convert_to_native(found_obj, material_mapping)
if blender_obj:
definition_collection.objects.link(blender_obj)
converted_objects[obj_id] = blender_obj
if hasattr(found_obj, "id"):
converted_objects[found_obj.id] = blender_obj
if hasattr(found_obj, "applicationId"):
converted_objects[found_obj.applicationId] = (
blender_obj
)
except Exception as e:
print(f"Error converting object: {str(e)}")
else:
print(f"Failed to find object with ID: {obj_id}")
processed_definitions[def_id] = definition_collection
return definition_collections, converted_objects
def proxy_scale(speckle_object: Base, fallback: float = 1.0) -> float:
"""
determines the correct scale factor based on object units and Blender settings
(will change it in the future)
"""
unit_settings = bpy.context.scene.unit_settings
if unit_settings.system != "METRIC":
original_system = unit_settings.system
unit_settings.system = "METRIC"
unit_settings.system = original_system
blender_scale = unit_settings.scale_length
unit_scale = 1.0
if hasattr(speckle_object, "units"):
if speckle_object.units == "cm":
unit_scale = 0.01
elif speckle_object.units == "mm":
unit_scale = 0.001
elif speckle_object.units == "m":
unit_scale = 1.0
final_scale = unit_scale / blender_scale
return final_scale
def instance_proxy_to_native(
speckle_instance: InstanceProxy,
definition_collection: bpy.types.Collection,
root_collection: bpy.types.Collection,
scale: float = 1.0,
) -> Optional[bpy.types.Object]:
"""
converts a Speckle InstanceProxy to Blender collection instance
"""
if not definition_collection:
print(f"Definition collection not found for instance {speckle_instance.id}")
return None
unit_scale = proxy_scale(speckle_instance)
# convert transformation matrix
matrix = mathutils.Matrix(
[
[
speckle_instance.transform[0],
speckle_instance.transform[1],
speckle_instance.transform[2],
speckle_instance.transform[3],
],
[
speckle_instance.transform[4],
speckle_instance.transform[5],
speckle_instance.transform[6],
speckle_instance.transform[7],
],
[
speckle_instance.transform[8],
speckle_instance.transform[9],
speckle_instance.transform[10],
speckle_instance.transform[11],
],
[
speckle_instance.transform[12],
speckle_instance.transform[13],
speckle_instance.transform[14],
speckle_instance.transform[15],
],
]
)
location, rotation, scale_vector = matrix.decompose()
location = location * unit_scale
bpy.ops.object.collection_instance_add(
collection=definition_collection.name,
align="WORLD",
location=(0, 0, 0),
rotation=(0, 0, 0),
scale=(1, 1, 1),
)
instance_obj = bpy.context.active_object
instance_obj.empty_display_size = 0
instance_name = f"Instance_{speckle_instance.id[:8]}"
instance_obj.name = instance_name
if instance_obj.name not in root_collection.objects:
for coll in instance_obj.users_collection:
coll.objects.unlink(instance_obj)
root_collection.objects.link(instance_obj)
instance_obj["speckle_id"] = speckle_instance.id
instance_obj["speckle_type"] = speckle_instance.speckle_type
instance_obj["definition_id"] = speckle_instance.definitionId
if hasattr(speckle_instance, "maxDepth"):
instance_obj["max_depth"] = speckle_instance.maxDepth
final_matrix = (
mathutils.Matrix.Translation(location)
@ rotation.to_matrix().to_4x4()
@ mathutils.Matrix.Diagonal(scale_vector).to_4x4()
)
instance_obj.matrix_world = final_matrix
return instance_obj
+95 -1
View File
@@ -1,5 +1,10 @@
from typing import Tuple, List
from typing import Tuple, List, Optional
import bpy
import mathutils
from specklepy.objects import Base
from specklepy.objects.graph_traversal.default_traversal import (
create_default_traversal_function,
)
def to_rgba(argb_int: int) -> Tuple[float, float, float, float]:
@@ -83,3 +88,92 @@ def create_material_from_proxy(
bsdf.inputs["Emission Strength"].default_value = 1.0
return material
def transform_matrix(transform: List[float]) -> mathutils.Matrix:
"""
converts a speckle transform array to a 4x4 matrix (blender needs it)
"""
if len(transform) != 16:
raise ValueError(f"Expected transform with 16 values, got {len(transform)}")
return mathutils.Matrix(
(
(transform[0], transform[4], transform[8], transform[12]),
(transform[1], transform[5], transform[9], transform[13]),
(transform[2], transform[6], transform[10], transform[14]),
(transform[3], transform[7], transform[11], transform[15]),
)
)
def find_object_by_id(root_object: Base, target_id: str) -> Optional[Base]:
"""
finds an object using traversal, checking both id and applicationId
"""
if hasattr(root_object, "__closure") and root_object.__closure:
if target_id in root_object.__closure:
if hasattr(root_object, "elements"):
for element in root_object.elements:
if hasattr(element, "id") and element.id == target_id:
return element
if (
hasattr(element, "referencedId")
and element.referencedId == target_id
):
return find_object_by_id(root_object, element.referencedId)
if hasattr(root_object, "@elements"):
for element in root_object["@elements"]:
if hasattr(element, "id") and element.id == target_id:
return element
if (
hasattr(element, "referencedId")
and element.referencedId == target_id
):
return find_object_by_id(root_object, element.referencedId)
traversal_function = create_default_traversal_function()
for traversal_item in traversal_function.traverse(root_object):
obj = traversal_item.current
if not hasattr(obj, "id"):
continue
if obj.id == target_id:
return obj
if hasattr(obj, "applicationId"):
app_id = obj.applicationId
if app_id == target_id:
return obj
def deep_search(search_obj):
if hasattr(search_obj, "id") and search_obj.id == target_id:
return search_obj
elements_attrs = ["elements", "@elements"]
for attr in elements_attrs:
if hasattr(search_obj, attr):
elements = getattr(search_obj, attr)
if elements and isinstance(elements, list):
for element in elements:
if hasattr(element, "id") and element.id == target_id:
return element
if (
hasattr(element, "referencedId")
and element.referencedId == target_id
):
ref_obj = find_object_by_id(
root_object, element.referencedId
)
if ref_obj:
return ref_obj
result = deep_search(element)
if result:
return result
return None
return deep_search(root_object)
+1 -3
View File
@@ -4,9 +4,7 @@ version = "3.0.0"
description = "Next-Gen Speckle connector for Blender!"
requires-python = ">=3.11.9, <4.0.0"
license = "Apache-2.0"
dependencies = [
"specklepy>=3.0.0a4"
]
dependencies = ["specklepy>=3.0.0a11"]
[dependency-groups]
dev = [