Skip to content

Commit a19eff4

Browse files
committed
add mesh colorfield visualization support
- visualize_slicer now accepts mesh_colorfield param for vertex/face attribute coloring - auto-detect vertex vs face attributes, use viridis colormap - fix HeatGeodesicSolver to accept (V, F) tuple - update examples to use scalar_field/overhang coloring closes #179
1 parent d004139 commit a19eff4

File tree

5 files changed

+78
-55
lines changed

5 files changed

+78
-55
lines changed

examples/2_curved_slicing/ex2_curved_slicing.py

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,19 +18,19 @@
1818
from compas_slicer.slicers import InterpolationSlicer
1919
from compas_slicer.visualization import should_visualize, visualize_slicer
2020

21-
DATA_PATH = Path(__file__).parent / 'data_Y_shape'
21+
DATA_PATH = Path(__file__).parent / "data_Y_shape"
2222
OUTPUT_PATH = utils.get_output_directory(DATA_PATH)
2323

2424

2525
def main(visualize: bool = False):
2626
start_time = time.time()
2727

2828
# Load initial_mesh
29-
mesh = Mesh.from_obj(DATA_PATH / 'mesh.obj')
29+
mesh = Mesh.from_obj(DATA_PATH / "mesh.obj")
3030

3131
# Load targets (boundaries)
32-
low_boundary_vs = utils.load_from_json(DATA_PATH, 'boundaryLOW.json')
33-
high_boundary_vs = utils.load_from_json(DATA_PATH, 'boundaryHIGH.json')
32+
low_boundary_vs = utils.load_from_json(DATA_PATH, "boundaryLOW.json")
33+
high_boundary_vs = utils.load_from_json(DATA_PATH, "boundaryHIGH.json")
3434
create_mesh_boundary_attributes(mesh, low_boundary_vs, high_boundary_vs)
3535

3636
avg_layer_height = 2.0
@@ -39,10 +39,13 @@ def main(visualize: bool = False):
3939

4040
preprocessor = InterpolationSlicingPreprocessor(mesh, config, DATA_PATH)
4141
preprocessor.create_compound_targets()
42-
g_eval = preprocessor.create_gradient_evaluation(norm_filename='gradient_norm.json', g_filename='gradient.json',
43-
target_1=preprocessor.target_LOW,
44-
target_2=preprocessor.target_HIGH)
45-
preprocessor.find_critical_points(g_eval, output_filenames=['minima.json', 'maxima.json', 'saddles.json'])
42+
g_eval = preprocessor.create_gradient_evaluation(
43+
norm_filename="gradient_norm.json",
44+
g_filename="gradient.json",
45+
target_1=preprocessor.target_LOW,
46+
target_2=preprocessor.target_HIGH,
47+
)
48+
preprocessor.find_critical_points(g_eval, output_filenames=["minima.json", "maxima.json", "saddles.json"])
4649

4750
# Slicing
4851
slicer = InterpolationSlicer(mesh, preprocessor, config)
@@ -51,7 +54,7 @@ def main(visualize: bool = False):
5154
simplify_paths_rdp(slicer, threshold=0.25)
5255
seams_smooth(slicer, smooth_distance=3)
5356
slicer.printout_info()
54-
utils.save_to_json(slicer.to_data(), OUTPUT_PATH, 'curved_slicer.json')
57+
utils.save_to_json(slicer.to_data(), OUTPUT_PATH, "curved_slicer.json")
5558

5659
# Print organizer
5760
print_organizer = InterpolationPrintOrganizer(slicer, config, DATA_PATH)
@@ -60,21 +63,25 @@ def main(visualize: bool = False):
6063
smooth_printpoints_up_vectors(print_organizer, strength=0.5, iterations=10)
6164
smooth_printpoints_layer_heights(print_organizer, strength=0.5, iterations=5)
6265

63-
set_linear_velocity_by_range(print_organizer, param_func=lambda ppt: ppt.layer_height,
64-
parameter_range=[avg_layer_height*0.5, avg_layer_height*2.0],
65-
velocity_range=[150, 70], bound_remapping=False)
66+
set_linear_velocity_by_range(
67+
print_organizer,
68+
param_func=lambda ppt: ppt.layer_height,
69+
parameter_range=[avg_layer_height * 0.5, avg_layer_height * 2.0],
70+
velocity_range=[150, 70],
71+
bound_remapping=False,
72+
)
6673
set_extruder_toggle(print_organizer, slicer)
6774
add_safety_printpoints(print_organizer, z_hop=10.0)
6875

