Skip to content

Commit ebb7481

Browse files
committed
fix(legend): Add a new method for contouring Mesh3D
1 parent 1362ae3 commit ebb7481

File tree

2 files changed

+90
-10
lines changed

2 files changed

+90
-10
lines changed

ladybug/legend.py

Lines changed: 75 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,8 @@
99
if (sys.version_info > (3, 0)): # python 3
1010
xrange = range
1111

12-
from ladybug_geometry.geometry3d.pointvector import Point3D, Vector3D
13-
from ladybug_geometry.geometry3d.plane import Plane
14-
from ladybug_geometry.geometry3d.mesh import Mesh3D
15-
from ladybug_geometry.geometry2d.pointvector import Point2D
16-
from ladybug_geometry.geometry2d.mesh import Mesh2D
12+
from ladybug_geometry.geometry2d import Point2D, Mesh2D
13+
from ladybug_geometry.geometry3d import Point3D, Vector3D, Polyline3D, Plane, Mesh3D
1714

1815
from .color import Color, Colorset, ColorRange
1916

@@ -441,6 +438,79 @@ def color_map_2d(self, width=800, height=600):
441438
color_mtx.append([black] * total_w)
442439
return color_mtx
443440

441+
def mesh_contours(self, mesh, tolerance):
442+
"""Get Polyline3Ds for contours of a Mesh3D associated with this legend's values.
443+
444+
Args:
445+
mesh: A ladybug-geometry Mesh3D for which contours will be derived.
446+
The number of faces or the number of vertices must match the
447+
number of values associated with this Legend.
448+
tolerance: The minimum difference between mesh vertices at which point
449+
they are considered equivalent.
450+
451+
Returns:
452+
A tuple with two elements.
453+
454+
- contours -- A list of lists where each sub-list represents
455+
contours associated with a specific threshold. Contours are
456+
composed of Polyline3D and LineSegment3D.
457+
458+
- thresholds -- list of numbers for the threshold value associated
459+
with each contour. The length of this list matches the contours.
460+
"""
461+
# check the input values and provide defaults
462+
val_count = len(self.values)
463+
face_match = val_count == len(mesh.faces)
464+
assert face_match or val_count == len(mesh.vertices), \
465+
'Number of values ({}) must match the number of mesh faces ({}) or ' \
466+
'the number of mesh vertices ({}).'.format(
467+
val_count, len(mesh.faces), len(mesh.vertices))
468+
469+
# figure out the thresholds to be used for the contour lines
470+
min_val, max_val = self.legend_parameters.min, self.legend_parameters.max
471+
if min_val == max_val:
472+
return [], [] # no contours to be generated
473+
thresholds = list(self.segment_numbers)
474+
if self.is_max_default:
475+
thresholds.pop(-1) # no need to make a contour
476+
if self.is_min_default:
477+
thresholds.pop(0) # no need to make a contour
478+
if len(thresholds) == 0: # ensure there is at least one threshold
479+
thresholds = [(max_val + min_val) / 2]
480+
481+
# loop through the thresholds and generate contour lines
482+
contours = []
483+
init_naked_edges = mesh.naked_edges
484+
for abs_thresh in thresholds:
485+
# remove faces below the threshold
486+
pattern = [val > abs_thresh for val in self.values]
487+
if all(v for v in pattern):
488+
contours.append([])
489+
continue # full mesh in contour; not a useful line
490+
elif all(not v for v in pattern):
491+
contours.append([])
492+
continue # none of the mesh lies in the contour; not a useful line
493+
sub_mesh, _ = mesh.remove_faces(pattern) if face_match else \
494+
mesh.remove_vertices(pattern)
495+
496+
# create the contour lines
497+
contour_segs = []
498+
for seg in sub_mesh.naked_edges:
499+
for i_seg in init_naked_edges:
500+
if seg.p1.is_equivalent(i_seg.p1, tolerance) and \
501+
seg.p2.is_equivalent(i_seg.p2, tolerance):
502+
break
503+
else: # we have found a new segment for contouring
504+
contour_segs.append(seg)
505+
polylines = Polyline3D.join_segments(contour_segs, tolerance)
506+
final_contours = []
507+
for cont in polylines:
508+
if isinstance(cont, Polyline3D):
509+
cont = Polyline3D(cont.vertices, True)
510+
final_contours.append(cont)
511+
contours.append(final_contours)
512+
return contours, thresholds
513+
444514
def duplicate(self):
445515
"""Return a copy of the current legend."""
446516
return self.__copy__()

tests/legend_test.py

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,8 @@
55
from ladybug.color import Color, Colorset, ColorRange
66
from ladybug.datatype.thermalcondition import PredictedMeanVote
77

8-
from ladybug_geometry.geometry2d.pointvector import Point2D
9-
from ladybug_geometry.geometry2d.mesh import Mesh2D
10-
from ladybug_geometry.geometry3d.pointvector import Point3D, Vector3D
11-
from ladybug_geometry.geometry3d.plane import Plane
12-
from ladybug_geometry.geometry3d.mesh import Mesh3D
8+
from ladybug_geometry.geometry2d import Point2D, Mesh2D
9+
from ladybug_geometry.geometry3d import Point3D, Vector3D, Polyline3D, Plane, Mesh3D
1310

1411
import pytest
1512

@@ -527,3 +524,16 @@ def test_categorized_category_names():
527524

528525
with pytest.raises(AssertionError):
529526
legend_par.category_names = ['low', 'desired', 'too much', 'not a category']
527+
528+
529+
def test_mesh_contours():
530+
"""Test the LegendParametersCategorized category_names property."""
531+
mesh2d = Mesh2D.from_grid(num_x=2, num_y=2)
532+
mesh3d = Mesh3D.from_mesh2d(mesh2d)
533+
data = [0, 1, 2, 3]
534+
535+
legend = Legend(data, LegendParameters(segment_count=3))
536+
contours, thresholds = legend.mesh_contours(mesh3d, 0.01)
537+
assert len(contours) == len(thresholds) == 1
538+
assert isinstance(contours[0][0], Polyline3D)
539+
assert thresholds[0] == 1.5

0 commit comments

Comments
 (0)