How to Enlarge an Object by a set value

Need help, or want to share a macro? Post here!
Forum rules
Be nice to others! Respect the FreeCAD code of conduct!
Post Reply
NilDesperandum
Posts: 27
Joined: Tue Sep 29, 2020 12:26 am

How to Enlarge an Object by a set value

Post by NilDesperandum »

Does anyone have experience with enlarging an object by a set value on each side? In this case I am specifically referring to a bias, where each side would be expanded or contracted by a given value (not a multiplication). I created a function (see below) that works for shapes using 90 degree angles and are on-axis (not just rectangles, but has to have 90 degree angles), but it does not always work for objects that appear curved, objects without 90 degree angles such as a triangle or octagon, or objects that are angled off-axis (such as the object is rotated 30 degrees with respect to the x-axis). My current method also creates a new shape, which is porbably easier than editing the existing shape.

Here's the function (also attached as file for easier viewing):

Code: Select all

def biasFeatures(feature, bias):                                                #Uses edge comparisons to bias a passed face object
    '''
    This function is responsible for biasing (expanding or shrinking) a given object by a given amount. It does this by comparing edge distances and making
    the distances either larger or smaller through a series of comparisons.
    Inputs: the feature (face) being biased and the value to bias by. A negative bias value enlarges an object and a positive bias shrinks the object
    Returns: a new face object that has been biased
    '''
    print("Bias begin")                                                         #For Debugging
    print("bias = %f" % bias)                                                   #For debugging
    '''
    The below section gathers boundary conditions needed to correctly bias the objects. The points from the bounds variable are used to keep
    an object from being biased off of the given substrate (if applicable) and the loop finds the outside edge points of the feature
    '''
    bounds = get_2D_outer_bounds()                                              #Grabs the outermost bounds of the device
    xLow = bounds[0][0]; xHigh = bounds[2][0]; yLow = bounds[0][1]; yHigh = bounds[2][1] #Used later to not bias objects off the substrate
    Z = feature.Vertexes[0].Point[2]                                            #Gets the height value for the new face
    lowX = 0; highX = 0; lowY = 0; highY = 0                                    #Used to find the boundaries of the feature being biased
    for points in feature.Vertexes:                                             #Runs through every vertex and grabs the boundary points
        point = points.Point
        if point[0] < lowX:
            lowX = point[0]
        if point[0] > highX:
            highX = point[0]
        if point[1] < lowY:
            lowY = point[1]
        if point[1] > highY:
            highY = point[1]
    xVals = []; yVals = []                                                      #Used to keep track of the new vertex points
    #print(len(feature.Edges))                                                  #For debugging
    '''
    The below section is divided into two categories: simple rectangular objects with 4 edges, and more complex objects with an even number of edges.
    The biasing works by taking an edge and checking it's distance to an edge on the opposite side of the object (by checking vector direction of the edges).
    It then shifts the edge by the biasing amount and checks to see if it shifted in the right direction. If not then it'll take the values being shifted the
    other direction. For more complex objects the edge being compared to will always be an outside edge since an inside edge might actually need to be shifted
    towards another inside edge for the object to get larger. The new vertices are recorded onto 2 arrays, 1 for the x values and 1 for the y values. These
    are updated independently since right now all biases are done on axis (all inputs expected to exist on axis). So, one edge will move only in the x direction
    and will create updated x values for the vertices and then the next edge will move in the y direction and create updated y values for the vertices.
    edge1 is always the edge that will undergo the bias and edge2 is always the edge that it is being compared to.
    **Will have to be updated for any objects with an odd number of edges (including "circular" objects). Will have to be updated for off-axis objects.
    '''
    if len(feature.Edges) == 4:                                                 #If rectangular with only 4 edges
        #print("TRUE")                                                          #For debugging
        for edge1 in feature.Edges:                                             #Runs through every edge of the passed feature
            edge1First = edge1.firstVertex().Point; edge1Last = edge1.lastVertex().Point    #Gets the two vertices of the edge
            if (edge1First[0] == edge1Last[0] == lowX == xLow) or (edge1First[0] == edge1Last[0] == highX == xHigh):    #If the edges are at the boundary of the device
                xVals.append(edge1First[0])                                     #Don't add any bias since edge at boundary of device
                xVals.append(edge1Last[0])                                      #Don't add any bias since edge at boundary of device
                if len(yVals) == 0:                                             #If no y values have been updated then add a value (used to balance vertices)
                    yVals.append(0)                                             #5 vertices required to close an object, this gives the fifth vertex
                continue                                                        #Continue to next edge
            if (edge1First[1] == edge1Last[1] == lowY == yLow) or (edge1First[1] == edge1Last[1] == highY == yHigh):#If the edges are at the boundary of the device
                yVals.append(edge1First[1])                                     #Don't add any bias since edge at boundary of device
                yVals.append(edge1Last[1])                                      #Don't add any bias since edge at boundary of device
                if len(xVals) == 0:                                             #If no x values have been updated then add a value (used to balance vertices)
                    xVals.append(0)                                             #5 vertices required to close an object, this gives the fifth vertex
                continue                                                        #Continue to next edge
            #print(edge1.firstVertex().Point[0], edge1.lastVertex().Point[0])   #For debugging
            edge1Dir = np.array(edge1.tangentAt(edge1.FirstParameter))          #Finds the vector direction of the edge and converts to a numpy array
            for edge2 in feature.Edges:                                         #Runs through all the edges of the feature to find a comparison edge
                edge2Dir = np.array(edge2.tangentAt(edge2.FirstParameter))      #Finds the direction of the second edge
                #Below line takes the dot product of the two edge directions. If they are exactly opposite directions then the angle will be 180 degrees
                if math.degrees(math.acos(np.dot(edge1Dir, edge2Dir) / (np.linalg.norm(edge1Dir)* np.linalg.norm(edge2Dir)))) == 180:
                    #Part.show(edge1)                                           #For debugging
                    #Part.show(edge2)                                           #For debugging
                    #print(edge1Dir)                                            #For debugging
                    #print(edge2Dir)                                            #For debugging
                    origDist = edge1.distToShape(edge2)[0]#np.linalg.norm(edge1-edge2) #Finds the original distance between the two FreeCAD edges
                    tempPts = [FreeCAD.Vector(edge1First[0]+bias, edge1First[1], Z), \
                                FreeCAD.Vector(edge1Last[0]+bias,edge1Last[1], Z)]  #This is used to create a new set of vertices with a bias
                    newEdge = Part.makePolygon(tempPts)                         #Creates a new FreeCAD edge that can be used for comparison
                    #Part.show(newEdge)                                         #For debugging
                    newDist = newEdge.distToShape(edge2)[0]#np.linalg.norm(newEdge-edge2) #Finds the distance between the new and old FreeCAD edges
                    '''
                    If the passed bias is negative then an expansion occurs, so the difference between the new and old edges should be positive
                    (shown as "-bias") and the reverse for a reduction in object size. If the expansion in the x direction is incorret then
                    an attempted expnasion in the y direction occurs.
                    '''
                    if (newDist-origDist) == -bias:                             #If the difference between the edges distances is equal to the bias value
                        #This is right way to move edge
                        xVals.append(edge1First[0]+bias)                        #Create first new x vertex with the bias added
                        xVals.append(edge1Last[0]+bias)                         #Create second new x vertex with the bias added
                        if len(yVals) == 0:
                            yVals.append(0)                                     #5 vertices required to close an object, this gives the fifth vertex
                    elif (newDist-origDist) == bias:
                        #Reverse the bias value
                        xVals.append(edge1First[0]-bias)                        #Create first new x vertex with the bias subtracted
                        xVals.append(edge1Last[0]-bias)                         #Create second new x vertex with the bias subtracted
                        if len(yVals) == 0:
                            yVals.append(0)                                     #5 vertices required to close an object, this gives the fifth vertex
                    else:
                        #Add bias to the y values
                        tempPts = [FreeCAD.Vector(edge1First[0], edge1First[1]+bias, Z), \
                                    FreeCAD.Vector(edge1Last[0],edge1Last[1]+bias, Z)]
                        newEdge = Part.makePolygon(tempPts)                     #Creates a new FreeCAD edge that can be used for comparison
                        #Part.show(newEdge)                                     #For debugging
                        newDist = newEdge.distToShape(edge2)[0]#np.linalg.norm(newEdge-edge2) #Finds the distance between the new and old FreeCAD edges
                        if (newDist-origDist) == -bias:
                            #This is right way to move edge
                            yVals.append(edge1First[1]+bias)
                            yVals.append(edge1Last[1]+bias)
                            if len(xVals) == 0:
                                xVals.append(0)                                 #5 vertices required to close an object, this gives the fifth vertex
                        elif (newDist-origDist) == bias:
                            #Reverse the bias value
                            yVals.append(edge1First[1]-bias)
                            yVals.append(edge1Last[1]-bias)
                            if len(xVals) == 0:
                                xVals.append(0)                                 #5 vertices required to close an object, this gives the fifth vertex
                    break
    else:                                                                       #Object has an even number of edges greater than 4 edges
        #internalEdgeNum = 0                                                    #Not currently in use - was going to be used for edge direction counts
        '''
        Much of the following section follows the same process as above. The only difference is that there is a check to see if the edge that is being
        biased is an inside or outside edge. If it is an outside edge then the same process as above happens, and if it is an inside edge then
        there is an additional check to verify that the edge it is being compared to is an outside edge. This verifies the correct movement direction
        of the edge (in accordance with the bias) and accounts for the fact that an enlargment will require inside edges to be closer versus farther
        apart.
        See above for inline comments describing each process.
        '''
        for edge1 in feature.Edges:
            edge1First = edge1.firstVertex().Point; edge1Last = edge1.lastVertex().Point
            if (edge1First[0] == edge1Last[0] == lowX == xLow) or (edge1First[0] == edge1Last[0] == highX == xHigh):
                xVals.append(edge1First[0])
                xVals.append(edge1Last[0])
                if len(yVals) == 0:
                    yVals.append(0)                                             #5 vertices required to close an object, this gives the fifth vertex
                continue
            if (edge1First[1] == edge1Last[1] == lowY == yLow) or (edge1First[1] == edge1Last[1] == highY == yHigh):
                yVals.append(edge1First[1])
                yVals.append(edge1Last[1])
                if len(xVals) == 0:
                    xVals.append(0)                                             #5 vertices required to close an object, this gives the fifth vertex
                continue
            #print(edge1.firstVertex().Point[0], edge1.lastVertex().Point[0])   #For debugging
            edge1Dir = np.array(edge1.tangentAt(edge1.FirstParameter))#edge1.tangentAt(edge1.FirstParameter)
            for edge2 in feature.Edges:
                edge2Dir = np.array(edge2.tangentAt(edge2.FirstParameter))#edge2.tangentAt(edge2.FirstParameter)
                if (math.degrees(math.acos(np.dot(edge1Dir, edge2Dir) / (np.linalg.norm(edge1Dir)* np.linalg.norm(edge2Dir)))) == 180):
                    edge2First = edge2.firstVertex().Point; edge2Last = edge2.lastVertex().Point
                    if (edge1First[0] == lowX or edge1First[0] == highX \
                            or edge1First[1] == lowY or edge1First[1] == highY) \
                        and (edge1Last[0] == lowX or edge1Last[0] == highX \
                            or edge1Last[1] == lowY or edge1Last[1] == highY):  #If the edge is an outside edge
                        print("Edge on outside")
                        #Part.show(edge1)
                        #Part.show(edge2)
                        #print(edge1Dir)
                        #print(edge2Dir)
                        origDist = edge1.distToShape(edge2)[0]#np.linalg.norm(edge1-edge2)
                        tempPts = [FreeCAD.Vector(edge1First[0]+bias, edge1First[1], Z), \
                                    FreeCAD.Vector(edge1Last[0]+bias, edge1Last[1], Z)]
                        newEdge = Part.makePolygon(tempPts)
                        #Part.show(newEdge)
                        newDist = newEdge.distToShape(edge2)[0]#np.linalg.norm(newEdge-edge2)
                        #print(newDist, origDist)                               #For debugging
                        if (newDist-origDist) == -bias:
                            #print("Bias added to X values with a newDist of %f and origDist of %f" % (newDist, origDist))   #For debugging
                            #this is right way to move edge
                            xVals.append(edge1First[0]+bias)
                            xVals.append(edge1Last[0]+bias)
                            if len(yVals) == 0:
                                yVals.append(0)                                 #5 vertices required to close an object, this gives the fifth vertex
                        elif (newDist-origDist) == bias:
                            #print("Bias substracted from X values with a newDist of %f and origDist of %f" % (newDist, origDist))
                            #reverse the bias value
                            xVals.append(edge1First[0]-bias)
                            xVals.append(edge1Last[0]-bias)
                            if len(yVals) == 0:
                                yVals.append(0)                                 #5 vertices required to close an object, this gives the fifth vertex
                        else:
                            #add bias to the y values
                            tempPts = [FreeCAD.Vector(edge1First[0], edge1First[1]+bias, Z), \
                                        FreeCAD.Vector(edge1Last[0],edge1Last[1]+bias, Z)]
                            newEdge = Part.makePolygon(tempPts)
                            #Part.show(newEdge)
                            newDist = newEdge.distToShape(edge2)[0]#np.linalg.norm(newEdge-edge2)
                            if (newDist-origDist) == -bias:
                                #print("Bias added to Y values with a newDist of %f and origDist of %f" % (newDist, origDist))  #For debugging
                                #this is right way to move edge
                                yVals.append(edge1First[1]+bias)
                                yVals.append(edge1Last[1]+bias)
                                if len(xVals) == 0:
                                    xVals.append(0)                             #5 vertices required to close an object, this gives the fifth vertex
                            elif (newDist-origDist) == bias:
                                #print("Bias subtracted from Y values with a newDist of %f and origDist of %f" % (newDist, origDist))   #For debugging
                                #reverse the bias value
                                yVals.append(edge1First[1]-bias)
                                yVals.append(edge1Last[1]-bias)
                                if len(xVals) == 0:
                                    xVals.append(0)                             #5 vertices required to close an object, this gives the fifth vertex
                        break
                    elif ((edge2First[0] == edge2Last[0] == lowX) or (edge2First[0] == edge2Last[0] == highX) \
                            or (edge2First[1] == edge2Last[1] == lowY) or (edge2First[1] == edge2Last[1] == highY)): #This must be an inside edge (not at boundary values)
                        print("Inside edge found")
                        #Part.show(edge1)
                        #Part.show(edge2)
                        #print(edge1Dir)
                        #print(edge2Dir)
                        origDist = edge1.distToShape(edge2)[0]#np.linalg.norm(edge1-edge2)
                        tempPts = [FreeCAD.Vector(edge1First[0]+bias, edge1First[1], Z), \
                                    FreeCAD.Vector(edge1Last[0]+bias, edge1Last[1], Z)]
                        newEdge = Part.makePolygon(tempPts)
                        #Part.show(newEdge)
                        newDist = newEdge.distToShape(edge2)[0]#np.linalg.norm(newEdge-edge2)
                        if (newDist-origDist) == -bias:
                            #print("Bias added to X values with a newDist of %f and origDist of %f" % (newDist, origDist)) #For debugging
                            #this is right way to move edge
                            xVals.append(edge1First[0]+bias)
                            xVals.append(edge1Last[0]+bias)
                            if len(yVals) == 0:
                                yVals.append(0)                                 #5 vertices required to close an object, this gives the fifth vertex
                        elif (newDist-origDist) == bias:
                            #print("Bias substracted from X values with a newDist of %f and origDist of %f" % (newDist, origDist)) #For debugging
                            #reverse the bias value
                            xVals.append(edge1First[0]-bias)
                            xVals.append(edge1Last[0]-bias)
                            if len(yVals) == 0:
                                yVals.append(0)                                 #5 vertices required to close an object, this gives the fifth vertex
                        else:
                            #add bias to the y values
                            tempPts = [FreeCAD.Vector(edge1First[0], edge1First[1]+bias, Z), \
                                        FreeCAD.Vector(edge1Last[0],edge1Last[1]+bias, Z)]
                            newEdge = Part.makePolygon(tempPts)
                            #Part.show(newEdge)
                            newDist = newEdge.distToShape(edge2)[0]#np.linalg.norm(newEdge-edge2)
                            if (newDist-origDist) == -bias:
                                #print("Bias added to Y values with a newDist of %f and origDist of %f" % (newDist, origDist)) #For debugging
                                #this is right way to move edge
                                yVals.append(edge1First[1]+bias)
                                yVals.append(edge1Last[1]+bias)
                                if len(xVals) == 0:
                                    xVals.append(0)                             #5 vertices required to close an object, this gives the fifth vertex
                            elif (newDist-origDist) == bias:
                                #print("Bias subtracted from Y values with a newDist of %f and origDist of %f" % (newDist, origDist)) #For debugging
                                #reverse the bias value
                                yVals.append(edge1First[1]-bias)
                                yVals.append(edge1Last[1]-bias)
                                if len(xVals) == 0:
                                    xVals.append(0)                             #5 vertices required to close an object, this gives the fifth vertex
                        break
    if xVals[0] == 0:                                                           #Corrects the 5th vertex so that the shape is properly closed
        xVals[0] = xVals[-1]
        yVals.append(yVals[0])
    elif yVals[0] == 0:                                                         #Corrects the 5th vertex so that the shape is properly closed
        yVals[0] = yVals[-1]
        xVals.append(xVals[0])
    #print(len(xVals),len(yVals))                                               #For debugging
    #print(xVals)
    #print(yVals)
    finalVerts = []                                                             #New array to hold a tuple of the final vertices
    for idx in range(0,len(xVals)):                                             #The length of xVals and yVals are equal, this loop runs through them to create new vertices
        if idx == 0:                                                            #If this is the first vertex, find the highest value from the ends of the arrays
            #Solve for first and last vertex
            if abs(xVals[0]) > abs(xVals[-1]):                                  #Determines the x value for the first vertex
                xFin = xVals[0]
            else:
                xFin = xVals[-1]
            if abs(yVals[0]) > abs(yVals[-1]):                                  #Determines the y value for the first vertex
                yFin = yVals[0]
            else:
                yFin = yVals[-1]
            finalVerts.append((xFin,yFin,Z))
        elif idx == len(xVals)-1:                                               #Sets the last vertex equal to the first vertex so the object is closed
            #set equal to first point
            finalVerts.append(finalVerts[0])
        else:
            finalVerts.append((xVals[idx],yVals[idx],Z))
    #print(len(finalVerts))
    #print(finalVerts)                                                          #For debugging
    pts=[]
    for i in range(0,len(finalVerts)):                                          #Used to create an array of the "Vector points" required in FreeCAD
        pts.append(FreeCAD.Vector(finalVerts[i][0],finalVerts[i][1],finalVerts[i][2])) #FreeCAD.Vector(x,y,z)
    #print(pts)
    wire=Part.makePolygon(pts)                                                  #Connects all points given above, makes a wire polygon (just a connected line)
    #Part.show(wire)
    face=Part.Face(wire)                                                        #Creates a face from the wire above
    print("Bias end")
    return face                                                                 #Returns the new face that has been biased

