M4: shape builder tool (Shift+M): merge or remove the regions of overlapping shapes (#39)

The selected shapes' outlines split the canvas into regions, computed as the faces of their
arrangement with linesweeper (curves kept, per-shape fill rules, holes and islands handled) and
cached per document version and selection.

- Engine: shapeBuilder.regions / merge / remove / mode commands, ToolId::ShapeBuilder and
  tool.shapeBuilder (catalog: Tools > Shape builder, Shift+M). Merge makes one vector from the
  picked regions styled like the top-most shape over the first; every shape covering a picked
  region loses that area (becomes a vector, or is deleted when nothing is left). One gesture or
  command is one undo step. Session::tool_key routes Enter (finish) and vector edit keys.
- Pathops: faces() (arrangement faces with their covering inputs) and union_faces().
- UI: region edges on the canvas, hovered/collected regions hatched (danger colour to remove),
  no transform box while building, a toolbar pill with Merge/Remove modes, a hint and Done; shared
  pill/tool-cell/divider/Done helpers; original merge/remove icons drawn in code.
- Tests: pathops faces; engine regions, merge, remove, curves, groups, rotation, pointer
  click/drag/Alt, modes, Escape/Enter, cache, junk parameters and degenerate geometry.
- Docs: ROADMAP, README, docs/mcp.md, docs/control-protocol.md, docs/parity.md.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
BFlat
2026-10-10 05:55:12 +07:00
committed by GitHub
co-authored by Claude Opus 5.5
parent e360b6fa52
commit 476cdd95f1
18 changed files with 1154 additions and 83 deletions
+1
View File
@@ -79,6 +79,7 @@ fn registry() -> &'static Registry {
list.extend(proto::specs());
list.extend(comment::specs());
list.extend(crate::vectoredit::specs());
list.extend(crate::shapebuilder::specs());
list.extend(crate::codegen::specs());
let mut index = HashMap::new();
for (i, s) in list.iter().enumerate() {
+2 -1
View File
@@ -181,7 +181,7 @@ pub fn specs() -> Vec<CommandSpec> {
spec!(
"tool.set",
"Choose tool",
"{tool: move|scale|hand|frame|section|slice|rectangle|line|arrow|ellipse|polygon|star|pen|pencil|text|comment}",
"{tool: move|scale|hand|frame|section|slice|rectangle|line|arrow|ellipse|polygon|star|pen|pencil|text|comment|shapeBuilder}",
false,
|s, p| {
let t = req_string("tool.set", p, "tool")?;
@@ -205,6 +205,7 @@ pub fn specs() -> Vec<CommandSpec> {
spec!("tool.pencil", "Pencil", "{}", false, |s, _| tool(s, ToolId::Pencil)),
spec!("tool.text", "Text", "{}", false, |s, _| tool(s, ToolId::Text)),
spec!("tool.comment", "Comment", "{}", false, |s, _| tool(s, ToolId::Comment)),
spec!("tool.shapeBuilder", "Shape builder", "{}", false, |s, _| tool(s, ToolId::ShapeBuilder)),
spec!("edit.pickColor", "Pick color", "{}", false, |s, _| tool(s, ToolId::Eyedropper)),
]
}