Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f036109020 | |||
| 86bc2dc590 | |||
| a34b6ad0c2 | |||
| e436949ef9 | |||
| 6d8f4a4a80 | |||
| dabb65427a | |||
| 57ece17e8b | |||
| 4362f737d0 | |||
| b55df58313 | |||
| afa6722253 | |||
| a3d4881578 | |||
| 1af158a5e0 | |||
| 47857a9db0 | |||
| 3b026e6027 | |||
| d572609f75 | |||
| 37032cc7aa | |||
| 6027325878 | |||
| 5ddb2aa052 | |||
| 67a18821cc | |||
| 2688a69286 | |||
| 56216a6137 | |||
| 319cbf8960 | |||
| d7ac6c0b95 |
@@ -41,42 +41,58 @@ Give Speckle a try in no time by:
|
||||
- [](https://speckle.guide/user/blender.html) reference on almost any end-user and developer functionality
|
||||
|
||||
|
||||
# Repo structure
|
||||
# Blender Connector
|
||||
|
||||
The Speckle UI can be found in the 3d viewport toolbar (N), under the Speckle tab.
|
||||
|
||||
Head to the [**📚 documentation**](https://speckle.guide/user/blender.html) for more information.
|
||||
|
||||
## Disclaimer
|
||||
This code is WIP and as such should be used with extreme caution on non-sensitive projects.
|
||||
|
||||
## Installation
|
||||
|
||||
1. Place `bpy_speckle` folder in your `addons` folder. On Windows this is typically `%APPDATA%/Blender Foundation/Blender/2.80/scripts/addons`.
|
||||
2. Go to `Edit->Preferences` (Ctrl + Alt + U)
|
||||
3. Go to the `Add-ons` tab
|
||||
4. Find and enable `SpeckleBlender 2.0` in the `Scene` category. <!-- **If enabling for the first time, expect the UI to freeze for bit while it silently installs all the dependencies.** -->
|
||||
5. The Speckle UI can be found in the 3d viewport toolbar (N), under the `Speckle` tab.
|
||||
Currently, we are supporting all Blender 3.X versions on Windows and Mac.
|
||||
We have experimental support for Blender 4.0 and greater.
|
||||
|
||||
Please follow our installation instructions on our [connector docs](https://speckle.guide/user/blender.html#installation)
|
||||
|
||||
## Usage
|
||||
Once enabled in `Preferences -> Addons`,
|
||||
The Speckle connector UI can be found in the 3d viewport toolbar (N), under the `Speckle` tab.
|
||||
|
||||
- Available user accounts are automatically detected and made available. To add user accounts use **Speckle Manager**.
|
||||
- Select the user from the dropdown list in the `Users` panel. This will populate the `Streams` list with available streams for the selected user.
|
||||
- Select a branch and commit from the dropdown menus.
|
||||
- Click on `Receive` to download the objects from the selected stream, branch, and commit. The stream objects will be loaded into a Blender Collection, named `<STREAM_NAME> [ <STREAM_BRANCH> @ <BRANCH_COMMIT> ]`. <!-- You can filter the stream by entering a query into the `Filter` field (i.e. `properties.weight>10` or `type="Mesh"`). -->
|
||||
- Click on `Open Stream in Web` to view the stream in your web browser.
|
||||
|
||||
## Caveats
|
||||
## Supported Elements
|
||||
|
||||
- Mesh objects are supported. Breps are imported as meshes using their `displayValue` data.
|
||||
- Curves have limited support: `Polylines` are supported; `NurbsCurves` are supported, though they are not guaranteed to look the same; `Lines` are supported; `Arcs` are not supported, though they are very roughly approximated; `PolyCurves` are supported for linear / polyline segments and very approximate arc segments. These conversions are a point of focus for further development.
|
||||
The Blender Connector is still a work in progress and, as such, data sent from the Blender connector is a highly lossy exchange. Our connectors are ever evolving to facilitate more and more Speckle usecases. We welcome feedback, requests, edge cases, and contributions!
|
||||
|
||||
## Custom properties
|
||||
The full matrix of supported Blender and Speckle types [can be found here](https://speckle.guide/user/support-tables.html#blender)
|
||||
|
||||
|
||||
## Additional Features
|
||||
|
||||
- **SpeckleBlender** will look for a `texture_coordinates` property and use that to create a UV layer for the imported object. These texture coordinates are a space-separated list of floats (`[u v u v u v etc...]`) that is encoded as a base64 blob. This is subject to change as **SpeckleBlender** develops.
|
||||
- If a `renderMaterial` property is found, **SpeckleBlender** will create a material named using the sub-property `renderMaterial.name`. If a material with that name already exists in Blender, **SpeckleBlender** will just assign that existing material to the object. This allows geometry to be updated without having to re-assign and re-create materials.
|
||||
- Vertex colors are supported. The `colors` list from Speckle meshes is translated to a vertex color layer.
|
||||
- Speckle properties will be imported as custom properties on Blender objects. Nested dictionaries are expanded to individual properties by flattening their key hierarchy. I.e. `propA:{'propB': {'propC':10, 'propD':'foobar'}}` is flattened to `propA.propB.propC = 10` and `propA.propB.propD = "foobar"`.
|
||||
|
||||
- If a `renderMaterial` property is found, **SpeckleBlender** will create a material named using the sub-property `renderMaterial.name`. If a material with that name already exists in Blender, **SpeckleBlender** will just assign that existing material to the object. This allows geometry to be updated without having to re-assign and re-create materials.
|
||||
|
||||
- Receiving vertex colors are supported. The `colors` list from Speckle meshes is translated to a vertex color layer.
|
||||
|
||||
- Receive/Send scripts. Allow injecting a custom python function to the receive/send process to automate any blender operations
|
||||
|
||||
## Dependency Installation and Compatibility with Other Blender Addons
|
||||
|
||||
Upon first launch of the addon, the Speckle connector installs its SpecklePy dependencies in `%appdata%/Speckle/connector_installations` on Windows and `~/.config/Speckle/connector_installations` on Mac.
|
||||
This is done through our [`installer.py`](https://github.com/specklesystems/speckle-blender/blob/main/bpy_speckle/installer.py). Through pip, we install the correct version of each dependency for your blender python version, host OS, and system architecture.
|
||||
As such, an internet connection is required for first launch of the connector.
|
||||
|
||||
Other blender addons may require dependencies that conflict with specklepy. In these cases, one or both addons may fail to load.
|
||||
If you suspect you're seeing a conflict, Please uninstall other third party addons one at a time to identify which addon is conflicting.
|
||||
|
||||
If you find an addon that conflicts, please try using a different version of that addon (newer or older).
|
||||
|
||||
If you can't find a version of an addon that works, please let us know on [our forums](https://speckle.community/) the name of the addon, the versions you've tried, the version of the Speckle connector you've tried, and your OS (win/mac/linux).
|
||||
|
||||
## Contributing
|
||||
|
||||
|
||||
@@ -3,29 +3,29 @@ import bpy
|
||||
from bpy.types import Object, Collection, ID
|
||||
from specklepy.objects.base import Base
|
||||
from bpy_speckle.functions import _report
|
||||
from bpy_speckle.specklepy_extras.commit_object_builder import CommitObjectBuilder, ROOT
|
||||
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(natvive_object: ID) -> str:
|
||||
def _id(native_object: ID) -> str:
|
||||
#NOTE: to avoid naming collisions, we prefix collections and objects differently
|
||||
return f"{type(natvive_object).__name__}:{natvive_object.name_full}"
|
||||
return f"{type(native_object).__name__}:{native_object.name_full}"
|
||||
|
||||
def _try_id(natvive_object: Optional[Union[Collection, Object]]) -> Optional[str]:
|
||||
return _id(natvive_object) if natvive_object else None
|
||||
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:
|
||||
convered_collection = SCollection(name = col.name_full, collectionType = "Blender Collection", elements = [])
|
||||
convered_collection.applicationId = _id(col)
|
||||
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":
|
||||
convered_collection["colorTag"] = col.color_tag
|
||||
converted_collection["colorTag"] = col.color_tag
|
||||
|
||||
return convered_collection
|
||||
return converted_collection
|
||||
|
||||
@define(slots=True)
|
||||
class BlenderCommitObjectBuilder(CommitObjectBuilder[Object]):
|
||||
@@ -67,11 +67,11 @@ class BlenderCommitObjectBuilder(CommitObjectBuilder[Object]):
|
||||
# parent = self.find_collection_parent(col)
|
||||
# self.set_relationship(id, (_try_builder_id(parent), ELEMENTS), (ROOT, ELEMENTS))
|
||||
|
||||
convered_collection = convert_collection_to_speckle(col)
|
||||
self.converted[id] = convered_collection
|
||||
self._collections[id] = convered_collection
|
||||
converted_collection = convert_collection_to_speckle(col)
|
||||
self.converted[id] = converted_collection
|
||||
self._collections[id] = converted_collection
|
||||
|
||||
return convered_collection
|
||||
return converted_collection
|
||||
|
||||
def build_commit_object(self, root_commit_object: Base) -> None:
|
||||
assert(root_commit_object.applicationId in self.converted)
|
||||
@@ -79,7 +79,7 @@ class BlenderCommitObjectBuilder(CommitObjectBuilder[Object]):
|
||||
# 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:
|
||||
for col in bpy.context.scene.collection.children_recursive: #type: ignore
|
||||
self.ensure_collection(col)
|
||||
|
||||
objects_to_build = set(self.converted.values())
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""
|
||||
Permanent handle on all user clients
|
||||
"""
|
||||
from specklepy.api.client import SpeckleClient
|
||||
from specklepy.core.api.client import SpeckleClient
|
||||
|
||||
|
||||
speckle_clients: list[SpeckleClient] = []
|
||||
|
||||
@@ -18,4 +18,5 @@ ELEMENTS_PROPERTY_ALIASES = {"elements", "@elements"}
|
||||
|
||||
OBJECT_NAME_MAX_LENGTH = 62
|
||||
SPECKLE_ID_LENGTH = 32
|
||||
OBJECT_NAME_SEPERATOR = " -- "
|
||||
OBJECT_NAME_SPECKLE_SEPARATOR = " -- "
|
||||
OBJECT_NAME_NUMERAL_SEPARATOR = '.'
|
||||
@@ -1,6 +1,6 @@
|
||||
import math
|
||||
from typing import Any, Dict, Iterable, List, Optional, Union, Collection, cast
|
||||
from bpy_speckle.convert.constants import DISPLAY_VALUE_PROPERTY_ALIASES, ELEMENTS_PROPERTY_ALIASES, OBJECT_NAME_MAX_LENGTH, OBJECT_NAME_SEPERATOR, SPECKLE_ID_LENGTH
|
||||
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 (
|
||||
@@ -20,7 +20,7 @@ from specklepy.objects.geometry import Mesh, Line, Polyline, Curve, Arc, Polycur
|
||||
from bpy.types import Object, Collection as BCollection
|
||||
|
||||
from .util import (
|
||||
add_to_heirarchy,
|
||||
add_to_hierarchy,
|
||||
get_render_material,
|
||||
get_vertex_color_material,
|
||||
render_material_to_native,
|
||||
@@ -40,7 +40,7 @@ CAN_CONVERT_TO_NATIVE = (
|
||||
)
|
||||
|
||||
|
||||
def _has_native_convesion(speckle_object: Base) -> bool:
|
||||
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:
|
||||
@@ -48,28 +48,11 @@ def _has_fallback_conversion(speckle_object: Base) -> bool:
|
||||
|
||||
def can_convert_to_native(speckle_object: Base) -> bool:
|
||||
|
||||
if(_has_native_convesion(speckle_object) or _has_fallback_conversion(speckle_object)):
|
||||
if(_has_native_conversion(speckle_object) or _has_fallback_conversion(speckle_object)):
|
||||
return True
|
||||
return False
|
||||
|
||||
def create_new_object(obj_data: Optional[bpy.types.ID], desired_name: str, counter: int = 0) -> 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 = desired_name if counter == 0 else f"{desired_name[:OBJECT_NAME_MAX_LENGTH - 4]}.{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 bpy.data.objects.keys():
|
||||
#Object already exists, increment counter and try again!
|
||||
return create_new_object(obj_data, desired_name, counter + 1)
|
||||
|
||||
blender_object = bpy.data.objects.new(name, obj_data)
|
||||
return blender_object
|
||||
|
||||
convert_instances_as: str #HACK: This is hacky, we need a better way to pass settings down to the converter
|
||||
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
|
||||
@@ -87,7 +70,7 @@ def convert_to_native(speckle_object: Base) -> Object:
|
||||
children: list[Object] = []
|
||||
|
||||
# convert elements/breps
|
||||
if not _has_native_convesion(speckle_object):
|
||||
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}")
|
||||
@@ -150,8 +133,8 @@ def _members_to_native(speckle_object: Base, name: str, scale: float, members: I
|
||||
display = getattr(speckle_object, alias, None)
|
||||
|
||||
count = 0
|
||||
MAX_DEPTH = 255 # some large value, to prevent infinite reccursion
|
||||
def seperate(value: Any) -> bool:
|
||||
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):
|
||||
@@ -163,11 +146,11 @@ def _members_to_native(speckle_object: Base, name: str, scale: float, members: I
|
||||
if(count > MAX_DEPTH):
|
||||
return True
|
||||
for x in value:
|
||||
seperate(x)
|
||||
separate(x)
|
||||
|
||||
return False
|
||||
|
||||
did_halt = seperate(display)
|
||||
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?")
|
||||
@@ -198,12 +181,15 @@ def view_to_native(speckle_view, name: str, scale: float) -> bpy.types.Object:
|
||||
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) #TODO: do these need to be scaled?
|
||||
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))
|
||||
@@ -532,7 +518,7 @@ def icurve_to_native(speckle_curve: Base, name: str, scale: float) -> bpy.types.
|
||||
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 ployline displayValue
|
||||
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)
|
||||
|
||||
@@ -540,7 +526,7 @@ def icurve_to_native(speckle_curve: Base, name: str, scale: float) -> bpy.types.
|
||||
|
||||
|
||||
"""
|
||||
Transforms and Intances
|
||||
Transforms and Instances
|
||||
"""
|
||||
|
||||
def transform_to_native(transform: Transform, scale: float) -> MMatrix:
|
||||
@@ -583,7 +569,7 @@ def _get_instance_name(instance: Instance) -> str:
|
||||
or _get_friendly_object_name(instance.definition)
|
||||
or _simplified_speckle_type(instance.speckle_type)
|
||||
)
|
||||
return f"{name_prefix}{OBJECT_NAME_SEPERATOR}{instance.id}"
|
||||
return f"{name_prefix}{OBJECT_NAME_SPECKLE_SEPARATOR}{instance.id}"
|
||||
|
||||
|
||||
def instance_to_native_object(instance: Instance, scale: float) -> Object:
|
||||
@@ -602,12 +588,12 @@ def instance_to_native_object(instance: Instance, scale: float) -> Object:
|
||||
traversal_root: Base = definition
|
||||
|
||||
if not can_convert_to_native(definition):
|
||||
# Non-convertable (like all blocks, and some revit instances) will not be converted as part of the deep_traversal.
|
||||
# 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 convertable
|
||||
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
|
||||
@@ -649,7 +635,7 @@ def instance_to_native_collection_instance(instance: Instance, scale: float) ->
|
||||
|
||||
instance_transform = transform_to_native(instance.transform, scale)
|
||||
|
||||
native_instance = bpy.data.objects.new(name, None)
|
||||
native_instance = create_new_object(None, name)
|
||||
|
||||
#add_custom_properties(instance, native_instance)
|
||||
# hide the instance axes so they don't clutter the viewport
|
||||
@@ -669,11 +655,11 @@ def _instance_definition_to_native(definition: Union[Base, BlockDefinition]) ->
|
||||
if native_def:
|
||||
return native_def
|
||||
|
||||
native_def = bpy.data.collections.new(name)
|
||||
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 convertable
|
||||
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)
|
||||
@@ -706,7 +692,7 @@ def _deep_conversion(root: Base, converted_objects: Dict[str, Union[Object, BCol
|
||||
|
||||
converted_objects[current.id] = converted
|
||||
|
||||
add_to_heirarchy(converted, item, converted_objects, preserve_transform)
|
||||
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:
|
||||
@@ -725,28 +711,66 @@ def collection_to_native(collection: SCollection) -> BCollection:
|
||||
return ret
|
||||
|
||||
def get_or_create_collection(name: str, clear_collection: bool = True) -> BCollection:
|
||||
existing = cast(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 = bpy.data.collections.new(name)
|
||||
#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
|
||||
#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
|
||||
return new_collection
|
||||
|
||||
|
||||
|
||||
"""
|
||||
Object Naming
|
||||
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)
|
||||
@@ -761,7 +785,7 @@ def _get_friendly_object_name(speckle_object: Base) -> Optional[str]:
|
||||
|
||||
def _truncate_object_name(name: str) -> str:
|
||||
|
||||
MAX_NAME_LENGTH = OBJECT_NAME_MAX_LENGTH - SPECKLE_ID_LENGTH - len(OBJECT_NAME_SEPERATOR)
|
||||
MAX_NAME_LENGTH = OBJECT_NAME_MAX_LENGTH - SPECKLE_ID_LENGTH - len(OBJECT_NAME_SPECKLE_SEPARATOR)
|
||||
|
||||
return name[:MAX_NAME_LENGTH]
|
||||
|
||||
@@ -777,7 +801,7 @@ def _generate_object_name(speckle_object: Base) -> str:
|
||||
else:
|
||||
prefix = _simplified_speckle_type(speckle_object.speckle_type)
|
||||
|
||||
return f"{prefix}{OBJECT_NAME_SEPERATOR}{speckle_object.id}"
|
||||
return f"{prefix}{OBJECT_NAME_SPECKLE_SEPARATOR}{speckle_object.id}"
|
||||
|
||||
|
||||
def get_scale_factor(speckle_object: Base, fallback: float = 1.0) -> float:
|
||||
|
||||
@@ -20,7 +20,7 @@ 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_SEPERATOR, SPECKLE_ID_LENGTH
|
||||
from bpy_speckle.convert.constants import OBJECT_NAME_SPECKLE_SEPARATOR, SPECKLE_ID_LENGTH
|
||||
from bpy_speckle.convert.util import (
|
||||
ConversionSkippedException,
|
||||
get_blender_custom_properties,
|
||||
@@ -34,7 +34,7 @@ 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")
|
||||
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:
|
||||
@@ -69,6 +69,8 @@ def convert_to_speckle(raw_blender_object: Object, units_scale: float, units: st
|
||||
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")
|
||||
|
||||
@@ -107,7 +109,7 @@ def mesh_to_speckle_meshes(blender_object: Object, data: bpy.types.Mesh) -> List
|
||||
for i in submesh_data:
|
||||
index_mapping: Dict[int, int] = {}
|
||||
|
||||
#Loop through each polygon, and map indicies to their new index in m_verts
|
||||
#Loop through each polygon, and map indices to their new index in m_verts
|
||||
|
||||
mesh_area = 0
|
||||
m_verts: List[float] = []
|
||||
@@ -176,8 +178,8 @@ def bezier_to_speckle(matrix: MMatrix, spline: bpy.types.Spline, name: Optional[
|
||||
|
||||
num_points = len(points)
|
||||
|
||||
flattend_points = []
|
||||
for row in points: flattend_points.extend(row)
|
||||
flattened_points = []
|
||||
for row in points: flattened_points.extend(row)
|
||||
|
||||
knot_count = num_points + degree - 1
|
||||
knots = [0] * knot_count
|
||||
@@ -192,7 +194,7 @@ def bezier_to_speckle(matrix: MMatrix, spline: bpy.types.Spline, name: Optional[
|
||||
degree=degree,
|
||||
closed=spline.use_cyclic_u,
|
||||
periodic= not spline.use_endpoint_u,
|
||||
points=flattend_points,
|
||||
points=flattened_points,
|
||||
weights=[1] * num_points,
|
||||
knots=knots,
|
||||
rational=True,
|
||||
@@ -219,15 +221,15 @@ def nurbs_to_speckle(matrix: MMatrix, spline: bpy.types.Spline, name: Optional[s
|
||||
|
||||
points = [tuple(matrix @ pt.co.xyz * UnitsScale) for pt in spline.points] # type: ignore
|
||||
|
||||
flattend_points = []
|
||||
for row in points: flattend_points.extend(row)
|
||||
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
|
||||
flattend_points.append(flattend_points[i + 0])
|
||||
flattend_points.append(flattend_points[i + 1])
|
||||
flattend_points.append(flattend_points[i + 2])
|
||||
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])
|
||||
@@ -237,7 +239,7 @@ def nurbs_to_speckle(matrix: MMatrix, spline: bpy.types.Spline, name: Optional[s
|
||||
degree=degree,
|
||||
closed=spline.use_cyclic_u,
|
||||
periodic= not spline.use_endpoint_u,
|
||||
points=flattend_points,
|
||||
points=flattened_points,
|
||||
weights=weights,
|
||||
knots=knots,
|
||||
rational=is_rational,
|
||||
@@ -305,27 +307,27 @@ def bezier_to_speckle_polyline(matrix: MMatrix, spline: bpy.types.Spline, 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_SEPERATOR)
|
||||
_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_SEPERATOR in blender_object.name
|
||||
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_SEPERATOR, 1)[0]
|
||||
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
|
||||
|
||||
flattend_points = []
|
||||
for row in points: flattend_points.extend(row)
|
||||
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(flattend_points),
|
||||
value=list(flattened_points),
|
||||
length=length,
|
||||
domain=domain,
|
||||
bbox=Box(area=0.0, volume=0.0),
|
||||
@@ -370,6 +372,12 @@ def curve_to_speckle_geometry(blender_object: Object, data: bpy.types.Curve) ->
|
||||
|
||||
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"
|
||||
@@ -410,8 +418,10 @@ def material_to_speckle(blender_mat: bpy.types.Material) -> RenderMaterial:
|
||||
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"].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
|
||||
@@ -466,7 +476,7 @@ def vector_to_speckle(xyz: MVector) -> Vector:
|
||||
)
|
||||
|
||||
def transform_to_speckle(blender_transform: Union[Iterable[Iterable[float]], MMatrix]) -> Transform:
|
||||
iterable_transform = cast(Iterable[Iterable[float]], blender_transform) #NOTE: Matrix are itterable, even if type hinting says they are not
|
||||
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):
|
||||
@@ -515,14 +525,13 @@ 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:
|
||||
#raise ConversionSkippedException("Sending non-collection instance empties are not currently supported")
|
||||
# Empty -> Point
|
||||
wrapper = Base()
|
||||
wrapper["@displayValue"] = matrix_to_speckle_point(cast(MMatrix, blender_object.matrix_world))
|
||||
return wrapper
|
||||
#TODO: we could do a Empty -> Point conversion here. However, the viewer (and likly other apps) don't support a pont with "elements"
|
||||
#return matrix_to_speckle_point(cast(MMatrix, blender_object.matrix_world))
|
||||
|
||||
|
||||
def matrix_to_speckle_point(matrix: MMatrix, units_scale: float = 1.0) -> Point:
|
||||
|
||||
@@ -8,9 +8,9 @@ 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
|
||||
from bpy.types import Material, Object, Collection as BCollection, Node, ShaderNodeVertexColor, NodeInputs
|
||||
|
||||
from bpy_speckle.specklepy_extras.traversal import TraversalContext
|
||||
from specklepy.objects.graph_traversal.traversal import TraversalContext
|
||||
|
||||
class ConversionSkippedException(Exception):
|
||||
pass
|
||||
@@ -88,11 +88,14 @@ def render_material_to_native(speckle_mat: RenderMaterial) -> Material:
|
||||
inputs = blender_mat.node_tree.nodes["Principled BSDF"].inputs
|
||||
|
||||
inputs["Base Color"].default_value = to_rgba(speckle_mat.diffuse) # type: ignore
|
||||
inputs["Emission"].default_value = to_rgba(speckle_mat.emissive) # 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"
|
||||
|
||||
@@ -254,8 +257,9 @@ ignored_keys = {
|
||||
"_chunkable",
|
||||
}
|
||||
|
||||
def get_blender_custom_properties(obj, max_depth: int = 200):
|
||||
if max_depth < 0:
|
||||
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"):
|
||||
@@ -449,7 +453,7 @@ def link_object_to_collection_nested(obj: Object, col: BCollection):
|
||||
for child in obj.children: #type: ignore
|
||||
link_object_to_collection_nested(child, col)
|
||||
|
||||
def add_to_heirarchy(converted: Union[Object, BCollection], traversalContext : 'TraversalContext', converted_objects: Dict[str, Union[Object, BCollection]], preserve_transform: bool) -> None:
|
||||
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
|
||||
|
||||
@@ -2,35 +2,11 @@ from typing import Callable
|
||||
from specklepy.objects.base import Base
|
||||
from bpy_speckle.convert.constants import ELEMENTS_PROPERTY_ALIASES
|
||||
|
||||
from bpy_speckle.specklepy_extras.traversal import GraphTraversal, TraversalRule
|
||||
|
||||
"""
|
||||
Speckle functions
|
||||
"""
|
||||
|
||||
UNIT_SCALE = {
|
||||
"meters": 1.0,
|
||||
"centimeters": 0.01,
|
||||
"millimeters": 0.001,
|
||||
"inches": 0.0254,
|
||||
"feet": 0.3048,
|
||||
"kilometers": 1000.0,
|
||||
"mm": 0.001,
|
||||
"cm": 0.01,
|
||||
"m": 1.0,
|
||||
"km": 1000.0,
|
||||
"in": 0.0254,
|
||||
"ft": 0.3048,
|
||||
"yd": 0.9144,
|
||||
"mi": 1609.340,
|
||||
}
|
||||
|
||||
"""
|
||||
Utility functions
|
||||
"""
|
||||
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):
|
||||
def _report(msg: object) -> None:
|
||||
"""
|
||||
Function for printing messages to the console
|
||||
"""
|
||||
@@ -38,15 +14,8 @@ def _report(msg):
|
||||
|
||||
|
||||
def get_scale_length(units: str) -> float:
|
||||
if units.lower() in UNIT_SCALE.keys():
|
||||
return UNIT_SCALE[units]
|
||||
_report("Units <{}> are not supported.".format(units))
|
||||
return 1.0
|
||||
|
||||
|
||||
"""
|
||||
Client, user, and stream functions
|
||||
"""
|
||||
"""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:
|
||||
@@ -56,13 +25,13 @@ def get_default_traversal_func(can_convert_to_native: Callable[[Base], bool]) ->
|
||||
|
||||
ignore_rule = TraversalRule(
|
||||
[
|
||||
lambda o: "Objects.Structural.Results" in o.speckle_type, #Sadly, this one is nessasary to avoid double conversion...
|
||||
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 _: [],
|
||||
)
|
||||
|
||||
convertable_rule = TraversalRule(
|
||||
convertible_rule = TraversalRule(
|
||||
[can_convert_to_native],
|
||||
lambda _: ELEMENTS_PROPERTY_ALIASES,
|
||||
)
|
||||
@@ -73,4 +42,4 @@ def get_default_traversal_func(can_convert_to_native: Callable[[Base], bool]) ->
|
||||
lambda o: o.get_member_names(), #TODO: avoid deprecated members
|
||||
)
|
||||
|
||||
return GraphTraversal([ignore_rule, convertable_rule, default_rule])
|
||||
return GraphTraversal([ignore_rule, convertible_rule, default_rule])
|
||||
|
||||
@@ -5,6 +5,7 @@ import bpy
|
||||
from bpy.props import BoolProperty
|
||||
from bpy_speckle.clients import speckle_clients
|
||||
from bpy_speckle.properties.scene import get_speckle
|
||||
from specklepy.logging import metrics
|
||||
|
||||
|
||||
class DeleteCommit(bpy.types.Operator):
|
||||
@@ -58,6 +59,15 @@ class DeleteCommit(bpy.types.Operator):
|
||||
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")
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import bpy
|
||||
import webbrowser
|
||||
from specklepy.logging import metrics
|
||||
|
||||
|
||||
class OpenSpeckleGuide(bpy.types.Operator):
|
||||
@@ -10,6 +11,13 @@ class OpenSpeckleGuide(bpy.types.Operator):
|
||||
|
||||
def execute(self, context):
|
||||
webbrowser.open("https://speckle.guide/user/blender.html")
|
||||
metrics.track(
|
||||
"Connector Action",
|
||||
None,
|
||||
custom_props={
|
||||
"name": "OpenSpeckleGuide"
|
||||
},
|
||||
)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -21,6 +29,13 @@ class OpenSpeckleTutorials(bpy.types.Operator):
|
||||
|
||||
def execute(self, context):
|
||||
webbrowser.open("https://speckle.systems/tutorials/")
|
||||
metrics.track(
|
||||
"Connector Action",
|
||||
None,
|
||||
custom_props={
|
||||
"name": "OpenSpeckleTutorials"
|
||||
},
|
||||
)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -32,4 +47,11 @@ class OpenSpeckleForum(bpy.types.Operator):
|
||||
|
||||
def execute(self, context):
|
||||
webbrowser.open("https://speckle.community/")
|
||||
metrics.track(
|
||||
"Connector Action",
|
||||
None,
|
||||
custom_props={
|
||||
"name": "OpenSpeckleForum"
|
||||
},
|
||||
)
|
||||
return {"FINISHED"}
|
||||
@@ -11,7 +11,7 @@ from bpy_speckle.convert.to_speckle import (
|
||||
)
|
||||
from bpy_speckle.functions import get_scale_length, _report
|
||||
from bpy_speckle.clients import speckle_clients
|
||||
|
||||
from specklepy.logging import metrics
|
||||
|
||||
class UpdateObject(bpy.types.Operator):
|
||||
"""
|
||||
@@ -56,6 +56,13 @@ class UpdateObject(bpy.types.Operator):
|
||||
_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"}
|
||||
@@ -79,6 +86,14 @@ class ResetObject(bpy.types.Operator):
|
||||
context.object.speckle.enabled = False
|
||||
context.view_layer.update()
|
||||
|
||||
metrics.track(
|
||||
"Connector Action",
|
||||
None,
|
||||
custom_props={
|
||||
"name": "ResetObject"
|
||||
},
|
||||
)
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -125,6 +140,14 @@ class DeleteObject(bpy.types.Operator):
|
||||
active.speckle.enabled = False
|
||||
context.view_layer.update()
|
||||
|
||||
metrics.track(
|
||||
"Connector Action",
|
||||
None,
|
||||
custom_props={
|
||||
"name": "DeleteObject"
|
||||
},
|
||||
)
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
@deprecated
|
||||
@@ -197,6 +220,13 @@ class UploadNgonsAsPolylines(bpy.types.Operator):
|
||||
context.view_layer.update()
|
||||
_report("Done.")
|
||||
|
||||
metrics.track(
|
||||
"Connector Action",
|
||||
None,
|
||||
custom_props={
|
||||
"name": "UploadNgonsAsPolylines"
|
||||
},
|
||||
)
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context, event):
|
||||
@@ -267,6 +297,14 @@ class SelectIfSameCustomProperty(bpy.types.Operator):
|
||||
else:
|
||||
obj.select_set(False)
|
||||
|
||||
metrics.track(
|
||||
"Connector Action",
|
||||
None,
|
||||
custom_props={
|
||||
"name": "SelectIfSameCustomProperty"
|
||||
},
|
||||
)
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -315,4 +353,13 @@ class SelectIfHasCustomProperty(bpy.types.Operator):
|
||||
else:
|
||||
obj.select_set(False)
|
||||
|
||||
metrics.track(
|
||||
"Connector Action",
|
||||
None,
|
||||
custom_props={
|
||||
"name": "SelectIfHasCustomProperty"
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -33,11 +33,11 @@ from bpy_speckle.functions import (
|
||||
from bpy_speckle.clients import speckle_clients
|
||||
from bpy_speckle.operators.users import add_user_stream
|
||||
from bpy_speckle.properties.scene import SpeckleSceneSettings, SpeckleUserObject, get_speckle
|
||||
from bpy_speckle.convert.util import ConversionSkippedException, add_to_heirarchy
|
||||
from specklepy.api.models import Commit
|
||||
from specklepy.api import operations, host_applications
|
||||
from specklepy.api.wrapper import StreamWrapper
|
||||
from specklepy.api.resources.stream import Stream
|
||||
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
|
||||
@@ -74,7 +74,7 @@ def get_receive_funcs(speckle: SpeckleSceneSettings) -> tuple[ObjectCallback, Re
|
||||
#]
|
||||
|
||||
INSTANCES_SETTINGS = [
|
||||
("collection_instance", "Collection Instace", "Receive Instances as Collection Instances"),
|
||||
("collection_instance", "Collection Instance", "Receive Instances as Collection Instances"),
|
||||
("linked_duplicates", "Linked Duplicates", "Receive Instances as Linked Duplicates"),
|
||||
]
|
||||
|
||||
@@ -91,7 +91,7 @@ class ReceiveStreamObjects(bpy.types.Operator):
|
||||
|
||||
clean_meshes: BoolProperty(name="Clean Meshes", default=False)
|
||||
|
||||
#receive_mode: EnumProperty(items=RECEIVE_MODES, name="Receive Type", default="replace", description="The behaviour of the recieve operation")
|
||||
#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")
|
||||
|
||||
|
||||
@@ -150,6 +150,15 @@ class ReceiveStreamObjects(bpy.types.Operator):
|
||||
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 commit from Speckle Blender",
|
||||
)
|
||||
|
||||
metrics.track(
|
||||
metrics.RECEIVE,
|
||||
@@ -158,17 +167,10 @@ class ReceiveStreamObjects(bpy.types.Operator):
|
||||
"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
|
||||
},
|
||||
)
|
||||
|
||||
# Fetch commit data
|
||||
commit_object = operations._untracked_receive(commit.referenced_object, transport)
|
||||
client.commit.received(
|
||||
stream.id,
|
||||
commit.id,
|
||||
source_application="blender",
|
||||
message="received commit from Speckle Blender",
|
||||
)
|
||||
|
||||
# Convert received data
|
||||
context.window_manager.progress_begin(0, commit_object.totalChildrenCount or 1)
|
||||
@@ -181,7 +183,7 @@ class ReceiveStreamObjects(bpy.types.Operator):
|
||||
(object_converted_callback, on_complete_callback) = get_receive_funcs(speckle)
|
||||
|
||||
# older commits will have a non-collection root object
|
||||
# for the sake of consistant behaviour, we will wrap any non-collection commit objects in a collection
|
||||
# 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]
|
||||
@@ -221,7 +223,7 @@ class ReceiveStreamObjects(bpy.types.Operator):
|
||||
|
||||
converted_objects[current.id] = converted
|
||||
|
||||
add_to_heirarchy(converted, item, converted_objects, True)
|
||||
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:
|
||||
@@ -350,6 +352,16 @@ class SendStreamObjects(bpy.types.Operator):
|
||||
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(
|
||||
@@ -395,6 +407,14 @@ class ViewStreamDataApi(bpy.types.Operator):
|
||||
|
||||
if not webbrowser.open("%s/streams/%s" % (user.server_url, stream.id), new=2):
|
||||
raise Exception("Failed to open stream in browser")
|
||||
|
||||
metrics.track(
|
||||
"Connector Action",
|
||||
None,
|
||||
custom_props={
|
||||
"name": "view_stream_data_api"
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
class AddStreamFromURL(bpy.types.Operator):
|
||||
@@ -481,6 +501,14 @@ class AddStreamFromURL(bpy.types.Operator):
|
||||
|
||||
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):
|
||||
@@ -541,6 +569,14 @@ class CreateStream(bpy.types.Operator):
|
||||
|
||||
if context.area:
|
||||
context.area.tag_redraw()
|
||||
|
||||
metrics.track(
|
||||
"Connector Action",
|
||||
client.account,
|
||||
custom_props={
|
||||
"name": "create_stream"
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
class DeleteStream(bpy.types.Operator):
|
||||
@@ -584,7 +620,7 @@ class DeleteStream(bpy.types.Operator):
|
||||
|
||||
def delete_stream(self, context: Context) -> None:
|
||||
if not self.are_you_sure:
|
||||
raise Exception("Cancled by user")
|
||||
raise Exception("Cancelled by user")
|
||||
|
||||
self.are_you_sure = False
|
||||
|
||||
@@ -607,6 +643,14 @@ class DeleteStream(bpy.types.Operator):
|
||||
if context.area:
|
||||
context.area.tag_redraw()
|
||||
|
||||
metrics.track(
|
||||
"Connector Action",
|
||||
client.account,
|
||||
custom_props={
|
||||
"name": "delete_stream"
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
class SelectOrphanObjects(bpy.types.Operator):
|
||||
"""
|
||||
@@ -632,6 +676,13 @@ class SelectOrphanObjects(bpy.types.Operator):
|
||||
else:
|
||||
o.select = False
|
||||
|
||||
metrics.track(
|
||||
"Connector Action",
|
||||
custom_props={
|
||||
"name": "SelectOrphanObjects"
|
||||
},
|
||||
)
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
class CopyStreamId(bpy.types.Operator):
|
||||
@@ -658,6 +709,12 @@ class CopyStreamId(bpy.types.Operator):
|
||||
(_, 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):
|
||||
"""
|
||||
@@ -683,6 +740,14 @@ class CopyCommitId(bpy.types.Operator):
|
||||
(_, _, _, commit) = speckle.validate_commit_selection()
|
||||
bpy.context.window_manager.clipboard = commit.id
|
||||
|
||||
metrics.track(
|
||||
"Connector Action",
|
||||
custom_props={
|
||||
"name": "copy_commit_id"
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
|
||||
class CopyBranchName(bpy.types.Operator):
|
||||
"""
|
||||
@@ -708,3 +773,10 @@ class CopyBranchName(bpy.types.Operator):
|
||||
(_, _, branch) = speckle.validate_branch_selection()
|
||||
|
||||
bpy.context.window_manager.clipboard = branch.name
|
||||
|
||||
metrics.track(
|
||||
"Connector Action",
|
||||
custom_props={
|
||||
"name": "copy_branch_id"
|
||||
},
|
||||
)
|
||||
|
||||
@@ -6,10 +6,11 @@ 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 SpeckleCommitObject, SpeckleSceneSettings, SpeckleUserObject, get_speckle
|
||||
from specklepy.api.client import SpeckleClient
|
||||
from specklepy.api.models import Stream
|
||||
from specklepy.api.credentials import get_local_accounts
|
||||
from bpy_speckle.properties.scene import SpeckleCommitObject, SpeckleSceneSettings, SpeckleStreamObject, SpeckleUserObject, get_speckle
|
||||
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):
|
||||
"""
|
||||
@@ -23,6 +24,14 @@ class ResetUsers(bpy.types.Operator):
|
||||
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()
|
||||
@@ -49,46 +58,77 @@ class LoadUsers(bpy.types.Operator):
|
||||
_report("Loading users...")
|
||||
|
||||
speckle = cast(SpeckleSceneSettings, context.scene.speckle) #type: ignore
|
||||
users = speckle.users
|
||||
users_list = speckle.users
|
||||
|
||||
ResetUsers.reset_ui(context)
|
||||
|
||||
profiles = get_local_accounts()
|
||||
active_user_index = 0
|
||||
|
||||
for profile in profiles:
|
||||
user = users.add()
|
||||
user.server_name = profile.serverInfo.name or "Speckle Server"
|
||||
user.server_url = profile.serverInfo.url
|
||||
user.id = profile.userInfo.id
|
||||
user.name = profile.userInfo.name
|
||||
user.email = profile.userInfo.email
|
||||
user.company = profile.userInfo.company or ""
|
||||
try:
|
||||
url = profile.serverInfo.url
|
||||
assert(url)
|
||||
client = SpeckleClient(
|
||||
host=url,
|
||||
use_ssl="https" in url,
|
||||
)
|
||||
client.authenticate_with_account(profile)
|
||||
speckle_clients.append(client)
|
||||
except Exception as ex:
|
||||
_report(ex)
|
||||
users.remove(len(users) - 1)
|
||||
if profile.isDefault:
|
||||
active_user_index = len(users) - 1
|
||||
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)
|
||||
|
||||
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):
|
||||
s = user.streams.add()
|
||||
"""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
|
||||
@@ -122,7 +162,7 @@ def add_user_stream(user: SpeckleUserObject, stream: Stream):
|
||||
|
||||
class LoadUserStreams(bpy.types.Operator):
|
||||
"""
|
||||
Load all available streams for active user user
|
||||
Load all available streams for active user
|
||||
"""
|
||||
|
||||
bl_idname = "speckle.load_user_streams"
|
||||
@@ -130,41 +170,50 @@ class LoadUserStreams(bpy.types.Operator):
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "(Re)load all available user streams"
|
||||
|
||||
stream_limit: int = 20
|
||||
branch_limit: int = 100
|
||||
|
||||
def execute(self, context):
|
||||
try:
|
||||
self.add_stream_from_url(context)
|
||||
self.load_user_stream(context)
|
||||
return {"FINISHED"}
|
||||
except Exception as ex:
|
||||
_report(f"{self.bl_idname} failed: {ex}")
|
||||
return {"CANCELLED"}
|
||||
|
||||
def add_stream_from_url(self, context: Context) -> None:
|
||||
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=20)
|
||||
except Exception as e:
|
||||
_report(f"Failed to retrieve streams: {e}")
|
||||
return
|
||||
streams = client.stream.list(stream_limit=self.stream_limit)
|
||||
except Exception as ex:
|
||||
raise Exception(f"Failed to retrieve streams") from ex
|
||||
|
||||
if not streams:
|
||||
_report("Failed to retrieve streams.")
|
||||
raise Exception("Zero streams found")
|
||||
return
|
||||
|
||||
user.streams.clear()
|
||||
|
||||
default_units = "Meters"
|
||||
|
||||
for s in streams:
|
||||
assert(s.id)
|
||||
sstream = client.stream.get(id=s.id, branch_limit=20)
|
||||
sstream = client.stream.get(id=s.id, branch_limit=self.branch_limit)
|
||||
add_user_stream(user, sstream)
|
||||
|
||||
bpy.context.view_layer.update()
|
||||
|
||||
if context.area:
|
||||
context.area.tag_redraw()
|
||||
|
||||
metrics.track(
|
||||
"Connector Action",
|
||||
client.account,
|
||||
custom_props={
|
||||
"name": "LoadUserStreams"
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Collection, Dict, Generic, Iterable, List, Optional, Tuple, TypeVar
|
||||
from attrs import define
|
||||
from specklepy.objects.base import Base
|
||||
|
||||
ROOT: str = "__Root"
|
||||
|
||||
T = TypeVar('T')
|
||||
PARENT_INFO = Tuple[Optional[str], str]
|
||||
|
||||
@define(slots=True)
|
||||
class CommitObjectBuilder(ABC, Generic[T]):
|
||||
|
||||
converted: Dict[str, Base]
|
||||
_parent_infos: Dict[str, Collection[PARENT_INFO]]
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.converted = {}
|
||||
self._parent_infos = {}
|
||||
|
||||
@abstractmethod
|
||||
def include_object(self, conversion_result: Base, native_object: T) -> None:
|
||||
pass
|
||||
|
||||
def build_commit_object(self, root_commit_object: Base) -> None:
|
||||
self.apply_relationships(self.converted.values(), root_commit_object)
|
||||
|
||||
def set_relationship(self, app_id: Optional[str], *parent_info : PARENT_INFO) -> None:
|
||||
|
||||
if not app_id:
|
||||
return
|
||||
|
||||
self._parent_infos[app_id] = parent_info
|
||||
|
||||
def apply_relationships(self, to_add: Iterable[Base], root_commit_object: Base) -> None:
|
||||
for c in to_add:
|
||||
try:
|
||||
self.apply_relationship(c, root_commit_object)
|
||||
except Exception as ex:
|
||||
print(f"Failed to add object {type(c)} to commit object: {ex}")
|
||||
|
||||
def apply_relationship(self, current: Base, root_commit_object: Base):
|
||||
if not current.applicationId: raise Exception(f"Expected applicationId to have been set")
|
||||
|
||||
parents = self._parent_infos[current.applicationId]
|
||||
|
||||
for (parent_id, prop_name) in parents:
|
||||
if not parent_id: continue
|
||||
|
||||
parent: Optional[Base]
|
||||
if parent_id == ROOT:
|
||||
parent = root_commit_object
|
||||
else:
|
||||
parent = self.converted[parent_id] if parent_id in self.converted else None
|
||||
|
||||
if not parent: continue
|
||||
|
||||
try:
|
||||
elements = get_detached_prop(parent, prop_name)
|
||||
if not isinstance(elements, list):
|
||||
elements = []
|
||||
set_detached_prop(parent, prop_name, elements)
|
||||
|
||||
elements.append(current)
|
||||
return
|
||||
except Exception as ex:
|
||||
# A parent was found, but it was invalid (Likely because of a type mismatch on a `elements` property)
|
||||
print(f"Failed to add object {type(current)} to a converted parent; {ex}")
|
||||
|
||||
raise Exception(f"Could not find a valid parent for object of type {type(current)}. Checked {len(parents)} potential parent, and non were converted!")
|
||||
|
||||
|
||||
def get_detached_prop(speckle_object: Base, prop_name: str) -> Optional[Any]:
|
||||
detached_prop_name = get_detached_prop_name(speckle_object, prop_name)
|
||||
return getattr(speckle_object, detached_prop_name, None)
|
||||
|
||||
def set_detached_prop(speckle_object: Base, prop_name: str, value: Optional[Any]) -> None:
|
||||
detached_prop_name = get_detached_prop_name(speckle_object, prop_name)
|
||||
setattr(speckle_object, detached_prop_name, value)
|
||||
|
||||
def get_detached_prop_name(speckle_object: Base, prop_name: str) -> str:
|
||||
return prop_name if hasattr(speckle_object, prop_name) else f"@{prop_name}"
|
||||
@@ -1,122 +0,0 @@
|
||||
from typing import Any, Callable, Collection, Iterable, Iterator, List, Optional, Set
|
||||
|
||||
from attrs import define
|
||||
from typing_extensions import Protocol, final
|
||||
|
||||
from specklepy.objects import Base
|
||||
|
||||
|
||||
class ITraversalRule(Protocol):
|
||||
def get_members_to_traverse(self, o: Base) -> Set[str]:
|
||||
"""Get the members to traverse."""
|
||||
pass
|
||||
|
||||
def does_rule_hold(self, o: Base) -> bool:
|
||||
"""Make sure the rule still holds."""
|
||||
pass
|
||||
|
||||
|
||||
@final
|
||||
class DefaultRule:
|
||||
def get_members_to_traverse(self, _) -> Set[str]:
|
||||
return set()
|
||||
|
||||
def does_rule_hold(self, _) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
# we're creating a local protected "singleton"
|
||||
_default_rule = DefaultRule()
|
||||
|
||||
|
||||
@final
|
||||
@define(slots=True, frozen=True)
|
||||
class TraversalContext:
|
||||
current: Base
|
||||
member_name: Optional[str] = None
|
||||
parent: Optional["TraversalContext"] = None
|
||||
|
||||
|
||||
@final
|
||||
@define(slots=True, frozen=True)
|
||||
class GraphTraversal:
|
||||
|
||||
_rules: List[ITraversalRule]
|
||||
|
||||
def traverse(self, root: Base) -> Iterator[TraversalContext]:
|
||||
stack: List[TraversalContext] = []
|
||||
|
||||
stack.append(TraversalContext(root))
|
||||
|
||||
while len(stack) > 0:
|
||||
head = stack.pop()
|
||||
yield head
|
||||
|
||||
current = head.current
|
||||
active_rule = self._get_active_rule_or_default_rule(current)
|
||||
members_to_traverse = active_rule.get_members_to_traverse(current)
|
||||
for child_prop in members_to_traverse:
|
||||
try:
|
||||
if child_prop in {"speckle_type", "units", "applicationId"}: continue #debug: to avoid noisy exceptions, explicitly avoid checking ones we know will fail, this is not exhaustive
|
||||
if getattr(current, child_prop, None):
|
||||
value = current[child_prop]
|
||||
self._traverse_member_to_stack(
|
||||
stack, value, child_prop, head
|
||||
)
|
||||
except KeyError as ex:
|
||||
# Unset application ids, and class variables like SpeckleType will throw when __getitem__ is called
|
||||
pass
|
||||
@staticmethod
|
||||
def _traverse_member_to_stack(
|
||||
stack: List[TraversalContext],
|
||||
value: Any,
|
||||
member_name: Optional[str] = None,
|
||||
parent: Optional[TraversalContext] = None,
|
||||
):
|
||||
if isinstance(value, Base):
|
||||
stack.append(TraversalContext(value, member_name, parent))
|
||||
elif isinstance(value, list):
|
||||
for obj in value:
|
||||
GraphTraversal._traverse_member_to_stack(stack, obj, member_name, parent)
|
||||
elif isinstance(value, dict):
|
||||
for obj in value.values():
|
||||
GraphTraversal._traverse_member_to_stack(stack, obj, member_name, parent)
|
||||
|
||||
@staticmethod
|
||||
def traverse_member(value: Optional[Any]) -> Iterator[Base]:
|
||||
if isinstance(value, Base):
|
||||
yield value
|
||||
elif isinstance(value, list):
|
||||
for obj in value:
|
||||
for o in GraphTraversal.traverse_member(obj):
|
||||
yield o
|
||||
elif isinstance(value, dict):
|
||||
for obj in value.values():
|
||||
for o in GraphTraversal.traverse_member(obj):
|
||||
yield o
|
||||
|
||||
|
||||
def _get_active_rule_or_default_rule(self, o: Base) -> ITraversalRule:
|
||||
return self._get_active_rule(o) or _default_rule
|
||||
|
||||
def _get_active_rule(self, o: Base) -> Optional[ITraversalRule]:
|
||||
for rule in self._rules:
|
||||
if rule.does_rule_hold(o):
|
||||
return rule
|
||||
return None
|
||||
|
||||
|
||||
@final
|
||||
@define(slots=True, frozen=True)
|
||||
class TraversalRule:
|
||||
_conditions: Collection[Callable[[Base], bool]]
|
||||
_members_to_traverse: Callable[[Base], Iterable[str]]
|
||||
|
||||
def get_members_to_traverse(self, o: Base) -> Set[str]:
|
||||
return set(self._members_to_traverse(o))
|
||||
|
||||
def does_rule_hold(self, o: Base) -> bool:
|
||||
for condition in self._conditions:
|
||||
if condition(o):
|
||||
return True
|
||||
return False
|
||||
Generated
+839
-650
File diff suppressed because it is too large
Load Diff
+2
-3
@@ -7,18 +7,17 @@ license = "Apache-2.0"
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = ">=3.8, <4.0.0"
|
||||
specklepy = "^2.15.1"
|
||||
specklepy = "^2.17.17"
|
||||
attrs = "^23.1.0"
|
||||
|
||||
# [tool.poetry.group.local_specklepy.dependencies]
|
||||
# specklepy = {path = "../specklepy", develop = true}
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
numpy = "^1.23.5"
|
||||
fake-bpy-module-latest = "^20230117"
|
||||
black = "^22.10.0"
|
||||
pylint = "^2.15.7"
|
||||
ruff = "^0.0.166"
|
||||
ruff = "^0.0.187"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core>=1.0.0"]
|
||||
|
||||
Reference in New Issue
Block a user