Commit graph

1062 commits

Author SHA1 Message Date
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 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 418c4acf3b fix(figma): codex review round 2 — pre-validation allocation guards
Addresses the 4 BLOCKs from the second codex review.

- kiwi: schema field-count pre-allocation is capped at the remaining
  buffer size (a hostile count no longer forces a huge Vec alloc).
- kiwi: array-length validation + pre-alloc now use the *remaining*
  byte count rather than the total buffer length (tighter bound).
- zip_reader: the aggregate-size budget is checked against the
  central directory's declared uncompressed size *before* the entry
  is decompressed, not after.
- zip_reader: stored (uncompressed) entries are now subject to the
  per-entry MAX_ENTRY_SIZE cap, matching the deflate path.

op-figma 84 tests green; clean build.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-05-17 16:43:44 +08:00
Kayshen-X ac63c701e6 fix(figma): codex review round 1 — hardening + ZIP entrypoint
Addresses the first codex review of the binary `.fig` parser.

BLOCKs:
- kiwi: 64-bit varint now uses Kiwi's terminal-byte rule (eight
  7-bit groups then a final full-8-bit byte) — the old `& 127` mask
  on every byte corrupted u64 values above 2^56.
- kiwi: an invalid schema definition kind (> 2) is now rejected
  instead of silently treated as a message.
- kiwi: array decode rejects a length exceeding the buffer size —
  guards against a hostile zero-byte-element array spinning the
  decode loop billions of times.
- zip_reader: aggregate 2 GiB decompression budget + 10k entry cap
  on top of the existing per-entry limit (zip-bomb defence).

CONCERNs:
- detect_kind now recognises the `PK\x03\x04` ZIP magic as Binary —
  the common Figma export form (`canvas.fig` + `images/` in a ZIP)
  was being rejected before container.rs could unwrap it.
- resolve_style_references now also resolves style refs inside
  instance `symbolData.symbolOverrides` entries.
- kiwi: enum field type codes are no longer resolved (unused; kiwi
  writes 0) so a stray code can't reject a valid schema.

Plus a zip-wrapped end-to-end test + the misleading zstd test rename.
op-figma 84 tests green (+1); clean build.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-05-17 16:37:31 +08:00
Kayshen-X 155f9daee7 feat(figma): wire parse_fig binary pipeline + image resolver
Stage G — the binary `.fig` parser is now end-to-end functional;
`parse_fig` no longer returns NotYetImplemented for binary input.

- image_resolver.rs: resolve_image_blobs walks the converted document
  and replaces `__blob:N` / `__hash:HEX` image-fill placeholders with
  base64 `data:` URLs (MIME sniffed from magic bytes).
- lib.rs: new `parse_fig_binary(bytes, name, layout_mode) -> FigImport`
  runs the whole pipeline — container split → Kiwi decode → tree
  build → node conversion → image resolution. `parse_fig`'s Binary
  arm delegates to it; `FigParseError::NotYetImplemented` retired in
  favour of `Binary(String)`. Entry points re-exported.
- binary_e2e_tests.rs: assembles a real fig-kiwi container from
  scratch (hand-built Kiwi schema + data chunks, deflate-compressed)
  and asserts the full pipeline yields a PenDocument with the
  rectangle at the right position/size + canonical-JSON round-trip.

op-figma 83 tests green (+6); clean build, no warnings.

