Я пытаюсь использовать инструмент «Перо» в своем приложении pyqt6. Я пытаюсь сделать так, как работает PenTool от Inkscape. На данный момент я пытаюсь достичь режима «B-Spline».
Проблема в том, что моя кривая не остается такой, какой она рисуется при движении мыши. Он как бы прыгает/перемещается после размещения третьей и последующих точек (со степенью = 2). Если вы запустите приведенный ниже код, вы увидите, что после размещения точек без перемещения мыши все выглядит нормально. Но когда вы перемещаете мышь, уже нарисованный путь обновляется/смещается/перемещается или что-то в этом роде.
Кривая Inkscape ведет себя не так. Он остается постоянным между щелчками и движениями мыши.
Пожалуйста, запустите этот код, чтобы увидеть, что я пытаюсь объяснить.
import sys
from PyQt6.QtWidgets import (
QApplication, QMainWindow, QGraphicsView, QGraphicsScene, QWidget,
QVBoxLayout, QPushButton, QHBoxLayout, QGraphicsLineItem, QGraphicsPathItem, QGraphicsEllipseItem
)
from PyQt6.QtCore import Qt, QObject, pyqtSignal, QPointF
from PyQt6.QtGui import QPainterPath, QBrush, QPen, QColor
import numpy as np
import scipy.interpolate as si
HANDLE_RADIUS = 4.0
class DrawSampler:
"""
Holds a stable dense evaluation grid used while drawing.
- dense_N: number of dense parameter samples (e.g. 512..2048). Higher = smoother while dragging.
- display_every: show every Nth dense sample (use >1 to reduce drawn points for performance).
- domain_max: parameter domain max when grid created (set by init_grid)
"""
def __init__(self, dense_N=1024, display_every=1):
self.dense_N = int(max(128, dense_N))
self.display_every = max(1, int(display_every))
self.u_dense = None
self.domain_max = None
def init_grid(self, domain_max):
# create a fixed dense grid over [0, domain_max]
self.domain_max = float(domain_max)
self.u_dense = np.linspace(0.0, float(domain_max), self.dense_N)
class PenTool(QObject):
class _PenToolNotifier(QObject):
pathFinished = pyqtSignal(QPainterPath, object)
def __init__(self, scene, parent=None):
super().__init__(parent)
self._notifier = self._PenToolNotifier()
self.scene = scene
self.view = None
self._draw_sampler = DrawSampler(dense_N=1024, display_every=1)
# Modes: 'bezier', 'spline', 'spiro', 'polyline', 'paraxial'
self._mode = 'spline'
# Drawing state
self.drawing = False
self.path = QPainterPath()
self.path_item = None
self.handles = []
self.anchor_points = [] # list of dicts: { 'pt': QPointF, 'in': QPointF or None, 'out': QPointF or None }
# Appearance
self.pen = QPen(Qt.GlobalColor.black, 1.5)
self.brush = QBrush(Qt.GlobalColor.transparent)
self.handle_pen = QPen(Qt.GlobalColor.darkGray)
self.handle_brush = QBrush(Qt.GlobalColor.white)
# Interaction bookkeeping
self._mouse_pressed = False
self._dragging = False
self._drag_start_pos = None
self._last_pos = QPointF()
# Preview items
self._preview_poly_item = None
self._preview_ctrl_items = []
# pyspiro wrapper lazy
self._pyspiro_available = None
# ----------------- public API --------------------------------------
def set_mode(self, mode_name: str):
if mode_name not in ('bezier', 'spline', 'spiro', 'polyline', 'paraxial'):
raise ValueError("invalid mode")
self._mode = mode_name
def activate(self, view):
self.view = view
view.setDragMode(QGraphicsView.DragMode.NoDrag)
view.viewport().installEventFilter(self)
view.setCursor(Qt.CursorShape.CrossCursor)
def deactivate(self):
if self.view:
try:
self.view.viewport().removeEventFilter(self)
except Exception as e:
print('error: ', e)
self.view.unsetCursor()
self.view = None
self._clear_temp()
# ----------------- internals ---------------------------------------
def _clear_temp(self):
if self.path_item:
try:
self.scene.removeItem(self.path_item)
except Exception as e:
print('error: ', e)
self.path_item = None
if self._preview_poly_item:
try:
self.scene.removeItem(self._preview_poly_item)
except Exception as e:
print('error: ', e)
self._preview_poly_item = None
for h in self.handles:
try:
self.scene.removeItem(h)
except Exception as e:
print('error: ', e)
self.handles.clear()
for l in self._preview_ctrl_items:
try:
self.scene.removeItem(l)
except Exception as e:
print('error: ', e)
self._preview_ctrl_items.clear()
self.anchor_points.clear()
self.path = QPainterPath()
self.drawing = False
def eventFilter(self, obj, event):
from PyQt6.QtGui import QMouseEvent, QKeyEvent
evtype = event.type()
# Mouse press - left or right
if evtype == QMouseEvent.Type.MouseButtonPress:
if event.button() == Qt.MouseButton.LeftButton:
self._mouse_pressed = True
pos = self.view.mapToScene(event.position().toPoint())
self._on_mouse_press(pos, event.modifiers())
return True
if event.button() == Qt.MouseButton.RightButton:
# finalize on right click
if self.drawing:
self._finish_path()
return True
# Mouse move
if evtype == QMouseEvent.Type.MouseMove:
pos = self.view.mapToScene(event.position().toPoint())
self._on_mouse_move(pos, event.buttons(), event.modifiers())
return True
# Mouse release - right can also finalize on release if preferred
if evtype == QMouseEvent.Type.MouseButtonRelease:
if event.button() == Qt.MouseButton.LeftButton:
pos = self.view.mapToScene(event.position().toPoint())
self._mouse_pressed = False
self._on_mouse_release(pos, event.modifiers())
return True
if event.button() == Qt.MouseButton.RightButton:
# finalizing on release as well (mirrors press behavior)
if self.drawing:
self._finish_path()
return True
# Key press handling
if evtype == QKeyEvent.Type.KeyPress:
key = event.key()
if key in (Qt.Key.Key_Return, Qt.Key.Key_Enter):
if self.drawing:
self._finish_path()
return True
if key in (Qt.Key.Key_Backspace, Qt.Key.Key_Delete):
if self.drawing:
self._remove_last_point()
return True
if key == Qt.Key.Key_Escape:
if self.drawing:
self._cancel_path()
return True
return False
# ----------------- input handlers ---------------------------------
def _on_mouse_press(self, scene_pos: QPointF, modifiers):
self._drag_start_pos = QPointF(scene_pos)
self._last_pos = QPointF(scene_pos)
self._dragging = False
if not self.drawing:
# start new path
self.drawing = True
self.path = QPainterPath(scene_pos)
self.anchor_points = [{'pt': QPointF(scene_pos), 'in': None, 'out': None}]
self.path_item = QGraphicsPathItem(self.path)
self.path_item.setPen(self.pen)
self.path_item.setBrush(self.brush)
self.scene.addItem(self.path_item)
self._add_handle(scene_pos)
self._update_preview_visuals()
return
# Already drawing: behavior depends on mode
if self._mode == 'bezier':
self.anchor_points.append({'pt': QPointF(scene_pos), 'in': None, 'out': None})
self._add_handle(scene_pos)
self._rebuild_path()
self._update_preview_visuals()
elif self._mode == 'polyline':
self.anchor_points.append({'pt': QPointF(scene_pos), 'in': None, 'out': None})
self._add_handle(scene_pos)
self._rebuild_path()
self._update_preview_visuals()
elif self._mode == 'paraxial':
prev = self.anchor_points[-1]['pt']
if modifiers & Qt.KeyboardModifier.ShiftModifier:
new_pt = QPointF(scene_pos)
else:
dx = abs(scene_pos.x() - prev.x())
dy = abs(scene_pos.y() - prev.y())
if dx > dy:
new_pt = QPointF(scene_pos.x(), prev.y())
else:
new_pt = QPointF(prev.x(), scene_pos.y())
self.anchor_points.append({'pt': new_pt, 'in': None, 'out': None})
self._add_handle(new_pt)
self._rebuild_path()
self._update_preview_visuals()
elif self._mode in ('spline', 'spiro'):
# add raw anchor; smoothing applied on finish
self.anchor_points.append({'pt': QPointF(scene_pos), 'in': None, 'out': None})
self._add_handle(scene_pos)
self._rebuild_path()
self._update_preview_visuals()
def _on_mouse_move(self, scene_pos: QPointF, buttons, modifiers):
if not self.drawing:
return
if self._mode == 'bezier' and self._mouse_pressed:
delta = scene_pos - self._drag_start_pos
if (delta.manhattanLength() > 3):
self._dragging = True
idx = len(self.anchor_points) - 1
if idx >= 0:
last = self.anchor_points[idx]
last['out'] = QPointF(scene_pos)
if idx - 1 >= 0:
vec = last['out'] - last['pt']
last['in'] = last['pt'] - (vec * 0.5)
self._update_handle_visual(idx)
self._rebuild_path()
self._update_preview_visuals(mouse_pos=scene_pos)
return
# For other modes or when not dragging: show preview line to mouse
self._update_preview_visuals(mouse_pos=scene_pos)
def _on_mouse_release(self, scene_pos, modifiers):
if not self.drawing:
return
if not self._dragging:
# click without drag: check for closing in bezier/polyline modes
if len(self.anchor_points) >= 2:
first = self.anchor_points[0]['pt']
if (scene_pos - first).manhattanLength() < 8.0:
self.anchor_points[-1]['pt'] = QPointF(first)
self._rebuild_path(close=True)
self._finish_path()
return
self._last_pos = QPointF(scene_pos)
self._dragging = False
self._drag_start_pos = None
self._update_preview_visuals()
# ----------------- path building ----------------------------------
# ----------------- finish / cancel / remove ------------------------
def _finish_path(self):
if not self.drawing:
return
# If spline mode, convert anchor_points to smoothed path first
pts = [pt['pt'] for pt in self.anchor_points]
if self._mode == 'spline':
try:
p = self._build_cubic_uniform_bspline(pts,
sampler=self._draw_sampler,
finalize=False)
if p is None:
p = QPainterPath()
except Exception as e:
print('error: ', e)
p = QPainterPath()
self.path = p
if self.path_item:
self.path_item.setPath(self.path)
# finalize item
if self.path_item:
self.path_item.setFlag(QGraphicsPathItem.GraphicsItemFlag.ItemIsSelectable, True)
self.path_item.setFlag(QGraphicsPathItem.GraphicsItemFlag.ItemIsMovable, True)
finished_path = QPainterPath(self.path)
finished_item = self.path_item
self.path_item = None
self.handles.clear()
self.anchor_points.clear()
self.path = QPainterPath()
self.drawing = False
# remove preview visuals
if self._preview_poly_item:
try:
self.scene.removeItem(self._preview_poly_item)
except Exception as e:
print('error: ', e)
self._preview_poly_item = None
for l in self._preview_ctrl_items:
try:
self.scene.removeItem(l)
except Exception as e:
print('error: ', e)
self._preview_ctrl_items.clear()
self._notifier.pathFinished.emit(finished_path, finished_item)
def _rebuild_path(self, close=False):
if not self.anchor_points:
return
mode = self._mode
anchors = self.anchor_points
p = None
if mode == 'spline':
pts = [a['pt'] for a in anchors]
try:
p = self._build_cubic_uniform_bspline(pts, closed=close,
sampler=self._draw_sampler,
finalize=False)
except Exception as e:
print('error: ', e)
p = None
if p is None:
# fallback previews: polyline-like for simple modes, or bezier for handle mode
if mode in ('polyline', 'paraxial', 'spiro', 'spline'):
p = QPainterPath(anchors[0]['pt'])
for a in anchors[1:]:
p.lineTo(a['pt'])
if close:
p.closeSubpath()
else:
p = QPainterPath(anchors[0]['pt'])
prev = anchors[0]
for cur in anchors[1:]:
prev_out = prev.get('out')
cur_in = cur.get('in')
if prev_out and cur_in:
p.cubicTo(prev_out, cur_in, cur['pt'])
elif prev_out and not cur_in:
in_pt = QPointF(cur['pt'] - (prev_out - prev['pt']) * 0.5)
p.cubicTo(prev_out, in_pt, cur['pt'])
else:
p.lineTo(cur['pt'])
prev = cur
if close:
p.closeSubpath()
# only set when changed to reduce unnecessary repaints
if p != self.path:
self.path = p
if self.path_item:
self.path_item.setPath(p)
def _cancel_path(self):
self._clear_temp()
def _remove_last_point(self):
if not self.drawing:
return
if len(self.anchor_points) eps or abs(y - last[1]) > eps):
pts.append((float(x), float(y)))
last = (x, y)
if not pts:
return QPainterPath()
cv = np.asarray(pts, dtype=float)
orig_count = len(cv)
k = max(1, int(degree))
if orig_count 1:
path.closeSubpath()
print('path1 = ', path)
return path
periodic = bool(closed)
count = orig_count
if periodic:
factor, fraction = divmod(count + k + 1, count)
cv = np.concatenate((cv,) * factor + (cv[:fraction],), axis=0)
kv = np.arange(-k, len(cv) + k + 1)
max_param = len(cv) - k
else:
k = int(np.clip(k, 1, count - 1))
kv = np.clip(np.arange(count + k + 1) - k, 0, count - k)
max_param = count - k
try:
spl = si.BSpline(kv, cv, k)
except Exception:
path = QPainterPath()
path.moveTo(QPointF(float(cv[0,0]), float(cv[0,1])))
for x, y in cv[1:]:
path.lineTo(QPointF(float(x), float(y)))
if periodic and count > 1:
path.closeSubpath()
print('path2 = ', path)
return path
# If a sampler is provided and we are not finalizing, use its dense grid for fast preview.
if sampler is not None and not finalize:
# ensure sampler grid matches current param range
if sampler.u_dense is None or sampler.domain_max is None or sampler.domain_max != float(max_param):
sampler.init_grid(max_param)
try:
pts_dense = spl(sampler.u_dense)
except Exception:
path = QPainterPath()
path.moveTo(QPointF(float(cv[0,0]), float(cv[0,1])))
for x, y in cv[1:]:
path.lineTo(QPointF(float(x), float(y)))
if periodic and count > 1:
path.closeSubpath()
print('path3 = ', path)
return path
idx = np.arange(0, len(sampler.u_dense), sampler.display_every, dtype=int)
out = pts_dense[idx]
else:
# final path evaluation with requested samples per segment
S = max(1, int(samples_per_segment))
segments = count if periodic else (count - k)
vals = []
for i in range(int(segments)):
for j in range(S):
vals.append(i + (j / float(S)))
vals.append(float(max_param))
u = np.asarray(vals, dtype=float)
try:
out = spl(u)
except Exception:
path = QPainterPath()
path.moveTo(QPointF(float(cv[0,0]), float(cv[0,1])))
for x, y in cv[1:]:
path.lineTo(QPointF(float(x), float(y)))
if periodic and count > 1:
path.closeSubpath()
print('path4 = ', path)
return path
path = QPainterPath()
path.moveTo(QPointF(float(out[0,0]), float(out[0,1])))
for x, y in out[1:]:
path.lineTo(QPointF(float(x), float(y)))
if periodic:
path.closeSubpath()
return path
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("PenTool Demo")
self.resize(900, 700)
central = QWidget()
self.setCentralWidget(central)
vlayout = QVBoxLayout(central)
# toolbar with toggle button
toolbar = QWidget()
th = QHBoxLayout(toolbar)
th.setContentsMargins(0, 0, 0, 0)
self.toggle_btn = QPushButton("Pen Tool")
self.toggle_btn.setCheckable(True)
self.toggle_btn.toggled.connect(self.handlePenToggled)
th.addWidget(self.toggle_btn)
th.addStretch()
vlayout.addWidget(toolbar)
# graphics view
self.view = QGraphicsView()
self.view.viewport().setMouseTracking(True)
self.scene = QGraphicsScene(0, 0, 2000, 2000)
self.view.setScene(self.scene)
# enable antialiasing
self.view.setRenderHints(self.view.renderHints())
vlayout.addWidget(self.view)
# pen tool
self.pen_tool = PenTool(self.scene)
self.pen_tool._notifier.pathFinished.connect(self.handlePathFinished)
def handlePenToggled(self, checked):
if checked:
self.pen_tool.activate(self.view)
else:
self.pen_tool.deactivate()
def handlePathFinished(self, path, meta):
print("Path finished; metadata:", meta)
# Create a visible item for the finished path
# item = QGraphicsPathItem(path)
# pen = QPen(QColor(0, 100, 200), 2)
# pen.setCosmetic(True)
# item.setPen(pen)
# self.scene.addItem(item)
# # call sample user hook (you provided earlier signature)
# print("Path finished; element:", item)
def main():
app = QApplication(sys.argv)
w = MainWindow()
w.show()
sys.exit(app.exec())
if __name__ == "__main__":
main()
Подробнее здесь: https://stackoverflow.com/questions/798 ... with-mouse
PyQt – попытка нарисовать кривую B-сплайна с помощью мыши ⇐ Python
Программы на Python
-
Anonymous
1762614368
Anonymous
Я пытаюсь использовать инструмент «Перо» в своем приложении pyqt6. Я пытаюсь сделать так, как работает PenTool от Inkscape. На данный момент я пытаюсь достичь режима «B-Spline».
Проблема в том, что моя кривая не остается такой, какой она рисуется при движении мыши. Он как бы прыгает/перемещается после размещения третьей и последующих точек (со степенью = 2). Если вы запустите приведенный ниже код, вы увидите, что после размещения точек без перемещения мыши все выглядит нормально. Но когда вы перемещаете мышь, уже нарисованный путь обновляется/смещается/перемещается или что-то в этом роде.
Кривая Inkscape ведет себя не так. Он остается постоянным между щелчками и движениями мыши.
Пожалуйста, запустите этот код, чтобы увидеть, что я пытаюсь объяснить.
import sys
from PyQt6.QtWidgets import (
QApplication, QMainWindow, QGraphicsView, QGraphicsScene, QWidget,
QVBoxLayout, QPushButton, QHBoxLayout, QGraphicsLineItem, QGraphicsPathItem, QGraphicsEllipseItem
)
from PyQt6.QtCore import Qt, QObject, pyqtSignal, QPointF
from PyQt6.QtGui import QPainterPath, QBrush, QPen, QColor
import numpy as np
import scipy.interpolate as si
HANDLE_RADIUS = 4.0
class DrawSampler:
"""
Holds a stable dense evaluation grid used while drawing.
- dense_N: number of dense parameter samples (e.g. 512..2048). Higher = smoother while dragging.
- display_every: show every Nth dense sample (use >1 to reduce drawn points for performance).
- domain_max: parameter domain max when grid created (set by init_grid)
"""
def __init__(self, dense_N=1024, display_every=1):
self.dense_N = int(max(128, dense_N))
self.display_every = max(1, int(display_every))
self.u_dense = None
self.domain_max = None
def init_grid(self, domain_max):
# create a fixed dense grid over [0, domain_max]
self.domain_max = float(domain_max)
self.u_dense = np.linspace(0.0, float(domain_max), self.dense_N)
class PenTool(QObject):
class _PenToolNotifier(QObject):
pathFinished = pyqtSignal(QPainterPath, object)
def __init__(self, scene, parent=None):
super().__init__(parent)
self._notifier = self._PenToolNotifier()
self.scene = scene
self.view = None
self._draw_sampler = DrawSampler(dense_N=1024, display_every=1)
# Modes: 'bezier', 'spline', 'spiro', 'polyline', 'paraxial'
self._mode = 'spline'
# Drawing state
self.drawing = False
self.path = QPainterPath()
self.path_item = None
self.handles = []
self.anchor_points = [] # list of dicts: { 'pt': QPointF, 'in': QPointF or None, 'out': QPointF or None }
# Appearance
self.pen = QPen(Qt.GlobalColor.black, 1.5)
self.brush = QBrush(Qt.GlobalColor.transparent)
self.handle_pen = QPen(Qt.GlobalColor.darkGray)
self.handle_brush = QBrush(Qt.GlobalColor.white)
# Interaction bookkeeping
self._mouse_pressed = False
self._dragging = False
self._drag_start_pos = None
self._last_pos = QPointF()
# Preview items
self._preview_poly_item = None
self._preview_ctrl_items = []
# pyspiro wrapper lazy
self._pyspiro_available = None
# ----------------- public API --------------------------------------
def set_mode(self, mode_name: str):
if mode_name not in ('bezier', 'spline', 'spiro', 'polyline', 'paraxial'):
raise ValueError("invalid mode")
self._mode = mode_name
def activate(self, view):
self.view = view
view.setDragMode(QGraphicsView.DragMode.NoDrag)
view.viewport().installEventFilter(self)
view.setCursor(Qt.CursorShape.CrossCursor)
def deactivate(self):
if self.view:
try:
self.view.viewport().removeEventFilter(self)
except Exception as e:
print('error: ', e)
self.view.unsetCursor()
self.view = None
self._clear_temp()
# ----------------- internals ---------------------------------------
def _clear_temp(self):
if self.path_item:
try:
self.scene.removeItem(self.path_item)
except Exception as e:
print('error: ', e)
self.path_item = None
if self._preview_poly_item:
try:
self.scene.removeItem(self._preview_poly_item)
except Exception as e:
print('error: ', e)
self._preview_poly_item = None
for h in self.handles:
try:
self.scene.removeItem(h)
except Exception as e:
print('error: ', e)
self.handles.clear()
for l in self._preview_ctrl_items:
try:
self.scene.removeItem(l)
except Exception as e:
print('error: ', e)
self._preview_ctrl_items.clear()
self.anchor_points.clear()
self.path = QPainterPath()
self.drawing = False
def eventFilter(self, obj, event):
from PyQt6.QtGui import QMouseEvent, QKeyEvent
evtype = event.type()
# Mouse press - left or right
if evtype == QMouseEvent.Type.MouseButtonPress:
if event.button() == Qt.MouseButton.LeftButton:
self._mouse_pressed = True
pos = self.view.mapToScene(event.position().toPoint())
self._on_mouse_press(pos, event.modifiers())
return True
if event.button() == Qt.MouseButton.RightButton:
# finalize on right click
if self.drawing:
self._finish_path()
return True
# Mouse move
if evtype == QMouseEvent.Type.MouseMove:
pos = self.view.mapToScene(event.position().toPoint())
self._on_mouse_move(pos, event.buttons(), event.modifiers())
return True
# Mouse release - right can also finalize on release if preferred
if evtype == QMouseEvent.Type.MouseButtonRelease:
if event.button() == Qt.MouseButton.LeftButton:
pos = self.view.mapToScene(event.position().toPoint())
self._mouse_pressed = False
self._on_mouse_release(pos, event.modifiers())
return True
if event.button() == Qt.MouseButton.RightButton:
# finalizing on release as well (mirrors press behavior)
if self.drawing:
self._finish_path()
return True
# Key press handling
if evtype == QKeyEvent.Type.KeyPress:
key = event.key()
if key in (Qt.Key.Key_Return, Qt.Key.Key_Enter):
if self.drawing:
self._finish_path()
return True
if key in (Qt.Key.Key_Backspace, Qt.Key.Key_Delete):
if self.drawing:
self._remove_last_point()
return True
if key == Qt.Key.Key_Escape:
if self.drawing:
self._cancel_path()
return True
return False
# ----------------- input handlers ---------------------------------
def _on_mouse_press(self, scene_pos: QPointF, modifiers):
self._drag_start_pos = QPointF(scene_pos)
self._last_pos = QPointF(scene_pos)
self._dragging = False
if not self.drawing:
# start new path
self.drawing = True
self.path = QPainterPath(scene_pos)
self.anchor_points = [{'pt': QPointF(scene_pos), 'in': None, 'out': None}]
self.path_item = QGraphicsPathItem(self.path)
self.path_item.setPen(self.pen)
self.path_item.setBrush(self.brush)
self.scene.addItem(self.path_item)
self._add_handle(scene_pos)
self._update_preview_visuals()
return
# Already drawing: behavior depends on mode
if self._mode == 'bezier':
self.anchor_points.append({'pt': QPointF(scene_pos), 'in': None, 'out': None})
self._add_handle(scene_pos)
self._rebuild_path()
self._update_preview_visuals()
elif self._mode == 'polyline':
self.anchor_points.append({'pt': QPointF(scene_pos), 'in': None, 'out': None})
self._add_handle(scene_pos)
self._rebuild_path()
self._update_preview_visuals()
elif self._mode == 'paraxial':
prev = self.anchor_points[-1]['pt']
if modifiers & Qt.KeyboardModifier.ShiftModifier:
new_pt = QPointF(scene_pos)
else:
dx = abs(scene_pos.x() - prev.x())
dy = abs(scene_pos.y() - prev.y())
if dx > dy:
new_pt = QPointF(scene_pos.x(), prev.y())
else:
new_pt = QPointF(prev.x(), scene_pos.y())
self.anchor_points.append({'pt': new_pt, 'in': None, 'out': None})
self._add_handle(new_pt)
self._rebuild_path()
self._update_preview_visuals()
elif self._mode in ('spline', 'spiro'):
# add raw anchor; smoothing applied on finish
self.anchor_points.append({'pt': QPointF(scene_pos), 'in': None, 'out': None})
self._add_handle(scene_pos)
self._rebuild_path()
self._update_preview_visuals()
def _on_mouse_move(self, scene_pos: QPointF, buttons, modifiers):
if not self.drawing:
return
if self._mode == 'bezier' and self._mouse_pressed:
delta = scene_pos - self._drag_start_pos
if (delta.manhattanLength() > 3):
self._dragging = True
idx = len(self.anchor_points) - 1
if idx >= 0:
last = self.anchor_points[idx]
last['out'] = QPointF(scene_pos)
if idx - 1 >= 0:
vec = last['out'] - last['pt']
last['in'] = last['pt'] - (vec * 0.5)
self._update_handle_visual(idx)
self._rebuild_path()
self._update_preview_visuals(mouse_pos=scene_pos)
return
# For other modes or when not dragging: show preview line to mouse
self._update_preview_visuals(mouse_pos=scene_pos)
def _on_mouse_release(self, scene_pos, modifiers):
if not self.drawing:
return
if not self._dragging:
# click without drag: check for closing in bezier/polyline modes
if len(self.anchor_points) >= 2:
first = self.anchor_points[0]['pt']
if (scene_pos - first).manhattanLength() < 8.0:
self.anchor_points[-1]['pt'] = QPointF(first)
self._rebuild_path(close=True)
self._finish_path()
return
self._last_pos = QPointF(scene_pos)
self._dragging = False
self._drag_start_pos = None
self._update_preview_visuals()
# ----------------- path building ----------------------------------
# ----------------- finish / cancel / remove ------------------------
def _finish_path(self):
if not self.drawing:
return
# If spline mode, convert anchor_points to smoothed path first
pts = [pt['pt'] for pt in self.anchor_points]
if self._mode == 'spline':
try:
p = self._build_cubic_uniform_bspline(pts,
sampler=self._draw_sampler,
finalize=False)
if p is None:
p = QPainterPath()
except Exception as e:
print('error: ', e)
p = QPainterPath()
self.path = p
if self.path_item:
self.path_item.setPath(self.path)
# finalize item
if self.path_item:
self.path_item.setFlag(QGraphicsPathItem.GraphicsItemFlag.ItemIsSelectable, True)
self.path_item.setFlag(QGraphicsPathItem.GraphicsItemFlag.ItemIsMovable, True)
finished_path = QPainterPath(self.path)
finished_item = self.path_item
self.path_item = None
self.handles.clear()
self.anchor_points.clear()
self.path = QPainterPath()
self.drawing = False
# remove preview visuals
if self._preview_poly_item:
try:
self.scene.removeItem(self._preview_poly_item)
except Exception as e:
print('error: ', e)
self._preview_poly_item = None
for l in self._preview_ctrl_items:
try:
self.scene.removeItem(l)
except Exception as e:
print('error: ', e)
self._preview_ctrl_items.clear()
self._notifier.pathFinished.emit(finished_path, finished_item)
def _rebuild_path(self, close=False):
if not self.anchor_points:
return
mode = self._mode
anchors = self.anchor_points
p = None
if mode == 'spline':
pts = [a['pt'] for a in anchors]
try:
p = self._build_cubic_uniform_bspline(pts, closed=close,
sampler=self._draw_sampler,
finalize=False)
except Exception as e:
print('error: ', e)
p = None
if p is None:
# fallback previews: polyline-like for simple modes, or bezier for handle mode
if mode in ('polyline', 'paraxial', 'spiro', 'spline'):
p = QPainterPath(anchors[0]['pt'])
for a in anchors[1:]:
p.lineTo(a['pt'])
if close:
p.closeSubpath()
else:
p = QPainterPath(anchors[0]['pt'])
prev = anchors[0]
for cur in anchors[1:]:
prev_out = prev.get('out')
cur_in = cur.get('in')
if prev_out and cur_in:
p.cubicTo(prev_out, cur_in, cur['pt'])
elif prev_out and not cur_in:
in_pt = QPointF(cur['pt'] - (prev_out - prev['pt']) * 0.5)
p.cubicTo(prev_out, in_pt, cur['pt'])
else:
p.lineTo(cur['pt'])
prev = cur
if close:
p.closeSubpath()
# only set when changed to reduce unnecessary repaints
if p != self.path:
self.path = p
if self.path_item:
self.path_item.setPath(p)
def _cancel_path(self):
self._clear_temp()
def _remove_last_point(self):
if not self.drawing:
return
if len(self.anchor_points) eps or abs(y - last[1]) > eps):
pts.append((float(x), float(y)))
last = (x, y)
if not pts:
return QPainterPath()
cv = np.asarray(pts, dtype=float)
orig_count = len(cv)
k = max(1, int(degree))
if orig_count 1:
path.closeSubpath()
print('path1 = ', path)
return path
periodic = bool(closed)
count = orig_count
if periodic:
factor, fraction = divmod(count + k + 1, count)
cv = np.concatenate((cv,) * factor + (cv[:fraction],), axis=0)
kv = np.arange(-k, len(cv) + k + 1)
max_param = len(cv) - k
else:
k = int(np.clip(k, 1, count - 1))
kv = np.clip(np.arange(count + k + 1) - k, 0, count - k)
max_param = count - k
try:
spl = si.BSpline(kv, cv, k)
except Exception:
path = QPainterPath()
path.moveTo(QPointF(float(cv[0,0]), float(cv[0,1])))
for x, y in cv[1:]:
path.lineTo(QPointF(float(x), float(y)))
if periodic and count > 1:
path.closeSubpath()
print('path2 = ', path)
return path
# If a sampler is provided and we are not finalizing, use its dense grid for fast preview.
if sampler is not None and not finalize:
# ensure sampler grid matches current param range
if sampler.u_dense is None or sampler.domain_max is None or sampler.domain_max != float(max_param):
sampler.init_grid(max_param)
try:
pts_dense = spl(sampler.u_dense)
except Exception:
path = QPainterPath()
path.moveTo(QPointF(float(cv[0,0]), float(cv[0,1])))
for x, y in cv[1:]:
path.lineTo(QPointF(float(x), float(y)))
if periodic and count > 1:
path.closeSubpath()
print('path3 = ', path)
return path
idx = np.arange(0, len(sampler.u_dense), sampler.display_every, dtype=int)
out = pts_dense[idx]
else:
# final path evaluation with requested samples per segment
S = max(1, int(samples_per_segment))
segments = count if periodic else (count - k)
vals = []
for i in range(int(segments)):
for j in range(S):
vals.append(i + (j / float(S)))
vals.append(float(max_param))
u = np.asarray(vals, dtype=float)
try:
out = spl(u)
except Exception:
path = QPainterPath()
path.moveTo(QPointF(float(cv[0,0]), float(cv[0,1])))
for x, y in cv[1:]:
path.lineTo(QPointF(float(x), float(y)))
if periodic and count > 1:
path.closeSubpath()
print('path4 = ', path)
return path
path = QPainterPath()
path.moveTo(QPointF(float(out[0,0]), float(out[0,1])))
for x, y in out[1:]:
path.lineTo(QPointF(float(x), float(y)))
if periodic:
path.closeSubpath()
return path
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("PenTool Demo")
self.resize(900, 700)
central = QWidget()
self.setCentralWidget(central)
vlayout = QVBoxLayout(central)
# toolbar with toggle button
toolbar = QWidget()
th = QHBoxLayout(toolbar)
th.setContentsMargins(0, 0, 0, 0)
self.toggle_btn = QPushButton("Pen Tool")
self.toggle_btn.setCheckable(True)
self.toggle_btn.toggled.connect(self.handlePenToggled)
th.addWidget(self.toggle_btn)
th.addStretch()
vlayout.addWidget(toolbar)
# graphics view
self.view = QGraphicsView()
self.view.viewport().setMouseTracking(True)
self.scene = QGraphicsScene(0, 0, 2000, 2000)
self.view.setScene(self.scene)
# enable antialiasing
self.view.setRenderHints(self.view.renderHints())
vlayout.addWidget(self.view)
# pen tool
self.pen_tool = PenTool(self.scene)
self.pen_tool._notifier.pathFinished.connect(self.handlePathFinished)
def handlePenToggled(self, checked):
if checked:
self.pen_tool.activate(self.view)
else:
self.pen_tool.deactivate()
def handlePathFinished(self, path, meta):
print("Path finished; metadata:", meta)
# Create a visible item for the finished path
# item = QGraphicsPathItem(path)
# pen = QPen(QColor(0, 100, 200), 2)
# pen.setCosmetic(True)
# item.setPen(pen)
# self.scene.addItem(item)
# # call sample user hook (you provided earlier signature)
# print("Path finished; element:", item)
def main():
app = QApplication(sys.argv)
w = MainWindow()
w.show()
sys.exit(app.exec())
if __name__ == "__main__":
main()
Подробнее здесь: [url]https://stackoverflow.com/questions/79814260/pyqt-trying-to-draw-b-spline-curve-with-mouse[/url]
Ответить
1 сообщение
• Страница 1 из 1
Перейти
- Кемерово-IT
- ↳ Javascript
- ↳ C#
- ↳ JAVA
- ↳ Elasticsearch aggregation
- ↳ Python
- ↳ Php
- ↳ Android
- ↳ Html
- ↳ Jquery
- ↳ C++
- ↳ IOS
- ↳ CSS
- ↳ Excel
- ↳ Linux
- ↳ Apache
- ↳ MySql
- Детский мир
- Для души
- ↳ Музыкальные инструменты даром
- ↳ Печатная продукция даром
- Внешняя красота и здоровье
- ↳ Одежда и обувь для взрослых даром
- ↳ Товары для здоровья
- ↳ Физкультура и спорт
- Техника - даром!
- ↳ Автомобилистам
- ↳ Компьютерная техника
- ↳ Плиты: газовые и электрические
- ↳ Холодильники
- ↳ Стиральные машины
- ↳ Телевизоры
- ↳ Телефоны, смартфоны, плашеты
- ↳ Швейные машинки
- ↳ Прочая электроника и техника
- ↳ Фототехника
- Ремонт и интерьер
- ↳ Стройматериалы, инструмент
- ↳ Мебель и предметы интерьера даром
- ↳ Cантехника
- Другие темы
- ↳ Разное даром
- ↳ Давай меняться!
- ↳ Отдам\возьму за копеечку
- ↳ Работа и подработка в Кемерове
- ↳ Давай с тобой поговорим...
Мобильная версия