Square to round transition

Need help, or want to share a macro? Post here!
Forum rules
Be nice to others! Respect the FreeCAD code of conduct!
edwilliams16
Veteran
Posts: 3107
Joined: Thu Sep 24, 2020 10:31 pm
Location: Hawaii
Contact:

Re: Square to round transition

Post by edwilliams16 »

Gui isn't my forte, but I managed to figure out how to get the inputs into the Macro:

Code: Select all

"""Create rectangular to circular metal transition

Name: squaretocircle.py

Author: Ed Williams (@edwilliams16)
Copyright: 2022
Licence: CC BY-NC-ND 4.0 IT
"""
import numpy as np
import Part, Mesh, MeshPart
import math
import flatmesh
from PySide import QtGui, QtCore

# UI Class definitions

class TransitionGuiClass(QtGui.QDialog):
    """"""
    def __init__(self):
        super(TransitionGuiClass, self).__init__()
        self.initUI()
    def initUI(self):
        self.inches = True
        self.result = userCancelled
        # create our window
        # define window		xLoc,yLoc,xDim,yDim
        self.setGeometry(	250, 250, 400, 350)
        self.setWindowTitle("Rectangle to circle transition")
        self.setWindowFlags(QtCore.Qt.WindowStaysOnTopHint)
        # create some Labels
        self.labelRectangle = QtGui.QLabel("        Rectangle", self)
        self.labelRectangle.setFont(QtGui.QFont("Times", 16, QtGui.QFont.Bold)) 
        self.labelRectangle.move(20, 20)
        self.labelLengthX = QtGui.QLabel("LengthX", self)
        self.labelLengthX.move(20, 55)
        self.labelLengthX.setFont('Courier') # set to a non-proportional font
        self.labelLengthY = QtGui.QLabel("LengthY", self)
        self.labelLengthY.move(20, 80)
        self.labelLengthY.setFont('Courier')
        self.labelCircle = QtGui.QLabel("        Circle", self)
        self.labelCircle.setFont(QtGui.QFont("Times", 16, QtGui.QFont.Bold)) 
        self.labelCircle.move(20, 110)
        self.labelRadius = QtGui.QLabel("Radius", self)
        self.labelRadius.move(20, 140)
        self.labelRadius.setFont('Courier')
        self.labelCenterXYZ = QtGui.QLabel("CenterXYZ", self)
        self.labelCenterXYZ.move(20, 170)
        self.labelCenterXYZ.setFont('Courier')
        self.labelRotAxis = QtGui.QLabel("Rot Axis", self)
        self.labelRotAxis.move(20, 200)
        self.labelRotAxis.setFont('Courier')
        self.labelRotAngle = QtGui.QLabel("Rot Angle", self)
        self.labelRotAngle.move(20, 230)
        self.labelRotAngle.setFont('Courier')
        self.noSegments = QtGui.QLabel("Number of Segments", self)
        self.noSegments.move(20, 260)
        self.noSegments.setFont('Courier')


        # set up lists for scale pop-up
        self.popupItems1 = ("inches","mm")
        # set up pop-up menu
        self.popup1 = QtGui.QComboBox(self)
        self.popup1.addItems(self.popupItems1)
        self.popup1.setCurrentIndex(self.popupItems1.index("inches"))
        self.popup1.activated[str].connect(self.onPopup1)
        self.popup1.move(210, 20)
        
        # numeric input fields
        self.lengthX = QtGui.QLineEdit(self)
        self.lengthX.setValidator(QtGui.QDoubleValidator())
        self.lengthX.setText("40.00")
        self.lengthX.setFixedWidth(70)
        self.lengthX.move(100, 50)
        self.lengthY = QtGui.QLineEdit(self)
        self.lengthY.setValidator(QtGui.QDoubleValidator())
        self.lengthY.setText("45.00")
        self.lengthY.setFixedWidth(70)
        self.lengthY.move(100, 75)
        self.radius = QtGui.QLineEdit(self)
        self.radius.setValidator(QtGui.QDoubleValidator())
        self.radius.setText("15.00")
        self.radius.setFixedWidth(70)
        self.radius.move(100, 135)
        self.centerX = QtGui.QLineEdit(self)
        self.centerX.setValidator(QtGui.QDoubleValidator())
        self.centerX.setText("0.0")
        self.centerX.setFixedWidth(70)
        self.centerX.move(100, 165)
        self.centerY = QtGui.QLineEdit(self)
        self.centerY.setValidator(QtGui.QDoubleValidator())
        self.centerY.setText("0.0")
        self.centerY.setFixedWidth(70)
        self.centerY.move(180, 165)
        self.centerZ = QtGui.QLineEdit(self)
        self.centerZ.setValidator(QtGui.QDoubleValidator())
        self.centerZ.setText("15.00")
        self.centerZ.setFixedWidth(70)
        self.centerZ.move(260, 165)
        self.axisX = QtGui.QLineEdit(self)
        self.axisX.setValidator(QtGui.QDoubleValidator())
        self.axisX.setText("1.0")
        self.axisX.setFixedWidth(70)
        self.axisX.move(100, 195)
        self.axisY = QtGui.QLineEdit(self)
        self.axisY.setValidator(QtGui.QDoubleValidator())
        self.axisY.setText("0.0")
        self.axisY.setFixedWidth(70)
        self.axisY.move(180, 195)
        self.axisZ = QtGui.QLineEdit(self)
        self.axisZ.setValidator(QtGui.QDoubleValidator())
        self.axisZ.setText("0.0")
        self.axisZ.setFixedWidth(70)
        self.axisZ.move(260, 195)
        self.angle = QtGui.QLineEdit(self)
        validator = QtGui.QDoubleValidator()
        #validator.setRange(-90, 90, 1)
        self.angle.setValidator(validator)
        self.angle.setText("0.0")
        self.angle.setFixedWidth(70)
        self.angle.move(100, 225)
        self.segments = QtGui.QLineEdit(self)
        self.segments.setText('8')
        self.segments.setValidator(QtGui.QIntValidator())
        self.segments.setFixedWidth(30)
        self.segments.move(180, 255)
        # cancel button
        cancelButton = QtGui.QPushButton('Cancel', self)
        cancelButton.clicked.connect(self.onCancel)
        cancelButton.setAutoDefault(True)
        cancelButton.move(150, 280)
        # OK button
        okButton = QtGui.QPushButton('OK', self)
        okButton.clicked.connect(self.onOk)
        okButton.move(260, 280)
        # now make the window visible
        self.show()
        #
    
    def onPopup1(self, selectedText):
        if selectedText == 'mm':
            self.inches = False
        else:
            self.inches = True
    def onCancel(self):
        self.result			= userCancelled
        self.close()
    def onOk(self):
        self.result			= userOK
        self.close()


