Commit graph

36 commits

Author SHA1 Message Date
Kayshen-X cb9a0ae692 fix(ci): clear rust check failures 2026-05-24 00:29:43 +08:00
Kayshen-X 0948e97de9 feat(panels,canvas): editable gradients/effects + SVG/image import + locale-aware dialogs
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.
2026-05-23 23:11:38 +08:00
Fini 23c0f2e4d1 fix(workspace): unblock CI — host visual_ref_enabled + radial-gradient clippy allow + fmt catch-up
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.
2026-05-23 20:43:39 +08:00
Kayshen-X 8851746e5e feat(canvas): linear + radial gradient rendering with editable stops
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
2026-05-23 19:13:35 +08:00
Kayshen-X 46eeaef226 feat(panels): editable inputs (arrows, caret, fill-opacity, effect params) + paint polish
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.
2026-05-23 12:59:28 +08:00
Fini 1fa2d9415a Merge branch 's3a-orchestrator' into v0.8.0-new 2026-05-23 01:27:30 +08:00
Fini b1c3136deb feat(editor): EditorCommand::InsertSubtree — nested PenNode subtree insert
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.
2026-05-22 22:56:10 +08:00
Kayshen-X 6e8bc4c5d0 feat(panels): inline export pickers, panel scrolling, and row hover
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.
2026-05-22 22:56:09 +08:00
Kayshen-X bb4a989709 feat(desktop): native window chrome + per-agent TopBar icons
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.
2026-05-22 15:48:21 +08:00
Kayshen-X b1eec55fc2 perf(canvas): batch the dotted grid + honour fill_rect alpha
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.
2026-05-22 15:47:49 +08:00
Kayshen-X bb25ef512d feat(chat): connected-agent model picker, input wrap + pan perf
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.
2026-05-22 11:40:43 +08:00
Kayshen-X f1c45d6b9c feat(chat): OS clipboard copy/paste for the chat input
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.
2026-05-22 11:40:30 +08:00
Kayshen-X d1093b7dc4 feat(editor): open with a single empty starter frame
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`.
2026-05-22 11:40:08 +08:00
Kayshen-X 25e644dbc7 build(deps): vendor casement (winit fork) as a submodule
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".
2026-05-21 22:23:34 +08:00
Kayshen-X 2aacde0ce2 style: apply rustfmt to the workspace
cargo fmt --all -- --check was failing on CI after the recent
feature work landed unformatted (insert_<comp> MCP tools,
Component-Browser panel, Design-MD panel, op-i18n locale tables,
op-opmerge). Running cargo fmt --all touches 48 files; no behaviour
change.
2026-05-21 22:03:43 +08:00
Kayshen-X 1902d57807 feat(editor): add UIKit component-browser panel
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.
2026-05-21 21:24:12 +08:00
Kayshen-X 6e012049e4 feat(editor): add Design-MD panel and complete i18n coverage
Design-MD panel — closes the last TS-vs-Rust parity gap:
- New floating op-editor-ui design_md_panel + design_md_markdown
  renderer: collapsible theme/colour/typography/component/layout/
  notes sections, colour swatches, inline markdown highlighting.
- op-editor-core: parse_design_md parser + panel UI state.
- Host: View-menu toggle, top-most paint + hit-test, drag,
  .md import/export; bumps the vendor/jian DesignMdSpec pointer.

i18n coverage:
- Git panel, merge/conflict/error dialogs and GitError messages
  routed through op-i18n; git locale tables split into
  <base>_git.rs under the 800-line cap.
- Closed 10 app-wide hardcoded-string gaps (AI chat, Figma import,
  file menu, layer context menu, property-panel fill, update /
  file-picker / load-error dialogs, accesskit labels).
2026-05-19 22:29:22 +08:00
Kayshen-X f6212e9091 fix(desktop): flush input drafts before a Git-panel reload
`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.
2026-05-18 22:08:16 +08:00
Kayshen-X b8f030b3bb feat(desktop): in-app Git panel with branch, diff and merge UI
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.
2026-05-18 21:45:21 +08:00
Kayshen-X 0730230ddb feat(desktop): adopt the casement winit fork
`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.
2026-05-18 21:45:09 +08:00
Kayshen-X ebe0e0bf0f feat(ai): polish chat panel with streaming, tool-calls, thinking, images
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).
2026-05-18 08:23:31 +08:00
Kayshen-X 25e9739e57 fix(canvas): codex review round 4 — arc clamp, path hit-test, drag cleanup
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)
2026-05-17 23:00:56 +08:00
Kayshen-X 5b6874b7d8 style: apply rustfmt across the AI-subsystem crates
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.
2026-05-17 22:50:27 +08:00
Kayshen-X 4adfa38bae feat(ai): chat image attachments + per-provider thinking/effort
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.
2026-05-17 22:44:28 +08:00
Kayshen-X 0428c6e17b fix(canvas): codex review round 3 — sweep sign, path bounds, closed fill
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)
2026-05-17 19:31:41 +08:00
Kayshen-X 4794e323ac fix(canvas): codex review round 2 — drag history + closed paths
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)
2026-05-17 19:16:47 +08:00
Kayshen-X 442dbabb78 fix(canvas): codex review — rotation, arc hit-test, path bounds
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)
2026-05-17 18:07:11 +08:00
Kayshen-X 112d1f08bb feat(canvas): pen bezier handle editing — render, handles, drag
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)
2026-05-17 17:47:08 +08:00
Kayshen-X b7bcd9db12 feat(canvas): ellipse arc drag handles
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)
2026-05-17 17:26:37 +08:00
Kayshen-X 722445b017 feat(panels): shadow/blur parameter steppers in the Effects section
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)
2026-05-17 14:38:26 +08:00
Kayshen-X e30f3b12b4 feat(panels): effect rows + remove in the Effects section
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)
2026-05-17 14:26:19 +08:00
Kayshen-X 87e9655a59 feat(canvas): wire smart guides into node dragging
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)
2026-05-17 13:17:16 +08:00
Kayshen-X 6a29fb17c7 docs: translate remaining Chinese source comments to English 2026-05-17 09:33:41 +08:00
Kayshen-X 6f4ab59313 fix: clear clippy -D warnings across the workspace
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).
2026-05-17 01:26:29 +08:00
Kayshen-X 90c1cc4360 style: apply cargo fmt across the workspace 2026-05-17 00:26:15 +08:00
Kayshen-X 1e0b3cab7a refactor(rust): rename native + desktop hosts to op-host-* crates
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
2026-05-16 23:49:58 +08:00