#!/usr/bin/env python3 import PyQt5.QtWidgets as QtWidgets import PyQt5.QtCore as QtCore import PyQt5.QtGui as QtGui import xasy2asy as xasy2asy import xasyUtils as xasyUtils import Widg_editBezier as Web import InplaceAddObj import math class CurrentlySelctedType: none = -1 node = 0 ctrlPoint = 1 class InteractiveBezierEditor(InplaceAddObj.InplaceObjProcess): editAccepted = QtCore.pyqtSignal() editRejected = QtCore.pyqtSignal() def __init__(self, parent: QtCore.QObject, obj: xasy2asy.xasyDrawnItem, info: dict={}): super().__init__(parent) self.info = info self.asyPathBackup = xasy2asy.asyPath.fromPath(obj.path) self.asyPath = obj.path self.curveMode = self.asyPath.containsCurve assert isinstance(self.asyPath, xasy2asy.asyPath) self.transf = obj.transfKeymap[obj.transfKey][0] self._active = True self.currentSelMode = None # (Node index, Node subindex for ) self.currentSelIndex = (None, 0) self.nodeSelRects = [] self.ctrlSelRects = [] self.setSelectionBoundaries() self.lastSelPoint = None self.preCtrlOffset = None self.postCtrlOffset = None self.inTransformMode = False self.opt = None self.obj = obj self.prosectiveNodes = [] self.prospectiveCtrlPts = [] #The magnification isn't being set. Here I'm manually setting it to be the square root of the determinant. self.info['magnification'] = math.sqrt(abs(self.transf.xx * self.transf.yy - self.transf.xy * self.transf.yx)) def setSelectionBoundaries(self): self.nodeSelRects = self.handleNodeSelectionBounds() if self.curveMode: self.ctrlSelRects = self.handleCtrlSelectionBoundaries() def handleNodeSelectionBounds(self): nodeSelectionBoundaries = [] for node in self.asyPath.nodeSet: if node == 'cycle': nodeSelectionBoundaries.append(None) continue selEpsilon = 6/self.info['magnification'] newRect = QtCore.QRectF(0, 0, 2 * selEpsilon, 2 * selEpsilon) x, y = self.transf * node x = int(round(x)) y = int(round(y)) newRect.moveCenter(QtCore.QPoint(x, y)) nodeSelectionBoundaries.append(newRect) return nodeSelectionBoundaries def handleCtrlSelectionBoundaries(self): ctrlPointSelBoundaries = [] for nodes in self.asyPath.controlSet: nodea, nodeb = nodes selEpsilon = 6/self.info['magnification'] newRect = QtCore.QRectF(0, 0, 2 * selEpsilon, 2 * selEpsilon) newRectb = QtCore.QRectF(0, 0, 2 * selEpsilon, 2 * selEpsilon) x, y = self.transf * nodea x2, y2 = self.transf * nodeb x = int(round(x)) y = int(round(y)) x2 = int(round(x2)) y2 = int(round(y2)) newRect.moveCenter(QtCore.QPoint(x, y)) newRectb.moveCenter(QtCore.QPoint(x2, y2)) ctrlPointSelBoundaries.append((newRect, newRectb)) return ctrlPointSelBoundaries def postDrawPreview(self, canvas: QtGui.QPainter): assert canvas.isActive() dashedPen = QtGui.QPen(QtCore.Qt.DashLine) dashedPen.setCosmetic(True) # draw the base points canvas.save() canvas.setWorldTransform(self.transf.toQTransform(), True) epsilonSize = 6/self.info['magnification'] if self.info['autoRecompute'] or not self.curveMode: ctrlPtsColor = 'gray' else: ctrlPtsColor = 'red' canvas.setPen(dashedPen) canvas.drawPath(self.asyPath.toQPainterPath()) nodePen = QtGui.QPen(QtGui.QColor('blue')) nodePen.setCosmetic(True) ctlPtsPen = QtGui.QPen(QtGui.QColor(ctrlPtsColor)) ctlPtsPen.setCosmetic(True) for index in range(len(self.asyPath.nodeSet)): point = self.asyPath.nodeSet[index] if point != 'cycle': basePoint = QtCore.QPointF(point[0], point[1]) canvas.setPen(nodePen) canvas.drawEllipse(basePoint, epsilonSize, epsilonSize) else: point = self.asyPath.nodeSet[0] basePoint = QtCore.QPointF(point[0], point[1]) if self.curveMode: if index != 0: canvas.setPen(ctlPtsPen) postCtrolSet = self.asyPath.controlSet[index - 1][1] postCtrlPoint = QtCore.QPointF(postCtrolSet[0], postCtrolSet[1]) canvas.drawEllipse(postCtrlPoint, epsilonSize, epsilonSize) canvas.setPen(dashedPen) canvas.drawLine(basePoint, postCtrlPoint) if index != len(self.asyPath.nodeSet) - 1: canvas.setPen(ctlPtsPen) preCtrlSet = self.asyPath.controlSet[index][0] preCtrlPoint = QtCore.QPointF(preCtrlSet[0], preCtrlSet[1]) canvas.drawEllipse(preCtrlPoint, epsilonSize, epsilonSize) canvas.setPen(dashedPen) canvas.drawLine(basePoint, preCtrlPoint) canvas.restore() def getPreAndPostCtrlPts(self, index): isCycle = self.asyPath.nodeSet[-1] == 'cycle' if index == 0 and not isCycle: preCtrl = None else: preCtrl = self.asyPath.controlSet[index - 1][1] if index == len(self.asyPath.nodeSet) - 1 and not isCycle: postCtrl = None else: postCtrl = self.asyPath.controlSet[index % (len(self.asyPath.nodeSet) - 1)][0] return preCtrl, postCtrl def findLinkingNode(self, index, subindex): """index and subindex are of the control points list.""" if subindex == 0: return index else: if self.asyPath.nodeSet[index + 1] == 'cycle': return 0 else: return index + 1 def resetObj(self): self.asyPath.setInfo(self.asyPathBackup) self.setSelectionBoundaries() def mouseDown(self, pos, info, mouseEvent: QtGui.QMouseEvent=None): self.lastSelPoint = pos if self.inTransformMode: return if self.prosectiveNodes and not self.inTransformMode: self.currentSelMode = CurrentlySelctedType.node self.currentSelIndex = (self.prosectiveNodes[0], 0) self.inTransformMode = True self.parentNodeIndex = self.currentSelIndex[0] elif self.prospectiveCtrlPts and not self.inTransformMode: self.currentSelMode = CurrentlySelctedType.ctrlPoint self.currentSelIndex = self.prospectiveCtrlPts[0] self.inTransformMode = True self.parentNodeIndex = self.findLinkingNode(*self.currentSelIndex) if self.inTransformMode: parentNode = self.asyPath.nodeSet[self.parentNodeIndex] # find the offset of each control point to the node if not self.curveMode: return preCtrl, postCtrl = self.getPreAndPostCtrlPts(self.parentNodeIndex) if parentNode == 'cycle': parentNode = self.asyPath.nodeSet[0] self.parentNodeIndex = 0 if preCtrl is not None: self.preCtrlOffset = xasyUtils.funcOnList( preCtrl, parentNode, lambda a, b: a - b) else: self.preCtrlOffset = None if postCtrl is not None: self.postCtrlOffset = xasyUtils.funcOnList( postCtrl, parentNode, lambda a, b: a - b) else: self.postCtrlOffset = None def mouseMove(self, pos, event: QtGui.QMouseEvent): if self.currentSelMode is None and not self.inTransformMode: # in this case, search for prosective nodes. prospectiveNodes = [] prospectiveCtrlpts = [] for i in range(len(self.nodeSelRects)): rect = self.nodeSelRects[i] if rect is None: continue if rect.contains(pos): prospectiveNodes.append(i) self.prosectiveNodes = prospectiveNodes if not self.info['autoRecompute'] and self.curveMode: for i in range(len(self.ctrlSelRects)): recta, rectb = self.ctrlSelRects[i] if recta.contains(pos): prospectiveCtrlpts.append((i, 0)) if rectb.contains(pos): prospectiveCtrlpts.append((i, 1)) self.prospectiveCtrlPts = prospectiveCtrlpts else: self.prospectiveCtrlPts = [] if self.inTransformMode: index, subindex = self.currentSelIndex newNode = (self.transf.inverted().toQTransform().map(pos.x(), pos.y())) if self.currentSelMode == CurrentlySelctedType.node: # static throughout the moving if self.asyPath.nodeSet[index] == 'cycle': return self.asyPath.setNode(index, newNode) # if also move node: if self.curveMode: checkPre, checkPost = self.getPreAndPostCtrlPts(index) if 1 == 1: # TODO: Replace this with an option to also move control pts. if checkPre is not None: self.asyPath.controlSet[index - 1][1] = xasyUtils.funcOnList( newNode, self.preCtrlOffset, lambda a, b: a + b ) if checkPost is not None: self.asyPath.controlSet[index][0] = xasyUtils.funcOnList( newNode, self.postCtrlOffset, lambda a, b: a + b ) if self.info['autoRecompute']: self.quickRecalculateCtrls() elif self.currentSelMode == CurrentlySelctedType.ctrlPoint and self.curveMode: self.asyPath.controlSet[index][subindex] = newNode parentNode = self.asyPath.nodeSet[self.parentNodeIndex] if parentNode == 'cycle': parentNode = self.asyPath.nodeSet[0] isCycle = True else: isCycle = False if self.parentNodeIndex == 0 and self.asyPath.nodeSet[-1] == 'cycle': isCycle = True rawNewNode = xasyUtils.funcOnList(newNode, parentNode, lambda a, b: a - b) rawAngle = math.atan2(rawNewNode[1], rawNewNode[0]) newNorm = xasyUtils.twonorm(rawNewNode) if self.info['editBezierlockMode'] >= Web.LockMode.angleLock: otherIndex = 1 - subindex # 1 if 0, 0 otherwise. if otherIndex == 0: if index < (len(self.asyPath.controlSet) - 1) or isCycle: newIndex = 0 if isCycle else index + 1 oldOtherCtrlPnt = xasyUtils.funcOnList( self.asyPath.controlSet[newIndex][0], parentNode, lambda a, b: a - b) if self.info['editBezierlockMode'] >= Web.LockMode.angleAndScaleLock: rawNorm = newNorm else: rawNorm = xasyUtils.twonorm(oldOtherCtrlPnt) newPnt = (rawNorm * math.cos(rawAngle + math.pi), rawNorm * math.sin(rawAngle + math.pi)) self.asyPath.controlSet[newIndex][0] = xasyUtils.funcOnList( newPnt, parentNode, lambda a, b: a + b) else: if index > 0 or isCycle: newIndex = -1 if isCycle else index - 1 oldOtherCtrlPnt = xasyUtils.funcOnList( self.asyPath.controlSet[newIndex][1], parentNode, lambda a, b: a - b) if self.info['editBezierlockMode'] >= Web.LockMode.angleAndScaleLock: rawNorm = newNorm else: rawNorm = xasyUtils.twonorm(oldOtherCtrlPnt) newPnt = (rawNorm * math.cos(rawAngle + math.pi), rawNorm * math.sin(rawAngle + math.pi)) self.asyPath.controlSet[newIndex][1] = xasyUtils.funcOnList( newPnt, parentNode, lambda a, b: a + b) def recalculateCtrls(self): self.quickRecalculateCtrls() self.setSelectionBoundaries() def quickRecalculateCtrls(self): self.asyPath.controlSet.clear() self.asyPath.computeControls() def mouseRelease(self): if self.inTransformMode: self.inTransformMode = False self.currentSelMode = None self.setSelectionBoundaries() def forceFinalize(self): self.objectUpdated.emit() def createOptWidget(self, info): self.opt = Web.Widg_editBezier(self.info, self.curveMode) self.opt.ui.btnOk.clicked.connect(self.editAccepted) self.opt.ui.btnCancel.clicked.connect(self.editRejected) self.opt.ui.btnForceRecompute.clicked.connect(self.recalculateCtrls) return self.opt def getObject(self): pass def getXasyObject(self): pass