# Constant definitions
userCancelled = "Cancelled"
userOK = "OK"

# code ***********************************************************************************



def printVertices(vs):
    App.Console.PrintMessage('Template Vertices\n')
    if useInches:
        App.Console.PrintMessage('Inch units\n')
    else:
        App.Console.PrintMessage('Inch units\n')
    for i, v in enumerate(vs):
        App.Console.PrintMessage(f'{i}:  ({v.x:.2f},{v.y:.2f})\n')

def flattenMesh(obj):
    #https://github.com/FreeCAD/FreeCAD/blob/0d0bd1168bb96ded8dfe5ab3ca60588dec5d2949/src/Mod/MeshPart/Gui/MeshFlatteningCommand.py
    points = np.array([[i.x, i.y, i.z] for i in obj.Mesh.Points])
    faces = np.array([list(i) for i in  obj.Mesh.Topology[1]])
    flattener = flatmesh.FaceUnwrapper(points, faces)
    flattener.findFlatNodes(5, 0.95)
    boundaries = flattener.getFlatBoundaryNodes()
    wires = []
    for edge in boundaries:
        pi = Part.makePolygon([App.Vector(*node) for node in edge])
        wires.append(Part.Wire(pi))
    return wires


doc = App.ActiveDocument
V3 = App.Vector
#get inputs from Gui
form = TransitionGuiClass()
form.exec_()
if form.result==userOK:
    useInches = form.inches
    if useInches:
        scale = 25.4
    else:
        scale = 1.
    rectX = float(form.lengthX.text()) * scale
    rectY = float(form.lengthY.text()) * scale
    height = float(form.centerZ.text()) * scale
    xoffset = float(form.centerX.text()) * scale
    yoffset = float(form.centerY.text()) * scale
    circleRotationAngle = float(form.angle.text())
    circleRotationAxis = V3(float(form.axisX.text()), float(form.axisY.text()), float(form.axisZ.text()))
    nSides = int(form.segments.text())
