refactor(mtext): add structured MTextDocument mirror to the editor (phase 1)
First phase of moving the in-app MTEXT editor onto acadrust's structured MTEXT model. Adds a `doc: MTextDocument` field kept in sync with the raw content via `parse_mtext` on every preview rebuild, plus `VisSlot` and `doc_vis_map` — the unified visible-index space that will replace the hand-rolled `visible_spans` walker (it matches the layout's vis counting: one caret slot per paragraph break, one per visible char, none for a tab). Purely additive scaffolding: nothing consumes the doc yet, so editor behaviour is unchanged. Later phases make the doc authoritative for edits, formatting, commit and load, then delete the raw-string path. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
d37dabb0fb
commit
4b605d0ede
1 changed files with 82 additions and 0 deletions
|
|
@ -6,6 +6,7 @@
|
|||
// change so the user sees the actual drawing result while typing.
|
||||
|
||||
use acadrust::entities::mtext::AttachmentPoint;
|
||||
use acadrust::entities::mtext_format::{parse_mtext, MTextDocument, StackingType};
|
||||
use acadrust::types::Vector3;
|
||||
use acadrust::{EntityType, Handle, MText};
|
||||
use glam::Vec3;
|
||||
|
|
@ -86,6 +87,11 @@ pub struct MTextEditorState {
|
|||
pub pos: Vec3,
|
||||
/// The editable text buffer (raw value with inline codes).
|
||||
pub content: text_editor::Content,
|
||||
/// Structured mirror of `content`, parsed via acadrust `parse_mtext` — the
|
||||
/// target authority for the editor. Phase 1: passive mirror kept in sync on
|
||||
/// every rebuild, cross-checked against the rendered glyph boxes; later
|
||||
/// phases make it the source of truth and delete the raw-string path.
|
||||
pub doc: MTextDocument,
|
||||
/// Text height, edited as a string so partial input is allowed.
|
||||
pub height: String,
|
||||
/// Text style name (entity field).
|
||||
|
|
@ -137,6 +143,7 @@ impl MTextEditorState {
|
|||
Self {
|
||||
pos,
|
||||
content: text_editor::Content::with_text(initial),
|
||||
doc: parse_mtext(initial, true),
|
||||
height: format!("{:.4}", height)
|
||||
.trim_end_matches('0')
|
||||
.trim_end_matches('.')
|
||||
|
|
@ -229,6 +236,77 @@ fn parse_non_default(s: &str, default: f64) -> Option<f64> {
|
|||
/// counts glyph boxes (paragraphs split on `\P`/`\n`/`\N`, leading/trailing
|
||||
/// spaces trimmed per paragraph, inline codes skipped). Lets a preview
|
||||
/// selection (visible-char range) be spliced back into the raw value.
|
||||
/// A slot in the flat visible-index space of a structured MText document — the
|
||||
/// structured replacement for [`visible_spans`]. `doc_vis_map(doc).len()` is the
|
||||
/// caret index space (`0..=count`) and each entry locates that visible slot
|
||||
/// inside the document. Built to match `layout_mtext`'s glyph-box `vis`
|
||||
/// assignment (`text_support.rs`) so the caret and the rendered glyphs never
|
||||
/// desync — one caret slot per paragraph break, one per visible char, and
|
||||
/// none for a tab (which has no glyph box).
|
||||
// Consumed by the caret/selection + edit ops in the next phase (structured
|
||||
// model migration); defined here so the map is ready and reviewed in isolation.
|
||||
#[allow(dead_code)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub(super) enum VisSlot {
|
||||
/// A visible glyph: the char at byte `byte` of
|
||||
/// `doc.paragraphs[para].spans[span].text`.
|
||||
Char { para: usize, span: usize, byte: usize },
|
||||
/// The zero-width caret slot at the start of paragraph `para` (a `\P` break).
|
||||
Break { para: usize },
|
||||
}
|
||||
|
||||
/// Flatten a structured MText document into the editor's visible-index space.
|
||||
/// Mirrors the layout's `vis` counting so `doc_vis_map(doc).len()` equals
|
||||
/// `glyph_boxes(mt).len()` for the same document.
|
||||
#[allow(dead_code)]
|
||||
pub(super) fn doc_vis_map(doc: &MTextDocument) -> Vec<VisSlot> {
|
||||
let mut out: Vec<VisSlot> = Vec::new();
|
||||
for (pi, para) in doc.paragraphs.iter().enumerate() {
|
||||
// Every paragraph after the first opens with a caret slot for its break
|
||||
// (mirrors the `i > 0 && is_first_in_paragraph` line-start box).
|
||||
if pi > 0 {
|
||||
out.push(VisSlot::Break { para: pi });
|
||||
}
|
||||
for (si, span) in para.spans.iter().enumerate() {
|
||||
// A stacking span (`\S`) renders its flattened `num<sep>den`; count
|
||||
// one slot per flattened char (edits inside are clamped to the span
|
||||
// boundary in later phases).
|
||||
if let Some(st) = &span.stacking {
|
||||
let sep = if st.stacking_type == StackingType::Limit {
|
||||
'^'
|
||||
} else {
|
||||
'/'
|
||||
};
|
||||
let mut flat = st.numerator.clone();
|
||||
if !st.denominator.is_empty() {
|
||||
flat.push(sep);
|
||||
flat.push_str(&st.denominator);
|
||||
}
|
||||
for _ in flat.chars() {
|
||||
out.push(VisSlot::Char {
|
||||
para: pi,
|
||||
span: si,
|
||||
byte: 0,
|
||||
});
|
||||
}
|
||||
continue;
|
||||
}
|
||||
for (byte, ch) in span.text.char_indices() {
|
||||
// Tab has no glyph box (`AtomKind::Tab`) → no vis slot.
|
||||
if ch == '\t' {
|
||||
continue;
|
||||
}
|
||||
out.push(VisSlot::Char {
|
||||
para: pi,
|
||||
span: si,
|
||||
byte,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
pub fn visible_spans(raw: &str) -> Vec<(usize, usize)> {
|
||||
let mut result: Vec<(usize, usize)> = Vec::new();
|
||||
let mut para: Vec<(usize, usize, char)> = Vec::new();
|
||||
|
|
@ -418,6 +496,10 @@ impl super::OpenCADStudio {
|
|||
if let Some(ed) = self.mtext_editor.as_mut() {
|
||||
ed.preview_wires = wires;
|
||||
ed.glyph_boxes = boxes;
|
||||
// Phase 1: keep the structured mirror in sync with the raw content.
|
||||
// The doc is passive (nothing consumes it yet), so this cannot change
|
||||
// behaviour — it just makes the model available for the next phase.
|
||||
ed.doc = parse_mtext(&ed.content.text(), true);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue