Commit graph

25 commits

Author SHA1 Message Date
Kayshen-X 66b6330914 fix(canvas): render image fills on canvas 2026-05-23 23:48:11 +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
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 65af173d79 fix(canvas): follow the editor's active page in the layout scene
editor_state_to_layout_scene took active_page_index from the payload, which pen_document_to_payload hardcodes to 0 — so picking a page in the LayerPanel updated the panel but never switched the canvas. Read the live EditorState.ui.active_page_index instead, clamped to the page count.
2026-05-22 22:55:55 +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 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 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 bfbe3b161d feat(canvas): render ellipse arcs / pie / donut sectors
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)
2026-05-17 17:10: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 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 4abe80213f refactor(op-pen-loader): delete dead Document-bridge code
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.
2026-05-16 22:42:44 +08:00
Kayshen-X 16a31251b5 refactor(op-pen-loader): build LayoutScene directly from PenDocument
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.
2026-05-16 22:37:46 +08:00
Kayshen-X 5dde9e8741 refactor(host): hit-test input against LayoutScene, drop per-frame paint_doc
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.
2026-05-16 22:18:31 +08:00
Kayshen-X 9fb8a42036 feat(layout-scene): add layout-resolved render scene type + builder 2026-05-16 21:35:03 +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 302c9750d6 feat(op-pen-loader): bridge EditorState UI/chat/components into a paint Document 2026-05-16 18:34:27 +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 77f89b6ef8 refactor: extract op-pen-loader (canonical .op → Document) into a shared crate 2026-05-16 18:04:46 +08:00