####

    eps =1e-6
    sV = [0.5*V3(*v) for v in [(rectX,0,0), (rectX, rectY, 0), (0, rectY, 0),(-rectX,rectY,0), 
    (-rectX, 0, 0), (-rectX, -rectY, 0), (0, -rectY, 0), (rectX,-rectY, 0), (rectX*(1-eps),0,0)]]
    # sv[3]      sV[2]      sV[1]     
    #
    # sv[4]      O          sV[0], sv[8] seam
    #
    # sv[5]     sV[6]       sV[7]
    # 4N-sided quarter circle
    thetas = [2 * math.pi * j/(4 * nSides) for j in range(4*nSides + 1)]
    thetas[-1] -= eps #seam
    #create and locate circle
    cV2 = [V3(radius * math.cos(t), radius * math.sin(t), 0) for t in thetas]
    pl = App.Placement(V3(xoffset, yoffset, height), App.Rotation(circleRotationAxis, circleRotationAngle))
    cV = [pl.multVec(v) for v in cV2]

    faceList = []
    for k in range(4):
        face = Part.Face(Part.makePolygon([sV[2*k], cV[nSides*k], sV[2*k + 1], sV[2*k]]))
        #Part.show(face)
        faceList.append(face)
        face = Part.Face(Part.makePolygon([sV[2*k + 1], cV[nSides*(k + 1)], sV[2*k + 2], sV[2*k + 1]]))
        #Part.show(face)
        faceList.append(face)

    for k in range(4):
        for j in range(nSides):
            f = Part.Face(Part.makePolygon([sV[2*k +1], cV[nSides*k +j], cV[nSides*k + j+1], sV[2*k +1]]))
            #Part.show(f)
            faceList.append(f)

    shell = Part.Shell(faceList)
    qshell = Part.show(shell, "QuarterShell")
    qshell.Visibility = False


    mesh = doc.addObject('Mesh::Feature','Mesh')
    mesh.Mesh=MeshPart.meshFromShape(Shape=shell, LinearDeflection=0.1, AngularDeflection=0.523599, Relative=False)
    mesh.Label="QuarterShell (Meshed)"


    wires = flattenMesh(mesh)
    doc.removeObject(mesh.Name)

    template = Part.show(wires[0], "Template") #only one outline here

    #change origin and orientation centering bottom edge along x-axis
    newOrigin = template.Shape.Vertexes[4].Point
    edgedir = template.Shape.Edges[3].Curve.Direction
    rot = App.Rotation(-edgedir, V3(),V3(0, 0, 1), 'XZY')
    template.Placement = App.Placement(newOrigin, rot).inverse()
    doc.recompute()

    templateVertices = [v.Point/scale for v in template.Shape.Vertexes]
    printVertices(templateVertices)
if form.result==userCancelled:
    pass 
Put it in your Macro folder and execute. It does rectangular to tilted offset circles.
jfc4120
Posts: 448
Joined: Sat Jul 02, 2022 11:16 pm

Re: Square to round transition

Post by jfc4120 »

@edwilliams16 got an error:
cV2 = [V3(radius * math.cos(t), radius * math.sin(t), 0) for t in thetas]
<class 'NameError'>: name 'radius' is not defined
I tried to assign a radius:

Code: Select all

    radius = 10
    cV2 = [V3(radius * math.cos(t), radius * math.sin(t), 0) for t in thetas]
But got unexpected results.
Last edited by jfc4120 on Fri Jul 22, 2022 1:04 am, edited 1 time in total.
edwilliams16
Veteran
Posts: 3107
Joined: Thu Sep 24, 2020 10:31 pm
Location: Hawaii
Contact:

Re: Square to round transition

Post by edwilliams16 »

Still works for me - despite the bug. There must be a radius variable around from earlier executions. I need to find out how to force a clean Macro environment... Anyhow

Code: Select all

"""Create rectangular to circular metal transitions


Name: squaretocircle.py

Author: Ed Williams (@edwilliams16)
Copyright: 2022
Licence: CC BY-NC-ND 4.0 IT
"""
import numpy as np
import Part, Mesh, MeshPart
import math
import flatmesh
from PySide import QtGui, QtCore
# UI Class definitions

