Commit graph

1013 commits

Author SHA1 Message Date
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
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 0b545e8b20 refactor(canvas): flip CanvasViewport paint onto LayoutScene + EditorState
The PAINT path of CanvasViewport was the last widget still consuming
shell-core's Document. It now reads viewport / selection / tool /
pen-draft from op_editor_core::EditorState and walks the layout-
resolved SceneNode tree from a LayoutScene instead of Document Nodes.

- CanvasViewport::from_document → from_editor(&EditorState, &LayoutScene).
- Per-kind painter ported into a new canvas_viewport_paint.rs module
  (walks SceneNode; fills are pre-$ref-resolved so no var_table lookup)
  to keep canvas_viewport.rs under the 800-line cap.
- Overlay painter migrated: paint_fill_then_stroke takes &SceneNode,
  paint_pen_rubber_band reads pen-draft + scene geometry.
- Both hosts cache a layout_scene field rebuilt by
  editor_state_to_layout_scene alongside paint_doc when
  editor_state_dirty is set.
- SceneNode gains find / aggregate_bounds derivation helpers
  (pure geometry over existing fields) for the selection overlay.

The host-input hit-test helpers rotation_corner_at_point /
selection_handle_at_point keep &Document — they serve the input path,
migrated by a later task.
2026-05-16 21:49:29 +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 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 9049946794 refactor(panels): migrate LayerPanel onto op-editor-core EditorState
LayerPanel now walks the canonical PenNode tree on EditorState.doc
instead of shell-core's derived Document. The page rows, the
depth-flattened layer rows, collapse state (editor_ui.collapsed_layers),
selection, hover and inline-rename drafts all read from EditorState.

The widget keeps a shell-core document::NodeId in LayerItem / the
hit-test surface — both id types are string newtypes, so the walk-time
conversion is lossless and the hosts' input/hit-test path stays
untouched. Constructors renamed from_document* -> from_editor*; both
hosts' paint passes and the input-path hit-test sites updated.
2026-05-16 21:12:31 +08:00
Kayshen-X 62c5f02f0a refactor(editor): migrate AIChat panel onto op-editor-core chat state
`AIChatPlaceholder` + the model-picker dropdown now read op-editor-core
types directly:

- `ChatState` / `ChatRole` / `ChatMessage` / `ModelEntry` /
  `AgentProvider` swap from shell-core (`document::*` /
  `chat_models::*` / `agent_settings_state::*`) to
  `op_editor_core::chat::*`.
- `AIChatPlaceholder::from_editor[_at]` takes `&EditorState`, reading
  `state.chat` + resolving chrome labels through `editor_state_ext`'s
  `translate` / `theme_for` instead of `Document::t` / `Document::theme`.

Both hosts' paint + click + press call sites feed `&EditorState`. The
desktop model-discovery worker keeps emitting shell-core `ModelEntry`s,
already translated to op-editor-core's at the `editor_state.chat`
write boundary in `model_discovery.rs` — no change needed there.
2026-05-16 20:59:39 +08:00
Kayshen-X 9fe4ef5192 refactor(editor): migrate AgentSettings modal onto op-editor-core state
The five agent-settings widget files now read op-editor-core types:

- `AgentSettings` / `AgentSettingsTab` / `McpCli` / `McpServer` /
  `AgentProvider` swap from `crate::document::*` to
  `op_editor_core::agent_settings::*` (and `chat::AgentProvider`).
- `AgentSettingsPanel` takes `&EditorState`; the four tab modules +
  `agent_settings_i18n::t` thread `&EditorUiState` instead of
  `&Document`, reading `locale` + `settings_input_draft` directly.

Both hosts' construction + hit-test call sites feed `&EditorState`;
the `SelectTab` / `Connect` / `ToggleMcpCli` / `nav_at` paths drop
their shell-core->op-editor-core enum translation shims since the
widget hit enum now carries op-editor-core types end to end.
2026-05-16 20:55:51 +08:00
Kayshen-X 7a4f958ff5 refactor(editor): migrate four modal widgets onto op-editor-core state
ColorPicker, FigmaImport, LayerContextMenu and VariablesPanel now read
op-editor-core types directly instead of shell-core's `Document`:

