Files
speckle-aectech-masterclass/server/mesh_diff.py
T
2021-10-31 20:59:49 +01:00

325 lines
13 KiB
Python

"""SpeckleMeshDiff for AEC Tech Masterclass"""
from typing import List
import math
from specklepy.api import operations
from specklepy.api.client import SpeckleClient
from specklepy.api.models import Branch
from specklepy.transports.server import ServerTransport
from specklepy.objects.geometry import Brep, Point, Mesh
from specklepy.objects import Base
from specklepy.objects.other import RenderMaterial
COLORS = [-6426, -13108, -19790, -26215, -
32640, -39322, -45747, -52429, -59111, -65536]
WHITE = -1
class SpeckleMeshDiff:
"""Class to handle diffing between commits in a stream."""
client: SpeckleClient = None
host: str = None
diff_branch: str = None
commit_prev: str = None
commit_current: str = None
stream_id: str = None
def __init__(self, token: str, host: str = "https://speckle.xyz", diff_branch: str = "diff"):
self.host = host
self.diff_branch = diff_branch
self.client = SpeckleClient(host=self.host)
self.client.authenticate(token=token)
def process_diff(self, stream_id: str, commit_current: str, commit_previous: str):
"""
Process a diff operation between the specified
'current' commit and the 'previous' one.
"""
# Set the global variables
self.stream_id = stream_id
self.commit_current = commit_current
self.commit_prev = commit_previous
print("Did not find existing diff, fetching commits now....")
# get meshes from commits
previous_commit = self.receive_data(
self.client, self.stream_id, self.commit_prev)
previous_meshes = self.get_all_meshes(previous_commit)
current_commit = self.receive_data(
self.client, self.stream_id, self.commit_current)
current_meshes = self.get_all_meshes(current_commit)
print("Comparing meshes...")
diff_base = self.compare_meshes(current_meshes, previous_meshes)
print("Diffing was successfull, sending to Speckle")
diff_commit_id = self.send_data(
self.client,
self.stream_id,
self.diff_branch,
diff_base,
self.commit_current + "-" + self.commit_prev)
print("Successfully sent data to Speckle")
return self.client.commit.get(self.stream_id, diff_commit_id)
# diff_url = f"{self.host}/streams/{self.stream_id}/commits/{diff_commit_id}"
# return diff_url
def check_existing_commits(self) -> bool or None:
"""Checks if a specific diff commit already exists in the diff_branch"""
branch_commits: Branch = self.client.branch.get(
self.stream_id, self.diff_branch, 50)
for commit in branch_commits.commits.items:
if commit.message == f"{self.commit_current}-{self.commit_prev}":
return commit
return None
def compare_meshes(self, current_meshes: List[Mesh], previous_meshes: List[Mesh]) -> Base:
"""
Compares the meshes from the first commit against the second, and sends the result to the `diff` branch.
It returns the commit url of the diff.
"""
# pre process meshes in the current commit to check for same object ID (this means obj hasn't changed) - skip these
# if object id has changed, check for application id - if these are the same, compare these objects directly
matched_current_indices = []
matched_previous_indices = []
paired_current_indices = []
paired_previous_indices = []
for i in range(0, len(current_meshes), 1):
for j in range(0, len(previous_meshes), 1):
if current_meshes[i][1] == previous_meshes[j][1]:
matched_current_indices.append(i)
matched_previous_indices.append(j)
break
elif current_meshes[i][2] == previous_meshes[j][2]:
paired_current_indices.append(i)
paired_previous_indices.append(j)
break
# remove matched previous meshes and matched pairs and get list of all mesh points from processed list
# this will be used as reference for all meshes that have changed and don't have a specific match to compare to
previous_meshes_ref_pool = []
for i in range(0, len(previous_meshes), 1):
if matched_previous_indices.__contains__(i) or paired_previous_indices.__contains__(i):
continue
previous_meshes_ref_pool.append(previous_meshes[i][0])
ref_pool = self.get_all_points(previous_meshes_ref_pool)
# create a ghosted render material
ghosted = RenderMaterial()
ghosted.diffuse = WHITE
ghosted.opacity = 0.1
# for each mesh in the current commit, compare mesh vertices with ref pool or matched pair to determine scale of change
diff_meshes = []
same_meshes = []
ref_meshes = []
diff_mesh_pairs = []
diff_mesh_ref_indices = [] # the corresponding ref pair mesh to diff mesh pairs
for i in range(0, len(current_meshes), 1):
mesh = current_meshes[i][0]
# send matched current meshes with rendermaterial semi-transparent (ghosted)
if matched_current_indices.__contains__(i):
mesh.renderMaterial = ghosted
same_meshes.append(mesh)
continue
diff_mesh = mesh
vertices = self.get_all_points([mesh])
diff_mesh_colors = [WHITE] * (len(vertices))
diff_values = []
# check for pairing
paired_mesh_points = []
paired_ref_mesh_index = None
is_paired = False
if paired_current_indices.__contains__(i):
paired_ref_mesh_index = paired_previous_indices[paired_current_indices.index(
i)]
paired_mesh_points = self.get_all_points(
[previous_meshes[paired_ref_mesh_index][0]])
is_paired = True
for vertex in vertices:
if is_paired:
diff_values.append(self.find_closest_point(
vertex, paired_mesh_points))
else:
diff_values.append(
self.find_closest_point(vertex, ref_pool))
# determine color value for vertex by remapping domain
changed = False
bin_size = max(diff_values) / len(COLORS)
for i in range(0, len(vertices), 1):
if diff_values[i] == 0:
continue
else:
index = math.floor(diff_values[i] / bin_size)
if index == len(COLORS):
index -= 1
diff_mesh_colors[i] = COLORS[index]
changed = True
if not changed: # if hasn't changed, append to same list
mesh.renderMaterial = ghosted
if is_paired:
matched_previous_indices.append(paired_ref_mesh_index)
same_meshes.append(mesh)
else: # set colors and add mesh to diff list or paired diff list
diff_mesh.colors = diff_mesh_colors
if is_paired:
diff_mesh_pairs.append(diff_mesh)
diff_mesh_ref_indices.append(paired_ref_mesh_index)
else:
diff_meshes.append(diff_mesh)
# process reference meshes
diff_mesh_refs = []
for j in range(0, len(previous_meshes)):
# skip matched reference meshes and paired refs
if matched_previous_indices.__contains__(j) or diff_mesh_ref_indices.__contains__(j):
continue
mesh = previous_meshes[j][0]
mesh.renderMaterial = ghosted
ref_meshes.append(mesh)
for diff_mesh_ref_index in diff_mesh_ref_indices:
mesh = previous_meshes[diff_mesh_ref_index]
if len(mesh) > 3:
diff_mesh_refs.append(mesh[3])
else:
diff_mesh_refs.append(mesh[0])
# Construct diff base object to return
base = Base()
base.units = current_meshes[0][0].units
base["changed"] = diff_meshes
for i in range(0, len(diff_mesh_pairs), 1):
layer = f"changed::{i}"
base[f"{layer}::changed"] = [diff_mesh_pairs[i]]
base[f"{layer}::ref"] = [diff_mesh_refs[i]]
base["same"] = same_meshes
base["ref"] = ref_meshes
return base
@staticmethod
def send_data(client: SpeckleClient, stream_id: str, branch: str, diff_object: Base, message: str) -> str:
"""Sends a Base object to a specified branch"""
# create a branch if necessary
branches = client.branch.list(stream_id)
has_res_branch = any(b.name == branch for b in branches)
if not has_res_branch:
client.branch.create(
stream_id, name=branch, description="This branch was created by the AEC Tech Masterclass App"
)
transport = ServerTransport(
client=client, stream_id=stream_id)
object_id = operations.send(base=diff_object, transports=[transport])
commit_id = client.commit.create(
stream_id,
object_id, # object id
branch,
message
)
return commit_id
@staticmethod
def receive_data(client: SpeckleClient, stream_id: str, commit_id: str) -> Base:
"""Get the data from a commit on the Speckle server"""
transport = ServerTransport(client, stream_id)
commit = client.commit.get(stream_id, commit_id)
res = operations.receive(commit.referencedObject, transport)
# if grasshopper, will be nested under data: res["data"]
# if rhino/autocad/revit, will be sent with layers or categories
return res
@staticmethod
def get_all_meshes(child: Base) -> List[Mesh]:
"""Returns all the meshes from a given Base object."""
meshes = []
names = child.get_dynamic_member_names()
for name in names:
prop = child[name]
if isinstance(prop, Base):
if isinstance(prop, Brep):
if not hasattr(prop, "displayMesh"):
break
meshes.append((prop.displayMesh, prop.id,
prop.applicationId, prop))
elif isinstance(prop, Mesh):
meshes.append((prop, prop.id, prop.applicationId))
elif isinstance(prop, list):
for p in prop:
if isinstance(p, Brep):
if not hasattr(p, "displayMesh"):
break
meshes.append(
(p.displayMesh, p.id, p.applicationId, p))
elif isinstance(p, Mesh):
meshes.append((p, p.id, p.applicationId))
elif isinstance(p, list):
for subp in p:
if isinstance(subp, Brep):
if not hasattr(subp, "displayMesh"):
break
meshes.append(
(subp.displayMesh, subp.id, subp.applicationId, subp))
elif isinstance(subp, Mesh):
meshes.append(
(subp, subp.id, subp.applicationId))
elif isinstance(subp, Base):
meshes += SpeckleMeshDiff.get_all_meshes(subp)
return meshes
@staticmethod
def get_all_points(meshes: List[Mesh]) -> List[Point]:
"""Returns a flat list of vertices of all the meshes in a list"""
points = []
for mesh in meshes:
for i in range(2, len(mesh.vertices), 3):
point = Point()
point.x = mesh.vertices[i-2]
point.y = mesh.vertices[i-1]
point.z = mesh.vertices[i]
points.append(point)
return points
@staticmethod
def find_point(current: Point, points: List[Point]) -> bool:
"""Attempts to find a specific point in a list. Returns True if successful"""
for point in points:
if (point.x == current.x and point.y == current.y and point.z == current.z):
return True
return False
@staticmethod
def find_closest_point(current: Point, points: List[Point]):
"""Find the closest point to a target given a list of points"""
smallest_distance = None
for point in points:
d = ((current.x - point.x)**2 + (current.y - point.y)
** 2 + (current.z - point.z)**2)**0.5
if smallest_distance is not None:
if d > smallest_distance:
continue
smallest_distance = d
return smallest_distance