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 <noreply@anthropic.com>
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]]
|
[[package]]
|
||||||
name = "opencadcodec"
|
name = "opencadcodec"
|
||||||
version = "0.5.5"
|
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 = [
|
dependencies = [
|
||||||
"ahash 0.8.12",
|
"ahash 0.8.12",
|
||||||
"anyhow",
|
"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" }
|
iced_widget = { git = "https://github.com/iced-rs/iced.git", rev = "23604ff22ab0aad9e00b9327cb7b8546ed84db39" }
|
||||||
|
|
||||||
[patch."https://github.com/HakanSeven12/opencadcodec.git"]
|
[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]
|
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||||
getrandom = "0.3"
|
getrandom = "0.3"
|
||||||
|
|||||||
@@ -29,11 +29,23 @@ impl OpenCADStudio {
|
|||||||
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
||||||
return Some(Task::none());
|
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 document = &self.tabs[i].scene.document;
|
||||||
let mut hosts = selected.clone();
|
let mut hosts = selected.clone();
|
||||||
for handle in &selected {
|
for handle in &selected {
|
||||||
if let Some(codec::EntityType::Insert(insert)) = document.get_entity(*handle) {
|
let record = match document.get_entity(*handle) {
|
||||||
hosts.extend(insert.attributes.iter().map(|a| a.common.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");
|
self.push_undo_snapshot(i, "UPDATEFIELD");
|
||||||
|
|||||||
@@ -110,12 +110,10 @@ pub fn update_fields(
|
|||||||
document.update_fields(&OcsFieldContext(Some(&snapshot)), event, hosts)
|
document.update_fields(&OcsFieldContext(Some(&snapshot)), event, hosts)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn resolve_handle(
|
/// The text a table cell shows for its field: a formula is evaluated live
|
||||||
document: &CadDocument,
|
/// (it reads other cells), any other field shows its stored value.
|
||||||
field: Handle,
|
pub fn cell_text(document: &CadDocument, field: Handle, table: Handle) -> Option<String> {
|
||||||
host: Handle,
|
codec::fields::cell_field_text(document, field, table, &OcsFieldContext(Some(document)))
|
||||||
) -> Option<String> {
|
|
||||||
codec::fields::resolve_handle(document, field, host, &OcsFieldContext(Some(document)))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Evaluate one field code (`\AcVar Date \f "yyyy-MM-dd"`, or the `%<…>%`
|
/// Evaluate one field code (`\AcVar Date \f "yyyy-MM-dd"`, or the `%<…>%`
|
||||||
|
|||||||
@@ -646,7 +646,7 @@ fn content_display_value(
|
|||||||
) -> String {
|
) -> String {
|
||||||
if let Some(handle) = content.field_handle {
|
if let Some(handle) = content.field_handle {
|
||||||
if let Some(value) =
|
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;
|
return value;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -488,18 +488,29 @@ fn font_stem(name: &str) -> String {
|
|||||||
.to_string()
|
.to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// How many inked glyphs (non-blank characters, stacked parts included) an
|
/// The visible-character offset that follows the MTEXT string `s` — the
|
||||||
/// MTEXT string draws, in drawing order: the index of the first glyph of
|
/// `vis` index [`layout_mtext`]'s glyph boxes give the first character of
|
||||||
/// what follows `s` in a longer string.
|
/// whatever comes after `s` in a longer string: every laid-out character,
|
||||||
pub fn mtext_glyph_count(s: &str) -> usize {
|
/// a tab or stack slot, and one per paragraph break.
|
||||||
let ink = |t: &str| t.chars().filter(|c| !c.is_whitespace()).count();
|
pub fn mtext_visible_count(s: &str) -> usize {
|
||||||
adapt_mtext_paragraphs(s, 1.0, false)
|
adapt_mtext_paragraphs(s, 1.0, false)
|
||||||
.iter()
|
.iter()
|
||||||
.flat_map(|line| &line.runs)
|
.enumerate()
|
||||||
.map(|run| match &run.kind {
|
.map(|(i, para)| {
|
||||||
MTextRunKind::Glyphs(text) => ink(text),
|
usize::from(i > 0)
|
||||||
MTextRunKind::Tab => 0,
|
+ para
|
||||||
MTextRunKind::Stack { numerator, denominator, .. } => ink(numerator) + ink(denominator),
|
.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()
|
.sum()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,70 +78,97 @@ fn oriented_text_corners(
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
/// FIELDDISPLAY boxes: the glyph bounds of each field's shown text, one box
|
/// FIELDDISPLAY box extent around a line of field text, in text heights:
|
||||||
/// per line the field spans, in the text's own frame (`origin`, `rotation`).
|
/// from a third of the height below the baseline to 1.19 heights above it
|
||||||
/// Glyph quads come six vertices each in drawing order, one per inked
|
/// (the reference's box, measured at 2.5 and 10 unit text).
|
||||||
/// character, so a field's characters select its quads. `None` when the
|
const FIELD_BOX_BELOW: f64 = 0.345;
|
||||||
/// host's fields cannot be located in its text (the whole text is boxed).
|
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(
|
fn field_run_corner_groups(
|
||||||
verts: &[crate::scene::pipeline::text_gpu::TextVertex],
|
|
||||||
document: &codec::CadDocument,
|
document: &codec::CadDocument,
|
||||||
entity: &EntityType,
|
entity: &EntityType,
|
||||||
origin: [f64; 2],
|
stroke_groups: &[crate::scene::convert::acad_to_render::TextStroke],
|
||||||
rotation: f64,
|
anno: f64,
|
||||||
) -> Option<Vec<[[f64; 2]; 4]>> {
|
) -> Option<Vec<[[f64; 2]; 4]>> {
|
||||||
use crate::entities::text_support::{mtext_glyph_count, resolve_dxf_special_chars};
|
use crate::entities::text_support::{mtext_visible_count, resolve_dxf_special_chars, text_local_bounds};
|
||||||
fn plain_glyph_count(s: &str) -> usize {
|
let handle = entity.common().handle;
|
||||||
resolve_dxf_special_chars(s).chars().filter(|c| !c.is_whitespace()).count()
|
// Lines of a field in a local frame: [left, baseline, right, height].
|
||||||
}
|
let mut lines: Vec<[f64; 4]> = Vec::new();
|
||||||
let (text, count): (&str, fn(&str) -> usize) = match entity {
|
let (origin, rotation) = match entity {
|
||||||
EntityType::MText(m) => (&m.value, mtext_glyph_count),
|
EntityType::MText(m) => {
|
||||||
EntityType::Text(t) => (&t.value, plain_glyph_count),
|
let spans = codec::fields::field_spans(document, handle, &m.value)?;
|
||||||
EntityType::AttributeEntity(a) => (&a.value, plain_glyph_count),
|
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,
|
_ => return None,
|
||||||
};
|
};
|
||||||
let spans = codec::fields::field_spans(document, entity.common().handle, text)?;
|
|
||||||
let (sin_r, cos_r) = rotation.sin_cos();
|
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 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(
|
Some(
|
||||||
groups
|
lines
|
||||||
.into_iter()
|
.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(),
|
.collect(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1489,29 +1516,14 @@ pub fn tessellate(
|
|||||||
&& crate::entities::field::display()
|
&& crate::entities::field::display()
|
||||||
&& crate::entities::field::hosts_field(document, entity)
|
&& crate::entities::field::hosts_field(document, entity)
|
||||||
{
|
{
|
||||||
let frame = match entity {
|
let runs = field_run_corner_groups(document, entity, &stroke_groups, anno);
|
||||||
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 corner_groups = match entity {
|
let corner_groups = match entity {
|
||||||
_ if runs.is_some() => runs.unwrap_or_default(),
|
_ if runs.is_some() => runs.unwrap_or_default(),
|
||||||
EntityType::MText(m) => {
|
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)
|
oriented_mtext_corner_groups(&sdf_verts, m, rotation, 0.0, anno)
|
||||||
}
|
}
|
||||||
EntityType::Text(t) => vec![oriented_text_corners(
|
EntityType::Text(t) => vec![oriented_text_corners(
|
||||||
|
|||||||
@@ -3443,6 +3443,12 @@ impl Scene {
|
|||||||
targets.render_handles.extend(users.iter().copied());
|
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;
|
targets.touches_block_definition = true;
|
||||||
}
|
}
|
||||||
self.invalidate_dependency_targets(targets);
|
self.invalidate_dependency_targets(targets);
|
||||||
|
|||||||
Reference in new issue
Block a user