There are a few function calls that return specific values; these functions can be found in this GitHub Repository.
bias_function.py
(23.48 KiB) Downloaded 16 times
OS: Windows 10 (10.0)
Word size of OS: 64-bit
Word size of FreeCAD: 64-bit
Version: 0.19.22960 (Git)
Build type: Release
Branch: master
Hash: c5a4b01d2e4218bcc0eb6650337650a6c65ef0e4
Python version: 3.8.6
Qt version: 5.12.5
Coin version: 4.0.0
OCC version: 7.4.0
Locale: English/United States (en_US)
User avatar
onekk
Veteran
Posts: 6149
Joined: Sat Jan 17, 2015 7:48 am
Contact:

Re: How to Enlarge an Object by a set value

Post by onekk »

if you have an object, you could use scale() on the Shape property.
scale accept only a float so it is scaled:

Code: Select all

obj = Part.makeBox(10,15,10)
Part.show(obj, "original")

obj1 = obj.copy()

obj1.scale(2.0)

FreeCAD.activeDocument().recompute()

Part.show(obj1,"scaled")
See the code above.

Regards

Carlo D.
GitHub page: https://github.com/onekk/freecad-doc.
- In deep articles on FreeCAD.
- Learning how to model with scripting.
- Various other stuffs.

Blog: https://okkmkblog.wordpress.com/
mario52
Veteran
Posts: 4674
Joined: Wed May 16, 2012 2:13 pm

