investigating profiles for validity

investigating profiles for validity

matthewaudio
Contributor Contributor
1,037 Views
4 Replies
Message 1 of 5

investigating profiles for validity

matthewaudio
Contributor
Contributor

Hey everyone.  I'm trying to use the ExtrudeFeatures_createInput and getting errors in my project.  When I try and create a simple example, the error disappears.  is there a way to examine the input to this function for validity?  like how do I check the arguments for what might be causing this error (below)

 

return _fusion.ExtrudeFeatures_createInput(self, profile, operation)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
RuntimeError: 2 : InternalValidationError : bSet

 

0 Likes
Accepted solutions (1)
1,038 Views
4 Replies
Replies (4)
Message 2 of 5

Jorge_Jaramillo
Collaborator
Collaborator
Could you share your code and model?
It is hard to provide an advice without them.
0 Likes
Message 3 of 5

matthewaudio
Contributor
Contributor

@Jorge_Jaramillo This is the code.  Didn't want to burden the forum with a 1500 line script.  But the operative code is the last 20-40 lines.    If you really want to try running it, you load the plugin, select two halves of a miter joint and the inside edge. 

#Author-Autodesk Inc.
#Description-Create matJoint

from mimetypes import init
import adsk.core, adsk.fusion, traceback
import math
from collections import namedtuple
from itertools import combinations


defaultMatJointName = 'MatJoint'




defaultShallowInsetLength = 1
defaultDeepInsetLength = 1
defaultBoardThickness = .75
defaultMillDiameter =.125
defaultFilletRadius = 1
defaultShallowInsetDepth = .25
defaultDeepInsetDepth = .5
defaultNumSplines = 1

# global set of event handlers to keep them referenced for the duration of the command
handlers = []
app = adsk.core.Application.get()
if app:
    ui = app.userInterface

newComp = None




######## GEOMETRY HELPERS
class RelevantAnatomy:
    def __init__(self, miterAngle, bodyA,miterFaceA,adjFaceA,edgeA, bodyB,miterFaceB,adjFaceB,edgeB,edgeOverlap):
        self.miterAngle=miterAngle
        self.bodyA=bodyA
        self.miterFaceA=miterFaceA
        self.adjFaceA=adjFaceA
        self.edgeA=edgeA
        self.bodyB=bodyB
        self.miterFaceB=miterFaceB
        self.adjFaceB=adjFaceB
        self.edgeB=edgeB
        self.edgeOverlap=edgeOverlap

def calculateRelevantAnatomyFromSelection(selectedEdge,selectedBodies):

    app.log("calculateRelevantAnatomyFromSelection")

    _edgeA = selectedEdge
    _bodyA, _miterFaceA, _bodyB, _miterFaceB = validate_bodies_and_edge(_edgeA, selectedBodies)
    if not all([_bodyA, _miterFaceA, _bodyB, _miterFaceB]):
        app.log("Validation failed: One or more elements are missing.")
        return None

    # Find the colinear edge on faceB
    _edgeB = find_colinear_edge_on_faceB(_edgeA, _miterFaceB)
    if not _edgeB:
        app.log("No colinear edge found on Face B.")
        return None

    # Find adjacent faces
    _adjFaceA = find_adjacent_faces(_bodyA, _miterFaceA, _edgeA)
    _adjFaceB = find_adjacent_faces(_bodyB, _miterFaceB, _edgeB)
    if not _adjFaceA or not _adjFaceB:
        app.log("Failed to find adjacent faces.")
        return None

    try:
        _miterAngle = calculate_miter_angle(_miterFaceA,_adjFaceA,_edgeA)
    except:
        if ui:
            ui.messageBox('miter exception?:\n{}'.format(traceback.format_exc()))
            
    if not _miterAngle:
        app.log("Failed to find miterAngle")
        return None


    _edgeOverlap = get_overlapping_segment(_edgeA,_edgeB)

    if not _edgeOverlap:
        app.log("edges don't overlap")
        return None

    app.log(f"geometryValid {_miterAngle}")


    #selection = ui.activeSelections.add(_adjFaceA)
    selection = ui.activeSelections.add(_miterFaceA)

    result = RelevantAnatomy(
        _miterAngle           ,
        _bodyA            ,
        _miterFaceA       ,
        _adjFaceA         ,
        _edgeA            ,
        _bodyB            ,
        _miterFaceB       ,
        _adjFaceB         ,
        _edgeB            ,
        _edgeOverlap
    )


    return result

def points_almost_equal(point1, point2, tolerance=1e-6):
    """Check if two points are approximately equal within a given tolerance.

    Args:
        point1 (adsk.core.Point3D): The first point.
        point2 (adsk.core.Point3D): The second point.
        tolerance (float): The tolerance within which the points are considered equal.

    Returns:
        bool: True if the points are approximately equal, False otherwise.
    """
    return point1.distanceTo(point2) < tolerance

def str_vertex(pt3d):
    # Assuming 'vertex' is an adsk.core.Point3D object
    x = pt3d.x
    y = pt3d.y
    z = pt3d.z

    return f"[{x:.2f}, {y:.2f}, {z:.2f}]"

def str_face(face):
    # Initialize an empty list to hold vertex strings
    vertices_str = []

    # Iterate over the vertices of the face
    for vertex in face.vertices:
        # Convert each vertex to a string using str_vertex
        vertex_str = str_vertex(vertex.geometry)
        # Append the formatted string to the list
        vertices_str.append(vertex_str)

    # Join all vertex strings into a single string
    return ' '.join(vertices_str)


def get_overlapping_segment(edge1, edge2):
    # Assuming edge1 and edge2 are co-linear and defined by startVertex and endVertex
    points = [
        edge1.startVertex.geometry, edge1.endVertex.geometry,
        edge2.startVertex.geometry, edge2.endVertex.geometry
    ]

    points[0].idx = 0
    points[1].idx = 1
    points[2].idx = 2
    points[3].idx = 3

    # Sort the points based on their position along the line
    # This simplistic sorting works if edges are perfectly aligned along an axis
    # For more complex cases, sorting may require calculating positions along the line direction
    points.sort(key=lambda point: (point.x, point.y, point.z))

    # Check for overlap
    if points_almost_equal(points[1],points[2]):
        # There is no overlap, or the overlap is a single point
        return None
    else:
        # Return the overlapping segment as a tuple of (startPoint, endPoint)
        return (points[1], points[2])

def calculate_miter_angle(miterFaceA, adjFaceA, sharedEdge):
   
    angleDegrees = 360-2*get_angle_between_faces(miterFaceA, adjFaceA,sharedEdge)


    # Calculate the miter angle
    miterAngle =  angleDegrees

    return miterAngle
def get_angle_between_faces(face1, face2, shared_edge):
    try:
        # Log the input face IDs (or other identifiable info)
        app.log(f"Calculating angle between faces: {face1.tempId}, {face2.tempId}")

        # Get the direction vector of the shared edge
        edge_start = shared_edge.startVertex.geometry
        edge_end = shared_edge.endVertex.geometry
        edge_direction = adsk.core.Vector3D.create(edge_end.x - edge_start.x, edge_end.y - edge_start.y, edge_end.z - edge_start.z)
        
        # Log the edge direction

        # Get the normal vectors of the faces
        normal1 = face1.geometry.normal
        normal2 = face2.geometry.normal

        # Log the normals

        # Project the normals onto a plane perpendicular to the edge
        normal1_projected = project_vector_onto_plane(normal1, edge_direction)
        normal2_projected = project_vector_onto_plane(normal2, edge_direction)

        # Log the projected normals

        # Calculate the dot product
        dot_product = normal1_projected.dotProduct(normal2_projected)
        
        # Log the dot product

        # Calculate the angle in radians and then convert to degrees
        angle_radians = math.acos(dot_product)
        angle_degrees = math.degrees(angle_radians)

        # Log the final angle

        return 180-angle_degrees

    except:
        if ui:
            ui.messageBox('Failed:\n{}'.format(traceback.format_exc()))


