Compare commits
21 Commits
main
...
oguzhan/dui3
| Author | SHA1 | Date | |
|---|---|---|---|
| 028099219b | |||
| a5824702ab | |||
| bb8486c94a | |||
| d32fc23e14 | |||
| 3e85a018fc | |||
| dd2e222c84 | |||
| bcdabb1226 | |||
| 8c1a5b4463 | |||
| 4811329d9e | |||
| 6c3ab4baef | |||
| a7295e7b25 | |||
| fb8fda27c5 | |||
| 32b114274c | |||
| 02a9da050f | |||
| c6ba0ff86d | |||
| 0d386aa93d | |||
| d439f65463 | |||
| 345bae9463 | |||
| cb1f9c0480 | |||
| 2be74ce617 | |||
| 56675ef88d |
+2
-1
@@ -13,4 +13,5 @@ Installers/
|
||||
modules/
|
||||
.tool-versions
|
||||
requirements.txt
|
||||
SEMVER
|
||||
SEMVER
|
||||
dui3/
|
||||
+78
-90
@@ -1,113 +1,101 @@
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but
|
||||
# WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTIBILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
# General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy
|
||||
from bpy_speckle.installer import ensure_dependencies
|
||||
from .ui import icons
|
||||
import json
|
||||
|
||||
ensure_dependencies(f"Blender {bpy.app.version[0]}.{bpy.app.version[1]}")
|
||||
# UI
|
||||
from .ui.main_panel import SPECKLE_PT_main_panel
|
||||
from .ui.project_selection_dialog import SPECKLE_OT_project_selection_dialog, speckle_project, SPECKLE_UL_projects_list, SPECKLE_OT_add_project_by_url
|
||||
from .ui.model_selection_dialog import SPECKLE_OT_model_selection_dialog, speckle_model, SPECKLE_UL_models_list
|
||||
from .ui.version_selection_dialog import SPECKLE_OT_version_selection_dialog, speckle_version, SPECKLE_UL_versions_list
|
||||
from .ui.selection_filter_dialog import SPECKLE_OT_selection_filter_dialog
|
||||
from .ui.model_card import speckle_model_card
|
||||
# Operators
|
||||
from .operators.publish import SPECKLE_OT_publish
|
||||
from .operators.load import SPECKLE_OT_load
|
||||
from .operators.model_card_settings import SPECKLE_OT_model_card_settings, SPECKLE_OT_view_in_browser, SPECKLE_OT_view_model_versions
|
||||
# Bindings
|
||||
from .bindings.account_binding import AccountBinding
|
||||
|
||||
from specklepy.logging import metrics
|
||||
|
||||
from bpy_speckle.ui import *
|
||||
from bpy_speckle.properties import *
|
||||
from bpy_speckle.operators import *
|
||||
from bpy_speckle.callbacks import *
|
||||
from bpy.app.handlers import persistent
|
||||
def save_model_cards(scene):
|
||||
model_cards_data = [card.to_dict() for card in scene.speckle_model_cards]
|
||||
scene["speckle_model_cards_data"] = json.dumps(model_cards_data)
|
||||
|
||||
bl_info = {
|
||||
"name": "SpeckleBlender 2.0",
|
||||
"author": "Speckle Systems",
|
||||
"version": (0, 2, 0),
|
||||
"blender": (2, 92, 0),
|
||||
"location": "3d viewport toolbar (N), under the Speckle tab.",
|
||||
"description": "The Speckle Connector using specklepy 2.0!",
|
||||
"warning": "This add-on is WIP and should be used with caution",
|
||||
"wiki_url": "https://github.com/specklesystems/speckle-blender",
|
||||
"category": "Scene",
|
||||
}
|
||||
def load_model_cards(scene):
|
||||
if "speckle_model_cards_data" in scene:
|
||||
model_cards_data = json.loads(scene["speckle_model_cards_data"])
|
||||
scene.speckle_model_cards.clear()
|
||||
for card_data in model_cards_data:
|
||||
card = speckle_model_card.from_dict(card_data)
|
||||
scene.speckle_model_cards.add().update(card)
|
||||
|
||||
|
||||
|
||||
"""
|
||||
Import SpeckleBlender classes
|
||||
"""
|
||||
# Classes to load
|
||||
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_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,
|
||||
AccountBinding)
|
||||
|
||||
"""
|
||||
Add load handler to initialize Speckle when
|
||||
loading a Blender file
|
||||
"""
|
||||
|
||||
@persistent
|
||||
@bpy.app.handlers.persistent
|
||||
def load_handler(dummy):
|
||||
pass
|
||||
# Calling users_load is an expensive operation, one that force users to wait a good 10s every time blender loads.
|
||||
# Until we can do this non-blocking, we will make the user hit the refresh button each time.
|
||||
#bpy.ops.speckle.users_load()
|
||||
|
||||
# Instead, we shall just reset the user selection to an uninitiailised state
|
||||
bpy.ops.speckle.users_reset()
|
||||
|
||||
"""
|
||||
Permanent handle on callbacks
|
||||
"""
|
||||
|
||||
callbacks = {}
|
||||
|
||||
"""
|
||||
Add Speckle classes for registering
|
||||
"""
|
||||
|
||||
speckle_classes = []
|
||||
speckle_classes.extend(operator_classes)
|
||||
speckle_classes.extend(property_classes)
|
||||
speckle_classes.extend(ui_classes)
|
||||
load_model_cards(bpy.context.scene)
|
||||
|
||||
@bpy.app.handlers.persistent
|
||||
def save_handler(dummy):
|
||||
save_model_cards(bpy.context.scene)
|
||||
|
||||
# Register and Unregister
|
||||
def register():
|
||||
from bpy.utils import register_class
|
||||
icons.load_icons()
|
||||
|
||||
for cls in speckle_classes:
|
||||
register_class(cls)
|
||||
|
||||
metrics.set_host_app("blender", f"blender {bpy.app.version_string}")
|
||||
|
||||
"""
|
||||
Register all new properties
|
||||
"""
|
||||
|
||||
bpy.types.Scene.speckle = bpy.props.PointerProperty(type=SpeckleSceneSettings)
|
||||
bpy.types.Collection.speckle = bpy.props.PointerProperty(
|
||||
type=SpeckleCollectionSettings
|
||||
)
|
||||
bpy.types.Object.speckle = bpy.props.PointerProperty(type=SpeckleObjectSettings)
|
||||
|
||||
"""
|
||||
Add callbacks
|
||||
"""
|
||||
|
||||
# Callback for displaying the current user account on top of the 3d view
|
||||
# callbacks['view3d_status'] = ((
|
||||
# bpy.types.SpaceView3D.draw_handler_remove, # Function pointer for removal
|
||||
# bpy.types.SpaceView3D.draw_handler_add(draw_speckle_info, (None, None), 'WINDOW', 'POST_PIXEL'), # Add handler
|
||||
# 'WINDOW' # Callback space for removal
|
||||
# ))
|
||||
for cls in classes:
|
||||
bpy.utils.register_class(cls)
|
||||
bpy.types.Scene.speckle_projects = bpy.props.CollectionProperty(type=speckle_project)
|
||||
bpy.types.Scene.speckle_models = bpy.props.CollectionProperty(type=speckle_model)
|
||||
bpy.types.Scene.speckle_versions = bpy.props.CollectionProperty(type=speckle_version)
|
||||
bpy.types.Scene.speckle_ui_mode = bpy.props.StringProperty(name="UI Mode", default="NONE")
|
||||
bpy.types.Scene.speckle_model_cards = bpy.props.CollectionProperty(type=speckle_model_card)
|
||||
bpy.types.Scene.speckle_model_card_index = bpy.props.IntProperty(name="Model Card Index", default=0)
|
||||
bpy.types.Scene.speckle_mouse_position = bpy.props.IntVectorProperty(size=2)
|
||||
|
||||
bpy.app.handlers.load_post.append(load_handler)
|
||||
|
||||
bpy.app.handlers.save_post.append(save_handler)
|
||||
|
||||
def unregister():
|
||||
icons.unload_icons()
|
||||
for cls in classes:
|
||||
bpy.utils.unregister_class(cls)
|
||||
del bpy.types.Scene.speckle_projects
|
||||
del bpy.types.Scene.speckle_models
|
||||
del bpy.types.Scene.speckle_versions
|
||||
del bpy.types.Scene.speckle_ui_mode
|
||||
del bpy.types.Scene.speckle_model_cards
|
||||
del bpy.types.Scene.speckle_model_card_index
|
||||
del bpy.types.Scene.speckle_mouse_position
|
||||
|
||||
bpy.app.handlers.load_post.remove(load_handler)
|
||||
bpy.app.handlers.save_post.remove(save_handler)
|
||||
|
||||
"""
|
||||
Remove callbacks
|
||||
"""
|
||||
|
||||
for cb in callbacks.values():
|
||||
cb[0](cb[1], cb[2])
|
||||
|
||||
from bpy.utils import unregister_class
|
||||
|
||||
for cls in reversed(speckle_classes):
|
||||
unregister_class(cls)
|
||||
|
||||
|
||||
# Run the register function when the script is executed
|
||||
if __name__ == "__main__":
|
||||
register()
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
|
||||
|
||||
class AccountBinding:
|
||||
def get_accounts(self): #-> Account[]:
|
||||
# call sqlite to get accounts from Accounts.db, return
|
||||
return
|
||||
@@ -0,0 +1,11 @@
|
||||
|
||||
|
||||
class SendBinding:
|
||||
|
||||
def send(self, model_card_id: str):
|
||||
# TODO:
|
||||
# 1- find model card from context(or whatever state)
|
||||
# 2- find objects to send
|
||||
# 3- call converter bla bla
|
||||
# .....
|
||||
return
|
||||
@@ -1,120 +0,0 @@
|
||||
from typing import Dict, Optional, Tuple, Union
|
||||
import bpy
|
||||
from bpy.types import Object, Collection, ID
|
||||
from specklepy.objects.base import Base
|
||||
from bpy_speckle.functions import _report
|
||||
from specklepy.objects.graph_traversal.commit_object_builder import CommitObjectBuilder, ROOT
|
||||
from specklepy.objects import Base
|
||||
from specklepy.objects.other import Collection as SCollection
|
||||
from attrs import define
|
||||
|
||||
ELEMENTS = "elements"
|
||||
|
||||
def _id(native_object: ID) -> str:
|
||||
#NOTE: to avoid naming collisions, we prefix collections and objects differently
|
||||
return f"{type(native_object).__name__}:{native_object.name_full}"
|
||||
|
||||
def _try_id(native_object: Optional[Union[Collection, Object]]) -> Optional[str]:
|
||||
return _id(native_object) if native_object else None
|
||||
|
||||
def convert_collection_to_speckle(col: Collection) -> SCollection:
|
||||
converted_collection = SCollection(name = col.name_full, collectionType = "Blender Collection", elements = [])
|
||||
converted_collection.applicationId = _id(col)
|
||||
|
||||
color_tag = col.color_tag
|
||||
if color_tag and color_tag != "NONE":
|
||||
converted_collection["colorTag"] = col.color_tag
|
||||
|
||||
return converted_collection
|
||||
|
||||
@define(slots=True)
|
||||
class BlenderCommitObjectBuilder(CommitObjectBuilder[Object]):
|
||||
|
||||
_collections: Dict[str, SCollection]
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self._collections = {}
|
||||
|
||||
def include_object(self, conversion_result: Base, native_object: Object) -> None:
|
||||
|
||||
# Set the Child -> Parent relationships
|
||||
parent = native_object.parent
|
||||
|
||||
parent_collections = native_object.users_collection
|
||||
parent_collection = parent_collections[0] if len(parent_collections) > 0 else None #NOTE: we don't support objects appearing in more than one collection, for now, we will just take the zeroth one
|
||||
|
||||
app_id = _id(native_object)
|
||||
conversion_result.applicationId = app_id
|
||||
self.converted[app_id] = conversion_result
|
||||
|
||||
# in order or priority, direct parent, direct parent collection, root
|
||||
self.set_relationship(app_id, (_try_id(parent), ELEMENTS), (_try_id(parent_collection), ELEMENTS), (ROOT, ELEMENTS))
|
||||
# if parent_collection:
|
||||
# self._include_collection(parent_collection)
|
||||
|
||||
def ensure_collection(self, col: Collection) -> SCollection:
|
||||
id = _id(col)
|
||||
if id in self._collections:
|
||||
return self._collections[id] # collection already converted!
|
||||
|
||||
# Set the Parent -> Children relationships
|
||||
for c in col.children:
|
||||
#NOTE: There's no falling back to the grandparent, if the direct parent collection wasn't converted, then we we fallback to the root
|
||||
self.set_relationship(_id(c), (id, ELEMENTS), (ROOT, ELEMENTS))
|
||||
|
||||
# Set Child -> Parent relationship
|
||||
# parent = self.find_collection_parent(col)
|
||||
# self.set_relationship(id, (_try_builder_id(parent), ELEMENTS), (ROOT, ELEMENTS))
|
||||
|
||||
converted_collection = convert_collection_to_speckle(col)
|
||||
self.converted[id] = converted_collection
|
||||
self._collections[id] = converted_collection
|
||||
|
||||
return converted_collection
|
||||
|
||||
def build_commit_object(self, root_commit_object: Base) -> None:
|
||||
assert(root_commit_object.applicationId in self.converted)
|
||||
|
||||
# Create all collections
|
||||
root_col = self.ensure_collection(bpy.context.scene.collection)
|
||||
root_col.collectionType = "Scene Collection"
|
||||
for col in bpy.context.scene.collection.children_recursive:
|
||||
self.ensure_collection(col)
|
||||
|
||||
objects_to_build = set(self.converted.values())
|
||||
objects_to_build.remove(root_commit_object)
|
||||
|
||||
self.apply_relationships(objects_to_build, root_commit_object)
|
||||
|
||||
assert(isinstance(root_commit_object, SCollection))
|
||||
# Kill unused collections
|
||||
|
||||
def should_remove_unuseful_collection(col: SCollection) -> bool: #TODO: this maybe could be optimised
|
||||
elements = col.elements
|
||||
if not elements: return True
|
||||
|
||||
should_remove_this_col = True
|
||||
|
||||
i = 0
|
||||
while i < len(elements):
|
||||
c = elements[i]
|
||||
if not isinstance(c, SCollection):
|
||||
# col has objects (c)
|
||||
should_remove_this_col = False
|
||||
i += 1
|
||||
continue
|
||||
|
||||
if should_remove_unuseful_collection(c):
|
||||
# c is not useful, kill it
|
||||
del elements[i]
|
||||
else:
|
||||
# col has a child (c) with objects
|
||||
should_remove_this_col = False
|
||||
i += 1
|
||||
continue
|
||||
|
||||
return should_remove_this_col
|
||||
|
||||
if should_remove_unuseful_collection(root_commit_object):
|
||||
_report("WARNING: Only empty collections have been converted!") #TODO: consider raising exception here, to halt the send operation
|
||||
@@ -0,0 +1,75 @@
|
||||
schema_version = "1.0.0"
|
||||
|
||||
# Example of manifest file for a Blender extension
|
||||
# Change the values according to your extension
|
||||
id = "speckle_blender_addon"
|
||||
version = "3.0.0"
|
||||
name = "Speckle for Blender BETA"
|
||||
tagline = "Speckle connector for Blender"
|
||||
maintainer = "Speckle Systems"
|
||||
# Supported types: "add-on", "theme"
|
||||
type = "add-on"
|
||||
|
||||
# Optional link to documentation, support, source files, etc
|
||||
website = "https://speckle.guide/user/blender.html"
|
||||
|
||||
# Optional list defined by Blender and server, see:
|
||||
# https://docs.blender.org/manual/en/dev/advanced/extensions/tags.html
|
||||
tags = ["Scene"]
|
||||
|
||||
blender_version_min = "4.2.0"
|
||||
# # Optional: Blender version that the extension does not support, earlier versions are supported.
|
||||
# # This can be omitted and defined later on the extensions platform if an issue is found.
|
||||
# blender_version_max = "5.1.0"
|
||||
|
||||
# License conforming to https://spdx.org/licenses/ (use "SPDX: prefix)
|
||||
# https://docs.blender.org/manual/en/dev/advanced/extensions/licenses.html
|
||||
license = [
|
||||
"SPDX:Apache-2.0",
|
||||
]
|
||||
# Optional: required by some licenses.
|
||||
# copyright = [
|
||||
# "2002-2024 Developer Name",
|
||||
# "1998 Company Name",
|
||||
# ]
|
||||
|
||||
# Optional list of supported platforms. If omitted, the extension will be available in all operating systems.
|
||||
# platforms = ["windows-x64", "macos-arm64", "linux-x64"]
|
||||
# Other supported platforms: "windows-arm64", "macos-x64"
|
||||
|
||||
# Optional: bundle 3rd party Python modules.
|
||||
# https://docs.blender.org/manual/en/dev/advanced/extensions/python_wheels.html
|
||||
# wheels = [
|
||||
# "./wheels/hexdump-3.3-py3-none-any.whl",
|
||||
# "./wheels/jsmin-3.0.1-py3-none-any.whl",
|
||||
# ]
|
||||
|
||||
# Optional: add-ons can list which resources they will require:
|
||||
# * files (for access of any filesystem operations)
|
||||
# * network (for internet access)
|
||||
# * clipboard (to read and/or write the system clipboard)
|
||||
# * camera (to capture photos and videos)
|
||||
# * microphone (to capture audio)
|
||||
# permissions = ["network"]
|
||||
|
||||
#
|
||||
# If using network, remember to also check `bpy.app.online_access`
|
||||
# https://docs.blender.org/manual/en/dev/advanced/extensions/addons.html#internet-access
|
||||
#
|
||||
# For each permission it is important to also specify the reason why it is required.
|
||||
# Keep this a single short sentence without a period (.) at the end.
|
||||
# For longer explanations use the documentation or detail page.
|
||||
#
|
||||
# [permissions]
|
||||
# network = "Need to sync motion-capture data to server"
|
||||
# files = "Import/export FBX from/to disk"
|
||||
# clipboard = "Copy and paste bone transforms"
|
||||
|
||||
# Optional: build settings.
|
||||
# https://docs.blender.org/manual/en/dev/advanced/extensions/command_line_arguments.html#command-line-args-extension-build
|
||||
# [build]
|
||||
# paths_exclude_pattern = [
|
||||
# "__pycache__/",
|
||||
# "/.git/",
|
||||
# "/*.zip",
|
||||
# ]
|
||||
@@ -1,2 +0,0 @@
|
||||
from .on_mesh_edit import scb_on_mesh_edit
|
||||
from .draw_speckle_info import draw_speckle_info
|
||||
@@ -1,23 +0,0 @@
|
||||
"""
|
||||
Drawing callback to display active Speckle user
|
||||
"""
|
||||
|
||||
import blf
|
||||
import bpy
|
||||
|
||||
|
||||
def draw_speckle_info(self, context):
|
||||
"""
|
||||
Draw active user info on the 3d viewport
|
||||
"""
|
||||
scn = bpy.context.scene
|
||||
if len(scn.speckle.users) > 0:
|
||||
user = scn.speckle.users[int(scn.speckle.active_user)]
|
||||
dpi = bpy.context.preferences.system.dpi
|
||||
|
||||
blf.position(0, 100, 50, 0)
|
||||
blf.size(0, 20, dpi)
|
||||
blf.draw(0, "Active Speckle user: {} ({})".format(user.name, user.email))
|
||||
blf.position(0, 100, 20, 0)
|
||||
blf.size(0, 16, dpi)
|
||||
blf.draw(0, "Server: {}".format(user.server))
|
||||
@@ -1,13 +0,0 @@
|
||||
import bpy
|
||||
from bpy.app.handlers import persistent
|
||||
|
||||
|
||||
@persistent
|
||||
def scb_on_mesh_edit(context):
|
||||
"""
|
||||
DEPRECATED
|
||||
Do something whenever a mesh is updated
|
||||
"""
|
||||
edit_obj = bpy.context.edit_object
|
||||
if edit_obj is not None and edit_obj.is_updated_data is True:
|
||||
print("Mesh edited: {}".format(edit_obj))
|
||||
@@ -1,7 +0,0 @@
|
||||
"""
|
||||
Permanent handle on all user clients
|
||||
"""
|
||||
from specklepy.core.api.client import SpeckleClient
|
||||
|
||||
|
||||
speckle_clients: list[SpeckleClient] = []
|
||||
@@ -1,22 +0,0 @@
|
||||
IGNORED_PROPERTY_KEYS = {
|
||||
"id",
|
||||
"elements",
|
||||
"displayMesh",
|
||||
"displayValue",
|
||||
"speckle_type",
|
||||
"parameters",
|
||||
"faces",
|
||||
"colors",
|
||||
"vertices",
|
||||
"renderMaterial",
|
||||
"textureCoordinates",
|
||||
"totalChildrenCount"
|
||||
}
|
||||
|
||||
DISPLAY_VALUE_PROPERTY_ALIASES = {"displayValue", "@displayValue"}
|
||||
ELEMENTS_PROPERTY_ALIASES = {"elements", "@elements"}
|
||||
|
||||
OBJECT_NAME_MAX_LENGTH = 62
|
||||
SPECKLE_ID_LENGTH = 32
|
||||
OBJECT_NAME_SPECKLE_SEPARATOR = " -- "
|
||||
OBJECT_NAME_NUMERAL_SEPARATOR = '.'
|
||||
@@ -1,817 +0,0 @@
|
||||
import math
|
||||
from typing import Any, Dict, Iterable, List, Optional, Sequence, Union, Collection, cast
|
||||
from bpy_speckle.convert.constants import DISPLAY_VALUE_PROPERTY_ALIASES, ELEMENTS_PROPERTY_ALIASES, OBJECT_NAME_MAX_LENGTH, OBJECT_NAME_NUMERAL_SEPARATOR, OBJECT_NAME_SPECKLE_SEPARATOR, SPECKLE_ID_LENGTH
|
||||
from bpy_speckle.functions import get_default_traversal_func, get_scale_length, _report
|
||||
from bpy_speckle.convert.util import ConversionSkippedException
|
||||
from mathutils import (
|
||||
Matrix as MMatrix,
|
||||
Vector as MVector,
|
||||
Quaternion as MQuaternion,
|
||||
)
|
||||
import bpy, bmesh
|
||||
from specklepy.objects.other import (
|
||||
Collection as SCollection,
|
||||
Instance,
|
||||
Transform,
|
||||
BlockDefinition,
|
||||
)
|
||||
from specklepy.objects.base import Base
|
||||
from specklepy.objects.geometry import Mesh, Line, Polyline, Curve, Arc, Polycurve, Ellipse, Circle, Plane
|
||||
from bpy.types import Object, Collection as BCollection
|
||||
|
||||
from .util import (
|
||||
add_to_hierarchy,
|
||||
get_render_material,
|
||||
get_vertex_color_material,
|
||||
render_material_to_native,
|
||||
add_custom_properties,
|
||||
add_vertices,
|
||||
add_faces,
|
||||
add_colors,
|
||||
add_uv_coords,
|
||||
)
|
||||
|
||||
SUPPORTED_CURVES = (Line, Polyline, Curve, Arc, Polycurve, Ellipse, Circle)
|
||||
CAN_CONVERT_TO_NATIVE = (
|
||||
|
||||
Mesh,
|
||||
*SUPPORTED_CURVES,
|
||||
Instance,
|
||||
)
|
||||
|
||||
|
||||
def _has_native_conversion(speckle_object: Base) -> bool:
|
||||
return any(isinstance(speckle_object, t) for t in CAN_CONVERT_TO_NATIVE) or "View" in speckle_object.speckle_type #hack
|
||||
|
||||
def _has_fallback_conversion(speckle_object: Base) -> bool:
|
||||
return any(getattr(speckle_object, alias, None) for alias in DISPLAY_VALUE_PROPERTY_ALIASES)
|
||||
|
||||
def can_convert_to_native(speckle_object: Base) -> bool:
|
||||
|
||||
if(_has_native_conversion(speckle_object) or _has_fallback_conversion(speckle_object)):
|
||||
return True
|
||||
return False
|
||||
|
||||
convert_instances_as: str = "" #HACK: This is hacky, we need a better way to pass settings down to the converter
|
||||
def set_convert_instances_as(value: str):
|
||||
global convert_instances_as
|
||||
convert_instances_as = value
|
||||
|
||||
#TODO: Check usages handle exceptions
|
||||
|
||||
def convert_to_native(speckle_object: Base) -> Object:
|
||||
|
||||
speckle_type = type(speckle_object)
|
||||
|
||||
object_name = _generate_object_name(speckle_object)
|
||||
scale = get_scale_factor(speckle_object)
|
||||
|
||||
converted: Union[bpy.types.ID, bpy.types.Object, None] = None
|
||||
children: list[Object] = []
|
||||
|
||||
# convert elements/breps
|
||||
if not _has_native_conversion(speckle_object):
|
||||
(converted, children) = display_value_to_native(speckle_object, object_name, scale)
|
||||
if not converted and not children:
|
||||
raise Exception(f"Zero geometry converted from displayValues for {speckle_object}")
|
||||
|
||||
# convert supported geometry
|
||||
elif isinstance(speckle_object, Mesh):
|
||||
converted = mesh_to_native(speckle_object, object_name, scale)
|
||||
elif speckle_type in SUPPORTED_CURVES:
|
||||
converted = icurve_to_native(speckle_object, object_name, scale)
|
||||
elif "View" in speckle_object.speckle_type:
|
||||
return view_to_native(speckle_object, object_name, scale)
|
||||
elif isinstance(speckle_object, Instance):
|
||||
if convert_instances_as == "linked_duplicates":
|
||||
converted = instance_to_native_object(speckle_object, scale)
|
||||
elif convert_instances_as == "collection_instance":
|
||||
converted = instance_to_native_collection_instance(speckle_object, scale)
|
||||
else:
|
||||
_report(f"convert_instances_as = '{convert_instances_as}' is not implemented, Instances will be converted as collection instances!")
|
||||
converted = instance_to_native_collection_instance(speckle_object, scale)
|
||||
else:
|
||||
raise Exception(f"Unsupported type {speckle_type}")
|
||||
|
||||
|
||||
if not isinstance(converted, Object):
|
||||
converted = create_new_object(converted, object_name)
|
||||
|
||||
converted.speckle.object_id = str(speckle_object.id) # type: ignore
|
||||
converted.speckle.enabled = True # type: ignore
|
||||
add_custom_properties(speckle_object, converted)
|
||||
|
||||
for c in children:
|
||||
c.parent = converted
|
||||
|
||||
return converted
|
||||
|
||||
|
||||
|
||||
def display_value_to_native(speckle_object: Base, name: str, scale: float) -> tuple[Optional[bpy.types.Mesh], list[bpy.types.Object]]:
|
||||
return _members_to_native(speckle_object, name, scale, DISPLAY_VALUE_PROPERTY_ALIASES, True)
|
||||
|
||||
def elements_to_native(speckle_object: Base, name: str, scale: float) -> list[bpy.types.Object]:
|
||||
(_, elements) = _members_to_native(speckle_object, name, scale, ELEMENTS_PROPERTY_ALIASES, False)
|
||||
return elements
|
||||
|
||||
def _members_to_native(speckle_object: Base, name: str, scale: float, members: Iterable[str], combineMeshes: bool) -> tuple[Optional[bpy.types.Mesh], list[bpy.types.Object]]:
|
||||
"""
|
||||
Converts a given speckle_object by converting specified members
|
||||
|
||||
if combineMeshes == True
|
||||
Converts mesh members as one mesh
|
||||
Converts non-mesh members as child Objects
|
||||
if combineMeshes == False
|
||||
Converts all members as child objects (first item of the returned tuple will be None)
|
||||
:returns: converted mesh, and any other converted child objects (may happen if members contained non-meshes)
|
||||
"""
|
||||
meshes: list[Mesh] = []
|
||||
others: list[Base] = []
|
||||
|
||||
for alias in members:
|
||||
display = getattr(speckle_object, alias, None)
|
||||
|
||||
count = 0
|
||||
MAX_DEPTH = 255 # some large value, to prevent infinite recursion
|
||||
def separate(value: Any) -> bool:
|
||||
nonlocal meshes, others, count, MAX_DEPTH
|
||||
|
||||
if combineMeshes and isinstance(value, Mesh):
|
||||
meshes.append(value)
|
||||
elif isinstance(value, Base):
|
||||
others.append(value)
|
||||
elif isinstance(value, list):
|
||||
count += 1
|
||||
if(count > MAX_DEPTH):
|
||||
return True
|
||||
for x in value:
|
||||
separate(x)
|
||||
|
||||
return False
|
||||
|
||||
did_halt = separate(display)
|
||||
|
||||
if did_halt:
|
||||
_report(f"Traversal of {speckle_object.speckle_type} {speckle_object.id} halted after traversal depth exceeds MAX_DEPTH={MAX_DEPTH}. Are there circular references object structure?")
|
||||
|
||||
|
||||
children: list[Object] = []
|
||||
mesh = None
|
||||
|
||||
if meshes:
|
||||
mesh = meshes_to_native(speckle_object, meshes, name, scale) #TODO: reconsider passing scale around...
|
||||
|
||||
for item in others:
|
||||
try:
|
||||
blender_object = convert_to_native(item)
|
||||
children.append(blender_object)
|
||||
except Exception as ex:
|
||||
_report(f"Failed to convert display value {item}: {ex}")
|
||||
|
||||
return (mesh, children)
|
||||
|
||||
|
||||
|
||||
def view_to_native(speckle_view, name: str, scale: float) -> bpy.types.Object:
|
||||
native_cam: bpy.types.Camera
|
||||
if name in bpy.data.cameras.keys():
|
||||
native_cam = bpy.data.cameras[name]
|
||||
else:
|
||||
native_cam = bpy.data.cameras.new(name=name)
|
||||
native_cam.lens = 18 # 90° horizontal fov
|
||||
|
||||
if not hasattr(speckle_view, "origin"):
|
||||
raise ConversionSkippedException("2D views not supported")
|
||||
|
||||
cam_obj = create_new_object(native_cam, name)
|
||||
|
||||
scale_factor = get_scale_factor(speckle_view, scale)
|
||||
tx = (speckle_view.origin.x * scale_factor)
|
||||
ty = (speckle_view.origin.y * scale_factor)
|
||||
tz = (speckle_view.origin.z * scale_factor)
|
||||
|
||||
forward = MVector((speckle_view.forwardDirection.x, speckle_view.forwardDirection.y, speckle_view.forwardDirection.z))
|
||||
up = MVector((speckle_view.upDirection.x, speckle_view.upDirection.y, speckle_view.upDirection.z))
|
||||
right = forward.cross(up).normalized()
|
||||
|
||||
cam_obj.matrix_world = MMatrix((
|
||||
(right.x, up.x, -forward.x, tx),
|
||||
(right.y, up.y, -forward.y, ty),
|
||||
(right.z, up.z, -forward.z, tz),
|
||||
(0, 0, 0, 1 )
|
||||
))
|
||||
return cam_obj
|
||||
|
||||
def mesh_to_native(speckle_mesh: Mesh, name: str, scale: float) -> bpy.types.Mesh:
|
||||
return meshes_to_native(speckle_mesh, [speckle_mesh], name, scale)
|
||||
|
||||
|
||||
|
||||
def meshes_to_native(element: Base, meshes: Collection[Mesh], name: str, scale: float) -> bpy.types.Mesh:
|
||||
if name in bpy.data.meshes.keys():
|
||||
return bpy.data.meshes[name]
|
||||
blender_mesh = bpy.data.meshes.new(name=name)
|
||||
|
||||
fallback_material = get_render_material(element)
|
||||
|
||||
bm = bmesh.new()
|
||||
|
||||
# First pass, add vertex data
|
||||
for mesh in meshes:
|
||||
scale = get_scale_factor(mesh, scale)
|
||||
add_vertices(mesh, bm, scale)
|
||||
|
||||
bm.verts.ensure_lookup_table()
|
||||
|
||||
# Second pass, add face data
|
||||
offset = 0
|
||||
for i, mesh in enumerate(meshes):
|
||||
if not mesh.vertices: continue
|
||||
|
||||
add_faces(mesh, bm, offset, i)
|
||||
|
||||
try:
|
||||
render_material = get_render_material(mesh) or fallback_material
|
||||
if render_material is not None:
|
||||
native_material = render_material_to_native(render_material)
|
||||
blender_mesh.materials.append(native_material)
|
||||
elif mesh.colors:
|
||||
native_material = get_vertex_color_material()
|
||||
blender_mesh.materials.append(native_material)
|
||||
except Exception as ex:
|
||||
_report(f"Failed converting render material for {name}: {ex}")
|
||||
|
||||
offset += len(mesh.vertices) // 3
|
||||
|
||||
bm.faces.ensure_lookup_table()
|
||||
bm.verts.index_update()
|
||||
|
||||
# Third pass, add vertex instance data
|
||||
for mesh in meshes:
|
||||
try:
|
||||
add_colors(mesh, bm)
|
||||
except Exception as ex:
|
||||
_report(f"Skipping converting vertex colors for {name}: {ex}")
|
||||
|
||||
try:
|
||||
add_uv_coords(mesh, bm)
|
||||
except Exception as ex:
|
||||
_report(f"Skipping converting uv coordinates for {name}: {ex}")
|
||||
|
||||
bm.to_mesh(blender_mesh)
|
||||
bm.free()
|
||||
|
||||
return blender_mesh
|
||||
|
||||
|
||||
"""
|
||||
Curves
|
||||
"""
|
||||
|
||||
def line_to_native(speckle_curve: Line, blender_curve: bpy.types.Curve, scale: float) -> List[bpy.types.Spline]:
|
||||
if not speckle_curve.end: return []
|
||||
|
||||
line = blender_curve.splines.new("POLY")
|
||||
line.points.add(1)
|
||||
|
||||
line.points[0].co = (
|
||||
float(speckle_curve.start.x) * scale,
|
||||
float(speckle_curve.start.y) * scale,
|
||||
float(speckle_curve.start.z) * scale,
|
||||
1,
|
||||
)
|
||||
|
||||
line.points[1].co = (
|
||||
float(speckle_curve.end.x) * scale,
|
||||
float(speckle_curve.end.y) * scale,
|
||||
float(speckle_curve.end.z) * scale,
|
||||
1,
|
||||
)
|
||||
|
||||
return [line]
|
||||
|
||||
|
||||
def polyline_to_native(scurve: Polyline, bcurve: bpy.types.Curve, scale: float) -> List[bpy.types.Spline]:
|
||||
if not (value := scurve.value): return []
|
||||
N = len(value) // 3
|
||||
|
||||
polyline = bcurve.splines.new("POLY")
|
||||
|
||||
if hasattr(scurve, "closed"):
|
||||
polyline.use_cyclic_u = scurve.closed or False
|
||||
|
||||
polyline.points.add(N - 1)
|
||||
for i in range(N):
|
||||
polyline.points[i].co = (
|
||||
float(value[i * 3]) * scale,
|
||||
float(value[i * 3 + 1]) * scale,
|
||||
float(value[i * 3 + 2]) * scale,
|
||||
1,
|
||||
)
|
||||
|
||||
return [polyline]
|
||||
|
||||
|
||||
|
||||
def nurbs_to_native(scurve: Curve, bcurve: bpy.types.Curve, scale: float) -> List[bpy.types.Spline]:
|
||||
if not (points := scurve.points): return []
|
||||
if not scurve.degree: raise Exception("curve is missing degree")
|
||||
if not scurve.weights: raise Exception("curve is missing weights")
|
||||
|
||||
# Closed curves from rhino will have n + degree points. We ignore the extras
|
||||
num_points = len(points) // 3 - scurve.degree if (scurve.closed) else (
|
||||
len(points) // 3)
|
||||
|
||||
nurbs = bcurve.splines.new("NURBS")
|
||||
nurbs.use_cyclic_u = scurve.closed or False
|
||||
nurbs.use_endpoint_u = not scurve.periodic
|
||||
|
||||
nurbs.points.add(num_points - 1)
|
||||
use_weights = len(scurve.weights) >= num_points
|
||||
for i in range(num_points):
|
||||
nurbs.points[i].co = (
|
||||
float(points[i * 3]) * scale,
|
||||
float(points[i * 3 + 1]) * scale,
|
||||
float(points[i * 3 + 2]) * scale,
|
||||
1,
|
||||
)
|
||||
|
||||
nurbs.points[i].weight = scurve.weights[i] if use_weights else 1
|
||||
|
||||
nurbs.order_u = scurve.degree + 1
|
||||
|
||||
return [nurbs]
|
||||
|
||||
|
||||
def arc_to_native(rcurve: Arc, bcurve: bpy.types.Curve, scale: float) -> Optional[bpy.types.Spline]:
|
||||
# TODO: improve Blender representation of arc - check autocad test stream
|
||||
if not rcurve.radius: raise Exception("curve is missing radius")
|
||||
if not rcurve.startAngle: raise Exception("curve is missing startAngle")
|
||||
if not rcurve.endAngle: raise Exception("curve is missing endAngle")
|
||||
|
||||
plane = rcurve.plane
|
||||
if not plane:
|
||||
return None
|
||||
|
||||
normal = MVector([plane.normal.x, plane.normal.y, plane.normal.z])
|
||||
|
||||
radius = rcurve.radius * scale
|
||||
startAngle = rcurve.startAngle
|
||||
endAngle = rcurve.endAngle
|
||||
|
||||
startQuat = MQuaternion(normal, startAngle) # type: ignore
|
||||
endQuat = MQuaternion(normal, endAngle) # type: ignore
|
||||
|
||||
# Get start and end vectors, centre point, angles, etc.
|
||||
r1 = MVector([plane.xdir.x, plane.xdir.y, plane.xdir.z])
|
||||
r1.rotate(startQuat)
|
||||
|
||||
r2 = MVector([plane.xdir.x, plane.xdir.y, plane.xdir.z])
|
||||
r2.rotate(endQuat)
|
||||
|
||||
c = MVector([plane.origin.x, plane.origin.y, plane.origin.z]) * scale
|
||||
|
||||
spt = c + r1 * radius
|
||||
ept = c + r2 * radius
|
||||
|
||||
angle = endAngle - startAngle
|
||||
|
||||
t1 = normal.cross(r1)
|
||||
|
||||
# Initialize arc data and calculate subdivisions
|
||||
arc = bcurve.splines.new("NURBS")
|
||||
|
||||
arc.use_cyclic_u = False
|
||||
|
||||
Ndiv = max(int(math.floor(angle / 0.3)), 2)
|
||||
step = angle / float(Ndiv)
|
||||
stepQuat = MQuaternion(normal, step) # type: ignore
|
||||
tan = math.tan(step / 2) * radius
|
||||
|
||||
arc.points.add(Ndiv + 1)
|
||||
|
||||
# Set start and end points
|
||||
arc.points[0].co = (spt.x, spt.y, spt.z, 1)
|
||||
arc.points[Ndiv + 1].co = (ept.x, ept.y, ept.z, 1)
|
||||
|
||||
# Set intermediate points
|
||||
for i in range(Ndiv):
|
||||
t1 = normal.cross(r1)
|
||||
pt = c + r1 * radius + t1 * tan
|
||||
arc.points[i + 1].co = (pt.x, pt.y, pt.z, 1)
|
||||
r1.rotate(stepQuat)
|
||||
|
||||
# Set curve settings
|
||||
arc.use_endpoint_u = True
|
||||
arc.order_u = 3
|
||||
|
||||
return arc
|
||||
|
||||
|
||||
def polycurve_to_native(scurve: Polycurve, bcurve: bpy.types.Curve, scale: float) -> list[bpy.types.Spline]:
|
||||
"""
|
||||
Convert Polycurve object
|
||||
"""
|
||||
if not scurve.segments: raise Exception("curve is missing segments")
|
||||
|
||||
curves = []
|
||||
|
||||
for seg in scurve.segments:
|
||||
speckle_type = type(seg)
|
||||
|
||||
if speckle_type in SUPPORTED_CURVES:
|
||||
curves.append(icurve_to_native_spline(seg, bcurve, scale))
|
||||
else:
|
||||
_report(f"Unsupported curve type: {speckle_type}")
|
||||
|
||||
return curves
|
||||
|
||||
def ellipse_to_native(ellipse: Union[Ellipse, Circle], bcurve: bpy.types.Curve, units_scale: float) -> List[bpy.types.Spline]:
|
||||
if not ellipse.plane: raise Exception("curve is missing plane")
|
||||
|
||||
radX: float
|
||||
radY: float
|
||||
if isinstance(ellipse, Ellipse):
|
||||
if not ellipse.firstRadius: raise Exception("curve is missing firstRadius")
|
||||
if not ellipse.secondRadius: raise Exception("curve is missing secondRadius")
|
||||
|
||||
radX = ellipse.firstRadius * units_scale
|
||||
radY = ellipse.secondRadius * units_scale
|
||||
else:
|
||||
if not ellipse.radius: raise Exception("curve is missing radius")
|
||||
|
||||
radX = ellipse.radius * units_scale
|
||||
radY = ellipse.radius * units_scale
|
||||
|
||||
|
||||
D = 0.5522847498307936 # (4/3)*tan(pi/8)
|
||||
|
||||
right_handles = [
|
||||
(+radX, +radY * D, 0.0),
|
||||
(-radX * D, +radY, 0.0),
|
||||
(-radX, -radY * D, 0.0),
|
||||
(+radX * D, -radY, 0.0),
|
||||
]
|
||||
|
||||
left_handles = [
|
||||
(+radX, -radY * D, 0.0),
|
||||
(+radX * D, +radY, 0.0),
|
||||
(-radX, +radY * D, 0.0),
|
||||
(-radX * D, -radY, 0.0),
|
||||
]
|
||||
|
||||
points = [
|
||||
(+radX, 0.0, 0.0),
|
||||
(0.0, +radY, 0.0),
|
||||
(-radX, 0.0, 0.0),
|
||||
(0.0, -radY, 0.0),
|
||||
]
|
||||
transform = plane_to_native_transform(ellipse.plane, units_scale)
|
||||
|
||||
spline = bcurve.splines.new("BEZIER")
|
||||
spline.bezier_points.add(len(points) - 1)
|
||||
|
||||
for i in range(len(points)):
|
||||
spline.bezier_points[i].co = transform @ MVector(points[i]) # type: ignore
|
||||
spline.bezier_points[i].handle_left = transform @ MVector(left_handles[i]) # type: ignore
|
||||
spline.bezier_points[i].handle_right = transform @ MVector(right_handles[i]) # type: ignore
|
||||
|
||||
spline.use_cyclic_u = True
|
||||
|
||||
#TODO support trims?
|
||||
return [spline]
|
||||
|
||||
|
||||
def icurve_to_native_spline(speckle_curve: Base, blender_curve: bpy.types.Curve, scale: float) -> List[bpy.types.Spline]:
|
||||
# polycurves
|
||||
if isinstance(speckle_curve, Polycurve):
|
||||
return polycurve_to_native(speckle_curve, blender_curve, scale)
|
||||
|
||||
splines: List[bpy.types.Spline]
|
||||
# single curves
|
||||
if isinstance(speckle_curve, Line):
|
||||
splines = line_to_native(speckle_curve, blender_curve, scale)
|
||||
elif isinstance(speckle_curve, Curve):
|
||||
splines = nurbs_to_native(speckle_curve, blender_curve, scale)
|
||||
elif isinstance(speckle_curve, Polyline):
|
||||
splines = polyline_to_native(speckle_curve, blender_curve, scale)
|
||||
elif isinstance(speckle_curve, Arc):
|
||||
spline = arc_to_native(speckle_curve, blender_curve, scale)
|
||||
splines = [spline] if spline else []
|
||||
elif isinstance(speckle_curve, Ellipse) or isinstance(speckle_curve, Circle):
|
||||
splines = ellipse_to_native(speckle_curve, blender_curve, scale)
|
||||
else:
|
||||
raise TypeError(f"{speckle_curve} is not a supported curve type. Supported types: {SUPPORTED_CURVES}")
|
||||
|
||||
return splines
|
||||
|
||||
|
||||
def icurve_to_native(speckle_curve: Base, name: str, scale: float) -> bpy.types.Curve:
|
||||
curve_type = type(speckle_curve)
|
||||
if curve_type not in SUPPORTED_CURVES:
|
||||
raise Exception(f"Unsupported curve type: {curve_type}")
|
||||
|
||||
blender_curve = (
|
||||
bpy.data.curves[name]
|
||||
if name in bpy.data.curves.keys()
|
||||
else bpy.data.curves.new(name, type="CURVE")
|
||||
)
|
||||
blender_curve.dimensions = "3D"
|
||||
blender_curve.resolution_u = 12 #TODO: We could maybe decern the resolution from the polyline displayValue
|
||||
|
||||
icurve_to_native_spline(speckle_curve, blender_curve, scale)
|
||||
|
||||
return blender_curve
|
||||
|
||||
|
||||
"""
|
||||
Transforms and Instances
|
||||
"""
|
||||
|
||||
def transform_to_native(transform: Transform, scale: float) -> MMatrix:
|
||||
mat = MMatrix(
|
||||
[
|
||||
transform.value[:4],
|
||||
transform.value[4:8],
|
||||
transform.value[8:12],
|
||||
transform.value[12:16],
|
||||
]
|
||||
)
|
||||
# scale the translation
|
||||
for i in range(3):
|
||||
mat[i][3] *= scale
|
||||
return mat
|
||||
|
||||
def plane_to_native_transform(plane: Plane, fallback_scale:float = 1) -> MMatrix:
|
||||
scale_factor = get_scale_factor(plane, fallback_scale)
|
||||
tx = (plane.origin.x * scale_factor)
|
||||
ty = (plane.origin.y * scale_factor)
|
||||
tz = (plane.origin.z * scale_factor)
|
||||
|
||||
|
||||
return MMatrix((
|
||||
(plane.xdir.x, plane.ydir.x, plane.normal.x, tx),
|
||||
(plane.xdir.y, plane.ydir.y, plane.normal.y, ty),
|
||||
(plane.xdir.z, plane.ydir.z, plane.normal.z, tz),
|
||||
(0, 0, 0, 1 )
|
||||
))
|
||||
|
||||
|
||||
"""
|
||||
Instances / Blocks
|
||||
"""
|
||||
|
||||
def _get_instance_name(instance: Instance) -> str:
|
||||
if not instance.definition: raise Exception("Instance is missing a definition")
|
||||
name_prefix = (
|
||||
_get_friendly_object_name(instance)
|
||||
or _get_friendly_object_name(instance.definition)
|
||||
or _simplified_speckle_type(instance.speckle_type)
|
||||
)
|
||||
return f"{name_prefix}{OBJECT_NAME_SPECKLE_SEPARATOR}{instance.id}"
|
||||
|
||||
|
||||
def instance_to_native_object(instance: Instance, scale: float) -> Object:
|
||||
"""
|
||||
Converts Instance to a unique object with (potentially) shared data (linked duplicate)
|
||||
"""
|
||||
if not instance.definition: raise Exception("Instance is missing a definition")
|
||||
if not instance.transform: raise Exception("Instance is missing a transform")
|
||||
definition = instance.definition
|
||||
if not definition.id: raise Exception("Instance is missing a valid definition")
|
||||
|
||||
name = _get_instance_name(instance)
|
||||
|
||||
native_instance: Optional[Object] = None
|
||||
converted_objects: Dict[str, Union[Object, BCollection]] = {}
|
||||
traversal_root: Base = definition
|
||||
|
||||
if not can_convert_to_native(definition):
|
||||
# Non-convertible (like all blocks, and some revit instances) will not be converted as part of the deep_traversal.
|
||||
# so we explicitly convert them as empties.
|
||||
native_instance = create_new_object(None, name)
|
||||
native_instance.empty_display_size = 0
|
||||
|
||||
converted_objects["__ROOT"] = native_instance # we create a dummy root to avoid id conflicts, since revit definitions have displayValues, they are convertible
|
||||
traversal_root = Base(elements=definition, id="__ROOT")
|
||||
|
||||
#Convert definition + "elements" on definition
|
||||
_deep_conversion(traversal_root, converted_objects, False)
|
||||
|
||||
if not native_instance:
|
||||
assert(can_convert_to_native(definition))
|
||||
|
||||
if not definition.id in converted_objects:
|
||||
raise Exception("Definition was not converted")
|
||||
|
||||
converted = converted_objects[definition.id]
|
||||
|
||||
if not isinstance(converted, Object):
|
||||
raise Exception("Definition was not converted to an Object")
|
||||
|
||||
native_instance = converted
|
||||
|
||||
instance_transform = transform_to_native(instance.transform, scale)
|
||||
native_instance.matrix_world = instance_transform
|
||||
|
||||
return native_instance
|
||||
|
||||
def instance_to_native_collection_instance(instance: Instance, scale: float) -> bpy.types.Object:
|
||||
"""
|
||||
Convert an Instance as a transformed Object with the `instance_collection` property
|
||||
set to be the `instance.Definition` converted as a collection
|
||||
|
||||
The definition collection won't be linked to the current scene
|
||||
Any Elements on the instance object will also be converted (and spacially transformed)
|
||||
"""
|
||||
if not instance.definition: raise Exception("Instance is missing a definition")
|
||||
if not instance.transform: raise Exception("Instance is missing a transform")
|
||||
|
||||
name = _get_instance_name(instance)
|
||||
|
||||
# Get/Convert definition collection
|
||||
collection_def = _instance_definition_to_native(instance.definition)
|
||||
|
||||
instance_transform = transform_to_native(instance.transform, scale)
|
||||
|
||||
native_instance = create_new_object(None, name)
|
||||
|
||||
#add_custom_properties(instance, native_instance)
|
||||
# hide the instance axes so they don't clutter the viewport
|
||||
native_instance.empty_display_size = 0
|
||||
native_instance.instance_collection = collection_def
|
||||
native_instance.instance_type = "COLLECTION"
|
||||
native_instance.matrix_world = instance_transform
|
||||
|
||||
return native_instance
|
||||
|
||||
def _instance_definition_to_native(definition: Union[Base, BlockDefinition]) -> bpy.types.Collection:
|
||||
"""
|
||||
Converts a geometry carrying Base as a collection (does not link it to the scene)
|
||||
"""
|
||||
name = _generate_object_name(definition)
|
||||
native_def = bpy.data.collections.get(name)
|
||||
if native_def:
|
||||
return native_def
|
||||
|
||||
native_def = create_new_collection(name)
|
||||
native_def["applicationId"] = definition.applicationId
|
||||
|
||||
converted_objects = {}
|
||||
converted_objects["__ROOT"] = native_def # we create a dummy root to avoid id conflicts, since revit definitions have displayValues, they are convertible
|
||||
dummyRoot = Base(elements=definition, id="__ROOT")
|
||||
|
||||
_deep_conversion(dummyRoot, converted_objects, True)
|
||||
|
||||
return native_def
|
||||
|
||||
def _deep_conversion(root: Base, converted_objects: Dict[str, Union[Object, BCollection]], preserve_transform: bool):
|
||||
traversal_func = get_default_traversal_func(can_convert_to_native)
|
||||
|
||||
for item in traversal_func.traverse(root):
|
||||
|
||||
current: Base = item.current
|
||||
if can_convert_to_native(current) or isinstance(current, SCollection):
|
||||
try:
|
||||
if not current or not current.id: raise Exception(f"{current} was an invalid speckle object")
|
||||
|
||||
#Convert the object!
|
||||
converted_data_type: str
|
||||
converted: Union[Object, BCollection, None]
|
||||
if isinstance(current, SCollection):
|
||||
if(current.collectionType == "Scene Collection"): raise ConversionSkippedException()
|
||||
converted = collection_to_native(current)
|
||||
converted_data_type = "COLLECTION"
|
||||
else:
|
||||
converted = convert_to_native(current)
|
||||
converted_data_type = "COLLECTION_INSTANCE" if converted.instance_collection else str(converted.type)
|
||||
|
||||
if converted is None:
|
||||
raise Exception("Conversion returned None")
|
||||
|
||||
converted_objects[current.id] = converted
|
||||
|
||||
add_to_hierarchy(converted, item, converted_objects, preserve_transform)
|
||||
|
||||
_report(f"Successfully converted {type(current).__name__} {current.id} as '{converted_data_type}'")
|
||||
except ConversionSkippedException as ex:
|
||||
_report(f"Skipped converting {type(current).__name__} {current.id}: {ex}")
|
||||
except Exception as ex:
|
||||
_report(f"Failed to converted {type(current).__name__} {current.id}: {ex}")
|
||||
|
||||
def collection_to_native(collection: SCollection) -> BCollection:
|
||||
name = collection.name or f"{collection.collectionType} -- {collection.applicationId or collection.id}" #TODO: consider consolidating name formatting with Rhino
|
||||
ret = get_or_create_collection(name)
|
||||
|
||||
color = getattr(collection, "colorTag", None)
|
||||
if color:
|
||||
ret.color_tag = color
|
||||
|
||||
return ret
|
||||
|
||||
def get_or_create_collection(name: str, clear_collection: bool = True) -> BCollection:
|
||||
#Disabled for now, since update mode needs rescoping.
|
||||
# existing = cast(Optional[BCollection], bpy.data.collections.get(name))
|
||||
# if existing:
|
||||
# if clear_collection:
|
||||
# for obj in existing.objects:
|
||||
# existing.objects.unlink(obj)
|
||||
# return existing
|
||||
# else:
|
||||
new_collection = create_new_collection(name)
|
||||
|
||||
#NOTE: We want to not render revit "Rooms" collections by default.
|
||||
if name == "Rooms":
|
||||
new_collection.hide_viewport = True
|
||||
new_collection.hide_render = True
|
||||
|
||||
return new_collection
|
||||
|
||||
|
||||
|
||||
"""
|
||||
Object Naming and Creation
|
||||
"""
|
||||
|
||||
def create_new_collection( desired_name: str) -> bpy.types.Collection:
|
||||
"""
|
||||
Creates a new blender collection with a unique name
|
||||
If the desired_name is already taken
|
||||
we'll append a number, with the format .xxx to the desired_name to ensure the name is unique.
|
||||
"""
|
||||
name = _make_unique_name(desired_name, bpy.data.collections.keys())
|
||||
|
||||
blender_collection = bpy.data.collections.new(name)
|
||||
return blender_collection
|
||||
|
||||
def create_new_object(obj_data: Optional[bpy.types.ID], desired_name: str) -> bpy.types.Object:
|
||||
"""
|
||||
Creates a new blender object with a unique name,
|
||||
If the desired_name is already taken
|
||||
we'll append a number, with the format .xxx to the desired_name to ensure the name is unique.
|
||||
"""
|
||||
name = _make_unique_name(desired_name, bpy.data.objects.keys())
|
||||
|
||||
blender_object = bpy.data.objects.new(name, obj_data)
|
||||
return blender_object
|
||||
|
||||
def _make_unique_name( desired_name: str, taken_names: Collection[str], counter: int = 0) -> str:
|
||||
"""
|
||||
Using Blenders default naming (append numeral in .xxx format) to avoid name conflicts with taken names
|
||||
"""
|
||||
name = desired_name if counter == 0 else f"{desired_name[:OBJECT_NAME_MAX_LENGTH - 4]}{OBJECT_NAME_NUMERAL_SEPARATOR}{counter:03d}" # format counter as name.xxx, truncate to ensure we don't exceed the object name max length
|
||||
|
||||
#TODO: This is very slow, and gets slower the more objects you receive with the same name...
|
||||
# We could use a binary/galloping search, and/or cache the name -> index within a receive.
|
||||
if name in taken_names:
|
||||
#Name already taken, increment counter and try again!
|
||||
return _make_unique_name(desired_name, taken_names, counter + 1)
|
||||
|
||||
return name
|
||||
|
||||
|
||||
def _get_friendly_object_name(speckle_object: Base) -> Optional[str]:
|
||||
return (getattr(speckle_object, "name", None)
|
||||
or getattr(speckle_object, "Name", None)
|
||||
or _get_revit_family_name(speckle_object)
|
||||
)
|
||||
|
||||
def _get_revit_family_name(speckle_object: Base) -> Optional[str]:
|
||||
family = getattr(speckle_object, "family", None)
|
||||
family_type = getattr(speckle_object, "type", None)
|
||||
|
||||
if family and family_type:
|
||||
return f"{family_type}-{family}"
|
||||
else:
|
||||
return None
|
||||
|
||||
# Blender object names must not exceed 62 characters
|
||||
# We need to ensure the complete ID is included in the name (to prevent identity collisions)
|
||||
# So we if the name is too long, we need to truncate
|
||||
def _truncate_object_name(name: str) -> str:
|
||||
|
||||
MAX_NAME_LENGTH = OBJECT_NAME_MAX_LENGTH - SPECKLE_ID_LENGTH - len(OBJECT_NAME_SPECKLE_SEPARATOR)
|
||||
|
||||
return name[:MAX_NAME_LENGTH]
|
||||
|
||||
|
||||
def _simplified_speckle_type(speckle_type: str) -> str:
|
||||
return(speckle_type.rsplit('.')[-1]) #Take only the most specific object type name (without namespace)
|
||||
|
||||
def _generate_object_name(speckle_object: Base) -> str:
|
||||
prefix: str
|
||||
name = _get_friendly_object_name(speckle_object)
|
||||
if name:
|
||||
prefix = _truncate_object_name(name)
|
||||
else:
|
||||
prefix = _simplified_speckle_type(speckle_object.speckle_type)
|
||||
|
||||
return f"{prefix}{OBJECT_NAME_SPECKLE_SEPARATOR}{speckle_object.id}"
|
||||
|
||||
|
||||
def get_scale_factor(speckle_object: Base, fallback: float = 1.0) -> float:
|
||||
scale = fallback
|
||||
if units := getattr(speckle_object, "units", None):
|
||||
scale = get_scale_length(units) / bpy.context.scene.unit_settings.scale_length
|
||||
return scale
|
||||
@@ -1,541 +0,0 @@
|
||||
from typing import Dict, Iterable, List, Optional, Tuple, Union, cast
|
||||
import bpy
|
||||
from bpy.types import (
|
||||
Depsgraph,
|
||||
MeshPolygon,
|
||||
Object,
|
||||
Curve as NCurve,
|
||||
Mesh as NMesh,
|
||||
Camera as NCamera,
|
||||
)
|
||||
from deprecated import deprecated
|
||||
from mathutils.geometry import interpolate_bezier
|
||||
from mathutils import (
|
||||
Matrix as MMatrix,
|
||||
Vector as MVector,
|
||||
)
|
||||
from specklepy.objects import Base
|
||||
from specklepy.objects.other import BlockInstance, BlockDefinition, RenderMaterial, Transform
|
||||
from specklepy.objects.geometry import (
|
||||
Mesh, Curve, Interval, Box, Point, Vector, Polyline,
|
||||
)
|
||||
from bpy_speckle.blender_commit_object_builder import BlenderCommitObjectBuilder
|
||||
from bpy_speckle.convert.constants import OBJECT_NAME_SPECKLE_SEPARATOR, SPECKLE_ID_LENGTH
|
||||
from bpy_speckle.convert.util import (
|
||||
ConversionSkippedException,
|
||||
get_blender_custom_properties,
|
||||
make_knots,
|
||||
nurb_make_curve,
|
||||
to_argb_int,
|
||||
)
|
||||
from bpy_speckle.functions import _report
|
||||
|
||||
|
||||
Units: str = "m" # The desired final units to send
|
||||
UnitsScale: float = 1 # The scale factor conversions need to apply to position data to get to the desired units
|
||||
|
||||
CAN_CONVERT_TO_SPECKLE = ("MESH", "CURVE", "EMPTY", "CAMERA", "FONT", "SURFACE", "META")
|
||||
|
||||
|
||||
def convert_to_speckle(raw_blender_object: Object, units_scale: float, units: str, depsgraph: Optional[Depsgraph]) -> Base:
|
||||
"""
|
||||
Converts supported 1 blender objects to 1 speckle object (potentially with children)
|
||||
:param raw_blender_object: the blender object (unevaluated by a Depsgraph) to convert
|
||||
:param units_scale: The scale factor conversions need to apply to position data to get to the desired units
|
||||
:param units: The desired final units to send
|
||||
:param depsgraph: Optional depsgraph if provided will evaluate modifiers on geometry data
|
||||
:return: The Converted blender object
|
||||
"""
|
||||
global Units, UnitsScale
|
||||
Units = units
|
||||
UnitsScale = units_scale
|
||||
|
||||
blender_type = raw_blender_object.type
|
||||
if blender_type not in CAN_CONVERT_TO_SPECKLE:
|
||||
raise ConversionSkippedException(f"Objects of type {blender_type} are not supported")
|
||||
|
||||
blender_object = cast(Object, (
|
||||
raw_blender_object.evaluated_get(depsgraph)
|
||||
if depsgraph
|
||||
else raw_blender_object
|
||||
))
|
||||
|
||||
converted: Optional[Base] = None
|
||||
if blender_type == "MESH":
|
||||
converted = mesh_to_speckle(blender_object, cast(NMesh, blender_object.data))
|
||||
elif blender_type == "CURVE":
|
||||
converted = curve_to_speckle(blender_object, cast(NCurve, blender_object.data))
|
||||
elif blender_type == "EMPTY":
|
||||
converted = empty_to_speckle(blender_object)
|
||||
elif blender_type == "CAMERA":
|
||||
converted = camera_to_speckle_view(blender_object, cast(NCamera, blender_object.data))
|
||||
elif blender_type == "FONT" or "SURFACE" or "META":
|
||||
converted = anything_to_speckle_mesh(blender_object)
|
||||
if not converted:
|
||||
raise Exception("Conversion returned None")
|
||||
|
||||
converted["properties"] = get_blender_custom_properties(raw_blender_object) #NOTE: Depsgraph copies don't have custom properties so we use the raw version
|
||||
|
||||
# Set object transform #TODO: this could be deprecated once we add proper geometry instancing support
|
||||
if blender_type != "EMPTY":
|
||||
converted["properties"]["transform"] = transform_to_speckle(
|
||||
blender_object.matrix_world
|
||||
)
|
||||
|
||||
return converted
|
||||
|
||||
def mesh_to_speckle(blender_object: Object, data: bpy.types.Mesh) -> Base:
|
||||
b = Base()
|
||||
b["name"] = to_speckle_name(blender_object)
|
||||
b["@displayValue"] = mesh_to_speckle_meshes(blender_object, data)
|
||||
return b
|
||||
|
||||
def mesh_to_speckle_meshes(blender_object: Object, data: bpy.types.Mesh) -> List[Mesh]:
|
||||
|
||||
# Categorise polygons by material index
|
||||
submesh_data: Dict[int, List[MeshPolygon]] = {}
|
||||
|
||||
for p in data.polygons:
|
||||
if p.material_index not in submesh_data:
|
||||
submesh_data[p.material_index] = []
|
||||
submesh_data[p.material_index].append(p)
|
||||
|
||||
transform = cast(MMatrix, blender_object.matrix_world)
|
||||
scaled_vertices = [tuple(transform @ x.co * UnitsScale) for x in data.vertices]
|
||||
|
||||
# Create Speckle meshes for each material
|
||||
submeshes = []
|
||||
index_counter = 0
|
||||
for i in submesh_data:
|
||||
index_mapping: Dict[int, int] = {}
|
||||
|
||||
#Loop through each polygon, and map indices to their new index in m_verts
|
||||
|
||||
mesh_area = 0
|
||||
m_verts: List[float] = []
|
||||
m_faces: List[int] = []
|
||||
m_texcoords: List[float] = []
|
||||
for face in submesh_data[i]:
|
||||
u_indices = face.vertices
|
||||
m_faces.append(len(u_indices))
|
||||
|
||||
mesh_area += face.area
|
||||
for u_index in u_indices:
|
||||
if u_index not in index_mapping:
|
||||
# Create mapping between index in blender mesh, and new index in speckle submesh
|
||||
index_mapping[u_index] = len(m_verts) // 3
|
||||
vert = scaled_vertices[u_index]
|
||||
m_verts.append(vert[0])
|
||||
m_verts.append(vert[1])
|
||||
m_verts.append(vert[2])
|
||||
|
||||
if data.uv_layers.active:
|
||||
vt = data.uv_layers.active.data[index_counter]
|
||||
uv = cast(MVector, vt.uv)
|
||||
m_texcoords.extend([uv.x, uv.y])
|
||||
|
||||
m_faces.append(index_mapping[u_index])
|
||||
index_counter += 1
|
||||
|
||||
speckle_mesh = Mesh(
|
||||
vertices=m_verts,
|
||||
faces=m_faces,
|
||||
colors=[],
|
||||
textureCoordinates=m_texcoords,
|
||||
units=Units,
|
||||
area = mesh_area,
|
||||
bbox=Box(area=0.0, volume=0.0),
|
||||
)
|
||||
|
||||
if i < len(data.materials):
|
||||
material = data.materials[i]
|
||||
if material is not None:
|
||||
speckle_mesh["renderMaterial"] = material_to_speckle(material)
|
||||
submeshes.append(speckle_mesh)
|
||||
|
||||
return submeshes
|
||||
|
||||
|
||||
def bezier_to_speckle(matrix: MMatrix, spline: bpy.types.Spline, name: Optional[str] = None) -> Curve:
|
||||
degree = 3
|
||||
closed = spline.use_cyclic_u
|
||||
points: List[Tuple[MVector]] = []
|
||||
for i, bp in enumerate(spline.bezier_points):
|
||||
if i > 0:
|
||||
points.append(tuple(matrix @ bp.handle_left * UnitsScale)) # type: ignore
|
||||
points.append(tuple(matrix @ bp.co * UnitsScale)) # type: ignore
|
||||
if i < len(spline.bezier_points) - 1:
|
||||
points.append(tuple(matrix @ bp.handle_right * UnitsScale)) # type: ignore
|
||||
|
||||
if closed:
|
||||
points.extend(
|
||||
(
|
||||
tuple(matrix @ spline.bezier_points[-1].handle_right * UnitsScale), # type: ignore
|
||||
tuple(matrix @ spline.bezier_points[0].handle_left * UnitsScale), # type: ignore
|
||||
tuple(matrix @ spline.bezier_points[0].co * UnitsScale), # type: ignore
|
||||
)
|
||||
)
|
||||
|
||||
num_points = len(points)
|
||||
|
||||
flattened_points = []
|
||||
for row in points: flattened_points.extend(row)
|
||||
|
||||
knot_count = num_points + degree - 1
|
||||
knots = [0] * knot_count
|
||||
|
||||
for i in range(1, len(knots)):
|
||||
knots[i] = i // 3
|
||||
|
||||
length = spline.calc_length()
|
||||
domain = Interval(start=0, end=length, totalChildrenCount=0)
|
||||
return Curve(
|
||||
name=name,
|
||||
degree=degree,
|
||||
closed=spline.use_cyclic_u,
|
||||
periodic= not spline.use_endpoint_u,
|
||||
points=flattened_points,
|
||||
weights=[1] * num_points,
|
||||
knots=knots,
|
||||
rational=True,
|
||||
area=0,
|
||||
volume=0,
|
||||
length=length,
|
||||
domain=domain,
|
||||
units=Units,
|
||||
bbox=Box(area=0.0, volume=0.0),
|
||||
displayValue = bezier_to_speckle_polyline(matrix, spline, length),
|
||||
)
|
||||
|
||||
|
||||
def nurbs_to_speckle(matrix: MMatrix, spline: bpy.types.Spline, name: Optional[str] = None) -> Curve:
|
||||
|
||||
degree = spline.order_u - 1
|
||||
knots = make_knots(spline)
|
||||
|
||||
length = spline.calc_length()
|
||||
domain = Interval(start=0, end=length, totalChildrenCount=0)
|
||||
|
||||
weights = [pt.weight for pt in spline.points]
|
||||
is_rational = all(w == weights[0] for w in weights)
|
||||
|
||||
points = [tuple(matrix @ pt.co.xyz * UnitsScale) for pt in spline.points] # type: ignore
|
||||
|
||||
flattened_points = []
|
||||
for row in points: flattened_points.extend(row)
|
||||
|
||||
if spline.use_cyclic_u:
|
||||
for i in range(0, degree * 3, 3):
|
||||
# Rhino expects n + degree number of points (for closed curves). So we need to add an extra point for each degree
|
||||
flattened_points.append(flattened_points[i + 0])
|
||||
flattened_points.append(flattened_points[i + 1])
|
||||
flattened_points.append(flattened_points[i + 2])
|
||||
|
||||
for i in range(0, degree):
|
||||
weights.append(weights[i])
|
||||
|
||||
return Curve(
|
||||
name=name,
|
||||
degree=degree,
|
||||
closed=spline.use_cyclic_u,
|
||||
periodic= not spline.use_endpoint_u,
|
||||
points=flattened_points,
|
||||
weights=weights,
|
||||
knots=knots,
|
||||
rational=is_rational,
|
||||
area=0,
|
||||
volume=0,
|
||||
length=length,
|
||||
domain=domain,
|
||||
units=Units,
|
||||
bbox=Box(area=0.0, volume=0.0),
|
||||
displayValue=nurbs_to_speckle_polyline(matrix, spline, length),
|
||||
)
|
||||
|
||||
def nurbs_to_speckle_polyline(matrix: MMatrix, spline: bpy.types.Spline, length: Optional[float] = None) -> Polyline:
|
||||
"""
|
||||
Samples a nurbs curve with resolution_u creating a polyline
|
||||
"""
|
||||
points: List[float] = []
|
||||
sampled_points = nurb_make_curve(spline, spline.resolution_u, 3)
|
||||
for i in range(0, len(sampled_points), 3):
|
||||
scaled_point = cast(Vector, matrix @ MVector((
|
||||
sampled_points[i + 0],
|
||||
sampled_points[i + 1],
|
||||
sampled_points[i + 2])) * UnitsScale)
|
||||
|
||||
points.append(scaled_point.x)
|
||||
points.append(scaled_point.y)
|
||||
points.append(scaled_point.z)
|
||||
|
||||
length = length or spline.calc_length()
|
||||
domain = Interval(start=0, end=length, totalChildrenCount=0)
|
||||
return Polyline(value=points, closed = spline.use_cyclic_u, domain=domain, area=0, len=length)
|
||||
|
||||
|
||||
#Inspired by https://blender.stackexchange.com/a/689 (CC BY-SA 3.0)
|
||||
def bezier_to_speckle_polyline(matrix: MMatrix, spline: bpy.types.Spline, length: Optional[float] = None) -> Optional[Polyline]:
|
||||
"""
|
||||
Samples a Bézier curve with resolution_u creating a polyline
|
||||
"""
|
||||
segments = len(spline.bezier_points)
|
||||
if segments < 2: return None
|
||||
|
||||
R = spline.resolution_u + 1
|
||||
|
||||
points = []
|
||||
if not spline.use_cyclic_u:
|
||||
segments -= 1
|
||||
|
||||
points: List[float] = []
|
||||
for i in range(segments):
|
||||
inext = (i + 1) % len(spline.bezier_points)
|
||||
|
||||
knot1 = spline.bezier_points[i].co
|
||||
handle1 = spline.bezier_points[i].handle_right
|
||||
handle2 = spline.bezier_points[inext].handle_left
|
||||
knot2 = spline.bezier_points[inext].co
|
||||
|
||||
_points = interpolate_bezier(knot1, handle1, handle2, knot2, R)
|
||||
for p in _points:
|
||||
scaled_point = matrix @ p * UnitsScale
|
||||
points.append(scaled_point.x)
|
||||
points.append(scaled_point.y)
|
||||
points.append(scaled_point.z)
|
||||
|
||||
length = length or spline.calc_length()
|
||||
domain = Interval(start=0, end=length, totalChildrenCount=0)
|
||||
return Polyline(value=points, closed = spline.use_cyclic_u, domain=domain, area=0, len=length)
|
||||
|
||||
_QUICK_TEST_NAME_LENGTH = SPECKLE_ID_LENGTH + len(OBJECT_NAME_SPECKLE_SEPARATOR)
|
||||
|
||||
def to_speckle_name(blender_object: bpy.types.ID) -> str:
|
||||
does_name_contain_id = len(blender_object.name) > _QUICK_TEST_NAME_LENGTH and OBJECT_NAME_SPECKLE_SEPARATOR in blender_object.name
|
||||
if does_name_contain_id:
|
||||
return blender_object.name.rsplit(OBJECT_NAME_SPECKLE_SEPARATOR, 1)[0]
|
||||
else:
|
||||
return blender_object.name
|
||||
|
||||
def poly_to_speckle(matrix: MMatrix, spline: bpy.types.Spline, name: Optional[str] = None) -> Polyline:
|
||||
points = [tuple(matrix @ pt.co.xyz * UnitsScale) for pt in spline.points] # type: ignore
|
||||
|
||||
flattened_points = []
|
||||
for row in points: flattened_points.extend(row)
|
||||
|
||||
length = spline.calc_length()
|
||||
domain = Interval(start=0, end=length, totalChildrenCount=0)
|
||||
return Polyline(
|
||||
name=name,
|
||||
closed=bool(spline.use_cyclic_u),
|
||||
value=list(flattened_points),
|
||||
length=length,
|
||||
domain=domain,
|
||||
bbox=Box(area=0.0, volume=0.0),
|
||||
area=0,
|
||||
units=Units,
|
||||
)
|
||||
|
||||
|
||||
def curve_to_speckle(blender_object: Object, data: bpy.types.Curve) -> Base:
|
||||
b = Base()
|
||||
(meshes, curves) = curve_to_speckle_geometry(blender_object, data)
|
||||
if meshes:
|
||||
b["@displayValue"] = meshes
|
||||
|
||||
b["name"] = to_speckle_name(blender_object)
|
||||
b["@elements"] = curves
|
||||
return b
|
||||
|
||||
def curve_to_speckle_geometry(blender_object: Object, data: bpy.types.Curve) -> Tuple[List[Mesh], List[Base]]:
|
||||
assert(blender_object.type == "CURVE")
|
||||
|
||||
blender_object = cast(Object, blender_object.evaluated_get(bpy.context.view_layer.depsgraph))
|
||||
|
||||
matrix = cast(MMatrix, blender_object.matrix_world)
|
||||
|
||||
meshes: List[Mesh] = []
|
||||
curves: List[Base] = []
|
||||
|
||||
#TODO: Could we support this better?
|
||||
if data.bevel_mode == "OBJECT" and data.bevel_object != None:
|
||||
meshes = mesh_to_speckle_meshes(blender_object, blender_object.to_mesh())
|
||||
|
||||
for spline in data.splines:
|
||||
if spline.type == "BEZIER":
|
||||
curves.append(bezier_to_speckle(matrix, spline, to_speckle_name(blender_object)))
|
||||
|
||||
elif spline.type == "NURBS":
|
||||
curves.append(nurbs_to_speckle(matrix, spline, to_speckle_name(blender_object)))
|
||||
|
||||
elif spline.type == "POLY":
|
||||
curves.append(poly_to_speckle(matrix, spline, to_speckle_name(blender_object)))
|
||||
|
||||
return (meshes, curves)
|
||||
|
||||
def anything_to_speckle_mesh(blender_object: Object) -> Base:
|
||||
|
||||
mesh = mesh_to_speckle(blender_object, blender_object.to_mesh())
|
||||
blender_object.to_mesh_clear()
|
||||
return mesh
|
||||
|
||||
@deprecated
|
||||
def ngons_to_speckle_polylines(blender_object: Object, data: bpy.types.Mesh) -> Optional[List[Polyline]]:
|
||||
UNITS = "m" if bpy.context.scene.unit_settings.system == "METRIC" else "ft"
|
||||
|
||||
if blender_object.type != "MESH":
|
||||
return None
|
||||
|
||||
mat = blender_object.matrix_world
|
||||
|
||||
verts = data.vertices
|
||||
polylines = []
|
||||
for i, poly in enumerate(data.polygons):
|
||||
value = []
|
||||
for v in poly.vertices:
|
||||
value.extend(mat @ verts[v].co * UnitsScale) # type: ignore
|
||||
|
||||
domain = Interval(start=0, end=1)
|
||||
poly = Polyline(
|
||||
name="{}_{}".format(blender_object.name, i),
|
||||
closed=True,
|
||||
value=value,
|
||||
length=0,
|
||||
domain=domain,
|
||||
bbox=Box(area=0.0, volume=0.0),
|
||||
area=0,
|
||||
units=UNITS,
|
||||
)
|
||||
|
||||
polylines.append(poly)
|
||||
|
||||
return polylines
|
||||
|
||||
|
||||
def material_to_speckle(blender_mat: bpy.types.Material) -> RenderMaterial:
|
||||
speckle_mat = RenderMaterial()
|
||||
speckle_mat.name = blender_mat.name
|
||||
|
||||
if blender_mat.use_nodes:
|
||||
if blender_mat.node_tree.nodes.get("Principled BSDF"):
|
||||
inputs = blender_mat.node_tree.nodes["Principled BSDF"].inputs
|
||||
emission_color = "Emission" if "Emission" in inputs else "Emission Color" # type: ignore
|
||||
|
||||
speckle_mat.diffuse = to_argb_int(inputs["Base Color"].default_value) # type: ignore
|
||||
speckle_mat.emissive = to_argb_int(inputs[emission_color].default_value) # type: ignore
|
||||
speckle_mat.roughness = inputs["Roughness"].default_value # type: ignore
|
||||
speckle_mat.metalness = inputs["Metallic"].default_value # type: ignore
|
||||
speckle_mat.opacity = inputs["Alpha"].default_value # type: ignore
|
||||
return speckle_mat
|
||||
elif blender_mat.node_tree.nodes.get("Diffuse BSDF"):
|
||||
inputs = blender_mat.node_tree.nodes["Diffuse BSDF"].inputs
|
||||
speckle_mat.diffuse = to_argb_int(inputs["Color"].default_value) # type: ignore
|
||||
speckle_mat.roughness = inputs["Roughness"].default_value # type: ignore
|
||||
return speckle_mat
|
||||
#TODO: Support more shaders
|
||||
|
||||
# fallback to standard material props
|
||||
speckle_mat.diffuse = to_argb_int(blender_mat.diffuse_color) # type: ignore
|
||||
speckle_mat.metalness = blender_mat.metallic
|
||||
speckle_mat.roughness = blender_mat.roughness
|
||||
|
||||
return speckle_mat
|
||||
|
||||
def camera_to_speckle_view(blender_object: Object, data: NCamera) -> Base:
|
||||
if data.type != 'PERSP':
|
||||
raise Exception(f"Cameras of type {data.type} are not currently supported")
|
||||
|
||||
matrix = cast(MMatrix, blender_object.matrix_world)
|
||||
up = cast(MVector, matrix.col[1].xyz)
|
||||
forwards = cast(MVector, -matrix.col[2].xyz)
|
||||
translation = matrix.translation
|
||||
|
||||
view = Base.of_type("Objects.BuiltElements.View:Objects.BuiltElements.View3D") #HACK: views are not in specklepy yet!
|
||||
view.name = to_speckle_name(blender_object)
|
||||
view.origin = vector_to_speckle_point(translation)
|
||||
view.upDirection = vector_to_speckle(up)
|
||||
view.forwardDirection = vector_to_speckle(forwards)
|
||||
view.target = vector_to_speckle_point(forwards) #TODO: do these need to be scaled?
|
||||
view.units = Units
|
||||
view.isOrthogonal = False
|
||||
return view
|
||||
|
||||
def vector_to_speckle_point(xyz: MVector) -> Point:
|
||||
return Point(
|
||||
x = xyz.x * UnitsScale,
|
||||
y = xyz.y * UnitsScale,
|
||||
z = xyz.z * UnitsScale,
|
||||
units = Units,
|
||||
)
|
||||
|
||||
def vector_to_speckle(xyz: MVector) -> Vector:
|
||||
return Vector(
|
||||
x = xyz.x * UnitsScale,
|
||||
y = xyz.y * UnitsScale,
|
||||
z = xyz.z * UnitsScale,
|
||||
units = Units,
|
||||
)
|
||||
|
||||
def transform_to_speckle(blender_transform: Union[Iterable[Iterable[float]], MMatrix]) -> Transform:
|
||||
iterable_transform = cast(Iterable[Iterable[float]], blender_transform) #NOTE: Matrix are iterable, even if type hinting says they are not
|
||||
value = [y for x in iterable_transform for y in x]
|
||||
# scale the translation
|
||||
for i in (3, 7, 11):
|
||||
value[i] *= UnitsScale
|
||||
|
||||
return Transform(value=value, units=Units)
|
||||
|
||||
|
||||
def block_def_to_speckle(blender_definition: bpy.types.Collection) -> BlockDefinition:
|
||||
geometryBuilder = BlenderCommitObjectBuilder()
|
||||
for geo in blender_definition.objects:
|
||||
try:
|
||||
c = convert_to_speckle(geo, UnitsScale, Units, None)
|
||||
geometryBuilder.include_object(c, geo)
|
||||
except ConversionSkippedException as ex:
|
||||
_report(f"Skipped converting '{geo.name_full}' inside collection instance: '{ex}")
|
||||
except Exception as ex:
|
||||
_report(f"Failed to converted '{geo.name_full}' inside collection instance: '{ex}'")
|
||||
|
||||
dummyRoot = Base()
|
||||
geometryBuilder.apply_relationships(geometryBuilder.converted.values(), dummyRoot)
|
||||
|
||||
block_def = BlockDefinition(
|
||||
units=Units,
|
||||
name=to_speckle_name(blender_definition),
|
||||
geometry=dummyRoot["@elements"],
|
||||
basePoint=Point(units=Units),
|
||||
)
|
||||
# blender_props = get_blender_custom_properties(blender_definition)
|
||||
# block_def.applicationId = blender_props.pop("applicationId", None) #TODO: remove?
|
||||
return block_def
|
||||
|
||||
|
||||
def block_instance_to_speckle(blender_instance: Object) -> BlockInstance:
|
||||
return BlockInstance(
|
||||
blockDefinition=block_def_to_speckle(
|
||||
blender_instance.instance_collection
|
||||
),
|
||||
transform=transform_to_speckle(blender_instance.matrix_world),
|
||||
name=to_speckle_name(blender_instance),
|
||||
units=Units,
|
||||
)
|
||||
|
||||
|
||||
def empty_to_speckle(blender_object: Object) -> Union[BlockInstance, Base]:
|
||||
# probably an instance collection (block) so let's try it
|
||||
|
||||
if blender_object.instance_collection and blender_object.instance_type == "COLLECTION":
|
||||
# Empty -> Block
|
||||
return block_instance_to_speckle(blender_object)
|
||||
else:
|
||||
# Empty -> Point
|
||||
wrapper = Base()
|
||||
wrapper["@displayValue"] = matrix_to_speckle_point(cast(MMatrix, blender_object.matrix_world))
|
||||
return wrapper
|
||||
|
||||
|
||||
def matrix_to_speckle_point(matrix: MMatrix, units_scale: float = 1.0) -> Point:
|
||||
transformed_pos = cast(MVector, matrix @ MVector((0,0,0)) * units_scale)
|
||||
return Point(x = transformed_pos.x,
|
||||
y = transformed_pos.y,
|
||||
z = transformed_pos.z)
|
||||
@@ -1,493 +0,0 @@
|
||||
import math
|
||||
from typing import Any, Dict, Optional, Tuple, Union, cast
|
||||
from bmesh.types import BMesh
|
||||
import bpy, idprop
|
||||
|
||||
from specklepy.objects.base import Base
|
||||
from specklepy.objects.geometry import Mesh
|
||||
from specklepy.objects.other import RenderMaterial
|
||||
from bpy_speckle.convert.constants import IGNORED_PROPERTY_KEYS
|
||||
from bpy_speckle.functions import _report
|
||||
from bpy.types import Material, Object, Collection as BCollection, Node, ShaderNodeVertexColor, NodeInputs
|
||||
|
||||
from specklepy.objects.graph_traversal.traversal import TraversalContext
|
||||
|
||||
class ConversionSkippedException(Exception):
|
||||
pass
|
||||
|
||||
def to_rgba(argb_int: int) -> Tuple[float, float, float, float]:
|
||||
"""Converts the int representation of a colour into a percent RGBA tuple"""
|
||||
alpha = ((argb_int >> 24) & 255) / 255
|
||||
red = ((argb_int >> 16) & 255) / 255
|
||||
green = ((argb_int >> 8) & 255) / 255
|
||||
blue = (argb_int & 255) / 255
|
||||
|
||||
return (red, green, blue, alpha)
|
||||
|
||||
|
||||
def to_argb_int(rgba_color: list[float]) -> int:
|
||||
"""Converts an RGBA array to an ARGB integer"""
|
||||
argb_color = rgba_color[-1:] + rgba_color[:3]
|
||||
int_color = [int(val * 255) for val in argb_color]
|
||||
|
||||
return int.from_bytes(int_color, byteorder="big", signed=True)
|
||||
|
||||
def set_custom_property(key: str, value: Any, blender_object: Object) -> None:
|
||||
try:
|
||||
#Expected c types: float, int, string, float[], int[]
|
||||
blender_object[key] = value
|
||||
except (OverflowError, TypeError) as ex:
|
||||
print(f"Skipping setting property ({key}={value}) on {blender_object.name_full}, Reason: {ex}")
|
||||
except Exception as ex:
|
||||
#TODO: Log this as it's unexpected!!!
|
||||
print(f"Skipping setting property ({key}={value}) on {blender_object.name_full}, Reason: {ex}")
|
||||
|
||||
def add_custom_properties(speckle_object: Base, blender_object: Object):
|
||||
if blender_object is None:
|
||||
return
|
||||
|
||||
blender_object["_speckle_type"] = type(speckle_object).__name__
|
||||
|
||||
app_id = getattr(speckle_object, "applicationId", None)
|
||||
if app_id:
|
||||
blender_object["applicationId"] = speckle_object.applicationId
|
||||
keys = speckle_object.get_dynamic_member_names() if "Geometry" in speckle_object.speckle_type else (set(speckle_object.get_member_names()) - IGNORED_PROPERTY_KEYS)
|
||||
for key in keys:
|
||||
val = getattr(speckle_object, key, None)
|
||||
if val is None:
|
||||
continue
|
||||
|
||||
if isinstance(val, (int, str, float)):
|
||||
set_custom_property(key, val, blender_object)
|
||||
elif key == "properties" and isinstance(val, Base):
|
||||
val["applicationId"] = None
|
||||
add_custom_properties(val, blender_object)
|
||||
elif isinstance(val, list):
|
||||
items = [item for item in val if not isinstance(item, Base)]
|
||||
if items:
|
||||
set_custom_property(key, items, blender_object)
|
||||
elif isinstance(val,dict):
|
||||
for (k,v) in val.items():
|
||||
if not isinstance(v, Base):
|
||||
set_custom_property(k, v, blender_object)
|
||||
|
||||
|
||||
def render_material_to_native(speckle_mat: RenderMaterial) -> Material:
|
||||
|
||||
mat_name = speckle_mat.name
|
||||
if not mat_name:
|
||||
mat_name = speckle_mat.applicationId or speckle_mat.id or speckle_mat.get_id()
|
||||
|
||||
blender_mat = bpy.data.materials.get(mat_name)
|
||||
if blender_mat is None:
|
||||
blender_mat = bpy.data.materials.new(mat_name)
|
||||
|
||||
# for now, we're not updating these materials. as per tom's suggestion, we should have a toggle
|
||||
# that enables this as the blender mats will prob be much more complex than whatever is coming in
|
||||
blender_mat.use_nodes = True
|
||||
inputs = blender_mat.node_tree.nodes["Principled BSDF"].inputs
|
||||
|
||||
inputs["Base Color"].default_value = to_rgba(speckle_mat.diffuse) # type: ignore
|
||||
inputs["Roughness"].default_value = speckle_mat.roughness # type: ignore
|
||||
inputs["Metallic"].default_value = speckle_mat.metalness # type: ignore
|
||||
inputs["Alpha"].default_value = speckle_mat.opacity # type: ignore
|
||||
|
||||
# Blender >=4.0 use "Emission Color"
|
||||
emission_color = "Emission" if "Emission" in inputs else "Emission Color" # type: ignore
|
||||
inputs[emission_color].default_value = to_rgba(speckle_mat.emissive) # type: ignore
|
||||
|
||||
if speckle_mat.opacity < 1.0:
|
||||
blender_mat.blend_method = "BLEND"
|
||||
|
||||
return blender_mat
|
||||
|
||||
_vertex_color_material: Optional[Material] = None
|
||||
|
||||
def get_vertex_color_material() -> Material:
|
||||
global _vertex_color_material
|
||||
|
||||
#see https://stackoverflow.com/a/69807985
|
||||
if not _vertex_color_material:
|
||||
_vertex_color_material = bpy.data.materials.new("Vertex Color Material")
|
||||
_vertex_color_material.use_nodes = True
|
||||
nodes = _vertex_color_material.node_tree.nodes
|
||||
principled_bsdf_node = cast(Node, nodes.get("Principled BSDF"))
|
||||
|
||||
if not "VERTEX_COLOR" in [node.type for node in nodes]:
|
||||
vertex_color_node = cast(ShaderNodeVertexColor, nodes.new(type = "ShaderNodeVertexColor"))
|
||||
else:
|
||||
vertex_color_node = cast(ShaderNodeVertexColor, nodes.get("Vertex Color"))
|
||||
vertex_color_node.layer_name = "Col"
|
||||
|
||||
links = _vertex_color_material.node_tree.links
|
||||
link = links.new(vertex_color_node.outputs[0], principled_bsdf_node.inputs[0])
|
||||
|
||||
return _vertex_color_material
|
||||
|
||||
def get_render_material(speckle_object: Base) -> Optional[RenderMaterial]:
|
||||
"""Trys to get a RenderMaterial on given speckle_object"""
|
||||
|
||||
speckle_mat = getattr(
|
||||
speckle_object,
|
||||
"renderMaterial",
|
||||
getattr(speckle_object, "@renderMaterial", None),
|
||||
)
|
||||
|
||||
if isinstance(speckle_mat, RenderMaterial):
|
||||
return speckle_mat
|
||||
|
||||
return None
|
||||
|
||||
|
||||
|
||||
def add_vertices(speckle_mesh: Mesh, blender_mesh: BMesh, scale=1.0):
|
||||
sverts = speckle_mesh.vertices
|
||||
|
||||
if sverts and len(sverts) > 0:
|
||||
for i in range(0, len(sverts), 3):
|
||||
blender_mesh.verts.new(
|
||||
(
|
||||
float(sverts[i]) * scale,
|
||||
float(sverts[i + 1]) * scale,
|
||||
float(sverts[i + 2]) * scale,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
|
||||
def add_faces(speckle_mesh: Mesh, blender_mesh: BMesh, indexOffset: int, materialIndex: int = 0, smooth:bool = True):
|
||||
sfaces = speckle_mesh.faces
|
||||
|
||||
if sfaces and len(sfaces) > 0:
|
||||
i = 0
|
||||
while i < len(sfaces):
|
||||
n = sfaces[i]
|
||||
if n < 3:
|
||||
n += 3 # 0 -> 3, 1 -> 4
|
||||
|
||||
i += 1
|
||||
try:
|
||||
f = blender_mesh.faces.new(
|
||||
[blender_mesh.verts[x + indexOffset] for x in sfaces[i : i + n]]
|
||||
)
|
||||
f.material_index = materialIndex
|
||||
f.smooth = smooth
|
||||
except Exception as e:
|
||||
_report(f"Failed to create face for mesh {speckle_mesh.id} \n{e}")
|
||||
i += n
|
||||
|
||||
|
||||
def add_colors(speckle_mesh: Mesh, blender_mesh: BMesh):
|
||||
|
||||
scolors = speckle_mesh.colors
|
||||
|
||||
if scolors:
|
||||
colors = []
|
||||
if len(scolors) > 0:
|
||||
|
||||
for i in range(len(scolors)):
|
||||
argb = int(scolors[i])
|
||||
(a, r, g, b) = argb_split(argb)
|
||||
colors.append(
|
||||
(
|
||||
float(r) / 255.0,
|
||||
float(g) / 255.0,
|
||||
float(b) / 255.0,
|
||||
float(a) / 255.0,
|
||||
)
|
||||
)
|
||||
|
||||
# Make vertex colors
|
||||
if len(scolors) == len(blender_mesh.verts):
|
||||
color_layer = blender_mesh.loops.layers.color.new("Col")
|
||||
|
||||
for face in blender_mesh.faces:
|
||||
for loop in face.loops:
|
||||
loop[color_layer] = colors[loop.vert.index]
|
||||
|
||||
def argb_split(argb: int) -> Tuple[int, int, int, int]:
|
||||
alpha = (argb >> 24) & 0xFF
|
||||
red = (argb >> 16) & 0xFF
|
||||
green = (argb >> 8) & 0xFF
|
||||
blue = argb & 0xFF
|
||||
|
||||
return (alpha, red, green, blue)
|
||||
|
||||
def add_uv_coords(speckle_mesh: Mesh, blender_mesh: BMesh):
|
||||
s_uvs = speckle_mesh.textureCoordinates
|
||||
if not s_uvs:
|
||||
return
|
||||
try:
|
||||
uv = []
|
||||
|
||||
if len(s_uvs) // 2 == len(blender_mesh.verts):
|
||||
uv.extend(
|
||||
(float(s_uvs[i]), float(s_uvs[i + 1]))
|
||||
for i in range(0, len(s_uvs), 2)
|
||||
)
|
||||
else:
|
||||
_report(
|
||||
f"Failed to match UV coordinates to vert data. Blender mesh verts: {len(blender_mesh.verts)}, Speckle UVs: {len(s_uvs) // 2}"
|
||||
)
|
||||
return
|
||||
|
||||
# Make UVs
|
||||
uv_layer = blender_mesh.loops.layers.uv.verify()
|
||||
|
||||
for f in blender_mesh.faces:
|
||||
for l in f.loops:
|
||||
luv = l[uv_layer]
|
||||
luv.uv = uv[l.vert.index]
|
||||
except:
|
||||
_report("Failed to decode texture coordinates.")
|
||||
raise
|
||||
|
||||
|
||||
ignored_keys = {
|
||||
"id",
|
||||
"speckle",
|
||||
"speckle_type"
|
||||
"_speckle_type",
|
||||
"_speckle_name",
|
||||
"_speckle_transform",
|
||||
"_RNA_UI",
|
||||
"elements",
|
||||
"transform",
|
||||
"_units",
|
||||
"_chunkable",
|
||||
}
|
||||
|
||||
def get_blender_custom_properties(obj, max_depth: int = 63):
|
||||
"""Recursively grabs custom properties on blender objects. Max depth is determined by the max allowed by Newtonsoft.NET, don't exceed unless you know what you're doing"""
|
||||
if max_depth <= 0:
|
||||
return obj
|
||||
|
||||
if hasattr(obj, "keys"):
|
||||
keys = set(obj.keys()) - ignored_keys
|
||||
return {
|
||||
key: get_blender_custom_properties(obj[key], max_depth - 1)
|
||||
for key in keys
|
||||
if not key.startswith("_")
|
||||
}
|
||||
|
||||
if isinstance(obj, (list, tuple, idprop.types.IDPropertyArray)):
|
||||
return [get_blender_custom_properties(o, max_depth - 1) for o in obj] # type: ignore
|
||||
|
||||
return obj
|
||||
|
||||
"""
|
||||
Python implementation of Blender's NURBS curve generation for to Speckle conversion
|
||||
from: https://blender.stackexchange.com/a/34276
|
||||
based on https://projects.blender.org/blender/blender/src/branch/main/source/blender/blenkernel/intern/curve.cc (check old version)
|
||||
"""
|
||||
|
||||
def macro_knotsu(nu: bpy.types.Spline) -> int:
|
||||
return nu.order_u + nu.point_count_u + (nu.order_u - 1 if nu.use_cyclic_u else 0)
|
||||
|
||||
def macro_segmentsu(nu: bpy.types.Spline) -> int:
|
||||
return nu.point_count_u if nu.use_cyclic_u else nu.point_count_u - 1
|
||||
|
||||
def make_knots(nu: bpy.types.Spline) -> list[float]:
|
||||
knots = [0.0] * macro_knotsu(nu)
|
||||
flag = nu.use_endpoint_u + (nu.use_bezier_u << 1)
|
||||
if nu.use_cyclic_u:
|
||||
calc_knots(knots, nu.point_count_u, nu.order_u, 0)
|
||||
else:
|
||||
calc_knots(knots, nu.point_count_u, nu.order_u, flag)
|
||||
return knots
|
||||
|
||||
|
||||
def calc_knots(knots: list[float], point_count: int, order: int, flag: int) -> None:
|
||||
pts_order = point_count + order
|
||||
if flag == 1: # CU_NURB_ENDPOINT
|
||||
k = 0.0
|
||||
for a in range(1, pts_order + 1):
|
||||
knots[a - 1] = k
|
||||
if a >= order and a <= point_count:
|
||||
k += 1.0
|
||||
elif flag == 2: # CU_NURB_BEZIER
|
||||
if order == 4:
|
||||
k = 0.34
|
||||
for a in range(pts_order):
|
||||
knots[a] = math.floor(k)
|
||||
k += 1.0 / 3.0
|
||||
elif order == 3:
|
||||
k = 0.6
|
||||
for a in range(pts_order):
|
||||
if a >= order and a <= point_count:
|
||||
k += 0.5
|
||||
knots[a] = math.floor(k)
|
||||
else:
|
||||
for a in range(1, len(knots) - 1):
|
||||
knots[a] = a - 1
|
||||
|
||||
knots[-1] = knots[-2]
|
||||
|
||||
def basis_nurb(t: float, order: int, point_count: int, knots: list[float], basis: list[float], start: int, end: int) -> Tuple[int, int]:
|
||||
i1 = i2 = 0
|
||||
orderpluspnts = order + point_count
|
||||
opp2 = orderpluspnts - 1
|
||||
|
||||
# this is for float inaccuracy
|
||||
if t < knots[0]:
|
||||
t = knots[0]
|
||||
elif t > knots[opp2]:
|
||||
t = knots[opp2]
|
||||
|
||||
# this part is order '1'
|
||||
o2 = order + 1
|
||||
for i in range(opp2):
|
||||
if knots[i] != knots[i + 1] and t >= knots[i] and t <= knots[i + 1]:
|
||||
basis[i] = 1.0
|
||||
i1 = i - o2
|
||||
if i1 < 0:
|
||||
i1 = 0
|
||||
i2 = i
|
||||
i += 1
|
||||
while i < opp2:
|
||||
basis[i] = 0.0
|
||||
i += 1
|
||||
break
|
||||
|
||||
else:
|
||||
basis[i] = 0.0
|
||||
|
||||
basis[i] = 0.0 #type: ignore
|
||||
|
||||
# this is order 2, 3, ...
|
||||
for j in range(2, order + 1):
|
||||
|
||||
if i2 + j >= orderpluspnts:
|
||||
i2 = opp2 - j
|
||||
|
||||
for i in range(i1, i2 + 1):
|
||||
if basis[i] != 0.0:
|
||||
d = ((t - knots[i]) * basis[i]) / (knots[i + j - 1] - knots[i])
|
||||
else:
|
||||
d = 0.0
|
||||
|
||||
if basis[i + 1] != 0.0:
|
||||
e = ((knots[i + j] - t) * basis[i + 1]) / (knots[i + j] - knots[i + 1])
|
||||
else:
|
||||
e = 0.0
|
||||
|
||||
basis[i] = d + e
|
||||
|
||||
start = 1000
|
||||
end = 0
|
||||
|
||||
for i in range(i1, i2 + 1):
|
||||
if basis[i] > 0.0:
|
||||
end = i
|
||||
if start == 1000:
|
||||
start = i
|
||||
|
||||
return start, end
|
||||
|
||||
def nurb_make_curve(nu: bpy.types.Spline, resolu: int, stride: int = 3) -> list[float]:
|
||||
""""BKE_nurb_makeCurve"""
|
||||
EPS = 1e-6
|
||||
coord_index = istart = iend = 0
|
||||
|
||||
coord_array = [0.0] * (3 * nu.resolution_u * macro_segmentsu(nu))
|
||||
sum_array = [0] * nu.point_count_u
|
||||
basisu = [0.0] * macro_knotsu(nu)
|
||||
knots = make_knots(nu)
|
||||
|
||||
resolu = resolu * macro_segmentsu(nu)
|
||||
ustart = knots[nu.order_u - 1]
|
||||
uend = knots[nu.point_count_u + nu.order_u - 1] if nu.use_cyclic_u else \
|
||||
knots[nu.point_count_u]
|
||||
ustep = (uend - ustart) / (resolu - (0 if nu.use_cyclic_u else 1))
|
||||
cycl = nu.order_u - 1 if nu.use_cyclic_u else 0
|
||||
|
||||
u = ustart
|
||||
while resolu:
|
||||
resolu -= 1
|
||||
istart, iend = basis_nurb(u, nu.order_u, nu.point_count_u + cycl, knots, basisu, istart, iend)
|
||||
|
||||
#/* calc sum */
|
||||
sumdiv = 0.0
|
||||
sum_index = 0
|
||||
pt_index = istart - 1
|
||||
for i in range(istart, iend + 1):
|
||||
if i >= nu.point_count_u:
|
||||
pt_index = i - nu.point_count_u
|
||||
else:
|
||||
pt_index += 1
|
||||
|
||||
sum_array[sum_index] = basisu[i] * nu.points[pt_index].co[3] #type: ignore
|
||||
sumdiv += sum_array[sum_index]
|
||||
sum_index += 1
|
||||
|
||||
if (sumdiv != 0.0) and (sumdiv < 1.0 - EPS or sumdiv > 1.0 + EPS):
|
||||
sum_index = 0
|
||||
for i in range(istart, iend + 1):
|
||||
sum_array[sum_index] /= sumdiv #type: ignore
|
||||
sum_index += 1
|
||||
|
||||
coord_array[coord_index: coord_index + 3] = (0.0, 0.0, 0.0)
|
||||
|
||||
sum_index = 0
|
||||
pt_index = istart - 1
|
||||
for i in range(istart, iend + 1):
|
||||
if i >= nu.point_count_u:
|
||||
pt_index = i - nu.point_count_u
|
||||
else:
|
||||
pt_index += 1
|
||||
|
||||
if sum_array[sum_index] != 0.0:
|
||||
for j in range(3):
|
||||
coord_array[coord_index + j] += sum_array[sum_index] * nu.points[pt_index].co[j]
|
||||
sum_index += 1
|
||||
|
||||
coord_index += stride
|
||||
u += ustep
|
||||
|
||||
return coord_array
|
||||
|
||||
def link_object_to_collection_nested(obj: Object, col: BCollection):
|
||||
if obj.name not in col.objects: #type: ignore
|
||||
col.objects.link(obj)
|
||||
|
||||
for child in obj.children:
|
||||
link_object_to_collection_nested(child, col)
|
||||
|
||||
def add_to_hierarchy(converted: Union[Object, BCollection], traversalContext : 'TraversalContext', converted_objects: Dict[str, Union[Object, BCollection]], preserve_transform: bool) -> None:
|
||||
nextParent = traversalContext.parent
|
||||
|
||||
# Traverse up the tree to find a direct parent object, and a containing collection
|
||||
parent_collection: Optional[BCollection] = None
|
||||
parent_object: Optional[Object] = None
|
||||
|
||||
while nextParent:
|
||||
if nextParent.current.id in converted_objects:
|
||||
c = converted_objects[nextParent.current.id]
|
||||
|
||||
if isinstance(c, BCollection):
|
||||
parent_collection = c
|
||||
break
|
||||
else: #isinstance(c, Object):
|
||||
parent_object = parent_object or c
|
||||
|
||||
nextParent = nextParent.parent
|
||||
|
||||
# If no containing collection is found, fall back to the scene collection
|
||||
if not parent_collection:
|
||||
parent_collection = bpy.context.scene.collection
|
||||
|
||||
if isinstance(converted, Object):
|
||||
if parent_object:
|
||||
set_parent(converted, parent_object, preserve_transform)
|
||||
link_object_to_collection_nested(converted, parent_collection)
|
||||
elif converted.name not in parent_collection.children.keys():
|
||||
parent_collection.children.link(converted)
|
||||
|
||||
|
||||
def set_parent(child: Object, parent: Object, preserve_transform: bool = False) -> None:
|
||||
if preserve_transform :
|
||||
previous = child.matrix_world.copy() # type: ignore
|
||||
child.parent = parent
|
||||
child.matrix_world = previous
|
||||
else:
|
||||
child.parent = parent
|
||||
@@ -1,45 +0,0 @@
|
||||
from typing import Callable
|
||||
from specklepy.objects.base import Base
|
||||
from bpy_speckle.convert.constants import ELEMENTS_PROPERTY_ALIASES
|
||||
|
||||
from specklepy.objects.graph_traversal.traversal import GraphTraversal, TraversalRule
|
||||
from specklepy.objects.units import get_scale_factor_to_meters, get_units_from_string
|
||||
|
||||
|
||||
def _report(msg: object) -> None:
|
||||
"""
|
||||
Function for printing messages to the console
|
||||
"""
|
||||
print("SpeckleBlender: {}".format(msg))
|
||||
|
||||
|
||||
def get_scale_length(units: str) -> float:
|
||||
"""Returns a scalar to convert distance values from one unit system to meters"""
|
||||
return get_scale_factor_to_meters(get_units_from_string(units))
|
||||
|
||||
|
||||
def get_default_traversal_func(can_convert_to_native: Callable[[Base], bool]) -> GraphTraversal:
|
||||
"""
|
||||
Traversal func for traversing a speckle commit object
|
||||
"""
|
||||
|
||||
ignore_rule = TraversalRule(
|
||||
[
|
||||
lambda o: "Objects.Structural.Results" in o.speckle_type, #Sadly, this one is necessary to avoid double conversion...
|
||||
lambda o: "Objects.BuiltElements.Revit.Parameter" in o.speckle_type, #This one is just for traversal performance of revit commits
|
||||
],
|
||||
lambda _: [],
|
||||
)
|
||||
|
||||
convertible_rule = TraversalRule(
|
||||
[can_convert_to_native],
|
||||
lambda _: ELEMENTS_PROPERTY_ALIASES,
|
||||
)
|
||||
|
||||
|
||||
default_rule = TraversalRule(
|
||||
[lambda _: True],
|
||||
lambda o: o.get_member_names(), #TODO: avoid deprecated members
|
||||
)
|
||||
|
||||
return GraphTraversal([ignore_rule, convertible_rule, default_rule])
|
||||
@@ -1,228 +0,0 @@
|
||||
"""
|
||||
Provides uniform and consistent path helpers for `specklepy`
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
from importlib import import_module, invalidate_caches
|
||||
|
||||
_user_data_env_var = "SPECKLE_USERDATA_PATH"
|
||||
|
||||
|
||||
def _path() -> Optional[Path]:
|
||||
"""Read the user data path override setting."""
|
||||
path_override = os.environ.get(_user_data_env_var)
|
||||
if path_override:
|
||||
return Path(path_override)
|
||||
return None
|
||||
|
||||
|
||||
_application_name = "Speckle"
|
||||
|
||||
|
||||
def override_application_name(application_name: str) -> None:
|
||||
"""Override the global Speckle application name."""
|
||||
global _application_name
|
||||
_application_name = application_name
|
||||
|
||||
|
||||
def override_application_data_path(path: Optional[str]) -> None:
|
||||
"""
|
||||
Override the global Speckle application data path.
|
||||
|
||||
If the value of path is `None` the environment variable gets deleted.
|
||||
"""
|
||||
if path:
|
||||
os.environ[_user_data_env_var] = path
|
||||
else:
|
||||
os.environ.pop(_user_data_env_var, None)
|
||||
|
||||
|
||||
def _ensure_folder_exists(base_path: Path, folder_name: str) -> Path:
|
||||
path = base_path.joinpath(folder_name)
|
||||
path.mkdir(exist_ok=True, parents=True)
|
||||
return path
|
||||
|
||||
|
||||
def user_application_data_path() -> Path:
|
||||
"""Get the platform specific user configuration folder path"""
|
||||
path_override = _path()
|
||||
if path_override:
|
||||
return path_override
|
||||
|
||||
try:
|
||||
if sys.platform.startswith("win"):
|
||||
app_data_path = os.getenv("APPDATA")
|
||||
if not app_data_path:
|
||||
raise Exception(
|
||||
"Cannot get appdata path from environment."
|
||||
)
|
||||
return Path(app_data_path)
|
||||
else:
|
||||
# try getting the standard XDG_DATA_HOME value
|
||||
# as that is used as an override
|
||||
app_data_path = os.getenv("XDG_DATA_HOME")
|
||||
if app_data_path:
|
||||
return Path(app_data_path)
|
||||
else:
|
||||
return _ensure_folder_exists(Path.home(), ".config")
|
||||
except Exception as ex:
|
||||
raise Exception(
|
||||
"Failed to initialize user application data path.", ex
|
||||
)
|
||||
|
||||
|
||||
def user_speckle_folder_path() -> Path:
|
||||
"""Get the folder where the user's Speckle data should be stored."""
|
||||
return _ensure_folder_exists(user_application_data_path(), _application_name)
|
||||
|
||||
|
||||
def user_speckle_connector_installation_path(host_application: str) -> Path:
|
||||
"""
|
||||
Gets a connector specific installation folder.
|
||||
|
||||
In this folder we can put our connector installation and all python packages.
|
||||
"""
|
||||
return _ensure_folder_exists(
|
||||
_ensure_folder_exists(user_speckle_folder_path(), "connector_installations"),
|
||||
host_application,
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
print("Starting module dependency installation")
|
||||
print(sys.executable)
|
||||
|
||||
PYTHON_PATH = sys.executable
|
||||
|
||||
|
||||
|
||||
def connector_installation_path(host_application: str) -> Path:
|
||||
connector_installation_path = user_speckle_connector_installation_path(host_application)
|
||||
connector_installation_path.mkdir(exist_ok=True, parents=True)
|
||||
|
||||
# set user modules path at beginning of paths for earlier hit
|
||||
if sys.path[0] != connector_installation_path:
|
||||
sys.path.insert(0, str(connector_installation_path))
|
||||
|
||||
print(f"Using connector installation path {connector_installation_path}")
|
||||
return connector_installation_path
|
||||
|
||||
|
||||
|
||||
def is_pip_available() -> bool:
|
||||
try:
|
||||
import_module("pip") # noqa F401
|
||||
return True
|
||||
except ImportError:
|
||||
return False
|
||||
|
||||
|
||||
def ensure_pip() -> None:
|
||||
print("Installing pip... ")
|
||||
|
||||
from subprocess import run
|
||||
|
||||
completed_process = run([PYTHON_PATH, "-m", "ensurepip"])
|
||||
|
||||
if completed_process.returncode == 0:
|
||||
print("Successfully installed pip")
|
||||
else:
|
||||
raise Exception(f"Failed to install pip, got {completed_process.returncode} return code")
|
||||
|
||||
|
||||
def get_requirements_path() -> Path:
|
||||
# we assume that a requirements.txt exists next to the __init__.py file
|
||||
path = Path(Path(__file__).parent, "requirements.txt")
|
||||
return path
|
||||
|
||||
|
||||
def install_requirements(host_application: str) -> None:
|
||||
# set up addons/modules under the user
|
||||
# script path. Here we'll install the
|
||||
# dependencies
|
||||
path = connector_installation_path(host_application)
|
||||
|
||||
from subprocess import run
|
||||
|
||||
def debugger_is_active() -> bool:
|
||||
"""Return if the debugger is currently active"""
|
||||
return hasattr(sys, 'gettrace') and sys.gettrace() is not None
|
||||
|
||||
requirements_path = get_requirements_path()
|
||||
|
||||
is_debug = debugger_is_active()
|
||||
|
||||
if not is_debug and not requirements_path.exists():
|
||||
print("Skipped installing dependencies")
|
||||
return
|
||||
|
||||
print(f"Installing Speckle dependencies to {path}")
|
||||
completed_process = run(
|
||||
[
|
||||
PYTHON_PATH,
|
||||
"-m",
|
||||
"pip",
|
||||
"-q",
|
||||
"--disable-pip-version-check",
|
||||
"install",
|
||||
"--prefer-binary",
|
||||
"--ignore-installed",
|
||||
"--no-compile",
|
||||
"-t",
|
||||
str(path),
|
||||
"-r",
|
||||
str(requirements_path),
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
|
||||
if completed_process.returncode != 0:
|
||||
m = f"Failed to install dependencies through pip, got {completed_process.returncode} return code"
|
||||
print(m)
|
||||
raise Exception(m)
|
||||
|
||||
print("Successfully installed dependencies")
|
||||
|
||||
if not is_debug:
|
||||
requirements_path.unlink()
|
||||
|
||||
|
||||
def install_dependencies(host_application: str) -> None:
|
||||
if not is_pip_available():
|
||||
ensure_pip()
|
||||
|
||||
install_requirements(host_application)
|
||||
|
||||
|
||||
def _import_dependencies() -> None:
|
||||
import_module("specklepy")
|
||||
# the code above doesn't work for now, it fails on importing graphql-core
|
||||
# despite that, the connector seams to be working as expected
|
||||
# But it would be nice to make this solution work
|
||||
# it would ensure that all dependencies are fully loaded
|
||||
# requirements = get_requirements_path().read_text()
|
||||
# reqs = [
|
||||
# req.split(" ; ")[0].split("==")[0].split("[")[0].replace("-", "_")
|
||||
# for req in requirements.split("\n")
|
||||
# if req and not req.startswith(" ")
|
||||
# ]
|
||||
# for req in reqs:
|
||||
# print(req)
|
||||
# import_module("specklepy")
|
||||
|
||||
def ensure_dependencies(host_application: str) -> None:
|
||||
try:
|
||||
install_dependencies(host_application)
|
||||
invalidate_caches()
|
||||
_import_dependencies()
|
||||
print("Successfully found dependencies")
|
||||
except ImportError:
|
||||
raise Exception(f"Cannot automatically ensure Speckle dependencies. Please try restarting the host application {host_application}!")
|
||||
|
||||
|
||||
@@ -1,64 +1,2 @@
|
||||
from .users import LoadUsers, LoadUserStreams, ResetUsers
|
||||
from .object import (
|
||||
UpdateObject,
|
||||
ResetObject,
|
||||
DeleteObject,
|
||||
UploadNgonsAsPolylines,
|
||||
SelectIfSameCustomProperty,
|
||||
SelectIfHasCustomProperty,
|
||||
)
|
||||
from .streams import (
|
||||
ReceiveStreamObjects,
|
||||
SendStreamObjects,
|
||||
ViewStreamDataApi,
|
||||
DeleteStream,
|
||||
SelectOrphanObjects,
|
||||
)
|
||||
from .streams import (
|
||||
AddStreamFromURL,
|
||||
CreateStream,
|
||||
CopyStreamId,
|
||||
CopyCommitId,
|
||||
CopyBranchName,
|
||||
CopyModelId,
|
||||
)
|
||||
from .commit import DeleteCommit
|
||||
from .misc import OpenSpeckleGuide, OpenSpeckleTutorials, OpenSpeckleForum
|
||||
|
||||
operator_classes = [
|
||||
LoadUsers,
|
||||
ResetUsers,
|
||||
ReceiveStreamObjects,
|
||||
SendStreamObjects,
|
||||
LoadUserStreams,
|
||||
CopyStreamId,
|
||||
CopyCommitId,
|
||||
CopyBranchName,
|
||||
CopyModelId,
|
||||
]
|
||||
|
||||
operator_classes.extend([DeleteCommit])
|
||||
|
||||
operator_classes.extend(
|
||||
[
|
||||
UpdateObject,
|
||||
ResetObject,
|
||||
DeleteObject,
|
||||
UploadNgonsAsPolylines,
|
||||
SelectIfSameCustomProperty,
|
||||
SelectIfHasCustomProperty,
|
||||
]
|
||||
)
|
||||
|
||||
operator_classes.extend(
|
||||
[
|
||||
ViewStreamDataApi,
|
||||
DeleteStream,
|
||||
SelectOrphanObjects,
|
||||
AddStreamFromURL,
|
||||
CreateStream,
|
||||
OpenSpeckleGuide,
|
||||
OpenSpeckleTutorials,
|
||||
OpenSpeckleForum,
|
||||
]
|
||||
)
|
||||
from .load import SPECKLE_OT_load
|
||||
from .publish import SPECKLE_OT_publish
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
"""
|
||||
Commit operators
|
||||
"""
|
||||
import bpy
|
||||
from bpy.props import BoolProperty
|
||||
from bpy_speckle.clients import speckle_clients
|
||||
from bpy_speckle.functions import _report
|
||||
from bpy_speckle.properties.scene import get_speckle
|
||||
from specklepy.logging import metrics
|
||||
|
||||
|
||||
class DeleteCommit(bpy.types.Operator):
|
||||
"""
|
||||
Permanently deletes the selected version from the selected model.
|
||||
To execute from code, call: `bpy.ops.speckle.delete_commit(are_you_sure=True)`
|
||||
"""
|
||||
|
||||
bl_idname = "speckle.delete_commit"
|
||||
bl_label = "Delete Version"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Permanently Deletes the selected version from the selected model"
|
||||
|
||||
are_you_sure: BoolProperty(
|
||||
name="Confirm",
|
||||
default=False,
|
||||
) # type: ignore
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
col = layout.column()
|
||||
col.prop(self, "are_you_sure")
|
||||
|
||||
def invoke(self, context, event):
|
||||
speckle = get_speckle(context)
|
||||
wm = context.window_manager
|
||||
if len(speckle.users) > 0:
|
||||
return wm.invoke_props_dialog(self)
|
||||
|
||||
return {"CANCELLED"}
|
||||
|
||||
def execute(self, context):
|
||||
if not self.are_you_sure:
|
||||
_report("Cancelled by user")
|
||||
return {"CANCELLED"}
|
||||
self.are_you_sure = False
|
||||
|
||||
self.delete_commit(context)
|
||||
return {"FINISHED"}
|
||||
|
||||
@staticmethod
|
||||
def delete_commit(context: bpy.types.Context) -> None:
|
||||
speckle = get_speckle(context)
|
||||
|
||||
(_, stream, branch, commit) = speckle.validate_commit_selection()
|
||||
|
||||
client = speckle_clients[int(speckle.active_user)]
|
||||
|
||||
deleted = client.commit.delete(stream_id=stream.id, commit_id=commit.id)
|
||||
|
||||
metrics.track(
|
||||
"Connector Action",
|
||||
client.account,
|
||||
custom_props={
|
||||
"name": "delete_commit"
|
||||
},
|
||||
)
|
||||
|
||||
if not deleted:
|
||||
raise Exception("Delete operation failed")
|
||||
|
||||
print(f"Version {commit.id} ({commit.message}) of model {branch.id} ({branch.name}) has been deleted from project {stream.id} ({stream.name})")
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import bpy
|
||||
|
||||
# Load Operator
|
||||
class SPECKLE_OT_load(bpy.types.Operator):
|
||||
bl_idname = "speckle.load"
|
||||
bl_label = "Load from Speckle"
|
||||
bl_description = "Load objects from Speckle"
|
||||
|
||||
def invoke(self, context, event):
|
||||
context.scene.speckle_mouse_position = (event.mouse_x, event.mouse_y)
|
||||
return self.execute(context)
|
||||
|
||||
def execute(self, context):
|
||||
context.scene.speckle_ui_mode = "LOAD"
|
||||
self.report({'INFO'}, f"Load button clicked at {context.scene.speckle_mouse_position[0], context.scene.speckle_mouse_position[1]}")
|
||||
bpy.ops.speckle.project_selection_dialog("INVOKE_DEFAULT")
|
||||
return {'FINISHED'}
|
||||
@@ -1,65 +0,0 @@
|
||||
import bpy
|
||||
import webbrowser
|
||||
from specklepy.logging import metrics
|
||||
|
||||
|
||||
|
||||
|
||||
class OpenSpeckleGuide(bpy.types.Operator):
|
||||
_guide_url = "https://speckle.guide/user/blender.html"
|
||||
|
||||
bl_idname = "speckle.open_speckle_guide"
|
||||
bl_label = "Speckle Docs"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = f"Browse the documentation on the Speckle Guide ({_guide_url})"
|
||||
|
||||
def execute(self, context):
|
||||
webbrowser.open(self._guide_url)
|
||||
metrics.track(
|
||||
"Connector Action",
|
||||
None,
|
||||
custom_props={
|
||||
"name": "OpenSpeckleGuide"
|
||||
},
|
||||
)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class OpenSpeckleTutorials(bpy.types.Operator):
|
||||
_tutorials_url = "https://speckle.systems/tutorials/"
|
||||
|
||||
bl_idname = "speckle.open_speckle_tutorials"
|
||||
bl_label = "Tutorials Portal"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = f"Visit our tutorials portal for learning resources ({_tutorials_url})"
|
||||
|
||||
def execute(self, context):
|
||||
webbrowser.open(self._tutorials_url)
|
||||
metrics.track(
|
||||
"Connector Action",
|
||||
None,
|
||||
custom_props={
|
||||
"name": "OpenSpeckleTutorials"
|
||||
},
|
||||
)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class OpenSpeckleForum(bpy.types.Operator):
|
||||
_forum_url = "https://speckle.community/"
|
||||
|
||||
bl_idname = "speckle.open_speckle_forum"
|
||||
bl_label = "Community Forum"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = f"Ask questions and join the discussion on our community forum ({_forum_url})"
|
||||
|
||||
def execute(self, context):
|
||||
webbrowser.open(self._forum_url)
|
||||
metrics.track(
|
||||
"Connector Action",
|
||||
None,
|
||||
custom_props={
|
||||
"name": "OpenSpeckleForum"
|
||||
},
|
||||
)
|
||||
return {"FINISHED"}
|
||||
@@ -0,0 +1,58 @@
|
||||
import bpy
|
||||
import webbrowser
|
||||
|
||||
class SPECKLE_OT_model_card_settings(bpy.types.Operator):
|
||||
"""
|
||||
Operator for managing model card settings.
|
||||
|
||||
This operator provides functionality to view and modify settings
|
||||
for a specific model card.
|
||||
"""
|
||||
bl_idname = "speckle.model_card_settings"
|
||||
bl_label = "Model Card Settings"
|
||||
bl_description = "Settings for the model card"
|
||||
model_name: bpy.props.StringProperty()
|
||||
|
||||
def execute(self, context):
|
||||
self.report({'INFO'}, f"Settings for {self.model_name}")
|
||||
return {'FINISHED'}
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
# Add a button for viewing 3d model in the browser
|
||||
layout.operator("speckle.view_in_browser", text="View in Browser")
|
||||
# Add a button for viewing model versions in the browser
|
||||
layout.operator("speckle.view_model_versions", text="View Model Versions")
|
||||
def invoke(self, context, event):
|
||||
wm = context.window_manager
|
||||
return wm.invoke_props_dialog(self)
|
||||
|
||||
# Operator for viewing the model in the browser
|
||||
class SPECKLE_OT_view_in_browser(bpy.types.Operator):
|
||||
"""
|
||||
Operator for viewing the model in the browser.
|
||||
"""
|
||||
bl_idname = "speckle.view_in_browser"
|
||||
bl_label = "View in Browser"
|
||||
bl_description = "View the model in the browser"
|
||||
|
||||
def execute(self, context):
|
||||
# TODO: Update this to model URL
|
||||
webbrowser.open(f"https://speckle.guide")
|
||||
self.report({'INFO'}, f"Viewing in the browser")
|
||||
return {'FINISHED'}
|
||||
|
||||
# Operator for viewing the model versions in the browser
|
||||
class SPECKLE_OT_view_model_versions(bpy.types.Operator):
|
||||
"""
|
||||
Operator for viewing the model versions in the browser.
|
||||
"""
|
||||
bl_idname = "speckle.view_model_versions"
|
||||
bl_label = "View Model Versions"
|
||||
bl_description = "View the model versions in the browser"
|
||||
|
||||
def execute(self, context):
|
||||
# TODO: Update this to model versions URL
|
||||
webbrowser.open(f"https://speckle.guide")
|
||||
self.report({'INFO'}, f"Viewing model's versions in the browser")
|
||||
return {'FINISHED'}
|
||||
@@ -1,366 +0,0 @@
|
||||
"""
|
||||
Object operators
|
||||
"""
|
||||
|
||||
import bpy
|
||||
from bpy.props import BoolProperty, EnumProperty
|
||||
from deprecated import deprecated
|
||||
from bpy_speckle.convert.to_speckle import (
|
||||
convert_to_speckle,
|
||||
ngons_to_speckle_polylines,
|
||||
)
|
||||
from bpy_speckle.functions import get_scale_length, _report
|
||||
from bpy_speckle.clients import speckle_clients
|
||||
from specklepy.logging import metrics
|
||||
|
||||
@deprecated
|
||||
class UpdateObject(bpy.types.Operator):
|
||||
"""
|
||||
Update local (receive) or remote (send) object depending on
|
||||
the update direction. If sending, updates the object on the
|
||||
server in-place.
|
||||
"""
|
||||
|
||||
bl_idname = "speckle.update_object"
|
||||
bl_label = "Update Object (DEPRECATED)"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
client = None
|
||||
|
||||
def execute(self, context):
|
||||
user = context.scene.speckle.users[int(context.scene.speckle.active_user)]
|
||||
client = speckle_clients[int(context.scene.speckle.active_user)]
|
||||
|
||||
active = context.active_object
|
||||
_report(active)
|
||||
|
||||
if active is not None and active.speckle.enabled:
|
||||
if active.speckle.send_or_receive == "send" and active.speckle.stream_id:
|
||||
sstream = client.streams.get(active.speckle.stream_id)
|
||||
# res = client.StreamGetAsync(active.speckle.stream_id)['resource']
|
||||
# res = client.streams.get(active.speckle.stream_id)
|
||||
|
||||
if sstream is None:
|
||||
_report("Getting stream failed.")
|
||||
return {"CANCELLED"}
|
||||
|
||||
stream_units = "Meters"
|
||||
if sstream.baseProperties:
|
||||
stream_units = sstream.baseProperties.units
|
||||
|
||||
scale = context.scene.unit_settings.scale_length / get_scale_length(
|
||||
stream_units
|
||||
)
|
||||
|
||||
sm = convert_to_speckle(active, scale)
|
||||
|
||||
_report("Updating object {}".format(sm["_id"]))
|
||||
client.objects.update(active.speckle.object_id, sm)
|
||||
|
||||
metrics.track(
|
||||
"Connector Action",
|
||||
None,
|
||||
custom_props={
|
||||
"name": "UpdateObject"
|
||||
},
|
||||
)
|
||||
return {"FINISHED"}
|
||||
|
||||
return {"CANCELLED"}
|
||||
return {"CANCELLED"}
|
||||
|
||||
@deprecated
|
||||
class ResetObject(bpy.types.Operator):
|
||||
"""
|
||||
Reset Speckle object settings
|
||||
"""
|
||||
|
||||
bl_idname = "speckle.reset_object"
|
||||
bl_label = "Reset Object (DEPRECATED)"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
|
||||
context.object.speckle.send_or_receive = "send"
|
||||
context.object.speckle.stream_id = ""
|
||||
context.object.speckle.object_id = ""
|
||||
context.object.speckle.enabled = False
|
||||
context.view_layer.update()
|
||||
|
||||
metrics.track(
|
||||
"Connector Action",
|
||||
None,
|
||||
custom_props={
|
||||
"name": "ResetObject"
|
||||
},
|
||||
)
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
@deprecated
|
||||
class DeleteObject(bpy.types.Operator):
|
||||
"""
|
||||
Delete object from the server and update relevant stream
|
||||
"""
|
||||
|
||||
bl_idname = "speckle.delete_object"
|
||||
bl_label = "Delete Object (DEPRECATED)"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
client = speckle_clients[int(context.scene.speckle.active_user)]
|
||||
active = context.object
|
||||
if active.speckle.enabled:
|
||||
res = client.StreamGetAsync(active.speckle.stream_id)
|
||||
existing = [
|
||||
x
|
||||
for x in res["resource"]["objects"]
|
||||
if x["_id"] == active.speckle.object_id
|
||||
]
|
||||
if existing is None:
|
||||
return {"CANCELLED"}
|
||||
new_objects = [
|
||||
x
|
||||
for x in res["resource"]["objects"]
|
||||
if x["_id"] != active.speckle.object_id
|
||||
]
|
||||
|
||||
res = client.GetLayers(active.speckle.stream_id)
|
||||
new_layers = res["resource"]["layers"]
|
||||
new_layers[-1]["objectCount"] = new_layers[-1]["objectCount"] - 1
|
||||
new_layers[-1]["topology"] = "0-%s" % new_layers[-1]["objectCount"]
|
||||
|
||||
res = client.StreamUpdateAsync(
|
||||
{"objects": new_objects, "layers": new_layers}, active.speckle.stream_id
|
||||
)
|
||||
res = client.ObjectDeleteAsync(active.speckle.object_id)
|
||||
|
||||
active.speckle.send_or_receive = "send"
|
||||
active.speckle.stream_id = ""
|
||||
active.speckle.object_id = ""
|
||||
active.speckle.enabled = False
|
||||
context.view_layer.update()
|
||||
|
||||
metrics.track(
|
||||
"Connector Action",
|
||||
None,
|
||||
custom_props={
|
||||
"name": "DeleteObject"
|
||||
},
|
||||
)
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
@deprecated
|
||||
class UploadNgonsAsPolylines(bpy.types.Operator):
|
||||
"""
|
||||
Upload mesh ngon faces as polyline outlines
|
||||
TODO: move to another category of specialized operators and fix to work with API 2.0
|
||||
"""
|
||||
|
||||
bl_idname = "speckle.upload_ngons_as_polylines"
|
||||
bl_label = "Upload Ngons As Polylines (DEPRECATED)"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
clear_stream: BoolProperty(
|
||||
name="Clear stream",
|
||||
default=False,
|
||||
)
|
||||
|
||||
def execute(self, context):
|
||||
active = context.active_object
|
||||
if active is not None and active.type == "MESH":
|
||||
|
||||
user = context.scene.speckle.users[int(context.scene.speckle.active_user)]
|
||||
client = speckle_clients[int(context.scene.speckle.active_user)]
|
||||
stream = user.streams[user.active_stream]
|
||||
|
||||
# scale = context.scene.unit_settings.scale_length / get_scale_length(
|
||||
# stream.units
|
||||
# )
|
||||
scale = 1.0
|
||||
|
||||
sp = ngons_to_speckle_polylines(active, scale)
|
||||
|
||||
if sp is None:
|
||||
return {"CANCELLED"}
|
||||
|
||||
placeholders = []
|
||||
for polyline in sp:
|
||||
|
||||
res = client.objects.create([polyline])
|
||||
|
||||
if res is None:
|
||||
_report(client.me)
|
||||
continue
|
||||
placeholders.extend(res)
|
||||
|
||||
if not placeholders:
|
||||
return {"CANCELLED"}
|
||||
|
||||
# Get list of existing objects in stream and append new object to list
|
||||
_report("Fetching stream...")
|
||||
sstream = client.streams.get(stream.id)
|
||||
|
||||
if self.clear_stream:
|
||||
_report("Clearing stream...")
|
||||
sstream.objects = placeholders
|
||||
N = 0
|
||||
else:
|
||||
sstream.objects.extend(placeholders)
|
||||
|
||||
N = sstream.layers[-1].objectCount
|
||||
if self.clear_stream:
|
||||
N = 0
|
||||
sstream.layers[-1].objectCount = N + len(placeholders)
|
||||
sstream.layers[-1].topology = "0-%s" % (N + len(placeholders))
|
||||
|
||||
res = client.streams.update(sstream.id, sstream)
|
||||
|
||||
# Update view layer
|
||||
context.view_layer.update()
|
||||
_report("Done.")
|
||||
|
||||
metrics.track(
|
||||
"Connector Action",
|
||||
None,
|
||||
custom_props={
|
||||
"name": "UploadNgonsAsPolylines"
|
||||
},
|
||||
)
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context, event):
|
||||
wm = context.window_manager
|
||||
return wm.invoke_props_dialog(self)
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.prop(self, "clear_stream")
|
||||
|
||||
|
||||
def get_custom_speckle_props(self, context):
|
||||
ignore = ["speckle", "cycles", "cycles_visibility"]
|
||||
|
||||
active = context.active_object
|
||||
if not active:
|
||||
return []
|
||||
|
||||
return [(x, "{}".format(x), "") for x in active.keys()]
|
||||
|
||||
@deprecated
|
||||
class SelectIfSameCustomProperty(bpy.types.Operator):
|
||||
"""
|
||||
Select scene objects if they have the same custom property
|
||||
value as the active object
|
||||
"""
|
||||
|
||||
bl_idname = "speckle.select_if_same_custom_props"
|
||||
bl_label = "Select Identical Custom Props (DEPRECATED)"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
custom_prop: EnumProperty(
|
||||
name="Custom properties",
|
||||
description="Available streams associated with user.",
|
||||
items=get_custom_speckle_props,
|
||||
)
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
col = layout.column()
|
||||
col.prop(self, "custom_prop")
|
||||
|
||||
def invoke(self, context, event):
|
||||
wm = context.window_manager
|
||||
return wm.invoke_props_dialog(self)
|
||||
|
||||
def execute(self, context):
|
||||
|
||||
active = context.active_object
|
||||
if not active:
|
||||
return {"CANCELLED"}
|
||||
|
||||
if self.custom_prop not in active.keys():
|
||||
return {"CANCELLED"}
|
||||
|
||||
value = active[self.custom_prop]
|
||||
|
||||
_report(
|
||||
"Looking for '{}' property with a value of '{}'.".format(
|
||||
self.custom_prop, value
|
||||
)
|
||||
)
|
||||
|
||||
for obj in bpy.data.objects:
|
||||
|
||||
if self.custom_prop in obj.keys() and obj[self.custom_prop] == value:
|
||||
obj.select_set(True)
|
||||
else:
|
||||
obj.select_set(False)
|
||||
|
||||
metrics.track(
|
||||
"Connector Action",
|
||||
None,
|
||||
custom_props={
|
||||
"name": "SelectIfSameCustomProperty"
|
||||
},
|
||||
)
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
@deprecated
|
||||
class SelectIfHasCustomProperty(bpy.types.Operator):
|
||||
"""
|
||||
Select scene objects if they have the same custom property
|
||||
as the active object, regardless of the value
|
||||
"""
|
||||
|
||||
bl_idname = "speckle.select_if_has_custom_props"
|
||||
bl_label = "Select Same Custom Prop (DEPRECATED)"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
custom_prop: EnumProperty(
|
||||
name="Custom properties",
|
||||
description="Custom properties yo",
|
||||
items=get_custom_speckle_props,
|
||||
)
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
col = layout.column()
|
||||
col.prop(self, "custom_prop")
|
||||
|
||||
def invoke(self, context, event):
|
||||
wm = context.window_manager
|
||||
return wm.invoke_props_dialog(self)
|
||||
|
||||
def execute(self, context):
|
||||
|
||||
active = context.active_object
|
||||
if not active:
|
||||
return {"CANCELLED"}
|
||||
|
||||
if self.custom_prop not in active.keys():
|
||||
return {"CANCELLED"}
|
||||
|
||||
value = active[self.custom_prop]
|
||||
|
||||
_report("Looking for '{}' property.".format(self.custom_prop))
|
||||
|
||||
for obj in bpy.data.objects:
|
||||
|
||||
if self.custom_prop in obj.keys():
|
||||
obj.select_set(True)
|
||||
else:
|
||||
obj.select_set(False)
|
||||
|
||||
metrics.track(
|
||||
"Connector Action",
|
||||
None,
|
||||
custom_props={
|
||||
"name": "SelectIfHasCustomProperty"
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
return {"FINISHED"}
|
||||
@@ -0,0 +1,18 @@
|
||||
import bpy
|
||||
|
||||
# Publish Operator
|
||||
class SPECKLE_OT_publish(bpy.types.Operator):
|
||||
bl_idname = "speckle.publish"
|
||||
|
||||
bl_label = "Publish to Speckle"
|
||||
bl_description = "Publish selected objects to Speckle"
|
||||
|
||||
def invoke(self, context, event):
|
||||
context.scene.speckle_mouse_position = (event.mouse_x, event.mouse_y)
|
||||
return self.execute(context)
|
||||
|
||||
def execute(self, context):
|
||||
context.scene.speckle_ui_mode = "PUBLISH"
|
||||
self.report({'INFO'}, f"Publish button clicked at {context.scene.speckle_mouse_position[0], context.scene.speckle_mouse_position[1]}")
|
||||
bpy.ops.speckle.project_selection_dialog("INVOKE_DEFAULT")
|
||||
return {'FINISHED'}
|
||||
@@ -1,864 +0,0 @@
|
||||
"""
|
||||
Stream operators
|
||||
"""
|
||||
from math import radians
|
||||
from typing import Callable, Dict, Optional, Tuple, Union, cast
|
||||
import webbrowser
|
||||
import bpy
|
||||
from bpy.props import (
|
||||
StringProperty,
|
||||
BoolProperty,
|
||||
EnumProperty,
|
||||
)
|
||||
from bpy.types import (
|
||||
Context,
|
||||
Object,
|
||||
Collection
|
||||
)
|
||||
from deprecated import deprecated
|
||||
from bpy_speckle.blender_commit_object_builder import BlenderCommitObjectBuilder
|
||||
from bpy_speckle.convert.to_native import (
|
||||
can_convert_to_native,
|
||||
collection_to_native,
|
||||
convert_to_native,
|
||||
set_convert_instances_as,
|
||||
)
|
||||
from bpy_speckle.convert.to_speckle import (
|
||||
convert_to_speckle,
|
||||
)
|
||||
from bpy_speckle.functions import (
|
||||
get_default_traversal_func,
|
||||
_report,
|
||||
get_scale_length,
|
||||
)
|
||||
from bpy_speckle.clients import speckle_clients
|
||||
from bpy_speckle.operators.users import LoadUserStreams, add_user_stream
|
||||
from bpy_speckle.properties.scene import SpeckleSceneSettings, SpeckleStreamObject, SpeckleUserObject, get_speckle, selection_state
|
||||
from bpy_speckle.convert.util import ConversionSkippedException, add_to_hierarchy
|
||||
from specklepy.core.api.models import Commit
|
||||
from specklepy.core.api import operations, host_applications
|
||||
from specklepy.core.api.wrapper import StreamWrapper
|
||||
from specklepy.core.api.resources.stream import Stream
|
||||
from specklepy.transports.server import ServerTransport
|
||||
from specklepy.objects import Base
|
||||
from specklepy.objects.other import Collection as SCollection
|
||||
from specklepy.logging.exceptions import SpeckleException
|
||||
from specklepy.logging import metrics
|
||||
|
||||
ObjectCallback = Optional[Callable[[bpy.types.Context, Object, Base], Object]]
|
||||
ReceiveCompleteCallback = Optional[Callable[[bpy.types.Context, Dict[str, Union[Object, Collection]]], None]]
|
||||
|
||||
def get_receive_funcs(speckle: SpeckleSceneSettings) -> tuple[ObjectCallback, ReceiveCompleteCallback]:
|
||||
"""
|
||||
Fetches the injected callback functions from user specified "Receive Script"
|
||||
"""
|
||||
|
||||
objectCallback: ObjectCallback = None
|
||||
receiveCompleteCallback: ReceiveCompleteCallback = None
|
||||
|
||||
if speckle.receive_script in bpy.data.texts:
|
||||
mod = bpy.data.texts[speckle.receive_script].as_module()
|
||||
if hasattr(mod, "execute_for_each"):
|
||||
objectCallback = mod.execute_for_each #type: ignore
|
||||
elif hasattr(mod, "execute"):
|
||||
objectCallback = lambda c, o, _ : mod.execute(c.scene, o) #type: ignore
|
||||
|
||||
if hasattr(mod, "execute_for_all"):
|
||||
receiveCompleteCallback = mod.execute_for_all #type: ignore
|
||||
|
||||
return (objectCallback, receiveCompleteCallback)
|
||||
|
||||
#RECEIVE_MODES = [#TODO: modes
|
||||
# ("create", "Create", "Add new geometry, without removing any existing objects"),
|
||||
# ("replace", "Replace", "Replace objects from previous receive operations from the same stream"),
|
||||
# #("update","Update", "") #TODO: update mode!
|
||||
#]
|
||||
|
||||
INSTANCES_SETTINGS = [
|
||||
("collection_instance", "Collection Instance", "Receive Instances as Collection Instances"),
|
||||
("linked_duplicates", "Linked Duplicates", "Receive Instances as Linked Duplicates"),
|
||||
]
|
||||
|
||||
class ReceiveStreamObjects(bpy.types.Operator):
|
||||
"""
|
||||
Receive objects from selected model version
|
||||
"""
|
||||
|
||||
bl_idname = "speckle.receive_stream_objects"
|
||||
bl_label = "Receive"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Receive objects from selected model version"
|
||||
|
||||
clean_meshes: BoolProperty(name="Clean Meshes", default=False) # type: ignore
|
||||
|
||||
#receive_mode: EnumProperty(items=RECEIVE_MODES, name="Receive Type", default="replace", description="The behaviour of the receive operation")
|
||||
receive_instances_as: EnumProperty(items=INSTANCES_SETTINGS, name="Receive Instances As", default="collection_instance", description="How to receive speckle Instances") # type: ignore
|
||||
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
col = layout.column()
|
||||
col.prop(self, "clean_meshes")
|
||||
#col.prop(self, "receive_mode")
|
||||
col.prop(self, "receive_instances_as")
|
||||
|
||||
def invoke(self, context, event):
|
||||
return context.window_manager.invoke_props_dialog(self)
|
||||
|
||||
@staticmethod
|
||||
def clean_converted_meshes(context: bpy.types.Context, convertedObjects: dict[str, Object]):
|
||||
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
|
||||
active = None
|
||||
for obj in convertedObjects.values():
|
||||
if obj.type != 'MESH':
|
||||
continue
|
||||
|
||||
obj.select_set(True, view_layer=context.scene.view_layers[0])
|
||||
active = obj
|
||||
|
||||
|
||||
if active == None:
|
||||
return
|
||||
context.view_layer.objects.active = active
|
||||
|
||||
bpy.ops.object.mode_set(mode='EDIT')
|
||||
bpy.ops.mesh.select_all(action='SELECT')
|
||||
bpy.ops.mesh.dissolve_limited(angle_limit=radians(0.1))
|
||||
|
||||
# Reset state to previous (not quite sure if this is 100% necessary)
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
bpy.context.view_layer.objects.active = None # type: ignore
|
||||
|
||||
def execute(self, context):
|
||||
self.receive(context)
|
||||
return {"FINISHED"}
|
||||
|
||||
def receive(self, context: Context) -> None:
|
||||
bpy.context.view_layer.objects.active = None # type: ignore
|
||||
|
||||
speckle = get_speckle(context)
|
||||
|
||||
(user, stream, branch, commit) = speckle.validate_commit_selection()
|
||||
|
||||
client = speckle_clients[int(speckle.active_user)]
|
||||
|
||||
transport = ServerTransport(stream.id, client)
|
||||
|
||||
# Fetch commit data
|
||||
commit_object = operations.receive(commit.referenced_object, transport)
|
||||
client.commit.received(
|
||||
stream.id,
|
||||
commit.id,
|
||||
source_application="blender",
|
||||
message="Received model version from Speckle Blender",
|
||||
)
|
||||
|
||||
metrics.track(
|
||||
metrics.RECEIVE,
|
||||
getattr(transport, "account", None),
|
||||
custom_props={
|
||||
"sourceHostApp": host_applications.get_host_app_from_string(commit.source_application).slug,
|
||||
"sourceHostAppVersion": commit.source_application,
|
||||
"isMultiplayer": commit.author_id != user.id,
|
||||
#"connector_version": "unknown", #TODO
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
# Convert received data
|
||||
context.window_manager.progress_begin(0, commit_object.totalChildrenCount or 1)
|
||||
|
||||
set_convert_instances_as(self.receive_instances_as) #HACK: we need a better way to pass settings down to the converter
|
||||
|
||||
traversalFunc = get_default_traversal_func(can_convert_to_native)
|
||||
converted_objects: Dict[str, Union[Object, Collection]] = {}
|
||||
converted_count: int = 0
|
||||
(object_converted_callback, on_complete_callback) = get_receive_funcs(speckle)
|
||||
|
||||
# older commits will have a non-collection root object
|
||||
# for the sake of consistent behaviour, we will wrap any non-collection commit objects in a collection
|
||||
if not isinstance(commit_object, SCollection):
|
||||
dummy_commit_object = SCollection()
|
||||
dummy_commit_object.elements = [commit_object]
|
||||
dummy_commit_object.name = getattr(commit_object, "name", None)
|
||||
dummy_commit_object.id = dummy_commit_object.get_id()
|
||||
commit_object = dummy_commit_object
|
||||
|
||||
# ensure commit object has a name if not already
|
||||
if not commit_object.name:
|
||||
commit_object.name = f"{stream.name} [ {branch.name} @ {commit.id} ]" # Matches Rhino "Create" naming
|
||||
|
||||
for item in traversalFunc.traverse(commit_object):
|
||||
|
||||
current: Base = item.current
|
||||
|
||||
if can_convert_to_native(current) or isinstance(current, SCollection):
|
||||
try:
|
||||
if not current or not current.id:
|
||||
raise Exception(f"{current} was an invalid Speckle object")
|
||||
|
||||
#Convert the object!
|
||||
converted_data_type: str
|
||||
converted: Union[Object, Collection, None]
|
||||
if isinstance(current, SCollection):
|
||||
if(current.collectionType == "Scene Collection"): raise ConversionSkippedException()
|
||||
converted = collection_to_native(current)
|
||||
converted_data_type = "COLLECTION"
|
||||
else:
|
||||
converted = convert_to_native(current)
|
||||
converted_data_type = "COLLECTION_INSTANCE" if converted.instance_collection else str(converted.type)
|
||||
|
||||
#Run the user specified callback function (AKA receive script)
|
||||
if object_converted_callback:
|
||||
converted = object_converted_callback(context, converted, current)
|
||||
|
||||
if converted is None:
|
||||
raise Exception("Conversion returned None")
|
||||
|
||||
converted_objects[current.id] = converted
|
||||
|
||||
add_to_hierarchy(converted, item, converted_objects, True)
|
||||
|
||||
_report(f"Successfully converted {type(current).__name__} {current.id} as '{converted_data_type}'")
|
||||
except ConversionSkippedException as ex:
|
||||
_report(f"Skipped converting {type(current).__name__} {current.id}: {ex}")
|
||||
except Exception as ex:
|
||||
_report(f"Failed to converted {type(current).__name__} {current.id}: {ex}")
|
||||
|
||||
converted_count += 1
|
||||
context.window_manager.progress_update(converted_count) #NOTE: We don't expect to ever reach 100% since not every object will be traversed
|
||||
|
||||
|
||||
context.window_manager.progress_end()
|
||||
|
||||
if self.clean_meshes:
|
||||
objects = {k: v for k, v in converted_objects.items() if isinstance(v, Object)}
|
||||
self.clean_converted_meshes(context, objects)
|
||||
|
||||
if on_complete_callback:
|
||||
on_complete_callback(context, converted_objects)
|
||||
|
||||
|
||||
|
||||
class SendStreamObjects(bpy.types.Operator):
|
||||
"""
|
||||
Send selected objects to selected model
|
||||
"""
|
||||
|
||||
bl_idname = "speckle.send_stream_objects"
|
||||
bl_label = "Send"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Send selected objects to selected model"
|
||||
|
||||
apply_modifiers: BoolProperty(name="Apply modifiers", default=True) # type: ignore
|
||||
commit_message: StringProperty(
|
||||
name="Message",
|
||||
default="Sent elements from Blender.",
|
||||
) # type: ignore
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
col = layout.column()
|
||||
col.prop(self, "commit_message")
|
||||
col.prop(self, "apply_modifiers")
|
||||
|
||||
def invoke(self, context, event):
|
||||
wm = context.window_manager
|
||||
speckle = get_speckle(context)
|
||||
if len(speckle.users) <= 0:
|
||||
_report("No user accounts")
|
||||
return {"CANCELLED"}
|
||||
|
||||
N = len(context.selected_objects)
|
||||
if N == 1:
|
||||
self.commit_message = f"Sent {N} element from Blender."
|
||||
else:
|
||||
self.commit_message = f"Sent {N} elements from Blender."
|
||||
return wm.invoke_props_dialog(self)
|
||||
|
||||
|
||||
def execute(self, context):
|
||||
self.send(context)
|
||||
return {"FINISHED"}
|
||||
|
||||
def send(self, context: Context) -> None:
|
||||
|
||||
selected = context.selected_objects
|
||||
if len(selected) < 1:
|
||||
raise Exception("No objects are selected, sending canceled")
|
||||
|
||||
speckle = get_speckle(context)
|
||||
(user, stream, branch) = speckle.validate_branch_selection()
|
||||
|
||||
client = speckle_clients[int(speckle.active_user)]
|
||||
|
||||
units = "m" if bpy.context.scene.unit_settings.system == "METRIC" else "ft"
|
||||
|
||||
units_scale = context.scene.unit_settings.scale_length / get_scale_length(units)
|
||||
|
||||
# Get script from text editor for injection
|
||||
func = None
|
||||
if speckle.send_script in bpy.data.texts:
|
||||
mod = bpy.data.texts[speckle.send_script].as_module()
|
||||
if hasattr(mod, "execute"):
|
||||
func = mod.execute #type: ignore
|
||||
|
||||
num_converted = 0
|
||||
context.window_manager.progress_begin(0, max(len(selected), 1))
|
||||
|
||||
depsgraph = bpy.context.evaluated_depsgraph_get() if self.apply_modifiers else None
|
||||
|
||||
commit_builder = BlenderCommitObjectBuilder()
|
||||
for obj in selected:
|
||||
try:
|
||||
# Run injected function
|
||||
new_object = obj
|
||||
if func:
|
||||
new_object = func(context.scene, obj)
|
||||
|
||||
if (new_object is None):
|
||||
raise ConversionSkippedException(f"Script '{func.__module__}' returned None.")
|
||||
|
||||
converted = convert_to_speckle(
|
||||
obj,
|
||||
units_scale,
|
||||
units,
|
||||
depsgraph
|
||||
)
|
||||
|
||||
if not converted:
|
||||
raise Exception("Converter returned None")
|
||||
|
||||
commit_builder.include_object(converted, obj)
|
||||
|
||||
_report(f"Successfully converted '{obj.name_full}' as '{converted.speckle_type}'")
|
||||
except ConversionSkippedException as ex:
|
||||
_report(f"Skipped converting '{obj.name_full}': '{ex}'")
|
||||
except Exception as ex:
|
||||
_report(f"Failed to converted '{obj.name_full}': '{ex}'")
|
||||
|
||||
num_converted += 1
|
||||
context.window_manager.progress_update(num_converted)
|
||||
|
||||
context.window_manager.progress_end()
|
||||
|
||||
commit_object = commit_builder.ensure_collection(context.scene.collection)
|
||||
commit_builder.build_commit_object(commit_object)
|
||||
|
||||
metrics.track(
|
||||
metrics.SEND,
|
||||
client.account,
|
||||
custom_props={
|
||||
"branches": len(stream.branches),
|
||||
#"collaborators": 0, #TODO:
|
||||
"isMain": branch.name == "main",
|
||||
},
|
||||
)
|
||||
|
||||
_report(f"Sending data to {stream.name}")
|
||||
transport = ServerTransport(stream.id, client)
|
||||
OBJECT_ID = operations.send(
|
||||
commit_object,
|
||||
[transport],
|
||||
)
|
||||
|
||||
COMMIT_ID = client.commit.create(
|
||||
stream.id,
|
||||
OBJECT_ID,
|
||||
branch.name,
|
||||
message=self.commit_message,
|
||||
source_application="blender",
|
||||
)
|
||||
|
||||
if client.account.serverInfo.frontend2:
|
||||
sent_url = f"{user.server_url}/projects/{stream.id}/models/{branch.id}@{COMMIT_ID}"
|
||||
else:
|
||||
sent_url = f"{user.server_url}/streams/{stream.id}/commits/{COMMIT_ID}"
|
||||
|
||||
_report(f"Commit Created {sent_url}")
|
||||
|
||||
selection_state.selected_commit_id = COMMIT_ID
|
||||
selection_state.selected_branch_id = branch.id
|
||||
selection_state.selected_stream_id = stream.id
|
||||
selection_state.selected_user_id = user.id
|
||||
|
||||
bpy.ops.speckle.load_user_streams() # refresh loaded commits
|
||||
context.view_layer.update()
|
||||
|
||||
if context.area:
|
||||
context.area.tag_redraw()
|
||||
|
||||
|
||||
|
||||
class ViewStreamDataApi(bpy.types.Operator):
|
||||
bl_idname = "speckle.view_stream_data_api"
|
||||
bl_label = "Open Model in Web"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "View the selected model in the web browser"
|
||||
|
||||
def execute(self, context):
|
||||
self.view_stream_data_api(context)
|
||||
return {"FINISHED"}
|
||||
|
||||
def view_stream_data_api(self, context: Context) -> None:
|
||||
speckle = get_speckle(context)
|
||||
|
||||
url = self._get_url_from_selection(speckle)
|
||||
|
||||
_report(f"Opening {url} in web browser")
|
||||
|
||||
if not webbrowser.open(url, new=2):
|
||||
raise Exception(f"Failed to open model in browser ({url})")
|
||||
|
||||
metrics.track(
|
||||
"Connector Action",
|
||||
None,
|
||||
custom_props={
|
||||
"name": "view_stream_data_api"
|
||||
},
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _get_url_from_selection(speckleScene : SpeckleSceneSettings) -> str:
|
||||
|
||||
client = speckle_clients[int(speckleScene.active_user)]
|
||||
(user, stream) = speckleScene.validate_stream_selection()
|
||||
branch = stream.get_active_branch()
|
||||
commit = branch.get_active_commit() if branch else None
|
||||
|
||||
if client.account.serverInfo.frontend2:
|
||||
server_url = f"{user.server_url}/projects/{stream.id}/"
|
||||
if branch:
|
||||
server_url += f"models/{branch.id}"
|
||||
if commit:
|
||||
server_url += f"@{commit.id}"
|
||||
else:
|
||||
server_url = f"{user.server_url}/streams/{stream.id}/"
|
||||
if commit:
|
||||
server_url += f"commits/{commit.id}"
|
||||
elif branch:
|
||||
server_url += f"branches/{branch.name}"
|
||||
|
||||
return server_url
|
||||
|
||||
class AddStreamFromURL(bpy.types.Operator):
|
||||
"""
|
||||
Add / select an existing project by providing its URL
|
||||
"""
|
||||
|
||||
bl_idname = "speckle.add_stream_from_url"
|
||||
bl_label = "Add Project From URL"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Add / select an existing project by providing its URL"
|
||||
stream_url: StringProperty(
|
||||
name="Project URL", default=""
|
||||
) # type: ignore
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
col = layout.column()
|
||||
col.prop(self, "stream_url")
|
||||
|
||||
def invoke(self, context, event):
|
||||
wm = context.window_manager
|
||||
speckle = get_speckle(context)
|
||||
if len(speckle.users) > 0:
|
||||
return wm.invoke_props_dialog(self)
|
||||
|
||||
return {"CANCELLED"}
|
||||
|
||||
def execute(self, context):
|
||||
self.add_stream_from_url(context)
|
||||
return {"FINISHED"}
|
||||
|
||||
@staticmethod
|
||||
def _get_or_add_stream(user : SpeckleUserObject, stream : Stream) -> Tuple[int, SpeckleStreamObject]:
|
||||
index, b_stream = next(
|
||||
((i, cast(SpeckleStreamObject, s)) for i, s in enumerate(user.streams) if s.id == stream.id),
|
||||
(None, None),
|
||||
)
|
||||
|
||||
if index is not None:
|
||||
assert(b_stream)
|
||||
return (index, b_stream)
|
||||
|
||||
add_user_stream(user, stream)
|
||||
return next(
|
||||
(i, cast(SpeckleStreamObject, s)) for i, s in enumerate(user.streams) if s.id == stream.id
|
||||
)
|
||||
|
||||
|
||||
def add_stream_from_url(self, context: Context) -> None:
|
||||
speckle = get_speckle(context)
|
||||
|
||||
wrapper = StreamWrapper(self.stream_url)
|
||||
user_index = next(
|
||||
(i for i, u in enumerate(speckle.users) if wrapper.host in u.server_url),
|
||||
None,
|
||||
)
|
||||
if user_index is None:
|
||||
raise Exception(f"No user account credentials for {wrapper.host}, have you added your account in Manager?")
|
||||
|
||||
speckle.active_user = str(user_index)
|
||||
user = cast(SpeckleUserObject, speckle.users[user_index])
|
||||
|
||||
client = speckle_clients[user_index]
|
||||
stream = client.stream.get(wrapper.stream_id, branch_limit=LoadUserStreams.branch_limit, commit_limit=LoadUserStreams.commits_limit)
|
||||
if not isinstance(stream, Stream):
|
||||
raise SpeckleException(f"Could not get the requested project {wrapper.stream_id}")
|
||||
|
||||
(index, b_stream) = self._get_or_add_stream(user, stream)
|
||||
user.active_stream = index
|
||||
|
||||
_report(f"Selecting project at index {index} ({b_stream.id} - {b_stream.name})")
|
||||
|
||||
if wrapper.branch_name:
|
||||
b_index = b_stream.branches.find(wrapper.branch_name)
|
||||
b_stream.branch = str(b_index if b_index != -1 else 0)
|
||||
elif wrapper.commit_id:
|
||||
commit = client.commit.get(wrapper.stream_id, wrapper.commit_id)
|
||||
if isinstance(commit, Commit):
|
||||
b_index = b_stream.branches.find(commit.branchName)
|
||||
if b_index == -1:
|
||||
b_index = 0
|
||||
b_stream.branch = str(b_index)
|
||||
c_index = b_stream.branches[b_index].commits.find(commit.id)
|
||||
b_stream.branches[b_index].commit = str(c_index if c_index != -1 else 0)
|
||||
|
||||
# Update view layer
|
||||
context.view_layer.update()
|
||||
|
||||
if context.area:
|
||||
context.area.tag_redraw()
|
||||
|
||||
metrics.track(
|
||||
"Connector Action",
|
||||
client.account,
|
||||
custom_props={
|
||||
"name": "add_stream_from_url"
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
class CreateStream(bpy.types.Operator):
|
||||
"""
|
||||
Create a new Speckle project using the selected user account
|
||||
"""
|
||||
|
||||
bl_idname = "speckle.create_stream"
|
||||
bl_label = "Create Project"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Create a new Speckle project using the selected user account"
|
||||
|
||||
stream_name: StringProperty(name="Project name") # type: ignore
|
||||
stream_description: StringProperty(
|
||||
name="Project description", default="My new project"
|
||||
) # type: ignore
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
col = layout.column()
|
||||
col.prop(self, "stream_name")
|
||||
col.prop(self, "stream_description")
|
||||
|
||||
def invoke(self, context, event):
|
||||
wm = context.window_manager
|
||||
speckle = get_speckle(context)
|
||||
if len(speckle.users) > 0:
|
||||
return wm.invoke_props_dialog(self)
|
||||
|
||||
return {"CANCELLED"}
|
||||
|
||||
def execute(self, context):
|
||||
self.create_stream(context)
|
||||
return {"FINISHED"}
|
||||
|
||||
def create_stream(self, context: Context) -> None:
|
||||
speckle = get_speckle(context)
|
||||
|
||||
user = speckle.validate_user_selection()
|
||||
|
||||
client = speckle_clients[int(speckle.active_user)]
|
||||
|
||||
client.stream.create(
|
||||
name=self.stream_name,
|
||||
description=self.stream_description,
|
||||
is_public=True
|
||||
)
|
||||
|
||||
bpy.ops.speckle.load_user_streams()
|
||||
user.active_stream = user.streams.find(self.stream_name)
|
||||
|
||||
# Update view layer
|
||||
context.view_layer.update()
|
||||
|
||||
if context.area:
|
||||
context.area.tag_redraw()
|
||||
|
||||
metrics.track(
|
||||
"Connector Action",
|
||||
client.account,
|
||||
custom_props={
|
||||
"name": "create_stream"
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@deprecated
|
||||
class DeleteStream(bpy.types.Operator):
|
||||
"""
|
||||
Permanently delete the selected project
|
||||
"""
|
||||
|
||||
bl_idname = "speckle.delete_stream"
|
||||
bl_label = "Delete Project"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Permanently delete the selected project"
|
||||
|
||||
are_you_sure: BoolProperty(
|
||||
name="Confirm",
|
||||
description="⚠ This action will delete your entire stream permanently ⚠",
|
||||
default=False,
|
||||
) # type: ignore
|
||||
|
||||
delete_collection: BoolProperty(name="Delete collection", default=False) # type: ignore
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
col = layout.column()
|
||||
col.prop(self, "are_you_sure")
|
||||
col.prop(self, "delete_collection")
|
||||
|
||||
def invoke(self, context, event):
|
||||
wm = context.window_manager
|
||||
speckle = get_speckle(context)
|
||||
if len(speckle.users) > 0:
|
||||
return wm.invoke_props_dialog(self)
|
||||
|
||||
return {"CANCELLED"}
|
||||
|
||||
def execute(self, context):
|
||||
if not self.are_you_sure:
|
||||
_report(f"Cancelled by user - are_you_sure was {self.are_you_sure}")
|
||||
return {"CANCELLED"}
|
||||
self.are_you_sure = False
|
||||
|
||||
self.delete_stream(context, self.delete_collection)
|
||||
return {"FINISHED"}
|
||||
|
||||
@staticmethod
|
||||
def delete_stream(context: Context, delete_collection: bool) -> None:
|
||||
speckle = get_speckle(context)
|
||||
(_, stream) = speckle.validate_stream_selection()
|
||||
|
||||
client = speckle_clients[int(speckle.active_user)]
|
||||
|
||||
client.stream.delete(id=stream.id)
|
||||
|
||||
if delete_collection:
|
||||
# This may not work anymore since we changed the collection naming...
|
||||
col_name = "SpeckleStream_{}_{}".format(stream.name, stream.id)
|
||||
if col_name in bpy.data.collections:
|
||||
collection = bpy.data.collections[col_name]
|
||||
bpy.data.collections.remove(collection)
|
||||
|
||||
bpy.ops.speckle.load_user_streams()
|
||||
context.view_layer.update()
|
||||
|
||||
if context.area:
|
||||
context.area.tag_redraw()
|
||||
|
||||
metrics.track(
|
||||
"Connector Action",
|
||||
client.account,
|
||||
custom_props={
|
||||
"name": "delete_stream"
|
||||
},
|
||||
)
|
||||
|
||||
@deprecated
|
||||
class SelectOrphanObjects(bpy.types.Operator):
|
||||
"""
|
||||
Select Speckle objects that don't belong to any stream
|
||||
"""
|
||||
|
||||
bl_idname = "speckle.select_orphans"
|
||||
bl_label = "Select Orphaned Objects (DEPRECATED)"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Select Speckle objects that don't belong to any stream"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
|
||||
def execute(self, context):
|
||||
|
||||
for o in context.scene.objects:
|
||||
if (
|
||||
o.speckle.stream_id
|
||||
and o.speckle.stream_id not in context.scene["speckle_streams"]
|
||||
):
|
||||
o.select = True
|
||||
else:
|
||||
o.select = False
|
||||
|
||||
metrics.track(
|
||||
"Connector Action",
|
||||
custom_props={
|
||||
"name": "SelectOrphanObjects"
|
||||
},
|
||||
)
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
class CopyStreamId(bpy.types.Operator):
|
||||
"""
|
||||
Copy the selected project id to clipboard
|
||||
"""
|
||||
|
||||
bl_idname = "speckle.stream_copy_id"
|
||||
bl_label = "Copy Project Id"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Copy the selected project id to clipboard"
|
||||
|
||||
def execute(self, context):
|
||||
self.copy_stream_id(context)
|
||||
return {"FINISHED"}
|
||||
|
||||
def copy_stream_id(self, context) -> None:
|
||||
speckle = get_speckle(context)
|
||||
|
||||
(_, stream) = speckle.validate_stream_selection()
|
||||
bpy.context.window_manager.clipboard = stream.id
|
||||
|
||||
metrics.track(
|
||||
"Connector Action",
|
||||
custom_props={
|
||||
"name": "copy_stream_id"
|
||||
},
|
||||
)
|
||||
|
||||
class CopyCommitId(bpy.types.Operator):
|
||||
"""
|
||||
Copy the selected version id to clipboard
|
||||
"""
|
||||
|
||||
bl_idname = "speckle.commit_copy_id"
|
||||
bl_label = "Copy Version Id"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Copy the selected version id to clipboard"
|
||||
|
||||
def execute(self, context):
|
||||
self.copy_commit_id(context)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
def copy_commit_id(self, context) -> None:
|
||||
speckle = get_speckle(context)
|
||||
|
||||
(_, _, _, commit) = speckle.validate_commit_selection()
|
||||
bpy.context.window_manager.clipboard = commit.id
|
||||
|
||||
metrics.track(
|
||||
"Connector Action",
|
||||
custom_props={
|
||||
"name": "copy_commit_id"
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
|
||||
class CopyModelId(bpy.types.Operator):
|
||||
"""
|
||||
Copy model id to clipboard
|
||||
"""
|
||||
|
||||
bl_idname = "speckle.model_copy_id"
|
||||
bl_label = "Copy model id"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Copy model id to clipboard"
|
||||
|
||||
def execute(self, context):
|
||||
self.copy_model_id(context)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
def copy_model_id(self, context) -> None:
|
||||
speckle = get_speckle(context)
|
||||
|
||||
(_, _, branch) = speckle.validate_branch_selection()
|
||||
|
||||
bpy.context.window_manager.clipboard = branch.id
|
||||
|
||||
metrics.track(
|
||||
"Connector Action",
|
||||
custom_props={
|
||||
"name": "copy_branch_id"
|
||||
},
|
||||
)
|
||||
|
||||
@deprecated
|
||||
class CopyBranchName(bpy.types.Operator):
|
||||
"""
|
||||
Copy branch name to clipboard
|
||||
"""
|
||||
|
||||
bl_idname = "speckle.branch_copy_name"
|
||||
bl_label = "Copy branch name"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Copy branch name to clipboard"
|
||||
|
||||
def execute(self, context):
|
||||
self.copy_branch_id(context)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
def copy_branch_id(self, context) -> None:
|
||||
speckle = get_speckle(context)
|
||||
|
||||
(_, _, branch) = speckle.validate_branch_selection()
|
||||
|
||||
bpy.context.window_manager.clipboard = branch.name
|
||||
|
||||
metrics.track(
|
||||
"Connector Action",
|
||||
custom_props={
|
||||
"name": "copy_branch_id"
|
||||
},
|
||||
)
|
||||
|
||||
@deprecated
|
||||
class SelectOrphanObjects(bpy.types.Operator):
|
||||
"""
|
||||
Select Speckle objects that don't belong to any stream
|
||||
"""
|
||||
|
||||
bl_idname = "speckle.select_orphans"
|
||||
bl_label = "Select orphaned objects"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Select Speckle objects that don't belong to any stream"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
|
||||
def execute(self, context):
|
||||
|
||||
for o in context.scene.objects:
|
||||
if (
|
||||
o.speckle.stream_id
|
||||
and o.speckle.stream_id not in context.scene["speckle_streams"]
|
||||
):
|
||||
o.select = True
|
||||
else:
|
||||
o.select = False
|
||||
|
||||
metrics.track(
|
||||
"Connector Action",
|
||||
custom_props={
|
||||
"name": "SelectOrphanObjects"
|
||||
},
|
||||
)
|
||||
|
||||
return {"FINISHED"}
|
||||
@@ -1,211 +0,0 @@
|
||||
"""
|
||||
User account operators
|
||||
"""
|
||||
from typing import List, cast
|
||||
import bpy
|
||||
from bpy.types import Context
|
||||
from bpy_speckle.functions import _report
|
||||
from bpy_speckle.clients import speckle_clients
|
||||
from bpy_speckle.properties.scene import SpeckleBranchObject, SpeckleCommitObject, SpeckleSceneSettings, SpeckleStreamObject, SpeckleUserObject, get_speckle, restore_selection_state
|
||||
from specklepy.core.api.client import SpeckleClient
|
||||
from specklepy.core.api.models import Stream
|
||||
from specklepy.core.api.credentials import get_local_accounts, Account
|
||||
from specklepy.logging import metrics
|
||||
|
||||
class ResetUsers(bpy.types.Operator):
|
||||
"""
|
||||
Reset loaded users
|
||||
"""
|
||||
|
||||
bl_idname = "speckle.users_reset"
|
||||
bl_label = "Reset Users"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
self.reset_ui(context)
|
||||
|
||||
metrics.track(
|
||||
"Connector Action",
|
||||
None,
|
||||
custom_props={
|
||||
"name": "ResetUsers"
|
||||
},
|
||||
)
|
||||
|
||||
bpy.context.view_layer.update()
|
||||
if context.area:
|
||||
context.area.tag_redraw()
|
||||
return {"FINISHED"}
|
||||
|
||||
@staticmethod
|
||||
def reset_ui(context: Context):
|
||||
speckle = get_speckle(context)
|
||||
|
||||
speckle.users.clear()
|
||||
speckle_clients.clear()
|
||||
|
||||
class LoadUsers(bpy.types.Operator):
|
||||
"""
|
||||
Loads all user accounts from the credentials in the local database.
|
||||
See docs to add accounts via Manager
|
||||
"""
|
||||
|
||||
bl_idname = "speckle.users_load"
|
||||
bl_label = "Load Users"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Loads all user accounts from the credentials in the local database.\nSee docs to add accounts via Manager"
|
||||
|
||||
def execute(self, context):
|
||||
|
||||
_report("Loading users...")
|
||||
|
||||
speckle = get_speckle(context)
|
||||
users_list = speckle.users
|
||||
|
||||
ResetUsers.reset_ui(context)
|
||||
|
||||
profiles = get_local_accounts()
|
||||
active_user_index = 0
|
||||
|
||||
metrics.track(
|
||||
"Connector Action",
|
||||
None,
|
||||
custom_props={
|
||||
"name": "LoadUsers",
|
||||
},
|
||||
)
|
||||
|
||||
if not profiles:
|
||||
raise Exception("Zero accounts were found, please add one through Speckle Manager or a local account")
|
||||
|
||||
for profile in profiles:
|
||||
try:
|
||||
add_user_account(profile, speckle)
|
||||
except Exception as ex:
|
||||
_report(f"Failed to authenticate user account {profile.userInfo.email} with server {profile.serverInfo.url}: {ex}")
|
||||
users_list.remove(len(users_list) - 1)
|
||||
continue
|
||||
|
||||
if profile.isDefault:
|
||||
active_user_index = len(users_list) - 1
|
||||
|
||||
_report(f"Authenticated {len(users_list)}/{len(profiles)} accounts")
|
||||
|
||||
if active_user_index < len(users_list):
|
||||
speckle.active_user = str(active_user_index)
|
||||
|
||||
bpy.context.view_layer.update()
|
||||
|
||||
if context.area:
|
||||
context.area.tag_redraw()
|
||||
|
||||
if not users_list:
|
||||
raise Exception("Zero valid user accounts were found, please ensure account is valid and the server is running")
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
def add_user_account(account: Account, speckle: SpeckleSceneSettings) -> SpeckleUserObject:
|
||||
"""Creates a new new SpeckleUserObject for the provided user Account and adds it to the SpeckleSceneSettings"""
|
||||
users_list = speckle.users
|
||||
|
||||
URL = account.serverInfo.url
|
||||
|
||||
user = cast(SpeckleUserObject, users_list.add())
|
||||
user.server_name = account.serverInfo.name or "Speckle Server"
|
||||
user.server_url = URL
|
||||
user.id = account.userInfo.id
|
||||
user.name = account.userInfo.name
|
||||
user.email = account.userInfo.email
|
||||
user.company = account.userInfo.company or ""
|
||||
|
||||
assert(URL)
|
||||
client = SpeckleClient(
|
||||
host=URL,
|
||||
use_ssl="https" in URL,
|
||||
)
|
||||
client.authenticate_with_account(account)
|
||||
speckle_clients.append(client)
|
||||
return user
|
||||
|
||||
|
||||
def add_user_stream(user: SpeckleUserObject, stream: Stream):
|
||||
"""Adds the provided Stream (with branch & commits) to the SpeckleUserObject"""
|
||||
s = cast(SpeckleStreamObject, user.streams.add())
|
||||
s.name = stream.name
|
||||
s.id = stream.id
|
||||
s.description = stream.description
|
||||
|
||||
_report(f"Adding stream {s.id} - {s.name}")
|
||||
|
||||
if stream.branches:
|
||||
s.load_stream_branches(stream)
|
||||
|
||||
|
||||
class LoadUserStreams(bpy.types.Operator):
|
||||
"""
|
||||
(Re)Load all available projects for active user
|
||||
"""
|
||||
|
||||
bl_idname = "speckle.load_user_streams"
|
||||
bl_label = "Load User's Projects"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "(Re)Load all available projects for active user"
|
||||
|
||||
stream_limit: int = 20
|
||||
branch_limit: int = 100
|
||||
commits_limit: int = 10
|
||||
|
||||
def execute(self, context):
|
||||
self.load_user_stream(context)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
def load_user_stream(self, context: Context) -> None:
|
||||
speckle = get_speckle(context)
|
||||
|
||||
user = speckle.validate_user_selection()
|
||||
|
||||
client = speckle_clients[int(speckle.active_user)]
|
||||
try:
|
||||
streams = client.stream.list(stream_limit=self.stream_limit)
|
||||
except Exception as ex:
|
||||
raise Exception(f"Failed to retrieve projects") from ex
|
||||
|
||||
if not streams:
|
||||
_report("Zero projects found")
|
||||
return
|
||||
|
||||
|
||||
active_stream_id = None
|
||||
if active_stream := user.get_active_stream():
|
||||
active_stream_id = active_stream.id
|
||||
elif len(user.streams) > 0:
|
||||
active_stream_id = user.streams[0].id
|
||||
|
||||
user.streams.clear()
|
||||
|
||||
for i, s in enumerate(streams):
|
||||
assert(s.id)
|
||||
load_branches = s.id == active_stream_id if active_stream_id else i == 0
|
||||
if load_branches:
|
||||
sstream = client.stream.get(id=s.id, branch_limit=self.branch_limit, commit_limit=10)
|
||||
add_user_stream(user, sstream)
|
||||
else:
|
||||
add_user_stream(user, s)
|
||||
|
||||
restore_selection_state(speckle)
|
||||
|
||||
bpy.context.view_layer.update()
|
||||
|
||||
if context.area:
|
||||
context.area.tag_redraw()
|
||||
|
||||
metrics.track(
|
||||
"Connector Action",
|
||||
client.account,
|
||||
custom_props={
|
||||
"name": "LoadUserStreams"
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
from .scene import (
|
||||
SpeckleSceneSettings,
|
||||
SpeckleSceneObject,
|
||||
SpeckleUserObject,
|
||||
SpeckleStreamObject,
|
||||
SpeckleBranchObject,
|
||||
SpeckleCommitObject,
|
||||
)
|
||||
from .object import SpeckleObjectSettings
|
||||
from .collection import SpeckleCollectionSettings
|
||||
from .addon import SpeckleAddonPreferences
|
||||
|
||||
property_classes = [
|
||||
SpeckleSceneObject,
|
||||
SpeckleCommitObject,
|
||||
SpeckleBranchObject,
|
||||
SpeckleStreamObject,
|
||||
SpeckleUserObject,
|
||||
SpeckleSceneSettings,
|
||||
SpeckleObjectSettings,
|
||||
SpeckleCollectionSettings,
|
||||
SpeckleAddonPreferences,
|
||||
]
|
||||
@@ -1,17 +0,0 @@
|
||||
"""
|
||||
Addon properties
|
||||
"""
|
||||
import bpy
|
||||
|
||||
|
||||
class SpeckleAddonPreferences(bpy.types.AddonPreferences):
|
||||
"""
|
||||
Add-on preferences
|
||||
TODO: add any preferences that might be relevant here
|
||||
"""
|
||||
|
||||
bl_idname = __package__
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.label(text="SpeckleBlender preferences")
|
||||
@@ -1,18 +0,0 @@
|
||||
"""
|
||||
Collection properties
|
||||
"""
|
||||
import bpy
|
||||
|
||||
|
||||
class SpeckleCollectionSettings(bpy.types.PropertyGroup):
|
||||
enabled: bpy.props.BoolProperty(default=False, name="Enabled") # type: ignore
|
||||
|
||||
send_or_receive: bpy.props.EnumProperty(
|
||||
name="Mode",
|
||||
items=(
|
||||
("send", "Send", "Send data to Speckle server."),
|
||||
("receive", "Receive", "Receive data from Speckle server."),
|
||||
),
|
||||
) # type: ignore
|
||||
stream_id: bpy.props.StringProperty(default="") # type: ignore
|
||||
name: bpy.props.StringProperty(default="") # type: ignore
|
||||
@@ -1,18 +0,0 @@
|
||||
"""
|
||||
Object properties
|
||||
"""
|
||||
import bpy
|
||||
|
||||
|
||||
class SpeckleObjectSettings(bpy.types.PropertyGroup):
|
||||
enabled: bpy.props.BoolProperty(default=False, name="Enabled")
|
||||
|
||||
send_or_receive: bpy.props.EnumProperty(
|
||||
name="Mode",
|
||||
items=(
|
||||
("send", "Send", "Send data to Speckle server."),
|
||||
("receive", "Receive", "Receive data from Speckle server."),
|
||||
),
|
||||
) # type: ignore
|
||||
stream_id: bpy.props.StringProperty(default="") # type: ignore
|
||||
object_id: bpy.props.StringProperty(default="") # type: ignore
|
||||
@@ -1,311 +0,0 @@
|
||||
"""
|
||||
Scene properties
|
||||
"""
|
||||
from typing import Iterable, Optional, Tuple, Union, cast
|
||||
from dataclasses import dataclass
|
||||
import bpy
|
||||
from bpy.props import (
|
||||
StringProperty,
|
||||
FloatProperty,
|
||||
CollectionProperty,
|
||||
EnumProperty,
|
||||
IntProperty,
|
||||
)
|
||||
|
||||
from bpy_speckle.clients import speckle_clients
|
||||
from specklepy.core.api.models import Stream
|
||||
|
||||
class SpeckleSceneObject(bpy.types.PropertyGroup):
|
||||
name: bpy.props.StringProperty(default="") # type: ignore
|
||||
|
||||
|
||||
class SpeckleCommitObject(bpy.types.PropertyGroup):
|
||||
id: StringProperty(default="") # type: ignore
|
||||
message: StringProperty(default="") # type: ignore
|
||||
author_name: StringProperty(default="") # type: ignore
|
||||
author_id: StringProperty(default="") # type: ignore
|
||||
created_at: StringProperty(default="") # type: ignore
|
||||
source_application: StringProperty(default="") # type: ignore
|
||||
referenced_object: StringProperty(default="") # type: ignore
|
||||
|
||||
|
||||
class SpeckleBranchObject(bpy.types.PropertyGroup):
|
||||
def get_commits(self, context):
|
||||
if self.commits != None and len(self.commits) > 0:
|
||||
COMMITS = cast(Iterable[SpeckleCommitObject], self.commits)
|
||||
return [
|
||||
(str(i), commit.id, commit.message, i)
|
||||
for i, commit in enumerate(COMMITS)
|
||||
]
|
||||
return [("0", "<none>", "<none>", 0)]
|
||||
|
||||
def commit_update_hook(self, context: bpy.types.Context):
|
||||
selection_state.selected_commit_id = SelectionState.get_item_id_by_index(self.commits, self.commit)
|
||||
selection_state.selected_branch_id = self.id
|
||||
# print(f"commit_update_hook: {selection_state.selected_commit_id=}, {selection_state.selected_branch_id=}")
|
||||
|
||||
name: StringProperty(default="main") # type: ignore
|
||||
id: StringProperty(default="") # type: ignore
|
||||
description: StringProperty(default="") # type: ignore
|
||||
commits: CollectionProperty(type=SpeckleCommitObject) # type: ignore
|
||||
commit: EnumProperty(
|
||||
name="Version",
|
||||
description="Selected model version",
|
||||
items=get_commits,
|
||||
update=commit_update_hook,
|
||||
) # type: ignore
|
||||
|
||||
def get_active_commit(self) -> Optional[SpeckleCommitObject]:
|
||||
selected_index = int(self.commit)
|
||||
if 0 <= selected_index < len(self.commits):
|
||||
return self.commits[selected_index]
|
||||
return None
|
||||
|
||||
class SpeckleStreamObject(bpy.types.PropertyGroup):
|
||||
def load_stream_branches(self, sstream: Stream):
|
||||
self.branches.clear()
|
||||
# branches = [branch for branch in stream.branches.items if branch.name != "globals"]
|
||||
for b in sstream.branches.items:
|
||||
branch = cast(SpeckleBranchObject, self.branches.add())
|
||||
branch.name = b.name
|
||||
branch.id = b.id
|
||||
branch.description = b.description or ""
|
||||
|
||||
if not b.commits:
|
||||
continue
|
||||
|
||||
for c in b.commits.items:
|
||||
commit: SpeckleCommitObject = branch.commits.add()
|
||||
commit.id = commit.name = c.id
|
||||
commit.message = c.message or ""
|
||||
commit.author_name = c.authorName
|
||||
commit.author_id = c.authorId
|
||||
commit.created_at = c.createdAt.strftime("%Y-%m-%d %H:%M:%S.%f%Z") if c.createdAt else ""
|
||||
commit.source_application = str(c.sourceApplication)
|
||||
commit.referenced_object = c.referencedObject
|
||||
|
||||
def get_branches(self, context):
|
||||
if self.branches:
|
||||
BRANCHES = cast(Iterable[SpeckleBranchObject], self.branches)
|
||||
return [
|
||||
(str(i), branch.name, branch.description, i)
|
||||
for i, branch in enumerate(BRANCHES)
|
||||
if branch.name != "globals"
|
||||
]
|
||||
return [("0", "<none>", "<none>", 0)]
|
||||
|
||||
def branch_update_hook(self, context: bpy.types.Context):
|
||||
selection_state.selected_branch_id = SelectionState.get_item_id_by_index(self.branches, self.branch)
|
||||
# print(f"branch_update_hook: {selection_state.selected_branch_id=}, {selection_state.selected_stream_id=}")
|
||||
|
||||
name: StringProperty(default="") # type: ignore
|
||||
description: StringProperty(default="") # type: ignore
|
||||
id: StringProperty(default="") # type: ignore
|
||||
branches: CollectionProperty(type=SpeckleBranchObject) # type: ignore
|
||||
branch: EnumProperty(
|
||||
name="Model",
|
||||
description="Selected Model",
|
||||
items=get_branches,
|
||||
update=branch_update_hook,
|
||||
) # type: ignore
|
||||
|
||||
def get_active_branch(self) -> Optional[SpeckleBranchObject]:
|
||||
selected_index = int(self.branch)
|
||||
if 0 <= selected_index < len(self.branches):
|
||||
return self.branches[selected_index]
|
||||
return None
|
||||
|
||||
class SpeckleUserObject(bpy.types.PropertyGroup):
|
||||
def fetch_stream_branches(self, context: bpy.types.Context, stream: SpeckleStreamObject):
|
||||
speckle = context.scene.speckle
|
||||
client = speckle_clients[int(speckle.active_user)]
|
||||
sstream = client.stream.get(id=stream.id, branch_limit=100, commit_limit=10) # TODO: refactor magic numbers
|
||||
stream.load_stream_branches(sstream)
|
||||
|
||||
def stream_update_hook(self, context: bpy.types.Context):
|
||||
stream = SelectionState.get_item_by_index(self.streams, self.active_stream)
|
||||
selection_state.selected_stream_id = stream.id
|
||||
# print(f"stream_update_hook: {selection_state.selected_stream_id=}, {selection_state.selected_user_id=}")
|
||||
if len(stream.branches) == 0: # do not reload on selection, same as the old behavior
|
||||
self.fetch_stream_branches(context, stream)
|
||||
|
||||
server_name: StringProperty(default="SpeckleXYZ") # type: ignore
|
||||
server_url: StringProperty(default="https://speckle.xyz") # type: ignore
|
||||
id: StringProperty(default="") # type: ignore
|
||||
name: StringProperty(default="Speckle User") # type: ignore
|
||||
email: StringProperty(default="user@speckle.xyz") # type: ignore
|
||||
company: StringProperty(default="SpeckleSystems") # type: ignore
|
||||
streams: CollectionProperty(type=SpeckleStreamObject) # type: ignore
|
||||
active_stream: IntProperty(default=0, update=stream_update_hook) # type: ignore
|
||||
|
||||
def get_active_stream(self) -> Optional[SpeckleStreamObject]:
|
||||
selected_index = int(self.active_stream)
|
||||
if 0 <= selected_index < len(self.streams):
|
||||
return self.streams[selected_index]
|
||||
return None
|
||||
|
||||
|
||||
class SpeckleSceneSettings(bpy.types.PropertyGroup):
|
||||
def get_scripts(self, context):
|
||||
return [
|
||||
("<none>", "<none>", "<none>"),
|
||||
*[(t.name, t.name, t.name) for t in bpy.data.texts],
|
||||
]
|
||||
|
||||
streams: EnumProperty(
|
||||
name="Available streams",
|
||||
description="Available streams associated with user.",
|
||||
items=[],
|
||||
) # type: ignore
|
||||
|
||||
users: CollectionProperty(type=SpeckleUserObject) # type: ignore
|
||||
|
||||
def get_users(self, context):
|
||||
USERS = cast(Iterable[SpeckleUserObject], self.users)
|
||||
return [
|
||||
(str(i), f"{user.email} ({user.server_name})", user.server_url, i)
|
||||
for i, user in enumerate(USERS)
|
||||
]
|
||||
|
||||
def user_update_hook(self, context):
|
||||
bpy.ops.speckle.load_user_streams() # type: ignore
|
||||
selection_state.selected_user_id = SelectionState.get_item_id_by_index(self.users, self.active_user)
|
||||
|
||||
active_user: EnumProperty(
|
||||
items=get_users,
|
||||
name="Account",
|
||||
description="Select account",
|
||||
update=user_update_hook,
|
||||
get=None,
|
||||
set=None,
|
||||
) # type: ignore
|
||||
|
||||
objects: CollectionProperty(type=SpeckleSceneObject) # type: ignore
|
||||
|
||||
scale: FloatProperty(default=0.001) # type: ignore
|
||||
|
||||
user: StringProperty(
|
||||
name="User",
|
||||
description="Current user",
|
||||
default="Speckle User",
|
||||
) # type: ignore
|
||||
|
||||
receive_script: EnumProperty(
|
||||
name="Receive script",
|
||||
description="Custom py script to execute when receiving objects. See docs for function signature.",
|
||||
items=get_scripts,
|
||||
) # type: ignore
|
||||
|
||||
send_script: EnumProperty(
|
||||
name="Send script",
|
||||
description="Custom py script to execute when sending objects. See docs for function signature",
|
||||
items=get_scripts,
|
||||
) # type: ignore
|
||||
|
||||
def get_active_user(self) -> Optional[SpeckleUserObject]:
|
||||
if self.active_user is None:
|
||||
return None
|
||||
selected_index = int(self.active_user)
|
||||
if 0 <= selected_index < len(self.users):
|
||||
return self.users[selected_index]
|
||||
return None
|
||||
|
||||
|
||||
def validate_user_selection(self) -> SpeckleUserObject:
|
||||
user = self.get_active_user()
|
||||
if not user:
|
||||
raise SelectionException("No user account selected/found")
|
||||
return user
|
||||
|
||||
def validate_stream_selection(self) -> Tuple[SpeckleUserObject, SpeckleStreamObject]:
|
||||
user = self.validate_user_selection()
|
||||
|
||||
stream = user.get_active_stream()
|
||||
if not stream:
|
||||
raise SelectionException("No project selected/found")
|
||||
|
||||
return (user, stream)
|
||||
|
||||
def validate_branch_selection(self) -> Tuple[SpeckleUserObject, SpeckleStreamObject, SpeckleBranchObject]:
|
||||
(user, stream) = self.validate_stream_selection()
|
||||
|
||||
branch = stream.get_active_branch()
|
||||
if not branch:
|
||||
raise SelectionException("No model selected/found")
|
||||
return (user, stream, branch)
|
||||
|
||||
def validate_commit_selection(self) ->Tuple[SpeckleUserObject, SpeckleStreamObject, SpeckleBranchObject, SpeckleCommitObject]:
|
||||
(user, stream, branch) = self.validate_branch_selection()
|
||||
commit = branch.get_active_commit()
|
||||
if commit is None:
|
||||
raise SelectionException("No model version selected/found")
|
||||
|
||||
return (user, stream, branch, commit)
|
||||
|
||||
class SelectionException(Exception):
|
||||
pass
|
||||
|
||||
def get_speckle(context: bpy.types.Context) -> SpeckleSceneSettings:
|
||||
"""
|
||||
Gets the speckle scene object
|
||||
"""
|
||||
return context.scene.speckle #type: ignore
|
||||
|
||||
@dataclass
|
||||
class SelectionState:
|
||||
selected_user_id : Optional[str] = None
|
||||
selected_stream_id : Optional[str] = None
|
||||
selected_branch_id : Optional[str] = None
|
||||
selected_commit_id : Optional[str] = None
|
||||
|
||||
@staticmethod
|
||||
def get_item_id_by_index(collection: bpy.types.PropertyGroup, index: Union[str, int]) -> Optional[str]:
|
||||
if item := SelectionState.get_item_by_index(collection, index):
|
||||
return item.id
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def get_item_by_index(collection: bpy.types.PropertyGroup, index: Union[str, int]) -> Optional[bpy.types.PropertyGroup]:
|
||||
items = collection.values()
|
||||
i = int(index)
|
||||
if 0 <= i <= len(items):
|
||||
return items[i]
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def get_item_index_by_id(collection: Iterable[SpeckleCommitObject], id: Optional[str]) -> Optional[str]:
|
||||
for index, item in enumerate(collection):
|
||||
if item.id == id:
|
||||
return str(index)
|
||||
return None
|
||||
|
||||
selection_state = SelectionState()
|
||||
|
||||
def restore_selection_state(speckle: SpeckleSceneSettings) -> None:
|
||||
# Restore branch selection state
|
||||
if selection_state.selected_branch_id != None:
|
||||
(active_user, active_stream) = speckle.validate_stream_selection()
|
||||
# print(f"restore_selection_state: {active_user.id=}, {active_stream.id=}")
|
||||
# print(f"restore_selection_state: {selection_state.selected_user_id=}, {selection_state.selected_stream_id=}, {selection_state.selected_branch_id=}, {selection_state.selected_commit_id=}")
|
||||
|
||||
is_same_user = active_user.id == selection_state.selected_user_id
|
||||
|
||||
if is_same_user:
|
||||
active_user.active_stream = int(SelectionState.get_item_index_by_id(active_user.streams, selection_state.selected_stream_id))
|
||||
active_stream = SelectionState.get_item_by_index(active_user.streams, active_user.active_stream)
|
||||
if branch := SelectionState.get_item_index_by_id(active_stream.branches, selection_state.selected_branch_id):
|
||||
active_stream.branch = branch
|
||||
|
||||
# Restore commit selection state
|
||||
if selection_state.selected_commit_id != None:
|
||||
(active_user, active_stream, active_branch) = speckle.validate_branch_selection()
|
||||
# print(f"restore_selection_state: {active_user.id=}, {active_stream.id=}, {active_branch.id=}")
|
||||
# print(f"restore_selection_state: {selection_state.selected_user_id=}, {selection_state.selected_stream_id=}, {selection_state.selected_branch_id=}, {selection_state.selected_commit_id=}")
|
||||
|
||||
is_same_user = active_user.id == selection_state.selected_user_id
|
||||
is_same_stream = active_stream.id == selection_state.selected_stream_id
|
||||
is_same_branch = active_branch.id == selection_state.selected_branch_id
|
||||
|
||||
if is_same_user and is_same_stream and is_same_branch:
|
||||
if commit := SelectionState.get_item_index_by_id(active_branch.commits, selection_state.selected_commit_id):
|
||||
active_branch.commit = commit
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"folders": [
|
||||
{
|
||||
"path": ".."
|
||||
},
|
||||
{
|
||||
"name": "speckle_blender_addon",
|
||||
"path": "."
|
||||
}
|
||||
],
|
||||
"settings": {
|
||||
"blender.addon.loadDirectory": "auto",
|
||||
"blender.executables": []
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1 @@
|
||||
from .object import OBJECT_PT_speckle
|
||||
from .view3d import (
|
||||
VIEW3D_UL_SpeckleUsers,
|
||||
VIEW3D_UL_SpeckleStreams,
|
||||
VIEW3D_PT_SpeckleUser,
|
||||
VIEW3D_PT_SpeckleStreams,
|
||||
VIEW3D_PT_SpeckleActiveStream,
|
||||
VIEW3D_PT_SpeckleHelp,
|
||||
)
|
||||
|
||||
ui_classes = [
|
||||
VIEW3D_PT_SpeckleUser,
|
||||
VIEW3D_PT_SpeckleStreams,
|
||||
VIEW3D_PT_SpeckleActiveStream,
|
||||
VIEW3D_UL_SpeckleUsers,
|
||||
VIEW3D_UL_SpeckleStreams,
|
||||
VIEW3D_PT_SpeckleHelp,
|
||||
]
|
||||
from .main_panel import SPECKLE_PT_main_panel
|
||||
@@ -0,0 +1,19 @@
|
||||
import bpy
|
||||
import os
|
||||
import bpy.utils.previews
|
||||
|
||||
speckle_icons = None
|
||||
|
||||
def load_icons():
|
||||
global speckle_icons
|
||||
speckle_icons = bpy.utils.previews.new()
|
||||
icons_dir = os.path.dirname(__file__)
|
||||
speckle_icons.load("speckle_logo", os.path.join(icons_dir, "speckle-logo.png"), 'IMAGE')
|
||||
|
||||
def unload_icons():
|
||||
global speckle_icons
|
||||
bpy.utils.previews.remove(speckle_icons)
|
||||
|
||||
def get_icon(icon_name):
|
||||
global speckle_icons
|
||||
return speckle_icons[icon_name].icon_id
|
||||
@@ -0,0 +1,67 @@
|
||||
import bpy
|
||||
from bpy.types import UILayout, Context
|
||||
from .icons import get_icon
|
||||
from bindings.account_binding import AccountBinding
|
||||
|
||||
# Main Panel
|
||||
class SPECKLE_PT_main_panel(bpy.types.Panel):
|
||||
"""
|
||||
Main panel for the Speckle addon in Blender.
|
||||
|
||||
This panel provides the primary user interface such as buttons for publishing and loading models, and model cards for each model added to the file.
|
||||
"""
|
||||
bl_label = "Speckle"
|
||||
|
||||
bl_idname = "SPECKLE_PT_main_panel"
|
||||
bl_space_type = 'VIEW_3D'
|
||||
bl_region_type = 'UI'
|
||||
bl_category = 'Speckle'
|
||||
|
||||
bindings = {"accountBinding": AccountBinding()}
|
||||
|
||||
def draw(self, context: Context) -> None:
|
||||
layout : UILayout = self.layout
|
||||
layout.label(text="Speckle Connector BETA", icon_value=get_icon("speckle_logo"))
|
||||
|
||||
# for (binding_name, binding) in self.bindings:
|
||||
# context[binding_name] = binding
|
||||
|
||||
# Check to see if there are any speckle models in the file
|
||||
if not context.scene.speckle_model_cards:
|
||||
layout.label(text="Hello!")
|
||||
layout.label(text="There are no Speckle models in this file yet.")
|
||||
|
||||
# Add some space
|
||||
layout.separator()
|
||||
|
||||
# Publish and Load buttons
|
||||
row = layout.row()
|
||||
row.operator("speckle.publish", text="Publish", icon='EXPORT')
|
||||
row.operator("speckle.load", text="Load", icon='IMPORT')
|
||||
|
||||
layout.separator()
|
||||
|
||||
for model_card in context.scene.speckle_model_cards:
|
||||
box = layout.box()
|
||||
row = box.row()
|
||||
icon = 'EXPORT' if model_card.is_publish else 'IMPORT'
|
||||
row.operator("speckle.publish", text="", icon=icon)
|
||||
row.label(text=f"{model_card.model_name} - {model_card.project_name}")
|
||||
row.operator("speckle.model_card_settings", text="", icon='PREFERENCES').model_name = model_card.model_name
|
||||
row = box.row()
|
||||
# Display selection summary or version ID
|
||||
if model_card.is_publish:
|
||||
# This adjusts the layout of the row (button 1/3, label 2/3 )
|
||||
split = row.split(factor=0.33)
|
||||
# TODO: Connect to selection operator
|
||||
split.operator("speckle.publish", text="Selection")
|
||||
split.label(text=f"{model_card.selection_summary}")
|
||||
else:
|
||||
# This adjusts the layout of the row (button 1/3, label 2/3 )
|
||||
split = row.split(factor=0.33)
|
||||
# TODO: Connect to version operator
|
||||
split.operator("speckle.load", text=f"{model_card.version_id}")
|
||||
# TODO: Get last updated time
|
||||
split.label(text="Last updated: 2 days ago")
|
||||
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import bpy
|
||||
|
||||
class speckle_model_card(bpy.types.PropertyGroup):
|
||||
project_name: bpy.props.StringProperty(name="Project Name", description="Name of the project", default="")
|
||||
model_name: bpy.props.StringProperty(name="Model Name", description="Name of the model", default="")
|
||||
is_publish: bpy.props.BoolProperty(name="Publish/Load", description="If the model is published or loaded", default=False)
|
||||
selection_summary: bpy.props.StringProperty(name="Selection Summary", description="Summary of the selection", default="")
|
||||
version_id: bpy.props.StringProperty(name="Version ID", description="ID of the selected version", default="")
|
||||
|
||||
def to_dict(self):
|
||||
return{
|
||||
"project_name" : self.project_name,
|
||||
"model_name" : self.model_name,
|
||||
"is_publish" : self.is_publish,
|
||||
"selection_summary" : self.selection_summary,
|
||||
"version_id" : self.version_id,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data):
|
||||
item = cls()
|
||||
item.project_name = data["project_name"]
|
||||
item.model_name = data["model_name"]
|
||||
item.is_publish = data["is_publish"]
|
||||
item.selection_summary = data["selection_summary"]
|
||||
item.version_id = data["version_id"]
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
import bpy
|
||||
from bpy.types import UILayout, Context, UIList, PropertyGroup, Operator, Event
|
||||
from .mouse_position_mixin import MousePositionMixin
|
||||
|
||||
class speckle_model(bpy.types.PropertyGroup):
|
||||
"""
|
||||
PropertyGroup for storing models.
|
||||
|
||||
This PropertyGroup is used to store information about a model,
|
||||
such as its name, source application, and update time.
|
||||
|
||||
These are then used in the model selection dialog.
|
||||
"""
|
||||
name: bpy.props.StringProperty()
|
||||
source_app: bpy.props.StringProperty(name="Source")
|
||||
updated: bpy.props.StringProperty(name="Updated")
|
||||
|
||||
class SPECKLE_UL_models_list(bpy.types.UIList):
|
||||
"""
|
||||
UIList for displaying a list of models.
|
||||
|
||||
This UIList is used to display a list of models in model selection dialog.
|
||||
"""
|
||||
#TODO: Adjust column widths so name has the most space.
|
||||
def draw_item(self, context: Context, layout: UILayout, data: PropertyGroup, item: PropertyGroup, icon: str, active_data: PropertyGroup, active_propname: str) -> None:
|
||||
if self.layout_type in {'DEFAULT', 'COMPACT'}:
|
||||
row = layout.row(align=True)
|
||||
split = row.split(factor=0.5)
|
||||
split.label(text=item.name)
|
||||
|
||||
right_split = split.split(factor=0.25)
|
||||
right_split.label(text=item.source_app)
|
||||
right_split.label(text=item.updated)
|
||||
# This handles when the list is in a grid layout
|
||||
elif self.layout_type == 'GRID':
|
||||
layout.alignment = 'CENTER'
|
||||
layout.label(text=item.name)
|
||||
|
||||
class SPECKLE_OT_model_selection_dialog(MousePositionMixin, bpy.types.Operator):
|
||||
"""
|
||||
Operator for displaying a dialog for selecting a model.
|
||||
"""
|
||||
bl_idname = "speckle.model_selection_dialog"
|
||||
bl_label = "Select Model"
|
||||
|
||||
search_query: bpy.props.StringProperty(
|
||||
name="Search",
|
||||
description="Search a project",
|
||||
default=""
|
||||
)
|
||||
|
||||
project_name: bpy.props.StringProperty(
|
||||
name="Project Name",
|
||||
description="The name of the project to select",
|
||||
default=""
|
||||
)
|
||||
|
||||
models: list[tuple[str, str, str]] = [
|
||||
("94-workset name", "RVT", "1 day ago"),
|
||||
("296/skp2skp3", "SKP", "16 days ago"),
|
||||
("49/rhn2viewer", "RHN", "21 days ago"),
|
||||
]
|
||||
|
||||
model_index: bpy.props.IntProperty(name="Model Index", default=0)
|
||||
|
||||
def execute(self, context: Context) -> set[str]:
|
||||
selected_model = context.scene.speckle_models[self.model_index]
|
||||
if context.scene.speckle_ui_mode == "PUBLISH":
|
||||
bpy.ops.speckle.selection_filter_dialog("INVOKE_DEFAULT", project_name=self.project_name, model_name=selected_model.name)
|
||||
elif context.scene.speckle_ui_mode == "LOAD":
|
||||
bpy.ops.speckle.version_selection_dialog("INVOKE_DEFAULT", project_name=self.project_name, model_name=selected_model.name)
|
||||
return {'FINISHED'}
|
||||
|
||||
def invoke(self, context: Context, event: Event) -> set[str]:
|
||||
# Clear existing models
|
||||
context.scene.speckle_models.clear()
|
||||
# Populate with new projects
|
||||
for name, source_app, updated in self.models:
|
||||
model = context.scene.speckle_models.add()
|
||||
model.name = name
|
||||
model.source_app = source_app
|
||||
model.updated = updated
|
||||
|
||||
# Store the original mouse position
|
||||
self.init_mouse_position(context, event)
|
||||
|
||||
return context.window_manager.invoke_props_dialog(self)
|
||||
|
||||
def draw(self, context: Context) -> None:
|
||||
layout : UILayout = self.layout
|
||||
layout.label(text=f"Project: {self.project_name}")
|
||||
# Search field
|
||||
row = layout.row(align=True)
|
||||
row.prop(self, "search_query", icon='VIEWZOOM', text="")
|
||||
|
||||
# Models UIList
|
||||
layout.template_list("SPECKLE_UL_models_list", "", context.scene, "speckle_models", self, "model_index")
|
||||
|
||||
layout.separator()
|
||||
|
||||
# Move cursor to original position
|
||||
self.restore_mouse_position(context)
|
||||
@@ -0,0 +1,15 @@
|
||||
import bpy
|
||||
|
||||
class MousePositionMixin:
|
||||
original_mouse_position: bpy.props.IntVectorProperty(size=2)
|
||||
mouse_snap: bpy.props.BoolProperty(name="Mouse Snap", default=False)
|
||||
|
||||
def init_mouse_position(self, context, event):
|
||||
self.original_mouse_position = (event.mouse_x, event.mouse_y)
|
||||
self.mouse_snap = False
|
||||
context.window.cursor_warp(context.scene.speckle_mouse_position[0], context.scene.speckle_mouse_position[1])
|
||||
|
||||
def restore_mouse_position(self, context):
|
||||
if not self.mouse_snap:
|
||||
self.mouse_snap = True
|
||||
context.window.cursor_warp(self.original_mouse_position[0], self.original_mouse_position[1])
|
||||
@@ -1,36 +0,0 @@
|
||||
"""
|
||||
Object UI elements
|
||||
"""
|
||||
|
||||
import bpy
|
||||
from bpy.props import (
|
||||
StringProperty,
|
||||
BoolProperty,
|
||||
FloatProperty,
|
||||
CollectionProperty,
|
||||
EnumProperty,
|
||||
)
|
||||
from deprecated import deprecated
|
||||
|
||||
@deprecated
|
||||
class OBJECT_PT_speckle(bpy.types.Panel):
|
||||
bl_space_type = "PROPERTIES"
|
||||
# bl_idname = 'OBJECT_PT_speckle'
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "object"
|
||||
bl_label = "Speckle"
|
||||
|
||||
def draw_header(self, context):
|
||||
self.layout.prop(context.object.speckle, "enabled", text="")
|
||||
|
||||
def draw(self, context):
|
||||
ob = context.object
|
||||
layout = self.layout
|
||||
layout.active = ob.speckle.enabled
|
||||
col = layout.column()
|
||||
col.prop(ob.speckle, "send_or_receive", expand=True)
|
||||
col.prop(ob.speckle, "stream_id", text="Project ID")
|
||||
col.prop(ob.speckle, "object_id", text="Object ID")
|
||||
col.operator("speckle.update_object", text="Update")
|
||||
col.operator("speckle.reset_object", text="Reset")
|
||||
col.operator("speckle.delete_object", text="Delete")
|
||||
@@ -0,0 +1,128 @@
|
||||
import bpy
|
||||
from bpy.types import UILayout, Context, UIList, PropertyGroup, Operator, Event
|
||||
|
||||
class speckle_project(bpy.types.PropertyGroup):
|
||||
"""
|
||||
PropertyGroup for storing projects.
|
||||
|
||||
This PropertyGroup is used to store information about a project,
|
||||
such as its name, role, and update time.
|
||||
|
||||
This is used in the project selection dialog.
|
||||
"""
|
||||
name: bpy.props.StringProperty()
|
||||
role: bpy.props.StringProperty(name="Role")
|
||||
updated: bpy.props.StringProperty(name="Updated")
|
||||
|
||||
class SPECKLE_UL_projects_list(bpy.types.UIList):
|
||||
"""
|
||||
UIList for displaying a list of projects.
|
||||
|
||||
This UIList is used to display a list of projects in a Blender dialog.
|
||||
This is used in the project selection dialog.
|
||||
"""
|
||||
def draw_item(self, context: Context, layout: UILayout, data: PropertyGroup, item: PropertyGroup, icon: str, active_data: PropertyGroup, active_propname: str) -> None:
|
||||
if self.layout_type in {'DEFAULT', 'COMPACT'}:
|
||||
row = layout.row(align=True)
|
||||
split = row.split(factor=0.5) # This gives project name 1/2
|
||||
split.label(text=item.name)
|
||||
|
||||
right_split = split.split(factor=0.5) # This gives project role and updated the other 1/2 of the row
|
||||
right_split.label(text=item.role)
|
||||
right_split.label(text=item.updated)
|
||||
# This handles when the list is in a grid layout
|
||||
elif self.layout_type == 'GRID':
|
||||
layout.alignment = 'CENTER'
|
||||
layout.label(text=item.name)
|
||||
|
||||
class SPECKLE_OT_project_selection_dialog(bpy.types.Operator):
|
||||
"""
|
||||
Operator for project selection dialog.
|
||||
"""
|
||||
bl_idname = "speckle.project_selection_dialog"
|
||||
bl_label = "Select Project"
|
||||
|
||||
account: bpy.props.EnumProperty(
|
||||
name="Account",
|
||||
description="Select the account to filter projects by",
|
||||
items=[("account1", "Account 1", "Account 1"), ("account2", "Account 2", "Account 2")],
|
||||
default="account1"
|
||||
)
|
||||
|
||||
search_query: bpy.props.StringProperty(
|
||||
name="Search",
|
||||
description="Search a project",
|
||||
default=""
|
||||
)
|
||||
|
||||
projects: list[tuple[str, str, str]] = [
|
||||
("RICK'S PORTAL", "contributor", "6 hours ago"),
|
||||
("[BETA] Revit Tests", "owner", "6 hours ago"),
|
||||
("Community Tickets", "owner", "a day ago"),
|
||||
("Bilal's CNX Testing Space", "owner", "a day ago"),
|
||||
("ArcGIS testing", "contributor", "3 days ago"),
|
||||
]
|
||||
|
||||
project_index: bpy.props.IntProperty(name="Project Index", default=0)
|
||||
|
||||
def execute(self, context: Context) -> set[str]:
|
||||
selected_project = context.scene.speckle_projects[self.project_index]
|
||||
bpy.ops.speckle.model_selection_dialog("INVOKE_DEFAULT", project_name=selected_project.name)
|
||||
return {'FINISHED'}
|
||||
|
||||
def invoke(self, context: Context, event: Event) -> set[str]:
|
||||
# Clear existing projects
|
||||
context.scene.speckle_projects.clear()
|
||||
|
||||
# Populate with new projects
|
||||
for name, role, updated in self.projects:
|
||||
project = context.scene.speckle_projects.add()
|
||||
project.name = name
|
||||
project.role = role
|
||||
project.updated = updated
|
||||
|
||||
return context.window_manager.invoke_props_dialog(self)
|
||||
|
||||
def draw(self, context: Context) -> None:
|
||||
# TODO: Add UI elements here
|
||||
layout : UILayout = self.layout
|
||||
accounts = context.accountBinding.get_accounts()
|
||||
# Account selection
|
||||
# TODO: Connect to Speckle API to get accounts
|
||||
layout.prop(self, "account", text="")
|
||||
|
||||
# Search field
|
||||
row = layout.row(align=True)
|
||||
row.prop(self, "search_query", icon='VIEWZOOM', text="")
|
||||
row.operator("speckle.add_project_by_url", icon='URL', text="")
|
||||
|
||||
# Projects UIList
|
||||
layout.template_list("SPECKLE_UL_projects_list", "", context.scene, "speckle_projects", self, "project_index")
|
||||
|
||||
layout.separator()
|
||||
|
||||
class SPECKLE_OT_add_project_by_url(bpy.types.Operator):
|
||||
"""
|
||||
Operator for adding a 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(
|
||||
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")
|
||||
@@ -0,0 +1,119 @@
|
||||
import bpy
|
||||
from .mouse_position_mixin import MousePositionMixin
|
||||
|
||||
class SPECKLE_OT_selection_filter_dialog(MousePositionMixin, bpy.types.Operator):
|
||||
"""
|
||||
Operator for selecting objects.
|
||||
"""
|
||||
bl_idname = "speckle.selection_filter_dialog"
|
||||
bl_label = "Select Objects"
|
||||
|
||||
selection_type: bpy.props.EnumProperty(
|
||||
name="Selection",
|
||||
items=[
|
||||
("SELECTION", "Selection", "Select objects manually"),
|
||||
],
|
||||
default="SELECTION"
|
||||
)
|
||||
|
||||
project_name: bpy.props.StringProperty(
|
||||
name="Project Name",
|
||||
description="Name of the selected project",
|
||||
default=""
|
||||
)
|
||||
|
||||
model_name: bpy.props.StringProperty(
|
||||
name="Model Name",
|
||||
description="Name of the selected model",
|
||||
default=""
|
||||
)
|
||||
|
||||
def execute(self, context):
|
||||
model_card = context.scene.speckle_model_cards.add()
|
||||
model_card.project_name = self.project_name
|
||||
model_card.model_name = self.model_name
|
||||
model_card.is_publish = True
|
||||
|
||||
# Create the selection summary
|
||||
selected_objects = context.selected_objects
|
||||
total_selected = len(selected_objects)
|
||||
object_types = {}
|
||||
for obj in selected_objects:
|
||||
if obj.type not in object_types:
|
||||
object_types[obj.type] = 1
|
||||
else:
|
||||
object_types[obj.type] += 1
|
||||
|
||||
summary = f"{total_selected} objects - "
|
||||
for obj_type, count in object_types.items():
|
||||
summary += f"{obj_type}: {count}, "
|
||||
|
||||
model_card.selection_summary = summary.strip()
|
||||
return {'FINISHED'}
|
||||
|
||||
def invoke(self, context, event):
|
||||
# Initialize mouse position
|
||||
self.init_mouse_position(context, event)
|
||||
return context.window_manager.invoke_props_dialog(self)
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
|
||||
layout.label(text=f"Project: {self.project_name}")
|
||||
layout.label(text=f"Model: {self.model_name}")
|
||||
|
||||
# Selection dropdown
|
||||
layout.prop(self, "selection_type")
|
||||
layout.separator()
|
||||
|
||||
# Get selected objects
|
||||
selected_objects = context.selected_objects
|
||||
total_selected = len(selected_objects)
|
||||
|
||||
# Create a box for the selection summary
|
||||
box = layout.box()
|
||||
row = box.row()
|
||||
row.label(text="Selection Summary", icon='OUTLINER_OB_GROUP_INSTANCE')
|
||||
row.label(text=f"Total: {total_selected}", icon='OBJECT_DATA')
|
||||
|
||||
# Display object types and counts
|
||||
|
||||
object_types = {}
|
||||
for obj in selected_objects:
|
||||
if obj.type not in object_types:
|
||||
object_types[obj.type] = 1
|
||||
else:
|
||||
object_types[obj.type] += 1
|
||||
|
||||
col = box.column(align=True)
|
||||
for obj_type, count in object_types.items():
|
||||
row = col.row()
|
||||
row.label(text=f"{obj_type}:", icon=self.get_icon_for_type(obj_type))
|
||||
row.label(text=str(count))
|
||||
|
||||
layout.separator()
|
||||
|
||||
# Restore mouse position
|
||||
self.restore_mouse_position(context)
|
||||
|
||||
def get_icon_for_type(self, obj_type):
|
||||
icon_map = {
|
||||
'MESH': 'OUTLINER_OB_MESH',
|
||||
'CURVE': 'OUTLINER_OB_CURVE',
|
||||
'SURFACE': 'OUTLINER_OB_SURFACE',
|
||||
'META': 'OUTLINER_OB_META',
|
||||
'FONT': 'OUTLINER_OB_FONT',
|
||||
'ARMATURE': 'OUTLINER_OB_ARMATURE',
|
||||
'LATTICE': 'OUTLINER_OB_LATTICE',
|
||||
'EMPTY': 'OUTLINER_OB_EMPTY',
|
||||
'GPENCIL': 'OUTLINER_OB_GREASEPENCIL',
|
||||
'CAMERA': 'OUTLINER_OB_CAMERA',
|
||||
'LIGHT': 'OUTLINER_OB_LIGHT',
|
||||
'SPEAKER': 'OUTLINER_OB_SPEAKER',
|
||||
'LIGHT_PROBE': 'OUTLINER_OB_LIGHTPROBE',
|
||||
}
|
||||
return icon_map.get(obj_type, 'OBJECT_DATA')
|
||||
|
||||
def check(self, context):
|
||||
return True # This forces the dialog to redraw
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 445 B |
@@ -0,0 +1,110 @@
|
||||
import bpy
|
||||
from .mouse_position_mixin import MousePositionMixin
|
||||
|
||||
class speckle_version(bpy.types.PropertyGroup):
|
||||
"""
|
||||
PropertyGroup for storing versions.
|
||||
|
||||
This PropertyGroup is used to store information about a version,
|
||||
such as its ID, message, and updated time.
|
||||
|
||||
These are then used in the version selection dialog.
|
||||
"""
|
||||
id: bpy.props.StringProperty(name="ID")
|
||||
message: bpy.props.StringProperty(name="Message")
|
||||
updated: bpy.props.StringProperty(name="Updated")
|
||||
source_app: bpy.props.StringProperty(name="Source")
|
||||
|
||||
class SPECKLE_UL_versions_list(bpy.types.UIList):
|
||||
"""
|
||||
UIList for displaying a list of versions.
|
||||
|
||||
This UIList is used to display a list of versions in the version selection dialog.
|
||||
"""
|
||||
#TODO: Adjust column widths so message has the most space.
|
||||
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
|
||||
if self.layout_type in {'DEFAULT', 'COMPACT'}:
|
||||
row = layout.row(align=True)
|
||||
split = row.split(factor=0.166)
|
||||
split.label(text=item.id)
|
||||
right_split = split.split(factor=0.7)
|
||||
right_split.label(text=item.message)
|
||||
right_split.label(text=item.updated)
|
||||
# This handles when the list is in a grid layout
|
||||
elif self.layout_type == 'GRID':
|
||||
layout.alignment = 'CENTER'
|
||||
layout.label(text=item.id)
|
||||
|
||||
class SPECKLE_OT_version_selection_dialog(MousePositionMixin, bpy.types.Operator):
|
||||
"""
|
||||
Operator for selecting a version.
|
||||
"""
|
||||
bl_idname = "speckle.version_selection_dialog"
|
||||
bl_label = "Select Version"
|
||||
|
||||
search_query: bpy.props.StringProperty(
|
||||
name="Search",
|
||||
description="Search a project",
|
||||
default=""
|
||||
)
|
||||
|
||||
project_name: bpy.props.StringProperty(
|
||||
name="Project Name",
|
||||
description="Name of the selected project",
|
||||
default=""
|
||||
)
|
||||
|
||||
model_name: bpy.props.StringProperty(
|
||||
name="Model Name",
|
||||
description="Name of the selected model",
|
||||
default=""
|
||||
)
|
||||
|
||||
versions = [
|
||||
("648896", "Message 1", "12 day ago"),
|
||||
("658465", "Message 2", "15 days ago"),
|
||||
("154651", "Message 3", "20 days ago"),
|
||||
]
|
||||
|
||||
version_index: bpy.props.IntProperty(name="Model Index", default=0)
|
||||
|
||||
|
||||
def execute(self, context):
|
||||
model_card = context.scene.speckle_model_cards.add()
|
||||
model_card.project_name = self.project_name
|
||||
model_card.model_name = self.model_name
|
||||
model_card.is_publish = False
|
||||
# Store the selected version ID
|
||||
selected_version = context.scene.speckle_versions[self.version_index]
|
||||
model_card.version_id = selected_version.id
|
||||
return {'FINISHED'}
|
||||
|
||||
def invoke(self, context, event):
|
||||
# Clear existing versions
|
||||
context.scene.speckle_versions.clear()
|
||||
# Populate with new versions
|
||||
for id, message, updated in self.versions:
|
||||
version = context.scene.speckle_versions.add()
|
||||
version.id = id
|
||||
version.message = message
|
||||
version.updated = updated
|
||||
|
||||
# Initialize mouse position
|
||||
self.init_mouse_position(context, event)
|
||||
return context.window_manager.invoke_props_dialog(self)
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.label(text=f"Project: {self.project_name}")
|
||||
layout.label(text=f"Model: {self.model_name}")
|
||||
# TODO: Add more UI elements here.
|
||||
# Search field
|
||||
row = layout.row(align=True)
|
||||
row.prop(self, "search_query", icon='VIEWZOOM', text="")
|
||||
# Versions UIList
|
||||
layout.template_list("SPECKLE_UL_versions_list", "", context.scene, "speckle_versions", self, "version_index")
|
||||
|
||||
layout.separator()
|
||||
|
||||
# Restore mouse position
|
||||
self.restore_mouse_position(context)
|
||||
@@ -1,260 +0,0 @@
|
||||
"""
|
||||
Speckle UI elements for the 3d viewport
|
||||
"""
|
||||
|
||||
|
||||
import bpy
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from bpy_speckle.properties.scene import get_speckle
|
||||
|
||||
Region = "TOOLS" if bpy.app.version < (2, 80, 0) else "UI"
|
||||
|
||||
|
||||
def wrap(width, text):
|
||||
"""
|
||||
Split strings into width for
|
||||
wrapping
|
||||
"""
|
||||
lines = []
|
||||
|
||||
arr = text.split()
|
||||
lengthSum = 0
|
||||
|
||||
line = []
|
||||
for var in arr:
|
||||
lengthSum += len(var) + 1
|
||||
if lengthSum <= width:
|
||||
line.append(var)
|
||||
else:
|
||||
lines.append(" ".join(line))
|
||||
line = [var]
|
||||
lengthSum = len(var)
|
||||
|
||||
lines.append(" ".join(line))
|
||||
|
||||
return lines
|
||||
|
||||
|
||||
def get_available_users(self, context):
|
||||
"""
|
||||
Function to populate users list
|
||||
"""
|
||||
return [(a, a, a.name) for a in context.scene.speckle.users]
|
||||
|
||||
|
||||
class VIEW3D_UL_SpeckleUsers(bpy.types.UIList):
|
||||
"""
|
||||
Speckle user list
|
||||
"""
|
||||
|
||||
def draw_item(self, context, layout, data, user, active_data, active_propname):
|
||||
if self.layout_type in {"DEFAULT", "COMPACT"}:
|
||||
if user:
|
||||
# layout.prop(user, "name", text=user.name, emboss=False, icon_value=0)
|
||||
layout.label(
|
||||
text=user.name + " (" + user.email + ")",
|
||||
translate=False,
|
||||
icon_value=0,
|
||||
)
|
||||
else:
|
||||
layout.label(text="", translate=False, icon_value=0)
|
||||
|
||||
elif self.layout_type in {"GRID"}:
|
||||
layout.alignment = "CENTER"
|
||||
layout.label(text="Users", icon_value=0)
|
||||
|
||||
|
||||
class VIEW3D_UL_SpeckleStreams(bpy.types.UIList):
|
||||
"""
|
||||
Speckle projects list
|
||||
"""
|
||||
|
||||
def draw_item(self, context, layout, data, stream, active_data, active_propname):
|
||||
if self.layout_type in {"DEFAULT", "COMPACT"}:
|
||||
if stream:
|
||||
layout.label(
|
||||
text=f"{stream.name} ({stream.id})",
|
||||
translate=False,
|
||||
icon_value=0,
|
||||
)
|
||||
else:
|
||||
layout.label(text=" ", translate=False, icon_value=0)
|
||||
|
||||
elif self.layout_type in {"GRID"}:
|
||||
layout.alignment = "CENTER"
|
||||
layout.label(text="Projects", icon_value=0)
|
||||
|
||||
|
||||
class VIEW3D_PT_SpeckleUser(bpy.types.Panel):
|
||||
"""
|
||||
Speckle Users UI panel in the 3d viewport
|
||||
"""
|
||||
|
||||
bl_space_type = "VIEW_3D"
|
||||
bl_region_type = Region
|
||||
bl_category = "Speckle"
|
||||
bl_context = "objectmode"
|
||||
bl_label = "User Account"
|
||||
|
||||
def draw(self, context):
|
||||
speckle = get_speckle(context)
|
||||
|
||||
layout = self.layout
|
||||
col = layout.column()
|
||||
|
||||
if len(speckle.users) < 1:
|
||||
col.label(text="Refresh to initialise")
|
||||
else:
|
||||
col.prop(speckle, "active_user", text="")
|
||||
user = speckle.users[int(speckle.active_user)]
|
||||
col.label(text=f"{user.server_name} ({user.server_url})")
|
||||
col.label(text=f"{user.name} ({user.email})")
|
||||
|
||||
col.operator("speckle.users_load", text="", icon="FILE_REFRESH")
|
||||
|
||||
class VIEW3D_PT_SpeckleStreams(bpy.types.Panel):
|
||||
"""
|
||||
Speckle projects UI panel in the 3d viewport
|
||||
"""
|
||||
|
||||
bl_space_type = "VIEW_3D"
|
||||
bl_region_type = Region
|
||||
bl_category = "Speckle"
|
||||
bl_context = "objectmode"
|
||||
bl_label = "Projects"
|
||||
|
||||
def draw(self, context):
|
||||
speckle = get_speckle(context)
|
||||
col = self.layout.column()
|
||||
|
||||
if len(speckle.users) < 1:
|
||||
col.label(text="No Projects")
|
||||
else:
|
||||
user = speckle.users[int(speckle.active_user)]
|
||||
col.template_list(
|
||||
"VIEW3D_UL_SpeckleStreams", "", user, "streams", user, "active_stream"
|
||||
)
|
||||
row = col.row(align=True)
|
||||
row.operator("speckle.add_stream_from_url", text="", icon="URL")
|
||||
row.operator("speckle.create_stream", text="", icon="ADD")
|
||||
row.operator("speckle.load_user_streams", text="", icon="FILE_REFRESH")
|
||||
|
||||
|
||||
class VIEW3D_PT_SpeckleActiveStream(bpy.types.Panel):
|
||||
"""
|
||||
Speckle Active Projects UI panel in the 3d viewport
|
||||
"""
|
||||
|
||||
bl_space_type = "VIEW_3D"
|
||||
bl_region_type = Region
|
||||
bl_category = "Speckle"
|
||||
bl_context = "objectmode"
|
||||
bl_label = "Active Project"
|
||||
|
||||
def draw(self, context):
|
||||
speckle = get_speckle(context)
|
||||
col = self.layout.column()
|
||||
|
||||
if len(speckle.users) < 1:
|
||||
col.label(text="No projects")
|
||||
else:
|
||||
user = speckle.validate_user_selection()
|
||||
#user = speckle.users[int(speckle.active_user)]
|
||||
if len(user.streams) < 1:
|
||||
col.label(text="No active project")
|
||||
else:
|
||||
stream = user.streams[user.active_stream]
|
||||
# user.active_stream = min(user.active_stream, len(user.streams) - 1)
|
||||
row = col.row()
|
||||
row.label(text=f"{stream.name} ({stream.id})")
|
||||
row.operator("speckle.stream_copy_id", text="", icon="COPY_ID")
|
||||
col.separator()
|
||||
|
||||
row = col.row()
|
||||
row.prop(stream, "branch", text="Model")
|
||||
row.operator("speckle.model_copy_id", text="", icon="COPY_ID")
|
||||
|
||||
if len(stream.branches) > 0:
|
||||
branch = stream.branches[int(stream.branch)]
|
||||
|
||||
row = col.row()
|
||||
row.prop(branch, "commit", text="Version")
|
||||
row.operator("speckle.commit_copy_id", text="", icon="COPY_ID")
|
||||
|
||||
if len(branch.commits) > 0:
|
||||
commit = branch.commits[int(branch.commit)]
|
||||
area = col.box()
|
||||
area.separator()
|
||||
|
||||
lines = wrap(32, commit.message)
|
||||
for line in lines:
|
||||
row = area.row(align=True)
|
||||
row.alignment = "EXPAND"
|
||||
row.scale_y = 0.4
|
||||
row.label(text=line)
|
||||
area.separator()
|
||||
|
||||
dt = datetime.strptime(
|
||||
commit.created_at, "%Y-%m-%d %H:%M:%S.%f%Z"
|
||||
)
|
||||
col.label(text=f"{dt.ctime()}")
|
||||
col.label(text=f"{commit.author_name} ({commit.author_id})")
|
||||
col.label(text=commit.source_application)
|
||||
else:
|
||||
col.label(text="No models found!")
|
||||
|
||||
col.separator()
|
||||
|
||||
area = col.box()
|
||||
row = area.row()
|
||||
subcol = row.column()
|
||||
subcol.operator("speckle.receive_stream_objects", text="Receive")
|
||||
subcol.prop(speckle, "receive_script", text="")
|
||||
subcol = row.column()
|
||||
subcol.operator("speckle.send_stream_objects", text="Send")
|
||||
subcol.prop(speckle, "send_script", text="")
|
||||
|
||||
col.separator()
|
||||
|
||||
row = col.row(align=True)
|
||||
subcol = row.column()
|
||||
|
||||
col.label(text="Description:")
|
||||
area = col.box()
|
||||
area.separator()
|
||||
|
||||
lines = wrap(32, stream.description)
|
||||
|
||||
for line in lines:
|
||||
row = area.row(align=True)
|
||||
row.alignment = "EXPAND"
|
||||
row.scale_y = 0.4
|
||||
row.label(text=line)
|
||||
|
||||
area.separator()
|
||||
col.separator()
|
||||
col.operator("speckle.view_stream_data_api", text="Open Model in Web")
|
||||
|
||||
|
||||
class VIEW3D_PT_SpeckleHelp(bpy.types.Panel):
|
||||
"""
|
||||
Speckle Help UI panel in the 3d viewport
|
||||
"""
|
||||
|
||||
bl_space_type = "VIEW_3D"
|
||||
bl_region_type = Region
|
||||
bl_category = "Speckle"
|
||||
bl_context = "objectmode"
|
||||
bl_label = "Help"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
col = layout.column()
|
||||
|
||||
col.operator("speckle.open_speckle_guide")
|
||||
col.separator()
|
||||
col.operator("speckle.open_speckle_tutorials")
|
||||
col.separator()
|
||||
col.operator("speckle.open_speckle_forum")
|
||||
@@ -1,54 +0,0 @@
|
||||
def find_key_case_insensitive(data, key, default=None):
|
||||
value = data.get(key)
|
||||
if value:
|
||||
return value
|
||||
|
||||
"""
|
||||
Necessary to find keys where the first character
|
||||
is capitalized
|
||||
"""
|
||||
value = data.get(key[0].upper() + key[1:])
|
||||
if value:
|
||||
return value
|
||||
|
||||
value = data.get(key.upper())
|
||||
if value:
|
||||
return value
|
||||
|
||||
return default
|
||||
|
||||
|
||||
def get_iddata(base, uuid, name, obdata):
|
||||
"""
|
||||
This is taken from the import_3dm add-on:
|
||||
https://github.com/jesterKing/import_3dm
|
||||
# Copyright (c) 2018-2019 Nathan Letwory, Joel Putnam,
|
||||
Tom Svilans
|
||||
|
||||
Get an iddata. If an object with given uuid is found in
|
||||
this .blend use that. Otherwise new up one with base.new,
|
||||
potentially with obdata if that is set
|
||||
"""
|
||||
founditem = None
|
||||
if uuid is not None:
|
||||
for item in base:
|
||||
if item.get("speckle_id", None) == str(uuid):
|
||||
founditem = item
|
||||
break
|
||||
elif name:
|
||||
for item in base:
|
||||
if item.get("name", None) == name:
|
||||
founditem = item
|
||||
break
|
||||
if founditem:
|
||||
theitem = founditem
|
||||
theitem["name"] = name
|
||||
if obdata:
|
||||
theitem.data = obdata
|
||||
else:
|
||||
if obdata:
|
||||
theitem = base.new(name=name, object_data=obdata)
|
||||
else:
|
||||
theitem = base.new(name=name)
|
||||
tag_data(theitem, uuid, name)
|
||||
return theitem
|
||||
Reference in New Issue
Block a user