Commit graph

39 commits

Author SHA1 Message Date
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 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 e8cf002a7f feat(mcp): expose UIKit components as insert_<comp> MCP tools
Closes the architectural piece of the "MCP element toolset" P1 gap
(TS pen-mcp ships ~100 add_card_*/add_toast_* element tools).

- op-editor-core: new `EditorCommand::InstantiateKitComponent`
  variant + applier branch that calls
  `EditorState::instantiate_kit_component` with the requested
  drop point (defaults to (0, 0)).
- op-mcp: `element_tools.rs` — `InsertKitComponent` per-component
  tool returning `OkWithCommand(_, InstantiateKitComponent)`;
  `insert_kit_component_tools(state)` walks every loaded kit;
  `element_tool_schemas(state)` emits the matching tools/list
  JSON. Tool names sanitize dashes: `insert_btn_primary`,
  `insert_card_basic`, etc.
- op-host-desktop: `rebuild_registry` chains the dynamic tools
  in; `tools_list_response` takes EditorState and appends the
  dynamic schemas next to TOOL_SCHEMAS.
- op-editor-core: tidies the empty-pages `ensure_pages` guard to
  `is_none_or`.

Result: 6 starter-kit components → 6 working MCP tools. The 100-tool
catalog parity is now a data fill-in (more components in op-editor-
core/uikit.rs auto-register as more MCP tools).
2026-05-21 21:55:44 +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 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 7b5cd150cc fix(ai): clear streaming flag on prior bubble when a new turn starts
A chat send fired while a turn was still streaming left the
interrupted turn's assistant bubble with streaming = true forever —
it never reached the terminal Done that clears the flag, so the
panel kept animating a caret on a stale message. begin_send now
clears every streaming flag before pushing the new bubble, keeping
the invariant that only the trailing assistant message streams.
2026-05-18 08:40:54 +08:00
Kayshen-X e775fd49c4 feat(desktop): winit shell platform integration
Add five native-platform features to the winit desktop host
(op-host-desktop), closing the gap with the Electron app:

- Native menu bar (muda) — File / Edit / View / Help plus the macOS
  app menu; selections route to the same host actions the keyboard
  shortcuts use. Gated to macOS / Windows — muda needs GTK, which
  this winit build does not link, so Linux keeps the in-canvas File
  menu.
- Auto-update — a background probe of the GitHub releases API
  reports status into the settings System tab; a found update
  offers to open the download page, and a "Check for Updates" menu
  item re-runs the probe.
- File association — argv parsing opens a .op / .pen document on
  launch; [package.metadata.bundle] declares the OS-level handler.
- Window-state persistence — position / size / maximized restore
  across restarts, with an off-screen guard for monitor changes.
- Drag-and-drop — dropping a .op / .pen file opens it.

Codex review round 1 findings (1 MAJOR + 3 MINOR) all addressed:
monitor-aware restore, failed-startup geometry guard, single-flight
update probe, case-insensitive extension match.

