fix(editor): alias Microsoft YaHei and YaHei UI as one family - #216
fix(editor): alias Microsoft YaHei and YaHei UI as one family#216atirna wants to merge 69 commits into
Conversation
Bring the iOS and Android touch shells to parity for canvas gestures, layer reordering, presentation controls, account access, and collaboration entry points. Keep device authentication in Rust, restrict embedded login navigation, and fail closed unless mobile auth link inputs satisfy the build policy.
op_set_active_page gated its state update behind the editor feature, so default-feature builds (cargo test --workspace, player shells) rebuilt the scene on the old page and the switch was a silent no-op. Keep the session state in lockstep with the editor host instead.
…host The mobile-player work pushed both spines past the boundary script's 800-line ceiling. Tighten the new field docs instead of regrouping fields (the existing substruct split was already measured and rejected beyond PreviewState / SizeToggleState / DesignMdPanelState).
The builtin-catalog commit swapped several preset defaults (gpt-5.6, doubao-seed-2-0-pro-260215, mimo-v2.5-pro, Qwen/Qwen3.5-35B-A3B) without updating the reasoning-control classification sweep, which failed CI on every platform. Each new id joins REASONING_CONTROL_WITHHELD with the same no-verified-field reason as its predecessor.
The mobile compile guard checks op-engine-jni with the platform GPU feature on every mobile target (metal on iOS, gl on Android), but the crate only forwarded gl. Add the metal passthrough so the iOS legs of the multi-platform workflow can check the JNI layer.
Writing a fake CLI to /tmp and spawning it back-to-back can hit ETXTBSY (Text file busy) on CI runners' overlay filesystems. Retry a Spawn failure with that message a bounded number of times so the probe test keeps testing a real subprocess round-trip without flaking.
…e-image registry The paint-time remote-image registry is process-global and drained once per frame, so parallel harnesses in one test binary can steal each other's recorded misses: a sibling's drain fires the upcall on its own callback and the recording harness reads 0. Hold a frame lock so each harness's enqueue-drain pair is atomic.
…o see its target The inspector stack has grown past 800 px (compositing rows, size toggles, per-corner radius), so the Effects section now sits below the fold in an 800 px window and its popup dropped off-screen — the overlap search found nothing. Raise the fixture window height so the Effects popup lands over the Interactions rows exactly as the test intends.
Two mobile chrome runs (the top-bar document title and the page pill counter) measured through the family-blind RenderBackend::measure_text, which resolves the backend default instead of the system-ui family the run is drawn in — under-reporting painted width on native. Route them through text_metrics::measure_chrome / centered_text_x so the text-measure boundary check stays green.
* fix(web): preserve account entry in managed embeds * perf(canvas): reduce interaction latency * test(canvas): split scene sync drag coverage * test(canvas): keep drag tests within line cap * refactor(canvas): keep widget host within line cap * style(canvas): normalize widget host ending --------- Co-authored-by: Fini <[email protected]>
* fix(web): preserve account entry in managed embeds * fix(agent): stabilize design execution --------- Co-authored-by: Fini <[email protected]>
Align the read-nodes CLI mapping with map_get/batch_get, which already emit nodeIds as a JSON string array. Comma-joined positionals are split client-side so `op read-nodes "n10,n11"` keeps resolving to two IDs.
Kayshen-X
left a comment
There was a problem hiding this comment.
The "… UI" suffix fold is too broad a rule for what it claims
The #211 diagnosis is right — msyh.ttc registers both Microsoft YaHei and Microsoft YaHei UI — but I don't think the mechanism is. is_same_font_family generalizes that single-file coincidence into a universal rule Name UI ≡ Name, applied symmetrically at every comparison site. For most Windows families that rule is false: the UI variant is a distinct face (often a distinct file) with intentionally different vertical metrics, and for a design tool that difference is a fidelity bug.
The predicate encodes a wrong general rule
Microsoft's own font list documents these as separate families:
Segoe(segoe.ttf) vsSegoe UI(segoeui.ttf) — different typefaces entirelyYu GothicvsYu Gothic UI— different faces; the UI variant deliberately tightens line spacingMeiryovsMeiryo UI— different faces, different vertical metricsLeelawadeevsLeelawadee UI— two separate files (leelawad.ttf / leelawui.ttf);Leelawadee UIis even in our own web fallback list (crates/op-host-web/src/web_fonts.rs:95)
Only Microsoft YaHei ↔ Microsoft YaHei UI shares one file — and even there they are two faces inside msyh.ttc with different line heights (the UI face is tighter).
Consequences at the call sites
missing_fonts.rs(family_available) — a document authored withYu Gothicon a machine that only hasYu Gothic UIis now silently "available" and renders with the wrong metrics. The old behavior was a noisy-but-true prompt; the new behavior is a silent fidelity bug.web_fonts.rs(font_data_key_for_family) — the fallback loads the wrong face's bytes: authoredSegoe+ enumerated-onlySegoe UIregisters the Segoe UI blob forSegoe. This is #211's mirror image — from "false missing-font prompt" to "silently loaded wrong font".missing_fonts_flow.rs(note_font_supplied) — suppressing the "File is X, not Y" note for all… UIpairs removes a real user safety signal: supplying aSegoe UIfile for a missingSegoerow is a genuine mismatch and should still warn. Only the explicit known alias should be exempted here.property_panel_typography_paint.rs— the picker now highlightsSegoe UIas the active row for a document authoredSegoe, presenting a family the user never chose. Also,is_same_font_familyallocates 1–2 Strings per comparison, in a per-frame per-row paint loop where the oldeq_ignore_ascii_casewas allocation-free.
Coverage is incomplete / inconsistent
The PR description says "every family-name comparison site", but:
command_font_replace.rs:199still uses plain equality — replacingMicrosoft YaHeiwon't touch nodes authoredMicrosoft YaHei UI(the same class of false negative as #211).missing_fonts_flow.rs:197(refresh_promptreconciliation) and:224now use two different equality rules in the same file.- Picker dedup sites (
op-host-native/src/widget_host/font_picker_dispatch.rs:197,op-host-web/src/widget_host/web_fonts.rs:19) andfont_catalog.rs:64are untouched, so the picker still listsYaHei/YaHei UIas two rows while another module claims they are one family. - The tests assert the predicate's own definition but never assert
!is_same_font_family("Yu Gothic", "Yu Gothic UI")— the case that would expose the over-breadth.
Suggested direction
The root issue is deciding availability by diffing an enumerated name list. Native already has the truth: op-host-native/src/backend/skia.rs enumerates via skia FontMgr (DirectWrite on Windows), and FontMgr::match_family_style("Microsoft YaHei") resolves straight to msyh.ttc. I'd rather see:
- Native: ask the font manager whether the family resolves (or enumerate each face's full family-name set) instead of string-diffing names.
- Web (where
queryLocalFontsis name-only): an explicit, documented alias table scoped to the known same-file pairs, e.g.:
/// Windows pairs where one font FILE ships two family NAMES. Deliberately
/// not a general "… UI" fold: for most Windows families the UI variant
/// is a distinct face with different vertical metrics (Yu Gothic UI,
/// Meiryo UI, Leelawadee UI are all separate families).
pub const WINDOWS_UI_FAMILY_ALIASES: &[(&str, &str)] = &[
("microsoft yahei", "microsoft yahei ui"),
];...and then route the mismatch-note exemption, picker highlight, and web byte fallback through that table (system families only — not user-imported fonts), leaving every other comparison site as-is.
If keeping a general predicate is preferred for the prompt UX, it should at minimum be scoped to system families, be one-directional (Name matches Name UI, not the reverse), and be renamed so the name doesn't promise "same face" (windows_ui_alias_equivalent or similar) — with the negative tests above added.
The overflow-trigger fixtures sat at 88px, which lands within platform font-metric slack of the 800px inner width: Windows CJK fallback measured the title narrow enough that the pinned-shadow case never crossed the threshold and CI went red while mac and linux stayed green. Raise every trigger font size to 200px so even the most conservative measurement backend proves the overflow; the negative cases gate on form, pinning, or fixed-width and never depended on the exact size. Claude-Session: https://claude.ai/code/session_01FqKQqNj8exYwopGDpYUU7x
…us revival Sibling ornament chips drifted through arbitrary fill colors even after structural equalisation, so same-position decorative fills join the two-thirds majority vote (solid hexes and matching variable refs only; mixed systems and gradients stay untouched). Card boards that outgrow the portrait band after text wrapping now surface a format-drift advisory instead of a silent ratio change — restoring 3:4 versus keeping the long form is the caller's call. The ab-v3 corpus returns from history into scripts/ab-v9 so the harness has a home again after the TS retirement removed the original; audited baseline: 52/52 clean under the geometry rubric. Claude-Session: https://claude.ai/code/session_01FqKQqNj8exYwopGDpYUU7x
A trailing ' UI' fold treated Segoe/Yu Gothic/Meiryo/Leelawadee UI as the same family as their non-UI faces. Keep only the documented msyh.ttc pair (Microsoft YaHei ↔ Microsoft YaHei UI).
|
yeah the trailing UI fold was too broad. segoe / yu gothic / meiryo / leelawadee ui are different faces. i swapped it for an explicit same-file alias table, currently just microsoft yahei <-> microsoft yahei ui. picker highlight, mismatch note, and the web byte lookup go through that. yu gothic vs yu gothic ui now stays missing (and supplying segoe ui for a segoe row still warns). checked with thanks for catching that! |
The builtin chat provider only handed the model the mutating tools (insert/update/move/delete_node) when the current selection was a pure Frame set, so chatting right after opening a document — or with any non-Frame node selected — silently degraded the agent to the three read-only tools. Users experienced this as modification tools randomly "dropping" and staying broken for whole documents. Advertise the full CRUD set on every turn; mutation safety already lives at the execution sink (collaboration gate + per-tool validation), so hiding definitions only created confusion. Removes the now-unused write-scope filter and frame-target probe. Closes ZSeven-W#209
Mobile editors previously dropped every export/save file action at the FFI shell-action drain, so tapping Export did nothing on iOS/Android. - Extract the shared raster/SVG/PDF renderer from op-host-services into the mobile-safe op-render-export crate (same scene painter as the desktop export path; op-host-services re-exports keep its API). - op-engine-ffi: freeze one export artifact per request and add the SHELL_ACTION_EXPORT_DOCUMENT bridge (copy file name, write once to a shell-owned staging path, cancel) so large payloads never cross the ABI as a second copy. - iOS: DocumentExportCoordinator stages the file and presents the system Files save picker. - Android: JNI bindings plus a SAF create-document flow — Rust writes the app-private staging file, the chosen destination receives a plain copy, and every terminal path removes the staging directory. - WebP is hidden on mobile because the pinned Skia archives ship without its encoder; PNG/JPEG/SVG/PDF stay available everywhere. - The full editor now starts on a canonical untitled document instead of the bundled demo, with a localized untitled app-bar title and a New File action in the mobile more panel.
HTML imports map visibility:hidden elements to visible:false nodes, and the old every-descendant-visible gate made every imported frame permanently undeletable, undraggable and unreorderable. Locks anywhere in the subtree still protect; visibility no longer does, on the selected root or its descendants (Figma parity).
elementFromPoint stops at a shadow host, which made everything inside a web-component sub-app (wujie, qiankun strictStyleIsolation) unpickable. Descend like DevTools inspect does, but stop at slot-bearing shadow roots: slotted light children are only captured through the host.
The extractor's 20,000-node budget drained depth-first, so on large pages everything late in DOM order — micro-frontend sub-app containers above all — was silently dropped whole. Double the node budget and lift the local byte chain (client precheck, ingest route, importer) from 32 to 48 MiB in lockstep, still under the endpoint-wide 64 MiB body cap. The hub inbox keeps its own 32 MiB server contract: its cap is now a separate constant with a dedicated oversize message, since a 32-48 MiB capture imports locally but cannot reach the account. The /mcp fallback envelope gets its own precheck (JSON re-escaping can inflate past the raw-snapshot cap), the request timeout no longer ties the editor's 15 s ack budget, and the measure cache is sized for 40k text leaves.
…r, and locale Region-aware auth configure (cn/global), begin-login/sign-out/account-snapshot FFI + JNI, one-shot pending flags for the native login / account center / language picker, runtime locale switching, safe-area band painting that blends with the app bar and dock, and a Language entry in the mobile more panel.
…cale UI Replaces the login WebView on iOS and Android with fully native screens in the ZSeven web design: labeled boxed inputs with lucide icons, gradient primary button, region-accurate provider cards fetched from the pairing origin, native register / password-reset with live password rules, a native account center, a 15-language picker, IP-informed cn/global region resolve with persisted override, and an iOS GitHub-releases update check.
Provider cards now open the pairing login page in an in-app Safari sheet (iOS) / Chrome Custom Tab (Android) instead of bouncing to the external browser, carrying a provider deep-link parameter for the tapped card. This is the interim path while per-provider native SDK sign-in lands.
The Apple provider card runs the system AuthenticationServices sheet: a fresh 32-byte base64url nonce binds the request, the identity token is exchanged at the SSO's new POST /api/v1/auth/providers/apple/native-login into the screen's cookie jar, and the running device pairing is approved directly — no web page involved. A user cancel returns to the screen and a sheet failure surfaces an inline error. Adds the Sign in with Apple entitlement plus SafariServices/AuthenticationServices links.
Generation quality from weaker models swings hard between runs on the same prompt. `OPENPENCIL_LLM_TEMPERATURE` (0.0..=2.0) lets a run pin the sampling temperature so the variance source itself can be measured; unset or unparsable leaves the field off the request body entirely, so provider defaults and existing behaviour are untouched. Wired in both openai-compat bodies: the smoke harness client and the builtin HTTP chat path shared by every model that rides it. Claude-Session: https://claude.ai/code/session_01FqKQqNj8exYwopGDpYUU7x
A single generation from a weaker model is close to a dice roll: across 18 candidates in a three-prompt trial, 8 carried geometry defects. Rather than fight that variance, harvest it — `OPENPENCIL_SMOKE_BEST_OF=N` (2..=4) runs N independent generations from clones of the prepared base state, scores each against the real-layout geometry diagnostics, and keeps the winner: fewest issues, then most nodes as a richness proxy, then first to arrive. Every candidate is also written to `<out>.cand<i>.op` so a human can compare, and a candidate whose generation errors scores a sentinel instead of aborting the rest. Selection, scoring and saving live in best_of.rs with unit tests over the ordering rule; main.rs keeps only the branch, and the single-run path is untouched when the knob is unset. Claude-Session: https://claude.ai/code/session_01FqKQqNj8exYwopGDpYUU7x
Non-Apple provider cards now open the SSO start endpoint (channel web_mobile) with the device pairing attached, so the in-app sheet lands directly on the provider's authorize screen and returns to the dedicated pairing-approval page — the ZSeven login web page no longer appears in the flow. Requires the SSO backend with device_pairing start support.
…-family-split # Conflicts: # .github/workflows/ios-app-store.yml # .github/workflows/version-sync.yml # README.md # crates/op-engine-ffi/src/editor.rs # crates/op-engine-ffi/tests/editor.rs # scripts/build-android-release.sh # scripts/build-mobile-auth-dev.sh # scripts/prepare-ios-app-store-runner.sh # scripts/publish-ios-testflight.sh # scripts/sync-version.test.sh # tools/check-android-release-workflow.sh # tools/check-android-release-workflow.test.sh # tools/check-ios-app-store-workflow.sh # tools/check-version-sync.sh # tools/check-version-sync.test.sh
3368ecc to
03bd531
Compare
Summary
Windows ships
msyh.ttcunder two family names,Microsoft YaHeiandMicrosoft YaHei UI, and a system enumeration may surface only one spelling. Missing-font detection compared names with plain equality, so opening a design authored with the other spelling raised a false missing-font prompt.This keeps an explicit same-file alias table for that pair only. Other Windows
… UIfaces (Segoe UI, Yu Gothic UI, Meiryo UI, Leelawadee UI) stay distinct, because those are different faces with different vertical metrics.Changes
op-editor-core/font_catalog:WINDOWS_UI_FAMILY_ALIASEScurrently lists onlymicrosoft yahei<->microsoft yahei ui;is_same_font_familyis ASCII-case-insensitive equality plus that table (no trailing-UIfold)op-editor-core/missing_fonts: availability still uses that predicate, so a YaHei document is not missing when only YaHei UI is enumeratedop-editor-ui/missing_fonts_flow: supplying a YaHei UI file for a YaHei row does not warn; supplying Segoe UI for Segoe still doesop-editor-ui/property_panel_typography_paint: picker highlight follows the same alias tableop-host-web/web_fonts: local-font byte lookup may fall back to the YaHei alias key, not to an arbitrary… UIfaceRelated Issues
Fixes #211
Type
fix— Bug fixChecklist
cargo test --workspacepassescargo clippy --workspace --all-targets -- -D warningspasses