Continues the gradient + property-panel polish from the previous
commit and rounds out two new flows the TS app already has:
Gradient stops + effects:
- ColorTarget gains GradientStop(i) + EffectColor(i); HSV picker
preserves alpha across hue/SV drags so a transparent stop stays
transparent. Hex pill stays 6-char; alpha is reattached at commit
and the swatch sits on a 2x2 alpha checker so #00000000 reads as
transparent rather than empty.
- Effects section reflowed into card-style blocks (image #9 spec):
title + minus, X/Y and Blur/Spread 2-col grids, color row with
swatch + rgba(...) text; clicking the swatch opens an HSV picker
bound to that effect index via SetEffectColor.
- Press dispatch on both hosts anchors picker overlays at the
clicked y so they pop adjacent to the swatch instead of the top.
Image + SVG import (toolbar + Fill section "图片" row):
- New FileAction::ImportImageOrSvg / PickFillImage; persistence_image
pops rfd, decodes raster as data: URL, inserts an Image node or
rewrites the selected node's primary fill.
- ImageNode actually renders on the canvas: NodePayload + SceneNode
carry image_src, canvas_viewport_paint.rs decodes the data URL
once and hands raw bytes to RenderBackend::draw_image with a
src-hash cache id. Grey placeholder paints only when decode fails
so transparent PNGs don't get a grey matte underneath.
- SVG import ported to TS-parity (packages/pen-engine svg-parser):
recursive <g> tree walk with inherited fill/stroke/style="...",
viewBox-aware scaling with maxDim cap, multi-subpath split, raw
d preserved on PathNode. Imports land wrapped in a Group named
after the source file.
Locale-aware first run:
- settings_io detects the OS locale (LC_ALL/LANG/LC_MESSAGES with
zh-Hans/zh-Hant heuristics) and seeds editor_ui.locale before
settings.json is read; persisted user choice still wins.
- macOS bundle declares CFBundleLocalizations + AllowMixedLocalizations
so NSOpenPanel / NSSavePanel render in the same language as the
rest of the chrome.
Web host kept exhaustive across the new variants (PickFillImage,
OpenEffectColorPicker, ColorTarget::EffectColor, GradientStop). Two
new files: persistence_image.rs (file-pick handlers, ≤120 lines) and
svg_path_data.rs (path-d tokenizer + bbox + normaliser, split from
svg_import.rs to stay under the 800-line cap). 277 op-editor-core
tests pass.
Three independent CI-unblockers grouped to keep history clean:
1. op-host-desktop/chat_orchestrator.rs — add `visual_ref_enabled: false`
to the DesignRequest literal so the workspace builds after S4 added
the field. Same stub-plumbing pattern as the S3b-2 `concurrency`,
S3b-4 `append_context`, S3c `validation_enabled` additions. Real
intent-gate routing is task #27.
2. op-editor-core/render_backend.rs::fill_round_rect_radial_gradient —
#[allow(clippy::too_many_arguments)] on the trait default. The
radial-gradient hook carries (rect, radius, stops, cx_frac, cy_frac,
radius_frac, opacity) per the TS pen-renderer contract; that's the
data shape, not refactorable without splitting the trait.
3. cargo fmt --all catch-up on 6 other files (op-editor-core /
op-editor-ui / op-host-native) that pre-dated this branch and were
never run through rustfmt. Pure whitespace, no semantic change. Lets
`cargo fmt --all -- --check` pass workspace-wide.
`cargo clippy --workspace --all-targets -- -D warnings` + `cargo build
--workspace` + `cargo fmt --all -- --check` all clean. Tests across
op-orchestrator (574+1) / op-design-lint (149) / op-mcp (144) /
op-editor-core (273) / op-host-desktop (96) all green.
Wires the linear/radial gradient fill type from the property panel
through to a real skia shader on the canvas — previously switching
fill type only changed the model and the panel painted hardcoded
placeholder stops. Stop hex (with alpha preserved), angle, offset %,
and per-stop HSV picker are all live.
- SceneNode + NodePayload carry a resolved gradient body; native
skia backend builds `linear_gradient` / `radial_gradient` shaders
with TS-renderer geometry (angle - 90° ellipse projection, radius
fraction of max(w, h)).
- PropertyFocus + EditContext gain GradientAngle / GradientStopHex /
GradientStopOffset; commit, snapshot, hit-test, paint, keyboard
validation, and the property-panel layout walker thread the new
variants end-to-end.
- ColorPickerState carries a per-session alpha so the gradient-stop
picker keeps transparency across HSV drags; hex pill stays 6-char
and re-attaches the stop's existing alpha at commit. Swatch paints
over a 2×2 checker so a fully transparent stop reads as such.
- skia.rs split into a spine + sibling gradient.rs to stay under the
workspace 800-line cap.
# Conflicts:
# crates/op-editor-core/src/lib.rs
Property-panel input editing now supports:
- Arrow keys: Up/Down step a numeric field; Left/Right move the text caret. Caret position is a real index into the draft, so typing inserts at the caret and Backspace deletes the char before it (not just append/pop).
- Layer opacity: full-width box with the localized 不透明度 label inside on the left, value next to it, % at the right edge; clipped so a long-locale label can't bleed past the half-width box.
- Fill opacity: new SolidFillBody.opacity getter / setter on the model + PropertyFocus::FillOpacity + the 100 % box in the Fill head row is now editable end-to-end.
- Effect-param values: each Drop Shadow X / Y / Blur / Spread cell is a click-to-type input box (new effect_param_focus state, FocusEffectParam action, commit_effect_param_focus_if_any path). The − / + steppers still work alongside. Web's apply_property_action makes the focus a no-op (no keyboard path on web yet) to avoid stranding focus.
Paint polish:
- Standardised input-text baselines to + 19.0 across prefix / suffix / icon helpers, the fill / stroke / opacity / hex paints, and the export section so every INPUT_HEIGHT row reads on the same baseline.
- Icon-prefixed inputs now have 10 px left padding (matching X / Y / W / H) and the icon is vertically centred ((30 - 14) / 2 = 8) instead of sitting at y + 5.
- Fill / stroke swatches vertically centred in their hex rows ((30 - 16) / 2 = 7).
- '-' / '#' caret-aware validation in apply_text so typing them at caret 0 of a non-empty draft is now a valid edit. Native input_tests seed property_caret_pos to mirror real focus state.
Codex review iterations: poison-guard the effect-param focus on web, content-clip the layer-opacity row, fix property-panel scroll clamping in paint, and a few related safety guards across hosts.
Existing EditorCommand variants are leaf-only (BatchInsertItem carries
only kind/name/x/y/w/h/fill_hex). The design orchestrator (S3a) must
apply rich nested designs — frames with children, layout, text — so
add InsertSubtree { nodes: Vec<PenNode>, parent_id }.
cmd_insert_subtree validates the parent is a container (or NONE = page
root), remaps every incoming node id to a fresh editor id via
remap_subtree_ids (so an externally-authored subtree can't collide
with live ids), and appends under the parent. The apply arm wraps it
in a history snapshot so the insert is one undo step.
NOT verified locally: op-editor-core does not currently build —
vendor/jian is pinned to unpushed commit 80121906 whose DesignMd*
types op-editor-core depends on are absent from every available jian.
The 8 InsertSubtree tests in command_subtree_tests.rs run once the
jian build is restored.
S3a Plan A.
Export section: the scale / format dropdowns open inline select
popups (1x/2x/3x, PNG/JPEG/WEBP/SVG/PDF) instead of the Export
modal, which is now reached only via File > Export Image. Adds a
full-width Export button; popup rows highlight on hover. Pickers
open upward so they are not clipped at the panel's bottom edge.
PropertyPanel scrolls when its content overflows the viewport,
with the Design / Code tab strip pinned; scroll is clamped on
every paint + hit-test, not just on wheel events.
LayerPanel: the Pages and Layers sections get bounded heights and
independent scroll regions; layer drag/drop is gated to the
visible Layers viewport.
Splits property_panel into property_panel_action / _export and
the host press paths into property_dispatch / scroll modules to
keep every edited file under the 800-line cap.
Hide the native title bar and let the TopBar own the window
chrome — the Electron `titleBarStyle: 'hidden'` recipe:
- macOS keeps a real `NSWindow` (rounded corners, shadow,
edge-resize, key-window responsiveness) with the title bar made
transparent + emptied; the native traffic-light buttons stay,
pushed down via casement's `with_traffic_light_inset` to centre
in the 40 px TopBar. Windows / Linux drop decorations and the
TopBar paints its own close / minimise / maximise dots.
- The TopBar reserves a left inset for the controls; it collapses
in macOS fullscreen (native lights hide), tracked by a
per-frame `window.fullscreen()` poll. `window_control_at`
returns `None` on macOS so a fullscreen click on a left-edge
app icon can't trigger a window control.
- A press on the TopBar's blank area drags the window.
TopBar chip also gains one brand icon per connected agent + an
`N agent[s] · M MCP` status (new `topbar.agentPlural` across all
15 locales).
Bumps the vendor/casement submodule to 5ad98f1c.
The infinite-canvas grid painted one `fill_round_rect` per dot —
~1200+ separate skia draw ops every frame, the dominant cost of an
empty-canvas pan / drag. New `RenderBackend::fill_dots` collects
the dot centres and the native backend draws them in a single
`Canvas::draw_points` (round-capped points); other backends keep a
`fill_oval` loop via the default impl.
Also: `NativeBackend::fill_rect` went through `Paint::solid`, which
hardcodes `opacity: 1.0` and dropped the colour's alpha — a
translucent fill (the 12% marquee-selection band) painted fully
opaque. It now carries alpha through `Paint.opacity` like
`stroke_rect` does.
Three chat-panel fixes that share `ai_chat_panel.rs` / `input.rs` /
`scroll.rs`, so they land together:
- Model picker scoped to connected agents: discovery still probes
every installed CLI into `chat.discovered_models`, but the picker
lists only providers the user connected (`rebuild_available_models`,
re-run on connect-toggle + discovery). Connect state persists in
settings.json. The dropdown is height-capped, scrolls (wheel /
trackpad) with a thumb, and tints the hovered row.
- Chat input wraps: long input flows across up to 3 visible rows,
clipped + bottom-anchored, instead of overflowing the panel edge.
- perf: a canvas pan / zoom no longer marks the layout scene dirty
— it only moves the viewport transform, so re-running the taffy
layout solve + skia text measurement every drag frame was pure
waste. The repaint still re-applies the viewport.
Cmd+C / Cmd+V / Cmd+X did document node-clipboard ops regardless
of focus, so there was no way to paste a prompt into the AI chat
input. They now branch on chat focus: with the chat input focused
they read/write the OS text clipboard (`arboard`), otherwise the
node clipboard as before.
- `clipboard.rs` — thin best-effort arboard wrapper.
- `WidgetHostNative::chat_input_paste` / `chat_input_cut` — append
/ cut on the focused chat buffer.
A fresh launch seeded the demo `sample()` document — a Frame with
a 'Hello OpenPencil' title and a 'Click me' button group. New
`EditorState::starter()` returns just one empty Frame (selected),
and both hosts open with it; `sample()` stays as the widget-test
fixture. input_tests retarget their `n11` selections to the
starter frame's `n10`.
The casement crate was depended on through a sibling-repo path
(`../../../winit`) that only existed on the maintainer's machine,
so CI couldn't load the workspace manifest and every Rust Check
job died with "failed to read winit/Cargo.toml".
Vendoring it as a real submodule under `vendor/casement` (matching
the `vendor/jian` pattern, picked up by CI's `submodules:
recursive` checkout) closes that gap. The renamed GitHub repo
`ZSeven-W/casement` (was `ZSeven-W/winit`) tracks the `op-file-open`
branch — `feat(macos): drain_opened_file_urls` + the package rename
landed there as commit 5877fa83.
- `.gitmodules`: add vendor/casement.
- Root Cargo.toml: exclude vendor/casement from the workspace glob
(it's its own workspace).
- op-host-native + op-host-desktop: path = "../../vendor/casement".
Closes the last TS-vs-Rust parity gap — the Rust shell lacked the
TS `component-browser-panel.tsx` (UIKit library browser +
click-to-instantiate).
- op-editor-core: `uikit.rs` with `UIKit` / `KitComponent` /
`ComponentCategory` types + a built-in starter kit (6 components
spanning button/input/card/nav/layout/feedback) as PenNode
templates; `EditorState::instantiate_kit_component` deep-clones
with fresh ids and translates the whole subtree to the drop point
(children carry document-absolute coords).
- op-editor-ui: `component_browser_panel.rs` floating draggable
panel — header (close), category pills (filtered to non-empty),
3-col card grid with name + scaled preview rect; kit-id + search
filters applied.
- op-host-native: paint at §11.5 (below the Design-MD panel),
`dispatch_component_browser_press`, drag lifecycle, shared
`over_topmost_panel` helper covers both top-most panels across
wheel / pan / right-press / cursor_hint / layer-hover / 4
overlay-hover blocks + align hover + stale-hover clear.
- op-host-desktop: View-menu toggle, `drain_component_browser_insert`
places at the viewport centre.
- op-editor-core: `active_children`/`active_children_mut` /
`ensure_pages` symmetric `pages: Some([])` fallback —
inserts land in `doc.children` and survive a subsequent
`add_page` (which migrates them into Page 1).
- op-i18n: new `componentBrowser.empty` × 15 locales.
Bumps vendor/jian for the rebased `DesignMdSpec` schema + the
`jian pack` designMd-filter for packaged apps.
`document_fingerprint` only sees the committed document, so an
in-progress text-input draft — a half-typed property field or
variable-row value — was invisible to the pull's edit detection and
to `document_is_dirty`. A reload then dropped the draft silently.
Add `WidgetHostNative::commit_pending_input_pub` (commits property +
variable-row focus) and call it before the reconciliation checks:
in `confirm_document_reload` (covering pull / branch switch / merge),
before the during-pull edit comparison in `poll_git_pull_job`, and
in `save_tracked_document`. A pending draft now counts as an edit
and is saved / discarded / kept by explicit choice.
The floating Git panel — opened from the View menu — shows branch,
working-tree status and recent commits, and offers commit / refresh /
pull plus one-click branch switching. Clicking the status line, a
commit row or a conflicted file opens an in-panel scrollable
unified-diff viewer (the panel widens to 620 px with ▲/▼/✕ controls
and per-line colouring). Each non-current branch row carries a "⤵"
button that requests an isolated worktree merge.
GitPanelState / GitDiffView / GitPanelAction are plain data on
op-editor-core so the widget layer stays wasm-clean — it never calls
git itself. Diff rendering is split into git_panel_diff.rs and the
native press dispatch into git_press.rs to honour the 800-line cap;
the panel is hit-tested before the right-rail blocks so its wide
diff mode cannot lose clicks to the property rail underneath.
`casement` is ZSeven-W's winit fork (sibling repo, referenced by
local path for now). It adds the macOS open-documents Apple-event
hook that upstream winit lacks, needed for Finder double-click open.
The `package = "casement"` key keeps the `winit` import name so all
`use winit::…` stays unchanged.
glutin-winit is dropped: it hard-depends on the upstream `winit`
package, which would pull a second, incompatible winit into the
tree. Its only use — GlWindow::build_surface_attributes — is replaced
by a direct glutin SurfaceAttributesBuilder call in provider.rs.
Restructure the floating AI chat transcript from flat text bubbles
into a structured view:
- ChatMessage carries thinking text, tool calls and images plus
per-block collapsed flags and a streaming flag. Image ids come
from a process-global counter so a fresh ChatState cannot collide
with the native decode cache.
- ChatPoll splits provider deltas into text / thinking / tool_calls;
apply_poll_to_message folds them into the in-flight assistant
message and clears streaming on the terminal Done. The no-provider
error path also ends the stream and drops staged attachments.
- New RenderBackend::draw_image (default no-op) backed by a bounded
FIFO decode cache + aspect-fit in the native skia backend; web
degrades to a framed placeholder.
- New ai_chat_transcript widget: deterministic layout shared by
paint and hit-test (no live text measurement), collapsible
thinking / tool-call blocks, a streaming caret + typing-dot
animation, and image thumbnails.
Reviewed with Codex (3 rounds to clean).
Addresses the remaining round-4 codex findings (the `short_src`
byte-slice panic + the arc-handle reverse-iteration fixes already
landed via an earlier sweep).
- `cmd_set_ellipse_arc` clamps `sweep_angle` to ±360° — an API /
MCP sweep beyond a full turn just over-draws; it now persists a
sane single-revolution value.
- Path hit-test (`point_in_node`) follows the flattened, bezier-
aware outline instead of the bounding box: a curved or thin path
no longer selects empty bbox space, a zero-height stroked path
stays clickable, and a filled closed path is hittable across its
interior (new `point_in_polygon` even-odd test).
- The viewport-less `apply_release` now commits / clears
`path_anchor_drag` + `arc_handle_drag` (parity with
`apply_release_with_viewport`) so a drag can't leak across that
release path.
op-editor-core 242 / op-editor-ui 157 / op-host-native 21 tests green.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
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.
ChatRequest gains an attachments field; ChatState carries a per-turn
thinking mode, effort level and staged attachments (capped at 4 files /
5 MiB, attachment-only sends allowed). The chat panel grows a controls
row (thinking / effort / attach) and a dedicated attachment-chip row,
with hit-test and paint sharing one input-block origin.
All five providers consume the knobs — Claude maps thinking onto the
SDK token budget, Copilot onto reasoning_effort, the CLI / built-in
transports prepend an in-band directive, the HTTP transport adds body
fields; attachments spill to a private per-turn temp dir (cleaned on
drop) or inline base64. Also wires op-ai-skills into the built-in
provider and op-acp in as the AcpProvider chat backend.
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)
Completes pen bezier-handle editing on top of the Stage-1 data model.
- canvas_viewport_paint: `flatten_path` — Path nodes with anchor
control handles render as tessellated cubic Beziers (16 steps /
segment); handle-free paths keep the straight `points` polyline.
- canvas_viewport: the Pen-tool anchor overlay now draws each
anchor's two control handles (line + dot), with a faint "ghost"
dot offset from the anchor when a handle is unset — grab it to
create the handle. `path_handle_positions` resolves real / ghost
handle positions, shared with the host hit-test.
- op-host-native: `path_anchor_hit` now distinguishes anchor body
vs handle_in / handle_out (`AnchorDragTarget`); `PathAnchorDragState`
carries the target, the anchor's fixed position, and the Shift
state. The move handler drags the anchor or a handle — a handle
drag sets the anchor's point type on first motion (Shift =
independent/broken, else mirrored/smooth) so `set_path_anchor_handle`
mirrors the opposite handle. Release commits history only on
actual motion.
op-editor-ui 148 / op-host-native 64 / op-host-desktop 21 /
op-pen-loader 21 tests green (+3 flatten-path units).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Adds the canvas drag handles that author an ellipse arc — start
angle, sweep (end) angle, and the donut inner-radius.
- op-editor-ui: `ArcHandle` enum + `arc_handle_positions` (doc-space
positions of the 3 handles for an Ellipse scene node), re-exported
from `widgets`. The canvas overlay paints them as filled
primary-tinted dots for a single-selected Ellipse with the Select
tool.
- op-host-native: `ArcHandleDragState` + `arc_handle_hit` (checked
before the resize handles since the sweep grip can overlap the
right-mid resize handle). Press starts the drag with a history
snapshot; each move recomputes start/sweep/inner from the cursor
via `arc_drag_command` and re-applies `SetEllipseArc` (no-history
apply); release commits the snapshot only when the arc changed.
Dragging the start handle keeps the end fixed; a sweep collapsing
to 0 snaps to a full 360° circle.
op-editor-ui 145 / op-host-native 64 / op-host-desktop 21 tests
green (+2 arc-handle-position units).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Completes the Effects-controls gap — each effect row now exposes its
editable scalar parameters:
- op-editor-core: `EffectField` enum + `EditorCommand::SetEffectParam`
+ `cmd_set_effect_param` — writes one shadow param (offset X/Y,
blur, spread) or a blur/background-blur radius; blur values clamp
to >= 0, field/effect mismatches reject.
- panels: each effect block paints a parameter row per field —
`<label> <value> [-] [+]`; the "-"/"+" steppers emit
`AdjustEffectParam` (the walker computes the post-step value from
the current one). `effects_section_height` / the action-rect
walker now take the effects slice so the variable per-kind block
height stays aligned with paint (`VisibleSections.effect_count`
retired in favour of the slice).
- both hosts dispatch `AdjustEffectParam` via `SetEffectParam`,
history-committed.
op-editor-core 233 / op-editor-ui 140 / op-host-desktop 64 tests
green.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
The property panel's Effects section painted only a header + the
"+" add affordance. It now lists the selected node's effects:
- op-editor-core: `node_effects` read accessor exposes a node's
`PenEffect` slice (container kinds via `container`, leaf shapes
directly).
- panels: `NodeSnapshot.effects` carries an `EffectSummary` per
effect; `paint_effects_section` paints one row per effect (type
label + a "✕"); `VisibleSections.effect_count` + the shared
`effects_section_height` keep paint and the action-rect walker's
y-math aligned through the now-variable-height section.
- `PropertyPanelAction::RemoveEffect(index)` — both hosts dispatch
it through `EditorCommand::RemoveNodeEffect` (history-committed).
Stage 1 of the Effects-controls gap (rows + add + remove); the
per-effect shadow/blur parameter inputs follow. op-editor-core 227 /
op-editor-ui 161 / op-host-desktop 64 tests green.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Completes the smart-guides gap on top of the align_guides geometry:
- editor: EditorUiState.active_guides — transient guide lines for the
current drag (view-only, never serialized / undone).
- host: apply_smart_guides() runs after each node-drag translate —
gathers the moving + sibling AABBs off the layout scene, calls
compute_alignment_guides, applies the snap offset, stores the guide
lines; drag release clears them.
- canvas: paints the active guide lines (magenta) over the nodes.
The "grid" half of the matrix row was already implemented
(canvas_viewport::paint_grid). op-editor-ui 140 + op-host-desktop 64
tests green.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
The op-* crate reorg never ran `cargo clippy -- -D warnings`, so the CI
lint gate failed. Fix every violation surgically: real fixes for
mechanical lints (needless_range_loop, derivable_impls, ptr_arg,
needless_borrow, doc_lazy_continuation, unused_imports, manual_find,
collapsible_match, never_loop, dead_code, complex types via type
aliases) and scoped `#[allow]` for intrusive ones (too_many_arguments
on paint helpers, result_large_err where ToolOutcome / PenNode payloads
are deliberately the Err type).
Phase 7.3 strangler reorg — rename the native widget host and the
desktop runner crates to the op- prefix. The desktop+native merge was
declined: keeping the library / binary split preserves the mobile-
checkable op-host-native lib (cargo check -p op-host-native on iOS /
Android, relied on by check-jian-boundaries.sh + the CI mobile job),
which a folded-in winit binary would break. A clean separate rename is
purely mechanical and the brief permits it.
- openpencil-shell-native -> op-host-native (lib op_host_native)
- openpencil-desktop -> op-host-desktop crate; the shipped executable
keeps the stable openpencil-desktop [[bin]] name so release
artifacts + external CLI integrations are unaffected
- every openpencil_shell_core:: path -> op_editor_ui::
- every openpencil_shell_native:: path -> op_host_native::
- doc-comment / manual-smoke note refs updated