Closes the §2.1 P0 gap — Figma binary `.fig` parsing.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-05-17 16:23:19 +08:00
Kayshen-X 7f41eeeeda feat(figma): node converters + figma-to-document mapper
Stages D+E — ports converters/* + figma-node-mapper.ts.

- common.rs: ConversionContext, JS-rounding helpers, transform →
  position/rotation/flip extraction, corner-radius mapping,
  common_props, resolve_width/height, scale_tree_children,
  collect_image_blobs, SKIPPED_TYPES.
- node_build.rs: PenNode constructors (frame/group/rectangle/ellipse/
  line/path/text/ref) — fill the behaviour-trait slots with None.
- converters.rs: convert_node dispatch + per-type converters
  (frame/group/component/instance/rectangle/ellipse/line/text/
  vector); auto-layout child ordering, arc-data flip absorption,
  zero-size vector bounds derivation, stroke-only-outline handling.
- instance.rs: apply_instance_overrides (size-scale fast path +
  direct-GUID override/derived resolution + nested forwarding) +
  merge_symbol_props.
- node_mapper.rs: resolve_style_references (inline style refs) +
  figma_to_pen_document / figma_all_pages_to_pen_document /
  get_figma_pages / figma_node_changes_to_pen_nodes entry points.

Whole pipeline compiles clean; op-figma 77 tests green (+5).

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-05-17 16:17:26 +08:00
Kayshen-X 203e424950 feat(figma): document tree builder
Stage D part 1 — ports figma-tree-builder.ts.

- TreeNode: owned figma node + ordered children tree.
- build_tree: indexes node changes by guid, builds parent→children
  adjacency, materializes from the DOCUMENT root; children sorted
  descending by parentIndex.position (z-order). REMOVED / guid-less
  changes skipped; cyclic chains capped at MAX_TREE_DEPTH.
- build_tree_for_clipboard: orphan-rooted trees for clipboard data
  with no DOCUMENT wrapper.
- is_user_page (CANVAS, non-"Internal Only"), guid_to_string,
  collect_components (SYMBOL guid → fig_N id), collect_symbol_tree
  (SYMBOL guid → subtree).

op-figma 72 tests green (+4).

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-05-17 16:09:23 +08:00
Kayshen-X e18916c68c feat(figma): text mapper + vector geometry decoder
Stage F part 2 — ports figma-text-mapper.ts + figma-vector-decoder.ts.

- text_mapper.rs: map_figma_text_props → TextProps. Builds TextContent
  (plain | per-run StyledTextSegment[] from characterStyleIDs +
  styleOverrideTable, UTF-16-indexed), parses font weight from the
  style name (ordered substring match), line-height → multiplier,
  letter-spacing → px, align / vertical-align / growth enums, and
  applies textCase (UPPER / LOWER / TITLE).
- vector_decoder.rs:
  - decode_figma_path_blob — the opcode command stream (Z/M/L/Q/C,
    f32-LE operands, graceful truncation).
  - compute_svg_path_bounds — coordinate-pair bbox.
  - decode_figma_vector_path — geometry-blob path (stroke centerline
    preferred for stroke-only shapes).
  - decode_vector_network_blob — vertex/segment table fallback, chain-
    walked into M/L/C subpaths, scaled by nodeSize/normalizedSize.

op-figma 68 tests green (+16).

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-05-17 16:07:16 +08:00
Kayshen-X 1e32c6f6cf feat(figma): fill/stroke/effect/layout/sizing style mappers
Stage F part 1 — ports figma-color-utils.ts + figma-fill-mapper.ts +
figma-stroke-mapper.ts + figma-effect-mapper.ts + figma-layout-mapper.ts.

- color.rs: figma_color_to_hex (linear 0-1 → #RRGGBB[AA], JS
  round-half-up) + figma_color_opacity.
- mappers.rs:
  - map_figma_fills → PenFill[] — solid / linear (angle from the
    paint transform's column-0 direction) / radial-angular-diamond
    (cx/cy/r = 0.5) / image (`__hash:`/`__blob:` placeholder URLs);
    invisible + unmappable paints dropped.
  - map_figma_stroke → PenStroke — uniform vs per-side thickness,
    align/join/cap, dash pattern; stroke paints reuse the fill mapper.
  - map_figma_effects → PenEffect[] — drop/inner shadow, fore/
    background blur.
  - map_figma_layout → LayoutProps — stack direction, gap (skipped
    under space-between), per-side padding collapse, justify/align,
    clip-content default.
  - map_width_sizing / map_height_sizing → SizingBehavior — auto-
    layout fit-content / fill-container resolution per axis.

op-figma 52 tests green (+17).

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-05-17 16:02:11 +08:00
Kayshen-X ed8e96a01d feat(figma): decoded-file model + parseFigFile pipeline
Stage 3 — ports figma-types.ts + fig-parser.ts::parseFigFile.

- FigGuid / FigMatrix / FigColor / FigVec2: the geometric primitives
  worth typing, each extracted from a decoded FigValue object;
  FigGuid::to_key is the canonical `sessionID:localID` map key.
- BlobOrString: the `blobs` pool element (raw geometry bytes | string).
- FigmaDecodedFile { node_changes, blobs, image_files }.
- parse_fig_file: the full pipeline — fig_to_binary_parts → decode the
  Kiwi schema chunk → decode the data chunk against it → extract
  nodeChanges (with the fallback scan for a guid-bearing array) +
  blobs. Mirrors parseFigFile / extractBlobs.
- FigValue gains generic get_f64/get_str/get_bool/get_array accessors.

op-figma 35 tests green (+5).

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-05-17 15:56:56 +08:00
Kayshen-X aa72296be0 feat(figma): Kiwi schema decoder
Stage 2 of the binary `.fig` parser — ports the `kiwi-schema` subset
the format needs.

- ByteBuffer: LEB128 var-uint/int (32 + 64-bit, zigzag), Kiwi's
  exponent-rotated var-float (single-0-byte zero optimisation),
  NUL-terminated UTF-8 strings, length-prefixed byte blocks.
- decode_binary_schema: the self-describing schema chunk — definitions
  (enum/struct/message) + fields; native type codes resolve via
  `!code` into NATIVE_TYPES, definition codes by index (forward refs
  handled with a two-pass bind).
- FigValue: dynamically-typed decoded tree (the Rust stand-in for the
  TS untyped `any`) with get/as_f64/as_str/as_bytes/as_array accessors.
- decode_message: dynamic decoder (the `compileSchema` + `decode*`
  equivalent) — message id-prefixed fields, ordered structs, enum
  ordinal→name, `byte[]` as a raw block, MAX_DEPTH recursion guard.
  Roots at the `Message` definition like Figma's findDecoder.

Tested via an in-test Kiwi writer that round-trips schema + data
fixtures (enum/struct/message/arrays/byte-arrays/floats). op-figma
30 tests green (+7).

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-05-17 15:54:32 +08:00
Kayshen-X 3fd1f20540 feat(figma): binary .fig container + decompression layer
First stage of the Figma binary `.fig` parser port (op-figma was
clipboard-JSON only; binary returned NotYetImplemented).

- zip_reader.rs: hand-rolled minimal ZIP reader — EOCD scan, central
  directory walk, store (method 0) + raw-deflate (method 8) entries.
  Avoids the full `zip` crate dependency tree. 512 MiB per-entry cap.
- container.rs: ports `fig-parser.ts::figToBinaryParts` — unwraps the
  ZIP archive form (`canvas.fig` + `images/*`), verifies the
  `fig-kiwi` magic, splits length-prefixed chunks, decompresses each
  via the deflate→zstd→raw fallback chain (PNG payloads passed
  through). Output: decompressed parts + embedded image map.
- deps: flate2 (miniz_oxide pure-Rust backend), ruzstd, base64 — all
  license-clean + wasm32-buildable.

Modules are `allow(dead_code)` until `parse_fig` is wired in the
final stage. op-figma 23 tests green (+6).

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-05-17 15:50:15 +08:00
Kayshen-X 448b39ec6c refactor(panels): split effects paint into property_panel_effects.rs
`property_panel_sections.rs` reached 862 lines after the effect
parameter-stepper work — over the 800-line repo file gate. Move
`paint_effects_section` + `paint_effect_row` + `paint_effect_param_row`
into a new `property_panel_effects.rs` (128 lines) and re-export
`paint_effects_section` from `property_panel_sections` so callers
keep using `sections::*`.

`property_panel_sections.rs` is now 755 lines. No behaviour change;
op-editor-ui 140 / op-host-desktop 64 tests green.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-05-17 14:45:43 +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 74c16077c1 ci: drop dead cross machinery from release + multiplatform workflows 2026-05-17 10:28:46 +08:00
Kayshen-X 7bead20e14 ci: build linux-aarch64 on native ARM runner instead of cross 2026-05-17 10:24:29 +08:00
Kayshen-X a3025d550e ci: fix aarch64 dev package name (libfreetype6-dev) 2026-05-17 10:16:38 +08:00
Kayshen-X efcac6d408 ci: install aarch64 freetype/fontconfig in cross container 2026-05-17 10:12:41 +08:00
Kayshen-X 031cedabf4 ci: run rust multi-platform build on all branches 2026-05-17 09:53:42 +08:00
Kayshen-X 6a29fb17c7 docs: translate remaining Chinese source comments to English 2026-05-17 09:33:41 +08:00
Kayshen-X 9b8f4baf19 fix(ci): drop redundant windows CommandExt import + tokio feature ban
- chat_subprocess.rs: `std::os::windows::process::CommandExt` is unused
  on Rust 1.94 (`creation_flags` resolves without it, like the unix
  `process_group` twin) — rejected by clippy `-D warnings` on Windows.
- deny.toml: remove the tokio process/rt-multi-thread feature ban; the
  native agent runtime legitimately needs them and the native cargo-deny
  job tripped on it. wasm exclusion stays structural.
2026-05-17 02:01:05 +08:00
Kayshen-X 3a084dd94c fix(ci): drop STEP1A_REQUIRE_GPU so headless runners use the raster fallback 2026-05-17 01:49:56 +08:00
Kayshen-X 7adfbd2e4d fix(ci): trigger rust-check on vendor/deny/workflow changes 2026-05-17 01:39:11 +08:00
Kayshen-X 22806a8d27 fix(ci): disable autocrlf on the Windows runner so cargo fmt --check passes 2026-05-17 01:37:09 +08:00
Kayshen-X 7d1e54b6e8 fix: track skia-safe/src/docs/pdf_document.rs (was caught by a global docs/ ignore) 2026-05-17 01:36:11 +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 81e8e79b99 fix(ci): point op-host-desktop's agent dep at the vendor/agent submodule
The dependency referenced a sibling working copy (../../../agent-rs)
that only exists on a local dev machine, so cargo metadata failed in
CI. vendor/agent is the same agent-rs repo as a git submodule (CI
checks out submodules recursively) and is pinned to the identical
commit, so the build is unchanged.
2026-05-17 00:12:09 +08:00
Kayshen-X 4d3a201a51 ci: run rust-check + wasm-bundle-check on any branch push 2026-05-17 00:05:11 +08:00
Kayshen-X 08c07604d0 chore(ci): re-point tooling + CI workflows at the op-host-* crates
Phase 7.3 strangler reorg — update every reference to the renamed /
dissolved crates across the boundary scripts and CI workflows.

tools/:
- check-wasm-bundle.sh: openpencil-shell-web -> op-host-web; the
  wasm-bindgen output filenames follow the op_host_web lib name
- check-jian-boundaries.sh: shell-native -> op-host-native,
  shell-web -> op-host-web
- check-widget-boundary.sh: WEB_SRC + path-exclusion regexes ->
  crates/op-host-web; openpencil_shell_core::widgets ->
  op_editor_ui::widgets (the dissolved shim's real source crate)

.github/workflows/:
- rust-multiplatform.yml: wasm + mobile-check jobs -> op-host-web /
  op-host-native; the mobile shell-core clean check -> op-editor-ui
- rust-release.yml: cargo build -p openpencil-desktop ->
  -p op-host-desktop (the shipped executable name is unchanged)
- wasm-bundle-check.yml: -p openpencil-shell-web -> -p op-host-web

Cargo.toml skia-patch comment updated. Boundary checks pass; the
wasm-bundle gate cleanly skips while EMSDK is unset.
2026-05-17 00:01:35 +08:00
Kayshen-X 4b8e0ce956 refactor(rust): add op-app composition-root crate
Phase 7.3 strangler reorg — add op-app, the thin crate that names the
editor application's composition root.

Investigation found no shared host bootstrap left to extract: the
editor-UI composition (widgets, theme, layout scene) already lives in
op-editor-ui, and each host (op-host-native / op-host-web) owns only
platform-specific backend wiring. So per YAGNI op-app stays thin — it
re-exports op-editor-ui plus the per-platform host entry point behind
its target cfg, and documents the composition. If real cross-host
wiring later emerges, it lands here.

Builds green on both native and wasm32-unknown-unknown.
2026-05-16 23:57:03 +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 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
Kayshen-X 65c2aa055a refactor(rust): rename openpencil-shell-web host to op-host-web
Phase 7.3 strangler reorg — rename the web widget host crate to the
op- prefix. The crate's openpencil-shell-core dependency is repointed
to op-editor-ui (the real source crate for the widget facade / theme
/ layout scene / scene vars / render-backend / gesture types).

- crate name: openpencil-shell-web -> op-host-web
- lib name: openpencil_shell_web -> op_host_web
- every openpencil_shell_core:: path -> op_editor_ui::
- native skia.rs include_bytes! path follows the moved assets dir
- smoke harness + lib doc-comment refs updated
2026-05-16 23:42:49 +08:00
Kayshen-X 43cb0fdf7e chore: sync Cargo.lock for the op-* crate extractions
Record the op-codegen / op-figma / op-mcp / op-ai / op-editor-ui
package entries in the workspace lockfile.
2026-05-16 23:35:10 +08:00
Kayshen-X 5f68ab3c1a refactor: extract op-editor-ui crate
Relocate the widget facade (widgets/, including the Widget trait,
render primitives, the editor-UI compositions, the CanvasViewport
center canvas, the lucide icon drawer, and editor_state_ext), the
theme tokens, the layout-resolved render scene (layout_scene +
layout_scene_hit), and the design-variable aggregation (scene_vars)
out of openpencil-shell-core into a dedicated op-editor-ui crate.

The canvas widgets (canvas_viewport*) stay inside op-editor-ui rather
than splitting into a separate op-canvas crate: they depend on the
widgets/ siblings editor_state_ext + icons and on the Widget trait, so
a clean mechanical split is not possible — per the task's explicit
allowance not to force a fragile split.

op-editor-ui's lib.rs mirrors the old shell-core crate-root re-exports
(render_backend facade types + jian gesture types + the i18n alias) so
every intra-module `crate::Color` / `crate::theme` / `crate::widgets`
path resolves unchanged — a pure relocation with no path rewrites
inside the moved modules. openpencil-shell-core becomes a thin
re-export shim (`pub use op_editor_ui::{widgets, theme, ...}`) so the
hosts keep resolving `openpencil_shell_core::*` until the Task 7.3
host rename dissolves the crate. The widgets_static integration test
moves to op-editor-ui/tests with its imports rewritten. The widget
boundary script's reverse-check path is updated to the new crate.
No behaviour change; all tests move with their code.
2026-05-16 23:33:47 +08:00
Kayshen-X 0dc231a5e9 refactor: extract op-ai crate
Relocate the AI chat layer (chat_provider.rs, chat_models.rs,
agent_settings_state.rs) out of openpencil-shell-core into a dedicated
op-ai crate. These three modules are dependency-free transport-free
data shapes — the ChatProvider trait, the ModelEntry catalog type, and
the Cmd+, settings-modal state — so they form a clean wasm32-clean
leaf crate. A fresh lib.rs declares all three as modules; chat_models'
`crate::agent_settings_state::` path stays valid. openpencil-desktop's
chat_*.rs transports + model_discovery.rs now import `op_ai::*`. Pure
relocation, no behaviour change.
2026-05-16 23:24:29 +08:00
Kayshen-X e2e287c008 refactor: extract op-mcp crate
Relocate mcp.rs + mcp/* + mcp_tests.rs out of openpencil-shell-core
into a dedicated op-mcp crate. The MCP tool registry + JSON-RPC stdio
layer only depends on jian-ops-schema + op-editor-core (its tools
build on EditorState / EditorCommand). mcp.rs becomes the crate's
lib.rs; the mcp/ submodule files flatten to src/ so the `pub mod`
declarations resolve unchanged, and intra-module `super::` paths stay
valid. mcp_tests.rs becomes a #[cfg(test)] mod of lib.rs with its
`super::mcp::` paths rewritten to `crate::`. openpencil-desktop's
mcp_serve.rs now imports `op_mcp::*`. Pure relocation, no behaviour
change; 138 tests move with their code.
2026-05-16 23:21:17 +08:00
Kayshen-X 9a0112b9a9 refactor: extract op-figma crate
Relocate figma.rs out of openpencil-shell-core into a dedicated
op-figma crate. The .fig detection + clipboard-node parser only
depends on jian-ops-schema (op-editor-core's PenNodeExt is test-only,
so it becomes a dev-dependency). figma.rs becomes the crate's lib.rs;
no intra-crate paths to adjust. Pure relocation, no behaviour change.
2026-05-16 23:17:05 +08:00
Kayshen-X 0cb56ecd9e refactor: extract op-codegen crate
Relocate codegen.rs + codegen_targets.rs out of openpencil-shell-core
into a dedicated op-codegen crate. The code generators only depend on
jian-ops-schema + op-editor-core, so they form a clean leaf crate.
codegen.rs becomes the crate's lib.rs; crate::codegen:: paths in
codegen_targets.rs collapse to crate::. Pure relocation, no behaviour
change; tests move with their code.
2026-05-16 23:14:45 +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 43cbf2131d refactor(electron): migrate export renderers off Document onto LayoutScene
Rewrite raster (PNG/JPEG/WEBP), SVG and PDF export to consume the
layout-resolved `LayoutScene` instead of shell-core's `Document`. The
export call site builds the scene from the live `EditorState` via
`op_pen_loader::editor_state_to_layout_scene`, so jian's flex pass and
`$ref` fill resolution run once and the exported pixels match the
on-screen canvas painter. Rotation now pivots around `aggregate_bounds`
to match `canvas_viewport_paint.rs`; drop shadows render behind Frame /
Rect / Ellipse fills.

Drops the native host's dead `document()` / `derive_paint_doc` path now
that nothing derives a paint `Document`. Splits the SVG serializer into
`export/export_svg.rs` to stay under the 800-line cap.
2026-05-16 22:28:27 +08:00