- ColorPicker / LayerContextMenu take `&EditorState`, swapping
  `ColorPickerState` / `ColorPickerDrag` / `LayerContextMenuState` /
  `LayerContextTarget` to their op-editor-core equivalents. `drag_for_hit`
  returns op-editor-core's `ColorPickerDrag`, dropping the host-side
  shell-core->op-editor-core translation shim.
- FigmaImport stores the active `Locale` directly off `EditorUiState`.
- VariablesPanel reads `doc.variables` / `doc.themes` +
  `ui.variables.active_theme`, snapshotting owned row / chip / theme
  data at construction.

Construction + hit-test call sites updated in both hosts' widget_host
modules to feed `&EditorState` instead of the derived `paint_doc`.
2026-05-16 20:48:55 +08:00
Kayshen-X 3e9f2bb26e refactor(editor): migrate ExportDialog onto EditorUiState
ExportDialog::paint now reads op_editor_core::EditorUiState
(export_format + export_scale) instead of shell-core's Document.
A doc_export_format converter bridges the state-layer ExportFormat
to the widget-layer enum. Native host paint site updated.
2026-05-16 20:33:20 +08:00
Kayshen-X 0567e4f9f0 refactor(editor): migrate chrome-leaf widgets onto EditorState
TopBar / StatusBar / Toolbar / ShapePicker / LocalePicker / FileMenu /
AlignToolbar now read op_editor_core::EditorState (or the narrowest
sub-struct each needs) instead of shell-core's Document. A new
widgets/editor_state_ext.rs provides the theme()/t() derivations and
the Tool / ShapeChoice / AlignAction / FileMenuChoice converters.
Both hosts' paint + hit-test construction sites updated to pass
&self.editor_state.
2026-05-16 20:27:44 +08:00
Kayshen-X e9960e6944 refactor(editor): make EditorState the web host's single source of truth
Mirror the native host migration for `openpencil-shell-web`. The web
`WidgetHost` now holds an `op_editor_core::EditorState` authoritatively
instead of a shell-core `Document`. The ~30 shared widgets stay
`&Document`-bound and read-only — they are fed a derived `paint_doc`
snapshot rebuilt lazily by `refresh_paint_doc()` whenever
`editor_state_dirty` is set.

- Every mutation routes through an `op-editor-core` mutator + flags the
  dirty bit; the snapshot re-derives once before paint / before any
  hit-testing input event.
- Hit-test-then-mutate handlers refresh, hit-test on `&paint_doc`,
  extract owned results, then mutate `editor_state`; shell-core hit
  enums translate via `op_pen_loader::rev::*`.
- `paint` takes `&mut self` to drain the cache at the top of the pass.
- `op-editor-core` / `op-pen-loader` are optional deps behind the
  `skia` feature (matching `skia-safe`) so the skia-free wasm32 CI stub
  baseline stays clean — `op-pen-loader` pulls `jian-skia` transitively.
- Fix two pre-existing `--features skia` build blockers in `lib.rs`
  (missing `Performance` web-sys feature, `canvas` moved before reuse).

No web-editor behaviour change — feature parity preserved.
2026-05-16 20:13: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 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
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 b9d7032474 refactor(host): scoped EditorState bridge on WidgetHostNative
Phase 6 strangler scaffolding: WidgetHostNative now holds an
op_editor_core::EditorState alongside the legacy shell-core Document
so the ~30 widgets can migrate onto the canonical model one group at
a time while the workspace stays build-green.

The bridge is intentionally minimal — Document remains the source of
truth (every un-migrated widget paints from it and every apply_*
mutates it), and editor_state is not yet wired into paint or input.
There is no Document -> PenDocument converter, and the desktop
pen_doc_adapter only goes the other way (baking flex layout into AABB
rects, which is irreversible), so a per-frame round-trip would be
unsound. Each later 6.x task adds its own per-group sync point as it
switches a widget group's paint/input onto editor_state.