6976
# Save printpoints dictionary to json file
7077
printpoints_data = print_organizer.output_printpoints_dict()
71-
utils.save_to_json(printpoints_data, OUTPUT_PATH, 'out_printpoints.json')
78+
utils.save_to_json(printpoints_data, OUTPUT_PATH, "out_printpoints.json")
7279

7380
end_time = time.time()
7481
print("Total elapsed time", round(end_time - start_time, 2), "seconds")
7582

7683
if visualize:
77-
visualize_slicer(slicer, mesh)
84+
visualize_slicer(slicer, mesh, mesh_colorfield="scalar_field")
7885

7986

8087
if __name__ == "__main__":

examples/6_attributes_transfer/example_6_attributes_transfer.py

Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@
1212
from compas_slicer.utilities.attributes_transfer import transfer_mesh_attributes_to_printpoints
1313
from compas_slicer.visualization import should_visualize, visualize_slicer
1414

15-
DATA_PATH = Path(__file__).parent / 'data'
15+
DATA_PATH = Path(__file__).parent / "data"
1616
OUTPUT_PATH = slicer_utils.get_output_directory(DATA_PATH)
17-
MODEL = 'distorted_v_closed_low_res.obj'
17+
MODEL = "distorted_v_closed_low_res.obj"
1818

1919

2020
def main(visualize: bool = False):
@@ -26,37 +26,37 @@ def main(visualize: bool = False):
2626
# Vertex attributes can only be entities that can be meaningfully multiplied with a float (ex. float, np.array ...)
2727

2828
# overhand attribute - Scalar value (per face)
29-
mesh.update_default_face_attributes({'overhang': 0.0})
29+
mesh.update_default_face_attributes({"overhang": 0.0})
3030
for f_key, data in mesh.faces(data=True):
3131
face_normal = mesh.face_normal(f_key, unitized=True)
32-
data['overhang'] = Vector(0.0, 0.0, 1.0).dot(face_normal)
32+
data["overhang"] = Vector(0.0, 0.0, 1.0).dot(face_normal)
3333

3434
# face looking towards the positive y axis - Boolean value (per face)
35-
mesh.update_default_face_attributes({'positive_y_axis': False})
35+
mesh.update_default_face_attributes({"positive_y_axis": False})
3636
for f_key, data in mesh.faces(data=True):
3737
face_normal = mesh.face_normal(f_key, unitized=True)
3838
is_positive_y = Vector(0.0, 1.0, 0.0).dot(face_normal) > 0 # boolean value
39-
data['positive_y_axis'] = is_positive_y
39+
data["positive_y_axis"] = is_positive_y
4040

4141
# distance from plane - Scalar value (per vertex)
42-
mesh.update_default_vertex_attributes({'dist_from_plane': 0.0})
42+
mesh.update_default_vertex_attributes({"dist_from_plane": 0.0})
4343
plane = (Point(0.0, 0.0, -30.0), Vector(0.0, 0.5, 0.5))
4444
for v_key, data in mesh.vertices(data=True):
45-
v_coord = mesh.vertex_coordinates(v_key, axes='xyz')
46-
data['dist_from_plane'] = distance_point_plane(v_coord, plane)
45+
v_coord = mesh.vertex_coordinates(v_key, axes="xyz")
46+
data["dist_from_plane"] = distance_point_plane(v_coord, plane)
4747

4848
# direction towards point - Vector value (per vertex)
49-
mesh.update_default_vertex_attributes({'direction_to_pt': 0.0})
49+
mesh.update_default_vertex_attributes({"direction_to_pt": 0.0})
5050
pt = Point(4.0, 1.0, 0.0)
5151
for v_key, data in mesh.vertices(data=True):
52-
v_coord = mesh.vertex_coordinates(v_key, axes='xyz')
53-
data['direction_to_pt'] = np.array(normalize_vector(Vector.from_start_end(v_coord, pt)))
52+
v_coord = mesh.vertex_coordinates(v_key, axes="xyz")
53+
data["direction_to_pt"] = np.array(normalize_vector(Vector.from_start_end(v_coord, pt)))
5454

5555
# --------------- Slice mesh
5656
slicer = PlanarSlicer(mesh, layer_height=5.0)
5757
slicer.slice_model()
5858
simplify_paths_rdp(slicer, threshold=1.0)
59-
slicer_utils.save_to_json(slicer.to_data(), OUTPUT_PATH, 'slicer_data.json')
59+
slicer_utils.save_to_json(slicer.to_data(), OUTPUT_PATH, "slicer_data.json")
6060

6161
# --------------- Create printpoints
6262
print_organizer = PlanarPrintOrganizer(slicer)
@@ -67,25 +67,25 @@ def main(visualize: bool = False):
6767

