Beispiel #1
0
    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)
Beispiel #2
0
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)