first receive attempts

This commit is contained in:
Dogukan Karatas
2025-03-12 12:41:31 +01:00
parent fdd05f7958
commit 5c2ecc6f97
8 changed files with 588 additions and 7 deletions
+459
View File
@@ -0,0 +1,459 @@
import bpy
import bmesh
from bpy.types import Object
from mathutils import Vector
from typing import Any, List, Optional, Tuple, Iterable
from specklepy.objects.base import Base
from specklepy.objects.geometry.line import Line
from specklepy.objects.geometry.mesh import Mesh
from specklepy.objects.geometry.polyline import Polyline
# Constants for naming and conversion
OBJECT_NAME_MAX_LENGTH = 62
OBJECT_NAME_SPECKLE_SEPARATOR = "::"
OBJECT_NAME_NUMERAL_SEPARATOR = "."
# Property aliases for finding geometry in various Speckle object types
DISPLAY_VALUE_PROPERTY_ALIASES = ["displayValue", "displayMesh", "displayStyle"]
ELEMENTS_PROPERTY_ALIASES = ["elements", "Elements", "@elements"]
def _has_native_conversion(speckle_object: Base) -> bool:
"""Check if object has a direct conversion method."""
return isinstance(speckle_object, (Line, Mesh, Polyline))
def _has_fallback_conversion(speckle_object: Base) -> bool:
"""Check if object has displayValue properties that can be converted."""
return any(getattr(speckle_object, alias, None) for alias in DISPLAY_VALUE_PROPERTY_ALIASES)
def can_convert_to_native(speckle_object: Base) -> bool:
"""Check if a Speckle object can be converted to Blender.
Args:
speckle_object: The Speckle object to check
Returns:
True if the object can be converted, False otherwise
"""
return _has_native_conversion(speckle_object) or _has_fallback_conversion(speckle_object)
def convert_to_native(speckle_object: Base) -> Object:
"""Convert a Speckle object to a Blender object.
Args:
speckle_object: The Speckle object to convert
Returns:
A Blender object
"""
# Generate a name for the object
object_name = _generate_object_name(speckle_object)
converted = None
children = []
# First try native conversion if available
if isinstance(speckle_object, Line):
converted = line_to_native(speckle_object, object_name)
elif isinstance(speckle_object, Mesh):
converted = mesh_to_native(speckle_object, object_name, 1.0) # Using 1.0 as default scale
elif isinstance(speckle_object, Polyline):
converted = polyline_to_native(speckle_object, object_name)
# If no native conversion was possible, try displayValue conversion
if not converted:
(converted, children) = display_value_to_native(
speckle_object, object_name, 1.0 # Using 1.0 as default scale
)
if not converted and not children:
raise ValueError(f"Failed to convert object: {speckle_object.speckle_type}")
# Create a Blender object if the converter returned data instead of an object
if not isinstance(converted, Object):
blender_object = create_new_object(converted, object_name)
else:
blender_object = converted
# Store Speckle ID
if hasattr(blender_object, "speckle"):
blender_object.speckle.object_id = str(speckle_object.id)
blender_object.speckle.enabled = True
# Parent children to the main object if any were created
for child in children:
child.parent = blender_object
return blender_object
def line_to_native(speckle_line: Line, name: str) -> bpy.types.Curve:
"""Convert a Speckle line to a Blender curve.
Args:
speckle_line: The Speckle line to convert
name: The name for the new Blender curve
Returns:
A Blender curve data block
"""
# Check if the line has valid start and end points
if not speckle_line.start or not speckle_line.end:
raise ValueError("Line is missing start or end point")
# Create a new curve data block
blender_curve = bpy.data.curves.new(name, type="CURVE")
blender_curve.dimensions = "3D"
# Create a new spline in the curve
spline = blender_curve.splines.new("POLY")
spline.points.add(1) # Add one point (default has 1, so total will be 2)
# Set the coordinates
# Note: Blender curve points are 4D (x, y, z, w) where w is weight
spline.points[0].co = (
float(speckle_line.start.x),
float(speckle_line.start.y),
float(speckle_line.start.z),
1.0,
)
spline.points[1].co = (
float(speckle_line.end.x),
float(speckle_line.end.y),
float(speckle_line.end.z),
1.0,
)
return blender_curve
def mesh_to_native(speckle_mesh: Mesh, name: str, scale: float) -> bpy.types.Mesh:
"""Convert a Speckle mesh to a Blender mesh.
Args:
speckle_mesh: The Speckle mesh to convert
name: The name for the new Blender mesh
scale: The scale factor to apply
Returns:
A Blender mesh data block
"""
# Check if mesh already exists with this name
if name in bpy.data.meshes.keys():
return bpy.data.meshes[name]
# Create a new mesh data block
blender_mesh = bpy.data.meshes.new(name=name)
# Create a BMesh for easier manipulation
bm = bmesh.new()
# Add vertices
add_vertices(speckle_mesh, bm, scale)
bm.verts.ensure_lookup_table()
# Add faces
add_faces(speckle_mesh, bm, 0, 0)
bm.faces.ensure_lookup_table()
# Finalize and cleanup
bm.to_mesh(blender_mesh)
bm.free()
return blender_mesh
def add_vertices(mesh: Mesh, bm: bmesh.types.BMesh, scale: float) -> None:
"""Add vertices from a Speckle mesh to a Blender BMesh.
Args:
mesh: The Speckle mesh containing vertices
bm: The Blender BMesh to add vertices to
scale: The scale factor to apply
"""
if not mesh.vertices:
return
# Add vertices
for i in range(0, len(mesh.vertices), 3):
x = float(mesh.vertices[i]) * scale
y = float(mesh.vertices[i + 1]) * scale
z = float(mesh.vertices[i + 2]) * scale
bm.verts.new(Vector((x, y, z)))
def add_faces(mesh: Mesh, bm: bmesh.types.BMesh, vertex_offset: int, material_index: int) -> None:
"""Add faces from a Speckle mesh to a Blender BMesh.
Args:
mesh: The Speckle mesh containing faces
bm: The Blender BMesh to add faces to
vertex_offset: Offset to apply to vertex indices
material_index: Material index to assign to faces
"""
if not mesh.faces:
return
# Ensure lookup table is up to date
bm.verts.ensure_lookup_table()
i = 0
while i < len(mesh.faces):
face_size = mesh.faces[i]
i += 1
# Skip invalid faces
if face_size < 3:
continue
# Get vertices for this face
verts = []
for j in range(face_size):
if i >= len(mesh.faces):
break
vert_idx = mesh.faces[i] + vertex_offset
i += 1
if vert_idx >= len(bm.verts):
continue
verts.append(bm.verts[vert_idx])
# Create the face if we have enough valid vertices
if len(verts) >= 3:
try:
face = bm.faces.new(verts)
face.material_index = material_index
except Exception as e:
print(f"Failed to create face: {e}")
def polyline_to_native(speckle_polyline: Polyline, name: str) -> bpy.types.Curve:
"""Convert a Speckle polyline to a Blender curve.
Args:
speckle_polyline: The Speckle polyline to convert
name: The name for the new Blender curve
Returns:
A Blender curve data block
"""
# Get points from the polyline
points = speckle_polyline.get_points()
if not points:
raise ValueError("Polyline has no points")
# Create a new curve data block
blender_curve = bpy.data.curves.new(name, type="CURVE")
blender_curve.dimensions = "3D"
# Create a new spline in the curve
spline = blender_curve.splines.new("POLY")
spline.points.add(len(points) - 1) # Add points (default has 1, so add n-1 more)
# Set the coordinates for each point
# Note: Blender curve points are 4D (x, y, z, w) where w is weight
for i, point in enumerate(points):
spline.points[i].co = (
float(point.x),
float(point.y),
float(point.z),
1.0,
)
# If the polyline is closed, set the spline to be cyclic
if speckle_polyline.is_closed():
spline.use_cyclic_u = True
return blender_curve
def display_value_to_native(
speckle_object: Base, name: str, scale: float
) -> Tuple[Optional[bpy.types.Mesh], List[Object]]:
"""Convert displayValue properties to Blender objects.
Args:
speckle_object: The Speckle object to convert
name: The name for the new Blender objects
scale: The scale factor to apply
Returns:
Tuple of (converted mesh, list of child objects)
"""
return _members_to_native(
speckle_object, name, scale, DISPLAY_VALUE_PROPERTY_ALIASES, True
)
def _members_to_native(
speckle_object: Base,
name: str,
scale: float,
members: Iterable[str],
combineMeshes: bool,
) -> Tuple[Optional[bpy.types.Mesh], List[Object]]:
"""Convert specific members of a Speckle object to Blender objects.
Args:
speckle_object: The Speckle object to convert
name: The name for the new Blender objects
scale: The scale factor to apply
members: The member properties to look for
combineMeshes: Whether to combine meshes into one
Returns:
Tuple of (combined mesh, list of child objects)
"""
meshes: List[Mesh] = []
others: List[Base] = []
for alias in members:
display = getattr(speckle_object, alias, None)
count = 0
MAX_DEPTH = 255 # Prevent infinite recursion
def separate(value: Any) -> bool:
nonlocal meshes, others, count, MAX_DEPTH
if combineMeshes and isinstance(value, Mesh):
meshes.append(value)
elif isinstance(value, Base):
others.append(value)
elif isinstance(value, list):
count += 1
if count > MAX_DEPTH:
return True
for x in value:
separate(x)
return False
did_halt = separate(display)
if did_halt:
print(f"Traversal halted after exceeding depth {MAX_DEPTH}")
# Convert meshes and other objects
children: List[Object] = []
mesh = None
if meshes:
mesh = meshes_to_native(speckle_object, meshes, name, scale)
for item in others:
try:
blender_object = convert_to_native(item)
children.append(blender_object)
except Exception as ex:
print(f"Failed to convert display value {item}: {ex}")
return (mesh, children)
def meshes_to_native(element: Base, meshes: List[Mesh], name: str, scale: float) -> bpy.types.Mesh:
"""Convert multiple Speckle meshes to a single Blender mesh.
Args:
element: The parent Speckle object
meshes: The Speckle meshes to convert
name: The name for the new Blender mesh
scale: The scale factor to apply
Returns:
A Blender mesh
"""
if name in bpy.data.meshes.keys():
return bpy.data.meshes[name]
blender_mesh = bpy.data.meshes.new(name=name)
bm = bmesh.new()
# First pass: add vertices
for mesh in meshes:
add_vertices(mesh, bm, scale)
bm.verts.ensure_lookup_table()
# Second pass: add faces
offset = 0
for i, mesh in enumerate(meshes):
if not mesh.vertices:
continue
add_faces(mesh, bm, offset, i)
offset += len(mesh.vertices) // 3
# Finalize and cleanup
bm.to_mesh(blender_mesh)
bm.free()
return blender_mesh
def create_new_object(obj_data, desired_name: str) -> bpy.types.Object:
"""Create a new Blender object with a unique name.
Args:
obj_data: The data to use for the object (e.g., mesh, curve)
desired_name: The desired name for the object
Returns:
A new Blender object
"""
# Make sure the name is unique
name = _make_unique_name(desired_name, bpy.data.objects.keys())
# Create the object
blender_object = bpy.data.objects.new(name, obj_data)
# Link it to the active collection if possible
if bpy.context.collection:
bpy.context.collection.objects.link(blender_object)
else:
# If no active collection, link to scene collection
bpy.context.scene.collection.objects.link(blender_object)
return blender_object
def _make_unique_name(desired_name: str, existing_names) -> str:
"""Create a unique name by appending a number if necessary.
Args:
desired_name: The desired name
existing_names: Collection of existing names to avoid duplicates
Returns:
A unique name
"""
if desired_name not in existing_names:
return desired_name
# If name exists, append numbers until we find a unique one
counter = 1
while True:
new_name = f"{desired_name}.{counter:03d}"
if new_name not in existing_names:
return new_name
counter += 1
def _generate_object_name(speckle_object: Base) -> str:
"""Generate a name for a Blender object based on a Speckle object.
Args:
speckle_object: The Speckle object
Returns:
A name for the object
"""
# Try to get a meaningful name
name = getattr(speckle_object, "name", None)
if not name:
# Use the object type as a fallback
speckle_type = speckle_object.speckle_type
name = speckle_type.split(".")[-1] # Get the last part of the type name
# Truncate if necessary
if len(name) > OBJECT_NAME_MAX_LENGTH - 10: # Leave room for speckle ID
name = name[:OBJECT_NAME_MAX_LENGTH - 10]
# Add the Speckle ID for uniqueness
if hasattr(speckle_object, "id") and speckle_object.id:
return f"{name}{OBJECT_NAME_SPECKLE_SEPARATOR}{speckle_object.id[:8]}"
else:
return name
+115 -1
View File
@@ -1,5 +1,11 @@
import bpy
from typing import Set
from specklepy.api.credentials import get_local_accounts
from specklepy.transports.server import ServerTransport
from specklepy.api import operations
from specklepy.api.client import SpeckleClient
from specklepy.objects.base import Base
from ..converter.to_native import can_convert_to_native, convert_to_native
class SPECKLE_OT_load(bpy.types.Operator):
bl_idname = "speckle.load"
@@ -18,4 +24,112 @@ class SPECKLE_OT_load(bpy.types.Operator):
self.report({'INFO'}, f"Load button clicked at {context.scene.speckle_state.mouse_position[0], context.scene.speckle_state.mouse_position[1]}")
# Opens project_selection_dialog
bpy.ops.speckle.project_selection_dialog("INVOKE_DEFAULT")
return {'FINISHED'}
return {'FINISHED'}
@classmethod
def load(cls, context: bpy.types.Context, model_card) -> None:
print("Load process started")
print(f"Loading project: {model_card.project_name}, model: {model_card.model_name}")
print(f"Project ID: {model_card.project_id}")
print(f"Version ID: {model_card.version_id}")
try:
# get the account from local accounts
account = next((acc for acc in get_local_accounts() if acc.id == context.window_manager.selected_account_id), None)
if not account:
raise Exception("No Speckle account found")
# initialize the Speckle client
client = SpeckleClient(host=account.serverInfo.url)
# authenticate with account
client.authenticate_with_account(account)
# now we need a transport
transport = ServerTransport(
stream_id=model_card.project_id,
client=client
)
# get the version
version = client.version.get(model_card.version_id, model_card.project_id)
obj_id = version.referenced_object
# receive the data
version_data = operations.receive(
obj_id,
transport
)
# TO DISCUSS
# create a root collection in Blender to hold all imported objects
root_collection_name = f"{model_card.model_name} - {model_card.version_id[:8]}"
root_collection = bpy.data.collections.new(root_collection_name)
context.scene.collection.children.link(root_collection)
# start conversion process
context.window_manager.progress_begin(0, 100)
# process and convert the received data to Blender objects
converted_objects = {}
traversal_queue = [version_data]
converted_count = 0
total_count = getattr(version_data, "totalChildrenCount", 100) or 100
while traversal_queue:
current_object = traversal_queue.pop(0)
# Skip if already processed
if hasattr(current_object, "id") and current_object.id in converted_objects:
continue
try:
# check if this object can be converted
if can_convert_to_native(current_object):
# convert the object to Blender
blender_obj = convert_to_native(current_object)
# store the converted object
if hasattr(current_object, "id"):
converted_objects[current_object.id] = blender_obj
# link to the root collection if not already in a collection
if blender_obj.name not in root_collection.objects:
try:
root_collection.objects.link(blender_obj)
except RuntimeError:
# Object might already be linked to another collection
pass
print(f"Successfully converted: {current_object.speckle_type}")
# check for children/elements to process
children = []
# look for common element properties
for prop_name in ["elements", "Elements", "@elements"]:
if hasattr(current_object, prop_name):
elements = getattr(current_object, prop_name)
if isinstance(elements, list):
children.extend(elements)
elif isinstance(elements, Base):
children.append(elements)
# add all children to the traversal queue
traversal_queue.extend(children)
except Exception as e:
print(f"Error converting object: {str(e)}")
# update progress
converted_count += 1
progress = int((converted_count / total_count) * 100)
context.window_manager.progress_update(min(progress, 100))
context.window_manager.progress_end()
print(f"Conversion completed. Converted {converted_count} objects.")
except Exception as e:
print(f"Error loading from Speckle: {str(e)}")
context.window_manager.progress_end()
return
+3
View File
@@ -15,6 +15,7 @@ class speckle_model_card(bpy.types.PropertyGroup):
version_id (StringProperty): Unique identifier of the selected version.
"""
project_name: bpy.props.StringProperty(name="Project Name", description="Name of the project", default="") # type: ignore
project_id: bpy.props.StringProperty(name="Project ID", description="ID of the selected project", default="") # type: ignore
model_name: bpy.props.StringProperty(name="Model Name", description="Name of the model", default="") # type: ignore
is_publish: bpy.props.BoolProperty(name="Publish/Load", description="If the model is published or loaded", default=False) # type: ignore
selection_summary: bpy.props.StringProperty(name="Selection Summary", description="Summary of the selection", default="") # type: ignore
@@ -28,6 +29,7 @@ class speckle_model_card(bpy.types.PropertyGroup):
"""
return {
"project_name": self.project_name,
"project_id": self.project_id,
"model_name": self.model_name,
"is_publish": self.is_publish,
"selection_summary": self.selection_summary,
@@ -46,6 +48,7 @@ class speckle_model_card(bpy.types.PropertyGroup):
"""
item = cls()
item.project_name = data["project_name"]
item.project_id = data["project_id"]
item.model_name = data["model_name"]
item.is_publish = data["is_publish"]
item.selection_summary = data["selection_summary"]
@@ -7,6 +7,7 @@ import bpy
from bpy.types import WindowManager, UILayout, Context, PropertyGroup, Event
from .mouse_position_mixin import MousePositionMixin
from ..utils.version_manager import get_versions_for_model
from ..operators.load import SPECKLE_OT_load
class speckle_version(bpy.types.PropertyGroup):
"""PropertyGroup for storing version information.
@@ -132,11 +133,16 @@ class SPECKLE_OT_version_selection_dialog(MousePositionMixin, bpy.types.Operator
def execute(self, context: Context) -> set[str]:
model_card = context.scene.speckle_state.model_cards.add()
model_card.project_name = self.project_name
model_card.project_id = self.project_id
model_card.model_name = self.model_name
model_card.is_publish = False
# Store the selected version ID
selected_version = context.window_manager.speckle_versions[self.version_index]
model_card.version_id = selected_version.id
# Call the load process class method
SPECKLE_OT_load.load(context, model_card)
return {'FINISHED'}
def invoke(self, context: Context, event: Event) -> set[str]:
+1 -1
View File
@@ -60,7 +60,7 @@ def get_models_for_project(account_id: str, project_id: str, search: Optional[st
# Get models
models: List[Model] = client.model.get_models(project_id=project_id, models_limit=10, models_filter=filter).items
return [(model.name, model.id, format_relative_time(model.createdAt)) for model in models]
return [(model.name, model.id, format_relative_time(model.created_at)) for model in models]
except Exception as e:
print(f"Error fetching models: {str(e)}")
+1 -1
View File
@@ -45,7 +45,7 @@ def get_projects_for_account(account_id: str, search: Optional[str] = None) -> L
# Fetch projects
projects = client.active_user.get_projects(limit=10, filter=filter).items
return [(project.name, format_role(project.role), format_relative_time(project.updatedAt), project.id) for project in projects]
return [(project.name, format_role(project.role), format_relative_time(project.updated_at), project.id) for project in projects]
except Exception as e:
import traceback
+1 -1
View File
@@ -53,7 +53,7 @@ def get_versions_for_model(account_id: str, project_id: str, model_id: str, sear
# Get versions
versions: List[Version] = client.version.get_versions(project_id=project_id, model_id=model_id, limit=10, filter=filter).items
return [(version.id, version.message or "No message", format_relative_time(version.createdAt)) for version in versions]
return [(version.id, version.message or "No message", format_relative_time(version.created_at)) for version in versions]
except Exception as e:
print(f"Error fetching versions: {str(e)}")
+2 -3
View File
@@ -2,11 +2,10 @@
name = "speckle-blender"
version = "3.0.0"
description = "Next-Gen Speckle connector for Blender!"
requires-python = ">=3.8, <4.0.0"
requires-python = ">=3.11.9, <4.0.0"
license = "Apache-2.0"
dependencies = [
"specklepy>=2.21.1,<3",
"attrs>=23.1.0,<24",
"specklepy>=3.0.0a3"
]
[dependency-groups]