fix(reflow): kill bulge/merge + font-change on edit; add center/right, IME; harden Raw Text
Reflow (primary editor): - Geometric backstop in reflow_paragraph: adopt any text object fully inside the paragraph bbox that the model's objectIndices omitted (PDFium glyph->object map can return -1), so it's deleted + its original font is resolved. Fixes both the leftover-glyph "bulge/merge" and the font-substitution-on-edit (same root cause). No-op on correctly-indexed paragraphs (gate-proven byte-identical: overlay diff unchanged at 1.69%/1.70%). - Preserve data-fid on edited contentEditable nodes (extractFlatRuns climbs to nearest styled ancestor / inherits from adjacent run) so edits keep their real font instead of the dominant. - Converge preview & commit through one buildReflowData (no preview-OK/commit-wrong drift). - Center/right alignment: additive emission branch (greedy path only) + heading-align inference. - IME composition handling (suppress render mid-composition; don't commit on composing Enter). - Fix WASM document-handle leak (free superseded versions on documentId change/unmount). - Quiet the backstop instrumentation (warn -> debug) now the root cause is confirmed. Raw Text / StreamEditor (beta companion): - Permission-gate both text_objects endpoints behind canModify; label tool "(beta)". - P1a: preserve TJ kerning numbers for same-length edits (redistribute into original slots), fall back to single-string collapse otherwise (never worse than before). - P1b: reject edits with characters unencodable in the run's font (clear 400 instead of corruption). - P2a: commit Raw Text edits as a new document version (add_document) and adopt the id via pushHistory, so they join undo/redo instead of mutating in place. - P2b: exact hit-boxes from real font metrics (re-measured once @font-face loads). WASM preview rebuilt with the reflow changes; cache version bumped. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
21f0bfab9a
commit
660649a562
@@ -54,6 +54,12 @@ void get_or_throw(std::expected<void, pdfengine::EngineError>&& res) {
|
||||
#include <serializer/content_serializer.hpp>
|
||||
#include <serializer/ast_serializer.hpp>
|
||||
|
||||
// A TJ array number more negative than this is treated as an inter-word space when flattening a
|
||||
// TJ run to display/edit text (numbers are thousandths-of-em, subtracted from the X position; a
|
||||
// word space is a large positive gap = large-negative adjustment). extract and replace MUST share
|
||||
// this constant so the flattened text they produce stays positionally aligned (see P1a redistribution).
|
||||
static constexpr double kTjSpaceKern = -500.0;
|
||||
|
||||
class StreamEditor {
|
||||
public:
|
||||
StreamEditor(const std::string& filepath) : filepath_(filepath) {}
|
||||
@@ -129,15 +135,50 @@ public:
|
||||
} else if (item->type == pdfengine::AstNodeType::HexString) {
|
||||
combinedText += std::string(item->bytesValue.begin(), item->bytesValue.end());
|
||||
} else if (item->type == pdfengine::AstNodeType::Number) {
|
||||
if (item->numberValue < -500.0) combinedText += " ";
|
||||
if (item->numberValue < kTjSpaceKern) combinedText += " ";
|
||||
}
|
||||
}
|
||||
if (!combinedText.empty()) {
|
||||
if (textCount == object_index) {
|
||||
arrNode->arrayItems.clear();
|
||||
auto newStrNode = std::make_shared<pdfengine::AstNode>(pdfengine::AstNodeType::String);
|
||||
newStrNode->stringValue = new_text;
|
||||
arrNode->arrayItems.push_back(std::move(newStrNode));
|
||||
// P1a — preserve TJ positioning. When the edit keeps the SAME length as the
|
||||
// extracted text (the typo-fix common case), slice new_text back into the
|
||||
// original string slots and KEEP every kern number, so glyph spacing/kerning
|
||||
// survives. We mirror extract's reconstruction (a synthetic space per
|
||||
// large-negative kern) to keep positions aligned; if a synthetic-space
|
||||
// position was itself edited (can't map cleanly) or the length changed, we
|
||||
// fall back to a single string at natural advances — exactly the old behaviour,
|
||||
// so this is never worse than before, only better when it aligns.
|
||||
bool redistributed = false;
|
||||
if (new_text.size() == combinedText.size()) {
|
||||
std::vector<std::pair<pdfengine::AstNode*, std::string>> assign;
|
||||
size_t pos = 0; bool ok = true;
|
||||
for (const auto& item : arrNode->arrayItems) {
|
||||
if (item->type == pdfengine::AstNodeType::String ||
|
||||
item->type == pdfengine::AstNodeType::HexString) {
|
||||
size_t L = (item->type == pdfengine::AstNodeType::HexString)
|
||||
? item->bytesValue.size() : item->stringValue.size();
|
||||
assign.emplace_back(item.get(), new_text.substr(pos, L));
|
||||
pos += L;
|
||||
} else if (item->type == pdfengine::AstNodeType::Number &&
|
||||
item->numberValue < kTjSpaceKern) {
|
||||
if (pos >= new_text.size() || new_text[pos] != ' ') { ok = false; break; }
|
||||
pos += 1; // consume the synthetic space; keep the kern number as-is
|
||||
}
|
||||
}
|
||||
if (ok && pos == new_text.size()) {
|
||||
for (auto& [node, content] : assign) {
|
||||
node->type = pdfengine::AstNodeType::String;
|
||||
node->stringValue = content;
|
||||
}
|
||||
redistributed = true;
|
||||
}
|
||||
}
|
||||
if (!redistributed) {
|
||||
arrNode->arrayItems.clear();
|
||||
auto newStrNode = std::make_shared<pdfengine::AstNode>(pdfengine::AstNodeType::String);
|
||||
newStrNode->stringValue = new_text;
|
||||
arrNode->arrayItems.push_back(std::move(newStrNode));
|
||||
}
|
||||
modified = true;
|
||||
break;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user