class TransitionGuiClass(QtGui.QDialog):
    """"""
    def __init__(self):
        super(TransitionGuiClass, self).__init__()
        self.initUI()
    def initUI(self):
        self.inches = True
        self.result = userCancelled
        # create our window
        # define window		xLoc,yLoc,xDim,yDim
        self.setGeometry(	250, 250, 400, 350)
        self.setWindowTitle("Rectangle to circle transition")
        self.setWindowFlags(QtCore.Qt.WindowStaysOnTopHint)
        # create some Labels
        self.labelRectangle = QtGui.QLabel("        Rectangle", self)
        self.labelRectangle.setFont(QtGui.QFont("Times", 16, QtGui.QFont.Bold)) 
        self.labelRectangle.move(20, 20)
        self.labelLengthX = QtGui.QLabel("LengthX", self)
        self.labelLengthX.move(20, 55)
        self.labelLengthX.setFont('Courier') # set to a non-proportional font
        self.labelLengthY = QtGui.QLabel("LengthY", self)
        self.labelLengthY.move(20, 80)
        self.labelLengthY.setFont('Courier')
        self.labelCircle = QtGui.QLabel("        Circle", self)
        self.labelCircle.setFont(QtGui.QFont("Times", 16, QtGui.QFont.Bold)) 
        self.labelCircle.move(20, 110)
        self.labelRadius = QtGui.QLabel("Radius", self)
        self.labelRadius.move(20, 140)
        self.labelRadius.setFont('Courier')
        self.labelCenterXYZ = QtGui.QLabel("CenterXYZ", self)
        self.labelCenterXYZ.move(20, 170)
        self.labelCenterXYZ.setFont('Courier')
        self.labelRotAxis = QtGui.QLabel("Rot Axis", self)
        self.labelRotAxis.move(20, 200)
        self.labelRotAxis.setFont('Courier')
        self.labelRotAngle = QtGui.QLabel("Rot Angle", self)
        self.labelRotAngle.move(20, 230)
        self.labelRotAngle.setFont('Courier')
        self.noSegments = QtGui.QLabel("Number of Segments", self)
        self.noSegments.move(20, 260)
        self.noSegments.setFont('Courier')


        # set up lists for scale pop-up
        self.popupItems1 = ("inches","mm")
        # set up pop-up menu
        self.popup1 = QtGui.QComboBox(self)
        self.popup1.addItems(self.popupItems1)
        self.popup1.setCurrentIndex(self.popupItems1.index("inches"))
        self.popup1.activated[str].connect(self.onPopup1)
        self.popup1.move(210, 20)
        
        # numeric input fields
        self.lengthX = QtGui.QLineEdit(self)
        self.lengthX.setValidator(QtGui.QDoubleValidator())
        self.lengthX.setText("40.00")
        self.lengthX.setFixedWidth(70)
        self.lengthX.move(100, 50)
        self.lengthY = QtGui.QLineEdit(self)
        self.lengthY.setValidator(QtGui.QDoubleValidator())
        self.lengthY.setText("45.00")
        self.lengthY.setFixedWidth(70)
        self.lengthY.move(100, 75)
        self.radius = QtGui.QLineEdit(self)
        self.radius.setValidator(QtGui.QDoubleValidator())
        self.radius.setText("15.00")
        self.radius.setFixedWidth(70)
        self.radius.move(100, 135)
        self.centerX = QtGui.QLineEdit(self)
        self.centerX.setValidator(QtGui.QDoubleValidator())
        self.centerX.setText("0.0")
        self.centerX.setFixedWidth(70)
        self.centerX.move(100, 165)
        self.centerY = QtGui.QLineEdit(self)
        self.centerY.setValidator(QtGui.QDoubleValidator())
        self.centerY.setText("0.0")
        self.centerY.setFixedWidth(70)
        self.centerY.move(180, 165)
        self.centerZ = QtGui.QLineEdit(self)
        self.centerZ.setValidator(QtGui.QDoubleValidator())
        self.centerZ.setText("15.00")
        self.centerZ.setFixedWidth(70)
        self.centerZ.move(260, 165)
        self.axisX = QtGui.QLineEdit(self)
        self.axisX.setValidator(QtGui.QDoubleValidator())
        self.axisX.setText("1.0")
        self.axisX.setFixedWidth(70)
        self.axisX.move(100, 195)
        self.axisY = QtGui.QLineEdit(self)
        self.axisY.setValidator(QtGui.QDoubleValidator())
        self.axisY.setText("0.0")
        self.axisY.setFixedWidth(70)
        self.axisY.move(180, 195)
        self.axisZ = QtGui.QLineEdit(self)
        self.axisZ.setValidator(QtGui.QDoubleValidator())
        self.axisZ.setText("0.0")
        self.axisZ.setFixedWidth(70)
        self.axisZ.move(260, 195)
        self.angle = QtGui.QLineEdit(self)
        validator = QtGui.QDoubleValidator()
        #validator.setRange(-90, 90, 1)
        self.angle.setValidator(validator)
        self.angle.setText("0.0")
        self.angle.setFixedWidth(70)
        self.angle.move(100, 225)
        self.segments = QtGui.QLineEdit(self)
        self.segments.setText('8')
        self.segments.setValidator(QtGui.QIntValidator())
        self.segments.setFixedWidth(30)
        self.segments.move(180, 255)
        # cancel button
        cancelButton = QtGui.QPushButton('Cancel', self)
        cancelButton.clicked.connect(self.onCancel)
        cancelButton.setAutoDefault(True)
        cancelButton.move(150, 280)
        # OK button
        okButton = QtGui.QPushButton('OK', self)
        okButton.clicked.connect(self.onOk)
        okButton.move(260, 280)
        # now make the window visible
        self.show()
        #
    
    def onPopup1(self, selectedText):
        if selectedText == 'mm':
            self.inches = False
        else:
            self.inches = True
    def onCancel(self):
        self.result			= userCancelled
        self.close()
    def onOk(self):
        self.result			= userOK
        self.close()


# Constant definitions
userCancelled = "Cancelled"
userOK = "OK"

# code ***********************************************************************************



