Skip to content

feat: edit nodes of imported vector paths - #557

Merged
knipknap merged 9 commits into
barebaric:mainfrom
switch87:feat/node-edit
Oct 18, 2026
Merged

knipknap merged 9 commits into
barebaric:mainfrom
switch87:feat/node-edit

Conversation

@switch87

@switch87 switch87 commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

All parts are in. It uses the new Geometry node operations from raygeo#58, released in raygeo 1.64.0, which main already pins; CI is green.

Implements #521, option B as agreed: node editing in the existing vector edit mode, without the solver.

  • Commands. DocEditor.node_edit covers move / delete / insert node, line ↔ curve and moving Bézier handles. It runs on the workpiece's boundaries through raygeo#58 and writes _edited_boundaries like delete_segments, so traced bitmaps work too.
    • Every edit is its own undo step: the command opts out of coalescing, so two quick node deletions don't merge into one undo step.
    • Sketch workpieces are left alone.
  • Vector edit mode.
    • Show the nodes of the contours that hold the selected segments, plus the handles of selected Béziers.
    • Drag a node or a handle with a live preview, committed on release as one undo step.
    • Delete removes the selected node (or the selected segments, as today). I inserts a node where the pointer is on a segment. C toggles the selected segments between line and curve.
  • Docs.

Checked in the running app (headless), driving the real handle_edit_* methods on an imported 40×20 mm rectangle: select a segment (its contour's nodes appear), drag a node (live preview, one undo step), I inserts a node at the pointer, C makes a curve, drag its handle, and Delete removes the node. Four undos bring back the original path exactly, and the edit mode stays active throughout. Hit testing now uses a per-axis tolerance, so it also works on non-square workpieces (the old segment hit test used the X scale only).

One question: a node dragged outside the workpiece's original bounding box goes outside the 0..1 boundary frame. The toolpath is right, but the canvas may clip that part, because nothing re-fits the bounding box after an edit. That is the same as segment deletion today. Should node edits re-normalise the boundaries (and compensate the matrix, like split does), or do you prefer to keep the frame fixed?

@switch87 switch87 changed the title WIP: edit nodes of imported vector paths feat: edit nodes of imported vector paths Oct 12, 2026
@switch87

Copy link
Copy Markdown
Contributor Author

Rebased onto current main, which conflicted again after #552 (only CHANGELOG.md; both entries kept). That replaced your 'Merge branch main into pr-557' commit with a linear history. Everything from it is in main, so nothing is lost. Sorry for the churn.

@switch87
switch87 marked this pull request as ready for review October 13, 2026 17:27
@switch87

Copy link
Copy Markdown
Contributor Author

I went over this again against the architecture and added three small commits on top (no force-push from here on):

  • 66a2cf3: the node edit command only blocked coalescing in one direction, so a node edit right after a segment deletion still merged into it. ChangePropertyCommand now has a coalesce flag checked on both sides, which replaces the private subclass.
  • 93017bb: the canvas no longer keeps a list of the workpiece's edit keys. It offers any key without Ctrl/Cmd to the edit context, so Ctrl+C in vector edit mode copies again instead of toggling a curve.
  • 86a19c2: inserting or deleting a node cleared nothing, so the segment selection pointed at other segments afterwards. Segment deletion now uses the same re-snapshot as the node edits instead of its own copy.

Each has tests. The bounding box question in the description is still open.

DocEditor.node_edit moves, deletes and inserts nodes, turns segments
into curves or lines and moves Bezier handles on the boundaries of an
imported workpiece, through raygeo's node editing operations. The
result goes to the workpiece's edited boundaries like segment deletion,
and every edit is its own undo step. Sketch workpieces are left alone.

Refs barebaric#521
Selecting a segment in the vector edit mode now shows the nodes of its
contour and the handles of selected Beziers. Nodes and handles can be
dragged with a live preview and are committed on release as one undo
step. Delete removes the selected node (or the selected segments, as
before), I inserts a node where the pointer is on a segment, and C
turns the selected segments into curves or lines. Hit testing uses a
per-axis tolerance, so it works on non-square workpieces.

Refs barebaric#521
The node edit command only refused to merge the next command into
itself, so a node edit right after a segment deletion was still merged
into that deletion. ChangePropertyCommand now takes a coalesce flag
that is checked on both sides, replacing the private subclass.

Refs barebaric#521
The canvas carried a list of the workpiece's node edit keys, and also
sent Ctrl+C and Ctrl+I to the edit mode, so copy toggled a curve there.
The canvas now offers every key without the primary modifier to the
edit context and leaves shortcuts to the accelerators.

Refs barebaric#521
Inserting or deleting a node shifts the command indices, so the kept
segment selection pointed at other segments afterwards. Segment
deletion now goes through the same re-snapshot as node edits instead
of its own copy, and the key handler is split into small helpers.

Refs barebaric#521
Add the node editing description of the vector edit mode to the
Canvas Tools page in every translated docs directory.

Refs barebaric#521
edited_boundaries_command() in doceditor/boundaries.py replaces the
four copies of the ChangePropertyCommand recipe in edit_cmd,
path_cleanup_cmd and node_edit_cmd. It opts out of coalescing, so two
quick segment or contour deletions no longer merge into one undo
step, matching node edits.

Bumps raygeo to 1.67.0 for Geometry.contour_info(): the seam of a
closed contour is one node in the node operations, but the edit
overlay drew it twice, once per command index, and hit testing always
picked the later one. visible_nodes now shows the seam node once at
its Move index, and visible_handles shows the handles of both
segments meeting there.

The node editing helpers also move from canvas2d/elements/ to
canvas2d/, since they are not CanvasElements.
@knipknap

Copy link
Copy Markdown
Contributor

I pushed 9cf1b05 ("refactor(doceditor): one undoable boundaries command, single seam node") directly to this branch — it folds the review follow-ups into the PR:

  • One edited-boundaries command. The write-\_edited_boundaries-undoably recipe existed four times (EditCmd.delete_segments, EditCmd.delete_contours, PathCleanupCmd._boundaries_command, and the new NodeEditCmd). It now lives once, as edited_boundaries_command() in doceditor/boundaries.py, and opts out of undo coalescing everywhere — so two rapid segment or contour deletions no longer merge into a single undo step, matching node edits. Covered by a new regression test.
  • node_editing.py moved from canvas2d/elements/ (which is exclusively CanvasElement subclasses) to canvas2d/, next to projection.py and ops_cache_registry.py.
  • The seam node of a closed contour is drawn once. raygeo's node operations treat the start and end node of a closed contour as one node, but the overlay drew it twice (once per command index) and hit testing always picked the later index. The new Geometry.contour_info() (raygeo#63, released in raygeo 1.67.0) exposes the contour range and closedness with the exact criterion the node operations use, so visible_nodes now shows the seam once at its Move index and visible_handles shows the handles of both segments meeting there. pixi.toml and requirements.txt pin raygeo 1.67.0 and pixi.lock is regenerated, so CI resolves against the released package.

Verification: pixi run lint passes (pre-commit green on the commit), the full test suite passes (7567 passed via the pre-push hook), and the node editing tests were also run against the PyPI raygeo 1.67.0 wheel.

@knipknap
knipknap merged commit 0ab2b32 into barebaric:main Oct 18, 2026
32 checks passed
@knipknap

Copy link
Copy Markdown
Contributor

Tested manually and everything seems to work. Thank you for the PR!

This branch was previously deployed

1 inactive deployment
pypi — 8ada087d Deployed Oct 18, 2026 by knipknap via Build and Publish Wheel #2380
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants