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 <[email protected]>
This commit is contained in:
ramox81andClaude Opus 5.5 committed 2026-10-06 12:25:24 +03:00
1 parent 1c242eb489
commit 383eba14e5
8 files changed
+134 -95

No files matched your search

Generated
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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"
+14 -2
View File
@@ -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");
+4 -6
View File
@@ -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<String> {
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<String> {
codec::fields::cell_field_text(document, field, table, &OcsFieldContext(Some(document)))
}
/// Evaluate one field code (`\AcVar Date \f "yyyy-MM-dd"`, or the `%<…>%`
+1 -1
View File
@@ -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;
}
+21 -10
View File
@@ -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::<usize>()
})
.sum()
}
+86 -74
View File
@@ -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<Vec<[[f64; 2]; 4]>> {
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(
+6
View File
@@ -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);