Also sink the agent-settings modal's hand-maintained EN/ZH string
table into the canonical 15-locale op-i18n tables, so the settings
chrome (including the new auto-update strings) is fully translated;
agent_settings_i18n.rs is now a thin op-i18n adapter.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-18 08:29:36 +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 e79ff82833 style: clear clippy lints for the workspace -D warnings gate
CI's Rust Check runs `cargo clippy --workspace --all-targets -- -D
warnings`; the fmt failure had masked it, so accumulated lints surfaced
once formatting was fixed. Resolve them:

- op-acp / op-ai-skills (this change set): while-let loop, redundant
  struct update, and `should_implement_trait` allows on the Option /
  infallible token parsers.
- Pre-existing in op-editor-core / op-figma, swept so the workspace
  gate is clean: redundant `drop`, `too_many_arguments` allow,
  collapsible `if let`, a type alias for a complex tuple, manual
  `Iterator::find`, and an `approx_constant` test value.

Lint fixes only — no behaviour change.
2026-05-17 23:12:16 +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 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 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 79b51573dd feat(editor): pen anchor bezier-handle data model + setters
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)
2026-05-17 17:33:44 +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 99a2140049 test(editor): split attribute-command tests under the 800-line cap
`command_tests.rs` had grown to 906 lines past the repo's 800-line
cap as the flip / ellipse-arc / node-effect command tests landed.
Move those per-node-attribute apply tests into a sibling
`command_attr_tests.rs` (both files now well under the cap); the
core CRUD / selection / variable apply tests stay in place.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-05-17 14:00:34 +08:00
Kayshen-X aca1af5447 feat(editor): add node-effect add/remove commands + MCP tools
Effects were inert — the schema carries `PenEffect` (Shadow / Blur /
BackgroundBlur) but nothing could add or drop one.

- editor: `EditorCommand::AddNodeEffect` / `RemoveNodeEffect` +
  appliers. `node_effects_slot` reaches the `effects` field on every
  variant that has one (Frame/Group/Rectangle via `container`, the
  leaf shapes directly); `add` appends a default-parameter effect,
  `remove` drops by index and clears the list to `None` when empty.
- mcp: `add_node_effect` / `remove_node_effect` tools, registered on
  the host server (catalog 80 -> 82).

This is the command + MCP layer of the Effects gap; the property-
panel editing UI is the remaining piece. op-editor-core 227 /
op-mcp 138 / op-host-desktop mcp tests green.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-05-17 13:48:31 +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 f65346c72e feat(editor): add smart-guide alignment geometry
`align_guides.rs` — pure alignment-guide computation for node
dragging: given the moving node's AABB + sibling AABBs it returns the
guide lines to paint and the snap offset that locks the drag onto the
nearest edge/centre alignment within a threshold. Two axes resolved
independently; closest candidate per axis wins, edge-to-edge
preferred on a tie.

This is the computation core of the smart-guides gap — fully unit
tested (6 tests). Host drag-handler wiring + canvas paint of the
returned guides follow as separate steps.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-05-17 13:07:50 +08:00
Kayshen-X c18ad5a776 feat: close six TS-parity gaps in the Rust shell
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)
2026-05-17 13:03:11 +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 8733f45e2d refactor(rust): dissolve openpencil-shell-core re-export shim
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
2026-05-16 23:54:33 +08:00
Kayshen-X a18ca7bbfe refactor: delete shell-core's document module
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.
2026-05-16 23:07:57 +08:00
Kayshen-X e9861a0e21 refactor(panels): migrate PropertyPanel onto op-editor-core EditorState
PropertyPanel now reads the selected node(s) off EditorState: the
NodeSnapshot is built from the canonical PenNode (geometry via
op-editor-core aggregate_bounds, fill/stroke via the fills helpers),
and the panel state (focus/draft/flex/size/fill-type/tab/export) reads
EditorState.ui + editor_ui. PropertyLabels::for_document becomes
for_editor_ui.

The widget keeps shell-core's document-typed NodeKind / PropertyFocus /
FillType / FlexLayout / PropertyTab in its paint + hit-test surface;
variant-identical converters were added to editor_state_ext so the
hosts' input/hit-test path is untouched. Added op-editor-core
fills::node_stroke_width for the Stroke section's scalar width.
Both hosts' paint passes and the input-path hit-test sites updated.
2026-05-16 21:20:42 +08:00
Kayshen-X 1b310c714a fix(editor): address host-migration review concerns (stale-doc hit-tests, dirty-mark, legacy load, view-state persist, fill payload)
- CursorMoved refreshes the derived paint Document before cursor_hint
  so a post-mutation / pre-paint cursor move can't read stale geometry.
- apply_cursor_move refreshes the paint doc up front so every hit-test
  inside it (align toolbar, color picker, layer menu, panel resize)
  sees a fresh document.
- commit_variable_row_focus_if_any routes every exit through one
  mark_dirty so an invalid variable edit can't leave the cache stale.
- Legacy private DocPayload files are detected and rejected with an
  explicit "saved by an older version, must be re-saved" message
  instead of a silent / opaque schema parse failure.
- active_page_index is persisted in a minimal .opmeta sidecar so it
  survives a save / load round-trip; the .op file stays canonical.
- set_primary_fill_type carries the existing fill body across a
  fill-type change (gradient stops / opacity / blend mode) instead of
  overwriting it with a default body, matching shell-core behaviour.
- collapsed_layers documented as intentionally view-only at the
  EditorSnapshot and editor_ui_state sites.
2026-05-16 19:53:08 +08:00
Kayshen-X f0bb56b6cd refactor(editor): make EditorState the native host's single source of truth
Flip WidgetHostNative off shell-core's Document onto
op_editor_core::EditorState. The host now owns one authoritative
state; every paint pass + hit-test reads a lazily-derived,
read-only Document snapshot (`paint_doc`), rebuilt via
op-pen-loader's pen_document_to_document + apply_editor_state_ui
whenever an `editor_state_dirty` flag is set.

Why: removes the dual-state strangler scaffold so the canonical
PenDocument model is the only editable state — input handlers
mutate EditorState, widgets stay read-only over the derived
Document. .op save now serializes editor_state.doc; load seeds
EditorState::from_document. shell-core's Document becomes a
paint-only target.

- op-editor-core: add host_support (EditorState::sample,
  create_node_for_tool, replace_paths_with_polyline) + re-export
  VariableScalar/VariableKind.
- shell-native: rewrite all 8 widget_host submodules onto
  editor_state; split press.rs -> press_helpers.rs and keyboard
  click routing -> click.rs to stay under the 800-line cap;
  boolean_ops becomes a pure compute_boolean_op committed back
  through EditorState.
- desktop: persistence / settings_io / chat_session /
  model_discovery / main / frame move onto editor_state accessors.
2026-05-16 19:29:34 +08:00
Kayshen-X 58346c2a9c feat(op-editor-core,op-pen-loader): host-migration prerequisites (fill-type, chat-model, layer-collapse, panel predicates, reverse enum bridge) 2026-05-16 18:54:04 +08:00
Kayshen-X a79dbaf0e3 feat(op-editor-core): close variable/theme gaps for the host migration
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.
2026-05-16 18:19:35 +08:00
Kayshen-X 88e155b324 refactor(op-editor-core): rename UiChrome → EditorUiState (drop the 'chrome' term) 2026-05-16 17:51:39 +08:00
Kayshen-X ec5ac70118 feat(op-editor-core): extend EditorState into a full state superset of Document 2026-05-16 17:47:53 +08:00
Kayshen-X 6cd2b5cd8c feat(op-editor-core): EditorCommand DTO + EditorState::apply 2026-05-16 16:32:44 +08:00
Kayshen-X 3e04f9a261 feat(op-editor-core): port color/align/variables/rename mutators 2026-05-16 16:21:12 +08:00
Kayshen-X 9d9862a099 feat(op-editor-core): port editing mutators onto EditorState 2026-05-16 16:10:36 +08:00
Kayshen-X 58a4ee58e0 feat(op-editor-core): EditorState on jian PenDocument 2026-05-16 15:54:32 +08:00
Kayshen-X 0174aba93a refactor(render): move RenderBackend trait into op-editor-core 2026-05-16 14:36:09 +08:00
Kayshen-X b67fe5fbcc chore: scaffold op-editor-core crate 2026-05-16 14:32:53 +08:00