Field, accessors and dependency are all marked TEMPORARY — deleted in
Phase 7 when Document dies and editor_state becomes the host's only
state.
2026-05-16 17:30:44 +08:00
Kayshen-X 8078a70d8c refactor(figma): port figma import onto PenNode
FigmaClipboardNode::to_node now produces canonical
jian_ops_schema::PenNode instead of shell-core's flat Node. Maps the
8 common Figma kinds onto Frame/Group/Rectangle/Ellipse/Line/Polygon/
Path/Text variants; unmapped kinds fall back to Frame with the
original kind preserved in base.role since PenNode has no Other
escape hatch. Tests ported to assert on PenNode variants + base
accessors. The import stays in shell-core (Option B, in-place port).
2026-05-16 17:21:35 +08:00
Kayshen-X fa6fe21ed5 refactor(codegen): port codegen onto PenDocument 2026-05-16 17:15:44 +08:00
Kayshen-X 39162ad08e refactor(mcp): port MCP server onto op-editor-core EditorCommand
Port the in-process MCP server off shell-core's legacy `Document` /
`McpCommand` onto `op_editor_core::EditorState` / `EditorCommand`. The
module physically stays inside `openpencil-shell-core` (an `op-mcp`
crate extraction is a later Phase-7 task).

- Read tools snapshot `EditorState` (canonical `PenDocument`); write
  tools emit `op_editor_core::EditorCommand` applied via
  `EditorState::apply`. Node ids are now canonical `.op` schema
  strings, not the old `u64`.
- Component commands surface a clean `ToolFailed` "known gap" error —
  `op-editor-core` has no component registry yet. Same for
  `set_node_collapsed` (`NodeFlag::Collapsed` has no schema field).
- `mcp_serve.rs` loads the `.op` file straight into an `EditorState`
  (plain `jian-ops-schema` deserialization) and saves the
  `PenDocument` back on every successful write.
- Delete the orphaned legacy apply path (`document/mcp_apply*.rs` +
  `Document`/`VariableTable::apply_mcp_command`); the widgets that use
  `Document` are untouched.
- Split `tools.rs` → `tools.rs` + `read_tools.rs` and
  `component_tools.rs` → `component_tools.rs` + `page_tools.rs` to
  hold the 800-line cap.
2026-05-16 17:07:42 +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 8f8b36b76f docs: fix stale i18n + renderer paths flagged by Codex review 2026-05-16 15:45:54 +08:00
Kayshen-X 01731d6af8 refactor(document): migrate test code to string NodeId 2026-05-16 15:42:46 +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
Kayshen-X 0ab38e1ca0 fix(i18n): point convert-locales.py output at the op-i18n crate 2026-05-16 14:25:58 +08:00
Kayshen-X 65e2dd9ebb chore: bump vendor/jian to the clippy-clean commit 2026-05-16 14:17:31 +08:00
Kayshen-X 08686c3b95 refactor(i18n): extract op-i18n crate (with Locale) from openpencil-shell-core 2026-05-16 14:13:38 +08:00
Kayshen-X 1ef513d5a6 chore: bump vendor/jian to the relocated-renderer commit 2026-05-16 13:57:08 +08:00
Kayshen-X 182ec78416 docs: fix stale anthropic-agent-sdk path in chat_claude.rs comment 2026-05-16 13:20:02 +08:00
Kayshen-X 5b6cf39775 chore: vendor anthropic-agent-sdk out of crates/ 2026-05-16 13:14:20 +08:00
Kayshen-X 764435b500 fix(ci): bump CI toolchain pins to Rust 1.94, correct README crate table 2026-05-16 13:05:40 +08:00
Kayshen-X e3a163dc2d fix(ci): scrub remaining refs to the deleted stub crates
Codex review of Task 1.2 found four dangling references to the deleted
stub crates (pen-types/core/engine/codegen/figma + openpencil-app):

- package.json: drop the five -p <crate> args from cargo:wasm-check
- rust-release.yml: restore the build job, re-pointed at the real
  openpencil-desktop crate so release-draft has artifacts to publish
