Files
design2garmentcode-impl/pygarment/meshgen/boxmeshgen.py
2025-07-03 17:03:00 +08:00

1630 lines
72 KiB
Python

"""
Module contains classes needed to generate a box
mesh from patterns and simulate it using warp
"""
#Basic
import igl
import numpy as np
import math
import svgpathtools as svgpath
import matplotlib.pyplot as plt
import shutil
import pickle
from pathlib import Path
import yaml
from typing import List, Dict, Tuple
#Personal Modules
import pygarment.pattern.core as core
import pygarment.pattern.wrappers as wrappers
from pygarment.pattern import rotation as rotation_tools
import pygarment.pattern.utils as pat_utils
import pygarment.meshgen.triangulation_utils as tri_utils
from pygarment.meshgen.sim_config import PathCofig
from pygarment.meshgen.render.texture_utils import texture_mesh_islands, save_obj
# TODOLOW Some stitching errors are not getting detected
# SECTION -- Errors
class PatternLoadingError(BaseException):
"""To be raised when a pattern cannot be loaded correctly to 3D"""
pass
class MultiStitchingError(BaseException):
"""To be raised when a panel edge is stitched together with more than one other edge"""
pass
class StitchingError(BaseException):
"""To be raised when a one cannot find successfull stitching sequence"""
pass
class DegenerateTrianglesError(BaseException):
"""To be raised when panel meshing produces degenrate triangles"""
pass
class NormError(BaseException):
"""To be raised when a panel norm is NAN"""
pass
# !SECTION
# SECTION Mesh objects
class Panel:
"""
Represents a panel of the pattern:
Input:
* panel: panel information
* panelName: panel name
"""
def __init__(self, panel, panelName, mesh_resolution):
self.panel_name = panelName
self.translation = np.asarray(panel['translation'])
self.rotation = np.asarray(panel['rotation'])
self.corner_vertices = np.asarray(panel['vertices'])
self.panel_vertices = []
self.panel_faces = []
self.edges: List[Edge] = []
self.n_stitches = 0 #needed later to decide whether vertex is stitch vertex or not
self.glob_offset = -1
for edge in np.asarray(panel['edges']):
edge_obj = Edge(edge, self.corner_vertices, mesh_resolution)
self.edges.append(edge_obj)
self.norm = []
def _verts(self, lin_edges):
"""
This function takes a sequence of linear edges and processes them to extract unique vertices.
Input:
* self (Panel object): Instance of Panel class from which the function is called
* lin_edges (list): Sequence of edges defined by their start and end vertices
Output:
* verts (list): List of unique vertices extracted from lin_edges, arranged in the order they were encountered
"""
verts = [lin_edges[0][0]]
for e in lin_edges:
if not np.array_equal(e[0], verts[-1]): # avoid adding the vertices of chained edges twice
verts.append(e[0])
verts.append(e[1])
if np.array_equal(verts[0], verts[-1]): # don't double count the loop origin
verts.pop(-1)
return verts
def _bbox(self, verts_2d):
"""
This function evaluates the 2D bounding box of the current panel and returns the panel vertices which are
located on the bounding box (b_points) as well as the mean point of b_points in 3D.
Input:
* self (Panel object): Instance of Panel class from which the function is called
* verts_2d (list): List of 2D panel edge vertices ordered in a loop
Output:
* b_points_mean_3d (ndarray): 3D vertex representing the rotated and translated mean point of b_points,
i.e., the vertices of verts_2d located on the bounding box
* b_points_3d (ndarray): Ndarray of 3D vertices representing the rotated and translated b_points, i.e.,
the vertices of verts_2d located on the bounding box
"""
verts_2d_arr = np.array(verts_2d)
mi = verts_2d_arr.min(axis=0)
ma = verts_2d_arr.max(axis=0)
xs = [mi[0],ma[0]]
ys = [mi[1],ma[1]]
#return points on bounding box
b_points = []
for v in verts_2d_arr:
if v[0] in xs or v[1] in ys:
b_points.append(v)
if len(b_points) == 2:
if not any(np.array_equal(arr, mi) for arr in b_points):
b_points = [b_points[0], mi, b_points[1]]
else:
p = [mi[0],ma[1]]
b_points = [b_points[0],p,b_points[1]]
elif len(b_points) < 2:
raise PatternLoadingError("Less than two vertices defining bounding box")
b_points_3d = self.rot_trans_panel(b_points)
b_points_mean_2d = np.mean((b_points),axis=0)
b_points_mean_3d = self.rot_trans_vertex(b_points_mean_2d)
plot_pts = b_points + [b_points_mean_2d]
# self.plot(plot_pts, f"{self.panel_name} BBOX")
return b_points_mean_3d, b_points_3d
def plot(self, pts, title):
"""
This function creates a scatter plot of points (used for debugging).
Input:
* self (Panel object): Instance of Panel class from which the function is called
* pts (list): Points to be plotted
* title (str): Title of the scatter plot
"""
pts = np.array(pts)
x_values = pts.T[0]
y_values = pts.T[1]
plt.scatter(x_values, y_values, c='blue', marker='o', label='Data Points')
# Annotate the data points with text
for i in range(len(x_values)):
plt.annotate(f'{i}', (x_values[i], y_values[i]), textcoords="offset points", xytext=(0, 5), ha='center')
# Customize the plot (optional)
plt.title(title)
plt.xlabel('X-axis')
plt.ylabel('Y-axis')
plt.legend()
# plt.axis('square')
# Show the plot
plt.show()
def set_panel_norm(self):
"""
This function computes the normal direction of the current panel.
Input:
* self (Panel object): Instance of Panel class from which the function is called
"""
# Take linear version of the edges
# To correctly process edges with extreme curvatures
lin_edges = []
for e in self.edges:
lin_edges += list(e.linearize(self))
verts = self._verts(lin_edges)
# self.plot(verts, f"{self.panel_name} VERTS")
center_3d, verts_3d = self._bbox(verts)
norms = []
num_verts_3d = len(verts_3d)
for i in range(num_verts_3d):
vert_0 = verts_3d[i]
vert_1 = verts_3d[(i+1) % num_verts_3d]
# Pylance + NP error for unreachanble code -- see https://github.com/numpy/numpy/issues/22146
# Works ok for numpy 1.23.4+
norm = np.cross(vert_0-center_3d, vert_1-center_3d)
norm /= np.linalg.norm(norm)
norms.append(norm)
# Current norm direction
avg_norm = sum(norms) / len(norms)
#final_norm = list(avg_norm / np.linalg.norm(avg_norm)) #before
if np.linalg.norm(avg_norm) == 0 or np.any(np.isnan(avg_norm)):
raise NormError(f"{self.__class__.__name__}::ERROR::invalid panel norm for {self.panel_name}; "
f"norms: {norms}; avg_norm: {avg_norm}")
else:
final_norm = list(avg_norm / np.linalg.norm(avg_norm))
#solve float errors
for i, ni in enumerate(final_norm):
if np.isclose([ni], [0.0]):
final_norm[i] = 0.0
self.norm = final_norm
def rot_trans_vertex(self, vertex):
"""
This function transforms a 2D vertex into a 3D vertex by rotating it with
respect to the XYZ Euler angles and applying the specified translation.
Input:
* self (Panel object): Instance of Panel class from which the function is called
* vertex (numpy array): Coorindates of the 2D vertex to be transformed
Output:
* r_t_vertex (numpy array): Coordinates of the rotated and translated 3D vertex
"""
rot_matrix = rotation_tools.euler_xyz_to_R(self.rotation)
r_t_vertex = BoxMesh._point_in_3D(vertex, rot_matrix, self.translation)
return r_t_vertex
def rot_trans_panel(self, vertices):
"""
This function transforms multiple 2D vertices into 3D vertices by rotating them with
respect to the XYZ Euler angles and applying the specified translation.
Input:
* self (Panel object): Instance of Panel class from which the function is called
* vertices (numpy ndarray): Coorindates of the 2D vertices to be transformed
Output:
* r_t_vertices (numpy ndarray): Coordinates of the rotated and translated 3D vertices
"""
if len(vertices) == 0:
return []
rot_matrix = rotation_tools.euler_xyz_to_R(self.rotation)
r_t_vertices = np.vstack(tuple([BoxMesh._point_in_3D(v, rot_matrix, self.translation) for v in np.array(vertices)]))
return r_t_vertices
def _get_exist_idx(self, find_list):
"""
This function returns the index of find_list (start or end vertex) in panel.panel_vertices.
If find_list is not in panel.panel_vertices, find_list is first added to panel.panel_vertices.
Input:
* self (Panel object): Instance of Panel class from which the function is called
* find_list (ndarray): Either start or end vertex of an edge
Output:
* (int): Index of find_list (start or end vertex) in panel.panel_vertices
"""
pvertices = np.array(self.panel_vertices)
len_pvertices = len(pvertices)
if len_pvertices == 0:
self.panel_vertices.append(find_list)
return 0
else:
index = np.where(np.all(pvertices == find_list, axis=1))
n_found_indices = len(index[0])
if n_found_indices == 1: # get index
return index[0][0]
elif n_found_indices == 0:
self.panel_vertices.append(find_list)
return len(self.panel_vertices) - 1
else: #n_found_indices > 1
raise PatternLoadingError(
f'{self.__class__.__name__}::{self.name}::Corner stitch vertex has been added more than once to panel vertices!')
def store_edge_verts(self, edge, edge_in_vertices):
"""
This function stores the panel.panel_vertices indices of the "start" vertex,
"edge_in_vertices" vertices, and "end" vertex of edge into edge.vertex_range
Input:
* self (Panel object): Instance of Panel class from which the function is called
* edge (Edge object): Instance of Edge class whose vertex indices are stored
* edge_in_vertices (list): Equally spread vertices along edge (without start and end vertex)
"""
start, end = edge.endpoints
start_index = self._get_exist_idx(start)
begin_in = len(self.panel_vertices)
end_in = begin_in + len(edge_in_vertices) # exclusive
for v in edge_in_vertices:
self.panel_vertices.append(v)
end_index = self._get_exist_idx(end)
edge.set_vertex_range(start_index, begin_in, end_in, end_index)
def sort_edges_by_stitchid(self):
"""
This function sorts the panel's edges by their edge_id (stitch edges first) and
returns them as well as the number of edges that are part of a stitch.
Input:
* self (Panel object): Instance of Panel class from which the function is called
Output:
* n_stitch_edges (int): number of panel edges that are part of a stitch
* sorted edges (list): list containing the stitch_edges first and then the non-stitch edges
"""
edges = self.edges
stitch_edges = []
non_stitch_edges = []
for edge_id, edge in enumerate(edges):
if edge.stitch_ref is not None:
stitch_edges.append((edge_id,edge))
else:
non_stitch_edges.append((edge_id,edge))
n_stitch_edges = len(stitch_edges)
sorted_edges = stitch_edges + non_stitch_edges
return n_stitch_edges, sorted_edges
def gen_panel_mesh(self, mesh_resolution, plot=False, check=False):
"""
This function generates the vertices inside the panel using the vertices along the edges.
Input:
* self (Panel object): Instance of Panel class from which the function is called
* plot (bool): Indicates if triangle mesh should be plotted
* check (bool): Indicates if point coordiantes should be compared
Output:
* keep_pts_f (list): Vertices inside the panel (without newly inserted boundary vertices)
* f (list): Triangle faces of the panel
"""
points = self.panel_vertices
len_points = len(points)
edge_verts_ids = tri_utils.get_edge_vert_ids(self.edges)
cdt_mesh = tri_utils.Mesh_2_Constrained_Delaunay_triangulation_2()
cdt_points_mesh = tri_utils.create_cdt_points(cdt_mesh,points)
tri_utils.cdt_insert_constraints(cdt_mesh,cdt_points_mesh,edge_verts_ids)
#Meshing the triangulation with default shape criterion; i.e. sqrt(1/(4 * 0.125)) = sqrt(2)
tri_utils.CGAL_Mesh_2.refine_Delaunay_mesh_2(cdt_mesh,
tri_utils.Delaunay_mesh_size_criteria_2(0.125, 1.43 * mesh_resolution)) #1.475
if plot:
# Mark faces that are inside the domain
face_info = tri_utils.mark_domain(cdt_mesh)
tri_utils.plot_triangulation(cdt_mesh, face_info)
keep_pts_f = tri_utils.get_keep_vertices(cdt_mesh, len_points)
# Triangulate mesh without newly inserted boundary points
cdt = tri_utils.Constrained_Delaunay_triangulation_2()
cdt_points = tri_utils.create_cdt_points(cdt, keep_pts_f)
new_points = tri_utils.cdt_insert_constraints(cdt, cdt_points, edge_verts_ids)
# Faces without accidentially inserted points -- again!
# NOTE: point insertion might be a sign of degenerate triangles.
# But instead a separate check was added
f = list(tri_utils.get_face_v_ids(cdt, keep_pts_f, new_points, check=check, plot=plot))
#Store
self.panel_vertices = keep_pts_f
self.panel_faces = f
def is_manifold(self, tol=1e-2):
return tri_utils.is_manifold(
np.asarray(self.panel_faces),
np.asarray(self.panel_vertices),
tol=tol
)
def save_panel_mesh_obj(self, folder_path: Path):
"""
This function creates an obj file of the generated panel mesh and stores it to folder_path
Assumes that panel meshes have already been generated.
Input:
* self (Panel object): Instance of Panel class from which the function is called
"""
folder_path.mkdir(exist_ok=True, parents=True)
filepath = folder_path / (self.panel_name + ".obj")
v = self.rot_trans_panel(self.panel_vertices)
f = np.array(self.panel_faces)
igl.write_triangle_mesh(str(filepath), v, f)
class Edge:
"""
Represents an edge of a panel:
Input:
* edge: panel information
* vertices: panel corner vertices
"""
def __init__(self, edge, vertices, mesh_resolution):
self.endpoints = vertices[edge['endpoints']]
self.stitch_ref = None
self.n_edge_verts = -1
self.curve = None
self.init_curve(edge, mesh_resolution)
self.vertex_range = []
self.label = edge['label'] if 'label' in edge else ''
def init_curve(self, edge, mesh_resolution):
"""
Initialize curve object (svgpathtools) and set the number
of vertices on the edge (n_edge_verts) depending
on the mesh_resolution (= 1.0 => Vertices are spread with distance ~1.0 cm)
Input:
* self (Edge object): Instance of Edge class from which the function is called
* edge (dict): edge information
"""
start, end = self.endpoints
if 'curvature' in edge:
if isinstance(edge['curvature'], list) or edge['curvature']['type'] == 'quadratic': # NOTE: placeholder for old curves for backward compatibility
control_scale = edge['curvature'] if isinstance(edge['curvature'], list) else edge['curvature']['params'][0] #maya _flip_y
control_point = pat_utils.rel_to_abs_2d(start, end, control_scale)
self.curve = svgpath.QuadraticBezier(*pat_utils.list_to_c([start, control_point, end]))
elif edge['curvature']['type'] == 'circle': # Assuming circle
# https://svgwrite.readthedocs.io/en/latest/classes/path.html#svgwrite.path.Path.push_arc
radius, large_arc, right = edge['curvature']['params']
self.curve = svgpath.Arc(
pat_utils.list_to_c(start), radius + 1j * radius,
rotation=0,
large_arc=large_arc,
sweep=right, #maya: not right
end=pat_utils.list_to_c(end)
)
elif edge['curvature']['type'] == 'cubic':
cps = []
for p in edge['curvature']['params']:
control_scale = p #maya: self.flip_y(p)
control_point = pat_utils.rel_to_abs_2d(start, end, control_scale)
cps.append(control_point)
self.curve = svgpath.CubicBezier(*pat_utils.list_to_c([start, *cps, end]))
else:
raise NotImplementedError(
f'{self.__class__.__name__}::{self.name}::Unknown curvature type {edge["curvature"]["type"]}')
else:
self.curve = svgpath.Line(*pat_utils.list_to_c([start, end]))
edgelength = self.curve.length()
res = mesh_resolution
n_edge_verts = math.ceil(edgelength / res) + 1
self.n_edge_verts = n_edge_verts
if n_edge_verts == 2 and res > 1.0:
print(f'{self.__class__.__name__}::{self.name}::WARNING::Detected edge represented only by two vertices..'
'mesh resolution might be too low. resolution = {}, edge length = {}'.format(res, edgelength))
def set_vertex_range(self, start_idx, begin_in, end_in, end_idx):
"""
This function sets the vertex range of the current edge in the context of a panel.
The vertex range contains the indices into panel_vertices, defining the edge vertices with
respect to the panel_vertices.
Input:
* self (Edge object): Instance of Edge class from which the function is called
* start_idx (int): Index of edge.start into panel_vertices
* begin_in (int): Index of 2nd edge vertex into panel_vertices
* end_in (int): Index + 1 of second to last edge vertex into panel_vertices
* end_idx (int): Index of edge.end into panel_vertices
"""
self.vertex_range = [start_idx] + list(range(begin_in, end_in)) + [end_idx]
def as_curve(self, absolute=True):
"""
Returns curve as a svgpath curve object.
Converting on the fly as exact vertex location might have been updated since
the creation of the edge
Input:
* self (Edge object): Instance of Edge class from which the function is called
* absolute (bool): True if correct start and end edge vertices are processed
else use start = [0,0] and end = [1,0]
Output:
* svgpath path object: either correct curve or approximation
"""
if absolute:
# Return correct curve
return self.curve
cp = [pat_utils.c_to_np(c) for c in self.curve.bpoints()[1:-1]]
nodes = np.vstack(([0, 0], cp, [1, 0]))
params = nodes[:, 0] + 1j * nodes[:, 1]
return svgpath.QuadraticBezier(*params) if len(cp) < 2 else svgpath.CubicBezier(*params)
def linearize(self, panel):
"""
Returns the current edge (self) as a sequence of lines
Input:
* self (Edge object): Instance of Edge class from which the function is called
Output:
* (numpy ndarray): a list of vertices (start and end vertices of corresponding line)
characterizing the current edge (self)
"""
if isinstance(self.curve, svgpath.Line):
return [self.endpoints]
else:
v_range = self.vertex_range
edge_vertices = np.array(panel.panel_vertices)[v_range]
edge_seq = []
for i in range(len(edge_vertices) - 1):
pair = [edge_vertices[i], edge_vertices[i + 1]]
edge_seq.append(pair)
return edge_seq
class Seam:
def __init__(self,
panel_1_name, edge_1,
panel_2_name, edge_2,
label=None,
n_verts=None,
swap=True
):
"""
Representation of a seam in a box mesh
Input:
* panel_1_name, edge_1, panel_2_name, edge_2 -- panel edge objects and corresponding
panel names for two edges connected by the stitch
* label -- label to assing to the seam on serialisaton (default: None)
* n_verts -- number of mesh vertices sampled for the seam (default: None, not samples)
* swap -- define the edge swap for the edge pair. Default: True --
swapped -- the end of one panel edge connects to the start vertex
of the other panel edge
"""
self.panel_1, self.panel_2 = panel_1_name, panel_2_name
self.edge_1, self.edge_2 = edge_1, edge_2
self.label = label
# NOTE: default connection of stitches is edge1 end-> edge2 start
# following manifold condition
# => stitch right side to the right side of the fabric pieces
self.swap = swap # Default swap state connects right side to the right side of fabric
self.n_verts = n_verts # Number of mesh vertices
# !SECTION
# SECTION Box Mesh
class BoxMesh(wrappers.VisPattern):
"""
Extends a pattern specification in custom JSON format to generate a box mesh from the pattern
Input:
* pattern_file: pattern template in custom JSON format
"""
def __init__(self, path, res=1.0):
super(BoxMesh, self).__init__(path)
self.mesh_resolution = res #Vertices are spread with distance ~mesh_resolution cm
self.loaded = False
self.panels: Dict[str, Panel] = {}
self.stitches: List[Seam] = []
self.panelNames = self.panel_order()
self.vertices = []
self.faces = []
self.orig_lens = {}
self.verts_loc_glob = {}
self.verts_glob_loc = []
self.stitch_segmentation = []
self.vertex_normals = []
self.faces_with_texture = []
self.vertex_texture = []
self.vertex_labels = {} # Additional vertex labels coming from panel edges' labels
# SECTION -- Top level
def load(self):
"""
Loads all relevant functions and prints their time consumptions
"""
if self.is_self_intersecting():
print(f'{self.__class__.__name__}::WARNING::{self.name}::Provided pattern has self-intersecting panels. Simulation might crash')
self.load_panels()
self.gen_panel_meshes()
# NOTE: Collapse stitch vertices and store to self.vertices as well as their stitch_id to self.stitch_segmentation
self.collapse_stitch_vertices()
self.finalise_mesh()
self.loaded = True
def load_panels(self):
"""
For each panel of the pattern create a panel object and load stitching info + set number of
stitching edge vertices
Input:
* self (BoxMesh object): Instance of BoxMesh class from which the function is called
"""
all_panels = self.pattern['panels']
for panel_name in self.panelNames:
panel = Panel(all_panels[panel_name], panel_name, self.mesh_resolution)
self.panels[panel_name] = panel
#Load stitching info
self.read_stitches()
# !SECTION
# SECTION -- Stitch references in panels
def _get_stitch_edge_info(self, stitch_id, side_id) -> Tuple[str, int, Edge]:
"""
This function returns the edge defined by stitch_id and side_id
as well as its edge id and the panel name of the panel the edge is part of
Input:
* self (BoxMesh object): Instance of BoxMesh class from which the function is called
* stitch_id (int)
* side_id (int)
Output:
* panel_name (str): panel name of edge with stitch_id and side_id
* edge_id (int): id of edge with stitch_id and side_id
* edge (Edge object): Edge object of edge with stitch_id and side_id
"""
panel_name = self.pattern['stitches'][stitch_id][side_id]['panel']
edge_id = self.pattern['stitches'][stitch_id][side_id]['edge']
ret_panel = self.panels[panel_name]
try:
edge = ret_panel.edges[edge_id]
except BaseException:
print(f'{self.__class__.__name__}::ERROR::{self.name}::Provided pattern'
f' fails for stitch id {stitch_id} and {[panel_name,edge_id]}')
raise PatternLoadingError(
f'{self.__class__.__name__}::{self.name}::ERROR::Provided pattern'
f' fails for stitch id {stitch_id} and {[panel_name,edge_id]}')
else:
return panel_name, edge_id, edge
def read_stitches(self):
"""
* Load the stitching information from the spec
* Determine the number of mesh vertices to be generated on edges, s.t. they match in the stitches
"""
multi_stitches_check = []
if 'stitches' in self.pattern:
for stitch_id in range(len(self.pattern['stitches'])):
stitch_spec = self.pattern['stitches'][stitch_id]
panel_name_0, edge_id0, edge0 = self._get_stitch_edge_info(stitch_id, 0)
panel_name_1, edge_id1, edge1 = self._get_stitch_edge_info(stitch_id, 1)
stitch = Seam(panel_name_0, edge_id0, panel_name_1, edge_id1)
stitch.swap = not (len(stitch_spec) == 3 and 'right_wrong' == stitch_spec[-1])
self.stitches.append(stitch)
edge0.stitch_ref, edge1.stitch_ref = stitch, stitch
n_0, n_1 = edge0.n_edge_verts, edge1.n_edge_verts
# Assign n of longer edge
n = n_0 if edge0.curve.length() > edge1.curve.length() else n_1
edge0.n_edge_verts = n
edge1.n_edge_verts = n
stitch.n_verts = n
#---
multi_edge = [(p,e) for (p,e) in [(panel_name_0, edge_id0), (panel_name_1, edge_id1)]
if (p,e) in multi_stitches_check]
if multi_edge:
raise MultiStitchingError(
f'{self.__class__.__name__}::{self.name}::ERROR::Multi stitching'
f' detected at stitch id {stitch_id} from {multi_edge}')
else:
multi_stitches_check.append((panel_name_0, edge_id0))
multi_stitches_check.append((panel_name_1, edge_id1))
# Propagate Edge labeling
if edge0.label or edge1.label:
if edge0.label and edge1.label and edge0.label != edge1.label: # Sanity check
raise ValueError(
f'{self.__class__.__name__}::{self.name}::ERROR::Edge labels '
f'in stitch do not match: {edge0.label} and {edge1.label}')
stitch.label = edge0.label if edge0.label else edge1.label
else:
print(f'{self.__class__.__name__}::INFO::No stitching information provided')
# !SECTION
# SECTION -- generate per-panel meshes
def _get_edge_in_verts(self, edge, plot=False):
"""
This function generates the pre-defined number of vertices for each edge
Input:
* self (BoxMesh object): Instance of BoxMesh class from which the function is called
* edge (Edge object): Instance of Edge class for which the vertices are generated
* panelname (str): Name of the panel to which edge belongs to; only used if plot = True
* edge_id (int): Edge identifier; only used if plot = True
* plot (bool): If plot == True, plots edge vertices
Output:
* edge_in_vertices (list): n_edge_verts equally spread vertices along edge
"""
n = edge.n_edge_verts
edge_in_vertices = []
t_vals = np.linspace(0, 1, n)
if isinstance(edge.curve, svgpath.QuadraticBezier) or isinstance(edge.curve, svgpath.CubicBezier):
# to achieve equal spread along bezier curve
curve_lengths = np.linspace(0,1,n) * edge.curve.length()
t_vals = [edge.curve.ilength(c_len) for c_len in curve_lengths]
ts = t_vals[1:(n - 1)] # remove start and end from "inside vertices"
if isinstance(edge.curve, svgpath.Arc):
for t in ts:
p = pat_utils.c_to_np(edge.curve.point(t))
edge_in_vertices.append(p)
else:
points = edge.curve.points(ts) # faster than .point(t) but unavailable for Arc
edge_in_vertices = [pat_utils.c_to_np(p) for p in points]
if plot:
c_type = "circle"
if isinstance(edge.curve, svgpath.QuadraticBezier) or isinstance(edge.curve, svgpath.CubicBezier):
c_type = "bezier"
elif isinstance(edge.curve, svgpath.Line):
c_type = "linear"
show_verts = np.array([edge.endpoints[0]] +list(edge_in_vertices) + [edge.endpoints[1]])
lis = show_verts.T #np.array(edge_in_vertices).T
x,y = lis
x = list(x)
y = list(y)
plt.scatter(x, y)
plt.axis('square')
plt.title(c_type)
for i in range(len(show_verts)):
plt.annotate(i, (x[i], y[i]), textcoords="offset points", xytext=(0, 10), ha='center')
plt.show()
return edge_in_vertices
def gen_panel_meshes(self):
"""
For each Panel:
* For each edge generate its edge vertices and store them in panel.panel_vertices.
Further, store "start", "inside-edge", and "end" indices for each edge vertex in edge.vertex_range
* Generate vertices inside the panel and its triangles using CGAL and store them in panel.panel_vertices
and panel.panel_triangles, respectively.
Input:
* self (BoxMesh object): Instance of BoxMesh class from which the function is called
"""
for panelname in self.panelNames:
panel = self.panels[panelname]
#Sort panel.edges by stitch id
n_stitch_edges, sorted_edges = panel.sort_edges_by_stitchid()
for i,(edge_id,edge) in enumerate(sorted_edges):
#Get vertices for edge (without start, end)
edge_in_vertices = self._get_edge_in_verts(edge, plot = False)
#Store start, inside, and end vertices to Panel.panel_vertices and indices to edge.sitch_range
panel.store_edge_verts(edge, edge_in_vertices)
if i == n_stitch_edges - 1:
panel.n_stitches = len(panel.panel_vertices)# until now we only have stitch vertices in Panel.panel_vertices
#Set panel norm
panel.set_panel_norm()
#Generate panel mesh and store them in panel.panel_vertices and panel.panel_faces
panel.gen_panel_mesh(self.mesh_resolution)
# Sanity check
if not panel.is_manifold():
raise DegenerateTrianglesError(
f'{self.__class__.__name__}::ERROR::{self.name}::{panel.panel_name}:'
':panel contains degenerate triangles'
)
# !SECTION
# SECTION -- Merge mesh vertices in stitches
def _swap_stitch_ranges(self, stitch:Seam):
"""
This function returns the stitch_ranges of stitched edges in the correct order,
so that the correct edge vertices are stitched together.
Input:
* stitch -- desired stitch to be updated
Output:
* stitch_range_1 (list): Correctly ordered indices for stitch.edge_1 in stitch.panel_1
* stitch_range_2 (list): Correctly ordered indices into stitch.edge_2 in stitch.panel_2
"""
panel1, panel2 = self.panels[stitch.panel_1], self.panels[stitch.panel_2]
stitch_range_1 = panel1.edges[stitch.edge_1].vertex_range
stitch_range_2 = panel2.edges[stitch.edge_2].vertex_range
# Force existing swap
if stitch.swap:
stitch_range_1 = stitch_range_1[::-1]
return stitch_range_1, stitch_range_2
def _stitch_same_loc_vertex(self, panel1, loc_id1, glob_idx, stitch_id):
"""
This function stitches two vertices together which are exactly the same local vertex and
have not participated in a stitch so far.
To this end, self.verts_loc_glob, self.verts_glob_loc, self.vertices, and
self.stitch_segmentation are changed accordingly.
Input:
* self (BoxMesh object): Instance of BoxMesh class from which the function is called
* panel1 (Panel object): Panel object participating in stitch
* loc_id1 (int): Local identifier of a vertex into panel1.panel_vertices that is stitched together with itself
* glob_idx (int): Global index of vertex that is stitched together with itself
* stitch_id (int): Stitch identifier indicating which stitch is currently performed
"""
p1_name = panel1.panel_name
self.verts_loc_glob[(p1_name, loc_id1)] = glob_idx
self.verts_glob_loc.append([(p1_name, loc_id1)])
v_2D = panel1.panel_vertices[loc_id1]
self.vertices.append(panel1.rot_trans_vertex(v_2D))
self.stitch_segmentation.append(["stitch_" + str(stitch_id)])
def _stitch_two_diff_existent_glob_verts(self, glob1, glob2, glob_idx, stitch_id):
"""
This function stitches two vertices together where both have already participated in a stitch.
To this end, self.verts_loc_glob, self.verts_glob_loc, self.vertices, and
self.stitch_segmentation are changed accordingly.
Input:
* self (BoxMesh object): Instance of BoxMesh class from which the function is called
* glob1 (int): Global identifier of first stitch vertex into self.vertices
* glob2 (int): Global identifier of second stitch vertex into self.vertices
* glob_idx (int): Current number of vertices stored in self.vertices
* stitch_id (int): Stitch identifier indicating which stitch is currently performed
"""
glob_min = glob1 if glob1 < glob2 else glob2
glob_max = glob1 if glob1 > glob2 else glob2
panels_locids = self.verts_glob_loc[glob_max]
for p_name, loc_id in panels_locids:
self.verts_loc_glob[(p_name, loc_id)] = glob_min
repl_glob_ids = list(range(glob_max + 1, glob_idx))
panel_locids_above = np.array(self.verts_glob_loc, dtype=object)[repl_glob_ids]
for p_id in panel_locids_above:
for p_name, loc_id in p_id:
self.verts_loc_glob[(p_name, loc_id)] -= 1
curr_glob_v1 = self.vertices[glob_min]
curr_glob_v2 = self.vertices[glob_max]
self.vertices[glob_min] = np.mean([curr_glob_v1, curr_glob_v2], axis=0)
set_verts_glob_loc = set(self.verts_glob_loc[glob_min] + self.verts_glob_loc[glob_max])
self.verts_glob_loc[glob_min] = list(set_verts_glob_loc)
del self.verts_glob_loc[glob_max]
del self.vertices[glob_max]
copy_stitch_ids = self.stitch_segmentation[glob_max]
self.stitch_segmentation[glob_min] += copy_stitch_ids + ["stitch_" + str(stitch_id)]
del self.stitch_segmentation[glob_max]
def _stitch_one_existent_glob_vert(self, panel_glob, panel_not_glob, loc_id_glob, loc_id_not_glob, stitch_id):
"""
This function stitches two vertices together where only one of them has already participated in a stitch.
To this end, self.verts_loc_glob, self.verts_glob_loc, self.vertices, and
self.stitch_segmentation are changed accordingly.
Input:
* self (BoxMesh object): Instance of BoxMesh class from which the function is called
* panel_glob (Panel object): Panel object referenced by vertex that has already participated in a stitch
* panel_not glob (Panel object): Panel object referenced by vertex that has not yet participated in a stitch
* loc_id_glob (int): Local identifier of a stitch vertex into panel_glob.panel_vertices. This vertex has
already participated in a stitch.
loc_id_not_glob (int): Local identifier of a stitch vertex into panel_not_glob.panel_vertices. This vertex
has not participated in a stitch so far.
* stitch_id (int): Stitch identifier indicating which stitch is currently performed
"""
panel_name_glob = panel_glob.panel_name
panel_name_not_glob = panel_not_glob.panel_name
glob = self.verts_loc_glob[(panel_name_glob, loc_id_glob)]
self.verts_loc_glob[(panel_name_not_glob, loc_id_not_glob)] = glob
self.verts_glob_loc[glob].append((panel_name_not_glob, loc_id_not_glob))
v_2D = panel_not_glob.panel_vertices[loc_id_not_glob]
v_3D = panel_not_glob.rot_trans_vertex(v_2D)
curr_glob_v = self.vertices[glob]
self.vertices[glob] = np.mean([v_3D, curr_glob_v], axis=0)
self.stitch_segmentation[glob].append("stitch_" + str(stitch_id))
def _stitch_none_existent_glob_verts(self, panel1, panel2, loc_id1, loc_id2, glob_idx, stitch_id):
"""
This function stitches two vertices together where both of them have not yet participated in a stitch.
To this end, self.verts_loc_glob, self.verts_glob_loc, self.vertices, and
self.stitch_segmentation are changed accordingly.
Input:
* self (BoxMesh object): Instance of BoxMesh class from which the function is called
* panel1 (Panel object): Panel object referenced by the first stitch vertex
* panel2 (Panel object): Panel object referenced by the second stitch vertex
* loc_id1 (int): Local identifier of the first stitch vertex into panel1.panel_vertices
* loc_id2 (int): Local identifier of the second stitch vertex into panel2.panel_vertices
* stitch_id (int): Stitch identifier indicating which stitch is currently performed
"""
p1_name = panel1.panel_name
p2_name = panel2.panel_name
self.verts_loc_glob[(p1_name, loc_id1)] = glob_idx
self.verts_loc_glob[(p2_name, loc_id2)] = glob_idx
self.verts_glob_loc.append([(p1_name, loc_id1), (p2_name, loc_id2)])
v1_2D = panel1.panel_vertices[loc_id1]
v1_3D = panel1.rot_trans_vertex(v1_2D)
v2_2D = panel2.panel_vertices[loc_id2]
v2_3D = panel2.rot_trans_vertex(v2_2D)
self.vertices.append(np.mean([v1_3D, v2_3D], axis=0))
self.stitch_segmentation.append(["stitch_" + str(stitch_id)])
def _stitch_vertices(self):
"""
This function:
* Determines if the stitch_range of one edge has to be reversed
(so that edges which are stitched together have the same direction)
* Computes stitch vertices by taking the mean of corresponding 3D panel vertex pairs
* Stores the local to global vertex indices relationship in self.verts_loc_glob
* Stores the glboal to local vertex indices relationship in self.verts_glob_loc
* Stores the 3D stitch vertices into self.vertices
* Stores the stitch_ids to the self.stitch_segmentation list
Output:
* same_panel_stitching_dict (dict): Dictionary storying the local vertex indices to which a local vertex
of the same panel is stitched together, i.e.,
(panel_name, local_vertex_id) = [local vertex ids of same panel stiched together with local_vertex_id)
"""
# Collapse stitch vertices
same_panel_stitching_dict = {} #Store stichings of same panel (panelname,loc_id) -> loc_id
glob_idx = 0
self.verts_loc_glob = {}
self.verts_glob_loc = []
self.vertices = []
self.stitch_segmentation = []
for stitch_id, stitch in enumerate(self.stitches):
panel1, panel2 = self.panels[stitch.panel_1], self.panels[stitch.panel_2]
stitch_range_1, stitch_range_2 = self._swap_stitch_ranges(stitch)
# Record same panel connections
if stitch.panel_1 == stitch.panel_2:
s1, e1 = stitch_range_1[0], stitch_range_1[-1]
s2, e2 = stitch_range_2[0], stitch_range_2[-1]
s_min, s_max = min(s1, s2), max(s1, s2)
e_min, e_max = min(e1, e2), max(e1, e2)
same_panel_stitching_dict.setdefault((stitch.panel_1, s_min), []).append(s_max)
same_panel_stitching_dict.setdefault((stitch.panel_2, e_min), []).append(e_max)
# Perform matching
for loc_id1, loc_id2 in zip(stitch_range_1, stitch_range_2):
if stitch.panel_1 == stitch.panel_2 and loc_id1 == loc_id2: #same vertex
if (stitch.panel_1, loc_id1) not in self.verts_loc_glob.keys():
self._stitch_same_loc_vertex(panel1, loc_id1, glob_idx, stitch_id)
glob_idx += 1
else:
glob_id = self.verts_loc_glob[(stitch.panel_1, loc_id1)]
self.stitch_segmentation[glob_id].append("stitch_" + str(stitch_id))
else:
v1_glob_exists = (stitch.panel_1, loc_id1) in self.verts_loc_glob.keys()
v2_glob_exists = (stitch.panel_2, loc_id2) in self.verts_loc_glob.keys()
if v1_glob_exists and v2_glob_exists: #both exist
glob1 = self.verts_loc_glob[(stitch.panel_1, loc_id1)]
glob2 = self.verts_loc_glob[(stitch.panel_2, loc_id2)]
if glob1 != glob2:
self._stitch_two_diff_existent_glob_verts(glob1, glob2, glob_idx, stitch_id)
glob_idx -= 1
elif v1_glob_exists:
self._stitch_one_existent_glob_vert(panel1, panel2, loc_id1, loc_id2, stitch_id)
elif v2_glob_exists:
self._stitch_one_existent_glob_vert(panel2, panel1, loc_id2, loc_id1, stitch_id)
else: #none exist
self._stitch_none_existent_glob_verts(panel1, panel2, loc_id1, loc_id2, glob_idx, stitch_id)
glob_idx += 1
return same_panel_stitching_dict
# !SECTION
# SECTION Stitching -- min validity checks
def check_local_vertices_stitching(self, dic, panel_name, loc_ids):
"""
This function checks for valid "same panel stitching" based on vertices given as a set of
local vertex ids and a dictionary with "same panel stitching" information.
Input:
* self (BoxMesh object): Instance of BoxMesh class from which the function is called.
* dic (dict): same_panel_stitching_dict, dictionary storing the local vertex indices to which a
local vertex of the same panel is stitched together
* loc_ids (list): List of vertex ids representing vertices that are stitched into one global vertex
in the same panel and are needed to be checked for validity.
* panel_name (str): Panel name used to identify the panel.
Output:
* True if any local vertex (defined by loc_ids) is stitched together with at least one other
local vertex but it happens outside of the valid panel stitch; otherwise, False.
"""
# Checking all the pairs:
# same id -> same id is a connection in the dart stitch (at the tip)
for i in loc_ids:
invalid = True
# NOTE: there could be some invalid pairings, but as long as we find
# a valid one for each loc_ids vertex, we are good.
for j in loc_ids:
min_id = min(i, j)
max_id = max(i, j)
if ((panel_name, min_id) in dic.keys()) and (max_id in dic[(panel_name, min_id)]):
# i is stitched to j in a valid same-panel stitch
# => i is supposed to be part of current global vertex in question
invalid = False
break
if invalid:
# Cannot find a intra-panel stitch that connects i
# into this global vertex -> incorrect
return True
return False
def _group_same_panel_stiches(self, inner_list):
"""
This function groups together stitched vertices that belong to the same panel.
Input:
* self (BoxMesh object): Instance of BoxMesh class from which the function is called.
* inner_list (list): List of tuples representing same panel stitching information,
where each tuple contains the panel name and a vertex id.
Output:
* final_result (list): List of tuples where the first element is the panel name, and the second
element is a list of vertex IDs that are stitched together with another vertex of that panel.
"""
result_dict = {}
for name, value in inner_list:
if name in result_dict:
result_dict[name].append(value)
else:
result_dict[name] = [value]
# Filter only the panels with more than one value
final_result = [(name, values) for name, values in result_dict.items() if len(values) > 1]
return final_result
def _check_same_panel_stitching(self, dic, global_ids):
"""
This function checks for stitching of local vertices within the same panel based on a given dictionary
containing "same panel stitching" information and global vertex IDs representing the end vertices of
two edges that are stitched together.
Input:
* self (BoxMesh object): Instance of BoxMesh class from which the function is called.
* dic (dict): same_panel_stitching_dict, dictionary storing the local vertex indices to which a
local vertex of the same panel is stitched together
* global_ids (list): global vertex IDs representing the end vertices of two edges that
are stitched together
Output:
* Returns True if stitching is incorrect: there are local vertices associated with
the provided global IDs that are stitched together within the same panel;
otherwise, returns False.
"""
for g_id in global_ids:
l_old = self.verts_glob_loc[g_id] # All local verts corresponding to g_id
# Groups by panels, if there are multiple vertices from the same panel
l = self._group_same_panel_stiches(l_old)
if not dic and l:
return True
for panel_name, loc_ids in l:
if self.check_local_vertices_stitching(dic, panel_name, loc_ids):
return True
return False
def _valid_stitch_front_end(self, stitch: Seam):
"""
This function checks if any front and end vertices of the two edges taking part in a stitch
have been stitched together.
Input:
* stitch object
Output:
* Returns False if any front and end vertices of the two edges taking part in a stitch
have been stitched together; otherwise, returns True.
"""
panel1 = self.panels[stitch.panel_1]
panel2 = self.panels[stitch.panel_2]
edge1 = panel1.edges[stitch.edge_1]
edge2 = panel2.edges[stitch.edge_2]
s1_glob = self.verts_loc_glob[(stitch.panel_1, edge1.vertex_range[0])]
e1_glob = self.verts_loc_glob[(stitch.panel_1, edge1.vertex_range[-1])]
s2_glob = self.verts_loc_glob[(stitch.panel_2, edge2.vertex_range[0])]
e2_glob = self.verts_loc_glob[(stitch.panel_2, edge2.vertex_range[-1])]
# Check if start and end was collapsed together
if s1_glob == e1_glob or s2_glob == e2_glob:
return False
else:
return True
def _valid_stitch_same_panel(self, stitch:Seam, same_panel_stitching_dict):
"""
This function examines whether the front and end vertices of two edges participating in a
stitching operation have been improperly stitched together with another vertex from the same panel.
Input:
* self (BoxMesh object): Instance of BoxMesh class from which the function is called.
* p1_name (str): panel name to which the frist stitch edge belongs.
* edge_id1 (int): edge id of first stitch edge
* p2_name (str): panel name to which the second stitch edge belongs.
* edge_id2 (int): edge id of second stitch edge
* same_panel_stitching_dict (dict): dictionary storing the local vertex indices to which a
local vertex of the same panel is stitched together
Output:
* Returns False if any front and end vertices of two edges participating in a
stitching operation have been improperly stitched together with another vertex from the same panel;
otherwise, returns True.
"""
panel1 = self.panels[stitch.panel_1]
panel2 = self.panels[stitch.panel_2]
edge1 = panel1.edges[stitch.edge_1]
edge2 = panel2.edges[stitch.edge_2]
s1_glob = self.verts_loc_glob[(stitch.panel_1, edge1.vertex_range[0])]
e1_glob = self.verts_loc_glob[(stitch.panel_1, edge1.vertex_range[-1])]
s2_glob = self.verts_loc_glob[(stitch.panel_2, edge2.vertex_range[0])]
e2_glob = self.verts_loc_glob[(stitch.panel_2, edge2.vertex_range[-1])]
return not self._check_same_panel_stitching(
same_panel_stitching_dict, [s1_glob, e1_glob, s2_glob, e2_glob])
def _is_stitching_valid(self, same_panel_stitching_dict, front_end_only=False):
"""Check validity of a current stitching"""
stitch_ids_invalid = []
for stitch_id, stitch in enumerate(self.stitches):
front_end_valid = self._valid_stitch_front_end(stitch)
same_panel_valid = self._valid_stitch_same_panel(stitch, same_panel_stitching_dict)
if front_end_only:
if not front_end_valid:
stitch_ids_invalid.append(stitch_id)
else:
if not front_end_valid or not same_panel_valid:
stitch_ids_invalid.append(stitch_id)
valid = len(stitch_ids_invalid) == 0
return valid, stitch_ids_invalid
# !SECTION
# SECTION -- Stitch collapsing init
def collapse_stitch_vertices(self):
"""
This function performs the stitching and checks if any anomalies can be detected
Input:
* self (BoxMesh object): Instance of BoxMesh class from which the function is called
"""
# NOTE: don't need this
# Try the stitching -- performs global vertex matching
same_panel_stitching_dict = self._stitch_vertices()
# Check stitches validity: edge collapse (start==end)
# NOTE: Separating checks by error type to reduce number of invalid stitch orientations to process
# in each case
valid, _ = self._is_stitching_valid(
same_panel_stitching_dict,
front_end_only=False)
if not valid:
print(f'{self.__class__.__name__}::{self.name}::ERROR::Invalid stitching. Unable to fix')
raise StitchingError()
# !SECTION
# SECTION -- Mesh finalization
def _get_glob_ids(self, panel, face):
"""
This function returns the global indices of the face vertices.
Input
* self (BoxMesh object): Instance of BoxMesh class from which the function is called
* panel_name (str): The panel name of the panel the face is from
* face (ndarray): Contains the local indices of the face vertices into panel.panel_faces
* len_B_verts (int): Current number of vertices stored in self.vertices
* n_stitches_panel (int): Number of stitch vertices of the whole panel
Output:
* glob_indices (list): Global indices of face vertices into self.vertices
"""
glob_indices = []
n_stitches_panel = panel.n_stitches
for loc_id in face:
if loc_id < n_stitches_panel:
glob_indices.append(self.verts_loc_glob[(panel.panel_name, loc_id)])
else:
glob_indices.append(loc_id + panel.glob_offset - n_stitches_panel)
return glob_indices
def calc_norm(self, a, b, c):
"""
This function calculates the norm based on the three points a, b, and c.
Input:
* self (BoxMesh object): Instance of BoxMesh class from which the function is called
* a (ndarray): first point taking part in norm calculation
* b (ndarray): second point taking part in norm calculation
* c (ndarray): third point taking part in norm calculation
Output:
* n_normalized (bool): norm(a,b,c) with length 1
"""
# Calculate the vectors AB and AC
AB = np.array(b - a)
AC = np.array(c - a)
# Calculate the cross product of AB and AC
n = np.cross(AB, AC)
n_normalized = n / np.linalg.norm(n)
return n_normalized
def _check_norm_local(self, idx_a, idx_b, idx_c, panel_norm, v_3D):
"""
This function checks if the norm defined by the three vertices a,b, and c equals panel_norm.
Input:
* self (BoxMesh object): Instance of BoxMesh class from which the function is called
* idx_a (int): Index of the first vertex into v_3D
* idx_b (int): Index of the second vertex into v_3D
* idx_c (int): Index of the third vertex into v_3D
* panel_norm (list): The norm of a panel to which norm(a,b,c) is compared to
* v_3D (list): The 3D vertices of a panel
Output:
* same_norm (bool): True if norm(a,b,c) equals panel_norm, else False
"""
a, b, c = np.array(v_3D)[[idx_a, idx_b, idx_c]]
n_normalized = self.calc_norm(a, b, c)
same_norm = np.allclose(n_normalized,panel_norm)
return same_norm
def _order_face_vertices(self, panel, v_3D):
"""
This function orders the face vertices of panel.panel_faces so that the face norms equal the panel's norm.
Input:
* self (BoxMesh object): Instance of BoxMesh class from which the function is called
* panel(Panel object): Panel object whose norm is used for comparison
* v_3D (list): The 3D vertices of panel.panel_vertices
"""
# Check first face:
idxa, idxb, idxc = panel.panel_faces[0]
if not self._check_norm_local(idxa, idxb, idxc, panel.norm, v_3D):
faces_array = np.array(panel.panel_faces)
# Swap the 2nd and 3rd columns
faces_array[:, [1, 2]] = faces_array[:, [2, 1]]
panel.panel_faces = list(faces_array)
def _set_el_within_range(self, low, up, tolerance_factor=0.02):
"""
This function returns a value between low and up based on the tolerance_factor.
Input:
* low (float): lower bound (exclusive)
* up (float): upper bound (exclusive)
* tolerance_factor (float): influences how close the GT edge length is to low
Output:
* el (float): new GT edge length close to low
"""
range_distance = up - low
tol = tolerance_factor * range_distance
el = low + tol
return el
def _get_seam_gt_el(self, el_i, el_j, el_k, id1, id2, stitch_edges_gt):
"""
This function returns the ground truth length of edges between two stitch vertices.
It returns the minimum edge length if the triangle inequality (e1 + e2 > e3, e2 + e3 > e1, e1 + e3 > e2),
is satisfied. Otherwise, it returns the smallest value that maintains the validity of the adjacent triangles
(if possible).
Input:
* self (BoxMesh object): Instance of BoxMesh class from which the function is called
* el_i (float): second edge length of stitch edge i
* el_j (float): edge length of edge j which is part of the same triangle
* el_k (float): edge length of edge k which is part of the same triangle
* id1 (int): global vertex id of first edge vertex (vertex is a stitch vertex)
* id2 (int): global vertex id of second edge vertex (vertex is a stitch vertex)
* stitch_edges_gt (dict): Dict storing lower bound, upper bound and current edge length of previously
encountered edge with vertices id1 and id2
"""
low_old, up_old, el_i_old = stitch_edges_gt[(id1, id2)]
min_el = min([el_i, el_i_old])
low = max(low_old, abs(el_j - el_k))
up = min(up_old, el_j + el_k)
if low < min_el and min_el < up:
el = min_el
elif low < up and min_el < low:
el = self._set_el_within_range(low,up)
else:
# raise ValueError(f"Not possible to set triangle edge of vertices {id1} and {id2}")
print(f'{self.__class__.__name__}::WARNING::{self.name}::Impossible to set '
f' ground truth edge length of vertices {id1} and {id2}. '
'Simulation is going to crash')
return low
return el
def _store_to_orig_lens(self, panel, face, f_glob_ids, stitch_edges_gt):
"""
This function stores the lengths between the local 2D face vertices
to self.orig_lens in terms of their global indices.
Input:
* self (BoxMesh object): Instance of BoxMesh class from which the function is called
* panel (Panel object): Panel object the face is from
* face (ndarray): Contains the face vertex indices into panel.panel_vertices
* f_glob_ids (list): The global indices of the face vertices into self.vertices
"""
# Sort f_glob_ids and get the corresponding indices
sorted_indices = sorted(range(3), key=lambda i: f_glob_ids[i])
# Sort f_glob_ids and face (local ids) based on the sorted indices
glob_id1, glob_id2, glob_id3 = np.array(f_glob_ids)[sorted_indices]
f_loc_id_1, f_loc_id_2, f_loc_id_3 = face[sorted_indices]
v1 = panel.panel_vertices[f_loc_id_1]
v2 = panel.panel_vertices[f_loc_id_2]
v3 = panel.panel_vertices[f_loc_id_3]
el1 = np.linalg.norm(np.array(v2 - v1))
el2 = np.linalg.norm(np.array(v3 - v2))
el3 = np.linalg.norm(np.array(v3 - v1))
e1_exists = (glob_id1,glob_id2) in stitch_edges_gt.keys()
e2_exists = (glob_id2, glob_id3) in stitch_edges_gt.keys()
e3_exists = (glob_id1, glob_id3) in stitch_edges_gt.keys()
low1_old, low2_old, low3_old = None, None, None
if e1_exists:
low1_old, up1_old, _ = stitch_edges_gt[glob_id1, glob_id2]
el1 = self._get_seam_gt_el(el1, el2, el3, glob_id1, glob_id2, stitch_edges_gt)
self.orig_lens[(glob_id1, glob_id2)] = el1
if e2_exists:
low2_old, up2_old, _ = stitch_edges_gt[glob_id2, glob_id3]
el2 = self._get_seam_gt_el(el2, el1, el3, glob_id2, glob_id3, stitch_edges_gt)
self.orig_lens[(glob_id2, glob_id3)] = el2
if e3_exists:
low3_old, up3_old, _ = stitch_edges_gt[glob_id1, glob_id3]
el3 = self._get_seam_gt_el(el3, el1, el2, glob_id1, glob_id3, stitch_edges_gt)
self.orig_lens[(glob_id1, glob_id3)] = el3
n_stitches = panel.n_stitches
v1_stitch = f_loc_id_1 < n_stitches
v2_stitch = f_loc_id_2 < n_stitches
v3_stitch = f_loc_id_3 < n_stitches
if v1_stitch and v2_stitch:
if low1_old:
stitch_edges_gt[glob_id1, glob_id2] = [max(low1_old, abs(el2 - el3)), min(up1_old, el2 + el3), el1]
else:
stitch_edges_gt[glob_id1, glob_id2] = [abs(el2 - el3), el2 + el3, el1]
if v2_stitch and v3_stitch:
if low2_old:
stitch_edges_gt[glob_id2, glob_id3] = [max(low2_old, abs(el1 - el3)), min(up2_old, el1 + el3), el2]
else:
stitch_edges_gt[glob_id2, glob_id3] = [abs(el1 - el3), el1 + el3, el2]
if v1_stitch and v3_stitch:
if low3_old:
stitch_edges_gt[glob_id1, glob_id3] = [max(low3_old, abs(el1 - el2)), min(up3_old, el1 + el2), el3]
else:
stitch_edges_gt[glob_id1, glob_id3] = [abs(el1 - el2), el1 + el2, el3]
def get_v_texture(self, panel_vertices):
"""
Returns the minimum x and y value of panel_vertices
"""
p_v_arr = np.array(panel_vertices)
trans = [min(p_v_arr[:,0]), min(p_v_arr[:,1])]
v_texture = p_v_arr - trans
return v_texture.tolist()
def finalise_mesh(self):
"""
This function finalizes box mesh after stitching has finished:
* Creates self.faces and self.vertices
* Creates stitch segmentation
Input:
* self (BoxMesh object): Instance of BoxMesh class from which the function is called
"""
stitch_edges_gt = {}
#Store orignal length between stitch vertices and their neighbors
for panelname in self.panelNames:
panel = self.panels[panelname]
n_stitches_panel = panel.n_stitches
len_B_verts = len(self.vertices)
panel.glob_offset = len_B_verts
# Add non-stitch vertices to self.vertices
v_3D = list(panel.rot_trans_panel(panel.panel_vertices))
v_3D_non_stitch = v_3D[n_stitches_panel:]
self.vertices += v_3D_non_stitch
# Assign edge labels to vertices
for edge in panel.edges:
if edge.label and edge.stitch_ref is None:
# Use vertex range to assign edge labels to non-stitching vertices
e_verts = np.array(edge.vertex_range[1:-1]) # Exclude ends which are located elsewhere and may be in a stitch
labeled_verts = e_verts + len_B_verts - n_stitches_panel
self.vertex_labels.setdefault(edge.label, []).extend(labeled_verts.tolist())
#Order face vertices so that face norms are equal to the panel.panel_norm
self._order_face_vertices(panel, v_3D)
texture_offset = len(self.vertex_texture)
for face in panel.panel_faces:
loc_stitch_ids = [loc_id for loc_id in face if loc_id < n_stitches_panel]
f_glob_ids = self._get_glob_ids(panel, face)
if f_glob_ids[0] == f_glob_ids[1] or f_glob_ids[1] == f_glob_ids[2] or f_glob_ids[0] == f_glob_ids[2]:
continue #Do not add faces which are points or lines after stitching
if loc_stitch_ids:
self._store_to_orig_lens(panel, face, f_glob_ids, stitch_edges_gt)
# Add face to self.faces
self.faces.append(f_glob_ids)
#Add texture
tex_id0, tex_id1, tex_id2 = face + texture_offset
id0, id1, id2 = f_glob_ids
textured_face = [id0, tex_id0, id1, tex_id1, id2, tex_id2]
self.faces_with_texture.append(textured_face)
self.vertex_texture += self.get_v_texture(panel.panel_vertices)
#Add panel name to stitch_segmentation
n_non_stitches_panel = len(panel.panel_vertices) - n_stitches_panel
self.stitch_segmentation += [panel.panel_name] * n_non_stitches_panel
# NOTE: self.vertices now contains all mesh vertices
# self.faces now contains all mesh faces
# !SECTION
# SECTION -- Serialization routines
def eval_vertex_normals(self):
vertex_normals = np.zeros((len(self.vertices), 4))
for panelname in self.panelNames:
panel = self.panels[panelname]
n_stitches_panel = panel.n_stitches
for face in panel.panel_faces:
f_glob_ids = self._get_glob_ids(panel, face)
loc_stitch_ids = [loc_id for loc_id in face if loc_id < n_stitches_panel]
if loc_stitch_ids:
v0, v1, v2 = np.array(self.vertices)[f_glob_ids]
face_norm = list(self.calc_norm(v0, v1, v2))
else:
face_norm = panel.norm
temp_update = face_norm + [1]
vertex_normals[f_glob_ids] += temp_update
vertex_normals = vertex_normals[:, :3] / (vertex_normals[:, 3][:, np.newaxis])
return vertex_normals
def save_vertex_labels(self):
"""Save labeled vertices"""
# Add labels on stitched vertices using stitch_id_label
for v_id, seg_labels in enumerate(self.stitch_segmentation):
if 'stitch' not in seg_labels[0]: # Processed all stitches
break
for stitch in seg_labels:
id = int(stitch.split('_')[-1])
label = self.stitches[id].label
if label is not None: # Found a labeled vertex!
self.vertex_labels.setdefault(label, []).append(v_id)
# Save to yaml
with open(self.paths.g_vert_labels, 'w') as file:
yaml.dump(self.vertex_labels, file, default_flow_style=False, sort_keys=False)
def save_box_mesh_obj(self, with_normals=False, in_uv_config={}, mat_name='panels_texture'):
"""
This function creates an obj file of the generated box mesh from pattern and stores it to save_path.
Input:
* self (BoxMesh object): Instance of BoxMesh class from which the function is called
* save_path (str): The path where the obj file is stored
* filename (str): Name of the boxmmesh
"""
if not self.loaded:
print(f'{self.__class__.__name__}::{self.name}::WARNING::Pattern is not yet loaded. Nothing saved')
return
uv_config = { # Defaults
'seam_width': 0.5,
'dpi': 600,
'fabric_grain_texture_path': None,
'fabric_grain_resolution': 1,
}
# Update with incoming values, if any
uv_config.update(in_uv_config)
uvs = texture_mesh_islands(
texture_coords=np.array(self.vertex_texture),
face_texture_coords=np.array([[tex_id0, tex_id1, tex_id2] for _, tex_id0, _, tex_id1, _, tex_id2, in self.faces_with_texture]),
out_texture_image_path=self.paths.g_texture,
out_fabric_tex_image_path=self.paths.g_texture_fabric,
out_mtl_file_path=self.paths.g_mtl,
boundary_width=uv_config['seam_width'],
dpi=uv_config['dpi'],
background_img_path=uv_config['fabric_grain_texture_path'],
background_resolution=uv_config['fabric_grain_resolution'],
mat_name=mat_name
)
save_obj(
self.paths.g_box_mesh,
self.vertices,
self.faces_with_texture,
uvs,
vert_normals=self.eval_vertex_normals() if with_normals else None,
mtl_file_name=self.paths.g_mtl.name,
mat_name=mat_name
)
def save_segmentation(self):
"""
This function stores the self.stitch_segmentation list as a txt file to save_path.
Input:
* self (BoxMesh object): Instance of BoxMesh class from which the function is called
* save_path (str): The path where the txt file is stored
* filename (str): Name of the stitch segmentation file
"""
if not self.loaded:
print(f'{self.__class__.__name__}::{self.name}::WARNING::Pattern is not yet loaded. Nothing saved')
return
rows = self.stitch_segmentation
with open(self.paths.g_mesh_segmentation, 'w') as file:
for row in rows:
# Join the entries in the row with a delimiter (e.g., comma)
if isinstance(row,list):
row_data = ','.join(row)
else:
row_data = row
# Write the row to the file
file.write(row_data + '\n')
def save_orig_lens(self,):
"""
This function stores the self.orig_lens dict as a pickle file to save_path.
Self.orig_lens is a dict indexed by two global vertex indices and contains the ground truth length
between those vertices in their 2D setting.
Input:
* self (BoxMesh object): Instance of BoxMesh class from which the function is called
* save_path (str): The path where the pickle file is stored
* filename (str): Name of the orig_lens file
"""
if not self.loaded:
print(f'{self.__class__.__name__}::{self.name}::WARNING::Pattern is not yet loaded. Nothing saved')
return
with open(self.paths.g_orig_edge_len, 'wb') as file:
pickle.dump(self.orig_lens, file)
def serialize(self, paths: PathCofig, tag='',
with_3d=False, with_text=False, view_ids=False,
empty_ok=False,
with_v_norms=False,
store_panels=False,
uv_config={}
):
"""
This function stores (annotated) visualisations (png,svg) of the pattern, the box mesh as an .obj file,
the segmentation as a .txt file and the ground truth lengths dict as a .pickle file by overloading
the serialize function of core.VisPattern.
Input:
* self (BoxMesh object): Instance of BoxMesh class from which the function is called
* path (str): The path where the files get stored
* to_subfolder (bool): if True, files will be stored in a subfolder rather than directly to path
* with_3d (bool): if True, stores the pattern in 3d
* annotated (bool): if True, stores visualisations without annotations
* not_annotated (bool): if True, stores visualisations with annotations
"""
if not self.loaded:
print(f'{self.__class__.__name__}::{self.name}::WARNING::Pattern is not yet loaded. Nothing saved')
return
self.paths = paths
log_dir = super().serialize(self.paths.out_el, to_subfolder=False, tag=tag, with_3d=with_3d,
with_text=with_text, view_ids=view_ids, empty_ok=empty_ok)
if store_panels:
# Store panel
for panel in self.panels.values():
folder_path = Path(log_dir) / "panels"
panel.save_panel_mesh_obj(folder_path)
print(f"Stored panels to {folder_path}...")
self.save_box_mesh_obj(with_normals=with_v_norms, in_uv_config=uv_config)
self.save_segmentation()
self.save_orig_lens()
self.save_vertex_labels()
# Copy yaml files
if self.paths.in_design_params.exists():
shutil.copy(self.paths.in_design_params, self.paths.design_params)
else:
print(f'{self.__class__.__name__}::{self.name}::WARNING::Path does not exist: {self.paths.in_design_params}')
if self.paths.in_body_mes.exists():
shutil.copy(self.paths.in_body_mes, self.paths.body_mes)
else:
print(f'{self.__class__.__name__}::{self.name}::WARNING::Path does not exist: {self.paths.in_body_mes}')
return log_dir
# !SECTION