Compare commits
60 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c70c9823d8 | |||
| f0495ab093 | |||
| e57dacc6ef | |||
| 69dbf2f117 | |||
| e7bf842508 | |||
| 3cf3867877 | |||
| 3577f3fd6a | |||
| 242af16e81 | |||
| 20f36ffe19 | |||
| b103d0da09 | |||
| 4add9c5d72 | |||
| aecb15e549 | |||
| bff8140559 | |||
| fe98f71be2 | |||
| f2008502f3 | |||
| 2981c1270b | |||
| 3011921df9 | |||
| 79ecba213a | |||
| 06db6653fb | |||
| 86200091dc | |||
| 4b2a678605 | |||
| 1ca2c03ec0 | |||
| 528b81d294 | |||
| d0dd57a731 | |||
| 25258fd39f | |||
| a884f7a6ea | |||
| 6e0df0d4e4 | |||
| 58ff3e667e | |||
| 3f5c933ee9 | |||
| d137c7b991 | |||
| 5eec02296b | |||
| 681acd81c8 | |||
| 09dd819504 | |||
| 211296c803 | |||
| 0fd5448342 | |||
| 3002d7a31b | |||
| 8eee1ede58 | |||
| 4a340ef1ae | |||
| b6a96802a8 | |||
| f2a0ffa9ee | |||
| 24479811f7 | |||
| 2153db9704 | |||
| dbcc820304 | |||
| 830632fa1e | |||
| 2f57ca96ca | |||
| 1b9ee91880 | |||
| 8fb7519e7b | |||
| 9dce548a05 | |||
| 487253babe | |||
| f245584428 | |||
| 1ac784d290 | |||
| 314a962014 | |||
| 6b07a0fff4 | |||
| d3208de754 | |||
| 4ba19231b7 | |||
| a93ac797fc | |||
| 4569b1e623 | |||
| ae3222683e | |||
| 780184c562 | |||
| aad9246463 |
@@ -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
@@ -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.")
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
@@ -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),
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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 = [
|
||||
|
||||
Reference in New Issue
Block a user