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
This commit is contained in:
Kayshen-X 2026-05-16 23:54:33 +08:00
parent 1e0b3cab7a
commit 8733f45e2d
14 changed files with 27 additions and 351 deletions

12
Cargo.lock generated
View file

@ -2409,21 +2409,11 @@ dependencies = [
"jian-ops-schema",
"jian-skia",
"op-editor-core",
"openpencil-shell-core",
"op-editor-ui",
"serde",
"serde_json",
]
[[package]]
name = "openpencil-shell-core"
version = "0.1.0"
dependencies = [
"jian-core",
"op-editor-core",
"op-editor-ui",
"op-i18n",
]
[[package]]
name = "option-ext"
version = "0.2.0"

View file

@ -16,5 +16,5 @@ jian-core = { path = "../../vendor/jian/crates/jian-core" }
op-i18n = { path = "../op-i18n" }
# render_backend.rs uses glam::Vec2 as Point2D (spec §5.2). Pinned to the
# same version openpencil-shell-core uses so both crates resolve one glam.
# same version op-editor-ui uses so both crates resolve one glam.
glam = { version = "0.29", default-features = false, features = ["std"] }

View file

@ -27,7 +27,7 @@ op-host-native = { path = "../op-host-native", version = "0.1.0" }
# old shell-core `Document`.
op-editor-core = { path = "../op-editor-core" }
# Phase 7 strangler reorg: the MCP tool registry + JSON-RPC stdio layer
# (`mcp.rs` + `mcp/*`) was extracted out of openpencil-shell-core into the
# (`mcp.rs` + `mcp/*`) was extracted out of the dissolved shell-core into the
# op-mcp crate; `mcp_serve.rs` registers tools through `op_mcp::*`.
op-mcp = { path = "../op-mcp" }
# Phase 7 strangler reorg: the transport-free AI chat data shapes
@ -37,7 +37,7 @@ op-mcp = { path = "../op-mcp" }
op-ai = { path = "../op-ai" }
# Canonical `.op` (PenDocument) → shell `Document` loader / adapter.
# Extracted out of this binary into a shared library crate so library
# crates (openpencil-shell-native) can reuse the conversion;
# crates (op-host-native) can reuse the conversion;
# `persistence.rs` keeps only the desktop-only `rfd` Save/Open + error
# dialogs.
op-pen-loader = { path = "../op-pen-loader" }

View file

@ -17,9 +17,10 @@ jian-core = { path = "../../vendor/jian/crates/jian-core" }
# `MeasureBackend` plugged into the layout engine. Desktop-side, so
# depending on skia is fine; this crate does NOT need to be wasm-clean.
jian-skia = { path = "../../vendor/jian/crates/jian-skia", features = ["textlayout"] }
# `Document` / `Node` / `VariableTable` etc. — the shell-core model
# this crate converts INTO.
openpencil-shell-core = { path = "../openpencil-shell-core", version = "0.1.0" }
# Phase 7.3 reorg: openpencil-shell-core dissolved. The layout scene /
# scene-var / render-backend facade this crate converts INTO now
# resolves through the op-editor-ui crate.
op-editor-ui = { path = "../op-editor-ui" }
# `EditorState` — the canonical editor-state superset this crate
# derives a paint-only `Document` (incl. its `var_table`) FROM. Lives
# in op-pen-loader, NOT op-editor-core, so op-editor-core stays free of

View file