def project_vector_onto_plane(vector, plane_normal):
    # Normalize the plane normal for accurate calculations
    plane_normal.normalize()

    # Calculate the component of the vector parallel to the plane normal
    parallel_component = plane_normal.copy()
    parallel_component.scaleBy(vector.dotProduct(plane_normal))

    # Subtract the parallel component from the original vector
    # to get the component that lies in the plane
    vector_projected = vector.copy()
    vector_projected.subtract(parallel_component)

    return vector_projected
def find_adjacent_faces(bodyA, faceA, edgeA):
    app.log("find_adjacent_faces")

    # Find the adjacent face on bodyA
    adjacent_faceA = None
    for face in bodyA.faces:
        if face != faceA and edgeA in face.edges:
            adjacent_faceA = face
            break
    return adjacent_faceA

def find_colinear_edge_on_faceB(selectedEdge, faceB):
    app.log("find_colinear_edge_on_faceB")
    for edge in faceB.edges:
        if are_edges_colinear(selectedEdge, edge):
            return edge
    return None

def are_edges_colinear(edge1, edge2):
    app.log("are_edges_colinear")
    # Get the Line3D representations of the edges
    line1 = edge1.geometry.asInfiniteLine()
    line2 = edge2.geometry.asInfiniteLine()

    # Check if the lines are colinear
    return line1.isColinearTo(line2)
def calculate_combined_bounding_box(elements):
    min_x = min_y = min_z = float('inf')
    max_x = max_y = max_z = float('-inf')

    for element in elements:
        points = []
        if isinstance(element, adsk.core.Point3D):
            points = [element]
        elif isinstance(element, adsk.fusion.SketchLine):
            points = [element.startSketchPoint.geometry, element.endSketchPoint.geometry]
        elif isinstance(element, adsk.fusion.SketchCircle):
            center = element.centerSketchPoint.geometry
            radius = element.radius
            points = [
                adsk.core.Point3D.create(center.x - radius, center.y - radius, center.z),
                adsk.core.Point3D.create(center.x + radius, center.y + radius, center.z)
            ]
        elif isinstance(element, adsk.fusion.SketchPoint):
            points = [element.geometry]
        elif isinstance(element, adsk.fusion.SketchLineList):
            for line in element:
                points.extend([line.startSketchPoint.geometry, line.endSketchPoint.geometry])
        else:
            app.log(f"Unhandled element type: {type(element)}")
            continue

        for point in points:
            min_x = min(min_x, point.x)
            min_y = min(min_y, point.y)
            min_z = min(min_z, point.z)
            max_x = max(max_x, point.x)
            max_y = max(max_y, point.y)
            max_z = max(max_z, point.z)

    if min_x == float('inf') or max_x == float('-inf'):
        app.log("No valid elements processed.")
        return None

    min_point = adsk.core.Point3D.create(min_x, min_y, min_z)
    max_point = adsk.core.Point3D.create(max_x, max_y, max_z)
    return adsk.core.BoundingBox3D.create(min_point, max_point)


#given edge and two bodies
#returns two bodies, associated faces of those bodies inside miter joint.
def validate_bodies_and_edge(selectedEdge, selectedBodies):
    app.log("validate_bodies_and_edge")
    bodyA, bodyB, faceA, faceB = None, None, None, None

    # Check if the selected edge belongs to one of the bodies
    for body in selectedBodies:
        if selectedEdge in body.edges:
            bodyA = body
            break

    if not bodyA:
        app.log("Selected edge does not belong to any of the selected bodies.")
        return None, None, None, None

    # Identify bodyB as the other body
    bodyB = selectedBodies[1] if bodyA == selectedBodies[0] else selectedBodies[0]

    # Find a coplanar face on bodyB that overlaps with a face on bodyA
    for face in bodyA.faces:
        if selectedEdge in face.edges:
            planeA = face.geometry
            for otherFace in bodyB.faces:
                planeB = otherFace.geometry

                cop = planeA.isCoPlanarTo(planeB)
                overlap = do_faces_overlap(face, otherFace)
                app.log(f"comparing {face.tempId} to {otherFace.tempId} cop:{cop} overlap:{overlap}")
                if cop:
                    app.log(f"  face:{str_face(face)}  otherface:{str_face(otherFace)}")

                if cop and overlap:
                    faceA, faceB = face, otherFace
                    break
            if faceA and faceB:
                break

    if not (faceA and faceB):
        app.log("No corresponding coplanar and overlapping face found.")
        return None, None, None, None

    return bodyA, faceA, bodyB, faceB

def do_faces_overlap(faceA, faceB):
    def calculate_midpoint(start, end):
        return adsk.core.Point3D.create((start.x + end.x) / 2, (start.y + end.y) / 2, (start.z + end.z) / 2)

    def create_check_vertex_list(faceA, faceB):
        checkVertexList = []

        # Add all vertices of faceA
        for vertex in faceA.vertices:
            checkVertexList.append(vertex.geometry)

        # Add all vertices of faceB
        for vertex in faceB.vertices:
            checkVertexList.append(vertex.geometry)

        # Add midpoints of each edge of faceA
        for edge in faceA.edges:
            start = edge.startVertex.geometry
            end = edge.endVertex.geometry
            checkVertexList.append(calculate_midpoint(start, end))

        # Add midpoints of each edge of faceB
        for edge in faceB.edges:
            start = edge.startVertex.geometry
            end = edge.endVertex.geometry
            checkVertexList.append(calculate_midpoint(start, end))

        return checkVertexList

    def create_direction_vector(face):
        # Create a direction vector on the plane of the face
        normal = face.geometry.normal
        return adsk.core.Vector3D.create(-normal.y, normal.x, 0) if normal.x != 0 or normal.y != 0 else adsk.core.Vector3D.create(0, -normal.z, normal.y)

    def create_infinite_line(point, direction_vector):
        # Fusion 360 may not support infinite lines directly, so return a tuple with the point and direction
        return (point, direction_vector)
    
    def calculate_line_edge_intersection(linePoint, lineDirection, edgeStart, edgeEnd):
        # Create vectors
        edgeDirection = adsk.core.Vector3D.create(edgeEnd.x - edgeStart.x, edgeEnd.y - edgeStart.y, edgeEnd.z - edgeStart.z)
        lineToEdgeStart = adsk.core.Vector3D.create(edgeStart.x - linePoint.x, edgeStart.y - linePoint.y, edgeStart.z - linePoint.z)

        # Cross products to find if line and edge are parallel
        cross_prod = lineDirection.crossProduct(edgeDirection)
        if cross_prod.length == 0:  # The line and edge are parallel
            return None

        # Manually calculate the squared length of the cross product
        denominator = cross_prod.x * cross_prod.x + cross_prod.y * cross_prod.y + cross_prod.z * cross_prod.z
        if denominator == 0:
            return None

        t = (lineToEdgeStart.crossProduct(edgeDirection).dotProduct(cross_prod)) / denominator
        u = (lineToEdgeStart.crossProduct(lineDirection).dotProduct(cross_prod)) / denominator

        # Check if intersection point is within the edge segment and on the line
        if 0 <= u <= 1:
            intersection = adsk.core.Point3D.create(
                linePoint.x + t * lineDirection.x,
                linePoint.y + t * lineDirection.y,
                linePoint.z + t * lineDirection.z
            )
            return intersection

        return None


    def project_point_on_line(point, linePoint, lineDirection):
        # Create a vector from linePoint to the point
        pointVector = adsk.core.Vector3D.create(point.x - linePoint.x, point.y - linePoint.y, point.z - linePoint.z)

        # Project this vector onto the line direction (dot product)
        projection = pointVector.dotProduct(lineDirection)

        return projection
    def do_segments_overlap(segmentA, segmentB):
        A1, A2 = segmentA  # Endpoints of segment A
        B1, B2 = segmentB  # Endpoints of segment B

        # Convert the 3D problem into a 2D problem by dropping one of the coordinates
        # Assuming the z-coordinate can be dropped (i.e., the segments are not vertical)
        # This simplification works when segments are coplanar
        A1_2d = (A1.x, A1.y)
        A2_2d = (A2.x, A2.y)
        B1_2d = (B1.x, B1.y)
        B2_2d = (B2.x, B2.y)

        return segments_intersect(A1_2d, A2_2d, B1_2d, B2_2d)
    
    def any_segments_overlap(segmentsA, segmentsB):
        for segmentA in segmentsA:
            for segmentB in segmentsB:
                if do_segments_overlap(segmentA, segmentB):
                    return True
        return False
    
    def segments_intersect(A1, A2, B1, B2):
        def ccw(A, B, C):
            return (C[1] - A[1]) * (B[0] - A[0]) > (B[1] - A[1]) * (C[0] - A[0])

        def intersect(A, B, C, D):
            return ccw(A, C, D) != ccw(B, C, D) and ccw(A, B, C) != ccw(A, B, D)

        return intersect(A1, A2, B1, B2) or \
            (point_on_segment(A1, B1, B2) or point_on_segment(A2, B1, B2) or \
                point_on_segment(B1, A1, A2) or point_on_segment(B2, A1, A2))

    def point_on_segment(P, A, B):
        return min(A[0], B[0]) <= P[0] <= max(A[0], B[0]) and \
            min(A[1], B[1]) <= P[1] <= max(A[1], B[1])

    def get_line_segments(face, line):
        linePoint, lineDirection = line
        segments = []

        for edge in face.edges:
            edgeStart = edge.startVertex.geometry
            edgeEnd = edge.endVertex.geometry
            intersectionPoint = calculate_line_edge_intersection(linePoint, lineDirection, edgeStart, edgeEnd)

            if intersectionPoint is not None:
                segments.append(intersectionPoint)

        # Sort segments along the line
        segments.sort(key=lambda point: project_point_on_line(point, linePoint, lineDirection))

        # Return pairs of points as segments
        return [(segments[i], segments[i + 1]) for i in range(0, len(segments) - 1, 2)]

    #list of vertices to create a cross section through
    checkVertexList = create_check_vertex_list(faceA, faceB)
    #the direction of that cross section
    direction_vector = create_direction_vector(faceA)  # Assuming faceA and faceB are coplanar

    for point in checkVertexList:
        line = create_infinite_line(point, direction_vector)
        segmentsA = get_line_segments(faceA, line)
        segmentsB = get_line_segments(faceB, line)
        if any_segments_overlap(segmentsA, segmentsB):
            return True


    return False





