From 383eba14e5e3c4ee90ef9176a1696e3dafc2d4f2 Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Tue, 6 Oct 2026 12:25:24 +0300 Subject: [PATCH] Fields: table cells keep stored values, UPDATEFIELD reaches blocks and tables, measured shading box Table cell fields show their stored value; only formula cells evaluate live. UPDATEFIELD on a block reference also updates the fields of its block definition, and on a table the fields of its cells; a changed table cell text redraws the table. The FIELDDISPLAY box is placed from the text layout (MTEXT layout cells, TEXT / attribute pen advances) and spans 0.345 text heights below the baseline to 1.19 above it. Co-Authored-By: Claude Opus 5.5 --- Cargo.lock | 2 +- Cargo.toml | 2 +- src/app/commands/field.rs | 16 +++- src/entities/field.rs | 10 +- src/entities/table.rs | 2 +- src/entities/text_support.rs | 31 +++++-- src/scene/convert/tessellate.rs | 160 +++++++++++++++++--------------- src/scene/mod.rs | 6 ++ 8 files changed, 134 insertions(+), 95 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6a2ad04b6..4f241e9e6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4206,7 +4206,7 @@ dependencies = [ [[package]] name = "opencadcodec" version = "0.5.5" -source = "git+https://github.com/ramox81/cadcodec.git?rev=4892548#4892548f141c9e1389f6a5f0ccf75b30c4753b1b" +source = "git+https://github.com/ramox81/cadcodec.git?rev=07191be#07191be6571c43d8334eb7da02e6d42283246628" dependencies = [ "ahash 0.8.12", "anyhow", diff --git a/Cargo.toml b/Cargo.toml index f522bd606..138a3301c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -95,7 +95,7 @@ iced_core = { git = "https://github.com/iced-rs/iced.git", rev = "23604ff22ab0aa iced_widget = { git = "https://github.com/iced-rs/iced.git", rev = "23604ff22ab0aad9e00b9327cb7b8546ed84db39" } [patch."https://github.com/HakanSeven12/opencadcodec.git"] -opencadcodec = { git = "https://github.com/ramox81/cadcodec.git", rev = "4892548" } +opencadcodec = { git = "https://github.com/ramox81/cadcodec.git", rev = "07191be" } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] getrandom = "0.3" diff --git a/src/app/commands/field.rs b/src/app/commands/field.rs index da40cab49..13643b2ed 100644 --- a/src/app/commands/field.rs +++ b/src/app/commands/field.rs @@ -29,11 +29,23 @@ impl OpenCADStudio { self.tabs[i].active_cmd = Some(Box::new(cmd)); return Some(Task::none()); } + // A block reference brings its attributes and the fields of its + // block definition; a table, the texts of its cells. let document = &self.tabs[i].scene.document; let mut hosts = selected.clone(); for handle in &selected { - if let Some(codec::EntityType::Insert(insert)) = document.get_entity(*handle) { - hosts.extend(insert.attributes.iter().map(|a| a.common.handle)); + let record = match document.get_entity(*handle) { + Some(codec::EntityType::Insert(insert)) => { + hosts.extend(insert.attributes.iter().map(|a| a.common.handle)); + document.block_records.iter().find(|r| r.name.eq_ignore_ascii_case(&insert.block_name)) + } + Some(codec::EntityType::Table(table)) => table + .block_record_handle + .and_then(|h| document.block_records.iter().find(|r| r.handle == h)), + _ => None, + }; + if let Some(record) = record { + hosts.extend(record.entity_handles.iter().copied()); } } self.push_undo_snapshot(i, "UPDATEFIELD"); diff --git a/src/entities/field.rs b/src/entities/field.rs index 17eab834d..29af2e20b 100644 --- a/src/entities/field.rs +++ b/src/entities/field.rs @@ -110,12 +110,10 @@ pub fn update_fields( document.update_fields(&OcsFieldContext(Some(&snapshot)), event, hosts) } -pub fn resolve_handle( - document: &CadDocument, - field: Handle, - host: Handle, -) -> Option { - codec::fields::resolve_handle(document, field, host, &OcsFieldContext(Some(document))) +/// The text a table cell shows for its field: a formula is evaluated live +/// (it reads other cells), any other field shows its stored value. +pub fn cell_text(document: &CadDocument, field: Handle, table: Handle) -> Option { + codec::fields::cell_field_text(document, field, table, &OcsFieldContext(Some(document))) } /// Evaluate one field code (`\AcVar Date \f "yyyy-MM-dd"`, or the `%<…>%` diff --git a/src/entities/table.rs b/src/entities/table.rs index 0130996b9..e6ddd855b 100644 --- a/src/entities/table.rs +++ b/src/entities/table.rs @@ -646,7 +646,7 @@ fn content_display_value( ) -> String { if let Some(handle) = content.field_handle { if let Some(value) = - crate::entities::field::resolve_handle(document, handle, table.common.handle) + crate::entities::field::cell_text(document, handle, table.common.handle) { return value; } diff --git a/src/entities/text_support.rs b/src/entities/text_support.rs index 429ff0466..d8aef296d 100644 --- a/src/entities/text_support.rs +++ b/src/entities/text_support.rs @@ -488,18 +488,29 @@ fn font_stem(name: &str) -> String { .to_string() } -/// How many inked glyphs (non-blank characters, stacked parts included) an -/// MTEXT string draws, in drawing order: the index of the first glyph of -/// what follows `s` in a longer string. -pub fn mtext_glyph_count(s: &str) -> usize { - let ink = |t: &str| t.chars().filter(|c| !c.is_whitespace()).count(); +/// The visible-character offset that follows the MTEXT string `s` — the +/// `vis` index [`layout_mtext`]'s glyph boxes give the first character of +/// whatever comes after `s` in a longer string: every laid-out character, +/// a tab or stack slot, and one per paragraph break. +pub fn mtext_visible_count(s: &str) -> usize { adapt_mtext_paragraphs(s, 1.0, false) .iter() - .flat_map(|line| &line.runs) - .map(|run| match &run.kind { - MTextRunKind::Glyphs(text) => ink(text), - MTextRunKind::Tab => 0, - MTextRunKind::Stack { numerator, denominator, .. } => ink(numerator) + ink(denominator), + .enumerate() + .map(|(i, para)| { + usize::from(i > 0) + + para + .runs + .iter() + .map(|run| match &run.kind { + MTextRunKind::Glyphs(text) => text.chars().count(), + MTextRunKind::Tab => 1, + MTextRunKind::Stack { numerator, denominator, .. } => { + numerator.chars().count() + + denominator.chars().count() + + usize::from(!denominator.is_empty()) + } + }) + .sum::() }) .sum() } diff --git a/src/scene/convert/tessellate.rs b/src/scene/convert/tessellate.rs index 8ab0ce15f..a6ae7f710 100644 --- a/src/scene/convert/tessellate.rs +++ b/src/scene/convert/tessellate.rs @@ -78,70 +78,97 @@ fn oriented_text_corners( ] } -/// FIELDDISPLAY boxes: the glyph bounds of each field's shown text, one box -/// per line the field spans, in the text's own frame (`origin`, `rotation`). -/// Glyph quads come six vertices each in drawing order, one per inked -/// character, so a field's characters select its quads. `None` when the -/// host's fields cannot be located in its text (the whole text is boxed). +/// FIELDDISPLAY box extent around a line of field text, in text heights: +/// from a third of the height below the baseline to 1.19 heights above it +/// (the reference's box, measured at 2.5 and 10 unit text). +const FIELD_BOX_BELOW: f64 = 0.345; +const FIELD_BOX_ABOVE: f64 = 1.19; + +/// FIELDDISPLAY boxes: each field's shown text, one box per line it spans, +/// placed from the text layout itself — MTEXT's per-character layout cells +/// (the editor's glyph boxes) and TEXT / attribute pen advances, the same +/// layout and shaping the glyphs are drawn with — so formatting codes, +/// missing glyphs or shaped scripts before a field do not shift it. A box +/// spans the field's character cells horizontally and FIELD_BOX_BELOW / +/// FIELD_BOX_ABOVE vertically. `None` when the host's fields cannot be +/// located in its text (the whole text is boxed then). fn field_run_corner_groups( - verts: &[crate::scene::pipeline::text_gpu::TextVertex], document: &codec::CadDocument, entity: &EntityType, - origin: [f64; 2], - rotation: f64, + stroke_groups: &[crate::scene::convert::acad_to_render::TextStroke], + anno: f64, ) -> Option> { - use crate::entities::text_support::{mtext_glyph_count, resolve_dxf_special_chars}; - fn plain_glyph_count(s: &str) -> usize { - resolve_dxf_special_chars(s).chars().filter(|c| !c.is_whitespace()).count() - } - let (text, count): (&str, fn(&str) -> usize) = match entity { - EntityType::MText(m) => (&m.value, mtext_glyph_count), - EntityType::Text(t) => (&t.value, plain_glyph_count), - EntityType::AttributeEntity(a) => (&a.value, plain_glyph_count), + use crate::entities::text_support::{mtext_visible_count, resolve_dxf_special_chars, text_local_bounds}; + let handle = entity.common().handle; + // Lines of a field in a local frame: [left, baseline, right, height]. + let mut lines: Vec<[f64; 4]> = Vec::new(); + let (origin, rotation) = match entity { + EntityType::MText(m) => { + let spans = codec::fields::field_spans(document, handle, &m.value)?; + let mut local = m.clone(); + local.insertion_point = codec::types::Vector3::new(0.0, 0.0, 0.0); + local.rotation = 0.0; + let boxes = crate::entities::mtext::glyph_boxes(&local, document); + for span in spans { + let first = mtext_visible_count(m.value.get(..span.start)?); + let last = mtext_visible_count(m.value.get(..span.end)?); + let mut line: Option<[f64; 4]> = None; + for b in boxes.iter().filter(|b| (first..last).contains(&b.vis)) { + let (l, base, r, h) = (b.xmin as f64, b.ymin as f64, b.xmax as f64, (b.ymax - b.ymin) as f64); + line = Some(match line { + Some(c) if (c[1] - base).abs() <= 1e-3 * h.max(c[3]) => { + [c[0].min(l), c[1], c[2].max(r), c[3].max(h)] + } + Some(c) => { + lines.push(c); + [l, base, r, h] + } + None => [l, base, r, h], + }); + } + lines.extend(line); + } + let rotation = stroke_groups + .iter() + .find_map(|g| g.run.as_ref().map(|run| run.rotation as f64)) + .unwrap_or(m.rotation); + for line in &mut lines { + *line = line.map(|v| v * anno); + } + ([m.insertion_point.x, m.insertion_point.y], rotation) + } + EntityType::Text(_) | EntityType::AttributeEntity(_) => { + let text = match entity { + EntityType::Text(t) => &t.value, + EntityType::AttributeEntity(a) => &a.value, + _ => return None, + }; + let spans = codec::fields::field_spans(document, handle, text)?; + // One laid-out line: a multi-line attribute keeps the whole box. + let [group] = stroke_groups else { return None }; + let run = group.run.as_ref()?; + let advance = |s: &str| { + text_local_bounds(&run.font, &resolve_dxf_special_chars(s), run.height, run.width_factor, run.oblique) + .map_or(0.0, |b| b.advance as f64) + }; + for span in spans { + let (l, r) = (advance(text.get(..span.start)?), advance(text.get(..span.end)?)); + lines.push([l * anno, 0.0, r * anno, run.height as f64 * anno]); + } + (group.origin, run.rotation as f64) + } _ => return None, }; - let spans = codec::fields::field_spans(document, entity.common().handle, text)?; let (sin_r, cos_r) = rotation.sin_cos(); - // Each quad's local bounds [left, bottom, right, top]. - let quads: Vec<[f64; 4]> = verts - .chunks_exact(6) - .map(|quad| { - let mut b = [f64::MAX, f64::MAX, f64::MIN, f64::MIN]; - for v in quad { - let x = v.pos[0] as f64 + v.pos_low[0] as f64 - origin[0]; - let y = v.pos[1] as f64 + v.pos_low[1] as f64 - origin[1]; - let (lx, ly) = (x * cos_r + y * sin_r, -x * sin_r + y * cos_r); - b = [b[0].min(lx), b[1].min(ly), b[2].max(lx), b[3].max(ly)]; - } - b - }) - .collect(); let to_world = |x: f64, y: f64| [origin[0] + x * cos_r - y * sin_r, origin[1] + x * sin_r + y * cos_r]; - let mut groups = Vec::new(); - for span in spans { - let first = count(text.get(..span.start)?); - let last = count(text.get(..span.end)?).min(quads.len()); - // A wrapped field starts a new box where the next glyph moves back - // left or off the current line. - let mut line: Option<[f64; 4]> = None; - for q in quads.get(first..last).unwrap_or_default() { - line = Some(match line { - Some(b) if q[0] >= b[0] && (q[1] + q[3]) * 0.5 > b[1] && (q[1] + q[3]) * 0.5 < b[3] => { - [b[0].min(q[0]), b[1].min(q[1]), b[2].max(q[2]), b[3].max(q[3])] - } - Some(b) => { - groups.push(b); - *q - } - None => *q, - }); - } - groups.extend(line); - } Some( - groups + lines .into_iter() - .map(|[l, b, r, t]| [to_world(l, b), to_world(r, b), to_world(r, t), to_world(l, t)]) + .filter(|[l, _, r, _]| r > l) + .map(|[l, base, r, h]| { + let (b, t) = (base - FIELD_BOX_BELOW * h, base + FIELD_BOX_ABOVE * h); + [to_world(l, b), to_world(r, b), to_world(r, t), to_world(l, t)] + }) .collect(), ) } @@ -1489,29 +1516,14 @@ pub fn tessellate( && crate::entities::field::display() && crate::entities::field::hosts_field(document, entity) { - let frame = match entity { - EntityType::MText(m) => Some(( - [m.insertion_point.x, m.insertion_point.y], - stroke_groups - .iter() - .find_map(|group| group.run.as_ref().map(|run| run.rotation as f64)) - .unwrap_or(m.rotation), - )), - EntityType::Text(t) => { - Some(([t.insertion_point.x, t.insertion_point.y], t.rotation)) - } - EntityType::AttributeEntity(a) => { - Some(([a.insertion_point.x, a.insertion_point.y], a.rotation)) - } - _ => None, - }; - let runs = frame.and_then(|(origin, rotation)| { - field_run_corner_groups(&sdf_verts, document, entity, origin, rotation) - }); + let runs = field_run_corner_groups(document, entity, &stroke_groups, anno); let corner_groups = match entity { _ if runs.is_some() => runs.unwrap_or_default(), EntityType::MText(m) => { - let rotation = frame.map_or(m.rotation, |(_, rotation)| rotation); + let rotation = stroke_groups + .iter() + .find_map(|group| group.run.as_ref().map(|run| run.rotation as f64)) + .unwrap_or(m.rotation); oriented_mtext_corner_groups(&sdf_verts, m, rotation, 0.0, anno) } EntityType::Text(t) => vec![oriented_text_corners( diff --git a/src/scene/mod.rs b/src/scene/mod.rs index 0246ce123..97c3d857d 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -3443,6 +3443,12 @@ impl Scene { targets.render_handles.extend(users.iter().copied()); } } + // A table draws its cells from its own block of cell texts. + let record_handle = record.handle; + targets.render_handles.extend(self.document.entities().filter_map(|e| match e { + EntityType::Table(t) if t.block_record_handle == Some(record_handle) => Some(t.common.handle), + _ => None, + })); targets.touches_block_definition = true; } self.invalidate_dependency_targets(targets);