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:
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.