@ -91,8 +91,8 @@ pub fn pen_document_to_payload(doc: &PenDocument) -> LoadedDoc {
/// the supported `VariableDefinition` variants; unknown future
/// `VariableKind`s round-trip via their `Color/Number/Boolean/String`
/// label since the enums are isomorphic.
pub fn build_var_table(doc: &PenDocument) -> openpencil_shell_core::scene_vars::VariableTable {
use openpencil_shell_core::scene_vars::{
pub fn build_var_table(doc: &PenDocument) -> op_editor_ui::scene_vars::VariableTable {
use op_editor_ui::scene_vars::{
ThemeAxis, ThemedValue, Variable, VariableKind, VariableTable, VariableValue,
};
let mut out = VariableTable::default();
@ -137,8 +137,8 @@ pub fn build_var_table(doc: &PenDocument) -> openpencil_shell_core::scene_vars::
fn map_scalar(
s: &jian_ops_schema::variable::VariableScalar,
) -> openpencil_shell_core::scene_vars::VariableScalar {
use openpencil_shell_core::scene_vars::VariableScalar;
) -> op_editor_ui::scene_vars::VariableScalar {
use op_editor_ui::scene_vars::VariableScalar;
match s {
jian_ops_schema::variable::VariableScalar::Bool(b) => VariableScalar::Bool(*b),
jian_ops_schema::variable::VariableScalar::Num(n) => VariableScalar::Num(*n),

View file

@ -4,7 +4,7 @@
use jian_ops_schema::node::PenNode;
use jian_ops_schema::style::PenEffect;
use openpencil_shell_core::layout_scene::{DropShadow, Effect};
use op_editor_ui::layout_scene::{DropShadow, Effect};
use serde::{Deserialize, Serialize};
/// Serializable mirror of `document::DropShadow`.
@ -50,7 +50,7 @@ fn shadow_payload_to_effect(s: &ShadowPayload) -> Effect {
offset_x: s.offset_x,
offset_y: s.offset_y,
blur: s.blur,
color: openpencil_shell_core::Color {
color: op_editor_ui::Color {
r: s.color[0],
g: s.color[1],
b: s.color[2],
@ -177,7 +177,7 @@ fn parse_hex(s: &str) -> Option<[f32; 4]> {
#[cfg(test)]
mod tests {
use super::*;
use openpencil_shell_core::Color;
use op_editor_ui::Color;
#[test]
fn parse_css_color_handles_rgba_and_hex() {

View file

@ -21,12 +21,12 @@
//! transforms on them are lossless format conversions (colour array →
//! struct, kind / fill-type string → enum).
use openpencil_shell_core::layout_scene::NodeKind;
use openpencil_shell_core::scene_vars::VariableTable;
use openpencil_shell_core::layout_scene::{
use op_editor_ui::layout_scene::NodeKind;
use op_editor_ui::scene_vars::VariableTable;
use op_editor_ui::layout_scene::{
LayoutScene, SceneFillType, SceneNode, SceneStroke, ScenePage,
};
use openpencil_shell_core::Color;
use op_editor_ui::Color;
use crate::editor_state_var_table;
use crate::payload::{DocPayload, NodePayload, StrokePayload};
@ -82,7 +82,7 @@ pub fn editor_state_to_layout_scene(state: &op_editor_core::EditorState) -> Layo
/// wins over the node's authored colour, mirroring the canvas
/// painter's `var_table.fill_for(id).or(node.fill)`.
fn node_payload_to_scene(node: &NodePayload, var_table: &VariableTable) -> SceneNode {
use openpencil_shell_core::{Point2D, Rect};
use op_editor_ui::{Point2D, Rect};
let node_id = op_editor_core::NodeId::new(node.id.clone());
SceneNode {
id: node.id.clone(),

View file

@ -1,7 +1,7 @@
//! Canonical `.op` (`PenDocument`) → `LayoutScene` loader / adapter.
//!
//! Extracted from `openpencil-desktop` so library crates (notably
//! `openpencil-shell-native`) can reuse the conversion without
//! Extracted from `op-host-desktop` so library crates (notably
//! `op-host-native`) can reuse the conversion without
//! depending on a binary crate.
//!
//! Three layers:
@ -13,7 +13,7 @@
//! - [`payload`] holds the `DocPayload` serde DTOs + the strict /
//! best-effort `load_canonical` parser.
//! - [`layout_scene`] re-shapes a resolved `DocPayload` into the
//! paint-only `openpencil_shell_core::layout_scene::LayoutScene`.
//! paint-only `op_editor_ui::layout_scene::LayoutScene`.
//!
//! This crate is desktop-side and may depend on skia; it does NOT
//! need to be wasm-clean. The `rfd` Save/Open dialogs + error
@ -63,10 +63,10 @@ pub use variables::{var_table_from_payload, var_table_to_payload, VarTablePayloa
/// any shell-core dependency to keep its `wasm32-unknown-unknown`
/// invariant.
///
/// [`VariableTable`]: openpencil_shell_core::scene_vars::VariableTable
/// [`VariableTable`]: op_editor_ui::scene_vars::VariableTable
pub fn editor_state_var_table(
state: &op_editor_core::EditorState,
) -> openpencil_shell_core::scene_vars::VariableTable {
) -> op_editor_ui::scene_vars::VariableTable {
use op_editor_core::NodeId;
// Persisted definitions + theme axes — `EditorState.doc` is a
// `PenDocument`, so `build_var_table` harvests them directly.

View file

@ -11,7 +11,7 @@
use std::collections::BTreeMap;
use openpencil_shell_core::scene_vars::{
use op_editor_ui::scene_vars::{
ThemeAxis, ThemedValue, Variable, VariableKind, VariableScalar, VariableTable,
VariableValue,
};
@ -219,7 +219,7 @@ pub fn var_table_from_payload(p: &VarTablePayload) -> VariableTable {
#[cfg(test)]
mod tests {
use super::*;
use openpencil_shell_core::scene_vars::VariableValue;
use op_editor_ui::scene_vars::VariableValue;
fn sample_table() -> VariableTable {
let mut t = VariableTable::default();

View file

@ -1,29 +0,0 @@
[package]
name = "openpencil-shell-core"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
description = "OpenPencil shell — re-export shim over op-editor-ui / op-editor-core / op-i18n (Phase 7 strangler reorg; dissolved in Task 7.3)"
[lib]
name = "openpencil_shell_core"
path = "src/lib.rs"
[dependencies]
# v19 pivot: re-export Jian render/geometry/scene types (spec §5.2). The
# `jian` module wraps jian-core; the crate root also re-exports
# `jian_core::gesture` event types.
jian-core = { path = "../../vendor/jian/crates/jian-core", version = "0.0.1" }
# Phase 3 strangler reorg: i18n extracted into op-i18n; re-exported as `i18n`.
op-i18n = { path = "../op-i18n" }
# Phase 4 strangler reorg: the wasm-clean RenderBackend trait + facade
# types live in op-editor-core; re-exported as `render_backend`.
op-editor-core = { path = "../op-editor-core" }
# Phase 7 strangler reorg: the widget facade + theme + layout scene +
# scene-var aggregation live in op-editor-ui; re-exported so the hosts
# keep resolving `openpencil_shell_core::widgets` / `::theme` / etc.
op-editor-ui = { path = "../op-editor-ui" }

View file

@ -1,41 +0,0 @@
//! Jian re-export module (spec v19 §2 / §1.2).
//!
//! `openpencil-shell-core` is a thin Jian wrapper — it re-exports
//! `jian_core::render::{DrawOp, Paint, TextRun, …}` through the OP layer for
//! shell-native's internal translation. geometry / scene types get a `Jian*`
//! prefix to avoid clashing with OP's own `Rect` / `Color` (`render_backend.rs`).
//!
//! **Contract (spec §5.2.1, line 746)**: inside the NativeBackend impl, the
//! v19 wrapper **must not let widget code see `jian_core::render::DrawOp`
//! directly** — widgets only call OP `RenderBackend` methods. These re-exports
//! are for **shell-native internal** translation, not for widget code.
// ────────────────────────────────────────────────────────────────────────────
// jian_core::render — DrawOp command buffer + Paint / TextRun / other draw descriptors
// ────────────────────────────────────────────────────────────────────────────
pub use jian_core::render::{
BorderRadii, DrawOp, GradientStop, ImageSource, LinearGradient, Paint, PathCommand,
RadialGradient, RenderCommand, ShadowSpec, StrokeOp, TextAlign, TextRun,
};
// ────────────────────────────────────────────────────────────────────────────
// jian_core::geometry — `Jian*` prefix to avoid clashing with OP `Rect`
// ────────────────────────────────────────────────────────────────────────────
/// Jian's axis-aligned rectangle (`euclid::Rect<f32>`).
/// OP's own `crate::render_backend::Rect` is for the widget facade; shell-native
/// translates OP `Rect` → `JianRect` internally (`origin/size` → `euclid::Rect::new`).
pub type JianRect = jian_core::geometry::Rect;
pub type Size = jian_core::geometry::Size;
pub type JianPoint = jian_core::geometry::Point;
pub type Affine2 = jian_core::geometry::Affine2;
// ────────────────────────────────────────────────────────────────────────────
// jian_core::scene — Color (packed u32) gets a `Jian*` prefix
// ────────────────────────────────────────────────────────────────────────────
/// Jian's packed-RGBA color (`pub struct Color(pub u32)`); distinct from OP's
/// `crate::render_backend::Color` (RGBA f32 quad). NativeBackend bit-packs when
/// translating OP Color → JianColor.
pub type JianColor = jian_core::scene::Color;

View file

@ -1,47 +0,0 @@
//! OpenPencil shell core — re-export shim.
//!
//! Per spec v19 §1.2 (FROZEN 2026-05-04): this crate must compile on
//! wasm32-unknown-unknown. winit / accesskit_winit / skia-safe live in
//! `openpencil-shell-native`; wasm-bindgen / web-sys / CanvasKit live in
//! `openpencil-shell-web`.
//!
//! Phase 7 strangler reorg: the widget facade, theme, layout-resolved
//! render scene, and design-variable aggregation were extracted into the
//! `op-editor-ui` crate; i18n into `op-i18n`; the `RenderBackend` trait
//! into `op-editor-core`; the MCP / codegen / figma / AI-chat modules
//! into their own `op-*` crates. What remains here is a thin re-export
//! shim so the hosts (`openpencil-shell-native` / `openpencil-shell-web`
//! / `openpencil-desktop`) keep resolving `openpencil_shell_core::*`
//! paths unchanged until the Task 7.3 host rename dissolves this crate.
//!
//! The [`jian`] module re-exports `jian_core::render::{DrawOp, Paint,
//! TextRun, …}` + geometry/scene aliases for shell-native's internal
//! translation (widget code never sees them).
pub mod jian;
// Phase 3 strangler reorg: i18n extracted into op-i18n. Re-exported as
// `i18n` so `crate::i18n::translate` / `crate::i18n::Locale` resolve.
pub use op_i18n as i18n;
// Phase 4 strangler reorg: the wasm-clean RenderBackend trait moved into
// op-editor-core. Re-exported as `render_backend`.
pub use op_editor_core::render_backend;
// Phase 7 strangler reorg: the widget facade + theme + layout scene +
// scene-var aggregation moved into op-editor-ui. Re-exported so
// `openpencil_shell_core::widgets` / `::theme` / `::layout_scene` /
// `::layout_scene_hit` / `::scene_vars` paths still resolve.
pub use op_editor_ui::{layout_scene, layout_scene_hit, scene_vars, theme, widgets};
// Re-export the primary API for upstream crates / widgets / tests.
pub use op_editor_core::render_backend::{Color, Point2D, Rect, RenderBackend, TextLayout};
pub use op_editor_ui::Theme;
/// Re-exports of Jian gesture / event types so shell consumers can use the
/// canonical Jian types directly without an OP-specific translation layer.
pub use jian_core::gesture::{
FocusEvent, ImeEvent, ImeKind, KeyCode, KeyEvent, KeyLocation, KeyState, KeyValue, Modifiers,
MouseButtons, NamedKey, PointerEvent, PointerId, PointerKind, PointerPhase, ScrollMode,
WheelEvent,
};

View file

@ -1,111 +0,0 @@
use openpencil_shell_core::{
FocusEvent, ImeEvent, ImeKind, KeyCode, KeyEvent, KeyLocation, KeyState, KeyValue, Modifiers,
MouseButtons, NamedKey, PointerEvent, PointerId, PointerKind, PointerPhase, ScrollMode,
WheelEvent,
};
#[test]
fn pointer_event_is_re_exported_from_jian() {
let event = PointerEvent::simple(
7,
PointerPhase::Down,
jian_core::geometry::Point::new(12.0, 34.0),
);
let _: PointerEvent = event.clone();
let _: jian_core::gesture::PointerEvent = event.clone();
assert_eq!(event.id, PointerId(7));
assert_eq!(event.kind, PointerKind::Touch);
assert_eq!(event.phase, PointerPhase::Down);
assert_eq!(event.position.x, 12.0);
assert_eq!(event.position.y, 34.0);
assert!(event.buttons.contains(MouseButtons::LEFT));
assert!(event.modifiers.is_empty());
}
#[test]
fn pointer_modifier_and_button_flags_keep_jian_names() {
let mods = Modifiers::SHIFT | Modifiers::CMD;
assert!(mods.contains(Modifiers::SHIFT));
assert!(mods.contains(Modifiers::CMD));
assert!(!mods.contains(Modifiers::CTRL));
let buttons = MouseButtons::LEFT | MouseButtons::RIGHT;
assert!(buttons.contains(MouseButtons::LEFT));
assert!(buttons.contains(MouseButtons::RIGHT));
assert!(!buttons.contains(MouseButtons::MIDDLE));
}
#[test]
fn key_event_is_re_exported_from_jian_with_all_w3c_fields() {
let event = KeyEvent {
key: KeyValue::Named(NamedKey::Enter),
code: KeyCode::Enter,
location: KeyLocation::Right,
modifiers: Modifiers::SHIFT,
state: KeyState::Pressed,
repeat: true,
is_composing: true,
};
let _: jian_core::gesture::KeyEvent = event.clone();
// Round 2 Q5 fix: assert every W3C field reads back the value we set
// so cross-crate type identity AND field-level binary compat are
// both verified through the OP re-export path.
assert_eq!(event.key, KeyValue::Named(NamedKey::Enter));
assert_eq!(event.code, KeyCode::Enter);
assert_eq!(event.location, KeyLocation::Right);
assert!(event.modifiers.contains(Modifiers::SHIFT));
assert_eq!(event.state, KeyState::Pressed);
assert!(event.repeat);
assert!(event.is_composing);
}
#[test]
fn ime_event_is_re_exported_from_jian() {
let event = ImeEvent {
kind: ImeKind::CompositionUpdate {
selection: Some(0..6),
},
text: "你好".to_string(),
};
let _: jian_core::gesture::ImeEvent = event.clone();
assert_eq!(event.text, "你好");
match event.kind {
ImeKind::CompositionUpdate { selection } => assert_eq!(selection, Some(0..6)),
_ => panic!("expected CompositionUpdate"),
}
}
#[test]
fn focus_event_is_re_exported_from_jian_with_all_w3c_fields() {
let event = FocusEvent {
gained: false,
node_id_hint: Some(11),
related_node_id_hint: Some(7),
};
let _: jian_core::gesture::FocusEvent = event;
// Round 2 Q5 fix: assert all three W3C fields, not just gained.
assert!(!event.gained);
assert_eq!(event.node_id_hint, Some(11));
assert_eq!(event.related_node_id_hint, Some(7));
}
#[test]
fn wheel_event_is_re_exported_from_jian_with_w3c_fields() {
let mut event = WheelEvent::simple(
jian_core::geometry::Point::new(10.0, 20.0),
jian_core::geometry::Point::new(0.0, 120.0),
);
// Defaults from WheelEvent::simple
assert_eq!(event.mode, ScrollMode::Pixel);
assert_eq!(event.delta_z, 0.0);
// Round 2 Q5 fix: assert mode + delta_z mutability + roundtrip
// through the OP re-export path matches Jian's behavior.
event.mode = ScrollMode::Line;
event.delta_z = -3.0;
assert_eq!(event.mode, ScrollMode::Line);
assert_eq!(event.delta_z, -3.0);
assert_eq!(event.delta.x, 0.0);
assert_eq!(event.delta.y, 120.0);
}

View file

@ -1,87 +0,0 @@
//! Task 1 Step 20-21: prove the Jian re-export wrapper compiles & is usable.
//!
//! Two anchor invariants for spec v19 §2 / §5.2:
//! 1. `DrawOp::Rect` constructible **through the OP re-export path**
//! (`openpencil_shell_core::jian::DrawOp` — used by shell-native's internal translation).
//! 2. `TextLayout::single_run` produces **exactly one** `TextRun` (spec §5.2,
//! confirming the explicit-field replacement for `..Default::default()` is semantically equivalent).
use openpencil_shell_core::jian::{DrawOp, JianRect, Paint};
use openpencil_shell_core::render_backend::{Color, Point2D, TextLayout};
#[test]
fn drawop_rect_constructible_via_re_export() {
// Construct a Jian DrawOp::Rect via the OP re-export path — proves shell-core's
// jian module exposes jian_core::render::{DrawOp, Paint}.
let rect = JianRect::new(
jian_core::geometry::Point::new(0.0, 0.0),
jian_core::geometry::Size::new(100.0, 50.0),
);
let paint = Paint::solid(jian_core::scene::Color::rgb(255, 0, 0));
let op = DrawOp::Rect { rect, paint };
match op {
DrawOp::Rect { rect, .. } => {
assert_eq!(rect.size.width, 100.0);
assert_eq!(rect.size.height, 50.0);
}
_ => panic!("expected DrawOp::Rect"),
}
}
#[test]
fn text_layout_single_run_creates_one_run() {
// Explicit-field construction (TextRun has no Default impl) — proves the
// spec §5.2 single_run path produces exactly one run with content /
// font_family / font_size set from the caller's arguments.
let layout = TextLayout::single_run(
"Hello",
"system-ui",
16.0,
jian_core::scene::Color::rgb(0, 0, 0),
Point2D::new(10.0, 20.0),
);
assert_eq!(layout.runs().len(), 1);
let run = &layout.runs()[0];
assert_eq!(run.content, "Hello");
assert_eq!(run.font_family, "system-ui");
assert_eq!(run.font_size, 16.0);
assert_eq!(run.font_weight, 400);
assert_eq!(run.origin.x, 10.0);
assert_eq!(run.origin.y, 20.0);
assert_eq!(run.max_width, 0.0);
assert_eq!(run.line_height, 0.0);
}
#[test]
fn text_layout_translated_offsets_origin() {
// translated() adds offset to each run's origin; the original layout is unchanged.
let layout = TextLayout::single_run(
"World",
"system-ui",
14.0,
jian_core::scene::Color::rgb(0, 0, 0),
Point2D::new(5.0, 10.0),
);
let shifted = layout.translated(Point2D::new(100.0, 200.0));
assert_eq!(shifted.runs()[0].origin.x, 105.0);
assert_eq!(shifted.runs()[0].origin.y, 210.0);
// Original layout untouched.
assert_eq!(layout.runs()[0].origin.x, 5.0);
assert_eq!(layout.runs()[0].origin.y, 10.0);
}
#[test]
fn op_color_constants_distinct() {
// spec §5.2 names all six constants — RED/GREEN/BLUE/BLACK/WHITE/TRANSPARENT.
assert_eq!(Color::RED.r, 1.0);
assert_eq!(Color::GREEN.g, 1.0);
assert_eq!(Color::BLUE.b, 1.0);
assert_eq!(Color::BLACK.r, 0.0);
assert_eq!(Color::WHITE.r, 1.0);
assert_eq!(Color::TRANSPARENT.a, 0.0);
}