Draw 90 degree Part.ArcOfEllipse from python?

Need help, or want to share a macro? Post here!
Forum rules
Be nice to others! Respect the FreeCAD code of conduct!
Post Reply
Axeia
Posts: 19
Joined: Sun Oct 02, 2022 9:42 pm
Location: United Kingdom
Contact:

Draw 90 degree Part.ArcOfEllipse from python?

Post by Axeia »

I'm working on my macro and I'm trying to draw rounded corners based off an ellipse inside a sketch.
I have simplified the code a little to the following to have an easy to reproduce example:

Code: Select all

                cornerRect: QtCore.QRectF = getattr(kbData, kbCorner.corner.value + 'Rect')
                startAngle: float = getattr(kbData, kbCorner.corner.value + 'StartAngle')

                arcOfEllipse = Part.ArcOfEllipse(
                    Part.Ellipse(
                        cornerRect.center().toV(),
                        cornerRect.width() / 2,
                        cornerRect.height() / 2
                    ),
                    math.radians(startAngle),
                    math.radians(startAngle + 90)
                )
                cornerLineId = self.sketch.addGeometry(arcOfEllipse)
                self.sketch.exposeInternalGeometry(cornerLineId)
                cornerLineIds.append(cornerLineId)
Technically it should be exactly aligned with the 'arms' of a quarter of the externalGeometry lines (the beige lines in the image), but it isn't.
It should also be flush with the default Y-axis in FreeCAD's sketch (the green line on the left in the image).
Image

My best guess is that math.radians() isn't as precise as FreeCADs internal radians and a slight deviation occurs but perhaps I'm making the wrong assumption and coming to the wrong conclusion. Basically what I want to do is draw a corner (a quarter Part.ArcOfEllipse) and my source is a QRectF along with the start of the angle in degrees.
It would be great if I could just use degrees because then it would be nice round numbers free of any funny rounding business but I don't think there's a way to draw a quarter ellipse in a sketch using degrees?
edwilliams16
Veteran
Posts: 3108
Joined: Thu Sep 24, 2020 10:31 pm
Location: Hawaii
Contact:

Re: Draw 90 degree Part.ArcOfEllipse from python?

Post by edwilliams16 »

An ellipse is parameterized by (a*cos(t), b*sin(t)). If you mean by a quarter-ellipse that it subtends 90 degrees at the center, that will only be the case for a range of pi/2 in t if the arc starts at a multiple of pi/2 (ie on the major or minor axes.) The angle is atan2(b*sin(t), a * cos(t)) - that is only the same as t for a circle.
jfc4120
Posts: 448
Joined: Sat Jul 02, 2022 11:16 pm

Re: Draw 90 degree Part.ArcOfEllipse from python?

Post by jfc4120 »

@Axeia Also see viewtopic.php?f=22&p=648805

Just work out your correct ellipse.MajorRadius and ellipse.MinorRadius and placement.
Axeia
Posts: 19
Joined: Sun Oct 02, 2022 9:42 pm
Location: United Kingdom
Contact:

Re: Draw 90 degree Part.ArcOfEllipse from python?

Post by Axeia »

Thank you to both of you, I did end up doing basically what jfc4120 suggested. I got confused for a while due to Ellipse wanting to always get the biggest size (the majorRadius) first and that threw me off. But this is what I ended up doing:

Code: Select all

        for corner in SvgKeyboardQ.Corner.Corners():
            rect: QtCore.QRectF = kbData.getCornerRect(corner)
            if rect.width() >= rect.height():
                ellipse = Part.Ellipse(
                    rect.center().toV(), rect.width()/2, rect.height()/2
                )
            else:
                aRect = rect.transposed()
                ellipse = Part.Ellipse(
                    rect.center().toV(), aRect.width()/2, aRect.height()/2
                )
                ellipse.AngleXU = math.radians(90)
            self.sketch.addGeometry(Part.ArcOfEllipse(
                ellipse, kbData.getAngleAsRad(corner, -90), kbData.getAngleAsRad(corner)
            ))
And my testcase of a bunch of oddly shaped corners is working.
Image
Basically if the corner representation is 'wide' things are easy and I can pass everything along as is. If the corner representation is 'tall' things get a bit more complicated and I transpose the rect (flip the height and width around making a wide shape again), then use the ellipse.AngleXU to rotate it into a tall shape - using the original centerpoint to line everything up.
Now the fun of adding all the constraints begins.
jfc4120
Posts: 448
Joined: Sat Jul 02, 2022 11:16 pm

Re: Draw 90 degree Part.ArcOfEllipse from python?

Post by jfc4120 »

@Axeia if it wasn't for @edwilliams16 and some of the other veterans of the forums help, I would have never learned some of the more complex python. Of course I have a long way to go, so much of their code is over my head still.

This forum has a great bunch of people. :D
User avatar
onekk
Veteran
Posts: 6146
Joined: Sat Jan 17, 2015 7:48 am
Contact:

