lang/pysideex/ SingleCubicCurveEditor001


It's not perfect as if you drag an endpoint, the control point nearest to it doesn't move. But it illustrates the basic idea.

#
# Aim: single cubic segment with draggable handles
#
import sys
from PySide6.QtCore import *
from PySide6.QtGui import *
from PySide6.QtWidgets import *
from PySide6.QtNetwork import *

class Handle:
  def __init__(self,x=0,y=0,r=10,shape="square",color=None):
    self.x = x
    self.y = y
    self.r = r
    self.shape = shape
    if color is not None:
      self.color = color
    else:
      self.color = QColor.fromRgb(0,0,0)
  def hit(self,x,y):
    if self.shape == "square":
      if (x < self.x - self.r) or (x > self.x + self.r) or (y < self.y - self.r ) or (y > self.y + self.r):
        return False
      return True
    elif self.shape == "circle":
      dx = x - self.x
      dy = y - self.y
      r2 = self.r * self.r
      dist2 = dx*dx + dy*dy
      return dist2 <= r2
    else:
      print(f"Invalid shape {self.shape}")
      return false
  def mouseMove(self,x,y):
    self.x = x
    self.y = y
  def paint(self,painter):
    x = self.x
    y = self.y
    r = self.r
    rect = QRect(x-r,y-r,2*r,2*r)
    path = QPainterPath()
    if self.shape == "square":
      path.addRect(rect)
    elif self.shape == "circle":
      path.addEllipse(rect)
    else:
      print(f"Invalid shape {self.shape}")
      return
    painter.save()
    painter.setBrush(self.color)
    painter.setPen(Qt.NoPen)
    painter.drawPath(path)
    painter.restore()

class Cubic:
  def __init__(self,x0,y0,xa,ya,xb,yb,x1,y1,color=None,width=5):
    self.p0 = Handle(x0,y0,10,"square",QColorConstants.Red)
    self.p1 = Handle(x1,y1,10,"square",QColorConstants.Green)
    self.pa = Handle(xa,ya,10,"circle",QColorConstants.Blue)
    self.pb = Handle(xb,yb,10,"circle",QColorConstants.Magenta)
    self.handles = [self.pa,self.pb,self.p0,self.p1]
    if color is not None:
      self.color = color
    else:
      self.color = QColor.fromRgb(0,0,0)
    self.width = width
  def hit(self,x,y):
    for handle in self.handles:
      if handle.hit(x,y):
        return handle
    return None
  def paint(self,painter):
    pa,pb,p0,p1 = self.handles
    path = QPainterPath()
    path.moveTo(p0.x,p0.y)
    path.cubicTo(pa.x,pa.y,pb.x,pb.y,p1.x,p1.y)
    painter.save()
    painter.setPen(QPen(self.color,self.width))
    painter.setBrush(Qt.NoBrush)
    painter.drawPath(path)
    painter.setPen(QPen(QColorConstants.Black,1))
    path = QPainterPath()
    path.moveTo(p0.x,p0.y)
    path.lineTo(pa.x,pa.y)
    painter.drawPath(path)
    path = QPainterPath()
    path.moveTo(p1.x,p1.y)
    path.lineTo(pb.x,pb.y)
    painter.drawPath(path)
    for handle in self.handles:
      handle.paint(painter)
    painter.restore()

class MyWidget(QWidget):
  def __init__(self,*xs,**kw):
    super().__init__(*xs,**kw)
    self.cubic = Cubic(50,50,150,50,150,150,250,150)
    self.sel = None
  def paintEvent(self,event):
    with QPainter(self) as painter:
      self.cubic.paint(painter)
  def mousePressEvent(self,event):
    pos = event.position().toPoint()
    x,y = pos.x(),pos.y()
    if handle := self.cubic.hit(x,y):
      self.sel = handle
    return super().mousePressEvent(event)
  def mouseReleaseEvent(self,event):
    self.sel = None
    return super().mouseReleaseEvent(event)
  def mouseMoveEvent(self,event):
    if self.sel is not None:
      pos = event.position().toPoint()
      x,y = pos.x(),pos.y()
      self.sel.mouseMove(x,y)
      self.update()
    return super().mouseMoveEvent(event)

app = QApplication(sys.argv)
win = QMainWindow()
w = MyWidget()
win.setCentralWidget(w)
win.setFixedSize(640,512)
win.show()
exit(app.exec())