- check-jian-boundaries.sh: 4 -> 3 invariants in the success message
- README.md: remove the deleted crate rows from the crate-list table
2026-05-16 12:56:39 +08:00
Kayshen-X 0a90f13aae chore: delete dead pen-* stub crates + openpencil-app 2026-05-16 12:45:07 +08:00
Kayshen-X 76c598a23d feat(ai): adopt the official github-copilot-sdk for the Copilot bridge
Replace the in-workspace copilot-sdk fork with the official
`github-copilot-sdk` crate (crates.io 0.1). Rewrite chat_copilot.rs
against its API: Client::start + a streaming SessionConfig whose
SessionHandler forwards `assistant.message_delta` / `session.error`
events into the ChatProvider channel, one client+session per turn.
Delete the crates/copilot-sdk fork directory. Build + 72 desktop
tests pass on the Rust 1.94 toolchain.
2026-05-16 10:26:44 +08:00
Kayshen-X 4a604bfe16 chore: bump workspace toolchain to Rust 1.94
The official github-copilot-sdk requires rust-version 1.94 +
edition 2024; the workspace was pinned to 1.85. Bump the toolchain
so the SDK can be adopted for the Copilot connection. Verified:
full native build, wasm32 web check, and shell-core tests all pass
on 1.94.1 with no new errors.
2026-05-16 10:05:27 +08:00
Kayshen-X 542ec113d1 feat(panels): wire the Effects section "+" to add a drop shadow
The property panel's 效果 header drew a "+" icon that did nothing.
Add a PropertyPanelAction::AddEffect, emit its hit rect over the
icon in the layout walker (no paint/layout change — the icon was
already painted), and dispatch it in both hosts to
Document::add_drop_shadow_to_selected. Clicking "+" now appends a
drop shadow that renders + persists.
2026-05-16 10:00:50 +08:00
Kayshen-X c0f503e99a feat(document): add_drop_shadow_to_selected mutator
Append a default drop-shadow effect (CSS-style 0 4px 8px
rgba(0,0,0,0.25)) to the selected node — the building block the
property panel's 效果 "+" action will call. Guarded on a real,
editable selection like the sibling set_selected_* mutators.
2026-05-16 03:27:52 +08:00
Kayshen-X 6e3404dcab feat(canvas): gaussian-blur drop-shadow on the web backend
Mirror the native fill_drop_shadow override on the wasm web
backend with skia MaskFilter::blur so drop-shadow effects paint as
a real soft shadow on both native and web — keeping the two
shells visually consistent.
2026-05-16 02:47:53 +08:00
Kayshen-X 6292556ccb feat(canvas): real gaussian blur for native drop-shadow paint
The fill_drop_shadow RenderBackend primitive only had the default
un-blurred approximation. Override it on the native backend with
skia's MaskFilter::blur (CSS blur-radius → sigma = radius/2) so
drop-shadow effects paint as a real soft shadow on desktop.
2026-05-16 02:46:45 +08:00
Kayshen-X 7a90583073 fix(ai): parse CLI JSON-RPC responses robustly across stdio framing
The Copilot / Codex response parsers ran serde on the whole stdout
line, so a `Content-Length:` header prefix or a glued next-frame
header suffix would fail the parse. Add brace-counted (string- and
escape-aware) JSON-object extraction so discovery works whether the
CLI replies newline-delimited or Content-Length-framed.
2026-05-16 02:39:06 +08:00
Kayshen-X e87d69cec4 fix(ai): Content-Length-frame Copilot stdio JSON-RPC requests
The Copilot discovery wrote newline-delimited requests to
`copilot --stdio`; the official copilot-sdk frames requests with
`Content-Length` headers. The CLI happens to accept both, but
relying on that leniency is fragile. Write proper LSP-style
Content-Length frames so we speak the protocol the SDK defines.
Verified live against the installed `copilot` CLI.
2026-05-16 02:28:07 +08:00
Kayshen-X 3eca00a099 feat(ai): query Copilot models via the official CLI JSON-RPC protocol
Copilot discovery now drives `copilot --stdio` over JSON-RPC
(connect → models.list) — the same wire protocol (version 3) the
official github-copilot-sdk uses. Speaking the protocol directly
rather than linking the SDK crate keeps the workspace on Rust 1.85
(the SDK crate requires 1.94). Documented model names stay as the
fallback when the CLI is installed but the query doesn't answer.
2026-05-16 02:07:35 +08:00