def printVertices(vs):
    App.Console.PrintMessage('Template Vertices\n')
    if useInches:
        App.Console.PrintMessage('Inch units\n')
    else:
        App.Console.PrintMessage('Inch units\n')
    for i, v in enumerate(vs):
        App.Console.PrintMessage(f'{i}:  ({v.x:.2f},{v.y:.2f})\n')

def flattenMesh(obj):
    #https://github.com/FreeCAD/FreeCAD/blob/0d0bd1168bb96ded8dfe5ab3ca60588dec5d2949/src/Mod/MeshPart/Gui/MeshFlatteningCommand.py
    points = np.array([[i.x, i.y, i.z] for i in obj.Mesh.Points])
    faces = np.array([list(i) for i in  obj.Mesh.Topology[1]])
    flattener = flatmesh.FaceUnwrapper(points, faces)
    flattener.findFlatNodes(5, 0.95)
    boundaries = flattener.getFlatBoundaryNodes()
    wires = []
    for edge in boundaries:
        pi = Part.makePolygon([App.Vector(*node) for node in edge])
        wires.append(Part.Wire(pi))
    return wires

def makeTemplate(form):
    if form.result==userOK:
        useInches = form.inches
        if useInches:
            scale = 25.4
        else:
            scale = 1.
        rectX = float(form.lengthX.text()) * scale
        rectY = float(form.lengthY.text()) * scale
        height = float(form.centerZ.text()) * scale
        xoffset = float(form.centerX.text()) * scale
        yoffset = float(form.centerY.text()) * scale
        radius = float(form.radius.text()) * scale
        circleRotationAngle = float(form.angle.text())
        circleRotationAxis = V3(float(form.axisX.text()), float(form.axisY.text()), float(form.axisZ.text()))
        nSides = int(form.segments.text())
    ####

        eps =1e-6
        sV = [0.5*V3(*v) for v in [(rectX,0,0), (rectX, rectY, 0), (0, rectY, 0),(-rectX,rectY,0), 
        (-rectX, 0, 0), (-rectX, -rectY, 0), (0, -rectY, 0), (rectX,-rectY, 0), (rectX*(1-eps),0,0)]]
        # sv[3]      sV[2]      sV[1]     
        #
        # sv[4]      O          sV[0], sv[8] seam
        #
        # sv[5]     sV[6]       sV[7]
        # 4N-sided quarter circle
        thetas = [2 * math.pi * j/(4 * nSides) for j in range(4*nSides + 1)]
        thetas[-1] -= eps #seam
        #create and locate circle
        cV2 = [V3(radius * math.cos(t), radius * math.sin(t), 0) for t in thetas]
        pl = App.Placement(V3(xoffset, yoffset, height), App.Rotation(circleRotationAxis, circleRotationAngle))
        cV = [pl.multVec(v) for v in cV2]

        faceList = []
        for k in range(4):
            face = Part.Face(Part.makePolygon([sV[2*k], cV[nSides*k], sV[2*k + 1], sV[2*k]]))
            #Part.show(face)
            faceList.append(face)
            face = Part.Face(Part.makePolygon([sV[2*k + 1], cV[nSides*(k + 1)], sV[2*k + 2], sV[2*k + 1]]))
            #Part.show(face)
            faceList.append(face)

        for k in range(4):
            for j in range(nSides):
                f = Part.Face(Part.makePolygon([sV[2*k +1], cV[nSides*k +j], cV[nSides*k + j+1], sV[2*k +1]]))
                #Part.show(f)
                faceList.append(f)

        shell = Part.Shell(faceList)
        qshell = Part.show(shell, "QuarterShell")
        qshell.Visibility = False


        mesh = doc.addObject('Mesh::Feature','Mesh')
        mesh.Mesh=MeshPart.meshFromShape(Shape=shell, LinearDeflection=0.1, AngularDeflection=0.523599, Relative=False)
        mesh.Label="QuarterShell (Meshed)"


        wires = flattenMesh(mesh)
        doc.removeObject(mesh.Name)

        template = Part.show(wires[0], "Template") #only one outline here

        #change origin and orientation centering bottom edge along x-axis
        newOrigin = template.Shape.Vertexes[4].Point
        edgedir = template.Shape.Edges[3].Curve.Direction
        rot = App.Rotation(-edgedir, V3(),V3(0, 0, 1), 'XZY')
        template.Placement = App.Placement(newOrigin, rot).inverse()
        doc.recompute()

        templateVertices = [v.Point/scale for v in template.Shape.Vertexes]
        printVertices(templateVertices)
    if form.result==userCancelled:
        pass 

doc = App.ActiveDocument
V3 = App.Vector
#get inputs from Gui
form = TransitionGuiClass()
form.exec_()
makeTemplate(form)

should fix it.

EDIT: made the active code into a function to try to immunize against this sort of error
jfc4120
Posts: 448
Joined: Sat Jul 02, 2022 11:16 pm