6868
# --------------- Save printpoints to json (only json-serializable attributes are saved)
6969
printpoints_data = print_organizer.output_printpoints_dict()
70-
utils.save_to_json(printpoints_data, OUTPUT_PATH, 'out_printpoints.json')
70+
utils.save_to_json(printpoints_data, OUTPUT_PATH, "out_printpoints.json")
7171

7272
# --------------- Print the info to see the attributes of the printpoints (you can also visualize them on gh)
7373
print_organizer.printout_info()
7474

7575
# --------------- Save printpoints attributes for visualization
76-
overhangs_list = print_organizer.get_printpoints_attribute(attr_name='overhang')
77-
positive_y_axis_list = print_organizer.get_printpoints_attribute(attr_name='positive_y_axis')
78-
dist_from_plane_list = print_organizer.get_printpoints_attribute(attr_name='dist_from_plane')
79-
direction_to_pt_list = print_organizer.get_printpoints_attribute(attr_name='direction_to_pt')
76+
overhangs_list = print_organizer.get_printpoints_attribute(attr_name="overhang")
77+
positive_y_axis_list = print_organizer.get_printpoints_attribute(attr_name="positive_y_axis")
78+
dist_from_plane_list = print_organizer.get_printpoints_attribute(attr_name="dist_from_plane")
79+
direction_to_pt_list = print_organizer.get_printpoints_attribute(attr_name="direction_to_pt")
8080

81-
utils.save_to_json(overhangs_list, OUTPUT_PATH, 'overhangs_list.json')
82-
utils.save_to_json(positive_y_axis_list, OUTPUT_PATH, 'positive_y_axis_list.json')
83-
utils.save_to_json(dist_from_plane_list, OUTPUT_PATH, 'dist_from_plane_list.json')
84-
utils.save_to_json(utils.point_list_to_dict(direction_to_pt_list), OUTPUT_PATH, 'direction_to_pt_list.json')
81+
utils.save_to_json(overhangs_list, OUTPUT_PATH, "overhangs_list.json")
82+
utils.save_to_json(positive_y_axis_list, OUTPUT_PATH, "positive_y_axis_list.json")
83+
utils.save_to_json(dist_from_plane_list, OUTPUT_PATH, "dist_from_plane_list.json")
84+
utils.save_to_json(utils.point_list_to_dict(direction_to_pt_list), OUTPUT_PATH, "direction_to_pt_list.json")
8585

8686
if visualize:
87-
visualize_slicer(slicer, mesh)
87+
visualize_slicer(slicer, mesh, mesh_opacity=0.6, mesh_colorfield="overhang")
8888

8989

