diff --git a/CHANGELOG.md b/CHANGELOG.md index b75621461..8e2918fc4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,9 +32,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed * Changed incorrect import of `compas.geometry.intersection_line_plane()` to `compas_timber.utils.intersection_line_plane()` +* Renamed `intersection_line_plane` to `intersection_line_plane_param`. +* Renamed `intersection_line_line_3D` to `intersection_line_line_param`. ### Removed +* Removed module `compas_timber.utils.compas_extra`. ## [0.11.0] 2024-09-17 diff --git a/src/compas_timber/connections/l_miter.py b/src/compas_timber/connections/l_miter.py index 120d1da6d..e396e27e3 100644 --- a/src/compas_timber/connections/l_miter.py +++ b/src/compas_timber/connections/l_miter.py @@ -5,7 +5,7 @@ from compas.geometry import cross_vectors from compas_timber._fabrication import JackRafterCut -from compas_timber.utils import intersection_line_line_3D +from compas_timber.utils import intersection_line_line_param from .joint import BeamJoinningError from .joint import Joint @@ -64,7 +64,7 @@ def get_cutting_planes(self): vA = Vector(*self.beam_a.frame.xaxis) # frame.axis gives a reference, not a copy vB = Vector(*self.beam_b.frame.xaxis) # intersection point (average) of both centrelines - [pxA, tA], [pxB, tB] = intersection_line_line_3D( + [pxA, tA], [pxB, tB] = intersection_line_line_param( self.beam_a.centerline, self.beam_b.centerline, max_distance=float("inf"), diff --git a/src/compas_timber/design/workflow.py b/src/compas_timber/design/workflow.py index bcf87fab6..31a063a72 100644 --- a/src/compas_timber/design/workflow.py +++ b/src/compas_timber/design/workflow.py @@ -1,6 +1,6 @@ from compas_timber.connections import LMiterJoint from compas_timber.connections import TButtJoint -from compas_timber.utils.compas_extra import intersection_line_line_3D +from compas_timber.utils import intersection_line_line_param class CollectionDef(object): @@ -204,7 +204,7 @@ def __str__(self): def guess_joint_topology_2beams(beamA, beamB, tol=1e-6, max_distance=1e-6): # TODO: replace default max_distance ~ zero with global project precision - [pa, ta], [pb, tb] = intersection_line_line_3D(beamA.centerline, beamB.centerline, max_distance, True, tol) + [pa, ta], [pb, tb] = intersection_line_line_param(beamA.centerline, beamB.centerline, max_distance, True, tol) if ta is None or tb is None: # lines do not intersect within max distance or they are parallel diff --git a/src/compas_timber/elements/beam.py b/src/compas_timber/elements/beam.py index b120285a0..2fb3a3cfd 100644 --- a/src/compas_timber/elements/beam.py +++ b/src/compas_timber/elements/beam.py @@ -15,7 +15,7 @@ from compas.tolerance import TOL from compas_model.elements import reset_computed -from compas_timber.utils import intersection_line_plane +from compas_timber.utils import intersection_line_plane_param from .features import FeatureApplicationError from .timber import TimberElement @@ -577,10 +577,10 @@ def extension_to_plane(self, pln): x = {} pln = Plane.from_frame(pln) # type: ignore for e in self.long_edges: - p, t = intersection_line_plane(e, pln) + p, t = intersection_line_plane_param(e, pln) x[t] = p - px = intersection_line_plane(self.centerline, pln)[0] + px = intersection_line_plane_param(self.centerline, pln)[0] if px is None: raise ValueError("The plane does not intersect with the centerline of the beam.") side, _ = self.endpoint_closest_to_point(px) diff --git a/src/compas_timber/fabrication/btlx_processes/btlx_jack_cut.py b/src/compas_timber/fabrication/btlx_processes/btlx_jack_cut.py index 110b5ccce..78331433e 100644 --- a/src/compas_timber/fabrication/btlx_processes/btlx_jack_cut.py +++ b/src/compas_timber/fabrication/btlx_processes/btlx_jack_cut.py @@ -8,7 +8,7 @@ from compas_timber.fabrication import BTLx from compas_timber.fabrication import BTLxProcess -from compas_timber.utils.compas_extra import intersection_line_plane +from compas_timber.utils import intersection_line_plane_param class BTLxJackCut(object): @@ -77,7 +77,9 @@ def generate_process(self): self.x_edge = Line.from_point_and_vector(self.reference_side.point, self.reference_side.xaxis) - self.startX = intersection_line_plane(self.x_edge, Plane.from_frame(self.cut_plane))[1] * self.x_edge.length + self.startX = ( + intersection_line_plane_param(self.x_edge, Plane.from_frame(self.cut_plane))[1] * self.x_edge.length + ) if self.startX < self.part.blank_length / 2: self.orientation = "start" else: diff --git a/src/compas_timber/ghpython/components/CT_ShowJointTypes/code.py b/src/compas_timber/ghpython/components/CT_ShowJointTypes/code.py index f358bc3f4..d3e7d4273 100644 --- a/src/compas_timber/ghpython/components/CT_ShowJointTypes/code.py +++ b/src/compas_timber/ghpython/components/CT_ShowJointTypes/code.py @@ -3,7 +3,7 @@ from compas_rhino.conversions import point_to_rhino from ghpythonlib.componentbase import executingcomponent as component -from compas_timber.utils.compas_extra import intersection_line_line_3D +from compas_timber.utils import intersection_line_line_param class ShowJointTypes(component): @@ -16,7 +16,7 @@ def RunScript(self, model): for joint in model.joints: line_a, line_b = joint.beams[0].centerline, joint.beams[1].centerline - [p1, t1], [p2, t2] = intersection_line_line_3D(line_a, line_b, float("inf"), False, 1e-3) + [p1, t1], [p2, t2] = intersection_line_line_param(line_a, line_b, float("inf"), False, 1e-3) p1 = point_to_rhino(p1) p2 = point_to_rhino(p2) diff --git a/src/compas_timber/ghpython/components/CT_ShowTopologyTypes/code.py b/src/compas_timber/ghpython/components/CT_ShowTopologyTypes/code.py index 8f001b2e0..922149d21 100644 --- a/src/compas_timber/ghpython/components/CT_ShowTopologyTypes/code.py +++ b/src/compas_timber/ghpython/components/CT_ShowTopologyTypes/code.py @@ -4,7 +4,7 @@ from ghpythonlib.componentbase import executingcomponent as component from compas_timber.connections import JointTopology -from compas_timber.utils.compas_extra import intersection_line_line_3D +from compas_timber.utils import intersection_line_line_param class ShowTopologyTypes(component): @@ -19,7 +19,7 @@ def RunScript(self, model): beam_b = topo["beam_b"] topology = topo.get("detected_topo") - [p1, _], [p2, _] = intersection_line_line_3D( + [p1, _], [p2, _] = intersection_line_line_param( beam_a.centerline, beam_b.centerline, float("inf"), False, 1e-3 ) p1 = point_to_rhino(p1) diff --git a/src/compas_timber/utils/__init__.py b/src/compas_timber/utils/__init__.py index d3b18776c..f417c0a09 100644 --- a/src/compas_timber/utils/__init__.py +++ b/src/compas_timber/utils/__init__.py @@ -1,4 +1,136 @@ -from .compas_extra import intersection_line_line_3D -from .compas_extra import intersection_line_plane +from math import fabs -__all__ = ["intersection_line_line_3D", "intersection_line_plane", "close", "are_objects_identical"] +from compas.geometry import Plane +from compas.geometry import Point +from compas.geometry import add_vectors +from compas.geometry import cross_vectors +from compas.geometry import distance_point_point +from compas.geometry import dot_vectors +from compas.geometry import length_vector +from compas.geometry import normalize_vector +from compas.geometry import scale_vector +from compas.geometry import subtract_vectors + + +def intersection_line_line_param(line1, line2, max_distance=1e-6, limit_to_segments=True, tol=1e-6): + """Find, if exists, the intersection point of `line1` and `line2` and returns parametric information about it. + + For each of the lines, the point of intersection and a `t` parameter are returned. + + The `t` parameter is the normalized parametric value (0.0 -> 1.0) of the location of the intersection point + in relation to the line's starting point. + 0.0 indicates intersaction near the starting point, 1.0 indicates intersection near the end. + + If no intersection is detected within the max_distance, or the intersection falls outside either of the line segments, + [None, None], [None, None] is returned. + + Parameters + ---------- + line1 : :class:`~compas.geometry.Line` + First line. + line2 : :class:`~compas.geometry.Line` + Second line. + max_distance : float + Maximum distance between the lines to still consider as intersection. + limit_to_segments : bool, defualt is True + If True, the lines are considered intersection only if the intersection point falls whithin the given line segments for both lines. + tol : float, default is 1e-6 + The tolerance used for floating point operations. + + Returns + ------- + tuple(:class:`~compas.geometry.Point`, float), tuple(:class:`~compas.geometry.Point`, float) + + """ + a, b = line1 + c, d = line2 + + ab = subtract_vectors(b, a) + cd = subtract_vectors(d, c) + + n = cross_vectors(ab, cd) + + # check if lines are parallel + if length_vector(n) < tol: # if any([abs(x) max_distance: + return [None, None], [None, None] + + # is intersection within the line segment? if not, override results with None + if limit_to_segments: + if t1 < 0.0 - tol or t1 > 1.0 + tol: + x1 = None + t1 = None + if t2 < 0.0 - tol or t2 > 1.0 + tol: + x2 = None + t2 = None + return [x1, t1], [x2, t2] + + +def intersection_line_plane_param(line, plane, tol=1e-6): + """Computes the intersection point of a line and a plane. + + A tuple containing the intersection point and a `t` value are returned. + + The `t` parameter is the normalized parametric value (0.0 -> 1.0) of the location of the intersection point + in relation to the line's starting point. + 0.0 indicates intersaction near the starting point, 1.0 indicates intersection near the end. + + If no intersection is found, [None, None] is returned. + + Parameters + ---------- + line : :class:`~compas.geometry.Line` + Two points defining the line. + plane : :class:`~compas.geometry.Plane` + The base point and normal defining the plane. + tol : float, optional. Default is 1e-6. + A tolerance for membership verification. + + Returns + ------- + tuple(:class:`~compas.geometry.Point`, float) + + """ + a, b = line + o, n = plane + + ab = subtract_vectors(b, a) + dotv = dot_vectors(n, ab) + + if fabs(dotv) <= tol: + # if the dot product (cosine of the angle between segment and plane) + # is close to zero the line and the normal are almost perpendicular + # hence there is no intersection + return None, None + + # based on the ratio = -dot_vectors(n, ab) / dot_vectors(n, oa) + # there are three scenarios + # 1) 0.0 < ratio < 1.0: the intersection is between a and b + # 2) ratio < 0.0: the intersection is on the other side of a + # 3) ratio > 1.0: the intersection is on the other side of b + oa = subtract_vectors(a, o) + t = -dot_vectors(n, oa) / dotv + ab = scale_vector(ab, t) + return Point(*add_vectors(a, ab)), t + + +__all__ = ["intersection_line_line_param", "intersection_line_plane_param"] diff --git a/src/compas_timber/utils/compas_extra.py b/src/compas_timber/utils/compas_extra.py deleted file mode 100644 index d5e65b617..000000000 --- a/src/compas_timber/utils/compas_extra.py +++ /dev/null @@ -1,133 +0,0 @@ -from math import fabs - -from compas.geometry import Plane -from compas.geometry import Point -from compas.geometry import add_vectors -from compas.geometry import cross_vectors -from compas.geometry import distance_point_point -from compas.geometry import dot_vectors -from compas.geometry import length_vector -from compas.geometry import normalize_vector -from compas.geometry import scale_vector -from compas.geometry import subtract_vectors - - -def intersection_line_line_3D(line1, line2, max_distance=1e-6, limit_to_segments=True, tol=1e-6): - """Find, if exists, the intersection point of `line1` and `line2` and returns parametric information about it. - - For each of the lines, the point of intersection and a `t` parameter are returned. - - The `t` parameter is the normalized parametric value (0.0 -> 1.0) of the location of the intersection point - in relation to the line's starting point. - 0.0 indicates intersaction near the starting point, 1.0 indicates intersection near the end. - - If no intersection is detected within the max_distance, or the intersection falls outside either of the line segments, - [None, None], [None, None] is returned. - - Parameters - ---------- - line1 : :class:`~compas.geometry.Line` - First line. - line2 : :class:`~compas.geometry.Line` - Second line. - max_distance : float - Maximum distance between the lines to still consider as intersection. - limit_to_segments : bool, defualt is True - If True, the lines are considered intersection only if the intersection point falls whithin the given line segments for both lines. - tol : float, default is 1e-6 - The tolerance used for floating point operations. - - Returns - ------- - tuple(:class:`~compas.geometry.Point`, float), tuple(:class:`~compas.geometry.Point`, float) - - """ - a, b = line1 - c, d = line2 - - ab = subtract_vectors(b, a) - cd = subtract_vectors(d, c) - - n = cross_vectors(ab, cd) - - # check if lines are parallel - if length_vector(n) < tol: # if any([abs(x) max_distance: - return [None, None], [None, None] - - # is intersection within the line segment? if not, override results with None - if limit_to_segments: - if t1 < 0.0 - tol or t1 > 1.0 + tol: - x1 = None - t1 = None - if t2 < 0.0 - tol or t2 > 1.0 + tol: - x2 = None - t2 = None - return [x1, t1], [x2, t2] - - -def intersection_line_plane(line, plane, tol=1e-6): - """Computes the intersection point of a line and a plane. - - A tuple containing the intersection point and a `t` value are returned. - - The `t` parameter is the normalized parametric value (0.0 -> 1.0) of the location of the intersection point - in relation to the line's starting point. - 0.0 indicates intersaction near the starting point, 1.0 indicates intersection near the end. - - If no intersection is found, [None, None] is returned. - - Parameters - ---------- - line : :class:`~compas.geometry.Line` - Two points defining the line. - plane : :class:`~compas.geometry.Plane` - The base point and normal defining the plane. - tol : float, optional. Default is 1e-6. - A tolerance for membership verification. - - Returns - ------- - tuple(:class:`~compas.geometry.Point`, float) - - """ - a, b = line - o, n = plane - - ab = subtract_vectors(b, a) - dotv = dot_vectors(n, ab) - - if fabs(dotv) <= tol: - # if the dot product (cosine of the angle between segment and plane) - # is close to zero the line and the normal are almost perpendicular - # hence there is no intersection - return None, None - - # based on the ratio = -dot_vectors(n, ab) / dot_vectors(n, oa) - # there are three scenarios - # 1) 0.0 < ratio < 1.0: the intersection is between a and b - # 2) ratio < 0.0: the intersection is on the other side of a - # 3) ratio > 1.0: the intersection is on the other side of b - oa = subtract_vectors(a, o) - t = -dot_vectors(n, oa) / dotv - ab = scale_vector(ab, t) - return Point(*add_vectors(a, ab)), t diff --git a/tests/compas_timber/test_utils.py b/tests/compas_timber/test_utils.py new file mode 100644 index 000000000..358ba2a07 --- /dev/null +++ b/tests/compas_timber/test_utils.py @@ -0,0 +1,42 @@ +from compas.tolerance import TOL +from compas.geometry import Line +from compas.geometry import Point +from compas.geometry import Plane +from compas.geometry import Vector + +from compas_timber.utils import intersection_line_line_param +from compas_timber.utils import intersection_line_plane_param + + +def test_intersection_line_line_param(): + line_a = Line(Point(x=5.53733031674, y=18.6651583710, z=0.0), Point(x=5.53733031674, y=0.248868778281, z=0.0)) + line_b = Line(Point(x=5.53733031674, y=12.3190045249, z=0.0), Point(x=20.8427601810, y=12.3190045249, z=0.0)) + + line_a_intersection, line_b_intersection = intersection_line_line_param(line_a, line_b) + + expected_point_a = Point(x=5.53733031674, y=12.3190045249, z=0.0) + expected_t_a = 0.34459459459459457 + + expected_point_b = Point(x=5.53733031674, y=12.3190045249, z=0.0) + expected_t_b = 0.0 + + line_a_intersection_point, line_a_intersection_t = line_a_intersection + line_b_intersection_point, line_b_intersection_t = line_b_intersection + + assert TOL.is_allclose(line_a_intersection_point, expected_point_a) + assert TOL.is_close(line_a_intersection_t, expected_t_a) + assert TOL.is_allclose(line_b_intersection_point, expected_point_b) + assert TOL.is_close(line_b_intersection_t, expected_t_b) + + +def test_intersection_line_plane_param(): + line = Line(Point(x=5.53733031674, y=12.3190045249, z=0.0), Point(x=20.8427601810, y=12.3190045249, z=0.0)) + plane = Plane(point=Point(x=15.436, y=16.546, z=-2.703), normal=Vector(x=-0.957, y=-0.289, z=0.000)) + + expected_point = Point(x=16.7100478890, y=12.3190045249, z=0.0) + expected_t = 0.72998391233079618 + + intersection_point, t = intersection_line_plane_param(line, plane) + + assert TOL.is_allclose(expected_point, intersection_point) + assert TOL.is_close(expected_t, t)