Re: Square to round transition

Post by jfc4120 »

A compare, mine and yours: One is both on top of each other.

radius 5,l=20, w=7, h=15

Edit: I'm catching on to basic variable usage, but much of the other python stuff is way over my head.
Attachments
overlayed.png
overlayed.png (9.99 KiB) Viewed 1010 times
srboth.png
srboth.png (12.37 KiB) Viewed 1012 times
Last edited by jfc4120 on Fri Jul 22, 2022 1:48 am, edited 1 time in total.
edwilliams16
Veteran
Posts: 3107
Joined: Thu Sep 24, 2020 10:31 pm
Location: Hawaii
Contact:

Re: Square to round transition

Post by edwilliams16 »

Looks good by eyeball.
jfc4120
Posts: 448
Joined: Sat Jul 02, 2022 11:16 pm

Re: Square to round transition

Post by jfc4120 »

I will do a couple of other random sizes and zoom in any problems I will get back with you.
edwilliams16
Veteran
Posts: 3107
Joined: Thu Sep 24, 2020 10:31 pm
Location: Hawaii
Contact:

Re: Square to round transition

Post by edwilliams16 »

I’m thinking we might want to tweak the circle radius to make the perimeter of its approximating polygon the same as that of the nominal circle. The larger the number of sections the smaller the tweak. You agree?
jfc4120
Posts: 448
Joined: Sat Jul 02, 2022 11:16 pm

Re: Square to round transition

Post by jfc4120 »

Hard to say because in real life if it's a huge Square to round you use more sections anyway I've seen as many as 48 used but for smaller fittings 12 to 16 in many cases is close enough.

I experimented once with the actual Arc Length versus straight and it's been years but if I remember correctly the difference was negligible.
edwilliams16
Veteran
Posts: 3107
Joined: Thu Sep 24, 2020 10:31 pm
Location: Hawaii
Contact:

Re: Square to round transition

Post by edwilliams16 »

It's a small error for many segments, but it's a one-liner to fix. For 2 segments (one crease) per quarter, it's visible:
Screen Shot 2022-07-21 at 7.16.38 PM.png
Screen Shot 2022-07-21 at 7.16.38 PM.png (32.24 KiB) Viewed 905 times
So might as well include.

Code: Select all

"""Create rectangular to circular metal transitions


Name: squaretocircle.py

Author: Ed Williams (@edwilliams16)
Copyright: 2022
Licence: CC BY-NC-ND 4.0 IT
"""
import numpy as np
import Part, Mesh, MeshPart
import math
import flatmesh
from PySide import QtGui, QtCore
# UI Class definitions

