3

I am trying to have a Pen Tool in my pyqt6 application. I am trying to make it how Inkscape's PenTool works. For now, I am trying to achieve the 'B-Spline' mode.

The issue is that my curve does not stay as it is being drawn by mouse movement. It sort of jumps/moves after placing the 3rd and subsequent point(with degree=2). If you run the below code you will see that after placing the points without moving the mouse it seems ok. But when you move the mouse the path already drawn is updated/displaced/moved or something.

Inkscape's curve doesn't behave like this. It remain consistent between clicks amd mouse movements.

Please, run this code to see what I am trying to explain.

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) <= 1:
            self._cancel_path()
            return
        self.anchor_points.pop()
        h = self.handles.pop()
        self.scene.removeItem(h)
        self._rebuild_path()
        self._update_preview_visuals()

    # ----------------- visuals -----------------------------------------
    def _add_handle(self, scene_pos: QPointF):
        h = QGraphicsEllipseItem(-HANDLE_RADIUS, -HANDLE_RADIUS, HANDLE_RADIUS * 2, HANDLE_RADIUS * 2)
        h.setPen(self.handle_pen)
        h.setBrush(self.handle_brush)
        h.setZValue(1000)
        h.setPos(scene_pos)
        self.scene.addItem(h)
        self.handles.append(h)
        self._ensure_ctrl_lines()

    def _update_handle_visual(self, idx):
        if 0 <= idx < len(self.handles):
            h = self.handles[idx]
            pt = self.anchor_points[idx]['pt']
            h.setPos(pt)
        
        self._update_preview_visuals()

    # ----------------- preview helpers --------------------------------
    def _ensure_preview_items(self):
        if not self._preview_poly_item:
            self._preview_poly_item = QGraphicsPathItem()
            pen = QPen(Qt.GlobalColor.darkGray, 1, Qt.PenStyle.DashLine)
            pen.setCosmetic(True)
            self._preview_poly_item.setPen(pen)
            self._preview_poly_item.setZValue(900)
            self.scene.addItem(self._preview_poly_item)

    def _ensure_ctrl_lines(self):
        needed = max(0, len(self.anchor_points) * 2)
        while len(self._preview_ctrl_items) < needed:
            line = QGraphicsLineItem()
            pen = QPen(Qt.GlobalColor.lightGray, 1, Qt.PenStyle.DotLine)
            pen.setCosmetic(True)
            line.setPen(pen)
            line.setZValue(950)
            self.scene.addItem(line)
            self._preview_ctrl_items.append(line)
        # hide extras
        for i in range(len(self._preview_ctrl_items)):
            if i >= needed:
                self._preview_ctrl_items[i].hide()
            else:
                self._preview_ctrl_items[i].show()


    def _update_preview_visuals(self, mouse_pos=None):
        self._ensure_preview_items()
        
        
        # Build control polyline
        if not self.anchor_points:
            self._preview_poly_item.setPath(QPainterPath())
        else:
            pp = QPainterPath(self.anchor_points[0]['pt'])
            for i in range(1, len(self.anchor_points)):
                pp.lineTo(self.anchor_points[i]['pt'])
            if mouse_pos:
                pp.lineTo(mouse_pos)
            self._preview_poly_item.setPath(pp)

        # Update control lines for bezier handles
        self._ensure_ctrl_lines()
        for i, anchor in enumerate(self.anchor_points):
            idx_in = i * 2
            idx_out = idx_in + 1
            in_line = self._preview_ctrl_items[idx_in] if idx_in < len(self._preview_ctrl_items) else None
            out_line = self._preview_ctrl_items[idx_out] if idx_out < len(self._preview_ctrl_items) else None
            a_pt = anchor['pt']
            if anchor.get('in') and in_line:
                in_line.setLine(a_pt.x(), a_pt.y(), anchor['in'].x(), anchor['in'].y())
                in_line.show()
            elif in_line:
                in_line.hide()
            if anchor.get('out') and out_line:
                out_line.setLine(a_pt.x(), a_pt.y(), anchor['out'].x(), anchor['out'].y())
                out_line.show()
            elif out_line:
                out_line.hide()

        # Smooth preview
        if self._mode == 'bezier':
            if not self.anchor_points:
                if self.path_item:
                    self.path_item.setPath(QPainterPath())
                return
            temp_path = QPainterPath(self.anchor_points[0]['pt'])
            for i in range(1, len(self.anchor_points)):
                prev = self.anchor_points[i - 1]
                cur = self.anchor_points[i]
                if prev.get('out') and cur.get('in'):
                    temp_path.cubicTo(prev['out'], cur['in'], cur['pt'])
                elif prev.get('out') and not cur.get('in'):
                    in_pt = QPointF(cur['pt'] - (prev['out'] - prev['pt']) * 0.5)
                    temp_path.cubicTo(prev['out'], in_pt, cur['pt'])
                else:
                    temp_path.lineTo(cur['pt'])
            if mouse_pos and self.anchor_points:
                temp_path.lineTo(mouse_pos)
            if self.path_item:
                self.path_item.setPath(temp_path)
            return

        # other modes: fast b-spline approx for preview (use sampler)
        pts = [pt['pt'] for pt in self.anchor_points]
        if mouse_pos:
            print('pts = ', pts)
            pts = pts + [mouse_pos]
        else:
            print('mouse points', pts)
        approx = QPainterPath()

        if pts:
            try:
                approx = self._build_cubic_uniform_bspline(
                    pts,
                    sampler=self._draw_sampler,
                    finalize=False
                )
                if approx is None:
                    approx = QPainterPath()
            except Exception as e:
                print('error: ', e)
                approx = QPainterPath()
        else:
            approx = QPainterPath()

        if self.path_item:
            self.path_item.setPath(approx)


    def _build_cubic_uniform_bspline(self, points, closed=False, degree=2,
                                    samples_per_segment=8, sampler: DrawSampler = None,
                                    finalize=False):
              
        if not points:
            return QPainterPath()
        pts = []
        last = None
        eps = 1e-9
        for p in points:
            x, y = (p.x(), p.y()) if hasattr(p, 'x') else tuple(p)
            if last is None or (abs(x - last[0]) > 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 <= k:
            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 closed and 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()

1
  • Hi, please consider producing a shorter example illustrating the problem - it looks like there's a lot of code for other types of curves, etc., which makes it a lot more difficult to help you! See overflow.tips/write-good-question/… Commented Nov 8 at 19:43

1 Answer 1

2

I believe your issue comes from how you are handling the knots for the end points. When you have 4 points, your knots are [0, 0, 0, 1, 2, 2, 2] but as soon as the user clicks to "place" the 4th point and the mouse becomes point 5, then the knots change to [0, 0, 0, 1, 2, 3, 3, 3]. So the positions of what were the last two knots jumps from 2 to 3, and the curve visibly jumps as well.

There are a variety of solutions, but the most straightforward is to include the last point twice. That is, in _build_cubic_uniform_bspline, right before you have cv = np.asarray(pts, dtype=float) add the line pts = pts + (degree - 1) * [pts[-1]]. This should make sure the B-spline still terminates at the final point but does not jump when a new point is added. I tested this change, and it appears to solve the problem.

Let me know if this solves your issue!

Sign up to request clarification or add additional context in comments.

3 Comments

Thanks for taking some time out of your precious time to help me with this. I appreciate it! Your suggested changes worked for degree=2 param. But for other values like 3 the issue doesn't resolve. I am also trying to debug it in the direction you pointed me to. Thank you very much, again.
Ahh sorry my bad, I should have done a bit more testing before posting my answer! I made a small edit to append degree - 1 extra copies of the last point inside of just 1. For degree=2 those are the same solution of course, which is why it only worked for the quadratic case!
That worked!! I went more complicated way with this. Yours is the simplest. Thank you!

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.