Compare commits
9 Commits
2.13.0-beta
...
2.13.0
| Author | SHA1 | Date | |
|---|---|---|---|
| c39298687d | |||
| bcdddbf930 | |||
| b5684e34f6 | |||
| 2203fe98f8 | |||
| bbfdf2863b | |||
| f25f6cb16c | |||
| 9e4e533ba8 | |||
| 8db12ca9b9 | |||
| 366c864247 |
@@ -1,7 +1,7 @@
|
||||
import bpy
|
||||
from bpy_speckle.installer import ensure_dependencies
|
||||
|
||||
ensure_dependencies()
|
||||
ensure_dependencies(f"Blender {bpy.app.version[0]}.{bpy.app.version[1]}")
|
||||
|
||||
from specklepy.logging import metrics
|
||||
|
||||
|
||||
@@ -48,7 +48,6 @@ def can_convert_to_native(speckle_object: Base) -> bool:
|
||||
_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,
|
||||
@@ -57,7 +56,9 @@ def create_new_object(obj_data: Optional[bpy.types.ID], desired_name: str, count
|
||||
"""
|
||||
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
|
||||
|
||||
if name in bpy.data.objects.keys():
|
||||
#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)
|
||||
|
||||
@@ -93,8 +94,12 @@ def convert_to_native(speckle_object: Base) -> list[Object]:
|
||||
elif isinstance(speckle_object, Instance):
|
||||
if convert_instances_as == "linked_duplicates":
|
||||
(obj_data, converted) = instance_to_native_object(speckle_object, scale)
|
||||
else: # convert_instances_as == collection_instance
|
||||
elif convert_instances_as != "collection_instance":
|
||||
obj_data = instance_to_native_collection_instance(speckle_object, scale)
|
||||
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)
|
||||
|
||||
else:
|
||||
_report(f"Unsupported type {speckle_type}")
|
||||
return []
|
||||
@@ -264,9 +269,6 @@ def polyline_to_native(scurve: Polyline, bcurve: bpy.types.Curve, scale: float)
|
||||
if hasattr(scurve, "closed"):
|
||||
polyline.use_cyclic_u = scurve.closed
|
||||
|
||||
# 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 = (
|
||||
@@ -504,12 +506,13 @@ def plane_to_native_transform(plane: Plane, fallback_scale:float = 1) -> MMatrix
|
||||
ty = (plane.origin.y * scale_factor)
|
||||
tz = (plane.origin.z * scale_factor)
|
||||
|
||||
|
||||
return MMatrix((
|
||||
(plane.xdir.x, plane.xdir.y, plane.xdir.z , 0),
|
||||
(plane.ydir.x, plane.ydir.y, plane.ydir.z , 0),
|
||||
(plane.normal.x, plane.normal.y, plane.normal.z , 0),
|
||||
(tx, ty, tz, 1)
|
||||
)).transposed()
|
||||
(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 )
|
||||
))
|
||||
|
||||
|
||||
"""
|
||||
@@ -517,7 +520,7 @@ Instances / Blocks
|
||||
"""
|
||||
|
||||
def _get_instance_name(instance: Instance) -> str:
|
||||
name_prefix = _speckle_object_name(instance) or _speckle_object_name(instance.definition) or _simplified_speckle_name(instance.speckle_type)
|
||||
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}"
|
||||
|
||||
|
||||
@@ -620,7 +623,7 @@ def _instance_definition_to_native(definition: Union[Base, BlockDefinition]) ->
|
||||
Object Naming
|
||||
"""
|
||||
|
||||
def _speckle_object_name(speckle_object: Base) -> Optional[str]:
|
||||
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)
|
||||
@@ -634,23 +637,23 @@ OBJECT_NAME_MAX_LENGTH = 62
|
||||
SPECKLE_ID_LENGTH = 32
|
||||
OBJECT_NAME_SEPERATOR = " -- "
|
||||
|
||||
def _truncate_name(name: str) -> str:
|
||||
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_name(speckle_type: str) -> str:
|
||||
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 = _speckle_object_name(speckle_object)
|
||||
name = _get_friendly_object_name(speckle_object)
|
||||
if name:
|
||||
prefix = _truncate_name(name)
|
||||
prefix = _truncate_object_name(name)
|
||||
else:
|
||||
prefix = _simplified_speckle_name(speckle_object.speckle_type)
|
||||
prefix = _simplified_speckle_type(speckle_object.speckle_type)
|
||||
|
||||
return f"{prefix}{OBJECT_NAME_SEPERATOR}{speckle_object.id}"
|
||||
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
from typing import Dict, Iterable, Optional, Tuple
|
||||
import bpy
|
||||
from bpy.types import Depsgraph, Material, MeshPolygon, Object
|
||||
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 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 (
|
||||
@@ -22,17 +24,20 @@ UNITS = "m"
|
||||
CAN_CONVERT_TO_SPECKLE = ("MESH", "CURVE", "EMPTY")
|
||||
|
||||
|
||||
def convert_to_speckle(blender_object: Object, scale: float, units: str, desgraph: Optional[Depsgraph]) -> Optional[list]:
|
||||
def convert_to_speckle(raw_blender_object: Object, scale: float, units: str, depsgraph: Optional[Depsgraph]) -> Optional[list]:
|
||||
global UNITS
|
||||
UNITS = units
|
||||
blender_type = blender_object.type
|
||||
|
||||
blender_type = raw_blender_object.type
|
||||
if blender_type not in CAN_CONVERT_TO_SPECKLE:
|
||||
return None
|
||||
|
||||
speckle_objects = []
|
||||
# speckle_material = material_to_speckle_old(blender_object) #TODO: What about curves with materials...
|
||||
if desgraph:
|
||||
blender_object = blender_object.evaluated_get(desgraph)
|
||||
blender_object: Object = (
|
||||
raw_blender_object.evaluated_get(depsgraph)
|
||||
if depsgraph
|
||||
else raw_blender_object
|
||||
)
|
||||
|
||||
converted = None
|
||||
if blender_type == "MESH":
|
||||
converted = mesh_to_speckle(blender_object, blender_object.data, scale)
|
||||
@@ -43,19 +48,20 @@ def convert_to_speckle(blender_object: Object, scale: float, units: str, desgrap
|
||||
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(blender_object)
|
||||
so.applicationId = so.properties.pop("applicationId", None)
|
||||
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)
|
||||
|
||||
|
||||
# Set object transform
|
||||
if blender_type != "EMPTY":
|
||||
so.properties["transform"] = transform_to_speckle(
|
||||
if blender_type != "EMPTY": #TODO: this could be deprecated once we add proper instancing support
|
||||
so["properties"]["transform"] = transform_to_speckle(
|
||||
blender_object.matrix_world
|
||||
)
|
||||
|
||||
@@ -84,12 +90,15 @@ def mesh_to_speckle(blender_object: Object, data: bpy.types.Mesh, scale: float =
|
||||
|
||||
#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
|
||||
@@ -112,6 +121,7 @@ def mesh_to_speckle(blender_object: Object, data: bpy.types.Mesh, scale: float =
|
||||
colors=[],
|
||||
textureCoordinates=m_texcoords,
|
||||
units=UNITS,
|
||||
area = mesh_area,
|
||||
bbox=Box(area=0.0, volume=0.0),
|
||||
)
|
||||
|
||||
@@ -344,7 +354,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, # magic (flatten list of tuples)
|
||||
value=value,
|
||||
length=0,
|
||||
domain=domain,
|
||||
bbox=Box(area=0.0, volume=0.0),
|
||||
|
||||
@@ -235,7 +235,7 @@ ignored_keys = {
|
||||
"_chunkable",
|
||||
}
|
||||
|
||||
def get_blender_custom_properties(obj, max_depth=1000):
|
||||
def get_blender_custom_properties(obj, max_depth: int = 1000):
|
||||
if max_depth < 0:
|
||||
return obj
|
||||
|
||||
|
||||
+113
-34
@@ -1,29 +1,118 @@
|
||||
"""
|
||||
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
|
||||
|
||||
import bpy
|
||||
import sys
|
||||
_user_data_env_var = "SPECKLE_USERDATA_PATH"
|
||||
|
||||
print("Starting Speckle Blender installation")
|
||||
|
||||
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(sys.executable)
|
||||
|
||||
PYTHON_PATH = sys.executable
|
||||
|
||||
|
||||
|
||||
def modules_path() -> Path:
|
||||
modules_path = Path(bpy.utils.script_path_user(), "addons", "modules")
|
||||
modules_path.mkdir(exist_ok=True, parents=True)
|
||||
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)
|
||||
|
||||
# set user modules path at beginning of paths for earlier hit
|
||||
if sys.path[1] != modules_path:
|
||||
sys.path.insert(1, str(modules_path))
|
||||
if sys.path[0] != connector_installation_path:
|
||||
sys.path.insert(0, str(connector_installation_path))
|
||||
|
||||
return modules_path
|
||||
print(f"Using connector installation path {connector_installation_path}")
|
||||
return connector_installation_path
|
||||
|
||||
|
||||
print(f"Found blender modules path {modules_path()}")
|
||||
|
||||
|
||||
def is_pip_available() -> bool:
|
||||
try:
|
||||
@@ -34,7 +123,7 @@ def is_pip_available() -> bool:
|
||||
|
||||
|
||||
def ensure_pip() -> None:
|
||||
print("Installing pip... "),
|
||||
print("Installing pip... ")
|
||||
|
||||
from subprocess import run
|
||||
|
||||
@@ -43,7 +132,7 @@ def ensure_pip() -> None:
|
||||
if completed_process.returncode == 0:
|
||||
print("Successfully installed pip")
|
||||
else:
|
||||
raise Exception("Failed to install pip.")
|
||||
raise Exception(f"Failed to install pip, got {completed_process.returncode} return code")
|
||||
|
||||
|
||||
def get_requirements_path() -> Path:
|
||||
@@ -53,11 +142,11 @@ def get_requirements_path() -> Path:
|
||||
return path
|
||||
|
||||
|
||||
def install_requirements() -> None:
|
||||
def install_requirements(host_application: str) -> None:
|
||||
# set up addons/modules under the user
|
||||
# script path. Here we'll install the
|
||||
# dependencies
|
||||
path = modules_path()
|
||||
path = connector_installation_path(host_application)
|
||||
print(f"Installing Speckle dependencies to {path}")
|
||||
|
||||
from subprocess import run
|
||||
@@ -78,20 +167,16 @@ def install_requirements() -> None:
|
||||
)
|
||||
|
||||
if completed_process.returncode != 0:
|
||||
print("Please try manually installing speckle-blender")
|
||||
raise Exception(
|
||||
"""
|
||||
Failed to install speckle-blender.
|
||||
See console for manual install instruction.
|
||||
"""
|
||||
)
|
||||
m = f"Failed to install dependenices through pip, got {completed_process.returncode} return code"
|
||||
print(m)
|
||||
raise Exception(m)
|
||||
|
||||
|
||||
def install_dependencies() -> None:
|
||||
def install_dependencies(host_application: str) -> None:
|
||||
if not is_pip_available():
|
||||
ensure_pip()
|
||||
|
||||
install_requirements()
|
||||
install_requirements(host_application)
|
||||
|
||||
|
||||
def _import_dependencies() -> None:
|
||||
@@ -110,19 +195,13 @@ def _import_dependencies() -> None:
|
||||
# print(req)
|
||||
# import_module("specklepy")
|
||||
|
||||
|
||||
def ensure_dependencies() -> None:
|
||||
def ensure_dependencies(host_application: str) -> None:
|
||||
try:
|
||||
install_dependencies()
|
||||
install_dependencies(host_application)
|
||||
invalidate_caches()
|
||||
_import_dependencies()
|
||||
print("Found all dependencies, proceed with loading")
|
||||
print("Successfully found dependencies")
|
||||
except ImportError:
|
||||
raise Exception(
|
||||
"Cannot automatically ensure Speckle dependencies. Please restart Blender!"
|
||||
)
|
||||
raise Exception(f"Cannot automatically ensure Speckle dependencies. Please try restarting the host application {host_application}!")
|
||||
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
ensure_dependencies()
|
||||
|
||||
@@ -4,6 +4,7 @@ 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,
|
||||
@@ -126,7 +127,7 @@ class DeleteObject(bpy.types.Operator):
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@deprecated
|
||||
class UploadNgonsAsPolylines(bpy.types.Operator):
|
||||
"""
|
||||
Upload mesh ngon faces as polyline outlines
|
||||
|
||||
@@ -343,7 +343,6 @@ 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)
|
||||
@@ -387,7 +386,8 @@ class ReceiveStreamObjects(bpy.types.Operator):
|
||||
getattr(transport, "account", None),
|
||||
custom_props={
|
||||
"sourceHostApp": host_applications.get_host_app_from_string(commit.source_application).slug,
|
||||
"sourceHostAppVersion": commit.source_application
|
||||
"sourceHostAppVersion": commit.source_application,
|
||||
"isMultiplayer": commit.author_id != user.id,
|
||||
},
|
||||
)
|
||||
commit_object = operations._untracked_receive(commit.referenced_object, transport)
|
||||
@@ -558,11 +558,6 @@ 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,
|
||||
|
||||
@@ -59,6 +59,7 @@ class LoadUsers(bpy.types.Operator):
|
||||
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 ""
|
||||
|
||||
@@ -84,6 +84,7 @@ class SpeckleStreamObject(bpy.types.PropertyGroup):
|
||||
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")
|
||||
@@ -153,6 +154,6 @@ class SpeckleSceneSettings(bpy.types.PropertyGroup):
|
||||
|
||||
def get_active_user(self) -> Optional[SpeckleUserObject]:
|
||||
selected_index = int(self.active_user)
|
||||
if 0 < selected_index < len(self.users):
|
||||
if 0 <= selected_index < len(self.users):
|
||||
return self.users[selected_index]
|
||||
return None
|
||||
|
||||
Reference in New Issue
Block a user