def is_point_inside_polygon(point, polygon_vertices, plane):
    # Project the point and polygon vertices onto the plane
    projected_point = project_point_onto_plane(point, plane)
    projected_polygon = [project_point_onto_plane(vertex.geometry, plane) for vertex in polygon_vertices]

    # Check if the point is inside the polygon (2D check)
    num_intersections = 0
    for i in range(len(projected_polygon)):
        p1 = projected_polygon[i]
        p2 = projected_polygon[(i + 1) % len(projected_polygon)]
        if (p1.y > projected_point.y) != (p2.y > projected_point.y):
            if projected_point.x < (p2.x - p1.x) * (projected_point.y - p1.y) / (p2.y - p1.y) + p1.x:
                num_intersections += 1
    return num_intersections % 2 == 1  # Point is inside if the number of intersections is odd


def check_edge_intersection(edgeA, edgeB, plane):
    # Project the start and end points of both edges onto the plane
    startA = project_point_onto_plane(edgeA.startVertex.geometry, plane)
    endA = project_point_onto_plane(edgeA.endVertex.geometry, plane)
    startB = project_point_onto_plane(edgeB.startVertex.geometry, plane)
    endB = project_point_onto_plane(edgeB.endVertex.geometry, plane)

    # Check if the lines intersect
    return lines_intersect(startA, endA, startB, endB)

def project_point_onto_plane(point, plane):
    vectorToPoint = adsk.core.Vector3D.create(point.x - plane.origin.x, point.y - plane.origin.y, point.z - plane.origin.z)
    distance = vectorToPoint.dotProduct(plane.normal)
    return adsk.core.Point3D.create(point.x - distance * plane.normal.x, point.y - distance * plane.normal.y, point.z - distance * plane.normal.z)

def lines_intersect(p1, p2, p3, p4):
    # Calculate intersection between two line segments
    # This is a simplified version and may need refinement for edge cases
    denom = ((p4.y - p3.y) * (p2.x - p1.x)) - ((p4.x - p3.x) * (p2.y - p1.y))
    if denom == 0:
        return False  # Lines are parallel or coincident

    uA = (((p4.x - p3.x) * (p1.y - p3.y)) - ((p4.y - p3.y) * (p1.x - p3.x))) / denom
    uB = (((p2.x - p1.x) * (p1.y - p3.y)) - ((p2.y - p1.y) * (p1.x - p3.x))) / denom

    if (0 <= uA <= 1) and (0 <= uB <= 1):
        return True  # Intersection point is within both line segments

    return False

def process_face(face):
    app = adsk.core.Application.get()
    #_ui  = app.userInterface
    #app.log("process_face")

    try:
        # Check if the face object is valid
        if face and face.isValid:
            #app.log(f"Processing face with temp ID: {face.tempId}")

            # Using the evaluator
            evaluator = face.evaluator

            # Check if evaluator is valid
            if evaluator.isValid:
                # Example usage of evaluator
                # Get the area of the face
                area = face.area
                #app.log(f"Area of the face: {area} square cm")

             
                # Get a point on the face
                pointOnFace = face.pointOnFace
                #app.log(f"Point on face: ({pointOnFace.x}, {pointOnFace.y}, {pointOnFace.z})")

            else:
                app.log("Invalid evaluator for the face.")
        else:
            app.log("Invalid or null BRepFace object provided.")

    except Exception as e:
        app.log(f"Error processing face: {str(e)}")

def is_point_inside_face(point, face):

    process_face(face)
    # Get the SurfaceEvaluator for the face
    evaluator = face.evaluator

    # Get the parameters (u, v) at the point on the surface
    success, parameter = evaluator.getParameterAtPoint(point)
    if not success:
        return False

    # Check if the parameters (u, v) are on the face
    # Accessing u and v coordinates of the Point2D directly
    u = parameter.x
    v = parameter.y
    point2D = adsk.core.Point2D.create(u, v)
    isInside = evaluator.isParameterOnFace(point2D)
    return isInside


def find_matching_vertex_in_edges(edges, point, tolerance=1e-6):
   
    for edge in edges:
        # Check start vertex
        if edge.startVertex.geometry.isEqualTo(point):
            return edge.startVertex

        # Check end vertex
        if edge.endVertex.geometry.isEqualTo(point):
            return edge.endVertex

    return None



###### HELP BUILD
def calculate_normalized_points(num_splines):
    normalized_interval = 1 / num_splines
    normalized_points = []

    for i in range(num_splines):
        point = (normalized_interval / 2) + (i * normalized_interval)
        normalized_points.append(point)

    return normalized_points

def interpolate_points(start_point, end_point, normalized_values):
    interpolated_points = []
    for value in normalized_values:
        # Linear interpolation for each coordinate
        x = start_point.x + (end_point.x - start_point.x) * value
        y = start_point.y + (end_point.y - start_point.y) * value
        z = start_point.z + (end_point.z - start_point.z) * value

        # Create a new Point3D for the interpolated point
        interpolated_point = adsk.core.Point3D.create(x, y, z)
        interpolated_points.append(interpolated_point)

    return interpolated_points


