Compare commits
52 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4c381bd809 | |||
| 8c3885ece8 | |||
| 15bd3f5070 | |||
| 6fd4571d34 | |||
| 5081177653 | |||
| 0d0ca2c811 | |||
| 230e27a162 | |||
| 669fd19c2e | |||
| 139f8ccb33 | |||
| 7f625bd468 | |||
| 4d07ba7637 | |||
| 7431b57e0e | |||
| 57c19ba3c5 | |||
| 8e3c2ece2f | |||
| cfc58d9456 | |||
| e74a6cebb1 | |||
| 5e01d5a976 | |||
| a2f7ab422f | |||
| 8c58d9d14c | |||
| 90e61b6dc1 | |||
| 5c479e4c0e | |||
| 97d20ad7b1 | |||
| 2800b84747 | |||
| 511d69314e | |||
| 24e7f02213 | |||
| c1d7947085 | |||
| 21281e5d77 | |||
| 29bbdc69a2 | |||
| efe6e6a4a0 | |||
| f036109020 | |||
| 86bc2dc590 | |||
| a34b6ad0c2 | |||
| e436949ef9 | |||
| 6d8f4a4a80 | |||
| dabb65427a | |||
| 57ece17e8b | |||
| 4362f737d0 | |||
| b55df58313 | |||
| afa6722253 | |||
| a3d4881578 | |||
| 1af158a5e0 | |||
| 47857a9db0 | |||
| 3b026e6027 | |||
| d572609f75 | |||
| 37032cc7aa | |||
| 6027325878 | |||
| 5ddb2aa052 | |||
| 67a18821cc | |||
| 2688a69286 | |||
| 56216a6137 | |||
| 319cbf8960 | |||
| d7ac6c0b95 |
+39
-15
@@ -71,24 +71,44 @@ jobs:
|
||||
build-installer-win:
|
||||
executor:
|
||||
name: win/default
|
||||
shell: cmd.exe
|
||||
environment:
|
||||
SSM: 'C:\Program Files\DigiCert\DigiCert One Signing Manager Tools'
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: ./
|
||||
- run:
|
||||
name: Patch installer
|
||||
shell: powershell.exe
|
||||
command: python patch_installer.py (Get-Content -Raw SEMVER)
|
||||
- run:
|
||||
name: Create Innosetup signing cert
|
||||
shell: powershell.exe
|
||||
command: |
|
||||
echo $env:PFX_B64 > "speckle-sharp-ci-tools\SignTool\AEC Systems Ltd.txt"
|
||||
certutil -decode "speckle-sharp-ci-tools\SignTool\AEC Systems Ltd.txt" "speckle-sharp-ci-tools\SignTool\AEC Systems Ltd.pfx"
|
||||
- run:
|
||||
name: Installer
|
||||
shell: cmd.exe #does not work in powershell
|
||||
command: speckle-sharp-ci-tools\InnoSetup\ISCC.exe speckle-sharp-ci-tools\blender.iss /Sbyparam=$p
|
||||
- unless: # Build installers unsigned on non-tagged builds
|
||||
condition: << pipeline.git.tag >>
|
||||
steps:
|
||||
- run:
|
||||
name: Build Installer
|
||||
command: speckle-sharp-ci-tools\InnoSetup\ISCC.exe speckle-sharp-ci-tools\blender.iss /Sbyparam=$p
|
||||
shell: cmd.exe #does not work in powershell
|
||||
- when: # Setup certificates and build installers signed for tagged builds
|
||||
condition: << pipeline.git.tag >>
|
||||
steps:
|
||||
- run:
|
||||
name: "Digicert Signing Manager Setup"
|
||||
command: |
|
||||
cd C:\
|
||||
curl.exe -X GET https://one.digicert.com/signingmanager/api-ui/v1/releases/smtools-windows-x64.msi/download -H "x-api-key:$env:SM_API_KEY" -o smtools-windows-x64.msi
|
||||
msiexec.exe /i smtools-windows-x64.msi /quiet /qn | Wait-Process
|
||||
- run:
|
||||
name: Create Auth & OV Signing Cert
|
||||
command: |
|
||||
cd C:\
|
||||
echo $env:SM_CLIENT_CERT_FILE_B64 > certificate.txt
|
||||
certutil -decode certificate.txt certificate.p12
|
||||
- run:
|
||||
name: Sync Certs
|
||||
command: |
|
||||
& $env:SSM\smksp_cert_sync.exe
|
||||
- run:
|
||||
name: Build Installer
|
||||
command: speckle-sharp-ci-tools\InnoSetup\ISCC.exe speckle-sharp-ci-tools\blender.iss /Sbyparam=$p /DSIGN_INSTALLER /DCODE_SIGNING_CERT_FINGERPRINT=%SM_CODE_SIGNING_CERT_SHA1_HASH%
|
||||
shell: cmd.exe #does not work in powershell
|
||||
- persist_to_workspace:
|
||||
root: ./
|
||||
paths:
|
||||
@@ -96,7 +116,8 @@ jobs:
|
||||
|
||||
build-installer-mac:
|
||||
macos:
|
||||
xcode: 12.5.1
|
||||
xcode: 13.4.1
|
||||
resource_class: macos.m1.medium.gen1
|
||||
parameters:
|
||||
runtime:
|
||||
type: string
|
||||
@@ -109,6 +130,9 @@ jobs:
|
||||
- checkout
|
||||
- attach_workspace:
|
||||
at: ./
|
||||
- run:
|
||||
name: Exit if External PR
|
||||
command: if [ "$CIRCLE_PR_REPONAME" ]; then circleci-agent step halt; fi
|
||||
- run:
|
||||
name: Install mono
|
||||
command: |
|
||||
@@ -214,7 +238,7 @@ workflows:
|
||||
filters: *build_filters
|
||||
|
||||
- build-installer-win:
|
||||
context: innosetup
|
||||
context: digicert-keylocker
|
||||
name: Windows Installer Build
|
||||
requires:
|
||||
- package-connector
|
||||
@@ -304,4 +328,4 @@ workflows:
|
||||
- Windows Installer Build
|
||||
- Mac Intel Build
|
||||
- Mac ARM Build
|
||||
filters: *deploy_filters
|
||||
filters: *deploy_filters
|
||||
|
||||
@@ -41,42 +41,57 @@ 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.
|
||||
We officially support Blender 3.3 and newer, on Windows and Mac.
|
||||
|
||||
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.
|
||||
- Select the user from the dropdown list in the `Users` panel. This will populate the `Projects` list with available projects for the selected user account.
|
||||
- Select a model and version from the dropdown menus.
|
||||
- Click on `Receive` to download and convert the objects from the selected model version. The objects will be linked into a Blender Collection, named `<PROJECT_NAME> [ <MODEL_NAME> @ <VERSION_ID> ]`.
|
||||
- Click on `Open Model in Web` to view the model 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 is 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]):
|
||||
@@ -41,7 +41,7 @@ class BlenderCommitObjectBuilder(CommitObjectBuilder[Object]):
|
||||
# Set the Child -> Parent relationships
|
||||
parent = native_object.parent
|
||||
|
||||
parent_collections: Tuple[Collection] = native_object.users_collection # type: ignore
|
||||
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)
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
@@ -554,7 +540,7 @@ def transform_to_native(transform: Transform, scale: float) -> MMatrix:
|
||||
)
|
||||
# scale the translation
|
||||
for i in range(3):
|
||||
mat[i][3] *= scale # type: ignore
|
||||
mat[i][3] *= scale
|
||||
return mat
|
||||
|
||||
def plane_to_native_transform(plane: Plane, fallback_scale:float = 1) -> 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,43 +711,87 @@ 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)
|
||||
or getattr(speckle_object, "family", 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_SEPERATOR)
|
||||
MAX_NAME_LENGTH = OBJECT_NAME_MAX_LENGTH - SPECKLE_ID_LENGTH - len(OBJECT_NAME_SPECKLE_SEPARATOR)
|
||||
|
||||
return name[:MAX_NAME_LENGTH]
|
||||
|
||||
@@ -777,7 +807,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")
|
||||
|
||||
@@ -99,7 +101,7 @@ def mesh_to_speckle_meshes(blender_object: Object, data: bpy.types.Mesh) -> List
|
||||
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] # type: ignore
|
||||
scaled_vertices = [tuple(transform @ x.co * UnitsScale) for x in data.vertices]
|
||||
|
||||
# Create Speckle meshes for each material
|
||||
submeshes = []
|
||||
@@ -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
|
||||
@@ -435,8 +445,8 @@ def camera_to_speckle_view(blender_object: Object, data: NCamera) -> Base:
|
||||
raise Exception(f"Cameras of type {data.type} are not currently supported")
|
||||
|
||||
matrix = cast(MMatrix, blender_object.matrix_world)
|
||||
up = matrix.col[1].xyz # type: ignore
|
||||
forwards = -matrix.col[2].xyz # type: ignore
|
||||
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!
|
||||
@@ -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:
|
||||
|
||||
+17
-13
@@ -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"
|
||||
|
||||
@@ -165,7 +168,7 @@ def add_faces(speckle_mesh: Mesh, blender_mesh: BMesh, indexOffset: int, materia
|
||||
i += 1
|
||||
try:
|
||||
f = blender_mesh.faces.new(
|
||||
[blender_mesh.verts[x + indexOffset] for x in sfaces[i : i + n]] # type: ignore
|
||||
[blender_mesh.verts[x + indexOffset] for x in sfaces[i : i + n]]
|
||||
)
|
||||
f.material_index = materialIndex
|
||||
f.smooth = smooth
|
||||
@@ -195,10 +198,10 @@ def add_colors(speckle_mesh: Mesh, blender_mesh: BMesh):
|
||||
)
|
||||
|
||||
# Make vertex colors
|
||||
if len(scolors) == len(blender_mesh.verts): # type: ignore
|
||||
if len(scolors) == len(blender_mesh.verts):
|
||||
color_layer = blender_mesh.loops.layers.color.new("Col")
|
||||
|
||||
for face in blender_mesh.faces: # type: ignore
|
||||
for face in blender_mesh.faces:
|
||||
for loop in face.loops:
|
||||
loop[color_layer] = colors[loop.vert.index]
|
||||
|
||||
@@ -217,21 +220,21 @@ def add_uv_coords(speckle_mesh: Mesh, blender_mesh: BMesh):
|
||||
try:
|
||||
uv = []
|
||||
|
||||
if len(s_uvs) // 2 == len(blender_mesh.verts): # type: ignore
|
||||
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}" # type: ignore
|
||||
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: # type: ignore
|
||||
for f in blender_mesh.faces:
|
||||
for l in f.loops:
|
||||
luv = l[uv_layer]
|
||||
luv.uv = uv[l.vert.index]
|
||||
@@ -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"):
|
||||
@@ -446,10 +450,10 @@ 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: #type: ignore
|
||||
for child in obj.children:
|
||||
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])
|
||||
|
||||
@@ -138,7 +138,6 @@ def ensure_pip() -> None:
|
||||
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")
|
||||
assert path.exists()
|
||||
return path
|
||||
|
||||
|
||||
@@ -147,29 +146,51 @@ def install_requirements(host_application: str) -> None:
|
||||
# script path. Here we'll install the
|
||||
# dependencies
|
||||
path = connector_installation_path(host_application)
|
||||
print(f"Installing Speckle dependencies to {path}")
|
||||
|
||||
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(get_requirements_path()),
|
||||
str(requirements_path),
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
|
||||
if completed_process.returncode != 0:
|
||||
m = f"Failed to install dependenices through pip, got {completed_process.returncode} return code"
|
||||
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:
|
||||
|
||||
@@ -20,6 +20,7 @@ from .streams import (
|
||||
CopyStreamId,
|
||||
CopyCommitId,
|
||||
CopyBranchName,
|
||||
CopyModelId,
|
||||
)
|
||||
from .commit import DeleteCommit
|
||||
from .misc import OpenSpeckleGuide, OpenSpeckleTutorials, OpenSpeckleForum
|
||||
@@ -33,6 +34,7 @@ operator_classes = [
|
||||
CopyStreamId,
|
||||
CopyCommitId,
|
||||
CopyBranchName,
|
||||
CopyModelId,
|
||||
]
|
||||
|
||||
operator_classes.extend([DeleteCommit])
|
||||
|
||||
@@ -4,24 +4,26 @@ 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):
|
||||
"""
|
||||
Deletes the selected commit from the selected stream.
|
||||
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 commit"
|
||||
bl_label = "Delete Version"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Delete active commit permanently"
|
||||
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
|
||||
@@ -37,29 +39,34 @@ class DeleteCommit(bpy.types.Operator):
|
||||
return {"CANCELLED"}
|
||||
|
||||
def execute(self, context):
|
||||
try:
|
||||
self.delete_commit(context)
|
||||
return {"FINISHED"}
|
||||
except Exception as ex:
|
||||
print(f"{self.bl_idname}: failed: {ex}")
|
||||
return {"CANCELLED"}
|
||||
|
||||
def delete_commit(self, context: bpy.types.Context) -> None:
|
||||
|
||||
if not self.are_you_sure:
|
||||
raise Exception("Cancelled by user")
|
||||
|
||||
_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, _, commit) = speckle.validate_commit_selection()
|
||||
(_, 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"{self.bl_idname}: succeeded - commit {commit.id} ({commit.message}) has been deleted from stream {stream.id}")
|
||||
print(f"Version {commit.id} ({commit.message}) of model {branch.id} ({branch.name}) has been deleted from project {stream.id} ({stream.name})")
|
||||
|
||||
|
||||
@@ -1,35 +1,65 @@
|
||||
import bpy
|
||||
import webbrowser
|
||||
from specklepy.logging import metrics
|
||||
|
||||
|
||||
|
||||
|
||||
class OpenSpeckleGuide(bpy.types.Operator):
|
||||
bl_idname = "speckle.open_speckle_guide"
|
||||
bl_label = "Speckle Guide"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Browse the documentation on the Speckle Guide"
|
||||
_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("https://speckle.guide/user/blender.html")
|
||||
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 = "Visit our tutorials portal for learning resources"
|
||||
bl_description = f"Visit our tutorials portal for learning resources ({_tutorials_url})"
|
||||
|
||||
def execute(self, context):
|
||||
webbrowser.open("https://speckle.systems/tutorials/")
|
||||
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 = "Ask questions and join the discussion on our community forum"
|
||||
bl_description = f"Ask questions and join the discussion on our community forum ({_forum_url})"
|
||||
|
||||
def execute(self, context):
|
||||
webbrowser.open("https://speckle.community/")
|
||||
webbrowser.open(self._forum_url)
|
||||
metrics.track(
|
||||
"Connector Action",
|
||||
None,
|
||||
custom_props={
|
||||
"name": "OpenSpeckleForum"
|
||||
},
|
||||
)
|
||||
return {"FINISHED"}
|
||||
@@ -11,8 +11,9 @@ 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
|
||||
|
||||
|
||||
@deprecated
|
||||
class UpdateObject(bpy.types.Operator):
|
||||
"""
|
||||
Update local (receive) or remote (send) object depending on
|
||||
@@ -21,7 +22,7 @@ class UpdateObject(bpy.types.Operator):
|
||||
"""
|
||||
|
||||
bl_idname = "speckle.update_object"
|
||||
bl_label = "Update Object"
|
||||
bl_label = "Update Object (DEPRECATED)"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
client = None
|
||||
@@ -56,19 +57,26 @@ 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"}
|
||||
return {"CANCELLED"}
|
||||
|
||||
|
||||
@deprecated
|
||||
class ResetObject(bpy.types.Operator):
|
||||
"""
|
||||
Reset Speckle object settings
|
||||
"""
|
||||
|
||||
bl_idname = "speckle.reset_object"
|
||||
bl_label = "Reset Object"
|
||||
bl_label = "Reset Object (DEPRECATED)"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
@@ -79,16 +87,24 @@ 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"}
|
||||
|
||||
|
||||
@deprecated
|
||||
class DeleteObject(bpy.types.Operator):
|
||||
"""
|
||||
Delete object from the server and update relevant stream
|
||||
"""
|
||||
|
||||
bl_idname = "speckle.delete_object"
|
||||
bl_label = "Delete Object"
|
||||
bl_label = "Delete Object (DEPRECATED)"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
@@ -125,6 +141,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
|
||||
@@ -135,7 +159,7 @@ class UploadNgonsAsPolylines(bpy.types.Operator):
|
||||
"""
|
||||
|
||||
bl_idname = "speckle.upload_ngons_as_polylines"
|
||||
bl_label = "Upload Ngons As Polylines"
|
||||
bl_label = "Upload Ngons As Polylines (DEPRECATED)"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
clear_stream: BoolProperty(
|
||||
@@ -197,6 +221,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):
|
||||
@@ -217,7 +248,7 @@ def get_custom_speckle_props(self, context):
|
||||
|
||||
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
|
||||
@@ -225,7 +256,7 @@ class SelectIfSameCustomProperty(bpy.types.Operator):
|
||||
"""
|
||||
|
||||
bl_idname = "speckle.select_if_same_custom_props"
|
||||
bl_label = "Select Identical Custom Props"
|
||||
bl_label = "Select Identical Custom Props (DEPRECATED)"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
custom_prop: EnumProperty(
|
||||
@@ -267,9 +298,17 @@ class SelectIfSameCustomProperty(bpy.types.Operator):
|
||||
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
|
||||
@@ -277,7 +316,7 @@ class SelectIfHasCustomProperty(bpy.types.Operator):
|
||||
"""
|
||||
|
||||
bl_idname = "speckle.select_if_has_custom_props"
|
||||
bl_label = "Select Same Custom Prop"
|
||||
bl_label = "Select Same Custom Prop (DEPRECATED)"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
custom_prop: EnumProperty(
|
||||
@@ -315,4 +354,13 @@ class SelectIfHasCustomProperty(bpy.types.Operator):
|
||||
else:
|
||||
obj.select_set(False)
|
||||
|
||||
metrics.track(
|
||||
"Connector Action",
|
||||
None,
|
||||
custom_props={
|
||||
"name": "SelectIfHasCustomProperty"
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
+347
-193
@@ -2,7 +2,7 @@
|
||||
Stream operators
|
||||
"""
|
||||
from math import radians
|
||||
from typing import Callable, Dict, Optional, Union, cast
|
||||
from typing import Callable, Dict, Optional, Tuple, Union, cast
|
||||
import webbrowser
|
||||
import bpy
|
||||
from bpy.props import (
|
||||
@@ -15,6 +15,7 @@ from bpy.types import (
|
||||
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,
|
||||
@@ -31,13 +32,13 @@ from bpy_speckle.functions import (
|
||||
get_scale_length,
|
||||
)
|
||||
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.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
|
||||
@@ -74,25 +75,24 @@ 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"),
|
||||
]
|
||||
|
||||
class ReceiveStreamObjects(bpy.types.Operator):
|
||||
"""
|
||||
Receive stream objects
|
||||
Receive objects from selected model version
|
||||
"""
|
||||
|
||||
bl_idname = "speckle.receive_stream_objects"
|
||||
bl_label = "Download Stream Objects"
|
||||
bl_label = "Receive"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Receive objects from active stream"
|
||||
bl_description = "Receive objects from selected model version"
|
||||
|
||||
|
||||
clean_meshes: BoolProperty(name="Clean Meshes", default=False)
|
||||
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 recieve operation")
|
||||
receive_instances_as: EnumProperty(items=INSTANCES_SETTINGS, name="Receive Instances As", default="collection_instance", description="How to receive speckle Instances")
|
||||
#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):
|
||||
@@ -130,26 +130,31 @@ class ReceiveStreamObjects(bpy.types.Operator):
|
||||
# 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
|
||||
bpy.context.view_layer.objects.active = None # type: ignore
|
||||
|
||||
def execute(self, context):
|
||||
try:
|
||||
self.receive(context)
|
||||
return {"FINISHED"}
|
||||
except Exception as ex:
|
||||
_report(f"Failed to receive objects: {type(ex)} {ex}")
|
||||
return {"CANCELLED"}
|
||||
self.receive(context)
|
||||
return {"FINISHED"}
|
||||
|
||||
def receive(self, context: Context) -> None:
|
||||
bpy.context.view_layer.objects.active = 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,
|
||||
@@ -158,17 +163,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 +179,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]
|
||||
@@ -191,7 +189,7 @@ class ReceiveStreamObjects(bpy.types.Operator):
|
||||
|
||||
# ensure commit object has a name if not already
|
||||
if not commit_object.name:
|
||||
commit_object.name = "{} [ {} @ {} ]".format(stream.name, branch.name, commit.id) # Matches Rhino "Create" naming
|
||||
commit_object.name = f"{stream.name} [ {branch.name} @ {commit.id} ]" # Matches Rhino "Create" naming
|
||||
|
||||
for item in traversalFunc.traverse(commit_object):
|
||||
|
||||
@@ -199,7 +197,8 @@ class ReceiveStreamObjects(bpy.types.Operator):
|
||||
|
||||
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")
|
||||
if not current or not current.id:
|
||||
raise Exception(f"{current} was an invalid Speckle object")
|
||||
|
||||
#Convert the object!
|
||||
converted_data_type: str
|
||||
@@ -221,7 +220,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:
|
||||
@@ -246,19 +245,19 @@ class ReceiveStreamObjects(bpy.types.Operator):
|
||||
|
||||
class SendStreamObjects(bpy.types.Operator):
|
||||
"""
|
||||
Send stream objects
|
||||
Send selected objects to selected model
|
||||
"""
|
||||
|
||||
bl_idname = "speckle.send_stream_objects"
|
||||
bl_label = "Send stream objects"
|
||||
bl_label = "Send"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Send selected objects to active stream"
|
||||
bl_description = "Send selected objects to selected model"
|
||||
|
||||
apply_modifiers: BoolProperty(name="Apply modifiers", default=True)
|
||||
apply_modifiers: BoolProperty(name="Apply modifiers", default=True) # type: ignore
|
||||
commit_message: StringProperty(
|
||||
name="Message",
|
||||
default="Pushed elements from Blender.",
|
||||
)
|
||||
default="Sent elements from Blender.",
|
||||
) # type: ignore
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
@@ -268,23 +267,22 @@ class SendStreamObjects(bpy.types.Operator):
|
||||
|
||||
def invoke(self, context, event):
|
||||
wm = context.window_manager
|
||||
if len(context.scene.speckle.users) <= 0: return {"CANCELLED"}
|
||||
|
||||
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"Pushed {N} element from Blender."
|
||||
self.commit_message = f"Sent {N} element from Blender."
|
||||
else:
|
||||
self.commit_message = f"Pushed {N} elements from Blender."
|
||||
self.commit_message = f"Sent {N} elements from Blender."
|
||||
return wm.invoke_props_dialog(self)
|
||||
|
||||
|
||||
def execute(self, context):
|
||||
try:
|
||||
self.send(context)
|
||||
return {"FINISHED"}
|
||||
except Exception as ex:
|
||||
_report(f"Send failed: {ex}")
|
||||
return {"CANCELLED"}
|
||||
self.send(context)
|
||||
return {"FINISHED"}
|
||||
|
||||
def send(self, context: Context) -> None:
|
||||
|
||||
@@ -350,6 +348,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(
|
||||
@@ -364,7 +372,18 @@ class SendStreamObjects(bpy.types.Operator):
|
||||
message=self.commit_message,
|
||||
source_application="blender",
|
||||
)
|
||||
_report(f"Commit Created {user.server_url}/streams/{stream.id}/commits/{COMMIT_ID}")
|
||||
|
||||
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()
|
||||
@@ -376,39 +395,67 @@ class SendStreamObjects(bpy.types.Operator):
|
||||
|
||||
class ViewStreamDataApi(bpy.types.Operator):
|
||||
bl_idname = "speckle.view_stream_data_api"
|
||||
bl_label = "Open Stream in Web"
|
||||
bl_label = "Open Model in Web"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "View the stream in the web browser"
|
||||
bl_description = "View the selected model in the web browser"
|
||||
|
||||
def execute(self, context):
|
||||
try:
|
||||
self.view_stream_data_api(context)
|
||||
return {"FINISHED"}
|
||||
except Exception as ex:
|
||||
_report(f"{self.bl_idname} failed: {ex}")
|
||||
return {"CANCELLED"}
|
||||
self.view_stream_data_api(context)
|
||||
return {"FINISHED"}
|
||||
|
||||
def view_stream_data_api(self, context: Context) -> None:
|
||||
speckle = get_speckle(context)
|
||||
|
||||
(user, stream) = speckle.validate_stream_selection()
|
||||
url = self._get_url_from_selection(speckle)
|
||||
|
||||
_report(f"Opening {url} in web browser")
|
||||
|
||||
if not webbrowser.open("%s/streams/%s" % (user.server_url, stream.id), new=2):
|
||||
raise Exception("Failed to open stream in 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 a stream using its url
|
||||
Add / select an existing project by providing its URL
|
||||
"""
|
||||
|
||||
bl_idname = "speckle.add_stream_from_url"
|
||||
bl_label = "Add stream from URL"
|
||||
bl_label = "Add Project From URL"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Add an existing stream by providing its URL"
|
||||
bl_description = "Add / select an existing project by providing its URL"
|
||||
stream_url: StringProperty(
|
||||
name="Stream URL", default="https://speckle.xyz/streams/3073b96e86"
|
||||
)
|
||||
name="Project URL", default=""
|
||||
) # type: ignore
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
@@ -424,13 +471,26 @@ class AddStreamFromURL(bpy.types.Operator):
|
||||
return {"CANCELLED"}
|
||||
|
||||
def execute(self, context):
|
||||
try:
|
||||
self.add_stream_from_url(context)
|
||||
return {"FINISHED"}
|
||||
except Exception as ex:
|
||||
_report(f"{self.bl_idname} failed: {ex}")
|
||||
return {"CANCELLED"}
|
||||
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)
|
||||
|
||||
@@ -440,28 +500,20 @@ class AddStreamFromURL(bpy.types.Operator):
|
||||
None,
|
||||
)
|
||||
if user_index is None:
|
||||
raise Exception("Unable to find user stream server")
|
||||
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=20)
|
||||
stream = client.stream.get(wrapper.stream_id, branch_limit=LoadUserStreams.branch_limit, commit_limit=LoadUserStreams.commits_limit)
|
||||
if not isinstance(stream, Stream):
|
||||
raise SpeckleException("Could not get the requested stream")
|
||||
raise SpeckleException(f"Could not get the requested project {wrapper.stream_id}")
|
||||
|
||||
index, b_stream = next(
|
||||
((i, s) for i, s in enumerate(user.streams) if s.id == stream.id),
|
||||
(None, None),
|
||||
)
|
||||
(index, b_stream) = self._get_or_add_stream(user, stream)
|
||||
user.active_stream = index
|
||||
|
||||
if index is None:
|
||||
add_user_stream(user, stream)
|
||||
user.active_stream, b_stream = next(
|
||||
(i, s) for i, s in enumerate(user.streams) if s.id == stream.id
|
||||
)
|
||||
else:
|
||||
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)
|
||||
@@ -481,22 +533,30 @@ 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):
|
||||
"""
|
||||
Create new stream
|
||||
Create a new Speckle project using the selected user account
|
||||
"""
|
||||
|
||||
bl_idname = "speckle.create_stream"
|
||||
bl_label = "Create stream"
|
||||
bl_label = "Create Project"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Create new stream"
|
||||
bl_description = "Create a new Speckle project using the selected user account"
|
||||
|
||||
stream_name: StringProperty(name="Stream name")
|
||||
stream_name: StringProperty(name="Project name") # type: ignore
|
||||
stream_description: StringProperty(
|
||||
name="Stream description", default="This is a Blender stream."
|
||||
)
|
||||
name="Project description", default="My new project"
|
||||
) # type: ignore
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
@@ -513,12 +573,8 @@ class CreateStream(bpy.types.Operator):
|
||||
return {"CANCELLED"}
|
||||
|
||||
def execute(self, context):
|
||||
try:
|
||||
self.create_stream(context)
|
||||
return {"FINISHED"}
|
||||
except Exception as ex:
|
||||
_report(f"{self.bl_idname} failed: {ex}")
|
||||
return {"CANCELLED"}
|
||||
self.create_stream(context)
|
||||
return {"FINISHED"}
|
||||
|
||||
def create_stream(self, context: Context) -> None:
|
||||
speckle = get_speckle(context)
|
||||
@@ -541,24 +597,34 @@ class CreateStream(bpy.types.Operator):
|
||||
|
||||
if context.area:
|
||||
context.area.tag_redraw()
|
||||
|
||||
metrics.track(
|
||||
"Connector Action",
|
||||
client.account,
|
||||
custom_props={
|
||||
"name": "create_stream"
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@deprecated
|
||||
class DeleteStream(bpy.types.Operator):
|
||||
"""
|
||||
Delete stream
|
||||
Permanently delete the selected project
|
||||
"""
|
||||
|
||||
bl_idname = "speckle.delete_stream"
|
||||
bl_label = "Delete stream"
|
||||
bl_label = "Delete Project"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Delete selected stream permanently"
|
||||
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)
|
||||
delete_collection: BoolProperty(name="Delete collection", default=False) # type: ignore
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
@@ -575,19 +641,16 @@ class DeleteStream(bpy.types.Operator):
|
||||
return {"CANCELLED"}
|
||||
|
||||
def execute(self, context):
|
||||
try:
|
||||
self.delete_stream(context)
|
||||
return {"FINISHED"}
|
||||
except Exception as ex:
|
||||
_report(f"{self.bl_idname} failed: {ex}")
|
||||
return {"CANCELLED"}
|
||||
|
||||
def delete_stream(self, context: Context) -> None:
|
||||
if not self.are_you_sure:
|
||||
raise Exception("Cancled by user")
|
||||
|
||||
_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()
|
||||
|
||||
@@ -595,7 +658,8 @@ class DeleteStream(bpy.types.Operator):
|
||||
|
||||
client.stream.delete(id=stream.id)
|
||||
|
||||
if self.delete_collection:
|
||||
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]
|
||||
@@ -607,7 +671,165 @@ class DeleteStream(bpy.types.Operator):
|
||||
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
|
||||
@@ -632,79 +854,11 @@ class SelectOrphanObjects(bpy.types.Operator):
|
||||
else:
|
||||
o.select = False
|
||||
|
||||
return {"FINISHED"}
|
||||
metrics.track(
|
||||
"Connector Action",
|
||||
custom_props={
|
||||
"name": "SelectOrphanObjects"
|
||||
},
|
||||
)
|
||||
|
||||
class CopyStreamId(bpy.types.Operator):
|
||||
"""
|
||||
Copy stream ID to clipboard
|
||||
"""
|
||||
|
||||
bl_idname = "speckle.stream_copy_id"
|
||||
bl_label = "Copy stream ID"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Copy stream ID to clipboard"
|
||||
|
||||
def execute(self, context):
|
||||
try:
|
||||
self.copy_stream_id(context)
|
||||
return {"FINISHED"}
|
||||
except Exception as ex:
|
||||
_report(f"{self.bl_idname} failed: {ex}")
|
||||
return {"CANCELLED"}
|
||||
|
||||
def copy_stream_id(self, context) -> None:
|
||||
speckle = get_speckle(context)
|
||||
|
||||
(_, stream) = speckle.validate_stream_selection()
|
||||
bpy.context.window_manager.clipboard = stream.id
|
||||
|
||||
|
||||
class CopyCommitId(bpy.types.Operator):
|
||||
"""
|
||||
Copy commit ID to clipboard
|
||||
"""
|
||||
|
||||
bl_idname = "speckle.commit_copy_id"
|
||||
bl_label = "Copy commit ID"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Copy commit ID to clipboard"
|
||||
|
||||
def execute(self, context):
|
||||
try:
|
||||
self.copy_commit_id(context)
|
||||
return {"FINISHED"}
|
||||
except Exception as ex:
|
||||
_report(f"{self.bl_idname} failed: {ex}")
|
||||
return {"CANCELLED"}
|
||||
|
||||
def copy_commit_id(self, context) -> None:
|
||||
speckle = get_speckle(context)
|
||||
|
||||
(_, _, _, commit) = speckle.validate_commit_selection()
|
||||
bpy.context.window_manager.clipboard = commit.id
|
||||
|
||||
|
||||
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):
|
||||
try:
|
||||
self.copy_branch_id(context)
|
||||
return {"FINISHED"}
|
||||
except Exception as ex:
|
||||
_report(f"{self.bl_idname} failed: {ex}")
|
||||
return {"CANCELLED"}
|
||||
|
||||
def copy_branch_id(self, context) -> None:
|
||||
speckle = get_speckle(context)
|
||||
|
||||
(_, _, branch) = speckle.validate_branch_selection()
|
||||
|
||||
bpy.context.window_manager.clipboard = branch.name
|
||||
return {"FINISHED"}
|
||||
+121
-80
@@ -1,15 +1,16 @@
|
||||
"""
|
||||
User account operators
|
||||
"""
|
||||
from typing import cast
|
||||
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 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 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):
|
||||
"""
|
||||
@@ -17,12 +18,20 @@ class ResetUsers(bpy.types.Operator):
|
||||
"""
|
||||
|
||||
bl_idname = "speckle.users_reset"
|
||||
bl_label = "Reset users"
|
||||
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()
|
||||
@@ -37,134 +46,166 @@ class ResetUsers(bpy.types.Operator):
|
||||
|
||||
class LoadUsers(bpy.types.Operator):
|
||||
"""
|
||||
Load all users from local user database
|
||||
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_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 = cast(SpeckleSceneSettings, context.scene.speckle) #type: ignore
|
||||
users = speckle.users
|
||||
speckle = get_speckle(context)
|
||||
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
|
||||
|
||||
if not stream.branches:
|
||||
return
|
||||
|
||||
# branches = [branch for branch in stream.branches.items if branch.name != "globals"]
|
||||
for b in stream.branches.items:
|
||||
branch = s.branches.add()
|
||||
branch.name = b.name
|
||||
|
||||
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
|
||||
|
||||
if hasattr(s, "baseProperties"):
|
||||
s.units = stream.baseProperties.units # type: ignore
|
||||
else:
|
||||
s.units = "Meters"
|
||||
_report(f"Adding stream {s.id} - {s.name}")
|
||||
|
||||
if stream.branches:
|
||||
s.load_stream_branches(stream)
|
||||
|
||||
|
||||
class LoadUserStreams(bpy.types.Operator):
|
||||
"""
|
||||
Load all available streams for active user user
|
||||
(Re)Load all available projects for active user
|
||||
"""
|
||||
|
||||
bl_idname = "speckle.load_user_streams"
|
||||
bl_label = "Load user streams"
|
||||
bl_label = "Load User's Projects"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "(Re)load all available user streams"
|
||||
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):
|
||||
try:
|
||||
self.add_stream_from_url(context)
|
||||
return {"FINISHED"}
|
||||
except Exception as ex:
|
||||
_report(f"{self.bl_idname} failed: {ex}")
|
||||
return {"CANCELLED"}
|
||||
self.load_user_stream(context)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
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 projects") from ex
|
||||
|
||||
if not streams:
|
||||
_report("Failed to retrieve 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()
|
||||
|
||||
default_units = "Meters"
|
||||
|
||||
for s in streams:
|
||||
for i, s in enumerate(streams):
|
||||
assert(s.id)
|
||||
sstream = client.stream.get(id=s.id, branch_limit=20)
|
||||
add_user_stream(user, sstream)
|
||||
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"
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import bpy
|
||||
|
||||
|
||||
class SpeckleCollectionSettings(bpy.types.PropertyGroup):
|
||||
enabled: bpy.props.BoolProperty(default=False, name="Enabled")
|
||||
enabled: bpy.props.BoolProperty(default=False, name="Enabled") # type: ignore
|
||||
|
||||
send_or_receive: bpy.props.EnumProperty(
|
||||
name="Mode",
|
||||
@@ -13,7 +13,6 @@ class SpeckleCollectionSettings(bpy.types.PropertyGroup):
|
||||
("send", "Send", "Send data to Speckle server."),
|
||||
("receive", "Receive", "Receive data from Speckle server."),
|
||||
),
|
||||
)
|
||||
stream_id: bpy.props.StringProperty(default="")
|
||||
name: bpy.props.StringProperty(default="")
|
||||
units: bpy.props.StringProperty(default="")
|
||||
) # type: ignore
|
||||
stream_id: bpy.props.StringProperty(default="") # type: ignore
|
||||
name: bpy.props.StringProperty(default="") # type: ignore
|
||||
|
||||
@@ -13,6 +13,6 @@ class SpeckleObjectSettings(bpy.types.PropertyGroup):
|
||||
("send", "Send", "Send data to Speckle server."),
|
||||
("receive", "Receive", "Receive data from Speckle server."),
|
||||
),
|
||||
)
|
||||
stream_id: bpy.props.StringProperty(default="")
|
||||
object_id: bpy.props.StringProperty(default="")
|
||||
) # type: ignore
|
||||
stream_id: bpy.props.StringProperty(default="") # type: ignore
|
||||
object_id: bpy.props.StringProperty(default="") # type: ignore
|
||||
|
||||
+175
-60
@@ -1,77 +1,113 @@
|
||||
"""
|
||||
Scene properties
|
||||
"""
|
||||
from typing import Optional, Tuple
|
||||
from typing import Iterable, Optional, Tuple, Union, cast
|
||||
from dataclasses import dataclass
|
||||
import bpy
|
||||
from bpy.props import (
|
||||
StringProperty,
|
||||
BoolProperty,
|
||||
FloatProperty,
|
||||
CollectionProperty,
|
||||
EnumProperty,
|
||||
IntProperty,
|
||||
PointerProperty,
|
||||
)
|
||||
|
||||
from bpy_speckle.clients import speckle_clients
|
||||
from specklepy.core.api.models import Stream
|
||||
|
||||
class SpeckleSceneObject(bpy.types.PropertyGroup):
|
||||
name: bpy.props.StringProperty(default="")
|
||||
name: bpy.props.StringProperty(default="") # type: ignore
|
||||
|
||||
|
||||
class SpeckleCommitObject(bpy.types.PropertyGroup):
|
||||
id: StringProperty(default="")
|
||||
message: StringProperty(default="")
|
||||
author_name: StringProperty(default="")
|
||||
author_id: StringProperty(default="")
|
||||
created_at: StringProperty(default="")
|
||||
source_application: StringProperty(default="")
|
||||
referenced_object: StringProperty(default="")
|
||||
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(self.commits)
|
||||
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")
|
||||
commits: CollectionProperty(type=SpeckleCommitObject)
|
||||
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="Commit",
|
||||
description="Active commit",
|
||||
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.name, i)
|
||||
for i, branch in enumerate(self.branches)
|
||||
(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="SpeckleStream")
|
||||
description: StringProperty(default="No description provided.")
|
||||
id: StringProperty(default="")
|
||||
units: StringProperty(default="Meters")
|
||||
query: StringProperty(default="")
|
||||
branches: CollectionProperty(type=SpeckleBranchObject)
|
||||
name: StringProperty(default="") # type: ignore
|
||||
description: StringProperty(default="") # type: ignore
|
||||
id: StringProperty(default="") # type: ignore
|
||||
branches: CollectionProperty(type=SpeckleBranchObject) # type: ignore
|
||||
branch: EnumProperty(
|
||||
name="Branch",
|
||||
description="Active branch",
|
||||
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)
|
||||
@@ -79,22 +115,35 @@ class SpeckleStreamObject(bpy.types.PropertyGroup):
|
||||
return self.branches[selected_index]
|
||||
return None
|
||||
|
||||
|
||||
class SpeckleUserObject(bpy.types.PropertyGroup):
|
||||
server_name: StringProperty(default="SpeckleXYZ")
|
||||
server_url: StringProperty(default="https://speckle.xyz")
|
||||
id: StringProperty(default="")
|
||||
name: StringProperty(default="Speckle User")
|
||||
email: StringProperty(default="user@speckle.xyz")
|
||||
company: StringProperty(default="SpeckleSystems")
|
||||
streams: CollectionProperty(type=SpeckleStreamObject)
|
||||
active_stream: IntProperty(default=0)
|
||||
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):
|
||||
@@ -107,51 +156,55 @@ class SpeckleSceneSettings(bpy.types.PropertyGroup):
|
||||
name="Available streams",
|
||||
description="Available streams associated with user.",
|
||||
items=[],
|
||||
)
|
||||
) # type: ignore
|
||||
|
||||
users: CollectionProperty(type=SpeckleUserObject)
|
||||
users: CollectionProperty(type=SpeckleUserObject) # type: ignore
|
||||
|
||||
def get_users(self, context):
|
||||
USERS = cast(Iterable[SpeckleUserObject], self.users)
|
||||
return [
|
||||
(str(i), "{} ({})".format(user.email, user.server_name), user.server_url, i)
|
||||
for i, user in enumerate(self.users)
|
||||
(str(i), f"{user.email} ({user.server_name})", user.server_url, i)
|
||||
for i, user in enumerate(USERS)
|
||||
]
|
||||
|
||||
def set_user(self, context):
|
||||
bpy.ops.speckle.load_user_streams()
|
||||
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=set_user,
|
||||
update=user_update_hook,
|
||||
get=None,
|
||||
set=None,
|
||||
)
|
||||
) # type: ignore
|
||||
|
||||
objects: CollectionProperty(type=SpeckleSceneObject)
|
||||
objects: CollectionProperty(type=SpeckleSceneObject) # type: ignore
|
||||
|
||||
scale: FloatProperty(default=0.001)
|
||||
scale: FloatProperty(default=0.001) # type: ignore
|
||||
|
||||
user: StringProperty(
|
||||
name="User",
|
||||
description="Current user.",
|
||||
description="Current user",
|
||||
default="Speckle User",
|
||||
)
|
||||
) # type: ignore
|
||||
|
||||
receive_script: EnumProperty(
|
||||
name="Receive script",
|
||||
description="Script to run when receiving stream objects.",
|
||||
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="Script to run when sending stream objects.",
|
||||
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]
|
||||
@@ -161,7 +214,7 @@ class SpeckleSceneSettings(bpy.types.PropertyGroup):
|
||||
def validate_user_selection(self) -> SpeckleUserObject:
|
||||
user = self.get_active_user()
|
||||
if not user:
|
||||
raise SelectionException("No user selected/found")
|
||||
raise SelectionException("No user account selected/found")
|
||||
return user
|
||||
|
||||
def validate_stream_selection(self) -> Tuple[SpeckleUserObject, SpeckleStreamObject]:
|
||||
@@ -169,7 +222,7 @@ class SpeckleSceneSettings(bpy.types.PropertyGroup):
|
||||
|
||||
stream = user.get_active_stream()
|
||||
if not stream:
|
||||
raise SelectionException("No stream selected/found")
|
||||
raise SelectionException("No project selected/found")
|
||||
|
||||
return (user, stream)
|
||||
|
||||
@@ -178,14 +231,14 @@ class SpeckleSceneSettings(bpy.types.PropertyGroup):
|
||||
|
||||
branch = stream.get_active_branch()
|
||||
if not branch:
|
||||
raise SelectionException("No branch selected/found")
|
||||
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 commit selected/found")
|
||||
raise SelectionException("No model version selected/found")
|
||||
|
||||
return (user, stream, branch, commit)
|
||||
|
||||
@@ -193,4 +246,66 @@ class SelectionException(Exception):
|
||||
pass
|
||||
|
||||
def get_speckle(context: bpy.types.Context) -> SpeckleSceneSettings:
|
||||
return context.scene.speckle #type: ignore
|
||||
"""
|
||||
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
|
||||
@@ -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
|
||||
@@ -10,8 +10,9 @@ from bpy.props import (
|
||||
CollectionProperty,
|
||||
EnumProperty,
|
||||
)
|
||||
from deprecated import deprecated
|
||||
|
||||
|
||||
@deprecated
|
||||
class OBJECT_PT_speckle(bpy.types.Panel):
|
||||
bl_space_type = "PROPERTIES"
|
||||
# bl_idname = 'OBJECT_PT_speckle'
|
||||
@@ -28,7 +29,7 @@ class OBJECT_PT_speckle(bpy.types.Panel):
|
||||
layout.active = ob.speckle.enabled
|
||||
col = layout.column()
|
||||
col.prop(ob.speckle, "send_or_receive", expand=True)
|
||||
col.prop(ob.speckle, "stream_id", text="Stream ID")
|
||||
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")
|
||||
|
||||
+24
-35
@@ -4,20 +4,10 @@ Speckle UI elements for the 3d viewport
|
||||
|
||||
|
||||
import bpy
|
||||
from bpy.props import (
|
||||
StringProperty,
|
||||
BoolProperty,
|
||||
FloatProperty,
|
||||
CollectionProperty,
|
||||
EnumProperty,
|
||||
)
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
"""
|
||||
Compatibility
|
||||
TODO: evaluate if we should still support Blender <2.80
|
||||
"""
|
||||
from bpy_speckle.properties.scene import get_speckle
|
||||
|
||||
Region = "TOOLS" if bpy.app.version < (2, 80, 0) else "UI"
|
||||
|
||||
@@ -78,7 +68,7 @@ class VIEW3D_UL_SpeckleUsers(bpy.types.UIList):
|
||||
|
||||
class VIEW3D_UL_SpeckleStreams(bpy.types.UIList):
|
||||
"""
|
||||
Speckle stream list
|
||||
Speckle projects list
|
||||
"""
|
||||
|
||||
def draw_item(self, context, layout, data, stream, active_data, active_propname):
|
||||
@@ -94,7 +84,7 @@ class VIEW3D_UL_SpeckleStreams(bpy.types.UIList):
|
||||
|
||||
elif self.layout_type in {"GRID"}:
|
||||
layout.alignment = "CENTER"
|
||||
layout.label(text="Streams", icon_value=0)
|
||||
layout.label(text="Projects", icon_value=0)
|
||||
|
||||
|
||||
class VIEW3D_PT_SpeckleUser(bpy.types.Panel):
|
||||
@@ -106,10 +96,10 @@ class VIEW3D_PT_SpeckleUser(bpy.types.Panel):
|
||||
bl_region_type = Region
|
||||
bl_category = "Speckle"
|
||||
bl_context = "objectmode"
|
||||
bl_label = "User"
|
||||
bl_label = "User Account"
|
||||
|
||||
def draw(self, context):
|
||||
speckle = context.scene.speckle
|
||||
speckle = get_speckle(context)
|
||||
|
||||
layout = self.layout
|
||||
col = layout.column()
|
||||
@@ -119,28 +109,28 @@ class VIEW3D_PT_SpeckleUser(bpy.types.Panel):
|
||||
else:
|
||||
col.prop(speckle, "active_user", text="")
|
||||
user = speckle.users[int(speckle.active_user)]
|
||||
col.label(text="{} ({})".format(user.server_name, user.server_url))
|
||||
col.label(text="{} ({})".format(user.name, user.email))
|
||||
|
||||
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 Streams UI panel in the 3d viewport
|
||||
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 = "Streams"
|
||||
bl_label = "Projects"
|
||||
|
||||
def draw(self, context):
|
||||
speckle = context.scene.speckle
|
||||
speckle = get_speckle(context)
|
||||
col = self.layout.column()
|
||||
|
||||
if len(speckle.users) < 1:
|
||||
col.label(text="No stream data.")
|
||||
col.label(text="No Projects")
|
||||
else:
|
||||
user = speckle.users[int(speckle.active_user)]
|
||||
col.template_list(
|
||||
@@ -149,31 +139,31 @@ class VIEW3D_PT_SpeckleStreams(bpy.types.Panel):
|
||||
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.delete_stream", text="", icon="REMOVE")
|
||||
row.operator("speckle.load_user_streams", text="", icon="FILE_REFRESH")
|
||||
|
||||
|
||||
class VIEW3D_PT_SpeckleActiveStream(bpy.types.Panel):
|
||||
"""
|
||||
Speckle Active Streams UI panel in the 3d viewport
|
||||
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 stream"
|
||||
bl_label = "Active Project"
|
||||
|
||||
def draw(self, context):
|
||||
speckle = context.scene.speckle
|
||||
speckle = get_speckle(context)
|
||||
col = self.layout.column()
|
||||
|
||||
if len(speckle.users) < 1:
|
||||
col.label(text="No stream data.")
|
||||
col.label(text="No projects")
|
||||
else:
|
||||
user = speckle.users[int(speckle.active_user)]
|
||||
user = speckle.validate_user_selection()
|
||||
#user = speckle.users[int(speckle.active_user)]
|
||||
if len(user.streams) < 1:
|
||||
col.label(text="No active stream.")
|
||||
col.label(text="No active project")
|
||||
else:
|
||||
stream = user.streams[user.active_stream]
|
||||
# user.active_stream = min(user.active_stream, len(user.streams) - 1)
|
||||
@@ -183,14 +173,14 @@ class VIEW3D_PT_SpeckleActiveStream(bpy.types.Panel):
|
||||
col.separator()
|
||||
|
||||
row = col.row()
|
||||
row.prop(stream, "branch", text="")
|
||||
row.operator("speckle.branch_copy_name", text="", icon="COPY_ID")
|
||||
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="")
|
||||
row.prop(branch, "commit", text="Version")
|
||||
row.operator("speckle.commit_copy_id", text="", icon="COPY_ID")
|
||||
|
||||
if len(branch.commits) > 0:
|
||||
@@ -213,7 +203,7 @@ class VIEW3D_PT_SpeckleActiveStream(bpy.types.Panel):
|
||||
col.label(text=f"{commit.author_name} ({commit.author_id})")
|
||||
col.label(text=commit.source_application)
|
||||
else:
|
||||
col.label(text="No branches found!")
|
||||
col.label(text="No models found!")
|
||||
|
||||
col.separator()
|
||||
|
||||
@@ -225,7 +215,6 @@ class VIEW3D_PT_SpeckleActiveStream(bpy.types.Panel):
|
||||
subcol = row.column()
|
||||
subcol.operator("speckle.send_stream_objects", text="Send")
|
||||
subcol.prop(speckle, "send_script", text="")
|
||||
area.prop(stream, "query", text="Filter")
|
||||
|
||||
col.separator()
|
||||
|
||||
@@ -246,7 +235,7 @@ class VIEW3D_PT_SpeckleActiveStream(bpy.types.Panel):
|
||||
|
||||
area.separator()
|
||||
col.separator()
|
||||
col.operator("speckle.view_stream_data_api", text="Open Stream in Web")
|
||||
col.operator("speckle.view_stream_data_api", text="Open Model in Web")
|
||||
|
||||
|
||||
class VIEW3D_PT_SpeckleHelp(bpy.types.Panel):
|
||||
|
||||
Generated
+893
-671
File diff suppressed because it is too large
Load Diff
+4
-5
@@ -7,18 +7,17 @@ license = "Apache-2.0"
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = ">=3.8, <4.0.0"
|
||||
specklepy = "^2.15.1"
|
||||
specklepy = "^2.19.1"
|
||||
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"
|
||||
fake-bpy-module-latest = "^20240524"
|
||||
black = "23.11.0"
|
||||
pylint = "^2.15.7"
|
||||
ruff = "^0.0.166"
|
||||
ruff = "^0.4.4"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core>=1.0.0"]
|
||||
|
||||
Reference in New Issue
Block a user