Re: How to Enlarge an Object by a set value

Post by mario52 »

hi

here other example Macro_CloneConvert Image

mario
Maybe you need a special feature, go into Macros_recipes and Code_snippets, Topological_data_scripting.
My macros on Gist.github here complete macros Wiki and forum.
User avatar
onekk
Veteran
Posts: 6149
Joined: Sat Jan 17, 2015 7:48 am
Contact:

Re: How to Enlarge an Object by a set value

Post by onekk »

Code: Select all

obj3 = Part.makeSphere(10)
m = obj.Matrix
m.scale(Vector(1.0, 1.25, 1.50))

obj4 = obj3.transformGeometry(m)

Part.show(obj4, "scaled")

print(obj4.Matrix)
more verbose, that is using the Matrix property and the transformGeometry().

note that you have to extract the matrix and then apply trasformation on the matrix.

It maybe could be useful as the macro use documentObjects, and in Scripting you may have to manipulate a topoShape without having to transform in a documentObject that maybe has to be deleted after use.

as you could note by the last print statement the geometry is returned as if it was created without scaling (the Matrix is a unity matrix)

So the scaling are retained even if you further manipulate his matrix.

Hope it helps

Carlo D.
GitHub page: https://github.com/onekk/freecad-doc.
- In deep articles on FreeCAD.
- Learning how to model with scripting.
- Various other stuffs.

Blog: https://okkmkblog.wordpress.com/
NilDesperandum
Posts: 27
Joined: Tue Sep 29, 2020 12:26 am

Re: How to Enlarge an Object by a set value

Post by NilDesperandum »

mario52 wrote: Sun Apr 11, 2021 10:46 am here other example Macro_CloneConvert Image
Thanks! I will take a look at the source code to see how it is done there.
onekk wrote: Sun Apr 11, 2021 3:58 pm more verbose, that is using the Matrix property and the transformGeometry().
Thanks for the suggestion! I can try out both the potential options, but I do have concerns that they both use multiplicative scaling which might be funky for some objects that have sections like below:
object44.PNG
object44.PNG (1.75 KiB) Viewed 383 times
The chamfer is done after the scaling, but there's a larger section and then a smaller section and it is likely that each will require a different multiplicative value to scale properly. Maybe I am off on this idea?
Post Reply