Skip to content

Instantly share code, notes, and snippets.

@adrientetar
Created October 15, 2015 17:26
Show Gist options
  • Save adrientetar/709e303c01cab61d0e0c to your computer and use it in GitHub Desktop.
Save adrientetar/709e303c01cab61d0e0c to your computer and use it in GitHub Desktop.
From d8c4fd9e783107b4acc54a3c65af7948061825a3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Adrien=20T=C3=A9tar?= <adri-from-59@hotmail.fr>
Date: Fri, 9 Oct 2015 16:44:22 +0200
Subject: [PATCH] meta: glyphView tools refactoring (wip)
It introduces quite some bugs so I'm not pursuing this further just yet.
---
Lib/defconQt/glyphView.py | 980 ++----------------------------------
Lib/defconQt/objects/qtItems.py | 505 +++++++++++++++++++
Lib/defconQt/tools/__init__.py | 0
Lib/defconQt/tools/baseTool.py | 33 ++
Lib/defconQt/tools/knifeTool.py | 69 +++
Lib/defconQt/tools/penTool.py | 184 +++++++
Lib/defconQt/tools/rulerTool.py | 96 ++++
Lib/defconQt/tools/selectionTool.py | 56 +++
Lib/defconQt/util/bezierMath.py | 48 ++
setup.py | 1 +
10 files changed, 1045 insertions(+), 927 deletions(-)
create mode 100644 Lib/defconQt/objects/qtItems.py
create mode 100644 Lib/defconQt/tools/__init__.py
create mode 100644 Lib/defconQt/tools/baseTool.py
create mode 100644 Lib/defconQt/tools/knifeTool.py
create mode 100644 Lib/defconQt/tools/penTool.py
create mode 100644 Lib/defconQt/tools/rulerTool.py
create mode 100644 Lib/defconQt/tools/selectionTool.py
create mode 100644 Lib/defconQt/util/bezierMath.py
diff --git a/Lib/defconQt/glyphView.py b/Lib/defconQt/glyphView.py
index d2eac03..ef691ac 100644
--- a/Lib/defconQt/glyphView.py
+++ b/Lib/defconQt/glyphView.py
@@ -1,10 +1,12 @@
-from enum import Enum
from math import copysign
import pickle
from defcon import Anchor, Component
from defconQt.objects.defcon import TContour, TGlyph
+from defconQt.objects.qtItems import (HandleLineItem, OffCurvePointItem, OnCurvePointItem,
+ ResizeHandleItem, AnchorItem, ComponentItem, PixmapItem, VGuidelinesTextItem, roundPosition,
+ backgroundColor, bluesColor, fillColor, metricsColor)
from defconQt.pens.copySelectionPen import CopySelectionPen
-from fontTools.misc import bezierTools
+from defconQt.tools import selectionTool, penTool, rulerTool, knifeTool
from PyQt5.QtCore import *#QFile, QLineF, QObject, QPointF, QRectF, QSize, Qt
from PyQt5.QtGui import *#QBrush, QColor, QImage, QKeySequence, QPainter, QPainterPath, QPixmap, QPen
from PyQt5.QtWidgets import *#(QAction, QActionGroup, QApplication, QFileDialog,
@@ -144,7 +146,6 @@ class MainGfxWindow(QMainWindow):
self.setAttribute(Qt.WA_DeleteOnClose)
self.setAttribute(Qt.WA_KeyCompression)
- self.view = GlyphView(glyph, self)
menuBar = self.menuBar()
fileMenu = QMenu("&File", self)
@@ -155,27 +156,11 @@ class MainGfxWindow(QMainWindow):
glyphMenu.addAction("&Jump", self.changeGlyph, "J")
menuBar.addMenu(glyphMenu)
- toolBar = QToolBar(self)
- toolBar.setMovable(False)
- selectionToolButton = toolBar.addAction("Selection", self.view.setSceneSelection)
- selectionToolButton.setCheckable(True)
- selectionToolButton.setChecked(True)
- selectionToolButton.setIcon(QIcon("defconQt/resources/cursor.svg"))
- penToolButton = toolBar.addAction("Pen", self.view.setSceneDrawing)
- penToolButton.setCheckable(True)
- penToolButton.setIcon(QIcon("defconQt/resources/curve.svg"))
- rulerToolButton = toolBar.addAction("Ruler", self.view.setSceneRuler)
- rulerToolButton.setCheckable(True)
- rulerToolButton.setIcon(QIcon("defconQt/resources/ruler.svg"))
- knifeToolButton = toolBar.addAction("Knife", self.view.setSceneKnife)
- knifeToolButton.setCheckable(True)
- knifeToolButton.setIcon(QIcon("defconQt/resources/cut.svg"))
- toolsGroup = QActionGroup(self)
- toolsGroup.addAction(selectionToolButton)
- toolsGroup.addAction(penToolButton)
- toolsGroup.addAction(rulerToolButton)
- toolsGroup.addAction(knifeToolButton)
- self.addToolBar(toolBar)
+ self.toolBar = QToolBar(self)
+ self.toolBar.setMovable(False)
+ self.toolsGroup = QActionGroup(self)
+ self.addToolBar(self.toolBar)
+ self.view = GlyphView(glyph, self)
self.setCentralWidget(self.view)
self.setWindowTitle(glyph.name, glyph.getParent())
@@ -212,517 +197,6 @@ class MainGfxWindow(QMainWindow):
if font is not None: title = "%s – %s %s" % (title, font.info.familyName, font.info.styleName)
super(MainGfxWindow, self).setWindowTitle(title)
-def roundPosition(value):
- value = value * 10#self._scale
- value = round(value) - .5
- value = value * .1#self._inverseScale
- return value
-
-offCurvePointSize = 8#5
-onCurvePointSize = 9#6
-onCurveSmoothPointSize = 10#7
-offWidth = offHeight = roundPosition(offCurvePointSize)# * self._inverseScale)
-offHalf = offWidth / 2.0
-onWidth = onHeight = roundPosition(onCurvePointSize)# * self._inverseScale)
-onHalf = onWidth / 2.0
-smoothWidth = smoothHeight = roundPosition(onCurveSmoothPointSize)# * self._inverseScale)
-smoothHalf = smoothWidth / 2.0
-onCurvePenWidth = 1.5
-offCurvePenWidth = 1.0
-
-anchorSize = 11
-anchorWidth = anchorHeight = roundPosition(anchorSize)
-anchorHalf = anchorWidth / 2.0
-
-bezierHandleColor = QColor.fromRgbF(0, 0, 0, .2)
-startPointColor = QColor.fromRgbF(0, 0, 0, .2)
-backgroundColor = Qt.white
-offCurvePointColor = QColor.fromRgbF(1, 1, 1, 1)
-offCurvePointStrokeColor = QColor.fromRgbF(.6, .6, .6, 1)
-onCurvePointColor = offCurvePointStrokeColor
-onCurvePointStrokeColor = offCurvePointColor
-anchorColor = QColor(120, 120, 255)
-anchorSelectionColor = Qt.blue
-bluesColor = QColor.fromRgbF(.5, .7, 1, .3)
-fillColor = QColor(200, 200, 200, 120)#QColor.fromRgbF(0, 0, 0, .4)
-componentFillColor = QColor.fromRgbF(0, 0, 0, .4)#QColor.fromRgbF(.2, .2, .3, .4)
-metricsColor = QColor(70, 70, 70)
-pointSelectionColor = Qt.red
-
-class SceneTools(Enum):
- SelectionTool = 0
- DrawingTool = 1
- RulerTool = 2
- KnifeTool = 3
-
-class HandleLineItem(QGraphicsLineItem):
- def __init__(self, x1, y1, x2, y2, parent):
- super(HandleLineItem, self).__init__(x1, y1, x2, y2, parent)
- self.setPen(QPen(bezierHandleColor, 1.0))
- self.setFlag(QGraphicsItem.ItemStacksBehindParent)
-
-class OffCurvePointItem(QGraphicsEllipseItem):
- def __init__(self, x, y, parent=None):
- super(OffCurvePointItem, self).__init__(parent)
- # since we have a parent, setPos must be relative to it
- self.setPointPath()
- self.setPos(x, y) # TODO: abstract and use pointX-self.parent().pos().x()
- self.setFlag(QGraphicsItem.ItemIsMovable)
- self.setFlag(QGraphicsItem.ItemIsSelectable)
- self.setFlag(QGraphicsItem.ItemSendsGeometryChanges)
- self.setFlag(QGraphicsItem.ItemStacksBehindParent)
-
- self.setBrush(QBrush(offCurvePointColor))
- self._needsUngrab = False
-
- def itemChange(self, change, value):
- if change == QGraphicsItem.ItemPositionChange:
- if self.scene()._integerPlane:
- value.setX(round(value.x()))
- value.setY(round(value.y()))
- if QApplication.keyboardModifiers() & Qt.ShiftModifier \
- and len(self.scene().selectedItems()) == 1:
- ax = abs(value.x())
- ay = abs(value.y())
- if ay >= ax * 2:
- value.setX(0)
- elif ay > ax / 2:
- avg = (ax + ay) / 2
- value.setX(copysign(avg, value.x()))
- value.setY(copysign(avg, value.y()))
- else:
- value.setY(0)
- elif change == QGraphicsItem.ItemPositionHasChanged:
- self.parentItem()._CPMoved(value)
- # TODO: consider what to do w offCurves
- #elif change == QGraphicsItem.ItemSelectedHasChanged:
- # pass#self.parentItem()._CPSelChanged(value)
- return value
-
- def mousePressEvent(self, event):
- if not self._needsUngrab and self.x() == 0 and self.y() == 0:
- event.ignore()
- super(OffCurvePointItem, self).mousePressEvent(event)
-
- def mouseReleaseEvent(self, event):
- super(OffCurvePointItem, self).mouseReleaseEvent(event)
- if self._needsUngrab:
- self.ungrabMouse()
- self._needsUngrab = False
-
- # http://www.qtfr.org/viewtopic.php?pid=21045#p21045
- def paint(self, painter, option, widget):
- #if self.x() == 0 and self.y() == 0: return
- newOption = QStyleOptionGraphicsItem(option)
- newOption.state = QStyle.State_None
- pen = self.pen()
- if option.state & QStyle.State_Selected:
- pen.setColor(pointSelectionColor)
- else:
- pen.setColor(offCurvePointStrokeColor)
- self.setPen(pen)
- super(OffCurvePointItem, self).paint(painter, newOption, widget)
-
- def setPointPath(self, scale=None):
- if scale is None:
- scene = self.scene()
- if scene is not None:
- scale = scene.getViewScale()
- else:
- scale = 1
- if scale > 4: scale = 4
- elif scale < .4: scale = .4
- self.prepareGeometryChange()
- self.setRect(-offHalf/scale, -offHalf/scale, offWidth/scale, offHeight/scale)
- self.setPen(QPen(offCurvePointStrokeColor, offCurvePenWidth/scale))
-
-class OnCurvePointItem(QGraphicsPathItem):
- def __init__(self, x, y, isSmooth, contour, point, scale=1, parent=None):
- super(OnCurvePointItem, self).__init__(parent)
- self._contour = contour
- self._point = point
- self._isSmooth = isSmooth
- self._posBeforeMove = None
-
- self.setPointPath(scale)
- self.setPos(x, y)
- self.setFlag(QGraphicsItem.ItemIsMovable)
- self.setFlag(QGraphicsItem.ItemIsSelectable)
- self.setFlag(QGraphicsItem.ItemSendsGeometryChanges)
- self.setBrush(QBrush(onCurvePointColor))
-
- def delete(self, preserveShape=True):
- def findNextOnCurve(self, index=0):
- for _ in self._contour:
- if self._contour[index].segmentType is not None:
- break
- index = (index+1) % len(self._contour)
- return index
-
- scene = self.scene()
- glyph = scene._glyphObject
- if len(self._contour.segments) < 2:
- glyph.removeContour(self._contour)
- else:
- ptIndex = self.getPointIndex()
- if self._contour.open and ptIndex == 0:
- nextOnCurveIndex = findNextOnCurve(self, 1)
- self._contour._points = self._contour[nextOnCurveIndex:]
- self._contour[0].segmentType = "move"
- self._contour.dirty = True
- else:
- # Using preserveShape at the edge of an open contour will traceback
- if ptIndex == len(self._contour): preserveShape = False
- self._contour.removeSegment(self.getSegmentIndex(), preserveShape)
- nextOnCurveIndex = findNextOnCurve(self)
- self._contour.setStartPoint(nextOnCurveIndex)
- # This object will be removed from scene by notification mechanism
-
- def setPointPath(self, scale=None):
- path = QPainterPath()
- if scale is None:
- scene = self.scene()
- if scene is not None:
- scale = scene.getViewScale()
- else:
- scale = 1
- if scale > 4: scale = 4
- elif scale < .4: scale = .4
- if self._isSmooth:
- path.addEllipse(-smoothHalf/scale, -smoothHalf/scale, smoothWidth/scale, smoothHeight/scale)
- else:
- path.addRect(-onHalf/scale, -onHalf/scale, onWidth/scale, onHeight/scale)
- self.prepareGeometryChange()
- self.setPath(path)
- self.setPen(QPen(onCurvePointStrokeColor, onCurvePenWidth/scale))
-
- def getPointIndex(self):
- return self._contour.index(self._point)
-
- def getSegmentIndex(self):
- # closed contour cycles and so the "previous" segment goes to current point
- index = 0 if self._contour.open else -1
- for pt in self._contour:
- if pt == self._point: break
- if pt.segmentType is not None: index += 1
- return index % len(self._contour.segments)
-
- def _CPMoved(self, newValue):
- pointIndex = self.getPointIndex()
- children = self.childItems()
- # nodes are stored after lines (for stacking order)
- if children[1].isSelected():
- selected = 1
- propagate = 3
- else:
- selected = 3
- propagate = 1
- curValue = children[selected].pos()
- line = children[selected-1].line()
- children[selected-1].setLine(line.x1(), line.y1(), newValue.x(), newValue.y())
-
- if not len(children) > 4:
- elemIndex = pointIndex-2+selected
- self._contour[elemIndex].x = self.pos().x()+newValue.x()
- self._contour[elemIndex].y = self.pos().y()+newValue.y()
- if not (self._isSmooth and children[propagate].isVisible()):
- self.setShallowDirty()
- return
- if children[selected]._needsUngrab:
- targetLen = children[selected-1].line().length()*2
- else:
- targetLen = children[selected-1].line().length()+children[propagate-1].line().length()
- tmpLine = QLineF(newValue, QPointF(0, 0))
- tmpLine.setLength(targetLen)
- children[propagate].setFlag(QGraphicsItem.ItemSendsGeometryChanges, False)
- children[propagate].setPos(tmpLine.x2(), tmpLine.y2())
- children[propagate].setFlag(QGraphicsItem.ItemSendsGeometryChanges)
- children[propagate-1].setLine(line.x1(), line.y1(), tmpLine.x2(), tmpLine.y2())
- propagateInContour = pointIndex-2+propagate
- self._contour[propagateInContour].x = self.pos().x()+tmpLine.x2()
- self._contour[propagateInContour].y = self.pos().y()+tmpLine.y2()
- self.setShallowDirty()
-
- def itemChange(self, change, value):
- if change == QGraphicsItem.ItemPositionChange:
- if self.scene()._integerPlane:
- value.setX(round(value.x()))
- value.setY(round(value.y()))
- elif change == QGraphicsItem.ItemPositionHasChanged:
- # have a look at defcon FuzzyNumber as well
- pointIndex = self.getPointIndex()
- self._contour[pointIndex].x = self.pos().x()
- self._contour[pointIndex].y = self.pos().y()
-
- children = self.childItems()
- if children[1].isVisible():
- prevPos = children[1].pos()
- self._contour[pointIndex-1].x = self.pos().x()+prevPos.x()
- self._contour[pointIndex-1].y = self.pos().y()+prevPos.y()
- if children[3].isVisible():
- nextPos = children[3].pos()
- self._contour[pointIndex+1].x = self.pos().x()+nextPos.x()
- self._contour[pointIndex+1].y = self.pos().y()+nextPos.y()
- self.setShallowDirty()
- elif change == QGraphicsItem.ItemSelectedHasChanged:
- self._point.selected = value
- return value
-
- def setShallowDirty(self):
- scene = self.scene()
- scene._blocked = True
- self._contour.dirty = True
- scene._blocked = False
-
- def mouseMoveEvent(self, event):
- modifiers = event.modifiers()
- children = self.childItems()
- # Ctrl: get and move prevCP, Alt: nextCP
- if modifiers & Qt.ControlModifier and children[1].x() == 0 and children[1].y() == 0:
- i, o = 1, 3
- elif modifiers & Qt.AltModifier and children[3].x() == 0 and children[3].y() == 0:
- i, o = 3, 1
- elif not (modifiers & Qt.ControlModifier or modifiers & Qt.AltModifier):
- super(OnCurvePointItem, self).mouseMoveEvent(event)
- return
- else: # eat the event if we are not going to yield an offCP
- event.accept()
- return
- ptIndex = self.getPointIndex()
- scene = self.scene()
- scene._blocked = True
- # if we have line segment, insert offCurve points
- insertIndex = (ptIndex+(i-1)//2) % len(self._contour)
- if self._contour[insertIndex].segmentType == "line":
- nextToCP = self._contour[(ptIndex-2+i) % len(self._contour)]
- assert(nextToCP.segmentType is not None)
- self._contour[insertIndex].segmentType = "curve"
- if i == 1:
- first, second = (self._point.x, self._point.y), (nextToCP.x, nextToCP.y)
- else:
- first, second = (nextToCP.x, nextToCP.y), (self._point.x, self._point.y)
- self._contour.insertPoint(insertIndex, self._contour._pointClass(first))
- self._contour.insertPoint(insertIndex, self._contour._pointClass(second))
- children[i].setVisible(True)
- # TODO: need a list of items to make this efficient
- scene.getItemForPoint(nextToCP).childItems()[o].setVisible(True)
- # release current onCurve
- scene.sendEvent(self, QEvent(QEvent.MouseButtonRelease))
- scene.mouseGrabberItem().ungrabMouse()
- self.setSelected(False)
- self.setIsSmooth(False)
- children[i]._needsUngrab = True
- scene.sendEvent(children[i], QEvent(QEvent.MouseButtonPress))
- children[i].setSelected(True)
- children[i].grabMouse()
- scene._blocked = False
- event.accept()
-
- def mouseDoubleClickEvent(self, event):
- view = self.scene().views()[0] # XXX: meh, maybe refactor doubleClick event into the scene?
- if view._currentTool == SceneTools.RulerTool or view._currentTool == SceneTools.KnifeTool:
- return
- self.setIsSmooth(not self._isSmooth)
-
- def setIsSmooth(self, isSmooth):
- self._isSmooth = isSmooth
- self._point.smooth = self._isSmooth
- self.setShallowDirty()
- self.setPointPath()
-
- # http://www.qtfr.org/viewtopic.php?pid=21045#p21045
- def paint(self, painter, option, widget):
- newOption = QStyleOptionGraphicsItem(option)
- newOption.state = QStyle.State_None
- pen = self.pen()
- if option.state & QStyle.State_Selected:
- pen.setColor(pointSelectionColor)
- else:
- pen.setColor(onCurvePointStrokeColor)
- self.setPen(pen)
- super(OnCurvePointItem, self).paint(painter, newOption, widget)
-
-class AnchorItem(QGraphicsPathItem):
- def __init__(self, anchor, scale=1, parent=None):
- super(AnchorItem, self).__init__(parent)
- self._anchor = anchor
-
- textItem = QGraphicsSimpleTextItem(self._anchor.name, parent=self)
- font = QFont()
- font.setPointSize(9)
- textItem.setFont(font)
- textItem.setFlag(QGraphicsItem.ItemIgnoresTransformations)
- self.setPointPath(scale)
- self.setPos(self._anchor.x, self._anchor.y)
- self.setFlag(QGraphicsItem.ItemIsMovable)
- self.setFlag(QGraphicsItem.ItemIsSelectable)
- self.setFlag(QGraphicsItem.ItemSendsGeometryChanges)
- self.setBrush(QBrush(anchorColor))
- self.setPen(QPen(Qt.NoPen))
-
- def delete(self):
- glyph = self._anchor.getParent()
- glyph.removeAnchor(self._anchor)
-
- def itemChange(self, change, value):
- if change == QGraphicsItem.ItemPositionChange:
- if self.scene()._integerPlane:
- value.setX(round(value.x()))
- value.setY(round(value.y()))
- elif change == QGraphicsItem.ItemPositionHasChanged:
- x = self.pos().x()
- y = self.pos().y()
- scene = self.scene()
- scene._blocked = True
- self._anchor.x = x
- self._anchor.y = y
- scene._blocked = False
- return value
-
- def setPointPath(self, scale=None):
- path = QPainterPath()
- if scale is None:
- scene = self.scene()
- if scene is not None:
- scale = scene.getViewScale()
- else:
- scale = 1
- if scale > 4: scale = 4
- elif scale < .4: scale = .4
-
- path.moveTo(-anchorHalf/scale, 0)
- path.lineTo(0, anchorHalf/scale)
- path.lineTo(anchorHalf/scale, 0)
- path.lineTo(0, -anchorHalf/scale)
- path.closeSubpath()
-
- self.prepareGeometryChange()
- self.setPath(path)
- textItem = self.childItems()[0]
- textItem.setPos(anchorHalf/scale, textItem.boundingRect().height()/2)
-
- # http://www.qtfr.org/viewtopic.php?pid=21045#p21045
- def paint(self, painter, option, widget):
- newOption = QStyleOptionGraphicsItem(option)
- newOption.state = QStyle.State_None
- pen = self.pen()
- if option.state & QStyle.State_Selected:
- self.setBrush(anchorSelectionColor)
- else:
- self.setBrush(anchorColor)
- super(AnchorItem, self).paint(painter, newOption, widget)
-
-class ComponentItem(QGraphicsPathItem):
- def __init__(self, path, component, parent=None):
- super(ComponentItem, self).__init__(path, parent)
- self._component = component
- self.setTransform(QTransform(*component.transformation))
- self.setBrush(QBrush(componentFillColor))
- self.setFlag(QGraphicsItem.ItemIsMovable)
- self.setFlag(QGraphicsItem.ItemIsSelectable)
- self.setFlag(QGraphicsItem.ItemSendsGeometryChanges)
-
- def delete(self):
- glyph = self._component.getParent()
- glyph.removeComponent(self._component)
-
- def itemChange(self, change, value):
- if change == QGraphicsItem.ItemPositionChange:
- if self.scene()._integerPlane:
- value.setX(round(value.x()))
- value.setY(round(value.y()))
- elif change == QGraphicsItem.ItemPositionHasChanged:
- t = self._component.transformation
- x = self.pos().x()
- y = self.pos().y()
- scene = self.scene()
- scene._blocked = True
- self._component.transformation = (t[0], t[1], t[2], t[3], x, y)
- scene._blocked = False
- return value
-
-class VGuidelinesTextItem(QGraphicsSimpleTextItem):
- def __init__(self, text, font, parent=None):
- super(VGuidelinesTextItem, self).__init__(text, parent)
- self.setBrush(metricsColor)
- self.setFlag(QGraphicsItem.ItemIgnoresTransformations)
- self.setFont(font)
-
-class ResizeHandleItem(QGraphicsRectItem):
- def __init__(self, parent=None):
- super(QGraphicsRectItem, self).__init__(parent)
- self.setPointPath()
- self.setBrush(QBrush(QColor(60, 60, 60)))
- self.setPen(QPen(Qt.NoPen))
- self.setFlag(QGraphicsItem.ItemIgnoresParentOpacity)
- self.setFlag(QGraphicsItem.ItemIsMovable)
- #self.setFlag(QGraphicsItem.ItemIsSelectable)
- self.setCursor(Qt.SizeFDiagCursor)
-
- rect = self.parentItem().boundingRect()
- self.setPos(rect.width(), rect.height())
-
- def itemChange(self, change, value):
- if change == QGraphicsItem.ItemSelectedChange:
- if not value: self.setVisible(value)
- return value
-
- def mouseMoveEvent(self, event):
- self.parentItem()._pixmapGeometryChanged(event)
-
- def setPointPath(self, scale=None):
- if scale is None:
- scene = self.scene()
- if scene is not None:
- scale = scene.getViewScale()
- else:
- scale = 1
- if scale > 4: scale = 4
- self.prepareGeometryChange()
- self.setRect(-onHalf/scale, -onHalf/scale, onWidth/scale, onHeight/scale)
-
-class PixmapItem(QGraphicsPixmapItem):
- def __init__(self, x, y, pixmap, parent=None):
- super(QGraphicsPixmapItem, self).__init__(pixmap, parent)
- self.setPos(x, y)
- self.setFlag(QGraphicsItem.ItemIsMovable)
- self.setFlag(QGraphicsItem.ItemIsSelectable)
- self.setTransform(QTransform().fromScale(1, -1))
- self.setOpacity(.5)
- self.setZValue(-1)
-
- rect = self.boundingRect()
- self._rWidth = rect.width()
- self._rHeight = rect.height()
- handle = ResizeHandleItem(self)
- handle.setVisible(False)
-
- def _pixmapGeometryChanged(self, event):
- modifiers = event.modifiers()
- pos = event.scenePos()
- if modifiers & Qt.ControlModifier:
- # rotate
- refLine = QLineF(self.x(), self.y(), self.x()+self._rWidth, self.y()-self._rHeight)
- curLine = QLineF(self.x(), self.y(), pos.x(), pos.y())
- self.setRotation(refLine.angleTo(curLine))
- else:
- # scale
- dy = (pos.y() - self.y()) / self._rHeight
- if modifiers & Qt.ShiftModifier:
- # keep original aspect ratio
- dx = -dy
- else:
- dx = (pos.x() - self.x()) / self._rWidth
- self.setTransform(QTransform().fromScale(dx, dy))
- event.accept()
-
- def itemChange(self, change, value):
- if change == QGraphicsItem.ItemSelectedChange:
- children = self.childItems()
- if not children[0].isUnderMouse():
- children[0].setVisible(value)
- return value
-
class GlyphScene(QGraphicsScene):
def __init__(self, parent):
super(GlyphScene, self).__init__(parent)
@@ -893,387 +367,24 @@ class GlyphScene(QGraphicsScene):
super(GlyphScene, self).keyReleaseEvent(event)
def mousePressEvent(self, event):
- if event.button() == Qt.RightButton:
- self._rightClickPos = event.scenePos()
view = self.views()[0]
- touched = self.itemAt(event.scenePos(), view.transform())
- if view._currentTool == SceneTools.RulerTool:
- self.rulerMousePress(event)
- return
- else:
- data = self._glyphObject.serialize()
- self._dataForUndo.append(data)
- self._dataForRedo = []
- if view._currentTool == SceneTools.KnifeTool:
- self.knifeMousePress(event)
- return
- elif view._currentTool == SceneTools.SelectionTool:
- super(GlyphScene, self).mousePressEvent(event)
- return
- self._blocked = True
- forceSelect = False
- sel = self.selectedItems()
- x, y = event.scenePos().x(), event.scenePos().y()
- if self._integerPlane:
- x, y = round(x), round(y)
- # XXX: not sure why isinstance does not work here
- if len(sel) == 1:
- isLastOnCurve = type(sel[0]) is OnCurvePointItem and sel[0]._contour.open and \
- sel[0].getPointIndex() == len(sel[0]._contour)-1
- # TODO: reimplement convenience methods in OffCurvePointItem
- isLastOffCurve = type(sel[0]) is OffCurvePointItem and sel[0].parentItem()._contour.open and \
- sel[0].parentItem().getPointIndex()+1 == len(sel[0].parentItem()._contour)-1
- if len(sel) == 1 and (isLastOffCurve or isLastOnCurve):
- if isLastOnCurve:
- lastContour = sel[0]._contour
- else:
- lastContour = sel[0].parentItem()._contour
- if (touched and isinstance(touched, OnCurvePointItem)) and touched.getPointIndex() == 0 \
- and lastContour == touched._contour and len(lastContour) > 1:
- # Changing the first point from move to line/curve will cycle and so close the contour
- if isLastOffCurve:
- lastContour.addPoint((x,y))
- lastContour[0].segmentType = "curve"
- touched.childItems()[1].setVisible(True)
- else:
- lastContour[0].segmentType = "line"
- elif touched and isinstance(touched, OnCurvePointItem):
- super(GlyphScene, self).mousePressEvent(event)
- return
- else:
- if QApplication.keyboardModifiers() & Qt.ShiftModifier:
- forceSelect = True
- if isLastOnCurve:
- refx = sel[0].x()
- refy = sel[0].y()
- else:
- refx = sel[0].parentItem().x()
- refy = sel[0].parentItem().y()
- if abs(x-refx) > abs(y-refy): y = copysign(refy, y)
- else: x = copysign(refx, x)
- if isLastOffCurve:
- lastContour.addPoint((x,y))
- lastContour.addPoint((x,y), "curve")
- else:
- lastContour.addPoint((x,y), "line")
- item = OnCurvePointItem(x, y, False, lastContour, lastContour[-1], self.getViewScale())
- self.addItem(item)
- for _ in range(2):
- lineObj = HandleLineItem(0, 0, 0, 0, item)
- CPObject = OffCurvePointItem(0, 0, item)
- CPObject.setVisible(False)
- if isLastOffCurve:
- item.childItems()[1].setVisible(True)
- lastContour.dirty = True
- self._editing = True
- elif not (touched and isinstance(touched, OnCurvePointItem)):
- nextC = TContour()
- self._glyphObject.appendContour(nextC)
- nextC.addPoint((x,y), "move")
-
- item = OnCurvePointItem(x, y, False, self._glyphObject[-1], self._glyphObject[-1][-1], self.getViewScale())
- self.addItem(item)
- for _ in range(2):
- lineObj = HandleLineItem(0, 0, 0, 0, item)
- CPObject = OffCurvePointItem(0, 0, item)
- CPObject.setVisible(False)
- self._editing = True
- self._blocked = False
- super(GlyphScene, self).mousePressEvent(event)
- # Since shift clamps, we might be missing the point in mousePressEvent
- if forceSelect: item.setSelected(True)
+ CurrentTool = view._currentTool
+ CurrentTool.mousePressEvent(self, event)
def mouseMoveEvent(self, event):
- if self._editing is True:
- sel = self.selectedItems()
- if len(sel) == 1:
- if isinstance(sel[0], OnCurvePointItem) and (event.scenePos() - sel[0].pos()).manhattanLength() >= 2:
- mouseGrabberItem = self.mouseGrabberItem()
- # If we drawn an onCurve w Shift and we're not touching the item, we wont have
- # a mouse grabber (anyways), return early here.
- if mouseGrabberItem is None:
- event.accept()
- return
- self._blocked = True
- if len(sel[0]._contour) < 2:
- # release current onCurve
- self.sendEvent(sel[0], QEvent(QEvent.MouseButtonRelease))
- mouseGrabberItem.ungrabMouse()
- sel[0].setSelected(False)
- # append an offCurve point and start moving it
- sel[0]._contour.addPoint((event.scenePos().x(), event.scenePos().y()))
- nextCP = sel[0].childItems()[3]
- nextCP.setVisible(True)
- nextCP._needsUngrab = True
- #nextCP.setSelected(True)
- self.sendEvent(nextCP, QEvent(QEvent.MouseButtonPress))
- nextCP.grabMouse()
- else:
- # release current onCurve, delete from contour
- self.sendEvent(sel[0], QEvent(QEvent.MouseButtonRelease))
- mouseGrabberItem.ungrabMouse()
- sel[0].setSelected(False)
-
- # construct a curve segment to the current point if there is not one
- onCurve = sel[0]._point
- if not onCurve.segmentType == "curve":
- # remove the last onCurve
- sel[0]._contour.removePoint(onCurve)
- prev = sel[0]._contour[-1]
- self.getItemForPoint(prev).childItems()[3].setVisible(True)
- # add a zero-length offCurve to the previous point
- sel[0]._contour.addPoint((prev.x, prev.y))
- # add prevOffCurve and activate
- sel[0]._contour.addPoint((sel[0].x(), sel[0].y()))
- sel[0].childItems()[1].setVisible(True)
- # add back current onCurve as a curve point
- sel[0]._contour.addPoint((onCurve.x, onCurve.y), "curve")
- sel[0]._point = sel[0]._contour[-1]
- if not QApplication.keyboardModifiers() & Qt.AltModifier:
- sel[0]._point.smooth = True
- sel[0]._isSmooth = True
- sel[0].setPointPath()
- if sel[0].getPointIndex() == 0:
- # we're probably dealing with the first point that we looped.
- # preserve nextCP whatsoever.
- lineObj = HandleLineItem(0, 0, 0, 0, sel[0])
- nextCP = OffCurvePointItem(0, 0, sel[0])
- # now we have l1, p1, l2, p2, l3, p3
- l2 = sel[0].childItems()[2]
- lineObj.stackBefore(l2)
- nextCP.stackBefore(l2)
- else:
- # add last offCurve
- sel[0]._contour.addPoint((sel[0].x(), sel[0].y()))
- nextCP = sel[0].childItems()[3]
- nextCP._needsUngrab = True
- nextCP.setVisible(True)
- #nextCP.setSelected(True)
- self.sendEvent(nextCP, QEvent(QEvent.MouseButtonPress))
- nextCP.grabMouse()
- self._blocked = False
- self._editing = None
- super(GlyphScene, self).mouseMoveEvent(event)
- else:
- # eat the event
- event.accept()
- else:
- currentTool = self.views()[0]._currentTool
- if currentTool == SceneTools.RulerTool:
- self.rulerMouseMove(event)
- return
- elif currentTool == SceneTools.KnifeTool:
- self.knifeMouseMove(event)
- return
- items = self.items(event.scenePos())
- # XXX: we must cater w mouse tracking
- # we dont need isSelected() once its rid
- if len(items) > 1 and isinstance(items[0], OnCurvePointItem) and \
- isinstance(items[1], OffCurvePointItem) and items[1].isSelected():
- items[1].setPos(0, 0)
- else:
- super(GlyphScene, self).mouseMoveEvent(event)
+ view = self.views()[0]
+ CurrentTool = view._currentTool
+ CurrentTool.mouseMoveEvent(self, event)
def mouseReleaseEvent(self, event):
- self._editing = False
- currentTool = self.views()[0]._currentTool
- if currentTool == SceneTools.DrawingTool:
- # cleanup extra point elements if we dealt w curved first point
- touched = self.itemAt(event.scenePos(), self.views()[0].transform())
- if touched and isinstance(touched, OffCurvePointItem):
- onCurve = touched.parentItem()
- children = onCurve.childItems()
- if len(children) > 4:
- # l1, p1, l3, p3, l2, p2
- children[3].prepareGeometryChange()
- self.removeItem(children[3])
- children[2].prepareGeometryChange()
- self.removeItem(children[2])
-
- onCurve._isSmooth = False
- onCurve.setPointPath()
- onCurve._point.smooth = False
- elif currentTool == SceneTools.RulerTool:
- self.rulerMouseRelease(event)
- elif currentTool == SceneTools.KnifeTool:
- self.knifeMouseRelease(event)
- super(GlyphScene, self).mouseReleaseEvent(event)
-
- def rulerMousePress(self, event):
- touched = self.itemAt(event.scenePos(), self.views()[0].transform())
- if touched is not None and isinstance(touched, OnCurvePointItem) or \
- isinstance(touched, OffCurvePointItem):
- x, y = touched.scenePos().x(), touched.scenePos().y()
- else:
- x, y = event.scenePos().x(), event.scenePos().y()
- if self._integerPlane:
- x, y = round(x), round(y)
- if self._cachedRuler is not None:
- self.removeItem(self._cachedRuler)
- self._cachedRuler = None
- path = QPainterPath()
- path.moveTo(x, y)
- path.lineTo(x+1, y)
- path.lineTo(x+1, y+1)
- path.closeSubpath()
- self._rulerObject = self.addPath(path)
- textItem = QGraphicsSimpleTextItem("0", self._rulerObject)
- font = self.font()
- font.setPointSize(9)
- textItem.setFont(font)
- textItem.setFlag(QGraphicsItem.ItemIgnoresTransformations)
- textItem.setPos(x, y + textItem.boundingRect().height())
- event.accept()
-
- def rulerMouseMove(self, event):
- # XXX: shouldnt have to do this, it seems mouseTracking is wrongly activated
- if self._rulerObject is None: return
- touched = self.itemAt(event.scenePos(), self.views()[0].transform())
- if touched is not None and isinstance(touched, OnCurvePointItem) or \
- isinstance(touched, OffCurvePointItem):
- x, y = touched.scenePos().x(), touched.scenePos().y()
- else:
- # TODO: 45deg clamp w ShiftModifier
- # maybe make a function for that + other occurences...
- x, y = event.scenePos().x(), event.scenePos().y()
- if self._integerPlane:
- x, y = round(x), round(y)
- path = self._rulerObject.path()
- baseElem = path.elementAt(0)
- path.setElementPositionAt(1, x, baseElem.y)
- path.setElementPositionAt(2, x, y)
- path.setElementPositionAt(3, baseElem.x, baseElem.y)
- self._rulerObject.setPath(path)
- textItem = self._rulerObject.childItems()[0]
- line = QLineF(baseElem.x, baseElem.y, x, y)
- l = line.length()
- # XXX: angle() doesnt go by trigonometric direction. Weird.
- # TODO: maybe split in positive/negative 180s (ff)
- a = 360 - line.angle()
- line.setP2(QPointF(x, baseElem.y))
- h = line.length()
- line.setP1(QPointF(x, y))
- v = line.length()
- text = "%d\n↔ %d\n↕ %d\nα %dº" % (l, h, v, a)
- textItem.setText(text)
- dx = x - baseElem.x
- if dx >= 0: px = x
- else: px = x - textItem.boundingRect().width()
- dy = y - baseElem.y
- if dy > 0: py = baseElem.y
- else: py = baseElem.y + textItem.boundingRect().height()
- textItem.setPos(px, py)
- event.accept()
-
- def rulerMouseRelease(self, event):
- textItem = self._rulerObject.childItems()[0]
- if textItem.text() == "0":
- # delete no-op ruler
- self.removeItem(self._rulerObject)
- self._rulerObject = None
- else:
- self._cachedRuler = self._rulerObject
- self._rulerObject = None
- event.accept()
-
- def knifeMousePress(self, event):
- scenePos = event.scenePos()
- x, y = scenePos.x(), scenePos.y()
- self._knifeLine = self.addLine(x, y, x, y)
- event.accept()
-
- """
- Computes intersection between a cubic spline and a line segment.
- Adapted from: https://www.particleincell.com/2013/cubic-line-intersection/
-
- Takes four defcon points describing curve and four scalars describing line
- parameters.
- """
- def computeIntersections(self, p1, p2, p3, p4, x1, y1, x2, y2):
- bx, by = x1 - x2, y2 - y1
- m = x1*(y1-y2) + y1*(x2-x1)
- a, b, c, d = bezierTools.calcCubicParameters((p1.x, p1.y), (p2.x, p2.y),
- (p3.x, p3.y), (p4.x, p4.y))
-
- pc0 = by*a[0] + bx*a[1]
- pc1 = by*b[0] + bx*b[1]
- pc2 = by*c[0] + bx*c[1]
- pc3 = by*d[0] + bx*d[1] + m
- r = bezierTools.solveCubic(pc0, pc1, pc2, pc3)
-
- sol = []
- for t in r:
- s0 = a[0]*t**3 + b[0]*t**2 + c[0]*t + d[0]
- s1 = a[1]*t**3 + b[1]*t**2 + c[1]*t + d[1]
- if (x2-x1) != 0:
- s = (s0-x1) / (x2-x1)
- else:
- s = (s1-y1) / (y2-y1)
- if not (t < 0 or t > 1 or s < 0 or s > 1):
- sol.append((s0, s1, t))
- return sol
-
- """
- G. Bach, http://stackoverflow.com/a/1968345
- """
- def lineIntersection(self, x1, y1, x2, y2, x3, y3, x4, y4):
- Bx_Ax = x2 - x1
- By_Ay = y2 - y1
- Dx_Cx = x4 - x3
- Dy_Cy = y4 - y3
- determinant = (-Dx_Cx * By_Ay + Bx_Ax * Dy_Cy)
- if abs(determinant) < 1e-20: return []
- s = (-By_Ay * (x1 - x3) + Bx_Ax * (y1 - y3)) / determinant
- t = ( Dx_Cx * (y1 - y3) - Dy_Cy * (x1 - x3)) / determinant
- if s >= 0 and s <= 1 and t >= 0 and t <= 1:
- return [(x1 + (t * Bx_Ax), y1 + (t * By_Ay), t)]
- return []
-
- def knifeMouseMove(self, event):
- # XXX: shouldnt have to do this, it seems mouseTracking is wrongly activated
- if self._knifeLine is None: return
- for dot in self._knifeDots:
- self.removeItem(dot)
- self._knifeDots = []
- scenePos = event.scenePos()
- x, y = scenePos.x(), scenePos.y()
- line = self._knifeLine.line()
- line.setP2(QPointF(x, y))
- # XXX: not nice
- glyph = self.views()[0]._glyph
- self._cachedIntersections = []
- for contour in glyph:
- segments = contour.segments
- for index, seg in enumerate(segments):
- prev = segments[index-1][-1]
- if len(seg) == 3:
- i = self.computeIntersections(prev, seg[0], seg[1], seg[2], line.x1(), line.y1(), x, y)
- else:
- i = self.lineIntersection(prev.x, prev.y, seg[0].x, seg[0].y, line.x1(), line.y1(), x, y)
- for pt in i:
- scale = self.getViewScale()
- item = self.addEllipse(-offHalf/scale, -offHalf/scale, offWidth/scale, offHeight/scale)
- item.setPos(pt[0], pt[1])
- self._cachedIntersections.append((contour, index, pt[2]))
- self._knifeDots.append(item)
- self._knifeLine.setLine(line)
- event.accept()
+ view = self.views()[0]
+ CurrentTool = view._currentTool
+ CurrentTool.mouseReleaseEvent(self, event)
- def knifeMouseRelease(self, event):
- self.removeItem(self._knifeLine)
- self._knifeLine = None
- for dot in self._knifeDots:
- self.removeItem(dot)
- self._knifeDots = []
- # reverse so as to not invalidate our cached segment indexes
- # XXX: multiple cuts on one segment don't work reliably
- self._cachedIntersections.reverse()
- if len(self._cachedIntersections):
- for intersect in self._cachedIntersections:
- contour, index, t = intersect
- contour.splitAndInsertPointAtSegmentAndT(index, t)
- self._cachedIntersections = []
- event.accept()
+ def mouseDoubleClickEvent(self, event):
+ view = self.views()[0]
+ CurrentTool = view._currentTool
+ CurrentTool.mouseDoubleClickEvent(self, event)
class GlyphView(QGraphicsView):
def __init__(self, glyph, parent=None):
@@ -1302,7 +413,18 @@ class GlyphView(QGraphicsView):
self.setResizeAnchor(QGraphicsView.AnchorUnderMouse)
#self.setViewportUpdateMode(QGraphicsView.BoundingRectViewportUpdate)
- self.setSceneSelection()
+ tools = (
+ selectionTool.SelectionTool,
+ penTool.PenTool,
+ rulerTool.RulerTool,
+ knifeTool.KnifeTool,
+ )
+ for tool in tools:
+ self.registerTool(tool)
+ # TODO: rethink this
+ selectionButton = self.parent().toolBar.actions()[0]
+ selectionButton.setChecked(True)
+ self._currentTool = selectionButton.data()
self.setRenderHint(QPainter.Antialiasing)
self.scale(1, -1)
@@ -1340,6 +462,26 @@ class GlyphView(QGraphicsView):
# this will not be the case anymore when drag sidebearings pops up
scene._widthItem.setRect(0, -1000, self._glyph.width, 3000)
+ def registerTool(self, tool, activate=False):
+ # TODO: enclose this better
+ mainWindow = self.parent()
+ toolBar = mainWindow.toolBar
+ toolsGroup = mainWindow.toolsGroup
+
+ instance = tool()
+ button = toolBar.addAction(instance.name(), self.setTool)
+ button.setCheckable(True)
+ button.setChecked(activate)
+ button.setData(instance)
+ button.setIcon(instance.icon())
+ toolsGroup.addAction(button)
+
+ def setTool(self, tool=None):
+ if tool is None:
+ tool = self.sender().data()
+ self._currentTool = tool
+ self.setDragMode(tool.dragMode())
+
def addBackground(self):
scene = self.scene()
font = self._glyph.getParent()
@@ -1568,22 +710,6 @@ class GlyphView(QGraphicsView):
self.setDragMode(QGraphicsView.RubberBandDrag)
super(GlyphView, self).mousePressEvent(event)
- def setSceneDrawing(self):
- self._currentTool = SceneTools.DrawingTool
- self.setDragMode(QGraphicsView.NoDrag)
-
- def setSceneRuler(self):
- self._currentTool = SceneTools.RulerTool
- self.setDragMode(QGraphicsView.NoDrag)
-
- def setSceneSelection(self):
- self._currentTool = SceneTools.SelectionTool
- self.setDragMode(QGraphicsView.RubberBandDrag)
-
- def setSceneKnife(self):
- self._currentTool = SceneTools.KnifeTool
- self.setDragMode(QGraphicsView.NoDrag)
-
# Lock/release handdrag does not seem to work…
'''
def mouseReleaseEvent(self, event):
diff --git a/Lib/defconQt/objects/qtItems.py b/Lib/defconQt/objects/qtItems.py
new file mode 100644
index 0000000..d12c2b4
--- /dev/null
+++ b/Lib/defconQt/objects/qtItems.py
@@ -0,0 +1,505 @@
+from PyQt5.QtCore import QLineF, Qt
+from PyQt5.QtGui import QBrush, QColor, QFont, QPainterPath, QPen, QTransform
+from PyQt5.QtWidgets import (QApplication, QGraphicsItem, QGraphicsLineItem, QGraphicsEllipseItem,
+ QGraphicsPathItem, QGraphicsSimpleTextItem, QGraphicsRectItem, QGraphicsPixmapItem,
+ QStyle, QStyleOptionGraphicsItem)
+
+def roundPosition(value):
+ value = value * 10#self._scale
+ value = round(value) - .5
+ value = value * .1#self._inverseScale
+ return value
+
+offCurvePointSize = 8#5
+onCurvePointSize = 9#6
+onCurveSmoothPointSize = 10#7
+offWidth = offHeight = roundPosition(offCurvePointSize)# * self._inverseScale)
+offHalf = offWidth / 2.0
+onWidth = onHeight = roundPosition(onCurvePointSize)# * self._inverseScale)
+onHalf = onWidth / 2.0
+smoothWidth = smoothHeight = roundPosition(onCurveSmoothPointSize)# * self._inverseScale)
+smoothHalf = smoothWidth / 2.0
+onCurvePenWidth = 1.5
+offCurvePenWidth = 1.0
+
+anchorSize = 11
+anchorWidth = anchorHeight = roundPosition(anchorSize)
+anchorHalf = anchorWidth / 2.0
+
+bezierHandleColor = QColor.fromRgbF(0, 0, 0, .2)
+startPointColor = QColor.fromRgbF(0, 0, 0, .2)
+backgroundColor = Qt.white
+offCurvePointColor = QColor.fromRgbF(1, 1, 1, 1)
+offCurvePointStrokeColor = QColor.fromRgbF(.6, .6, .6, 1)
+onCurvePointColor = offCurvePointStrokeColor
+onCurvePointStrokeColor = offCurvePointColor
+anchorColor = QColor(120, 120, 255)
+anchorSelectionColor = Qt.blue
+bluesColor = QColor.fromRgbF(.5, .7, 1, .3)
+fillColor = QColor(200, 200, 200, 120)#QColor.fromRgbF(0, 0, 0, .4)
+componentFillColor = QColor.fromRgbF(0, 0, 0, .4)#QColor.fromRgbF(.2, .2, .3, .4)
+metricsColor = QColor(70, 70, 70)
+pointSelectionColor = Qt.red
+
+class HandleLineItem(QGraphicsLineItem):
+ def __init__(self, x1, y1, x2, y2, parent):
+ super(HandleLineItem, self).__init__(x1, y1, x2, y2, parent)
+ self.setPen(QPen(bezierHandleColor, 1.0))
+ self.setFlag(QGraphicsItem.ItemStacksBehindParent)
+
+class OffCurvePointItem(QGraphicsEllipseItem):
+ def __init__(self, x, y, parent=None):
+ super(OffCurvePointItem, self).__init__(parent)
+ # since we have a parent, setPos must be relative to it
+ self.setPointPath()
+ self.setPos(x, y) # TODO: abstract and use pointX-self.parent().pos().x()
+ self.setFlag(QGraphicsItem.ItemIsMovable)
+ self.setFlag(QGraphicsItem.ItemIsSelectable)
+ self.setFlag(QGraphicsItem.ItemSendsGeometryChanges)
+ self.setFlag(QGraphicsItem.ItemStacksBehindParent)
+
+ self.setBrush(QBrush(offCurvePointColor))
+ self._needsUngrab = False
+
+ def itemChange(self, change, value):
+ if change == QGraphicsItem.ItemPositionChange:
+ if self.scene()._integerPlane:
+ value.setX(round(value.x()))
+ value.setY(round(value.y()))
+ if QApplication.keyboardModifiers() & Qt.ShiftModifier \
+ and len(self.scene().selectedItems()) == 1:
+ ax = abs(value.x())
+ ay = abs(value.y())
+ if ay >= ax * 2:
+ value.setX(0)
+ elif ay > ax / 2:
+ avg = (ax + ay) / 2
+ value.setX(copysign(avg, value.x()))
+ value.setY(copysign(avg, value.y()))
+ else:
+ value.setY(0)
+ elif change == QGraphicsItem.ItemPositionHasChanged:
+ self.parentItem()._CPMoved(value)
+ # TODO: consider what to do w offCurves
+ #elif change == QGraphicsItem.ItemSelectedHasChanged:
+ # pass#self.parentItem()._CPSelChanged(value)
+ return value
+
+ #def mousePressEvent(self, event):
+ # if not self._needsUngrab and self.x() == 0 and self.y() == 0:
+ # event.ignore()
+ # super(OffCurvePointItem, self).mousePressEvent(event)
+
+ def mouseReleaseEvent(self, event):
+ super(OffCurvePointItem, self).mouseReleaseEvent(event)
+ if self._needsUngrab:
+ self.ungrabMouse()
+ self._needsUngrab = False
+
+ # http://www.qtfr.org/viewtopic.php?pid=21045#p21045
+ def paint(self, painter, option, widget):
+ #if self.x() == 0 and self.y() == 0: return
+ newOption = QStyleOptionGraphicsItem(option)
+ newOption.state = QStyle.State_None
+ pen = self.pen()
+ if option.state & QStyle.State_Selected:
+ pen.setColor(pointSelectionColor)
+ else:
+ pen.setColor(offCurvePointStrokeColor)
+ self.setPen(pen)
+ super(OffCurvePointItem, self).paint(painter, newOption, widget)
+
+ def setPointPath(self, scale=None):
+ if scale is None:
+ scene = self.scene()
+ if scene is not None:
+ scale = scene.getViewScale()
+ else:
+ scale = 1
+ if scale > 4: scale = 4
+ elif scale < .4: scale = .4
+ self.prepareGeometryChange()
+ self.setRect(-offHalf/scale, -offHalf/scale, offWidth/scale, offHeight/scale)
+ self.setPen(QPen(offCurvePointStrokeColor, offCurvePenWidth/scale))
+
+class OnCurvePointItem(QGraphicsPathItem):
+ def __init__(self, x, y, isSmooth, contour, point, scale=1, parent=None):
+ super(OnCurvePointItem, self).__init__(parent)
+ self._contour = contour
+ self._point = point
+ self._isSmooth = isSmooth
+ self._posBeforeMove = None
+
+ self.setPointPath(scale)
+ self.setPos(x, y)
+ self.setFlag(QGraphicsItem.ItemIsMovable)
+ self.setFlag(QGraphicsItem.ItemIsSelectable)
+ self.setFlag(QGraphicsItem.ItemSendsGeometryChanges)
+ self.setBrush(QBrush(onCurvePointColor))
+
+ def delete(self, preserveShape=True):
+ def findNextOnCurve(self, index=0):
+ for _ in self._contour:
+ if self._contour[index].segmentType is not None:
+ break
+ index = (index+1) % len(self._contour)
+ return index
+
+ scene = self.scene()
+ glyph = scene._glyphObject
+ if len(self._contour.segments) < 2:
+ glyph.removeContour(self._contour)
+ else:
+ ptIndex = self.getPointIndex()
+ if self._contour.open and ptIndex == 0:
+ nextOnCurveIndex = findNextOnCurve(self, 1)
+ self._contour._points = self._contour[nextOnCurveIndex:]
+ self._contour[0].segmentType = "move"
+ self._contour.dirty = True
+ else:
+ # Using preserveShape at the edge of an open contour will traceback
+ if ptIndex == len(self._contour): preserveShape = False
+ self._contour.removeSegment(self.getSegmentIndex(), preserveShape)
+ nextOnCurveIndex = findNextOnCurve(self)
+ self._contour.setStartPoint(nextOnCurveIndex)
+ # This object will be removed from scene by notification mechanism
+
+ def setPointPath(self, scale=None):
+ path = QPainterPath()
+ if scale is None:
+ scene = self.scene()
+ if scene is not None:
+ scale = scene.getViewScale()
+ else:
+ scale = 1
+ if scale > 4: scale = 4
+ elif scale < .4: scale = .4
+ if self._isSmooth:
+ path.addEllipse(-smoothHalf/scale, -smoothHalf/scale, smoothWidth/scale, smoothHeight/scale)
+ else:
+ path.addRect(-onHalf/scale, -onHalf/scale, onWidth/scale, onHeight/scale)
+ self.prepareGeometryChange()
+ self.setPath(path)
+ self.setPen(QPen(onCurvePointStrokeColor, onCurvePenWidth/scale))
+
+ def getPointIndex(self):
+ return self._contour.index(self._point)
+
+ def getSegmentIndex(self):
+ # closed contour cycles and so the "previous" segment goes to current point
+ index = 0 if self._contour.open else -1
+ for pt in self._contour:
+ if pt == self._point: break
+ if pt.segmentType is not None: index += 1
+ return index % len(self._contour.segments)
+
+ def _CPMoved(self, newValue):
+ pointIndex = self.getPointIndex()
+ children = self.childItems()
+ # nodes are stored after lines (for stacking order)
+ if children[1].isSelected():
+ selected = 1
+ propagate = 3
+ else:
+ selected = 3
+ propagate = 1
+ curValue = children[selected].pos()
+ line = children[selected-1].line()
+ children[selected-1].setLine(line.x1(), line.y1(), newValue.x(), newValue.y())
+
+ if not len(children) > 4:
+ # we need the modulo if the start point was to be an offCurve
+ elemIndex = (pointIndex-2+selected) % len(self._contour)
+ self._contour[elemIndex].x = self.pos().x()+newValue.x()
+ self._contour[elemIndex].y = self.pos().y()+newValue.y()
+ if not (self._isSmooth and children[propagate].isVisible()):
+ self.setShallowDirty()
+ return
+ if children[selected]._needsUngrab:
+ targetLen = children[selected-1].line().length()*2
+ else:
+ targetLen = children[selected-1].line().length()+children[propagate-1].line().length()
+ tmpLine = QLineF(newValue, QPointF(0, 0))
+ tmpLine.setLength(targetLen)
+ children[propagate].setFlag(QGraphicsItem.ItemSendsGeometryChanges, False)
+ children[propagate].setPos(tmpLine.x2(), tmpLine.y2())
+ children[propagate].setFlag(QGraphicsItem.ItemSendsGeometryChanges)
+ children[propagate-1].setLine(line.x1(), line.y1(), tmpLine.x2(), tmpLine.y2())
+ propagateInContour = (pointIndex-2+propagate) % len(self._contour)
+ self._contour[propagateInContour].x = self.pos().x()+tmpLine.x2()
+ self._contour[propagateInContour].y = self.pos().y()+tmpLine.y2()
+ self.setShallowDirty()
+
+ def itemChange(self, change, value):
+ if change == QGraphicsItem.ItemPositionChange:
+ if self.scene()._integerPlane:
+ value.setX(round(value.x()))
+ value.setY(round(value.y()))
+ elif change == QGraphicsItem.ItemPositionHasChanged:
+ # have a look at defcon FuzzyNumber as well
+ pointIndex = self.getPointIndex()
+ self._contour[pointIndex].x = self.pos().x()
+ self._contour[pointIndex].y = self.pos().y()
+
+ children = self.childItems()
+ if children[1].isVisible():
+ prevPos = children[1].pos()
+ self._contour[pointIndex-1].x = self.pos().x()+prevPos.x()
+ self._contour[pointIndex-1].y = self.pos().y()+prevPos.y()
+ if children[3].isVisible():
+ nextPos = children[3].pos()
+ self._contour[pointIndex+1].x = self.pos().x()+nextPos.x()
+ self._contour[pointIndex+1].y = self.pos().y()+nextPos.y()
+ self.setShallowDirty()
+ elif change == QGraphicsItem.ItemSelectedHasChanged:
+ self._point.selected = value
+ return value
+
+ def setShallowDirty(self):
+ scene = self.scene()
+ scene._blocked = True
+ self._contour.dirty = True
+ scene._blocked = False
+
+ def mouseMoveEvent(self, event):
+ modifiers = event.modifiers()
+ children = self.childItems()
+ # Ctrl: get and move prevCP, Alt: nextCP
+ if modifiers & Qt.ControlModifier and children[1].x() == 0 and children[1].y() == 0:
+ i, o = 1, 3
+ elif modifiers & Qt.AltModifier and children[3].x() == 0 and children[3].y() == 0:
+ i, o = 3, 1
+ elif not (modifiers & Qt.ControlModifier or modifiers & Qt.AltModifier):
+ super(OnCurvePointItem, self).mouseMoveEvent(event)
+ return
+ else: # eat the event if we are not going to yield an offCP
+ event.accept()
+ return
+ ptIndex = self.getPointIndex()
+ scene = self.scene()
+ scene._blocked = True
+ # if we have line segment, insert offCurve points
+ insertIndex = (ptIndex+(i-1)//2) % len(self._contour)
+ if self._contour[insertIndex].segmentType == "line":
+ nextToCP = self._contour[(ptIndex-2+i) % len(self._contour)]
+ assert(nextToCP.segmentType is not None)
+ self._contour[insertIndex].segmentType = "curve"
+ if i == 1:
+ first, second = (self._point.x, self._point.y), (nextToCP.x, nextToCP.y)
+ else:
+ first, second = (nextToCP.x, nextToCP.y), (self._point.x, self._point.y)
+ self._contour.insertPoint(insertIndex, self._contour._pointClass(first))
+ self._contour.insertPoint(insertIndex, self._contour._pointClass(second))
+ children[i].setVisible(True)
+ # TODO: need a list of items to make this efficient
+ scene.getItemForPoint(nextToCP).childItems()[o].setVisible(True)
+ # release current onCurve
+ scene.sendEvent(self, QEvent(QEvent.MouseButtonRelease))
+ scene.mouseGrabberItem().ungrabMouse()
+ self.setSelected(False)
+ self.setIsSmooth(False)
+ children[i]._needsUngrab = True
+ scene.sendEvent(children[i], QEvent(QEvent.MouseButtonPress))
+ children[i].setSelected(True)
+ children[i].grabMouse()
+ scene._blocked = False
+ event.accept()
+
+ def setIsSmooth(self, isSmooth):
+ self._isSmooth = isSmooth
+ self._point.smooth = self._isSmooth
+ self.setShallowDirty()
+ self.setPointPath()
+
+ # http://www.qtfr.org/viewtopic.php?pid=21045#p21045
+ def paint(self, painter, option, widget):
+ newOption = QStyleOptionGraphicsItem(option)
+ newOption.state = QStyle.State_None
+ pen = self.pen()
+ if option.state & QStyle.State_Selected:
+ pen.setColor(pointSelectionColor)
+ else:
+ pen.setColor(onCurvePointStrokeColor)
+ self.setPen(pen)
+ super(OnCurvePointItem, self).paint(painter, newOption, widget)
+
+class AnchorItem(QGraphicsPathItem):
+ def __init__(self, anchor, scale=1, parent=None):
+ super(AnchorItem, self).__init__(parent)
+ self._anchor = anchor
+
+ textItem = QGraphicsSimpleTextItem(self._anchor.name, parent=self)
+ font = QFont()
+ font.setPointSize(9)
+ textItem.setFont(font)
+ textItem.setFlag(QGraphicsItem.ItemIgnoresTransformations)
+ self.setPointPath(scale)
+ self.setPos(self._anchor.x, self._anchor.y)
+ self.setFlag(QGraphicsItem.ItemIsMovable)
+ self.setFlag(QGraphicsItem.ItemIsSelectable)
+ self.setFlag(QGraphicsItem.ItemSendsGeometryChanges)
+ self.setBrush(QBrush(anchorColor))
+ self.setPen(QPen(Qt.NoPen))
+
+ def delete(self):
+ glyph = self._anchor.getParent()
+ glyph.removeAnchor(self._anchor)
+
+ def itemChange(self, change, value):
+ if change == QGraphicsItem.ItemPositionChange:
+ if self.scene()._integerPlane:
+ value.setX(round(value.x()))
+ value.setY(round(value.y()))
+ elif change == QGraphicsItem.ItemPositionHasChanged:
+ x = self.pos().x()
+ y = self.pos().y()
+ scene = self.scene()
+ scene._blocked = True
+ self._anchor.x = x
+ self._anchor.y = y
+ scene._blocked = False
+ return value
+
+ def setPointPath(self, scale=None):
+ path = QPainterPath()
+ if scale is None:
+ scene = self.scene()
+ if scene is not None:
+ scale = scene.getViewScale()
+ else:
+ scale = 1
+ if scale > 4: scale = 4
+ elif scale < .4: scale = .4
+
+ path.moveTo(-anchorHalf/scale, 0)
+ path.lineTo(0, anchorHalf/scale)
+ path.lineTo(anchorHalf/scale, 0)
+ path.lineTo(0, -anchorHalf/scale)
+ path.closeSubpath()
+
+ self.prepareGeometryChange()
+ self.setPath(path)
+ textItem = self.childItems()[0]
+ textItem.setPos(anchorHalf/scale, textItem.boundingRect().height()/2)
+
+ # http://www.qtfr.org/viewtopic.php?pid=21045#p21045
+ def paint(self, painter, option, widget):
+ newOption = QStyleOptionGraphicsItem(option)
+ newOption.state = QStyle.State_None
+ pen = self.pen()
+ if option.state & QStyle.State_Selected:
+ self.setBrush(anchorSelectionColor)
+ else:
+ self.setBrush(anchorColor)
+ super(AnchorItem, self).paint(painter, newOption, widget)
+
+class ComponentItem(QGraphicsPathItem):
+ def __init__(self, path, component, parent=None):
+ super(ComponentItem, self).__init__(path, parent)
+ self._component = component
+ self.setTransform(QTransform(*component.transformation))
+ self.setBrush(QBrush(componentFillColor))
+ self.setFlag(QGraphicsItem.ItemIsMovable)
+ self.setFlag(QGraphicsItem.ItemIsSelectable)
+ self.setFlag(QGraphicsItem.ItemSendsGeometryChanges)
+
+ def delete(self):
+ glyph = self._component.getParent()
+ glyph.removeComponent(self._component)
+
+ def itemChange(self, change, value):
+ if change == QGraphicsItem.ItemPositionChange:
+ if self.scene()._integerPlane:
+ value.setX(round(value.x()))
+ value.setY(round(value.y()))
+ elif change == QGraphicsItem.ItemPositionHasChanged:
+ t = self._component.transformation
+ x = self.pos().x()
+ y = self.pos().y()
+ scene = self.scene()
+ scene._blocked = True
+ self._component.transformation = (t[0], t[1], t[2], t[3], x, y)
+ scene._blocked = False
+ return value
+
+class VGuidelinesTextItem(QGraphicsSimpleTextItem):
+ def __init__(self, text, font, parent=None):
+ super(VGuidelinesTextItem, self).__init__(text, parent)
+ self.setBrush(metricsColor)
+ self.setFlag(QGraphicsItem.ItemIgnoresTransformations)
+ self.setFont(font)
+
+class ResizeHandleItem(QGraphicsRectItem):
+ def __init__(self, parent=None):
+ super(QGraphicsRectItem, self).__init__(parent)
+ self.setPointPath()
+ self.setBrush(QBrush(QColor(60, 60, 60)))
+ self.setPen(QPen(Qt.NoPen))
+ self.setFlag(QGraphicsItem.ItemIgnoresParentOpacity)
+ self.setFlag(QGraphicsItem.ItemIsMovable)
+ #self.setFlag(QGraphicsItem.ItemIsSelectable)
+ self.setCursor(Qt.SizeFDiagCursor)
+
+ rect = self.parentItem().boundingRect()
+ self.setPos(rect.width(), rect.height())
+
+ def itemChange(self, change, value):
+ if change == QGraphicsItem.ItemSelectedChange:
+ if not value: self.setVisible(value)
+ return value
+
+ def mouseMoveEvent(self, event):
+ self.parentItem()._pixmapGeometryChanged(event)
+
+ def setPointPath(self, scale=None):
+ if scale is None:
+ scene = self.scene()
+ if scene is not None:
+ scale = scene.getViewScale()
+ else:
+ scale = 1
+ if scale > 4: scale = 4
+ self.prepareGeometryChange()
+ self.setRect(-onHalf/scale, -onHalf/scale, onWidth/scale, onHeight/scale)
+
+class PixmapItem(QGraphicsPixmapItem):
+ def __init__(self, x, y, pixmap, parent=None):
+ super(QGraphicsPixmapItem, self).__init__(pixmap, parent)
+ self.setPos(x, y)
+ self.setFlag(QGraphicsItem.ItemIsMovable)
+ self.setFlag(QGraphicsItem.ItemIsSelectable)
+ self.setTransform(QTransform().fromScale(1, -1))
+ self.setOpacity(.5)
+ self.setZValue(-1)
+
+ rect = self.boundingRect()
+ self._rWidth = rect.width()
+ self._rHeight = rect.height()
+ handle = ResizeHandleItem(self)
+ handle.setVisible(False)
+
+ def _pixmapGeometryChanged(self, event):
+ modifiers = event.modifiers()
+ pos = event.scenePos()
+ if modifiers & Qt.ControlModifier:
+ # rotate
+ refLine = QLineF(self.x(), self.y(), self.x()+self._rWidth, self.y()-self._rHeight)
+ curLine = QLineF(self.x(), self.y(), pos.x(), pos.y())
+ self.setRotation(refLine.angleTo(curLine))
+ else:
+ # scale
+ dy = (pos.y() - self.y()) / self._rHeight
+ if modifiers & Qt.ShiftModifier:
+ # keep original aspect ratio
+ dx = -dy
+ else:
+ dx = (pos.x() - self.x()) / self._rWidth
+ self.setTransform(QTransform().fromScale(dx, dy))
+ event.accept()
+
+ def itemChange(self, change, value):
+ if change == QGraphicsItem.ItemSelectedChange:
+ children = self.childItems()
+ if not children[0].isUnderMouse():
+ children[0].setVisible(value)
+ return value
diff --git a/Lib/defconQt/tools/__init__.py b/Lib/defconQt/tools/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/Lib/defconQt/tools/baseTool.py b/Lib/defconQt/tools/baseTool.py
new file mode 100644
index 0000000..e19c328
--- /dev/null
+++ b/Lib/defconQt/tools/baseTool.py
@@ -0,0 +1,33 @@
+from defconQt.glyphView import OnCurvePointItem
+from PyQt5.QtGui import QIcon
+from PyQt5.QtWidgets import QGraphicsView
+
+class BaseTool(object):
+ def name(self):
+ return "Tool"
+
+ def icon(self):
+ return QIcon()
+
+ def dragMode(self):
+ return QGraphicsView.NoDrag
+
+ def doesUndo(self):
+ return True
+
+ # events
+
+ def mousePressEvent(self, scene, event):
+ raise NotImplementedError
+
+ def mouseMoveEvent(self, scene, event):
+ raise NotImplementedError
+
+ def mouseReleaseEvent(self, scene, event):
+ raise NotImplementedError
+
+ def mouseDoubleClickEvent(self, scene, event):
+ view = scene.views()[0]
+ touched = scene.itemAt(event.scenePos(), view.transform())
+ if touched and isinstance(touched, OnCurvePointItem):
+ touched.setIsSmooth(not touched._isSmooth)
diff --git a/Lib/defconQt/tools/knifeTool.py b/Lib/defconQt/tools/knifeTool.py
new file mode 100644
index 0000000..02d1b6b
--- /dev/null
+++ b/Lib/defconQt/tools/knifeTool.py
@@ -0,0 +1,69 @@
+from defconQt.tools.baseTool import BaseTool
+from defconQt.util.bezierMath import computeIntersections, lineIntersection
+from PyQt5.QtCore import QPointF
+from PyQt5.QtGui import QIcon
+from PyQt5.QtWidgets import QGraphicsScene, QGraphicsView
+
+class KnifeTool(BaseTool):
+ def name(self):
+ return "Knife"
+
+ def icon(self):
+ return QIcon("defconQt/resources/cut.svg")
+
+ # events
+
+ def mousePressEvent(self, scene, event):
+ scenePos = event.scenePos()
+ x, y = scenePos.x(), scenePos.y()
+ scene._knifeLine = scene.addLine(x, y, x, y)
+ event.accept()
+
+ def mouseMoveEvent(self, scene, event):
+ # XXX: shouldnt have to do this, it seems mouseTracking is wrongly activated
+ if scene._knifeLine is None: return
+ for dot in scene._knifeDots:
+ scene.removeItem(dot)
+ scene._knifeDots = []
+ scenePos = event.scenePos()
+ x, y = scenePos.x(), scenePos.y()
+ line = scene._knifeLine.line()
+ line.setP2(QPointF(x, y))
+ # XXX: not nice
+ glyph = scene.views()[0]._glyph
+ scene._cachedIntersections = []
+ for contour in glyph:
+ segments = contour.segments
+ for index, seg in enumerate(segments):
+ prev = segments[index-1][-1]
+ if len(seg) == 3:
+ i = computeIntersections(prev, seg[0], seg[1], seg[2], line.x1(), line.y1(), x, y)
+ else:
+ i = lineIntersection(prev.x, prev.y, seg[0].x, seg[0].y, line.x1(), line.y1(), x, y)
+ for pt in i:
+ scale = scene.getViewScale()
+ item = scene.addEllipse(-offHalf/scale, -offHalf/scale, offWidth/scale, offHeight/scale)
+ item.setPos(pt[0], pt[1])
+ scene._cachedIntersections.append((contour, index, pt[2]))
+ scene._knifeDots.append(item)
+ scene._knifeLine.setLine(line)
+ event.accept()
+
+ def mouseReleaseEvent(self, scene, event):
+ scene.removeItem(scene._knifeLine)
+ scene._knifeLine = None
+ for dot in scene._knifeDots:
+ scene.removeItem(dot)
+ scene._knifeDots = []
+ # reverse so as to not invalidate our cached segment indexes
+ # XXX: multiple cuts on one segment don't work reliably
+ scene._cachedIntersections.reverse()
+ if len(scene._cachedIntersections):
+ for intersect in scene._cachedIntersections:
+ contour, index, t = intersect
+ contour.splitAndInsertPointAtSegmentAndT(index, t)
+ scene._cachedIntersections = []
+ event.accept()
+
+ def mouseDoubleClickEvent(self, scene, event):
+ pass
diff --git a/Lib/defconQt/tools/penTool.py b/Lib/defconQt/tools/penTool.py
new file mode 100644
index 0000000..1634965
--- /dev/null
+++ b/Lib/defconQt/tools/penTool.py
@@ -0,0 +1,184 @@
+from defconQt.objects.defcon import TContour
+from defconQt.tools.baseTool import BaseTool
+from defconQt.glyphView import OffCurvePointItem, OnCurvePointItem
+from PyQt5.QtGui import QIcon
+from PyQt5.QtWidgets import QGraphicsScene, QGraphicsView
+
+# XXX: self._editing should go in there
+
+class PenTool(BaseTool):
+ def name(self):
+ return "Pen"
+
+ def icon(self):
+ return QIcon("defconQt/resources/curve.svg")
+
+ # events
+
+ def mousePressEvent(self, scene, event):
+ view = scene.views()[0]
+ touched = scene.itemAt(event.scenePos(), view.transform())
+ scene._blocked = True
+ forceSelect = False
+ sel = scene.selectedItems()
+ x, y = event.scenePos().x(), event.scenePos().y()
+ if scene._integerPlane:
+ x, y = round(x), round(y)
+ # XXX: not sure why isinstance does not work here
+ if len(sel) == 1:
+ isLastOnCurve = type(sel[0]) is OnCurvePointItem and sel[0]._contour.open and \
+ sel[0].getPointIndex() == len(sel[0]._contour)-1
+ # TODO: reimplement convenience methods in OffCurvePointItem
+ isLastOffCurve = type(sel[0]) is OffCurvePointItem and sel[0].parentItem()._contour.open and \
+ sel[0].parentItem().getPointIndex()+1 == len(sel[0].parentItem()._contour)-1
+ if len(sel) == 1 and (isLastOffCurve or isLastOnCurve):
+ if isLastOnCurve:
+ lastContour = sel[0]._contour
+ else:
+ lastContour = sel[0].parentItem()._contour
+ if (touched and isinstance(touched, OnCurvePointItem)) and touched.getPointIndex() == 0 \
+ and lastContour == touched._contour and len(lastContour) > 1:
+ # Changing the first point from move to line/curve will cycle and so close the contour
+ if isLastOffCurve:
+ lastContour.addPoint((x,y))
+ lastContour[0].segmentType = "curve"
+ touched.childItems()[1].setVisible(True)
+ else:
+ lastContour[0].segmentType = "line"
+ elif touched and isinstance(touched, OnCurvePointItem):
+ QGraphicsScene.mousePressEvent(scene, event)
+ return
+ else:
+ if QApplication.keyboardModifiers() & Qt.ShiftModifier:
+ forceSelect = True
+ if isLastOnCurve:
+ refx = sel[0].x()
+ refy = sel[0].y()
+ else:
+ refx = sel[0].parentItem().x()
+ refy = sel[0].parentItem().y()
+ if abs(x-refx) > abs(y-refy): y = copysign(refy, y)
+ else: x = copysign(refx, x)
+ if isLastOffCurve:
+ lastContour.addPoint((x,y))
+ lastContour.addPoint((x,y), "curve")
+ else:
+ lastContour.addPoint((x,y), "line")
+ item = OnCurvePointItem(x, y, False, lastContour, lastContour[-1], scene.getViewScale())
+ scene.addItem(item)
+ for _ in range(2):
+ lineObj = HandleLineItem(0, 0, 0, 0, item)
+ CPObject = OffCurvePointItem(0, 0, item)
+ CPObject.setVisible(False)
+ if isLastOffCurve:
+ item.childItems()[1].setVisible(True)
+ lastContour.dirty = True
+ scene._editing = True
+ elif not (touched and isinstance(touched, OnCurvePointItem)):
+ nextC = TContour()
+ scene._glyphObject.appendContour(nextC)
+ nextC.addPoint((x,y), "move")
+
+ item = OnCurvePointItem(x, y, False, scene._glyphObject[-1], scene._glyphObject[-1][-1], scene.getViewScale())
+ scene.addItem(item)
+ for _ in range(2):
+ lineObj = HandleLineItem(0, 0, 0, 0, item)
+ CPObject = OffCurvePointItem(0, 0, item)
+ CPObject.setVisible(False)
+ scene._editing = True
+ scene._blocked = False
+ QGraphicsScene.mousePressEvent(scene, event)
+ # Since shift clamps, we might be missing the point in mousePressEvent
+ if forceSelect: item.setSelected(True)
+
+ def mouseMoveEvent(self, scene, event):
+ if scene._editing is True:
+ sel = scene.selectedItems()
+ if len(sel) == 1:
+ if isinstance(sel[0], OnCurvePointItem) and (event.scenePos() - sel[0].pos()).manhattanLength() >= 2:
+ mouseGrabberItem = scene.mouseGrabberItem()
+ # If we drawn an onCurve w Shift and we're not touching the item, we wont have
+ # a mouse grabber (anyways), return early here.
+ if mouseGrabberItem is None:
+ event.accept()
+ return
+ scene._blocked = True
+ if len(sel[0]._contour) < 2:
+ # release current onCurve
+ scene.sendEvent(sel[0], QEvent(QEvent.MouseButtonRelease))
+ mouseGrabberItem.ungrabMouse()
+ sel[0].setSelected(False)
+ # append an offCurve point and start moving it
+ sel[0]._contour.addPoint((event.scenePos().x(), event.scenePos().y()))
+ nextCP = sel[0].childItems()[3]
+ nextCP.setVisible(True)
+ nextCP._needsUngrab = True
+ #nextCP.setSelected(True)
+ scene.sendEvent(nextCP, QEvent(QEvent.MouseButtonPress))
+ nextCP.grabMouse()
+ else:
+ # release current onCurve, delete from contour
+ scene.sendEvent(sel[0], QEvent(QEvent.MouseButtonRelease))
+ mouseGrabberItem.ungrabMouse()
+ sel[0].setSelected(False)
+
+ # construct a curve segment to the current point if there is not one
+ onCurve = sel[0]._point
+ if not onCurve.segmentType == "curve":
+ # remove the last onCurve
+ sel[0]._contour.removePoint(onCurve)
+ prev = sel[0]._contour[-1]
+ scene.getItemForPoint(prev).childItems()[3].setVisible(True)
+ # add a zero-length offCurve to the previous point
+ sel[0]._contour.addPoint((prev.x, prev.y))
+ # add prevOffCurve and activate
+ sel[0]._contour.addPoint((sel[0].x(), sel[0].y()))
+ sel[0].childItems()[1].setVisible(True)
+ # add back current onCurve as a curve point
+ sel[0]._contour.addPoint((onCurve.x, onCurve.y), "curve")
+ sel[0]._point = sel[0]._contour[-1]
+ if not QApplication.keyboardModifiers() & Qt.AltModifier:
+ sel[0]._point.smooth = True
+ sel[0]._isSmooth = True
+ sel[0].setPointPath()
+ if sel[0].getPointIndex() == 0:
+ # we're probably dealing with the first point that we looped.
+ # preserve nextCP whatsoever.
+ lineObj = HandleLineItem(0, 0, 0, 0, sel[0])
+ nextCP = OffCurvePointItem(0, 0, sel[0])
+ # now we have l1, p1, l2, p2, l3, p3
+ l2 = sel[0].childItems()[2]
+ lineObj.stackBefore(l2)
+ nextCP.stackBefore(l2)
+ else:
+ # add last offCurve
+ sel[0]._contour.addPoint((sel[0].x(), sel[0].y()))
+ nextCP = sel[0].childItems()[3]
+ nextCP._needsUngrab = True
+ nextCP.setVisible(True)
+ #nextCP.setSelected(True)
+ scene.sendEvent(nextCP, QEvent(QEvent.MouseButtonPress))
+ nextCP.grabMouse()
+ scene._blocked = False
+ scene._editing = None
+ QGraphicsScene.mouseMoveEvent(scene, event)
+ else:
+ # eat the event
+ event.accept()
+
+ def mouseReleaseEvent(self, scene, event):
+ # cleanup extra point elements if we dealt w curved first point
+ touched = scene.itemAt(event.scenePos(), scene.views()[0].transform())
+ if touched and isinstance(touched, OffCurvePointItem):
+ onCurve = touched.parentItem()
+ children = onCurve.childItems()
+ if len(children) > 4:
+ # l1, p1, l3, p3, l2, p2
+ children[3].prepareGeometryChange()
+ scene.removeItem(children[3])
+ children[2].prepareGeometryChange()
+ scene.removeItem(children[2])
+
+ onCurve._isSmooth = False
+ onCurve.setPointPath()
+ onCurve._point.smooth = False
diff --git a/Lib/defconQt/tools/rulerTool.py b/Lib/defconQt/tools/rulerTool.py
new file mode 100644
index 0000000..d024128
--- /dev/null
+++ b/Lib/defconQt/tools/rulerTool.py
@@ -0,0 +1,96 @@
+from defconQt.tools.baseTool import BaseTool
+from defconQt.glyphView import OffCurvePointItem, OnCurvePointItem
+from PyQt5.QtGui import QIcon, QPainterPath
+from PyQt5.QtWidgets import QGraphicsScene, QGraphicsSimpleTextItem, QGraphicsView
+
+class RulerTool(BaseTool):
+ def name(self):
+ return "Ruler"
+
+ def icon(self):
+ return QIcon("defconQt/resources/ruler.svg")
+
+ def doesUndo(self):
+ return False
+
+ # events
+
+ def mousePressEvent(self, scene, event):
+ touched = scene.itemAt(event.scenePos(), scene.views()[0].transform())
+ if touched is not None and isinstance(touched, OnCurvePointItem) or \
+ isinstance(touched, OffCurvePointItem):
+ x, y = touched.scenePos().x(), touched.scenePos().y()
+ else:
+ x, y = event.scenePos().x(), event.scenePos().y()
+ if scene._integerPlane:
+ x, y = round(x), round(y)
+ if scene._cachedRuler is not None:
+ scene.removeItem(scene._cachedRuler)
+ scene._cachedRuler = None
+ path = QPainterPath()
+ path.moveTo(x, y)
+ path.lineTo(x+1, y)
+ path.lineTo(x+1, y+1)
+ path.closeSubpath()
+ scene._rulerObject = scene.addPath(path)
+ textItem = QGraphicsSimpleTextItem("0", scene._rulerObject)
+ font = scene.font()
+ font.setPointSize(9)
+ textItem.setFont(font)
+ textItem.setFlag(QGraphicsItem.ItemIgnoresTransformations)
+ textItem.setPos(x, y + textItem.boundingRect().height())
+ event.accept()
+
+ def mouseMoveEvent(self, scene, event):
+ # XXX: shouldnt have to do this, it seems mouseTracking is wrongly activated
+ if scene._rulerObject is None: return
+ touched = scene.itemAt(event.scenePos(), scene.views()[0].transform())
+ if touched is not None and isinstance(touched, OnCurvePointItem) or \
+ isinstance(touched, OffCurvePointItem):
+ x, y = touched.scenePos().x(), touched.scenePos().y()
+ else:
+ # TODO: 45deg clamp w ShiftModifier
+ # maybe make a function for that + other occurences...
+ x, y = event.scenePos().x(), event.scenePos().y()
+ if scene._integerPlane:
+ x, y = round(x), round(y)
+ path = scene._rulerObject.path()
+ baseElem = path.elementAt(0)
+ path.setElementPositionAt(1, x, baseElem.y)
+ path.setElementPositionAt(2, x, y)
+ path.setElementPositionAt(3, baseElem.x, baseElem.y)
+ scene._rulerObject.setPath(path)
+ textItem = scene._rulerObject.childItems()[0]
+ line = QLineF(baseElem.x, baseElem.y, x, y)
+ l = line.length()
+ # XXX: angle() doesnt go by trigonometric direction. Weird.
+ # TODO: maybe split in positive/negative 180s (ff)
+ a = 360 - line.angle()
+ line.setP2(QPointF(x, baseElem.y))
+ h = line.length()
+ line.setP1(QPointF(x, y))
+ v = line.length()
+ text = "%d\n↔ %d\n↕ %d\nα %dº" % (l, h, v, a)
+ textItem.setText(text)
+ dx = x - baseElem.x
+ if dx >= 0: px = x
+ else: px = x - textItem.boundingRect().width()
+ dy = y - baseElem.y
+ if dy > 0: py = baseElem.y
+ else: py = baseElem.y + textItem.boundingRect().height()
+ textItem.setPos(px, py)
+ event.accept()
+
+ def mouseReleaseEvent(self, scene, event):
+ textItem = scene._rulerObject.childItems()[0]
+ if textItem.text() == "0":
+ # delete no-op ruler
+ scene.removeItem(scene._rulerObject)
+ scene._rulerObject = None
+ else:
+ scene._cachedRuler = scene._rulerObject
+ scene._rulerObject = None
+ event.accept()
+
+ def mouseDoubleClickEvent(self, scene, event):
+ pass
diff --git a/Lib/defconQt/tools/selectionTool.py b/Lib/defconQt/tools/selectionTool.py
new file mode 100644
index 0000000..8896f71
--- /dev/null
+++ b/Lib/defconQt/tools/selectionTool.py
@@ -0,0 +1,56 @@
+from defconQt.tools.baseTool import BaseTool
+from defconQt.glyphView import OffCurvePointItem, OnCurvePointItem
+from PyQt5.QtGui import QIcon
+from PyQt5.QtWidgets import QGraphicsScene, QGraphicsView
+
+class SelectionTool(BaseTool):
+ def name(self):
+ return "Selection"
+
+ def icon(self):
+ return QIcon("defconQt/resources/cursor.svg")
+
+ def dragMode(self):
+ return QGraphicsView.RubberBandDrag
+
+ # events
+
+ def mousePressEvent(self, scene, event):
+ QGraphicsScene.mousePressEvent(scene, event)
+
+ def mouseMoveEvent(self, scene, event):
+ items = scene.items(event.scenePos())
+ # XXX: we must cater w mouse tracking
+ # we dont need isSelected() once its rid
+ mouseGrabberItem = scene.mouseGrabberItem()
+ """
+ if len(items) > 1 and isinstance(items[0], OnCurvePointItem) and \
+ isinstance(items[1], OffCurvePointItem) and items[1].isSelected():
+ items[1].setPos(0, 0)
+ """
+ if isinstance(mouseGrabberItem, OffCurvePointItem):
+ for item in items:
+ if isinstance(item, OnCurvePointItem):
+ print("CLAMP!")
+ mouseGrabberItem.setPos(0, 0)
+ event.accept()
+ return
+ QGraphicsScene.mouseMoveEvent(scene, event)
+
+ def mouseReleaseEvent(self, scene, event):
+ scene._editing = False
+ # cleanup extra point elements if we dealt w curved first point
+ touched = scene.itemAt(event.scenePos(), scene.views()[0].transform())
+ if touched and isinstance(touched, OffCurvePointItem):
+ onCurve = touched.parentItem()
+ children = onCurve.childItems()
+ if len(children) > 4:
+ # l1, p1, l3, p3, l2, p2
+ children[3].prepareGeometryChange()
+ scene.removeItem(children[3])
+ children[2].prepareGeometryChange()
+ scene.removeItem(children[2])
+
+ onCurve._isSmooth = False
+ onCurve.setPointPath()
+ onCurve._point.smooth = False
diff --git a/Lib/defconQt/util/bezierMath.py b/Lib/defconQt/util/bezierMath.py
new file mode 100644
index 0000000..c1dc15c
--- /dev/null
+++ b/Lib/defconQt/util/bezierMath.py
@@ -0,0 +1,48 @@
+from fontTools.misc import bezierTools
+
+"""
+Computes intersection between a cubic spline and a line segment.
+Adapted from: https://www.particleincell.com/2013/cubic-line-intersection/
+
+Takes four defcon points describing curve and four scalars describing line
+parameters.
+"""
+def computeIntersections(scene, p1, p2, p3, p4, x1, y1, x2, y2):
+ bx, by = x1 - x2, y2 - y1
+ m = x1*(y1-y2) + y1*(x2-x1)
+ a, b, c, d = bezierTools.calcCubicParameters((p1.x, p1.y), (p2.x, p2.y),
+ (p3.x, p3.y), (p4.x, p4.y))
+
+ pc0 = by*a[0] + bx*a[1]
+ pc1 = by*b[0] + bx*b[1]
+ pc2 = by*c[0] + bx*c[1]
+ pc3 = by*d[0] + bx*d[1] + m
+ r = bezierTools.solveCubic(pc0, pc1, pc2, pc3)
+
+ sol = []
+ for t in r:
+ s0 = a[0]*t**3 + b[0]*t**2 + c[0]*t + d[0]
+ s1 = a[1]*t**3 + b[1]*t**2 + c[1]*t + d[1]
+ if (x2-x1) != 0:
+ s = (s0-x1) / (x2-x1)
+ else:
+ s = (s1-y1) / (y2-y1)
+ if not (t < 0 or t > 1 or s < 0 or s > 1):
+ sol.append((s0, s1, t))
+ return sol
+
+"""
+G. Bach, http://stackoverflow.com/a/1968345
+"""
+def lineIntersection(scene, x1, y1, x2, y2, x3, y3, x4, y4):
+ Bx_Ax = x2 - x1
+ By_Ay = y2 - y1
+ Dx_Cx = x4 - x3
+ Dy_Cy = y4 - y3
+ determinant = (-Dx_Cx * By_Ay + Bx_Ax * Dy_Cy)
+ if abs(determinant) < 1e-20: return []
+ s = (-By_Ay * (x1 - x3) + Bx_Ax * (y1 - y3)) / determinant
+ t = ( Dx_Cx * (y1 - y3) - Dy_Cy * (x1 - x3)) / determinant
+ if s >= 0 and s <= 1 and t >= 0 and t <= 1:
+ return [(x1 + (t * Bx_Ax), y1 + (t * By_Ay), t)]
+ return []
diff --git a/setup.py b/setup.py
index c0f3249..5323133 100644
--- a/setup.py
+++ b/setup.py
@@ -44,6 +44,7 @@ setup(name="defconQt",
"defconQt.objects",
"defconQt.pens",
"defconQt.representationFactories",
+ "defconQt.tools",
],
package_dir={"":"Lib"}
)
\ No newline at end of file
--
2.1.1
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment