def pointerPressEvent(self, event): """ Start freehand drawing. Init pipe and new graphics item. """ self.initFilterPipe(self._scenePositionFromEvent(event)) startPosition = self._scenePositionFromEvent(event) # Create contiguous PointerTrack in a new single QGraphicPathItem self.path = SegmentString(startingPoint=startPosition) self.scene.addItem(self.path) # Display pointerTrack self.pathTailGhost.showAt(startPosition)
class FreehandTool(object): def __init__(self, scene, view): self.turnGenerator = None # Flag, indicates pipe is generating # Ghost ungenerated tail of PointerPath with LinePathElement self.pathTailGhost = PointerTrackGhost(scene) self.path = None # None until start using tool # GUI self.scene = scene self.view = view def initFilterPipe(self, startPosition): """ Initialize pipe of filters. They feed to each other in same order of creation. """ self.turnGenerator = self.TurnGenerator(startPosition) # call to generator function returns a generator self.turnGenerator.send(None) # Execute preamble of generator and pause at first yield self.lineGenerator = self.LineGenerator(startPosition) self.lineGenerator.send(None) self.curveGenerator = self.CurveGenerator(nullLine(startPosition)) self.curveGenerator.send(None) def closeFilterPipe(self): """ Close generators. They will finally generate SOME of final objects (i.e. turn, PathLine) to current PointerPosition. Assume we already received a pointerMoveEvent at same coords of pointerReleaseEvent. """ if self.turnGenerator is not None: # Ignore race condition: pointerRelease without prior pointerPress self.turnGenerator.close() self.turnGenerator = None # Flag pipe is closed self.lineGenerator.close() self.curveGenerator.close() def _scenePositionFromEvent(self, event): """ Return scene coords mapped from window coords, as a QPointF. """ result = self.view.mapToScene(event.x(), event.y()) # print result return result def pointerMoveEvent(self, event): """ Feed pointerMoveEvent into a pipe. """ try: # Generate if pointer button down if self.turnGenerator is not None: newPosition = self._scenePositionFromEvent(event) self.turnGenerator.send(newPosition) # Feed pipe self.pathTailGhost.updateEnd(newPosition) except StopIteration: """ While user is moving pointer, we don't expect pipe to stop. If programming error stops pipe, quit app so we can see error trace. """ sys.exit() def pointerPressEvent(self, event): """ Start freehand drawing. Init pipe and new graphics item. """ self.initFilterPipe(self._scenePositionFromEvent(event)) startPosition = self._scenePositionFromEvent(event) # Create contiguous PointerTrack in a new single QGraphicPathItem self.path = SegmentString(startingPoint=startPosition) self.scene.addItem(self.path) # Display pointerTrack self.pathTailGhost.showAt(startPosition) def pointerReleaseEvent(self, event): """ Stop freehand drawing. """ self.closeFilterPipe() """ CurveGenerator only finally draws to midpoint of current PathLine. Add final element to path, a LinePathElement from midpoint to current PointerPosition. Note path already ends at the midpoint, don't need to "return" it from close() (and close() CANNOT return a value.) If last generated MidToEnd, we might not need this, but that might leave end of PointerTrack one pixel off. """ self.path.appendSegments( [LineSegment(self.path.getEndPoint(), self._scenePositionFromEvent(event))], segmentCuspness=[False] ) def keyPressEvent(self, event): """ For testing, simulate a GUI that moves ControlPoints. """ # TODO: we don't need this on every keyPress, just the first controlPointSet = self.path.getControlPointSet() # delta an arbitrary control point if event.modifiers() & Qt.ControlModifier: alternateMode = True else: alternateMode = False # 8 start anchor of second segment # 6 is Direction CP of second seg # 7 is the end anchor of second segment self.path.moveRelated( controlPoint=controlPointSet[8], deltaCoordinate=QPointF(5, 5), alternateMode=alternateMode ) # Result should be visible """ Generator filters """ def TurnGenerator(self, startPosition): """ Takes PointerPosition on explicit call to send(). Generates turn positions between lines that lie on a axis (vertical or horizontal). Qt doesn't have event.time . Fabricate it here. X11 has event.time. """ position = None # if events are: send(None), close(), need this defined previousPosition = startPosition positionClock = QTime.currentTime() # note restart returns elapsed positionClock.restart() # I also tried countPositionsSinceTurn to solve lag for cusp-like # print "init turn" try: while True: position = (yield) positionElapsedTime = positionClock.restart() turn = self.detectTurn(previousPosition, position) if turn is not None: self.lineGenerator.send((turn, positionElapsedTime)) previousPosition = position # Roll forward else: # path is still on an axis: wait pass finally: # assert position is defined if previousPosition != position: """ Have position not sent. Fabricate a turn (equal to position) and send() """ self.lineGenerator.send((position, 0)) print "Closing turn generator" def LineGenerator(self, startPosition): """ Takes pointer turn on explicit call to send(). Consumes turns until pixels of PointerPath cannot be approximated by (impinged upon by) one vector. Generates vectors on integer plane (grid), not necessarily axial, roughly speaking: diagonals. Note structure of this filter differs from others: - uses three turns (input objects): start, previous, and current. - on startup, previousTurn and startTurn are same - rolls forward previousTurn every iter, instead of on send(). """ startTurn = startPosition previousTurn = startPosition constraints = Constraints() # directions = Directions() # turnClock = QTime.currentTime() # note restart returns elapsed # turnClock.restart() try: while True: turn, positionElapsedTime = (yield) # turnElapsedTime = turnClock.restart() # print "Turn elapsed", turnElapsedTime # line = self.smallestLineFromPath(previousTurn, turn) # TEST line = self.lineFromPath(startTurn, previousTurn, turn, constraints) # ,directions) if line is not None: # if turn not satisfied by vector self.curveGenerator.send((line, False)) # self.labelLine(str(positionElapsedTime), turn) startTurn = previousTurn # !!! current turn is part of next line elif positionElapsedTime > MAX_POINTER_ELAPSED_FOR_SMOOTH: # User turned slowly, send a forced PathLine which subsequently makes cusp-like graphic # Effectively, eliminate generation lag by generating a LinePathElement. forcedLine = self.forceLineFromPath(startTurn, previousTurn, turn, constraints) self.curveGenerator.send((forcedLine, True)) # self.labelLine("F" + str(positionElapsedTime), turn) startTurn = previousTurn # !!! current turn is part of next PathLine # else current path (all turns) still satisfied by a PathLine: wait previousTurn = turn # Roll forward !!! Every turn, not just on send() except Exception: # !!! GeneratorExit is a BaseException, not an Exception # Unexpected programming errors, which are obscured unless caught print "Exception in LineGenerator" traceback.print_exc() finally: if previousTurn != startTurn: """ Have turn not sent. Fabricate a PathLine and send() it now. """ self.curveGenerator.send((QLineF(startTurn, previousTurn), 0)) print "closing line generator" def CurveGenerator(self, startLine): """ Takes lines, generates tuples of segments (lines or splines). Returns spline or cusp (two straight lines) defined between midpoints of previous two lines. On startup, previous PathLine is nullLine (!!! not None), but this still works. """ previousLine = startLine # null PathLine try: while True: line, isLineForced = (yield) if isLineForced: """ User's pointer speed indicates wants a cusp-like fit, regardless of angle between lines.""" segments, pathEndPoint, cuspness = self.segmentsFromLineMidToEnd(previousLine, line) previousLine = nullLine(pathEndPoint) # !!! next element from midpoint of nullLine else: """ Fit to path, possibly a cusp. """ segments, pathEndPoint, cuspness = self.segmentsFromLineMidToMid(previousLine, line) # segments = nullcurveFromLines(previousLine, line) # TEST previousLine = line # Roll forward # Add results to PointerTrack. self.path.appendSegments(segments, segmentCuspness=cuspness) # add segment to existing path self.pathTailGhost.updateStart(pathEndPoint) # Update ghost to start at end of PointerTrack except Exception: # !!! GeneratorExit is a BaseException, not an Exception # Unexpected programming errors, which are obscured unless caught print "Exception in CurveGenerator" traceback.print_exc() finally: """ Last drawn element stopped at midpoint of PathLine. Caller must draw one last element from there to current PointerPosition. Here we don't know PointerPosition, and caller doesn't *know* PathLine midpoint, but path stops at last PathLine midpoint. IOW midpoint is *known* by caller as end of PointerTrack. GeneratorExit exception is still in effect after finally, but caller does not see it, and Python does NOT allow it to return a value. """ print "closing curve generator" """ Turn detecting filter. """ def detectTurn(self, position1, position2): """ Return position2 if it turns, i.e. if not on horiz or vert axis with position1, else return None. """ if position1.x() != position2.x() and position1.y() != position2.y(): # print "Turn", position2 return position2 else: # print "Not turn", position2 return None """ Line fitting filter. """ def smallestLineFromPath(self, turn1, turn2): """ For TESTING: just emit a vector regardless of fit. """ return QLineF(turn1, turn2) def lineFromPath(self, startTurn, previousTurn, currentTurn, constraints, directions=None): """ Fit a vector to an integer path. If no one vector fits path (a pivot): return vector and start new vector. Otherwise return None. Generally speaking, this is a "line simplification" algorithm (e.g. Lang or Douglas-Puecker). Given an input path (a sequence of small lines between pointer turns.) Output a longer line that approximates path. More generally, input line sequence are vectors on a real plane, here they are vectors on a integer plane. More generally, there is an epsilon parameter that defines goodness of fit. Here, epsilon is half width of a pixel (one half.) A vector approximates a path (sequence of small lines between pointer turns) until either: - path has four directions - OR constraints are violated. Any turn can violate constraints, but more precisely, constraint is violated between turns. A series of turns need not violate a constraint. Only check constraints at each turn, then when constraints ARE violated by a turn, calculate exactly which PointerPosition (between turns) violated constraints. """ """ I found that for PointerTracks, this happens so rarely it is useless. Only useful for traced bitmap images? directions.update(previousTurn, currentTurn) if len(directions) > 3: # a path with four directions can't be approximated with one vector # end point is starting pixel of segment ??? print "Four directions" self.resetLineFittingFilter() # Note end is previousTurn, not current Turn return QLineF(startTurn, previousTurn) else: """ # Vector from startTurn, via many turns, to currentTurn vectorViaAllTurns = currentTurn - startTurn if constraints.isViolatedBy(vector=vectorViaAllTurns): # print "Constraint violation", constraints, "vector", vectorViaAllTurns result = self.interpolateConstraintViolating( startTurn=startTurn, lastSatisfyingTurn=previousTurn, firstNonsatisfingTurn=currentTurn ) # reset constraints.__init__() # directions.reset() return result else: constraints.update(vectorViaAllTurns) return None # Defer, until subsequent corner def interpolateConstraintViolating(self, startTurn, lastSatisfyingTurn, firstNonsatisfingTurn): """ Interpolate precise violating pixel position Return a PathLine. This version simply returns PathLine to lastSatisfyingTurn (a null interpolation.) potrace does more, a non-null interpolation. """ return QLineF(startTurn, lastSatisfyingTurn) def forceLineFromPath(self, startTurn, previousTurn, currentTurn, constraints, directions=None): """ Force a PathLine to currentTurn, regardless of constraints. Note this is a PathLine, not a LinePathElement. """ constraints.__init__() # print "Force PathLine", startTurn, currentTurn return QLineF(startTurn, currentTurn) """ Curve fitting filter. Fit a spline to two vectors. """ def segmentsFromLineMidToMid(self, line1, line2): """ Return a sequence of segments that fit midpoints of two lines. Also return new path end point. Two cases, depend on angle between lines: - acute angle: cusp: returns two LineSegments. - obtuse angle: not cusp: return one CurveSegment that smoothly fits bend. """ # aliases for three points defined by two abutting PathLines point1 = line1.p1() point2 = line1.p2() point3 = line2.p2() # midpoints of PathLines midpoint1 = self.interval(1 / 2.0, point2, point1) # needed if creating QGraphicPathItem directly midpoint2 = self.interval(1 / 2.0, point3, point2) denom = self.ddenom(point1, point3) if denom != 0.0: dd = abs(self.areaOfParallelogram(point1, point2, point3) / denom) if dd > 1: alpha = (1 - 1.0 / dd) / 0.75 else: alpha = 0 else: alpha = 4 / 3.0 if alpha > ALPHAMAX: return self.segmentsForCusp(cuspPoint=point2, endPoint=midpoint2) else: alpha = self.clampAlpha(alpha) """ Since first control point for this spline is on same PathLine as second control point for previous spline, said control points are colinear and joint between consecutive splines is smooth. """ # print "mid to mid curve" return ( [ CurveSegment( startPoint=midpoint1, controlPoint1=self.interval(0.5 + 0.5 * alpha, point1, point2), controlPoint2=self.interval(0.5 + 0.5 * alpha, point3, point2), endPoint=midpoint2, ) ], midpoint2, [False], ) # Not a cusp def segmentsFromLineMidToEnd(self, line1, line2): """ Return sequence (two or three) of segments that fit midpoint of first PathLine to end of second PathLine. At least the last segment is a cusp. Cases for results: - [line, line, line], cuspness = [True, False, True] - [curve, line], cuspness = [False, True] """ midToMidsegments, endOfMidToMid, cuspness = self.segmentsFromLineMidToMid(line1, line2) finalEndPoint = line2.p2() print "Mid to end" midToEnd = LineSegment(endOfMidToMid, finalEndPoint) return midToMidsegments + [midToEnd], finalEndPoint, cuspness + [True] """ Auxiliary functions for segmentsFromLineMidToMid() etc """ def segmentsForCusp(self, cuspPoint, endPoint): """ Return list of segments for sharp cusp. Return two straight LinePathElements and endPoint. from midpoints of two generating lines (not passed end of path, and endPoint) to point where generating lines meet (cuspPoint). Note we already generated segment to first midpoint, and will subsequently generate segment from second midpoint. """ print "cusp <<<" return ( [LineSegment(self.path.getEndPoint(), cuspPoint), LineSegment(cuspPoint, endPoint)], endPoint, [True, False], ) # First segment is cusp def interval(self, fraction, point1, point2): """ Return point fractionally along line from point1 to point2 I.E. fractional sect (eg bisect) between vectors. """ return QPointF( point1.x() + fraction * (point2.x() - point1.x()), point1.y() + fraction * (point2.y() - point1.y()) ) """ ddenom/areaOfParallelogram have property that the square of radius 1 centered at p1 intersects line p0p2 iff |areaOfParallelogram(p0,p1,p2)| <= ddenom(p0,p2) """ def ddenom(self, p0, p1): """ ??? """ r = self.cardinalDirectionLeft90(p0, p1) return r.y() * (p1.x() - p0.x()) - r.x() * (p1.y() - p0.y()) def areaOfParallelogram(self, p0, p1, p2): """ Vector cross product of vector point1 - point0 and point2 - point0 I.E. area of the parallelogram defined by these three points. Scalar. """ return (p1.x() - p0.x()) * (p2.y() - p0.y()) - (p2.x() - p0.x()) * (p1.y() - p0.y()) def cardinalDirectionLeft90(self, p0, p1): """ Return unit (length doesn't matter?) vector 90 degrees counterclockwise from p1-p0, but clamped to one of eight cardinal direction (n, nw, w, etc) """ return QPointF(-sign(p1.y() - p0.y()), sign(p1.x() - p0.x())) def clampAlpha(self, alpha): if alpha < 0.55: return 0.55 elif alpha > 1: return 1 else: return alpha """ def getCurveQGraphicsItem(self, startPt, controlPt1, controlPt2, endPt): ''' In Qt > v4.0 there is no QGraphicsCurveItem, only QGraphicsPathItem with a curve in its path. ''' path = QPainterPath() path.moveTo(startPt) path.cubicTo(controlPt1, controlPt2, endPt) return QGraphicsPathItem(path) """ def labelLine(self, string, position): """ For testing """ text = self.scene.addSimpleText(string) text.setPos(position)