90-
if __name__ == '__main__':
90+
if __name__ == "__main__":
9191
main(visualize=should_visualize())

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,9 @@ classifiers = [
3030
dependencies = [
3131
"attrs>=25.0",
3232
"compas>=2.15",
33-
"compas_cgal>=0.9",
33+
"compas_cgal>=0.9.1",
3434
"loguru>=0.7",
35-
"networkx>=3.6",
35+
"networkx>=3.2",
3636
"numpy>=2.0",
3737
"progressbar2>=4.5",
3838
"pyclipper>=1.4",

src/compas_slicer/pre_processing/preprocessing_utils/geodesics.py

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,18 +19,14 @@
1919
from compas.datastructures import Mesh
2020

2121

22-
__all__ = ['get_heat_geodesic_distances',
23-
'get_custom_HEAT_geodesic_distances',
24-
'GeodesicsCache']
22+
__all__ = ["get_heat_geodesic_distances", "get_custom_HEAT_geodesic_distances", "GeodesicsCache"]
2523

2624

2725
# CGAL heat method solver cache (for precomputation reuse)
2826
_cgal_solver_cache: dict[int, object] = {}
2927

3028

31-
def get_heat_geodesic_distances(
32-
mesh: Mesh, vertices_start: list[int]
33-
) -> NDArray[np.floating]:
29+
def get_heat_geodesic_distances(mesh: Mesh, vertices_start: list[int]) -> NDArray[np.floating]:
3430
"""
3531
Calculate geodesic distances using CGAL heat method.
3632
@@ -55,7 +51,9 @@ def get_heat_geodesic_distances(
5551
mesh_hash = hash((len(list(mesh.vertices())), len(list(mesh.faces()))))
5652
if mesh_hash not in _cgal_solver_cache:
5753
_cgal_solver_cache.clear() # Clear old solvers
58-
_cgal_solver_cache[mesh_hash] = HeatGeodesicSolver(mesh)
54+
V = mesh.vertices_attributes("xyz")
55+
F = [mesh.face_vertices(f) for f in mesh.faces()]
56+
_cgal_solver_cache[mesh_hash] = HeatGeodesicSolver((V, F))
5957

6058
solver = _cgal_solver_cache[mesh_hash]
6159

@@ -90,9 +88,7 @@ def clear(self) -> None:
9088
self._cache.clear()
9189
self._mesh_hash = None
9290

93-
def get_distances(
94-
self, mesh: Mesh, sources: list[int], method: str = 'heat'
95-
) -> NDArray[np.floating]:
91+
def get_distances(self, mesh: Mesh, sources: list[int], method: str = "heat") -> NDArray[np.floating]:
9692
"""Get geodesic distances from sources, using cache when possible.
9793
9894
Parameters
@@ -166,7 +162,7 @@ class GeodesicsSolver:
166162
"""
167163

168164
def __init__(self, mesh: Mesh, OUTPUT_PATH: str) -> None:
169-
logger.info('GeodesicsSolver')
165+
logger.info("GeodesicsSolver")
170166
self.mesh = mesh
171167
self.OUTPUT_PATH = OUTPUT_PATH
172168

@@ -227,7 +223,7 @@ def diffuse_heat(
227223
# reverse values (to make sources at 0, increasing outward)
228224
u = ([np.max(u)] * len(u)) - u
229225

230-
utils.save_to_json([float(value) for value in u], self.OUTPUT_PATH, 'diffused_heat.json')
226+
utils.save_to_json([float(value) for value in u], self.OUTPUT_PATH, "diffused_heat.json")
231227
return u
232228

233229
def get_geodesic_distances(

src/compas_slicer/visualization/visualization.py

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
"""Visualization utilities for compas_slicer using compas_viewer."""
2+
23
from __future__ import annotations
34

45
import sys
@@ -33,6 +34,7 @@ def visualize_slicer(
3334
mesh: Mesh | None = None,
3435
show_mesh: bool = True,
3536
mesh_opacity: float = 0.3,
37+
mesh_colorfield: str = "z",
3638
) -> None:
3739
"""Visualize slicer toolpaths in compas_viewer.
3840
@@ -46,17 +48,35 @@ def visualize_slicer(
4648
If True, display the mesh.
4749
mesh_opacity : float
4850
Opacity for mesh display (0-1).
51+
mesh_colorfield : str
52+
Vertex attribute name to use for mesh coloring.
4953
5054
"""
5155
from compas.colors import Color
56+
from compas.colors import ColorMap
5257
from compas.geometry import Polyline
5358
from compas_viewer import Viewer
5459

5560
viewer = Viewer()
5661

57-
# Add mesh if provided
62+
# Add mesh if provided, colored by vertex or face attribute
5863
if mesh and show_mesh:
59-
viewer.scene.add(mesh, opacity=mesh_opacity)
64+
cmap = ColorMap.from_mpl("viridis")
65+
66+
# check if colorfield is vertex or face attribute
67+
first_vertex = next(mesh.vertices())
68+
if mesh.vertex_attribute(first_vertex, mesh_colorfield) is not None:
69+
scalars = {v: mesh.vertex_attribute(v, mesh_colorfield) for v in mesh.vertices()}
70+
smin, smax = min(scalars.values()), max(scalars.values())
71+
vertexcolor = {v: cmap(s, minval=smin, maxval=smax) for v, s in scalars.items()}
72+
viewer.scene.add(
73+
mesh, vertexcolor=vertexcolor, opacity=mesh_opacity, use_vertexcolors=True, show_lines=False
74+
)
75+
else:
76+
scalars = {f: mesh.face_attribute(f, mesh_colorfield) for f in mesh.faces()}
77+
smin, smax = min(scalars.values()), max(scalars.values())
78+
facecolor = {f: cmap(s, minval=smin, maxval=smax) for f, s in scalars.items()}
79+
viewer.scene.add(mesh, facecolor=facecolor, opacity=mesh_opacity, show_lines=False)
6080

6181
# Add paths as polylines with color gradient by layer
6282
n_layers = len(slicer.layers)
@@ -87,5 +107,5 @@ def plot_networkx_graph(G: nx.Graph) -> None:
87107
import matplotlib.pyplot as plt
88108

89109
plt.subplot(121)
90-
nx.draw(G, with_labels=True, font_weight='bold', node_color=range(len(list(G.nodes()))))
110+
nx.draw(G, with_labels=True, font_weight="bold", node_color=range(len(list(G.nodes()))))
91111
plt.show()

0 commit comments

Comments
 (0)