Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature request: optional alternative centering in triangles #928

Open
javimixet opened this issue Mar 4, 2025 · 1 comment
Open

Feature request: optional alternative centering in triangles #928

javimixet opened this issue Mar 4, 2025 · 1 comment
Labels
enhancement New feature or request

Comments

@javimixet
Copy link

javimixet commented Mar 4, 2025

Triangles can only be centered in their centroid or barycenter. Would be nice to have more options.
Examples of working modified Triangle class, in object_sketch.py, and test snippet:

class Triangle(BaseSketchObject):
    """Sketch Object: Triangle

    Add any triangle to the sketch by specifying the length of any side and any
    two other side lengths or interior angles. Note that the interior angles are
    opposite the side with the same designation (i.e. side 'a' is opposite angle 'A').

    Args:
        a (float, optional): side 'a' length. Defaults to None.
        b (float, optional): side 'b' length. Defaults to None.
        c (float, optional): side 'c' length. Defaults to None.
        A (float, optional): interior angle 'A' in degrees. Defaults to None.
        B (float, optional): interior angle 'B' in degrees. Defaults to None.
        C (float, optional): interior angle 'C' in degrees. Defaults to None.
        rotation (float, optional): angles to rotate objects. Defaults to 0.
        center (str, optional): Where to center the triangle. Defaults to 'centroid'.
            Choices are vertices 'A', 'B' or 'C' and 'orthocenter', 'circumcenter', 'incenter' or 'centroid'.
        align (Union[Align, tuple[Align, Align]], optional): align min, center, or max of object.
            Defaults to None.
        mode (Mode, optional): combination mode. Defaults to Mode.ADD.

    Raises:
        ValueError: One length and two other values were not provided
    """

    _applies_to = [BuildSketch._tag]

    def __init__(
        self,
        *,
        a: float | None = None,
        b: float | None = None,
        c: float | None = None,
        A: float | None = None,
        B: float | None = None,
        C: float | None = None,
        center: str = "centroid",
        align: Align | tuple[Align, Align] | None = None,
        rotation: float = 0,
        mode: Mode = Mode.ADD,
    ):
        context: BuildSketch | None = BuildSketch._get_context(self)
        validate_inputs(context, self)

        if [v is None for v in [a, b, c]].count(True) == 3 or [
            v is None for v in [a, b, c, A, B, C]
        ].count(True) != 3:
            raise ValueError("One length and two other values must be provided")

        A, B, C = (radians(angle) if angle is not None else None for angle in [A, B, C])
        ar, br, cr, Ar, Br, Cr = trianglesolver.solve(a, b, c, A, B, C)
        self.a = ar  #: length of side 'a'
        self.b = br  #: length of side 'b'
        self.c = cr  #: length of side 'c'
        self.A = degrees(Ar)  #: interior angle 'A' in degrees
        self.B = degrees(Br)  #: interior angle 'B' in degrees
        self.C = degrees(Cr)  #: interior angle 'C' in degrees
        triangle = Face(
            Wire.make_polygon(
                [Vector(0, 0), Vector(ar, 0), Vector(cr, 0).rotate(Axis.Z, self.B)]
            )
        )
        
        # Center the triangle
        vertices = triangle.vertices()
        ax, ay, _ = vertices[0]  # Vertex A
        bx, by, _ = vertices[1]  # Vertex B
        cx, cy, _ = vertices[2]  # Vertex C

        if center == "A":  
            pass  # Already centered at A

        elif center == "B":  
            triangle.move(Location(-Vector(bx, by)))

        elif center == "C":  
            triangle.move(Location(-Vector(cx, cy)))

        elif center == "centroid":  
            centroid = sum((Vector(v) for v in vertices), Vector(0, 0, 0)) / 3
            triangle.move(Location(-centroid))

        elif center == "orthocenter":
            # Compute perpendicular slopes for altitudes
            def perp_bisector(x1, y1, x2, y2):
                """Returns midpoint and perpendicular slope"""
                mid_x, mid_y = (x1 + x2) / 2, (y1 + y2) / 2
                slope = None if x1 == x2 else -(x2 - x1) / (y2 - y1)
                return mid_x, mid_y, slope

            mid_bc, _, slope_a = perp_bisector(bx, by, cx, cy)
            mid_ac, _, slope_b = perp_bisector(ax, ay, cx, cy)

            # Solve for intersection of two altitudes
            if slope_a is None:  # Vertical line case
                hx, hy = ax, slope_b * ax + (by - slope_b * bx)
            elif slope_b is None:
                hx, hy = bx, slope_a * bx + (ay - slope_a * ax)
            else:
                hx = (by - ay + slope_a * ax - slope_b * bx) / (slope_a - slope_b)
                hy = slope_a * hx + (ay - slope_a * ax)

            triangle.move(Location(-Vector(hx, hy)))

        elif center == "circumcenter":
            d = 2 * (ax * (by - cy) + bx * (cy - ay) + cx * (ay - by))
            ux = ((ax**2 + ay**2) * (by - cy) + (bx**2 + by**2) * (cy - ay) + (cx**2 + cy**2) * (ay - by)) / d
            uy = ((ax**2 + ay**2) * (cx - bx) + (bx**2 + by**2) * (ax - cx) + (cx**2 + cy**2) * (bx - ax)) / d
            triangle.move(Location(-Vector(ux, uy)))

        elif center == "incenter":
            # Compute side lengths
            a = ((bx - cx)**2 + (by - cy)**2) ** 0.5  # Opposite A
            b = ((ax - cx)**2 + (ay - cy)**2) ** 0.5  # Opposite B
            c = ((ax - bx)**2 + (ay - by)**2) ** 0.5  # Opposite C

            # Compute incenter
            ix = (a * ax + b * bx + c * cx) / (a + b + c)
            iy = (a * ay + b * by + c * cy) / (a + b + c)
            triangle.move(Location(-Vector(ix, iy)))

        else:
            raise ValueError("Invalid center value")

            
        alignment = None if align is None else tuplify(align, 2)
        super().__init__(obj=triangle, rotation=rotation, align=alignment, mode=mode)
        self.edge_a = self.edges().filter_by(lambda e: abs(e.length - ar) < TOLERANCE)[
            0
        ]  #: edge 'a'
        self.edge_b = self.edges().filter_by(
            lambda e: abs(e.length - br) < TOLERANCE and e not in [self.edge_a]
        )[
            0
        ]  #: edge 'b'
        self.edge_c = self.edges().filter_by(
            lambda e: e not in [self.edge_a, self.edge_b]
        )[
            0
        ]  #: edge 'c'
        self.vertex_A = topo_explore_common_vertex(
            self.edge_b, self.edge_c
        )  #: vertex 'A'
        self.vertex_B = topo_explore_common_vertex(
            self.edge_a, self.edge_c
        )  #: vertex 'B'
        self.vertex_C = topo_explore_common_vertex(
            self.edge_a, self.edge_b
        )  #: vertex 'C'