def create_sketch_and_project_points(face,normalized_points):
    # Access the application and user interface
    app = adsk.core.Application.get()
    ui = app.userInterface

    try:
        # Get the product and design
        product = app.activeProduct
        design = adsk.fusion.Design.cast(product)
        rootComp = design.rootComponent

        # Create a new sketch on the specified face
        sketches = rootComp.sketches
        newSketch = sketches.add(face)

        # Iterate through the normalized points and add them to the sketch
        for point in normalized_points:
            # Create a sketch point for each normalized point
            newSketch.project(point)

        return newSketch

    except:
        if ui:
            ui.messageBox('Failed to create sketch and project points:\n{}'.format(traceback.format_exc()))
        return None


def scale(x, in_min, in_max, out_min, out_max):
    return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min


def create_parametric_point(app, sketch, vertexA, vertexB, percentage):
    try:
        # Project the vertices onto the sketch
        projectedPointA = sketch.project(vertexA).item(0)
        projectedPointB = sketch.project(vertexB).item(0)

        # Create or get a user parameter for the percentage
        design = app.activeProduct
        param = design.userParameters.itemByName('Percentage')
        if not param:
            param = design.userParameters.add('Percentage', adsk.core.ValueInput.createByString(str(percentage)), '', 'Percentage along the line')
        param.expression = str(percentage)

        # Calculate the distance between the two points
        distance = vertexA.geometry.distanceTo(vertexB.geometry)

        # Add a dimension (driven by the parameter) between the two points
        dimension = sketch.sketchDimensions.addDistanceDimension(
            projectedPointA, projectedPointB,
            adsk.fusion.DimensionOrientations.AlignedDimensionOrientation,
            adsk.core.Point3D.create((projectedPointA.geometry.x + projectedPointB.geometry.x) / 2, 
                                     (projectedPointA.geometry.y + projectedPointB.geometry.y) / 2, 
                                     (projectedPointA.geometry.z + projectedPointB.geometry.z) / 2))

        # Set the dimension to be driven by the parameter
        dimension.parameter.expression = 'Percentage * ' + str(distance)

        return projectedPointA, projectedPointB

    except Exception as e:
        if app.userInterface:
            app.userInterface.messageBox('Failed:\n{}'.format(traceback.format_exc()))
        return None, None
    
    
def create_parametric_point_on_line(app, fraction, lineStart, lineEnd):
    try:
        design = app.activeProduct
        rootComp = design.rootComponent

        # Create the main sketch on a plane that intersects the line
        mainSketch = rootComp.sketches.add(rootComp.xYConstructionPlane)

        # Project the start and end points onto the main sketch
        projectedPointStart = mainSketch.project(lineStart).item(0)
        projectedPointEnd = mainSketch.project(lineEnd).item(0)

        # Create a line between the projected points
        sketchLine = mainSketch.sketchCurves.sketchLines.addByTwoPoints(projectedPointStart, projectedPointEnd)

        # Create a user parameter for the fraction if it doesn't exist
        param = design.userParameters.itemByName('Fraction')
        if not param:
            param = design.userParameters.add('Fraction', adsk.core.ValueInput.createByString(str(fraction)), '', 'Fraction along the line')
        param.expression = str(fraction)

        # Add a sketch point at the start of the line
        sketchPoint = mainSketch.sketchPoints.add(sketchLine.startSketchPoint.geometry)

        # Add a dimension controlled by the fraction
        dimension = mainSketch.sketchDimensions.addDistanceDimension(
            sketchLine.startSketchPoint, sketchPoint,
            adsk.fusion.DimensionOrientations.AlignedDimensionOrientation,
            sketchPoint.geometry)
        dimension.parameter.expression = 'Fraction * ' + sketchLine.length.expression

        return sketchPoint

    except Exception as e:
        if app.userInterface:
            app.userInterface.messageBox('Failed:\n{}'.format(traceback.format_exc()))
        return None
        
def add_parametric_point_on_line(app, line, percentage):
    try:
        design = app.activeProduct
        rootComp = design.rootComponent

        # Create a new sketch on the same plane as the line
        sketch = rootComp.sketches.add(line.parentSketch.referencePlane)

        # Use the start and end points of the line
        lineStart = line.startSketchPoint.geometry
        lineEnd = line.endSketchPoint.geometry

        # Add a sketch point at the start point of the line
        sketchPoint = sketch.sketchPoints.add(lineStart)

        # Constrain the point to the line
        sketch.geometricConstraints.addCoincident(sketchPoint, line)

        # Create or get a user parameter for the percentage
        param = design.userParameters.itemByName('LinePercentage')
        if not param:
            param = design.userParameters.add('LinePercentage', adsk.core.ValueInput.createByString(str(percentage)), 'cm', 'Percentage along the line')
        param.expression = str(percentage)

        # Add a dimension driven by the parameter
        dimension = sketch.sketchDimensions.addDistanceDimension(
            line.startSketchPoint, sketchPoint, 
            adsk.fusion.DimensionOrientations.AlignedDimensionOrientation, 
            sketchPoint.geometry)
        dimension.parameter.expression = 'LinePercentage * ' + line.length.expression

        return sketchPoint

    except Exception as e:
        app.log(f"Error in add_parametric_point_on_line: {str(e)}")
        if app.userInterface:
            app.userInterface.messageBox('Failed:\n{}'.format(traceback.format_exc()))
        return None


def add_point_at_midpoint_of_line(sketch, line):
    try:
        # Calculate the midpoint coordinates
        startX = line.startSketchPoint.geometry.x
        startY = line.startSketchPoint.geometry.y
        endX = line.endSketchPoint.geometry.x
        endY = line.endSketchPoint.geometry.y
        midX = (startX + endX) / 2
        midY = (startY + endY) / 2

        # Create a point at the midpoint
        midpoint = adsk.core.Point3D.create(midX, midY, 0)
        sketchPoint = sketch.sketchPoints.add(midpoint)

        # Add a midpoint constraint between the sketch point and the line
        sketch.geometricConstraints.addMidPoint(sketchPoint, line)

        return sketchPoint

    except Exception as e:
        if app.userInterface:
            app.userInterface.messageBox('Failed to add point at midpoint:\n{}'.format(traceback.format_exc()))
        return None


######## ADD IN SETUP AND GLUE PARAM DEFINITIONS

def createNewComponent():
    # Get the active design.
    product = app.activeProduct
    design = adsk.fusion.Design.cast(product)
    rootComp = design.rootComponent
    allOccs = rootComp.occurrences
    newOcc = allOccs.addNewComponent(adsk.core.Matrix3D.create())
    return newOcc.component

class MatJointCommandExecuteHandler(adsk.core.CommandEventHandler):
    def __init__(self):
        super().__init__()
    def notify(self, args):
        try:
            unitsMgr = app.activeProduct.unitsManager
            command = args.firingEvent.sender
            inputs = command.commandInputs
            matJoint = MatJoint()
            for input in inputs:
                if input.id == 'selectBody':
                    matJoint._selectedBodies = []
                    for i in range(input.selectionCount):
                        selectedBody = input.selection(i).entity
                        matJoint._selectedBodies.append(selectedBody)
                elif input.id == 'selectEdge' and input.selectionCount > 0:
                    matJoint._selectedEdge = None
                    matJoint._selectedEdge = input.selection(0).entity
                elif input.id == 'matJointName':
                    matJoint.matJointName = input.value
                elif input.id == 'shallowInsetLength':
                    matJoint.shallowInsetLength = unitsMgr.evaluateExpression(input.expression, unitsMgr.defaultLengthUnits)
                elif input.id == 'deepInsetLength':
                    matJoint.deepInsetLength = unitsMgr.evaluateExpression(input.expression, unitsMgr.defaultLengthUnits)
                elif input.id == 'boardThickness':
                    matJoint.boardThickness = unitsMgr.evaluateExpression(input.expression, unitsMgr.defaultLengthUnits)
                elif input.id == 'millDiameter':
                    matJoint.millDiameter = unitsMgr.evaluateExpression(input.expression, unitsMgr.defaultLengthUnits)
                elif input.id == 'filletRadius':
                    matJoint.filletRadius = unitsMgr.evaluateExpression(input.expression, unitsMgr.defaultLengthUnits) 
                elif input.id == 'shallowInsetDepth':
                    matJoint.shallowInsetDepth =unitsMgr.evaluateExpression(input.expression, unitsMgr.defaultLengthUnits)
                elif input.id == 'deepInsetDepth':
                    matJoint.deepInsetDepth = unitsMgr.evaluateExpression(input.expression, unitsMgr.defaultLengthUnits)
                elif input.id == 'numSplines':
                    matJoint.numSplines = input.value                  


            matJoint.buildMatJoint()
            args.isValidResult = True

        except:
            if ui:
                ui.messageBox('Failed:\n{}'.format(traceback.format_exc()))