Re: Draw 90 degree Part.ArcOfEllipse from python?

Post by onekk »

Axeia wrote: Sun May 14, 2023 10:31 pm ...
Now the fun of adding all the constraints begins.
You should add two constraint for each corner, center should have vertical and horizontal constraints with ellipse points, this way they are centered. I have used this way for circles, but with ellipse should be the same.

The fun is not incur in an overconstrained sketch, I have to delete some of them to make it work, Sadly I have not found a rule, I'm too busy now to put together a MWE and ask here where is the problem but as soon I can I will make a proper example code and post here.

Kinf 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/
Axeia
Posts: 19
Joined: Sun Oct 02, 2022 9:42 pm
Location: United Kingdom
Contact:

Re: Draw 90 degree Part.ArcOfEllipse from python?

Post by Axeia »

onekk wrote: Wed May 17, 2023 4:42 am
Axeia wrote: Sun May 14, 2023 10:31 pm ...
Now the fun of adding all the constraints begins.
You should add two constraint for each corner, center should have vertical and horizontal constraints with ellipse points, this way they are centered. I have used this way for circles, but with ellipse should be the same.

The fun is not incur in an overconstrained sketch, I have to delete some of them to make it work, Sadly I have not found a rule, I'm too busy now to put together a MWE and ask here where is the problem but as soon I can I will make a proper example code and post here.

Kinf Regards

Carlo D.
We might have run into similar problems then, I had to resort to deleting (or rather preventing adding) some constraints as well for my last corner piece. I haven't done much testing yet but this seems to work for now:

I got these convenience classes to hopefully keep the rest of the code more readable:
(The rounding is because sometimes I ended up with the value '-3.55271E-15' instead of 0. Rounding to 10 decimals is plenty precise for me and gets around it so I can run comparisons)

Code: Select all

# FreeCAD.Vector is immutable this class encapsulates it to add functionality.
# As precision to the extreme isn't needed and arc calculations introduce very minor
# imprecisions rx() and ry() and comparisons done by this class do some rounding
# to get around these imprecisions.
class V:
    def __init__(self, vector: FreeCAD.Vector):
        self.vector = vector

    def rx(self):
        return round(self.vector.x, 10)

    def ry(self):
        return round(self.vector.y, 10)

    def isLeftOf(self, otherVector: 'V') -> bool:
        return self.rx() < otherVector.rx()

    def isAbove(self, otherVector: 'V') -> bool:
        return self.ry() < otherVector.ry()

    def isHorizontalTo(self, otherVector: 'V') -> bool:
        return self.ry() == otherVector.ry()

    def isVerticalTo(self, otherVector: 'V') -> bool:
        return self.rx() == otherVector.rx()


class VP(V):
    def __init__(self, vector: FreeCAD.Vector, id: int = 0):
        super().__init__(vector)
        self.id = id

class L():
    def __init__(self, line: Part.Line, id):
        self.line = line
        self.start = VP(line.StartPoint, 1) 
        self.end = VP(line.EndPoint, 2)
        self.id = id

    def isVertical(self) -> bool:
        return self.start.isVerticalTo(self.end)

    def isHorizontal(self) -> bool:
        return self.start.isHorizontalTo(self.end)
    
    def length(self):
        if self.isHorizontal():
            return abs(self.start.rx() - self.end.rx())
        else:
            return abs(self.start.ry() - self.end.ry())
        

    def addDistanceXConstraint(self, constraints):
        if self.start.isLeftOf(self.end):
            constraints.append(Sketcher.Constraint(
                'DistanceX', self.id, self.start.id, self.id, self.end.id, self.length()
            ))
        elif self.end.isLeftOf(self.start):
            constraints.append(Sketcher.Constraint(
                'DistanceX', self.id, self.end.id, self.id, self.start.id, self.length()
            ))


    def addDistanceYConstraint(self, constraints):
        if self.start.isAbove(self.end):
            constraints.append(Sketcher.Constraint(
                'DistanceY', self.id, self.start.id, self.id, self.end.id, self.length()
            ))
        elif self.end.isAbove(self.start):
            constraints.append(Sketcher.Constraint(
                'DistanceY', self.id, self.end.id, self.id, self.start.id, self.length()
            ))


    def getConstraints(self):
        constraints = []
        if self.isHorizontal():
            constraints.append(Sketcher.Constraint('Horizontal', self.id))
            self.addDistanceXConstraint(constraints)
        else:
            constraints.append(Sketcher.Constraint('Vertical', self.id))
            self.addDistanceYConstraint(constraints)

        return constraints
And then the actual code:

