Compare commits

...

9 Commits

Author SHA1 Message Date
Jedd Morgan c39298687d Merge pull request #160 from specklesystems/jrm/ismultiplayer
Added `isMultiplayer` property
2023-04-13 14:26:29 +01:00
Jedd Morgan bcdddbf930 Added isMultiplayer property 2023-04-13 14:24:17 +01:00
Jedd Morgan b5684e34f6 Merge pull request #159 from specklesystems/jrm/curve-fix
Removed merge vertices by distance from clean mesh
2023-04-05 12:51:27 +01:00
Jedd Morgan 2203fe98f8 Removed merge vertices by distance from clean mesh 2023-04-05 12:47:03 +01:00
Jedd Morgan bbfdf2863b Merge pull request #158 from specklesystems/jrm/curve-fix
Using new installer.py
2023-04-04 20:22:48 +01:00
Jedd Morgan f25f6cb16c Fixed some misc issues 2023-04-04 20:21:20 +01:00
Jedd Morgan 9e4e533ba8 Using new installer.py 2023-03-28 17:06:40 +01:00
Jedd Morgan 8db12ca9b9 Merge pull request #157 from specklesystems/jrm/curve-fix
Mesh area calc + minor cleanup
2023-03-28 16:47:59 +01:00
Jedd Morgan 366c864247 Mesh area calc + minor cleanup 2023-03-28 16:47:07 +01:00
9 changed files with 166 additions and 76 deletions
+1 -1
View File
@@ -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
+21 -18
View File
@@ -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}"
+23 -13
View File
@@ -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),
+1 -1
View File
@@ -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
View File
@@ -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()
+2 -1
View File
@@ -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
+2 -7
View File
@@ -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,
+1
View File
@@ -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 ""
+2 -1
View File
@@ -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