CI's Rust Check runs a workspace-wide `cargo fmt --check`. Reformat the
new op-acp / op-ai-skills crates and the Part A chat changes to rustfmt
canon. Also sweeps two files an earlier commit left non-compliant
(canvas_viewport_paint.rs, op-pen-loader/adapter.rs) so the workspace
check is clean. Formatting only — no behaviour change.
Addresses the 3 CONCERNs from the third codex review.
- `signed_sweep`: a negative-sweep drag that collapses onto the fixed
endpoint now snaps to a full -360° circle instead of 0° (mirrors
`norm_sweep`'s positive 0 → 360 rule) — a negative arc no longer
silently loses its sign in that degenerate case.
- Path fallback sizing (`path_to_payload`): an unsized path now
derives its width/height from the handle-aware
`path_bounds_from_anchors` (cubic extrema included) instead of the
endpoint-only point bbox, so handles bowing past the anchors no
longer under-size the scene node.
- Closed-path fill: a closed `Path` with a fill now paints its
enclosed area via `fill_polygon` over the flattened outline — was
stroked-only, so authored fills rendered as bare outlines.
Verified against op-editor-core 239 / op-pen-loader 21 / op-editor-ui
155 (op-host-native not test-built — an unrelated in-progress
AIChat thinking/effort change in the working tree leaves its
`AIChatHit` match non-exhaustive; `signed_sweep` is a pure-fn change).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Addresses the 4 CONCERNs from the second codex review.
- No-op undo: arc + path/handle drags now mutate nothing until the
cursor first travels — a press-release leaves the document and
undo stack untouched. Once the drag has moved, every event keeps
writing (so a drag back to the start point still lands), gated on
`is_move || already_moved`. This closes the hole where a ghost-
handle press-release created a handle with no undo entry.
- Negative arc sweep: `signed_sweep` keeps the sign of the arc being
dragged, so an MCP-authored counter-clockwise (negative) sweep no
longer flips to the major arc under a canvas drag.
- Closed paths: `path_closed` threaded through NodePayload +
SceneNode; `flatten_path` appends the last-anchor → first-anchor
closing segment (cubic or straight) so a closed canonical path
draws its closing edge.
op-editor-core 235 / op-pen-loader 21 / op-editor-ui 148 /
op-host-native 64 / op-host-desktop 21 tests green.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Addresses the first codex review of the arc + pen-handle features.
BLOCKs:
- Rotation: the arc + pen-handle overlays and the host hit-tests now
account for node rotation. The overlay paint wraps in save/rotate
like the selection overlay; the hit-tests + drag math un-rotate the
cursor into the node's local frame via `rotate_point`.
- Arc ellipse hit-test: a pie / arc / donut now hit-tests against the
actual sector — the missing wedge + the donut hole are no longer
selectable (was always the full oval).
- Arc no-op undo: `ArcHandleDragState` gains `start_doc`; the move
handler gates `moved` on real cursor travel so a press-release
pushes no undo entry.
CONCERNs:
- Path bounds: the bezier handle-aware bounds algorithm moved into a
shared `op_editor_core::path_bounds`; `refit_path_bounds` uses it
(and is now called after handle / point-type edits) so the loader's
absolutize scale stays 1.0 — a handle no longer rescales the path.
- `cmd_set_ellipse_arc` now honours the locked/hidden editability
guard like the other geometry mutators.
- A full-ring donut strokes its two concentric ovals instead of the
polygon, so the radial seam is not drawn.
op-editor-core 235 / op-editor-ui 148 / op-pen-loader 21 /
op-host-native 64 / op-host-desktop 21 tests green.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Stage 1 of pen bezier-handle editing — the data + command layer.
- op-editor-core: `EditorState::set_path_anchor_handle` (set / clear
an anchor's in/out handle; a `Mirrored` anchor keeps both handles
collinear) + `set_path_anchor_point_type` (switching to `Mirrored`
snaps the handles collinear). New `PathHandleSide` enum.
- op-pen-loader: `AnchorPayload` (absolute-coord anchor + resolved
handles + point-type code) on `NodePayload.path_anchors`;
`absolutize_path_anchors` now resolves the schema's anchor-relative
handle deltas into the same absolute frame as `points`.
- op-editor-ui: `SceneAnchor` + `ScenePointType` on
`SceneNode.path_anchors` so the painter + host can read the
editable handles.
op-editor-core 234 / op-pen-loader 21 / op-editor-ui 145 tests green
(+3 handle-setter units).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Ellipse nodes with authored arc geometry (`start_angle` /
`sweep_angle` / `inner_radius`) were always painted as a full oval —
the arc fields never reached the painter.
- NodePayload + SceneNode gain `arc_start_angle` / `arc_sweep_angle`
/ `arc_inner_radius`; threaded through `ellipse_to_payload` and
`node_payload_to_scene`.
- canvas_viewport_paint: new `arc_polygon` tessellator (pie wedge =
centre + outer arc; donut sector = outer arc + reversed inner arc)
+ `paint_ellipse` — full oval when no arc is authored, otherwise a
filled/stroked polygon. A 360° sweep with no donut hole still
short-circuits to the plain oval path.
Prep for the arc canvas drag-handle UI. op-editor-ui 143 /
op-pen-loader 21 / op-host-desktop 64 tests green.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Closes six verified gaps from the 2026-05-17 TS-vs-Rust gap analysis,
each build- and test-green:
- editor: SetNodeFlip + SetEllipseArc commands (+ set_node_flip /
set_ellipse_arc MCP tools) — schema already had the fields, only
the command path was missing.
- export: export_node_raster crops a raster to one node's bbox;
File -> Export is now selection-aware (single selection -> layer).
- editor: SVG import — hand-rolled parser (shapes + path M/L/H/V/
C/S/Q/T/Z) in svg_import.rs; cubic curves flatten to dense straight
anchors at import time so the renderer/pen-tool stay on their 1:1
straight-segment model. + EditorCommand::ImportSvg + import_svg tool.
- mcp: HTTP transport — mcp_serve::run_http serves MCP over a
TcpListener (--mcp-http <port> <path>); process_message is shared
with the stdio path.
- cli: new op-cli crate (binary `op`) — a dependency-free HTTP MCP
client driving every tool via `op <tool> key=value...`.
- ai: ChatRequest gains thinking/effort fields (ThinkingMode /
EffortLevel, defaults Adaptive/Low to match the TS runtime config).
MCP tool catalog 77 -> 80. Oversized files split to honour the
800-line cap (svg_import, export, mcp_serve, adapter).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Phase 7.3 strangler reorg — the final consumer (op-pen-loader) is
repointed off the openpencil-shell-core shim onto op-editor-ui (the
real source crate for the layout scene / scene-var / render-backend
facade), then the shim crate is deleted.
- op-pen-loader: openpencil-shell-core dep -> op-editor-ui;
every openpencil_shell_core:: path -> op_editor_ui::
- git rm crates/openpencil-shell-core/ (lib + jian.rs module + the
two re-export anchor tests, all superseded by op-editor-ui's own
surface; the jian.rs module had no consumers outside the shim)
- stale shell-core / shell-native comment refs in op-editor-core +
op-host-desktop manifests updated
Phase 7 Task 7.1 final step. The `crate::document` module's shared
enum/value types were the last remnant of shell-core's dead `Document`
model. This repoints every consumer onto canonical types and deletes
the module entirely.
- Enum types (`Tool`, `AlignAction`, `FillType`, `FlexLayout`,
`PropertyTab`, `PropertyFocus`, `ColorTarget`, `Viewport`, `NodeId`,
`BooleanOp`, `ChatAnchor`, `ReorderDirection`) repointed onto their
variant-identical `op_editor_core` equivalents.
- `NodeKind` / `Effect` / `DropShadow` are paint-time scene types with
no canonical home — moved into `layout_scene.rs` (the surviving
scene type that is their only consumer). `Stroke` consumers switched
to the existing `SceneStroke`.
- `VariableTable` + friends (`Variable`, `ThemeAxis`, `ThemedValue`,
`VariableKind/Scalar/Value`) are a shell-core-local paint-time
aggregate consumed by the `LayoutScene` builder — moved to a new
crate-root `scene_vars` module.
- `editor_state_ext.rs` converters that became identity now that
widgets speak canonical types deleted (`doc_tool`, `doc_align_action`,
`doc_fill_type`, `doc_flex_layout`, `doc_property_tab`,
`doc_property_focus`). The widget-local→canonical reverse bridges
(`file_menu_choice`, `shape_choice`, `export_format`) added there.
- `op-pen-loader/bridge_enums_rev.rs` (the SC document→EC enum bridge)
deleted; hosts feed canonical widget hit-test results straight in.
- Dead `large_document_perf.rs` integration test (exercised the
removed `Document` struct directly) deleted.
Pure type-repointing — widgets/canvas/export paint identically.
With the LayoutScene builder no longer routing through the shell-core
Document model, the EditorState -> paint-Document bridge is fully
dead. Removes editor_state_bridge.rs (apply_editor_state_ui,
editor_chat_to_chat_state, editor_components_to_component_library),
bridge_ui.rs (editor_ui_to_ui_state), the forward bridge_enums.rs
translators, and payload.rs's pen_document_to_document / to_payload /
apply_payload / DocPayload<->Node converters. None had a production
caller.
payload.rs keeps the DocPayload DTOs + load_canonical (live: adapter
builds DocPayload, the LayoutScene builder reads it, persistence.rs
calls load_canonical). bridge_enums_rev.rs stays — its rev:: enum
translators are still used by the native + web hosts.
The shell-core document.rs / document/ model itself is NOT deleted:
~20 shell-core widget files + layout_scene_hit.rs still consume its
enum types (NodeKind, Tool, FillType, PropertyFocus, AlignAction, ...).
Migrating those onto op-editor-core enums is Phase 7 Task 7.2's scope.
editor_state_to_layout_scene now constructs SceneNodes straight from
the layout-resolved DocPayload (pen_document_to_payload) instead of
routing through the intermediate shell-core Document model. The
DocPayload already carries the resolved geometry + paint fields;
apply_payload's only transforms on them are lossless format
conversions, so the scene output is byte-identical (verified by the
existing layout_scene tests). Adds effects_from_payload_ref so the
builder can rebuild effects from a borrowed payload slice.
Both editor hosts now hit-test the input path against the cached
LayoutScene instead of a per-frame derived Document. Hit-test helpers
(node_at_doc_point, selection_handle_at_point, rotation_corner_at_point,
drop_target_at) moved into layout_scene_hit.rs and rewritten onto
SceneNode. The paint_doc field is removed; Document survives only in the
export renderers and the op-pen-loader payload bridge.
Three additive variable-handling fixes so the editor host can migrate
onto EditorState as a mechanical port:
- Gap 1: add `variable: Option<String>` to ColorPickerState, port
`open_color_picker_for_variable`, and route `color_picker_set_hsv` /
`close_color_picker` through `set_variable_color` when set.
- Gap 2: add `op_pen_loader::editor_state_var_table(&EditorState)`,
folding persisted variables/themes plus the transient active-theme
selection + ref caches into a shell-core VariableTable. Lives in
op-pen-loader (not op-editor-core) to keep op-editor-core wasm-clean
and free of any shell-core dependency.
- Gap 3: confirmed undo/redo of variable create/delete/rename round-
trips for free (EditorSnapshot clones the whole PenDocument); added
tests to lock it in.