Compare commits

..

35 Commits

Author SHA1 Message Date
Gergő Jedlicska 13421a23a0 build for osx 13 for arm 2022-12-13 16:48:23 +01:00
Gergő Jedlicska 4328c5b8cb only parameter 2022-12-13 13:12:49 +01:00
Gergő Jedlicska 4d44a49988 no CD 2022-12-13 13:11:36 +01:00
Gergő Jedlicska c174101176 pipe the zip 2022-12-13 13:11:18 +01:00
Gergő Jedlicska 62566b1bab top level cp operation 2022-12-13 12:59:27 +01:00
Gergő Jedlicska 88ec39caa4 mkdir 101 2022-12-13 12:52:37 +01:00
Gergő Jedlicska d65920a774 copy 101 2022-12-13 12:42:26 +01:00
Gergő Jedlicska c133689fea force copy 2022-12-13 12:37:52 +01:00
Gergő Jedlicska 6aaf4a5a99 mkdir -p 2022-12-13 12:33:42 +01:00
Gergő Jedlicska 66ec9d5af1 mkdir for the zip folder 2022-12-13 12:31:29 +01:00
Gergő Jedlicska 3cabadf552 fix zipping dir 2022-12-13 12:28:01 +01:00
Gergő Jedlicska c15bb7f045 ci(circleci): fix persist to workspace pathing 2022-12-13 12:21:26 +01:00
Gergő Jedlicska b0aa9cb48f ci(circleci): wtf is microsoft doing with env vars in net6 containers? 2022-12-13 12:05:58 +01:00
Gergő Jedlicska 7bbc286ae2 ci(circleci): fix missing semver in files 2022-12-13 11:48:56 +01:00
Gergő Jedlicska 99b130290f ci(circleci): fix env var restore in deploy 2022-12-13 11:38:51 +01:00
Gergő Jedlicska a0f782af5f make sure semver is set 2022-12-13 11:26:49 +01:00
Gergő Jedlicska f48a5c9582 ci(cirleci): fix mac build triggers 2022-12-13 11:21:12 +01:00
Gergő Jedlicska a9875ffbaa ci(circleci): fix filters 2022-12-13 11:18:13 +01:00
Gergő Jedlicska cee9d83e1d ci(circleci): update details in deploy 2022-12-13 11:09:56 +01:00
Gergő Jedlicska f76b6cf59b publish only usefull stuff 2022-12-12 18:10:52 +01:00
Gergő Jedlicska 8cd9d03806 only zip the published isntaller 2022-12-12 18:08:23 +01:00
Gergő Jedlicska 28ace1f591 add zip again 2022-12-12 18:04:26 +01:00
Gergő Jedlicska beebe10dff add python to dotnet installer 2022-12-12 17:58:33 +01:00
Gergő Jedlicska 1f964bf939 properly saving packaged zip 2022-12-12 17:52:40 +01:00
Gergő Jedlicska 997b94dab9 fix installer patch pathing 2022-12-12 17:46:48 +01:00
Gergő Jedlicska ea962952e5 force remove new lines from installer tags 2022-12-12 17:45:33 +01:00
Gergő Jedlicska 5aff85b586 get proper ci tools 2022-12-12 17:37:28 +01:00
Gergő Jedlicska e1553ed389 getting the specific installer branch 2022-12-12 17:36:20 +01:00
Gergő Jedlicska 88235402f1 ci(circleci): update dependency graph 2022-12-12 17:28:14 +01:00
Gergő Jedlicska a1eea0a6fc ci(circleci): update semver location 2022-12-12 17:26:34 +01:00
Gergő Jedlicska 80496be394 ci(circleci): update filters 2022-12-12 17:21:01 +01:00
Gergő Jedlicska ac63cbe17d ci(circleci): fix naming 2022-12-12 17:18:15 +01:00
Gergő Jedlicska de0c97cdbd ci(circleci): updates 2022-12-12 17:16:51 +01:00
Gergő Jedlicska 14e8445cf2 fix(installer): ensure pip call was commented outy 2022-12-12 16:09:45 +01:00
Gergő Jedlicska ddf908b3f9 ci(circleci): rewrite full ci WIP 2022-12-09 13:58:48 +01:00
23 changed files with 2002 additions and 3249 deletions
+22 -89
View File
@@ -1,7 +1,7 @@
version: 2.1
orbs:
win: circleci/windows@5.0.0
win: circleci/windows@2.4.0
jobs:
package-connector:
@@ -49,20 +49,11 @@ jobs:
docker:
- image: cimg/base:2021.01
steps:
- add_ssh_keys:
fingerprints:
- "77:64:03:93:c5:f3:1d:a6:fd:bd:fb:d1:05:56:ca:e9"
- run:
name: I know Github as a host
command: |
mkdir ~/.ssh
touch ~/.ssh/known_hosts
ssh-keyscan github.com >> ~/.ssh/known_hosts
- run:
- run: # Could not get ssh to work, so using a personal token
name: Clone
command: git clone git@github.com:specklesystems/speckle-sharp-ci-tools.git speckle-sharp-ci-tools
command: git clone https://$GITHUB_TOKEN@github.com/specklesystems/speckle-sharp-ci-tools.git speckle-sharp-ci-tools
- run:
command: cd speckle-sharp-ci-tools
command: cd speckle-sharp-ci-tools && git checkout blender/installer-changes
- persist_to_workspace:
root: ./
paths:
@@ -79,12 +70,6 @@ jobs:
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
@@ -94,9 +79,9 @@ jobs:
paths:
- speckle-sharp-ci-tools/Installers/blender/blender-*.exe
build-installer-mac:
macos:
xcode: 12.5.1
build-dotnet-installer:
docker:
- image: mcr.microsoft.com/dotnet/sdk:6.0
parameters:
runtime:
type: string
@@ -106,26 +91,23 @@ jobs:
type: string
default: speckle-sharp-ci-tools/Mac/SpeckleBlenderInstall
steps:
- checkout
- attach_workspace:
at: ./
- run:
name: Install mono
command: |
HOMEBREW_NO_AUTO_UPDATE=1 brew install mono
# Compress build files
- run:
name: Install dotnet
command: curl -sSL https://dot.net/v1/dotnet-install.sh | bash /dev/stdin
- run: *restore_semver
- run:
name: ensure zip
command: apt update && apt install -y zip
- run:
name: Copy connector files to installer
command: |
mkdir -p <<parameters.installer_path >>/.installationFiles/
mkdir -p << parameters.installer_path >>/.installationFiles
cp bpy_speckle.zip << parameters.installer_path >>/.installationFiles
- run:
name: Build Mac installer
command: ~/.dotnet/dotnet publish << parameters.installer_path >>/SpeckleBlenderInstall.sln -r << parameters.runtime >> -c Release
name: Build installer
command: dotnet publish <<parameters.installer_path>>/SpeckleBlenderInstall.sln -r << parameters.runtime >> -c Release
# cd <<parameters.installer_path>>/bin/Release/net6.0/<< parameters.runtime >>/publish/
# zip -r << parameters.slug >>-${SEMVER}.zip <<parameters.installer_path>>/bin/Release/net6.0/<< parameters.runtime >>/publish/.
# cd ${CIRCLE_WORKING_DIRECTORY}
- run:
name: Zip installer
command: |
@@ -140,28 +122,6 @@ jobs:
paths:
- speckle-sharp-ci-tools/Installers/blender/<< parameters.slug >>*.zip
build-installer-manual:
docker:
- image: cimg/base:2021.01
parameters:
slug:
type: string
default: bpy_speckle
steps:
- attach_workspace:
at: ./
- run: *restore_semver
- run:
name: Copy zip with semver
command: |
SEMVER=$(cat ./SEMVER)
mkdir -p speckle-sharp-ci-tools/Installers/blender
cp bpy_speckle.zip speckle-sharp-ci-tools/Installers/blender/<< parameters.slug >>-${SEMVER}.zip
- persist_to_workspace:
root: ./
paths:
- speckle-sharp-ci-tools/Installers/blender/<< parameters.slug >>*.zip
deploy-connector:
docker:
- image: mcr.microsoft.com/dotnet/sdk:6.0
@@ -203,18 +163,18 @@ workflows:
- package-connector:
filters: &build_filters
tags:
only: /([0-9]+)\.([0-9]+)\.([0-9]+)(?:-\w+)?(?:\.[0-9]+)?/
only: /([0-9]+)\.([0-9]+)\.([0-9]+)(?:-\w+)?$/
- build-connector-zip:
requires:
- package-connector
filters: *build_filters
- get-ci-tools:
filters: *build_filters
- build-installer-win:
context: innosetup
name: Windows Installer Build
requires:
- package-connector
@@ -222,14 +182,12 @@ workflows:
filters: *build_filters
- deploy-connector:
context: do-spaces-speckle-releases
name: deploy-windows
file_slug: blender
os: WIN
arch: Any
extension: exe
requires:
- Manual Installer Build
- Windows Installer Build
- Mac Intel Build
- Mac ARM Build
@@ -237,32 +195,30 @@ workflows:
branches:
ignore: /.*/
tags:
only: /([0-9]+)\.([0-9]+)\.([0-9]+)(?:-\w+)?(?:\.[0-9]+)?/
only: /([0-9]+)\.([0-9]+)\.([0-9]+)(?:-\w+)?$/
- build-installer-mac:
- build-dotnet-installer:
name: Mac ARM Build
slug: blender-mac-arm
runtime: osx-arm64
runtime: osx.13-arm64
requires:
- get-ci-tools
- build-connector-zip
filters: *build_filters
- deploy-connector:
context: do-spaces-speckle-releases
name: deploy-mac-arm
file_slug: blender-mac-arm
os: OSX
arch: Arm
extension: zip
requires:
- Manual Installer Build
- Windows Installer Build
- Mac Intel Build
- Mac ARM Build
filters: *deploy_filters
- build-installer-mac:
- build-dotnet-installer:
name: Mac Intel Build
slug: blender-mac-intel
runtime: osx-x64
@@ -272,36 +228,13 @@ workflows:
filters: *build_filters
- deploy-connector:
context: do-spaces-speckle-releases
name: deploy-mac-intel
file_slug: blender-mac-intel
os: OSX
arch: Intel
extension: zip
requires:
- Manual Installer Build
- Windows Installer Build
- Mac Intel Build
- Mac ARM Build
filters: *deploy_filters
- build-installer-manual:
name: Manual Installer Build
requires:
- get-ci-tools
- build-connector-zip
filters: *build_filters
- deploy-connector:
context: do-spaces-speckle-releases
name: deploy-manual
file_slug: bpy_speckle
os: Any
arch: Any
extension: zip
requires:
- Manual Installer Build
- Windows Installer Build
- Mac Intel Build
- Mac ARM Build
filters: *deploy_filters
+70 -4
View File
@@ -6,7 +6,73 @@ on:
jobs:
update_issue:
uses: specklesystems/github-actions/.github/workflows/project-add-issue.yml@main
secrets: inherit
with:
issue-id: ${{ github.event.issue.node_id }}
runs-on: ubuntu-latest
steps:
- name: Get project data
env:
GITHUB_TOKEN: ${{secrets.GHPROJECT_TOKEN}}
ORGANIZATION: specklesystems
PROJECT_NUMBER: 9
run: |
gh api graphql --header 'GraphQL-Features: projects_next_graphql' -f query='
query($org: String!, $number: Int!) {
organization(login: $org){
projectNext(number: $number) {
id
fields(first:20) {
nodes {
id
name
settings
}
}
}
}
}' -f org=$ORGANIZATION -F number=$PROJECT_NUMBER > project_data.json
echo 'PROJECT_ID='$(jq '.data.organization.projectNext.id' project_data.json) >> $GITHUB_ENV
echo 'STATUS_FIELD_ID='$(jq '.data.organization.projectNext.fields.nodes[] | select(.name== "Status") | .id' project_data.json) >> $GITHUB_ENV
echo "$PROJECT_ID"
echo "$STATUS_FIELD_ID"
echo 'DONE_ID='$(jq '.data.organization.projectNext.fields.nodes[] | select(.name== "Status") | .settings | fromjson | .options[] | select(.name== "Done") | .id' project_data.json) >> $GITHUB_ENV
echo "$DONE_ID"
- name: Add Issue to project #it's already in the project, but we do this to get its node id!
env:
GITHUB_TOKEN: ${{secrets.GHPROJECT_TOKEN}}
ISSUE_ID: ${{ github.event.issue.node_id }}
run: |
item_id="$( gh api graphql --header 'GraphQL-Features: projects_next_graphql' -f query='
mutation($project:ID!, $id:ID!) {
addProjectNextItem(input: {projectId: $project, contentId: $id}) {
projectNextItem {
id
}
}
}' -f project=$PROJECT_ID -f id=$ISSUE_ID --jq '.data.addProjectNextItem.projectNextItem.id')"
echo 'ITEM_ID='$item_id >> $GITHUB_ENV
- name: Update Status
env:
GITHUB_TOKEN: ${{secrets.GHPROJECT_TOKEN}}
ISSUE_ID: ${{ github.event.issue.node_id }}
run: |
gh api graphql --header 'GraphQL-Features: projects_next_graphql' -f query='
mutation($project:ID!, $status:ID!, $id:ID!, $value:String!) {
set_status: updateProjectNextItemField(
input: {
projectId: $project
itemId: $id
fieldId: $status
value: $value
}
) {
projectNextItem {
id
}
}
}' -f project=$PROJECT_ID -f status=$STATUS_FIELD_ID -f id=$ITEM_ID -f value=${{ env.DONE_ID }}
+42 -4
View File
@@ -6,7 +6,45 @@ on:
jobs:
track_issue:
uses: specklesystems/github-actions/.github/workflows/project-add-issue.yml@main
secrets: inherit
with:
issue-id: ${{ github.event.issue.node_id }}
runs-on: ubuntu-latest
steps:
- name: Get project data
env:
GITHUB_TOKEN: ${{secrets.GHPROJECT_TOKEN}}
ORGANIZATION: specklesystems
PROJECT_NUMBER: 9
run: |
gh api graphql --header 'GraphQL-Features: projects_next_graphql' -f query='
query($org: String!, $number: Int!) {
organization(login: $org){
projectNext(number: $number) {
id
fields(first:20) {
nodes {
id
name
settings
}
}
}
}
}' -f org=$ORGANIZATION -F number=$PROJECT_NUMBER > project_data.json
echo 'PROJECT_ID='$(jq '.data.organization.projectNext.id' project_data.json) >> $GITHUB_ENV
echo 'STATUS_FIELD_ID='$(jq '.data.organization.projectNext.fields.nodes[] | select(.name== "Status") | .id' project_data.json) >> $GITHUB_ENV
- name: Add Issue to project
env:
GITHUB_TOKEN: ${{secrets.GHPROJECT_TOKEN}}
ISSUE_ID: ${{ github.event.issue.node_id }}
run: |
item_id="$( gh api graphql --header 'GraphQL-Features: projects_next_graphql' -f query='
mutation($project:ID!, $id:ID!) {
addProjectNextItem(input: {projectId: $project, contentId: $id}) {
projectNextItem {
id
}
}
}' -f project=$PROJECT_ID -f id=$ISSUE_ID --jq '.data.addProjectNextItem.projectNextItem.id')"
echo 'ITEM_ID='$item_id >> $GITHUB_ENV
+3 -8
View File
@@ -1,7 +1,7 @@
import bpy
from bpy_speckle.installer import ensure_dependencies
ensure_dependencies(f"Blender {bpy.app.version[0]}.{bpy.app.version[1]}")
ensure_dependencies()
from specklepy.logging import metrics
@@ -36,13 +36,8 @@ loading a Blender file
@persistent
def load_handler(dummy):
pass
# Calling users_load is an expensive operation, one that force users to wait a good 10s every time blender loads.
# Until we can do this non-blocking, we will make the user hit the refresh button each time.
#bpy.ops.speckle.users_load()
# Instead, we shall just reset the user selection to an uninitiailised state
bpy.ops.speckle.users_reset()
bpy.ops.speckle.users_load()
"""
Permanent handle on callbacks
@@ -1,120 +0,0 @@
from typing import Dict, Optional, Tuple, Union
import bpy
from bpy.types import Object, Collection, ID
from specklepy.objects.base import Base
from bpy_speckle.functions import _report
from specklepy.objects.graph_traversal.commit_object_builder import CommitObjectBuilder, ROOT
from specklepy.objects import Base
from specklepy.objects.other import Collection as SCollection
from attrs import define
ELEMENTS = "elements"
def _id(natvive_object: ID) -> str:
#NOTE: to avoid naming collisions, we prefix collections and objects differently
return f"{type(natvive_object).__name__}:{natvive_object.name_full}"
def _try_id(natvive_object: Optional[Union[Collection, Object]]) -> Optional[str]:
return _id(natvive_object) if natvive_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)
color_tag = col.color_tag
if color_tag and color_tag != "NONE":
convered_collection["colorTag"] = col.color_tag
return convered_collection
@define(slots=True)
class BlenderCommitObjectBuilder(CommitObjectBuilder[Object]):
_collections: Dict[str, SCollection]
def __init__(self) -> None:
super().__init__()
self._collections = {}
def include_object(self, conversion_result: Base, native_object: Object) -> None:
# Set the Child -> Parent relationships
parent = native_object.parent
parent_collections: Tuple[Collection] = native_object.users_collection # type: ignore
parent_collection = parent_collections[0] if len(parent_collections) > 0 else None #NOTE: we don't support objects appearing in more than one collection, for now, we will just take the zeroth one
app_id = _id(native_object)
conversion_result.applicationId = app_id
self.converted[app_id] = conversion_result
# in order or priority, direct parent, direct parent collection, root
self.set_relationship(app_id, (_try_id(parent), ELEMENTS), (_try_id(parent_collection), ELEMENTS), (ROOT, ELEMENTS))
# if parent_collection:
# self._include_collection(parent_collection)
def ensure_collection(self, col: Collection) -> SCollection:
id = _id(col)
if id in self._collections:
return self._collections[id] # collection already converted!
# Set the Parent -> Children relationships
for c in col.children:
#NOTE: There's no falling back to the grandparent, if the direct parent collection wasn't converted, then we we fallback to the root
self.set_relationship(_id(c), (id, ELEMENTS), (ROOT, ELEMENTS))
# Set Child -> Parent relationship
# parent = self.find_collection_parent(col)
# self.set_relationship(id, (_try_builder_id(parent), ELEMENTS), (ROOT, ELEMENTS))
convered_collection = convert_collection_to_speckle(col)
self.converted[id] = convered_collection
self._collections[id] = convered_collection
return convered_collection
def build_commit_object(self, root_commit_object: Base) -> None:
assert(root_commit_object.applicationId in self.converted)
# Create all collections
root_col = self.ensure_collection(bpy.context.scene.collection)
root_col.collectionType = "Scene Collection"
for col in bpy.context.scene.collection.children_recursive: #type: ignore
self.ensure_collection(col)
objects_to_build = set(self.converted.values())
objects_to_build.remove(root_commit_object)
self.apply_relationships(objects_to_build, root_commit_object)
assert(isinstance(root_commit_object, SCollection))
# Kill unused collections
def should_remove_unuseful_collection(col: SCollection) -> bool: #TODO: this maybe could be optimised
elements = col.elements
if not elements: return True
should_remove_this_col = True
i = 0
while i < len(elements):
c = elements[i]
if not isinstance(c, SCollection):
# col has objects (c)
should_remove_this_col = False
i += 1
continue
if should_remove_unuseful_collection(c):
# c is not useful, kill it
del elements[i]
else:
# col has a child (c) with objects
should_remove_this_col = False
i += 1
continue
return should_remove_this_col
if should_remove_unuseful_collection(root_commit_object):
_report("WARNING: Only empty collections have been converted!") #TODO: consider raising exception here, to halt the send operation
+1 -1
View File
@@ -1,7 +1,7 @@
"""
Permanent handle on all user clients
"""
from specklepy.core.api.client import SpeckleClient
from specklepy.api.client import SpeckleClient
speckle_clients: list[SpeckleClient] = []
+29
View File
@@ -0,0 +1,29 @@
from typing import Union
from bpy_speckle.convert.to_native import convert_to_native
from specklepy.objects.base import Base
def get_speckle_subobjects(attr: Union[dict, Base], scale: float, name: str) -> list:
subobjects = []
keys = attr.keys() if isinstance(attr, dict) else attr.get_dynamic_member_names()
for key in keys:
if isinstance(attr[key], dict):
subtype = attr[key].get("type", None)
if subtype:
name = f"{name}.{key}"
subobject = convert_to_native(attr[key], name)
subobjects.append(subobject)
props = attr[key].get("properties", None)
if props:
subobjects.extend(get_speckle_subobjects(props, scale, name))
elif hasattr(attr[key], "type"):
subtype = attr[key].type
if subtype:
name = "{}.{}".format(name, key)
subobject = convert_to_native(attr[key], name)
subobjects.append(subobject)
props = attr[key].get("properties", None)
if props:
subobjects.extend(get_speckle_subobjects(props, scale, name))
return subobjects
-22
View File
@@ -1,22 +0,0 @@
IGNORED_PROPERTY_KEYS = {
"id",
"elements",
"displayMesh",
"displayValue",
"speckle_type",
"parameters",
"faces",
"colors",
"vertices",
"renderMaterial",
"textureCoordinates",
"totalChildrenCount"
}
DISPLAY_VALUE_PROPERTY_ALIASES = {"displayValue", "@displayValue"}
ELEMENTS_PROPERTY_ALIASES = {"elements", "@elements"}
OBJECT_NAME_MAX_LENGTH = 62
SPECKLE_ID_LENGTH = 32
OBJECT_NAME_SPECKLE_SEPARATOR = " -- "
OBJECT_NAME_NUMERAL_SEPARATOR = '.'
File diff suppressed because it is too large Load Diff
+182 -371
View File
@@ -1,183 +1,134 @@
from typing import Dict, Iterable, List, Optional, Tuple, Union, cast
from typing import Optional
import bpy
from bpy.types import (
Depsgraph,
MeshPolygon,
Object,
Curve as NCurve,
Mesh as NMesh,
Camera as NCamera,
)
from deprecated import deprecated
from mathutils.geometry import interpolate_bezier
from mathutils import (
Matrix as MMatrix,
Vector as MVector,
)
from specklepy.objects import Base
from specklepy.objects.other import BlockInstance, BlockDefinition, RenderMaterial, Transform
from specklepy.objects.geometry import (
Mesh, Curve, Interval, Box, Point, Vector, Polyline,
)
from bpy_speckle.blender_commit_object_builder import BlenderCommitObjectBuilder
from bpy_speckle.convert.constants import OBJECT_NAME_SPECKLE_SEPARATOR, SPECKLE_ID_LENGTH
from bpy.types import Depsgraph, MeshVertColor, MeshVertex, Object
from specklepy.objects.geometry import Mesh, Curve, Interval, Box, Point, Polyline
from specklepy.objects.other import *
from bpy_speckle.functions import _report
from bpy_speckle.convert.util import (
ConversionSkippedException,
get_blender_custom_properties,
make_knots,
nurb_make_curve,
to_argb_int,
)
from bpy_speckle.functions import _report
UNITS = "m"
CAN_CONVERT_TO_SPECKLE = ("MESH", "CURVE", "EMPTY")
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")
def convert_to_speckle(raw_blender_object: Object, units_scale: float, units: str, depsgraph: Optional[Depsgraph]) -> Base:
"""
Converts supported 1 blender objects to 1 speckle object (potentially with children)
:param raw_blender_object: the blender object (unevaluated by a Depsgraph) to convert
:param units_scale: The scale factor conversions need to apply to position data to get to the desired units
:param units: The desired final units to send
:param depsgraph: Optional depsgraph if provided will evaluate modifiers on geometry data
:return: The Converted blender object
"""
global Units, UnitsScale
Units = units
UnitsScale = units_scale
blender_type = raw_blender_object.type
def convert_to_speckle(blender_object: Object, scale: float, units: str, desgraph: Optional[Depsgraph]) -> Optional[list]:
global UNITS
UNITS = units
blender_type = blender_object.type
if blender_type not in CAN_CONVERT_TO_SPECKLE:
raise ConversionSkippedException(f"Objects of type {blender_type} are not supported")
return None
blender_object = cast(Object, (
raw_blender_object.evaluated_get(depsgraph)
if depsgraph
else raw_blender_object
))
converted: Optional[Base] = None
speckle_objects = []
speckle_material = material_to_speckle(blender_object)
if desgraph:
blender_object = blender_object.evaluated_get(desgraph)
converted = None
if blender_type == "MESH":
converted = mesh_to_speckle(blender_object, cast(NMesh, blender_object.data))
converted = mesh_to_speckle(blender_object, blender_object.data, scale)
elif blender_type == "CURVE":
converted = curve_to_speckle(blender_object, cast(NCurve, blender_object.data))
converted = icurve_to_speckle(blender_object, blender_object.data, scale)
elif blender_type == "EMPTY":
converted = empty_to_speckle(blender_object)
elif blender_type == "CAMERA":
converted = camera_to_speckle_view(blender_object, cast(NCamera, blender_object.data))
converted = empty_to_speckle(blender_object, scale)
if not converted:
raise Exception("Conversion returned None")
return None
converted["properties"] = get_blender_custom_properties(raw_blender_object) #NOTE: Depsgraph copies don't have custom properties so we use the raw version
if isinstance(converted, list):
speckle_objects.extend([c for c in converted if c != None])
else:
speckle_objects.append(converted)
for so in speckle_objects:
so.properties = get_blender_custom_properties(blender_object)
so.applicationId = so.properties.pop("applicationId", None)
# Set object transform #TODO: this could be deprecated once we add proper geometry instancing support
if blender_type != "EMPTY":
converted["properties"]["transform"] = transform_to_speckle(
blender_object.matrix_world
)
if speckle_material:
so["renderMaterial"] = speckle_material
return converted
# Set object transform
if blender_type != "EMPTY":
so.properties["transform"] = transform_to_speckle(
blender_object.matrix_world
)
def mesh_to_speckle(blender_object: Object, data: bpy.types.Mesh) -> Base:
b = Base()
b["name"] = to_speckle_name(blender_object)
b["@displayValue"] = mesh_to_speckle_meshes(blender_object, data)
return b
def mesh_to_speckle_meshes(blender_object: Object, data: bpy.types.Mesh) -> List[Mesh]:
# Categorise polygons by material index
submesh_data: Dict[int, List[MeshPolygon]] = {}
for p in data.polygons:
if p.material_index not in submesh_data:
submesh_data[p.material_index] = []
submesh_data[p.material_index].append(p)
transform = cast(MMatrix, blender_object.matrix_world)
scaled_vertices = [tuple(transform @ x.co * UnitsScale) for x in data.vertices] # type: ignore
# Create Speckle meshes for each material
submeshes = []
index_counter = 0
for i in submesh_data:
index_mapping: Dict[int, int] = {}
#Loop through each polygon, and map indices to their new index in m_verts
mesh_area = 0
m_verts: List[float] = []
m_faces: List[int] = []
m_texcoords: List[float] = []
for face in submesh_data[i]:
u_indices = face.vertices
m_faces.append(len(u_indices))
mesh_area += face.area
for u_index in u_indices:
if u_index not in index_mapping:
# Create mapping between index in blender mesh, and new index in speckle submesh
index_mapping[u_index] = len(m_verts) // 3
vert = scaled_vertices[u_index]
m_verts.append(vert[0])
m_verts.append(vert[1])
m_verts.append(vert[2])
if data.uv_layers.active:
vt = data.uv_layers.active.data[index_counter]
uv = cast(MVector, vt.uv)
m_texcoords.extend([uv.x, uv.y])
m_faces.append(index_mapping[u_index])
index_counter += 1
speckle_mesh = Mesh(
vertices=m_verts,
faces=m_faces,
colors=[],
textureCoordinates=m_texcoords,
units=Units,
area = mesh_area,
bbox=Box(area=0.0, volume=0.0),
)
if i < len(data.materials):
material = data.materials[i]
if material is not None:
speckle_mesh["renderMaterial"] = material_to_speckle(material)
submeshes.append(speckle_mesh)
return submeshes
return speckle_objects
def bezier_to_speckle(matrix: MMatrix, spline: bpy.types.Spline, name: Optional[str] = None) -> Curve:
def mesh_to_speckle(blender_object: Object, data: bpy.types.Mesh, scale=1.0) -> List[Mesh]:
if data.loop_triangles is None or len(data.loop_triangles) < 1:
data.calc_loop_triangles()
mat = blender_object.matrix_world
verts = [tuple(mat @ x.co * scale) for x in data.vertices]
flattend_verts = []
for row in verts: flattend_verts.extend(row)
faces = [p.vertices for p in data.polygons]
unit_system = bpy.context.scene.unit_settings.system
sm = Mesh(
name=blender_object.name,
vertices=flattend_verts,
faces=[],
colors=[],
textureCoordinates=[],
units=UNITS,
bbox=Box(area=0.0, volume=0.0),
)
if data.uv_layers.active:
for vt in data.uv_layers.active.data:
sm.textureCoordinates.extend([vt.uv.x, vt.uv.y])
for f in faces:
n = len(f)
if n == 3:
sm.faces.append(0)
elif n == 4:
sm.faces.append(1)
else:
sm.faces.append(n)
sm.faces.extend(f)
# TODO: figure out how to align vertex colors and vertices consistantly in receiving applications
# we are seeing the same issue as with texture coordinate alignment
#if data.color_attributes.active_color:
# sm.colors = [to_argb_int(x.color) for x in data.color_attributes.active_color.data]
return [sm]
def bezier_to_speckle(matrix: List[float], spline: bpy.types.Spline, scale: float, name: Optional[str] = None) -> Curve:
degree = 3
closed = spline.use_cyclic_u
points: List[Tuple[MVector]] = []
points = []
for i, bp in enumerate(spline.bezier_points):
if i > 0:
points.append(tuple(matrix @ bp.handle_left * UnitsScale)) # type: ignore
points.append(tuple(matrix @ bp.co * UnitsScale)) # type: ignore
points.append(tuple(matrix @ bp.handle_left * scale))
points.append(tuple(matrix @ bp.co * scale))
if i < len(spline.bezier_points) - 1:
points.append(tuple(matrix @ bp.handle_right * UnitsScale)) # type: ignore
points.append(tuple(matrix @ bp.handle_right * scale))
if closed:
points.extend(
(
tuple(matrix @ spline.bezier_points[-1].handle_right * UnitsScale), # type: ignore
tuple(matrix @ spline.bezier_points[0].handle_left * UnitsScale), # type: ignore
tuple(matrix @ spline.bezier_points[0].co * UnitsScale), # type: ignore
tuple(matrix @ spline.bezier_points[-1].handle_right * scale),
tuple(matrix @ spline.bezier_points[0].handle_left * scale),
tuple(matrix @ spline.bezier_points[0].co * scale),
)
)
num_points = len(points)
flattened_points = []
for row in points: flattened_points.extend(row)
flattend_points = []
for row in points: flattend_points.extend(row)
knot_count = num_points + degree - 1
knots = [0] * knot_count
@@ -191,187 +142,99 @@ def bezier_to_speckle(matrix: MMatrix, spline: bpy.types.Spline, name: Optional[
name=name,
degree=degree,
closed=spline.use_cyclic_u,
periodic= not spline.use_endpoint_u,
points=flattened_points,
periodic=spline.use_cyclic_u,
points=flattend_points,
weights=[1] * num_points,
knots=knots,
rational=True,
rational=False,
area=0,
volume=0,
length=length,
domain=domain,
units=Units,
units=UNITS,
bbox=Box(area=0.0, volume=0.0),
displayValue = bezier_to_speckle_polyline(matrix, spline, length),
)
def nurbs_to_speckle(matrix: MMatrix, spline: bpy.types.Spline, name: Optional[str] = None) -> Curve:
degree = spline.order_u - 1
def nurbs_to_speckle(matrix: List[float], spline: bpy.types.Spline, scale: float, name: Optional[str] = None) -> Curve:
knots = make_knots(spline)
points = [tuple(matrix @ pt.co.xyz * scale) for pt in spline.points]
degree = spline.order_u - 1
length = spline.calc_length()
domain = Interval(start=0, end=length, totalChildrenCount=0)
weights = [pt.weight for pt in spline.points]
is_rational = all(w == weights[0] for w in weights)
points = [tuple(matrix @ pt.co.xyz * UnitsScale) for pt in spline.points] # type: ignore
flattened_points = []
for row in points: flattened_points.extend(row)
if spline.use_cyclic_u:
for i in range(0, degree * 3, 3):
# Rhino expects n + degree number of points (for closed curves). So we need to add an extra point for each degree
flattened_points.append(flattened_points[i + 0])
flattened_points.append(flattened_points[i + 1])
flattened_points.append(flattened_points[i + 2])
for i in range(0, degree):
weights.append(weights[i])
flattend_points = []
for row in points: flattend_points.extend(row)
return Curve(
name=name,
degree=degree,
closed=spline.use_cyclic_u,
periodic= not spline.use_endpoint_u,
points=flattened_points,
weights=weights,
periodic=spline.use_cyclic_u,
points=flattend_points,
weights=[pt.weight for pt in spline.points],
knots=knots,
rational=is_rational,
rational=False,
area=0,
volume=0,
length=length,
domain=domain,
units=Units,
units=UNITS,
bbox=Box(area=0.0, volume=0.0),
displayValue=nurbs_to_speckle_polyline(matrix, spline, length),
)
def nurbs_to_speckle_polyline(matrix: MMatrix, spline: bpy.types.Spline, length: Optional[float] = None) -> Polyline:
"""
Samples a nurbs curve with resolution_u creating a polyline
"""
points: List[float] = []
sampled_points = nurb_make_curve(spline, spline.resolution_u, 3)
for i in range(0, len(sampled_points), 3):
scaled_point = cast(Vector, matrix @ MVector((
sampled_points[i + 0],
sampled_points[i + 1],
sampled_points[i + 2])) * UnitsScale)
points.append(scaled_point.x)
points.append(scaled_point.y)
points.append(scaled_point.z)
length = length or spline.calc_length()
domain = Interval(start=0, end=length, totalChildrenCount=0)
return Polyline(value=points, closed = spline.use_cyclic_u, domain=domain, area=0, len=length)
def poly_to_speckle(matrix: List[float], spline: bpy.types.Spline, scale: float, name: Optional[str] = None) -> Polyline:
points = [tuple(matrix @ pt.co.xyz * scale) for pt in spline.points]
#Inspired by https://blender.stackexchange.com/a/689 (CC BY-SA 3.0)
def bezier_to_speckle_polyline(matrix: MMatrix, spline: bpy.types.Spline, length: Optional[float] = None) -> Optional[Polyline]:
"""
Samples a Bézier curve with resolution_u creating a polyline
"""
segments = len(spline.bezier_points)
if segments < 2: return None
R = spline.resolution_u + 1
points = []
if not spline.use_cyclic_u:
segments -= 1
points: List[float] = []
for i in range(segments):
inext = (i + 1) % len(spline.bezier_points)
knot1 = spline.bezier_points[i].co
handle1 = spline.bezier_points[i].handle_right
handle2 = spline.bezier_points[inext].handle_left
knot2 = spline.bezier_points[inext].co
_points = interpolate_bezier(knot1, handle1, handle2, knot2, R)
for p in _points:
scaled_point = matrix @ p * UnitsScale
points.append(scaled_point.x)
points.append(scaled_point.y)
points.append(scaled_point.z)
length = length or spline.calc_length()
domain = Interval(start=0, end=length, totalChildrenCount=0)
return Polyline(value=points, closed = spline.use_cyclic_u, domain=domain, area=0, len=length)
_QUICK_TEST_NAME_LENGTH = SPECKLE_ID_LENGTH + len(OBJECT_NAME_SPECKLE_SEPARATOR)
def to_speckle_name(blender_object: bpy.types.ID) -> str:
does_name_contain_id = len(blender_object.name) > _QUICK_TEST_NAME_LENGTH and OBJECT_NAME_SPECKLE_SEPARATOR in blender_object.name
if does_name_contain_id:
return blender_object.name.rsplit(OBJECT_NAME_SPECKLE_SEPARATOR, 1)[0]
else:
return blender_object.name
def poly_to_speckle(matrix: MMatrix, spline: bpy.types.Spline, name: Optional[str] = None) -> Polyline:
points = [tuple(matrix @ pt.co.xyz * UnitsScale) for pt in spline.points] # type: ignore
flattened_points = []
for row in points: flattened_points.extend(row)
flattend_points = []
for row in points: flattend_points.extend(row)
length = spline.calc_length()
domain = Interval(start=0, end=length, totalChildrenCount=0)
return Polyline(
name=name,
closed=bool(spline.use_cyclic_u),
value=list(flattened_points),
value=list(flattend_points),
length=length,
domain=domain,
bbox=Box(area=0.0, volume=0.0),
area=0,
units=Units,
units=UNITS,
)
def curve_to_speckle(blender_object: Object, data: bpy.types.Curve) -> Base:
b = Base()
(meshes, curves) = curve_to_speckle_geometry(blender_object, data)
if meshes:
b["@displayValue"] = meshes
def icurve_to_speckle(blender_object: Object, data: bpy.types.Curve, scale=1.0) -> Optional[List[Base]]:
UNITS = "m" if bpy.context.scene.unit_settings.system == "METRIC" else "ft"
b["name"] = to_speckle_name(blender_object)
b["@elements"] = curves
return b
if blender_object.type != "CURVE":
return None
def curve_to_speckle_geometry(blender_object: Object, data: bpy.types.Curve) -> Tuple[List[Mesh], List[Base]]:
assert(blender_object.type == "CURVE")
blender_object = blender_object.evaluated_get(bpy.context.view_layer.depsgraph)
blender_object = cast(Object, blender_object.evaluated_get(bpy.context.view_layer.depsgraph))
mat = blender_object.matrix_world
matrix = cast(MMatrix, blender_object.matrix_world)
curves = []
meshes: List[Mesh] = []
curves: List[Base] = []
#TODO: Could we support this better?
if data.bevel_mode == "OBJECT" and data.bevel_object != None:
meshes = mesh_to_speckle_meshes(blender_object, blender_object.to_mesh())
mesh = mesh_to_speckle(blender_object, blender_object.to_mesh(), scale)
curves.extend(mesh)
for spline in data.splines:
if spline.type == "BEZIER":
curves.append(bezier_to_speckle(matrix, spline, to_speckle_name(blender_object)))
curves.append(bezier_to_speckle(mat, spline, scale, blender_object.name))
elif spline.type == "NURBS":
curves.append(nurbs_to_speckle(matrix, spline, to_speckle_name(blender_object)))
curves.append(nurbs_to_speckle(mat, spline, scale, blender_object.name))
elif spline.type == "POLY":
curves.append(poly_to_speckle(matrix, spline, to_speckle_name(blender_object)))
curves.append(poly_to_speckle(mat, spline, scale, blender_object.name))
return (meshes, curves)
return curves
@deprecated
def ngons_to_speckle_polylines(blender_object: Object, data: bpy.types.Mesh) -> Optional[List[Polyline]]:
def ngons_to_speckle_polylines(blender_object: Object, data: bpy.types.Mesh, scale=1.0) -> Optional[List[Polyline]]:
UNITS = "m" if bpy.context.scene.unit_settings.system == "METRIC" else "ft"
if blender_object.type != "MESH":
@@ -384,13 +247,13 @@ def ngons_to_speckle_polylines(blender_object: Object, data: bpy.types.Mesh) ->
for i, poly in enumerate(data.polygons):
value = []
for v in poly.vertices:
value.extend(mat @ verts[v].co * UnitsScale) # type: ignore
value.extend(mat @ verts[v].co * scale)
domain = Interval(start=0, end=1)
poly = Polyline(
name="{}_{}".format(blender_object.name, i),
closed=True,
value=value,
value=value, # magic (flatten list of tuples)
length=0,
domain=domain,
bbox=Box(area=0.0, volume=0.0),
@@ -403,130 +266,78 @@ def ngons_to_speckle_polylines(blender_object: Object, data: bpy.types.Mesh) ->
return polylines
def material_to_speckle(blender_mat: bpy.types.Material) -> RenderMaterial:
def material_to_speckle(blender_object: Object) -> Optional[RenderMaterial]:
"""Create and return a render material from a blender object"""
if not getattr(blender_object.data, "materials", None):
return None
blender_mat: bpy.types.Material = blender_object.data.materials[0]
if not blender_mat:
return None
speckle_mat = RenderMaterial()
speckle_mat.name = blender_mat.name
if blender_mat.use_nodes:
if blender_mat.node_tree.nodes.get("Principled BSDF"):
inputs = blender_mat.node_tree.nodes["Principled BSDF"].inputs
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.roughness = inputs["Roughness"].default_value # type: ignore
speckle_mat.metalness = inputs["Metallic"].default_value # type: ignore
speckle_mat.opacity = inputs["Alpha"].default_value # type: ignore
return speckle_mat
elif blender_mat.node_tree.nodes.get("Diffuse BSDF"):
inputs = blender_mat.node_tree.nodes["Diffuse BSDF"].inputs
speckle_mat.diffuse = to_argb_int(inputs["Color"].default_value) # type: ignore
speckle_mat.roughness = inputs["Roughness"].default_value # type: ignore
return speckle_mat
#TODO: Support more shaders
if blender_mat.use_nodes is True and blender_mat.node_tree.nodes.get(
"Principled BSDF"
):
inputs = blender_mat.node_tree.nodes["Principled BSDF"].inputs
speckle_mat.diffuse = to_argb_int(inputs["Base Color"].default_value)
speckle_mat.emissive = to_argb_int(inputs["Emission"].default_value)
speckle_mat.roughness = inputs["Roughness"].default_value
speckle_mat.metalness = inputs["Metallic"].default_value
speckle_mat.opacity = inputs["Alpha"].default_value
# fallback to standard material props
speckle_mat.diffuse = to_argb_int(blender_mat.diffuse_color) # type: ignore
speckle_mat.metalness = blender_mat.metallic
speckle_mat.roughness = blender_mat.roughness
else:
speckle_mat.diffuse = to_argb_int(blender_mat.diffuse_color)
speckle_mat.metalness = blender_mat.metallic
speckle_mat.roughness = blender_mat.roughness
return speckle_mat
def camera_to_speckle_view(blender_object: Object, data: NCamera) -> Base:
if data.type != 'PERSP':
raise Exception(f"Cameras of type {data.type} are not currently supported")
matrix = cast(MMatrix, blender_object.matrix_world)
up = matrix.col[1].xyz # type: ignore
forwards = -matrix.col[2].xyz # type: ignore
translation = matrix.translation
view = Base.of_type("Objects.BuiltElements.View:Objects.BuiltElements.View3D") #HACK: views are not in specklepy yet!
view.name = to_speckle_name(blender_object)
view.origin = vector_to_speckle_point(translation)
view.upDirection = vector_to_speckle(up)
view.forwardDirection = vector_to_speckle(forwards)
view.target = vector_to_speckle_point(forwards) #TODO: do these need to be scaled?
view.units = Units
view.isOrthogonal = False
return view
def vector_to_speckle_point(xyz: MVector) -> Point:
return Point(
x = xyz.x * UnitsScale,
y = xyz.y * UnitsScale,
z = xyz.z * UnitsScale,
units = Units,
)
def vector_to_speckle(xyz: MVector) -> Vector:
return Vector(
x = xyz.x * UnitsScale,
y = xyz.y * UnitsScale,
z = xyz.z * UnitsScale,
units = Units,
)
def transform_to_speckle(blender_transform: Union[Iterable[Iterable[float]], MMatrix]) -> Transform:
iterable_transform = cast(Iterable[Iterable[float]], blender_transform) #NOTE: Matrix are iterable, even if type hinting says they are not
value = [y for x in iterable_transform for y in x]
def transform_to_speckle(blender_transform: List[float], scale=1.0) -> Transform:
value = [y for x in blender_transform for y in x]
# scale the translation
for i in (3, 7, 11):
value[i] *= UnitsScale
value[i] *= scale
return Transform(value=value, units=Units)
return Transform(value=value, units=UNITS)
def block_def_to_speckle(blender_definition: bpy.types.Collection) -> BlockDefinition:
geometryBuilder = BlenderCommitObjectBuilder()
def block_def_to_speckle(blender_definition: bpy.types.Collection, scale=1.0) -> BlockDefinition:
geometry = []
for geo in blender_definition.objects:
try:
c = convert_to_speckle(geo, UnitsScale, Units, None)
geometryBuilder.include_object(c, geo)
except ConversionSkippedException as ex:
_report(f"Skipped converting '{geo.name_full}' inside collection instance: '{ex}")
except Exception as ex:
_report(f"Failed to converted '{geo.name_full}' inside collection instance: '{ex}'")
dummyRoot = Base()
geometryBuilder.apply_relationships(geometryBuilder.converted.values(), dummyRoot)
geometry.extend(convert_to_speckle(geo, scale, UNITS, None))
block_def = BlockDefinition(
units=Units,
name=to_speckle_name(blender_definition),
geometry=dummyRoot["@elements"],
basePoint=Point(units=Units),
units=UNITS,
name=blender_definition.name,
geometry=geometry,
basePoint=Point(units=UNITS),
)
# blender_props = get_blender_custom_properties(blender_definition)
# block_def.applicationId = blender_props.pop("applicationId", None) #TODO: remove?
blender_props = get_blender_custom_properties(blender_definition)
block_def.applicationId = blender_props.pop("applicationId", None)
return block_def
def block_instance_to_speckle(blender_instance: Object) -> BlockInstance:
def block_instance_to_speckle(blender_instance: Object, scale=1.0) -> BlockInstance:
return BlockInstance(
blockDefinition=block_def_to_speckle(
blender_instance.instance_collection
blender_instance.instance_collection, scale
),
transform=transform_to_speckle(blender_instance.matrix_world),
name=to_speckle_name(blender_instance),
units=Units,
name=blender_instance.name,
units=UNITS,
)
def empty_to_speckle(blender_object: Object) -> Union[BlockInstance, Base]:
def empty_to_speckle(blender_object: Object, scale=1.0) -> Optional[BlockInstance]:
# probably an instance collection (block) so let's try it
if blender_object.instance_collection and blender_object.instance_type == "COLLECTION":
return block_instance_to_speckle(blender_object)
else:
#raise ConversionSkippedException("Sending non-collection instance empties are not currently supported")
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:
transformed_pos = cast(MVector, matrix @ MVector((0,0,0)) * units_scale)
return Point(x = transformed_pos.x,
y = transformed_pos.y,
z = transformed_pos.z)
try:
geo = blender_object.instance_collection.objects.items()
return block_instance_to_speckle(blender_object, scale)
except AttributeError as err:
_report(
f"No instance collection found in empty. Skipping object {blender_object.name}"
)
return None
+95 -272
View File
@@ -1,21 +1,31 @@
import math
from typing import Any, Dict, Optional, Tuple, Union, cast
from typing import Tuple
from bmesh.types import BMesh
import bpy, idprop
import bpy, struct, idprop
from specklepy.objects.base import Base
from specklepy.objects.geometry import Mesh
from specklepy.objects.other import RenderMaterial
from bpy_speckle.convert.constants import IGNORED_PROPERTY_KEYS
from specklepy.serialization.base_object_serializer import BaseObjectSerializer
from bpy_speckle.functions import _report
from bpy.types import Material, Object, Collection as BCollection, Node, ShaderNodeVertexColor
from bpy.types import Object
from specklepy.objects.graph_traversal.traversal import TraversalContext
IGNORED_PROPERTY_KEYS = {
"id",
"elements",
"displayMesh",
"displayValue",
"speckle_type",
"parameters",
"faces",
"colors",
"vertices",
"renderMaterial",
"textureCoordinates",
"totalChildrenCount"
}
class ConversionSkippedException(Exception):
pass
def to_rgba(argb_int: int) -> Tuple[float, float, float, float]:
def to_rgba(argb_int: int) -> Tuple[float]:
"""Converts the int representation of a colour into a percent RGBA tuple"""
alpha = ((argb_int >> 24) & 255) / 255
red = ((argb_int >> 16) & 255) / 255
@@ -25,27 +35,18 @@ def to_rgba(argb_int: int) -> Tuple[float, float, float, float]:
return (red, green, blue, alpha)
def to_argb_int(rgba_color: list[float]) -> int:
def to_argb_int(diffuse_colour) -> int:
"""Converts an RGBA array to an ARGB integer"""
argb_color = rgba_color[-1:] + rgba_color[:3]
int_color = [int(val * 255) for val in argb_color]
diffuse_colour = diffuse_colour[-1:] + diffuse_colour[:3]
diffuse_colour = [int(val * 255) for val in diffuse_colour]
return int.from_bytes(int_color, byteorder="big", signed=True)
def set_custom_property(key: str, value: Any, blender_object: Object) -> None:
try:
#Expected c types: float, int, string, float[], int[]
blender_object[key] = value
except (OverflowError, TypeError) as ex:
print(f"Skipping setting property ({key}={value}) on {blender_object.name_full}, Reason: {ex}")
except Exception as ex:
#TODO: Log this as it's unexpected!!!
print(f"Skipping setting property ({key}={value}) on {blender_object.name_full}, Reason: {ex}")
return int.from_bytes(diffuse_colour, byteorder="big", signed=True)
def add_custom_properties(speckle_object: Base, blender_object: Object):
if blender_object is None:
return
serializer = BaseObjectSerializer()
blender_object["_speckle_type"] = type(speckle_object).__name__
app_id = getattr(speckle_object, "applicationId", None)
@@ -58,28 +59,39 @@ def add_custom_properties(speckle_object: Base, blender_object: Object):
continue
if isinstance(val, (int, str, float)):
set_custom_property(key, val, blender_object)
blender_object[key] = val
elif key == "properties" and isinstance(val, Base):
val["applicationId"] = None
add_custom_properties(val, blender_object)
elif isinstance(val, list):
items = [item for item in val if not isinstance(item, Base)]
if items:
set_custom_property(key, items, blender_object)
blender_object[key] = items
elif isinstance(val,dict):
for (k,v) in val.items():
if not isinstance(v, Base):
set_custom_property(k, v, blender_object)
blender_object[k] = v
def render_material_to_native(speckle_mat: RenderMaterial) -> Material:
mat_name = speckle_mat.name
def add_blender_material(speckle_object: Base, blender_object: Object) -> None:
"""Add material to a blender object if the corresponding speckle object has a render material"""
if blender_object.data is None:
return
speckle_mat = getattr(
speckle_object,
"renderMaterial",
getattr(speckle_object, "@renderMaterial", None),
)
if not speckle_mat:
return
mat_name = getattr(speckle_mat, "name", None) or speckle_mat.__dict__.get("@name")
if not mat_name:
mat_name = speckle_mat.applicationId or speckle_mat.id or speckle_mat.get_id()
blender_mat = bpy.data.materials.get(mat_name)
if blender_mat is None:
if not blender_mat:
blender_mat = bpy.data.materials.new(mat_name)
# for now, we're not updating these materials. as per tom's suggestion, we should have a toggle
@@ -87,54 +99,16 @@ def render_material_to_native(speckle_mat: RenderMaterial) -> Material:
blender_mat.use_nodes = True
inputs = blender_mat.node_tree.nodes["Principled BSDF"].inputs
inputs["Base Color"].default_value = to_rgba(speckle_mat.diffuse) # type: ignore
inputs["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
inputs["Base Color"].default_value = to_rgba(speckle_mat.diffuse)
inputs["Emission"].default_value = to_rgba(speckle_mat.emissive)
inputs["Roughness"].default_value = speckle_mat.roughness
inputs["Metallic"].default_value = speckle_mat.metalness
inputs["Alpha"].default_value = speckle_mat.opacity
if speckle_mat.opacity < 1.0:
if speckle_mat.opacity < 1:
blender_mat.blend_method = "BLEND"
return blender_mat
_vertex_color_material: Optional[Material] = None
def get_vertex_color_material() -> Material:
global _vertex_color_material
#see https://stackoverflow.com/a/69807985
if not _vertex_color_material:
_vertex_color_material = bpy.data.materials.new("Vertex Color Material")
_vertex_color_material.use_nodes = True
nodes = _vertex_color_material.node_tree.nodes
principled_bsdf_node = cast(Node, nodes.get("Principled BSDF"))
if not "VERTEX_COLOR" in [node.type for node in nodes]:
vertex_color_node = cast(ShaderNodeVertexColor, nodes.new(type = "ShaderNodeVertexColor"))
else:
vertex_color_node = cast(ShaderNodeVertexColor, nodes.get("Vertex Color"))
vertex_color_node.layer_name = "Col"
links = _vertex_color_material.node_tree.links
link = links.new(vertex_color_node.outputs[0], principled_bsdf_node.inputs[0])
return _vertex_color_material
def get_render_material(speckle_object: Base) -> Optional[RenderMaterial]:
"""Trys to get a RenderMaterial on given speckle_object"""
speckle_mat = getattr(
speckle_object,
"renderMaterial",
getattr(speckle_object, "@renderMaterial", None),
)
if isinstance(speckle_mat, RenderMaterial):
return speckle_mat
return None
blender_object.data.materials.append(blender_mat)
def add_vertices(speckle_mesh: Mesh, blender_mesh: BMesh, scale=1.0):
@@ -150,11 +124,12 @@ def add_vertices(speckle_mesh: Mesh, blender_mesh: BMesh, scale=1.0):
)
)
blender_mesh.verts.ensure_lookup_table()
def add_faces(speckle_mesh: Mesh, blender_mesh: BMesh, indexOffset: int, materialIndex: int = 0, smooth:bool = True):
def add_faces(speckle_mesh: Mesh, blender_mesh: BMesh, smooth=False):
sfaces = speckle_mesh.faces
if sfaces and len(sfaces) > 0:
i = 0
while i < len(sfaces):
@@ -165,14 +140,16 @@ 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[int(x)] for x in sfaces[i : i + n]]
)
f.material_index = materialIndex
f.smooth = smooth
except Exception as e:
_report(f"Failed to create face for mesh {speckle_mesh.id} \n{e}")
i += n
blender_mesh.faces.ensure_lookup_table()
blender_mesh.verts.index_update()
def add_colors(speckle_mesh: Mesh, blender_mesh: BMesh):
@@ -183,8 +160,10 @@ def add_colors(speckle_mesh: Mesh, blender_mesh: BMesh):
if len(scolors) > 0:
for i in range(len(scolors)):
argb = int(scolors[i])
(a, r, g, b) = argb_split(argb)
col = int(scolors[i])
(a, r, g, b) = [
int(x) for x in struct.unpack("!BBBB", struct.pack("!i", col))
]
colors.append(
(
float(r) / 255.0,
@@ -195,20 +174,13 @@ 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]
def argb_split(argb: int) -> Tuple[int, int, int, int]:
alpha = (argb >> 24) & 0xFF
red = (argb >> 16) & 0xFF
green = (argb >> 8) & 0xFF
blue = argb & 0xFF
return (alpha, red, green, blue)
def add_uv_coords(speckle_mesh: Mesh, blender_mesh: BMesh):
s_uvs = speckle_mesh.textureCoordinates
@@ -217,21 +189,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 * 2: {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,9 +226,8 @@ ignored_keys = {
"_chunkable",
}
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"""
if max_depth <= 0:
def get_blender_custom_properties(obj, max_depth=1000):
if max_depth < 0:
return obj
if hasattr(obj, "keys"):
@@ -268,41 +239,45 @@ def get_blender_custom_properties(obj, max_depth: int = 63):
}
if isinstance(obj, (list, tuple, idprop.types.IDPropertyArray)):
return [get_blender_custom_properties(o, max_depth - 1) for o in obj] # type: ignore
return [get_blender_custom_properties(o, max_depth - 1) for o in obj]
return obj
"""
Python implementation of Blender's NURBS curve generation for to Speckle conversion
from: https://blender.stackexchange.com/a/34276
based on https://projects.blender.org/blender/blender/src/branch/main/source/blender/blenkernel/intern/curve.cc (check old version)
"""
def macro_knotsu(nu: bpy.types.Spline) -> int:
def macro_knotsu(nu):
return nu.order_u + nu.point_count_u + (nu.order_u - 1 if nu.use_cyclic_u else 0)
def macro_segmentsu(nu: bpy.types.Spline) -> int:
def macro_segmentsu(nu):
return nu.point_count_u if nu.use_cyclic_u else nu.point_count_u - 1
def make_knots(nu: bpy.types.Spline) -> list[float]:
knots = [0.0] * macro_knotsu(nu)
def make_knots(nu):
knots = [0.0] * (4 + macro_knotsu(nu))
flag = nu.use_endpoint_u + (nu.use_bezier_u << 1)
if nu.use_cyclic_u:
calc_knots(knots, nu.point_count_u, nu.order_u, 0)
makecyclicknots(knots, nu.point_count_u, nu.order_u)
else:
calc_knots(knots, nu.point_count_u, nu.order_u, flag)
return knots
def calc_knots(knots: list[float], point_count: int, order: int, flag: int) -> None:
def calc_knots(knots, point_count, order, flag):
pts_order = point_count + order
if flag == 1: # CU_NURB_ENDPOINT
if flag == 1:
k = 0.0
for a in range(1, pts_order + 1):
knots[a - 1] = k
if a >= order and a <= point_count:
k += 1.0
elif flag == 2: # CU_NURB_BEZIER
elif flag == 2:
if order == 4:
k = 0.34
for a in range(pts_order):
@@ -315,176 +290,24 @@ def calc_knots(knots: list[float], point_count: int, order: int, flag: int) -> N
k += 0.5
knots[a] = math.floor(k)
else:
for a in range(1, len(knots) - 1):
knots[a] = a - 1
for a in range(pts_order):
knots[a] = a
knots[-1] = knots[-2]
def basis_nurb(t: float, order: int, point_count: int, knots: list[float], basis: list[float], start: int, end: int) -> Tuple[int, int]:
i1 = i2 = 0
orderpluspnts = order + point_count
opp2 = orderpluspnts - 1
# this is for float inaccuracy
if t < knots[0]:
t = knots[0]
elif t > knots[opp2]:
t = knots[opp2]
def makecyclicknots(knots, point_count, order):
order2 = order - 1
# this part is order '1'
o2 = order + 1
for i in range(opp2):
if knots[i] != knots[i + 1] and t >= knots[i] and t <= knots[i + 1]:
basis[i] = 1.0
i1 = i - o2
if i1 < 0:
i1 = 0
i2 = i
i += 1
while i < opp2:
basis[i] = 0.0
i += 1
break
else:
basis[i] = 0.0
basis[i] = 0.0 #type: ignore
# this is order 2, 3, ...
for j in range(2, order + 1):
if i2 + j >= orderpluspnts:
i2 = opp2 - j
for i in range(i1, i2 + 1):
if basis[i] != 0.0:
d = ((t - knots[i]) * basis[i]) / (knots[i + j - 1] - knots[i])
else:
d = 0.0
if basis[i + 1] != 0.0:
e = ((knots[i + j] - t) * basis[i + 1]) / (knots[i + j] - knots[i + 1])
else:
e = 0.0
basis[i] = d + e
start = 1000
end = 0
for i in range(i1, i2 + 1):
if basis[i] > 0.0:
end = i
if start == 1000:
start = i
return start, end
def nurb_make_curve(nu: bpy.types.Spline, resolu: int, stride: int = 3) -> list[float]:
""""BKE_nurb_makeCurve"""
EPS = 1e-6
coord_index = istart = iend = 0
coord_array = [0.0] * (3 * nu.resolution_u * macro_segmentsu(nu))
sum_array = [0] * nu.point_count_u
basisu = [0.0] * macro_knotsu(nu)
knots = make_knots(nu)
resolu = resolu * macro_segmentsu(nu)
ustart = knots[nu.order_u - 1]
uend = knots[nu.point_count_u + nu.order_u - 1] if nu.use_cyclic_u else \
knots[nu.point_count_u]
ustep = (uend - ustart) / (resolu - (0 if nu.use_cyclic_u else 1))
cycl = nu.order_u - 1 if nu.use_cyclic_u else 0
u = ustart
while resolu:
resolu -= 1
istart, iend = basis_nurb(u, nu.order_u, nu.point_count_u + cycl, knots, basisu, istart, iend)
#/* calc sum */
sumdiv = 0.0
sum_index = 0
pt_index = istart - 1
for i in range(istart, iend + 1):
if i >= nu.point_count_u:
pt_index = i - nu.point_count_u
else:
pt_index += 1
sum_array[sum_index] = basisu[i] * nu.points[pt_index].co[3] #type: ignore
sumdiv += sum_array[sum_index]
sum_index += 1
if (sumdiv != 0.0) and (sumdiv < 1.0 - EPS or sumdiv > 1.0 + EPS):
sum_index = 0
for i in range(istart, iend + 1):
sum_array[sum_index] /= sumdiv #type: ignore
sum_index += 1
coord_array[coord_index: coord_index + 3] = (0.0, 0.0, 0.0)
sum_index = 0
pt_index = istart - 1
for i in range(istart, iend + 1):
if i >= nu.point_count_u:
pt_index = i - nu.point_count_u
else:
pt_index += 1
if sum_array[sum_index] != 0.0:
for j in range(3):
coord_array[coord_index + j] += sum_array[sum_index] * nu.points[pt_index].co[j]
sum_index += 1
coord_index += stride
u += ustep
return coord_array
def link_object_to_collection_nested(obj: Object, col: BCollection):
if obj.name not in col.objects: #type: ignore
col.objects.link(obj)
for child in obj.children: #type: ignore
link_object_to_collection_nested(child, col)
def add_to_hierarchy(converted: Union[Object, BCollection], traversalContext : 'TraversalContext', converted_objects: Dict[str, Union[Object, BCollection]], preserve_transform: bool) -> None:
nextParent = traversalContext.parent
# Traverse up the tree to find a direct parent object, and a containing collection
parent_collection: Optional[BCollection] = None
parent_object: Optional[Object] = None
while nextParent:
if nextParent.current.id in converted_objects:
c = converted_objects[nextParent.current.id]
if isinstance(c, BCollection):
parent_collection = c
if order > 2:
b = point_count + order2
for a in range(1, order2):
if knots[b] != knots[b - a]:
break
else: #isinstance(c, Object):
parent_object = parent_object or c
nextParent = nextParent.parent
if a == order2:
knots[point_count + order - 2] += 1.0
# If no containing collection is found, fall back to the scene collection
if not parent_collection:
parent_collection = bpy.context.scene.collection
if isinstance(converted, Object):
if parent_object:
set_parent(converted, parent_object, preserve_transform)
link_object_to_collection_nested(converted, parent_collection)
elif converted.name not in parent_collection.children.keys():
parent_collection.children.link(converted)
def set_parent(child: Object, parent: Object, preserve_transform: bool = False) -> None:
if preserve_transform :
previous = child.matrix_world.copy() # type: ignore
child.parent = parent
child.matrix_world = previous
else:
child.parent = parent
b = order
c = point_count + order + order2
for a in range(point_count + order2, c):
knots[a] = knots[a - 1] + (knots[b] - knots[b - 1])
b -= 1
+53 -26
View File
@@ -1,12 +1,32 @@
from typing import Callable
from specklepy.objects.base import Base
from bpy_speckle.convert.constants import ELEMENTS_PROPERTY_ALIASES
from bpy_speckle.clients import speckle_clients
from specklepy.objects.graph_traversal.traversal import GraphTraversal, TraversalRule
from specklepy.objects.units import get_scale_factor_to_meters, get_units_from_string
"""
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
"""
def _report(msg: object) -> None:
def _report(msg):
"""
Function for printing messages to the console
"""
@@ -14,32 +34,39 @@ def _report(msg: object) -> None:
def get_scale_length(units: str) -> float:
"""Returns a scalar to convert distance values from one unit system to meters"""
return get_scale_factor_to_meters(get_units_from_string(units))
if units.lower() in unit_scale.keys():
return unit_scale[units]
_report("Units <{}> are not supported.".format(units))
return 1.0
def get_default_traversal_func(can_convert_to_native: Callable[[Base], bool]) -> GraphTraversal:
"""
Client, user, and stream functions
"""
def _check_speckle_client_user_stream(scene):
"""
Traversal func for traversing a speckle commit object
Verify that there is a valid user and stream
"""
ignore_rule = TraversalRule(
[
lambda o: "Objects.Structural.Results" in o.speckle_type, #Sadly, this one is necessary to avoid double conversion...
lambda o: "Objects.BuiltElements.Revit.Parameter" in o.speckle_type, #This one is just for traversal performance of revit commits
],
lambda _: [],
speckle = scene.speckle
user = (
speckle.users[int(speckle.active_user)]
if len(speckle.users) > int(speckle.active_user)
else None
)
convertible_rule = TraversalRule(
[can_convert_to_native],
lambda _: ELEMENTS_PROPERTY_ALIASES,
if user is None:
print("No users loaded.")
stream = (
user.streams[user.active_stream]
if len(user.streams) > user.active_stream
else None
)
if stream is None:
print("Account contains no streams.")
default_rule = TraversalRule(
[lambda _: True],
lambda o: o.get_member_names(), #TODO: avoid deprecated members
)
return GraphTraversal([ignore_rule, convertible_rule, default_rule])
return (user, stream)
+33 -115
View File
@@ -1,118 +1,29 @@
"""
Provides uniform and consistent path helpers for `specklepy`
"""
import os
import sys
from pathlib import Path
from typing import Optional
from importlib import import_module, invalidate_caches
from importlib import import_module
_user_data_env_var = "SPECKLE_USERDATA_PATH"
import bpy
import sys
def _path() -> Optional[Path]:
"""Read the user data path override setting."""
path_override = os.environ.get(_user_data_env_var)
if path_override:
return Path(path_override)
return None
_application_name = "Speckle"
def override_application_name(application_name: str) -> None:
"""Override the global Speckle application name."""
global _application_name
_application_name = application_name
def override_application_data_path(path: Optional[str]) -> None:
"""
Override the global Speckle application data path.
If the value of path is `None` the environment variable gets deleted.
"""
if path:
os.environ[_user_data_env_var] = path
else:
os.environ.pop(_user_data_env_var, None)
def _ensure_folder_exists(base_path: Path, folder_name: str) -> Path:
path = base_path.joinpath(folder_name)
path.mkdir(exist_ok=True, parents=True)
return path
def user_application_data_path() -> Path:
"""Get the platform specific user configuration folder path"""
path_override = _path()
if path_override:
return path_override
try:
if sys.platform.startswith("win"):
app_data_path = os.getenv("APPDATA")
if not app_data_path:
raise Exception(
"Cannot get appdata path from environment."
)
return Path(app_data_path)
else:
# try getting the standard XDG_DATA_HOME value
# as that is used as an override
app_data_path = os.getenv("XDG_DATA_HOME")
if app_data_path:
return Path(app_data_path)
else:
return _ensure_folder_exists(Path.home(), ".config")
except Exception as ex:
raise Exception(
"Failed to initialize user application data path.", ex
)
def user_speckle_folder_path() -> Path:
"""Get the folder where the user's Speckle data should be stored."""
return _ensure_folder_exists(user_application_data_path(), _application_name)
def user_speckle_connector_installation_path(host_application: str) -> Path:
"""
Gets a connector specific installation folder.
In this folder we can put our connector installation and all python packages.
"""
return _ensure_folder_exists(
_ensure_folder_exists(user_speckle_folder_path(), "connector_installations"),
host_application,
)
print("Starting module dependency installation")
print("Starting Speckle Blender installation")
print(sys.executable)
PYTHON_PATH = sys.executable
def connector_installation_path(host_application: str) -> Path:
connector_installation_path = user_speckle_connector_installation_path(host_application)
connector_installation_path.mkdir(exist_ok=True, parents=True)
def modules_path() -> Path:
modules_path = Path(bpy.utils.script_path_user(), "addons", "modules")
modules_path.mkdir(exist_ok=True, parents=True)
# set user modules path at beginning of paths for earlier hit
if sys.path[0] != connector_installation_path:
sys.path.insert(0, str(connector_installation_path))
if sys.path[1] != modules_path:
sys.path.insert(1, modules_path)
print(f"Using connector installation path {connector_installation_path}")
return connector_installation_path
return modules_path
print(f"Found blender modules path {modules_path()}")
def is_pip_available() -> bool:
try:
@@ -123,7 +34,7 @@ def is_pip_available() -> bool:
def ensure_pip() -> None:
print("Installing pip... ")
print("Installing pip... "),
from subprocess import run
@@ -132,7 +43,7 @@ def ensure_pip() -> None:
if completed_process.returncode == 0:
print("Successfully installed pip")
else:
raise Exception(f"Failed to install pip, got {completed_process.returncode} return code")
raise Exception("Failed to install pip.")
def get_requirements_path() -> Path:
@@ -142,11 +53,11 @@ def get_requirements_path() -> Path:
return path
def install_requirements(host_application: str) -> None:
def install_requirements() -> None:
# set up addons/modules under the user
# script path. Here we'll install the
# dependencies
path = connector_installation_path(host_application)
path = modules_path()
print(f"Installing Speckle dependencies to {path}")
from subprocess import run
@@ -167,16 +78,20 @@ def install_requirements(host_application: str) -> None:
)
if completed_process.returncode != 0:
m = f"Failed to install dependenices through pip, got {completed_process.returncode} return code"
print(m)
raise Exception(m)
print("Please try manually installing speckle-blender")
raise Exception(
"""
Failed to install speckle-blender.
See console for manual install instruction.
"""
)
def install_dependencies(host_application: str) -> None:
def install_dependencies() -> None:
if not is_pip_available():
ensure_pip()
install_requirements(host_application)
install_requirements()
def _import_dependencies() -> None:
@@ -195,13 +110,16 @@ def _import_dependencies() -> None:
# print(req)
# import_module("specklepy")
def ensure_dependencies(host_application: str) -> None:
def ensure_dependencies() -> None:
try:
install_dependencies(host_application)
invalidate_caches()
_import_dependencies()
print("Successfully found dependencies")
print("Found all dependencies, proceed with loading")
except ImportError:
raise Exception(f"Cannot automatically ensure Speckle dependencies. Please try restarting the host application {host_application}!")
print("Failed to load all dependencies, trying to install them...")
install_dependencies()
raise Exception("Please restart Blender.")
if __name__ == "__main__":
ensure_dependencies()
+3 -2
View File
@@ -1,4 +1,4 @@
from .users import LoadUsers, LoadUserStreams, ResetUsers
from .users import LoadUsers, LoadUserStreams
from .object import (
UpdateObject,
ResetObject,
@@ -15,6 +15,7 @@ from .streams import (
SelectOrphanObjects,
)
from .streams import (
UpdateGlobal,
AddStreamFromURL,
CreateStream,
CopyStreamId,
@@ -26,7 +27,6 @@ from .misc import OpenSpeckleGuide, OpenSpeckleTutorials, OpenSpeckleForum
operator_classes = [
LoadUsers,
ResetUsers,
ReceiveStreamObjects,
SendStreamObjects,
LoadUserStreams,
@@ -53,6 +53,7 @@ operator_classes.extend(
ViewStreamDataApi,
DeleteStream,
SelectOrphanObjects,
UpdateGlobal,
AddStreamFromURL,
CreateStream,
OpenSpeckleGuide,
+19 -31
View File
@@ -3,15 +3,13 @@ Commit operators
"""
import bpy
from bpy.props import BoolProperty
from bpy_speckle.functions import _check_speckle_client_user_stream
from bpy_speckle.clients import speckle_clients
from bpy_speckle.properties.scene import get_speckle
from specklepy.logging import metrics
class DeleteCommit(bpy.types.Operator):
"""
Deletes the selected commit from the selected stream.
To execute from code, call: `bpy.ops.speckle.delete_commit(are_you_sure=True)`
Delete stream
"""
bl_idname = "speckle.delete_commit"
@@ -30,46 +28,36 @@ class DeleteCommit(bpy.types.Operator):
col.prop(self, "are_you_sure")
def invoke(self, context, event):
speckle = get_speckle(context)
wm = context.window_manager
if len(speckle.users) > 0:
if len(context.scene.speckle.users) > 0:
return wm.invoke_props_dialog(self)
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")
return {"CANCELLED"}
self.are_you_sure = False
speckle = get_speckle(context)
speckle = context.scene.speckle
(_, stream, _, commit) = speckle.validate_commit_selection()
check = _check_speckle_client_user_stream(context.scene)
if check is None:
return {"CANCELLED"}
client = speckle_clients[int(speckle.active_user)]
user, stream = check
client = speckle_clients[int(context.scene.speckle.active_user)]
stream = user.streams[user.active_stream]
if len(stream.branches) < 1:
return {"CANCELLED"}
branch = stream.branches[int(stream.branch)]
if len(branch.commits) < 1:
return {"CANCELLED"}
commit = branch.commits[int(branch.commit)]
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}")
return {"FINISHED"}
-22
View File
@@ -1,6 +1,5 @@
import bpy
import webbrowser
from specklepy.logging import metrics
class OpenSpeckleGuide(bpy.types.Operator):
@@ -11,13 +10,6 @@ class OpenSpeckleGuide(bpy.types.Operator):
def execute(self, context):
webbrowser.open("https://speckle.guide/user/blender.html")
metrics.track(
"Connector Action",
None,
custom_props={
"name": "OpenSpeckleGuide"
},
)
return {"FINISHED"}
@@ -29,13 +21,6 @@ class OpenSpeckleTutorials(bpy.types.Operator):
def execute(self, context):
webbrowser.open("https://speckle.systems/tutorials/")
metrics.track(
"Connector Action",
None,
custom_props={
"name": "OpenSpeckleTutorials"
},
)
return {"FINISHED"}
@@ -47,11 +32,4 @@ class OpenSpeckleForum(bpy.types.Operator):
def execute(self, context):
webbrowser.open("https://speckle.community/")
metrics.track(
"Connector Action",
None,
custom_props={
"name": "OpenSpeckleForum"
},
)
return {"FINISHED"}
+3 -50
View File
@@ -4,14 +4,13 @@ Object operators
import bpy
from bpy.props import BoolProperty, EnumProperty
from deprecated import deprecated
from bpy_speckle.convert.to_speckle import (
convert_to_speckle,
ngons_to_speckle_polylines,
)
from bpy_speckle.functions import get_scale_length, _report
from bpy_speckle.clients import speckle_clients
from specklepy.logging import metrics
class UpdateObject(bpy.types.Operator):
"""
@@ -29,6 +28,7 @@ class UpdateObject(bpy.types.Operator):
def execute(self, context):
user = context.scene.speckle.users[int(context.scene.speckle.active_user)]
client = speckle_clients[int(context.scene.speckle.active_user)]
stream = user.streams[user.active_stream]
active = context.active_object
_report(active)
@@ -56,13 +56,6 @@ 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"}
@@ -86,14 +79,6 @@ 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"}
@@ -140,17 +125,9 @@ 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
class UploadNgonsAsPolylines(bpy.types.Operator):
"""
Upload mesh ngon faces as polyline outlines
@@ -220,13 +197,6 @@ 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):
@@ -297,14 +267,6 @@ class SelectIfSameCustomProperty(bpy.types.Operator):
else:
obj.select_set(False)
metrics.track(
"Connector Action",
None,
custom_props={
"name": "SelectIfSameCustomProperty"
},
)
return {"FINISHED"}
@@ -353,13 +315,4 @@ class SelectIfHasCustomProperty(bpy.types.Operator):
else:
obj.select_set(False)
metrics.track(
"Connector Action",
None,
custom_props={
"name": "SelectIfHasCustomProperty"
},
)
return {"FINISHED"}
File diff suppressed because it is too large Load Diff
+46 -118
View File
@@ -1,48 +1,14 @@
"""
User account operators
"""
from typing import 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.core.api.client import SpeckleClient
from specklepy.core.api.models import Stream
from specklepy.core.api.credentials import get_local_accounts
from specklepy.logging import metrics
from specklepy.api.client import SpeckleClient
from specklepy.api.models import Stream, User
from specklepy.api.credentials import get_local_accounts
from datetime import datetime
class ResetUsers(bpy.types.Operator):
"""
Reset loaded users
"""
bl_idname = "speckle.users_reset"
bl_label = "Reset users"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
self.reset_ui(context)
metrics.track(
"Connector Action",
None,
custom_props={
"name": "ResetUsers"
},
)
bpy.context.view_layer.update()
if context.area:
context.area.tag_redraw()
return {"FINISHED"}
@staticmethod
def reset_ui(context: Context):
speckle = get_speckle(context)
speckle.users.clear()
speckle_clients.clear()
class LoadUsers(bpy.types.Operator):
"""
@@ -57,65 +23,44 @@ class LoadUsers(bpy.types.Operator):
_report("Loading users...")
speckle = cast(SpeckleSceneSettings, context.scene.speckle) #type: ignore
users = speckle.users
users = context.scene.speckle.users
ResetUsers.reset_ui(context)
context.scene.speckle.users.clear()
speckle_clients.clear()
profiles = get_local_accounts()
active_user_index = 0
metrics.track(
"Connector Action",
None,
custom_props={
"name": "LoadUsers",
},
)
if not profiles:
raise Exception("Zero accounts were found, please add one through Speckle Manager or a local account")
for profile in profiles:
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 ""
user.authToken = profile.token
try:
url = profile.serverInfo.url
assert(url)
client = SpeckleClient(
host=url,
use_ssl="https" in url,
host=profile.serverInfo.url,
use_ssl="https" in profile.serverInfo.url,
)
client.authenticate_with_account(profile)
client.authenticate(user.authToken)
speckle_clients.append(client)
except Exception as ex:
_report(f"Failed to authenticate user {user.email} with server {user.server_url}: {ex}")
_report(ex)
users.remove(len(users) - 1)
if profile.isDefault:
active_user_index = len(users) - 1
_report(f"Authenticated {len(users)}/{len(profiles)} accounts")
if active_user_index < len(users):
speckle.active_user = str(active_user_index)
context.scene.speckle.active_user = str(len(users) - 1)
context.scene.speckle.active_user_index = int(context.scene.speckle.active_user)
bpy.ops.speckle.load_user_streams()
bpy.context.view_layer.update()
if context.area:
context.area.tag_redraw()
if not users:
raise Exception("Zero valid user accounts were found, please ensure account is valid and the server is running")
return {"FINISHED"}
def add_user_stream(user: SpeckleUserObject, stream: Stream):
def add_user_stream(user: User, stream: Stream):
s = user.streams.add()
s.name = stream.name
s.id = stream.id
@@ -133,24 +78,23 @@ def add_user_stream(user: SpeckleUserObject, stream: Stream):
continue
for c in b.commits.items:
commit: SpeckleCommitObject = branch.commits.add()
commit = 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.created_at = datetime.strftime(c.createdAt, "%Y-%m-%d %H:%M:%S.%f%Z")
commit.source_application = str(c.sourceApplication)
commit.referenced_object = c.referencedObject
if hasattr(s, "baseProperties"):
s.units = stream.baseProperties.units # type: ignore
s.units = stream.baseProperties.units
else:
s.units = "Meters"
class LoadUserStreams(bpy.types.Operator):
"""
Load all available streams for active user
Load all available streams for active user user
"""
bl_idname = "speckle.load_user_streams"
@@ -158,50 +102,34 @@ class LoadUserStreams(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
bl_description = "(Re)load all available user streams"
stream_limit: int = 20
branch_limit: int = 20
def execute(self, context):
try:
self.load_user_stream(context)
speckle = context.scene.speckle
if len(speckle.users) > 0:
user = speckle.users[int(context.scene.speckle.active_user)]
client = speckle_clients[int(context.scene.speckle.active_user)]
try:
streams = client.stream.list(stream_limit=20)
except Exception as e:
_report(f"Failed to retrieve streams: {e}")
return
if not streams:
_report("Failed to retrieve streams.")
return
user.streams.clear()
default_units = "Meters"
for s in streams:
sstream = client.stream.get(id=s.id, branch_limit=20)
add_user_stream(user, sstream)
bpy.context.view_layer.update()
return {"FINISHED"}
except Exception as ex:
_report(f"{self.bl_idname} failed: {ex}")
return {"CANCELLED"}
def load_user_stream(self, context: Context) -> None:
speckle = get_speckle(context)
user = speckle.validate_user_selection()
client = speckle_clients[int(speckle.active_user)]
try:
streams = client.stream.list(stream_limit=self.stream_limit)
except Exception as ex:
raise Exception(f"Failed to retrieve streams") from ex
if not streams:
raise Exception("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)
add_user_stream(user, sstream)
bpy.context.view_layer.update()
if context.area:
context.area.tag_redraw()
metrics.track(
"Connector Action",
client.account,
custom_props={
"name": "LoadUserStreams"
},
)
return {"CANCELLED"}
+10 -72
View File
@@ -1,7 +1,6 @@
"""
Scene properties
"""
from typing import Optional, Tuple
import bpy
from bpy.props import (
StringProperty,
@@ -13,18 +12,18 @@ from bpy.props import (
PointerProperty,
)
class SpeckleSceneObject(bpy.types.PropertyGroup):
name: bpy.props.StringProperty(default="")
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="abc")
message: StringProperty(default="A simple commit")
author_name: StringProperty(default="Author name")
author_id: StringProperty(default="Author ID")
created_at: StringProperty(default="Today")
source_application: StringProperty(default="Unknown")
class SpeckleBranchObject(bpy.types.PropertyGroup):
@@ -43,12 +42,6 @@ class SpeckleBranchObject(bpy.types.PropertyGroup):
description="Active commit",
items=get_commits,
)
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):
@@ -73,28 +66,17 @@ class SpeckleStreamObject(bpy.types.PropertyGroup):
items=get_branches,
)
def get_active_branch(self) -> Optional[SpeckleBranchObject]:
selected_index = int(self.branch)
if 0 <= selected_index < len(self.branches):
return self.branches[selected_index]
return None
class SpeckleUserObject(bpy.types.PropertyGroup):
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")
authToken: StringProperty(default="")
streams: CollectionProperty(type=SpeckleStreamObject)
active_stream: IntProperty(default=0)
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):
@@ -122,8 +104,8 @@ class SpeckleSceneSettings(bpy.types.PropertyGroup):
active_user: EnumProperty(
items=get_users,
name="Account",
description="Select account",
name="User",
description="Select user",
update=set_user,
get=None,
set=None,
@@ -150,47 +132,3 @@ class SpeckleSceneSettings(bpy.types.PropertyGroup):
description="Script to run when sending stream objects.",
items=get_scripts,
)
def get_active_user(self) -> Optional[SpeckleUserObject]:
selected_index = int(self.active_user)
if 0 <= selected_index < len(self.users):
return self.users[selected_index]
return None
def validate_user_selection(self) -> SpeckleUserObject:
user = self.get_active_user()
if not user:
raise SelectionException("No user selected/found")
return user
def validate_stream_selection(self) -> Tuple[SpeckleUserObject, SpeckleStreamObject]:
user = self.validate_user_selection()
stream = user.get_active_stream()
if not stream:
raise SelectionException("No stream selected/found")
return (user, stream)
def validate_branch_selection(self) -> Tuple[SpeckleUserObject, SpeckleStreamObject, SpeckleBranchObject]:
(user, stream) = self.validate_stream_selection()
branch = stream.get_active_branch()
if not branch:
raise SelectionException("No branch 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")
return (user, stream, branch, commit)
class SelectionException(Exception):
pass
def get_speckle(context: bpy.types.Context) -> SpeckleSceneSettings:
return context.scene.speckle #type: ignore
+2 -3
View File
@@ -115,14 +115,13 @@ class VIEW3D_PT_SpeckleUser(bpy.types.Panel):
col = layout.column()
if len(speckle.users) < 1:
col.label(text="Refresh to initialise")
col.label(text="No users found.")
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.operator("speckle.users_load", text="", icon="FILE_REFRESH")
class VIEW3D_PT_SpeckleStreams(bpy.types.Panel):
"""
Generated
+649 -910
View File
File diff suppressed because it is too large Load Diff
+4 -4
View File
@@ -7,17 +7,17 @@ license = "Apache-2.0"
[tool.poetry.dependencies]
python = ">=3.8, <4.0.0"
specklepy = "^2.16.2"
attrs = "^23.1.0"
specklepy = "^2.9.1"
# [tool.poetry.group.local_specklepy.dependencies]
# specklepy = {path = "../specklepy", develop = true}
[tool.poetry.group.dev.dependencies]
fake-bpy-module-latest = "^20230117"
numpy = "^1.23.5"
fake-bpy-module-latest = "^20221006"
black = "^22.10.0"
pylint = "^2.15.7"
ruff = "^0.0.187"
ruff = "^0.0.166"
[build-system]
requires = ["poetry-core>=1.0.0"]