class TransitionGuiClass(QtGui.QDialog):
    """"""
    def __init__(self):
        super(TransitionGuiClass, self).__init__()
        self.initUI()
    def initUI(self):
        self.inches = True
        self.result = userCancelled
        # create our window
        # define window		xLoc,yLoc,xDim,yDim
        self.setGeometry(	250, 250, 400, 350)
        self.setWindowTitle("Rectangle to circle transition")
        self.setWindowFlags(QtCore.Qt.WindowStaysOnTopHint)
        # create some Labels
        self.labelRectangle = QtGui.QLabel("        Rectangle", self)
        self.labelRectangle.setFont(QtGui.QFont("Times", 16, QtGui.QFont.Bold)) 
        self.labelRectangle.move(20, 20)
        self.labelLengthX = QtGui.QLabel("LengthX", self)
        self.labelLengthX.move(20, 55)
        self.labelLengthX.setFont('Courier') # set to a non-proportional font
        self.labelLengthY = QtGui.QLabel("LengthY", self)
        self.labelLengthY.move(20, 80)
        self.labelLengthY.setFont('Courier')
        self.labelCircle = QtGui.QLabel("        Circle", self)
        self.labelCircle.setFont(QtGui.QFont("Times", 16, QtGui.QFont.Bold)) 
        self.labelCircle.move(20, 110)
        self.labelRadius = QtGui.QLabel("Radius", self)
        self.labelRadius.move(20, 140)
        self.labelRadius.setFont('Courier')
        self.labelCenterXYZ = QtGui.QLabel("CenterXYZ", self)
        self.labelCenterXYZ.move(20, 170)
        self.labelCenterXYZ.setFont('Courier')
        self.labelRotAxis = QtGui.QLabel("Rot Axis", self)
        self.labelRotAxis.move(20, 200)
        self.labelRotAxis.setFont('Courier')
        self.labelRotAngle = QtGui.QLabel("Rot Angle", self)
        self.labelRotAngle.move(20, 230)
        self.labelRotAngle.setFont('Courier')
        self.noSegments = QtGui.QLabel("Number of Segments", self)
        self.noSegments.move(20, 260)
        self.noSegments.setFont('Courier')


        # set up lists for scale pop-up
        self.popupItems1 = ("inches","mm")
        # set up pop-up menu
        self.popup1 = QtGui.QComboBox(self)
        self.popup1.addItems(self.popupItems1)
        self.popup1.setCurrentIndex(self.popupItems1.index("inches"))
        self.popup1.activated[str].connect(self.onPopup1)
        self.popup1.move(210, 20)
        
        # numeric input fields
        self.lengthX = QtGui.QLineEdit(self)
        self.lengthX.setValidator(QtGui.QDoubleValidator())
        self.lengthX.setText("40.00")
        self.lengthX.setFixedWidth(70)
        self.lengthX.move(100, 50)
        self.lengthY = QtGui.QLineEdit(self)
        self.lengthY.setValidator(QtGui.QDoubleValidator())
        self.lengthY.setText("45.00")
        self.lengthY.setFixedWidth(70)
        self.lengthY.move(100, 75)
        self.radius = QtGui.QLineEdit(self)
        self.radius.setValidator(QtGui.QDoubleValidator())
        self.radius.setText("15.00")
        self.radius.setFixedWidth(70)
        self.radius.move(100, 135)
        self.centerX = QtGui.QLineEdit(self)
        self.centerX.setValidator(QtGui.QDoubleValidator())
        self.centerX.setText("0.0")
        self.centerX.setFixedWidth(70)
        self.centerX.move(100, 165)
        self.centerY = QtGui.QLineEdit(self)
        self.centerY.setValidator(QtGui.QDoubleValidator())
        self.centerY.setText("0.0")
        self.centerY.setFixedWidth(70)
        self.centerY.move(180, 165)
        self.centerZ = QtGui.QLineEdit(self)
        self.centerZ.setValidator(QtGui.QDoubleValidator())
        self.centerZ.setText("15.00")
        self.centerZ.setFixedWidth(70)
        self.centerZ.move(260, 165)
        self.axisX = QtGui.QLineEdit(self)
        self.axisX.setValidator(QtGui.QDoubleValidator())
        self.axisX.setText("1.0")
        self.axisX.setFixedWidth(70)
        self.axisX.move(100, 195)
        self.axisY = QtGui.QLineEdit(self)
        self.axisY.setValidator(QtGui.QDoubleValidator())
        self.axisY.setText("0.0")
        self.axisY.setFixedWidth(70)
        self.axisY.move(180, 195)
        self.axisZ = QtGui.QLineEdit(self)
        self.axisZ.setValidator(QtGui.QDoubleValidator())
        self.axisZ.setText("0.0")
        self.axisZ.setFixedWidth(70)
        self.axisZ.move(260, 195)
        self.angle = QtGui.QLineEdit(self)
        validator = QtGui.QDoubleValidator()
        #validator.setRange(-90, 90, 1)
        self.angle.setValidator(validator)
        self.angle.setText("0.0")
        self.angle.setFixedWidth(70)
        self.angle.move(100, 225)
        self.segments = QtGui.QLineEdit(self)
        self.segments.setText('8')
        self.segments.setValidator(QtGui.QIntValidator())
        self.segments.setFixedWidth(30)
        self.segments.move(180, 255)
        # cancel button
        cancelButton = QtGui.QPushButton('Cancel', self)
        cancelButton.clicked.connect(self.onCancel)
        cancelButton.setAutoDefault(True)
        cancelButton.move(150, 280)
        # OK button
        okButton = QtGui.QPushButton('OK', self)
        okButton.clicked.connect(self.onOk)
        okButton.move(260, 280)
        # now make the window visible
        self.show()
        #
    
    def onPopup1(self, selectedText):
        if selectedText == 'mm':
            self.inches = False
        else:
            self.inches = True
    def onCancel(self):
        self.result			= userCancelled
        self.close()
    def onOk(self):
        self.result			= userOK
        self.close()


# Constant definitions
userCancelled = "Cancelled"
userOK = "OK"

# code ***********************************************************************************



def printVertices(vs):
    App.Console.PrintMessage('Template Vertices\n')
    if useInches:
        App.Console.PrintMessage('Inch units\n')
    else:
        App.Console.PrintMessage('Inch units\n')
    for i, v in enumerate(vs):
        App.Console.PrintMessage(f'{i}:  ({v.x:.2f},{v.y:.2f})\n')

def flattenMesh(obj):
    #https://github.com/FreeCAD/FreeCAD/blob/0d0bd1168bb96ded8dfe5ab3ca60588dec5d2949/src/Mod/MeshPart/Gui/MeshFlatteningCommand.py
    points = np.array([[i.x, i.y, i.z] for i in obj.Mesh.Points])
    faces = np.array([list(i) for i in  obj.Mesh.Topology[1]])
    flattener = flatmesh.FaceUnwrapper(points, faces)
    flattener.findFlatNodes(5, 0.95)
    boundaries = flattener.getFlatBoundaryNodes()
    wires = []
    for edge in boundaries:
        pi = Part.makePolygon([App.Vector(*node) for node in edge])
        wires.append(Part.Wire(pi))
    return wires

