Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 85a73cb8eb | |||
| 64c7fa7d48 | |||
| 380a2ee844 | |||
| 7ba4467217 | |||
| 463b53f8c8 | |||
| 1f908aa8d2 | |||
| d40821dfa6 |
@@ -0,0 +1,6 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
@@ -29,7 +29,7 @@ jobs:
|
||||
run: |
|
||||
python main.py generate_schema ${HOME}/${{ env.FUNCTION_SCHEMA_FILE_NAME }}
|
||||
- name: Speckle Automate Function - Build and Publish
|
||||
uses: specklesystems/speckle-automate-github-composite-action@0.7.2
|
||||
uses: specklesystems/speckle-automate-github-composite-action@0.7.4
|
||||
with:
|
||||
speckle_automate_url: ${{ env.SPECKLE_AUTOMATE_URL || 'https://automate.speckle.dev' }}
|
||||
speckle_token: ${{ secrets.SPECKLE_FUNCTION_TOKEN }}
|
||||
|
||||
+11
@@ -311,3 +311,14 @@ pyrightconfig.json
|
||||
.ionide
|
||||
|
||||
# End of https://www.toptal.com/developers/gitignore/api/visualstudiocode,python,pycharm
|
||||
Dockerfile copy
|
||||
Dockerfile copy 2
|
||||
.idea/git_toolbox_prj.xml
|
||||
.gitignore
|
||||
.idea/misc.xml
|
||||
.idea/inspectionProfiles/Project_Default.xml
|
||||
.gitignore
|
||||
.idea/vcs.xml
|
||||
.idea/speckle-automate-basic-clash-demo.iml
|
||||
.idea/modules.xml
|
||||
.idea/inspectionProfiles/profiles_settings.xml
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
from concurrent.futures import ProcessPoolExecutor, as_completed
|
||||
from typing import List, Tuple, Any, Optional
|
||||
|
||||
try:
|
||||
import pymesh
|
||||
except ImportError:
|
||||
pymesh = None # Or handle it in another appropriate way
|
||||
|
||||
from speckle_automate import AutomationContext
|
||||
|
||||
from Geometry.element import Element
|
||||
from Geometry.mesh import cast
|
||||
|
||||
|
||||
def detect_clashes_old(
|
||||
reference_elements: List[Element], latest_elements: List[Element], _tolerance: float
|
||||
) -> list[tuple[str, str, float]]:
|
||||
"""
|
||||
Detect clashes between two sets of mesh elements using Pymesh.
|
||||
|
||||
Args:
|
||||
reference_elements (List[Element]): Elements from the reference model.
|
||||
latest_elements (List[Element]): Elements from the latest model.
|
||||
_tolerance (float): Tolerance value for clash detection. TODO: how to implement this?
|
||||
|
||||
Returns:
|
||||
List[Tuple[str, str]]: List of tuples indicating clashes, with each tuple
|
||||
containing the IDs of the clashing elements.
|
||||
"""
|
||||
# TODO: Spatial partitioning to reduce number of comparisons
|
||||
# TODO: Tolerance
|
||||
# TODO: parallel processing
|
||||
|
||||
clashes = []
|
||||
for ref_element in reference_elements:
|
||||
for latest_element in latest_elements:
|
||||
for ref_mesh in ref_element.meshes:
|
||||
for latest_mesh in latest_element.meshes:
|
||||
# Convert Trimesh meshes to Pymesh if necessary
|
||||
ref_pymesh: pymesh.Mesh = cast(ref_mesh, pymesh.Mesh)
|
||||
latest_pymesh: pymesh.Mesh = cast(latest_mesh, pymesh.Mesh)
|
||||
|
||||
if not ref_pymesh or not latest_pymesh:
|
||||
continue
|
||||
|
||||
intersection = pymesh.boolean(
|
||||
ref_pymesh, latest_pymesh, operation="intersection"
|
||||
)
|
||||
|
||||
if (
|
||||
intersection and intersection.volume > 0
|
||||
): # TODO: could tolerance relate to this?
|
||||
severity = intersection.volume / min(
|
||||
ref_pymesh.volume, latest_pymesh.volume
|
||||
)
|
||||
clashes.append((ref_element.id, latest_element.id, severity))
|
||||
break
|
||||
|
||||
return clashes
|
||||
|
||||
|
||||
def check_for_clash(
|
||||
ref_element: Element, latest_element: Element
|
||||
) -> Optional[tuple[Any, Any, Any]]:
|
||||
"""
|
||||
Check for a clash between two elements and calculate the severity of the clash.
|
||||
|
||||
Args:
|
||||
ref_element (Element): An element from the reference model.
|
||||
latest_element (Element): An element from the latest model.
|
||||
|
||||
Returns:
|
||||
Tuple[str, str, float]: A tuple containing the IDs of the clashing elements and the severity, if a clash is found.
|
||||
"""
|
||||
for ref_mesh in ref_element.meshes:
|
||||
for latest_mesh in latest_element.meshes:
|
||||
ref_pymesh = cast(ref_mesh, pymesh.Mesh)
|
||||
latest_pymesh = cast(latest_mesh, pymesh.Mesh)
|
||||
|
||||
if not ref_pymesh or not latest_pymesh:
|
||||
continue
|
||||
|
||||
intersection = pymesh.boolean(
|
||||
ref_pymesh, latest_pymesh, operation="intersection"
|
||||
)
|
||||
if intersection and intersection.volume > 0:
|
||||
severity = intersection.volume / min(
|
||||
ref_pymesh.volume, latest_pymesh.volume
|
||||
)
|
||||
return ref_element.id, latest_element.id, severity
|
||||
return None
|
||||
|
||||
|
||||
def detect_clashes(
|
||||
reference_elements: List[Element], latest_elements: List[Element], _tolerance: float
|
||||
) -> List[Tuple[str, str, float]]:
|
||||
"""
|
||||
Detect clashes between two sets of mesh elements using parallel processing.
|
||||
|
||||
Args:
|
||||
reference_elements (List[Element]): Elements from the reference model.
|
||||
latest_elements (List[Element]): Elements from the latest model.
|
||||
_tolerance (float): Tolerance value for clash detection. TODO: how to implement this?
|
||||
|
||||
Returns:
|
||||
List[Tuple[str, str, float]]: A list of tuples indicating clashes.
|
||||
"""
|
||||
clashes = []
|
||||
with ProcessPoolExecutor() as executor:
|
||||
future_clash = {
|
||||
executor.submit(check_for_clash, ref, latest): (ref, latest)
|
||||
for ref in reference_elements
|
||||
for latest in latest_elements
|
||||
}
|
||||
for future in as_completed(future_clash):
|
||||
result = future.result()
|
||||
if result:
|
||||
clashes.append(result)
|
||||
|
||||
return clashes
|
||||
|
||||
|
||||
def detect_and_report_clashes(
|
||||
reference_elements: list[Element],
|
||||
latest_elements: list[Element],
|
||||
tolerance: float,
|
||||
automate_context: AutomationContext,
|
||||
) -> list[tuple[str, str, float]]:
|
||||
clashes = detect_clashes(reference_elements, latest_elements, tolerance)
|
||||
|
||||
total_clashes = len(clashes)
|
||||
padding_length = len(str(total_clashes))
|
||||
|
||||
for i, (ref_id, latest_id, severity) in enumerate(clashes, start=1):
|
||||
clash_number = str(i).zfill(padding_length)
|
||||
combined_message = f"Clash {clash_number}: between {ref_id} and {latest_id} with severity {severity:.2f}"
|
||||
object_ids = [ref_id, latest_id]
|
||||
|
||||
# Assuming severity levels: Low (<0.25), Medium (0.25-0.75), High (>0.75) TODO: Determine severity levels
|
||||
if severity > 0.75:
|
||||
category = "High"
|
||||
elif severity > 0.25:
|
||||
category = "Medium"
|
||||
else:
|
||||
category = "Low"
|
||||
|
||||
automate_context.attach_error_to_objects(
|
||||
category=category, object_ids=object_ids, message=combined_message
|
||||
)
|
||||
|
||||
return clashes
|
||||
@@ -0,0 +1,64 @@
|
||||
from typing import Tuple, Optional, List
|
||||
|
||||
import numpy as np
|
||||
import trimesh
|
||||
from specklepy.objects import Base
|
||||
from specklepy.objects.geometry import Mesh as SpeckleMesh
|
||||
from specklepy.objects.other import Transform
|
||||
|
||||
from Geometry.helpers import combine_transform_matrices
|
||||
from Geometry.mesh import speckle_mesh_to_trimesh
|
||||
|
||||
|
||||
class Element:
|
||||
def __init__(self, id, meshes):
|
||||
"""
|
||||
Initialize an Element object with an ID and a list of meshes.
|
||||
|
||||
Args:
|
||||
id (str): The ID of the Element.
|
||||
meshes (List[Trimesh]): List of trimesh Mesh objects.
|
||||
"""
|
||||
self.id = id
|
||||
self.meshes = meshes
|
||||
|
||||
|
||||
def speckle_to_element(
|
||||
base_id_transforms: Tuple[Base, str, Optional[List[Transform]]]
|
||||
) -> Element:
|
||||
"""
|
||||
Convert a SpecklePy Base object, its identifier, and an optional list of transforms
|
||||
to an Element object.
|
||||
|
||||
Args:
|
||||
base_id_transforms (tuple): Contains a SpecklePy Base object, its identifier,
|
||||
and an optional list of Transform objects.
|
||||
|
||||
Returns:
|
||||
Element: The resulting Element object.
|
||||
"""
|
||||
base, speckle_id, transforms = base_id_transforms
|
||||
|
||||
display_value = base.displayValue
|
||||
if isinstance(display_value, SpeckleMesh):
|
||||
display_value = [display_value]
|
||||
|
||||
element = Element(speckle_id, meshes=[])
|
||||
|
||||
# Combine all transforms into a single matrix
|
||||
combined_transform = (
|
||||
combine_transform_matrices(transforms) if transforms else np.identity(4)
|
||||
)
|
||||
|
||||
if isinstance(display_value, list):
|
||||
for mesh in display_value:
|
||||
if mesh:
|
||||
t_mesh = speckle_mesh_to_trimesh(mesh)
|
||||
if not isinstance(t_mesh, trimesh.Trimesh):
|
||||
continue
|
||||
|
||||
# Apply the combined transformation matrix
|
||||
t_mesh.apply_transform(combined_transform)
|
||||
element.meshes.append(t_mesh)
|
||||
|
||||
return element
|
||||
@@ -0,0 +1,143 @@
|
||||
from typing import List
|
||||
|
||||
import numpy as np
|
||||
from specklepy.objects.geometry import Vector
|
||||
from specklepy.objects.other import Transform as SpeckleTransform
|
||||
|
||||
|
||||
def calculate_polygon_normal(vertices: List[Vector]) -> Vector:
|
||||
"""
|
||||
Calculate the normal vector for a polygon represented by a list of vertices.
|
||||
|
||||
Args:
|
||||
vertices (List[Vector]): A list of vertices representing the polygon.
|
||||
|
||||
Returns:
|
||||
Vector: The normal vector of the polygon.
|
||||
"""
|
||||
normal = Vector.from_list([0.0, 0.0, 0.0])
|
||||
num_vertices = len(vertices)
|
||||
for i in range(num_vertices):
|
||||
curr, nxt = vertices[i], vertices[(i + 1) % num_vertices]
|
||||
# Cross product components are accumulated to find the normal.
|
||||
normal.x += (curr.y - nxt.y) * (curr.z + nxt.z)
|
||||
normal.y += (curr.z - nxt.z) * (curr.x + nxt.x)
|
||||
normal.z += (curr.x - nxt.x) * (curr.y + nxt.y)
|
||||
|
||||
# Normalize the calculated normal vector.
|
||||
length = np.sqrt(normal.x**2 + normal.y**2 + normal.z**2)
|
||||
normal.x, normal.y, normal.z = (
|
||||
normal.x / length,
|
||||
normal.y / length,
|
||||
normal.z / length,
|
||||
)
|
||||
return normal
|
||||
|
||||
|
||||
def is_point_within_triangle(pt: Vector, v1: Vector, v2: Vector, v3: Vector) -> bool:
|
||||
"""
|
||||
Check if a point is inside a given triangle.
|
||||
|
||||
Args:
|
||||
pt (Vector): The point to check.
|
||||
v1, v2, v3 (Vector): The vertices of the triangle.
|
||||
|
||||
Returns:
|
||||
bool: True if the point is inside the triangle, False otherwise.
|
||||
"""
|
||||
|
||||
def sign(p1, p2, p3):
|
||||
return (p1.x - p3.x) * (p2.y - p3.y) - (p2.x - p3.x) * (p1.y - p3.y)
|
||||
|
||||
b1 = sign(pt, v1, v2) < 0.0
|
||||
b2 = sign(pt, v2, v3) < 0.0
|
||||
b3 = sign(pt, v3, v1) < 0.0
|
||||
|
||||
return (b1 == b2) and (b2 == b3)
|
||||
|
||||
|
||||
def triangulate_face(vertices: List[Vector]) -> List[List[int]]:
|
||||
"""
|
||||
Triangulate a polygon defined by a list of vertices.
|
||||
|
||||
Args:
|
||||
vertices (List[Vector]): The vertices of the polygon.
|
||||
|
||||
Returns:
|
||||
List[List[int]]: A list of triangles, each represented as a list of vertex indices.
|
||||
"""
|
||||
triangles = []
|
||||
indices = list(range(len(vertices)))
|
||||
normal = calculate_polygon_normal(vertices)
|
||||
|
||||
# The ear clipping algorithm is used for triangulation.
|
||||
while len(indices) > 2:
|
||||
for i in range(len(indices)):
|
||||
prev, curr, nxt = (
|
||||
indices[i - 1],
|
||||
indices[i],
|
||||
indices[(i + 1) % len(indices)],
|
||||
)
|
||||
if is_convex(vertices[prev], vertices[curr], vertices[nxt], normal):
|
||||
triangles.append([prev, curr, nxt])
|
||||
del indices[i]
|
||||
break
|
||||
|
||||
return triangles
|
||||
|
||||
|
||||
def is_convex(a: Vector, b: Vector, c: Vector, normal: Vector) -> bool:
|
||||
"""
|
||||
Check if a triangle formed by three vertices (a, b, c) is convex with respect to a given normal.
|
||||
|
||||
Args:
|
||||
a (Vector): The first vertex of the triangle.
|
||||
b (Vector): The second vertex of the triangle.
|
||||
c (Vector): The third vertex of the triangle.
|
||||
normal (Vector): The normal vector with respect to which convexity is checked.
|
||||
|
||||
Returns:
|
||||
bool: True if the triangle is convex with respect to the normal, False otherwise.
|
||||
"""
|
||||
ab = Vector.from_list([b.x - a.x, b.y - a.y, b.z - a.z])
|
||||
bc = Vector.from_list([c.x - b.x, c.y - b.y, c.z - b.z])
|
||||
cross = Vector.from_list(
|
||||
[
|
||||
ab.y * bc.z - ab.z * bc.y,
|
||||
ab.z * bc.x - ab.x * bc.z,
|
||||
ab.x * bc.y - ab.y * bc.x,
|
||||
]
|
||||
)
|
||||
|
||||
# Dot product to compare with the face normal
|
||||
return cross.x * normal.x + cross.y * normal.y + cross.z * normal.z > 0
|
||||
|
||||
|
||||
def combine_transform_matrices(transforms: List[SpeckleTransform]) -> np.ndarray:
|
||||
"""
|
||||
Combine multiple transformation matrices into a single matrix.
|
||||
|
||||
Args:
|
||||
transforms (List[SpeckleTransform]): A list of Speckle Transform objects.
|
||||
|
||||
Returns:
|
||||
np.ndarray: A combined 4x4 transformation matrix.
|
||||
"""
|
||||
combined_matrix = np.identity(4)
|
||||
for transform in transforms:
|
||||
matrix = convert_speckle_transform_to_matrix(transform)
|
||||
combined_matrix = np.dot(combined_matrix, matrix)
|
||||
return combined_matrix
|
||||
|
||||
|
||||
def convert_speckle_transform_to_matrix(transform: SpeckleTransform) -> np.ndarray:
|
||||
"""
|
||||
Convert a Speckle Transform object to a 4x4 NumPy matrix.
|
||||
|
||||
Args:
|
||||
transform (SpeckleTransform): The Speckle Transform object.
|
||||
|
||||
Returns:
|
||||
np.ndarray: A 4x4 transformation matrix.
|
||||
"""
|
||||
return np.array(transform.value).reshape(4, 4)
|
||||
+74
-96
@@ -1,107 +1,85 @@
|
||||
from typing import Tuple, Optional
|
||||
from typing import Union, Type
|
||||
|
||||
try:
|
||||
import pymesh
|
||||
except ImportError:
|
||||
pymesh = None
|
||||
|
||||
import trimesh
|
||||
|
||||
|
||||
def trimesh_to_pymesh(mesh: trimesh.Trimesh) -> pymesh.Mesh:
|
||||
"""
|
||||
Convert a Trimesh object to a Pymesh object.
|
||||
Args:
|
||||
mesh (Trimesh): The Trimesh object to convert.
|
||||
Returns:
|
||||
pymesh.Mesh: The resulting Pymesh object.
|
||||
"""
|
||||
return pymesh.form_mesh(mesh.vertices, mesh.faces)
|
||||
|
||||
|
||||
def pymesh_to_trimesh(mesh: pymesh.Mesh) -> trimesh.Trimesh:
|
||||
"""
|
||||
Convert a Pymesh object to a Trimesh object.
|
||||
Args:
|
||||
mesh (pymesh.Mesh): The Pymesh object to convert.
|
||||
Returns:
|
||||
trimesh.Trimesh: The resulting Trimesh object.
|
||||
"""
|
||||
return trimesh.Trimesh(vertices=mesh.vertices, faces=mesh.faces)
|
||||
|
||||
|
||||
def cast(
|
||||
mesh: Union[trimesh.Trimesh, pymesh.Mesh], target_type: Type
|
||||
) -> Union[trimesh.Trimesh, pymesh.Mesh]:
|
||||
"""
|
||||
Casts a mesh object to a specified type.
|
||||
|
||||
Args:
|
||||
mesh (Union[trimesh.Trimesh, pymesh.Mesh]): The mesh object to cast.
|
||||
target_type (Type): The type to cast the mesh to.
|
||||
|
||||
Returns:
|
||||
Union[trimesh.Trimesh, pymesh.Mesh]: The cast mesh object.
|
||||
"""
|
||||
|
||||
if isinstance(mesh, trimesh.Trimesh) and target_type is pymesh.Mesh:
|
||||
return trimesh_to_pymesh(mesh)
|
||||
elif isinstance(mesh, pymesh.Mesh) and target_type is trimesh.Trimesh:
|
||||
return pymesh_to_trimesh(mesh)
|
||||
else:
|
||||
raise TypeError("Unsupported mesh type or target type.")
|
||||
|
||||
|
||||
import numpy as np
|
||||
import trimesh
|
||||
from specklepy.objects import Base
|
||||
from specklepy.objects.geometry import Mesh as SpeckleMesh
|
||||
from specklepy.objects.other import Transform
|
||||
from trimesh import Trimesh
|
||||
from specklepy.objects.geometry import Mesh as SpeckleMesh, Vector
|
||||
|
||||
|
||||
class Element:
|
||||
def __init__(self, id, meshes):
|
||||
"""
|
||||
Initialize an Element object with an ID and a list of meshes.
|
||||
def speckle_mesh_to_trimesh(input_mesh: SpeckleMesh) -> trimesh.Trimesh:
|
||||
vertices = np.array(input_mesh.vertices).reshape((-1, 3))
|
||||
faces = []
|
||||
|
||||
Args:
|
||||
id (str): The ID of the Element.
|
||||
meshes (List[Trimesh]): List of trimesh Mesh objects.
|
||||
"""
|
||||
self.id = id
|
||||
self.meshes = meshes
|
||||
i = 0
|
||||
while i < len(input_mesh.faces):
|
||||
face_vertex_count = input_mesh.faces[i]
|
||||
i += 1 # Skip the vertex count
|
||||
|
||||
face_vertex_indices = input_mesh.faces[i : i + face_vertex_count]
|
||||
|
||||
def speckle_transform_to_trimesh_matrix(transform: Transform) -> np.ndarray:
|
||||
"""
|
||||
Convert the Speckle Transform matrix to a NumPy array format suitable for trimesh.
|
||||
face_vertices = [
|
||||
Vector.from_list(vertices[idx].tolist()) for idx in face_vertex_indices
|
||||
]
|
||||
|
||||
Returns:
|
||||
np.ndarray: 4x4 transformation matrix in NumPy array format.
|
||||
"""
|
||||
return np.array(transform.value).reshape(4, 4)
|
||||
if face_vertex_count == 3:
|
||||
faces.append(face_vertex_indices)
|
||||
else:
|
||||
triangulated = triangulate_face(face_vertices)
|
||||
faces.extend(
|
||||
[[face_vertex_indices[idx] for idx in tri] for tri in triangulated]
|
||||
)
|
||||
|
||||
i += face_vertex_count
|
||||
|
||||
def speckle_to_element(
|
||||
base_with_transforms: Tuple[Base, str, Optional[Transform]]
|
||||
) -> Element:
|
||||
"""
|
||||
Convert a SpecklePy Base object and its associated Transform to an Element object.
|
||||
|
||||
Args:
|
||||
base_with_transforms (tuple): Contains a SpecklePy Base object and its
|
||||
associated Transform object.
|
||||
|
||||
Returns:
|
||||
Element: The resulting Element object.
|
||||
"""
|
||||
|
||||
# Unpack the tuple to get the base, speckle ID, and transform.
|
||||
base, speckle_id, transform = base_with_transforms
|
||||
|
||||
# To convert the Base object to a trimesh Mesh, use the displayValue property.
|
||||
# This property provides the display mesh, expected to be an iterable of
|
||||
# SpecklePy Mesh objects. However, legacy objects might be a single mesh.
|
||||
display_value = base.displayValue
|
||||
if isinstance(display_value, SpeckleMesh):
|
||||
display_value = [display_value]
|
||||
|
||||
if isinstance(display_value, list):
|
||||
# Initialize an Element with an empty list of meshes.
|
||||
element = Element(speckle_id, meshes=[])
|
||||
|
||||
for mesh in display_value:
|
||||
if mesh:
|
||||
# Convert the SpecklePy Mesh to a trimesh Mesh.
|
||||
t_mesh = speckle_to_trimesh(mesh)
|
||||
if not isinstance(t_mesh, Trimesh):
|
||||
continue
|
||||
|
||||
# If there's a transform, apply it to the trimesh Mesh.
|
||||
if transform is not None:
|
||||
trimesh_matrix = speckle_transform_to_trimesh_matrix(transform)
|
||||
t_mesh.apply_transform(trimesh_matrix)
|
||||
|
||||
# Append the trimesh Mesh to the Element's list of meshes.
|
||||
element.meshes.append(t_mesh)
|
||||
|
||||
return element
|
||||
|
||||
|
||||
def speckle_to_trimesh(speckle_mesh: SpeckleMesh) -> Trimesh:
|
||||
"""
|
||||
Convert a SpecklePy Mesh to a trimesh Mesh object.
|
||||
|
||||
Args:
|
||||
speckle_mesh: The SpecklePy Mesh to convert.
|
||||
|
||||
Returns:
|
||||
trimesh.Trimesh: The resulting trimesh Mesh object.
|
||||
"""
|
||||
|
||||
# Convert the list of vertices to a numpy array. Reshape it to
|
||||
# (num_vertices, 3) to fit the trimesh format.
|
||||
vertices_array = np.array(speckle_mesh.vertices).reshape((-1, 3))
|
||||
|
||||
# Faces are expected to be triangular. Reshape the faces list accordingly.
|
||||
|
||||
# Convert the faces list to a numpy array
|
||||
faces_array_raw = np.array(speckle_mesh.faces)
|
||||
|
||||
# Remove the leading 3s by skipping every 4th value
|
||||
faces_cleaned = np.delete(faces_array_raw, np.arange(0, faces_array_raw.size, 4))
|
||||
|
||||
# Reshape the array into (-1, 3) shape
|
||||
faces_array = faces_cleaned.reshape((-1, 3))
|
||||
|
||||
# Return a new trimesh object using the reshaped vertices and faces.
|
||||
return trimesh.Trimesh(vertices=vertices_array, faces=faces_array)
|
||||
return trimesh.Trimesh(vertices=vertices, faces=np.array(faces))
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
import pymesh
|
||||
import numpy as np
|
||||
|
||||
vertices = [
|
||||
[0, 0, 0],
|
||||
[1, 0, 0],
|
||||
[1, 1, 0],
|
||||
[0, 1, 0]
|
||||
]
|
||||
|
||||
faces = [
|
||||
[0, 1, 2],
|
||||
[0, 2, 3]
|
||||
]
|
||||
|
||||
mesh = pymesh.form_mesh(np.array(vertices), faces)
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
use the automation_context module to wrap your function in an Automate context helper
|
||||
"""
|
||||
from typing import List, Optional, Tuple
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import Field
|
||||
from speckle_automate import (
|
||||
@@ -16,9 +16,9 @@ from specklepy.objects import Base
|
||||
from specklepy.objects.other import Transform
|
||||
from specklepy.objects.units import Units
|
||||
from specklepy.transports.server import ServerTransport
|
||||
from trimesh import Trimesh
|
||||
|
||||
from Geometry.mesh import speckle_to_element, Element
|
||||
from Geometry.clash import detect_and_report_clashes
|
||||
from Geometry.element import speckle_to_element
|
||||
from Rules.checks import ElementCheckRules
|
||||
from Utilities.flatten import extract_base_and_transform
|
||||
|
||||
@@ -31,18 +31,6 @@ class FunctionInputs(AutomateBase):
|
||||
https://docs.pydantic.dev/latest/usage/models/
|
||||
"""
|
||||
|
||||
tolerance: float = Field(
|
||||
default=25.0,
|
||||
title="Tolerance",
|
||||
description="Specify the tolerance value for the analysis. \
|
||||
Negative values relaxes the test, positive values make it more strict.",
|
||||
)
|
||||
tolerance_unit: str = Field( # Using the SpecklePy Units enum here
|
||||
default=Units.mm,
|
||||
json_schema_extra={"examples": ["mm", "cm", "m"]},
|
||||
title="Tolerance Unit",
|
||||
description="Unit of the tolerance value.",
|
||||
)
|
||||
static_model_name: str = Field(
|
||||
...,
|
||||
title="Static Model Name",
|
||||
@@ -50,6 +38,22 @@ class FunctionInputs(AutomateBase):
|
||||
)
|
||||
|
||||
|
||||
tolerance: float = Field(
|
||||
default=25.0,
|
||||
title="Tolerance",
|
||||
description="Specify the tolerance value for the analysis. \
|
||||
Negative values relaxes the test, positive values make it more strict.",
|
||||
readonly=True,
|
||||
)
|
||||
tolerance_unit: str = Field( # Using the SpecklePy Units enum here
|
||||
default=Units.mm,
|
||||
json_schema_extra={"examples": ["mm", "cm", "m"]},
|
||||
title="Tolerance Unit",
|
||||
description="Unit of the tolerance value.",
|
||||
readonly=True,
|
||||
)
|
||||
|
||||
|
||||
def automate_function(
|
||||
automate_context: AutomationContext,
|
||||
function_inputs: FunctionInputs,
|
||||
@@ -128,76 +132,41 @@ def automate_function(
|
||||
# using trimesh library process all these meshes in the form of A vs B
|
||||
# and get the clashes
|
||||
|
||||
clashes = detect_clashes(
|
||||
reference_mesh_elements, latest_mesh_elements, function_inputs.tolerance
|
||||
clashes = detect_and_report_clashes(
|
||||
reference_mesh_elements, latest_mesh_elements, tolerance, automate_context
|
||||
)
|
||||
|
||||
print(len(clashes))
|
||||
# all object count
|
||||
# all reference objects count
|
||||
# all latest objects count
|
||||
|
||||
automate_context.mark_run_success(status_message="Clash detection completed.")
|
||||
percentage_reference_objects_clashing = (
|
||||
len(set([ref_id for ref_id, latest_id, severity in clashes]))
|
||||
/ len(reference_mesh_elements)
|
||||
* 100
|
||||
)
|
||||
percentage_latest_objects_clashing = (
|
||||
len(set([latest_id for ref_id, latest_id, severity in clashes]))
|
||||
/ len(latest_mesh_elements)
|
||||
* 100
|
||||
)
|
||||
|
||||
# all clashes count
|
||||
all_objects_count = len(reference_mesh_elements) + len(latest_mesh_elements)
|
||||
all_clashes_count = len(clashes)
|
||||
|
||||
clash_report_message = (
|
||||
f"Clash detection report: {all_clashes_count} clashes found "
|
||||
f"between {all_objects_count} objects. "
|
||||
f"Percentage of reference objects clashing: "
|
||||
f"{percentage_reference_objects_clashing}%. "
|
||||
f"Percentage of latest objects clashing: "
|
||||
f"{percentage_latest_objects_clashing}%."
|
||||
)
|
||||
|
||||
def detect_clashes(
|
||||
elements_a: List[Element], elements_b: List[Element], length_tolerance: float
|
||||
) -> List[Tuple[Element, Element]]:
|
||||
"""
|
||||
Detects clashes between two sets of elements with a specified tolerance.
|
||||
|
||||
This function checks each combination of elements from `elements_a` and `elements_b`
|
||||
to see if any of their respective meshes intersect within the specified tolerance.
|
||||
If a clash is detected between any mesh from an element in `elements_a` and any mesh
|
||||
from an element in `elements_b`, the pair of elements is added to the results.
|
||||
|
||||
Args:
|
||||
- elements_a (List[Element]): A list of `Element` objects to be checked for clashes.
|
||||
- elements_b (List[Element]): A second list of `Element` objects to be checked for clashes against `elements_a`.
|
||||
- length_tolerance (float): The distance to offset mesh vertices for intersection check.
|
||||
|
||||
Returns:
|
||||
- List[Tuple[Element, Element]]: A list of tuples where each tuple contains a pair of `Element` objects that clash.
|
||||
"""
|
||||
|
||||
# Use list comprehension to get pairs of elements that have clashing meshes
|
||||
clashes = [
|
||||
(element_a, element_b)
|
||||
for element_a in elements_a
|
||||
for element_b in elements_b
|
||||
if any(
|
||||
check_intersection_with_tolerance(mesh_a, mesh_b, length_tolerance)
|
||||
for mesh_a in element_a.meshes
|
||||
for mesh_b in element_b.meshes
|
||||
)
|
||||
]
|
||||
|
||||
return clashes
|
||||
|
||||
|
||||
def check_intersection_with_tolerance(
|
||||
mesh_a: Trimesh, mesh_b: Trimesh, tolerance: float
|
||||
) -> bool:
|
||||
"""
|
||||
Checks for intersections between two meshes within a specified tolerance.
|
||||
|
||||
Args:
|
||||
- mesh_a: The first mesh to check.
|
||||
- mesh_b: The second mesh to check.
|
||||
- tolerance (float): The distance to offset mesh vertices for intersection check.
|
||||
Positive values expand the mesh, negative values contract it.
|
||||
|
||||
Returns:
|
||||
- bool: True if the meshes intersect within the specified tolerance, otherwise False.
|
||||
"""
|
||||
half_tolerance = tolerance / 2.0 # TODO: how to shrink bloat mesh?
|
||||
offset_mesh_a: Trimesh = mesh_a # mesh_a.offset_mesh(half_tolerance)
|
||||
offset_mesh_b: Trimesh = mesh_b # mesh_b.offset_mesh(half_tolerance)
|
||||
|
||||
# return offset_mesh_a.intersection(offset_mesh_b).volume > 0 TODO: Install Blender as the engine
|
||||
|
||||
# return a random boolean for testing - significantly favouring false
|
||||
import random
|
||||
|
||||
return random.random() < 0.05
|
||||
automate_context.mark_run_success(
|
||||
status_message="Clash detection completed. " + clash_report_message
|
||||
)
|
||||
|
||||
|
||||
def get_reference_model(
|
||||
|
||||
Reference in New Issue
Block a user