class MatJointCommandDestroyHandler(adsk.core.CommandEventHandler):
    def __init__(self):
        super().__init__()
    def notify(self, args):
        try:
            # when the command is done, terminate the script
            # this will release all globals which will remove all event handlers
            adsk.terminate()
        except:
            if ui:
                ui.messageBox('Failed:\n{}'.format(traceback.format_exc()))

class MatJointCommandCreatedHandler(adsk.core.CommandCreatedEventHandler):    
    def __init__(self):
        super().__init__()        
    def notify(self, args):
        try:
            cmd = args.command
            cmd.isRepeatable = False
            onExecute = MatJointCommandExecuteHandler()
            cmd.execute.add(onExecute)
            onExecutePreview = MatJointCommandExecuteHandler()
            cmd.executePreview.add(onExecutePreview)
            onDestroy = MatJointCommandDestroyHandler()
            cmd.destroy.add(onDestroy)
            # keep the handler referenced beyond this function
            handlers.append(onExecute)
            handlers.append(onExecutePreview)
            handlers.append(onDestroy)
            
            unitsMgr = app.activeProduct.unitsManager

            #define the inputs
            inputs = cmd.commandInputs
            inputs.addStringValueInput('matJointName', 'MatJoint Name', defaultMatJointName)

            bodySelectionInput = inputs.addSelectionInput('selectBody', 'Select Two Bodies', 'Select Two Bodies')
            bodySelectionInput.addSelectionFilter('SolidBodies')
            bodySelectionInput.setSelectionLimits(2, 2)

            edgeSelectionInput = inputs.addSelectionInput('selectEdge', 'Select an Edge', 'Select a single edge')
            edgeSelectionInput.addSelectionFilter('LinearEdges')  # Filter to select only linear edges
            edgeSelectionInput.setSelectionLimits(1, 1)  # Limit to a single selection

            initShallowInsetLength = adsk.core.ValueInput.createByReal(defaultShallowInsetLength)
            inputs.addValueInput('shallowInsetLength', 'Shallow Inset Length',unitsMgr.defaultLengthUnits, initShallowInsetLength)

            initDeepInsetLength = adsk.core.ValueInput.createByReal(defaultDeepInsetLength)
            inputs.addValueInput('deepInsetLength', 'Deep Inset Length',unitsMgr.defaultLengthUnits, initDeepInsetLength)

            initBoardThickness = adsk.core.ValueInput.createByReal(defaultBoardThickness)
            inputs.addValueInput('boardThickness', 'Board Thickness',unitsMgr.defaultLengthUnits, initBoardThickness)

            initMillDiameter = adsk.core.ValueInput.createByReal(defaultMillDiameter)
            inputs.addValueInput('millDiameter', 'mill Diameter',unitsMgr.defaultLengthUnits, initMillDiameter)

            initFilletRadius = adsk.core.ValueInput.createByReal(defaultFilletRadius)
            inputs.addValueInput('filletRadius', 'fillet Radius',unitsMgr.defaultLengthUnits, initFilletRadius)

            initShallowInsetDepth = adsk.core.ValueInput.createByReal(defaultShallowInsetDepth)
            inputs.addValueInput('shallowInsetDepth', 'shallow Inset Depth',unitsMgr.defaultLengthUnits, initShallowInsetDepth)

            initDeepInsetDepth = adsk.core.ValueInput.createByReal(defaultDeepInsetDepth)
            inputs.addValueInput('deepInsetDepth', 'Deep Inset Depth',unitsMgr.defaultLengthUnits, initDeepInsetDepth)

            inputs.addIntegerSpinnerCommandInput('numSplines', 'Number of Splines',1 ,7,1, 1)


        except:
            if ui:
                ui.messageBox('Failed:\n{}'.format(traceback.format_exc()))


def run(context):
    try:
        product = app.activeProduct
        design = adsk.fusion.Design.cast(product)
        if not design:
            ui.messageBox('A Fusion design must be active when running this script.')
            return
        commandDefinitions = ui.commandDefinitions
        #check the command exists or not
        cmdDef = commandDefinitions.itemById('MatJoint')
        if not cmdDef:
            cmdDef = commandDefinitions.addButtonDefinition('MatJoint',
                    'Create MatJoint',
                    'Create a matJoint.',
                    './resources') # relative resource file path is specified

        onCommandCreated = MatJointCommandCreatedHandler()
        cmdDef.commandCreated.add(onCommandCreated)
        # keep the handler referenced beyond this function
        handlers.append(onCommandCreated)
        inputs = adsk.core.NamedValues.create()
        cmdDef.execute(inputs)

        # prevent this module from being terminate when the script returns, because we are waiting for event handlers to fire
        adsk.autoTerminate(False)
    except:
        if ui:
            ui.messageBox('Failed:\n{}'.format(traceback.format_exc()))