def makeTemplate(form):
    if form.result==userOK:
        useInches = form.inches
        if useInches:
            scale = 25.4
        else:
            scale = 1.
        rectX = float(form.lengthX.text()) * scale
        rectY = float(form.lengthY.text()) * scale
        height = float(form.centerZ.text()) * scale
        xoffset = float(form.centerX.text()) * scale
        yoffset = float(form.centerY.text()) * scale
        radius = float(form.radius.text()) * scale
        circleRotationAngle = float(form.angle.text())
        circleRotationAxis = V3(float(form.axisX.text()), float(form.axisY.text()), float(form.axisZ.text()))
        nSides = int(form.segments.text())
        radius *= (math.pi/(4 * nSides * math.sin(math.pi/(4 * nSides)))) #correct perimeter discretization
    ####

        eps =1e-6
        sV = [0.5*V3(*v) for v in [(rectX,0,0), (rectX, rectY, 0), (0, rectY, 0),(-rectX,rectY,0), 
        (-rectX, 0, 0), (-rectX, -rectY, 0), (0, -rectY, 0), (rectX,-rectY, 0), (rectX*(1-eps),0,0)]]
        # sv[3]      sV[2]      sV[1]     
        #
        # sv[4]      O          sV[0], sv[8] seam
        #
        # sv[5]     sV[6]       sV[7]
        # 4N-sided quarter circle
        thetas = [2 * math.pi * j/(4 * nSides) for j in range(4*nSides + 1)]
        thetas[-1] -= eps #seam
        #create and locate circle
        cV2 = [V3(radius * math.cos(t), radius * math.sin(t), 0) for t in thetas]
        pl = App.Placement(V3(xoffset, yoffset, height), App.Rotation(circleRotationAxis, circleRotationAngle))
        cV = [pl.multVec(v) for v in cV2]

        faceList = []
        for k in range(4):
            face = Part.Face(Part.makePolygon([sV[2*k], cV[nSides*k], sV[2*k + 1], sV[2*k]]))
            #Part.show(face)
            faceList.append(face)
            face = Part.Face(Part.makePolygon([sV[2*k + 1], cV[nSides*(k + 1)], sV[2*k + 2], sV[2*k + 1]]))
            #Part.show(face)
            faceList.append(face)

        for k in range(4):
            for j in range(nSides):
                f = Part.Face(Part.makePolygon([sV[2*k +1], cV[nSides*k +j], cV[nSides*k + j+1], sV[2*k +1]]))
                #Part.show(f)
                faceList.append(f)

        shell = Part.Shell(faceList)
        qshell = Part.show(shell, "QuarterShell")
        qshell.Visibility = False


        mesh = doc.addObject('Mesh::Feature','Mesh')
        mesh.Mesh=MeshPart.meshFromShape(Shape=shell, LinearDeflection=0.1, AngularDeflection=0.523599, Relative=False)
        mesh.Label="QuarterShell (Meshed)"


        wires = flattenMesh(mesh)
        doc.removeObject(mesh.Name)

        template = Part.show(wires[0], "Template") #only one outline here

        #change origin and orientation centering bottom edge along x-axis
        newOrigin = template.Shape.Vertexes[4].Point
        edgedir = template.Shape.Edges[3].Curve.Direction
        rot = App.Rotation(-edgedir, V3(),V3(0, 0, 1), 'XZY')
        template.Placement = App.Placement(newOrigin, rot).inverse()
        doc.recompute()

        templateVertices = [v.Point/scale for v in template.Shape.Vertexes]
        printVertices(templateVertices)
    if form.result==userCancelled:
        pass 

doc = App.ActiveDocument
V3 = App.Vector
#get inputs from Gui
form = TransitionGuiClass()
form.exec_()
makeTemplate(form)

jfc4120
Posts: 448
Joined: Sat Jul 02, 2022 11:16 pm

Re: Square to round transition

Post by jfc4120 »

@edwilliams16 Some compares, and got one error:
File "C:/Users/Owner/AppData/Roaming/FreeCAD/Macro/rect_round_trueearc.FCMacro", line 163, in printVertices
if useInches:
<class 'NameError'>: name 'useInches' is not defined
Images attached.
Attachments
c1.png
c1.png (11.45 KiB) Viewed 829 times
overlayed.png
overlayed.png (27.61 KiB) Viewed 829 times
c2.png
c2.png (7.06 KiB) Viewed 829 times
c3.png
c3.png (9.43 KiB) Viewed 829 times
Post Reply