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
17 changed files with 1006 additions and 1750 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
+211 -480
View File
@@ -1,23 +1,13 @@
import math
from typing import Tuple, Union, Collection
from typing import Union
from bpy_speckle.functions import get_scale_length, _report
from mathutils import (
Matrix as MMatrix,
Vector as MVector,
Quaternion as MQuaternion,
)
import bpy, bmesh
from specklepy.objects.other import (
Instance,
Transform,
BlockDefinition,
)
import mathutils
import bpy, bmesh, bpy_types
from specklepy.objects.other import *
from specklepy.objects.geometry import *
from bpy.types import Object
from .util import (
get_render_material,
link_object_to_collection_nested,
render_material_to_native,
add_blender_material,
add_custom_properties,
add_vertices,
add_faces,
@@ -25,221 +15,147 @@ from .util import (
add_uv_coords,
)
SUPPORTED_CURVES = (Line, Polyline, Curve, Arc, Polycurve, Ellipse, Circle)
SUPPORTED_CURVES = (Line, Polyline, Curve, Arc, Polycurve)
CAN_CONVERT_TO_NATIVE = (
Mesh,
Brep,
*SUPPORTED_CURVES,
Instance,
Transform,
BlockDefinition,
BlockInstance,
)
def _has_native_convesion(speckle_object: Base) -> bool:
return any(isinstance(speckle_object, t) for t in CAN_CONVERT_TO_NATIVE)
def _has_fallback_conversion(speckle_object: Base) -> bool:
return any(getattr(speckle_object, alias, None) for alias in DISPLAY_VALUE_PROPERTY_ALIASES)
def can_convert_to_native(speckle_object: Base) -> bool:
if(_has_native_convesion(speckle_object) or _has_fallback_conversion(speckle_object)):
if type(speckle_object) in CAN_CONVERT_TO_NATIVE:
return True
if getattr(
speckle_object, "displayValue", getattr(speckle_object, "displayMesh", None)
):
return True
_report(f"Could not convert unsupported Speckle object: {speckle_object}")
return False
def create_new_object(obj_data: Optional[bpy.types.ID], desired_name: str, counter: int = 0) -> bpy.types.Object:
"""
Creates a new blender object with a unique name,
if the desired_name is already taken
we'll append a number, with the format .xxx to the desired_name to ensure the name is unique.
"""
name = desired_name if counter == 0 else f"{desired_name[:OBJECT_NAME_MAX_LENGTH - 4]}.{counter:03d}" # format counter as name.xxx, truncate to ensure we don't exceed the object name max length
#TODO: This is very slow, and gets slower the more objects you receive with the same name...
# We could use a binary/galloping search, and/or cache the name -> index within a receive.
if name in bpy.data.objects.keys():
#Object already exists, increment counter and try again!
return create_new_object(obj_data, desired_name, counter + 1)
blender_object = bpy.data.objects.new(name, obj_data)
return blender_object
convert_instances_as: str #HACK: This is hacky, we need a better way to pass settings down to the converter
def set_convert_instances_as(value: str):
global convert_instances_as
convert_instances_as = value
def convert_to_native(speckle_object: Base) -> list[Object]:
def convert_to_native(speckle_object: Base, name: Optional[str] = None) -> Optional[Union[list, Object]]:
speckle_type = type(speckle_object)
try:
object_name = _generate_object_name(speckle_object)
scale = get_scale_factor(speckle_object)
obj_data: Optional[Union[bpy.types.ID, bpy.types.Object]] = None
converted: list[Object] = []
# convert elements/breps
if not _has_native_convesion(speckle_object):
(obj_data, converted) = element_to_native(speckle_object, object_name, scale)
if not obj_data and not converted:
_report(f"Unsupported type {speckle_object.speckle_type}")
# convert supported geometry
elif isinstance(speckle_object, Mesh):
obj_data = mesh_to_native(speckle_object, object_name, scale)
elif speckle_type in SUPPORTED_CURVES:
obj_data = icurve_to_native(speckle_object, object_name, scale)
elif isinstance(speckle_object, Instance):
if convert_instances_as == "linked_duplicates":
(obj_data, converted) = instance_to_native_object(speckle_object, scale)
elif convert_instances_as != "collection_instance":
obj_data = instance_to_native_collection_instance(speckle_object, scale)
speckle_name = (
name
or getattr(speckle_object, "name", None)
or f"{speckle_object.speckle_type} -- {speckle_object.id}"
)
# convert unsupported types with display values
if speckle_type not in CAN_CONVERT_TO_NATIVE:
elements = getattr(speckle_object, "elements", []) or []
display = getattr(
speckle_object, "displayValue", getattr(speckle_object, "displayMesh", None)
)
if not elements and not display:
_report(f"Could not convert unsupported Speckle object: {speckle_object}")
return None
if isinstance(display, list):
elements.extend(display)
else:
elements.append(display)
# TODO: depreciate the parent type
# add parent type here so we can use it as a blender custom prop
# not making it hidden, so it will get added on send as i think it might be helpful? can reconsider
converted = []
for item in elements:
if not isinstance(item, Base):
continue
item.parent_speckle_type = speckle_object.speckle_type
blender_object = convert_to_native(item)
if isinstance(blender_object, list):
converted.extend(blender_object)
else:
_report(f"convert_instances_as = '{convert_instances_as}' is not implemented, Instances will be converted as collection instances!")
obj_data = instance_to_native_collection_instance(speckle_object, scale)
add_custom_properties(speckle_object, blender_object)
converted.append(blender_object)
return converted
try:
# convert breps
if speckle_type is Brep:
meshes = getattr(
speckle_object, "displayValue", getattr(speckle_object, "displayMesh", iter([]))
)
if material := getattr(speckle_object, "renderMaterial", getattr(speckle_object, "@renderMaterial", None),):
for mesh in meshes:
mesh["renderMaterial"] = material
return [convert_to_native(mesh) for mesh in meshes]
scale = 1.0
if units := getattr(speckle_object, "units", None):
scale = get_scale_length(units) / bpy.context.scene.unit_settings.scale_length
# convert supported geometry
if isinstance(speckle_object, Mesh):
obj_data = mesh_to_native(speckle_object, name=speckle_name, scale=scale)
elif speckle_type in SUPPORTED_CURVES:
obj_data = icurve_to_native(speckle_object, name=speckle_name, scale=scale)
elif isinstance(speckle_object, Transform):
obj_data = transform_to_native(speckle_object, scale=scale)
elif isinstance(speckle_object, BlockDefinition):
obj_data = block_def_to_native(speckle_object, scale=scale)
elif isinstance(speckle_object, BlockInstance): # speckle_type is BlockInstance:
obj_data = block_instance_to_native(speckle_object, scale=scale)
else:
_report(f"Unsupported type {speckle_type}")
return []
return None
except Exception as ex: # conversion error
_report(f"Error converting {speckle_object} \n{ex}")
return []
return None
if speckle_name in bpy.data.objects.keys():
blender_object = bpy.data.objects[speckle_name]
blender_object.data = (
obj_data.data if isinstance(obj_data, bpy_types.Object) else obj_data
)
blender_object.matrix_world = (
blender_object.matrix_world
if speckle_type is BlockInstance
else mathutils.Matrix()
)
if hasattr(obj_data, "materials"):
blender_object.data.materials.clear()
else:
blender_object = (
obj_data
if isinstance(obj_data, bpy_types.Object)
else bpy.data.objects.new(speckle_name, obj_data)
)
blender_object = obj_data if isinstance(obj_data, Object) else create_new_object(obj_data, object_name)
blender_object.speckle.object_id = str(speckle_object.id)
blender_object.speckle.enabled = True
add_custom_properties(speckle_object, blender_object)
add_blender_material(speckle_object, blender_object)
for child in converted:
child.parent = blender_object
converted.append(blender_object)
_report(f"Successfully converted {object_name} as {blender_object.type}")
return converted
return blender_object
DISPLAY_VALUE_PROPERTY_ALIASES = ["displayValue", "@displayValue", "displayMesh", "@displayMesh", "elements", "@elements"]
def mesh_to_native(speckle_mesh: Mesh, name: str, scale=1.0) -> bpy.types.Mesh:
def element_to_native(speckle_object: Base, name: str, scale: float, combineMeshes: bool = True) -> tuple[Optional[bpy.types.Mesh], list[bpy.types.Object]]:
"""
Converts a given speckle_object by converting displayValue properties (elements treated the same as displayValues)
if combineMeshes == True
Converts mesh displayValues as one mesh
Converts non-mesh displayValues as child Objects
if combineMeshes == False
Converts all displayValues as child objects (first item of the returned tuple will be None)
"""
meshes: list[Mesh] = []
elements: list[Base] = []
#NOTE: raw Mesh elements will be treated like displayValues, which is not ideal, but no connector sends raw Mesh elements so it's fine
for alias in DISPLAY_VALUE_PROPERTY_ALIASES:
display = getattr(speckle_object, alias, None)
count = 0
MAX_DEPTH = 255 # some large value, to prevent infinite reccursion
def seperate(value: Any) -> bool:
nonlocal meshes, elements, count, MAX_DEPTH
if combineMeshes and isinstance(value, Mesh):
meshes.append(value)
elif isinstance(value, Base):
elements.append(value)
elif isinstance(value, list):
count += 1
if(count > MAX_DEPTH):
return True
for x in value:
seperate(x)
return False
did_halt = seperate(display)
if did_halt:
_report(f"Traversal of {speckle_object.speckle_type} {speckle_object.id} halted after traversal depth exceeds MAX_DEPTH={MAX_DEPTH}. Are there circular references object structure?")
converted: list[Object] = []
mesh = None
if meshes:
mesh = meshes_to_native(speckle_object, meshes, name, scale)
for item in elements:
# add parent type here so we can use it as a blender custom prop
# not making it hidden, so it will get added on send as i think it might be helpful? can reconsider
item.parent_speckle_type = speckle_object.speckle_type #TODO: consider if this is still useful, as we now properly structure object parenting
blender_object = convert_to_native(item)
if isinstance(blender_object, list):
converted.extend(blender_object)
else:
add_custom_properties(speckle_object, blender_object)
converted.append(blender_object)
return (mesh, converted)
def mesh_to_native(speckle_mesh: Mesh, name: str, scale: float) -> bpy.types.Mesh:
return meshes_to_native(speckle_mesh, [speckle_mesh], name, scale)
def meshes_to_native(element: Base, meshes: Collection[Mesh], name: str, scale: float) -> bpy.types.Mesh:
if name in bpy.data.meshes.keys():
return bpy.data.meshes[name]
blender_mesh = bpy.data.meshes.new(name=name)
fallback_material = get_render_material(element)
blender_mesh = bpy.data.meshes[name]
else:
blender_mesh = bpy.data.meshes.new(name=name)
bm = bmesh.new()
# First pass, add vertex data
for mesh in meshes:
scale = get_scale_factor(mesh, scale)
add_vertices(mesh, bm, scale)
bm.verts.ensure_lookup_table()
# Second pass, add face data
offset = 0
for i, mesh in enumerate(meshes):
add_faces(mesh, bm, offset, i)
render_material = get_render_material(mesh) or fallback_material
if render_material is not None:
native_material = render_material_to_native(render_material)
blender_mesh.materials.append(native_material)
offset += len(mesh.vertices) // 3
bm.faces.ensure_lookup_table()
bm.verts.index_update()
# Third pass, add vertex instance data
for mesh in meshes:
add_colors(mesh, bm)
add_uv_coords(mesh, bm)
add_vertices(speckle_mesh, bm, scale)
add_faces(speckle_mesh, bm)
add_colors(speckle_mesh, bm)
add_uv_coords(speckle_mesh, bm)
bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
bm.to_mesh(blender_mesh)
bm.free()
bm.free()
return blender_mesh
"""
Curves
"""
def line_to_native(speckle_curve: Line, blender_curve: bpy.types.Curve, scale: float) -> list[bpy.types.Spline]:
if not speckle_curve.end: return []
def line_to_native(speckle_curve: Line, blender_curve: bpy.types.Curve, scale: float) -> Optional[bpy.types.Spline]:
line = blender_curve.splines.new("POLY")
line.points.add(1)
@@ -250,90 +166,95 @@ def line_to_native(speckle_curve: Line, blender_curve: bpy.types.Curve, scale: f
1,
)
line.points[1].co = (
float(speckle_curve.end.x) * scale,
float(speckle_curve.end.y) * scale,
float(speckle_curve.end.z) * scale,
1,
)
if speckle_curve.end:
return [line]
def polyline_to_native(scurve: Polyline, bcurve: bpy.types.Curve, scale: float) -> list[bpy.types.Spline]:
if not (value := scurve.value): return []
N = len(value) // 3
polyline = bcurve.splines.new("POLY")
if hasattr(scurve, "closed"):
polyline.use_cyclic_u = scurve.closed
polyline.points.add(N - 1)
for i in range(N):
polyline.points[i].co = (
float(value[i * 3]) * scale,
float(value[i * 3 + 1]) * scale,
float(value[i * 3 + 2]) * scale,
line.points[1].co = (
float(speckle_curve.end.x) * scale,
float(speckle_curve.end.y) * scale,
float(speckle_curve.end.z) * scale,
1,
)
return [polyline]
return line
def nurbs_to_native(scurve: Curve, bcurve: bpy.types.Curve, scale: float) -> list[bpy.types.Spline]:
if not (points := scurve.points): return []
def polyline_to_native(scurve: Polyline, bcurve: bpy.types.Curve, scale: float) -> Optional[bpy.types.Spline]:
if value := scurve.value:
N = len(value) // 3
# Closed curves from rhino will have n + degree points. We ignore the extras
num_points = len(points) // 3 - scurve.degree if (scurve.closed) else (
len(points) // 3)
nurbs = bcurve.splines.new("NURBS")
nurbs.use_cyclic_u = scurve.closed
nurbs.use_endpoint_u = not scurve.periodic
nurbs.points.add(num_points - 1)
use_weights = len(scurve.weights) >= num_points
for i in range(num_points):
nurbs.points[i].co = (
float(points[i * 3]) * scale,
float(points[i * 3 + 1]) * scale,
float(points[i * 3 + 2]) * scale,
1,
)
nurbs.points[i].weight = scurve.weights[i] if use_weights else 1
polyline = bcurve.splines.new("POLY")
nurbs.order_u = scurve.degree + 1
if hasattr(scurve, "closed"):
polyline.use_cyclic_u = scurve.closed
return [nurbs]
# if "closed" in scurve.keys():
# polyline.use_cyclic_u = scurve["closed"]
polyline.points.add(N - 1)
for i in range(N):
polyline.points[i].co = (
float(value[i * 3]) * scale,
float(value[i * 3 + 1]) * scale,
float(value[i * 3 + 2]) * scale,
1,
)
return polyline
def nurbs_to_native(scurve: Curve, bcurve: bpy.types.Curve, scale: float) -> Optional[bpy.types.Spline]:
if points := scurve.points:
N = len(points) // 3
nurbs = bcurve.splines.new("NURBS")
if hasattr(scurve, "closed"):
nurbs.use_cyclic_u = scurve.closed != 0
nurbs.points.add(N - 1)
for i in range(N):
nurbs.points[i].co = (
float(points[i * 3]) * scale,
float(points[i * 3 + 1]) * scale,
float(points[i * 3 + 2]) * scale,
1,
)
if len(scurve.weights) == len(nurbs.points):
for i, w in enumerate(scurve.weights):
nurbs.points[i].weight = w
# TODO: anaylize curve knots to decide if use_endpoint_u or use_bezier_u should be enabled
# nurbs.use_endpoint_u = True
nurbs.order_u = scurve.degree + 1
return nurbs
def arc_to_native(rcurve: Arc, bcurve: bpy.types.Curve, scale: float) -> Optional[bpy.types.Spline]:
# TODO: improve Blender representation of arc - check autocad test stream
# TODO: improve Blender representation of arc
plane = rcurve.plane
if not plane:
return None
normal = MVector([plane.normal.x, plane.normal.y, plane.normal.z])
normal = mathutils.Vector([plane.normal.x, plane.normal.y, plane.normal.z])
radius = rcurve.radius * scale
startAngle = rcurve.startAngle
endAngle = rcurve.endAngle
startQuat = MQuaternion(normal, startAngle)
endQuat = MQuaternion(normal, endAngle)
startQuat = mathutils.Quaternion(normal, startAngle)
endQuat = mathutils.Quaternion(normal, endAngle)
# Get start and end vectors, centre point, angles, etc.
r1 = MVector([plane.xdir.x, plane.xdir.y, plane.xdir.z])
r1 = mathutils.Vector([plane.xdir.x, plane.xdir.y, plane.xdir.z])
r1.rotate(startQuat)
r2 = MVector([plane.xdir.x, plane.xdir.y, plane.xdir.z])
r2 = mathutils.Vector([plane.xdir.x, plane.xdir.y, plane.xdir.z])
r2.rotate(endQuat)
c = MVector([plane.origin.x, plane.origin.y, plane.origin.z]) * scale
c = mathutils.Vector([plane.origin.x, plane.origin.y, plane.origin.z]) * scale
spt = c + r1 * radius
ept = c + r2 * radius
@@ -349,7 +270,7 @@ def arc_to_native(rcurve: Arc, bcurve: bpy.types.Curve, scale: float) -> Optiona
Ndiv = max(int(math.floor(angle / 0.3)), 2)
step = angle / float(Ndiv)
stepQuat = MQuaternion(normal, step)
stepQuat = mathutils.Quaternion(normal, step)
tan = math.tan(step / 2) * radius
arc.points.add(Ndiv + 1)
@@ -372,7 +293,7 @@ def arc_to_native(rcurve: Arc, bcurve: bpy.types.Curve, scale: float) -> Optiona
return arc
def polycurve_to_native(scurve: Polycurve, bcurve: bpy.types.Curve, scale: float) -> list[bpy.types.Spline]:
def polycurve_to_native(scurve: Polycurve, bcurve: bpy.types.Curve, scale: float):
"""
Convert Polycurve object
"""
@@ -384,110 +305,48 @@ def polycurve_to_native(scurve: Polycurve, bcurve: bpy.types.Curve, scale: float
speckle_type = type(seg)
if speckle_type in SUPPORTED_CURVES:
curves.append(icurve_to_native_spline(seg, bcurve, scale))
curves.append(icurve_to_native_spline(seg, bcurve, scale=scale))
else:
_report(f"Unsupported curve type: {speckle_type}")
return curves
def circle_to_native(circle: Circle, bcurve: bpy.types.Curve, units_scale: float) -> list[bpy.types.Spline]:
#HACK: violates typing, but it works...
circle["firstRadius"] = circle.radius
circle["secondRadius"] = circle.radius
return ellipse_to_native(circle, bcurve, units_scale)
def ellipse_to_native(ellipse: Ellipse, bcurve: bpy.types.Curve, units_scale: float) -> list[bpy.types.Spline]:
plane = ellipse.plane
radX = ellipse.firstRadius * units_scale
radY = ellipse.secondRadius * units_scale
D = 0.5522847498307936 # (4/3)*tan(pi/8)
right_handles = [
(+radX, +radY * D, 0.0),
(-radX * D, +radY, 0.0),
(-radX, -radY * D, 0.0),
(+radX * D, -radY, 0.0),
]
left_handles = [
(+radX, -radY * D, 0.0),
(+radX * D, +radY, 0.0),
(-radX, +radY * D, 0.0),
(-radX * D, -radY, 0.0),
]
points = [
(+radX, 0.0, 0.0),
(0.0, +radY, 0.0),
(-radX, 0.0, 0.0),
(0.0, -radY, 0.0),
]
transform = plane_to_native_transform(plane, units_scale)
spline = bcurve.splines.new("BEZIER")
spline.bezier_points.add(len(points) - 1)
for i in range(len(points)):
spline.bezier_points[i].co = transform @ MVector(points[i])
spline.bezier_points[i].handle_left = transform @ MVector(left_handles[i])
spline.bezier_points[i].handle_right = transform @ MVector(right_handles[i])
spline.use_cyclic_u = True
#TODO support trims?
return [spline]
def icurve_to_native_spline(speckle_curve: Base, blender_curve: bpy.types.Curve, scale: float) -> list[bpy.types.Spline]:
# polycurves
if isinstance(speckle_curve, Polycurve):
def icurve_to_native_spline(speckle_curve: Base, blender_curve: bpy.types.Curve, scale=1.0):
curve_type = type(speckle_curve)
if curve_type is Line:
return line_to_native(speckle_curve, blender_curve, scale)
if curve_type is Polyline:
return polyline_to_native(speckle_curve, blender_curve, scale)
if curve_type is Curve:
return nurbs_to_native(speckle_curve, blender_curve, scale)
if curve_type is Polycurve:
return polycurve_to_native(speckle_curve, blender_curve, scale)
# single curves
if isinstance(speckle_curve, Line):
spline = line_to_native(speckle_curve, blender_curve, scale)
elif isinstance(speckle_curve, Curve):
spline = nurbs_to_native(speckle_curve, blender_curve, scale)
elif isinstance(speckle_curve, Polyline):
spline = polyline_to_native(speckle_curve, blender_curve, scale)
elif isinstance(speckle_curve, Arc):
spline = arc_to_native(speckle_curve, blender_curve, scale)
elif isinstance(speckle_curve, Ellipse):
spline = ellipse_to_native(speckle_curve, blender_curve, scale)
elif isinstance(speckle_curve, Circle):
spline = circle_to_native(speckle_curve, blender_curve, scale)
else:
raise TypeError(f"{speckle_curve} is not a supported curve type. Supported types: {SUPPORTED_CURVES}")
return [spline] if spline is not None else []
if curve_type is Arc:
return arc_to_native(speckle_curve, blender_curve, scale)
def icurve_to_native(speckle_curve: Base, name: str, scale: float) -> Optional[bpy.types.Curve]:
def icurve_to_native(speckle_curve: Base, name=None, scale=1.0) -> Optional[Curve]:
curve_type = type(speckle_curve)
if curve_type not in SUPPORTED_CURVES:
_report(f"Unsupported curve type: {curve_type}")
return None
name = name or f"{curve_type} -- {speckle_curve.id}"
blender_curve = (
bpy.data.curves[name]
if name in bpy.data.curves.keys()
else bpy.data.curves.new(name, type="CURVE")
)
blender_curve.dimensions = "3D"
blender_curve.resolution_u = 12 #TODO: We could maybe decern the resolution from the ployline displayValue
blender_curve.resolution_u = 12
icurve_to_native_spline(speckle_curve, blender_curve, scale)
return blender_curve
"""
Transforms and Intances
"""
def transform_to_native(transform: Transform, scale: float) -> MMatrix:
mat = MMatrix(
def transform_to_native(transform: Transform, scale=1.0) -> mathutils.Matrix:
mat = mathutils.Matrix(
[
transform.value[:4],
transform.value[4:8],
@@ -500,166 +359,38 @@ def transform_to_native(transform: Transform, scale: float) -> MMatrix:
mat[i][3] *= scale
return mat
def plane_to_native_transform(plane: Plane, fallback_scale:float = 1) -> MMatrix:
scale_factor = get_scale_factor(plane, fallback_scale)
tx = (plane.origin.x * scale_factor)
ty = (plane.origin.y * scale_factor)
tz = (plane.origin.z * scale_factor)
return MMatrix((
(plane.xdir.x, plane.ydir.x, plane.normal.x, tx),
(plane.xdir.y, plane.ydir.y, plane.normal.y, ty),
(plane.xdir.z, plane.ydir.z, plane.normal.z, tz),
(0, 0, 0, 1 )
))
"""
Instances / Blocks
"""
def _get_instance_name(instance: Instance) -> str:
name_prefix = _get_friendly_object_name(instance) or _get_friendly_object_name(instance.definition) or _simplified_speckle_type(instance.speckle_type)
return f"{name_prefix}{OBJECT_NAME_SEPERATOR}{instance.id}"
def instance_to_native_object(instance: Instance, scale: float) -> Tuple[bpy.types.Object, List[bpy.types.Object]]:
"""
Converts Instance to a unique object with (potentially) shared data (linked duplicate)
"""
if not instance.definition: raise Exception(f"Instance is missing a definition")
if not instance.transform: raise Exception(f"Instance is missing a transform")
name = _get_instance_name(instance)
definition = instance.definition
native_instance: Object
native_elements: List[Object] = []
elements_on_instance: List[Object] = []
if isinstance(definition, BlockDefinition): #NOTE: We have to handle BlockDefinitions specially here, since they don't follow normal traversal rules
native_instance = create_new_object(None, name) #Instance will be empty
native_instance.empty_display_size = 0
for geo in definition.geometry:
native_elements.append(convert_to_native(geo)[-1])
else:
native_instance = convert_to_native(instance.definition)[-1] # Convert assuming that definition is convertable
instance_transform = transform_to_native(instance.transform, scale)
instance_transform_inverted = instance_transform.inverted()
native_instance.matrix_world = instance_transform
(_, elements_on_instance) = element_to_native(instance, name, scale)
for c in elements_on_instance:
c.matrix_world = instance_transform_inverted @ c.matrix_world #Undo the instance transform on elements
native_elements.extend(elements_on_instance)
return (native_instance, native_elements) #TODO: need to double check that all child objects have custom props attached correctly
def instance_to_native_collection_instance(instance: Instance, scale: float) -> bpy.types.Object:
"""
Convert an Instance as a transformed Object with the `instance_collection` property
set to be the `instance.Definition` converted as a collection
The definition collection won't be linked to the current scene
Any Elements on the instance object will also be converted (and spacially transformed)
"""
if not instance.definition: raise Exception(f"Instance is missing a definition")
if not instance.transform: raise Exception(f"Instance is missing a transform")
name = _get_instance_name(instance)
# Get/Convert definition collection
collection_def = _instance_definition_to_native(instance.definition)
# Convert elements as children of collection instance object
(_, elements) = element_to_native(instance, name, scale, False)
instance_transform = transform_to_native(instance.transform, scale)
instance_transform_inverted = instance_transform.inverted()
native_instance = bpy.data.objects.new(name, None)
#add_custom_properties(instance, native_instance)
# hide the instance axes so they don't clutter the viewport
native_instance.empty_display_size = 0
native_instance.instance_collection = collection_def
native_instance.instance_type = "COLLECTION"
native_instance.matrix_world =instance_transform
for c in elements:
c.matrix_world = instance_transform_inverted @ c.matrix_world #Undo the instance transform on elements
c.parent = native_instance #TODO: need to double check that all child objects have custom props attached correctly
return native_instance
def _instance_definition_to_native(definition: Union[Base, BlockDefinition]) -> bpy.types.Collection:
"""
Converts a geometry carrying Base as a collection (does not link it to the scene)
"""
name = _generate_object_name(definition)
native_def = bpy.data.collections.get(name)
def block_def_to_native(definition: BlockDefinition, scale=1.0) -> bpy.types.Collection:
native_def = bpy.data.collections.get(definition.name)
if native_def:
return native_def
native_def = bpy.data.collections.new(name)
native_def = bpy.data.collections.new(definition.name)
native_def["applicationId"] = definition.applicationId
#TODO could maybe replace BlockDefinition awareness with a single traverse member call
geometry = definition.geometry if isinstance(definition, BlockDefinition) else [definition]
for geo in geometry:
if not geo: continue
converted = convert_to_native(geo)[-1] #NOTE: we assume the last item is the root converted item
link_object_to_collection_nested(converted, native_def)
for geo in definition.geometry:
if b_obj := convert_to_native(geo):
native_def.objects.link(
b_obj
if isinstance(b_obj, bpy_types.Object)
else bpy.data.objects.new(b_obj.name, b_obj)
)
return native_def
"""
Object Naming
"""
def block_instance_to_native(instance: BlockInstance, scale=1.0) -> bpy.types.Object:
"""
Convert BlockInstance to native
"""
name = f"{getattr(instance, 'name', None) or instance.blockDefinition.name} -- {instance.id}"
native_def = block_def_to_native(instance.blockDefinition, scale)
def _get_friendly_object_name(speckle_object: Base) -> Optional[str]:
return (getattr(speckle_object, "name", None)
or getattr(speckle_object, "Name", None)
or getattr(speckle_object, "family", None)
)
# Blender object names must not exceed 62 characters
# We need to ensure the complete ID is included in the name (to prevent identity collisions)
# So we if the name is too long, we need to truncate
OBJECT_NAME_MAX_LENGTH = 62
SPECKLE_ID_LENGTH = 32
OBJECT_NAME_SEPERATOR = " -- "
def _truncate_object_name(name: str) -> str:
MAX_NAME_LENGTH = OBJECT_NAME_MAX_LENGTH - SPECKLE_ID_LENGTH - len(OBJECT_NAME_SEPERATOR)
return name[:MAX_NAME_LENGTH]
def _simplified_speckle_type(speckle_type: str) -> str:
return(speckle_type.rsplit('.')[-1]) #Take only the most specific object type name (without namespace)
def _generate_object_name(speckle_object: Base) -> str:
prefix: str
name = _get_friendly_object_name(speckle_object)
if name:
prefix = _truncate_object_name(name)
else:
prefix = _simplified_speckle_type(speckle_object.speckle_type)
return f"{prefix}{OBJECT_NAME_SEPERATOR}{speckle_object.id}"
def get_scale_factor(speckle_object: Base, fallback: float = 1.0) -> float:
scale = fallback
if units := getattr(speckle_object, "units", None):
scale = get_scale_length(units) / bpy.context.scene.unit_settings.scale_length
return scale
native_instance = bpy.data.objects.new(name, None)
add_custom_properties(instance, native_instance)
native_instance["name"] = getattr(instance, 'name', None) or instance.blockDefinition.name
# hide the instance axes so they don't clutter the viewport
native_instance.empty_display_size = 0
native_instance.instance_collection = native_def
native_instance.instance_type = "COLLECTION"
native_instance.matrix_world = transform_to_native(instance.transform, scale)
return native_instance
+83 -188
View File
@@ -1,21 +1,12 @@
from typing import Dict, Iterable, Optional, Tuple
from typing import Optional
import bpy
from bpy.types import Depsgraph, MeshPolygon, Object
from deprecated import deprecated
from mathutils.geometry import interpolate_bezier
from mathutils import (
Matrix as MMatrix,
Vector as MVector,
)
from specklepy.objects.geometry import (
Mesh, Curve, Interval, Box, Point, Polyline
)
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 (
get_blender_custom_properties,
make_knots,
nurb_make_curve,
to_argb_int,
)
@@ -24,20 +15,17 @@ UNITS = "m"
CAN_CONVERT_TO_SPECKLE = ("MESH", "CURVE", "EMPTY")
def convert_to_speckle(raw_blender_object: Object, scale: float, units: str, depsgraph: Optional[Depsgraph]) -> Optional[list]:
def convert_to_speckle(blender_object: Object, scale: float, units: str, desgraph: Optional[Depsgraph]) -> Optional[list]:
global UNITS
UNITS = units
blender_type = raw_blender_object.type
blender_type = blender_object.type
if blender_type not in CAN_CONVERT_TO_SPECKLE:
return None
blender_object: Object = (
raw_blender_object.evaluated_get(depsgraph)
if depsgraph
else raw_blender_object
)
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, blender_object.data, scale)
@@ -48,93 +36,75 @@ def convert_to_speckle(raw_blender_object: Object, scale: float, units: str, dep
if not converted:
return None
speckle_objects = []
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(raw_blender_object) #NOTE: Depsgraph copies don't have custom properties so we use the raw version
so["applicationId"] = so.properties.pop("applicationId", None)
so.properties = get_blender_custom_properties(blender_object)
so.applicationId = so.properties.pop("applicationId", None)
if speckle_material:
so["renderMaterial"] = speckle_material
# Set object transform
if blender_type != "EMPTY": #TODO: this could be deprecated once we add proper instancing support
so["properties"]["transform"] = transform_to_speckle(
if blender_type != "EMPTY":
so.properties["transform"] = transform_to_speckle(
blender_object.matrix_world
)
return speckle_objects
def mesh_to_speckle(blender_object: Object, data: bpy.types.Mesh, scale: float = 1.0) -> List[Mesh]:
#if data.loop_triangles is None or len(data.loop_triangles) < 1:
# data.calc_loop_triangles()
# 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 = blender_object.matrix_world
scaled_vertices = [tuple(transform @ x.co * scale) for x in data.vertices]
# Create Speckle meshes for each material
submeshes = []
index_counter = 0
for i in submesh_data:
index_mapping: Dict[int, int] = {}
#Loop through each polygon, and map indicies 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]
m_texcoords.extend([vt.uv.x, vt.uv.y])
m_faces.append(index_mapping[u_index])
index_counter += 1
speckle_mesh = Mesh(
vertices=m_verts,
faces=m_faces,
colors=[],
textureCoordinates=m_texcoords,
units=UNITS,
area = mesh_area,
bbox=Box(area=0.0, volume=0.0),
)
if i < len(data.materials):
material = data.materials[i]
if material is not None:
speckle_mesh["renderMaterial"] = material_to_speckle(material)
submeshes.append(speckle_mesh)
return submeshes
def 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()
def bezier_to_speckle(matrix: MMatrix, spline: bpy.types.Spline, scale: float, name: Optional[str] = None) -> Curve:
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
@@ -172,121 +142,50 @@ def bezier_to_speckle(matrix: MMatrix, spline: bpy.types.Spline, scale: float, n
name=name,
degree=degree,
closed=spline.use_cyclic_u,
periodic= not spline.use_endpoint_u,
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,
bbox=Box(area=0.0, volume=0.0),
displayValue = bezier_to_speckle_polyline(matrix, spline, scale, length),
)
def nurbs_to_speckle(matrix: MMatrix, spline: bpy.types.Spline, scale: float, 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 * scale) for pt in spline.points]
flattend_points = []
for row in points: flattend_points.extend(row)
if spline.use_cyclic_u:
for i in range(0, degree * 3, 3):
# Rhino expects n + degree number of points (for closed curves). So we need to add an extra point for each degree
flattend_points.append(flattend_points[i + 0])
flattend_points.append(flattend_points[i + 1])
flattend_points.append(flattend_points[i + 2])
for i in range(0, degree):
weights.append(weights[i])
return Curve(
name=name,
degree=degree,
closed=spline.use_cyclic_u,
periodic= not spline.use_endpoint_u,
periodic=spline.use_cyclic_u,
points=flattend_points,
weights=weights,
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,
bbox=Box(area=0.0, volume=0.0),
displayValue=nurbs_to_speckle_polyline(matrix, spline, scale, length),
)
def nurbs_to_speckle_polyline(matrix: MMatrix, spline: bpy.types.Spline, scale: float, length: Optional[float] = None) -> Polyline:
"""
Samples a nurbs curve with resolution_u creating a polyline
"""
points = []
sampled_points = nurb_make_curve(spline, spline.resolution_u, 3)
for i in range(0, len(sampled_points), 3):
scaled_point = matrix @ MVector((
sampled_points[i + 0],
sampled_points[i + 1],
sampled_points[i + 2])) * scale
points.append(scaled_point.x)
points.append(scaled_point.y)
points.append(scaled_point.z)
length = length or spline.calc_length()
domain = Interval(start=0, end=length, totalChildrenCount=0)
return Polyline(value=points, closed = spline.use_cyclic_u, domain=domain, area=0, len=length)
#Inspired by https://blender.stackexchange.com/a/689 (CC BY-SA 3.0)
def bezier_to_speckle_polyline(matrix: MMatrix, spline: bpy.types.Spline, scale: float, 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 * scale
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: MMatrix, spline: bpy.types.Spline, scale: float, name: Optional[str] = None) -> Polyline:
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]
flattend_points = []
@@ -334,7 +233,7 @@ def icurve_to_speckle(blender_object: Object, data: bpy.types.Curve, scale=1.0)
return curves
@deprecated
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"
@@ -354,7 +253,7 @@ def ngons_to_speckle_polylines(blender_object: Object, data: bpy.types.Mesh, sca
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),
@@ -367,7 +266,15 @@ def ngons_to_speckle_polylines(blender_object: Object, data: bpy.types.Mesh, sca
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
@@ -389,19 +296,7 @@ def material_to_speckle(blender_mat: bpy.types.Material) -> RenderMaterial:
return speckle_mat
def material_to_speckle_old(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
return material_to_speckle(blender_mat)
def transform_to_speckle(blender_transform: Iterable[Iterable[float]], scale=1.0) -> Transform:
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):
+67 -185
View File
@@ -1,13 +1,13 @@
import math
from typing import Any, Optional, Tuple
from typing import Tuple
from bmesh.types import BMesh
import bpy, struct, idprop
from specklepy.objects.base import Base
from specklepy.objects.geometry import Circle, Mesh, Ellipse
from specklepy.objects.other import RenderMaterial
from specklepy.objects.geometry import Mesh
from specklepy.serialization.base_object_serializer import BaseObjectSerializer
from bpy_speckle.functions import _report
from bpy.types import Material, Object
from bpy.types import Object
IGNORED_PROPERTY_KEYS = {
"id",
@@ -25,7 +25,7 @@ IGNORED_PROPERTY_KEYS = {
}
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
@@ -35,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)
@@ -68,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
@@ -103,24 +105,10 @@ def render_material_to_native(speckle_mat: RenderMaterial) -> Material:
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
def get_render_material(speckle_object: Base) -> Optional[RenderMaterial]:
"""Trys to get a RenderMaterial on given speckle_object and convert it to a blender material"""
speckle_mat = getattr(
speckle_object,
"renderMaterial",
getattr(speckle_object, "@renderMaterial", None),
)
if not isinstance(speckle_mat, RenderMaterial):
return None
return speckle_mat
blender_object.data.materials.append(blender_mat)
def add_vertices(speckle_mesh: Mesh, blender_mesh: BMesh, scale=1.0):
@@ -136,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):
@@ -151,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]]
[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):
@@ -205,7 +196,7 @@ def add_uv_coords(speckle_mesh: Mesh, blender_mesh: BMesh):
)
else:
_report(
f"Failed to match UV coordinates to vert data. Blender mesh verts: {len(blender_mesh.verts)}, Speckle UVs: {len(s_uvs) // 2}"
f"Failed to match UV coordinates to vert data. Blender mesh verts: {len(blender_mesh.verts)}, Speckle UVs * 2: {len(s_uvs) * 2}"
)
return
@@ -235,7 +226,7 @@ ignored_keys = {
"_chunkable",
}
def get_blender_custom_properties(obj, max_depth: int = 1000):
def get_blender_custom_properties(obj, max_depth=1000):
if max_depth < 0:
return obj
@@ -252,37 +243,41 @@ def get_blender_custom_properties(obj, max_depth: int = 1000):
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):
@@ -295,137 +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
def makecyclicknots(knots, point_count, order):
order2 = order - 1
# this is for float inaccuracy
if t < knots[0]:
t = knots[0]
elif t > knots[opp2]:
t = knots[opp2]
if order > 2:
b = point_count + order2
for a in range(1, order2):
if knots[b] != knots[b - a]:
break
# 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
if a == order2:
knots[point_count + order - 2] += 1.0
else:
basis[i] = 0.0
basis[i] = 0.0
# 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]
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
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: bpy.types.Object, col: bpy.types.Collection):
if obj.name not in col.objects:
col.objects.link(obj)
for child in obj.children:
link_object_to_collection_nested(child, col)
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
+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()
+1 -2
View File
@@ -1,4 +1,4 @@
from .users import LoadUsers, LoadUserStreams, ResetUsers
from .users import LoadUsers, LoadUserStreams
from .object import (
UpdateObject,
ResetObject,
@@ -27,7 +27,6 @@ from .misc import OpenSpeckleGuide, OpenSpeckleTutorials, OpenSpeckleForum
operator_classes = [
LoadUsers,
ResetUsers,
ReceiveStreamObjects,
SendStreamObjects,
LoadUserStreams,
+12 -26
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, _report
from bpy_speckle.functions import _check_speckle_client_user_stream
from bpy_speckle.clients import speckle_clients
from bpy_speckle.properties.scene import SpeckleSceneSettings
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"
@@ -39,39 +37,27 @@ class DeleteCommit(bpy.types.Operator):
def execute(self, context):
if not self.are_you_sure:
_report(f"{self.bl_idname}: cancelled by user")
return {"CANCELLED"}
self.are_you_sure = False
speckle: SpeckleSceneSettings = context.scene.speckle
speckle = context.scene.speckle
user = speckle.get_active_user()
if user is None:
print(f"{self.bl_idname}: failed - No user selected/found")
check = _check_speckle_client_user_stream(context.scene)
if check is None:
return {"CANCELLED"}
stream = user.get_active_stream()
if stream is None:
print(f"{self.bl_idname}: failed - No stream selected/found")
return {"CANCELLED"}
user, stream = check
client = speckle_clients[int(context.scene.speckle.active_user)]
branch = stream.get_active_branch()
if branch is None:
print(f"{self.bl_idname}: failed - No branch selected/found")
stream = user.streams[user.active_stream]
if len(stream.branches) < 1:
return {"CANCELLED"}
commit = branch.get_active_commit()
if commit is None:
print(f"{self.bl_idname}: failed - No commit selected/found")
branch = stream.branches[int(stream.branch)]
if len(branch.commits) < 1:
return {"CANCELLED"}
client = speckle_clients[int(speckle.active_user)]
commit = branch.commits[int(branch.commit)]
deleted = client.commit.delete(stream_id=stream.id, commit_id=commit.id)
if not deleted:
print(f"{self.bl_idname}: failed - Delete operation failed")
return {"CANCELLED"}
print(f"{self.bl_idname}: succeeded - commit {commit.id} ({commit.message}) has been deleted from stream {stream.id}")
return {"FINISHED"}
+2 -2
View File
@@ -4,7 +4,6 @@ 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,
@@ -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)
@@ -127,7 +127,7 @@ class DeleteObject(bpy.types.Operator):
return {"FINISHED"}
@deprecated
class UploadNgonsAsPolylines(bpy.types.Operator):
"""
Upload mesh ngon faces as polyline outlines
+61 -92
View File
@@ -3,23 +3,16 @@ Stream operators
"""
from itertools import chain
from math import radians
from typing import Callable, Dict, Iterable, Optional
from typing import Callable, Dict, Iterable
import bpy
from bpy_speckle.convert.util import link_object_to_collection_nested
from bpy_speckle.properties.scene import SpeckleSceneSettings
from specklepy.api.models import Commit
import webbrowser
from bpy.props import (
StringProperty,
BoolProperty,
EnumProperty,
)
from bpy.types import Context, Object
from bpy_speckle.convert.to_native import (
can_convert_to_native,
convert_to_native,
set_convert_instances_as,
)
from bpy_speckle.convert.to_native import can_convert_to_native, convert_to_native
from bpy_speckle.convert.to_speckle import (
convert_to_speckle,
)
@@ -28,6 +21,7 @@ from bpy_speckle.functions import (
get_scale_length,
_report,
)
from bpy_speckle.convert import get_speckle_subobjects
from bpy_speckle.clients import speckle_clients
from bpy_speckle.operators.users import add_user_stream
@@ -58,8 +52,6 @@ def get_objects_collections(base: Base) -> Dict[str, list]:
def get_objects_nested_lists(items: list, parent_col: Optional[bpy.types.Collection] = None) -> List:
"""For handling the weird nested lists that come from Grasshopper"""
objects = []
if not items:
return objects
if isinstance(items[0], list):
items = list(chain.from_iterable(items))
@@ -85,6 +77,8 @@ def get_objects_collections_recursive(base: Base, parent_col: Optional[bpy.types
for name in base.get_dynamic_member_names():
value = base[name]
if name == "parameters" and "Revit" in base.speckle_type:
continue
if isinstance(value, list):
objects.extend(item for item in value if isinstance(item, Base))
if isinstance(value, Base):
@@ -131,7 +125,7 @@ def get_receive_funcs(context: Context, created_objects: Dict[str, Object]) -> t
nonlocal created_objects
nonlocal objectCallback
progress += 1 #NOTE:XXX Progress bar never reaches 100 because func is only called for convertible objects
progress += 1 #TODO: Progress bar never reaches 100 because func is only called for convertible objects
context.window_manager.progress_update(progress)
created_objects[obj.name] = obj
@@ -152,7 +146,7 @@ def bases_to_native(context: bpy.types.Context, collections: Dict[str, list], sc
for obj in objects:
if isinstance(obj, dict):
bases_to_native(context, obj, scale, stream_id, func)
elif isinstance(obj, list): #FIXME: wtf are these nested if statement, can this not be a recursive call?
elif isinstance(obj, list):
for item in obj:
if isinstance(item, dict):
bases_to_native(context, item, scale, stream_id, func)
@@ -165,7 +159,7 @@ def bases_to_native(context: bpy.types.Context, collections: Dict[str, list], sc
else:
_report(
f"Something went wrong when receiving collection: {col_name}" #FIXME: undescript report message
f"Something went wrong when receiving collection: {col_name}"
)
bpy.context.view_layer.update()
@@ -183,17 +177,17 @@ def base_to_native(context: bpy.types.Context,
existing: Dict[str, Object],
func: ObjectCallback = None
):
new_objects = convert_to_native(base)
if not isinstance(new_objects, list):
new_objects = [new_objects]
if hasattr(base, "properties") and base.properties is not None:
new_objects.extend(get_speckle_subobjects(base.properties, scale, base.id))
elif isinstance(base, dict) and "properties" in base.keys():
new_objects.extend(
get_speckle_subobjects(base["properties"], scale, base["id"])
)
#NOTE: this code is ancient, and in testing does nothing, so we are removing it.
# if hasattr(base, "properties") and base.properties is not None:
# new_objects.extend(get_speckle_subobjects(base.properties, scale, base.id))
# elif isinstance(base, dict) and "properties" in base.keys():
# new_objects.extend(
# get_speckle_subobjects(base["properties"], scale, base["id"])
# )
"""
Set object Speckle settings
"""
@@ -205,11 +199,11 @@ def base_to_native(context: bpy.types.Context,
Run injected function
"""
if func:
new_object = func(context, new_object, base) #this base object isn't always the right one for hosted elements! #TODO: may be it now, need to double check!
new_object = func(context, new_object, base) #this base object isn't the right one for hosted elements!
if (
new_object is None
): # If the injected function returned None, then we should ignore this object.
): # Make sure that the injected function returned an object
_report(f"Script '{func.__module__}' returned None.")
continue
@@ -222,9 +216,8 @@ def base_to_native(context: bpy.types.Context,
new_object.name = name
col.objects.unlink(existing[new_object.speckle.object_id])
link_object_to_collection_nested(new_object, col)
#if new_object.name not in col.objects:
#col.objects.link(new_object)
if new_object.name not in col.objects:
col.objects.link(new_object)
def create_collection(name: str, clear_collection=True) -> bpy.types.Collection:
@@ -251,7 +244,7 @@ def get_existing_collection_objs(col: bpy.types.Collection) -> Dict[str, bpy.typ
}
def get_collection_parents(collection: bpy.types.Collection, names: list[str]) -> None:
def get_collection_parents(collection, names):
for parent in bpy.data.collections:
if collection.name in parent.children.keys():
# TODO: this should be rethought to make it clear when this is an IFC delim so we know to replace it
@@ -260,7 +253,7 @@ def get_collection_parents(collection: bpy.types.Collection, names: list[str]) -
get_collection_parents(parent, names)
def get_collection_hierarchy(collection: Optional[bpy.types.Collection]) -> list[str]:
def get_collection_hierarchy(collection):
if not collection:
return []
names = [collection.name.replace("/", "::").replace(".", "::")]
@@ -269,7 +262,7 @@ def get_collection_hierarchy(collection: Optional[bpy.types.Collection]) -> list
return names
def create_nested_hierarchy(base: Base, hierarchy: List[str], objects: Any):
def create_nested_hierarchy(base, hierarchy, objects):
child = base
while hierarchy:
@@ -279,23 +272,13 @@ def create_nested_hierarchy(base: Base, hierarchy: List[str], objects: Any):
child.add_detachable_attrs({name})
child = child[name]
if not hasattr(child, "@elements"):
child["@elements"] = []
child["@elements"].extend(objects)
# TODO: what do we call this attribute?
if not hasattr(child, "@objects"):
child["@objects"] = []
child["@objects"].extend(objects)
return base
#RECEIVE_MODES = [#TODO: modes
# ("create", "Create", "Add new geometry, without removing any existing objects"),
# ("replace", "Replace", "Replace objects from previous receive operations from the same stream"),
# #("update","Update", "") #TODO: update mode!
#]
INSTANCES_SETTINGS = [
("collection_instance", "Collection Instace", "Receive Instances as Collection Instances"),
("linked_duplicates", "Linked Duplicates", "Receive Instances as Linked Duplicates"),
]
class ReceiveStreamObjects(bpy.types.Operator):
"""
Receive stream objects
@@ -309,16 +292,10 @@ class ReceiveStreamObjects(bpy.types.Operator):
clean_meshes: BoolProperty(name="Clean Meshes", default=False)
#receive_mode: EnumProperty(items=RECEIVE_MODES, name="Receive Type", default="replace", description="The behaviour of the recieve operation")
receive_instances_as: EnumProperty(items=INSTANCES_SETTINGS, name="Receive Instances As", default="collection_instance", description="How to receive speckle Instances")
def draw(self, context):
layout = self.layout
col = layout.column()
col.prop(self, "clean_meshes")
#col.prop(self, "receive_mode")
col.prop(self, "receive_instances_as")
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self)
@@ -343,6 +320,7 @@ class ReceiveStreamObjects(bpy.types.Operator):
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action='SELECT')
bpy.ops.mesh.remove_doubles()
bpy.ops.mesh.dissolve_limited(angle_limit=radians(0.1))
# Reset state to previous (not quite sure if this is 100% necessary)
@@ -353,31 +331,30 @@ class ReceiveStreamObjects(bpy.types.Operator):
def execute(self, context):
bpy.context.view_layer.objects.active = None
speckle: SpeckleSceneSettings = context.scene.speckle
#Get UI Selection
user = speckle.get_active_user()
if not user:
print("No user selected/found")
check = _check_speckle_client_user_stream(context.scene)
if check is None:
return {"CANCELLED"}
stream = user.get_active_stream()
if not stream:
print("No stream selected/found")
user, bstream = check
client = speckle_clients[int(context.scene.speckle.active_user)]
stream = client.stream.get(id=bstream.id, branch_limit=20)
if stream.branches.totalCount < 1:
return {"CANCELLED"}
branch = stream.get_active_branch()
if not branch:
print("No branch selected/found")
if not stream.branches:
return {"CANCELLED"}
commit = branch.get_active_commit()
if commit is None:
print("No commit selected/found")
branch = stream.branches.items[int(bstream.branch)]
bbranch = bstream.branches[int(bstream.branch)]
if branch.commits.totalCount < 1:
_report("No commits found. Probably an empty stream.")
return {"CANCELLED"}
#Get actual stream data
client = speckle_clients[int(speckle.active_user)]
commit: Commit = branch.commits.items[int(bbranch.commit)]
transport = ServerTransport(stream.id, client)
@@ -385,41 +362,33 @@ class ReceiveStreamObjects(bpy.types.Operator):
metrics.RECEIVE,
getattr(transport, "account", None),
custom_props={
"sourceHostApp": host_applications.get_host_app_from_string(commit.source_application).slug,
"sourceHostAppVersion": commit.source_application,
"isMultiplayer": commit.author_id != user.id,
"sourceHostApp": host_applications.get_host_app_from_string(commit.sourceApplication).slug,
"sourceHostAppVersion": commit.sourceApplication
},
)
commit_object = operations._untracked_receive(commit.referenced_object, transport)
stream_data = operations._untracked_receive(commit.referencedObject, transport)
client.commit.received(
stream.id,
bstream.id,
commit.id,
source_application="blender",
message="received commit from Speckle Blender",
)
context.window_manager.progress_begin(0, commit_object.totalChildrenCount or 1)
context.window_manager.progress_begin(0, stream_data.totalChildrenCount)
set_convert_instances_as(self.receive_instances_as) #HACK: we need a better way to pass settings down to the converter
"""
Create or get Collection for stream objects
"""
collections = get_objects_collections(commit_object)
collections = get_objects_collections(stream_data)
if not collections:
print("Unusual commit structure - did not correctly create collections")
return {"CANCELLED"}
# name = ""
# if self.receive_mode == "create":
name = "{} [ {} @ {} ]".format(stream.name, branch.name, commit.id) # Matches Rhino "Create" naming
# else:
# name = stream.name # Doesn't quite match rhino's Update layer naming, but is close enough no?
name = "{} [ {} @ {} ]".format(stream.name, branch.name, commit.id)
col = create_collection(name)
col.speckle.stream_id = stream.id
col.speckle.units = commit_object.units or "m"
col.speckle.units = stream_data.units or "m"
if col.name not in bpy.context.scene.collection.children:
bpy.context.scene.collection.children.link(col)
@@ -447,7 +416,6 @@ class ReceiveStreamObjects(bpy.types.Operator):
"""
Iterate through retrieved resources
"""
bases_to_native(context, collections, scale, stream.id, func)
context.window_manager.progress_end()
@@ -506,11 +474,10 @@ class SendStreamObjects(bpy.types.Operator):
return {"CANCELLED"}
check = _check_speckle_client_user_stream(context.scene)
user, bstream = check
if user is None:
if check is None:
return {"CANCELLED"}
user, bstream = check
stream = user.streams[user.active_stream]
branch = stream.branches[int(stream.branch)]
@@ -558,6 +525,11 @@ class SendStreamObjects(bpy.types.Operator):
_report("Converting {}".format(obj.name))
# ngons = obj.get("speckle_ngons_as_polylines", False)
# if ngons:
# converted = ngons_to_speckle_polylines(obj, scale)
# else:
converted = convert_to_speckle(
obj,
scale,
@@ -584,20 +556,17 @@ class SendStreamObjects(bpy.types.Operator):
transport = ServerTransport(stream.id, client)
_report(f"Sending to {stream}")
obj_id = operations.send(
base,
[transport],
)
commitId = client.commit.create(
client.commit.create(
stream.id,
obj_id,
branch.name,
message=self.commit_message,
source_application="blender",
)
_report(f"Commit Created {user.server_url}/streams/{stream.id}/commits/{commitId}")
bpy.ops.speckle.load_user_streams()
+9 -35
View File
@@ -4,35 +4,11 @@ User account operators
import bpy
from bpy_speckle.functions import _report
from bpy_speckle.clients import speckle_clients
from bpy_speckle.properties.scene import SpeckleCommitObject, SpeckleSceneSettings
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)
bpy.context.view_layer.update()
if context.area:
context.area.tag_redraw()
return {"FINISHED"}
@staticmethod
def reset_ui(context: bpy.types.Context):
speckle: SpeckleSceneSettings = context.scene.speckle
speckle.users.clear()
speckle_clients.clear()
class LoadUsers(bpy.types.Operator):
"""
@@ -47,37 +23,36 @@ class LoadUsers(bpy.types.Operator):
_report("Loading users...")
speckle: SpeckleSceneSettings = context.scene.speckle
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
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:
client = SpeckleClient(
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(ex)
users.remove(len(users) - 1)
if profile.isDefault:
active_user_index = len(users) - 1
context.scene.speckle.active_user = str(len(users) - 1)
#speckle.active_user_index = int(speckle.active_user) #TODO Wtf is this?
speckle.active_user = str(active_user_index)
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:
@@ -103,14 +78,13 @@ def add_user_stream(user: User, 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 = 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
+9 -34
View File
@@ -1,7 +1,6 @@
"""
Scene properties
"""
from typing import Optional
import bpy
from bpy.props import (
StringProperty,
@@ -19,13 +18,12 @@ class SpeckleSceneObject(bpy.types.PropertyGroup):
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):
@@ -44,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):
@@ -74,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):
@@ -123,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,
@@ -151,9 +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
+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
+377 -481
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -7,14 +7,14 @@ license = "Apache-2.0"
[tool.poetry.dependencies]
python = ">=3.8, <4.0.0"
specklepy = "^2.13.0"
specklepy = "^2.9.1"
# [tool.poetry.group.local_specklepy.dependencies]
# specklepy = {path = "../specklepy", develop = true}
[tool.poetry.group.dev.dependencies]
numpy = "^1.23.5"
fake-bpy-module-latest = "^20230117"
fake-bpy-module-latest = "^20221006"
black = "^22.10.0"
pylint = "^2.15.7"
ruff = "^0.0.166"