forked from 1b-agent/opencadstudio-upstream
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:
1 parent
1c242eb489
commit
383eba14e5
8 files changed
+134
-95
No files matched your search
Generated
+1
-1
@@ -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
@@ -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"
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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 `%<…>%`
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user