# %%
from build123d import *
from ocp_vscode import *
from math import sqrt

set_defaults(reset_camera=Camera.KEEP)
set_colormap(ColorMap.seeded(colormap="rgb", alpha=1, seed_value="vscod"))

side_a = 1
side_b = 1
angle_C = 30

# %%
default = Triangle(a=side_a, b=side_b, C=angle_C)
centroid = Triangle(a=side_a, b=side_b, C=angle_C, center="centroid")

show(default, centroid, names=["default", "centroid"])

# %%
A = Triangle(a=side_a, b=side_b, C=angle_C, center="A")
B = Triangle(a=side_a, b=side_b, C=angle_C, center="B")
C = Triangle(a=side_a, b=side_b, C=angle_C, center="C")

show(A, B, C, names=["A", "B", "C"])

# %%
orthocenter = Triangle(a=side_a, b=side_b, C=angle_C, center="orthocenter")

show(orthocenter, names=["orthocenter"])

# %%
circumcenter = Triangle(a=side_a, b=side_b, C=angle_C, center="circumcenter")
circumcircle = Circle(
    sqrt(
        circumcenter.vertices()[0].position.X ** 2
        + circumcenter.vertices()[0].position.Y ** 2
    )
)

show(circumcenter, circumcircle, names=["circumcenter", "circumcircle"])

# %%
incenter = Triangle(a=side_a, b=side_b, C=angle_C, center="incenter")
incircle = Circle(-incenter.vertices()[0].position.Y)

show(incenter, incircle, names=["incenter", "incircle"])
@gumyr gumyr added the enhancement New feature or request label Mar 6, 2025
@gumyr gumyr added this to the Not Gating Release 1.0.0 milestone Mar 6, 2025
@gumyr
Copy link
Owner

gumyr commented Mar 6, 2025

The implementation shown here would violate make of the build123d conventions - having a center parameter, a parameter with a string type, etc. This type of positioning functionality is currently provided by align which also be enhanced (align=Align.CENTROID). It also seems totally reasonable to add a set of properties like orthocenter, circumcenter, etc. such that a user could do:

t = Triangle(...)
t.locate(t.orthocenter)

or something similar (doesn't work as well in builder mode).

Keep in mind that there already are edge_a, edge_b, edge_c, vertex_A, vertex_B, and vertex_C attributes of the Triangle class so all of the positioning calculations can be done fairly easily.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

2 participants