###### NUTS BOLTS CONSTRUCTION
class MatJoint:
    def __init__(self):
        self._matJointName = defaultMatJointName
        self._shallowInsetLength = defaultShallowInsetLength
        self._deepInsetLength=defaultDeepInsetLength
        self._boardThickness=defaultBoardThickness
        self._millDiameter=defaultMillDiameter
        self._filletRadius=defaultFilletRadius
        self._shallowInsetDepth=defaultShallowInsetDepth
        self._deepInsetDepth=defaultDeepInsetDepth
        self._numSplines=defaultNumSplines


    #properties
    @Anonymous
    def matJointName(self):
        return self._matJointName
    @matJointName.setter
    def matJointName(self, value):
        self._matJointName = value

    @Anonymous
    def shallowInsetLength(self):
        return self._shallowInsetLength
    @shallowInsetLength.setter
    def shallowInsetLength(self, value):
        self._shallowInsetLength = value

    @Anonymous
    def deepInsetLength(self):
        return self._deepInsetLength
    @deepInsetLength.setter
    def deepInsetLength(self, value):
        self._deepInsetLength = value
    
    @Anonymous
    def boardThickness(self):
        return self._boardThickness
    @boardThickness.setter
    def boardThickness(self, value):
        self._boardThickness = value

    @Anonymous
    def millDiameter(self):
        return self._millDiameter
    @millDiameter.setter
    def millDiameter(self, value):
        self._millDiameter = value

    @Anonymous
    def filletRadius(self):
        return self._filletRadius
    @filletRadius.setter
    def filletRadius(self, value):
        self._filletRadius = value

    @Anonymous
    def shallowInsetDepth(self):
        return self._shallowInsetDepth
    @shallowInsetDepth.setter
    def shallowInsetDepth(self, value):
        self._shallowInsetDepth = value
    
    @Anonymous
    def deepInsetDepth(self):
        return self._deepInsetDepth
    @deepInsetDepth.setter
    def deepInsetDepth(self, value):
        self._deepInsetDepth = value

    @Anonymous
    def numSplines(self):
        return self._numSplines
    @numSplines.setter
    def numSplines(self, value):
        self._numSplines = value
        
    




    def buildMatJoint(self):
        def create_perpendicular_sketch_line(sketch, sketchLine, face):
            
            # Calculate the midpoint of the sketchLine
            midX = (sketchLine.startSketchPoint.geometry.x + sketchLine.endSketchPoint.geometry.x) / 2
            midY = (sketchLine.startSketchPoint.geometry.y + sketchLine.endSketchPoint.geometry.y) / 2
            midPoint = adsk.core.Point3D.create(midX, midY, 0)

            # Calculate the direction vector of the sketchLine
            lineVec = adsk.core.Vector3D.create(sketchLine.endSketchPoint.geometry.x - sketchLine.startSketchPoint.geometry.x,
                                                sketchLine.endSketchPoint.geometry.y - sketchLine.startSketchPoint.geometry.y, 0)
            lineVec.normalize()

            # Get the normal of the face
            faceEval = face.evaluator
            (returnValue, normal) = faceEval.getNormalAtPoint(midPoint)

            # Calculate the perpendicular direction within the sketch plane
            perpDir = lineVec.crossProduct(normal)
            perpDir = perpDir.crossProduct(lineVec)
            perpDir.normalize()

            # Scale the perpendicular direction to half the length of the sketchLine
            halfLength = sketchLine.length / 2
            scaledPerpDir = adsk.core.Vector3D.create(perpDir.x, perpDir.y, perpDir.z)
            scaledPerpDir.scaleBy(halfLength)

            # Create the end point for the perpendicular line
            perpLineEnd = midPoint.copy()
            perpLineEnd.translateBy(scaledPerpDir)

            # Create the perpendicular line in the sketch
            return sketch.sketchCurves.sketchLines.addByTwoPoints(midPoint, perpLineEnd)

        def create_coordinate_system(newXAxisLine, newYAxisLine):
            # Extract start and end points of the new axes lines
            newXAxisStart = newXAxisLine.startSketchPoint.geometry
            newXAxisEnd = newXAxisLine.endSketchPoint.geometry
            newYAxisStart = newYAxisLine.startSketchPoint.geometry
            newYAxisEnd = newYAxisLine.endSketchPoint.geometry

            # Determine the shared origin
            points = [newXAxisStart, newXAxisEnd, newYAxisStart, newYAxisEnd]
            sharedOrigin = None
            for i in range(len(points)):
                for j in range(i + 1, len(points)):
                    if points[i].isEqualTo(points[j]):
                        sharedOrigin = points[i]
                        break
                if sharedOrigin:
                    break

            if not sharedOrigin:
                raise ValueError("No shared origin found between the provided axes lines.")

            # Determine the direction vectors for the new axes
            if sharedOrigin.isEqualTo(newXAxisStart):
                newXAxisVector = newXAxisStart.vectorTo(newXAxisEnd)  # Adjusted this line
            else:
                newXAxisVector = newXAxisEnd.vectorTo(newXAxisStart)

            if sharedOrigin.isEqualTo(newYAxisStart):
                newYAxisVector = newYAxisStart.vectorTo(newYAxisEnd)  # Adjusted this line for consistency
            else:
                newYAxisVector = newYAxisEnd.vectorTo(newYAxisStart)

            return (sharedOrigin,newXAxisVector,newYAxisVector)


        def transform_point_to_new_coordinate_system(point, coords):

            sharedOrigin,newXAxisVector, newYAxisVector = coords

            # Normalize the vectors to create an orthonormal basis
            newXAxisVector.normalize()
            newYAxisVector.normalize()

            # Create transformation matrix
            matrix = adsk.core.Matrix3D.create()
            matrix.setWithCoordinateSystem(
                sharedOrigin,  # Shared origin of the new X and Y axes
                newXAxisVector,  # X axis
                newYAxisVector,  # Y axis
                newXAxisVector.crossProduct(newYAxisVector)  # Z axis (cross product of X and Y)
            )

            # Remove the matrix inversion if transforming from original to new coordinate system
            # matrix.invert()

            # Apply the transformation to the point
            transformedPoint = point.copy()
            transformedPoint.transformBy(matrix)


            app.log(f'FROM:{str_vertex(point)}    TO: {str_vertex(transformedPoint)}')


            return transformedPoint
        








        ######BUILD
        try:
            app.log("buildMatJoint")
            global newComp
            newComp = createNewComponent()
            if newComp is None:
                ui.messageBox('New component failed to create', 'New Component Failed')
                return
            

            #Calculate Relevant Anatomy
            relevantAnatomy = calculateRelevantAnatomyFromSelection(self._selectedEdge,self._selectedBodies)
            if relevantAnatomy is None:
                ui.messageBox('relevantAnatomy=None')
                return None 
            

            # Access fusion 360 stuff
            product = app.activeProduct
            design = adsk.fusion.Design.cast(product)
            rootComp = design.rootComponent
            constPoints = rootComp.constructionPoints


            # Find matching vertices in the relevant edges for projecting.
            overlapLineSegStart = find_matching_vertex_in_edges([relevantAnatomy.edgeA,relevantAnatomy.edgeB],relevantAnatomy.edgeOverlap[0])
            overlapLineSegEnd = find_matching_vertex_in_edges([relevantAnatomy.edgeA,relevantAnatomy.edgeB],relevantAnatomy.edgeOverlap[1])
           
            # Get the parent component of body A from the relevant anatomy.
            parentComp = relevantAnatomy.bodyA.parentComponent

                        
            #
            #
            #   Add Face A Inset Sketch
            #
            #

                        
            def createCutoutOnFace(overlapLineSegStart, overlapLineSegEnd,faceToSketch,bodyToCut):

                # Create a new sketch on Face A of the relevant anatomy.
                sketchA = parentComp.sketches.add(faceToSketch)
                
                #calc midpoint of overlap line
                splineIndexLineStart = sketchA.project(overlapLineSegStart).item(0)
                splineIndexLineEnd = sketchA.project(overlapLineSegEnd).item(0)
                splineIndexLine = sketchA.sketchCurves.sketchLines.addByTwoPoints(splineIndexLineStart, splineIndexLineEnd)
                splineIndexLineMidPoint = add_point_at_midpoint_of_line(sketchA,splineIndexLine)

                #Create a coodinate system around the midpoint of the overlap line.
                splineIndexXAxis = create_perpendicular_sketch_line(sketchA, splineIndexLine, faceToSketch)
                splineIndexYAxis = sketchA.sketchCurves.sketchLines.addByTwoPoints(splineIndexLineMidPoint, splineIndexLineEnd)
                coords = create_coordinate_system(splineIndexXAxis,splineIndexYAxis)
                
                #Create Expressions
                xDimLeftRect = f'({self.shallowInsetLength})'
                xDimRightRect= f'({self.deepInsetLength})'
            
                xRightSideOfShape = f'({self.deepInsetLength}) + ({self.shallowInsetLength})'
                xLeftCirclesCenter = f'({self.shallowInsetLength}) + (({self.millDiameter}) / 2)'
                xRightCirclesCenter = f'({xRightSideOfShape}) - (({self.millDiameter}) / 2)'
                
                millCircleRadius =f'(({self.millDiameter}) / 2)'

                yTopOfCutout = f'({self.boardThickness/2})'
                yBottomOfCutout = f'(-{self.boardThickness/2})'
                yDimRect = f'({self.boardThickness})'
                
                unitsManager = app.activeProduct.unitsManager


                #Evaluate expressions for initial positions of geometry
                yDimTopRectVal = unitsManager.evaluateExpression(yTopOfCutout) 
                yDimBottomRectVal = unitsManager.evaluateExpression(yBottomOfCutout)

                xDimLeftRectVal = unitsManager.evaluateExpression(xDimLeftRect)
                xDimRightRectVal = unitsManager.evaluateExpression(xDimRightRect)
                circleDiameterVal = self.millDiameter
                xRightSideOfShapeVal = unitsManager.evaluateExpression(xRightSideOfShape)
                xLeftCirclesCenterVal = unitsManager.evaluateExpression(xLeftCirclesCenter)
                xRightCirclesCenterVal = unitsManager.evaluateExpression(xRightCirclesCenter)
                radiusVal = unitsManager.evaluateExpression(millCircleRadius)


                #3D Points
                #rect points
                tll_pt3d = adsk.core.Point3D.create(-xDimLeftRectVal, yDimTopRectVal, 0)
                tl_pt3d = adsk.core.Point3D.create(0, yDimTopRectVal, 0)                   
                tm_pt3d = adsk.core.Point3D.create(xDimLeftRectVal, yDimTopRectVal, 0)     
                tr_pt3d = adsk.core.Point3D.create(xRightSideOfShapeVal, yDimTopRectVal, 0)
                
                bll_pt3d = adsk.core.Point3D.create(-xDimLeftRectVal, yDimBottomRectVal, 0)
                bl_pt3d = adsk.core.Point3D.create(0, yDimBottomRectVal, 0)  
                bm_pt3d = adsk.core.Point3D.create(xDimLeftRectVal, yDimBottomRectVal, 0)  
                br_pt3d = adsk.core.Point3D.create(xRightSideOfShapeVal, yDimBottomRectVal, 0)  

                #circle points
                tl_circle_pt3d = adsk.core.Point3D.create(xLeftCirclesCenterVal,yDimTopRectVal,0)
                tr_circle_pt3d = adsk.core.Point3D.create(xRightCirclesCenterVal,yDimTopRectVal,0)
                bl_circle_pt3d = adsk.core.Point3D.create(xLeftCirclesCenterVal,yDimBottomRectVal,0)
                br_circle_pt3d = adsk.core.Point3D.create(xRightCirclesCenterVal,yDimBottomRectVal,0)

                #Rotate Points to coordinate system of face
                tll_pt3d = transform_point_to_new_coordinate_system(tll_pt3d,coords)
                tl_pt3d = transform_point_to_new_coordinate_system(tl_pt3d,coords)
                tm_pt3d = transform_point_to_new_coordinate_system(tm_pt3d,coords)
                tr_pt3d = transform_point_to_new_coordinate_system(tr_pt3d,coords)

                bll_pt3d = transform_point_to_new_coordinate_system(bll_pt3d,coords)
                bl_pt3d = transform_point_to_new_coordinate_system(bl_pt3d,coords)
                bm_pt3d = transform_point_to_new_coordinate_system(bm_pt3d,coords)
                br_pt3d = transform_point_to_new_coordinate_system(br_pt3d,coords)

                tl_circle_pt3d = transform_point_to_new_coordinate_system(tl_circle_pt3d,coords)
                tr_circle_pt3d = transform_point_to_new_coordinate_system(tr_circle_pt3d,coords)
                bl_circle_pt3d = transform_point_to_new_coordinate_system(bl_circle_pt3d,coords)
                br_circle_pt3d = transform_point_to_new_coordinate_system(br_circle_pt3d,coords)
                
                ##########ADD POINTS TO SKETCH
                #circle centers
                tl_circle_pt = sketchA.sketchPoints.add(tl_circle_pt3d)
                tr_circle_pt = sketchA.sketchPoints.add(tr_circle_pt3d)
                bl_circle_pt = sketchA.sketchPoints.add(bl_circle_pt3d)
                br_circle_pt = sketchA.sketchPoints.add(br_circle_pt3d)

                #rectangle vertices
                tll_pt = sketchA.sketchPoints.add(tll_pt3d)
                tl_pt = sketchA.sketchPoints.add(tl_pt3d)
                tm_pt = sketchA.sketchPoints.add(tm_pt3d)
                tr_pt = sketchA.sketchPoints.add(tr_pt3d)

                bll_pt = sketchA.sketchPoints.add(bll_pt3d)
                bl_pt = sketchA.sketchPoints.add(bl_pt3d)
                bm_pt = sketchA.sketchPoints.add(bm_pt3d)
                br_pt = sketchA.sketchPoints.add(br_pt3d)

                text_point = adsk.core.Point3D.create(0, 0, 0)  # Midpoint between point1 and point2 for the text


                ##add shapes to sketch
                circles = sketchA.sketchCurves.sketchCircles
                tl_circle = circles.addByCenterRadius(tl_circle_pt3d, radiusVal)
                tr_circle = circles.addByCenterRadius(tr_circle_pt3d, radiusVal)
                bl_circle = circles.addByCenterRadius(bl_circle_pt3d, radiusVal)
                br_circle = circles.addByCenterRadius(br_circle_pt3d, radiusVal)

                tl_tm_line = sketchA.sketchCurves.sketchLines.addByTwoPoints(tl_pt, tm_pt)
                tr_tm_line = sketchA.sketchCurves.sketchLines.addByTwoPoints(tr_pt, tm_pt)

                bl_bm_line = sketchA.sketchCurves.sketchLines.addByTwoPoints(bl_pt, bm_pt)
                br_bm_line = sketchA.sketchCurves.sketchLines.addByTwoPoints(br_pt, bm_pt)

                tl_bl_line = sketchA.sketchCurves.sketchLines.addByTwoPoints(tl_pt, bl_pt)
                tm_bm_line = sketchA.sketchCurves.sketchLines.addByTwoPoints(tm_pt, bm_pt)
                tr_br_line = sketchA.sketchCurves.sketchLines.addByTwoPoints(tr_pt, br_pt)


                tll_tl_line = sketchA.sketchCurves.sketchLines.addByTwoPoints(tll_pt, tl_pt)
                bll_bl_line = sketchA.sketchCurves.sketchLines.addByTwoPoints(bll_pt, bl_pt)
                tll_bll_line = sketchA.sketchCurves.sketchLines.addByTwoPoints(bll_pt, tll_pt)




                #Constraints
                #
                #
                #
                tl_tm_line_perpendicular = sketchA.geometricConstraints.addPerpendicular(splineIndexLine, tl_tm_line)
                bl_bm_line_perpendicular = sketchA.geometricConstraints.addPerpendicular(splineIndexLine, bl_bm_line)
                
                top_colinear = sketchA.geometricConstraints.addCollinear(tl_tm_line, tr_tm_line)
                bottom_colinear = sketchA.geometricConstraints.addCollinear(bl_bm_line, br_bm_line)

                tll_colinear = sketchA.geometricConstraints.addCollinear(tll_tl_line, tl_tm_line)
                bll_colinear = sketchA.geometricConstraints.addCollinear(bll_bl_line, bl_bm_line)



                tl_circle_tangent = sketchA.geometricConstraints.addTangent(tm_bm_line, tl_circle)
                bl_circle_tangent = sketchA.geometricConstraints.addTangent(tm_bm_line, bl_circle)
                tr_circle_tangent = sketchA.geometricConstraints.addTangent(tr_br_line, tr_circle)
                br_circle_tangent = sketchA.geometricConstraints.addTangent(tr_br_line, br_circle)


                #Dimensions
                #
                #
                #

                #circles
                tl_circle_dim = sketchA.sketchDimensions.addDistanceDimension(
                    tl_circle_pt, 
                    tm_pt, 
                    adsk.fusion.DimensionOrientations.AlignedDimensionOrientation, 
                    tl_pt.geometry,
                    True)
                tl_circle_dim.parameter.expression = millCircleRadius

                bl_circle_dim = sketchA.sketchDimensions.addDistanceDimension(
                    bl_circle_pt, 
                    bm_pt, 
                    adsk.fusion.DimensionOrientations.AlignedDimensionOrientation, 
                    bl_pt.geometry,
                    True)
                bl_circle_dim.parameter.expression = millCircleRadius

                tr_circle_dim = sketchA.sketchDimensions.addDistanceDimension(
                    tr_circle_pt, 
                    tr_pt, 
                    adsk.fusion.DimensionOrientations.AlignedDimensionOrientation, 
                    tr_pt.geometry,
                    True)
                tr_circle_dim.parameter.expression = millCircleRadius

                br_circle_dim = sketchA.sketchDimensions.addDistanceDimension(
                    br_circle_pt, 
                    br_pt, 
                    adsk.fusion.DimensionOrientations.AlignedDimensionOrientation, 
                    br_pt.geometry,
                    True)
                br_circle_dim.parameter.expression = millCircleRadius
                



                #rects
                ySketchDim = sketchA.sketchDimensions.addDistanceDimension(
                    tl_pt, 
                    bl_pt, 
                    adsk.fusion.DimensionOrientations.AlignedDimensionOrientation, 
                    bl_pt.geometry,
                    True)
                ySketchDim.parameter.expression = yDimRect
                
    
                x_tll_Dim = sketchA.sketchDimensions.addDistanceDimension(
                    tll_pt, 
                    tl_pt, 
                    adsk.fusion.DimensionOrientations.AlignedDimensionOrientation, 
                    tl_pt.geometry,
                    True)
                x_tll_Dim.parameter.expression = xDimLeftRect

                x_bll_Dim = sketchA.sketchDimensions.addDistanceDimension(
                    bll_pt, 
                    bl_pt, 
                    adsk.fusion.DimensionOrientations.AlignedDimensionOrientation, 
                    bl_pt.geometry,
                    True)
                x_bll_Dim.parameter.expression = xDimLeftRect

                x_tl_Dim = sketchA.sketchDimensions.addDistanceDimension(
                    tl_pt, 
                    tm_pt, 
                    adsk.fusion.DimensionOrientations.AlignedDimensionOrientation, 
                    tl_pt.geometry,
                    True)
                x_tl_Dim.parameter.expression = xDimLeftRect

                x_bl_Dim = sketchA.sketchDimensions.addDistanceDimension(
                    bl_pt, 
                    bm_pt, 
                    adsk.fusion.DimensionOrientations.AlignedDimensionOrientation, 
                    bl_pt.geometry,
                    True)
                x_bl_Dim.parameter.expression = xDimLeftRect
                
                x_tr_Dim = sketchA.sketchDimensions.addDistanceDimension(
                    tr_pt, 
                    tm_pt, 
                    adsk.fusion.DimensionOrientations.AlignedDimensionOrientation, 
                    tr_pt.geometry,
                    True)
                x_tr_Dim.parameter.expression = xDimRightRect

                x_br_Dim = sketchA.sketchDimensions.addDistanceDimension(
                    br_pt, 
                    bm_pt, 
                    adsk.fusion.DimensionOrientations.AlignedDimensionOrientation, 
                    br_pt.geometry,
                    True)
                x_br_Dim.parameter.expression = xDimRightRect

                



                def find_all_combination_profiles(sketch, sketchElements, tolerance=1e-6):
                    """
                    Finds profiles in a sketch that match the bounding boxes of all possible combinations
                    of given sketch elements.

                    Parameters:
                    sketch (adsk.fusion.Sketch): The sketch containing the profiles.
                    sketchElements (list): A list of sketch elements (e.g., lines, circles).
                    tolerance (float): A tolerance value for comparing bounding boxes.

                    Returns:
                    adsk.core.ObjectCollection: An ObjectCollection of matching profiles.
                    """

                    matching_profiles = adsk.core.ObjectCollection.create()
                
                    # Generate all possible combinations of elements
                    for r in range(1, len(sketchElements) + 1):
                        for combination in combinations(sketchElements, r):
                            # Calculate the bounding box for the current combination
                            combined_bbox = calculate_combined_bounding_box(combination)
                            combined_min_x = combined_bbox.minPoint.x
                            combined_min_y = combined_bbox.minPoint.y
                            combined_max_x = combined_bbox.maxPoint.x
                            combined_max_y = combined_bbox.maxPoint.y

                            # Search for matching profiles in the sketch
                            for profile in sketch.profiles:
                                bbox = profile.boundingBox

                                if (abs(bbox.minPoint.x - combined_min_x) < tolerance and
                                    abs(bbox.minPoint.y - combined_min_y) < tolerance and
                                    abs(bbox.maxPoint.x - combined_max_x) < tolerance and
                                    abs(bbox.maxPoint.y - combined_max_y) < tolerance):
                                    if profile not in matching_profiles:
                                        matching_profiles.add(profile)

                    
                    return matching_profiles

                # Find profiles to extrude
                shallowProfileSketchComponents = [tll_pt, tl_pt, bll_pt, bl_pt]
                target_profiles = find_all_combination_profiles(sketchA, shallowProfileSketchComponents)
                app.log(f"Profiles found: {target_profiles.count}")
                
                app.log(f"sketchA parent {sketchA.parentComponent}")

                # Proceed only if there are profiles to extrude
                if len(target_profiles.asArray()) > 0:
 
                    for profile in target_profiles:
                        # Log details about each profile
                        area = profile.areaProperties().area
                        app.log(f"Profile area: {area}")

                    extrudes = rootComp.features.extrudeFeatures
                    # Create an extrude input for the ObjectCollection of profiles
                    extrudeInput = extrudes.createInput(target_profiles.item(0),adsk.fusion.FeatureOperations.CutFeatureOperation)
                    extrudeInput.participantBodies = [bodyToCut]

                    mm100 = adsk.core.ValueInput.createByString("-100 mm")
                    extent_distance = adsk.fusion.DistanceExtentDefinition.create(mm100)
                    extrudeInput.setOneSideExtent(extent_distance, adsk.fusion.ExtentDirections.NegativeExtentDirection)

                    # Add the extrude feature to the model
                    extrusion = extrudes.add(extrudeInput)
                else:
                    app.log("Matching profiles not found.")
                

                
            sketchA = createCutoutOnFace(overlapLineSegStart,overlapLineSegEnd,relevantAnatomy.adjFaceA,relevantAnatomy.bodyA)
            sketchB = createCutoutOnFace(overlapLineSegStart,overlapLineSegEnd,relevantAnatomy.adjFaceB,relevantAnatomy.bodyB)



        except Exception as e:
            app.log(f"Error in add_parametric_point_on_line: {str(e)}")
            if ui:
                ui.messageBox('Failed to compute matJoint:\n{}'.format(traceback.format_exc()))

 

0 Likes
Message 4 of 5

kandennti
Mentor
Mentor
Accepted solution

Hi @matthewaudio -San.

 

I think the cause is here.

extrudes = rootComp.features.extrudeFeatures

I think it is because the ExtrudeFeatures object you are using is different from the component with the profile (sketch).


To avoid the error, I made the following modifications.

・・・
                    # extrudes = rootComp.features.extrudeFeatures
                    xProf: adsk.fusion.Profile = target_profiles.item(0)
                    xComp: adsk.fusion.Component = xProf.parentSketch.parentComponent
                    extrudes: adsk.fusion.ExtrudeFeatures = xComp.features.extrudeFeatures
・・・

Please forgive the lax names of the variables to avoid duplication.

 

It avoids the error, but it may not give you the result you want.
Also, I couldn't figure out why the f3d file uses "in" units for the length, but the script uses "mm" units.

0 Likes
Message 5 of 5

matthewaudio
Contributor
Contributor

SUCCESS!!!!!  my oh my do I wish there was an error thrown with decent information there!!! Thank you!!!   And as for mm and in, a lot of this was written by chatgpt with the intention of going back over it later.  anyway thank you so so so much!!!!!!

0 Likes