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.
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
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
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
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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`.
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.
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.
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.
- 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.
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.
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.
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.
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).
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.