Code: Select all

    def __sketchKeyboardCase(self):
        def toV(self) -> FreeCAD.Vector():
            return FreeCAD.Vector(self.x(), self.y())
        QtCore.QPointF.toV = toV

        def toLineSegment(self) -> Part.LineSegment:
            return Part.LineSegment(self.p1().toV(), self.p2().toV())
        QtCore.QLineF.toLineSegment = toLineSegment

        kbData = self.getKbIntermediaryData()
        borderIdAndLines: List[IdAndLineSegment] = []
        for border in kbData.getBorders():
            borderLine = border.toLineSegment()
            id = self.sketch.addGeometry(borderLine)
            borderIdAndLines.append(IdAndLineSegment(id, borderLine))
        self.keyboardLeftLineId, self.keyboardTopLineId = borderIdAndLines[0], borderIdAndLines[1]

        cornerLineIds = []
        for i, (corner, border) in enumerate(zip(SvgKeyboardQ.Corner.Corners(), borderIdAndLines)):
            beforeLineId = border.id
            nextI = i+1 if i+1 < len(borderIdAndLines) else 0
            afterLineId = borderIdAndLines[nextI].id

            rect: QtCore.QRectF = kbData.getCornerRect(corner)
            if rect.width() >= rect.height():
                ellipse = Part.Ellipse(
                    rect.center().toV(), rect.width()/2, rect.height()/2
                )
            else:
                aRect = rect.transposed()
                ellipse = Part.Ellipse(
                    rect.center().toV(), aRect.width()/2, aRect.height()/2
                )
                ellipse.AngleXU = math.radians(90)
            cornerLineId = self.sketch.addGeometry(Part.ArcOfEllipse(
                ellipse, kbData.getAngleAsRad(corner, -90), kbData.getAngleAsRad(corner)
            ))
            cornerLineIds.append(cornerLineId)
            self.sketch.exposeInternalGeometry(cornerLineId)

            self.sketch.addConstraint(Sketcher.Constraint(
                'Coincident', beforeLineId, 2, cornerLineId, 1)
            )
            self.sketch.addConstraint(Sketcher.Constraint(
                'Coincident', afterLineId, 1, cornerLineId, 2)
            )
            self.addLineConstraints(beforeLineId)
            
            # Avoid overconstraining by skipping constraints on the last corner
            skipConstraintX = i < len(SvgKeyboardQ.Corner.Corners())-1
            self.addArcConstraints(cornerLineId, skipConstraintX)

        for cornerLineId in cornerLineIds:
            cornerInternalGeometryVline = cornerLineId+1
            self.sketch.addConstraint(
                Sketcher.Constraint('Vertical', cornerInternalGeometryVline)
            )
        
    def addLineConstraints(self, lineId: int):
        line: Part.LineSegment = self.sketch.Geometry[lineId]
        l = L(line, lineId)
        self.sketch.addConstraint(l.getConstraints())

    def addArcConstraints(self, geoId: int, skipConstraintX: bool):
        aoe: Part.ArcOfEllipse = self.sketch.Geometry[geoId]

        startOfArc = VP(aoe.StartPoint, 1)
        endOfArc = VP(aoe.EndPoint, 2)
        center = VP(aoe.Location, 3)

        constraints = []
        for arcPoint in [endOfArc, startOfArc]:
            if arcPoint.isHorizontalTo(center):
                constraints.append(Sketcher.Constraint('Horizontal', geoId, arcPoint.id, geoId, center.id))    
                if arcPoint.isLeftOf(center) and skipConstraintX:
                    constraints.append(Sketcher.Constraint(
                        'DistanceX', geoId, arcPoint.id, geoId, center.id, center.rx()-arcPoint.rx()
                    ))
                elif center.isLeftOf(arcPoint) and skipConstraintX:
                    constraints.append(Sketcher.Constraint(
                        'DistanceX', geoId, center.id, geoId, arcPoint.id, arcPoint.rx()-center.rx()
                    ))

            if arcPoint.isVerticalTo(center):
                constraints.append(Sketcher.Constraint('Vertical', geoId, arcPoint.id, geoId, center.id))
                if arcPoint.isAbove(center):
                    constraints.append(Sketcher.Constraint(
                        'DistanceY', geoId, arcPoint.id, geoId, center.id, center.ry()-arcPoint.ry()
                    ))
                elif center.isAbove(arcPoint) and skipConstraintX:
                    constraints.append(Sketcher.Constraint(
                        'DistanceY', geoId, center.id, geoId, arcPoint.id, arcPoint.ry()-center.ry()
                    ))
        self.sketch.addConstraint(constraints)
That gets me a sketch with 2 remaining degrees of freedom which are for moving the entire thing about as one whole unit. Took me a while to figure it all out, I have been going back and forth between manually adding steps then reproducing those in code and sometimes swapping the order of them around as they'd move things about.

Resolving the last 2 remaining DoF are easy enough as I can show you here:
Image

I might leave them unresolved for my own purposes though. This sketch is one of quite a few I'm trying to generate through a macro - the final 2 constraints on this one I might just leave up to the user.
Post Reply