Compare commits

...

15 Commits

Author SHA1 Message Date
Jedd Morgan 97d20ad7b1 Ci tweaks (#191) 2024-03-14 11:09:48 +00:00
Jedd Morgan 511d69314e feat(ci): [CNX-9125] Update to digicert-keylocker (#189)
* feat(ci): Update to digicert-keylocker

* removed pem
2024-03-11 17:14:36 +01:00
Jedd Morgan 21281e5d77 deprecated delete stream (#187) 2024-02-28 16:46:08 +00:00
Jedd Morgan 29bbdc69a2 chore(ci): CNX-9051 Update ci signing (#185)
* feat: CNX-8705 fe2 ur ls in blender (#182)

* Increased default branch get to 100 limit, and added as mesh conversion for text + surfaces + metaball

* poetry lock

* Upgraded typing module

* FE2 URL support

* Raised exceptions now display to user

* Fixed unused imports

* Update ci signing

* Update config.yml

* Bump Deps

* powershell

* More powershell
2024-02-27 11:29:33 +00:00
Jedd Morgan efe6e6a4a0 2.18 Update (#183)
* feat: CNX-8705 fe2 ur ls in blender (#182)

* Increased default branch get to 100 limit, and added as mesh conversion for text + surfaces + metaball

* poetry lock

* Upgraded typing module

* FE2 URL support

* Raised exceptions now display to user

* Fixed unused imports
2024-02-26 17:32:38 +00:00
Jedd Morgan f036109020 Increased default branch get to 100 limit, and added as mesh conversi… (#181)
* Increased default branch get to 100 limit, and added as mesh conversion for text + surfaces + metaball

* poetry lock
2024-02-07 18:05:00 +00:00
Jedd Morgan 86bc2dc590 Merge pull request #180 from specklesystems/jrm/update/2.17
chore(deps): lock deps
2023-11-29 11:57:35 +00:00
Jedd Morgan a34b6ad0c2 lock + readme 2023-11-29 11:56:50 +00:00
Jedd Morgan e436949ef9 Merge pull request #179 from specklesystems/jrm/4.0/material-support
feat(4.X): Added support for Blender 4.X material nodes
2023-11-11 20:18:37 +00:00
Jedd Morgan 6d8f4a4a80 Added support for Blender 4.X BSDF materials 2023-11-11 20:16:49 +00:00
Jedd Morgan dabb65427a Added defaults to converter settings so converter can be used without connector 2023-10-18 15:48:31 +01:00
Jedd Morgan 57ece17e8b Merge pull request #177 from specklesystems/jrm/chore/comments
chore: fixed some mistakes in code comments
2023-10-16 11:43:58 +01:00
Jedd Morgan 4362f737d0 chore: fixed some mistakes in code comments 2023-10-16 11:43:36 +01:00
Jedd Morgan b55df58313 Merge pull request #176 from specklesystems/jrm/blender/automate
Extracted some functions for automate
2023-10-13 13:02:28 +01:00
Jedd Morgan afa6722253 Extracted some functions for automate 2023-10-13 12:56:30 +01:00
16 changed files with 1021 additions and 933 deletions
+37 -14
View File
@@ -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:
@@ -109,6 +129,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 +237,7 @@ workflows:
filters: *build_filters
- build-installer-win:
context: innosetup
context: digicert-keylocker
name: Windows Installer Build
requires:
- package-connector
@@ -304,4 +327,4 @@ workflows:
- Windows Installer Build
- Mac Intel Build
- Mac ARM Build
filters: *deploy_filters
filters: *deploy_filters
+32 -16
View File
@@ -41,42 +41,58 @@ Give Speckle a try in no time by:
- [![docs](https://img.shields.io/badge/docs-speckle.guide-orange?style=for-the-badge&logo=read-the-docs&logoColor=white)](https://speckle.guide/user/blender.html) reference on almost any end-user and developer functionality
# Repo structure
# Blender Connector
The Speckle UI can be found in the 3d viewport toolbar (N), under the Speckle tab.
Head to the [**📚 documentation**](https://speckle.guide/user/blender.html) for more information.
## Disclaimer
This code is WIP and as such should be used with extreme caution on non-sensitive projects.
## Installation
1. Place `bpy_speckle` folder in your `addons` folder. On Windows this is typically `%APPDATA%/Blender Foundation/Blender/2.80/scripts/addons`.
2. Go to `Edit->Preferences` (Ctrl + Alt + U)
3. Go to the `Add-ons` tab
4. Find and enable `SpeckleBlender 2.0` in the `Scene` category. <!-- **If enabling for the first time, expect the UI to freeze for bit while it silently installs all the dependencies.** -->
5. The Speckle UI can be found in the 3d viewport toolbar (N), under the `Speckle` tab.
Currently, we are supporting all Blender 3.X versions on Windows and Mac.
We have experimental support for Blender 4.0 and greater.
Please follow our installation instructions on our [connector docs](https://speckle.guide/user/blender.html#installation)
## Usage
Once enabled in `Preferences -> Addons`,
The Speckle connector UI can be found in the 3d viewport toolbar (N), under the `Speckle` tab.
- Available user accounts are automatically detected and made available. To add user accounts use **Speckle Manager**.
- Select the user from the dropdown list in the `Users` panel. This will populate the `Streams` list with available streams for the selected user.
- Select a branch and commit from the dropdown menus.
- Click on `Receive` to download the objects from the selected stream, branch, and commit. The stream objects will be loaded into a Blender Collection, named `<STREAM_NAME> [ <STREAM_BRANCH> @ <BRANCH_COMMIT> ]`. <!-- You can filter the stream by entering a query into the `Filter` field (i.e. `properties.weight>10` or `type="Mesh"`). -->
- Click on `Open Stream in Web` to view the stream in your web browser.
## Caveats
## Supported Elements
- Mesh objects are supported. Breps are imported as meshes using their `displayValue` data.
- Curves have limited support: `Polylines` are supported; `NurbsCurves` are supported, though they are not guaranteed to look the same; `Lines` are supported; `Arcs` are not supported, though they are very roughly approximated; `PolyCurves` are supported for linear / polyline segments and very approximate arc segments. These conversions are a point of focus for further development.
The Blender Connector is still a work in progress and, as such, data sent from the Blender connector is a highly lossy exchange. Our connectors are ever evolving to facilitate more and more Speckle usecases. We welcome feedback, requests, edge cases, and contributions!
## Custom properties
The full matrix of supported Blender and Speckle types [can be found here](https://speckle.guide/user/support-tables.html#blender)
## Additional Features
- **SpeckleBlender** will look for a `texture_coordinates` property and use that to create a UV layer for the imported object. These texture coordinates are a space-separated list of floats (`[u v u v u v etc...]`) that is encoded as a base64 blob. This is subject to change as **SpeckleBlender** develops.
- If a `renderMaterial` property is found, **SpeckleBlender** will create a material named using the sub-property `renderMaterial.name`. If a material with that name already exists in Blender, **SpeckleBlender** will just assign that existing material to the object. This allows geometry to be updated without having to re-assign and re-create materials.
- Vertex colors are supported. The `colors` list from Speckle meshes is translated to a vertex color layer.
- Speckle properties will be imported as custom properties on Blender objects. Nested dictionaries are expanded to individual properties by flattening their key hierarchy. I.e. `propA:{'propB': {'propC':10, 'propD':'foobar'}}` is flattened to `propA.propB.propC = 10` and `propA.propB.propD = "foobar"`.
- If a `renderMaterial` property is found, **SpeckleBlender** will create a material named using the sub-property `renderMaterial.name`. If a material with that name already exists in Blender, **SpeckleBlender** will just assign that existing material to the object. This allows geometry to be updated without having to re-assign and re-create materials.
- Receiving vertex colors are supported. The `colors` list from Speckle meshes is translated to a vertex color layer.
- Receive/Send scripts. Allow injecting a custom python function to the receive/send process to automate any blender operations
## Dependency Installation and Compatibility with Other Blender Addons
Upon first launch of the addon, the Speckle connector installs its SpecklePy dependencies in `%appdata%/Speckle/connector_installations` on Windows and `~/.config/Speckle/connector_installations` on Mac.
This is done through our [`installer.py`](https://github.com/specklesystems/speckle-blender/blob/main/bpy_speckle/installer.py). Through pip, we install the correct version of each dependency for your blender python version, host OS, and system architecture.
As such, an internet connection is required for first launch of the connector.
Other blender addons may require dependencies that conflict with specklepy. In these cases, one or both addons may fail to load.
If you suspect you're seeing a conflict, Please uninstall other third party addons one at a time to identify which addon is conflicting.
If you find an addon that conflicts, please try using a different version of that addon (newer or older).
If you can't find a version of an addon that works, please let us know on [our forums](https://speckle.community/) the name of the addon, the versions you've tried, the version of the Speckle connector you've tried, and your OS (win/mac/linux).
## Contributing
+14 -14
View File
@@ -10,22 +10,22 @@ 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)
@@ -79,7 +79,7 @@ class BlenderCommitObjectBuilder(CommitObjectBuilder[Object]):
# Create all collections
root_col = self.ensure_collection(bpy.context.scene.collection)
root_col.collectionType = "Scene Collection"
for col in bpy.context.scene.collection.children_recursive: #type: ignore
for col in bpy.context.scene.collection.children_recursive:
self.ensure_collection(col)
objects_to_build = set(self.converted.values())
+3 -3
View File
@@ -52,7 +52,7 @@ def can_convert_to_native(speckle_object: Base) -> bool:
return True
return False
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
@@ -518,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 +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:
+17 -8
View File
@@ -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 = []
@@ -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!
@@ -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 likely 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:
+13 -10
View File
@@ -8,7 +8,7 @@ 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 specklepy.objects.graph_traversal.traversal import TraversalContext
@@ -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]
@@ -255,7 +258,7 @@ ignored_keys = {
}
def get_blender_custom_properties(obj, max_depth: int = 63):
"""Recursivly 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"""
"""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
@@ -447,7 +450,7 @@ 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_hierarchy(converted: Union[Object, BCollection], traversalContext : 'TraversalContext', converted_objects: Dict[str, Union[Object, BCollection]], preserve_transform: bool) -> None:
+9 -12
View File
@@ -4,6 +4,7 @@ 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
@@ -38,20 +39,16 @@ 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()
@@ -71,5 +68,5 @@ class DeleteCommit(bpy.types.Operator):
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"Commit {commit.id} ({commit.message}) has been deleted from stream {stream.id}")
+2
View File
@@ -3,6 +3,8 @@ import webbrowser
from specklepy.logging import metrics
class OpenSpeckleGuide(bpy.types.Operator):
bl_idname = "speckle.open_speckle_guide"
bl_label = "Speckle Guide"
+5 -4
View File
@@ -13,6 +13,7 @@ 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
@@ -68,7 +69,7 @@ class UpdateObject(bpy.types.Operator):
return {"CANCELLED"}
return {"CANCELLED"}
@deprecated
class ResetObject(bpy.types.Operator):
"""
Reset Speckle object settings
@@ -96,7 +97,7 @@ class ResetObject(bpy.types.Operator):
return {"FINISHED"}
@deprecated
class DeleteObject(bpy.types.Operator):
"""
Delete object from the server and update relevant stream
@@ -247,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
@@ -307,7 +308,7 @@ class SelectIfSameCustomProperty(bpy.types.Operator):
return {"FINISHED"}
@deprecated
class SelectIfHasCustomProperty(bpy.types.Operator):
"""
Select scene objects if they have the same custom property
+94 -99
View File
@@ -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,8 +32,8 @@ 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.operators.users import LoadUserStreams, add_user_stream
from bpy_speckle.properties.scene import SpeckleSceneSettings, SpeckleStreamObject, SpeckleUserObject, get_speckle
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
@@ -74,7 +75,7 @@ def get_receive_funcs(speckle: SpeckleSceneSettings) -> tuple[ObjectCallback, Re
#]
INSTANCES_SETTINGS = [
("collection_instance", "Collection Instace", "Receive Instances as Collection Instances"),
("collection_instance", "Collection Instance", "Receive Instances as Collection Instances"),
("linked_duplicates", "Linked Duplicates", "Receive Instances as Linked Duplicates"),
]
@@ -88,11 +89,10 @@ class ReceiveStreamObjects(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
bl_description = "Receive objects from active stream"
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,23 +130,19 @@ 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)
@@ -183,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]
@@ -201,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
@@ -256,11 +253,11 @@ class SendStreamObjects(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
bl_description = "Send selected objects to active stream"
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.",
)
) # type: ignore
def draw(self, context):
layout = self.layout
@@ -270,8 +267,11 @@ 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."
@@ -281,12 +281,8 @@ class SendStreamObjects(bpy.types.Operator):
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:
@@ -376,7 +372,13 @@ 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}")
bpy.ops.speckle.load_user_streams() # refresh loaded commits
context.view_layer.update()
@@ -393,19 +395,21 @@ class ViewStreamDataApi(bpy.types.Operator):
bl_description = "View the stream 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()
if not webbrowser.open("%s/streams/%s" % (user.server_url, stream.id), new=2):
client = speckle_clients[int(speckle.active_user)]
if client.account.serverInfo.frontend2:
stream_url = f"{user.server_url}/projects/{stream.id}"
else:
stream_url= f"{user.server_url}/streams/{stream.id}"
if not webbrowser.open(stream_url, new=2):
raise Exception("Failed to open stream in browser")
metrics.track(
@@ -428,7 +432,7 @@ class AddStreamFromURL(bpy.types.Operator):
bl_description = "Add an existing stream by providing its URL"
stream_url: StringProperty(
name="Stream URL", default="https://speckle.xyz/streams/3073b96e86"
)
) # type: ignore
def draw(self, context):
layout = self.layout
@@ -444,13 +448,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)
@@ -460,28 +477,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 stream {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 stream at index {index} ({b_stream.id} - {b_stream.name})")
if wrapper.branch_name:
b_index = b_stream.branches.find(wrapper.branch_name)
@@ -521,10 +530,10 @@ class CreateStream(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
bl_description = "Create new stream"
stream_name: StringProperty(name="Stream name")
stream_name: StringProperty(name="Stream name") # type: ignore
stream_description: StringProperty(
name="Stream description", default="This is a Blender stream."
)
) # type: ignore
def draw(self, context):
layout = self.layout
@@ -541,12 +550,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)
@@ -579,6 +584,7 @@ class CreateStream(bpy.types.Operator):
)
@deprecated
class DeleteStream(bpy.types.Operator):
"""
Delete stream
@@ -591,10 +597,11 @@ class DeleteStream(bpy.types.Operator):
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
@@ -611,19 +618,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()
@@ -631,7 +635,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]
@@ -651,7 +656,7 @@ class DeleteStream(bpy.types.Operator):
},
)
@deprecated
class SelectOrphanObjects(bpy.types.Operator):
"""
Select Speckle objects that don't belong to any stream
@@ -696,12 +701,8 @@ class CopyStreamId(bpy.types.Operator):
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"}
self.copy_stream_id(context)
return {"FINISHED"}
def copy_stream_id(self, context) -> None:
speckle = get_speckle(context)
@@ -727,12 +728,9 @@ class CopyCommitId(bpy.types.Operator):
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"}
self.copy_commit_id(context)
return {"FINISHED"}
def copy_commit_id(self, context) -> None:
speckle = get_speckle(context)
@@ -760,12 +758,9 @@ class CopyBranchName(bpy.types.Operator):
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"}
self.copy_branch_id(context)
return {"FINISHED"}
def copy_branch_id(self, context) -> None:
speckle = get_speckle(context)
+51 -37
View File
@@ -1,15 +1,15 @@
"""
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 bpy_speckle.properties.scene import SpeckleBranchObject, SpeckleCommitObject, SpeckleSceneSettings, SpeckleStreamObject, SpeckleUserObject, get_speckle
from specklepy.core.api.client import SpeckleClient
from specklepy.core.api.models import Stream
from specklepy.core.api.credentials import get_local_accounts
from specklepy.core.api.credentials import get_local_accounts, Account
from specklepy.logging import metrics
class ResetUsers(bpy.types.Operator):
@@ -57,8 +57,8 @@ class LoadUsers(bpy.types.Operator):
_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)
@@ -77,31 +77,19 @@ class LoadUsers(bpy.types.Operator):
raise Exception("Zero accounts were found, please add one through Speckle Manager or a local account")
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)
add_user_account(profile, speckle)
except Exception as ex:
_report(f"Failed to authenticate user {user.email} with server {user.server_url}: {ex}")
users.remove(len(users) - 1)
_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) - 1
active_user_index = len(users_list) - 1
_report(f"Authenticated {len(users)}/{len(profiles)} accounts")
_report(f"Authenticated {len(users_list)}/{len(profiles)} accounts")
if active_user_index < len(users):
if active_user_index < len(users_list):
speckle.active_user = str(active_user_index)
bpy.context.view_layer.update()
@@ -109,25 +97,53 @@ class LoadUsers(bpy.types.Operator):
if context.area:
context.area.tag_redraw()
if not users:
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
_report(f"Adding stream {s.id} - {s.name}")
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 = cast(SpeckleBranchObject, s.branches.add())
branch.name = b.name
branch.id = b.id
branch.description = b.description or ""
if not b.commits:
continue
@@ -159,15 +175,13 @@ class LoadUserStreams(bpy.types.Operator):
bl_description = "(Re)load all available user streams"
stream_limit: int = 20
branch_limit: int = 20
branch_limit: int = 100
commits_limit: int = 10
def execute(self, context):
try:
self.load_user_stream(context)
return {"FINISHED"}
except Exception as ex:
_report(f"{self.bl_idname} failed: {ex}")
return {"CANCELLED"}
self.load_user_stream(context)
return {"FINISHED"}
def load_user_stream(self, context: Context) -> None:
speckle = get_speckle(context)
@@ -181,14 +195,14 @@ class LoadUserStreams(bpy.types.Operator):
raise Exception(f"Failed to retrieve streams") from ex
if not streams:
raise Exception("Zero streams found")
_report("Zero streams found")
return
user.streams.clear()
for s in streams:
assert(s.id)
sstream = client.stream.get(id=s.id, branch_limit=self.branch_limit)
sstream = client.stream.get(id=s.id, branch_limit=self.branch_limit, commit_limit=10)
add_user_stream(user, sstream)
bpy.context.view_layer.update()
+2
View File
@@ -37,6 +37,8 @@ class SpeckleBranchObject(bpy.types.PropertyGroup):
return [("0", "<none>", "<none>", 0)]
name: StringProperty(default="main")
id: StringProperty(default="")
description: StringProperty(default="")
commits: CollectionProperty(type=SpeckleCommitObject)
commit: EnumProperty(
name="Commit",
+2 -1
View File
@@ -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'
-2
View File
@@ -149,7 +149,6 @@ 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")
@@ -225,7 +224,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()
Generated
+737 -710
View File
File diff suppressed because it is too large Load Diff
+3 -3
View File
@@ -7,15 +7,15 @@ license = "Apache-2.0"
[tool.poetry.dependencies]
python = ">=3.8, <4.0.0"
specklepy = "^2.16.2"
specklepy = "^2.18.2"
attrs = "^23.1.0"
# [tool.poetry.group.local_specklepy.dependencies]
# specklepy = {path = "../specklepy", develop = true}
[tool.poetry.group.dev.dependencies]
fake-bpy-module-latest = "^20230117"
black = "^22.10.0"
fake-bpy-module-latest = "^20240212"
black = "23.11.0"
pylint = "^2.15.7"
ruff = "^0.0.187"