forked from 1b-agent/opencadstudio-upstream
Merge pull request #1627 from ramox81/codex/plotdate-refresh
Fields: evaluation events, UPDATEFIELD, FIELDEVAL, per-field shading and PlotDate redraw. # Conflicts: # Cargo.lock # src/entities/field.rs
This commit is contained in:
commit
711799371a
14 files changed
+533
-36
No files matched your search
@@ -78,6 +78,130 @@ fn oriented_text_corners(
|
||||
]
|
||||
}
|
||||
|
||||
/// 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, in the text's own (rotated) frame —
|
||||
/// MTEXT and multi-line attributes from the layout's character cells, TEXT
|
||||
/// and single-line attributes from each line's character cells (the
|
||||
/// shaper's glyph cells for TrueType fonts and shaped scripts, else the
|
||||
/// stroke font's advances) — so formatting codes, missing glyphs, shaped or
|
||||
/// right-to-left scripts before a field do not shift it. A box spans the
|
||||
/// field's character cells along the line and FIELD_BOX_BELOW /
|
||||
/// FIELD_BOX_ABOVE across it. `None` when the host's fields cannot be
|
||||
/// located in its text (the whole text is boxed then).
|
||||
fn field_run_corner_groups(
|
||||
document: &codec::CadDocument,
|
||||
entity: &EntityType,
|
||||
stroke_groups: &[crate::scene::convert::acad_to_render::TextStroke],
|
||||
anno: f64,
|
||||
) -> Option<Vec<[[f64; 2]; 4]>> {
|
||||
use crate::entities::text_support::{
|
||||
mtext_visible_count, resolve_dxf_special_chars, resolve_text_style, text_char_cells, GlyphBox,
|
||||
};
|
||||
let handle = entity.common().handle;
|
||||
// Field lines as (origin, rotation, [left, baseline, right, height]) in
|
||||
// the origin's frame.
|
||||
let mut lines: Vec<([f64; 2], f64, [f64; 4])> = Vec::new();
|
||||
// Group a field's layout cells by line (a shared baseline).
|
||||
let from_cells = |lines: &mut Vec<([f64; 2], f64, [f64; 4])>,
|
||||
cells: &[GlyphBox],
|
||||
text: &str,
|
||||
spans: &[std::ops::Range<usize>],
|
||||
origin: [f64; 2],
|
||||
rotation: f64|
|
||||
-> Option<()> {
|
||||
for span in spans {
|
||||
let first = mtext_visible_count(text.get(..span.start)?);
|
||||
let last = mtext_visible_count(text.get(..span.end)?);
|
||||
let mut line: Option<[f64; 4]> = None;
|
||||
for b in cells.iter().filter(|b| (first..last).contains(&b.vis)) {
|
||||
let [l, base, r, top] = b.local.map(|v| v as f64 * anno);
|
||||
let h = top - base;
|
||||
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((origin, rotation, c));
|
||||
[l, base, r, h]
|
||||
}
|
||||
None => [l, base, r, h],
|
||||
});
|
||||
}
|
||||
lines.extend(line.map(|c| (origin, rotation, c)));
|
||||
}
|
||||
Some(())
|
||||
};
|
||||
match entity {
|
||||
EntityType::MText(m) => {
|
||||
let spans = codec::fields::field_spans(document, handle, &m.value)?;
|
||||
let cells = crate::entities::mtext::glyph_boxes(m, document);
|
||||
let flip = if resolve_text_style(&m.style, document).is_upside_down { std::f64::consts::PI } else { 0.0 };
|
||||
from_cells(
|
||||
&mut lines,
|
||||
&cells,
|
||||
&m.value,
|
||||
&spans,
|
||||
[m.insertion_point.x, m.insertion_point.y],
|
||||
m.rotation + flip,
|
||||
)?;
|
||||
}
|
||||
EntityType::AttributeEntity(a) if crate::entities::attribute::attribute_cells(a, document).is_some() => {
|
||||
let spans = codec::fields::field_spans(document, handle, &a.value)?;
|
||||
let (cells, origin, rotation) = crate::entities::attribute::attribute_cells(a, document)?;
|
||||
from_cells(&mut lines, &cells, &a.value, &spans, origin, rotation as f64)?;
|
||||
}
|
||||
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 per `\P`-separated line, in order.
|
||||
let mut start = 0;
|
||||
for (line_text, group) in text.split("\\P").zip(stroke_groups) {
|
||||
let range = start..start + line_text.len();
|
||||
start = range.end + 2;
|
||||
let run = group.run.as_ref()?;
|
||||
let resolved = resolve_dxf_special_chars(line_text);
|
||||
let cells = text_char_cells(&run.font, &resolved, run.height, run.width_factor);
|
||||
let char_at = |byte: usize| resolve_dxf_special_chars(&line_text[..byte]).chars().count();
|
||||
for span in &spans {
|
||||
let (a, b) = (span.start.max(range.start), span.end.min(range.end));
|
||||
if a >= b {
|
||||
continue;
|
||||
}
|
||||
let picked = cells.get(char_at(a - range.start)..char_at(b - range.start)).unwrap_or_default();
|
||||
let l = picked.iter().map(|c| c.0).fold(f32::INFINITY, f32::min) as f64;
|
||||
let r = picked.iter().map(|c| c.1).fold(f32::NEG_INFINITY, f32::max) as f64;
|
||||
if l.is_finite() && r.is_finite() {
|
||||
lines.push((group.origin, run.rotation as f64, [l * anno, 0.0, r * anno, run.height as f64 * anno]));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => return None,
|
||||
}
|
||||
Some(
|
||||
lines
|
||||
.into_iter()
|
||||
.filter(|(_, _, [l, _, r, _])| r > l)
|
||||
.map(|(origin, rotation, [l, base, r, h])| {
|
||||
let (sin_r, cos_r) = rotation.sin_cos();
|
||||
let to_world = |x: f64, y: f64| [origin[0] + x * cos_r - y * sin_r, origin[1] + x * sin_r + y * cos_r];
|
||||
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(),
|
||||
)
|
||||
}
|
||||
|
||||
fn oriented_mtext_corner_groups(
|
||||
verts: &[crate::scene::pipeline::text_gpu::TextVertex],
|
||||
text: &codec::MText,
|
||||
@@ -1431,15 +1555,16 @@ pub fn tessellate(
|
||||
// entity still has a hit-test target via snap_pts. With SDF on
|
||||
// this is the normal path (strokes suppressed) and the wire
|
||||
// also carries the glyph quads built above.
|
||||
if bins.is_empty() {
|
||||
let mut wires: Vec<WireModel> = Vec::new();
|
||||
// FIELDDISPLAY: a field shows on a gray box behind its
|
||||
// glyphs, on screen only.
|
||||
if text_aabb != WireModel::UNBOUNDED_AABB
|
||||
&& crate::entities::field::display()
|
||||
// FIELDDISPLAY: a field shows on a gray box behind its glyphs,
|
||||
// on screen only — for SDF glyphs and shaped stroke text alike.
|
||||
let mut field_background: Option<WireModel> = None;
|
||||
if crate::entities::field::display()
|
||||
&& crate::entities::field::hosts_field(document, entity)
|
||||
{
|
||||
let runs = field_run_corner_groups(document, entity, &stroke_groups, anno);
|
||||
let corner_groups = match entity {
|
||||
_ if runs.is_some() => runs.unwrap_or_default(),
|
||||
_ if text_aabb == WireModel::UNBOUNDED_AABB => Vec::new(),
|
||||
EntityType::MText(m) => {
|
||||
let rotation = stroke_groups
|
||||
.iter()
|
||||
@@ -1471,7 +1596,7 @@ pub fn tessellate(
|
||||
}
|
||||
}
|
||||
if !ft.is_empty() {
|
||||
wires.push(WireModel {
|
||||
field_background = Some(WireModel {
|
||||
display_visible: true,
|
||||
plot_visible: false,
|
||||
name: name.clone(),
|
||||
@@ -1486,6 +1611,9 @@ pub fn tessellate(
|
||||
});
|
||||
}
|
||||
}
|
||||
if bins.is_empty() {
|
||||
let mut wires: Vec<WireModel> = Vec::new();
|
||||
wires.extend(field_background.take());
|
||||
// MTEXT background and frame follow the glyph bounds.
|
||||
if text_aabb != WireModel::UNBOUNDED_AABB {
|
||||
if let EntityType::MText(m) = entity {
|
||||
@@ -1722,6 +1850,7 @@ pub fn tessellate(
|
||||
}
|
||||
|
||||
let mut out: Vec<WireModel> = Vec::new();
|
||||
out.extend(field_background);
|
||||
let mut is_first = true;
|
||||
for bin in bins {
|
||||
let wire_color = match bin.color {
|
||||
|
||||
@@ -568,6 +568,9 @@ struct SceneDependencyIndex {
|
||||
text_geometry: DependencyTargets,
|
||||
annotation_geometry: DependencyTargets,
|
||||
signatures: HashMap<Handle, u64>,
|
||||
/// Block name (normalised) → the layout entities that draw it, nested
|
||||
/// references included.
|
||||
block_roots: HashMap<String, HashSet<Handle>>,
|
||||
}
|
||||
|
||||
fn hatch_interaction_aabb(hatch: &model::hatch_model::HatchModel) -> Option<[f64; 4]> {
|
||||
@@ -3408,6 +3411,64 @@ impl Scene {
|
||||
self.bump_entities_with_parametric_policy(changes, &[], false);
|
||||
}
|
||||
|
||||
/// Update the fields an evaluation event re-evaluates (see
|
||||
/// [`crate::entities::field::update_fields`]) and redraw the hosts whose
|
||||
/// text changed. Returns the number of fields the hosts hold.
|
||||
pub fn update_fields(&mut self, event: i32, hosts: Option<&[Handle]>) -> usize {
|
||||
let (changed, found) = crate::entities::field::update_fields(&mut self.document, event, hosts);
|
||||
if !changed.is_empty() {
|
||||
self.bump_text_hosts(&changed);
|
||||
}
|
||||
found
|
||||
}
|
||||
|
||||
/// Redraw text hosts whose stored text changed (re-evaluated fields). An
|
||||
/// attribute is drawn by its INSERT, and a host inside a block definition
|
||||
/// by every reference to that block, so those redraw instead of the host;
|
||||
/// nothing else re-tessellates.
|
||||
pub fn bump_text_hosts(&mut self, hosts: &[Handle]) {
|
||||
if self.dependency_index_cache.borrow().is_none() {
|
||||
*self.dependency_index_cache.borrow_mut() = Some(self.rebuild_dependency_index());
|
||||
}
|
||||
let mut targets = DependencyTargets::default();
|
||||
for &host in hosts {
|
||||
let drawn = match self.document.get_entity(host) {
|
||||
Some(entity) => entity,
|
||||
None => match self.document.entities().find(|e| {
|
||||
matches!(e, EntityType::Insert(i) if i.attributes.iter().any(|a| a.common.handle == host))
|
||||
}) {
|
||||
Some(insert) => insert,
|
||||
None => continue,
|
||||
},
|
||||
};
|
||||
let handle = drawn.common().handle;
|
||||
let owner = drawn.common().owner_handle;
|
||||
if !self.changes_touch_block_definition(&[(handle, ChangeKind::Modified)]) {
|
||||
targets.render_handles.insert(handle);
|
||||
continue;
|
||||
}
|
||||
let Some(record) = self.document.block_records.iter().find(|r| {
|
||||
r.handle == owner || r.entity_handles.contains(&handle)
|
||||
}) else {
|
||||
continue;
|
||||
};
|
||||
let name = normalize_name(&record.name);
|
||||
if let Some(index) = self.dependency_index_cache.borrow().as_ref() {
|
||||
if let Some(users) = index.block_roots.get(&name) {
|
||||
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);
|
||||
}
|
||||
|
||||
pub fn bump_entities_with_parametric_driven(
|
||||
&mut self,
|
||||
changes: &[(Handle, ChangeKind)],
|
||||
@@ -11727,6 +11788,7 @@ vis_index={:.1} visible_probe={:.1}",
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
index.block_roots = roots;
|
||||
index
|
||||
}
|
||||
|
||||
|
||||
@@ -577,6 +577,30 @@ pub struct ShapedRun {
|
||||
pub glyphs: Vec<PlacedGlyph>,
|
||||
/// Total pen advance of the run (9-unit).
|
||||
pub advance: f32,
|
||||
/// Every laid-out glyph (blank ones too) as the byte range of the text it
|
||||
/// shows and its visual [left, right] pen cell, 9-unit.
|
||||
pub cells: Vec<(usize, usize, f32, f32)>,
|
||||
}
|
||||
|
||||
/// Each character's visual [left, right] cell (9-unit) in logical order:
|
||||
/// the cell of the glyph that shows it; a glyph showing several characters
|
||||
/// shares its cell among them in reading order.
|
||||
pub fn char_cells(run: &ShapedRun, text: &str) -> Vec<(f32, f32)> {
|
||||
text.char_indices()
|
||||
.map(|(byte, _)| {
|
||||
let Some(&(start, end, x0, x1)) =
|
||||
run.cells.iter().find(|(start, end, _, _)| (*start..(*end).max(start + 1)).contains(&byte))
|
||||
else {
|
||||
return (0.0, 0.0);
|
||||
};
|
||||
let chars: Vec<usize> = text[start..end.min(text.len())].char_indices().map(|(i, _)| start + i).collect();
|
||||
let (n, k) = (chars.len().max(1), chars.iter().position(|&b| b == byte).unwrap_or(0));
|
||||
let rtl = text[start..end.min(text.len())].chars().any(crate::entities::text_support::is_rtl_char);
|
||||
let slot = (x1 - x0) / n as f32;
|
||||
let k = if rtl { n - 1 - k } else { k };
|
||||
(x0 + slot * k as f32, x0 + slot * (k + 1) as f32)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
type ShapeCache = HashMap<(String, String), Option<Arc<ShapedRun>>>;
|
||||
@@ -881,10 +905,12 @@ fn build_shaped(_family: &str, text: &str) -> Option<ShapedRun> {
|
||||
buffer.shape_until_scroll(&mut font_system, false);
|
||||
|
||||
let mut glyphs = Vec::new();
|
||||
let mut cells = Vec::new();
|
||||
let mut advance = 0.0_f32;
|
||||
for run in buffer.layout_runs() {
|
||||
advance = advance.max(run.line_w * px_to_9);
|
||||
for glyph in run.glyphs.iter() {
|
||||
cells.push((glyph.start, glyph.end, glyph.x * px_to_9, (glyph.x + glyph.w) * px_to_9));
|
||||
let face_index = font_system
|
||||
.db_mut()
|
||||
.face(glyph.font_id)
|
||||
@@ -923,7 +949,7 @@ fn build_shaped(_family: &str, text: &str) -> Option<ShapedRun> {
|
||||
if advance <= 0.0 && glyphs.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(ShapedRun { glyphs, advance })
|
||||
Some(ShapedRun { glyphs, advance, cells })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -986,10 +1012,12 @@ fn build_shaped(family: &str, text: &str) -> Option<ShapedRun> {
|
||||
let px_to_9 = CAP_UNITS * upem_p / (SHAPE_FS * cap_p);
|
||||
|
||||
let mut glyphs: Vec<PlacedGlyph> = Vec::new();
|
||||
let mut cells = Vec::new();
|
||||
let mut advance = 0.0_f32;
|
||||
for run in buf.layout_runs() {
|
||||
advance = advance.max(run.line_w * px_to_9);
|
||||
for g in run.glyphs.iter() {
|
||||
cells.push((g.start, g.end, g.x * px_to_9, (g.x + g.w) * px_to_9));
|
||||
let face_index = fs.db_mut().face(g.font_id).map(|f| f.index).unwrap_or(0);
|
||||
let Some(font) = fs.get_font(g.font_id, g.font_weight) else {
|
||||
continue;
|
||||
@@ -1028,7 +1056,7 @@ fn build_shaped(family: &str, text: &str) -> Option<ShapedRun> {
|
||||
if advance <= 0.0 && glyphs.is_empty() {
|
||||
return None;
|
||||
}
|
||||
Some(ShapedRun { glyphs, advance })
|
||||
Some(ShapedRun { glyphs, advance, cells })
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
Reference in new issue
Block a user